[
  {
    "path": ".babelrc",
    "content": "{\n    \"presets\": [\"@babel/preset-env\", \"@babel/preset-react\",\"@babel/preset-typescript\"],\n    \"plugins\": [\"@babel/plugin-transform-runtime\", \"istanbul\"]\n}\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"parser\": \"@typescript-eslint/parser\",\n  \"plugins\": [\"@typescript-eslint\", \"react\"],\n  \"extends\": [\"eslint:recommended\", \"prettier\", \"plugin:@typescript-eslint/recommended\"], // this is optional\n  \"env\": {\n    \"browser\": true,\n    \"node\": true,\n    \"jest\": true\n  },\n  \"settings\": {\n    \"react\": {\n      \"version\": \"detect\"\n    }\n  },\n  \"ignorePatterns\": [\"node_modules/**\", \"packages/**/dist/**\", \"packages/**/coverage/**\"],\n  \"rules\": {\n    \"@typescript-eslint/no-explicit-any\": \"off\", // Off for v1\n    \"@typescript-eslint/ban-ts-comment\": \"off\", // Off for v1\n    \"@typescript-eslint/no-empty-function\": \"off\", // Off for v1\n    \"@typescript-eslint/no-unused-vars\": [\n      \"error\",\n      { \"vars\": \"all\", \"varsIgnorePattern\": \"^_*\", \"args\": \"after-used\", \"argsIgnorePattern\": \"^_\" }\n    ],\n    \"array-callback-return\": \"off\", // Off for v1\n    \"arrow-body-style\": \"off\",\n    \"block-scoped-var\": \"error\",\n    \"camelcase\": \"off\", // Off for v1\n    \"consistent-return\": \"off\", // Off for v1\n    \"consistent-this\": [\"error\", \"self\"],\n    \"constructor-super\": \"error\",\n    \"curly\": [\"error\", \"all\"],\n    \"default-case\": \"error\",\n    \"default-param-last\": \"off\", // Off for v1\n    \"dot-notation\": \"error\",\n    \"eqeqeq\": \"off\", // Off for v1\n    \"func-names\": \"error\",\n    \"func-style\": [\n      \"error\",\n      \"declaration\",\n      {\n        \"allowArrowFunctions\": true\n      }\n    ],\n    \"grouped-accessor-pairs\": \"error\",\n    \"line-comment-position\": \"off\", // Off for v1\n    \"lines-between-class-members\": \"error\",\n    \"max-depth\": \"error\",\n    \"max-len\": [\n      \"off\", // Off for v1\n      {\n        \"code\": 120,\n        \"comments\": 120,\n        \"ignoreUrls\": true,\n        \"ignoreTemplateLiterals\": true\n      }\n    ],\n    \"max-lines-per-function\": [\"off\"],\n    \"max-nested-callbacks\": [\"error\", 5],\n    \"max-statements\": [\"off\"],\n    \"max-statements-per-line\": \"error\",\n    \"no-alert\": \"off\", // Off for v1\n    \"no-array-constructor\": \"error\",\n    \"no-await-in-loop\": \"off\", // Off for v1\n    \"no-buffer-constructor\": \"error\",\n    \"no-caller\": \"error\",\n    \"no-confusing-arrow\": \"error\",\n    \"no-console\": \"warn\",\n    \"no-constructor-return\": \"error\",\n    \"no-constant-condition\": \"error\",\n    \"no-debugger\": \"warn\",\n    \"no-dupe-else-if\": \"error\",\n    \"no-else-return\": \"error\",\n    \"no-empty-function\": [\n      \"off\", // Off for v1\n      {\n        \"allow\": [\"constructors\"]\n      }\n    ],\n    \"no-eq-null\": \"off\", // Off for V1\n    \"no-eval\": \"error\",\n    \"no-extend-native\": \"error\",\n    \"no-extra-bind\": \"error\",\n    \"no-extra-label\": \"error\",\n    \"no-implicit-coercion\": \"error\",\n    \"no-implicit-globals\": \"error\",\n    \"no-implied-eval\": \"error\",\n    \"no-import-assign\": \"error\",\n    \"no-invalid-this\": \"off\",\n    \"no-iterator\": \"error\",\n    \"no-labels\": \"error\",\n    \"no-lone-blocks\": \"error\",\n    \"no-lonely-if\": \"error\",\n    \"no-loop-func\": \"error\",\n    \"no-magic-numbers\": \"off\",\n    \"no-multi-assign\": \"error\",\n    \"no-multi-str\": \"error\",\n    \"no-nested-ternary\": \"off\", // Off for v1\n    \"no-new\": \"error\",\n    \"no-new-func\": \"error\",\n    \"no-new-object\": \"error\",\n    \"no-new-wrappers\": \"error\",\n    \"no-octal-escape\": \"error\",\n    \"no-param-reassign\": \"off\", // Off for v1\n    \"no-path-concat\": \"error\",\n    \"no-plusplus\": [\n      \"error\",\n      {\n        \"allowForLoopAfterthoughts\": true\n      }\n    ],\n    \"no-proto\": \"off\", // Off for v1\n    \"no-restricted-globals\": \"error\",\n    \"no-return-assign\": \"error\",\n    \"no-return-await\": \"error\",\n    \"no-self-compare\": \"error\",\n    \"no-sequences\": \"error\",\n    \"no-setter-return\": \"error\",\n    \"no-sync\": \"error\",\n    \"no-tabs\": \"error\",\n    \"no-template-curly-in-string\": \"error\",\n    \"no-underscore-dangle\": \"off\", // Off for v1\n    \"no-unmodified-loop-condition\": \"error\",\n    \"no-unneeded-ternary\": \"error\",\n    \"no-unreachable\": \"error\",\n    \"no-unused-expressions\": \"off\", // Off for v1\n    \"no-useless-call\": \"error\",\n    \"no-useless-computed-key\": \"error\",\n    \"no-useless-concat\": \"off\", // Off for v1\n    \"no-useless-rename\": \"error\",\n    \"no-useless-return\": \"error\",\n    \"no-var\": \"error\",\n    \"no-void\": [\"error\", { \"allowAsStatement\": true }],\n    \"one-var\": [\"error\", \"never\"],\n    \"operator-assignment\": \"error\",\n    \"padding-line-between-statements\": \"error\",\n    \"prefer-arrow-callback\": \"warn\",\n    \"prefer-const\": \"off\", // Off for v1\n    \"prefer-destructuring\": [\n      // Off for v1\n      \"warn\",\n      {\n        \"VariableDeclarator\": {\n          \"array\": true,\n          \"object\": true\n        },\n        \"AssignmentExpression\": {\n          \"array\": false,\n          \"object\": false\n        }\n      }\n    ],\n    \"prefer-numeric-literals\": \"warn\",\n    \"prefer-promise-reject-errors\": \"warn\",\n    \"prefer-rest-params\": \"warn\",\n    \"prefer-spread\": \"warn\",\n    \"prefer-template\": \"warn\",\n    \"radix\": \"off\", // Off for v1\n    \"require-atomic-updates\": \"error\",\n    \"require-await\": \"warn\", // Warn for v1\n    \"sort-keys\": \"off\",\n    \"spaced-comment\": [\n      \"warn\",\n      \"always\",\n      {\n        \"markers\": [\"/\"]\n      }\n    ],\n    \"symbol-description\": \"error\",\n    \"yoda\": \"error\"\n  },\n  \"globals\": {\n    \"cy\": \"readonly\",\n    \"Cypress\": \"readonly\"\n  }\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n## Guidelines\nPlease note that GitHub issues are only meant for bug reports/feature requests. \n\n\nBefore creating a new issue, please check whether someone else has raised the same issue. You may be able to add context to that issue instead of duplicating the report. However, each issue should also only be focussed on a _single_ problem, so do not describe new problems within an existing thread - these are very hard to track and manage, and your problem may be ignored. Finally, do not append comments to closed issues; if the same problem re-occurs, open a new issue, and include a link to the old one.\n\nTo help us understand your issue, please specify important details, primarily:\n\n- NeoDash version: X.Y.Z\n- Neo4j Database version: X.Y.Z (Community/Enterprise/Aura).\n\n- **Steps to reproduce**\n- Expected behavior\n- Actual behavior\n\nAdditionally, include (as appropriate) screenshots, drawings, etc.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature\nassignees: ''\n\n---\n\n## Guidelines\nPlease note that GitHub issues are only meant for bug reports/feature requests.\n\n## Feature request template\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here."
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: 'npm'\n    directory: '/'\n    schedule:\n      interval: 'weekly'\n    target-branch: 'develop'\n\n  - package-ecosystem: 'npm'\n    directory: '/gallery'\n    schedule:\n      interval: 'weekly'\n    target-branch: 'develop'\n\n  - package-ecosystem: 'npm'\n    directory: '/docs'\n    schedule:\n      interval: 'weekly'\n    target-branch: 'develop'\n"
  },
  {
    "path": ".github/workflows/develop-deployment.yml",
    "content": "name: Test/Deploy Develop\n\non:\n  push:\n    branches: [develop]\n\njobs:\n  build-test:\n    if: github.event.pull_request.draft == false\n    runs-on: neodash-runners\n    strategy:\n      matrix:\n        node-version: [18.x]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v1\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Creating Neo4j Container\n        run: |\n          chmod +x ./scripts/docker-neo4j-initializer/docker-neo4j.sh\n          ./scripts/docker-neo4j-initializer/docker-neo4j.sh\n          sleep 30s\n          chmod +x ./scripts/docker-neo4j-initializer/start-movies-db.sh\n          ./scripts/docker-neo4j-initializer/start-movies-db.sh\n      - run: yarn install\n      - name: Eslint check\n        run: yarn run lint\n      - name: Cypress run\n        uses: cypress-io/github-action@v4\n        with:\n          build: yarn run build\n          start: yarn run prod\n          wait-on: \"http://localhost:3000\"\n          browser: chrome\n  build-s3:\n    needs: build-test\n    runs-on: neodash-runners\n    strategy:\n      matrix:\n        node-version: [18.x]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v1\n        with:\n          node-version: ${{ matrix.node-version }}\n      - run: yarn install\n      - run: yarn run build-minimal\n      - name: Set AWS credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: us-west-1\n      - run: curl ${{ secrets.INDEX_HTML_DEPLOYMENT_URL }} > dist/index.html\n      - run: aws s3 rm s3://neodash-test.graphapp.io/ --recursive && aws s3 sync dist s3://neodash-test.graphapp.io/ --acl public-read\n"
  },
  {
    "path": ".github/workflows/develop-test.yml",
    "content": "name: Test Develop\n\non:\n  pull_request:\n    branches: [develop]\n\njobs:\n  build-test:\n    if: github.event.pull_request.draft == false\n    runs-on: neodash-runners\n    strategy:\n      matrix:\n        node-version: [18.x]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v1\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Creating Neo4j Container\n        run: |\n          chmod +x ./scripts/docker-neo4j-initializer/docker-neo4j.sh\n          ./scripts/docker-neo4j-initializer/docker-neo4j.sh\n          sleep 30s\n          chmod +x ./scripts/docker-neo4j-initializer/start-movies-db.sh\n          ./scripts/docker-neo4j-initializer/start-movies-db.sh\n      - run: yarn install\n      - name: Eslint check\n        run: yarn run lint\n      - name: Cypress run\n        uses: cypress-io/github-action@v4\n        with:\n          build: yarn run build\n          start: yarn run prod\n          wait-on: 'http://localhost:3000'\n          browser: chrome\n      - name: Upload coverage reports to Codecov\n        uses: codecov/codecov-action@v3\n"
  },
  {
    "path": ".github/workflows/master-deployment.yml",
    "content": "name: Test/Deploy Master\n\non:\n  push:\n    branches: [master]\n\njobs:\n  build-test:\n    runs-on: neodash-runners\n    strategy:\n      matrix:\n        node-version: [18.x]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v1\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Creating Neo4j Container\n        run: |\n          chmod +x ./scripts/docker-neo4j-initializer/docker-neo4j.sh\n          ./scripts/docker-neo4j-initializer/docker-neo4j.sh\n          sleep 30s\n          chmod +x ./scripts/docker-neo4j-initializer/start-movies-db.sh\n          ./scripts/docker-neo4j-initializer/start-movies-db.sh\n      - run: yarn install\n      - name: Eslint check\n        run: yarn run lint\n      - name: Cypress run\n        uses: cypress-io/github-action@v4\n        with:\n          build: yarn run build\n          start: yarn run dev\n          wait-on: 'http://localhost:3000'\n          browser: chrome\n  build-s3:\n    needs: build-test\n    runs-on: neodash-runners\n    strategy:\n      matrix:\n        node-version: [18.x]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v1\n        with:\n          node-version: ${{ matrix.node-version }}\n      - run: rm -rf docs\n      - run: yarn install\n      - run: yarn run build-minimal\n      - name: Set AWS credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: us-west-1\n      - run: curl ${{ secrets.INDEX_HTML_DEPLOYMENT_URL }} > dist/index.html\n      # - run: curl https://gist.githubusercontent.com/nielsdejong/944d8f8f30dd2719f9b275e31df22f92/raw/f363cf5280eb5095e12e56a278f6616b6220adcf/config.json > dist/config.json\n      - run: aws s3 rm s3://neodash.graphapp.io/ --recursive && aws s3 sync dist s3://neodash.graphapp.io/ --acl public-read\n  build-docker:\n    needs: build-test\n    runs-on: neodash-runners\n    strategy:\n      matrix:\n        node-version: [18.x]\n    steps:\n      - name: run Docker\n        uses: actions/checkout@v2\n      - run: rm -rf docs\n      - name: Login to Docker Hub\n        uses: docker/login-action@v1\n        with:\n          username: ${{ secrets.DOCKER_HUB_LABS_USERNAME }}\n          password: ${{ secrets.DOCKER_HUB_LABS_ACCESS_TOKEN }}\n      - name: Set up Docker Build\n        uses: docker/setup-buildx-action@v1\n      - name: Build and push\n        uses: docker/build-push-action@v2\n        with:\n          context: .\n          file: ./Dockerfile\n          push: true\n          tags: ${{ secrets.DOCKER_HUB_LABS_USERNAME }}/neodash:latest,${{ secrets.DOCKER_HUB_LABS_USERNAME }}/neodash:2.4.11\n  build-docker-legacy:\n    needs: build-test\n    runs-on: neodash-runners\n    strategy:\n      matrix:\n        node-version: [18.x]\n    steps:\n      - name: run Docker\n        uses: actions/checkout@v2\n      - run: rm -rf docs\n      - name: Login to Docker Hub\n        uses: docker/login-action@v1\n        with:\n          username: ${{ secrets.DOCKER_HUB_USERNAME }}\n          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v1\n      - name: Build and push\n        uses: docker/build-push-action@v2\n        with:\n          context: .\n          file: ./Dockerfile\n          push: true\n          tags: ${{ secrets.DOCKER_HUB_USERNAME }}/neodash:latest,${{ secrets.DOCKER_HUB_USERNAME }}/neodash:2.4.11\n  deploy-gallery:\n    runs-on: neodash-runners\n    strategy:\n      matrix:\n        node-version: [18.x]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v1\n        with:\n          node-version: ${{ matrix.node-version }}\n      - run: cd gallery && yarn install\n      - run: cd gallery && yarn run build\n      - name: Set AWS credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: us-west-1\n      - run: aws s3 rm s3://neodash-gallery.graphapp.io/ --recursive && aws s3 sync gallery/build s3://neodash-gallery.graphapp.io/ --acl public-read\n  deploy-docs:\n    needs: build-test\n    runs-on: neodash-runners\n    steps:\n      - name: Trigger Developer Event\n        uses: peter-evans/repository-dispatch@main\n        with:\n          token: ${{ secrets.DOCS_REFRESH_TOKEN }}\n          repository: neo4j-documentation/docs-refresh\n          event-type: labs\n  build-npm:\n    needs: build-test\n    runs-on: neodash-runners\n    strategy:\n      matrix:\n        node-version: [18.x]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v1\n        with:\n          node-version: ${{ matrix.node-version }}\n      - run: rm -rf docs\n      - run: yarn install\n      - run: PRODUCTION=true && yarn run build-minimal\n      - run: curl https://gist.githubusercontent.com/nielsdejong/944d8f8f30dd2719f9b275e31df22f92/raw/f363cf5280eb5095e12e56a278f6616b6220adcf/config.json > dist/config.json\n      - run: curl ${{ secrets.INDEX_HTML_DEPLOYMENT_URL }} > dist/index.html\n      - run: npm pack\n      - run: rm -rf target\n      - run: mkdir target\n      - run: mv *.tgz target/\n      - run: tar -xvf target/*.tgz\n      - run: rm -f target/*.tgz\n      - run: cp package/dist/favicon.ico package/favicon.ico\n      - run: echo \"${{ secrets.NEO4J_LABS_APP_KEY }}\" > neo4j-labs-app.pem\n      - run: echo \"${{ secrets.NEO4J_LABS_APP_CERTIFICATE }}\" > neo4j-labs-app.cert\n      - run: npx @neo4j/code-signer --app ./package --private-key neo4j-labs-app.pem --cert neo4j-labs-app.cert --passphrase ${{ secrets.NEO4J_DESKTOP_PASSPHRASE }}\n      - run: echo \"${{ secrets.NEO4J_DESKTOP_CERTIFICATE }}\" > neo4j_desktop.cert\n      - run: npx @neo4j/code-signer --verify  --app ./package --root-cert neo4j_desktop.cert\n      - run: cd package && npm pack\n      - run: mv package/*.tgz .\n      - run: rm -rf package\n      - run: tar xvf *.tgz package\n      - run: npx @neo4j/code-signer --verify  --app ./package --root-cert neo4j_desktop.cert\n      - run: rm -rf package\n      - run: echo \"//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}\" > .npmrc\n      - name: Publish package to NPM 📦\n        run: npm publish --access public neodash*.tgz\n"
  },
  {
    "path": ".github/workflows/master-test.yml",
    "content": "name: Test Master\n\non:\n  pull_request:\n    branches: [master]\n\njobs:\n  build-test:\n    runs-on: neodash-runners\n    strategy:\n      matrix:\n        node-version: [18.x]\n    steps:\n      - uses: actions/checkout@v2\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v1\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Creating Neo4j Container\n        run: |\n          chmod +x ./scripts/docker-neo4j-initializer/docker-neo4j.sh\n          ./scripts/docker-neo4j-initializer/docker-neo4j.sh\n          sleep 30s\n          chmod +x ./scripts/docker-neo4j-initializer/start-movies-db.sh\n          ./scripts/docker-neo4j-initializer/start-movies-db.sh\n      - run: yarn install\n      - name: Eslint check\n        run: yarn run lint\n      - name: Cypress run\n        uses: cypress-io/github-action@v4\n        with:\n          build: yarn run build\n          start: yarn run dev\n          wait-on: \"http://localhost:3000\"\n          browser: chrome\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\n*.iml\n*.pem\n*.cert\n*.passphrase\nnode_modules\nbuild\ntarget\n/node_modules\n/.pnp\n.pnp.js\n.vscode\n\n# testing\n/coverage\n/.nyc_output\ncypress/videos\ncypress/screenshots\n# production\n/build\n/dist\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\nyarn-debug.log*\nyarn-error.log*\n\n# Ignore builds\n*.tgz\n\n# package directories\nnode_modules\njspm_packages\n\n# Serverless directories\n.serverless\n# Sentry Auth Token\n.env.sentry-build-plugin\n"
  },
  {
    "path": ".husky/common.sh",
    "content": "command_exists () {\n  command -v \"$1\" >/dev/null 2>&1\n}\n\n# Workaround for Windows 10, Git Bash and Yarn\nif command_exists winpty && test -t 1; then\n  exec < /dev/tty\nfi"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n. \"$(dirname -- \"$0\")/common.sh\"\n\nyarn run lint-staged\n"
  },
  {
    "path": ".lintstagedrc.json",
    "content": "{\n  \"*.ts\": [\"prettier --write\", \"eslint --fix\"],\n  \"*.tsx\": [\"prettier --write\", \"eslint --fix\"],\n  \"*.json\": [\"prettier --write\"],\n  \"*.js\": [\"prettier --write\"]\n}\n"
  },
  {
    "path": ".npmignore",
    "content": "public\nsrc\n.env\n.env.hosted\n.gitignore\ntsconfig.json\nyarn.lock\nnode_modules\n*.tgz\ndesktop.passphrase\n*.config.js\ndesktop-signer.sh"
  },
  {
    "path": ".prettierignore",
    "content": "coverage\ndist\nnode_modules\ndocs"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"printWidth\": 120,\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": true,\n  \"useTabs\": false,\n  \"tabWidth\": 2,\n  \"arrowParens\": \"always\",\n  \"trailingComma\": \"es5\",\n  \"bracketSpacing\": true,\n  \"endOfLine\": \"lf\"\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "# build stage\nFROM node:lts-alpine3.18 AS build-stage\n\nRUN yarn global add typescript jest\nWORKDIR /usr/local/src/neodash\n\n# Pull source code if you have not cloned the repository\n#RUN apk add --no-cache git\n#RUN git clone https://github.com/neo4j-labs/neodash.git /usr/local/src/neodash\n\n# Copy sources and install/build\nCOPY ./package.json /usr/local/src/neodash/package.json\nCOPY ./yarn.lock /usr/local/src/neodash/yarn.lock\n\nRUN yarn install\nCOPY ./ /usr/local/src/neodash\nRUN yarn run build-minimal\n\n# production stage\nFROM nginx:alpine3.18 AS neodash\nRUN apk upgrade\n\nENV NGINX_PORT=5005\n\nCOPY --from=build-stage /usr/local/src/neodash/dist /usr/share/nginx/html\nCOPY ./conf/default.conf.template /etc/nginx/templates/\nCOPY ./scripts/config-entrypoint.sh /docker-entrypoint.d/config-entrypoint.sh\nCOPY ./scripts/message-entrypoint.sh /docker-entrypoint.d/message-entrypoint.sh\n\nRUN chown -R nginx:nginx /var/cache/nginx && \\\n    chown -R nginx:nginx /var/log/nginx && \\\n    chown -R nginx:nginx /etc/nginx/conf.d && \\\n    chown -R nginx:nginx /etc/nginx/templates && \\\n    chown -R nginx:nginx /docker-entrypoint.d/config-entrypoint.sh && \\\n    chmod +x /docker-entrypoint.d/config-entrypoint.sh  && \\\n    chmod +x /docker-entrypoint.d/message-entrypoint.sh\nRUN touch /var/run/nginx.pid && \\\n    chown -R nginx:nginx /var/run/nginx.pid\nRUN chown -R nginx:nginx /usr/share/nginx/html/\n\n## Launch webserver as non-root user.\nUSER nginx\n\nEXPOSE $NGINX_PORT\n\nHEALTHCHECK cmd curl --fail \"http://localhost:$NGINX_PORT\" || exit 1\nLABEL version=\"2.4.11\"\n"
  },
  {
    "path": "LICENSE",
    "content": "     Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2023 Niels de Jong\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "## NeoDash Labs\nNeoDash is a dashboard builder for Neo4j, letting you build a graph dashboard in minutes.\n\n**This project is no longer maintained, use at your own risk.** \n\nIf you'd like to continue building dashboards, you have the following options:\n\n1. For the best experience, [upgrade](https://console-preview.neo4j.io/tools/dashboards) to Dashboards in the Neo4j Console (free for everyone).\n2. If you'd like to keep using NeoDash for free, you can fork this repository and [run NeoDash yourself](https://github.com/neo4j-labs/neodash/blob/master/about.md).\n3. If you're in need of a supported version of NeoDash, you can [purchase](https://neo4j.com/docs/neodash-commercial/current/#_getting_access_to_neodash_commercial) a NeoDash commercial license together with a Neo4j Enterprise license.  \n\n"
  },
  {
    "path": "about.md",
    "content": "## About NeoDash Labs\n\n> NeoDash Labs is an unmaintained and unsupported tool. Use at your own risk!\n\nNeoDash is a web-based tool for visualizing your Neo4j data. It lets you group visualizations together as dashboards, and allow for interactions between reports.\n\nNeodash supports presenting your data as tables, graphs, bar charts, line charts, maps and more. It contains a Cypher editor to directly write the Cypher queries that populate the reports. You can save dashboards to your database, and share them with others.\n\n## Try NeoDash Labs\nYou can build NeoDash yourself, or pull the latest Docker image from Docker Hub.\n```\n# Run the application on http://localhost:5005\ndocker pull neo4jlabs/neodash:latest\ndocker run -it --rm -p 5005:5005 neo4jlabs/neodash\n```\n\n> Windows users may need to prefix the `docker run` command with `winpty`.\n\n\n\n## Build and Run\nThis project uses `yarn` to install, run, build prettify and apply linting to the code.\n\nTo install dependencies:\n```\nyarn install\n```\n\nTo run the application in development mode:\n```\nyarn run dev\n```\n\nTo build the app for deployment:\n```\nyarn run build\n```\n\nTo manually prettify all the project `.ts` and `.tsx` files, run:\n```\nyarn run format\n```\n\nTo manually run linting of all your .ts and .tsx files, run:\n```\nyarn run lint\n```\n\nTo manually run linting of all your .ts and .tsx staged files, run:\n```\nyarn run lint-staged\n```\n\nSee the [Developer Guide](https://neo4j.com/labs/neodash/2.3/developer-guide/) for more on installing, building, and running the application.\n\n### Pre-Commit Hook\nWhile commiting, a pre-commit hook will be executed in order to prettify and run the Linter on your staged files. Linter warnings are currently accepted. The commands executed by this hook can be found in /.lintstagedrc.json.\n\nThere is also a dedicated linting step in the Github project pipeline in order to catch each potential inconsistency.\n\n> Don't hesitate to setup your IDE formatting feature to use the Prettier module and our defined rules (.prettierrc.json).\n\n\n## User Guide\nNeoDash comes with built-in examples of dashboards and reports. For more details on the types of reports and how to customize them, see the [User Guide](\nhttps://neo4j.com/labs/neodash/2.3/user-guide/).\n\n## Publish Dashboards\nAfter building a dashboard, you can chose to deploy a read-only, standalone instance for users. See [Publishing](https://neo4j.com/labs/neodash/2.3/user-guide/publishing/) for more on publishing dashboards.\n\n\n> NeoDash Labs is a free and open-source tool developed by the Neo4j community - not an official Neo4j product. Use at your own risk!"
  },
  {
    "path": "changelog.md",
    "content": "## NeoDash 2.4.10 - Community contributions\nThis is a minor release containing bug fixes and improvements contributed by the NeoDash community.\n- [#1039](https://github.com/neo4j-labs/neodash/pull/1039) - Fix default color scheme for bar charts\n- [#1038](https://github.com/neo4j-labs/neodash/pull/1038) - Fix rule-based styling for line charts\n- [#1036](https://github.com/neo4j-labs/neodash/pull/1036) - Fix table cell rule-based styling\n- [#1029](https://github.com/neo4j-labs/neodash/pull/1029) - Fix rule-based styling for numeric values\n- [#1028](https://github.com/neo4j-labs/neodash/pull/1028) - Fix OpenStreetMap leaflet display\n- [#1020](https://github.com/neo4j-labs/neodash/pull/1020) - Fix boolean handling in parameter selection\n- [#1014](https://github.com/neo4j-labs/neodash/pull/1014) - Remove autoPageSize flag (defaults to 0)\n- [#1009](https://github.com/neo4j-labs/neodash/pull/1009) - Fix SSO parameters lost on browser redirect\n- [#1008](https://github.com/neo4j-labs/neodash/pull/1008) - Fix existence check for `value.low`\n- [#1005](https://github.com/neo4j-labs/neodash/pull/1005) - Replace Neo4j Logo\n- [#1002](https://github.com/neo4j-labs/neodash/pull/1002) - Patch FAQ on supportability\n- [#999](https://github.com/neo4j-labs/neodash/pull/999) - Fix dark mode table header styling\n- [#956](https://github.com/neo4j-labs/neodash/pull/956) - Change default protocol to `neo4j+s`\n\n## NeoDash 2.4.9\nThis release adds some minor changes to documentation and implements some community contributions.\n- Added notice about project evolution: [#967](https://github.com/neo4j-labs/neodash/pull/967)\n- Added community contributions and bug fixes: \n[#967](https://github.com/neo4j-labs/neodash/pull/967)\n[#894](https://github.com/neo4j-labs/neodash/pull/894)\n[#822](https://github.com/neo4j-labs/neodash/pull/822)\n[#951](https://github.com/neo4j-labs/neodash/pull/951)\n[#946](https://github.com/neo4j-labs/neodash/pull/946)\n[#944](https://github.com/neo4j-labs/neodash/pull/944)\n[#943](https://github.com/neo4j-labs/neodash/pull/943)\n[#938](https://github.com/neo4j-labs/neodash/pull/938)\n[#935](https://github.com/neo4j-labs/neodash/pull/935)\n[#918](https://github.com/neo4j-labs/neodash/pull/918)\n[#908](https://github.com/neo4j-labs/neodash/pull/908)\n[#906](https://github.com/neo4j-labs/neodash/pull/906)\n[#902](https://github.com/neo4j-labs/neodash/pull/902)\n[#895](https://github.com/neo4j-labs/neodash/pull/895)\n[#893](https://github.com/neo4j-labs/neodash/pull/893)\n\n## NeoDash 2.4.8\nThis is a minor release containing an important fix and other minor fixes:\n\n- Fixed a bug where loading a dashboard would reset parameters to null ([887](https://github.com/neo4j-labs/neodash/pull/887)).\n- Fix relationship width parameter for Graph report ([889](https://github.com/neo4j-labs/neodash/pull/889)).\n  \nThanks to all the contributors for this release: \n- [alfredorubin96](https://github.com/alfredorubin96),\n- [nielsdejong](https://github.com/nielsdejong).\n\n## NeoDash 2.4.7\nThis is a minor release containing a few critical fixes and general code quality improvements:\n\n- Fix multiple parameter select ([881](https://github.com/neo4j-labs/neodash/pull/881)).\n- Fix parameter casting error when loading dashboards([874](https://github.com/neo4j-labs/neodash/pull/874)).\n- Fix the fraud demo in the [Example Gallery](https://neodash-gallery.graphapp.io/).\n  \nThanks to all the contributors for this release: \n- [alfredorubin96](https://github.com/alfredorubin96),\n- [MariusC](https://github.com/mariusconjeaud),\n- [elizarp](https://github.com/elizarp).\n\n## NeoDash 2.4.6\nThis is a minor release containing a few critical fixes and some extra style customizations:\n\n- Fix bad text wrapping for arrays in tables ([868](https://github.com/neo4j-labs/neodash/pull/868)).\n- Make wrapping in table optional, disabled by default ([872](https://github.com/neo4j-labs/neodash/pull/872)).\n- Fixed issues where cross database dashboard sharing always reverted back to the default database ([873](https://github.com/neo4j-labs/neodash/pull/873)).\n- Added option to define style config using environment variables for the Docker image ([876](https://github.com/neo4j-labs/neodash/pull/876)). \n\n## NeoDash 2.4.5\nThis is a small release containing a few fixes:\n- Fixed rendering of string arrays inside tables, report titles, and report action buttons [849](https://github.com/neo4j-labs/neodash/pull/849)\n- Allowed text to wrap in tables, preserving the number of rows [852](https://github.com/neo4j-labs/neodash/pull/852)\n- Disabled auto-sorting of Cypher query-based Parameter Select ; use Cypher ORDER BY to control result order [857](https://github.com/neo4j-labs/neodash/pull/857)\n- Updated role selector menu, and made user updates more robust [854](https://github.com/neo4j-labs/neodash/pull/854)\n\nThanks to all the contributors for this release: \n- [MariusC](https://github.com/mariusconjeaud),\n- [LiamEdwardsLamarche](https://github.com/LiamEdwardsLamarche),\n- [AleSim94](https://github.com/AleSim94)\n\n## NeoDash 2.4.4\nThis is a hotfix release fixing some breaking issues in the 2.4.3:\n- Fixed number parsing using newer versions of the Neo4j driver. [811](https://github.com/neo4j-labs/neodash/pull/811)\n- Reverted new connection handler for auto-renewed SSO sessions. [815](https://github.com/neo4j-labs/neodash/pull/815)\n- Improved handling of parameters in form extension, resolved local state issues. [813](https://github.com/neo4j-labs/neodash/pull/813)\n- Updated Role management extension to no longer execute queries in parallel, improved UX and error handling [813](https://github.com/neo4j-labs/neodash/pull/813)\n\nIf you are currently using NeoDash version 2.4.3, we recommend updating as soon as possible.\n\n## NeoDash 2.4.3\nThis release contains several improvements and additions to multi-dashboard management, as well as a bug fixes and a variety of quality-of-life improvements:\n\nDashboard management and access control:\n- Added a UI for handling dashboard access using RBAC, as well as a new extension to simply access control.\n- Added button to sidebar to refresh the list of dashboards saved in the database.\n- Improved handling and detection of draft dashboards in the dashboard sidebar.\n\nOther improvements:\n- Changed CSV export functionality for tables to use UTF-8 format.\n- Various improvements / fixes to the documentation to include new images, and up-to-date functionality.\n- Added logic for handling refresh tokens when connected to NeoDash via SSO.\n- Incorporated tooltips for bar charts with and without custom labels.\n\nBug fixes and testing:\n- Implemented bug fixes on type casting for numeric parameter selectors.\n- Fixed issue with report actions not functioning properly on node click events.\n- Extended test suite with Cypress tests for advanced settings in the bar chart.\n\nThanks to all the contributors for this release: \n- [OskarDamkjaer](https://github.com/OskarDamkjaer)\n- [alfredorubin96](https://github.com/alfredorubin96),\n- [AleSim94](https://github.com/AleSim94),\n- [BennuFire](https://github.com/BennuFire),\n- [jacobbleakley-neo4j](https://github.com/jacobbleakley-neo4j),\n- [josepmonclus](https://github.com/josepmonclus)\n- [nielsdejong](https://github.com/nielsdejong)\n\n\n## NeoDash 2.4.2\nThis is a release with a large amount of quality of life improvements, as well as some new features:\n\n- Visualize graphs in 3D with the new 3D graph report. [#737](https://github.com/neo4j-labs/neodash/pull/737)\n- Improved dashboard management sidebar and handling of drafts. [#734](https://github.com/neo4j-labs/neodash/pull/734)\n- Added parameter select setting for autopopulating first selector value. [#746](https://github.com/neo4j-labs/neodash/pull/746)\n- Improved UX for editing page names & dashboard titles. [#743](https://github.com/neo4j-labs/neodash/pull/743)\n- Unified common settings for each report type. [#724](https://github.com/neo4j-labs/neodash/pull/724)\n- Title of the browser tab NeoDash runs on is now automatically set to the dashboard name.  [#708](https://github.com/neo4j-labs/neodash/pull/708)\n- Fixed issue where invisible table columns were not handled correctly. [#695](https://github.com/neo4j-labs/neodash/pull/695)\n- Miscellaneous bug fixes, style improvements & stability fixes. [#744](https://github.com/neo4j-labs/neodash/pull/744)\n\n\n## NeoDash 2.4.1\nThis is a patch release following 2.4.0. It contains several new features for self-hosted (standalone) NeoDash deployments, as well as a variety of UX improvements for dashboard editors.\n\n\nIncluded:\n- Improvements to customizability of the bar chart (styling, legend customization, report actions). [#689](https://github.com/neo4j-labs/neodash/pull/689)\n- Improved dashboard settings interface, fixed alignment for table download button. [#729](https://github.com/neo4j-labs/neodash/pull/729)\n- Adjusted ordering of suggested labels/properties for parameter selectors. [#728](https://github.com/neo4j-labs/neodash/pull/728)\n- Better handling of date parameters when saving/loading dashboards. [#727](https://github.com/neo4j-labs/neodash/pull/727)\n- Fixed incorrect z-index issue for form creation modals. [#726](https://github.com/neo4j-labs/neodash/pull/726)\n- Adjusted filtering tooltip on tables to avoid hiding result data. [#712](https://github.com/neo4j-labs/neodash/pull/712)\n- Fixed uncontrolled component issue for dashboard import modal. [#711](https://github.com/neo4j-labs/neodash/pull/711)\n- Adjusted font color of graph context popups to use theme colors. [#699](https://github.com/neo4j-labs/neodash/pull/699)\n- Adjust sidebar database selector to only show active databases. [#698](https://github.com/neo4j-labs/neodash/pull/698)\n- Incorporated logging functionality for self-hosted NeoDash deployments. [#705](https://github.com/neo4j-labs/neodash/pull/705)\n- Improved dashboard management in standalone-mode deployments. [#705](https://github.com/neo4j-labs/neodash/pull/705)\n- Added Docker parameter for overriding the app's logo & custom header.  [#705](https://github.com/neo4j-labs/neodash/pull/705)\n- Changed the dashboard 'save' action to a logical merge, rather than a delete + create, allowing to persist labels across saves. [#705](https://github.com/neo4j-labs/neodash/pull/705)\n- Docker: Updated Alpine base image to mitigate CVE-2023-38039 & CVE-2023-4863. [#705](https://github.com/neo4j-labs/neodash/pull/705)\n\n\n## NeoDash 2.4.0\nNeoDash 2.4 is out! 🎂 This release packs a ton of new features, as well as improvements to the existing visualizations.\n\nKey new features:\n- A new sidebar with support for managing, save and load multiple dashboards directly from the UI.\n   [#657](https://github.com/neo4j-labs/neodash/pull/657)\n- Added **Forms** as a new extension. Forms let you combine multiple parameter selectors in one card and have users edit/submit data to Neo4j.  [#568](https://github.com/neo4j-labs/neodash/pull/568)\n- Added a new advanced visualization type: Gantt charts. [#684](https://github.com/neo4j-labs/neodash/pull/684)\n- Doubled the grid resolution for dashboards, giving you more freedom to arrange visualizations. [#682](https://github.com/neo4j-labs/neodash/pull/682)\n- Several improvements for the natural language queries extension - including customizable prompting, and faster schema retrieval. [#600](https://github.com/neo4j-labs/neodash/pull/600)\n\nOther improvements:\n- Support for multiselect checkboxes as a report action for tables. [#688](https://github.com/neo4j-labs/neodash/pull/688/commits)\n- Added keyboard shortcuts (CMD/CTRL+Enter) for running Cypher queries from the editor. [#694](https://github.com/neo4j-labs/neodash/pull/694/)\n- Added new experimental graph layouts (trees in various directions), with customizable level distance. [#690](https://github.com/neo4j-labs/neodash/pull/690)\n- Increased customizability for the Pie chart's styling.  [#638](https://github.com/neo4j-labs/neodash/pull/638/)\n- Fixed issues with parameter selector: Better handling of integer / long parameters and processing external updates. [#641](https://github.com/neo4j-labs/neodash/pull/641/)\n- Improvements on text readability for the experimental dark mode. [#668](https://github.com/neo4j-labs/neodash/pull/668/)\n- UX improvements on database connection interface. [#675](https://github.com/neo4j-labs/neodash/pull/675/)\n- Added option to provide a custom message when no data is returned by a report. [#683](https://github.com/neo4j-labs/neodash/pull/683/)\n- Fixed issue where column names were not hidden correctly. [#685](https://github.com/neo4j-labs/neodash/pull/685/commits)\n\nThanks to all the contributors for this release: \n[alfredorubin96](https://github.com/alfredorubin96),\n[AleSim94](https://github.com/AleSim94),\n[BennuFire](https://github.com/BennuFire),\n[jacobbleakley-neo4j](https://github.com/jacobbleakley-neo4j),\n[hugorplobo](https://github.com/hugorplobo),\n[brahmprakashMishra](https://github.com/brahmprakashMishra),\n[m-o-n-i-s-h](https://github.com/m-o-n-i-s-h),\n[JonanOribe](https://github.com/JonanOribe),\n[nielsdejong](https://github.com/nielsdejong)\n\n## NeoDash 2.3.5\nThis is a bugfix / stability release directly following 2.3.4.\n\nImprovements:\n- Fixed issue where orphan relationships prevented graph charts from working ([@BennuFire](https://github.com/BennuFire), [#586](https://github.com/neo4j-labs/neodash/pull/586))\n- Fix issue where only one style rule was used a time on tables. ([@BennuFire](https://github.com/BennuFire), [#632](https://github.com/neo4j-labs/neodash/pull/632))\n- Added information about source and target on Graph Chart information modal . ([@BennuFire](https://github.com/BennuFire), [#627](https://github.com/neo4j-labs/neodash/pull/627)) Based on [@brahmprakashMishra](https://github.com/brahmprakashMishra) PR\n- Fixed issue where bar charts where displaying black bars instead of scheme colors. ([@BennuFire](https://github.com/BennuFire), [#626](https://github.com/neo4j-labs/neodash/pull/626))\n- Added right subpath replacement on shared links redirection while in self deployments. ([@m-o-n-i-s-h](https://github.com/m-o-n-i-s-h), [#618](https://github.com/neo4j-labs/neodash/pull/618))\n- Dark theme tweaks. ([@BennuFire](https://github.com/BennuFire), [#585](https://github.com/neo4j-labs/neodash/pull/585))\n- Fixed parameter selector search where numbers were not found and sporadically displayed with decimal points. ([@BennuFire](https://github.com/BennuFire), [#633](https://github.com/neo4j-labs/neodash/pull/633))\n- Added a configuration in order to list sso providers to be used whenever a database has more than one configured. ([@BennuFire](https://github.com/BennuFire), [#624](https://github.com/neo4j-labs/neodash/pull/624))\n- Added 'Ignore undefined parameters' advanced setting support for optional parameters on a query. Now queries will assume a null value instead of returning the error 'Parameter not defined'.  ([@BennuFire](https://github.com/BennuFire), [#625](https://github.com/neo4j-labs/neodash/pull/625))\n\n## NeoDash 2.3.3 & 2.3.4\nThis is a bugfix / stability release directly following 2.3.2.\n\nImprovements:\n- Cleaned up dependencies, add lazy loading and code splitting in the bundle file for faster loading times. ([@BennuFire](https://github.com/BennuFire), [#545](https://github.com/neo4j-labs/neodash/pull/571))\n- Migrated all icons from Material UI to Needle icons. ([@BennuFire](https://github.com/BennuFire), [#545](https://github.com/neo4j-labs/neodash/pull/571))\n- Improved contrast for light and dark theme. ([@nielsdejong](https://github.com/nielsdejong), [#545](https://github.com/neo4j-labs/neodash/pull/566))\n- Fixed issue where dashboards were locked in read-only mode, after toggling in the dashboard settings. ([@nielsdejong](https://github.com/nielsdejong), [#545](https://github.com/neo4j-labs/neodash/pull/566))\n- Fixed issue where editing the name of a non-selected page changed the wrong page data. ([@BennuFire](https://github.com/BennuFire), [#545](https://github.com/neo4j-labs/neodash/pull/571))\n- Fixed issue where color picker was only working on popup selections. ([@BennuFire](https://github.com/BennuFire), [#579](https://github.com/neo4j-labs/neodash/pull/579))\n- Add user agent to driver session for better logging of NeoDash queries. ([@nielsdejong](https://github.com/nielsdejong), [#545](https://github.com/neo4j-labs/neodash/pull/574))\n\n\n## NeoDash 2.3.2\nWhat's new in NeoDash 2.3.2? A few bug fixes, performance improvements and more important, it ships phase 2 of our migration to [Needle](https://neo4j.com/developer-blog/needle-neo4j-design-system/)  !\n\n- Key Features:\n  - UI updated to use the **[Neo4j Design Language](https://www.neo4j.design/)** phase 2, giving NeoDash a similar look-and-feel to other Neo4j tools. This includes the removal of the sidebar and a complete refactor on the header component. ([@mariusconjeaud](https://github.com/mariusconjeaud),[@konsalex](https://github.com/konsalex),[@BennuFire](https://github.com/bennufire), [#552](https://github.com/neo4j-labs/neodash/pull/552))\n  - *Experimental* Support for **Dark Mode**.\n- Parameter Selector Chart\n  - New advanced setting 'Manual Parameter Save' allowing  dashboard parameters propagation on demand (instead of automatically on change) ([@BennuFire](https://github.com/bennufire), [#545](https://github.com/neo4j-labs/neodash/pull/545))\n  - Fix delete button leading to inconsistent values on click. ([@BennuFire](https://github.com/bennufire), [#545](https://github.com/neo4j-labs/neodash/pull/545))\n  \n  - Fix search on numbers not being triggered. ([@BennuFire](https://github.com/bennufire), [#545](https://github.com/neo4j-labs/neodash/pull/545))\n\n- Others\n  - Fix performance degradation on schema calculation ([@BennuFire](https://github.com/bennufire), [#555](https://github.com/neo4j-labs/neodash/pull/555))\n  - Fix standalone bug that prevent user from using username and password fields([@BennuFire](https://github.com/bennufire), [#551](https://github.com/neo4j-labs/neodash/pull/551))\n  - Added Sentry Support on https://neodash.graphapp.io ([@mariusconjeaud](https://github.com/mariusconjeaud), [#546](https://github.com/neo4j-labs/neodash/pull/546))\n  - Fix SSO redirection on editor mode ([@BennuFire](https://github.com/bennufire), [#543](https://github.com/neo4j-labs/neodash/pull/543))\n\n## NeoDash 2.3.1\nWhat's new in NeoDash 2.3.1? A few bug fixes, improvement of natural language queries with support of Azure Open AI and parameters, Graph Vizualization relationship styling and more below!\n\n- Natural language queries\n  - **Support of Azure Open AI** ([@BennuFire](https://github.com/bennufire), [#515](https://github.com/neo4j-labs/neodash/pull/515))\n  - Support parameters on natural language queries ([@BennuFire](https://github.com/bennufire), [#514](https://github.com/neo4j-labs/neodash/pull/514))\n\n- Graph Visualization\n  - Added styling rules for relationship color ([@brahmprakashMishra](https://github.com/brahmprakashMishra) [@BennuFire](https://github.com/bennufire), [#537](https://github.com/neo4j-labs/neodash/pull/537))\n\n- Table Chart\n  - Update TableChart to use first returned row values as titles when transposed ([@bastienhubert](https://github.com/bastienhubert), [#513](https://github.com/neo4j-labs/neodash/pull/513))\n  - Fix falsy boolean display on table ([@bastienhubert](https://github.com/bastienhubert), [#536](https://github.com/neo4j-labs/neodash/pull/536))\n\n- Report Actions\n  - Fix on Style and Action modal that was preventing from setting params on low resolutions ([@mariusconjeaud](https://github.com/mariusconjeaud), [#533](https://github.com/neo4j-labs/neodash/pull/533))\n\n- Others\n  - New setting for parameters selector to allow selection of multiple values instead of one + Fix multi selector on dates ([@BennuFire](https://github.com/bennufire), [#535](https://github.com/neo4j-labs/neodash/pull/535))\n  - Fix bug where protocol was not set properly on share links ([@nielsdejong](https://github.com/nielsdejong), [#521](https://github.com/neo4j-labs/neodash/pull/521))\n  - Update word-wrap from 1.2.3 to 1.2.4 ([@BennuFire](https://github.com/bennufire), [#526](https://github.com/neo4j-labs/neodash/pull/526) [#527](https://github.com/neo4j-labs/neodash/pull/527))\n\n## NeoDash 2.3.0\nNeoDash 2.3 is out! This release brings a brand new look-and-feel, improved speed for large dashboards, and a new extension for querying Neo4j with natural language (using LLMs).\n\nKey features:\n- Write **[Natural Language Queries](https://neo4j.com/labs/neodash/2.3/user-guide/extensions/natural-language-queries/)** and use OpenAI to generate Cypher queries for your visualizations.\n- UI updated to use the **[Neo4j Design Language](https://www.neo4j.design/)**, giving NeoDash a similar look-and-feel to other Neo4j tools.\n- Customize branding, colors dynamically with a new [Style Configuration File](https://neo4j.com/labs/neodash/2.3/developer-guide/style-configuration).\n  \nOther changes:\n- Fixed issues with date picker / free-text parameter sometimes not initializing.\n- Improved documentation by fixing broken links, and adding more details around complex concepts. \n- **Pro Extensions have evolved to open Expert Extensions.**\n- Fixed issue where deep-linked parameters were not set from the URL.\n- Added option to specify absolute width for table columns (in pixels or as percentages).\n- Fixed map charts to auto-cluster markers when they collide, or are too close together.\n- ... and dozens of other improvements!\n\n\n\nContributors to this release:\n- [Alfredo Rubin](https://github.com/alfredorubin96)\n- [Harold Agudelo](https://github.com/BennuFire)\n- [Aleksandar Simeunovic](https://github.com/AleSim94)\n- [Marius Conjeaud](https://github.com/mariusconjeaud)\n- [Brahm Prakash Mishra](https://github.com/brahmprakashMishra)\n- [Pierre Martignon](https://github.com/pierremartignon)\n- [Kim Zachariassen](https://github.com/KiZach)\n- [Paolo Baldini](https://github.com/8Rav3n)\n- [Niels de Jong](https://github.com/nielsdejong/)\n\n\n## NeoDash 2.2.5\nThis is a minor release with some small bug fixes, directly following the 2.2.4 release.\n- Fixed replacement rules for parameters in iFrames/Markdown reports. [#417](https://github.com/neo4j-labs/neodash/pull/417)\n- Added automatic header text color switch for reports with a dark background [#420](https://github.com/neo4j-labs/neodash/pull/420)\n- Fixed handling right click events (for graph exploration) in Neo4j Desktop [#415](https://github.com/neo4j-labs/neodash/pull/415).\n- Added support for unweighted Sankey charts [#419](https://github.com/neo4j-labs/neodash/pull/419)\n\n\n## NeoDash 2.2.4\nThis release is a feature-rich package with a variety of new features and bug fixes. NeoDash 2.2.4 features new visualizations, as well as new features in existing visualization components. \n\n\n- Area Map - **New!** \n  - Added a new advanced chart interactive area map visualization for rendering geo json polygons. ([@alfredorubin96](https://github.com/alfredorubin96), [#401](https://github.com/neo4j-labs/neodash/pull/401))\n  - Assign color scale automatically based on numeric values.\n  - Assign colors to countries based on Alpha-2 and Alpha-3 codes, and area codes by ISO 3166 code.\n  - Interactive drilldown by clicking on regions in a country.\n\n- Graph Visualization\n  - Added **lightweight, ad-hoc graph exploration** by relationship type and direction. ([@nielsdejong](https://github.com/nielsdejong), [#401](https://github.com/neo4j-labs/neodash/pull/401))\n  - Added experimental graph editing: nodes and relationships, plus creating relationships between existing nodes. ([@nielsdejong](https://github.com/nielsdejong), [#401](https://github.com/neo4j-labs/neodash/pull/401))\n  - Fixed incorrect assignment of chip colors in graph visualization footer. ([@BennuFire](https://github.com/bennufire), [#296](https://github.com/neo4j-labs/neodash/issues/296))\n  - Added experimental CSV download button to graph visualizations. ([@JonanOribe](https://github.com/JonanOribe), [#288](https://github.com/neo4j-labs/neodash/issues/288), [#363](https://github.com/neo4j-labs/neodash/issues/363))\n  - Fixed a bug where dashboard parameters were not dynamically injected into drilldown links. ([@nielsdejong](https://github.com/nielsdejong), [#397](https://github.com/neo4j-labs/neodash/pull/397))\n  - Added setting to customize the size of the arrow head on an edge. Set to zero to disable directional rendering. ([@BennuFire](https://github.com/bennufire), [#410](https://github.com/neo4j-labs/neodash/pull/410))\n \n- Single Value Chart\n  - Added support for outputting dictionaries in YML format, and rendering new lines. ([@nielsdejong](https://github.com/nielsdejong), [#315](https://github.com/neo4j-labs/neodash/issues/315))\n\n- Choropleth Map\n  - Added polygon information for missing countries: France, Kosovo, and others. ([@BennuFire](https://github.com/bennufire), [#357](https://github.com/neo4j-labs/neodash/issues/357))\n\n- Parameter Selector\n  - Fixed bug where the parameter selector was not using the selected database to populate results. ([@BennuFire](https://github.com/bennufire), [#366](https://github.com/neo4j-labs/neodash/issues/366))\n  - Added a date picker parameter selector type for natively specifying dates. ([@alfredorubin96](https://github.com/alfredorubin96), [#401](https://github.com/neo4j-labs/neodash/pull/401))\n  - Added support for injecting custom queries as a populator for parameter selector suggestions. ([@BennuFire](https://github.com/bennufire), [#236](https://github.com/neo4j-labs/neodash/issues/236), [#369](https://github.com/neo4j-labs/neodash/issues/369))\n\n- Table Chart\n  - Added support for customizing the seperator in csv exports. ([@nielsdejong](https://github.com/nielsdejong), [#337](https://github.com/neo4j-labs/neodash/issues/337))\n- Others\n  - Added support for easily configurable branding/color schemes of the editor. ([@nielsdejong](https://github.com/nielsdejong), [#401](https://github.com/neo4j-labs/neodash/pull/401))\n  - Added a new report action to switch pages based on a user interaction. ([@BennuFire](https://github.com/BennuFire), [#324](https://github.com/neo4j-labs/neodash/issues/324))\n  - Added handler for mulitple report actions to be executed on the same event. ([@BennuFire](https://github.com/BennuFire), [#324](https://github.com/neo4j-labs/neodash/issues/324))\n  - Integrated the official released version of the Neo4j Cypher editor component. ([@jharris4](https://github.com/jharris4), [#365](https://github.com/neo4j-labs/neodash/pull/365))\n  - Fixed hot-module replacement inside webpack configuration.  ([@konsalex](https://github.com/konsalex), [#396](https://github.com/neo4j-labs/neodash/pull/396))\n  - Fixed husky pre-commit hook not triggering correctly on Windows environments. ([@bastienhubert](https://github.com/bastienhubert), [#342](https://github.com/neo4j-labs/neodash/issues/342))\n  - Add support for using complex objects in markdown, iframes and report titles. ([@BennuFire](https://github.com/bennufire), [#413](https://github.com/neo4j-labs/neodash/pull/413))\n\n\n## NeoDash 2.2.3\nThis releases fixes a small set of bugs that slipped through the 2.2.3 release, and adds some minor features:\n- Added support for scatter plots by overriding a parameter in the line chart.\n- Added the ability to use dashboard parameter as filters in custom parameter selector queries.\n- Fixed breaking bug in parameter selector settings causing a white-screen error.\n- Fixed auto-coloring of bar charts (resolved back to logic of 2.2.1 and earlier).\n- Added a quick fix for automatically resetting the parameter display value when the property display override is toggled.\n- Upversioned outdated dashboards and in the NeoDash Gallery.\n\n  \n## NeoDash 2.2.2\nThe NeoDash 2.2.2 release is packed with a bunch of new usability features:\n- Changed the built-in Cypher editor to a brand-new [CodeMirror Editor](https://github.com/neo4j-contrib/cypher-editor).\n- Rebuilt the **Parameter Select** component from scratch for improved stability, performance and extendability:\n  - Added an optional setting to the parameter selector to display a different property from the one that is set by the selector.\n  - Use this to - for example - let users choose a name and set an ID for use by other reports.\n  - Fields no longer reset randomly when parameters are changed.\n  - Freetext fields are no longer slow - perform as fast as the other selectors.\n- Add the option to use rule-based styling based on dashboard parameters.\n- Changed rule-based styling on bar and pie charts to override color scheme instead of clear the scheme.\n- Extended the [Example Gallery](https://neodash-gallery.graphapp.io/) with several new demos.\n- Adding intermediate report error boundaries for improved app stability. \n- Changed docker image name to `neo4jlabs/neodash`.\n- Improved documementation for developers.\n- Fixed inconsistent styling between different pop-up screens, and fixed report title placeholders.\n\n## NeoDash 2.2.1\nThis update provides a number of usability improves over the 2.2.0 release.\nIn addition, it entails various improvements to the codebase, including security patches on the dependencies.\n\nTable:\n- Column names prefixed with `__` are now hidden in the table view.\n  \nMap:\n- Added documentation for adding a custom map provider.\n\nParameter selector:\n- Added support for boolean parameters.\n\nEditor:\n- Parameters are now automatically replaced **inside report titles**.\n- Image downloads now include the report title alongside the visualization.\n\nOthers:\n- Applied security patches for dependencies.\n- Set test container for release pipeline to fixed version of Neo4j.\n- Aligned code style / linting with Neo4j product standards.\n- Updated Docker setup to inject `standaloneDashboardURL` into the application config.\n\n## NeoDash 2.2.0\nThis release marks the official arrival of **[Extensions](https://neo4j.com/labs/neodash/2.2/user-guide/extensions/)**, which provide a simple way of extending NeoDash with additional features. Adding your own features to NeoDash just became a lot easier!\n\nNeoDash 2.2 comes with three in-built extensions.\n- **Rule-Based Styling**\n- **Advanced Visualizations**: These provide a means to enable complex visualizations in a dashboard. These were previously available as Radar charts, Treemaps, Circle Packing reports, Sankey charts, Choropleth and a Gauge Chart).\n- **Report Actions**: Which let you create interactivity in dashboards, using the output of one report as input for another visualization. (Expert Extension)\n\nYou can enable extensions by clicking the 🧩 icon on the left sidebar of the screen.\n\nOther changes include:\n- New example dashboards available in the [Dashboard Gallery](https://neodash-gallery.graphapp.io).\n- Customizable background colors for all report types.\n- Fixing a bug where the Choropleth map chart was unable to parse country-codes.\n\n## NeoDash 2.1.10\nThis is a minor update which adds some operational/styling improvements, and a bug fix for line charts.\n\nChanges:\n- Added customizable label positions for bar charts.\n- Fixed bug where datetimes were not handled correctly by line charts. (https://github.com/neo4j-labs/neodash/issues/243)\n- Added **session parameters**, set automatically and available to Cypher queries ([Documentation](https://neo4j.com/labs/neodash/2.1/user-guide/reports/)).\n- Added option to restore debug reports in recovery mode.\n- Added option to share dashboards from self-hosted deployments.\n\n## NeoDash 2.1.8 & 2.1.9\nNew features:\n- Added the [Dashboard Gallery](https://neodash-gallery.graphapp.io), a live gallery of example NeoDash dashboards.\n- Added **Gauge Charts**, a contribution of the [BlueHound](https://github.com/zeronetworks/BlueHound) fork.\n- Updated testing pipeline to work as an independent procedure.\n- Added option to select a different Neo4j database for each report. ([#188](https://github.com/neo4j-labs/neodash/issues/118))\n- Added **Report Actions**, a neodash extension (available in beta) only on [https://neodash.graphapp.io](https://neodash.graphapp.io). ([#27](https://github.com/neo4j-labs/neodash/issues/27))\n \nBug fixes:\n- Fixed issue preventing dashboards to be shared with a non-standard database name.\n- Fixed table chart breaking when returning a property called 'id' with a null value.\n- Fixed bug not allowing users to select a different database when loading/saving a dashboard.\n- **Added error handler for database list race condition in Neo4j Desktop**.\n\n\n## NeoDash 2.1.6 & 2.1.7\nNew features:\n- Added *Radar Charts/Spider Charts*.\n- Added optional markdown description for each report, to be displayed via the header.\n\nExtensions:\n- Added option to provide a custom map provider for map charts.\n- Added support for default values in parameter selectors.\n- Added documentation on deep-linking into NeoDash.\n- Added tick-rotation customization for line charts.\n- Added option to have children in the sunburst chart inherit colors from their parents.\n\nImprovements:\n- Rewiring of the internal query/rendering engine - resulting in far fewer query executions and a smoother UX.\n- Changed package manager from `npm` to `yarn`, and bumped node version to 18. Cleaned up `package.json`.\n- Reduced flaky behaviour in parameter selectors.\n- Added cycle-detection logic for sankey charts.\n- Fixed report documentation pop-up to open link in a new window.\n\n## NeoDash 2.1.5\nAdded *New* Sankey charts:\n- Visualize nodes and relationships as a flow diagram.\n- Select a customizable flow value from relationship properties.\n- Configure a variety of style customizations.\n\nParameter select:\n- Fixed bug where values would randomly be deleted after changing the parameter.\n- Added option to customize the number of suggested values when a user enters (part of) a property value.\n- Added option to customize search type (CONTAINS, STARTS WITH, or ENDS WITH).\n- Added option to enable/disable case-sensitive search.\n- Added option to enable/disable removing duplicate suggestions.\n\nMiscellaneous:\n- Extended documentation with examples on running NeoDash in Kubernetes.\n- Fixed issue where duplicate database names were visible when running NeoDash on an on-prem Neo4j cluster.\n\n\n## NeoDash 2.1.4\nAdded hotfix for missing function in map visualization (https://github.com/neo4j-labs/neodash/issues/183).\n\n\n## NeoDash 2.1.3\nThe 2.1.3 release contains updates to the map visualization, as well as a new Choropleth map report type.\nSeveral usability improvements were also added, including fixing all links into the new documentation pages.\n\n- Extended the map visualization with a heatmap mode & marker clustering.\n- Added a Choropleth map visualization report type.\n- Added support for auto-linking into a predefined database from https://tools.neo4jlabs.com/.\n- Added optional background color setting for reports.\n- Added a new 'resize mode' for page layout creation.\n- Added support for drawing dates on a time chart (in addition to existing datetime types).\n- Fixed broken links in the documentation portal, all in-app links now point to this portal as well.\n\n\n## NeoDash 2.1.2\nThe 2.1.2 release contains some bug fixes and minor improvements to the application.\n\nApplication changes:\n- Added button to clone (duplicate) a report inside a dashboard.\n- Added option to show/hide labels inside circle packing charts.\n- Changed dashboard layout compaction strategy to be more natural.\n- Fixed card headers not rendering correctly in read-only mode.\n- Fixed rendering issues for table columns containing null values.\n\nOperational changes:\n- Added support for username/password environment variables in Docker.\n\n\n## NeoDash 2.1.0, NeoDash 2.1.1\nThe 2.1 release is a major update to the NeoDash application.\n\nMain updates:\n- Added new drag-and-drop dashboard layout - reports can be **moved** and **resized** freely within the dashboard.\n- Updated dashboard file format for new layout (2.0 dashboards are automatically migrated).\n- Pages can now be reordered by dragging and dropping. \n- Added three new hierarchical report types:\n  - Treemaps\n  - Sunburst Charts\n  - Circle Packing Charts\n- Styling/usability improvements for pie charts.\n- Improved image download (screenshot functionality) for all report types.\n- Parameter select reports now resize the selector to fit the available space.\n\nOther changes:\n- Added continuous integration and deployment workflows.\n- Created a new [User Guide](https://github.com/neo4j-labs/neodash/wiki/User-Guide) with documentation on all report customizations is available.\n- Added a new [Developer Guide](https://github.com/neo4j-labs/neodash/wiki/Developer-Guide) with info on installing, building and extending the application.\n\n\n## NeoDash 2.0.15\nThis is the final minor update before the 2.1 release.\n\nChanges:\n- Several stability improvements before the 2.1 release.\n- Updated Dockerfile to make better use of caching, and pick up environment variables at run time.\n- Added option to replace dashboard parameters in Markdown/iFrames to make them dynamic.\n- Removed unneeded index column from the CSV download for tables.\n- Added optional dashboard setting to enable image downloads for reports/the entire dashboard.\n\n\n## NeoDash 2.0.14\nReport features:\n- Added optional \"Download as CSV\" button to table reports.\n- Dashboard parameters can now be used in iFrames/Graph drilldown links, and they are automatically replaced when parameters get updated.\n- Updating a dashboard parameter now only refreshes the reports that use the parameter.\n\nStandalone mode:\n- Enabled deploying standalone dashboards with a direct URL to the dashboard.\n- Added functionality to deep link into a NeoDash dashboard with dashboard parameters (use ?neodash_variable_name=value in the URL).\n\n\nMiscellaneous Bug fixes and improvements:\n- Resolved crash caused by invalid geospatial properties in a Map visualization.\n- Saving a dashboard now lets users override an existing dashboard with the same name (enabled by default).\n- Increased the default row limits for line/bar/pie charts to 250. Added option to override the row limiter in the dashboard settings.\n- Updated project README file to refer to the correct port number on Docker deployments.\n- Enabled a configurable timeout for parameter selection reports, both a timeout for the suggestion retrieval and a timeout for updating the parameters.\n- Fixed dependency issues when installing the application on Windows systems. Bumped suggested npm version to 8.6.\n\n## NeoDash 2.0.13\nThis is a bug fix/minor usability update.\n\nChanges:\n- Resolved error where the float value 0.0 was rendered as 'null' in tables.\n- Added alphabetical sorting to all node/relationship inspection pop-ups & parameter select reports.\n- Resolved bug where switching pages quickly resulting in an error message.\n- Resolved bug where rule-based styling would break on null values.\n- Replaced margin-based styling on single value reports with a vertical alignment option.\n\n## NeoDash 2.0.12\nAdded **rule-based styling**:\n- Use the card settings to specify styling rules for tables, graphs, bar/pie/line charts and single values.\n- Conditional rules are evaluated on each report render in order of priority.\n- Rules can customize colors in tables, node colors & dynamically set the colors of components in your chart.\n\nMinor improvements:\n- Better handling of null values in tables.\n- Tweaking/reorganization of the Docker file and deployment scripts.\n- Renaming/restructuring of source code.\n\n## NeoDash 2.0.8 / 2.0.9 / 2.0.10 / 2.0.11\nStability fixes to supplement 2.0.7:\n- Hotfix for missing config file in Neo4j Desktop causing startup issue.\n- Hotfix for application crashes caused by rendering custom data types in transposed table views.\n- Hotfix for object rendering in tables & line-chart type detection.\n- Fix for rendering dictionaries in tables/single value charts.\n- Added resize handler for fullscreen map views.\n- Added missing auto-run config to pie charts.\n- Fixed broken value scale parameter for bar charts.\n\n## NeoDash 2.0.7\nApplication functionality:\n- Added standalone 'dashboard viewer' mode.\n- Added option to save/load dashboards from other Neo4j databases.\n\nReports/Visualizations:\n- Fixed bug in creating line charts.\n- Added support for datetime axis in line charts.\n- Added auto-locale formatting to number values in single value / table reports.\n- Added unified renderer for value types.\n- Updated default font size for single value reports.\n- Added optional deep-link button for graph visualizations.\n- Added option to disable auto-running a report, to let users explore the query first.\n- Minor styling tweaks to the graph views.\n\nFor Developers:\n- Added more documentation on extending the app.\n- New security-vetted docker image available on Docker hub.\n\n\n## NeoDash 2.0.6\nMajor version updates to all internal dependencies. \nNeoDash 2.0.6 uses Node 17+, react 17+ and recent versions of all visualization libraries.\n\nVisualizations:\n- Added pie charts (Including examples and new demo dashboard).\n- Added setting to transpose table rows and columns.\n- Improved styling on graph pop-up windows.\n- Graph visualizations now auto-fit to the report size.\n- Added button to reset the zoom on a graph report.\n\nParameter selection:\n- Added relationship property / free text selection options.\n\nEditor:\n- Improved performance of inbuilt Cypher editor.\n- Added button to maximize cards while in edit-mode.\n- All reports are now maximizable by default.\n- Added tiny report sizes.\n- Added option to override the default query timeout of twenty seconds.\n\nOther:\n- Updated docker image build scripts.\n- Fixed share link geneneration incorrectly removing capitals from usernames/passwords.\n\n## NeoDash 2.0.5\nGraph report:\n- Fixed node position after dragging nodes.\n- Added option to 'lock' graph views, storing the current positions of the nodes in the graph.\n- Added experimental graph layouts.\n\nTable:\n- Fixed bug where the report freezes for very wide tables.\n- Added support for rendering native/custom Neo4j types in the table.\n\nParameter select:\n- Fixed issue where the dashboard crashes for slow connections.\n\nEditor:\n- Added button to create a debug file from the 'About' screen.\n\n\n## NeoDash 2.0.4\nNew features:\n- Added option dashboard setting to let users view reports in a fullscreen pop-up.\n- Added inspection pop-up for graph visualizations.\n- Added option to manually specify node labels/property names in parameter selection reports (for large databases).\n- Added example of how to user map visualizations from derived properties.\n- Added button to return to the welcome screen.\n- iFrames can now take live parameter selections in the hash-part of the URL.\n\nBug fixes:\n- Dashboards will now remember the active selection(s) made in parameter select reports.\n- Graph visualizations will no longer draw overlapping lines when a pair of nodes shares bidirectional relationships.\n- connection screen is now dismissable if an existing connection exists.\n    \nSpecial thanks to @JipSogeti for their contributions to this release.\n\n## NeoDash 2.0.3\nUX improvements + bug fixes.\n- Parameter selection report:\n    - fixed bug to allow for selecting properties from nodes with >5 distinct properties.\n    - Added support for nodes and properties with spaces in their name.\n- Sharing:\n    - Removed persisted URL in share links to avoid getting stuck on shared dashboards\n- Table:\n    - Added option to specify relative column sizes\n- Graph:\n    - Changed node styling to use the last (most specific label) for applying customizations\n    - Fixed error where incorrect properties were extracted from graphs with multi-labeled nodes\n    - Fixed node display to hide \"undefined\" when a non-existing property is selected for that node.\n    \n## NeoDash 2.0.0, 2.0.1 & 2.0.2\n\n**New & Improved Dashboard Editor**\n- Added new Cypher editor with syntax highlighting / live syntax validation.\n- Redesigned Cypher query runner to be 2x more performant.\n- Easy custom styling of reports with the \"advanced report settings\" window.\n- Added in-built documentation with example queries and visualizations.\n- Updated dashboard layout to better use screen real estate.\n\n**Visualizations**\n- Table View\n    - New table view with post-query sorting and filtering, and highlighting of native Neo4j types.\n    - Fixed array property display in table reports.\n    - Added automatic link generation from URL properties in the table report.\n- Graph View\n    - Updated graph visualization library to a canvas-based renderer, handling 4x larger graphs.\n    - Added custom node/relationship styling with custom colors, width, and font-size.\n    - Better property display on graph visualization hover.\n- Bar/Line Chart\n    - New bar/line chart visualizations based on the Charts graph app.\n    - Added support for multi-line charts, stacked/grouped bar charts.\n    - Added log scale + explicit limit setting to bar/line charts.\n    - Line chart hover values are no longer rounded and incorrectly stacked.\n- Map View\n    - Added custom styling options to map visualizations.\n    - Added dictionary-based point property rendering on maps.\n    - Stability improvement of map views for offline deployments.\n- Single Value Report\n    - Improved single value report.\n    - Custom styling (text alignment) of single value reports.\n- Property Selection:\n    - Improved property selection documentation.\n    - Added optional \"clear parameter\" setting to parameter selection report.\n    - property selector now uses the filter to gather more results.\n\n**Saving, loading and sharing**\n- Added setting to turn entire dashboard into 'Standalone mode' from a share link.\n- Added option to save/load dashboards from both files and text.\n- New \"Try a demo\" button on the welcome screen.\n- added save/load to Neo4j database feature.\n- Auto-convert older versions of NeoDash on load.\n\n\n\n"
  },
  {
    "path": "compose.yaml",
    "content": "services:\n  neodash:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - \"5005:5005\"\n    environment:\n      - NGINX_PORT=5005\n"
  },
  {
    "path": "conf/default.conf.template",
    "content": "server {\n    listen       ${NGINX_PORT};\n    server_name  localhost;\n    include      mime.types;\n    location / {\n        root   /usr/share/nginx/html;\n        try_files $uri $uri/ /index.html;\n        index  index.html index.htm;\n    }\n    # redirect server error pages to the static page /50x.html\n    # Note: This is optional, depending on the implementation in React\n    error_page   500 502 503 504  /50x.html;\n    location = /50x.html {\n        root   /usr/share/nginx/html;\n    }\n}"
  },
  {
    "path": "cypress/Page.js",
    "content": "const DB_URL = 'localhost';\nconst DB_USERNAME = 'neo4j';\nconst DB_PASSWORD = 'test1234';\n\nexport class Page {\n  constructor(cardSelector) {\n    this.cardSelector = cardSelector;\n  }\n\n  init() {\n    cy.viewport(1920, 1080);\n    cy.visit('/', {\n      onBeforeLoad(win) {\n        win.localStorage.clear();\n      },\n    });\n    return this;\n  }\n\n  createNewDashboard() {\n    cy.get('#form-dialog-title').then(($div) => {\n      const text = $div.text();\n      if (text == 'NeoDash - Neo4j Dashboard Builder') {\n        cy.wait(100);\n        // Create new dashboard\n        cy.contains('New Dashboard').click();\n      }\n    });\n    return this;\n  }\n\n  connectToNeo4j() {\n    cy.get('#form-dialog-title', { timeout: 20000 }).should('contain', 'Connect to Neo4j');\n    cy.get('#protocol').type('neo4j{enter}');\n    cy.get('#url').clear().type(DB_URL);\n    cy.get('#dbusername').clear().type(DB_USERNAME);\n    cy.get('#dbpassword').type(DB_PASSWORD);\n    cy.get('button').contains('Connect').click();\n    cy.wait(100);\n    return this;\n  }\n\n  enableReportActions() {\n    cy.get('main button[aria-label=\"Extensions').should('be.visible').click();\n    cy.get('#checkbox-actions').scrollIntoView();\n    cy.get('#checkbox-actions').should('be.visible').click();\n    cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click();\n    cy.wait(100);\n    return this;\n  }\n\n  enableAdvancedVisualizations() {\n    cy.get('main button[aria-label=\"Extensions').should('be.visible').click();\n    cy.get('#checkbox-advanced-charts').should('be.visible').click();\n    cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click();\n    cy.wait(100);\n    return this;\n  }\n\n  enableFormsExtension() {\n    cy.get('main button[aria-label=\"Extensions').should('be.visible').click();\n    cy.get('#checkbox-forms').scrollIntoView();\n    cy.get('#checkbox-forms').should('be.visible').click();\n    cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click();\n    cy.wait(100);\n    return this;\n  }\n\n  selectReportOfType(type) {\n    cy.get('main .react-grid-item button[aria-label=\"add report\"]').should('be.visible').click();\n    cy.get('main .react-grid-item')\n      .contains('No query specified.')\n      .parentsUntil('.react-grid-item')\n      .find('button[aria-label=\"settings\"]', { timeout: 2000 })\n      .should('be.visible')\n      .click();\n    cy.get(`${this.cardSelector} #type`, { timeout: 2000 }).should('be.visible').click();\n    cy.contains(type).click();\n    cy.wait(100);\n    return this;\n  }\n\n  createReportOfType(type, query, fast = false, run = true) {\n    this.selectReportOfType(type);\n    if (fast) {\n      cy.get(`${this.cardSelector} .ReactCodeMirror`).type(query, {\n        delay: 1,\n        parseSpecialCharSequences: false,\n      });\n    } else {\n      cy.get(`${this.cardSelector} .ReactCodeMirror`).type(query, { parseSpecialCharSequences: false });\n    }\n    cy.wait(400);\n\n    if (run) {\n      this.closeSettings();\n    }\n\n    cy.wait(100);\n    return this;\n  }\n\n  openSettings() {\n    cy.get(this.cardSelector).find('button[aria-label=\"settings\"]', { WAITING_TIME: 2000 }).click();\n    cy.wait(100);\n    return this;\n  }\n\n  closeSettings() {\n    cy.get(`${this.cardSelector} button[aria-label=\"run\"]`).click();\n    cy.wait(100);\n    return this;\n  }\n\n  openAdvancedSettings() {\n    this.openSettings();\n    cy.get(this.cardSelector).contains('Advanced settings').click();\n    cy.wait(100);\n    return this;\n  }\n\n  closeAdvancedSettings() {\n    cy.get(this.cardSelector).contains('Advanced settings').click();\n    this.closeSettings();\n    return this;\n  }\n\n  openReportActionsMenu() {\n    this.openSettings();\n    cy.get(this.cardSelector).find('button[aria-label=\"custom actions\"]').click();\n    cy.wait(100);\n    return this;\n  }\n\n  updateDropdownAdvancedSetting(settingLabel, targetValue) {\n    this.openAdvancedSettings();\n    cy.get(`${this.cardSelector} .ndl-dropdown`).contains(settingLabel).siblings('div').click();\n    cy.contains(targetValue).click();\n    this.closeAdvancedSettings();\n    return this;\n  }\n\n  updateChartQuery(query) {\n    this.openSettings();\n\n    cy.get(this.cardSelector)\n      .find('.ndl-cypher-editor div[role=\"textbox\"]')\n      .should('be.visible')\n      .click()\n      .clear()\n      .type(query);\n    cy.wait(100);\n\n    this.closeSettings();\n    return this;\n  }\n}\n"
  },
  {
    "path": "cypress/e2e/charts/array.cy.js",
    "content": "import { stringArrayCypherQuery, intArrayCypherQuery, pathArrayCypherQuery } from '../../fixtures/cypher_queries';\nimport { Page } from '../../Page';\n\nconst CARD_SELECTOR = 'main .react-grid-item:eq(2)';\nconst page = new Page(CARD_SELECTOR);\n\n// Ignore warnings that may appear when using the Cypress dev server\nCypress.on('uncaught:exception', (err, runnable) => {\n  console.log(err, runnable);\n  return false;\n});\n\ndescribe('Testing array rendering', () => {\n  beforeEach('open neodash', () => {\n    page.init().createNewDashboard().connectToNeo4j();\n    cy.wait(100);\n  });\n\n  it('creates a table that contains string arrays', () => {\n    cy.checkInitialState();\n    page.enableReportActions();\n    page.createReportOfType('Table', stringArrayCypherQuery, true, true);\n\n    // Standard array, displays strings joined with comma and whitespace\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', 'initial, list');\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', 'other, list');\n\n    // Now, transpose the table\n    page.updateDropdownAdvancedSetting('Transpose Rows & Columns', 'on');\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-columnHeaderTitle:eq(1)`).should('have.text', 'initial,list');\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', 'other, list');\n\n    // Transpose back\n    // And add a report action\n    page.updateDropdownAdvancedSetting('Transpose Rows & Columns', 'off');\n    page.openReportActionsMenu();\n    cy.get('.ndl-modal').find('button[aria-label=\"add\"]').click();\n    cy.get('.ndl-modal').find('input:eq(2)').type('column');\n    cy.get('.ndl-modal').find('input:eq(5)').type('test_param');\n    cy.get('.ndl-modal').find('input:eq(6)').type('column');\n    cy.get('.ndl-modal').find('button').contains('Save').click();\n    page.closeSettings();\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`)\n      .find('button')\n      .should('be.visible')\n      .should('have.text', 'initial, list')\n      .click();\n\n    // Previous step's click set a parameter from the array\n    // Test that parameter rendering works\n    cy.get(`${CARD_SELECTOR} .MuiCardHeader-root`).find('input').type('$neodash_test_param').blur();\n    cy.get(`${CARD_SELECTOR} .MuiCardHeader-root`).find('input').should('have.value', 'initial, list');\n  });\n\n  it('creates a table that contains int arrays', () => {\n    cy.checkInitialState();\n    page.createReportOfType('Table', intArrayCypherQuery, true, true);\n\n    // Standard array, displays strings joined with comma and whitespace\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', '1, 2');\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', '3, 4');\n\n    // Now, transpose the table\n    page.updateDropdownAdvancedSetting('Transpose Rows & Columns', 'on');\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-columnHeaderTitle:eq(1)`).should('have.text', '1,2');\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', '3, 4');\n  });\n\n  it('creates a table that contains nodes and rels', () => {\n    cy.checkInitialState();\n    page.createReportOfType('Table', pathArrayCypherQuery, true, true);\n\n    // Standard array, displays a path with two nodes and a relationship\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', 'PersonACTED_INMovie');\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button`).should('have.length', 2);\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button:eq(0)`).should('have.text', 'Person');\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button:eq(1)`).should('have.text', 'Movie');\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) .MuiChip-root`).should('have.length', 1);\n    cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) .MuiChip-root`).should('have.text', 'ACTED_IN');\n  });\n\n  it('creates a single value report which is an array', () => {\n    cy.checkInitialState();\n    page.createReportOfType('Single Value', stringArrayCypherQuery, true, true);\n    cy.get(CARD_SELECTOR).should('have.text', 'initial, list');\n  });\n\n  it('creates a multi parameter select', () => {\n    cy.checkInitialState();\n    page.selectReportOfType('Parameter Select');\n    cy.get('main .react-grid-item:eq(2) label[for=\"Selection Type\"]').siblings('div').click();\n    // Set up the parameter select\n    cy.contains('Node Property').click();\n    cy.wait(100);\n    cy.contains('Node Label').click();\n    cy.contains('Node Label').siblings('div').find('input').type('Movie');\n    cy.wait(1000);\n    cy.get('.MuiAutocomplete-popper').contains('Movie').click();\n    cy.contains('Property Name').click();\n    cy.contains('Property Name').siblings('div').find('input').type('title');\n    cy.wait(1000);\n    cy.get('.MuiAutocomplete-popper').contains('title').click();\n    // Enable multiple selection\n    page.closeSettings();\n    page.updateDropdownAdvancedSetting('Multiple Selection', 'on');\n    // Finally, select a few values in the parameter select\n    cy.get(CARD_SELECTOR).contains('Movie title').click();\n    cy.get(CARD_SELECTOR).contains('Movie title').siblings('div').find('input').type('a');\n    cy.get('.MuiAutocomplete-popper').contains('Apollo 13').click();\n    cy.get(CARD_SELECTOR).contains('Movie title').siblings('div').find('input').type('t');\n    cy.get('.MuiAutocomplete-popper').contains('The Matrix').click();\n    cy.get(CARD_SELECTOR).contains('Apollo 13').should('be.visible');\n    cy.get(CARD_SELECTOR).contains('The Matrix').should('be.visible');\n  });\n});\n"
  },
  {
    "path": "cypress/e2e/charts/bar.cy.js",
    "content": "import { barChartCypherQuery } from '../../fixtures/cypher_queries';\nimport { Page } from '../../Page';\n\nconst CARD_SELECTOR = '.react-grid-layout:eq(0) .MuiGrid-root:eq(2)';\nconst page = new Page(CARD_SELECTOR);\n\n// Ignore warnings that may appear when using the Cypress dev server\nCypress.on('uncaught:exception', (err, runnable) => {\n  console.log(err, runnable);\n  return false;\n});\n\ndescribe('Testing bar chart', () => {\n  beforeEach('open neodash', () => {\n    page.init().createNewDashboard().connectToNeo4j().createReportOfType('Bar Chart', barChartCypherQuery);\n  });\n\n  it('Checking Colour Picker settings', () => {\n    //Opens advanced settings\n    cy.get('.react-grid-layout')\n      .first()\n      .within(() => {\n        //Finds the 2nd card\n        cy.get('.MuiGrid-root:eq(2)').within(() => {\n          // Access advanced settings\n          cy.get('button').eq(1).click();\n          cy.get('[role=\"switch\"]').click();\n          cy.wait(200);\n          // Changing setting for colour picker\n          cy.get('[data-testid=\"colorpicker-input\"]').find('input').click().type('{selectall}').type('red');\n          cy.get('button[aria-label=\"run\"]').click();\n          // Checking that colour picker was applied correctly\n          cy.get('.card-view').should('have.css', 'background-color', 'rgb(255, 0, 0)');\n          cy.wait(200);\n          // Changing colour back to white\n          cy.get('button').eq(1).click();\n          cy.get('[data-testid=\"colorpicker-input\"]').find('input').click().type('{selectall}').type('white');\n          cy.get('button[aria-label=\"run\"]').click();\n          // Checking colour has been set back to white\n          cy.wait(200);\n          cy.get('.card-view').should('have.css', 'background-color', 'rgb(255, 255, 255)');\n        });\n      });\n  });\n\n  it('Checking Selector Description', () => {\n    //Opens first 2nd card\n    cy.get('.react-grid-layout:eq(0) .MuiGrid-root:eq(2)').within(() => {\n      // Access advanced settings\n      cy.get('button').eq(1).click();\n      cy.get('[role=\"switch\"]').click();\n      cy.wait(200);\n      // Changing Selector Description to 'Test'\n      cy.get('.ndl-textarea').contains('span', 'Selector Description').click().type('Test');\n      cy.get('button[aria-label=\"run\"]').click();\n      // Pressing Selector Description button\n      cy.get('button[aria-label=\"details\"]').click();\n    });\n    // Checking that Selector Description is behaving as expected\n    cy.get('.MuiDialog-paper').should('be.visible').and('contain.text', 'Test');\n    cy.wait(1000);\n\n    // Click elsewhere on the page to close dialog box\n    cy.get('div[role=\"dialog\"]').parent().click(-100, -100, { force: true });\n  });\n\n  it('Checking full screen bar chart setting', () => {\n    page.updateDropdownAdvancedSetting('Fullscreen enabled', 'on');\n    cy.get('button[aria-label=\"maximize\"]').click();\n    // Checking existence of full-screen modal\n    cy.get('.dialog-xxl').should('be.visible');\n    // Action to close full-screen modal\n    cy.get('button[aria-label=\"un-maximize\"]').click();\n    // Checking that fullscreen has un-maximized\n    // Check that the div is no longer in the DOM\n    cy.get('div[data-focus-lock-disabled=\"false\"]').should('not.exist');\n  });\n\n  it('Checking \"Autorun Query\" works as intended', () => {\n    page.updateDropdownAdvancedSetting('Auto-run query', 'off');\n    cy.get('.MuiCardContent-root').find('.ndl-cypher-editor').should('be.visible');\n    cy.get('.MuiCardContent-root').find('g').should('not.exist');\n    cy.wait(100);\n    cy.get('.MuiCardContent-root').find('button[aria-label=\"run\"]').filter(':visible').click();\n    cy.get('g').should('exist');\n  });\n\n  it('Checking Legend integration works as intended', () => {\n    page.updateDropdownAdvancedSetting('Show Legend', 'on');\n    // Checking that legend matches value specified: in the case - 'count'\n    cy.get('svg g g text').last().contains(/count/i);\n\n    page.updateDropdownAdvancedSetting('Show Legend', 'off');\n    cy.get('svg g g text').last().contains(/count/i).should('not.exist');\n  });\n\n  it('Checking the stacked grouping function works as intended', () => {\n    const TRANSLATE_REGEXP = /translate\\(([0-9]{1,3}), [0-9]{1,3}\\)/;\n\n    page\n      .updateChartQuery(\n        'MATCH (p:Person)-[:DIRECTED]->(n:Movie) RETURN n.released AS released, p.name AS Director, count(n.title) AS count LIMIT 5'\n      )\n      .updateDropdownAdvancedSetting('Grouping', 'on');\n\n    cy.get('.MuiGrid-root:eq(2)')\n      .find('.ndl-dropdown:contains(\"Group\")')\n      .find('svg')\n      .parent()\n      .click()\n      .type('Director{enter}');\n    // Checking that the groups are stacked\n    cy.get('.MuiGrid-root:eq(2)')\n      .find('g')\n      .children('g')\n      .eq(3) // Get the fourth g element (index starts from 0)\n      .invoke('attr', 'transform')\n      .then((transformValue) => {\n        // Captures the first number in the translate attribute using the parenthesis to capture the first digit and put it in the second value of the resulting array\n        // if transformValue is translate(100,200), then it will produce an array like [\"translate(100,200)\", \"100\"],\n        const match = transformValue.match(TRANSLATE_REGEXP);\n        if (match?.[1]) {\n          const xValue = match[1];\n          // Now find sibling g elements with the same x transform value\n          cy.get('.MuiCardContent-root')\n            .find('g')\n            .children('g')\n            .filter((_, element) => {\n              const siblingTransform = Cypress.$(element).attr('transform');\n              return siblingTransform?.includes(`translate(${xValue},`);\n            })\n            .should('have.length', 3); // Check that there's at least one element\n        } else {\n          throw new Error('Transform attribute not found or invalid format');\n        }\n      });\n    cy.get('.ndl-dropdown:contains(\"Group\")').find('svg').parent().click().type('(none){enter}');\n    //Checking that the stacked grouped elements do not exist\n    cy.get('.MuiCardContent-root')\n      .find('g')\n      .children('g')\n      .eq(3) // Get the fourth g element (index starts from 0)\n      .invoke('attr', 'transform')\n      .then((transformValue) => {\n        // Captures the first number in the translate attribute using the parenthesis to capture the first digit and put it in the second value of the resulting array\n        // if transformValue is translate(100,200), then it will produce an array like [\"translate(100,200)\", \"100\"],\n        const match = transformValue.match(TRANSLATE_REGEXP);\n        if (match?.[1]) {\n          const xValue = match[1];\n          // Now find sibling g elements with the same x transform value\n          cy.get('.MuiCardContent-root')\n            .find('g')\n            .children('g')\n            .filter((_, element) => {\n              const siblingTransform = Cypress.$(element).attr('transform');\n              return siblingTransform?.includes(`translate(${xValue},`);\n            })\n            .should('have.length', 1); // Check that there are no matching elements\n        } else {\n          throw new Error('Transform attribute not found or invalid format');\n        }\n      });\n  });\n\n  // How to properly test this?\n  it.skip('Testing grouped grouping mode', () => {\n    page\n      .updateChartQuery(\n        'MATCH (p:Person)-[:DIRECTED]->(n:Movie) RETURN n.released AS released, p.name AS Director, count(n.title) AS count LIMIT 5'\n      )\n      .updateDropdownAdvancedSetting('Grouping', 'on')\n      .updateDropdownAdvancedSetting('Group Mode', 'grouped');\n    cy.get('.ndl-dropdown:contains(\"Group\")').find('svg').parent().click().type('Director{enter}');\n  });\n\n  it('Testing \"Show Value on Bars\"', () => {\n    page.updateDropdownAdvancedSetting('Show Values On Bars', 'on');\n    cy.get('.react-grid-layout:eq(0) .MuiGrid-root:eq(2)').find('div svg > g > g > text').should('have.length', 5);\n\n    page.updateDropdownAdvancedSetting('Show Values On Bars', 'off');\n    cy.get('.react-grid-layout:eq(0) .MuiGrid-root:eq(2)').find('div svg > g > g > text').should('not.exist');\n  });\n\n  describe('Y axis display', () => {\n    it('Checking Y axis is displayed', () => {\n      page.updateDropdownAdvancedSetting('Display Y axis', 'on');\n      cy.get('.MuiCardContent-root svg > g > g:nth-child(3)')\n        .invoke('attr', 'transform')\n        .should('eq', 'translate(0,0)');\n    });\n\n    it('Checking Y axis is hidden', () => {\n      page.updateDropdownAdvancedSetting('Display Y axis', 'off');\n      cy.get('.MuiCardContent-root svg > g > g:nth-child(3)')\n        .invoke('attr', 'transform')\n        .should('not.eq', 'translate(0,0)');\n    });\n  });\n\n  describe('Y grid lines display', () => {\n    it('Checking Y grid lines are displayed', () => {\n      page.updateDropdownAdvancedSetting('Display Y grid lines', 'on');\n      cy.get('.MuiCardContent-root svg g > g > line').invoke('attr', 'stroke').should('eq', '#dddddd');\n    });\n\n    it('Checking Y grid lines are hidden', () => {\n      page.updateDropdownAdvancedSetting('Display Y grid lines', 'off');\n      cy.get('.MuiCardContent-root svg g > g > line').invoke('attr', 'stroke').should('not.eq', '#dddddd');\n    });\n  });\n});\n"
  },
  {
    "path": "cypress/e2e/charts/table.cy.js",
    "content": "import { tableCypherQuery } from '../../fixtures/cypher_queries';\nimport { Page } from '../../Page';\n\nconst page = new Page();\n// Ignore warnings that may appear when using the Cypress dev server\nCypress.on('uncaught:exception', (err, runnable) => {\n  console.log(err, runnable);\n  return false;\n});\n\ndescribe('Testing table', () => {\n  beforeEach('open neodash', () => {\n    page.init().createNewDashboard().connectToNeo4j();\n    cy.wait(100);\n  });\n\n  it.skip('create a table', () => {\n    //Opens the div containing all report cards\n    cy.get('.react-grid-layout:eq(0)')\n      .first()\n      .within(() => {\n        //Finds the 2nd card\n        cy.get('.MuiGrid-root')\n          .eq(1)\n          .within(() => {\n            //Clicks the 2nd button (opens settings)\n            cy.get('button').eq(1).click();\n            // cy.get('div[role=\"textbox\"')\n          });\n      });\n    cy.get('.react-grid-layout')\n      .first()\n      .within(() => {\n        //Finds the 2nd card\n        cy.get('.MuiGrid-root')\n          .eq(1)\n          .within(() => {\n            //Opens the drop down\n            cy.getDataTest('type-dropdown').click();\n          });\n      });\n    // Selects the Table option\n    cy.get('[id^=\"react-select-5-option\"]').contains(/Table/).should('be.visible').click({ force: true });\n    cy.get('.react-grid-layout .MuiGrid-root:eq(1) #type input[name=\"Type\"]').should('have.value', 'Table');\n\n    //Removes text in cypher editor and types new query\n    cy.get('.react-grid-layout')\n      .first()\n      .within(() => {\n        //Finds the 2nd card\n        cy.get('.MuiGrid-root')\n          .eq(1)\n          .within(() => {\n            //Replaces default query with new query\n            cy.get('.ndl-cypher-editor div[role=\"textbox\"]').clear().type(tableCypherQuery);\n            cy.get('button[aria-label=\"run\"]').click();\n          });\n      });\n  });\n});\n"
  },
  {
    "path": "cypress/e2e/start_page.cy.js",
    "content": "import {\n  tableCypherQuery,\n  barChartCypherQuery,\n  mapChartCypherQuery,\n  sunburstChartCypherQuery,\n  iFrameText,\n  markdownText,\n  loadDashboardURL,\n  sankeyChartCypherQuery,\n  gaugeChartCypherQuery,\n  formCypherQuery,\n} from '../fixtures/cypher_queries';\n\nimport { Page } from '../Page';\n\nconst CARD_SELECTOR = 'main .react-grid-item:eq(2)';\nconst page = new Page(CARD_SELECTOR);\n\n// Ignore warnings that may appear when using the Cypress dev server\nCypress.on('uncaught:exception', (err, runnable) => {\n  console.log(err, runnable);\n  return false;\n});\n\ndescribe('NeoDash E2E Tests', () => {\n  beforeEach(() => {\n    page.init().createNewDashboard().connectToNeo4j();\n    cy.wait(100);\n  });\n\n  it('initializes the dashboard', () => {\n    cy.checkInitialState();\n  });\n\n  it('creates a new card', () => {\n    cy.checkInitialState();\n    cy.createCard();\n  });\n\n  // Test each type of card\n  it('creates a table report', () => {\n    cy.checkInitialState();\n    cy.get('main .react-grid-item button[aria-label=\"add report\"]').should('be.visible').click();\n    cy.get('main .react-grid-item')\n      .contains('No query specified.')\n      .parentsUntil('.react-grid-item')\n      .find('button[aria-label=\"settings\"]', { timeout: 2000 })\n      .should('be.visible')\n      .click();\n\n    cy.get('main .react-grid-item:eq(2) #type input[name=\"Type\"]').should('have.value', 'Table');\n    cy.get('main .react-grid-item:eq(2) .ReactCodeMirror').type(tableCypherQuery);\n    cy.wait(400);\n\n    cy.get('main .react-grid-item:eq(2)').contains('Advanced settings').click();\n\n    cy.get('main .react-grid-item:eq(2) button[aria-label=\"run\"]').click();\n    cy.get('main .react-grid-item:eq(2) .MuiDataGrid-columnHeaders')\n      .should('contain', 'title')\n      .and('contain', 'released')\n      .and('not.contain', '__id');\n    // cy.get('main .react-grid-item:eq(2) .MuiDataGrid-virtualScroller .MuiDataGrid-row').should('have.length', 8);\n    // cy.get('main .react-grid-item:eq(2) .MuiDataGrid-footerContainer').should('contain', '1–8 of 8');\n    // cy.get('main .react-grid-item:eq(2) .MuiDataGrid-footerContainer button[aria-label=\"Go to next page\"]').click();\n    // cy.get('main .react-grid-item:eq(2) .MuiDataGrid-virtualScroller .MuiDataGrid-row').should('have.length', 3);\n    // cy.get('main .react-grid-item:eq(2) .MuiDataGrid-footerContainer').should('contain', '6–8 of 8');\n  });\n\n  it('creates a bar chart report', () => {\n    cy.checkInitialState();\n    page.createReportOfType('Bar Chart', barChartCypherQuery);\n    cy.get('main .react-grid-item:eq(2) #index input[name=\"Category\"]').should('have.value', 'released');\n    cy.get('main .react-grid-item:eq(2) #value input[name=\"Value\"]').should('have.value', 'count');\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g').should('have.length', 8);\n  });\n\n  it('creates a pie chart report', () => {\n    cy.checkInitialState();\n    page.createReportOfType('Pie Chart', barChartCypherQuery);\n    cy.get('main .react-grid-item:eq(2) #index input[name=\"Category\"]').should('have.value', 'released');\n    cy.get('main .react-grid-item:eq(2) #value input[name=\"Value\"]').should('have.value', 'count');\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g').should('have.length', 3);\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g > path').should('have.length', 5);\n  });\n\n  it('creates a line chart report', () => {\n    cy.checkInitialState();\n    page.createReportOfType('Line Chart', barChartCypherQuery);\n    cy.get('main .react-grid-item:eq(2) #x input[name=\"X-value\"]').should('have.value', 'released');\n    cy.get('main .react-grid-item:eq(2) #value input[name=\"Y-value\"]').should('have.value', 'count');\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g').should('have.length', 6);\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g:nth-child(2) > line').should(\n      'have.length',\n      11\n    );\n  });\n\n  it('creates a map chart report', () => {\n    cy.checkInitialState();\n    page.createReportOfType('Map', mapChartCypherQuery, true);\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > path').should('have.length', 5);\n  });\n\n  it('creates a single value report', () => {\n    cy.checkInitialState();\n    page.createReportOfType('Single Value', barChartCypherQuery);\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root > div > div:nth-child(2) > span')\n      .invoke('text')\n      .then((text) => {\n        expect(text).to.be.oneOf(['1999', '1,999', '1 999']);\n      });\n  });\n\n  it.skip('creates a gauge chart report', () => {\n    page.enableAdvancedVisualizations();\n    cy.checkInitialState();\n    page.createReportOfType('Gauge Chart', gaugeChartCypherQuery);\n    cy.get('.text-group > text').contains('69');\n  });\n\n  it('creates a sunburst chart report', () => {\n    page.enableAdvancedVisualizations();\n    cy.checkInitialState();\n    page.createReportOfType('Sunburst Chart', sunburstChartCypherQuery);\n    cy.get('main .react-grid-item:eq(2) #index input[name=\"Path\"]').should('have.value', 'x.path');\n    cy.get('main .react-grid-item:eq(2) #value input[name=\"Value\"]').should('have.value', 'x.value');\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g:nth-child(1) > path').should('have.length', 5);\n  });\n\n  it('creates a circle packing report', () => {\n    page.enableAdvancedVisualizations();\n    cy.checkInitialState();\n    page.createReportOfType('Circle Packing', sunburstChartCypherQuery);\n    cy.get('main .react-grid-item:eq(2) #index input[name=\"Path\"]').should('have.value', 'x.path');\n    cy.get('main .react-grid-item:eq(2) #value input[name=\"Value\"]').should('have.value', 'x.value');\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > circle').should('have.length', 6);\n  });\n\n  it('creates a tree map report', () => {\n    page.enableAdvancedVisualizations();\n    cy.checkInitialState();\n    page.createReportOfType('Treemap', sunburstChartCypherQuery);\n    cy.get('main .react-grid-item:eq(2) #index input[name=\"Path\"]').should('have.value', 'x.path');\n    cy.get('main .react-grid-item:eq(2) #value input[name=\"Value\"]').should('have.value', 'x.value');\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g').should('have.length', 6);\n  });\n\n  it('creates a sankey chart report', () => {\n    page.enableAdvancedVisualizations();\n    cy.checkInitialState();\n    page.createReportOfType('Sankey Chart', sankeyChartCypherQuery, true);\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > path').should('have.attr', 'fill-opacity', 0.5);\n  });\n\n  it('creates a raw json report', () => {\n    cy.checkInitialState();\n    page.createReportOfType('Raw JSON', barChartCypherQuery);\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root textarea:nth-child(1)', { timeout: 45000 }).should(\n      ($div) => {\n        const text = $div.text();\n        expect(text.length).to.eq(1387);\n      }\n    );\n  });\n\n  it('creates a parameter select report', () => {\n    cy.checkInitialState();\n    page.selectReportOfType('Parameter Select');\n    cy.wait(500);\n    cy.get('#autocomplete-label-type').type('Movie');\n    cy.get('#autocomplete-label-type-option-0').click();\n    cy.wait(500);\n    cy.get('#autocomplete-property').type('title');\n    cy.get('#autocomplete-property-option-0').click();\n    cy.get('main .react-grid-item:eq(2) button[aria-label=\"run\"]').click();\n    cy.get('#autocomplete').type('The Matrix');\n    cy.get('#autocomplete-option-0').click();\n  });\n\n  it('creates an iframe report', () => {\n    cy.checkInitialState();\n    page.createReportOfType('iFrame', iFrameText);\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root iframe', { timeout: 45000 }).should('be.visible');\n  });\n\n  it('creates a markdown report', () => {\n    cy.checkInitialState();\n    page.createReportOfType('Markdown', markdownText);\n    cy.get('main .react-grid-item:eq(2) .MuiCardContent-root h1', { timeout: 45000 }).should('have.text', 'Hello');\n  });\n\n  it.skip('creates a form report', () => {\n    page.enableFormsExtension();\n    cy.checkInitialState();\n    page.createReportOfType('Form', formCypherQuery, true, false);\n    cy.get('main .react-grid-item:eq(2) .form-add-parameter').click();\n    cy.wait(200);\n    cy.get('#autocomplete-label-type').type('Movie');\n    cy.get('#autocomplete-label-type-option-0').click();\n    cy.wait(200);\n    cy.get('#autocomplete-property').type('title');\n    cy.get('#autocomplete-property-option-0').click();\n\n    cy.get('.ndl-dialog-close').click();\n\n    cy.get('main .react-grid-item:eq(2) button[aria-label=\"run\"]').scrollIntoView().should('be.visible').click();\n    cy.wait(500);\n    cy.get('#form-submit').should('be.disabled');\n    cy.get('#autocomplete').type('The Matrix');\n    cy.get('#autocomplete-option-0').click();\n    cy.get('#form-submit').should('not.be.disabled');\n    cy.get('#form-submit').click();\n    cy.wait(500);\n    cy.get('.form-submitted-message').should('have.text', 'Form Submitted.Reset Form');\n  });\n\n  // Test load stress-test dashboard from file\n  // TODO - this test is flaky, especially in GitHub actions environment.\n  it.skip('test load dashboard from file and stress test report customizations', () => {\n    try {\n      const NUMBER_OF_PAGES_IN_STRESS_TEST_DASHBOARD = 5;\n      const file = cy.request(loadDashboardURL).should((response) => {\n        cy.get('#root .MuiDrawer-root .MuiIconButton-root:eq(2)').click();\n        cy.get('.MuiDialog-root .MuiPaper-root .MuiDialogContent-root textarea:eq(0)')\n          .invoke('val', response.body)\n          .trigger('change');\n        cy.get('.MuiDialog-root .MuiPaper-root .MuiDialogContent-root textarea:eq(0)').type(' ');\n        cy.get('.MuiDialog-root .MuiDialogContent-root .MuiButtonBase-root:eq(2)').click();\n        cy.wait(2500);\n\n        // Click on each page and wait ~3 seconds for it to load completely\n        for (let i = 1; i < NUMBER_OF_PAGES_IN_STRESS_TEST_DASHBOARD; i++) {\n          cy.get('.MuiAppBar-root .react-grid-item:eq(' + i + ')').click();\n          cy.wait(3000);\n        }\n      });\n    } catch (e) {\n      console.log('Unable to fetch test dashboard. Skipping test.');\n    }\n  });\n});\n"
  },
  {
    "path": "cypress/fixtures/cypher_queries.js",
    "content": "// Cypher queries - for component testing\nexport const defaultCypherQuery = 'MATCH (n) RETURN n LIMIT 25';\nexport const tableCypherQuery =\n  'MATCH (n:Movie) RETURN n.title AS title, n.released AS released, id(n) AS __id LIMIT 8';\nexport const barChartCypherQuery = 'MATCH (n:Movie) RETURN n.released AS released, count(n.title) AS count LIMIT 5';\nexport const mapChartCypherQuery =\n  \"UNWIND [{id: 'Tilburg', label: 'Cinema', point: point({latitude:51.59444886664065 , longitude:5.088862976119185})}, {id: 'Antwerp', label: 'Cinema', point: point({latitude:51.22065200961528  , longitude:4.414094044161085})}, \\n\" +\n  \"{id: 'Brussels', label: 'Cinema', point: point({latitude:50.854284724408664, longitude:4.344177490986771})},{id: 'Cologne', label: 'Cinema', point: point({latitude:50.94247712506476  , longitude:6.9699327434361855 })}, \\n\" +\n  \"{id: 'Nijmegen', label: 'Cinema', point: point({latitude:51.81283449474347 , longitude:5.866804797140869})},{start: 'Tilburg', end: 'Antwerp', type: 'ROUTE_TO', distance: '125km', id: 100}, {start: 'Antwerp', end: 'Brussels', type: 'ROUTE_TO', distance: '70km', id: 101}, \\n\" +\n  \"{start: 'Brussels', end: 'Cologne', type: 'ROUTE_TO', distance: '259km', id: 102},{start: 'Cologne', end: 'Nijmegen', type: 'ROUTE_TO', distance: '180km', id: 103},{start: 'Nijmegen', end: 'Tilburg', type: 'ROUTE_TO', distance: '92km', id: 104}] as value RETURN value//\";\nexport const sunburstChartCypherQuery =\n  \"UNWIND [{path: ['a', 'b'], value: 3}, {path: ['a', 'c'], value: 5},{path: ['a', 'd', 'e'], value: 2},{path: ['a', 'd', 'f'], value: 3}] as x RETURN x.path, x.value\";\nexport const sankeyChartCypherQuery =\n  \"WITH [ { path: {  start: {labels: ['Person'], identity: 1, properties: {name: 'Jim'}},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: ['Person'], identity: 1, properties: {name: 'Jim'}}, relationship: {type: 'RATES', start: 1, end: 11, identity: 10001, properties: {value: 4.5}}, end: {labels: ['Movie'], identity: 11,properties: {title: 'The Matrix', released: 1999}} } ] }, person: 'Jim', movie: 'The Matrix', value: 4.5 }, { path: {  start: {labels: ['Person'], identity: 2, properties: {name: 'Mike'}},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: ['Person'], identity: 2, properties: {name: 'Mike'}}, relationship: {type: 'RATES', start: 2, end: 11, identity: 10002, properties: {value: 3.8}}, end: {labels: ['Movie'], identity: 11,properties: {title: 'The Matrix', released: 1999}} } ] }, person: 'Mike', movie: 'The Matrix', value: 3.8 } ] as data UNWIND data as row RETURN row.path as Path\";\nexport const gaugeChartCypherQuery = 'RETURN 69';\nexport const formCypherQuery = 'MATCH (n:Movie) WHERE n.title = $neodash_movie_title SET n.rating = 92';\n\n// Cypher queries - for renderer testing\nexport const stringArrayCypherQuery = \"RETURN ['initial', 'list'] AS column, ['other', 'list'] AS otherColumn\";\nexport const intArrayCypherQuery = 'RETURN [1, 2] AS column, [3, 4] AS otherColumn';\nexport const pathArrayCypherQuery = 'MATCH p=(:Person)-[:ACTED_IN]->(:Movie) WITH p LIMIT 1 RETURN p';\n\n// Other content fixtures\nexport const iFrameText = 'https://www.wikipedia.org/';\nexport const markdownText = '# Hello';\nexport const loadDashboardURL =\n  'https://gist.githubusercontent.com/nielsdejong/ee33245256b471f92901ca4073b16ec1/raw/cfaae47e0fcdf430a5de6d0d8e3ac13cfd97742e/dashboard-cypress.json';\n"
  },
  {
    "path": "cypress/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport './support/commands';\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "cypress/plugins/index.js",
    "content": "module.exports = (on, config) => {\n  require('@cypress/code-coverage/task')(on, config);\n  //Used to instrument code ran like unit tests\n  on('file:preprocessor', require('@cypress/code-coverage/use-babelrc'));\n  on('before:browser:launch', (browser, launchOptions) => {\n    if (browser.family === 'chromium') {\n      console.log('Adding Chrome flag: --disable-dev-shm-usage');\n      launchOptions.args.push('--disable-dev-shm-usage');\n    }\n    return launchOptions;\n  });\n  return config;\n};\n"
  },
  {
    "path": "cypress/support/commands.js",
    "content": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom commands and overwrite\n// existing commands.\n//\n// For more comprehensive examples of custom\n// commands please read more here:\n// https://on.cypress.io/custom-commands\n// ***********************************************\n//\n//\n// -- This is a parent command --\n// Cypress.Commands.add('login', (email, password) => { ... })\n//\n//\n// -- This is a child command --\n// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })\n//\n//\n// -- This is a dual command --\n// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })\n//\n//\n// -- This will overwrite an existing command --\n// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })\nCypress.Commands.add('getDataTest', (dataTestSelector) => {\n  return cy.get(`[data-test=\"${dataTestSelector}\"]`);\n});\n\n/**\n * Function to interact with a specific element and execute additional custom commands.\n * @param {Function} customAction - A callback function containing custom Cypress commands.\n */\n\n//Used in start_page.cy.js\nCypress.Commands.add('checkInitialState', () => {\n  // Check the starter cards\n  cy.get('main .react-grid-item:eq(0)').should('contain', 'This is your first dashboard!');\n  cy.get('main .react-grid-item:eq(1) .force-graph-container canvas').should('be.visible');\n  cy.get('main .react-grid-item:eq(2) button').should('have.attr', 'aria-label', 'add report');\n});\n\n// Creates a card\nconst WAITING_TIME = 20000;\nCypress.Commands.add('createCard', () => {\n  // Check the starter cards\n  cy.get('main .react-grid-item button[aria-label=\"add report\"]', { timeout: WAITING_TIME })\n    .should('be.visible')\n    .click();\n  cy.wait(1000);\n  cy.get('main .react-grid-item:eq(2)').should('contain', 'No query specified.');\n});\n"
  },
  {
    "path": "cypress/support/e2e.ts",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport './commands';\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n\nimport '@cypress/code-coverage/support';\n"
  },
  {
    "path": "cypress.config.ts",
    "content": "/* eslint @typescript-eslint/no-var-requires: \"off\" */\nimport { defineConfig } from 'cypress';\n\nexport default defineConfig({\n  projectId: 'a8nh14',\n  video: false,\n  e2e: {\n    defaultCommandTimeout: 20000,\n    experimentalMemoryManagement: true,\n    numTestsKeptInMemory: 0,\n    baseUrl: 'http://localhost:3000',\n    setupNodeEvents(on, config) {\n      return require('./cypress/plugins/index.js')(on, config);\n    },\n    retries: {\n      runMode: 2,\n      openMode: 2,\n    },\n  },\n  env: {\n    codeCoverage: {\n      exclude: ['cypress/**/*.*'],\n    },\n  },\n});\n"
  },
  {
    "path": "docs/README.md",
    "content": "# NeoDash Documentation\n\nThis folder contains the documentation for the NeoDash project. The pages are written in AsciiDoc, and generated into webpages by Antora.\n\nAn external workflow picks up this directory, embeds it into the Neo4j docs, and makes sure generated files are automatically deployed to:\n```\nhttps://neo4j.com/labs/neodash/{version}\n```\nFor example: https://neo4j.com/labs/neodash/2.4\n\n## Local Build\nTo compile and view the documentation locally, navigate to this (`./docs`) folder and run:\n```\nyarn install\nyarn start\n```\n\nThen, open your browser and navigate to http://localhost:8000/."
  },
  {
    "path": "docs/antora.yml",
    "content": "name: neodash\nversion: 2.4\ntitle: NeoDash\nstart_page: ROOT:index.adoc\nnav:\n  - modules/ROOT/nav.adoc\n\nasciidoc:\n  attributes:\n    docs-version: 2.4\n    page-product: NeoDash\n    page-type: NeoDash Manual\n    page-canonical-root: /labs"
  },
  {
    "path": "docs/modules/ROOT/nav.adoc",
    "content": "* xref:index.adoc[Introduction]\n* xref:quickstart.adoc[Quickstart]\n* xref:user-guide/index.adoc[User Guide]\n** xref:user-guide/dashboards.adoc[Dashboards]\n** xref:user-guide/pages.adoc[Pages]\n** xref:user-guide/reports/index.adoc[Reports]\n*** xref:user-guide/reports/table.adoc[Table]\n*** xref:user-guide/reports/graph.adoc[Graph]\n*** xref:user-guide/reports/bar-chart.adoc[Bar Chart]\n*** xref:user-guide/reports/pie-chart.adoc[Pie Chart]\n*** xref:user-guide/reports/line-chart.adoc[Line Chart]\n*** xref:user-guide/reports/graph3d.adoc[3D Graph]\n*** xref:user-guide/reports/sunburst.adoc[Sunburst]\n*** xref:user-guide/reports/circle-packing.adoc[Circle Packing]\n*** xref:user-guide/reports/choropleth.adoc[Choropleth]\n*** xref:user-guide/reports/areamap.adoc[Area Map]\n*** xref:user-guide/reports/treemap.adoc[Treemap]\n*** xref:user-guide/reports/radar.adoc[Radar Chart]\n*** xref:user-guide/reports/sankey.adoc[Sankey Chart]\n*** xref:user-guide/reports/gantt.adoc[Gantt Chart]\n*** xref:user-guide/reports/map.adoc[Map]\n*** xref:user-guide/reports/single-value.adoc[Single Value]\n*** xref:user-guide/reports/gauge-chart.adoc[Gauge Chart]\n*** xref:user-guide/reports/raw-json.adoc[Raw JSON]\n*** xref:user-guide/reports/parameter-select.adoc[Parameter Select]\n*** xref:user-guide/reports/form.adoc[Form]\n*** xref:user-guide/reports/iframe.adoc[iFrame]\n*** xref:user-guide/reports/markdown.adoc[Markdown]\n** xref:user-guide/publishing.adoc[Publishing]\n** xref:user-guide/bloom-integration.adoc[Bloom Integration]\n** xref:user-guide/extensions/index.adoc[Extensions]\n*** xref:user-guide/extensions/advanced-visualizations.adoc[Advanced Visualizations]\n*** xref:user-guide/extensions/rule-based-styling.adoc[Rule-Based Styling]\n*** xref:user-guide/extensions/report-actions.adoc[Report Actions]\n*** xref:user-guide/extensions/natural-language-queries.adoc[Text2Cypher - Natural Language Queries]\n*** xref:user-guide/extensions/forms.adoc[Forms]\n*** xref:user-guide/extensions/access-control-management.adoc[Access Control Management]\n** xref:user-guide/faq.adoc[FAQ]\n* xref:developer-guide/index.adoc[Developer Guide]\n** xref:developer-guide/build-and-run.adoc[Build & Run]\n** xref:developer-guide/deploy-a-build.adoc[Deploy a Build]\n** xref:developer-guide/configuration.adoc[Configuration]\n** xref:developer-guide/standalone-mode.adoc[Standalone Mode]\n** xref:developer-guide/component-overview.adoc[Component Overview]\n** xref:developer-guide/design.adoc[Design]\n** xref:developer-guide/style-configuration.adoc[Style Configuration]\n** xref:developer-guide/adding-visualizations.adoc[Adding Visualizations]\n** xref:developer-guide/state-management.adoc[State Management]\n** xref:developer-guide/session-storage.adoc[Session Storage]\n** xref:developer-guide/testing.adoc[Testing]\n** xref:developer-guide/contributing.adoc[Contributing]\n\n"
  },
  {
    "path": "docs/modules/ROOT/pages/banner.adoc",
    "content": "[NOTE]\n====\nThis documentation pertains to the unsupported version of NeoDash, as part of Neo4j Labs.\nFor users of the supported NeoDash offering, refer to https://neo4j.com/docs/neodash-commercial/[NeoDash commercial].\n\n===="
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/adding-visualizations.adoc",
    "content": "include::../banner.adoc[]\n\n= Adding Visualizations\n\ninclude::../banner.adoc[]\n\nYou can extend NeoDash with your own visualizations without diving deep\ninto the core application. Likewise, adding a new customization to an\nexisting report requires minimal changes.\n\n== Add a Visualization\n\nYou can add a new chart to NeoDash in three steps:\n\n[arabic]\n. Make sure you have a local copy of NeoDash installed and running:\n\n....\ngit clone git@github.com:neo4j-labs/neodash.git\ngit checkout develop\nyarn install\nyarn run dev\n....\n\n[arabic, start=2]\n. Create a new file `src/charts/ABCChart.tsx`. In here, add a new object\nthat implements the `ChartProps` interface:\n\n....\nexport interface ChartProps {\n   records: Neo4jRecord[]; // Query output, Neo4j records as returned from the driver.\n   selection?: Record<string, any>; // A dictionary with the selection made in the report footer.\n   settings?: Record<string, any>; // A dictionary with the 'advanced settings' specified through the NeoDash interface.\n   dimensions?: Number[]; // a 2D array with the dimensions of the report (likely not needed, charts automatically fill up space).\n   fullscreen?: boolean; // flag indicating whether the report is rendered in a fullscreen view.\n   queryCallback?: (query: string, parameters: Record<string, any>, records: Neo4jRecord[]) => null; // Optionally, a way for the report to read more data from Neo4j.\n   setGlobalParameter?: (name: string, value: string) => void; // Allows a chart to update a global dashboard parameter to be used in Cypher queries for other reports.\n   getGlobalParameter?: (name) => string; // Allows a chart to get a global dashboard parameter.\n}\n....\n\nNote that the only mandatory property is `records`. This contains a list\nof\nhttps://neo4j.com/docs/api/javascript-driver/current/class/lib6/record.js~Record.html[records]\nreturned from the Cypher query specified by the user.\n\nFor inspiration, below is a basic example of a component that renders\nall returned data as a list:\n\n....\nimport React from 'react';\nimport { ChartProps } from './Chart';\nimport { renderValueByType } from '../report/ReportRecordProcessing';\n\nconst NeoListReport = (props: ChartProps) => {\n   const records = props.records;\n   return records.map(r => {\n       return <div>{\n           r[\"_fields\"].map(value => {\n               return <>{renderValueByType(value)},</>\n           })}\n       </div>\n   })\n}\n\nexport default NeoListReport;\n....\n\n[arabic, start=3]\n. Make your component selectable. Now that you’ve created a new chart\ntype, you need to tell the card settings window that it can be chosen by\na user.\n\nTo accomplish this, open `config/ReportConfig.tsx`. Add a new entry to\nthe `REPORT_TYPES` dictionary:\n\n....\nexport const REPORT_TYPES = {\n   ...\n   \"list\": {\n       label: \"List\",\n       helperText: \"I'm a list\",\n       component: NeoListReport,\n       maxRecords: 10,\n       settings: {}\n   },\n   ...\n}\n....\n\nInspect the other entries for examples of the fields that each entry can\nhave. Restart the application, and you should be able to select your new\nchart type. Finally, *Cypress* can be used to develop an end-to-end test\nfor your component in a matter of minutes. See Testing for more on\nCypress testing.\n\n____\nAfter you added a visualization or a new customization, consider\ncontributing it to the NeoDash project by creating a\nhttps://github.com/neo4j-labs/neodash/pulls[Pull Request].\n____\n"
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/build-and-run.adoc",
    "content": "include::../banner.adoc[]\n\n= Build & Run\n\ninclude::../banner.adoc[]\n\nTo start developing the application, you will need to set up the\ndevelopment environment.\n\n== Run & Build using yarn\n\nNeoDash is built with React. You will need `yarn` installed to run the web\napp.\n\n____\nUse a recent version of `yarn` and `node` to build NeoDash. The\napplication has been tested with yarn 1.22.17 & node v18.8.0.\n____\n\nTo run the application in development mode: \n\n- https://github.com/neo4j-labs/neodash[clone this repository.]\n- open a terminal and navigate to the directory you just cloned. \n- run `yarn install` to install the necessary dependencies. \n- run `yarn run dev` to run the app in development mode. \n- the application should be available at http://localhost:3000.\n\nTo build the app for production: \n\n- follow the steps above to clone the repository and install dependencies. \n- execute `yarn run build`. This will create a `build` folder in your project directory. \n- deploy the contents of the build folder to a web server. You should then be able to run the web app.\n\n== Run locally with Docker\n\nPull the latest image from Docker Hub to run the application locally:\n\n....\n# Run the application on http://localhost:5005\ndocker pull neo4jlabs/neodash:latest\ndocker run -it --rm -p 5005:5005 neo4jlabs/neodash\n\n# If you want to run on a custom port, set an environment variable\nexport NGINX_PORT=5008\ndocker run -it --rm -e NGINX_PORT=5008 -p 5008:5008 neo4jlabs/neodash\n....\n\n____\nWindows users may need to prefix the `docker run` command with `winpty`.\n____\n\n== Build Docker image\n\nA pre-built Docker image is available\nhttps://hub.docker.com/r/neo4jlabs/neodash[on DockerHub]. This image\nis built using the default configuration (running in editor mode,\nwithout SSO).\n\n=== To build the image yourself:\n\nMake sure you have a recent version of `docker` installed to build the\nmulti-stage NeoDash image and run it.\n\nOn Unix (Mac/Linux) systems:\n\n....\ndocker build . -t neodash\n....\n\nIf you use Windows, you might need to prefix the command with `winpty`:\n\n....\nwinpty docker build . -t neodash\n....\n\nAfter building, you can run the image with:\n....\ndocker run -it –rm -p 5005:5005 neodash\n.... \n\n== Run on Kubernetes\n\n=== To deploy using YAML files\n\nYAML examples are available in the https://github.com/neo4j-labs/neodash[NeoDash repository]. Here is an example of a pod definition YAML file to create a NeoDash pod in a cluster:\n\n....\napiVersion: v1\nkind: Pod\nmetadata:\n  name: neodash\n  labels:\n    project: neodash\nspec:\n  containers:\n    - name: neodash\n      image: neo4jlabs/neodash:latest\n      ports:\n        - containerPort: 5005\n....\n\n\nCreating a Kubernetes service to expose the application:\n....\napiVersion: v1\nkind: Service\nmetadata:\n    name: neodash-svc\nspec:\n    type: LoadBalancer\n    ports:\n    - port: 5005\n      targetPort: 5005\n    selector:\n      project: neodash\n....\n\n=== To deploy using a Helm Charts\n\nA Kubernetes Helm chart is available in the https://github.com/neo4j-labs/neodash[the NeoDash repository] and here is the full example of the Helm chart values.yaml file,\n\n....\n# Name override or full name override\nnameOverride: ''\nfullnameOverride: neodash-test\n\n# Number of pods\nreplicaCount: 1\n\n# Image Details\nimage:\n  repository: neo4jlabs/neodash\n  pullPolicy: IfNotPresent\n  tag: 'latest'\nimagePullSecrets: [] # Image pull secret if any\n\n# Pod annotations, labels and security context\npodAnnotations: {}\npodLabels: {}\npodSecurityContext: {}\n\n# Mode configuration using environment variables\n# Set reader mode environment variables when enable_reader_mode is true\nenable_reader_mode: true\nenv: \n  - name: \"ssoEnabled\"\n    value: \"false\"\n  - name: \"standalone\"\n    value: \"true\"\n  - name: \"standaloneProtocol\"\n    value: \"neo4j+s\"\n  - name: \"standaloneHost\"\n    value: \"localhost\"\n  - name: \"standalonePort\"\n    value: \"7687\"\n  - name: \"standaloneDatabase\"\n    value: neo4j\n  - name: \"standaloneDashboardName\"\n    value: \"test\"\n  - name: \"standaloneDashboardDatabase\"\n    value: neo4j\n  - name: \"standaloneAllowLoad\"\n    value: \"false\"\n  - name: \"standaloneLoadFromOtherDatabases\"\n    value: \"false\"\n  - name: \"standaloneMultiDatabase\"\n    value: \"false\"\n\n# Environment variable from secret\nenvFromSecrets: []\n  # standaloneUsername: \n      # secretName: \"neo4j-connection-secrets\"\n      # key: \"username\"\n  # standalonePassword: \n      # secretName: \"neo4j-connection-secrets\"\n      # key: \"password\"\n\n# Service details\nservice:\n  type: LoadBalancer # Can also be ClusterIP or NodePort  \n  port: 5005 # For the service to listen in for Traffic\n  targetPort: 5005 # Target port is the container port\n  annotations: {} # Service annotations for the LoadBalance\n\n# Ingress\ningress:\n  enabled: false # Enable Kubernetes Ingress\n  className: 'alb' # Class Name\n  annotations: {} # Cloud LoadBalancer annotations\n  hosts: []\n    # - host: neodash.example.com\n    #   paths:\n    #     - path: '/'\n    #       pathType: Prefix\n  tls: []\n\n# Pod resources request, limits and health check\nresources: \n  requests:\n    memory: \"64Mi\"\n    cpu: \"250m\"\n  limits:\n    memory: \"128Mi\"\n    cpu: \"500m\"\nlivenessProbe:\n  httpGet:\n    path: /*\n    port: 5005\nreadinessProbe:\n  httpGet:\n    path: /*\n    port: 5005\n\n# Pod Autoscaler\nautoscaling:\n  enabled: false\n  # minReplicas: 1\n  # maxReplicas: 100\n  # targetCPUUtilizationPercentage: 80\n\n# Pod Volumes\nvolumes: []\nvolumeMounts: []\n\n# Service Account\nserviceAccount:\n  create: true\n  automount: true\n  # annotations: {}\n  # name: ''\n...."
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/component-overview.adoc",
    "content": "include::../banner.adoc[]\n\n= Component Overview\n\ninclude::../banner.adoc[]\n\nThe image below contains a high-level overview of the component\nhierarchy within the application. The following conceptual building\nblocks are used to create the interface:\n\nimage::component-hierarchy.png[NeoDash Component Hierarchy]\n\n* *Application* - highest level in the component structure. Handles all\napplication-level logic (e.g. initalizing the app).\n* *Modals* - all pop-up windows used by the tool. (Connection modal,\nsave-dashboard modal, errors/warnings, etc.)\n* *Drawer* - the sidebar on the left side of the screen. Contains\nbuttons to perform application-level actions.\n* *The Dashboard* - Main dashboard component. Renders components\ndynamically based on the current state.\n* *Dashboard Header* - the textbox at the top of the screen that lets\nyou set a title for the dashboard, plus the page selector.\n* *Pages* - a dashboard has one or more pages, each of which can have a\nlist of cards.\n* *Cards* - a `block' inside a dashboard. Each card contains a `view'\nwindow, and a `settings' window.\n* *Card View* - the front of the card containing the selected report.\n* *Card Settings* - the back of the card, containing the cypher editor\nand advanced settings for the report.\n* *Card View Header* - the header of the card, containing a text box\nthat acts as the name of the report.\n* *Report* - the component inside the card view that handles query\nexecution and result parsing. Contains a single chart (visualization)\n* *Card View Footer* - The footer of the card view. Depending on the\ntype, contains several `selectors' that modify the visualization.\n* *Card Settings Header* - Header of the card settings, used for\nmoving/deleting the card.\n* *Card Settings Content* - the component containing the main content of\nthe report. This is most often the Cypher query editor.\n* *Card Settings Footer* - the `footer' of the card. This contains the\n`advanced settings' window for reports.\n* *Charts* - the different visualizations used by the application: bar\ncharts, tables, graphs, etc.\n\n\n== A note on Cards v.s. Reports\n\nWhereas a user might associate a Card in NeoDash to a report directly,\nthe application has a more nuanced seggration of responsibilities:\n\n* The *Card* is responsible for positioning the component in a page.\n* The *Card Content* is the core element of the card (exclusive of the\ntitle header and any optional footer).\n* A *Report* sits inside the card content, and handles the running of\nqueries and displaying errors.\n* A *Chart* is rendered by the report and is solely responsible for\nrendering a specific visualization.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/configuration.adoc",
    "content": "include::../banner.adoc[]\n\n= Configuration\n\ninclude::../banner.adoc[]\n\nWhen using a custom NeoDash deployment, there are several settings that\ncan be configured. These mostly relate to\nlink:../standalone-mode[Standalone Mode] and SSO configurations.\n\nFor a simple (non-Dockerized) deployment, these configuration parameters\ncan be changed by modifying `dist/config.json` after you have built the\napplication. When Docker image, these can be passed as environment\nvariables. See link:../standalone-mode[Standalone Mode] for more on\nDocker deployments.\n\nAn example configuration for NeoDash (default, running in editor mode)\nwill look like this:\n\n....\n{\n    \"ssoEnabled\": false,\n    \"ssoProviders\": [],\n    \"ssoDiscoveryUrl\": \"https://example.com\",\n    \"standalone\": false,\n    \"standaloneProtocol\": \"neo4j+s\",\n    \"standaloneHost\": \"localhost\",\n    \"standalonePort\": \"7687\",\n    \"standaloneDatabase\": \"neo4j\",\n    \"standaloneDashboardName\": \"My Dashboard\",\n    \"standaloneDashboardDatabase\": \"dashboards\",\n    \"standaloneDashboardURL\": \"\",\n    \"standaloneAllowLoad\": false,\n    \"standaloneLoadFromOtherDatabases\": false,\n    \"standaloneMultiDatabase\": false,\n    \"standaloneDatabaseList\": \"neo4j\"    \n    \"loggingMode\": \"0\",\n    \"loggingDatabase\": \"logs\",\n    \"customHeader\": \"\",\n}\n....\n\n== Configuration Options\n\n[width=\"100%\",cols=\"19%,17%,26%,38%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|ssoEnabled |boolean |false |If enabled, lets users connect to Neo4j\nusing SSO. This requires a\nvalid ssoDiscoveryUrl to be set.\n\n|ssoProviders |List |[] |When using multiple SSO providers on the database, you can configure the list of providers (by id) to be used on Neodash. If empty, all providers will be displayed.\n\n|ssoDiscoveryUrl |string |https://example.com |If ssoEnabled is true &\nstandalone mode is enabled, the URL to retrieve SSO auth config from.\n\n|standalone |boolean |false |Determines whether to run NeoDash in editor\nmode (false), or reader mode (true). The terms ``Reader mode'' and\n``Standalone mode'' are used interchangibly.\n\n|standaloneProtocol |string |neo4j |When running in standalone mode, the\nprotocol to used for the Neo4j driver. This should be set to one of\n`neo4j`, `neo4j+s`, `neo4j+ssc`, `bolt`, `bolt+s`, or `bolt+ssc`.\n\n|standaloneHost |string |localhost |When running in standalone mode, the\nhostname to connect to. This should be *just* the hostname, no protocols\nor ports.\n\n|standalonePort |string |7687 |When running in standalone mode, the Bolt\nport to connect to.\n\n|standaloneDatabase |string |neo4j |When running in standalone mode, the\ndatabase to use for reporting. Cypher queries used in reports will read\ndata from this database.\n\n|standaloneUsername ⚠️ |string |… |A hidden config parameter enables you\nto set the username for standalone mode by default. Keep in mind this is\na security risk, as it exposes the Neo4j username to anyone who can\naccess the NeoDash deployment.\n\n|standalonePassword ⚠️ |string |… |A hidden config parameter enables you\nto set the password for standalone mode by default. If this value is set\nconnections are also made automatically. Keep in mind this is a security\nrisk, as it exposes the Neo4j username to anyone who can access the\nNeoDash deployment.\n\n|standaloneDashboardName |string |My Dashboard |The exact name\n(case-sensitive) of the dashboard to be loaded when running in\nstandalone mode. This must be a dashboard that is saved as a node in the\ngraph.\n\n|standaloneDashboardDatabase |string |neo4j |The name of the Neo4j\ndatabase that contains the saved dashboard node. This is neo4j by\ndefault, _unless you are using Neo4j Enterprise Edition_, which lets you\nuse multiple databases.\n\n|standaloneDashboardURL |string |neo4j |If you do not save a dashboard\ninside Neo4j and would like to run a standalone mode deployment with a\ndashboard from a URL, set this parameter to the complete URL pointing to\nthe dashboard JSON.\n\n|standaloneAllowLoad |boolean |false |If set to yes the \"Load Dashboard\"\nbutton will be enabled in standalone mode, allowing users to load\nadditional dashboards from Neo4J. This parameter is false by default \n_unless you are using Neo4j Enterprise Edition_, which lets you use multiple \ndatabases.\n*NOTE*: when Load is enabled in standalone mode, only Database is available\nas a source, not file.\n\n|standaloneLoadFromOtherDatabases |boolean |false |If _standaloneAllowLoad_ is\nset to true, this parmeter enables or not users to load dashboards from\nother databases than the one deifned in _standaloneDashboardDatabase_. If\n_standaloneAllowLoad_ is set to false this parameters has no effect.\n\n|standaloneMultiDatabase |boolean |false |If this parameter set to true, the\nstandalone configuration will ignore the _standaloneDatabase_ parameter and\nallow users to choose which database to connect to in the login screen, among\nthe ones provided in _standaloneDatabaseList_, with a dropdown list. This\nparameter is false by default _unless you are using Neo4j Enterprise Edition_,\nwhich lets you use multiple databases.\n\n|standaloneDatabaseList |string |neo4j |If _standaloneMultiDatabase_ is\nset to true, this parmeter must contain a comma separated list of database\nnames that will be displayed as options in the Database dropdown at user\nlogin (e.g. 'neo4j,database1,database2' will populate the database dropdown\nwith the values 'neo4j','database1' and 'database2' in the connection screen).\nIf _standaloneMultiDatabase_ is set to false this parameters has no effect. \n\n|loggingMode |string |none |Determines whether neodash should create any\nuser activity logs. possible values include: `0` (no log is created), \n`1` (user login are tracked), `2` (tracks when a specific dashboard is \naccessed/loaded or saved by a user*). \n\n⚠️ Logs are created in Neo4J DB using the current user credentials \n(or standaloneUsername if configured); write access to the log database \nmust be granted to enble any user to create logs.\n\n⚠️ * Load/Save from/to file are not logged (only from/to Database)   \n\n|loggingDatabase |string |logs |When loggingMode is set to anything \nelse than '0', the database to use for logging. Log records (nodes)\nwill be created in this database.\n\n|customHeader |string |none |When set the dashboard header will display\nthe prameter value as a fixed string, otherwise it will display the host \nand port of current connection.\n|===\n\n== Configuring SSO\n\nNeoDash can use SSO as an alternative for password-based sign-in, if\nyour Neo4j database is enabled to use single sign on. To enable SSO, set\n`ssoEnabled` to `true`. Then, set `ssoDiscoveryUrl` to the place where\nyour `discovery.json` is located (This will often be the hostname of\nyour database, appended by `/discovery.json`).\n\n____\nNote that SSO is only available when Standalone Mode is enabled.\n____\n\n== Auth Provider\n\nTo set up NeoDash to use an external identity provider, you can add a\n/auth_provider resource to nginx (in `/conf/default.conf`):\n\n....\nlocation /auth_provider {\n        default_type application/json;\n        return 200 '{\n                        \"auth_config\" : {\n                            \"oidc_providers\" : [ ... ]\n                        }\n                    }';\n    }\n....\n\nFor basic deployments it might suffice to route requests to\n`/auth_provider` on the https port of the neo4j database.\n\n== Configuring Standalone Mode\n\nStandalone mode, or reader-mode, overrides the functionality of NeoDash,\nallowing you to deploy a fixed dashboard to users. Standalone mode can\nbe enabled by changing the `standalone` config parameter:\n\n* If standalone mode is `false`, all other configuration parameters are\nignored. NeoDash will run in Editor mode, and require a manual sign-in.\n* If standalone mode is `true`, NeoDash will read all configuration\nparameters. A *predefined dashboard* will be auto-loaded, and no changes to\nthe dashboard can be made. There are two types of valid standalone\ndeployments:\n** A standalone deployment that *reads the fixed dashboard from Neo4j*.\nThe `standaloneDashboardName` and `standaloneDashboardDatabase` config\nparameters are used to define these.\n** A standalone deployment that *reads the fixed dashboard from a URL*.\nThe `standaloneDashboardURL` config parameter is used to define this.\n\n* Standalone mode can also be configured to allow users load a different\ndashboard after the predefined one is loaded (a `Load Dashboard` button\nwill be displayed on the right side of dashboard title). \nThe `standaloneAllowLoad` and `standaloneLoadFromOtherDatabases` are used\nto define this.\n* When allowing users to load dashboards dyamically in standalone mode,\nthey may also need to connect to different databases, depending on the\nspecific dashboard bing loaded. this can be enabled setting \n`standaloneMultiDatabase` to true and providing a comma separated list\nof the allowed database names in the`standaloneDatabaseList` parameter.\n\n"
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/contributing.adoc",
    "content": "include::../banner.adoc[]\n\n= Contributing\n\ninclude::../banner.adoc[]\n\nContributions to the project are highly welcomed. Please consider\ncreating a https://github.com/neo4j-labs/neodash/pulls[Pull Request].\nEnsure you start from the `develop` branch, and set the merge base to\n`develop` as well.\n\nFor your feature to be accepted, ensure: \n\n1. The component is tested (if relevant, see Testing). \n2. Your code is aligned with\nhttps://www.w3.org/wiki/JavaScript_best_practices[JS Best Practices]. \n3. The component is well documented in the documentation portal (if\napplicable).\n\n== Feature Requests / Bugs\n\nIf you have a request for a feature, or have found a bug, consider\ncreating an https://github.com/neo4j-labs/neodash/issues[issue] on\nGitHub. Please include a link:./testing#debug-report[Debug Report] if\navailable.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/deploy-a-build.adoc",
    "content": "= Deploy a Build\n\ninclude::../banner.adoc[]\n\nIf you have a pre-built NeoDash application, you can easily deploy it on an any webserver.\nA NeoDash build is \"just\" a collection of HTML, CSS and JavaScript files, so it can run virtually anywhere.\n\nThis guide walks you through the process of deploying a NeoDash build onto your own webserver.\n\n== 1. Prepare the files\nFirst, check that you have the correct files.\nWe typically provide builds as either a zip file or tarball with the following naming convention: \n`neodash-2.X.X.zip` or `neodash-2.X.X.tar.gz`.\n\nFor zip files, open up the terminal and run:\n```bash\nunzip neodash-2.X.X.zip\n```\n\nFor tar.gz files, open up the terminal and run:\n```bash\ntar -xf neodash-2.X.X.tar.gz\n```\n\nAfter running either of these, you should now have a folder `neodash-2.X.X` in the current directory.\n\n== 2. Edit Configuration (Optional)\nThis is an optional step if you want to configure optional settings for your NeoDash deployment (e.g. SSO or standalone mode).\n\n1. Inside the folder you just unzipped, open up `config.json`. \n2. Edit this file to modify your link:../configuration[Configuration] settings.\n3. Save the file.\n4. Inside the folder you just unzipped, open up `style.config.json`.\n5. Edit this file to modify your link:../styleConfiguration[Style Configuration] settings.\n6. Save the file\n\n== 3. Move the tarball/zip to your webserver\nFinally, copy the files to the correct folder on your webserver.\nDepending on the webserver type and version, this could be different directory.\nAs an example - to copy the files to an nginx webserver using `scp`:\n\n```bash\nscp neodash-2.4.11-labs username@host:/usr/share/nginx/html\n```\n\nNeoDash should now be visible by visiting your (sub)domain in the browser.\n Can't see the application? Check that the webserver user has read-permissions on the files you copied into the HTML directory."
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/design.adoc",
    "content": "include::../banner.adoc[]\n\n= Design\n\ninclude::../banner.adoc[]\n\n\nThis page contains some key guidelines for design of the application.\nThis entails code architecture, as well as UX/UI design.\n\n== File Structure\n\nThe source code of NeoDash is organized as a flat file structure based\non components. Given a component `ABC` is to be added, you should create\na directory called `abc` with the following files:\n\n* `ABC.tsx` (component renderer)\n* `ABCActions.ts` (objects defining state manipulation)\n* `ABCReducer.ts` (handling state changes based on actions)\n* `ABCSelectors.ts` (used by components to retrieve part of the state)\n* `ABCThunks.ts` (Complex state handling logic, to fire one or more\nactions)\n\n=== Structure of the other folders\n\n....\nconf: nginx configuration for Docker image.\ndist: directory for generated webpack files.\nnode_modules: downloaded dependencies\npublic: style files/images. Runtime app config.\nscripts: utility scripts for deployment.\nsrc: source code. \ntarget: compiled package as tgz file.\n.babelrc: javascript compiled settings.\n.gitignore: gitignore files.\nDockerfile: docker image definition.\n....\n\n== UX Design\n\nAt it’s core, NeoDash aims to be a tool that is _easy to learn, but hard\nto master_. This translates into the following five design principles in\nmind:\n\n[arabic]\n. Use a limited set of core visualizations, with high customizability.\n. It should be easy to get started without reading documentation.\n. The tool should be self-documenting.\n. Complex data transformations should be done by dashboard builders in\nCypher, and not by the application.\n. The tool should be easy to extend with custom visualizations.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/index.adoc",
    "content": "include::../banner.adoc[]\n\n= Developer Guide\n\ninclude::../banner.adoc[]\n\n\nThis guide contains information for developers looking to deploy NeoDash, or extend it for their own needs. \n\n- link:build-and-run[Build & Run] \n- link:configuration[Configuration]\n- link:standalone-mode[Standalone Mode] \n- link:component-overview[Component Overview] \n- link:design[Design]\n- link:style-configuration[Style Configuration]\n- link:adding-visualizations[Adding Visualizations] \n- link:state-management[State Management] \n- link:testing[Testing]\n- link:contributing[Contributing]\n\n== Prerequisites for extending NeoDash\n\nNeoDash is a web application written in TypeScript. Knowledge of React &\nRedux is also highly recommended when extending the application.\nConcretely, the following languages and frameworks make up the core of\nNeoDash: \n\n- https://reactjs.org/[React] \n- https://redux.js.org/[Redux] \n- https://redux.js.org/usage/writing-logic-thunks[Redux Thunks] \n- https://www.cypress.io/[Cypress] \n- https://mui.com/[Material UI] \n- https://webpack.js.org/[Webpack]\n\nThe following core libraries are used to build the visualizations for\nreports: \n\n- https://github.com/vasturiano/react-force-graph[react-force-graph\n(Graph)] \n- https://mui.com/components/data-grid/[@mui/datagrid (Table)]\n- https://nivo.rocks/[@nivo (Bar, Line, Pie charts)] \n- https://leafletjs.com/[leaflet (Map)] \n- https://github.com/remarkjs/react-markdown[react-markdown (Markdown)]\n"
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/session-storage.adoc",
    "content": "include::../banner.adoc[]\n\n= Session Storage\n\ninclude::../banner.adoc[]\n\nThis reducer serves only to store data that we want to reset at each new session.\nTo connect to it, just define a key and use the predefined actions to set a new pair (key,value) inside of it. \nInside the actions there is also an action to delete all the keys that match a precise prefix, it can be useful, for example, to wipe the sessionStorage state for a certain extension, if it stores the data inside the sessionStorage using a prefix (for example look at the query-translator extension at getSessionStorageHistoryKey)."
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/standalone-mode.adoc",
    "content": "include::../banner.adoc[]\n\n= Standalone Mode\n\ninclude::../banner.adoc[]\n\nNext to being a dashboard editor, NeoDash can be deployed in a\n`standalone mode' - allowing you set up a architecture to publish and\nread dashboards.\n\nRunning in standalone modec mode will: \n\n- Disable all editing options \n- Have a hardcoded Neo4j URL and database name \n- Load a dashboard from Neo4j with a fixed name.\n\nThe diagram below illustrates how NeoDash standalone mode can be\ndeployed next to a standard `Editor Mode' instance:\n\nimage:standalone-architecture.png[image]\n\n== Option 1 - Standard Deployment (Non-Docker)\n\nFirst, build NeoDash as described link:../build-and-run[here]. After\nbuilding, you’ll have a `dist` directory that you can deploy to a web\nserver.\n\nTo configure the app to run in standalone mode, you’ll need to edit\n`dist/config.json` and change the `standalone` property to `true`. The\nother variables inside `config.json` should also be configured to match\nthe hostname, port and database name of your Neo4j instance. See\nConfiguration for more on configuration variables.\n\nAs `config.json` gets picked up at runtime by the application, users\nviewing the application will now access the dashboard in standalone\nmode.\n\n== Option 2 - Docker Deployment\n\nYou can configure the app to run in standalone by passing environment\nvariables to Docker:\n\n....\ndocker run  -it --rm -p 5005:5005 \\\n    -e ssoEnabled=false \\\n    -e ssoProviders=[] \\\n    -e ssoDiscoveryUrl=\"https://example.com\" \\\n    -e standalone=true \\\n    -e standaloneProtocol=\"neo4j+s\" \\\n    -e standaloneHost=\"localhost\" \\\n    -e standalonePort=\"7687\" \\\n    -e standaloneDatabase=\"neo4j\" \\\n    -e standaloneDashboardName=\"My Dashboard\" \\\n    -e standaloneDashboardDatabase=\"dashboards\" \\\n    -e standaloneDashboardURL=\"dashboards\" \\\n    -e standaloneAllowLoad=false \\\n    -e standaloneLoadFromOtherDatabases=false \\\n    -e standaloneMultiDatabase=false \\\n    -e standaloneDatabaseList=\"neo4j\" \\\n    neo4jlabs/neodash\n....\n\nMake sure that all of the environment variables are set to the correct\nvalues. This is described in more detail link:../configuration[here].\n\n____\nAlternatively, environment variables from docker compose or a kubernetes\ndeployment can be used.\n____\n\n== Deep Linking\nTo dynamically view a deployed NeoDash dashboard, you can deep-link into a deployed dashboard.\n the following deeplinking options are available via URL parameters:\n- Appending `?page=1` to the URL will open up a dashboard at a given page. (Starting at zero).\n- Appending `?neodash_person_name=Tom` to the URL will set a dashboard parameter as a default for the entire dashboard.\n\nMultiple parameters can be used in a deep-link by concatinating them:\n....\nhttps://myneodashdeployment.com/?page=1&neodash_person_name=Tom&neodash_movie_name=The%20Matrix\n....\n"
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/state-management.adoc",
    "content": "include::../banner.adoc[]\n\n= State Management\n\ninclude::../banner.adoc[]\n\nNeoDash is an application with a complex internal state. If you are\nplanning to extend the application state in some way, make sure you are\nfamiliar with https://redux.js.org/[Redux] design patterns.\n\nThe app’s entire state object is encapsulated in the following JSON\nstructure:\n\n....\n{\n  \"dashboard\": {\n    \"title\": \"My Dashboard Name\",\n    \"version\": \"2.4\",\n    \"settings\": {\n      \"pagenumber\": 0,\n      \"editable\": true,\n      ...\n      \"parameters\": {\n          ...\n       }\n    },\n    \"pages\": [\n        ...\n    ]\n  },\n  \"application\": {\n        ...\n   },\n  \"version\": \"2.1.0\"\n}\n....\n\nAt the highest level, this object consists of three entries: \n\n- `dashboard`: all state related to the currently active dashboard. This\nis changed when a dashboard gets loaded, modified or removed. \n- `application`: all state related to the application itself. This\ndescribes which windows are open, what database you are connected to,\netc. \n- `version`: the version of NeoDash that is running. Note that\nthese are complete version numbers (of the shape X.Y.Z), unlike\ndashboard versions, which have a different versioning scheme.\n\n____\nWant to see the complete state object for your application? Generate a\n*Debug Report* from the About window.\n____\n\n== Dashboard State\n\nThe dashboard entry contains the entire state of the currently loaded\ndashboard. Take the following simple dashboard as an example.\n\n....\n{\n  \"dashboard\": {\n    \"title\": \"A Simple Dashboard\",\n    \"version\": \"2.4\",\n    \"settings\": {\n      \"pagenumber\": 0,\n      \"editable\": true,\n      \"fullscreenEnabled\": true,\n      \"parameters\": {\n          \"neodash_person_name\": \"Bob\"\n       }\n    },\n    \"pages\": [\n        {\n          title: “My Page”\n          reports: [\n            {\n                \"title\": \"My Report\",\n                \"query\": \"MATCH (n)-[e]->(m) RETURN n,e,m\",\n                \"type\": \"graph\",\n                \"x\": \"1\",\n                \"y\": \"2\",\n                \"width\": \"6\",\n                \"height\": \"3\",\n                \"settings\": {\n                    \"nodeColorSchmeme\": \"blue\"\n                }\n            }\n        ]\n     }\n    ]\n  }\n}\n....\n\nKey entries of the object are: \n\n- `title`: the title of the dashboard. This is displayed on the top of the window. \n- `version`: _Main_ version of the dashboard that is loaded. \n- `settings`: contains settings for the dashboard. This includes the current page number, whether the dashboard\nis editable, whether the dashboard is in fullscreen mode, and the\ndashboard parameters that are currently set. \n- `pages`: contains the list of all pages in the dashboard. Each page has a title and a list of\nreports.\n\n== Application State\n\nThe application state is a flat dictionary of values that determine what\nthe user’s window looks like (which windows are open?) as well as the\ncurrent database connection, and whether the app is running in\nstandalone mode.\n\n....\n\"application\": {\n    \"notificationTitle\": null,\n    \"notificationMessage\": null,\n    \"connectionModalOpen\": false,\n    \"welcomeScreenOpen\": true,\n    \"aboutModalOpen\": true,\n    \"connection\": {\n      \"protocol\": \"neo4j+s\",\n      \"url\": \"localhost\",\n      \"port\": \"\",\n      \"database\": \"\",\n      \"username\": \"neo4j\",\n      \"password\": \"************\"\n    },\n    \"desktopConnection\": null,\n    \"connected\": false,\n    \"dashboardToLoadAfterConnecting\": null,\n    \"waitForSSO\": false,\n    \"standalone\": false,\n    \"oldDashboard\": null,\n    \"ssoEnabled\": false,\n    \"ssoProviders\": [],\n    \"ssoDiscoveryUrl\": \"https://example.com\",\n    \"standaloneProtocol\": \"neo4j+s\",\n    \"standaloneHost\": \"localhost\",\n    \"standalonePort\": \"7687\",\n    \"standaloneDatabase\": \"neo4j\",\n    \"standaloneDashboardName\": \"My Dashboard\",\n    \"standaloneDashboardDatabase\": \"dashboards\",\n    \"standaloneDashboardURL\": \"dashboards\",\n    \"loggingMode\": \"0\",\n    \"loggingDatabase\": \"logging\",\n    \"standaloneAllowLoad\": false,\n    \"standaloneLoadFromOtherDatabases \": false,\n    \"standaloneMultiDatabase\": false,\n    \"standaloneDatabaseList\": \"neo4j\",\n    \"notificationIsDismissable\": null\n}\n....\n"
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/style-configuration.adoc",
    "content": "include::../banner.adoc[]\n\n= Style Configuration\n\ninclude::../banner.adoc[]\n\nWhen using a custom NeoDash deployment, there are several theme variables that\ncan be configured. These mostly relate to css tokens for\nlink:https://cdn.jsdelivr.net/npm/@neo4j-ndl/base@1.4.0/lib/tokens/css/tokens.css[Needle] and some other brand specific options.\n\nFor a simple (non-Dockerized) deployment, these configuration parameters\ncan be changed by modifying `dist/style.config.json` after you have built the\napplication. When using the NeoDash Docker image, these can be passed as environment\nvariables. For example:\n\n....\ndocker run -p 5005:5005 \\\n -e DASHBOARD_HEADER_BRAND_LOGO=https://picsum.photos/500/100 \\\n neo4jlabs/neodash\n....\n\nAn example configuration for NeoDash\n\n....\n{\n  \"DASHBOARD_HEADER_BRAND_LOGO\": \"logo_lightsand.png\",\n  \"DASHBOARD_HEADER_COLOR\" : \"#F3F3F0\",\n  \"DASHBOARD_HEADER_BUTTON_COLOR\" : \"#009999\",\n  \"DASHBOARD_HEADER_TITLE_COLOR\" : \"#00C1B6\",\n  \"DASHBOARD_PAGE_LIST_COLOR\" : \"#F3F3F0\",\n  \"DASHBOARD_PAGE_LIST_ACTIVE_COLOR\": \"#009999\",\n  \"style\": {\n    \"--palette-light-neutral-bg-weak\" : \"243, 243, 240\"\n  }\n}\n....\n\n== Configuration Options\n\n[width=\"100%\",cols=\"19%,17%,26%,38%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|DASHBOARD_HEADER_BRAND_LOGO |string |undefined |This variable defines the name of the logo file located on the public folder of the Neodash deployment, if you want your own logo instead of the Neo4j one.\n\n|DASHBOARD_HEADER_COLOR |string |#0B297D |Determines the color of the header.\n\n|DASHBOARD_HEADER_BUTTON_COLOR |string |#FFFFFF22 |Determines the color of the header buttons.\n\n|DASHBOARD_HEADER_TITLE_COLOR |string |#FFFFFF |Determines the color of the header title.\n\n|DASHBOARD_PAGE_LIST_COLOR |string |#F0F0F0 |Determines the color of the page selector tabs.\n\n|DASHBOARD_PAGE_LIST_ACTIVE_COLOR |string |#FFFFFF |Determines the color of the page selector active tabs.\n\n|style |object |{} | Determines css needle tokens that should be overridden at the root level. Colors should be defined with an rgb comma separated string (e.g \"243, 243, 240\")\n\n\n|===\n"
  },
  {
    "path": "docs/modules/ROOT/pages/developer-guide/testing.adoc",
    "content": "include::../banner.adoc[]\n\n= Testing \n\ninclude::../banner.adoc[]\n\nNeoDash uses *Cypress* for automated testing. To install Cypress, check\nout the official\nhttps://docs.cypress.io/guides/getting-started/installing-cypress#What-you-ll-learn[installation\ninstructions].\n\nAfter cypress is installed, you can use:\n\n....\nyarn run test\n....\n\nTo open the Cypress GUN. Alternatively, use:\n\n....\nyarn run test-headless\n....\n\nTo run Cypress from the UI.\n\nBefore starting the tests, make sure you have a local instance of\nNeoDash running at `http://localhost:3000` using `yarn run dev`.\n\nimage:cypress.png[Cypress] Above: a screenshot of the Cypress GUI.\n\n== Debug Report\n\nFor ad-hoc testing, a debug report can be generated by NeoDash. This\nreport contains a JSON representation of the current state of the\nNeoDash application.\n\nTo generate a debug report, open the `About' screen. Then, click the\n'Debug Report' button in the bottom left corner.\n\nimage::about.png[About]\n"
  },
  {
    "path": "docs/modules/ROOT/pages/index.adoc",
    "content": "include::/banner.adoc[]\n\n= Introduction\n\nThis portal contains information on getting started with NeoDash - A Low-Code Dashboard Builder for Neo4j.\n\nNeoDash is an open source tool for visualizing your Neo4j data. It lets you group visualizations together as dashboards, and allow for interactions between reports.\n\n\nimage::dashboard.png[Dashboard]\n\nNeodash supports presenting your data as tables, graphs, bar charts, line charts, maps and more. It contains a Cypher editor to directly write the Cypher queries that populate the reports. You can save dashboards to your database, and share them with others.\n\n- To get started, see the link:quickstart[Quickstart] page.\n- For more on building dashboards, visit the link:user-guide[User Guide].\n- For deploying, configuring and extending NeoDash, check out the link:developer-guide[Developer Guide]."
  },
  {
    "path": "docs/modules/ROOT/pages/quickstart.adoc",
    "content": "include::/banner.adoc[]\n\n= Quickstart\n\ninclude::/banner.adoc[]\n\nThere are three easy ways to run NeoDash and start dashboarding your Neo4j data:\n\n. The latest version is always available online:\nhttps://neodash.graphapp.io.\n. Neo4j Desktop: Install it from the https://install.graphapp.io[Graph\nApp Gallery].\n. Using Docker:\n```\ndocker pull neo4jlabs/neodash:latest\ndocker run -it --rm -p 5005:5005 neo4jlabs/neodash\n```\n\nOr, build it yourself:\n```\ngit clone https://github.com/neo4j-labs/neodash     \nyarn install      \nyarn run dev\n```\n\nNeoDash connects to any recent version of the Neo4j database. (Neo4j 4.0\nor later). The quickest way to get started is to create a free cloud\ndatabase on https://console.neo4j.io[Neo4j Aura].\n\nTo get started with building your own dashboard, see the Dashboards\npage.\n\n== NeoDash in Five Minutes\n\nSee the video below for tips on how to get started with NeoDash in 5 minutes:\nhttps://www.youtube.com/watch?v=Ygzj0Y4cYm4[image:https://img.youtube.com/vi/Ygzj0Y4cYm4/0.jpg[Youtube\nVideo]]\n\nSee also the link:../user-guide/faq#1-how-can-i-learn-more-about-neodash[list of\nblog posts] in the FAQ.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/access-control.adoc",
    "content": "include::../banner.adoc[]\n\n= Access Control\n\ninclude::../banner.adoc[]\n\nThe Access Control feature in NeoDash is a security measure that allows Users with write access or higher privileges to manage who has access to specific dashboards.\n\n\n== How it Works\n\nNavigate to a specific dashboard and inside the dashboard settings click on the 'Access Control' option in the dashboard sidebar. This opens a modal where users can add labels to the dashboard. These labels are then used to determine which users have access to the dashboard. Please keep in mind that prior to doing this, an administrator needs to provide certain privileges for different user roles for each label in order for this to work.  You can read more about how RBAC works in Neo4j by reading the [Neo4j RBAC documentation](https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/).\n\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/bloom-integration.adoc",
    "content": "include::../banner.adoc[]\n\n= Bloom Integration\n\ninclude::../banner.adoc[]\n\nNeoDash can be linked to Neo4j Bloom perspectives by using\nhttps://neo4j.com/docs/bloom-user-guide/current/bloom-tutorial/deep-links/[Bloom\nDeep Links]. This functionality allows you to combine the power of graph\nreporting (NeoDash) with intuitive graph exploration (Bloom).\n\n== Bloom Deep-Linking\n\nTo link NeoDash to a Bloom perspective, you will need to: \n\n1. Create a Neo4j Bloom https://neo4j.com/docs/bloom-user-guide/current/bloom-perspectives/bloom-perspectives/[perspective].\n2. Define a https://neo4j.com/docs/bloom-user-guide/current/bloom-tutorial/search-phrases-advanced/[Bloom\nSearch Phrase] for the perspective. \n3. Generate a https://neo4j.com/docs/bloom-user-guide/current/bloom-tutorial/deep-links/#_server_hosted_bloom[Deep\nLink] for your perspective and respective search phrase. This requires\nthat you have a\nhttps://neo4j.com/docs/bloom-user-guide/current/bloom-installation/installation-activation/#installing-server-plugin[Server-hosted\nBloom installation] running. \n\n4. Use the deep link you created in either:\n- an iFrame Report (optionally passing in the dashboard parameters into\nthe search phrase). \n- a Graph Report (Adding your deep link inside the\n`Drilldown Link' field under advanced settings):\n\nimage::graphdrilldown.png[Graph Drilldown]\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/dashboards.adoc",
    "content": "include::../banner.adoc[]\n\n= Dashboards\n\ninclude::../banner.adoc[]\n\nIn NeoDash, a dashboard consists of several pages, each of which can\nconsist of multiple reports.\n\nimage::dashboardnew.png[Dashboard]\n\nAs an example: The screenshot above shows a dashboard with three pages:\n`Breweries`, `Beer Ratings` and `Styles`. The dashboard title `My\nBeer Database Dashboard 🍺` is displayed on the top of the window.\n\nThe first page is selected, and contains three reports, a table, a graph\nand a map. Each report can be given their own name, and has exactly one\nCypher query used to populate the report. See Reports for more info on\nhow reports work.\n\n== Dashboard Management\n\nAfter startup up NeoDash, you will be given the choice to create a new\ndashboard or open an existing one (if available). After being connected,\nthe buttons on the sidebar can be used to save, load or share a\ndashboard.\n\nimage::dashboardnewsettings.png[Save/Load/Share Button]\n\n=== Save a Dashboard\n\nA NeoDash dashboard is, simply put, a JSON file. As an example, the\ndefault dashboard has the following structure:\n\n....\n{\n  \"title\": \"\",\n  \"version\": \"2.0\",\n  \"settings\": {\n    \"pagenumber\": 0,\n    \"editable\": true,\n    \"fullscreenEnabled\": true,\n    \"parameters\": {}\n  },\n  \"pages\": [\n    {\n      \"title\": \"Main Page\",\n      \"reports\": [\n        {\n          \"title\": \"Hi there 👋\",\n          \"query\": \"**This is your first dashboard!** \\n \\nYou can click (⋮) to edit this report, or add a new report to get started. You can run any Cypher query directly from each report and render data in a variety of formats. \\n \\nTip: try _renaming_ this report by editing the title text. You can also edit the dashboard header at the top of the screen.\\n\\n\\n\",\n          \"width\": 3,\n          \"type\": \"text\",\n          \"height\": 3,\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"\",\n          \"query\": \"MATCH (n)-[e]->(m) RETURN n,e,m LIMIT 20\\n\\n\\n\",\n          \"width\": 3,\n          \"type\": \"graph\",\n          \"height\": 3,\n          \"selection\": {\n            \"Movie\": \"title\",\n            \"Genre\": \"name\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        }\n      ]\n    }\n  ]\n}\n....\n\nAfter opening the save dialog, there are three options for saving your\ndashboard: \n\n1. Save as a file. This triggers a download of the current\ndashboard as `.json` file. \n2. Save inside Neo4j. This stores a\nstringified representation of the dashboard as a node in the database.\nWhen using Neo4j multi-database, you will be given the choice of which\ndatabase to save the dashboard in. \n3. Copy-paste the JSON file directly.\n\n> Keep in mind that your currently active dashboard is stored in the browser cache. If you clear your cache (cookies), the dashboard is gone.\n\n=== Load a Dashboard\n\nJust like in the save screen, a dashboard can be loaded in one of three\nways: \n\n1. Load from a file. This requires you to select a `.json`\nsomewhere on your computer. \n2. Load from Neo4j. This requires you to\nselect a dashboard node stored in the database. When loading from Neo4j,\nyou will be presented with the list of dashboards in reverse\nchronological order. \n3. Loading a JSON file by pasting it directly into\nthe editor.\n\n=== Share a Dashboard\n\nA dashboard can be shared with other users by generating a direct link\nto it. This link will contain: \n\n- A link to the dashboard (either a\ndirect URL or the name of the dashboard inside Neo4j). \n- (Optionally),\nthe credentials of the database that the dashboard is reporting on. *Be\nwarned*, when using this feature, the share link will contain the\ndatabase credentials, which can be a security risk. \n- If the dashboard should be viewed in `editor mode', or `standalone mode'. The latter configures neodash to run in a stripped down UI without any of the editor features enabled.\n\nWhen creating a NeoDash deployment on a production database, it is not\nrecommended to use the `Share' feature. Rather, set up a dedicated\nstandalone deployment of NeoDash. See Publishing for more infomation.\n\n=== Dashboard Access Control\nWith this feature, you can manage dashboard access by leveraging the native Neo4j Role-based Access Control (RBAC) functionality. Attach additional labels to the currently selected dashboard node within this window, either by utilizing existing labels in your database or creating new ones, to regulate access permissions. \n\nYou can find the Dashboard Access Control feature by clicking on the three dots next to the dashboard name in the sidebar and selecting the \"Access Control\" option.\n\n> This approach should be used together with restricted privileges on labels, assigned to certain roles. See link:../extensions/access-control-management[Access Control Management] for details.\n\nimage::dashboardaccesscontrol.png[Dashboard Access Control]\n\n== Dashboard Settings\n\nSettings for the entire dashboard can be accessed by clicking the\n*Settings ⚙️* button in the dashboard sidebar.\n\nimage::dashboardsettings.png[Dashboard Settings]\n\nThis window can be used to control the followng settings:\n\n[width=\"100%\",cols=\"19%,17%,26%,38%\",options=\"header\",]\n|===\n|Name |Changeable |Default Value |Description\n|Editable |Yes |on |If enabled, show the dashboard in `editing mode'. If\nnot, show it in `view mode'. In view mode, all editing is disabled,\npages and reports can not be moved, edited or renamed.\n\n|Enable Fullscreen Report Views |Yes |on |If enabled, show the *🔳\nFullscreen* button on the top-right of a report, letting users maximize\na visualization.\n\n|Maximum Query Time (seconds) |Yes |20 |The maximum time is a query is\nallowed to take before being cancelled automatically. Increase this if\nyou have complex analytical queries.\n\n|Disable Row Limiting |Yes |off |If enabled, the automatic\nlink:reports#row-limiting[row limiting] feature of dashboards is\ndisabled.\n\n|Page Number |No |0 |The current page number of the dashboard being\nviewed. This can only be changed by switching pages in the dashboard\nheader.\n\n|Global Parameters |No | {} |The global parameters that are shared among\nall reports in the dashboard. See the next section for more on global\nparameters.\n|===\n\n== Parameters\n\nDashboard parameters are key-value pairs that can be used inside the\nqueries of reports. A convention is that a dashboard parameter in\nNeoDash will always start with `$neodash_`.\n\nParameters can only be set (and unset) using the\nlink:../reports/parameter-select[Parameter Select] reports. After setting a\nparameter, it will be available to all reports in the dashboard. A query\nthat uses a dashboard parameter will look like this:\n\n....\nMATCH (m:Movie)<-[a:ACTED_IN]-(p:Person)\nWHERE m.title = $neodash_movie_title\nRETURN m, a, p\n....\n\n=== Deep-Linking Parameters\n\nFor browser-based NeoDash deployments, you set NeoDash parameters by\nmeans of URL parameters. For example, when a user visits the following\nURL:\n\n....\nhttps://neodash.graphapp.io/?neodash_person_name=Adam\n....\n\nThis will set the parameter `$neodash_person_name` to `Adam` after\nloading the dashboard.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/extensions/access-control-management.adoc",
    "content": "\n= Access Control Management\n\ninclude::../../banner.adoc[]\n\nThis extension lets you manage access control for roles and users, letting you assign users to roles as well as controlling which node labels can be read by a user.\n\nThis extension is only visible to users with the role of \"Administrator\" or \"Super User\". Enabling this extension will allow the admin user to manage the labels of the roles in the database and then attach them to the users.\n\n\n== Using the Extension ==\nIf you have logged in to Neodash as an admin user, you will be able to enable the extension in the \"Extensions\" menu. Clicking on this extension will give the user a new button next to the settings button in the dashboard header. If the user click on this button, a menu will appear with all the roles in the database. \n\nimage::rolesmenu.png[Role menu]\n\nThe user can then click on any role and a window will appear with the role's context:\n\n* User list - This is a list of users from your database. You can select multiple users from the list and the role will be added to all the selected users.\n\n* Allow list - This is a list of labels that the role will be granted to read. You can select multiple labels from the list or if you want every label to be granted, you can select \"*\" from the list. (Requires a database to be selected)\n\n* Deny list - This is a list of labels that the role will be denied to read. You can select multiple labels from the list or if you want every label to be denied, you can select \"*\" from the list.  (Requires a database to be selected)\n\n\nFinally when the admin user clicks on the \"Save\" button, the role will be updated in the database and the labels will be granted or denied to the users that were selected for the specific role and database.\n\nimage::rolelabelmodal.png[Role modal]\n\n> Universal (Cross-database) `GRANT` and `DENY` privileges are not supported by this extension. Privileges must be added on a database-specific level. See the Neo4j https://neo4j.com/docs/operations-manual/current/authentication-authorization/privileges-reads/[documentation on read privileges] for more information.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/extensions/advanced-visualizations.adoc",
    "content": "\n= Advanced Visualizations\n\ninclude::../../banner.adoc[]\n\nAdvanced visualizations let you extend your dashboard with complex, powerful visualizations beyond the standard visualizations.\nFor specific use-cases, these visualizations may convey information that a simple visualization cannot.\nTo use advanced visualizations, enable them in the **Extensions Window**. This makes them selectable inside reports, as well as add examples to the Example window.\n\nThe following visualizations are part of this extension:\n- A link:../../reports/graph3d[3D Graph] to visualize a graph in three dimensions.\n- A link:../../reports/sankey[Sankey Chart] to visualize flows.\n- Three charts to plot hierarchical data (link:../../reports/sunburst[Sunburst], link:../../reports/circle-packing[Circle Packing], link:../../reports/treemap[Treemap])\n- A link:../../reports/gauge-chart[Gauge Chart] to show percentages.\n- An link:../../reports/choropleth[Choropleth] to visualize numeric, country-data.\n- An link:../../reports/areamap[Area Map] to show an interactive world map, annotated with numeric country / region values.\n- A link:../../reports/gantt[Gantt] chart to visualize dependencies between tasks. \n- A link:../../reports/radar[Radar Chart] to create a radial view of multiple categoric values.\n\nimage::advanced-visualizations.png[Advanced Visualizations]\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/extensions/forms.adoc",
    "content": "\n= Forms\n\ninclude::../../banner.adoc[]\n\nThe 'forms' extension lets you combine different parameter selectors to update / modify your graph data.\nUpdate queries are predefined by the dashboard builder, and the user is limited to specifying the parameters for the query only.\n\nSee link:../../reports/form[Form] on how to create, configure, and use forms.\n\n> Keep in mind that data-altering forms require your Neo4j user to have **write-access** to the graph. Make sure you give access to a select group of power-users only.\n\n\n\n\nimage::forms.png[Forms]\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/extensions/index.adoc",
    "content": "\n= Extensions\n\ninclude::../../banner.adoc[]\n\nExtensions provide a way to expand the basic functionality of NeoDash with extra features.\nTo enable an extension, open up the extensions window by clicking the puzzle piece icon in the left-sidebar of the screen.\nThis will open up the **Extensions Window**, which lets you toggle active extensions for the current dashboard.\n\nimage::extensions.png[The Extensions Window]\n\nThe following types of functionality can be added through NeoDash extensions:\n\n- A new type of visualization.\n- A more customizable version of an existing visualization.\n- New core features, such as rule-based styling or interactive reports.\n\nThe currently available extensions in NeoDash are:\n\n- link:advanced-visualizations[Advanced Visualizations]\n- link:rule-based-styling[Rule-based Styling]\n- link:report-actions[Report Actions]\n- link:natural-language-queries[Text2Cypher - Natural Language Queries]\n- link:forms[Forms]\n- link:access-control-management[Access Control Management]\n\n== Types of Extensions\n\n=== 1. Core Extensions\nCore Extensions are available as part of the open-source NeoDash project.\nThese are available to use for free anywhere - Neo4j Desktop, public NeoDash deployments, and self-hosted NeoDash deployments.\n\n=== 2. Expert Extensions\nExpert Extensions are built by the Neo4j Professional Services team.\nThese extensions push NeoDash to the next level, by providing extra functionality to create interactive graph applications.\nReach out to link:mailto:emea_pmo@neotechnology.com[Neo4j Professional Services] if you are interested in a customized / new expert extension for your use-case.\n\n=== 3. Custom Extensions\nCustom Extensions are self-built extensions that you can plug into the project.\nTo learn about how to fork and extend NeoDash, check out the link:../../developer-guide[Developer Guide].\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/extensions/natural-language-queries.adoc",
    "content": "\n= Text2Cypher - Natural Language Queries\n\ninclude::../../banner.adoc[]\n\nUse natural language to generate Cypher queries in NeoDash. Connect to an LLM through an API, and let NeoDash use your database schema + the report types to generate queries automatically.\n\n== How it works\nThis extension feature allows users to interact with NeoDash using natural language to generate Cypher queries for querying Neo4j graph databases. \nThis integration leverages Language Models (LLMs) to interpret user inputs and generate Cypher queries based on the provided schema definition.\n\n== Configuration\nTo enable Natural Language Queries in NeoDash, follow these configuration steps:\n\n1. Open NeoDash and navigate to the \"Extensions\" section in the left sidebar.\n2. Locate the \"Text2Cypher\" extension and click on it to activate it.\n3. Once activated, a new button will appear on top of the screen, with a red exclamation mark (⚠️). Click this button.\n4. In the configuration window, you will be prompted to provide the necessary information to connect to the Language Model (LLM). Enter the model provider, API key, deployment url if needed by the model provider, and select the desired model to use.\n5. After providing the required information, click on the \"Start Querying\" button to finalize the configuration.\n\nimage::llmconfiguration.png[Configuration settings for the Natural Language Queries extension]\n\n== Usage\nOnce the extension is configured, you can start using it in your NeoDash reports. Here's how:\n\n1. Open the Report settings for the desired report.\n2. In the report settings, you will find a toggle switch located above the editor. This switch allows you to toggle between Cypher and English languages.\n3. Since you have enabled the extension and authenticated by providing your API key, you can switch to the English language mode.\n4. Start formulating your queries in plain English, using natural language expressions to describe the data you want to retrieve.\n5. After composing your query, you have two options for further actions:\n\n* Translate: By clicking the \"Translate\" button, your query will be translated into Cypher using the Language Model. The translated Cypher query will be displayed in the editor when you toggle to the Cypher view. \nThis allows you to review and modify the generated Cypher query before execution.\n* Run: If you wish to directly execute the query and view the results, click the \"Run\" button in the top right corner. The execution of the query will depend on the selected report type, and the results will be displayed accordingly.\n\nimage::englisheditor.png[Example of the English editor in NeoDash]\n\n== Improving Accuracy with Custom Prompting\nTo boost the accuracy of the language model, you can provide your own example queries to be fed into the prompt.\nSpecifying queries specific to your data model & use-cases can significantly improve the quality of Text2Cypher translations.\n\nTo access the model examples screen, open up the settings for the extensions.\nAfter specifying the provider and model, click the \"Tweak Prompts\" button on the bottom-left of the window.\nThis leads you to the example interface:\n\nimage::llm-examples.png[Custom Examples for your prompt]\n\nIn this interface, you can specify one or more examples that are sent to the language model.\nAn example consists of both a Cypher query, and a natural language equivalent of that query.\nYou can create as many examples as you want, but keeping them close to your user queries will yield best results.\n\n== Underlying Functionality\n* Retrieve the Schema: The system prompts at the beginning of the interaction to retrieve the database schema. This ensures that the generated queries adhere to the provided schema and available relationship types and properties.\n\n* Prompting in English: Once the schema is retrieved, you can start prompting your queries in plain English. NeoDash, powered by the LLM, will interpret your English query and generate the corresponding Cypher query based on the provided schema.\n\n* Automatic Query Generation: NeoDash automatically generates the Cypher queries for you, taking into account the report type you specified. Whether it's a table, graph, bar chart, line chart, or any other supported report type, the generated queries will retrieve the necessary data based on the report requirements.\n\n* Retry Logic: To enhance the reliability of the generated queries, we have implemented retry logic. If there is any issue or error during the query generation process, the system will attempt to retry three times as a maximum and provide a valid query to ensure smooth query execution.\n\n== Prompting Tips\n\nWhen using Natural Language Queries in NeoDash, keep the following tips in mind to enhance your experience:\n\n1. Be clear and specific in your queries: Provide detailed descriptions of the data you want to retrieve, including node labels, relationship types, and property values.\n2. Use keywords and phrases: Incorporate relevant keywords and phrases that are commonly used in the context of your data to improve query accuracy.\n3. Ask precise questions: Frame your queries as questions to obtain specific information. For example, instead of \"Show me all customers,\" try \"Which customers have made a purchase in the last month?\"\n4. Experiment with different phrasings: If you're not getting the desired results, try rephrasing your query using synonyms or alternative expressions.\n5. Avoid ambiguous queries: Ambiguous or vague queries may yield unexpected results. Make sure to provide sufficient context and clarify any ambiguities.\n6. Validate and review generated queries: Always review the generated Cypher queries to ensure they accurately represent your intent and produce the expected results.\n\n\n== Important Considerations\n\nWhen using Natural Language Queries with Language Models, it's important to be aware of the following considerations:\n\n1. Multiple model providers: Depending on your configuration, your queries may be processed by different model providers. Take into account that this means your data is being sent to different providers.\n2. Non-deterministic nature: Language Models can produce non-deterministic outputs. The generated queries may vary between different runs, even with the same input prompt. Validate the generated queries and perform thorough testing to ensure correctness.\n3. Potential hallucination: Language Models can generate outputs that may not align with the specific schema or data constraints. Exercise caution and verify the results to prevent potential inaccuracies or hallucinations."
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/extensions/report-actions.adoc",
    "content": "\n= Report Actions \n\ninclude::../../banner.adoc[]\n\nlink:../#_2_pro_extensions[label:Pro&nbsp;Extension[]]\n\nReport actions let dashboard builders add interactivity into dashboards. Actions can be used to achieve:\n\n1. Cross-report filtering.\n2. Using the output of one report in another report.\n3. Providing users with more parameterized control beyond parameter selectors.\n\nThe image below displays an example of two reports interacting using report actions:\n\n- An action is defined for the table: **If a user clicks on a row in the Customer column, set the parameter `$neodash_customer_name` to `row.Customer`**.\n- The graph visualization uses the parameter `$neodash_customer_name` to select a specific node. The graph is automatically updated when the user clicks on a row entry inside the table.\n\nimage::report-actions.png[Report Actions]\n\n\n\n== Configuration\nFirst, ensure that the extension is enabled.\nThen, to create a **Report Action**, open up the report settings, Then, click the 'Report Action' button on the bottom right (marked with the red circle):\n\nimage::reportactionsbutton.png[Report]\n\n\nThis will open up the rule definition window. Inside this screen, a list\nof rules can be defined. An unlimited number of rules can be defined, and based on the visualization, different actions can be specified. Each rule will have the following structure:\n\n IF [CONDITION] SET [OBJECT] TO [VALUE] \n\nimage::reportactions.png[Report]\n\n== Supported Visualizations\n\nReport Actions are available for the following report types:\n\n- Tables\n- Graphs\n- Maps\n\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/extensions/rule-based-styling.adoc",
    "content": "\n= Rule-Based Styling \n\ninclude::../../banner.adoc[]\n\nThe rule-based styling extension allows users to dynamically color elements in a visualization based on output values. This can be applied to tables, graphs, bar charts, line charts, and more. To use the extension, click on the 'rule-based styling' icon inside the settings of a report.\n\n\nimage::rule-based-styling.png[Rule-Based Styling]\n\n\n== Configuration\nFirst, ensure that the extension is enabled.\n\nThen, on several report types, rule-based styling can be applied to the\nvisualization. To do this, open up the report settings, Then, click the\n*Rule-Based Styling* button on the bottom right (marked with the red\ncircle):\n\nimage::rulebasedstylingbutton.png[Report]\n\nThis will open up the action definition window. Inside this screen, a list\nof action rules can be defined. Each rule will have the following structure:\n\n IF [CONDITION] THEN [STYLE]\n\nimage::rulebasedstyling.png[Report]\n\nConditions are always based on one of the return fields of the query.\nThis can be a simple field (text, number) or a node property. Style\nrules are (as of NeoDash 2.1) always color-based.\n\nFor example, the following rule will set the color of all `Warning`\nnodes to red:\n\n`IF Warning.level = \"critical\" THEN 'Node Color' = \"red\"`\n\nUltimately, it is important to understand that the order of the rules is\nimportant. If a node matches multiple rules, the first rule that matches\nwill be used. If no rules are matched, the default style will be used.\n\n== Supported Visualizations\nRule-Based Styling is available for the following report types:\n\n- Tables\n- Bar Charts\n- Line Charts\n- Pie Charts\n- Graphs\n- Maps\n- Single Values\n\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/extensions/workflows.adoc",
    "content": "\n= Workflows\n\ninclude::../../banner.adoc[]\n\nIntroducing an advanced extension for creating, managing, and running workflows with Cypher queries. Simplify ETL flows, execute complex query chains, and run graph data science workloads effortlessly from Neodash.\n\n== Enable the extension\n\n== Create a Workflow\n\n== Create a new step in the workflow\n\n== Check status\n\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/faq.adoc",
    "content": "include::../banner.adoc[]\n\n= FAQ\n\ninclude::../banner.adoc[]\n\n== 1. How can I learn more about NeoDash?\n\nTo learn more, check out the following list of resources (blogs, videos\nand sites): \n\n- https://www.youtube.com/watch?v=Ygzj0Y4cYm4[NeoDash 2.0 in\nFive Minutes] \n- https://www.youtube.com/watch?v=vjZ9M7JpExA[NeoDash 2.0 - Hands On at Neo4j Live] \n- https://medium.com/p/ddc938ff82fa[Investigating Supply Chains with NeoDash] \n- https://thatdavestevens.medium.com/social-recommendations-slack-neo4j-and-neodash-fe916588e65b[Social Recommendations with Neo4j & NeoDash] \n- https://neo4j.com/developer-blog/bitcoin-transactions-dashboard-neo4j-neodash/[Real-Time Dashboard of Bitcoin Transactions With Neo4j and NeoDash]\n- https://medium.com/@a.emrevarol/european-natural-gas-network-via-knowledge-graph-3c3decb5f2ec[European\nNatural Gas Pipelines] \n- http://blog.bruggen.com/2020/11/exporting-spotify-playlists-into-neo4j.html[Exporting Spotify Playlists into Neo4j] \n- https://nielsdejong.nl/neo4j%20projects/2021/12/14/neodash-2.0-a-brand-new-way-of-visualizing-neo4j-data.html[NeoDash\n2.0 Release Overview] \n- https://nielsdejong.nl/neo4j%20projects/2021/06/06/neodash-1.1-extensible-interactive-dashboards.html[NeoDash\n1.1 Release Overview] \n- https://nielsdejong.nl/neo4j%20projects/2020/11/16/neodash[NeoDash 1.0\nRelease Overview]\n\n_Have a blog post about NeoDash you’d like to share? Let us know and we\ncan add it to this list!_\n\n== 2. Is NeoDash free to use?\n\nNeoDash 2.X is available under the\nhttps://www.apache.org/licenses/LICENSE-2.0[Apache 2.0 license], which\nmeans you can use it for free for with your project.\n\n____\nKeep in mind! As NeoDash is a https://neo4j.com/labs/[Neo4j Labs]\nproject, it is not part of the official Neo4j product suite, and is not\nsupported as part of a Neo4j license. (See also question #8 below…)\n____\n\n== 3. Can I publish the dashboard that I built?\n\nWhen you’re done building a dashboard and want to show to others as a\nread-only web page, you can set up a link:standalone%20mode[Standalone\nMode] deployment of NeoDash.\n\nIf you need help setting this up, please contact the NeoDash team.\n\n== 4. Is NeoDash Production Ready?\n\nNeoDash Labs is an experimental tool without official support.\nFor production-grade usage with Neo4j Enterprise Edition, we recommend a `NeoDash commercial` license.\n\n== 5. Can I use NeoDash with Neo4j Community Edition?\n\nYes, NeoDash can be used with any type of Neo4j deployment (On-premise,\ncloud, or fully managed in Neo4j Aura). We do however recommend that you use the\nEnterprise version, as it lets you create a\ndedicated read-only user for reporting.\n\n== 6. I’m missing a feature. Can I ask for help?\nFeature requests and bug reports are more than welcome. Please open an\nissue on https://github.com/neo4j-labs/neodash/issues[Github]. Issues\nwill be addressed on a best-effort basis.\n\nIf you’re looking for a specific feature with high priority, you can\nreach out to the Neo4j team.\n\n== 7. How can I contribute to NeoDash myself?\n\nNeoDash is an open-source tool that can be extended by anyone. If you\nare interested in contributing, please check out the\nhttps://github.com/neo4j-labs/neodash[Github repository].\n\nAside from code contributions, we are also very happy to hear about how\nyou use NeoDash. If you have a blog post, podcast or video of your graph\ndashboard, let us know!\n\n== 8. Can I get professional help with NeoDash?\n\nIf you are interested in a services agreement to support your NeoDash deployment, please reach out to the\nhttps://neo4j.com/professional-services/[Neo4j Services Team].\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/index.adoc",
    "content": "include::../banner.adoc[]\n\n= User Guide\n\ninclude::../banner.adoc[]\n\nThe following pages contain everything you need to get started with NeoDash.\n\n\n* link:../quickstart[Quickstart] tells you how to get started with NeoDash right away.\n* link:dashboards[Dashboards] elaborates on how dashboards are created and used in\nNeoDash.\n* link:pages[Pages] explains how to manage pages inside a dashboard.\n* link:reports[Reports] contains information on how a report works, and lists the\ndifferent types that can be used.\n* link:publishing[Publishing] explains how to publish a dashboard for others to view.\n* link:extensions[Extensions] lists the different extensions available for NeoDash."
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/pages.adoc",
    "content": "include::../banner.adoc[]\n\n= Pages\n\ninclude::../banner.adoc[]\n\nA page is a collection of link:../reports[reports] that can be viewed at\nthe same time. Each page can have an unlimited number of reports in it,\nand will switch to a scrollable view when the number of reports do not\nfit in the user’s window.\n\nAn example of a dashboard with 4 pages can be seen below:\n\nimage::page.png[Page]\n\nA dashboard may have as many pages as required by the dashboard builder.\nTo switch pages, simply click on the page title in the dashboard header.\n\n== Editing Pages\n\nPages can be added, renamed, and deleted by using the buttons in the\ndashboard header (if editing is enabled). Pages can additionally be\n*re-ordered* by dragging and dropping them in the header.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/publishing.adoc",
    "content": "include::../banner.adoc[]\n\n= Publishing\n\ninclude::../banner.adoc[]\n\nWhen you are done building a dashboard, you may want to *publish* that\ndashboard for others to view. The workflow for a continuous dashboarding\ncycle may look something like this:\n\nimage::publish.png[Workflow]\n\nKeep in mind that the purpose of an application in the `View' phase is\nvery different from the `Build' phase: \n\n1. A dashboard cannot be edited\nafter it has been published. \n2. A fixed dashboard must be loaded and a fixed database must be connected to. \n3. Users in the `View' phase should not see the Cypher queries configuration powering the visualizations.\n\n== Architecture\n\nNeoDash enables the Build, Publish, View workflow by having two seperate\ndeployments of the NeoDash application: \n\n1. An `editor` deployment for the build phase. \n2. A `viewer` deployment for the view phase.\n\nThe *editor* deployment is the app you are using from Neo4j Desktop,\nfrom https://neodash.graphapp.io, or from your own deployment.\n\nThe *viewer* deployment will require some configuration to be set up.\nThese three configurations must be set for NeoDash to be able to run in\n`View' mode: \n\n1. A flag telling the app to disable all editing features.\n2. A hardcoded Neo4j database to connect to. \n3. A hardcoded dashboard to load.\n\nTechnical details on setting this up are documented in the link:../../developer-guide/standalone-mode[Standalone\nMode] page. \n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/areamap.adoc",
    "content": "\n= Area Map\n\ninclude::../../banner.adoc[]\n\nlink:../../extensions/advanced-visualizations[label:Advanced&nbsp;Visualization[]]\n\nThe Area Map charts can be used to render geographical based information on geoJson polygons. It's possible to click a polygon to visualize its regions and their related data.\nIt takes two fields: \n\n- *code*: String. This represents the country code or regional code that must be binded to the visualization. The map supports Alpha-3 and Alpha-2 country codes (by default Alpha-2). Instead the supported format for the region polygons is ISO 3166.\n- *value*:  Number. Cardinal data to be used on the chart.\n\n== Examples\n\n=== Basic Area Map\n\n\n[source,cypher]\n----\nMATCH (:Company{name:'NeoDash'})-[:HAS_DEPARTMENT]->(:Department)<-[:IN_DEPARTMENT]-(e:Employee),\n(e)-[:LIVES]->(city:City)-[:IN_COUNTRY]->(country:Country)\nWITH city, country\nCALL {\n    WITH country\n    RETURN country.countryCode as code, count(*) as value\n    UNION\n    WITH city\n    RETURN city.countryCode as code, count(*) as value\n}\nWITH code, sum(value) as totalCount\nRETURN code,totalCount\n----\nimage::areamap-countries.png[Country Level Visualization]\nimage::areamap-regions.png[Example of Drill Down inside a Country]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"15%,2%,26%,57%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Map Provider URL|Text|https://\\{s}.tile.openstreetmap.org/\\{z}/\\{x}/\\{y}.png| When specified, overrides Open Street map provider with a custom map tiles provider.\n\n|Color Scheme |List | |The color scheme to use for the area map. Country colors\nwill vary according their min to max ratio.\n\n|Country Code Format |List |Alpha-2 |Type of the country code (two/three letters).\n\n|Color Legend|on/off |on |Option to show color legend.\n\n|Drilldown Enabled |on/off |off |Enable map drilldown to visualize regional data.\n\n|Refreshable |on/off |off |Enables a refresh button for the report.\n\n|Fullscreen enabled |on/off |off |Enables a fullscreen view button for the report.\n\n|Download image enabled |on/off |off |Enables an image download button for the report.\n\n|Auto-run query |on/off |on |When activated, automatically runs the\nquery when the report is displayed. When set to `off', the query is\ndisplayed and will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|==="
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/bar-chart.adoc",
    "content": "\n= Bar Chart\n\ninclude::../../banner.adoc[]\n\nA bar chart will draw categories and values in a familiar bar-layout.\nThe bar chart will require you to choose the following selections:\n\n* *Category*: a text field. These will be the labels on the bars.\n* *Value*: a numeric field. This will be the height of the bars.\n* *Group*: Optionally, a second textual field. When ``Grouping'' is\nenabled in the advanced settings, the group can be used to draw a\nstacked bar chart, with several groups per category.\n\n== Examples\n\n=== Simple Bar Chart\n\n[source,cypher]\n----\nMATCH (p:Person)-[e]->(m:Movie)\nRETURN m.title as Title, COUNT(p) as People\n----\n\nimage::bar.png[Basic Table]\n\n=== Stacked Bar Chart\n\n[source,cypher]\n----\nMatch (p:Person)-[e]->(m:Movie)\nRETURN m.title AS Title, COUNT(p) as People, type(e) as Role\n----\n\nimage::barstacked.png[Basic Table]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"19%,17%,26%,38%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Show Legend |on/off |off |If enabled, shows a legend at the top right\nof the visualization.\n\n|Custom Dimensions |on/off |off |If enabled, the chart will no longer autofit to the size of the report card. If width extends beyond the report card, a scroll bar will be introduced to explore the chart horizontally.\n\n|Value Scale |List |linear |When set to symlog, uses a Symmetric\nlogarithmic scale instead of the default linear scale.\n\n|Min Value |Number |auto |If not set to ``auto'', this variable is\nminimum value for the bar chart.\n\n|Max Value |Number |auto |If not set to ``auto'', this variable is the\nmaximum value for the bar chart.\n\n|Group Mode |List |stacked |This setting determines how different groups\nare visualized when grouping is enabled. If set to stacked, different\ngroups of the same category are stacked on top of each other. If set to\ngrouped, they are placed alongside each other.\n\n|Layout |List |vertical |Whether to use a vertical or horizontal bar\nchart layout.\n\n|Color Scheme |List | |The color scheme to use for the category groups.\nColors are assigned automatically (consequitevely) to the different\ngroups returned by the Cypher query.\n\n|Show Values on Bars |on/off |off |If enabled, shows the category value\ninside the respective bar.\n\n|Skip label on width (px) |number |0 |Skip label if bar width is lower than provided value, ignored if 0.\n\n|Skip label on height (px) |number |0 |Skip label if bar height is lower than provided value, ignored if 0.\n\n|Custom label position |off/top/bottom |off | Allow user to place label out of the bar. This will override any other\nlabel configuration.\n\n|Label Rotation (degrees) |number |45 |the angle at which the bar labels\nare rotated.\n\n|Margin Left (px) |number |50 |The margin in pixels on the left side of\nthe visualization.\n\n|Margin Right (px) |number |24 |The margin in pixels on the right side\nof the visualization.\n\n|Margin Top (px) |number |24 |The margin in pixels on the top side of\nthe visualization.\n\n|Margin Bottom (px) |number |40 |The margin in pixels on the bottom side\nof the visualization.\n\n|Legend Width (px) |number |128 |The width in pixels of each legend\nlabel on top of the visualization (if enabled).\n\n|Hide Selections |on/off |off |If enabled, hides the property selector\n(footer of the visualization).\n\n|Auto-run query |on/off |on |when activated automatically runs the query\nwhen the report is displayed. When set to `off', the query is displayed\nand will need to be executed manually.\n\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n\n|Bar Width |number |10 |*Only active when 'Custom Dimensions' is on.* The width of each bar. Increasing the bar width will increase the width of the chart. This setting will have the largest influence on the width of the chart.\n\n|Expand Height For Legend |on/off |off |Useful for when the legend has many labels. When enabled the chart height will adjust to the number of rows returned by the query and therefore will prevent legend labels being cut off.\n\n|Inner Padding |number |0 |When specified, will add padding between any grouped elements.\n\n|Legend Position |Vertical/Horizontal |Vertical |Will dictate whether the lagend is displayed vertically on the right hand side of the chart or horizontally on the bottom of the chart.\n|===\n\n== Rule-Based Styling\n\nUsing the link:../#_rule_based_styling[Rule-Based Styling] menu, the\nfollowing style rules can be applied to the bar chart: \n\n- The color of the bar.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/choropleth.adoc",
    "content": "\n= Choropleth\n\ninclude::../../banner.adoc[]\n\nlink:../../extensions/advanced-visualizations[label:Advanced&nbsp;Visualization[]]\n\nA Choropleth chart will render geographical data in geoJson polygons\nlayout. It takes two fields: \n\n- *code*: String. This represents the Alpha-3 country code of region to be used. Alpha-2 it's not supported.\n- *value*:  Number. Cardinal data to be used on the chart.\n\n== Examples\n\n=== Basic Choropleth\n\n\n\n\n[source,cypher]\n----\nMATCH p=(n:Wine)-[:IS_FROM|PART_OF*]->(c:Country)\nWITH DISTINCT c.iso3 as country, count(DISTINCT n) as wines\nRETURN country, wines\n----\n\nimage::choropleth.png[Choropleth Chart]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"15%,2%,26%,57%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Enable interactivity |on/off |on |If enabled, turn on animations when a\nuser hovers over a polygon.\n\n|Color Scheme |List | |The color scheme to use for the choropleth. Country colors\nwill vary according their min to max ratio.\n\n|Polygon border width (px) |number |0 |The width of the border of each\nrectangle.\n\n|Margin Left (px) |number |24 |The margin in pixels on the left side of\nthe visualization.\n\n|Margin Right (px) |number |24 |The margin in pixels on the right side\nof the visualization.\n\n|Margin Top (px) |number |24 |The margin in pixels on the top side of\nthe visualization.\n\n|Margin Bottom (px) |number |40 |The margin in pixels on the bottom side\nof the visualization.\n\n|Country Code Format |List |iso_a3 |ISO Standard used on country codes.\n\n\n|Projection Scale |number |100 |Projection Scale of the visualization\n\n|Projection x translation |number |0.5 |This parameter will move the center of\nthe visualization on the x axis\n\n|Projection y translation |number |0.5 |This parameter will move the center of\nthe visualization on the y axis\n\n|Auto-run query |on/off |on |When activated, automatically runs the\nquery when the report is displayed. When set to `off', the query is\ndisplayed and will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|==="
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/circle-packing.adoc",
    "content": "\n= Circle Packing\n\ninclude::../../banner.adoc[]\n\nlink:../../extensions/advanced-visualizations[label:Advanced&nbsp;Visualization[]]\n\nA circle packing chart will render hierarchical data in a group of\nnested circles. It takes two fields: \n\n- *Path*: a list of strings. This represents the hierarchy (from highest to lowest level).\n - *Value*: a number that matches the size of the element at the lowest level. Sizes of non-leaf levels are determined from the sum of their children.\n\n== Examples\n\n=== Basic Circle Packing Chart\n\n[source,cypher]\n----\nMATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department)\nWITH nodes(path) as no\nWITH no, last(no) as leaf\nWITH  [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val\nRETURN result, val\n----\n\nimage::circlepacking.png[Circle Packing Chart]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"13%,3%,26%,58%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Enable interactivity |on/off |on |If enabled, turn on animations when a\nuser hovers over an circle.\n\n|Color Scheme |List | |The color scheme to use for the circles. Colors\nare assigned automatically for each of the sub-hierarchies.\n\n|Circle border width (px) |number |0 |The width of the border of each\ncircle.\n\n|Margin Left (px) |number |24 |The margin in pixels on the left side of\nthe visualization.\n\n|Margin Right (px) |number |24 |The margin in pixels on the right side\nof the visualization.\n\n|Margin Top (px) |number |24 |The margin in pixels on the top side of\nthe visualization.\n\n|Margin Bottom (px) |number |40 |The margin in pixels on the bottom side\nof the visualization.\n\n|Auto-run query |on/off |on |When activated, automatically runs the\nquery when the report is displayed. When set to `off', the query is\ndisplayed and will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/form.adoc",
    "content": "\n= Form\n\ninclude::../../banner.adoc[]\n\nA form is a special type of report that lets users run predefined, parameterized queries.\nA single form can consist of:\n\n- Zero or more link:../parameter-select[parameter selectors].\n- A button that triggers submitting the form.\n\nWhen creating a form, you write the Cypher query that is called when the submit button is clicked.\nThis query can then use the parameters specified as input. The image below provides an example of a form. On the left, the settings used to define the form, on the right, the final form as visible to the user.\n\nimage::createform.png[Complex Form]\n\n\n== Examples\n\n=== Simple Button\n\nA form without parameters is a button that runs a specified query. \nOne or more buttons can be used to perform simple operations in the graph. \nBelow is an example of a simple button form. On submitting, the following query is executed:\n\n[source,cypher]\n----\nMERGE (c:Counter)\nSET c.count = c.count+1\n----\n\nimage::formbutton.png[Button Form]\n\n=== A Parameter and a Button\nTo create a form with dynamic input, use both a parameter and a button. \nBelow is an example of a form that deletes nodes from the graph. On submit, the following query is executed:\n\n[source,cypher]\n----\nMATCH (p:Person)\nWHERE p.name = $neodash_person_name\nDETACH DELETE p\n----\n\nimage::formsimple.png[Simple Form]\n\n=== Parameters Only\n\nBy hiding the submit button, a form can also be used as a space-efficient way to embed multiple parameter selectors.\nDisable `Has Submit Button` in the report's advanced settings, and add two or more parameter selectors to the form.\n\nimage::formselector.png[Parameter-only Form]\n\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"19%,17%,26%,38%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n\n|Form Button Text |text |Submit |Text displayed on the button that submits the form.\n\n|Reset Button Text | text |Reset Form |Text displayed on the button that resets the form to data entry mode.\n\n|Confirmation Message | multiline text |Form submitted. |Text displayed to the user after the form is submitted successfully.\n\n|Clear parameters after submit |on/off |on | Clears all dashboard parameters in the form after submitting.\n\n|Has Submit Button |on/off |on | When enabled, lets the user submit the form with a button. Disabling turns the form into parameters-only mode.\n\n|Has Reset Button |on/off |on |When enabled, lets the user reset the form to enter more data. \n\n|Has Submit Message |on/off |on |When enabled, the user to a seperate screen after submitting the form. Else, always stay in data-entry mode.\n\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n\n\n|===\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/gantt.adoc",
    "content": "\n= Gantt Chart\n\ninclude::../../banner.adoc[]\n\nlink:../../extensions/advanced-visualizations[label:Advanced&nbsp;Visualization[]]\n\nA Gantt chart can be used to visualize tasks on a timeline, as well as their dependencies.\nThe NeoDash Gantt chart views your tasks are nodes in the graph, and your relationships are dependencies between them.\n\nTo use the Sankey chart, nodes must have at least three properties on them:\n\n- A `startDate`\n- An `endDate`\n- A `title`\n\nIn addition, different types of task dependencies can be visualized. The dependency must be stored as a property on a relationship, and can be one of four values:\n\n- `**SE**`: The dependency is from the **S**tart of the origin task, to the **E**nd of the next task.\n- `**SS**`: The dependency is from the **S**tart of the origin task, to the **S**start of the next task.\n- `**ES**`: The dependency is from the **E**nd of the origin task, to the **S**start of the next task.\n- `**EE**`: The dependency is from the **E**nd of the origin task, to the **E**nd of the next task.\n\n== Examples\n\n=== Gantt Chart\nReturn nodes and relationships to be visualized in the chart.\nIt is mandatory to specify the three node properties (start date, end date and title) in the report's advanced settings.\n\n[source,cypher]\n----\nMATCH (a:Activity)-[r:FOLLOWS]->(a2:Activity)\nRETURN a, r, a2\n----\n\nimage::gantt.png[Gantt Chart]\n\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"15%,2%,6%,77%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n\n| Bar Color  | string  | '#a3a3ff' | Default color for the task bars (with no style rules applied.)\n\n| Task Label Property  | string  | 'activityName' | Node property to display on the task bar.\n\n| Task Start Date Property  | string  | 'startDate' | Node property to use as a start date for the task.\n\n| Task End Date Property  | string  | 'endDate' | Node property to use as an end date for the task.\n\n| Task Ordering Property  | string  | (auto) | Custom ordering of the tasks. Defaults to use the start date property.\n\n| Dependency Type Property  | string  | 'rel_type' | The relationship property that stores the dependency type. Property values must be one of `['SS', 'SE', 'ES', 'EE']`\n\n| View mode  | string  | 'auto' | Zoom level of the chart. one of `['auto', 'Half Day', 'Day', 'Week', 'Month', 'Year']`.\n\n|Margin Left (px) |number |24 |The margin in pixels on the left side of\nthe visualization.\n\n|Margin Right (px) |number |24 |The margin in pixels on the right side\nof the visualization.\n\n|Margin Top (px) |number |24 |The margin in pixels on the top side of\nthe visualization.\n\n|Margin Bottom (px) |number |40 |The margin in pixels on the bottom side\nof the visualization.\n\n|Auto-run query |on/off |on |when activated automatically runs the query\nwhen the report is displayed. When set to `off', the query is displayed\nand will need to be executed manually.\n\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n\n\n== Rule-Based Styling\n\nUsing the link:../#_rule_based_styling[Rule-Based Styling] menu, the\nfollowing style rules can be applied to the Gantt chart: \n\n- The color of a task bar.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/gauge-chart.adoc",
    "content": "\n= Gauge Chart\n\ninclude::../../banner.adoc[]\n\nlink:../../extensions/advanced-visualizations[label:Advanced&nbsp;Visualization[]]\n\nA gauge chart takes a single numeric value, and plots it on an animated gauge:\n\n- The value returned should be in the range of 0 to 100.\n- The gauge chart can be customized with different colors and levels (arc segments).\n\n== Examples\n\n=== Basic Gauge Chart\n\n[source,cypher]\n----\nMATCH (c:CPU)\nWHERE c.id = 1\nRETURN c.load_percentage * 100 \n----\n\nimage::gauge.png[Gauge Chart]\n\n== Advanced Settings\n\n              \n[width=\"100%\",cols=\"15%,2%,6%,77%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Number of levels | number | 3 | The number of distinct colored levels in the gauge.\n\n| Comma-separated length of each arc | List |  \"0.15, 0.55, 0.3\" | A comma-separated list of length for each of the colored arc segments on the gauge.\n\n| Comma-separated arc colors | List | \"#5BE12C, #F5CD19, #EA4228\" | The HEX color values to assign to each arc.\n\n| Color of the text | string | black | The color of the number on the gauge.\n\n| Delay in ms before needle animation | number | 0 | Delay in milliseconds before starting the animation.\n\n| Duration in ms for needle animation | number | 2000 | The duration of the moving needle animation when the chart renders.\n\n|===\n\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/graph.adoc",
    "content": "\n= Graph\n\ninclude::../../banner.adoc[]\n\nThe graph report will render all returned nodes, relationships and paths\nin a force-directed graph layout. This includes collections (lists) of\nthese objects.\n\nThe library `react-force-graph` is used to create the visualizations.\nDepending on your browser, the visualization should be able to handle\ndrawing 1000-3000 nodes/relationships with custom styling options.\n\nThe graph layout contains an extensive set of features, including:\n\n- Drag and drop nodes. \n- Custom node/relationship styling. \n- Tooltips/inspect window on nodes/relationships.\n\n== Examples\n\n=== Basic Graph\n\n....\nMATCH (p:Person)-[a:ACTED_IN]->(m:Movie)\nWHERE m.title = 'The Matrix'\nRETURN p, a, m\n....\n\nimage::graph.png[Basic Graph]\n\n== Virtual Graph\n\n....\nMATCH (p:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person)\nWHERE m.title = \"The Matrix\"\nRETURN p, p2, apoc.create.vRelationship(p, \"KNOWS\", {}, p2)\n....\n\nimage::graph2.png[Virtual Graph]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"12%,2%,3%,83%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Node Color Scheme |List |neodash |The color scheme to use for the node\nlabels. Colors are assigned automatically (consequitevely) to the\ndifferent labels returned by the Cypher query.\n\n|Node Label Color |Text |black |The color of the labels drawn on the\nnodes.\n\n|Node Label Font Size |Number |3.5 |Size of the labels drawn on the\nnodes.\n\n|Node Size |Number |2 |Default size of a node in the graph\nvisualization. This size is applied if no custom size styling is defined\nand no Rule-Based styling is active.\n\n|Node Size Property |Text |size |Optionally, the name of the node\nproperty to map to the node size. This lets you define sizes on a\nnode-specific level, if you have a property that directly maps to the\nnumeric size value.\n\n|Node Color Property |Text |color |Optionally, the name of the node\nproperty to map to the node color. This lets you define colors on a\nnode-specific level, if you have a property that directly maps to the\nHTML color value.\n\n|Relationship Color |Text |#a0a0a0 |The color used for drawing the\nrelationship arrows in the visualization.\n\n|Relationship Width |Text |1 |The (default) width of the relationship\narrows in the visualization.\n\n|Relationship Label Color |Text |#a0a0a0 |The color of the labels\n(relationship type) drawn next to the relationship arrows.\n\n|Relationship Label Font Size |Text |2.75 |The font size of the labels\n(relationship type) drawn next to the relationship arrows.\n\n|Relationship Color Property |Text |color |Optionally, the name of the\nrelationship property to map to the arrow color. This lets you define\ncolors on a relationship-specific level, if you have a property that\ndirectly maps to the HTML color value.\n\n|Relationship Width Property |Text |width |Optionally, the name of the\nrelationship property to map to the arrow width. This lets you define\nwidths on a relationship-specific level, if you have a property that\ndirectly maps to the width value.\n\n|Animated Particles on Relationships |on/off |off |If enabled, draw\nrelationships with animated particles on them, moving in the direction\nof the relationship.\n\n|Arrow head size |Number |3 |Use this to set the length of the arrow head, size is adjusted automatically.\nIf 0, no arrow will be drawn.\n\n|Background Color |Text |#fafafa |The background color of the\nvisualization.\n\n|Layout (experimental) |List |force-directed |tree-top-down |tree-bottom-up |tree-left-right |tree-right-left |radial | Use this to switch from\nthe main (force-directed) layout to one of the experimental layouts\n(tree, radial). For the experimental layouts, make sure\nyour graph is a DAG (directed acyclic graph).\n\n| Graph Depth Separation | Number | 30 | Specify the level distance for the tree layout. \nThis setting controls the separation between different levels in the tree hierarchy. Adjusting this value impacts the overall spacing of the tree layout in your graph visualization.\n\n|Enable graph exploration |on/off |on |Enables basic exploration functionality for the graph. Exploration can be done by right clicking on a node, and choosing 'Expand' to choose a type to traverse. Data is retrieved real-time and not cached in the visualization.\n\n|Enable graph editing |on/off |off |Enables editing of nodes and relationships in the graph from the right-click context menu. In addition, lets users create new relationships with existing types/property keys as present in the database.\n\n|Show pop-up on Hover |on/off |on |if enabled, shows a pop-up when a\nuser hovers over one of the nodes/relationships in the visualization.\nThe pop-up contains the label and properties of the node/relationship.\n\n|Show properties on Click |on/off |on |if enabled, opens up a window\nwhen a user clicks on one of the nodes/relationships in the\nvisualization. The window contains the label and properties of the\nnode/relationship.\n\n|Fix node positions after drag |on/off |on |If enabled, locks in\n(freezes) the node positions after a user drags them.\n\n|Drilldown Link |Text (URL) |(no value) |Specifying a URL here will\ndisplay a floating button on the top right of the visualization. This\nbutton can be used to drilldown into a different tool (e.g. Bloom) so\nthat the graph can be explored further. Dynamic Dashboard Parameters\n(e.g. $neodash_person_name) can be used in these links as well.\n\n|Hide Selections |on/off |off |If enabled, hides the property selector\n(footer of the visualization).\n\n|Override no data message |Text |Query returned no data. |Override the message displayed to the user when their query returns no data.\n\n|Auto-run query |on/off |on |when activated automatically runs the query\nwhen the report is displayed. When set to `off', the query is displayed\nand will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n\n== Rule-Based Styling\n\nUsing the link:../#_rule_based_styling[Rule-Based Styling] menu, the\nfollowing style rules can be applied to the graph: \n\n- The background color of a node. \n- The label color of a node.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/graph3d.adoc",
    "content": "\n= 3D Graph\n\ninclude::../../banner.adoc[]\n\nlink:../../extensions/advanced-visualizations[label:Advanced&nbsp;Visualization[]]\n\nThe 3D graph report extends the default graph visualization with another dimension.\nIt supports most of the features & customizations for the regular (2D) graph, including rule-based styling and report actions.\nUsers can explore the 3D graph by zooming and panning through 3D space.\n\n\n== Examples\n\n=== Basic Graph\n\n....\nMATCH (p:Person)-[a:ACTED_IN]->(m:Movie)\nWHERE m.title = 'The Matrix'\nRETURN p, a, m\n....\n\nimage::graph3d.png[Basic 3D Graph]\n\n== Virtual Graph\n\n....\nMATCH (p:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person)\nWHERE m.title = \"The Matrix\"\nRETURN p, p2, apoc.create.vRelationship(p, \"KNOWS\", {}, p2)\n....\n\nimage::graph3dvirtual.png[Virtual 3D Graph]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"12%,2%,3%,83%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Node Color Scheme |List |neodash |The color scheme to use for the node\nlabels. Colors are assigned automatically (consequitevely) to the\ndifferent labels returned by the Cypher query.\n\n|Node Label Color |Text |black |The color of the labels drawn on the\nnodes.\n\n|Node Label Font Size |Number |3.5 |Size of the labels drawn on the\nnodes.\n\n|Node Size |Number |2 |Default size of a node in the graph\nvisualization. This size is applied if no custom size styling is defined\nand no Rule-Based styling is active.\n\n|Node Size Property |Text |size |Optionally, the name of the node\nproperty to map to the node size. This lets you define sizes on a\nnode-specific level, if you have a property that directly maps to the\nnumeric size value.\n\n|Node Color Property |Text |color |Optionally, the name of the node\nproperty to map to the node color. This lets you define colors on a\nnode-specific level, if you have a property that directly maps to the\nHTML color value.\n\n|Relationship Color |Text |#a0a0a0 |The color used for drawing the\nrelationship arrows in the visualization.\n\n|Relationship Width |Text |1 |The (default) width of the relationship\narrows in the visualization.\n\n|Relationship Label Color |Text |#a0a0a0 |The color of the labels\n(relationship type) drawn next to the relationship arrows.\n\n|Relationship Label Font Size |Text |2.75 |The font size of the labels\n(relationship type) drawn next to the relationship arrows.\n\n|Relationship Color Property |Text |color |Optionally, the name of the\nrelationship property to map to the arrow color. This lets you define\ncolors on a relationship-specific level, if you have a property that\ndirectly maps to the HTML color value.\n\n|Relationship Width Property |Text |width |Optionally, the name of the\nrelationship property to map to the arrow width. This lets you define\nwidths on a relationship-specific level, if you have a property that\ndirectly maps to the width value.\n\n|Animated Particles on Relationships |on/off |off |If enabled, draw\nrelationships with animated particles on them, moving in the direction\nof the relationship.\n\n|Arrow head size |Number |3 |Use this to set the length of the arrow head, size is adjusted automatically.\nIf 0, no arrow will be drawn.\n\n|Background Color |Text |#fafafa |The background color of the\nvisualization.\n\n|Layout (experimental) |List |force-directed |tree-top-down |tree-bottom-up |tree-left-right |tree-right-left |radial | Use this to switch from\nthe main (force-directed) layout to one of the experimental layouts\n(tree, radial). For the experimental layouts, make sure\nyour graph is a DAG (directed acyclic graph).\n\n| Graph Depth Separation | Number | 30 | Specify the level distance for the tree layout. \nThis setting controls the separation between different levels in the tree hierarchy. Adjusting this value impacts the overall spacing of the tree layout in your graph visualization.\n\n|Enable graph exploration |on/off |on |Enables basic exploration functionality for the graph. Exploration can be done by right clicking on a node, and choosing 'Expand' to choose a type to traverse. Data is retrieved real-time and not cached in the visualization.\n\n|Enable graph editing |on/off |off |Enables editing of nodes and relationships in the graph from the right-click context menu. In addition, lets users create new relationships with existing types/property keys as present in the database.\n\n|Show pop-up on Hover |on/off |on |if enabled, shows a pop-up when a\nuser hovers over one of the nodes/relationships in the visualization.\nThe pop-up contains the label and properties of the node/relationship.\n\n|Show properties on Click |on/off |on |if enabled, opens up a window\nwhen a user clicks on one of the nodes/relationships in the\nvisualization. The window contains the label and properties of the\nnode/relationship.\n\n|Drilldown Link |Text (URL) |(no value) |Specifying a URL here will\ndisplay a floating button on the top right of the visualization. This\nbutton can be used to drilldown into a different tool (e.g. Bloom) so\nthat the graph can be explored further. Dynamic Dashboard Parameters\n(e.g. $neodash_person_name) can be used in these links as well.\n\n|Hide Selections |on/off |off |If enabled, hides the property selector\n(footer of the visualization).\n\n|Override no data message |Text |Query returned no data. |Override the message displayed to the user when their query returns no data.\n\n|Auto-run query |on/off |on |when activated automatically runs the query\nwhen the report is displayed. When set to `off', the query is displayed\nand will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n\n== Rule-Based Styling\n\nUsing the link:../#_rule_based_styling[Rule-Based Styling] menu, the\nfollowing style rules can be applied to the graph: \n\n- The background color of a node. \n- The label color of a node.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/iframe.adoc",
    "content": "\n= iFrame\n\ninclude::../../banner.adoc[]\n\nAn iFrame report lets you embed a webpage inside your NeoDash dashboard.\nThe page can be loaded from any web address starting with `http://` or\n`https://`, with some exceptions*.\n\n____\nThe webpage must not explicitly disallow itself to be embedded, such as\nhttps://google.com.\n____\n\nTo render iFrames interactively based on the dashboard state, your\nglobal dashboard parameters can be passed into it dynamically. See the\n*Advanced Settings* below for more information.\n\n== Examples\n\n=== Basic iFrame\n\nimage::iframe.png[Basic iFrame]\n\n=== Dynamic iFrame\n\nimage::iframe2.png[Dynamic iFrame]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"19%,17%,26%,38%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Replace global parameters in URL |on/off |on |If enabled, replaces all\ninstances of query parameters (e.g. $neodash_person_name) inside the\niFrame URL.\n\n|Append global parameters to iFrame URL |on/off |off |If enabled,\nappends the full list of global parameters as URL parameters to the\nspecified URL.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/index.adoc",
    "content": "\n= Reports\n\ninclude::../../banner.adoc[]\n\nA report is the smallest building build of your dashboard. Each report\nwill have a single Cypher query behind it that is used to populate the\nreport. Reports can be of several types (graph, table, bar chart, etc.),\neach of which expect different types of data. See the relevant\ndocumentation pages for more information.\n\nA report can be given a title, which will be displayed in the dashboard\nheader. To change the query of a report, open the settings by clicking\nthe (⋮) icon on the top right of the report.\n\nimage::report.gif[Report]\n\nThe settings window additionally allows you to change the type of\nreport, the refresh rate of the report, and a number of *Advanced\nSettings*. The advanced settings differ between the different report\ntypes, and can be viewed by toggling the switch on the bottom left of\nthe settings page.\n\n== Create and Delete Reports\n\nA new report can be added to a page by clicking the large (+) button at\nthe end of the page. By default, a report will have nothing defined, so\nyou will need to set the query before any data is visualized.\n\nReports can be deleted by opening the report settings, and clicking the\n🗑️ icon in the report header.\n\n== Re-order Reports\n\nAs of NeoDash 2.1, reports can be re-ordered by dragging and dropping\nthem around the page. To move a report, grab it by the handle (top left\ncorner), and drag it to the desired location.\n\nimage::movereport.gif[Report]\n\n== Resize Reports\n\nAs of NeoDash 2.1, reports can be resized by grabbing their bottom-right\ncorner, and dragging your mouse to the desired size.\n\nimage::resizereport.gif[Report]\n\n== Writing Queries\n\nA single Cypher query is used to populate each report. As any Cypher\nsyntax is supported, this includes\nhttps://neo4j.com/developer/neo4j-apoc/[APOC],\nhttps://neo4j.com/docs/graph-data-science/current/[GDS], and even\nhttps://neo4j.com/docs/operations-manual/current/fabric/queries/[Fabric]!\n\nKeep the following best practises in mind when writing your Cypher\nqueries: \n\n1. Always use a `LIMIT` in your query to keep the result size\nmanageable. \n2. Ensure you return the right data types for the right\nreport type. For example, a graph report expects nodes and\nrelationships, whereas a line chart expects numbers.\n\n== Row Limiting\n\nNeoDash has a built-in post-query *row limiter*. This means that results\nare truncated to a maximum number of rows, depending on the report type.\nThe row limiter is in place to ensure that visualizations do not become\ntoo complex for the browser to handle.\n\nNote that even though the row limiter is enabled by default, rows are\nonly limited after the query is executed. For this reason, it is\nrecommended to use the `LIMIT` clause in your query at all times.\n\n== Parameters\n\nParameters can be set in a dashboard by using a link:parameter-select[Parameter Select] report. Set parameters are then available in any Cypher query across the dashboard.\n\nIn addition, **session parameters** are available based on the currently active database connection.\n\n|===\n|Parameter | Description\n| $session_uri | The URI of the current active database connection.\n| $session_database | The Neo4j database that was connected to when the user logged in.\n| $session_username | The username used to authenticate to Neo4j.\n|===\n\n== Report Types\n\nTo learn more about a specific report type, see one of the following\npages: \n\n- link:table[Table] \n- link:graph[Graph]\n- link:bar-chart[Bar Chart]\n- link:pie-chart[Pie Chart] \n- link:line-chart[Line Chart] \n- link:graph3d[3D Graph]\n- link:sunburst[Sunburst]\n- link:circle-packing[Circle Packing] \n- link:treemap[Treemap]\n- link:radar[Radar Chart] \n- link:map[Map]\n- link:choropleth[Choropleth Chart] \n- link:areamap[Area Map] \n- link:single-value[Single Value] \n- link:sankey[Sankey Chart] \n- link:gantt[Gantt Chart] \n- link:gauge[Gauge Chart]\n- link:raw-json[Raw JSON] \n- link:parameter-select[Parameter Select] \n- link:form[Form] \n- link:iframe[iFrame]\n- link:markdown[Markdown]\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/line-chart.adoc",
    "content": "\n= Line Chart\n\ninclude::../../banner.adoc[]\n\nA line chart can be used to draw one or more lines in a two-dimensional\nplane. It requires two numeric fields:\n\n* *X-value*: a numeric field. These will be the values used as an\nX-coordinate.\n* *Y-Value*: a numeric field. These will be the values used as an\nY-coordinate.\n\nAlways ensure that the X-value are sorted in ascending order. If not,\nthe chart will not be displayed correctly.\n\nThe line chart supports plotting both simple numbers and time values on\nthe x-axis. If you select a Neo4j datetime property on the x-axis, the\nchart will automatically be drawn as a time series.\n\n== Examples\n\n=== Basic Line Chart (Actors born by decade)\n\n....\nMATCH (p:Person)\nRETURN  (p.born/10)*10 as Decade, COUNT(p) as People\nORDER BY Decade ASC\n....\n\nimage::line1.png[Basic Line Chart]\n\n=== Multi-Line Chart (Actors born & movies released by decade)\n\n....\nMATCH (p:Person)\nWITH  (p.born/10)*10 as Decade, COUNT(p) as People\nORDER BY Decade ASC\nMATCH (m:Movie)\nWHERE (m.released/10)*10 = Decade\nRETURN Decade, People, COUNT(DISTINCT m) as Movies\n....\n\nimage::line2.png[Multi Line Chart]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"13%,2%,6%,79%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Plot Type |List |line | Whether to use a line plot (with connections) or a scatter plot of disjointed points\n\n|Show Legend |on/off |off |If enabled, shows a legend at the top right\nof the visualization.\n\n|Color Scheme |List |neodash |The color scheme to use for the lines.\nColors are assigned automatically to the different fields selected in\nthe report footer.\n\n|X Scale |List |linear |How to scale the values on the x-axis. Can be\neither linear, logarithmic or point. Use point for categorical data.\n\n|Y Scale |List |linear |How to scale the values on the y-axis. Can be\neither linear or logarithmic.\n\n|Min X Value |Number |auto |If not set to ``auto'', this variable is the\nminimum value on the x-axis.\n\n|Max X Value |Number |auto |If not set to ``auto'', this variable is the\nmaximum value on the x-axis.\n\n|Min Y Value |Number |auto |If not set to ``auto'', this variable is the\nminimum value on the y-axis.\n\n|Max Y Value |Number |auto |If not set to ``auto'', this variable is the\nmaximum value on the y-axis.\n\n|X-axis Tick Count |Number |auto |If not set to ``auto'', the number of\nticks to be made on the x-axis. This is an approximate number that the\nvisualization tries to adhere to (numeric X-axis only)\n\n|X-axis Format (Time chart) |Text |%Y-%m-%dT%H:%M:%SZ |When using a time\nchart, this setting lets you override how time values are rendered on\nthe x-axis. This uses the ISO 8601 time notations.\n\n|X-axis Tick Size (Time chart) |Text |every 1 year |When using a time\nchart, this setting helps you set the frequency of ticks. The text\nformat should look like this:\n`\"every [number] ['years', 'months', 'weeks', 'days', 'hours', 'seconds', 'milliseconds']\"`.\n\n|Line Smoothing |List |linear |Determines how the lines in the chart are\nsmoothened. One of linear (no smoothing), basis (interpolating),\ncardinal (through each point) and step (step-based interpolation).\n\n|X-axis Tick Rotation (Degrees) |number |0 | The angle at which the tick labels on the x-axis are rotated.\n\n|Y-axis Tick Rotation (Degrees) |number |0 | The angle at which the tick labels on the y-axis are rotated.\n\n|Show Grid |on/off |on |If enabled, shows a grid in the line chart that\nintersects at the axis ticks.\n\n|Point Radius (px) |number |10 |The size of a point on each line.\n\n|Line Width (px) |number |2 |The width (in pixels) of each line in the\nchart.\n\n|Margin Left (px) |number |50 |The margin in pixels on the left side of\nthe visualization.\n\n|Margin Right (px) |number |24 |The margin in pixels on the right side\nof the visualization.\n\n|Margin Top (px) |number |24 |The margin in pixels on the top side of\nthe visualization.\n\n|Margin Bottom (px) |number |40 |The margin in pixels on the bottom side\nof the visualization.\n\n|Legend Width (px) |number |128 |The width in pixels of each legend\nlabel on top of the visualization (if enabled).\n\n|Hide Property Selection |on/off |off |If enabled, hides the property\nselector (footer of the visualization).\n\n|Auto-run query |on/off |on |when activated automatically runs the query\nwhen the report is displayed. When set to `off', the query is displayed\nand will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n\n== Rule-Based Styling\n\nUsing the link:../#_rule_based_styling[Rule-Based Styling] menu, the\nfollowing style rules can be applied to the line chart: \n\n- The color of the line.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/map.adoc",
    "content": "\n= Map\n\ninclude::../../banner.adoc[]\n\nThe map report will render all returned nodes, relationships and paths\non a geomap. https://www.openstreetmap.org[Open Street Map] is used to\nvisualize the data on the map.\n\nMap visualizations work best with\nhttps://neo4j.com/docs/cypher-manual/current/syntax/spatial/#cypher-spatial-specifying-spatial-instants[Neo4j\nSpatial Data]. Make sure that the nodes in your database have their\nlocations stored as a spatial property.\n\nCustomizations are available to change several parts of the\nvisualization, including the label for each node as well as the colors\nand sizes of the markers/lines.\nThe nodes can also automatically cluster together and expand depending on the level of zoom. A heatmap mode is also available.\n\n== Examples\n\n=== Nodes on a map\n\n____\nNote that the nodes returned here have\nhttps://neo4j.com/docs/cypher-manual/current/syntax/spatial/[spatial]\nproperties on them, so they can be visualized on a map.\n____\n\n....\nMATCH (b:Brewery)\nRETURN b\n....\n\nimage::map.png[Basic Map]\n\n=== Nodes and relationships on a map\n\n....\nMATCH (b:Brewery)-[e]->(b2:Brewery)\nRETURN b, e, b2\n....\n\nimage::map2.png[Relationships on a Map]\n\n=== Clustered nodes on a map\n\n....\nMATCH (b:Brewery)\nRETURN b\n....\n\nimage::map_cluster.png[Clustered nodes on a map]\n\n=== Heatmap\n\n....\nMATCH (b:Brewery)\nRETURN b\n....\n\nimage::map_heatmap.png[Heatmap]\n\n=== Artificial map data\n\nBy returning a dictionary instead of a node directly, you can work\naround the visualization expecting nodes and relationships directly.\n\n....\nMATCH (l1:Location)<--(a:Person),\n      (a:Person)-[:KNOWS]-(b:Person),\n      (b:Person)-->(l2:Location)\nRETURN {id: a.name, label: \"Person\", point: l1.point},\n       {id: b.name, label: \"Person\", point: l2.point},\n       {start: a.name, end: b.name, type: \"KNOWS\", id: 1}\n....\n\nimage::map3.png[Artificial Map Data]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"19%,17%,26%,38%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Layer Type |List |markers |Allows you to choose between the standard map with markers, or a heatmap.\n|Cluster markers |on/off |off |Whether to automatically cluster and expand the markers on the map.\n|Node Color Scheme |List |neodash |The color scheme to use for the node\nlabels. Colors are assigned automatically (consequitevely) to the\ndifferent labels returned by the Cypher query.\n\n|Node Marker Size |List |large |The size of the markers for the nodes on\nthe map. One of [small, medium, large].\n\n|Node Color Property |Text |color |Optionally, the name of the node\nproperty to map to the node color. This lets you define colors on a\nnode-specific level, if you have a property that directly maps to the\nHTML color value.\n\n|Relationship Color |Text |#a0a0a0 |The color used for drawing the\nrelationships on the map.\n\n|Relationship Width |Text |1 |The (default) width of the relationships\non the map.\n\n|Relationship Color Property |Text |color |Optionally, the name of the\nrelationship property to map to the relationship color. This lets you\ndefine colors on a relationship-specific level, if you have a property\nthat directly maps to the HTML color value.\n\n|Relationship Width Property |Text |width |Optionally, the name of the\nrelationship property to map to the arrow width. This lets you define\nwidths on a relationship-specific level, if you have a property that\ndirectly maps to the width value.\n\n|Map Provider URL|Text|https://\\{s}.tile.openstreetmap.org/\\{z}/\\{x}/\\{y}.png| When specified, overrides Open Street map provider with a custom map tiles provider.\n\n|Intensity Property (for heatmap)|Text|intensity|Optionally, and only for heatmaps, the node property to use as the intensity of that point on the heatmap. If left empty, all points will have the same intensity of 1. If one of the nodes in the results doesn't have the specific property, its intensity will be set to 0.\n\n|Hide Property Selection |on/off |off |If enabled, hides the property\nselector (footer of the visualization).\n\n|Auto-run query |on/off |on |when activated automatically runs the query\nwhen the report is displayed. When set to `off', the query is displayed\nand will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n\n== Rule-Based Styling\n\nUsing the link:../#_rule_based_styling[Rule-Based Styling] menu, the\nfollowing style rules can be applied to the map: \n\n- The color of a node marker.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/markdown.adoc",
    "content": "\n= Markdown\n\ninclude::../../banner.adoc[]\n\nMarkdown reports let you specify\nhttps://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#styling-text[Markdown]\ntext, to be renderer as rich HTML. This lets you turn your dashboards\ninto a storybook with textual descriptions, hyperlinks, images and\nvideos.\n\nTo use dashboard parameters in Markdown, turn on the `Replace global parameters in Markdown` setting.\nThen, include a variable surrounded by backticks inside the markdown string. For example:\n\n```\n== This is a title\nMy variable is equal to `$neodash_person_object['name']`\n```\n\n\n== Examples\n\n=== Basic Markdown\n\n....\n## Hello there!\nI'm a **Markdown** file.\n\nCheck out this cool image:\n\n![image](https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/how-to-keep-ducks-call-ducks-1615457181.jpg?resize=240:*)\n....\n\nimage::markdown.png[Basic Markdown]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"19%,17%,26%,38%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Replace global parameters in Markdown |on/off |on |If enabled, replaces\nall instances of query parameters (e.g. $neodash_person_name) inside the\nmarkdown source.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/parameter-select.adoc",
    "content": "\n= Parameter Select\n\ninclude::../../banner.adoc[]\n\nParameter select reports provide you with an easy way to add\ninteractivity into your dashboards.\n\nSimply put, a parameter select report lets users set a Neo4j query\nparameter (e.g. *$neodash_person_name*) dynamically. This means that\nyour reports can be created to show different data depending on the\nvalue of a parameter.\n\nThere are five types of parameter select reports: \n\n- Node property-based selections \n- Relationship property-based selections \n- Free text selections\n- Date picker selections\n- Custom query selections\n\n\n== Examples\n\n=== Node Property Select\nA node property selector lets users choose a property from a node with a given label, to be used as a parameter in the dashboard.\n\nimage::select.png[Node Property Select]\n\n=== Relationship Property Select\nA relationship property selector lets users choose a property from a relationship with a given type, to be used as a parameter in the dashboard.\n\nimage::select2.png[Relationship Property Select]\n\n=== Free Text Select\nA free text selectors lets users enter any string value, which can then be used as a parameter inside dashboard queries.\n\nimage::select3.png[Free Text Select]\n\n== Date Select\nA date selector lets users specify dates using a calendar widget, or by entering a date format.\n\nimage::select4.png[Free Text Select]\n\n== Custom Query Select\nA custom query selectors lets you specify a custom selector widget, where user suggestions are populated based on an `$input` variable that is passed down into your custom query.\n\nimage::select5.png[Custom Query Select]\n\n\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"19%,17%,26%,38%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Clear Parameter on Field Reset |on/off |off |If enabled, removes the\nglobal parameter completely when the field is cleared. This may break\nsome visualizations. If disabled, sets the parameter value to “” (empty\nstring) when the input field is cleared.\n\n|Multiple Selection |on/off |off |If enabled, allows user to select multiple choices. Parameter will be then an array of selections.\n\n|Manual Parameter Save |on/off |off |If enabled, adds a confirmation button in order to propagate the selection into the dashboard parameter.\n\n|Enable Manual Label/Property Name Specification |on/off |off |If\nenabled, does not enforce you to select a node label/property using an\nauto-complete field, instead, you can enter any value. This is useful\nfor large datasets where the autocomplete field is too slow to render.\n\n|Helper Text (Override) |Text |(none) |Text to show above the user input\nfield. This will override the autogenerated text from the\nnode/relationship property pair.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/pie-chart.adoc",
    "content": "\n= Pie Chart\n\ninclude::../../banner.adoc[]\n\nA pie chart will draw categories and values in a circular disc layout.\nThe pie chart will require you to choose the following selections:\n\n* *Category*: a text field. These will be the labels on the pie slices.\n* *Value*: a numeric field. This will be the size of the slices.\n\nThe pie chart can be additionally be customized to become a donut chart,\nshow categories as a legend, and to show the percentage of the total\nvalue of the pie. See *Advanced Settings* for more information.\n\n== Examples\n\n=== Basic Pie Chart\n\n[source,cypher]\n----\nMatch (p:Person)-[e]->(m:Movie)\nRETURN m.title as Title, COUNT(p) as People\nLIMIT 8\n----\n\nimage::pie.png[Pie Chart]\n\n=== Donut Chart\n\n[source,cypher]\n----\nMATCH (p:Person)-[e]->(m:Movie)\nWHERE m.title = \"Cloud Atlas\"\nWITH TYPE(e) as Role\nRETURN Role, COUNT(Role) as Count\n----\n\nimage::piedonut.png[Donut Chart]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"15%,2%,6%,77%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Show Legend |on/off |off |If enabled, shows a legend on the bottom of\nthe visualization.\n\n|Auto-sort slices by value |on/off |off |If enabled, automatically sorts\nthe pie slices in order of size.\n\n|Show Values in Slices |on/off |off |If enabled, show the category\nvalues inside the pie slices.\n\n|Labels font Size |Number |13 |Define the size of the font for internal and external labels on the pie.\n\n|Show categories next to Slices |on/off |off |If enabled, show the\ncategory values next to the pie slices.\n\n|Enable interactivity |on/off |on |If enabled, turn on animations when a\nuser hovers over a pie slice.\n\n|Color Scheme |List | |The color scheme to use for the slices. Colors\nare assigned automatically (consequitevely) to the different categories\nreturned by the Cypher query.\n\n|Pie inner radius |Number |0 |The radius of the ``donut hole'' inside\nthe pie. When set to zero, no hole is present, when set to 0.99, the pie\nwill be almost completely visualized as a thin disc.\n\n|Slice padding angle (degrees) |number |0 |the angle between each pie\nslice reserved for white space. For example, when set to 3.6, there will\nbe 1/100th of the total circle space reserved between each slice.\n\n|Slice border with (px) |number |0 |The width of the border of each\nslice.\n\n|Margin Left (px) |number |50 |The margin in pixels on the left side of\nthe visualization.\n\n|Margin Right (px) |number |50 |The margin in pixels on the right side\nof the visualization.\n\n|Margin Top (px) |number |50 |The margin in pixels on the top side of\nthe visualization.\n\n|Margin Bottom (px) |number |50 |The margin in pixels on the bottom side\nof the visualization.\n\n|Hide Selections |on/off |off |If enabled, hides the property selector\n(footer of the visualization).\n\n|Auto-run query |on/off |on |when activated automatically runs the query\nwhen the report is displayed. When set to `off', the query is displayed\nand will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n\n== Rule-Based Styling\n\nUsing the link:../#_rule_based_styling[Rule-Based Styling] menu, the\nfollowing style rules can be applied to the pie chart:\n\n- The background color of a pie slice.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/radar.adoc",
    "content": "\n= Radar Chart\n\ninclude::../../banner.adoc[]\n\nlink:../../extensions/advanced-visualizations[label:Advanced&nbsp;Visualization[]]\n\nA Radar chart can be used to render multivariate data from an array of nodes\ninto the form of a two dimensional chart of three or more quantitative variables.\n\nA radar chart expects a single index, to which a list of numeric fields can be linked.\n\n== Examples\n\n=== Basic Radar\n\n[source,cypher]\n----\nMATCH (s:Skill)\nMATCH (:Player{name:\"Messi\"})-[h1:HAS_SKILL]->(s)\nMATCH (:Player{name:\"Mbappe\"})-[h2:HAS_SKILL]->(s)\nMATCH (:Player{name:\"Benzema\"})-[h3:HAS_SKILL]->(s)\nMATCH (:Player{name:\"C Ronaldo\"})-[h4:HAS_SKILL]->(s)\nMATCH (:Player{name:\"Lewandowski\"})-[h5:HAS_SKILL]->(s)\nRETURN s.name as Skill, h1.value as Messi, h2.value as Mbappe, h3.value as Benzema,\n                        h4.value as `C Ronaldo`, h5.value as Lewandowski\n----\n\nimage::radar.png[Radar Chart]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"15%,2%,26%,57%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Enable interactivity |on/off |on |If enabled, turn on animations when a\nuser hovers over a layer.\n\n|Show Legend |on/off |off |If enabled, shows a legend on the bottom of\nthe visualization.\n\n|Color Scheme |List | |The color scheme to use for the Radar. Each polygon will have a color from the list.\n\n|Margin Left (px) |number |24 |The margin in pixels on the left side of\nthe visualization.\n\n|Margin Right (px) |number |24 |The margin in pixels on the right side\nof the visualization.\n\n|Margin Top (px) |number |24 |The margin in pixels on the top side of\nthe visualization.\n\n|Margin Bottom (px) |number |40 |The margin in pixels on the bottom side\nof the visualization.\n\n|Dot Size |number |10 |Size of the dots (px).\n\n|Dot Border Width |number |2 |Width of the dots border (px).\n\n|Grid Levels |number |5 |Number of levels to display for grid.\n\n|Grid Label Offset (px) |number |16 |Label offset from outer radius (px)\n\n|Blend Mode |List |normal |This will define CSS mix-blend-mode for layers\n\n|Motion Configuration |List |gentle |This parameter will select the motion config for react-spring.\n\n|Curve |List |linearClosed |This parameter will select the type of curve interpolation.\n\n|Auto-run query |on/off |on |When activated, automatically runs the\nquery when the report is displayed. When set to `off', the query is\ndisplayed and will need to be executed manually.\n|===\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/raw-json.adoc",
    "content": "\n= Raw JSON\n\ninclude::../../banner.adoc[]\n\nThe Raw JSON report renders the JSON response received from Neo4j\ndirectly. This is typically used for debugging queries, or,\nunderstanding the exact data types being returned from Neo4j.\n\n== Examples\n\n=== Raw JSON\n\n....\nMATCH (n)\nRETURN COUNT(n)\n....\n\nimage::json.png[Basic Value]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"19%,17%,26%,38%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Auto-run query |on/off |on |when activated automatically runs the query\nwhen the report is displayed. When set to `off', the query is displayed\nand will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/sankey.adoc",
    "content": "\n= Sankey Chart\n\ninclude::../../banner.adoc[]\n\nlink:../../extensions/advanced-visualizations[label:Advanced&nbsp;Visualization[]]\n\nA Sankey visualization will generate a flow diagram from nodes and links.\nBeware that cyclic dependencies are not supported.\n\n== Examples\n\n=== Basic Sankey Chart\nFor a sankey chart to use the correct relationship weights - it is mandatory to set a 'Relationship Property' in the report's advanced settings.\n\n[source,cypher]\n----\nMATCH (p:Person)-[r:RATES]->(m:Movie)\nRETURN p, r, m\n----\n\nimage::sankey.png[Sankey Chart]\n\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"15%,2%,6%,77%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Show Legend |on/off |off |If enabled, shows a legend on the bottom of\nthe visualization.\n\n|Enable interactivity |on/off |on |If enabled, turn on animations when a\nuser hovers over a node or link.\n\n|Relationship Property |text | value | Name of the property with an integer value that is going\nto be used to determine thickness of links. Using 'SANKEY_UNWEIGHTED', this sankey will assume\nevery relationship with a weight of 1.\n\n|Color Scheme |List | |The color scheme to use for the slices. Colors\nare assigned automatically (consecutively) to the different categories\nreturned by the Cypher query.\n\n|Layout |List |horizontal |Sankey layout direction.\n\n|Label Position |List |inside |Control sankey label position.\n\n|Label Orientation |List |horizontal |Control sankey label orientation.\n\n|Node Border Width (px) |number |0 |Controls Node border width.\n\n|Node Spacing (px) |number |18 |Controls spacing between nodes at an identical level (px).\n\n|Node thickness (px) |number |18 |Controls Node thickness.\n\n|Margin Left (px) |number |24 |The margin in pixels on the left side of\nthe visualization.\n\n|Margin Right (px) |number |24 |The margin in pixels on the right side\nof the visualization.\n\n|Margin Top (px) |number |24 |The margin in pixels on the top side of\nthe visualization.\n\n|Margin Bottom (px) |number |40 |The margin in pixels on the bottom side\nof the visualization.\n\n|Auto-run query |on/off |on |when activated automatically runs the query\nwhen the report is displayed. When set to `off', the query is displayed\nand will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n\n\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/single-value.adoc",
    "content": "\n= Single Value\n\ninclude::../../banner.adoc[]\n\nA single value report will render the first column of the first row\nreturned by the Cypher query. Single value reports are typically used\nfor key metrics: \n\n- The total number of nodes \n- The total number of data integrity violations \n- The name of a node or relationship that is standing out in the data.\n\n== Examples\n\n=== Number value\n\n....\nMATCH (n)\nRETURN COUNT(n)\n....\n\nimage::value.png[Basic Value]\n\n=== Text value with custom styling\n\n....\n// Who's the biggest Fraudster?\nMATCH (n:Person)-[:CREATED]->(t:Transaction{fraud:true})\nRETURN n.name, COUNT(t)\nORDER BY COUNT(t) DESC\n....\n\nimage::value2.png[Styled Value]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"10%,3%,29%,58%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Font Size |Number |64 |The font size of the value text.\n\n|Color |Text |rgba(0, 0, 0, 0.87) |The HTML color value of the text.\n|Background Color |Text | white |The HTML color value of the background of the report.\n|Horizontal Align |List |left |The horizontal alignment of the text.\n\n|Vertical Align |List |top |The vertical alignment of the text.\n\n|Auto-run query |on/off |on |when activated automatically runs the query\nwhen the report is displayed. When set to `off', the query is displayed\nand will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n\n== Rule-Based Styling\n\nUsing the link:../#_rule_based_styling[Rule-Based Styling] menu, the\nfollowing style rules can be applied to the map: \n\n- The color of the text.\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/sunburst.adoc",
    "content": "\n= Sunburst\n\ninclude::../../banner.adoc[]\n\nlink:../../extensions/advanced-visualizations[label:Advanced&nbsp;Visualization[]]\n\nA sunburst chart will render hierarchical data in a multi-level pie\nvisualization. It takes two fields: \n\n- *Path*: a list of strings. This represents the hierarchy (from highest to lowest level). \n- *Value*: a number that matches the size of the element at the lowest level. Sizes of non-leaf levels are determined from the sum of their children.\n\n== Examples\n\n=== Basic Sunburst Chart\n\n[source,cypher]\n----\nMATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department)\nWITH nodes(path) as no\nWITH no, last(no) as leaf\nWITH  [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val\nRETURN result, val\n----\n\nimage::sunburst.png[Sunburst Chart]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"19%,2%,26%,53%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Show Values on Arcs |on/off |off |If enabled, show the category values\ninside the sunburst arcs.\n\n|Enable interactivity |on/off |on |If enabled, turn on animations when a\nuser hovers over an arc.\n\n|Color Scheme |List | |The color scheme to use for the arcs. Colors are\nassigned automatically for each of the sub-hierarchies.\n\n|Arc border width (px) |number |0 |The width of the border of each arc.\n\n|Margin Left (px) |number |24 |The margin in pixels on the left side of\nthe visualization.\n\n|Margin Right (px) |number |24 |The margin in pixels on the right side\nof the visualization.\n\n|Margin Top (px) |number |24 |The margin in pixels on the top side of\nthe visualization.\n\n|Margin Bottom (px) |number |40 |The margin in pixels on the bottom side\nof the visualization.\n\n|Minimum Arc Angle for Label (degrees) |number |10 |The minimum angle of\nan arc needed to display a label (if labels are enabled).\n\n|Slice Corner Radius |number |3 |The rounding angle of each of the arcs\nin the visualization.\n\n|Inherit color from parent |on/off |on |If enabled, starting from level 2, each\nlevel will inherit the same color of his parent. If disabled, color will be randomly\nassigned based on the color scheme.\n\n|Auto-run query |on/off |on |When activated, automatically runs the\nquery when the report is displayed. When set to `off', the query is\ndisplayed and will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/table.adoc",
    "content": "\n= Table\n\ninclude::../../banner.adoc[]\n\nThe most common report in a dashboard is often a simple table view.\nNeoDash contains a powerful table component that can render all the data\nreturned by a Cypher query. This includes simple data like numbers or\ntext, but also Neo4j native data like nodes, relationships, and paths.\n\nThe table report supports the following additional features: \n\n- automatic pagination of results. \n- Sorting/filtering by clicking on the table headers. \n- Prefixing a column header with __ (double underscore) will make the column hidden\n- Downloading your data as a CSV file.\n\nDouble-clicking on a table cell will copy that cell's value to the user's clipboard.\n\n== Examples\n\n=== Basic Table\n\n....\nMATCH (n:Movie)<-[:ACTED_IN]-(p:Person)\nRETURN n.title AS Title, n.released AS Released, count(p) as Actors\n....\n\nimage::table1.png[Basic Table]\n\n=== Table with nodes / collections\n\n....\nMATCH (n:Movie)<-[:ACTED_IN]-(p:Person)\nRETURN n, collect(p.name) as actors LIMIT 200\n....\n\nimage::table2.png[Table with nodes / collections]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"12%,6%,26%,56%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Transpose Rows & Columns |on/off |off |when activated, transposes the\nrows and columns of the table. This means that each of the returned rows\nfrom Neo4j will be shown as a column instead of a row.\n\n|Compact Table |on/off |off |When activated, makes the rows half height and increase the number of rows per page accordingly.\n\n|Relative Column Sizes |List of numbers |[1, 1, 1, …] |The relative\nwidth between each of the columns in the table. For example, if the\nfirst column should be twice the width of the 2nd and 3rd, this will be\nset to ``[2, 1, 1]''.\n\n|Enable CSV Download |on/off |off |when activated, displays a button on\nthe bottom right of the table footer. This button lets the user download\nthe complete set of table results (all pages) as a CSV file.\n\n|Override no data message |Text |Query returned no data. |Override the message displayed to the user when their query returns no data.\n\n|Auto-run query |on/off |on |when activated automatically runs the query\nwhen the report is displayed. When set to `off', the query is displayed\nand will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n\n== Rule-Based Styling\n\nUsing the link:../#_rule_based_styling[Rule-Based Styling] menu, the\nfollowing style rules can be applied to the table: \n\n- The background color of an entire row in a table. \n- The text color of an entire row in a table. \n- The background color of a single cell in the table. \n- The text color of a single cell in the table.\n\nIf a column is hidden (header prefixed with __ double underscore), it can still be used as an entry point for a styling rule.\n\n== Report Actions\n\nWith the link:../../extensions/report-actions[Report Actions] extension, tables can be turned into interactive components that set parameters.\nTwo flavours of report actions for tables exist:\n\n=== 1. Select a value from a row\nAdding a **Cell Click** action to a table column, turns the values in that row into clickable buttons.\nWhen the user clicks on the button, a predefined parameter is set to one of the columns in that row.\n\nimage::select-single-table.png[Select a value from a table to be used as a parameter]\n\n=== 2. Select multiple from a row\nAdding a **Row Clicked** action to a table prepends each row with a checkbox.\nThe user can then check one or more boxes to update a dashboard parameter.\n\n> Keep in mind that regardless if one or more values are selected, the type of the dashboard parameter is a list of values. The queries using the parameter must ensure that the list type is handled correctly.\n\nimage::select-multiple-table.png[Select multiple values to be used as a parameter]"
  },
  {
    "path": "docs/modules/ROOT/pages/user-guide/reports/treemap.adoc",
    "content": "\n= Treemap\n\ninclude::../../banner.adoc[]\n\nlink:../../extensions/advanced-visualizations[label:Advanced&nbsp;Visualization[]]\n\nA treemap chart will render hierarchical data in a nested rectangle\nlayout. It takes two fields: \n\n- *Path*: a list of strings. This represents the hierarchy (from highest to lowest level). \n- *Value*: a number that matches the size of the element at the lowest level. Sizes of non-leaf levels are determined from the sum of their children.\n\n== Examples\n\n=== Basic Treemap\n\n[source,cypher]\n----\nMATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department)\nWITH nodes(path) as no\nWITH no, last(no) as leaf\nWITH  [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val\nRETURN result, val\n----\n\nimage::treemap.png[Treemap Chart]\n\n== Advanced Settings\n\n[width=\"100%\",cols=\"15%,2%,26%,57%\",options=\"header\",]\n|===\n|Name |Type |Default Value |Description\n|Enable interactivity |on/off |on |If enabled, turn on animations when a\nuser hovers over a rectangle.\n\n|Color Scheme |List | |The color scheme to use for the rectangle. Colors\nare assigned automatically for each of the sub-hierarchies.\n\n|Rectangle border width (px) |number |0 |The width of the border of each\nrectangle.\n\n|Margin Left (px) |number |24 |The margin in pixels on the left side of\nthe visualization.\n\n|Margin Right (px) |number |24 |The margin in pixels on the right side\nof the visualization.\n\n|Margin Top (px) |number |24 |The margin in pixels on the top side of\nthe visualization.\n\n|Margin Bottom (px) |number |40 |The margin in pixels on the bottom side\nof the visualization.\n\n|Auto-run query |on/off |on |When activated, automatically runs the\nquery when the report is displayed. When set to `off', the query is\ndisplayed and will need to be executed manually.\n|Report Description |markdown text | | When specified, adds another button the report header that opens a pop-up. This pop-up contains the rendered markdown from this setting. \n|===\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n    \"name\": \"docs\",\n    \"version\": \"1.0.0\",\n    \"description\": \"\",\n    \"main\": \"server.js\",\n    \"scripts\": {\n      \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n      \"start\": \"npm run dev\",\n      \"dev\": \"node server.js & npm-watch preview\",\n      \"preview\": \"antora preview.yml\",\n      \"publish\": \"git push origin HEAD:publish\"\n    },\n    \"watch\": {\n      \"preview\": {\n        \"patterns\": [\n          \"modules\"\n        ],\n        \"extensions\": \"adoc\"\n      }\n    },\n    \"dependencies\": {\n      \"@antora/cli\": \"^3.1.1\",\n      \"@antora/site-generator-default\": \"^3.1.1\",\n      \"@neo4j-documentation/macros\": \"^1.0.0\",\n      \"@neo4j-documentation/remote-include\": \"^1.0.0\",\n      \"express\": \"^4.17.1\",\n      \"npm-watch\": \"^0.11.0\"\n    }\n  }"
  },
  {
    "path": "docs/preview.yml",
    "content": "site:\n  title: NeoDash\n\ncontent:\n  sources:\n  - url: ../\n    start_path: docs\n    branches: HEAD\n    exclude:\n    - '!**/_includes/*'\n    - '!**/readme.adoc'\n    - '!**/README.adoc'\nui:\n  bundle:\n    url: https://static-content.neo4j.com/build/ui-bundle-latest.zip\n    snapshot: true\nurls:\n  html_extension_style: indexify\nasciidoc:\n  extensions:\n  - \"@neo4j-documentation/remote-include\"\n  - \"@neo4j-documentation/macros\"\n  attributes:\n    page-theme: labs"
  },
  {
    "path": "docs/server.js",
    "content": "const express = require('express')\n\nconst app = express()\nconst version = \"2.4\"\napp.use(express.static('./build/site'))\napp.get('/', (req, res) => res.redirect('neodash/' + version))\napp.listen(8000, () => console.log('📘 http://localhost:8000'))"
  },
  {
    "path": "gallery/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "gallery/LICENSE",
    "content": "     Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2023 Niels de Jong\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "gallery/README.md",
    "content": "# NeoDash Dashboard Gallery 🎨\nThis is the source code for the NeoDash dashboard gallery located at [https://neodash-gallery.graphapp.io](https://neodash-gallery.graphapp.io).\n\n## Run the app locally\nThis app is built with React, Tailwind and the Neo4j Design Language.\n\n- `yarn install` installs the dependencies.\n- `yarn start` runs the app in development mode.\n- `yarn build` builds the app for production.\n\n## Contribute to the Gallery\nWant to add a dashboard to the gallery?\nCreate an [issue on GitHub](https://github.com/neo4j-labs/neodash/issues) with the following information:\n- Your name.\n- A URL to your page (GitHub, LinkedIn, Personal Website, ...)\n- The name of your dashboard.\n- A one sentence description for the dashboard.\n- A screenshot of the main page.\n- A data dump of the Neo4j database populating the dashboard.\n- A list of 5 keywords.\n\nKeep in mind that the data you provide needs to be public data, as it will be accessible by anyone.\n"
  },
  {
    "path": "gallery/dashboards/assessment.json",
    "content": "{\n  \"title\": \"Graph Assessment\",\n  \"version\": \"2.1\",\n  \"settings\": {\n    \"pagenumber\": 1,\n    \"editable\": true,\n    \"fullscreenEnabled\": false,\n    \"parameters\": {\n      \"neodash_customer_name\": \"Black Mesa\"\n    },\n    \"extensions\": [\"core\", \"actions\"]\n  },\n  \"pages\": [\n    {\n      \"title\": \"Main Page\",\n      \"reports\": [\n        {\n          \"title\": \"Neo4j Graph Framework\",\n          \"query\": \"https://dist.neo4j.com/wp-content/uploads/20220812112237/FrameworkBrighter.jpg\\n\\n\",\n          \"width\": 6,\n          \"height\": 3,\n          \"x\": 6,\n          \"y\": 0,\n          \"type\": \"iframe\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Neo4j Graph Framework\",\n          \"query\": \"**The Neo4j Graph Framework is the single trusted reference for any organization to act upon to ensure all areas are addressed throughout the lifecycle of the solution development or project implementation.**\\n\\nBroken down into five key pillars, the framework covers aspects from the initial use case assessment to scaling and expanding the solution (and the adoption of Graph technologies) with new data and business requirements.\\n\\n### The Five Core Pillars\\n\\n**Use Case Assessment:** Understand the business requirements and goals of the solution. Who are the stakeholders and users and what are their goals?\\n\\n**Graph Readiness:** Review, advise, and design the overall solution architecture, ensuring the best possible outcomes are achieved.\\n\\n**Graph Development:** Review, advise, and design the detailed technical architecture required to meet the goals of the solution.\\n\\n**Graph Operations:** Review, advise, and design the deployment and monitoring environment for the solution.\\n\\n**Graph Scale and Expand:** Advise and consult on the full adoption and expansion of the solution with users and stakeholders.\\n\\n---\\n\\nYou can learn more about the Neo4j Graph Framework and project assessment [here](https://neo4j.com/blog/neo4j-graph-framework-project-assessment/), or contact your Customer Success Manager or Professional Services Engagement Manager.\\n\",\n          \"width\": 6,\n          \"height\": 3,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"text\",\n          \"selection\": {},\n          \"settings\": {}\n        }\n      ]\n    },\n    {\n      \"title\": \"Customer Summary\",\n      \"reports\": [\n        {\n          \"title\": \"Select Customer\",\n          \"query\": \"MATCH (n:`Customer`) \\nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`name` as value ORDER BY size(toString(value)) ASC LIMIT 5\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 5,\n          \"y\": 0,\n          \"type\": \"select\",\n          \"selection\": {},\n          \"settings\": {\n            \"type\": \"Node Property\",\n            \"entityType\": \"Customer\",\n            \"propertyType\": \"name\",\n            \"parameterName\": \"neodash_customer_name\"\n          }\n        },\n        {\n          \"title\": \"Overall Summary\",\n          \"query\": \"MATCH (c:Customer)-[:HAS]->(pr:Project)<-[:ASSOCIATED]-(pa:ProjectAssessment)-[r]-(t:Topic)--(p:Pillar)\\nwhere c.name=$neodash_customer_name\\nreturn p.name as pillar,AVG(r.current) as current,AVG(r.target) as target\\n\\n\\n\\n\",\n          \"width\": 4,\n          \"height\": 2,\n          \"x\": 8,\n          \"y\": 0,\n          \"type\": \"radar\",\n          \"selection\": {\n            \"index\": \"pillar\",\n            \"values\": [\"current\", \"target\"]\n          },\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Use Case Assessment\",\n          \"query\": \"MATCH (c:Customer)-[:HAS]->(pr:Project)<-[:ASSOCIATED]-(n:ProjectAssessment)-[r]-(t:Topic)--(p:Pillar)\\nWHERE c.name=$neodash_customer_name AND p.name='Use Case Assessment'\\nreturn t.name as Topic,AVG(r.current) as current,AVG(r.target) as target\\n\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 2,\n          \"type\": \"radar\",\n          \"selection\": {\n            \"index\": \"Topic\",\n            \"values\": [\"current\", \"target\"]\n          },\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Operations\",\n          \"query\": \"MATCH (c:Customer)-[:HAS]->(pr:Project)<-[:ASSOCIATED]-(n:ProjectAssessment)-[r]-(t:Topic)--(p:Pillar)\\nWHERE c.name=$neodash_customer_name AND p.name='Operations'\\nreturn t.name as Topic,AVG(r.current) as current,AVG(r.target) as target\\n\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 6,\n          \"y\": 2,\n          \"type\": \"radar\",\n          \"selection\": {\n            \"index\": \"Topic\",\n            \"values\": [\"current\", \"target\"]\n          },\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Graph Readiness\",\n          \"query\": \"MATCH (c:Customer)-[:HAS]->(pr:Project)<-[:ASSOCIATED]-(n:ProjectAssessment)-[r]-(t:Topic)--(p:Pillar)\\nWHERE c.name=$neodash_customer_name AND p.name='Graph Readiness'\\nreturn t.name as Topic,AVG(r.current) as current,AVG(r.target) as target\\n\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 6,\n          \"y\": 4,\n          \"type\": \"radar\",\n          \"selection\": {\n            \"index\": \"Topic\",\n            \"values\": [\"current\", \"target\"]\n          },\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Number of responses\",\n          \"query\": \"MATCH (c:Customer)-[:HAS]->(pr:Project)<-[r:ASSOCIATED]-(pa:ProjectAssessment)\\nwhere c.name=$neodash_customer_name\\nreturn count (r)\\n\\n\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 5,\n          \"y\": 1,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Customer List\",\n          \"query\": \"match (c:Customer)-[:HAS]->(p:Project)\\nreturn c.name as Customer, p.name as Project\\norder by c.name\\n\\n\\n\\n\",\n          \"width\": 5,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {\n            \"actionsRules\": [\n              {\n                \"condition\": \"Click\",\n                \"field\": \"Customer\",\n                \"value\": \"Customer\",\n                \"customization\": \"set variable\",\n                \"customizationValue\": \"customer_name\"\n              }\n            ]\n          }\n        },\n        {\n          \"title\": \"Graph Development\",\n          \"query\": \"MATCH (c:Customer)-[:HAS]->(pr:Project)<-[:ASSOCIATED]-(n:ProjectAssessment)-[r]-(t:Topic)--(p:Pillar)\\nWHERE c.name=$neodash_customer_name AND p.name='Graph Development'\\nreturn t.name as Topic,AVG(r.current) as current,AVG(r.target) as target\\n\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 4,\n          \"type\": \"radar\",\n          \"selection\": {\n            \"index\": \"Topic\",\n            \"values\": [\"current\", \"target\"]\n          },\n          \"settings\": {}\n        }\n      ]\n    }\n  ],\n  \"parameters\": {}\n}\n"
  },
  {
    "path": "gallery/dashboards/bom-english.json",
    "content": "{\n    \"title\": \"BOM - Bill of Material\",\n    \"version\": \"2.1\",\n    \"settings\": {\n      \"pagenumber\": 0,\n      \"editable\": true,\n      \"fullscreenEnabled\": true,\n      \"parameters\": {\n        \"neodash_supplier_name\": \"Audio Wizardry\",\n        \"neodash_model_number\": null,\n        \"neodash_model_name\": \"EveryRoad GPS Car Navigation Unit - Model 300 - US Edition\",\n        \"neodash_model_name_1\": \"EveryRoad GPS Car Navigation Unit - Model 300 - US Edition\",\n        \"neodash_model_name_2\": \"EveryRoad GPS Car Navigation Unit - Model 500 - UK Edition\"\n      }\n    },\n    \"pages\": [\n      {\n        \"title\": \"Suppliers\",\n        \"reports\": [\n          {\n            \"x\": 0,\n            \"y\": 0,\n            \"title\": \"Data model\",\n            \"query\": \"CALL db.schema.visualization()\\nYIELD nodes, relationships\\nWITH [x IN nodes WHERE NOT apoc.node.labels(x)[0] CONTAINS \\\"_\\\"] AS nodes, [r IN relationships WHERE NOT type(r) ='SIMILAR'] AS rels\\nRETURN *;\\n\\n\\n\",\n            \"width\": \"6\",\n            \"type\": \"graph\",\n            \"height\": 2,\n            \"selection\": {\n              \"Model\": \"name\",\n              \"Supplier\": \"name\",\n              \"Component\": \"name\"\n            },\n            \"settings\": {\n              \"nodePositions\": {}\n            }\n          },\n          {\n            \"x\": 6,\n            \"y\": 0,\n            \"title\": \"Suppliers\",\n            \"query\": \"MATCH (s:Supplier) RETURN s.name AS `supplier name`\\n\\n\\n\",\n            \"width\": 3,\n            \"type\": \"table\",\n            \"height\": 2,\n            \"selection\": {},\n            \"settings\": {\n              \"nodePositions\": {}\n            }\n          },\n          {\n            \"x\": 9,\n            \"y\": 0,\n            \"title\": \"Pick a supplier\",\n            \"query\": \"MATCH (n:`Supplier`) \\nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`name` as value LIMIT 5\",\n            \"width\": \"3\",\n            \"type\": \"select\",\n            \"height\": 2,\n            \"selection\": {},\n            \"settings\": {\n              \"type\": \"Node Property\",\n              \"entityType\": \"Supplier\",\n              \"propertyType\": \"name\",\n              \"parameterName\": \"neodash_supplier_name\"\n            }\n          },\n          {\n            \"x\": 0,\n            \"y\": 2,\n            \"title\": \"Model connected to supplier\",\n            \"query\": \"MATCH p=(m:Model)-[*]->(s:Supplier {name: $neodash_supplier_name}) return p\\n\\n\\n\",\n            \"width\": \"12\",\n            \"type\": \"graph\",\n            \"height\": 2,\n            \"selection\": {\n              \"Model\": \"name\",\n              \"Component\": \"name\",\n              \"Supplier\": \"name\"\n            },\n            \"settings\": {\n              \"nodePositions\": {}\n            }\n          }\n        ]\n      },\n      {\n        \"title\": \"BOM models\",\n        \"reports\": [\n          {\n            \"x\": 0,\n            \"y\": 0,\n            \"title\": \"Models\",\n            \"query\": \"MATCH (m:Model) RETURN m.number AS ID, m.name AS name\\n\\n\\n\",\n            \"width\": \"6\",\n            \"type\": \"table\",\n            \"height\": 2,\n            \"selection\": {},\n            \"settings\": {\n              \"nodePositions\": {}\n            }\n          },\n          {\n            \"x\": 6,\n            \"y\": 0,\n            \"title\": \"Pick a model\",\n            \"query\": \"MATCH (n:`Model`) \\nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`name` as value LIMIT 5\",\n            \"width\": \"6\",\n            \"type\": \"select\",\n            \"height\": 2,\n            \"selection\": {},\n            \"settings\": {\n              \"nodePositions\": {},\n              \"type\": \"Node Property\",\n              \"entityType\": \"Model\",\n              \"propertyType\": \"name\",\n              \"parameterName\": \"neodash_model_name\"\n            }\n          },\n          {\n            \"x\": 0,\n            \"y\": 2,\n            \"title\": \"BOM\",\n            \"query\": \"MATCH path = (m:Model {name: $neodash_model_name})-[:HAS*]->(:Component)\\nRETURN path\\n\\n\",\n            \"width\": \"6\",\n            \"type\": \"graph\",\n            \"height\": 2,\n            \"selection\": {\n              \"Model\": \"name\",\n              \"Component\": \"number\"\n            },\n            \"settings\": {\n              \"nodePositions\": {},\n              \"nodeColorScheme\": \"neodash\",\n              \"layout\": \"tree\"\n            }\n          },\n          {\n            \"x\": 6,\n            \"y\": 2,\n            \"title\": \"Prices\",\n            \"query\": \"MATCH path = (m:Model {name: $neodash_model_name})-[:HAS*]->(c:Component)\\nWITH c.name AS name, toFloat(c.price) AS price, reduce(acc = 1, qty IN [r IN relationships(path)| toInteger(r.count)] | acc * qty) AS quantity\\nRETURN name, round(price, 2) AS price, quantity, round(price*quantity, 2) AS total_price\\nORDER BY total_price DESC\",\n            \"width\": \"6\",\n            \"type\": \"table\",\n            \"height\": 2,\n            \"selection\": {},\n            \"settings\": {\n              \"nodePositions\": {}\n            }\n          }\n        ]\n      },\n      {\n        \"title\": \" Model vs Model\",\n        \"reports\": [\n          {\n            \"x\": 0,\n            \"y\": 0,\n            \"title\": \"Models List\",\n            \"query\": \"MATCH (m:Model) RETURN m.name AS name\",\n            \"width\": 7,\n            \"type\": \"table\",\n            \"height\": 2,\n            \"selection\": {},\n            \"settings\": {\n              \"type\": \"Node Property\",\n              \"entityType\": \"Model\",\n              \"propertyType\": \"name\",\n              \"parameterName\": \"neodash_model_name\"\n            }\n          },\n          {\n            \"x\": 7,\n            \"y\": 0,\n            \"title\": \"Model 1\",\n            \"query\": \"MATCH (n:`Model`) \\nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`name` as value LIMIT 5\",\n            \"width\": 2,\n            \"type\": \"select\",\n            \"height\": 2,\n            \"selection\": {},\n            \"settings\": {\n              \"type\": \"Node Property\",\n              \"entityType\": \"Model\",\n              \"propertyType\": \"name\",\n              \"parameterName\": \"neodash_model_name_1\",\n              \"id\": \"1\"\n            }\n          },\n          {\n            \"x\": 9,\n            \"y\": 0,\n            \"title\": \"Model 2\",\n            \"query\": \"MATCH (n:`Model`) \\nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`name` as value LIMIT 5\",\n            \"width\": 2,\n            \"type\": \"select\",\n            \"height\": 2,\n            \"selection\": {},\n            \"settings\": {\n              \"type\": \"Node Property\",\n              \"entityType\": \"Model\",\n              \"propertyType\": \"name\",\n              \"parameterName\": \"neodash_model_name_2\",\n              \"id\": \"2\"\n            }\n          },\n          {\n            \"x\": 7,\n            \"y\": 2,\n            \"title\": \"Components in both\",\n            \"query\": \"MATCH (m:Model {name: $neodash_model_name_1})-[:HAS*]->(c:Component)\\nWITH collect(c) as in_first\\nMATCH (m2:Model {name: $neodash_model_name_2})-[:HAS*]->(c2:Component) WHERE c2 IN in_first\\nRETURN c2.name AS component, c2.number AS ref\\n\\n\\n\",\n            \"width\": 4,\n            \"type\": \"table\",\n            \"height\": 2,\n            \"selection\": {},\n            \"settings\": {\n              \"nodePositions\": {}\n            }\n          },\n          {\n            \"x\": 0,\n            \"y\": 2,\n            \"title\": \"Components only in first\",\n            \"query\": \"MATCH (m:Model {name: $neodash_model_name_2})-[:HAS*]->(c:Component)\\nWITH collect(c) as in_first\\nMATCH (m2:Model {name: $neodash_model_name_1})-[:HAS*]->(c2:Component) WHERE NOT c2 IN in_first\\nRETURN c2.name AS component, c2.number AS ref\\n\\n\\n\",\n            \"width\": 4,\n            \"type\": \"table\",\n            \"height\": 2,\n            \"selection\": {},\n            \"settings\": {\n              \"nodePositions\": {}\n            }\n          },\n          {\n            \"x\": 4,\n            \"y\": 2,\n            \"title\": \"Similarity\",\n            \"query\": \"MATCH (m:Model {name: $neodash_model_name_1})-[:HAS*]->(c:Component)\\nWITH collect(c) as in_first\\nMATCH (m2:Model {name: $neodash_model_name_2})-[:HAS*]->(c2:Component) WHERE c2 IN in_first\\nWITH in_first, count(c2) AS inter\\nMATCH (m3:Model {name: $neodash_model_name_2})-[:HAS*]->(c3:Component)\\nWITH size(in_first) - inter AS in_first, inter, size(collect(c3)) - inter as in_second\\nWITH apoc.coll.zip([\\\"first only\\\", \\\"both\\\", \\\"second only\\\"], [in_first, inter, in_second]) AS l\\nUNWIND l AS row\\nRETURN row[0] AS name, row[1] AS cardinality\\n\\n\\n\",\n            \"width\": 3,\n            \"type\": \"pie\",\n            \"height\": 2,\n            \"selection\": {\n              \"index\": \"name\",\n              \"value\": \"cardinality\",\n              \"key\": \"(none)\"\n            },\n            \"settings\": {\n              \"nodePositions\": {}\n            }\n          }\n        ]\n      }\n    ],\n    \"parameters\": {}\n  }"
  },
  {
    "path": "gallery/dashboards/bom.json",
    "content": "{\n  \"title\": \"BOM - Lista de materiales\",\n  \"version\": \"2.1\",\n  \"settings\": {\n    \"pagenumber\": 0,\n    \"editable\": true,\n    \"fullscreenEnabled\": true,\n    \"parameters\": {\n      \"neodash_supplier_name\": \"Manchester Manufacturing\",\n      \"neodash_model_number\": null,\n      \"neodash_model_name\": \"EveryRoad GPS Car Navigation Unit - Model 500 - UK Edition\",\n      \"neodash_model_name_1\": \"EveryRoad GPS Car Navigation Unit - Model 300 - US Edition\",\n      \"neodash_model_name_2\": \"EveryRoad GPS Car Navigation Unit - Model 500 - UK Edition\"\n    }\n  },\n  \"pages\": [\n    {\n      \"title\": \"Provedores\",\n      \"reports\": [\n        {\n          \"x\": 0,\n          \"y\": 0,\n          \"title\": \"Data Model\",\n          \"query\": \"CALL db.schema.visualization();\\n\\n\\n\",\n          \"width\": 6,\n          \"type\": \"graph\",\n          \"height\": 2,\n          \"selection\": {\n            \"Model\": \"name\",\n            \"Supplier\": \"name\",\n            \"Component\": \"name\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"x\": 6,\n          \"y\": 0,\n          \"title\": \"Provedores\",\n          \"query\": \"MATCH (s:Supplier) RETURN s.name\\n\\n\\n\",\n          \"width\": 3,\n          \"type\": \"table\",\n          \"height\": 2,\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"x\": 9,\n          \"y\": 0,\n          \"title\": \"Elija un provedor\",\n          \"query\": \"MATCH (n:`Supplier`) \\nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`name` as value LIMIT 5\",\n          \"width\": 3,\n          \"type\": \"select\",\n          \"height\": 2,\n          \"selection\": {},\n          \"settings\": {\n            \"type\": \"Node Property\",\n            \"entityType\": \"Supplier\",\n            \"propertyType\": \"name\",\n            \"parameterName\": \"neodash_supplier_name\"\n          }\n        },\n        {\n          \"x\": 0,\n          \"y\": 2,\n          \"title\": \"Modelos conectados a provedor\",\n          \"query\": \"MATCH p=(m:Model)-[*]->(s:Supplier {name: $neodash_supplier_name}) return p\\n\\n\\n\",\n          \"width\": 12,\n          \"type\": \"graph\",\n          \"height\": 2,\n          \"selection\": {\n            \"Model\": \"name\",\n            \"Component\": \"name\",\n            \"Supplier\": \"name\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"BOM Modelos\",\n      \"reports\": [\n        {\n          \"x\": 0,\n          \"y\": 0,\n          \"title\": \"Modelos\",\n          \"query\": \"MATCH (m:Model) RETURN m.number AS ID, m.name AS name\\n\\n\\n\",\n          \"width\": 6,\n          \"type\": \"table\",\n          \"height\": 2,\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"x\": 6,\n          \"y\": 0,\n          \"title\": \"Elija un modelo de producto\",\n          \"query\": \"MATCH (n:`Model`) \\nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`name` as value LIMIT 5\",\n          \"width\": 6,\n          \"type\": \"select\",\n          \"height\": 2,\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {},\n            \"type\": \"Node Property\",\n            \"entityType\": \"Model\",\n            \"propertyType\": \"name\",\n            \"parameterName\": \"neodash_model_name\"\n          }\n        },\n        {\n          \"x\": 6,\n          \"y\": 2,\n          \"title\": \"BOM\",\n          \"query\": \"MATCH path = (m:Model {name: $neodash_model_name})-[:HAS*]->(:Component)\\nRETURN path\\n\\n\",\n          \"width\": 6,\n          \"type\": \"graph\",\n          \"height\": 2,\n          \"selection\": {\n            \"Model\": \"name\",\n            \"Component\": \"number\"\n          },\n          \"settings\": {\n            \"nodePositions\": {},\n            \"nodeColorScheme\": \"neodash\",\n            \"layout\": \"tree\"\n          }\n        },\n        {\n          \"x\": 0,\n          \"y\": 2,\n          \"title\": \"Precios\",\n          \"query\": \"MATCH path = (m:Model {name: $neodash_model_name})-[:HAS*]->(c:Component)\\nWITH c.name AS name, toFloat(c.price) AS price, reduce(acc = 1, qty IN [r IN relationships(path)| toInteger(r.count)] | acc * qty) AS quantity\\nRETURN name, round(price, 2) AS price, quantity, round(price*quantity, 2) AS total_price\\nORDER BY total_price DESC\",\n          \"width\": 6,\n          \"type\": \"table\",\n          \"height\": 2,\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"Comparar Modelos\",\n      \"reports\": [\n        {\n          \"x\": 0,\n          \"y\": 0,\n          \"title\": \"Lista de Modelos\",\n          \"query\": \"MATCH (m:Model) RETURN m.name AS name\",\n          \"width\": 3,\n          \"type\": \"table\",\n          \"height\": 2,\n          \"selection\": {},\n          \"settings\": {\n            \"type\": \"Node Property\",\n            \"entityType\": \"Model\",\n            \"propertyType\": \"name\",\n            \"parameterName\": \"neodash_model_name\"\n          }\n        },\n        {\n          \"x\": 3,\n          \"y\": 0,\n          \"title\": \"Modelo 1\",\n          \"query\": \"MATCH (n:`Model`) \\nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`name` as value LIMIT 5\",\n          \"width\": 4,\n          \"type\": \"select\",\n          \"height\": 2,\n          \"selection\": {},\n          \"settings\": {\n            \"type\": \"Node Property\",\n            \"entityType\": \"Model\",\n            \"propertyType\": \"name\",\n            \"parameterName\": \"neodash_model_name_1\",\n            \"id\": \"1\"\n          }\n        },\n        {\n          \"x\": 7,\n          \"y\": 0,\n          \"title\": \"Modelo 2\",\n          \"query\": \"MATCH (n:`Model`) \\nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`name` as value LIMIT 5\",\n          \"width\": 5,\n          \"type\": \"select\",\n          \"height\": 2,\n          \"selection\": {},\n          \"settings\": {\n            \"type\": \"Node Property\",\n            \"entityType\": \"Model\",\n            \"propertyType\": \"name\",\n            \"parameterName\": \"neodash_model_name_2\",\n            \"id\": \"2\"\n          }\n        },\n        {\n          \"x\": 7,\n          \"y\": 2,\n          \"title\": \"Components en ambos\",\n          \"query\": \"MATCH (m:Model {name: $neodash_model_name_1})-[:HAS*]->(c:Component)\\nWITH collect(c) as in_first\\nMATCH (m2:Model {name: $neodash_model_name_2})-[:HAS*]->(c2:Component) WHERE c2 IN in_first\\nRETURN c2.name AS component, c2.number AS ref\\n\\n\\n\",\n          \"width\": 5,\n          \"type\": \"table\",\n          \"height\": 2,\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"x\": 0,\n          \"y\": 2,\n          \"title\": \"Components en primero solo\",\n          \"query\": \"MATCH (m:Model {name: $neodash_model_name_2})-[:HAS*]->(c:Component)\\nWITH collect(c) as in_first\\nMATCH (m2:Model {name: $neodash_model_name_1})-[:HAS*]->(c2:Component) WHERE NOT c2 IN in_first\\nRETURN c2.name AS component, c2.number AS ref\\n\\n\\n\",\n          \"width\": 4,\n          \"type\": \"table\",\n          \"height\": 2,\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"x\": 4,\n          \"y\": 2,\n          \"title\": \"Similaridad\",\n          \"query\": \"MATCH (m:Model {name: $neodash_model_name_1})-[:HAS*]->(c:Component)\\nWITH collect(c) as in_first\\nMATCH (m2:Model {name: $neodash_model_name_2})-[:HAS*]->(c2:Component) WHERE c2 IN in_first\\nWITH in_first, count(c2) AS inter\\nMATCH (m3:Model {name: $neodash_model_name_2})-[:HAS*]->(c3:Component)\\nWITH size(in_first) - inter AS in_first, inter, size(collect(c3)) - inter as in_second\\nWITH apoc.coll.zip([\\\"first only\\\", \\\"both\\\", \\\"second only\\\"], [in_first, inter, in_second]) AS l\\nUNWIND l AS row\\nRETURN row[0] AS name, row[1] AS cardinality\\n\\n\\n\",\n          \"width\": 3,\n          \"type\": \"pie\",\n          \"height\": 2,\n          \"selection\": {\n            \"index\": \"name\",\n            \"value\": \"cardinality\",\n            \"key\": \"(none)\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "gallery/dashboards/citation.json",
    "content": "{\n  \"title\": \"Citation graph - Topic extraction, recommendation, and Bloom exploration\",\n  \"version\": \"2.2\",\n  \"settings\": {\n    \"pagenumber\": 0,\n    \"editable\": true,\n    \"fullscreenEnabled\": false,\n    \"parameters\": {\n      \"neodash_community_property\": \"2892\",\n      \"neodash_title\": \"Modeling of architectures with UML panel\"\n    },\n    \"extensions\": [\"core\", \"actions\"]\n  },\n  \"pages\": [\n    {\n      \"title\": \"Overview\",\n      \"reports\": [\n        {\n          \"title\": \"Summary\",\n          \"query\": \"**Citation graph in scientific research**\\n\\nThe data used is from the DBLP Citation Network, which includes citation data from various academic sources.\\nYou can recreate the database with [this guide](https://neo4j.com/developer/graph-data-science/link-prediction/graph-data-science-library/).\\n\\nThis dashboard will explore the following use cases :\\n- Topic extraction\\n- Recommendation\\n- Using Bloom with NeoDash to extend the data exploration capabilities\\n\\nBut first, here is an overview of the data.\",\n          \"width\": 3,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"text\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"A sample of the graph with some citations\",\n          \"query\": \"MATCH p=(n)-[e]->(m) RETURN n,e,m LIMIT 18\\n\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 6,\n          \"y\": 0,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"Article\": \"title\",\n            \"Author\": \"name\",\n            \"Venue\": \"name\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"title\": \"Data model\",\n          \"query\": \"CALL db.schema.visualization()\",\n          \"width\": 3,\n          \"height\": 2,\n          \"x\": 3,\n          \"y\": 0,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"Venue\": \"name\",\n            \"Article\": \"name\",\n            \"Author\": \"name\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"title\": \"Some articles\",\n          \"query\": \"MATCH (n:Article)--(v:Venue)\\nWHERE EXISTS(n.abstract)\\nRETURN n.title as title, n.abstract as abstract, v.name as published_for LIMIT 50\\n\\n\\n\",\n          \"width\": 9,\n          \"height\": 2,\n          \"x\": 3,\n          \"y\": 2,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Number of articles\",\n          \"query\": \"MATCH (n:Article) RETURN count(n)\\n\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 0,\n          \"y\": 2,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Number of citations\",\n          \"query\": \"MATCH ()-[rel:CITED]->()\\nRETURN count(rel)\\n\\n\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 0,\n          \"y\": 3,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        }\n      ]\n    },\n    {\n      \"title\": \"Do articles cluster together ?\",\n      \"reports\": [\n        {\n          \"title\": \"Article clusters in Bloom - set username \\\"citation\\\" and password \\\"citation\\\" to view the perspective.\",\n          \"query\": \"https://bloom.neo4j.io/index.html?connectURL=neo4j%2Bs%3A%2F%2Facb5b6ae.databases.neo4j.io&search=Article%20cited%20Article%20cited%20Article&run=true\\n\\n\\n\",\n          \"width\": 9,\n          \"height\": 4,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"iframe\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"# of distinct clusters\",\n          \"query\": \"MATCH (n:Article)\\nWITH n.wcc as community, count(n) as communitySize\\nWHERE communitySize > 1\\nRETURN count(DISTINCT community)\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 9,\n          \"y\": 2,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"What's next?\",\n          \"query\": \"**This means we can run community detection algorithms!**\\n\\nFirst of all, here's an analysis using the Weakly Connected Components, which identifies disjoint clusters.\\nTwo nodes are in disjoint clusters if no path exists between them.\\n\\n\",\n          \"width\": 3,\n          \"height\": 2,\n          \"x\": 9,\n          \"y\": 0,\n          \"type\": \"text\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Biggest community has :\",\n          \"query\": \"MATCH (n:Article)\\nWITH n.wcc as community, count(n) as communitySize\\nRETURN toString(max(communitySize)) + ' Articles'\\n\\n\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 9,\n          \"y\": 3,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {\n            \"fontSize\": 56\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"Topic extraction\",\n      \"reports\": [\n        {\n          \"title\": \"Topic extraction\",\n          \"query\": \"Inside of that big community of 14k articles we've identified, let's see if we can identify different topics.\\n\\nFor this, we will identify communities of articles that cite each other ; and then, find the most influential article in each community.\\nThis can be used as the topic for that community.\\n\",\n          \"width\": 3,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"text\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Louvain detected :\",\n          \"query\": \"MATCH (n:Article)\\nWHERE EXISTS(n.louvain)\\nRETURN toString(count(DISTINCT n.louvain)) + \\\" communities\\\"\\n\",\n          \"width\": 3,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 2,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {\n            \"fontSize\": 56\n          }\n        },\n        {\n          \"title\": \"You might see \\\"disconnected islands\\\" because not all nodes and relationships are displayed.\",\n          \"query\": \"https://bloom.neo4j.io/index.html?connectURL=neo4j%2Bs%3A%2F%2Facb5b6ae.databases.neo4j.io&search=Article%20cited%20Article%20with%20wcc%200&run=true\\n\\n\",\n          \"width\": 9,\n          \"height\": 4,\n          \"x\": 3,\n          \"y\": 0,\n          \"type\": \"iframe\",\n          \"selection\": {},\n          \"settings\": {\n            \"description\": \"Set username \\\"citation\\\" and password \\\"citation\\\" to view the perspective.\\n\\nBe aware that this Bloom view limits the number of nodes displayed. So you might see \\\"disconnected islands\\\" because not all nodes and relationships are displayed.\"\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"Topic extraction - 2\",\n      \"reports\": [\n        {\n          \"title\": \"Click on a community number to update view\",\n          \"query\": \"MATCH (n:Article)\\nWHERE EXISTS(n.louvain)\\nWITH toString(n.louvain) as community, n.pagerank as pagerank, n.title as title ORDER BY pagerank DESC\\nWITH community, head(collect(title)) AS summary, head(collect(pagerank)) AS pagerank\\nRETURN community, summary\\n\",\n          \"width\": 4,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 2,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {\n            \"actionsRules\": [\n              {\n                \"condition\": \"Click\",\n                \"field\": \"community\",\n                \"value\": \"community\",\n                \"customization\": \"set variable\",\n                \"customizationValue\": \"community_property\"\n              }\n            ],\n            \"columnWidths\": \"[0.5,2]\"\n          }\n        },\n        {\n          \"title\": \"Top 10 topics\",\n          \"query\": \"MATCH (n:Article)\\nWHERE EXISTS(n.louvain)\\nWITH n.louvain as community, n.pagerank as pagerank, n.title as title, n ORDER BY pagerank DESC\\nWITH community, head(collect(title)) AS summary, count(n) as count LIMIT 10\\nRETURN summary, count\\n\\n\",\n          \"width\": 4,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"pie\",\n          \"selection\": {\n            \"index\": \"summary\",\n            \"value\": \"count\",\n            \"key\": \"(none)\"\n          },\n          \"settings\": {\n            \"marginTop\": 88,\n            \"marginBottom\": 88,\n            \"marginRight\": 88,\n            \"marginLeft\": 88\n          }\n        },\n        {\n          \"title\": \"Size indicates an Article's influence - Biggest node for a given community = topic\",\n          \"query\": \"https://bloom.neo4j.io/index.html?connectURL=neo4j%2Bs%3A%2F%2Facb5b6ae.databases.neo4j.io&run=true&search=Article%20louvain%20$neodash_community_property%20cited%20Article%20louvain%20 $neodash_community_property\",\n          \"width\": 8,\n          \"height\": 4,\n          \"x\": 4,\n          \"y\": 0,\n          \"type\": \"iframe\",\n          \"selection\": {},\n          \"settings\": {\n            \"passGlobalParameters\": false,\n            \"replaceGlobalParameters\": true,\n            \"description\": \"Set username \\\"citation\\\" and password \\\"citation\\\" to view the perspective.\"\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"Recommendation\",\n      \"reports\": [\n        {\n          \"title\": \"Pick an article you like (a random one is also fine)\",\n          \"query\": \"MATCH (n:Article WHERE EXISTS(n.louvain))\\nRETURN \\\"Click me\\\" AS click, n.title AS title LIMIT 100\",\n          \"width\": 8,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {\n            \"actionsRules\": [\n              {\n                \"condition\": \"Click\",\n                \"field\": \"click\",\n                \"value\": \"title\",\n                \"customization\": \"set variable\",\n                \"customizationValue\": \"title\"\n              }\n            ],\n            \"type\": \"Node Property\",\n            \"entityType\": \"Article\",\n            \"propertyType\": \"title\",\n            \"parameterName\": \"neodash_article_title\",\n            \"columnWidths\": \"[1,3]\"\n          }\n        },\n        {\n          \"title\": \"Recommended articles - Same topic\",\n          \"query\": \"MATCH (n:Article WHERE n.title=$neodash_title)\\nMATCH (m:Article WHERE m.louvain=n.louvain)\\nWITH n, m ORDER BY m.pagerank DESC LIMIT 5\\nRETURN n.title AS `You read`, m.title AS `Read next`\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 2,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Some explanations\",\n          \"query\": \"To make recommendations based on our current data, the process is the following, you can go two ways :\\n- Pick the articles with the top PageRank scores in the *same* community (same topic)\\n- Pick the articles with the top PageRank scores in a *different* community (related topic)\\n\\nTo try it out, **pick an article** to the left, and see the results below.\\n\",\n          \"width\": 4,\n          \"height\": 2,\n          \"x\": 8,\n          \"y\": 0,\n          \"type\": \"text\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Recommended articles - Related topic\",\n          \"query\": \"MATCH (n:Article WHERE n.title=$neodash_title)\\nMATCH (m:Article WHERE m.louvain=n.louvain)\\nWITH n, m ORDER BY m.pagerank DESC LIMIT 5\\nMATCH (o:Article WHERE NOT o.louvain=n.louvain)-[:CITED]-(m)\\nWITH n, m, o ORDER BY o.pagerank DESC LIMIT 5\\nRETURN o.title AS `Read next`\\n\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 6,\n          \"y\": 2,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {}\n        }\n      ]\n    }\n  ],\n  \"parameters\": {},\n  \"extensions\": {\n    \"advanced-charts\": true,\n    \"styling\": true,\n    \"actions\": true\n  }\n}\n"
  },
  {
    "path": "gallery/dashboards/domains.json",
    "content": "{\n    \"title\": \"New Caledonia Domains Dashboard\",\n    \"version\": \"2.1\",\n    \"settings\": {\n      \"pagenumber\": 0,\n      \"editable\": true,\n      \"fullscreenEnabled\": false,\n      \"parameters\": {\n        \"neodash_gestionnaires_name\": \"\",\n        \"neodash_gestionnaires_id\": \"\"\n      }\n    },\n    \"pages\": [\n      {\n        \"title\": \"Overview\",\n        \"reports\": [\n          {\n            \"title\": \"New Caledonia Domain Names\",\n            \"query\": \"This is a dashboard containing data about the domain names registered for New Caledonia.\\n\\n![new caledonia](https://upload.wikimedia.org/wikipedia/commons/1/18/New_Caledonia-CIA_WFB_Map.png)\\n\\nThis page contains an overview of the dataset. The second page lets you drill down into a specific beneficiary and view the graph around them.\",\n            \"width\": 3,\n            \"height\": 3,\n            \"x\": 0,\n            \"y\": 0,\n            \"type\": \"text\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Graph Schema\",\n            \"query\": \"CALL db.schema.visualization()\\n\\n\",\n            \"width\": 3,\n            \"height\": 3,\n            \"x\": 3,\n            \"y\": 0,\n            \"type\": \"graph\",\n            \"selection\": {\n              \"Site\": \"name\",\n              \"DNS\": \"name\",\n              \"Gestionnaires\": \"name\",\n              \"Beneficiaires\": \"name\"\n            },\n            \"settings\": {\n              \"nodeColorScheme\": \"nivo\"\n            }\n          },\n          {\n            \"title\": \"Total nodes\",\n            \"query\": \"MATCH (n)\\nRETURN COUNT(n) as Nodes\\n\\n\\n\",\n            \"width\": 3,\n            \"height\": 1,\n            \"x\": 6,\n            \"y\": 0,\n            \"type\": \"value\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Total relationships\",\n            \"query\": \"MATCH ()-[e]->()\\nRETURN count(e) as rels\\n\\n\\n\",\n            \"width\": 3,\n            \"height\": 1,\n            \"x\": 9,\n            \"y\": 0,\n            \"type\": \"value\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Site Status\",\n            \"query\": \"MATCH (n:Site)\\nRETURN n as node, n.id as id, n.status as status\\nSKIP 1025 LIMIT 1000\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 0,\n            \"y\": 3,\n            \"type\": \"table\",\n            \"selection\": {},\n            \"settings\": {\n              \"styleRules\": [\n                {\n                  \"field\": \"status\",\n                  \"condition\": \"=\",\n                  \"value\": \"green\",\n                  \"customization\": \"cell color\",\n                  \"customizationValue\": \"#00e300\"\n                },\n                {\n                  \"field\": \"status\",\n                  \"condition\": \"=\",\n                  \"value\": \"orange\",\n                  \"customization\": \"cell color\",\n                  \"customizationValue\": \"orange\"\n                },\n                {\n                  \"field\": \"status\",\n                  \"condition\": \"=\",\n                  \"value\": \"red\",\n                  \"customization\": \"cell color\",\n                  \"customizationValue\": \"red\"\n                }\n              ],\n              \"description\": \"Click the column headers to sort the website by status code.\"\n            }\n          },\n          {\n            \"title\": \"A sample of the data\",\n            \"query\": \"MATCH (s:Site)\\nWHERE s.id in [\\\"1013\\\",\\\"1050\\\",\\\"1016\\\"]\\nMATCH p=(s)--()\\nRETURN p\",\n            \"width\": 6,\n            \"height\": 2,\n            \"x\": 6,\n            \"y\": 1,\n            \"type\": \"graph\",\n            \"selection\": {\n              \"Site\": \"id\",\n              \"DNS\": \"id\",\n              \"Beneficiaires\": \"id\",\n              \"Gestionnaires\": \"id\"\n            },\n            \"settings\": {\n              \"nodeColorScheme\": \"pastel1\"\n            }\n          },\n          {\n            \"title\": \"DNS'es\",\n            \"query\": \"MATCH (n:DNS)\\nRETURN n as node, n.id as name\\nLIMIT 1000\\n\\n\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 3,\n            \"y\": 3,\n            \"type\": \"table\",\n            \"selection\": {},\n            \"settings\": {\n              \"columnWidths\": \"[1,2]\"\n            }\n          },\n          {\n            \"title\": \"Gestionnaires (Managers)\",\n            \"query\": \"MATCH (n:Gestionnaires)\\nRETURN n as node, n.id as name\\nLIMIT 1000\\n\\n\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 6,\n            \"y\": 3,\n            \"type\": \"table\",\n            \"selection\": {},\n            \"settings\": {\n              \"columnWidths\": \"[1,2]\"\n            }\n          },\n          {\n            \"title\": \"Beneficiaires (Clients)\",\n            \"query\": \"MATCH (n:Beneficiaires)\\nRETURN n as node, n.id as name\\nLIMIT 1000\\n\\n\\n\\n\\n\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 9,\n            \"y\": 3,\n            \"type\": \"table\",\n            \"selection\": {},\n            \"settings\": {\n              \"columnWidths\": \"[1,2]\"\n            }\n          },\n          {\n            \"title\": \"Domains created by year\",\n            \"query\": \"MATCH (s:Site)\\nWITH date(s.dateCreation).year as year, COUNT(*) as number\\nWHERE year <> 0\\nRETURN year, number\\nORDER BY year ASC \\n\\n\\n\",\n            \"width\": 6,\n            \"height\": 2,\n            \"x\": 0,\n            \"y\": 5,\n            \"type\": \"line\",\n            \"selection\": {\n              \"x\": \"year\",\n              \"value\": [\n                \"number\"\n              ]\n            },\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Gestionnaires with most clients\",\n            \"query\": \"MATCH (g:Gestionnaires)<-[:CLIENTDE]-(b:Beneficiaires)\\nRETURN g.id as Gestionnaires, COUNT(b) as clients\\nORDER BY clients DESC LIMIT 20\\n\\n\",\n            \"width\": 6,\n            \"height\": 2,\n            \"x\": 6,\n            \"y\": 5,\n            \"type\": \"bar\",\n            \"selection\": {\n              \"index\": \"Gestionnaires\",\n              \"value\": \"clients\",\n              \"key\": \"(none)\"\n            },\n            \"settings\": {\n              \"marginBottom\": 120\n            }\n          }\n        ]\n      },\n      {\n        \"title\": \"Drilldown\",\n        \"reports\": [\n          {\n            \"title\": \"Select a Gestionnaire\",\n            \"query\": \"MATCH (n:`Gestionnaires`) \\nWHERE toLower(toString(n.`id`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`id` as value ORDER BY size(toString(value)) ASC LIMIT 5\",\n            \"width\": 3,\n            \"height\": 1,\n            \"x\": 0,\n            \"y\": 1,\n            \"type\": \"select\",\n            \"selection\": {},\n            \"settings\": {\n              \"type\": \"Node Property\",\n              \"entityType\": \"Gestionnaires\",\n              \"propertyType\": \"id\",\n              \"parameterName\": \"neodash_gestionnaires_id\"\n            }\n          },\n          {\n            \"title\": \"Drilldown page\",\n            \"query\": \"On this page, you can select a specific Gestionnaire, and drilldown into the graph attached to it.\\n\\n**TIP**: Try selecting \\\"TLI\\\" in the textbox below:\",\n            \"width\": 3,\n            \"height\": 1,\n            \"x\": 0,\n            \"y\": 0,\n            \"type\": \"text\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Clients and number of domains for the selected provider\",\n            \"query\": \"MATCH (g:Gestionnaires)<-[:CLIENTDE]-(b:Beneficiaires)-[:POSSEDE]->(d:Site)\\nWHERE g.id = $neodash_gestionnaires_id\\nRETURN b.id as Client, COUNT(d) as Domains\\n\\n\\n\\n\\n\\n\\n\",\n            \"width\": 9,\n            \"height\": 4,\n            \"x\": 3,\n            \"y\": 0,\n            \"type\": \"bar\",\n            \"selection\": {\n              \"index\": \"Client\",\n              \"value\": \"Domains\",\n              \"key\": \"(none)\"\n            },\n            \"settings\": {\n              \"marginBottom\": 160\n            }\n          },\n          {\n            \"title\": \"Graph view\",\n            \"query\": \"MATCH path=(g:Gestionnaires)<-[:CLIENTDE]-(b:Beneficiaires)-[:POSSEDE]->(d:Site)\\nWHERE g.id = $neodash_gestionnaires_id\\nRETURN path\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 0,\n            \"y\": 4,\n            \"type\": \"graph\",\n            \"selection\": {\n              \"Gestionnaires\": \"id\",\n              \"Beneficiaires\": \"id\",\n              \"Site\": \"id\"\n            },\n            \"settings\": {}\n          }\n        ]\n      }\n    ],\n    \"parameters\": {}\n  }"
  },
  {
    "path": "gallery/dashboards/fraud.json",
    "content": "{\n  \"uuid\": \"b3236f88-ff8b-492d-8a84-d620a3dd629d\",\n  \"title\": \"Financial Crimes Enforcement Dashboard 🕵️\",\n  \"version\": \"2.4\",\n  \"settings\": {\n    \"pagenumber\": 0,\n    \"editable\": true,\n    \"parameters\": {\n      \"neodash_entity_name\": null,\n      \"neodash_country_name\": null\n    },\n    \"fullscreenEnabled\": true\n  },\n  \"pages\": [\n    {\n      \"title\": \"Countries\",\n      \"reports\": [\n        {\n          \"x\": 0,\n          \"y\": 0,\n          \"title\": \"About this dashboard\",\n          \"query\": \"This is an example dashboard on financial crime data. It uses the `fincen` dataset from \\n[https://demo.neo4jlabs.com/](https://demo.neo4jlabs.com/).\\n\\nThis dashboard's purpose is to provide examples on how to use and customize all the different NeoDash report types.\\n\\nIt consists of three pages:\\n- **Countries**: high-level data on specific countries.\\n- **Investigate Entity**: a way to drill down into a specific entity.\\n- **Statistics**: high-level statistics about the data.\\n\\nTry out the Documentation 📄 button on the left for basic examples of the different visualization reports.\",\n          \"width\": 8,\n          \"type\": \"text\",\n          \"height\": 4,\n          \"selection\": {},\n          \"settings\": {},\n          \"id\": \"bd17ccad-c12e-4e5a-8e45-48504b071698\"\n        },\n        {\n          \"x\": 8,\n          \"y\": 0,\n          \"title\": \"How much does each entity benefit in total? (Hint: try clicking the table headers to sort/filter data)\",\n          \"query\": \"MATCH Path=(e:Entity)-[:COUNTRY]->(c:Country), (f:Filing)-[:BENEFITS]->(e)\\nRETURN Path, e.name as Entity, c.name as Country, suM(f.amount) as `Total Benefit ($)`\\nLIMIT 1000\",\n          \"width\": 16,\n          \"type\": \"table\",\n          \"height\": 4,\n          \"selection\": {},\n          \"settings\": {},\n          \"id\": \"6db3061c-12c5-4a92-a1a1-bf7e40c068a4\"\n        },\n        {\n          \"x\": 0,\n          \"y\": 4,\n          \"title\": \"Where in Europe does the Netherlands send money to?\",\n          \"query\": \"MATCH (c1:Country)--(:Entity)<-[:ORIGINATOR]-(f:Filing)-[:BENEFITS]->(:Entity)--(c2:Country)\\nWHERE c1.name =  \\\"Netherlands\\\"\\nAND point.distance(c2.location, point({latitude: 53, longitude: 9})) < 3000000\\nWITH c1, c2, sum(f.amount) as amount ORDER BY amount DESC\\nRETURN c1, c2, apoc.create.vRelationship(c1, \\\"TRANSFER\\\", {amount: amount}, c2) \",\n          \"width\": 12,\n          \"type\": \"map\",\n          \"height\": 4,\n          \"selection\": {\n            \"Country\": \"(no label)\",\n            \"TRANSFER\": \"(label)\"\n          },\n          \"settings\": {\n            \"defaultRelColor\": \"rgba(120,120,120,0.5)\",\n            \"defaultRelWidth\": 5,\n            \"defaultNodeSize\": \"medium\",\n            \"nodeColorScheme\": \"category10\"\n          },\n          \"id\": \"5484e81c-52b2-416d-8b7f-fa112887fbec\",\n          \"schema\": [\n            [\"Country\", \"code\", \"name\", \"location\", \"tld\"],\n            [\"TRANSFER\", \"amount\"]\n          ]\n        },\n        {\n          \"x\": 12,\n          \"y\": 4,\n          \"title\": \"Which entities are involved?\",\n          \"query\": \"MATCH (c1:Country)--(:Entity)<-[:ORIGINATOR]-(f:Filing)-[:BENEFITS]->(:Entity)--(c2:Country)\\nWHERE c1.name =  \\\"Netherlands\\\"\\nAND point.distance(c2.location, point({latitude: 53, longitude: 9})) < 3000000\\nWITH c1, c2, sum(f.amount) as amount\\nWITH c1, c2, apoc.create.vRelationship(c1, \\\"TRANSFER\\\", {amount: amount}, c2) as t\\n\\nMATCH path=(c2:Country)-[r]-(e:Entity)\\nRETURN c1, t, c2, collect(path)[0..10]\",\n          \"width\": 12,\n          \"type\": \"graph\",\n          \"height\": 4,\n          \"selection\": {\n            \"Country\": \"name\",\n            \"TRANSFER\": \"(label)\",\n            \"Entity\": \"name\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          },\n          \"id\": \"a4f84cff-996b-4bfd-b5de-2d8f0c9aa8b1\",\n          \"schema\": [\n            [\"Country\", \"code\", \"name\", \"location\", \"tld\"],\n            [\"TRANSFER\", \"amount\"],\n            [\"Entity\", \"name\", \"location\", \"id\", \"country\"]\n          ]\n        }\n      ]\n    },\n    {\n      \"title\": \"Entities\",\n      \"reports\": [\n        {\n          \"x\": 0,\n          \"y\": 0,\n          \"title\": \"Entity Investigator 🔎\",\n          \"query\": \"You can use this page to explore information about a single entity in the dataset. All reports are automatically updated based on the selected entity.\\n\\n**Hint**: Try typing **ING Bank NV** \\nin the \\\"Entity name\\\" box to the right of this text.\\n\\n\\n\",\n          \"width\": 6,\n          \"type\": \"text\",\n          \"height\": 4,\n          \"selection\": {},\n          \"settings\": {},\n          \"id\": \"e33482d3-a7a2-4090-8868-0ed3931bc99e\"\n        },\n        {\n          \"x\": 6,\n          \"y\": 0,\n          \"title\": \"Select an entity to view reports\",\n          \"query\": \"MATCH (n:`Entity`) \\nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`name` as value,  n.`name` as display ORDER BY size(toString(value)) ASC LIMIT 5\",\n          \"width\": 5,\n          \"type\": \"select\",\n          \"height\": 4,\n          \"selection\": {},\n          \"settings\": {\n            \"type\": \"Node Property\",\n            \"entityType\": \"Entity\",\n            \"propertyType\": \"name\",\n            \"parameterName\": \"neodash_entity_name\"\n          },\n          \"id\": \"5a5a46cb-d586-4831-88bc-b193edfa9e9c\"\n        },\n        {\n          \"x\": 11,\n          \"y\": 0,\n          \"title\": \"Details \",\n          \"query\": \"  MATCH (e:Entity)\\nWHERE e.name = $neodash_entity_name\\nWITH e LIMIT 1\\nMATCH (c:Country)--(e)--(f:Filing)\\nWITH e, c, sum(f.amount) AS totalAmount, min(f.begin) AS startOperation\\nWITH e, c, totalAmount, startOperation\\nRETURN e.name as `Entity full name`,\\n  c.name as `Country of origin`,\\n  \\\"$\\\" + toInteger(totalAmount/1000000) + \\\" million\\\" as `Total filings`,\\n  toString(date(startOperation)) as `Start of operations`\\n\",\n          \"width\": 7,\n          \"type\": \"table\",\n          \"height\": 4,\n          \"selection\": {},\n          \"settings\": {\n            \"compact\": false\n          },\n          \"id\": \"6e2e57b7-09c8-46cf-b33e-19fd3645693b\",\n          \"schema\": []\n        },\n        {\n          \"x\": 18,\n          \"y\": 0,\n          \"title\": \"Entity interactions\",\n          \"query\": \"MATCH path=(e:Entity)<--()-->(e2:Entity)\\nWHERE e.name = $neodash_entity_name\\nWITH DISTINCT e, e2\\nRETURN e, e2, apoc.create.vRelationship(e, \\\"INTERACTS\\\", {}, e2) \\n\\n\\n\",\n          \"width\": 6,\n          \"type\": \"map\",\n          \"height\": 4,\n          \"selection\": {\n            \"Entity\": \"(no label)\",\n            \"INTERACTS\": \"(label)\"\n          },\n          \"settings\": {\n            \"hideSelections\": true\n          },\n          \"id\": \"6070f205-23ce-42bb-abc1-96ec3839e531\",\n          \"schema\": [[\"Entity\", \"name\", \"location\", \"id\", \"country\"], [\"INTERACTS\"]]\n        },\n        {\n          \"x\": 0,\n          \"y\": 4,\n          \"title\": \"Who receives most money from this entity?\",\n          \"query\": \"MATCH path=(e:Entity)<--(f:Filing)-->(e2:Entity)\\nWHERE e.name = $neodash_entity_name\\nWITH DISTINCT e, f, e2\\nRETURN e2.name as `Other`, sum(f.amount) as Amount\\nORDER BY Amount ASC\",\n          \"width\": 12,\n          \"type\": \"bar\",\n          \"height\": 4,\n          \"selection\": {\n            \"index\": \"Other\",\n            \"value\": \"Amount\",\n            \"key\": \"(none)\"\n          },\n          \"settings\": {\n            \"valueScale\": \"linear\",\n            \"marginLeft\": 90,\n            \"marginBottom\": 100,\n            \"marginRight\": 50,\n            \"colors\": \"paired\",\n            \"groupMode\": \"grouped\"\n          },\n          \"id\": \"7023ae0c-68af-4fa6-8c59-3ba91d7980aa\"\n        },\n        {\n          \"x\": 12,\n          \"y\": 4,\n          \"title\": \"Details on a filing by the entity\",\n          \"query\": \"MATCH path=(e:Entity)<--(f:Filing)\\nWHERE e.name = $neodash_entity_name\\nRETURN f LIMIT 1\\n\",\n          \"width\": 6,\n          \"type\": \"json\",\n          \"height\": 4,\n          \"selection\": {},\n          \"settings\": {},\n          \"id\": \"ceb4d37e-8d9c-45c9-9062-485d40bed6cd\"\n        },\n        {\n          \"x\": 18,\n          \"y\": 4,\n          \"title\": \"Number of Filings\",\n          \"query\": \"MATCH (e:Entity)--(:Filing)\\nWHERE e.name = $neodash_entity_name\\nRETURN COUNT(*)\\n\\n\",\n          \"width\": 6,\n          \"type\": \"value\",\n          \"height\": 4,\n          \"selection\": {},\n          \"settings\": {\n            \"fontSize\": 80\n          },\n          \"id\": \"0efecd19-10ea-4475-b649-19b3b1b1e511\"\n        }\n      ]\n    },\n    {\n      \"title\": \"Statistics\",\n      \"reports\": [\n        {\n          \"x\": 0,\n          \"y\": 0,\n          \"title\": \"Total number of nodes\",\n          \"query\": \"MATCH (n)\\nRETURN COUNT(n)\",\n          \"width\": 6,\n          \"type\": \"value\",\n          \"height\": 4,\n          \"selection\": {},\n          \"settings\": {\n            \"textAlign\": \"center\",\n            \"fontSize\": 80,\n            \"marginTop\": 50\n          },\n          \"id\": \"073fc2f0-c1a8-4206-ac48-53171ed98696\"\n        },\n        {\n          \"x\": 6,\n          \"y\": 0,\n          \"title\": \"Total number of relationships\",\n          \"query\": \"MATCH (n)-[e]->(m)\\nRETURN COUNT(e)\\n\\n\\n\",\n          \"width\": 6,\n          \"type\": \"value\",\n          \"height\": 4,\n          \"selection\": {},\n          \"settings\": {\n            \"fontSize\": 80,\n            \"marginTop\": 50,\n            \"textAlign\": \"center\"\n          },\n          \"id\": \"294a7cc4-bd1e-4d6f-b440-16e9ad9a95f8\"\n        },\n        {\n          \"x\": 12,\n          \"y\": 0,\n          \"title\": \"Number of nodes by label\",\n          \"query\": \"MATCH (n)\\nRETURN labels(n), count(*) as count\\nORDER BY count ASC\\n\\n\\n\",\n          \"width\": 6,\n          \"type\": \"pie\",\n          \"height\": 4,\n          \"selection\": {\n            \"index\": \"labels(n)\",\n            \"value\": \"count\",\n            \"key\": \"(none)\"\n          },\n          \"settings\": {\n            \"colors\": \"pastel1\",\n            \"marginBottom\": 60\n          },\n          \"id\": \"bb995076-94b4-4e58-822b-19d4f8c62ac8\"\n        },\n        {\n          \"x\": 18,\n          \"y\": 0,\n          \"title\": \"Number of relationship types\",\n          \"query\": \"MATCH (n)-[e]->(m)\\nRETURN type(e),count(*) as count\\nORDER BY count ASC\\n\\n\\n\\n\\n\\n\\n\",\n          \"width\": 6,\n          \"type\": \"pie\",\n          \"height\": 4,\n          \"selection\": {\n            \"index\": \"type(e)\",\n            \"value\": \"count\",\n            \"key\": \"(none)\"\n          },\n          \"settings\": {\n            \"colors\": \"pastel1\",\n            \"marginBottom\": 60,\n            \"marginLeft\": 120,\n            \"marginRight\": 120\n          },\n          \"id\": \"6b604fad-6104-49e7-9aea-0a878a887e49\"\n        },\n        {\n          \"x\": 0,\n          \"y\": 4,\n          \"title\": \"Number of filing per year\",\n          \"query\": \"MATCH (f:Filing)\\nWHERE f.begin IS NOT NULL\\nWITH f, date(f.begin).year as Year\\nRETURN Year, COUNT(f) as Total\\nORDER BY Year ASC\\n\",\n          \"width\": 12,\n          \"type\": \"line\",\n          \"height\": 4,\n          \"selection\": {\n            \"x\": \"Year\",\n            \"value\": [\"Total\"]\n          },\n          \"settings\": {\n            \"marginLeft\": 60\n          },\n          \"id\": \"93ce2eab-4f7e-4e6e-a71e-257ebf5f68a9\"\n        },\n        {\n          \"x\": 12,\n          \"y\": 4,\n          \"title\": \"Example: using iFrames to embed custom visualizations (3D graph)\",\n          \"query\": \"https://vasturiano.github.io/react-force-graph/example/basic/\",\n          \"width\": 12,\n          \"type\": \"iframe\",\n          \"height\": 4,\n          \"selection\": {},\n          \"settings\": {},\n          \"id\": \"94463630-ed46-4cbe-b140-316151e23ed1\"\n        }\n      ]\n    }\n  ],\n  \"extensions\": {\n    \"advanced-charts\": {\n      \"active\": true\n    },\n    \"styling\": {\n      \"active\": true\n    },\n    \"active\": true,\n    \"activeReducers\": []\n  }\n}\n"
  },
  {
    "path": "gallery/dashboards/jokes.json",
    "content": "{\n  \"title\": \"Dad Jokes Dashboard\",\n  \"version\": \"2.2\",\n  \"settings\": {\n    \"pagenumber\": 0,\n    \"editable\": true,\n    \"fullscreenEnabled\": false,\n    \"parameters\": {\n      \"neodash_entity_text\": \"bananas\"\n    },\n    \"downloadImageEnabled\": false,\n    \"resizing\": \"bottom-right\"\n  },\n  \"pages\": [\n    {\n      \"title\": \"Start\",\n      \"reports\": [\n        {\n          \"title\": \"Let's start this crazy trip down the dadjoke rathole!\",\n          \"query\": \"\\n\\nThe graph powering this dashboard was made using Google's NLP API. Curious how this dashboard was made? Check out the blog post!\\n\\n\\n[https://blog.bruggen.com/2022/10/a-graph-database-and-dadjoke-walk-into.html](https://blog.bruggen.com/2022/10/a-graph-database-and-dadjoke-walk-into.html)\\n\\n![Logo](https://drive.google.com/uc?export=view&id=1Fmv5Ap0IJrUEjpHiYa7p1JxXWgGBvlf0)\\n\",\n          \"width\": 12,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"text\",\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"Tweets and Handles \",\n      \"reports\": [\n        {\n          \"title\": \"Handles tweeting the same text\",\n          \"query\": \"MATCH (t1:Tweet), (t2:Tweet)\\nWHERE t1 <> t2\\nAND t1.Text = t2.Text\\nRETURN t1.`Screen Name`, t2.`Screen Name`, t1.Text;\\n\",\n          \"width\": 12,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {}\n        }\n      ]\n    },\n    {\n      \"title\": \"Jeff Bezos and his Pijamas\",\n      \"reports\": [\n        {\n          \"title\": \"Jokes about Jeff Bezos\",\n          \"query\": \"MATCH path = (dj:Dadjoke)-[*..2]-(conn)\\nWHERE dj.Text CONTAINS \\\"pyjamazon\\\"\\n    RETURN path\\nlimit 10;\\n\\n\\n\",\n          \"width\": 12,\n          \"height\": 3,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"Dadjoke\": \"Text\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"title\": \"With Entities (after NLP)\",\n          \"query\": \"\\nMATCH path = (e:Entity)--(dj:Dadjoke)-[REFERENCES_DADJOKE]-(t:Tweet)--(h:Handle)\\nWHERE dj.Text CONTAINS \\\"amazon\\\"\\n    RETURN path;\\n\\n\",\n          \"width\": 12,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 3,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"Entity\": \"text\",\n            \"Person\": \"text\",\n            \"Dadjoke\": \"Text\",\n            \"Tweet\": \"Text\",\n            \"Handle\": \"name\",\n            \"Other\": \"text\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"String Metrics\",\n      \"reports\": [\n        {\n          \"title\": \"Levenshtein and Sørensen-Dice Similarity\",\n          \"query\": \"\\nMATCH (dj1:Dadjoke), (dj2:Dadjoke)\\n  WHERE id(dj1)<id(dj2)\\n  AND dj1.Text <> dj2.Text\\n  AND left(dj1.Text,30) = left(dj2.Text,30)\\nWITH dj1.Text AS dj1text, dj2.Text AS dj2text\\nLIMIT 100\\nwith dj1text, dj2text, apoc.text.levenshteinSimilarity(dj1text, dj2text) AS LevenshteinSimilarity,\\napoc.text.sorensenDiceSimilarity(dj1text, dj2text) AS SorensenDiceSimilarity\\n  WHERE LevenshteinSimilarity < 0.65\\nRETURN left(dj1text,60) as `First 60 chars of dadjoke1`,left(dj2text,60) as `First 60 chars of dadjoke2`,LevenshteinSimilarity,SorensenDiceSimilarity\\nORDER BY LevenshteinSimilarity DESC;\\n\\n\",\n          \"width\": 12,\n          \"height\": 4,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {},\n            \"autorun\": true\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"Jokes about cars\",\n      \"reports\": [\n        {\n          \"title\": \"Jokes with \\\"cars\\\" in the text\",\n          \"query\": \"MATCH (dj:Dadjoke) WHERE dj.Text CONTAINS \\\"car\\\" RETURN dj.Text as Dadjoke LIMIT 10;\\n\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"title\": \"Jokes with \\\"car\\\" in the assosiated entity\",\n          \"query\": \"MATCH (e:Entity)--(dj:Dadjoke) WHERE e.text CONTAINS \\\"car\\\" RETURN e.text as Entity, dj.Text as Dadjoke LIMIT 10;\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 6,\n          \"y\": 0,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Dadjoke with entity equal to \\\"car\\\"\",\n          \"query\": \"MATCH p = (e:Entity)--(dj:Dadjoke) WHERE e.text = \\\"car\\\" RETURN dj.Text as Dadjoke LIMIT 10;\\n\\n\",\n          \"width\": 12,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 3,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {}\n        }\n      ]\n    },\n    {\n      \"title\": \"Jokes about spaghetti\",\n      \"reports\": [\n        {\n          \"title\": \"Spaghetti jokes\",\n          \"query\": \"\\nMATCH p=(h:Handle)--(t:Tweet)--(dj:Dadjoke)-[r:JACCARD_SIMILAR]->() \\nWHERE dj.Text CONTAINS \\\"spaghetti\\\" \\n    AND (dj.Text CONTAINS \\\"bike\\\" OR dj.Text CONTAINS \\\"car\\\")\\n    RETURN p;\\n\\n\\n\\n\\n\\n\",\n          \"width\": 12,\n          \"height\": 3,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"Handle\": \"name\",\n            \"Tweet\": \"Tweet Id\",\n            \"Dadjoke\": \"Text\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"Jokes about entities\",\n      \"reports\": [\n        {\n          \"title\": \"Select the Entity\",\n          \"query\": \"MATCH (n:`Entity`) \\nWHERE toLower(toString(n.`text`)) CONTAINS toLower($input) \\nRETURN DISTINCT n.`text` as value ORDER BY size(toString(value)) ASC LIMIT 5\",\n          \"width\": 3,\n          \"height\": 3,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"select\",\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {},\n            \"type\": \"Node Property\",\n            \"entityType\": \"Entity\",\n            \"propertyType\": \"text\",\n            \"parameterName\": \"neodash_entity_text\"\n          }\n        },\n        {\n          \"title\": \"\",\n          \"query\": \"WITH [\\\"car\\\",\\\"spaghetti\\\",\\\"water\\\",\\\"boil\\\",\\\"hell\\\"] AS entities\\nMATCH p = (h:Handle)--(t:Tweet)--(dj:Dadjoke)--(e:Entity)\\nWHERE e.text IN entities\\nRETURN p;\\n\\n\\n\\n\",\n          \"width\": 12,\n          \"height\": 3,\n          \"x\": 0,\n          \"y\": 3,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"Handle\": \"name\",\n            \"Tweet\": \"(label)\",\n            \"Dadjoke\": \"(label)\",\n            \"Entity\": \"(label)\",\n            \"ConsumerGood\": \"(label)\",\n            \"Other\": \"(label)\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"title\": \"Graph of jokes about the Entity selected\",\n          \"query\": \"MATCH p = (h:Handle)--(t:Tweet)--(dj:Dadjoke)--(e:Entity)\\nWHERE e.text = $neodash_entity_text\\nRETURN p;\\n\\n\\n\\n\\n\\n\",\n          \"width\": 9,\n          \"height\": 3,\n          \"x\": 3,\n          \"y\": 0,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"Handle\": \"name\",\n            \"Tweet\": \"Tweet Id\",\n            \"Dadjoke\": \"Text\",\n            \"Entity\": \"text\",\n            \"Other\": \"text\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"Dadjoke Twitterverse\",\n      \"reports\": [\n        {\n          \"title\": \"Twitterverse cliques\",\n          \"query\": \"\\nMATCH path = (h1:Handle)-[*2..2]->(dj:Dadjoke)<-[*2..2]-(h2:Handle)\\nWHERE id(h1)<id(h2)\\nRETURN path;\\n\\n\",\n          \"width\": 12,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"Handle\": \"name\",\n            \"Tweet\": \"(label)\",\n            \"Dadjoke\": \"(label)\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"title\": \"Twitterverse stats\",\n          \"query\": \"\\nMATCH path = (h1:Handle)-[*2..2]-(dj:Dadjoke)-[*2..2]-(h2:Handle)\\nWHERE id(h1)<id(h2)\\nWITH h1.name AS FirstHandle, h2.name AS SecondHandle, count(path) AS NrOfSharedJokes\\nRETURN FirstHandle, SecondHandle,NrOfSharedJokes\\nORDER BY NrOfSharedJokes DESC;\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 2,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"title\": \"It's all about...\",\n          \"query\": \"\\nMATCH (e:Entity)\\nRETURN e.text as Entity, e.SumOfSumOfFavorites AS EntityFavoriteScore, e.SumOfSumOfRetweets AS EntityRetweetScore\\nORDER BY EntityFavoriteScore DESC\\nLIMIT 10;\\n\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 6,\n          \"y\": 2,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        }\n      ]\n    }\n  ],\n  \"parameters\": {},\n  \"extensions\": {\n    \"advanced-charts\": true,\n    \"styling\": true\n  }\n}\n"
  },
  {
    "path": "gallery/dashboards/movies.json",
    "content": "{\n    \"title\": \"NeoDash Movies Dashboard 🎬\",\n    \"version\": \"2.1\",\n    \"settings\": {\n      \"pagenumber\": 0,\n      \"editable\": true,\n      \"fullscreenEnabled\": false,\n      \"parameters\": {\n        \"neodash_person_name\": \"Tom Hanks\"\n      },\n      \"extensions\": [\n        \"core\",\n        \"actions\"\n      ]\n    },\n    \"pages\": [\n      {\n        \"title\": \"Overview\",\n        \"reports\": [\n          {\n            \"title\": \"The Movies Dashboard\",\n            \"query\": \"The *Movies Database* is a great way to get to know Neo4j and Cypher. This graph contains the following data:\\n- `Person` nodes with two properties (`name` and `born`).\\n- `Movie` nodes with four properties (`title`, `tagline`, `released` and `votes`).\\n- Five relationship types between `Person` and `Movie` (`ACTED_IN`,  `DIRECTED`, `PRODUCED`, `WROTE`, `FOLLOWS`)\\n- One relationship type between different person nodes - `FOLLOWS`.\\n\\nThis dashboard uses the sample dataset from the Neo4j developer guide.\\n[https://neo4j.com/developer/example-data/](https://neo4j.com/developer/example-data/)\\n\\n____\\n\\n\\nYou will find two pages in this same dashboard:\\n1. An **Overview** page with general info about the graph.\\n3. An **Actor view** page that lets you drill down on a specific person.\\n\\nTo inspect the Cypher behind each of the visualizations, click the (⋮) button on the top right of a report. This lets you see and edit the query.\",\n            \"width\": 6,\n            \"height\": 2,\n            \"x\": 0,\n            \"y\": 0,\n            \"type\": \"text\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"The entire graph\",\n            \"query\": \"// It's the entire graph!\\nMATCH (a)-[r]->(b)\\nRETURN *\",\n            \"width\": 6,\n            \"height\": 3,\n            \"x\": 6,\n            \"y\": 1,\n            \"type\": \"graph\",\n            \"selection\": {\n              \"Person\": \"name\",\n              \"Movie\": \"title\"\n            },\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Total movies\",\n            \"query\": \"MATCH (n)\\nRETURN COUNT(n) as Total\",\n            \"width\": 2,\n            \"height\": 1,\n            \"x\": 6,\n            \"y\": 0,\n            \"type\": \"value\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Total people\",\n            \"query\": \"MATCH (n:Person)\\nRETURN COUNT(n)\\n\\n\\n\",\n            \"width\": 2,\n            \"height\": 1,\n            \"x\": 8,\n            \"y\": 0,\n            \"type\": \"value\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Relationships\",\n            \"query\": \"MATCH ()-[r]->()\\nRETURN count(r)\\n\\n\\n\",\n            \"width\": 2,\n            \"height\": 1,\n            \"x\": 10,\n            \"y\": 0,\n            \"type\": \"value\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Relationship types\",\n            \"query\": \"MATCH ()-[r]->()\\nRETURN type(r) as Relationship, COUNT(r) as Total\\n\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 0,\n            \"y\": 2,\n            \"type\": \"bar\",\n            \"selection\": {\n              \"index\": \"Relationship\",\n              \"value\": \"Total\",\n              \"key\": \"(none)\"\n            },\n            \"settings\": {\n              \"marginBottom\": 80\n            }\n          },\n          {\n            \"title\": \"Browse the movies\",\n            \"query\": \"MATCH (m:Movie)\\nRETURN m.title as Movie, m.released as Year\\n\\n\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 6,\n            \"y\": 4,\n            \"type\": \"table\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Movies with most actors\",\n            \"query\": \"MATCH (m:Movie)<-[:ACTED_IN]-(p:Person)\\nRETURN m.title as Movie, COUNT(p) as Actors\\nORDER BY Actors\\nDESC LIMIT 10\\n\\n\\n\",\n            \"width\": 6,\n            \"height\": 2,\n            \"x\": 0,\n            \"y\": 4,\n            \"type\": \"pie\",\n            \"selection\": {\n              \"index\": \"Movie\",\n              \"value\": \"Actors\",\n              \"key\": \"(none)\"\n            },\n            \"settings\": {\n              \"marginRight\": 60,\n              \"marginLeft\": 60,\n              \"marginTop\": 40,\n              \"marginBottom\": 60\n            }\n          },\n          {\n            \"title\": \"Movies by decade\",\n            \"query\": \"MATCH (m:Movie)\\nRETURN toInteger(m.released/10)*10 as Year, COUNT(m) as Total\\nORDER BY Year ASC\\n\\n\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 3,\n            \"y\": 2,\n            \"type\": \"line\",\n            \"selection\": {\n              \"x\": \"Year\",\n              \"value\": [\n                \"Total\"\n              ]\n            },\n            \"settings\": {\n              \"curve\": \"cardinal\",\n              \"marginTop\": 50\n            }\n          },\n          {\n            \"title\": \"Customized, grouped bar chart\",\n            \"query\": \"MATCH (m:Movie)<-[r]-(p:Person)\\nWHERE m.title CONTAINS \\\"Matrix\\\"\\nRETURN \\nm.title as Movie, \\ntype(r) as Role, \\nCOUNT(p) as People\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 9,\n            \"y\": 4,\n            \"type\": \"bar\",\n            \"selection\": {\n              \"index\": \"Movie\",\n              \"value\": \"People\",\n              \"key\": \"Role\"\n            },\n            \"settings\": {\n              \"showOptionalSelections\": true,\n              \"legend\": true,\n              \"marginBottom\": 100\n            }\n          }\n        ]\n      },\n      {\n        \"title\": \"Actor View\",\n        \"reports\": [\n          {\n            \"title\": \"Select a person\",\n            \"query\": \"MATCH (p:Person)--(m:Movie)\\nRETURN p.name as Person, COUNT(m) as Movies \\nORDER BY Movies DESC\\n\\n\\n\",\n            \"width\": 4,\n            \"height\": 2,\n            \"x\": 2,\n            \"y\": 0,\n            \"type\": \"table\",\n            \"selection\": {},\n            \"settings\": {\n              \"actionsRules\": [\n                {\n                  \"condition\": \"Click\",\n                  \"field\": \"Person\",\n                  \"value\": \"Person\",\n                  \"customization\": \"set variable\",\n                  \"customizationValue\": \"person_name\"\n                }\n              ]\n            }\n          },\n          {\n            \"title\": \"Graph view for this person\",\n            \"query\": \"MATCH path=(p:Person)--()\\nWHERE p.name = $neodash_person_name\\nRETURN path\\n\\n\",\n            \"width\": 6,\n            \"height\": 2,\n            \"x\": 6,\n            \"y\": 0,\n            \"type\": \"graph\",\n            \"selection\": {\n              \"Person\": \"name\",\n              \"Movie\": \"title\"\n            },\n            \"settings\": {\n              \"nodeColorScheme\": \"paired\"\n            }\n          },\n          {\n            \"title\": \"About this page\",\n            \"query\": \"On this page, you can select a person from a table, and dynamically view other visualizations update.\\n\\n\",\n            \"width\": 2,\n            \"height\": 1,\n            \"x\": 0,\n            \"y\": 0,\n            \"type\": \"text\",\n            \"selection\": {},\n            \"settings\": {\n              \"replaceGlobalParameters\": true\n            }\n          },\n          {\n            \"title\": \"Movies for the selected person\",\n            \"query\": \"MATCH path=(p)-->(m:Movie)\\nWHERE p.name = $neodash_person_name\\nRETURN m.title as Movie, m.tagline as Tagline, m.released as Released\\n\\n\",\n            \"width\": 5,\n            \"height\": 2,\n            \"x\": 0,\n            \"y\": 3,\n            \"type\": \"table\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"The highest voted movies for this person\",\n            \"query\": \"MATCH path=(p:Person)--(m:Movie)\\nWHERE p.name = $neodash_person_name\\nRETURN p, apoc.create.vRelationship(p, \\\"IN\\\", {weight: m.votes}, m), m\\n\\n\",\n            \"width\": 7,\n            \"height\": 2,\n            \"x\": 5,\n            \"y\": 2,\n            \"type\": \"sankey\",\n            \"selection\": {\n              \"Person\": \"name\",\n              \"Movie\": \"title\"\n            },\n            \"settings\": {\n              \"labelProperty\": \"weight\"\n            }\n          },\n          {\n            \"title\": \"Selected person\",\n            \"query\": \"RETURN $neodash_person_name\\n\\n\\n\",\n            \"width\": 2,\n            \"height\": 1,\n            \"x\": 0,\n            \"y\": 2,\n            \"type\": \"value\",\n            \"selection\": {},\n            \"settings\": {\n              \"fontSize\": 26\n            }\n          }\n        ]\n      }\n    ],\n    \"parameters\": {}\n  }"
  },
  {
    "path": "gallery/dashboards/recommendations.json",
    "content": "{\n    \"title\": \"NeoDash Recommendations Dashboard 🎬\",\n    \"version\": \"2.1\",\n    \"settings\": {\n      \"pagenumber\": 0,\n      \"editable\": true,\n      \"fullscreenEnabled\": false,\n      \"parameters\": {\n        \"neodash_person_name\": \"Woody Allen\"\n      },\n      \"extensions\": [\n        \"core\",\n        \"actions\"\n      ]\n    },\n    \"pages\": [\n      {\n        \"title\": \"Overview\",\n        \"reports\": [\n          {\n            \"title\": \"The Reccomendations Dashboard\",\n            \"query\": \"The *Recommendations Database* is an extension of the *Movies Database*, a great way to get to know Neo4j and Cypher. This graph contains the following data:\\n- `Person` nodes with nine properties (`name`, `born`, `born`, `died`, etc...).\\n- `Movie` nodes with seventeen properties (`title`, `plot`, `released`, `imdbRating`, etc...).\\n- `User` nodes with three properties (`degree`, `name`, `userId`).\\n- `Genre` nodes with two properties (`degree`, `name`).\\n- `Actor` and `Director` nodes that are subsets of the `Person` nodes.\\n- Two relationship types between `Person` and `Movie` (`ACTED_IN`,  `DIRECTED`).\\n- One relationship types between `User` and `Movie` (`RATED`).\\n- One relationship types between `Genre` and `Movie` (`IN_GENRE`).\\n- One relationship type between different `Movie` nodes - `SIMILAR_JACCARD`.\\n\\nThis dashboard uses the sample dataset from the Neo4j developer guide.\\n[https://neo4j.com/developer/example-data/](https://neo4j.com/developer/example-data/)\\n\\n____\\n\\n\\nYou will find two pages in this same dashboard:\\n1. An **Overview** page with general info about the graph.\\n3. An **Person view** page that lets you drill down on a specific person.\\n\\nTo inspect the Cypher behind each of the visualizations, click the (⋮) button on the top right of a report. This lets you see and edit the query.\",\n            \"width\": 6,\n            \"height\": 2,\n            \"x\": 0,\n            \"y\": 0,\n            \"type\": \"text\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Total movies\",\n            \"query\": \"MATCH (n)\\nRETURN COUNT(n) as Total\",\n            \"width\": 3,\n            \"height\": 1,\n            \"x\": 6,\n            \"y\": 0,\n            \"type\": \"value\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Total people\",\n            \"query\": \"MATCH (n:Person)\\nRETURN COUNT(n)\\n\\n\\n\",\n            \"width\": 3,\n            \"height\": 1,\n            \"x\": 9,\n            \"y\": 0,\n            \"type\": \"value\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Relationships\",\n            \"query\": \"MATCH ()-[r]->()\\nRETURN count(r)\\n\\n\\n\",\n            \"width\": 6,\n            \"height\": 1,\n            \"x\": 6,\n            \"y\": 1,\n            \"type\": \"value\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Relationship types\",\n            \"query\": \"MATCH ()-[r]->()\\nRETURN type(r) as Relationship, COUNT(r) as Total\\n\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 0,\n            \"y\": 2,\n            \"type\": \"bar\",\n            \"selection\": {\n              \"index\": \"Relationship\",\n              \"value\": \"Total\",\n              \"key\": \"(none)\"\n            },\n            \"settings\": {\n              \"marginBottom\": 80\n            }\n          },\n          {\n            \"title\": \"Browse the movies\",\n            \"query\": \"MATCH (m:Movie)\\nRETURN m.title as Movie, m.released as Year\\n\\n\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 6,\n            \"y\": 6,\n            \"type\": \"table\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"Movies with most actors\",\n            \"query\": \"MATCH (m:Movie)<-[:ACTED_IN]-(p:Person)\\nRETURN m.title as Movie, COUNT(p) as Actors\\nORDER BY Actors\\nDESC LIMIT 10\\n\\n\\n\",\n            \"width\": 6,\n            \"height\": 2,\n            \"x\": 0,\n            \"y\": 4,\n            \"type\": \"pie\",\n            \"selection\": {\n              \"index\": \"Movie\",\n              \"value\": \"Actors\",\n              \"key\": \"(none)\"\n            },\n            \"settings\": {\n              \"marginRight\": 60,\n              \"marginLeft\": 60,\n              \"marginTop\": 40,\n              \"marginBottom\": 60\n            }\n          },\n          {\n            \"title\": \"Movies by decade\",\n            \"query\": \"MATCH (m:Movie)\\nwith toInteger(substring(m.released,0,4))/10*10 as Year, COUNT(m) as Total\\nwhere Year is not null\\nreturn Year, Total\\nORDER BY Year ASC\\n\\n\\n\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 3,\n            \"y\": 2,\n            \"type\": \"line\",\n            \"selection\": {\n              \"x\": \"Year\",\n              \"value\": [\n                \"Total\"\n              ]\n            },\n            \"settings\": {\n              \"curve\": \"cardinal\",\n              \"marginTop\": 50\n            }\n          },\n          {\n            \"title\": \"Customized, grouped bar chart\",\n            \"query\": \"MATCH (m:Movie)<-[r]-(p:Person)\\nWHERE m.title CONTAINS \\\"Matrix\\\"\\nRETURN \\nm.title as Movie, \\ntype(r) as Role, \\nCOUNT(p) as People\\n\",\n            \"width\": 3,\n            \"height\": 2,\n            \"x\": 6,\n            \"y\": 2,\n            \"type\": \"bar\",\n            \"selection\": {\n              \"index\": \"Movie\",\n              \"value\": \"People\",\n              \"key\": \"Role\"\n            },\n            \"settings\": {\n              \"showOptionalSelections\": true,\n              \"legend\": true,\n              \"marginBottom\": 100\n            }\n          }\n        ]\n      },\n      {\n        \"title\": \"Actor View\",\n        \"reports\": [\n          {\n            \"title\": \"Select a person\",\n            \"query\": \"MATCH (p:Person)--(m:Movie)\\nWHERE p.name is not null\\nRETURN p.name as Person, COUNT(m) as Movies \\nORDER BY Movies DESC\\n\\n\\n\",\n            \"width\": 4,\n            \"height\": 2,\n            \"x\": 2,\n            \"y\": 0,\n            \"type\": \"table\",\n            \"selection\": {},\n            \"settings\": {\n              \"actionsRules\": [\n                {\n                  \"condition\": \"Click\",\n                  \"field\": \"Person\",\n                  \"value\": \"Person\",\n                  \"customization\": \"set variable\",\n                  \"customizationValue\": \"person_name\"\n                }\n              ]\n            }\n          },\n          {\n            \"title\": \"Graph view for this person\",\n            \"query\": \"MATCH path=(p:Person)--()\\nWHERE p.name = $neodash_person_name\\nRETURN path\\n\\n\",\n            \"width\": 6,\n            \"height\": 2,\n            \"x\": 6,\n            \"y\": 0,\n            \"type\": \"graph\",\n            \"selection\": {\n              \"Actor\": \"name\",\n              \"Director\": \"name\",\n              \"Person\": \"name\",\n              \"Movie\": \"title\"\n            },\n            \"settings\": {\n              \"nodeColorScheme\": \"paired\"\n            }\n          },\n          {\n            \"title\": \"About this page\",\n            \"query\": \"On this page, you can select a person from a table, and dynamically view other visualizations update.\\n\\n\",\n            \"width\": 2,\n            \"height\": 1,\n            \"x\": 0,\n            \"y\": 0,\n            \"type\": \"text\",\n            \"selection\": {},\n            \"settings\": {\n              \"replaceGlobalParameters\": true\n            }\n          },\n          {\n            \"title\": \"Movies for the selected person\",\n            \"query\": \"MATCH path=(p)-->(m:Movie)\\nWHERE p.name = $neodash_person_name\\nRETURN m.title as Movie, m.tagline as Tagline, m.released as Released\\n\\n\",\n            \"width\": 5,\n            \"height\": 2,\n            \"x\": 0,\n            \"y\": 3,\n            \"type\": \"table\",\n            \"selection\": {},\n            \"settings\": {}\n          },\n          {\n            \"title\": \"The highest voted movies for this person\",\n            \"query\": \"MATCH path=(p:Person)--(m:Movie)\\nWHERE p.name = $neodash_person_name\\nRETURN p, apoc.create.vRelationship(p, \\\"IN\\\", {weight: m.imdbRating}, m), m\\n\\n\",\n            \"width\": 7,\n            \"height\": 2,\n            \"x\": 5,\n            \"y\": 2,\n            \"type\": \"sankey\",\n            \"selection\": {\n              \"Actor\": \"name\",\n              \"Director\": \"name\",\n              \"Person\": \"name\",\n              \"Movie\": \"title\"\n            },\n            \"settings\": {\n              \"labelProperty\": \"weight\"\n            }\n          },\n          {\n            \"title\": \"Selected person\",\n            \"query\": \"RETURN $neodash_person_name\\n\\n\\n\",\n            \"width\": 2,\n            \"height\": 1,\n            \"x\": 0,\n            \"y\": 2,\n            \"type\": \"value\",\n            \"selection\": {},\n            \"settings\": {\n              \"fontSize\": 26\n            }\n          }\n        ]\n      }\n    ],\n    \"parameters\": {}\n  }\n"
  },
  {
    "path": "gallery/dashboards/twitter.json",
    "content": "{\n  \"title\": \"NeoDash Twitter Dashboard 📲\",\n  \"version\": \"2.1\",\n  \"settings\": {\n    \"pagenumber\": 0,\n    \"editable\": true,\n    \"fullscreenEnabled\": false,\n    \"parameters\": {\n      \"neodash_person_name\": \"Florent Biville\",\n      \"neodash_tweet_info\": {\n        \"low\": -1013501951,\n        \"high\": 311222644\n      },\n      \"neodash_tweet_url\": \"https://twitter.com/i/web/status/1352796210650886145\",\n      \"neodash_user_pic\": \"http://pbs.twimg.com/profile_images/792577726230237184/8ZSDZEvI_normal.jpg\"\n    },\n    \"extensions\": [\n      \"core\",\n      \"actions\"\n    ]\n  },\n  \"pages\": [\n    {\n      \"title\": \"Overview\",\n      \"reports\": [\n        {\n          \"title\": \"The Twitter Dashboard\",\n          \"query\": \"The *Twitter Database* is a great way to get to know Neo4j and Cypher. This graph contains the following data:\\n- `Tweet` nodes with four properties (`id`, `created_at`, `text`, `favorites`).\\n- `User` nodes with six properties (`name`, `followers`, `following`, `location`, `profile_image_url`, `screen_name`).\\n- `HashTag` nodes with the property `name`.\\n- `Link` nodes with the property `url`.\\n- `Source` nodes with the property `name`.\\n- Two relationship types between `Tweet` and `User` (`POSTS`,  `MENTIONS`).\\n- Five relationship type between different `User` nodes (`FOLLOWS`, `INTERACTS_WITH`, `RT_MENTIONS`, `AMPLIFIES`, `SIMILAR_TO`).\\n- Five relationship type between different `Tweet` nodes (`REPLY_TO`, `RETWEETS`).\\n- One relationship types between `Tweet` and `Hashtag`, `TAGS`.\\n- One relationship types between `Tweet` and `Link`, `CONTAINS`.\\n- One relationship types between `Tweet` and `Source`, `USING`.\\n\\nThis dashboard uses the sample dataset from the Neo4j developer guide.\\n[https://neo4j.com/developer/example-data/](https://neo4j.com/developer/example-data/)\\n\\n____\\n\\n\\nYou will find two pages in this same dashboard:\\n1. An **Overview** page with general info about the graph.\\n2. An **User view** page that lets you drill down on a specific person.\\n3. A **Tweet view** page that lets you drill down on a specific tweet.\\n\\nTo inspect the Cypher behind each of the visualizations, click the (⋮) button on the top right of a report. This lets you see and edit the query.\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"text\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"The entire graph\",\n          \"query\": \"// It's the entire graph!\\nMATCH (u:User)-[r:FOLLOWS]->(b)\\nRETURN *\",\n          \"width\": 6,\n          \"height\": 3,\n          \"x\": 6,\n          \"y\": 2,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"User\": \"name\",\n            \"Me\": \"name\"\n          },\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Total Tweets\",\n          \"query\": \"MATCH (n:Tweet)\\nRETURN COUNT(n) as Total\",\n          \"width\": 2,\n          \"height\": 1,\n          \"x\": 6,\n          \"y\": 0,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Total Users\",\n          \"query\": \"MATCH (n:User)\\nRETURN COUNT(n)\\n\\n\\n\",\n          \"width\": 2,\n          \"height\": 1,\n          \"x\": 8,\n          \"y\": 0,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Relationships\",\n          \"query\": \"MATCH ()-[r]->()\\nRETURN count(r)\\n\\n\\n\",\n          \"width\": 2,\n          \"height\": 1,\n          \"x\": 10,\n          \"y\": 0,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Relationship types\",\n          \"query\": \"MATCH ()-[r]->()\\nRETURN type(r) as Relationship, COUNT(r) as Total\\n\\n\",\n          \"width\": 3,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 2,\n          \"type\": \"bar\",\n          \"selection\": {\n            \"index\": \"Relationship\",\n            \"value\": \"Total\",\n            \"key\": \"(none)\"\n          },\n          \"settings\": {\n            \"marginBottom\": 80\n          }\n        },\n        {\n          \"title\": \"Browse the movies\",\n          \"query\": \"MATCH (m:Tweet)\\nRETURN m.id_str as idx, m.text as Text\\n\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 6,\n          \"y\": 5,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Users with most tweets\",\n          \"query\": \"MATCH (t:Tweet)<-[:POSTS]-(u:User)\\nRETURN u.name as User, COUNT(t) as Tweets\\nORDER BY Tweets\\nDESC LIMIT 10\\n\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 4,\n          \"type\": \"pie\",\n          \"selection\": {\n            \"index\": \"User\",\n            \"value\": \"Tweets\",\n            \"key\": \"(none)\"\n          },\n          \"settings\": {\n            \"marginRight\": 60,\n            \"marginLeft\": 60,\n            \"marginTop\": 40,\n            \"marginBottom\": 60\n          }\n        },\n        {\n          \"title\": \"Tweets by Month\",\n          \"query\": \"MATCH (t:Tweet)\\nWHERE t.created_at is not null\\nWITH datetime(t.created_at).month as nMonth, apoc.temporal.format(t.created_at, \\\"MMMM\\\") as month\\nwith nMonth, month, count(nMonth) as Total\\nRETURN nMonth, Total\\nORDER by nMonth asc\",\n          \"width\": 3,\n          \"height\": 2,\n          \"x\": 3,\n          \"y\": 2,\n          \"type\": \"line\",\n          \"selection\": {\n            \"x\": \"nMonth\",\n            \"value\": [\n              \"Total\"\n            ]\n          },\n          \"settings\": {\n            \"curve\": \"cardinal\",\n            \"marginTop\": 50\n          }\n        },\n        {\n          \"title\": \"Total Sources\",\n          \"query\": \"MATCH (n:Source)\\nRETURN COUNT(n) as Sources\",\n          \"width\": 2,\n          \"height\": 1,\n          \"x\": 6,\n          \"y\": 1,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Total Hashtags\",\n          \"query\": \"MATCH (n:Hashtag)\\nRETURN COUNT(n) as Hashtags\",\n          \"width\": 2,\n          \"height\": 1,\n          \"x\": 8,\n          \"y\": 1,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Total Links\",\n          \"query\": \"MATCH (n:Link)\\nRETURN COUNT(n) as Links\",\n          \"width\": 2,\n          \"height\": 1,\n          \"x\": 10,\n          \"y\": 1,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Tweets distribution by Hasgtag\",\n          \"query\": \"MATCH (t:Tweet)-[:TAGS]->(h:Hashtag)\\nRETURN h.name as Hashtag, COUNT(t) as Tweets\\nORDER BY Tweets\\nDESC LIMIT 10\\n\\n\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 6,\n          \"type\": \"pie\",\n          \"selection\": {\n            \"index\": \"Hashtag\",\n            \"value\": \"Tweets\",\n            \"key\": \"(none)\"\n          },\n          \"settings\": {\n            \"marginTop\": 45\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"User View\",\n      \"reports\": [\n        {\n          \"title\": \"Select an user\",\n          \"query\": \"MATCH (t:Tweet)<-[:POSTS]-(u:User)\\nRETURN u.name as User, COUNT(t) as Tweets\\nORDER BY Tweets desc\\n\\n\",\n          \"width\": 4,\n          \"height\": 2,\n          \"x\": 3,\n          \"y\": 0,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {\n            \"actionsRules\": [\n              {\n                \"condition\": \"Click\",\n                \"field\": \"User\",\n                \"value\": \"User\",\n                \"customization\": \"set variable\",\n                \"customizationValue\": \"person_name\"\n              }\n            ]\n          }\n        },\n        {\n          \"title\": \"Graph view for this user follow relationships\",\n          \"query\": \"MATCH path=(p:User)-[:FOLLOWS]-()\\nWHERE p.name = $neodash_person_name\\nRETURN path\\n\\n\",\n          \"width\": 5,\n          \"height\": 2,\n          \"x\": 7,\n          \"y\": 0,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"User\": \"name\",\n            \"Me\": \"name\"\n          },\n          \"settings\": {\n            \"nodeColorScheme\": \"paired\"\n          }\n        },\n        {\n          \"title\": \"About this page\",\n          \"query\": \"On this page, you can select a person from a table, and dynamically view other visualizations update.\\n\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"text\",\n          \"selection\": {},\n          \"settings\": {\n            \"replaceGlobalParameters\": true\n          }\n        },\n        {\n          \"title\": \"Tweets for the selected user\",\n          \"query\": \"MATCH path=(p)-->(m:Tweet)\\nWHERE p.name = $neodash_person_name\\nand m.text is not null\\nRETURN m.created_at as Creation_Date, m.favorites as Favs, m.text as Text order by Favs desc\\n\\n\",\n          \"width\": 7,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 2,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {\n            \"columnWidths\": \"[3,1,11]\"\n          }\n        },\n        {\n          \"title\": \"The highest tweeted hashtags by this user\",\n          \"query\": \"MATCH path=(p)-->(m:Tweet)-[:TAGS]->(t)\\nWHERE p.name = $neodash_person_name\\nRETURN t.name as HashTag, count(*) as Total\\n\\n\",\n          \"width\": 5,\n          \"height\": 2,\n          \"x\": 7,\n          \"y\": 3,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {\n            \"labelProperty\": \"weight\"\n          }\n        },\n        {\n          \"title\": \"Selected User\",\n          \"query\": \"RETURN $neodash_person_name\\n\\n\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 0,\n          \"y\": 1,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {\n            \"fontSize\": 26\n          }\n        }\n      ]\n    },\n    {\n      \"title\": \"Tweet View\",\n      \"reports\": [\n        {\n          \"title\": \"About this page\",\n          \"query\": \"On this page, you can select a tweet from a table, and dynamically view other visualizations update.\\n\\n\\n\\n\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"text\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Selected User\",\n          \"query\": \"RETURN $neodash_person_name\\n\\n\\n\\n\\n\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 0,\n          \"y\": 1,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {\n            \"fontSize\": 26\n          }\n        },\n        {\n          \"title\": \"Graph of Tweets for this User. (Click on a Tweet for more info)\",\n          \"query\": \"MATCH path=(p)-->(m:Tweet)\\nWHERE p.name = $neodash_person_name\\nRETURN path\\n\\n\",\n          \"width\": 5,\n          \"height\": 2,\n          \"x\": 3,\n          \"y\": 0,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"User\": \"name\",\n            \"Tweet\": \"id\"\n          },\n          \"settings\": {\n            \"actionsRules\": [\n              {\n                \"condition\": \"onNodeClick\",\n                \"field\": \"Tweet\",\n                \"value\": \"id\",\n                \"customization\": \"set variable\",\n                \"customizationValue\": \"tweet_info\"\n              },\n              {\n                \"condition\": \"onNodeClick\",\n                \"field\": \"User\",\n                \"value\": \"profile_image_url\",\n                \"customization\": \"set variable\",\n                \"customizationValue\": \"user_pic\"\n              }\n            ]\n          }\n        },\n        {\n          \"title\": \"Tweet Info\",\n          \"query\": \"MATCH path = (a:Tweet)-[b]-(c)\\nWHERE a.id = $neodash_tweet_info\\nRETURN path\\n\\n\\n\",\n          \"width\": 3,\n          \"height\": 2,\n          \"x\": 8,\n          \"y\": 0,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"Tweet\": \"id\",\n            \"User\": \"name\"\n          },\n          \"settings\": {}\n        }\n      ]\n    }\n  ],\n  \"parameters\": {}\n}\n"
  },
  {
    "path": "gallery/dashboards/wine.json",
    "content": "{\n  \"title\": \"Revue de vins par Winemag\",\n  \"version\": \"2.2\",\n  \"settings\": {\n    \"pagenumber\": 0,\n    \"editable\": true,\n    \"fullscreenEnabled\": false,\n    \"parameters\": {\n      \"neodash_wine\": \"DFJ Vinhos 2012 Paxis Red (Lisboa)\"\n    },\n    \"extensions\": [\"core\", \"actions\"],\n    \"disableRowLimiting\": true\n  },\n  \"pages\": [\n    {\n      \"title\": \"Aperçu\",\n      \"reports\": [\n        {\n          \"title\": \"Bonjour !\",\n          \"query\": \"**Analyse des notations WineMag** \\n \\nCe tableau de bord s'appuie sur des données de notations fournies par le magazine WineMag.\\n\\nNous pouvons étudier comment se répartissent les notes, l'influence des cépages sur les notes, de la note sur le prix,...\\n\\nEnfin, la dernière page fournit des recommandations sur les meilleurs rapports qualité prix !\\n\",\n          \"width\": 3,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"text\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Modèle de données\",\n          \"query\": \"CALL db.schema.visualization()\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 3,\n          \"y\": 0,\n          \"type\": \"graph\",\n          \"selection\": {\n            \"Wine\": \"(label)\",\n            \"Winery\": \"(label)\",\n            \"Designation\": \"(label)\",\n            \"Country\": \"(label)\",\n            \"Region\": \"(label)\",\n            \"Review\": \"(label)\",\n            \"Variety\": \"(label)\",\n            \"Province\": \"(label)\",\n            \"Taster\": \"(label)\"\n          },\n          \"settings\": {\n            \"nodePositions\": {}\n          }\n        },\n        {\n          \"title\": \"Répartition des vins par pays\",\n          \"query\": \"MATCH p=(n:Wine)-[:IS_FROM|PART_OF*]->(c:Country)\\nWITH DISTINCT c.iso3 as country, count(DISTINCT n) as wines\\nRETURN country, wines\\n\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 3,\n          \"y\": 2,\n          \"type\": \"choropleth\",\n          \"selection\": {\n            \"index\": \"country\",\n            \"value\": \"wines\",\n            \"key\": \"(none)\"\n          },\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Nombre de vins dégustés\",\n          \"query\": \"MATCH (n:Wine)\\nRETURN count(n)\\n\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 0,\n          \"y\": 2,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Nombre de dégustations\",\n          \"query\": \"MATCH (n:Review)\\nRETURN count(n)\\n\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 0,\n          \"y\": 3,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Nombre de vignobles\",\n          \"query\": \"MATCH (n:Winery)\\nRETURN count(n)\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 9,\n          \"y\": 0,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Exemples de vins\",\n          \"query\": \"MATCH (w:Wine)\\nRETURN w.title as Désignation\\nLIMIT 50\\n\",\n          \"width\": 3,\n          \"height\": 3,\n          \"x\": 9,\n          \"y\": 1,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {}\n        }\n      ]\n    },\n    {\n      \"title\": \"Répartition des notes\",\n      \"reports\": [\n        {\n          \"title\": \"Note moyenne\",\n          \"query\": \"MATCH (n:Review)\\nRETURN avg(n.points)\\n\\n\",\n          \"width\": 3,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"gauge\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Vins les mieux notés\",\n          \"query\": \"MATCH (c:Country)<-[:IS_FROM|PART_OF*]-(n:Wine)<--(r:Review)\\nWITH n, c, avg(r.points) as noteMoyenne\\nRETURN n.title as vin, c.name as pays, noteMoyenne\\nORDER BY noteMoyenne DESC LIMIT 100\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 3,\n          \"y\": 0,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Note moyenne par pays (nombre de notes > 100)\",\n          \"query\": \"MATCH (c:Country)<-[:IS_FROM|PART_OF*]-(n:Wine)<--(r:Review)\\nWITH c, count(distinct r) as count, avg(r.points) as averageScore, avg(n.price) as averagePrice\\nWHERE count >= 100\\nRETURN c.name as pays, count as `Nombre de notes`, averageScore AS `Note moyenne`, averagePrice AS `Prix moyen`\\nORDER BY `Note moyenne` DESC\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 2,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Cépages les mieux notés\",\n          \"query\": \"MATCH (v:Variety)<-[:HAS_VARIETY]-(:Wine)<-[:ON]-(r:Review)\\nWITH v.name AS variety, avg(r.points) as averageScore\\nRETURN variety AS `Type de vin/Cépages`, averageScore AS `Note moyenne` ORDER BY averageScore DESC\",\n          \"width\": 3,\n          \"height\": 2,\n          \"x\": 9,\n          \"y\": 0,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {}\n        },\n        {\n          \"title\": \"Evolution de la note moyenne en fonction du prix\",\n          \"query\": \"MATCH (w:Wine)<-[:ON]-(r:Review)\\nWHERE w.price IS NOT NULL AND w.price < 750\\nWITH w.price AS price, avg(r.points) as averageScore\\nRETURN price AS Prix, averageScore AS `Note moyenne`\\nORDER BY Prix\\n\",\n          \"width\": 6,\n          \"height\": 2,\n          \"x\": 6,\n          \"y\": 3,\n          \"type\": \"line\",\n          \"selection\": {\n            \"x\": \"Prix\",\n            \"value\": [\"Note moyenne\"]\n          },\n          \"settings\": {}\n        }\n      ]\n    },\n    {\n      \"title\": \"Vins recommandés\",\n      \"reports\": [\n        {\n          \"title\": \"Meilleur rapport qualité prix (note >= 90)\",\n          \"query\": \"MATCH (n:Wine)<--(r:Review)\\nWHERE n.price IS NOT NULL\\nWITH n, avg(r.points) as averageScore, n.price as price\\nWITH n, averageScore, price, averageScore/price as ratio\\nWHERE averageScore >= 90\\nRETURN 'Sélectionner' as Sélectionner, n.title AS `Vin`, price AS `Prix`\\nORDER BY ratio DESC LIMIT 100\\n\",\n          \"width\": 5,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 0,\n          \"type\": \"table\",\n          \"selection\": {},\n          \"settings\": {\n            \"actionsRules\": [\n              {\n                \"condition\": \"Click\",\n                \"field\": \"Sélectionner\",\n                \"value\": \"Vin\",\n                \"customization\": \"set variable\",\n                \"customizationValue\": \"wine\"\n              }\n            ]\n          }\n        },\n        {\n          \"title\": \"Pays\",\n          \"query\": \"MATCH (n:Wine)-[:IS_FROM|PART_OF*]->(c:Country)\\nWHERE n.title=$neodash_wine\\nRETURN DISTINCT c.name\\n\\n\",\n          \"width\": 2,\n          \"height\": 1,\n          \"x\": 5,\n          \"y\": 0,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {\n            \"fontSize\": 32\n          }\n        },\n        {\n          \"title\": \"Région\",\n          \"query\": \"MATCH (n:Wine)-[:IS_FROM]->(p)\\nWHERE n.title=$neodash_wine\\nRETURN p.name\\n\\n\",\n          \"width\": 2,\n          \"height\": 1,\n          \"x\": 7,\n          \"y\": 0,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {\n            \"fontSize\": 32\n          }\n        },\n        {\n          \"title\": \"Plus d'informations sur ce vin / Acheter\",\n          \"query\": \"https://www.winemag.com/?s=$neodash_wine\\n\\n\",\n          \"width\": 7,\n          \"height\": 3,\n          \"x\": 5,\n          \"y\": 1,\n          \"type\": \"iframe\",\n          \"selection\": {},\n          \"settings\": {\n            \"replaceGlobalParameters\": true,\n            \"passGlobalParameters\": false\n          }\n        },\n        {\n          \"title\": \"Type de vin\",\n          \"query\": \"MATCH (n:Wine)-->(d:Designation)\\nWHERE n.title=$neodash_wine\\nRETURN d.title\\nLIMIT 1\\n\",\n          \"width\": 3,\n          \"height\": 1,\n          \"x\": 9,\n          \"y\": 0,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {\n            \"fontSize\": 32\n          }\n        },\n        {\n          \"title\": \"Commentaires - $neodash_wine\",\n          \"query\": \"MATCH (n:Wine)<-[:ON]-(r:Review)\\nWHERE n.title=$neodash_wine\\nRETURN r.content AS commentaire\\n\",\n          \"width\": 5,\n          \"height\": 2,\n          \"x\": 0,\n          \"y\": 2,\n          \"type\": \"value\",\n          \"selection\": {},\n          \"settings\": {\n            \"fontSize\": 24\n          }\n        }\n      ]\n    }\n  ],\n  \"parameters\": {},\n  \"extensions\": {\n    \"advanced-charts\": true,\n    \"styling\": true,\n    \"actions\": true\n  }\n}\n"
  },
  {
    "path": "gallery/package.json",
    "content": "{\n  \"name\": \"neodash-gallery\",\n  \"version\": \"0.2\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@neo4j-ndl/base\": \"^0.8.3\",\n    \"@neo4j-ndl/react\": \"^0.8.3\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"neo4j-driver\": \"^5.0.1\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-scripts\": \"^5.0.1\",\n    \"typescript\": \"^4.4.2\",\n    \"web-vitals\": \"^2.1.0\"\n  },\n  \"scripts\": {\n    \"start\": \"DISABLE_ESLINT_PLUGIN=true react-scripts start\",\n    \"build\": \"DISABLE_ESLINT_PLUGIN=true react-scripts build\",\n    \"test\": \"DISABLE_ESLINT_PLUGIN=true react-scripts test\",\n    \"eject\": \"DISABLE_ESLINT_PLUGIN=true react-scripts eject\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"devDependencies\": {\n    \"autoprefixer\": \"^10.4.12\",\n    \"postcss\": \"^8.4.31\",\n    \"tailwindcss\": \"^3.1.8\"\n  }\n}\n"
  },
  {
    "path": "gallery/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "gallery/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"NeoDash Dashboard Gallery\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>NeoDash Dashboard Gallery</title>\n\n    <script async src=\"https://www.googletagmanager.com/gtag/js?id=G-JQ8SZ0S4TK\"></script>\n    <script>\n        window.dataLayer = window.dataLayer || [];\n        function gtag() { dataLayer.push(arguments); }\n        gtag('js', new Date());\n\n        gtag('config', 'G-JQ8SZ0S4TK');\n    </script>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "gallery/public/manifest.json",
    "content": "{\n  \"short_name\": \"NeoDash Dashboard Gallery\",\n  \"name\": \"NeoDash Dashboard Gallery\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"favicon.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"favicon.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "gallery/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "gallery/setup.md",
    "content": "## Setting up the gallery back-end\n\nThe NeoDash gallery is powered by a Neo4j Aura Enterprise instance, available at `acb5b6ae.databases.neo4j.io`.\nA set \n\n### Create a read-only user to list dashboards.\n\n```\n:use system;\n\nCREATE USER gallery SET PASSWORD 'gallery' CHANGE NOT REQUIRED;\nCREATE ROLE gallery;\nGRANT ROLE gallery TO gallery;\nGRANT ACCESS ON DATABASE neo4j TO `gallery`;\nGRANT MATCH {*} ON GRAPH neo4j NODE `_Neodash_Dashboard` TO gallery;\n```\n\n### Create read-only users for each of the use-cases.\nBill of Materials:\n```\n:use system;\n\nCREATE USER bom SET PASSWORD 'bom' CHANGE NOT REQUIRED;\nCREATE ROLE bom;\nGRANT ROLE bom TO bom;\nGRANT ACCESS ON DATABASE neo4j TO `bom`;\n\nGRANT MATCH {*} ON GRAPH neo4j NODE Component TO `bom`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Model TO `bom`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Supplier TO `bom`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP HAS TO `bom`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP SUPPLIED_BY TO `bom`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP SIMILAR TO `bom`;\n```\n\nGraph Assessment:\n```\n:use system;\n\nCREATE USER assessment SET PASSWORD 'assessment' CHANGE NOT REQUIRED;\nCREATE ROLE assessment;\nGRANT ROLE assessment TO assessment;\nGRANT ACCESS ON DATABASE neo4j TO `assessment`;\n\nGRANT MATCH {*} ON GRAPH neo4j NODE GraphAssessment TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Pillar TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Topic TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Person TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j NODE ProjectAssessment TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Customer TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Project TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Model TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Component TO `assessment`;\n\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP CONSISTS_OF TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP CONTAINS TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP ASSESSED TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP COMPLETED TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP EMPLOYEES TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP MEMBEROF TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP ASSOCIATED TO `assessment`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP HAS TO `assessment`;\n```\n\nDomain Names:\n```\n:use system;\n\nCREATE USER domains SET PASSWORD 'domains' CHANGE NOT REQUIRED;\nCREATE ROLE domains;\nGRANT ROLE domains TO domains;\nGRANT ACCESS ON DATABASE neo4j TO `domains`;\n\nGRANT MATCH {*} ON GRAPH neo4j NODE DNS TO `domains`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Site TO `domains`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Gestionnaires TO `domains`;\nGRANT MATCH {*} ON GRAPH neo4j NODE Beneficiaires TO `domains`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP HEBERGESUR TO `domains`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP GERE TO `domains`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP CLIENTDE TO `domains`;\nGRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP POSSEDE TO `domains`;\n\n\n```\n\nBitcoin:\n```\n:use system;\n\nCREATE USER bitcoin SET PASSWORD 'bitcoin' CHANGE NOT REQUIRED;\nCREATE ROLE bitcoin;\nGRANT ROLE bitcoin TO bitcoin;\nGRANT ACCESS ON DATABASE neo4j TO `bitcoin`;\n```"
  },
  {
    "path": "gallery/src/App.css",
    "content": ""
  },
  {
    "path": "gallery/src/App.tsx",
    "content": "import React from 'react';\nimport './App.css';\nimport { Button, TextInput, HeroIcon, Tag, Alert } from '@neo4j-ndl/react';\n\n// These are the read-only credentials of the public database where the gallery exists.\nconst uri = 'neo4j+s://acb5b6ae.databases.neo4j.io';\nconst user = 'gallery';\nconst password = 'gallery';\n\nasync function loadDashboards(setResults: any) {\n  // eslint-disable-next-line @typescript-eslint/no-var-requires\n  const neo4j = require('neo4j-driver');\n  const driver = neo4j.driver(uri, neo4j.auth.basic(user, password));\n  const session = driver.session();\n\n  try {\n    const result = await session.run(\n      'MATCH (n:_Neodash_Dashboard) RETURN properties(n) as entry ORDER BY entry.index ASC'\n    );\n    setResults(\n      result.records.map((r: { _fields: any }) => {\n        return r._fields[0];\n      })\n    );\n  } finally {\n    await session.close();\n  }\n  await driver.close();\n}\nfunction App() {\n  const [searchText, setSearchText] = React.useState('');\n  const [list, setList] = React.useState([]);\n  if (list.length == 0) {\n    loadDashboards(setList);\n  }\n\n  const filteredList = list.filter(\n    (item: { title: string; author: string; description: string; keywords: any }) =>\n      item.keywords &&\n      `${item.title} ${item.author} ${item.description} ${item.keywords}`\n        .toLowerCase()\n        .includes(searchText.toLowerCase())\n  );\n\n  return (\n    <div className='n-bg-neutral-20 h-100'>\n      {/* <Alert\n        title='Deprecation notice'\n        type='warning'\n        // closeable={true}\n        icon={true}\n        // onClose={() => setBannerOpen(false)}\n      >\n        This app will no longer be available in the near future. &nbsp;\n        <u>\n          <b>\n            <a target='_blank' href='https://console-preview.neo4j.io/tools/dashboards'>\n              Migrate\n            </a>\n          </b>\n        </u>\n        &nbsp;your dashboards to the Neo4j Console, or{' '}\n        <u>\n          <b>\n            <a target='_blank' href='https://github.com/neo4j-labs/neodash'>\n              visit\n            </a>\n          </b>\n        </u>{' '}\n        the NeoDash repository to run NeoDash yourself.\n      </Alert> */}\n\n      {/* Header */}\n      <div className='n-bg-neutral-10'>\n        <div className='md:container md:mx-auto m-5 p-8 '>\n          <h3 className='flex item-center justify-center'>NeoDash Dashboard Gallery 🎨</h3>\n          <p className='flex item-center justify-center'>\n            This page contains a set of sample NeoDash dashboards built on public data.\n          </p>\n          <p className='flex item-center justify-center'>\n            This gallery is created and maintained by the NeoDash community.\n          </p>\n          <div className='flex item-center justify-center p-2'>\n            <TextInput\n              label=''\n              value={searchText}\n              onChange={(e) => setSearchText(e.target.value)}\n              leftIcon={<HeroIcon iconName='SearchIcon' />}\n              placeholder='Filter Dashboards...'\n              rightIcon={<HeroIcon className='n-cursor-pointer' iconName='XIcon' />}\n            />\n          </div>\n        </div>\n      </div>\n      {/* Grid */}\n      <div className='md:container md:mx-auto n-bg-neutral-00'>\n        <div className='grid grid-cols-3 grid-flow-row gap-2'>\n          {filteredList.map((item: Record<string, string>) => {\n            return (\n              <div className='m-4 n-bg-neutral-10 n-shadow-l4'>\n                <div className=''>\n                  <h4 className='p-3 float-right'>\n                    {item.language}\n                    {item.logo ? (\n                      <a target='_blank' href={item.authorURL}>\n                        <img style={{ width: 30 }} src={item.logo}></img>\n                      </a>\n                    ) : (\n                      <></>\n                    )}\n                  </h4>\n                  <h4 className='p-3'>{item.title}</h4>\n                  <p className='p-3'>\n                    {item.description}\n                    <br />\n                    <span className='n-text-neutral-70'>\n                      Author:{' '}\n                      <a className='underline' target='_blank' href={item.authorURL}>\n                        {item.author}\n                      </a>\n                    </span>\n                  </p>\n                  <span className='mx-2'>\n                    {`${item.keywords}`.split(' ').map((k) => (\n                      <Tag className='mx-1'>{k}</Tag>\n                    ))}\n                  </span>\n                  <img width='1000' height='350' className='p-3' src={item.image}></img>\n                  <div className='m-2 flex item-center justify-center'>\n                    <a target='_blank' href={item.url}>\n                      <Button>Load</Button>\n                    </a>\n                  </div>\n                </div>\n              </div>\n            );\n          })}\n        </div>\n        {list.length == 0 ? <p className='item-center flex justify-center n-text-neutral-60'> Loading... </p> : <></>}\n        {list.length != 0 && filteredList.length == 0 ? (\n          <p className='item-center flex justify-center n-text-neutral-60'> No results. </p>\n        ) : (\n          <></>\n        )}\n      </div>\n      {/* Footer */}\n      <div className='n-bg-neutral-10'>\n        <div className='md:container md:mx-auto m-5 p-8 '>\n          <p className='flex item-center justify-center n-text-neutral-60'>\n            Want to add a dashboard to this gallery? Check out the\n            <ul>\n              <a className='mx-1 underline' href='https://github.com/neo4j-labs/neodash/tree/master/gallery'>\n                Guidelines\n              </a>\n            </ul>\n            on GitHub.\n          </p>\n          <br />\n          <code className='flex item-center justify-center n-text-neutral-40'> {'-- neodash-gallery v0.2 --'} </code>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "gallery/src/index.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nbody {\n  margin: 0;\n  height: 100%;\n  background-color: white;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n"
  },
  {
    "path": "gallery/src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport './index.css';\nimport App from './App';\nimport reportWebVitals from './reportWebVitals';\n\n// We need to include the base CSS in the root of\n// the app so all of our components can inherit the styles\nimport '@neo4j-ndl/base/lib/neo4j-ds-styles.css';\n\nconst root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);\nroot.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);\n\n// If you want to start measuring performance in your app, pass a function\n// to log results (for example: reportWebVitals(console.log))\n// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals\nreportWebVitals();\n"
  },
  {
    "path": "gallery/src/react-app-env.d.ts",
    "content": "/// <reference types=\"react-scripts\" />\n"
  },
  {
    "path": "gallery/src/reportWebVitals.ts",
    "content": "import { ReportHandler } from 'web-vitals';\n\nconst reportWebVitals = (onPerfEntry?: ReportHandler) => {\n  if (onPerfEntry && onPerfEntry instanceof Function) {\n    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {\n      getCLS(onPerfEntry);\n      getFID(onPerfEntry);\n      getFCP(onPerfEntry);\n      getLCP(onPerfEntry);\n      getTTFB(onPerfEntry);\n    });\n  }\n};\n\nexport default reportWebVitals;\n"
  },
  {
    "path": "gallery/src/setupTests.ts",
    "content": "// jest-dom adds custom jest matchers for asserting on DOM nodes.\n// allows you to do things like:\n// expect(element).toHaveTextContent(/react/i)\n// learn more: https://github.com/testing-library/jest-dom\nimport '@testing-library/jest-dom';\n"
  },
  {
    "path": "gallery/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\n    \"./src/**/*.{js,jsx,ts,tsx}\",\n  ],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n  presets: [require('@neo4j-ndl/base/lib/optimised.config')],\n  prefix: '',\n  corePlugins: {\n    preflight: false,\n  },\n}\n\n"
  },
  {
    "path": "gallery/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "k8s-deploy/neodash/.helmignore",
    "content": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation (prefixed with !). Only one pattern per line.\n.DS_Store\n# Common VCS dirs\n.git/\n.gitignore\n.bzr/\n.bzrignore\n.hg/\n.hgignore\n.svn/\n# Common backup files\n*.swp\n*.bak\n*.tmp\n*.orig\n*~\n# Various IDEs\n.project\n.idea/\n*.tmproj\n.vscode/\n"
  },
  {
    "path": "k8s-deploy/neodash/Chart.yaml",
    "content": "apiVersion: v2\nname: neodash\ndescription: A NeoDash Helm chart for Kubernetes\n\n# A chart can be either an 'application' or a 'library' chart.\n#\n# Application charts are a collection of templates that can be packaged into versioned archives\n# to be deployed.\n#\n# Library charts provide useful utilities or functions for the chart developer. They're included as\n# a dependency of application charts to inject those utilities and functions into the rendering\n# pipeline. Library charts do not define any templates and therefore cannot be deployed.\ntype: application\n\n# This is the chart version. This version number should be incremented each time you make changes\n# to the chart and its templates, including the app version.\n# Versions are expected to follow Semantic Versioning (https://semver.org/)\nversion: 1.0.0\n\n# This is the version number of the application being deployed. This version number should be\n# incremented each time you make changes to the application. Versions are not expected to\n# follow Semantic Versioning. They should reflect the version the application is using.\n# It is recommended to use it with quotes.\nappVersion: \"2.4.10\""
  },
  {
    "path": "k8s-deploy/neodash/README.md",
    "content": "# NeoDash\n\n![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.16.0](https://img.shields.io/badge/AppVersion-1.16.0-informational?style=flat-square)\n\nA NeoDash Helm chart for Kubernetes\n\n## Resources\n\nFollowing are the Kubernetes resources utilized for the NeoDash.\n\n- Deployment\n- Service\n- Ingress\n- Service Account\n- Horizontal Pod Autoscalar (HPA)\n\n## Values Configuration\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| autoscaling.enabled | bool | `false` | Enable/disable Autoscaling |\n| enable_reader_mode | bool | `true` | Enable/disable Reader mode |\n| envFromSecrets | list | `[]` | Environment variables from secrets |\n| fullnameOverride | string | `\"neodash-test\"` | Name override applies to all resources |\n| image.pullPolicy | string | `\"IfNotPresent\"` | Image pull policy |\n| image.repository | string | `\"neo4jlabs/neodash\"` |  Image repository and Image name |\n| image.tag | string | `\"latest\"` | Image version |\n| imagePullSecrets | list | `[]` | Image pull secrets if any |\n| podAnnotations | object | `{}` | Pod annotations |\n| podLabels | object | `{}` | Additional labels |\n| podSecurityContext | object | `{}` | Security Context if any |\n| ingress.annotations | object | `{}` | Ingress Annotations for load balancers |\n| ingress.className | string | `\"alb\"` | Ingress Class |\n| ingress.enabled | bool | `false` | Enable/disable Ingress |\n| ingress.hosts | list | `[]` | Host Details |\n| ingress.tls | list | `[]` | TLS details |\n| livenessProbe.httpGet.path | string | `\"/*\"` | LivenessProbe path |\n| livenessProbe.httpGet.port | int | `5005` | LivenessProbe port |\n| readinessProbe.httpGet.path | string | `\"/*\"` | Readiness path |\n| readinessProbe.httpGet.port | int | `5005` | Readiness port |\n| replicaCount | int | `1` | Replica count |\n| resources.limits.cpu | string | `\"500m\"` | CPU limit |\n| resources.limits.memory | string | `\"128Mi\"` | Memory limit |\n| resources.requests.cpu | string | `\"250m\"` | CPU request |\n| resources.requests.memory | string | `\"64Mi\"` | Memory request |\n| service.annotations | object | `{}` | Service annotations |\n| service.port | int | `5005` | Service port |\n| service.targetPort | int | `5005` | Service target port |\n| service.type | string | `\"LoadBalancer\"` | Type of service, other options are `ClusterIP` or `NodePort`  |\n| serviceAccount.automount | bool | `true` | Enable/disable service account auto mount to pod |\n| serviceAccount.create | bool | `true` | Enable/disable service account |\n| volumeMounts | list | `[]` | Volume mounts on pod |\n| volumes | list | `[]` | Volumes for pod |\n| env | list |<br><pre lang=\"YAML\">- name: \"ssoEnabled\" &#13;  value: \"false\" &#13;- name: \"standalone\" &#13;  value: \"true\" &#13;- name: \"standaloneProtocol\" &#13;  value: \"neo4j+s\" &#13;- name: \"standaloneHost\" &#13;  value: \"localhost\" &#13;- name: \"standalonePort\" &#13;  value: \"7687\" &#13;- name: \"standaloneDatabase\" &#13;  value: \"neo4j\" &#13;- name: \"standaloneDashboardName\" &#13;  value: \"test\" &#13;- name: \"standaloneDashboardDatabase\" &#13;  value: \"neo4j\" &#13;- name: \"standaloneAllowLoad\" &#13;  value: \"false\" &#13;- name: \"standaloneLoadFromOtherDatabases\" &#13;  value: \"false\" &#13;- name: \"standaloneMultiDatabase\" &#13;  value: \"false\" &#13;</pre> | Env variables for reader mode |\n\n## Usage\n\n- To install this helm chart run the following command,\n\n    ```bash\n    helm install <release-name> ./neodash -n <namespace-name>\n    ```\n\n- To upgrade the release run the following command,\n\n    ```bash\n    helm upgrade <release-name> ./neodash -n <namespace-name>\n    ```\n\n- To uninstall the release run the following command,\n\n    ```bash\n    helm uninstall <release-name> -n <namespace-name>\n    ```\n\n> **Note:** To use custom values files, pass `-f <path-to-values-file>.yaml` for the above command.\n> **Note:** To use custom values, pass `--set param=value` for the above command.\nFor example, to install neodash and set the service type to NodePort, run: `helm install <release-name> ./neodash -n <namespace-name> --set service.type=NodePort`\n"
  },
  {
    "path": "k8s-deploy/neodash/templates/NOTES.txt",
    "content": "The NeoDash application has been successfully deployed, here is the application URL:\n{{- if .Values.ingress.enabled }}\n{{- range $host := .Values.ingress.hosts }}\n  {{- range .paths }}\n  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}\n  {{- end }}\n{{- end }}\n{{- else if contains \"NodePort\" .Values.service.type }}\n  Run the following command to retrieve the IP address:\n  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath=\"{.spec.ports[0].nodePort}\" services {{ include \"neodash.fullname\" . }})\n  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath=\"{.items[0].status.addresses[0].address}\")\n  echo http://$NODE_IP:$NODE_PORT\n{{- else if contains \"LoadBalancer\" .Values.service.type }}\n     NOTE: It may take a few minutes for the LoadBalancer IP to be available.\n           You can watch the status of the LoadBalancer by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include \"neodash.fullname\" . }}'\n\n  Once available, run the following command to retrieve the IP address:\n  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include \"neodash.fullname\" . }} --template \"{{\"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}\"}}\")\n  echo http://$SERVICE_IP:{{ .Values.service.port }}\n{{- else if contains \"ClusterIP\" .Values.service.type }}\n  Run the following command to retrieve the IP address:\n  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l \"app.kubernetes.io/name={{ include \"neodash.name\" . }},app.kubernetes.io/instance={{ .Release.Name }}\" -o jsonpath=\"{.items[0].metadata.name}\")\n  export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath=\"{.spec.containers[0].ports[0].containerPort}\")\n  echo \"Visit http://127.0.0.1:8080 to use your application\"\n  kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT\n{{- end }}"
  },
  {
    "path": "k8s-deploy/neodash/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"neodash.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\nWe truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).\nIf release name contains chart name it will be used as a full name.\n*/}}\n{{- define \"neodash.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"neodash.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"neodash.labels\" -}}\nhelm.sh/chart: {{ include \"neodash.chart\" . }}\n{{ include \"neodash.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"neodash.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"neodash.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n\n{{/*\nCreate the name of the service account to use\n*/}}\n{{- define \"neodash.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.create }}\n{{- default (include \"neodash.fullname\" .) .Values.serviceAccount.name }}\n{{- else }}\n{{- default \"default\" .Values.serviceAccount.name }}\n{{- end }}\n{{- end }}\n"
  },
  {
    "path": "k8s-deploy/neodash/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"neodash.fullname\" . }}\n  labels:\n    {{- include \"neodash.labels\" . | nindent 4 }}\nspec:\n  {{- if not .Values.autoscaling.enabled }}\n  replicas: {{ .Values.replicaCount }}\n  {{- end }}\n  selector:\n    matchLabels:\n      {{- include \"neodash.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      {{- with .Values.podAnnotations }}\n      annotations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      labels:\n        {{- include \"neodash.labels\" . | nindent 8 }}\n        {{- with .Values.podLabels }}\n        {{- toYaml . | nindent 8 }}\n        {{- end }}\n    spec:\n      {{- with .Values.imagePullSecrets }}\n      imagePullSecrets:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      serviceAccountName: {{ include \"neodash.serviceAccountName\" . }}\n      automountServiceAccountToken: false\n      containers:\n        - name: {{ .Chart.Name }}\n          image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n          imagePullPolicy: {{ .Values.image.pullPolicy }}\n          {{- with .Values.podSecurityContext }}\n          securityContext:\n            {{- toYaml .Values.podSecurityContext | nindent 12 }}\n          {{- end }}\n          ports:\n            - name: http\n              containerPort: {{ .Values.service.targetPort }}\n              protocol: TCP\n          env:\n            {{- if ne 5005 (int .Values.service.targetPort) }}\n            - name: NGINX_PORT\n              value: {{ .Values.service.port | quote }}\n            {{- end }}\n            {{- if .Values.enable_reader_mode}}\n            {{- with .Values.env }}\n            {{- toYaml . | nindent 12 }}\n            {{- end }}\n            {{- if .Values.envFromSecrets }}\n            {{- range $key, $value := .Values.envFromSecrets }}\n            - name: {{ $key }}\n              valueFrom:\n                secretKeyRef:\n                  name: {{ $value.secretName }}\n                  key: {{ $value.key }}\n            {{- end }}\n            {{- end }}\n            {{- end }}\n          livenessProbe:\n            {{- toYaml .Values.livenessProbe | nindent 12 }}\n          readinessProbe:\n            {{- toYaml .Values.readinessProbe | nindent 12 }}\n          resources:\n            {{- toYaml .Values.resources | nindent 12 }}\n          {{- with .Values.volumeMounts }}\n          volumeMounts:\n            {{- toYaml . | nindent 12 }}\n          {{- end }}\n      {{- with .Values.volumes }}\n      volumes:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}"
  },
  {
    "path": "k8s-deploy/neodash/templates/hpa.yaml",
    "content": "{{- if .Values.autoscaling.enabled }}\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: {{ include \"neodash.fullname\" . }}\n  labels:\n    {{- include \"neodash.labels\" . | nindent 4 }}\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: {{ include \"neodash.fullname\" . }}\n  minReplicas: {{ .Values.autoscaling.minReplicas }}\n  maxReplicas: {{ .Values.autoscaling.maxReplicas }}\n  metrics:\n    {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}\n    {{- end }}\n    {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}\n    {{- end }}\n{{- end }}\n"
  },
  {
    "path": "k8s-deploy/neodash/templates/ingress.yaml",
    "content": "{{- if .Values.ingress.enabled -}}\n{{- $fullName := include \"neodash.fullname\" . -}}\n{{- $svcPort := .Values.service.port -}}\n{{- if and .Values.ingress.className (not (semverCompare \">=1.18-0\" .Capabilities.KubeVersion.GitVersion)) }}\n  {{- if not (hasKey .Values.ingress.annotations \"kubernetes.io/ingress.class\") }}\n  {{- $_ := set .Values.ingress.annotations \"kubernetes.io/ingress.class\" .Values.ingress.className}}\n  {{- end }}\n{{- end }}\n{{- if semverCompare \">=1.19-0\" .Capabilities.KubeVersion.GitVersion -}}\napiVersion: networking.k8s.io/v1\n{{- else if semverCompare \">=1.14-0\" .Capabilities.KubeVersion.GitVersion -}}\napiVersion: networking.k8s.io/v1beta1\n{{- else -}}\napiVersion: extensions/v1beta1\n{{- end }}\nkind: Ingress\nmetadata:\n  name: {{ $fullName }}\n  labels:\n    {{- include \"neodash.labels\" . | nindent 4 }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- if and .Values.ingress.className (semverCompare \">=1.18-0\" .Capabilities.KubeVersion.GitVersion) }}\n  ingressClassName: {{ .Values.ingress.className }}\n  {{- end }}\n  {{- if .Values.ingress.tls }}\n  tls:\n    {{- range .Values.ingress.tls }}\n    - hosts:\n        {{- range .hosts }}\n        - {{ . | quote }}\n        {{- end }}\n      secretName: {{ .secretName }}\n    {{- end }}\n  {{- end }}\n  rules:\n    {{- range .Values.ingress.hosts }}\n    - host: {{ .host | quote }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            {{- if and .pathType (semverCompare \">=1.18-0\" $.Capabilities.KubeVersion.GitVersion) }}\n            pathType: {{ .pathType }}\n            {{- end }}\n            backend:\n              {{- if semverCompare \">=1.19-0\" $.Capabilities.KubeVersion.GitVersion }}\n              service:\n                name: {{ $fullName }}\n                port:\n                  number: {{ $svcPort }}\n              {{- else }}\n              serviceName: {{ $fullName }}\n              servicePort: {{ $svcPort }}\n              {{- end }}\n          {{- end }}\n    {{- end }}\n{{- end }}\n"
  },
  {
    "path": "k8s-deploy/neodash/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"neodash.fullname\" . }}\n  labels:\n    {{- include \"neodash.labels\" . | nindent 4 }}\n  {{- with .Values.service.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: {{ .Values.service.port }}\n      targetPort: {{ .Values.service.targetPort }}\n      protocol: TCP\n      name: http\n  selector:\n    {{- include \"neodash.selectorLabels\" . | nindent 4 }}"
  },
  {
    "path": "k8s-deploy/neodash/templates/serviceaccount.yaml",
    "content": "{{- if .Values.serviceAccount.create -}}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {{ include \"neodash.serviceAccountName\" . }}\n  labels:\n    {{- include \"neodash.labels\" . | nindent 4 }}\n  {{- with .Values.serviceAccount.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nautomountServiceAccountToken: {{ .Values.serviceAccount.automount }}\n{{- end }}\n"
  },
  {
    "path": "k8s-deploy/neodash/templates/tests/test-connection.yaml",
    "content": "apiVersion: v1\nkind: Pod\nmetadata:\n  name: \"{{ include \"neodash.fullname\" . }}-test-connection\"\n  labels:\n    {{- include \"neodash.labels\" . | nindent 4 }}\n  annotations:\n    \"helm.sh/hook\": test\nspec:\n  automountServiceAccountToken: false\n  containers:\n    - name: wget\n      image: busybox\n      command: ['wget']\n      args: ['{{ include \"neodash.fullname\" . }}:{{ .Values.service.port }}']\n      resources:\n        {{- toYaml .Values.resources | nindent 8 }}\n  restartPolicy: Never\n"
  },
  {
    "path": "k8s-deploy/neodash/values.yaml",
    "content": "# Name override or full name override\nnameOverride: ''\nfullnameOverride: neodash-test\n\n# Number of pods\nreplicaCount: 1\n\n# Image Details\nimage:\n  repository: neo4jlabs/neodash\n  pullPolicy: IfNotPresent\n  tag: 'latest'\nimagePullSecrets: [] # Image pull secret if any\n\n# Pod annotations, labels and security context\npodAnnotations: {}\npodLabels: {}\npodSecurityContext: {}\n\n# Mode configuration using environment variables\n# Set reader mode environment variables when enable_reader_mode is true\nenable_reader_mode: true\nenv: \n  - name: \"ssoEnabled\"\n    value: \"false\"\n  - name: \"standalone\"\n    value: \"true\"\n  - name: \"standaloneProtocol\"\n    value: \"neo4j+s\"\n  - name: \"standaloneHost\"\n    value: \"localhost\"\n  - name: \"standalonePort\"\n    value: \"7687\"\n  - name: \"standaloneDatabase\"\n    value: neo4j\n  - name: \"standaloneDashboardName\"\n    value: \"test\"\n  - name: \"standaloneDashboardDatabase\"\n    value: neo4j\n  - name: \"standaloneAllowLoad\"\n    value: \"false\"\n  - name: \"standaloneLoadFromOtherDatabases\"\n    value: \"false\"\n  - name: \"standaloneMultiDatabase\"\n    value: \"false\"\n\n# Environment variable from secret\nenvFromSecrets: []\n  # standaloneUsername: \n      # secretName: \"neo4j-connection-secrets\"\n      # key: \"username\"\n  # standalonePassword: \n      # secretName: \"neo4j-connection-secrets\"\n      # key: \"password\"\n\n# Service details\nservice:\n  type: LoadBalancer # Can also be ClusterIP or NodePort  \n  port: 5005 # For the service to listen in for Traffic\n  targetPort: 5005 # Target port is the container port\n  annotations: {} # Service annotations for the LoadBalance\n\n# Ingress\ningress:\n  enabled: false # Enable Kubernetes Ingress\n  className: 'alb' # Class Name\n  annotations: {} # Cloud LoadBalancer annotations\n  hosts: []\n    # - host: neodash.example.com\n    #   paths:\n    #     - path: '/'\n    #       pathType: Prefix\n  tls: []\n\n# Pod resources request, limits and health check\nresources: \n  requests:\n    memory: \"64Mi\"\n    cpu: \"250m\"\n  limits:\n    memory: \"128Mi\"\n    cpu: \"500m\"\nlivenessProbe:\n  httpGet:\n    path: /*\n    port: 5005\nreadinessProbe:\n  httpGet:\n    path: /*\n    port: 5005\n\n# Pod Autoscaler\nautoscaling:\n  enabled: false\n  # minReplicas: 1\n  # maxReplicas: 100\n  # targetCPUUtilizationPercentage: 80\n\n# Pod Volumes\nvolumes: []\nvolumeMounts: []\n\n# Service Account\nserviceAccount:\n  create: true\n  automount: true\n  # annotations: {}\n  # name: ''"
  },
  {
    "path": "k8s-deploy/sample-k8s-yamls/deployment.yaml",
    "content": "---\n# Source: neodash/templates/deployment.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: neodash\n  labels:\n    application: neodash-deploy\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      application: neodash-deploy\n  template:\n    metadata:\n      labels:\n        application: neodash-deploy\n    spec:\n      serviceAccountName: neodash-test\n      automountServiceAccountToken: false\n      containers:\n        - name: neodash\n          image: \"neo4jlabs/neodash:latest\"\n          imagePullPolicy: IfNotPresent\n          ports:\n            - name: http\n              containerPort: 5005\n              protocol: TCP\n          env:\n            - name: ssoEnabled\n              value: \"false\"\n            - name: standalone\n              value: \"true\"\n            - name: standaloneProtocol\n              value: neo4j+s\n            - name: standaloneHost\n              value: localhost\n            - name: standalonePort\n              value: \"7687\"\n            - name: standaloneDatabase\n              value: neo4j\n            - name: standaloneDashboardName\n              value: test\n            - name: standaloneDashboardDatabase\n              value: neo4j\n            - name: standaloneAllowLoad\n              value: \"false\"\n            - name: standaloneLoadFromOtherDatabases\n              value: \"false\"\n            - name: standaloneMultiDatabase\n              value: \"false\"\n          livenessProbe:\n            httpGet:\n              path: /*\n              port: 5005\n          readinessProbe:\n            httpGet:\n              path: /*\n              port: 5005\n          resources:\n            limits:\n              cpu: 500m\n              memory: 128Mi\n            requests:\n              cpu: 250m\n              memory: 64Mi"
  },
  {
    "path": "k8s-deploy/sample-k8s-yamls/service.yaml",
    "content": "---\n# Source: neodash/templates/service.yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: neodash\n  labels:\n    application: neodash-deploy\nspec:\n  type: LoadBalancer\n  ports:\n    - port: 5005\n      targetPort: 5005\n      protocol: TCP\n      name: http\n  selector:\n    application: neodash-deploy"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"neodash\",\n  \"version\": \"2.4.11\",\n  \"description\": \"NeoDash - Neo4j Dashboard Builder\",\n  \"neo4jDesktop\": {\n    \"apiVersion\": \"^1.2.0\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/neo4j-labs/neodash/\"\n  },\n  \"license\": \"Apache-2.0\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"type\": \"ico\"\n    },\n    {\n      \"src\": \"favicon.png\",\n      \"type\": \"png\"\n    }\n  ],\n  \"scripts\": {\n    \"dev\": \"yarn webpack-dev-server --mode development\",\n    \"prod\": \"yarn webpack-dev-server --mode production\",\n    \"debug\": \"yarn --node-options='--inspect' webpack-dev-server --mode development\",\n    \"build\": \"yarn webpack --mode production --env production && cp -r public/* dist/\",\n    \"build-minimal\": \"yarn webpack --mode production --env production && cp -r public/* dist/\",\n    \"format\": \"prettier --write \\\"**/*.{ts,tsx}\\\"\",\n    \"lint\": \"eslint --ext .ts --ext .tsx .\",\n    \"lint-staged\": \"lint-staged --config .lintstagedrc.json\",\n    \"test\": \"yarn cypress open\",\n    \"test-headless\": \"yarn cypress run\"\n  },\n  \"keywords\": [],\n  \"author\": \"Neo4j Labs\",\n  \"dependencies\": {\n    \"@azure/openai\": \"^1.0.0-beta.2\",\n    \"@codemirror/lang-markdown\": \"^6.1.1\",\n    \"@dnd-kit/core\": \"^6.0.8\",\n    \"@dnd-kit/sortable\": \"^7.0.2\",\n    \"@mui/material\": \"^5.12.3\",\n    \"@mui/styles\": \"^5.12.3\",\n    \"@mui/x-data-grid\": \"7.4.0\",\n    \"@mui/x-date-pickers\": \"^5.0.17\",\n    \"@neo4j-cypher/react-codemirror\": \"^1.0.3\",\n    \"@neo4j-ndl/base\": \"1.10.3\",\n    \"@neo4j-ndl/react\": \"1.10.8\",\n    \"@nivo/bar\": \"^0.83.0\",\n    \"@nivo/circle-packing\": \"^0.83.0\",\n    \"@nivo/core\": \"^0.83.0\",\n    \"@nivo/geo\": \"^0.83.0\",\n    \"@nivo/line\": \"^0.83.0\",\n    \"@nivo/pie\": \"^0.83.0\",\n    \"@nivo/radar\": \"^0.83.0\",\n    \"@nivo/sankey\": \"^0.83.0\",\n    \"@nivo/scatterplot\": \"^0.83.0\",\n    \"@nivo/sunburst\": \"^0.83.0\",\n    \"@nivo/treemap\": \"^0.83.0\",\n    \"@sentry/react\": \"^7.57.0\",\n    \"@sentry/webpack-plugin\": \"^2.7.1\",\n    \"babel-runtime\": \"^6.26.0\",\n    \"chroma-js\": \"^2.4.2\",\n    \"classnames\": \"^2.3.1\",\n    \"d3-scale-chromatic\": \"^3.0.0\",\n    \"dayjs\": \"^1.11.7\",\n    \"dom-to-image\": \"^2.6.0\",\n    \"dompurify\": \"^3.1.0\",\n    \"leaflet\": \"^1.7.1\",\n    \"lodash.debounce\": \"^4.0.8\",\n    \"lodash.isequal\": \"^4.5.0\",\n    \"lodash.merge\": \"^4.6.2\",\n    \"mui-color\": \"^2.0.0-beta.2\",\n    \"mui-nested-menu\": \"^3.2.1\",\n    \"neo4j-client-sso\": \"^1.2.2\",\n    \"openai\": \"^3.3.0\",\n    \"postcss\": \"^8.4.21\",\n    \"postcss-loader\": \"^7.2.4\",\n    \"postcss-preset-env\": \"^8.3.0\",\n    \"prop-types\": \"^15.8.1\",\n    \"react\": \"^17.0.2\",\n    \"react-cool-dimensions\": \"^2.0.7\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-force-graph-2d\": \"^1.23.8\",\n    \"react-force-graph-3d\": \"^1.24.1\",\n    \"react-gauge-chart\": \"^0.4.1\",\n    \"react-grid-layout\": \"^1.3.4\",\n    \"react-leaflet\": \"^3.2.5\",\n    \"react-leaflet-cluster\": \"^1.0.4\",\n    \"react-leaflet-enhanced-marker\": \"^1.0.21\",\n    \"react-leaflet-heatmap-layer-v3\": \"^3.0.3-beta-1\",\n    \"react-markdown\": \"^8.0.0\",\n    \"react-redux\": \"^7.2.6\",\n    \"react-show-more-text\": \"^1.6.2\",\n    \"react-toggle-dark-mode\": \"^1.1.1\",\n    \"react-use-error-boundary\": \"^3.0.0\",\n    \"redux-persist\": \"^6.0.0\",\n    \"redux-thunk\": \"^2.4.1\",\n    \"remark-gfm\": \"^3.0.1\",\n    \"reselect\": \"^4.1.8\",\n    \"tailwindcss\": \"^3.3.2\",\n    \"three\": \"^0.159.0\",\n    \"three-spritetext\": \"^1.8.1\",\n    \"urijs\": \"^1.19.11\",\n    \"use-neo4j\": \"^0.3.13\",\n    \"yaml\": \"^2.2.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/cli\": \"^7.16.8\",\n    \"@babel/core\": \"^7.16.12\",\n    \"@babel/plugin-transform-runtime\": \"^7.16.10\",\n    \"@babel/preset-env\": \"^7.16.11\",\n    \"@babel/preset-react\": \"^7.16.7\",\n    \"@babel/preset-typescript\": \"^7.16.7\",\n    \"@babel/register\": \"^7.16.9\",\n    \"@cypress/code-coverage\": \"^3.10.8\",\n    \"@emotion/react\": \"^11.7.1\",\n    \"@emotion/styled\": \"^11.6.0\",\n    \"@pmmmwh/react-refresh-webpack-plugin\": \"^0.5.10\",\n    \"@redux-devtools/extension\": \"^3.2.3\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.42.0\",\n    \"@typescript-eslint/parser\": \"^5.42.0\",\n    \"babel-loader\": \"^8.2.3\",\n    \"babel-plugin-istanbul\": \"^6.1.1\",\n    \"circular-dependency-plugin\": \"^5.2.2\",\n    \"css-loader\": \"^3.6.0\",\n    \"cypress\": \"^12.17.4\",\n    \"eslint\": \"^8.26.0\",\n    \"eslint-config-prettier\": \"^8.5.0\",\n    \"eslint-plugin-import\": \"^2.26.0\",\n    \"eslint-plugin-react\": \"^7.30.1\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"file-loader\": \"^6.2.0\",\n    \"husky\": \"^8.0.1\",\n    \"lint-staged\": \"^13.0.3\",\n    \"prettier\": \"^2.7.1\",\n    \"react-refresh\": \"^0.14.0\",\n    \"serverless-finch\": \"^4.0.0\",\n    \"source-map-loader\": \"^4.0.0\",\n    \"style-loader\": \"^1.1.3\",\n    \"styled-components\": \"^5.3.3\",\n    \"typescript\": \"^4.8.4\",\n    \"webpack\": \"^5.77.0\",\n    \"webpack-cli\": \"^4.9.1\",\n    \"webpack-dev-server\": \"^4.7.3\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "const tailwindcss = require('tailwindcss');\n\nmodule.exports = {\n  plugins: ['postcss-preset-env', tailwindcss],\n};\n"
  },
  {
    "path": "public/README.md",
    "content": "# Web Content Directory\nAfter building the application with `npm run build`, deploy this folder (now renamed from `public` to `build`) to your webserver."
  },
  {
    "path": "public/config.json",
    "content": "{\n  \"ssoEnabled\": false,\n  \"ssoProviders\": [],\n  \"ssoDiscoveryUrl\": \"https://example.com\",\n  \"standalone\": false,\n  \"standaloneProtocol\": \"neo4j+s\",\n  \"standaloneHost\": \"localhost\",\n  \"standalonePort\": \"7687\",\n  \"standaloneDatabase\": \"neo4j\",\n  \"standaloneDashboardName\": \"My Dashboard\",\n  \"standaloneDashboardDatabase\": \"dashboards\",\n  \"standaloneDashboardURL\": \"\",\n  \"standaloneAllowLoad\": false,\n  \"standaloneLoadFromOtherDatabases\": false,\n  \"standaloneMultiDatabase\": false,\n  \"standaloneDatabaseList\": \"neo4j\",\n  \"loggingMode\": \"0\",\n  \"loggingDatabase\": \"logs\",\n  \"customHeader\": \"\"\n}\n"
  },
  {
    "path": "public/embed-test.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\" />\n    <title>Embed test</title>\n</head>\n\n<body>\n    <p>I am an iFrame of the page located at <a href=\"https://neodash.graphapp.io/embed-test.html\" target=\"_blank\">https://neodash.graphapp.io/embed-test.html</a></p> \n    <p>I'm embedded directly into a dashboard, and dynamically passed the user-made parameter selections.</p>\n    <p>I will not refresh when selections are updated, but, I can see variables change.</p>\n    <p>You can use me to embed external visualizations that are updated together with other charts.</p>\n    <b>Your dashboard variables:</b>\n    <pre></pre>\n    <p>\n </body>\n\n<script>\n    var pre = document.querySelector('pre');\n    \n    var applyHash = function() {\n        const hashValue = window.location.hash.substr(1);\n        var output = \"\";\n\n        searchParams = new URLSearchParams(hashValue);\n        searchParams.forEach(function(value, key) {\n            output += key + \": \" + value + \"\\n\"\n        });\n        pre.textContent = output;\n    }\n\n    window.onhashchange = applyHash;\n    applyHash();\n</script>\n</html>\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"description\" content=\"Neo4j Dashboard Builder\">\n\n    <!-- <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" /> -->\n    <!-- <link rel=\"icon\" href=\"%PUBLIC_URL%/logo192.png\" /> -->\n    <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap\"/>\n    <link rel=\"stylesheet\" href=\"style.css\"/>\n    <link rel=\"manifest\" href=\"manifest.json\">\n    <link rel=\"shortcut icon\" href=\"favicon.ico\">\n    <title>NeoDash - Neo4j Dashboard Builder</title>\n    <!--\n            manifest.json provides metadata used when your web app is installed on a\n            user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n        -->\n    <!-- <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" /> -->\n    <!--\n            Notice the use of %PUBLIC_URL% in the tags above.\n            It will be replaced with the URL of the `public` folder during the build.\n\n            Only files inside the `public` folder can be referenced from the HTML.\n            Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n\n            work correctly both with client-side routing and a non-root public URL.\n            Learn how to configure a non-root public URL by running `yarn run build`.\n        -->\n</head>\n\n<body>\n    <div id=\"overlay\"></div>\n    <div id=\"root\"></div>\n    <noscript>Please enable JavaScript to view this site.</noscript>\n    <script src=\"bundle.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n    \"short_name\": \"NeoDash\",\n    \"name\": \"NeoDash\",\n    \"homepage\": \"https://github.com/neo4j-labs/neodash/\",\n    \"neo4jDesktop\": {\n        \"apiVersion\": \"^1.2.0\"\n    },\n    \"icons\": [\n        {\n            \"src\": \"favicon.ico\",\n            \"type\": \"ico\"\n        },\n        {\n            \"src\": \"favicon.png\",\n            \"type\": \"png\"\n        }\n    ],\n    \"start_url\": \"./index.html\",\n    \"display\": \"standalone\",\n    \"theme_color\": \"#000000\",\n    \"background_color\": \"#ffffff\"\n}"
  },
  {
    "path": "public/style.config.json",
    "content": "{}\n"
  },
  {
    "path": "public/style.css",
    "content": "/* Needle */\n.logo-btn.large .ndl-icon {\n  width: 36px !important;\n  height: 36px !important;\n}\n\n.ndl-modal hr {\n  margin-top: 0.5em !important;\n  margin-bottom: 0.5em !important;\n}\n\n.centered {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n\n.MuiInputBase-root {\n  font-family: 'Nunito Sans', sans-serif !important;\n}\n/* End Needle */\n\n.MuiDrawer-paper {\n  position: relative !important;\n}\n\n.blue-grey {\n  background: #607d8b !important;\n  color: white !important;\n}\n\n.no-underline .MuiInput-underline::before {\n  border-bottom: none;\n}\n\n.MuiContainer-root {\n  padding-left: 0px !important;\n  padding-right: 0px !important;\n}\n\n.MuiFormControl-root {\n  padding-top: 0px;\n}\n\n.large input {\n  font-size: 20px;\n  margin-top: -6px;\n}\n\n.MuiChip-root {\n  border-radius: 16px !important;\n}\n\n.CodeMirror-lint-message-error {\n  background-image: none !important;\n}\n\n.leaflet-custom-node-popup {\n  margin-left: 1px;\n  margin-bottom: 34px !important;\n}\n\n.leaflet-custom-rel-popup {\n  margin-left: 0px;\n}\n\n.leaflet-marker-icon {\n  width: 50px !important;\n  margin-left: -25px !important;\n}\n\n/* Hack to make the table header smaller, TODO - clean this up */\n.table-small-header {\n  height: 36px;\n}\n\n.MuiDataGrid-root {\n  border: none !important;\n}\n\n.MuiDataGrid-footerContainer > div {\n  margin-top: -40px;\n}\n\n.MuiChip-root:before {\n  border: none !important;\n}\n\n.react-resizable-handle {\n  bottom: 4px !important;\n  right: -2px !important;\n  opacity: 0.5;\n  color: rgb(222, 222, 222);\n}\n\n.MuiDataGrid-footerContainer {\n  border: none !important;\n}\n.neodash-card-editable-false .react-resizable-handle {\n  display: none;\n}\n\n.react-grid-item > .react-resizable-handle.react-resizable-handle-se {\n  cursor: nwse-resize !important;\n}\n\n.react-grid-item > .react-resizable-handle::after {\n  border-right: 2px solid rgba(0, 0, 0, 0.4) !important;\n  border-bottom: 2px solid rgba(0, 0, 0, 0.4) !important;\n}\n\ndiv:has(> .table-small-header) {\n  background: unset !important;\n}\n\n.MuiDataGrid-footerContainer {\n  border-top: none !important;\n}\n\n.MuiTablePagination-root {\n  margin-top: -10px;\n}\n\n.MuiDataGrid-panel {\n  translate: 0px -152%;\n}\n\n.MuiCard-root {\n  box-shadow: 0 0 #0000, 0 0 #0000, var(--tw-shadow) !important;\n  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow) !important;\n}\n\n.white-text {\n  color: white !important;\n}\n.MuiCardHeader-content .MuiInputBase-root.Mui-disabled {\n  color: inherit !important;\n}\n\n.MuiCardHeader-content .MuiInput-underline.Mui-disabled:before {\n  border-bottom-style: none !important;\n}\n\n.textinput-linenumbers {\n  scrollbar-width: none;\n  resize: none;\n  background: url(linenumbers.png);\n  background-size: 20;\n  background-attachment: local;\n  background-repeat: no-repeat;\n  font-family: monospace;\n  font-size: 14px;\n  line-height: 16px;\n  border-color: #fff;\n  width: calc(100% + 30px);\n  display: block;\n  background-size: 26px;\n  white-space: pre;\n  background-position: 0px -11px;\n  padding-right: 0px !important;\n  margin-top: 0px;\n  padding-left: 30px;\n  padding-top: 0px !important;\n  padding-bottom: 0px !important;\n  margin-bottom: 0px !important;\n  overflow-x: scroll !important;\n  overflow-y: scroll;\n  min-height: 116px;\n}\n\n.textinput-linenumbers::-webkit-scrollbar {\n  display: none;\n}\n\n.MuiDrawer-docked .MuiDrawer-paper {\n  overflow-x: hidden;\n}\n\n#center-aligned {\n  text-align: center;\n}\n\n.card-view.expanded {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background: white;\n  z-index: 1299;\n}\n\n.force-graph-container .graph-tooltip {\n  color: black !important;\n  background: none !important;\n}\n\n.not-animated .react-grid-item.cssTransforms {\n  transition-property: none !important;\n}\n\n.react-grid-layout {\n  overflow-x: hidden;\n}\n\n.cm-tooltip-autocomplete {\n  margin-top: 4px;\n}\n\n.card-view .MuiTablePagination-root {\n  margin-top: 0px;\n}\n\n@keyframes pulse {\n  0% {\n    transform: scale(0.95);\n    box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7);\n  }\n\n  70% {\n    transform: scale(1);\n    box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);\n  }\n\n  100% {\n    transform: scale(0.95);\n    box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);\n  }\n}\n\n/* Workaround for Needle not handling menu placement of dropdowns on modals */\n#overlay {\n  z-index: 99 !important;\n  position: absolute;\n}\n\n/* End workaround */\n\n/* Workaround for cleaning the Gantt chart UI */\n.gantt-wrapper > div > div:first-child > div:first-child > div:first-child > div > div:not(:first-child) {\n  display: none;\n}\n.gantt-wrapper > div > div > div > div > div > div > div:not(:first-child) {\n  display: none;\n}\n/* End Gantt chart workaround */\n\n.markdown-widget a {\n  text-decoration: underline;\n}\n"
  },
  {
    "path": "release-notes.md",
    "content": "## NeoDash 2.4.11\n- Fixed deeplinking in standalone mode\n- Added deprecation notice.\n"
  },
  {
    "path": "scripts/config-entrypoint.sh",
    "content": "#!/bin/sh\n###########\nset -e \n\necho \" \\\n    { \\\n    \\\"ssoEnabled\\\": ${ssoEnabled:=false}, \\\n    \\\"ssoProviders\\\": ${ssoProviders:=[]}, \\\n    \\\"ssoDiscoveryUrl\\\": \\\"${ssoDiscoveryUrl:='https://example.com'}\\\",  \\\n    \\\"standalone\\\": ${standalone:=false}, \\\n    \\\"standaloneProtocol\\\": \\\"${standaloneProtocol:='neo4j+s'}\\\", \\\n    \\\"standaloneHost\\\": \\\"${standaloneHost:='test.databases.neo4j.io'}\\\", \\\n    \\\"standalonePort\\\": ${standalonePort:=7687}, \\\n    \\\"standaloneDatabase\\\": \\\"${standaloneDatabase:='neo4j'}\\\",  \\\n    \\\"standaloneUsername\\\": \\\"${standaloneUsername:=}\\\", \\\n    \\\"standalonePassword\\\": \\\"${standalonePassword:=}\\\", \\\n    \\\"standaloneDashboardName\\\": \\\"${standaloneDashboardName:='My Dashboard'}\\\", \\\n    \\\"standaloneDashboardDatabase\\\": \\\"${standaloneDashboardDatabase:='neo4j'}\\\",  \\\n    \\\"standaloneDashboardURL\\\": \\\"${standaloneDashboardURL:=}\\\",  \\\n    \\\"standaloneAllowLoad\\\": ${standaloneAllowLoad:=false},  \\\n    \\\"standaloneLoadFromOtherDatabases\\\": ${standaloneLoadFromOtherDatabases:=false},  \\\n    \\\"standaloneMultiDatabase\\\": ${standaloneMultiDatabase:=false}, \\\n    \\\"standaloneDatabaseList\\\": \\\"${standaloneDatabaseList:='neo4j'}\\\", \\\n    \\\"standalonePasswordWarningHidden\\\": ${standalonePasswordWarningHidden:=false},  \\\n    \\\"loggingMode\\\": \\\"${loggingMode:='0'}\\\",  \\\n    \\\"loggingDatabase\\\": \\\"${loggingDatabase:='logs'}\\\",  \\\n    \\\"customHeader\\\": \\\"${customHeader:=}\\\"  \\\n   }\" > /usr/share/nginx/html/config.json\n\necho \" \\\n  { \\\n  \\\"DASHBOARD_HEADER_BRAND_LOGO\\\": \\\"${DASHBOARD_HEADER_BRAND_LOGO:=}\\\",  \\\n  \\\"DASHBOARD_HEADER_COLOR\\\" : \\\"${DASHBOARD_HEADER_COLOR:=}\\\",  \\\n  \\\"DASHBOARD_HEADER_BUTTON_COLOR\\\" : \\\"${DASHBOARD_HEADER_BUTTON_COLOR:=}\\\",  \\\n  \\\"DASHBOARD_HEADER_TITLE_COLOR\\\" : \\\"${DASHBOARD_HEADER_TITLE_COLOR:=}\\\",  \\\n  \\\"DASHBOARD_PAGE_LIST_COLOR\\\" : \\\"${DASHBOARD_PAGE_LIST_COLOR:=}\\\", \\\n  \\\"DASHBOARD_PAGE_LIST_ACTIVE_COLOR\\\": \\\"${DASHBOARD_PAGE_LIST_ACTIVE_COLOR:=}\\\", \\\n  \\\"style\\\": { \\\n    \\\"--palette-light-neutral-bg-weak\\\": \\\"${STYLE_PALETTE_LIGHT_NEUTRAL_BG_WEAK:=}\\\" \\\n  } \\\n}\" > /usr/share/nginx/html/style.config.json\n\n\n"
  },
  {
    "path": "scripts/docker-neo4j-initializer/docker-neo4j.sh",
    "content": "docker run \\\n    --name neo4j \\\n    -p7474:7474 -p7687:7687 \\\n    -d \\\n    --env NEO4J_AUTH=neo4j/test1234 \\\n    neo4j:4.4"
  },
  {
    "path": "scripts/docker-neo4j-initializer/movies.cypher",
    "content": "CREATE CONSTRAINT IF NOT EXISTS FOR (p:Person) REQUIRE (p.name) IS UNIQUE;\nCREATE INDEX IF NOT EXISTS FOR (p:Person) ON (p.born);\nCREATE CONSTRAINT IF NOT EXISTS FOR (m:Movie) REQUIRE (m.title) IS UNIQUE;\nCREATE INDEX IF NOT EXISTS FOR (m:Movie) ON (m.released);\n\nCREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})\nCREATE (Keanu:Person {name:'Keanu Reeves', born:1964})\nCREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967})\nCREATE (Laurence:Person {name:'Laurence Fishburne', born:1961})\nCREATE (Hugo:Person {name:'Hugo Weaving', born:1960})\nCREATE (LillyW:Person {name:'Lilly Wachowski', born:1967})\nCREATE (LanaW:Person {name:'Lana Wachowski', born:1965})\nCREATE (JoelS:Person {name:'Joel Silver', born:1952})\nCREATE\n(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix),\n(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix),\n(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix),\n(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix),\n(LillyW)-[:DIRECTED]->(TheMatrix),\n(LanaW)-[:DIRECTED]->(TheMatrix),\n(JoelS)-[:PRODUCED]->(TheMatrix)\n\nCREATE (Emil:Person {name:\"Emil Eifrem\", born:1978})\nCREATE (Emil)-[:ACTED_IN {roles:[\"Emil\"]}]->(TheMatrix)\n\nCREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'})\nCREATE\n(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded),\n(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded),\n(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded),\n(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded),\n(LillyW)-[:DIRECTED]->(TheMatrixReloaded),\n(LanaW)-[:DIRECTED]->(TheMatrixReloaded),\n(JoelS)-[:PRODUCED]->(TheMatrixReloaded)\n\nCREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'})\nCREATE\n(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions),\n(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions),\n(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions),\n(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions),\n(LillyW)-[:DIRECTED]->(TheMatrixRevolutions),\n(LanaW)-[:DIRECTED]->(TheMatrixRevolutions),\n(JoelS)-[:PRODUCED]->(TheMatrixRevolutions)\n\nCREATE (TheDevilsAdvocate:Movie {title:\"The Devil's Advocate\", released:1997, tagline:'Evil has its winning ways'})\nCREATE (Charlize:Person {name:'Charlize Theron', born:1975})\nCREATE (Al:Person {name:'Al Pacino', born:1940})\nCREATE (Taylor:Person {name:'Taylor Hackford', born:1944})\nCREATE\n(Keanu)-[:ACTED_IN {roles:['Kevin Lomax']}]->(TheDevilsAdvocate),\n(Charlize)-[:ACTED_IN {roles:['Mary Ann Lomax']}]->(TheDevilsAdvocate),\n(Al)-[:ACTED_IN {roles:['John Milton']}]->(TheDevilsAdvocate),\n(Taylor)-[:DIRECTED]->(TheDevilsAdvocate)\n\nCREATE (AFewGoodMen:Movie {title:\"A Few Good Men\", released:1992, tagline:\"In the heart of the nation's capital, in a courthouse of the U.S. government, one man will stop at nothing to keep his honor, and one will stop at nothing to find the truth.\"})\nCREATE (TomC:Person {name:'Tom Cruise', born:1962})\nCREATE (JackN:Person {name:'Jack Nicholson', born:1937})\nCREATE (DemiM:Person {name:'Demi Moore', born:1962})\nCREATE (KevinB:Person {name:'Kevin Bacon', born:1958})\nCREATE (KieferS:Person {name:'Kiefer Sutherland', born:1966})\nCREATE (NoahW:Person {name:'Noah Wyle', born:1971})\nCREATE (CubaG:Person {name:'Cuba Gooding Jr.', born:1968})\nCREATE (KevinP:Person {name:'Kevin Pollak', born:1957})\nCREATE (JTW:Person {name:'J.T. Walsh', born:1943})\nCREATE (JamesM:Person {name:'James Marshall', born:1967})\nCREATE (ChristopherG:Person {name:'Christopher Guest', born:1948})\nCREATE (RobR:Person {name:'Rob Reiner', born:1947})\nCREATE (AaronS:Person {name:'Aaron Sorkin', born:1961})\nCREATE\n(TomC)-[:ACTED_IN {roles:['Lt. Daniel Kaffee']}]->(AFewGoodMen),\n(JackN)-[:ACTED_IN {roles:['Col. Nathan R. Jessup']}]->(AFewGoodMen),\n(DemiM)-[:ACTED_IN {roles:['Lt. Cdr. JoAnne Galloway']}]->(AFewGoodMen),\n(KevinB)-[:ACTED_IN {roles:['Capt. Jack Ross']}]->(AFewGoodMen),\n(KieferS)-[:ACTED_IN {roles:['Lt. Jonathan Kendrick']}]->(AFewGoodMen),\n(NoahW)-[:ACTED_IN {roles:['Cpl. Jeffrey Barnes']}]->(AFewGoodMen),\n(CubaG)-[:ACTED_IN {roles:['Cpl. Carl Hammaker']}]->(AFewGoodMen),\n(KevinP)-[:ACTED_IN {roles:['Lt. Sam Weinberg']}]->(AFewGoodMen),\n(JTW)-[:ACTED_IN {roles:['Lt. Col. Matthew Andrew Markinson']}]->(AFewGoodMen),\n(JamesM)-[:ACTED_IN {roles:['Pfc. Louden Downey']}]->(AFewGoodMen),\n(ChristopherG)-[:ACTED_IN {roles:['Dr. Stone']}]->(AFewGoodMen),\n(AaronS)-[:ACTED_IN {roles:['Man in Bar']}]->(AFewGoodMen),\n(RobR)-[:DIRECTED]->(AFewGoodMen),\n(AaronS)-[:WROTE]->(AFewGoodMen)\n\nCREATE (TopGun:Movie {title:\"Top Gun\", released:1986, tagline:'I feel the need, the need for speed.'})\nCREATE (KellyM:Person {name:'Kelly McGillis', born:1957})\nCREATE (ValK:Person {name:'Val Kilmer', born:1959})\nCREATE (AnthonyE:Person {name:'Anthony Edwards', born:1962})\nCREATE (TomS:Person {name:'Tom Skerritt', born:1933})\nCREATE (MegR:Person {name:'Meg Ryan', born:1961})\nCREATE (TonyS:Person {name:'Tony Scott', born:1944})\nCREATE (JimC:Person {name:'Jim Cash', born:1941})\nCREATE\n(TomC)-[:ACTED_IN {roles:['Maverick']}]->(TopGun),\n(KellyM)-[:ACTED_IN {roles:['Charlie']}]->(TopGun),\n(ValK)-[:ACTED_IN {roles:['Iceman']}]->(TopGun),\n(AnthonyE)-[:ACTED_IN {roles:['Goose']}]->(TopGun),\n(TomS)-[:ACTED_IN {roles:['Viper']}]->(TopGun),\n(MegR)-[:ACTED_IN {roles:['Carole']}]->(TopGun),\n(TonyS)-[:DIRECTED]->(TopGun),\n(JimC)-[:WROTE]->(TopGun)\n\nCREATE (JerryMaguire:Movie {title:'Jerry Maguire', released:2000, tagline:'The rest of his life begins now.'})\nCREATE (ReneeZ:Person {name:'Renee Zellweger', born:1969})\nCREATE (KellyP:Person {name:'Kelly Preston', born:1962})\nCREATE (JerryO:Person {name:\"Jerry O'Connell\", born:1974})\nCREATE (JayM:Person {name:'Jay Mohr', born:1970})\nCREATE (BonnieH:Person {name:'Bonnie Hunt', born:1961})\nCREATE (ReginaK:Person {name:'Regina King', born:1971})\nCREATE (JonathanL:Person {name:'Jonathan Lipnicki', born:1996})\nCREATE (CameronC:Person {name:'Cameron Crowe', born:1957})\nCREATE\n(TomC)-[:ACTED_IN {roles:['Jerry Maguire']}]->(JerryMaguire),\n(CubaG)-[:ACTED_IN {roles:['Rod Tidwell']}]->(JerryMaguire),\n(ReneeZ)-[:ACTED_IN {roles:['Dorothy Boyd']}]->(JerryMaguire),\n(KellyP)-[:ACTED_IN {roles:['Avery Bishop']}]->(JerryMaguire),\n(JerryO)-[:ACTED_IN {roles:['Frank Cushman']}]->(JerryMaguire),\n(JayM)-[:ACTED_IN {roles:['Bob Sugar']}]->(JerryMaguire),\n(BonnieH)-[:ACTED_IN {roles:['Laurel Boyd']}]->(JerryMaguire),\n(ReginaK)-[:ACTED_IN {roles:['Marcee Tidwell']}]->(JerryMaguire),\n(JonathanL)-[:ACTED_IN {roles:['Ray Boyd']}]->(JerryMaguire),\n(CameronC)-[:DIRECTED]->(JerryMaguire),\n(CameronC)-[:PRODUCED]->(JerryMaguire),\n(CameronC)-[:WROTE]->(JerryMaguire)\n\nCREATE (StandByMe:Movie {title:\"Stand By Me\", released:1986, tagline:\"For some, it's the last real taste of innocence, and the first real taste of life. But for everyone, it's the time that memories are made of.\"})\nCREATE (RiverP:Person {name:'River Phoenix', born:1970})\nCREATE (CoreyF:Person {name:'Corey Feldman', born:1971})\nCREATE (WilW:Person {name:'Wil Wheaton', born:1972})\nCREATE (JohnC:Person {name:'John Cusack', born:1966})\nCREATE (MarshallB:Person {name:'Marshall Bell', born:1942})\nCREATE\n(WilW)-[:ACTED_IN {roles:['Gordie Lachance']}]->(StandByMe),\n(RiverP)-[:ACTED_IN {roles:['Chris Chambers']}]->(StandByMe),\n(JerryO)-[:ACTED_IN {roles:['Vern Tessio']}]->(StandByMe),\n(CoreyF)-[:ACTED_IN {roles:['Teddy Duchamp']}]->(StandByMe),\n(JohnC)-[:ACTED_IN {roles:['Denny Lachance']}]->(StandByMe),\n(KieferS)-[:ACTED_IN {roles:['Ace Merrill']}]->(StandByMe),\n(MarshallB)-[:ACTED_IN {roles:['Mr. Lachance']}]->(StandByMe),\n(RobR)-[:DIRECTED]->(StandByMe)\n\nCREATE (AsGoodAsItGets:Movie {title:'As Good as It Gets', released:1997, tagline:'A comedy from the heart that goes for the throat.'})\nCREATE (HelenH:Person {name:'Helen Hunt', born:1963})\nCREATE (GregK:Person {name:'Greg Kinnear', born:1963})\nCREATE (JamesB:Person {name:'James L. Brooks', born:1940})\nCREATE\n(JackN)-[:ACTED_IN {roles:['Melvin Udall']}]->(AsGoodAsItGets),\n(HelenH)-[:ACTED_IN {roles:['Carol Connelly']}]->(AsGoodAsItGets),\n(GregK)-[:ACTED_IN {roles:['Simon Bishop']}]->(AsGoodAsItGets),\n(CubaG)-[:ACTED_IN {roles:['Frank Sachs']}]->(AsGoodAsItGets),\n(JamesB)-[:DIRECTED]->(AsGoodAsItGets)\n\nCREATE (WhatDreamsMayCome:Movie {title:'What Dreams May Come', released:1998, tagline:'After life there is more. The end is just the beginning.'})\nCREATE (AnnabellaS:Person {name:'Annabella Sciorra', born:1960})\nCREATE (MaxS:Person {name:'Max von Sydow', born:1929})\nCREATE (WernerH:Person {name:'Werner Herzog', born:1942})\nCREATE (Robin:Person {name:'Robin Williams', born:1951})\nCREATE (VincentW:Person {name:'Vincent Ward', born:1956})\nCREATE\n(Robin)-[:ACTED_IN {roles:['Chris Nielsen']}]->(WhatDreamsMayCome),\n(CubaG)-[:ACTED_IN {roles:['Albert Lewis']}]->(WhatDreamsMayCome),\n(AnnabellaS)-[:ACTED_IN {roles:['Annie Collins-Nielsen']}]->(WhatDreamsMayCome),\n(MaxS)-[:ACTED_IN {roles:['The Tracker']}]->(WhatDreamsMayCome),\n(WernerH)-[:ACTED_IN {roles:['The Face']}]->(WhatDreamsMayCome),\n(VincentW)-[:DIRECTED]->(WhatDreamsMayCome)\n\nCREATE (SnowFallingonCedars:Movie {title:'Snow Falling on Cedars', released:1999, tagline:'First loves last. Forever.'})\nCREATE (EthanH:Person {name:'Ethan Hawke', born:1970})\nCREATE (RickY:Person {name:'Rick Yune', born:1971})\nCREATE (JamesC:Person {name:'James Cromwell', born:1940})\nCREATE (ScottH:Person {name:'Scott Hicks', born:1953})\nCREATE\n(EthanH)-[:ACTED_IN {roles:['Ishmael Chambers']}]->(SnowFallingonCedars),\n(RickY)-[:ACTED_IN {roles:['Kazuo Miyamoto']}]->(SnowFallingonCedars),\n(MaxS)-[:ACTED_IN {roles:['Nels Gudmundsson']}]->(SnowFallingonCedars),\n(JamesC)-[:ACTED_IN {roles:['Judge Fielding']}]->(SnowFallingonCedars),\n(ScottH)-[:DIRECTED]->(SnowFallingonCedars)\n\nCREATE (YouveGotMail:Movie {title:\"You've Got Mail\", released:1998, tagline:'At odds in life... in love on-line.'})\nCREATE (ParkerP:Person {name:'Parker Posey', born:1968})\nCREATE (DaveC:Person {name:'Dave Chappelle', born:1973})\nCREATE (SteveZ:Person {name:'Steve Zahn', born:1967})\nCREATE (TomH:Person {name:'Tom Hanks', born:1956})\nCREATE (NoraE:Person {name:'Nora Ephron', born:1941})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Joe Fox']}]->(YouveGotMail),\n(MegR)-[:ACTED_IN {roles:['Kathleen Kelly']}]->(YouveGotMail),\n(GregK)-[:ACTED_IN {roles:['Frank Navasky']}]->(YouveGotMail),\n(ParkerP)-[:ACTED_IN {roles:['Patricia Eden']}]->(YouveGotMail),\n(DaveC)-[:ACTED_IN {roles:['Kevin Jackson']}]->(YouveGotMail),\n(SteveZ)-[:ACTED_IN {roles:['George Pappas']}]->(YouveGotMail),\n(NoraE)-[:DIRECTED]->(YouveGotMail)\n\nCREATE (SleeplessInSeattle:Movie {title:'Sleepless in Seattle', released:1993, tagline:'What if someone you never met, someone you never saw, someone you never knew was the only someone for you?'})\nCREATE (RitaW:Person {name:'Rita Wilson', born:1956})\nCREATE (BillPull:Person {name:'Bill Pullman', born:1953})\nCREATE (VictorG:Person {name:'Victor Garber', born:1949})\nCREATE (RosieO:Person {name:\"Rosie O'Donnell\", born:1962})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Sam Baldwin']}]->(SleeplessInSeattle),\n(MegR)-[:ACTED_IN {roles:['Annie Reed']}]->(SleeplessInSeattle),\n(RitaW)-[:ACTED_IN {roles:['Suzy']}]->(SleeplessInSeattle),\n(BillPull)-[:ACTED_IN {roles:['Walter']}]->(SleeplessInSeattle),\n(VictorG)-[:ACTED_IN {roles:['Greg']}]->(SleeplessInSeattle),\n(RosieO)-[:ACTED_IN {roles:['Becky']}]->(SleeplessInSeattle),\n(NoraE)-[:DIRECTED]->(SleeplessInSeattle)\n\nCREATE (JoeVersustheVolcano:Movie {title:'Joe Versus the Volcano', released:1990, tagline:'A story of love, lava and burning desire.'})\nCREATE (JohnS:Person {name:'John Patrick Stanley', born:1950})\nCREATE (Nathan:Person {name:'Nathan Lane', born:1956})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Joe Banks']}]->(JoeVersustheVolcano),\n(MegR)-[:ACTED_IN {roles:['DeDe', 'Angelica Graynamore', 'Patricia Graynamore']}]->(JoeVersustheVolcano),\n(Nathan)-[:ACTED_IN {roles:['Baw']}]->(JoeVersustheVolcano),\n(JohnS)-[:DIRECTED]->(JoeVersustheVolcano)\n\nCREATE (WhenHarryMetSally:Movie {title:'When Harry Met Sally', released:1998, tagline:'Can two friends sleep together and still love each other in the morning?'})\nCREATE (BillyC:Person {name:'Billy Crystal', born:1948})\nCREATE (CarrieF:Person {name:'Carrie Fisher', born:1956})\nCREATE (BrunoK:Person {name:'Bruno Kirby', born:1949})\nCREATE\n(BillyC)-[:ACTED_IN {roles:['Harry Burns']}]->(WhenHarryMetSally),\n(MegR)-[:ACTED_IN {roles:['Sally Albright']}]->(WhenHarryMetSally),\n(CarrieF)-[:ACTED_IN {roles:['Marie']}]->(WhenHarryMetSally),\n(BrunoK)-[:ACTED_IN {roles:['Jess']}]->(WhenHarryMetSally),\n(RobR)-[:DIRECTED]->(WhenHarryMetSally),\n(RobR)-[:PRODUCED]->(WhenHarryMetSally),\n(NoraE)-[:PRODUCED]->(WhenHarryMetSally),\n(NoraE)-[:WROTE]->(WhenHarryMetSally)\n\nCREATE (ThatThingYouDo:Movie {title:'That Thing You Do', released:1996, tagline:'In every life there comes a time when that thing you dream becomes that thing you do'})\nCREATE (LivT:Person {name:'Liv Tyler', born:1977})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Mr. White']}]->(ThatThingYouDo),\n(LivT)-[:ACTED_IN {roles:['Faye Dolan']}]->(ThatThingYouDo),\n(Charlize)-[:ACTED_IN {roles:['Tina']}]->(ThatThingYouDo),\n(TomH)-[:DIRECTED]->(ThatThingYouDo)\n\nCREATE (TheReplacements:Movie {title:'The Replacements', released:2000, tagline:'Pain heals, Chicks dig scars... Glory lasts forever'})\nCREATE (Brooke:Person {name:'Brooke Langton', born:1970})\nCREATE (Gene:Person {name:'Gene Hackman', born:1930})\nCREATE (Orlando:Person {name:'Orlando Jones', born:1968})\nCREATE (Howard:Person {name:'Howard Deutch', born:1950})\nCREATE\n(Keanu)-[:ACTED_IN {roles:['Shane Falco']}]->(TheReplacements),\n(Brooke)-[:ACTED_IN {roles:['Annabelle Farrell']}]->(TheReplacements),\n(Gene)-[:ACTED_IN {roles:['Jimmy McGinty']}]->(TheReplacements),\n(Orlando)-[:ACTED_IN {roles:['Clifford Franklin']}]->(TheReplacements),\n(Howard)-[:DIRECTED]->(TheReplacements)\n\nCREATE (RescueDawn:Movie {title:'RescueDawn', released:2006, tagline:\"Based on the extraordinary true story of one man's fight for freedom\"})\nCREATE (ChristianB:Person {name:'Christian Bale', born:1974})\nCREATE (ZachG:Person {name:'Zach Grenier', born:1954})\nCREATE\n(MarshallB)-[:ACTED_IN {roles:['Admiral']}]->(RescueDawn),\n(ChristianB)-[:ACTED_IN {roles:['Dieter Dengler']}]->(RescueDawn),\n(ZachG)-[:ACTED_IN {roles:['Squad Leader']}]->(RescueDawn),\n(SteveZ)-[:ACTED_IN {roles:['Duane']}]->(RescueDawn),\n(WernerH)-[:DIRECTED]->(RescueDawn)\n\nCREATE (TheBirdcage:Movie {title:'The Birdcage', released:1996, tagline:'Come as you are'})\nCREATE (MikeN:Person {name:'Mike Nichols', born:1931})\nCREATE\n(Robin)-[:ACTED_IN {roles:['Armand Goldman']}]->(TheBirdcage),\n(Nathan)-[:ACTED_IN {roles:['Albert Goldman']}]->(TheBirdcage),\n(Gene)-[:ACTED_IN {roles:['Sen. Kevin Keeley']}]->(TheBirdcage),\n(MikeN)-[:DIRECTED]->(TheBirdcage)\n\nCREATE (Unforgiven:Movie {title:'Unforgiven', released:1992, tagline:\"It's a hell of a thing, killing a man\"})\nCREATE (RichardH:Person {name:'Richard Harris', born:1930})\nCREATE (ClintE:Person {name:'Clint Eastwood', born:1930})\nCREATE\n(RichardH)-[:ACTED_IN {roles:['English Bob']}]->(Unforgiven),\n(ClintE)-[:ACTED_IN {roles:['Bill Munny']}]->(Unforgiven),\n(Gene)-[:ACTED_IN {roles:['Little Bill Daggett']}]->(Unforgiven),\n(ClintE)-[:DIRECTED]->(Unforgiven)\n\nCREATE (JohnnyMnemonic:Movie {title:'Johnny Mnemonic', released:1995, tagline:'The hottest data on earth. In the coolest head in town'})\nCREATE (Takeshi:Person {name:'Takeshi Kitano', born:1947})\nCREATE (Dina:Person {name:'Dina Meyer', born:1968})\nCREATE (IceT:Person {name:'Ice-T', born:1958})\nCREATE (RobertL:Person {name:'Robert Longo', born:1953})\nCREATE\n(Keanu)-[:ACTED_IN {roles:['Johnny Mnemonic']}]->(JohnnyMnemonic),\n(Takeshi)-[:ACTED_IN {roles:['Takahashi']}]->(JohnnyMnemonic),\n(Dina)-[:ACTED_IN {roles:['Jane']}]->(JohnnyMnemonic),\n(IceT)-[:ACTED_IN {roles:['J-Bone']}]->(JohnnyMnemonic),\n(RobertL)-[:DIRECTED]->(JohnnyMnemonic)\n\nCREATE (CloudAtlas:Movie {title:'Cloud Atlas', released:2012, tagline:'Everything is connected'})\nCREATE (HalleB:Person {name:'Halle Berry', born:1966})\nCREATE (JimB:Person {name:'Jim Broadbent', born:1949})\nCREATE (TomT:Person {name:'Tom Tykwer', born:1965})\nCREATE (DavidMitchell:Person {name:'David Mitchell', born:1969})\nCREATE (StefanArndt:Person {name:'Stefan Arndt', born:1961})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Zachry', 'Dr. Henry Goose', 'Isaac Sachs', 'Dermot Hoggins']}]->(CloudAtlas),\n(Hugo)-[:ACTED_IN {roles:['Bill Smoke', 'Haskell Moore', 'Tadeusz Kesselring', 'Nurse Noakes', 'Boardman Mephi', 'Old Georgie']}]->(CloudAtlas),\n(HalleB)-[:ACTED_IN {roles:['Luisa Rey', 'Jocasta Ayrs', 'Ovid', 'Meronym']}]->(CloudAtlas),\n(JimB)-[:ACTED_IN {roles:['Vyvyan Ayrs', 'Captain Molyneux', 'Timothy Cavendish']}]->(CloudAtlas),\n(TomT)-[:DIRECTED]->(CloudAtlas),\n(LillyW)-[:DIRECTED]->(CloudAtlas),\n(LanaW)-[:DIRECTED]->(CloudAtlas),\n(DavidMitchell)-[:WROTE]->(CloudAtlas),\n(StefanArndt)-[:PRODUCED]->(CloudAtlas)\n\nCREATE (TheDaVinciCode:Movie {title:'The Da Vinci Code', released:2006, tagline:'Break The Codes'})\nCREATE (IanM:Person {name:'Ian McKellen', born:1939})\nCREATE (AudreyT:Person {name:'Audrey Tautou', born:1976})\nCREATE (PaulB:Person {name:'Paul Bettany', born:1971})\nCREATE (RonH:Person {name:'Ron Howard', born:1954})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Dr. Robert Langdon']}]->(TheDaVinciCode),\n(IanM)-[:ACTED_IN {roles:['Sir Leight Teabing']}]->(TheDaVinciCode),\n(AudreyT)-[:ACTED_IN {roles:['Sophie Neveu']}]->(TheDaVinciCode),\n(PaulB)-[:ACTED_IN {roles:['Silas']}]->(TheDaVinciCode),\n(RonH)-[:DIRECTED]->(TheDaVinciCode)\n\nCREATE (VforVendetta:Movie {title:'V for Vendetta', released:2006, tagline:'Freedom! Forever!'})\nCREATE (NatalieP:Person {name:'Natalie Portman', born:1981})\nCREATE (StephenR:Person {name:'Stephen Rea', born:1946})\nCREATE (JohnH:Person {name:'John Hurt', born:1940})\nCREATE (BenM:Person {name: 'Ben Miles', born:1967})\nCREATE\n(Hugo)-[:ACTED_IN {roles:['V']}]->(VforVendetta),\n(NatalieP)-[:ACTED_IN {roles:['Evey Hammond']}]->(VforVendetta),\n(StephenR)-[:ACTED_IN {roles:['Eric Finch']}]->(VforVendetta),\n(JohnH)-[:ACTED_IN {roles:['High Chancellor Adam Sutler']}]->(VforVendetta),\n(BenM)-[:ACTED_IN {roles:['Dascomb']}]->(VforVendetta),\n(JamesM)-[:DIRECTED]->(VforVendetta),\n(LillyW)-[:PRODUCED]->(VforVendetta),\n(LanaW)-[:PRODUCED]->(VforVendetta),\n(JoelS)-[:PRODUCED]->(VforVendetta),\n(LillyW)-[:WROTE]->(VforVendetta),\n(LanaW)-[:WROTE]->(VforVendetta)\n\nCREATE (SpeedRacer:Movie {title:'Speed Racer', released:2008, tagline:'Speed has no limits'})\nCREATE (EmileH:Person {name:'Emile Hirsch', born:1985})\nCREATE (JohnG:Person {name:'John Goodman', born:1960})\nCREATE (SusanS:Person {name:'Susan Sarandon', born:1946})\nCREATE (MatthewF:Person {name:'Matthew Fox', born:1966})\nCREATE (ChristinaR:Person {name:'Christina Ricci', born:1980})\nCREATE (Rain:Person {name:'Rain', born:1982})\nCREATE\n(EmileH)-[:ACTED_IN {roles:['Speed Racer']}]->(SpeedRacer),\n(JohnG)-[:ACTED_IN {roles:['Pops']}]->(SpeedRacer),\n(SusanS)-[:ACTED_IN {roles:['Mom']}]->(SpeedRacer),\n(MatthewF)-[:ACTED_IN {roles:['Racer X']}]->(SpeedRacer),\n(ChristinaR)-[:ACTED_IN {roles:['Trixie']}]->(SpeedRacer),\n(Rain)-[:ACTED_IN {roles:['Taejo Togokahn']}]->(SpeedRacer),\n(BenM)-[:ACTED_IN {roles:['Cass Jones']}]->(SpeedRacer),\n(LillyW)-[:DIRECTED]->(SpeedRacer),\n(LanaW)-[:DIRECTED]->(SpeedRacer),\n(LillyW)-[:WROTE]->(SpeedRacer),\n(LanaW)-[:WROTE]->(SpeedRacer),\n(JoelS)-[:PRODUCED]->(SpeedRacer)\n\nCREATE (NinjaAssassin:Movie {title:'Ninja Assassin', released:2009, tagline:'Prepare to enter a secret world of assassins'})\nCREATE (NaomieH:Person {name:'Naomie Harris'})\nCREATE\n(Rain)-[:ACTED_IN {roles:['Raizo']}]->(NinjaAssassin),\n(NaomieH)-[:ACTED_IN {roles:['Mika Coretti']}]->(NinjaAssassin),\n(RickY)-[:ACTED_IN {roles:['Takeshi']}]->(NinjaAssassin),\n(BenM)-[:ACTED_IN {roles:['Ryan Maslow']}]->(NinjaAssassin),\n(JamesM)-[:DIRECTED]->(NinjaAssassin),\n(LillyW)-[:PRODUCED]->(NinjaAssassin),\n(LanaW)-[:PRODUCED]->(NinjaAssassin),\n(JoelS)-[:PRODUCED]->(NinjaAssassin)\n\nCREATE (TheGreenMile:Movie {title:'The Green Mile', released:1999, tagline:\"Walk a mile you'll never forget.\"})\nCREATE (MichaelD:Person {name:'Michael Clarke Duncan', born:1957})\nCREATE (DavidM:Person {name:'David Morse', born:1953})\nCREATE (SamR:Person {name:'Sam Rockwell', born:1968})\nCREATE (GaryS:Person {name:'Gary Sinise', born:1955})\nCREATE (PatriciaC:Person {name:'Patricia Clarkson', born:1959})\nCREATE (FrankD:Person {name:'Frank Darabont', born:1959})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Paul Edgecomb']}]->(TheGreenMile),\n(MichaelD)-[:ACTED_IN {roles:['John Coffey']}]->(TheGreenMile),\n(DavidM)-[:ACTED_IN {roles:['Brutus \"Brutal\" Howell']}]->(TheGreenMile),\n(BonnieH)-[:ACTED_IN {roles:['Jan Edgecomb']}]->(TheGreenMile),\n(JamesC)-[:ACTED_IN {roles:['Warden Hal Moores']}]->(TheGreenMile),\n(SamR)-[:ACTED_IN {roles:['\"Wild Bill\" Wharton']}]->(TheGreenMile),\n(GaryS)-[:ACTED_IN {roles:['Burt Hammersmith']}]->(TheGreenMile),\n(PatriciaC)-[:ACTED_IN {roles:['Melinda Moores']}]->(TheGreenMile),\n(FrankD)-[:DIRECTED]->(TheGreenMile)\n\nCREATE (FrostNixon:Movie {title:'Frost/Nixon', released:2008, tagline:'400 million people were waiting for the truth.'})\nCREATE (FrankL:Person {name:'Frank Langella', born:1938})\nCREATE (MichaelS:Person {name:'Michael Sheen', born:1969})\nCREATE (OliverP:Person {name:'Oliver Platt', born:1960})\nCREATE\n(FrankL)-[:ACTED_IN {roles:['Richard Nixon']}]->(FrostNixon),\n(MichaelS)-[:ACTED_IN {roles:['David Frost']}]->(FrostNixon),\n(KevinB)-[:ACTED_IN {roles:['Jack Brennan']}]->(FrostNixon),\n(OliverP)-[:ACTED_IN {roles:['Bob Zelnick']}]->(FrostNixon),\n(SamR)-[:ACTED_IN {roles:['James Reston, Jr.']}]->(FrostNixon),\n(RonH)-[:DIRECTED]->(FrostNixon)\n\nCREATE (Hoffa:Movie {title:'Hoffa', released:1992, tagline:\"He didn't want law. He wanted justice.\"})\nCREATE (DannyD:Person {name:'Danny DeVito', born:1944})\nCREATE (JohnR:Person {name:'John C. Reilly', born:1965})\nCREATE\n(JackN)-[:ACTED_IN {roles:['Hoffa']}]->(Hoffa),\n(DannyD)-[:ACTED_IN {roles:['Robert \"Bobby\" Ciaro']}]->(Hoffa),\n(JTW)-[:ACTED_IN {roles:['Frank Fitzsimmons']}]->(Hoffa),\n(JohnR)-[:ACTED_IN {roles:['Peter \"Pete\" Connelly']}]->(Hoffa),\n(DannyD)-[:DIRECTED]->(Hoffa)\n\nCREATE (Apollo13:Movie {title:'Apollo 13', released:1995, tagline:'Houston, we have a problem.'})\nCREATE (EdH:Person {name:'Ed Harris', born:1950})\nCREATE (BillPax:Person {name:'Bill Paxton', born:1955})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Jim Lovell']}]->(Apollo13),\n(KevinB)-[:ACTED_IN {roles:['Jack Swigert']}]->(Apollo13),\n(EdH)-[:ACTED_IN {roles:['Gene Kranz']}]->(Apollo13),\n(BillPax)-[:ACTED_IN {roles:['Fred Haise']}]->(Apollo13),\n(GaryS)-[:ACTED_IN {roles:['Ken Mattingly']}]->(Apollo13),\n(RonH)-[:DIRECTED]->(Apollo13)\n\nCREATE (Twister:Movie {title:'Twister', released:1996, tagline:\"Don't Breathe. Don't Look Back.\"})\nCREATE (PhilipH:Person {name:'Philip Seymour Hoffman', born:1967})\nCREATE (JanB:Person {name:'Jan de Bont', born:1943})\nCREATE\n(BillPax)-[:ACTED_IN {roles:['Bill Harding']}]->(Twister),\n(HelenH)-[:ACTED_IN {roles:['Dr. Jo Harding']}]->(Twister),\n(ZachG)-[:ACTED_IN {roles:['Eddie']}]->(Twister),\n(PhilipH)-[:ACTED_IN {roles:['Dustin \"Dusty\" Davis']}]->(Twister),\n(JanB)-[:DIRECTED]->(Twister)\n\nCREATE (CastAway:Movie {title:'Cast Away', released:2000, tagline:'At the edge of the world, his journey begins.'})\nCREATE (RobertZ:Person {name:'Robert Zemeckis', born:1951})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Chuck Noland']}]->(CastAway),\n(HelenH)-[:ACTED_IN {roles:['Kelly Frears']}]->(CastAway),\n(RobertZ)-[:DIRECTED]->(CastAway)\n\nCREATE (OneFlewOvertheCuckoosNest:Movie {title:\"One Flew Over the Cuckoo's Nest\", released:1975, tagline:\"If he's crazy, what does that make you?\"})\nCREATE (MilosF:Person {name:'Milos Forman', born:1932})\nCREATE\n(JackN)-[:ACTED_IN {roles:['Randle McMurphy']}]->(OneFlewOvertheCuckoosNest),\n(DannyD)-[:ACTED_IN {roles:['Martini']}]->(OneFlewOvertheCuckoosNest),\n(MilosF)-[:DIRECTED]->(OneFlewOvertheCuckoosNest)\n\nCREATE (SomethingsGottaGive:Movie {title:\"Something's Gotta Give\", released:2003})\nCREATE (DianeK:Person {name:'Diane Keaton', born:1946})\nCREATE (NancyM:Person {name:'Nancy Meyers', born:1949})\nCREATE\n(JackN)-[:ACTED_IN {roles:['Harry Sanborn']}]->(SomethingsGottaGive),\n(DianeK)-[:ACTED_IN {roles:['Erica Barry']}]->(SomethingsGottaGive),\n(Keanu)-[:ACTED_IN {roles:['Julian Mercer']}]->(SomethingsGottaGive),\n(NancyM)-[:DIRECTED]->(SomethingsGottaGive),\n(NancyM)-[:PRODUCED]->(SomethingsGottaGive),\n(NancyM)-[:WROTE]->(SomethingsGottaGive)\n\nCREATE (BicentennialMan:Movie {title:'Bicentennial Man', released:1999, tagline:\"One robot's 200 year journey to become an ordinary man.\"})\nCREATE (ChrisC:Person {name:'Chris Columbus', born:1958})\nCREATE\n(Robin)-[:ACTED_IN {roles:['Andrew Marin']}]->(BicentennialMan),\n(OliverP)-[:ACTED_IN {roles:['Rupert Burns']}]->(BicentennialMan),\n(ChrisC)-[:DIRECTED]->(BicentennialMan)\n\nCREATE (CharlieWilsonsWar:Movie {title:\"Charlie Wilson's War\", released:2007, tagline:\"A stiff drink. A little mascara. A lot of nerve. Who said they couldn't bring down the Soviet empire.\"})\nCREATE (JuliaR:Person {name:'Julia Roberts', born:1967})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Rep. Charlie Wilson']}]->(CharlieWilsonsWar),\n(JuliaR)-[:ACTED_IN {roles:['Joanne Herring']}]->(CharlieWilsonsWar),\n(PhilipH)-[:ACTED_IN {roles:['Gust Avrakotos']}]->(CharlieWilsonsWar),\n(MikeN)-[:DIRECTED]->(CharlieWilsonsWar)\n\nCREATE (ThePolarExpress:Movie {title:'The Polar Express', released:2004, tagline:'This Holiday Season… Believe'})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Hero Boy', 'Father', 'Conductor', 'Hobo', 'Scrooge', 'Santa Claus']}]->(ThePolarExpress),\n(RobertZ)-[:DIRECTED]->(ThePolarExpress)\n\nCREATE (ALeagueofTheirOwn:Movie {title:'A League of Their Own', released:1992, tagline:'Once in a lifetime you get a chance to do something different.'})\nCREATE (Madonna:Person {name:'Madonna', born:1954})\nCREATE (GeenaD:Person {name:'Geena Davis', born:1956})\nCREATE (LoriP:Person {name:'Lori Petty', born:1963})\nCREATE (PennyM:Person {name:'Penny Marshall', born:1943})\nCREATE\n(TomH)-[:ACTED_IN {roles:['Jimmy Dugan']}]->(ALeagueofTheirOwn),\n(GeenaD)-[:ACTED_IN {roles:['Dottie Hinson']}]->(ALeagueofTheirOwn),\n(LoriP)-[:ACTED_IN {roles:['Kit Keller']}]->(ALeagueofTheirOwn),\n(RosieO)-[:ACTED_IN {roles:['Doris Murphy']}]->(ALeagueofTheirOwn),\n(Madonna)-[:ACTED_IN {roles:['\"All the Way\" Mae Mordabito']}]->(ALeagueofTheirOwn),\n(BillPax)-[:ACTED_IN {roles:['Bob Hinson']}]->(ALeagueofTheirOwn),\n(PennyM)-[:DIRECTED]->(ALeagueofTheirOwn)\n\nCREATE (PaulBlythe:Person {name:'Paul Blythe'})\nCREATE (AngelaScope:Person {name:'Angela Scope'})\nCREATE (JessicaThompson:Person {name:'Jessica Thompson'})\nCREATE (JamesThompson:Person {name:'James Thompson'})\n\nCREATE\n(JamesThompson)-[:FOLLOWS]->(JessicaThompson),\n(AngelaScope)-[:FOLLOWS]->(JessicaThompson),\n(PaulBlythe)-[:FOLLOWS]->(AngelaScope)\n\nCREATE\n(JessicaThompson)-[:REVIEWED {summary:'An amazing journey', rating:95}]->(CloudAtlas),\n(JessicaThompson)-[:REVIEWED {summary:'Silly, but fun', rating:65}]->(TheReplacements),\n(JamesThompson)-[:REVIEWED {summary:'The coolest football movie ever', rating:100}]->(TheReplacements),\n(AngelaScope)-[:REVIEWED {summary:'Pretty funny at times', rating:62}]->(TheReplacements),\n(JessicaThompson)-[:REVIEWED {summary:'Dark, but compelling', rating:85}]->(Unforgiven),\n(JessicaThompson)-[:REVIEWED {summary:\"Slapstick redeemed only by the Robin Williams and Gene Hackman's stellar performances\", rating:45}]->(TheBirdcage),\n(JessicaThompson)-[:REVIEWED {summary:'A solid romp', rating:68}]->(TheDaVinciCode),\n(JamesThompson)-[:REVIEWED {summary:'Fun, but a little far fetched', rating:65}]->(TheDaVinciCode),\n(JessicaThompson)-[:REVIEWED {summary:'You had me at Jerry', rating:92}]->(JerryMaguire);"
  },
  {
    "path": "scripts/docker-neo4j-initializer/start-movies-db.sh",
    "content": "echo \"Loading Dataset\"\ncat ./scripts/docker-neo4j-initializer/movies.cypher | docker exec --interactive neo4j bin/cypher-shell -u neo4j -p test1234\necho \"MATCH () RETURN count(*)\" | docker exec --interactive neo4j bin/cypher-shell -u neo4j -p test1234"
  },
  {
    "path": "scripts/message-entrypoint.sh",
    "content": "#!/bin/sh\n###########\n\necho \"-----------------------------------------------------------------------------------------------------------\"\necho \"| WARNING: You are using an unmaintained version of NeoDash. Use at your own risk!                        |\"\necho \"| NeoDash is available on http://localhost:$NGINX_PORT by default.                                        |\"\necho \"| Make sure your ports are mapped correctly (-p ${NGINX_PORT}:${NGINX_PORT}) when starting the container. |\"\necho \"-----------------------------------------------------------------------------------------------------------\"\n"
  },
  {
    "path": "src/application/Application.tsx",
    "content": "import React, { Suspense, useEffect } from 'react';\nimport NeoWelcomeScreenModal from '../modal/WelcomeScreenModal';\nimport { connect } from 'react-redux';\nimport {\n  applicationGetConnection,\n  applicationGetShareDetails,\n  applicationGetOldDashboard,\n  applicationHasNeo4jDesktopConnection,\n  applicationHasAboutModalOpen,\n  applicationHasCachedDashboard,\n  applicationHasConnectionModalOpen,\n  applicationIsConnected,\n  applicationHasWelcomeScreenOpen,\n  applicationGetDebugState,\n  applicationGetStandaloneSettings,\n  applicationGetSsoSettings,\n  applicationHasReportHelpModalOpen,\n  applicationIsStandalone,\n  applicationIsDeprecated,\n} from '../application/ApplicationSelectors';\nimport {\n  createConnectionThunk,\n  createConnectionFromDesktopIntegrationThunk,\n  onConfirmLoadSharedDashboardThunk,\n  loadApplicationConfigThunk,\n} from '../application/ApplicationThunks';\nimport {\n  clearNotification,\n  resetShareDetails,\n  setAboutModalOpen,\n  setCachedSSODiscoveryUrl,\n  setConnected,\n  setConnectionModalOpen,\n  setConnectionProperties,\n  setOldDashboard,\n  setReportHelpModalOpen,\n  setWaitForSSO,\n  setWelcomeScreenOpen,\n} from '../application/ApplicationActions';\nimport { resetDashboardState } from '../dashboard/DashboardActions';\nimport { NeoDashboardPlaceholder } from '../dashboard/placeholder/DashboardPlaceholder';\nimport NeoConnectionModal from '../modal/ConnectionModal';\n\nimport { loadDashboardThunk } from '../dashboard/DashboardThunks';\nimport { downloadComponentAsImage } from '../chart/ChartUtils';\nimport '@neo4j-ndl/base/lib/neo4j-ds-styles.css';\nimport { resetSessionStorage } from '../sessionStorage/SessionStorageActions';\nimport { getDashboardTheme } from '../dashboard/DashboardSelectors';\nimport { Banner } from '@neo4j-ndl/react';\n\nconst NeoUpgradeOldDashboardModal = React.lazy(() => import('../modal/UpgradeOldDashboardModal'));\nconst NeoLoadSharedDashboardModal = React.lazy(() => import('../modal/LoadSharedDashboardModal'));\nconst NeoReportHelpModal = React.lazy(() => import('../modal/ReportHelpModal'));\nconst NeoNotificationModal = React.lazy(() => import('../modal/NotificationModal'));\nconst NeoAboutModal = React.lazy(() => import('../modal/AboutModal'));\nconst Dashboard = React.lazy(() => import('../dashboard/Dashboard'));\n\n/**\n * This is the main application component for NeoDash.\n * It contains:\n * - The Dashboard component\n * - A number of modals (pop-up windows) that handle connections, loading/saving dashboards, etc.\n *\n * Parts of the application state are retrieved here and passed to the relevant compoenents.\n * State-changing actions are also dispatched from here. See `ApplicationThunks.tsx`, `ApplicationActions.tsx` and `ApplicationSelectors.tsx` for more info.\n */\nconst Application = ({\n  connection,\n  connected,\n  hasCachedDashboard,\n  oldDashboard,\n  clearOldDashboard,\n  connectionModalOpen,\n  reportHelpModalOpen,\n  ssoSettings,\n  standalone,\n  standaloneSettings,\n  aboutModalOpen,\n  loadDashboard,\n  hasNeo4jDesktopConnection,\n  deprecated,\n  shareDetails,\n  createConnection,\n  createConnectionFromDesktopIntegration,\n  setConnectionDetails,\n  onResetShareDetails,\n  onConfirmLoadSharedDashboard,\n  initializeApplication,\n  resetDashboard,\n  onAboutModalOpen,\n  onAboutModalClose,\n  resetApplication,\n  getDebugState,\n  onReportHelpModalClose,\n  welcomeScreenOpen,\n  setWelcomeScreenOpen,\n  onConnectionModalOpen,\n  onConnectionModalClose,\n  onSSOAttempt,\n  themeMode,\n}) => {\n  const [initialized, setInitialized] = React.useState(false);\n\n  useEffect(() => {\n    if (!initialized) {\n      // Tell Neo4j Desktop to disable capturing right clicking\n      window.neo4jDesktopApi &&\n        window.neo4jDesktopApi.showMenuOnRightClick &&\n        window.neo4jDesktopApi.showMenuOnRightClick(false);\n      setInitialized(true);\n      initializeApplication(initialized);\n    }\n  }, []);\n\n  const ref = React.useRef();\n  const [bannerOpen, setBannerOpen] = React.useState(true);\n  useEffect(() => {\n    if (themeMode === 'dark') {\n      document.body.classList.add('ndl-theme-dark');\n    } else {\n      document.body.classList.remove('ndl-theme-dark');\n    }\n  }, [themeMode]);\n\n  // Only render the dashboard component if we have an active Neo4j connection.\n  return (\n    <div\n      ref={ref}\n      className={`n-bg-palette-neutral-bg-default n-h-screen n-w-screen n-flex n-flex-col n-overflow-hidden`}\n    >\n      {deprecated && bannerOpen && connected ? (\n        <Banner\n          title='Deprecation notice'\n          type='warning'\n          closeable={true}\n          icon={true}\n          onClose={() => setBannerOpen(false)}\n        >\n          This app will no longer be available in the near future. &nbsp;\n          <u>\n            <b>\n              <a target='_blank' href='https://console-preview.neo4j.io/tools/dashboards'>\n                Migrate\n              </a>\n            </b>\n          </u>\n          &nbsp;your dashboards to the Neo4j Console, or{' '}\n          <u>\n            <b>\n              <a target='_blank' href='https://github.com/neo4j-labs/neodash'>\n                visit\n              </a>\n            </b>\n          </u>{' '}\n          the NeoDash repository to run NeoDash yourself.\n        </Banner>\n      ) : (\n        <></>\n      )}\n      {connected ? (\n        <Suspense fallback=''>\n          <Dashboard\n            onDownloadDashboardAsImage={(_) => downloadComponentAsImage(ref)}\n            onAboutModalOpen={onAboutModalOpen}\n            resetApplication={resetApplication}\n          ></Dashboard>\n        </Suspense>\n      ) : (\n        <NeoDashboardPlaceholder></NeoDashboardPlaceholder>\n      )}\n      {/* TODO - move all models into a pop-ups (or modals) component. */}\n      <Suspense fallback=''>\n        <NeoAboutModal open={aboutModalOpen} handleClose={onAboutModalClose} getDebugState={getDebugState} />\n      </Suspense>\n      <NeoConnectionModal\n        open={connectionModalOpen}\n        connected={connected}\n        dismissable={!standalone}\n        connection={connection}\n        ssoSettings={ssoSettings}\n        standalone={standaloneSettings.standalone}\n        standaloneSettings={standaloneSettings}\n        createConnection={createConnection}\n        onSSOAttempt={onSSOAttempt}\n        setConnectionProperties={setConnectionDetails}\n        onConnectionModalClose={onConnectionModalClose}\n        setWelcomeScreenOpen={setWelcomeScreenOpen}\n      ></NeoConnectionModal>\n      <NeoWelcomeScreenModal\n        welcomeScreenOpen={welcomeScreenOpen}\n        setWelcomeScreenOpen={setWelcomeScreenOpen}\n        hasCachedDashboard={hasCachedDashboard}\n        hasNeo4jDesktopConnection={hasNeo4jDesktopConnection}\n        onConnectionModalOpen={onConnectionModalOpen}\n        createConnectionFromDesktopIntegration={createConnectionFromDesktopIntegration}\n        onAboutModalOpen={onAboutModalOpen}\n        resetDashboard={resetDashboard}\n      ></NeoWelcomeScreenModal>\n      <Suspense fallback=''>\n        <NeoUpgradeOldDashboardModal\n          open={oldDashboard}\n          text={oldDashboard}\n          loadDashboard={loadDashboard}\n          clearOldDashboard={clearOldDashboard}\n        />\n      </Suspense>\n      <Suspense fallback=''>\n        <NeoLoadSharedDashboardModal\n          shareDetails={shareDetails}\n          onResetShareDetails={onResetShareDetails}\n          onConfirmLoadSharedDashboard={onConfirmLoadSharedDashboard}\n        />\n      </Suspense>\n      <Suspense fallback=''>\n        <NeoReportHelpModal open={reportHelpModalOpen} handleClose={onReportHelpModalClose} />\n      </Suspense>\n      <Suspense fallback=''>\n        <NeoNotificationModal></NeoNotificationModal>\n      </Suspense>\n    </div>\n  );\n};\n\nconst mapStateToProps = (state) => ({\n  connected: applicationIsConnected(state),\n  connection: applicationGetConnection(state),\n  shareDetails: applicationGetShareDetails(state),\n  oldDashboard: applicationGetOldDashboard(state),\n  ssoSettings: applicationGetSsoSettings(state),\n  standalone: applicationIsStandalone(state),\n  standaloneSettings: applicationGetStandaloneSettings(state),\n  connectionModalOpen: applicationHasConnectionModalOpen(state),\n  aboutModalOpen: applicationHasAboutModalOpen(state),\n  reportHelpModalOpen: applicationHasReportHelpModalOpen(state),\n  welcomeScreenOpen: applicationHasWelcomeScreenOpen(state),\n  hasCachedDashboard: applicationHasCachedDashboard(state),\n  deprecated: applicationIsDeprecated(state),\n  getDebugState: () => {\n    return applicationGetDebugState(state);\n  }, // TODO - change this to be variable instead of a function?\n  hasNeo4jDesktopConnection: applicationHasNeo4jDesktopConnection(state),\n  themeMode: getDashboardTheme(state),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  createConnection: (protocol, url, port, database, username, password) => {\n    dispatch(setConnected(false));\n    dispatch(resetSessionStorage());\n    dispatch(createConnectionThunk(protocol, url, port, database, username, password));\n  },\n  createConnectionFromDesktopIntegration: () => {\n    dispatch(setConnected(false));\n    dispatch(createConnectionFromDesktopIntegrationThunk());\n  },\n  loadDashboard: (uuid, text) => {\n    dispatch(clearNotification());\n    dispatch(loadDashboardThunk(uuid, text));\n  },\n  resetDashboard: () => dispatch(resetDashboardState()),\n  clearOldDashboard: () => dispatch(setOldDashboard(null)),\n  initializeApplication: (initialized) => {\n    if (!initialized) {\n      dispatch(loadApplicationConfigThunk());\n    }\n  },\n  onResetShareDetails: (_) => {\n    dispatch(setWelcomeScreenOpen(true));\n    dispatch(resetShareDetails());\n  },\n  onSSOAttempt: (discoveryUrlValidated) => {\n    dispatch(setWaitForSSO(true));\n    dispatch(setCachedSSODiscoveryUrl(discoveryUrlValidated));\n  },\n  setConnectionDetails: (protocol, url, port, database, username, password) => {\n    dispatch(setConnectionProperties(protocol, url, port, database, username, password));\n  },\n  onConfirmLoadSharedDashboard: (_) => dispatch(onConfirmLoadSharedDashboardThunk()),\n  onConnectionModalOpen: (_) => dispatch(setConnectionModalOpen(true)),\n  onConnectionModalClose: (_) => dispatch(setConnectionModalOpen(false)),\n  onReportHelpModalClose: (_) => dispatch(setReportHelpModalOpen(false)),\n  onAboutModalOpen: (_) => dispatch(setAboutModalOpen(true)),\n  setWelcomeScreenOpen: (open) => dispatch(setWelcomeScreenOpen(open)),\n  onAboutModalClose: (_) => dispatch(setAboutModalOpen(false)),\n  resetApplication: () => {\n    dispatch(setWelcomeScreenOpen(true));\n    dispatch(setConnected(false));\n  },\n});\n\nApplication.displayName = 'Application';\n\nexport default connect(mapStateToProps, mapDispatchToProps)(Application);\n"
  },
  {
    "path": "src/application/ApplicationActions.ts",
    "content": "/**\n * This file contains all state-changing actions relevant for the main application.\n */\n\nexport const CLEAR_NOTIFICATION = 'APPLICATION/CLEAR_NOTIFICATION';\nexport const clearNotification = () => ({\n  type: CLEAR_NOTIFICATION,\n  payload: {},\n});\n\nexport const CREATE_NOTIFICATION = 'APPLICATION/CREATE_NOTIFICATION';\nexport const createNotification = (title: any, message: any) => ({\n  type: CREATE_NOTIFICATION,\n  payload: { title, message },\n});\n\nexport const SET_CONNECTED = 'APPLICATION/SET_CONNECTED';\nexport const setConnected = (connected: boolean) => ({\n  type: SET_CONNECTED,\n  payload: { connected },\n});\n\nexport const SET_DRAFT = 'APPLICATION/SET_DRAFT';\nexport const setDraft = (draft: boolean) => ({\n  type: SET_DRAFT,\n  payload: { draft },\n});\n\nexport const SET_CONNECTION_MODAL_OPEN = 'APPLICATION/SET_CONNECTION_MODAL_OPEN';\nexport const setConnectionModalOpen = (open: boolean) => ({\n  type: SET_CONNECTION_MODAL_OPEN,\n  payload: { open },\n});\n\nexport const SET_ABOUT_MODAL_OPEN = 'APPLICATION/SET_ABOUT_MODAL_OPEN';\nexport const setAboutModalOpen = (open: boolean) => ({\n  type: SET_ABOUT_MODAL_OPEN,\n  payload: { open },\n});\n\nexport const SET_REPORT_HELP_MODAL_OPEN = 'APPLICATION/SET_REPORT_HELP_MODAL_OPEN';\nexport const setReportHelpModalOpen = (open: boolean) => ({\n  type: SET_REPORT_HELP_MODAL_OPEN,\n  payload: { open },\n});\n\nexport const SET_WELCOME_SCREEN_OPEN = 'APPLICATION/SET_WELCOME_SCREEN_OPEN';\nexport const setWelcomeScreenOpen = (open: boolean) => ({\n  type: SET_WELCOME_SCREEN_OPEN,\n  payload: { open },\n});\nexport const SET_CONNECTION_PROPERTIES = 'APPLICATION/SET_CONNECTION_PROPERTIES';\nexport const setConnectionProperties = (\n  protocol: string,\n  url: string,\n  port: string,\n  database: string,\n  username: string,\n  password: string\n) => ({\n  type: SET_CONNECTION_PROPERTIES,\n  payload: { protocol, url, port, database, username, password },\n});\n\nexport const SET_BASIC_CONNECTION_PROPERTIES = 'APPLICATION/SET_BASIC_CONNECTION_PROPERTIES';\nexport const setBasicConnectionProperties = (\n  protocol: string,\n  url: string,\n  port: string,\n  database: string,\n  username: string,\n  password: string\n) => ({\n  type: SET_CONNECTION_PROPERTIES,\n  payload: { protocol, url, port, database, username, password },\n});\n\nexport const SET_DESKTOP_CONNECTION_PROPERTIES = 'APPLICATION/SET_DESKTOP_CONNECTION_PROPERTIES';\nexport const setDesktopConnectionProperties = (\n  protocol: string,\n  url: string,\n  port: string,\n  database: string,\n  username: string,\n  password: string\n) => ({\n  type: SET_DESKTOP_CONNECTION_PROPERTIES,\n  payload: { protocol, url, port, database, username, password },\n});\n\nexport const CLEAR_DESKTOP_CONNECTION_PROPERTIES = 'APPLICATION/CLEAR_DESKTOP_CONNECTION_PROPERTIES';\nexport const clearDesktopConnectionProperties = () => ({\n  type: CLEAR_DESKTOP_CONNECTION_PROPERTIES,\n  payload: {},\n});\n\n// Legacy pre1-v2 dashboard that can be optionally upgraded.\nexport const SET_OLD_DASHBOARD = 'APPLICATION/SET_OLD_DASHBOARD';\nexport const setOldDashboard = (text: string) => ({\n  type: SET_OLD_DASHBOARD,\n  payload: { text },\n});\n\n// Legacy pre1-v2 dashboard that can be optionally upgraded.\nexport const RESET_SHARE_DETAILS = 'APPLICATION/RESET_SHARE_DETAILS';\nexport const resetShareDetails = () => ({\n  type: RESET_SHARE_DETAILS,\n  payload: {},\n});\n\nexport const SET_SHARE_DETAILS_FROM_URL = 'APPLICATION/SET_SHARE_DETAILS_FROM_URL';\nexport const setShareDetailsFromUrl = (\n  type: string,\n  id: string,\n  standalone: boolean,\n  protocol: string,\n  url: string,\n  port: string,\n  database: string,\n  username: string,\n  password: string,\n  dashboardDatabase: string,\n  skipConfirmation: boolean\n) => ({\n  type: SET_SHARE_DETAILS_FROM_URL,\n  payload: {\n    type,\n    id,\n    standalone,\n    protocol,\n    url,\n    port,\n    database,\n    username,\n    password,\n    dashboardDatabase,\n    skipConfirmation,\n  },\n});\n\nexport const SET_STANDALONE_ENABLED = 'APPLICATION/SET_STANDALONE_ENABLED';\nexport const setStandaloneEnabled = (\n  standalone: boolean,\n  standaloneProtocol: string,\n  standaloneHost: string,\n  standalonePort: string,\n  standaloneDatabase: string,\n  standaloneDashboardName: string,\n  standaloneDashboardDatabase: string,\n  standaloneDashboardURL: string,\n  standaloneUsername: string,\n  standalonePassword: string,\n  standalonePasswordWarningHidden: boolean,\n  standaloneAllowLoad: boolean,\n  standaloneLoadFromOtherDatabases: boolean,\n  standaloneMultiDatabase: boolean,\n  standaloneDatabaseList: string\n) => ({\n  type: SET_STANDALONE_ENABLED,\n  payload: {\n    standalone,\n    standaloneProtocol,\n    standaloneHost,\n    standalonePort,\n    standaloneDatabase,\n    standaloneDashboardName,\n    standaloneDashboardDatabase,\n    standaloneDashboardURL,\n    standaloneUsername,\n    standalonePassword,\n    standalonePasswordWarningHidden,\n    standaloneAllowLoad,\n    standaloneLoadFromOtherDatabases,\n    standaloneMultiDatabase,\n    standaloneDatabaseList,\n  },\n});\n\nexport const SET_STANDALONE_MODE = 'APPLICATION/SET_STANDALONE_MODE';\nexport const setStandaloneMode = (standalone: boolean) => ({\n  type: SET_STANDALONE_MODE,\n  payload: { standalone },\n});\n\nexport const SET_STANDALONE_DASHBOARD_DATEBASE = 'APPLICATION/SET_STANDALONE_DASHBOARD_DATEBASE';\nexport const setStandaloneDashboardDatabase = (dashboardDatabase: string) => ({\n  type: SET_STANDALONE_DASHBOARD_DATEBASE,\n  payload: { dashboardDatabase },\n});\n\nexport const SET_SSO_ENABLED = 'APPLICATION/SET_SSO_ENABLED';\nexport const setSSOEnabled = (enabled: boolean, discoveryUrl: string) => ({\n  type: SET_SSO_ENABLED,\n  payload: { enabled, discoveryUrl },\n});\n\nexport const SET_SSO_PROVIDERS = 'APPLICATION/SET_SSO_PROVIDERS';\nexport const setSSOProviders = (providers: []) => ({\n  type: SET_SSO_PROVIDERS,\n  payload: { providers },\n});\n\nexport const SET_WAIT_FOR_SSO = 'APPLICATION/SET_WAIT_FOR_SSO';\nexport const setWaitForSSO = (wait: boolean) => ({\n  type: SET_WAIT_FOR_SSO,\n  payload: { wait },\n});\n\nexport const SET_CACHED_SSO_DISCOVERY_URL = 'APPLICATION/SET_CACHED_SSO_DISCOVERY_URL';\nexport const setCachedSSODiscoveryUrl = (url: string) => ({\n  type: SET_CACHED_SSO_DISCOVERY_URL,\n  payload: { url },\n});\n\nexport const SET_SESSION_PARAMETERS = 'APPLICATION/SET_SESSION_PARAMETERS';\nexport const setSessionParameters = (parameters: any) => ({\n  type: SET_SESSION_PARAMETERS,\n  payload: { parameters },\n});\n\nexport const SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING = 'APPLICATION/SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING';\nexport const setDashboardToLoadAfterConnecting = (id: any) => ({\n  type: SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING,\n  payload: { id },\n});\n\nexport const SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING = 'APPLICATION/SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING';\nexport const setParametersToLoadAfterConnecting = (parameters: any) => ({\n  type: SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING,\n  payload: { parameters },\n});\n\nexport const SET_CUSTOM_HEADER = 'APPLICATION/SET_CUSTOM_HEADER';\nexport const setCustomHeader = (customHeader: any) => ({\n  type: SET_CUSTOM_HEADER,\n  payload: { customHeader },\n});\n\nexport const SET_DEPRECATION_NOTICE = 'APPLICATION/SET_DEPRECATION_NOTICE';\nexport const setDeprecationNotice = (deprecated: boolean) => ({\n  type: SET_DEPRECATION_NOTICE,\n  payload: { deprecated },\n});\n"
  },
  {
    "path": "src/application/ApplicationReducer.ts",
    "content": "/**\n * Reducers define changes to the application state when a given action is taken.\n */\n\nimport {\n  HARD_RESET_CARD_SETTINGS,\n  TOGGLE_CARD_SETTINGS,\n  UPDATE_ALL_SELECTIONS,\n  UPDATE_FIELDS,\n  UPDATE_SCHEMA,\n  UPDATE_SELECTION,\n} from '../card/CardActions';\nimport { DEFAULT_NEO4J_URL } from '../config/ApplicationConfig';\nimport { SET_DASHBOARD, SET_DASHBOARD_UUID } from '../dashboard/DashboardActions';\nimport { UPDATE_DASHBOARD_SETTING } from '../settings/SettingsActions';\nimport {\n  CLEAR_DESKTOP_CONNECTION_PROPERTIES,\n  CLEAR_NOTIFICATION,\n  CREATE_NOTIFICATION,\n  RESET_SHARE_DETAILS,\n  SET_ABOUT_MODAL_OPEN,\n  SET_CACHED_SSO_DISCOVERY_URL,\n  SET_CONNECTED,\n  SET_CONNECTION_MODAL_OPEN,\n  SET_CONNECTION_PROPERTIES,\n  SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING,\n  SET_DESKTOP_CONNECTION_PROPERTIES,\n  SET_DRAFT,\n  SET_OLD_DASHBOARD,\n  SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING,\n  SET_REPORT_HELP_MODAL_OPEN,\n  SET_SESSION_PARAMETERS,\n  SET_SHARE_DETAILS_FROM_URL,\n  SET_SSO_ENABLED,\n  SET_SSO_PROVIDERS,\n  SET_STANDALONE_DASHBOARD_DATEBASE,\n  SET_STANDALONE_ENABLED,\n  SET_STANDALONE_MODE,\n  SET_WAIT_FOR_SSO,\n  SET_WELCOME_SCREEN_OPEN,\n  SET_CUSTOM_HEADER,\n  SET_DEPRECATION_NOTICE,\n} from './ApplicationActions';\nimport {\n  SET_LOGGING_MODE,\n  SET_LOGGING_DATABASE,\n  SET_LOG_ERROR_NOTIFICATION,\n  LOGGING_PREFIX,\n} from './logging/LoggingActions';\nimport { loggingReducer, LOGGING_INITIAL_STATE } from './logging/LoggingReducer';\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\nconst initialState = {\n  notificationTitle: null,\n  notificationMessage: null,\n  connectionModalOpen: false,\n  welcomeScreenOpen: true,\n  draft: false,\n  aboutModalOpen: false,\n  connection: {\n    protocol: 'neo4j+s',\n    url: DEFAULT_NEO4J_URL,\n    port: '7687',\n    database: '',\n    username: 'neo4j',\n    password: '',\n  },\n  shareDetails: undefined,\n  desktopConnection: null,\n  connected: false,\n  dashboardToLoadAfterConnecting: null,\n  waitForSSO: false,\n  standalone: false,\n  logging: LOGGING_INITIAL_STATE,\n};\nexport const applicationReducer = (state = initialState, action: { type: any; payload: any }) => {\n  const { type, payload } = action;\n\n  // This is a special application-level flag used to determine whether the dashboard needs to be saved to the database.\n  if (action.type.startsWith('DASHBOARD/') || action.type.startsWith('PAGE/') || action.type.startsWith('CARD/')) {\n    // if anything changes EXCEPT for the selected page, we flag that we are drafting a dashboard.\n    const NON_TRANSFORMATIVE_ACTIONS = [\n      UPDATE_DASHBOARD_SETTING,\n      UPDATE_SCHEMA,\n      HARD_RESET_CARD_SETTINGS,\n      SET_DASHBOARD,\n      UPDATE_ALL_SELECTIONS,\n      UPDATE_FIELDS,\n      SET_DASHBOARD_UUID,\n      TOGGLE_CARD_SETTINGS,\n      UPDATE_SELECTION,\n    ];\n\n    if (!state.draft && !NON_TRANSFORMATIVE_ACTIONS.includes(type)) {\n      state = update(state, { draft: true });\n      return state;\n    }\n  }\n\n  // Ignore any non-application actions.\n  if (!action.type.startsWith('APPLICATION/')) {\n    return state;\n  }\n  if (action.type.startsWith(LOGGING_PREFIX)) {\n    const enrichedPayload = update(payload, { logging: state.logging });\n    const enrichedAction = { type, payload: enrichedPayload };\n    return { ...state, logging: loggingReducer(state.logging, enrichedAction) };\n  }\n\n  // Application state updates are handled here.\n  switch (type) {\n    case CREATE_NOTIFICATION: {\n      const { title, message } = payload;\n      state = update(state, { notificationTitle: title, notificationMessage: message });\n      return state;\n    }\n    case CLEAR_NOTIFICATION: {\n      state = update(state, { notificationTitle: null, notificationMessage: null, notificationIsDismissable: null });\n      return state;\n    }\n    case SET_CONNECTED: {\n      const { connected } = payload;\n      state = update(state, { connected: connected });\n      return state;\n    }\n    case SET_DRAFT: {\n      const { draft } = payload;\n      state = update(state, { draft: draft });\n      return state;\n    }\n    case SET_CONNECTION_MODAL_OPEN: {\n      const { open } = payload;\n      state = update(state, { connectionModalOpen: open });\n      return state;\n    }\n    case SET_ABOUT_MODAL_OPEN: {\n      const { open } = payload;\n      state = update(state, { aboutModalOpen: open });\n      return state;\n    }\n    case SET_REPORT_HELP_MODAL_OPEN: {\n      const { open } = payload;\n      state = update(state, { reportHelpModalOpen: open });\n      return state;\n    }\n    case SET_WELCOME_SCREEN_OPEN: {\n      const { open } = payload;\n      state = update(state, { welcomeScreenOpen: open });\n      return state;\n    }\n    case SET_STANDALONE_DASHBOARD_DATEBASE: {\n      const { dashboardDatabase } = payload;\n      state = update(state, { standaloneDashboardDatabase: dashboardDatabase });\n      return state;\n    }\n    case SET_STANDALONE_MODE: {\n      const { standalone } = payload;\n      state = update(state, { standalone: standalone });\n      return state;\n    }\n    case SET_LOGGING_MODE: {\n      const { loggingMode } = payload;\n      state = update(state, { loggingMode: loggingMode });\n      return state;\n    }\n    case SET_LOGGING_DATABASE: {\n      const { loggingDatabase } = payload;\n      state = update(state, { loggingDatabase: loggingDatabase });\n      return state;\n    }\n    case SET_LOG_ERROR_NOTIFICATION: {\n      const { logErrorNotification } = payload;\n      state = update(state, { logErrorNotification: logErrorNotification });\n      return state;\n    }\n    case SET_SSO_ENABLED: {\n      const { enabled, discoveryUrl } = payload;\n      state = update(state, { ssoEnabled: enabled, ssoDiscoveryUrl: discoveryUrl });\n      return state;\n    }\n    case SET_SSO_PROVIDERS: {\n      const { providers } = payload;\n      state = update(state, { ssoProviders: providers });\n      return state;\n    }\n    case SET_WAIT_FOR_SSO: {\n      const { wait } = payload;\n      state = update(state, { waitForSSO: wait });\n      return state;\n    }\n    case SET_SESSION_PARAMETERS: {\n      const { parameters } = payload;\n      state = update(state, { sessionParameters: parameters });\n      return state;\n    }\n    case SET_STANDALONE_ENABLED: {\n      const {\n        standalone,\n        standaloneProtocol,\n        standaloneHost,\n        standalonePort,\n        standaloneDatabase,\n        standaloneDashboardName,\n        standaloneDashboardDatabase,\n        standaloneDashboardURL,\n        standaloneUsername,\n        standalonePassword,\n        standalonePasswordWarningHidden,\n        standaloneAllowLoad,\n        standaloneLoadFromOtherDatabases,\n        standaloneMultiDatabase,\n        standaloneDatabaseList,\n      } = payload;\n      state = update(state, {\n        standalone: standalone,\n        standaloneProtocol: standaloneProtocol,\n        standaloneHost: standaloneHost,\n        standalonePort: standalonePort,\n        standaloneDatabase: standaloneDatabase,\n        standaloneDashboardName: standaloneDashboardName,\n        standaloneDashboardDatabase: standaloneDashboardDatabase,\n        standaloneDashboardURL: standaloneDashboardURL,\n        standaloneUsername: standaloneUsername,\n        standalonePassword: standalonePassword,\n        standalonePasswordWarningHidden: standalonePasswordWarningHidden,\n        standaloneAllowLoad: standaloneAllowLoad,\n        standaloneLoadFromOtherDatabases: standaloneLoadFromOtherDatabases,\n        standaloneMultiDatabase: standaloneMultiDatabase,\n        standaloneDatabaseList: standaloneDatabaseList,\n      });\n      return state;\n    }\n    case SET_OLD_DASHBOARD: {\n      const { text } = payload;\n      state = update(state, { oldDashboard: text });\n      return state;\n    }\n    case SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING: {\n      const { id } = payload;\n      state = update(state, { dashboardToLoadAfterConnecting: id });\n      return state;\n    }\n    case SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING: {\n      const { parameters } = payload;\n      state = update(state, { parametersToLoadAfterConnecting: parameters });\n      return state;\n    }\n    case SET_CONNECTION_PROPERTIES: {\n      const { protocol, url, port, database, username, password } = payload;\n      state = update(state, {\n        connection: {\n          protocol: protocol,\n          url: url,\n          port: port,\n          database: database,\n          username: username,\n          password: password,\n        },\n      });\n      return state;\n    }\n    case CLEAR_DESKTOP_CONNECTION_PROPERTIES: {\n      state = update(state, { desktopConnection: null });\n      return state;\n    }\n    case SET_DESKTOP_CONNECTION_PROPERTIES: {\n      const { protocol, url, port, database, username, password } = payload;\n      state = update(state, {\n        desktopConnection: {\n          protocol: protocol,\n          url: url,\n          port: port,\n          database: database,\n          username: username,\n          password: password,\n        },\n      });\n      return state;\n    }\n    case RESET_SHARE_DETAILS: {\n      state = update(state, { shareDetails: undefined });\n      return state;\n    }\n    case SET_CACHED_SSO_DISCOVERY_URL: {\n      const { url } = payload;\n      state = update(state, { cachedSSODiscoveryUrl: url });\n      return state;\n    }\n    case SET_SHARE_DETAILS_FROM_URL: {\n      const {\n        type,\n        id,\n        standalone,\n        protocol,\n        url,\n        port,\n        database,\n        username,\n        password,\n        dashboardDatabase,\n        skipConfirmation,\n      } = payload;\n      state = update(state, {\n        shareDetails: {\n          type: type,\n          id: id,\n          standalone: standalone,\n          protocol: protocol,\n          url: url,\n          port: port,\n          database: database,\n          username: username,\n          password: password,\n          dashboardDatabase: dashboardDatabase,\n          skipConfirmation: skipConfirmation,\n        },\n      });\n      return state;\n    }\n    case SET_CUSTOM_HEADER: {\n      const { customHeader } = payload;\n      state = update(state, { customHeader: customHeader });\n      return state;\n    }\n    case SET_DEPRECATION_NOTICE: {\n      const { deprecated } = payload;\n      state = update(state, { deprecated: deprecated });\n      return state;\n    }\n    default: {\n      return state;\n    }\n  }\n};\n"
  },
  {
    "path": "src/application/ApplicationSelectors.ts",
    "content": "import { initialState } from '../dashboard/DashboardReducer';\nimport isEqual from 'lodash.isequal';\n\n/**\n * Selectors define a way to retrieve parts of the global application state for a sub-component.\n */\n\nexport const applicationHasNotification = (state: any) => {\n  return state.application.notificationMessage != null;\n};\n\nexport const getNotification = (state: any) => {\n  return state.application.notificationMessage;\n};\n\nexport const getNotificationIsDismissable = (state: any) => {\n  return state.application.notificationTitle !== 'Unable to load application configuration';\n};\n\nexport const getNotificationTitle = (state: any) => {\n  return state.application.notificationTitle;\n};\n\nexport const dashboardIsDraft = (state: any) => {\n  return state.application.draft;\n};\n\nexport const applicationIsConnected = (state: any) => {\n  return state.application.connected;\n};\n\nexport const applicationGetConnection = (state: any) => {\n  return state.application.connection;\n};\n\nexport const applicationGetConnectionDatabase = (state: any) => {\n  return state.application.connection.database;\n};\n\nexport const applicationGetConnectionUser = (state: any) => {\n  return state.application.connection.username;\n};\n\nexport const applicationGetShareDetails = (state: any) => {\n  return state.application.shareDetails;\n};\n\nexport const applicationIsStandalone = (state: any) => {\n  return state.application.standalone;\n};\n\nexport const applicationGetLoggingMode = (state: any) => {\n  return state.application.loggingMode;\n};\n\nexport const applicationHasNeo4jDesktopConnection = (state: any) => {\n  return state.application.desktopConnection != null;\n};\n\nexport const applicationHasConnectionModalOpen = (state: any) => {\n  return state.application.connectionModalOpen;\n};\n\nexport const applicationGetOldDashboard = (state: any) => {\n  return state.application.oldDashboard;\n};\n\nexport const applicationHasAboutModalOpen = (state: any) => {\n  return state.application.aboutModalOpen;\n};\n\nexport const applicationHasReportHelpModalOpen = (state: any) => {\n  return state.application.reportHelpModalOpen;\n};\n\nexport const applicationGetSsoSettings = (state: any) => {\n  return {\n    ssoEnabled: state.application.ssoEnabled,\n    ssoProviders: state.application.ssoProviders,\n    ssoDiscoveryUrl: state.application.ssoDiscoveryUrl,\n    cachedSSODiscoveryUrl: state.application.cachedSSODiscoveryUrl,\n  };\n};\n\nexport const applicationGetStandaloneSettings = (state: any) => {\n  return {\n    standalone: state.application.standalone,\n    standaloneProtocol: state.application.standaloneProtocol,\n    standaloneHost: state.application.standaloneHost,\n    standalonePort: state.application.standalonePort,\n    standaloneDatabase: state.application.standaloneDatabase,\n    standaloneDashboardName: state.application.standaloneDashboardName,\n    standaloneDashboardDatabase: state.application.standaloneDashboardDatabase,\n    standaloneDashboardURL: state.application.standaloneDashboardURL,\n    standaloneUsername: state.application.standaloneUsername,\n    standalonePassword: state.application.standalonePassword,\n    standalonePasswordWarningHidden: state.application.standalonePasswordWarningHidden,\n    standaloneAllowLoad: state.application.standaloneAllowLoad,\n    standaloneLoadFromOtherDatabases: state.application.standaloneLoadFromOtherDatabases,\n    standaloneMultiDatabase: state.application.standaloneMultiDatabase,\n    standaloneDatabaseList: state.application.standaloneDatabaseList,\n  };\n};\n\nexport const applicationHasWelcomeScreenOpen = (state: any) => {\n  return state.application.welcomeScreenOpen;\n};\n\nexport const applicationIsDeprecated = (state: any) => {\n  return state.application.deprecated;\n};\n\nexport const applicationHasCachedDashboard = (state: any) => {\n  // Avoid this expensive check when the application is connected, as it's only for the welcome screen.\n  if (state.application.connected) {\n    return false;\n  }\n  return !isEqual(state.dashboard, initialState);\n};\n\n/**\n * Deep-copy the current state, and remove the password.\n */\nexport const applicationGetDebugState = (state: any) => {\n  const copy = JSON.parse(JSON.stringify(state));\n  copy.application.connection.password = '************';\n  if (copy.application.desktopConnection) {\n    copy.application.desktopConnection.password = '************';\n  }\n  return copy;\n};\n\nexport const applicationGetCustomHeader = (state: any) => {\n  return state.application.customHeader;\n};\n"
  },
  {
    "path": "src/application/ApplicationThunks.ts",
    "content": "import { createDriver } from 'use-neo4j';\nimport { initializeSSO } from '../component/sso/SSOUtils';\nimport { DEFAULT_SCREEN, Screens } from '../config/ApplicationConfig';\nimport { setDashboard } from '../dashboard/DashboardActions';\nimport { NEODASH_VERSION, VERSION_TO_MIGRATE } from '../dashboard/DashboardReducer';\nimport {\n  assignDashboardUuidIfNotPresentThunk,\n  loadDashboardFromNeo4jByNameThunk,\n  loadDashboardFromNeo4jThunk,\n  loadDashboardThunk,\n  upgradeDashboardVersion,\n} from '../dashboard/DashboardThunks';\nimport { createNotificationThunk } from '../page/PageThunks';\nimport { runCypherQuery } from '../report/ReportQueryRunner';\nimport {\n  setPageNumberThunk,\n  updateGlobalParametersThunk,\n  updateSessionParameterThunk,\n} from '../settings/SettingsThunks';\nimport {\n  setConnected,\n  setConnectionModalOpen,\n  setConnectionProperties,\n  setDesktopConnectionProperties,\n  resetShareDetails,\n  setShareDetailsFromUrl,\n  setWelcomeScreenOpen,\n  setDashboardToLoadAfterConnecting,\n  setOldDashboard,\n  clearDesktopConnectionProperties,\n  clearNotification,\n  setSSOEnabled,\n  setSSOProviders,\n  setStandaloneEnabled,\n  setAboutModalOpen,\n  setStandaloneMode,\n  setStandaloneDashboardDatabase,\n  setWaitForSSO,\n  setParametersToLoadAfterConnecting,\n  setReportHelpModalOpen,\n  setDraft,\n  setCustomHeader,\n  setDeprecationNotice,\n} from './ApplicationActions';\nimport { setLoggingMode, setLoggingDatabase, setLogErrorNotification } from './logging/LoggingActions';\nimport { version } from '../modal/AboutModal';\nimport { applicationIsStandalone } from './ApplicationSelectors';\nimport { applicationGetLoggingSettings } from './logging/LoggingSelectors';\nimport { createLogThunk } from './logging/LoggingThunk';\nimport { createUUID } from '../utils/uuid';\n\n/**\n * Application Thunks (https://redux.js.org/usage/writing-logic-thunks) handle complex state manipulations.\n * Several actions/other thunks may be dispatched from here.\n */\n\n/**\n * Establish a connection to Neo4j with the specified credentials. Open/close the relevant windows when connection is made (un)successfully.\n * @param protocol - the neo4j protocol (e.g. bolt, bolt+s, neo4j+s, ...)\n * @param url - URL of the host.\n * @param port - port on which Neo4j is running.\n * @param database - the Neo4j database to connect to.\n * @param username - Neo4j username.\n * @param password - Neo4j password.\n */\nexport const createConnectionThunk =\n  (protocol, url, port, database, username, password) => (dispatch: any, getState: any) => {\n    const loggingState = getState();\n    const loggingSettings = applicationGetLoggingSettings(loggingState);\n    const neodashMode = applicationIsStandalone(loggingState) ? 'Standalone' : 'Editor';\n    try {\n      const driver = createDriver(protocol, url, port, username, password, { userAgent: `neodash/v${version}` });\n      // eslint-disable-next-line no-console\n      console.log('Attempting to connect...');\n      const validateConnection = (records) => {\n        // eslint-disable-next-line no-console\n        console.log('Confirming connection was established...');\n        if (records && records[0] && records[0].error) {\n          dispatch(createNotificationThunk('Unable to establish connection', records[0].error));\n          if (loggingSettings.loggingMode > '0') {\n            dispatch(\n              createLogThunk(\n                driver,\n                loggingSettings.loggingDatabase,\n                neodashMode,\n                username,\n                'ERR - connect to DB',\n                database,\n                '',\n                `Error while trying to establish connection to Neo4j DB in ${neodashMode} mode at ${Date(\n                  Date.now()\n                ).substring(0, 33)}`\n              )\n            );\n          }\n        } else if (records && records[0] && records[0].keys[0] == 'connected') {\n          dispatch(setConnectionProperties(protocol, url, port, database, username, password));\n          dispatch(setConnectionModalOpen(false));\n          dispatch(setConnected(true));\n          // An old dashboard (pre-2.3.5) may not always have a UUID. We catch this case here.\n          dispatch(assignDashboardUuidIfNotPresentThunk());\n          dispatch(updateSessionParameterThunk('session_uri', `${protocol}://${url}:${port}`));\n          dispatch(updateSessionParameterThunk('session_database', database));\n          dispatch(updateSessionParameterThunk('session_username', username));\n          if (loggingSettings.loggingMode > '0') {\n            dispatch(\n              createLogThunk(\n                driver,\n                loggingSettings.loggingDatabase,\n                neodashMode,\n                username,\n                'INF - connect to DB',\n                database,\n                '',\n                `${username} established connection to Neo4j DB in ${neodashMode} mode at ${Date(Date.now()).substring(\n                  0,\n                  33\n                )}`\n              )\n            );\n          }\n          // If we have remembered to load a specific dashboard after connecting to the database, take care of it here.\n          const { application } = getState();\n          if (\n            application.dashboardToLoadAfterConnecting &&\n            (application.dashboardToLoadAfterConnecting.startsWith('http') ||\n              application.dashboardToLoadAfterConnecting.startsWith('./') ||\n              application.dashboardToLoadAfterConnecting.startsWith('/'))\n          ) {\n            fetch(application.dashboardToLoadAfterConnecting)\n              .then((response) => response.text())\n              .then((data) => dispatch(loadDashboardThunk(createUUID(), data)));\n            dispatch(setDashboardToLoadAfterConnecting(null));\n          } else if (application.dashboardToLoadAfterConnecting) {\n            const setDashboardAfterLoadingFromDatabase = (value) => {\n              dispatch(loadDashboardThunk(createUUID(), value));\n            };\n\n            // If we specify a dashboard by name, load the latest version of it.\n            // If we specify a dashboard by UUID, load it directly.\n            if (application.dashboardToLoadAfterConnecting.startsWith('name:')) {\n              dispatch(\n                loadDashboardFromNeo4jByNameThunk(\n                  driver,\n                  application.standaloneDashboardDatabase,\n                  application.dashboardToLoadAfterConnecting.substring(5),\n                  setDashboardAfterLoadingFromDatabase\n                )\n              );\n            } else {\n              dispatch(\n                loadDashboardFromNeo4jThunk(\n                  driver,\n                  application.standaloneDashboardDatabase,\n                  application.dashboardToLoadAfterConnecting,\n                  setDashboardAfterLoadingFromDatabase\n                )\n              );\n            }\n            dispatch(setDashboardToLoadAfterConnecting(null));\n          }\n        } else {\n          dispatch(createNotificationThunk('Unknown Connection Error', 'Check the browser console.'));\n        }\n      };\n      const query = 'RETURN true as connected';\n      const parameters = {};\n      runCypherQuery(\n        driver,\n        database,\n        query,\n        parameters,\n        1,\n        () => {},\n        (records) => validateConnection(records)\n      );\n    } catch (e) {\n      dispatch(createNotificationThunk('Unable to establish connection', e));\n    }\n  };\n\n/**\n * Establish a connection directly from the Neo4j Desktop integration (if running inside Neo4j Desktop)\n */\nexport const createConnectionFromDesktopIntegrationThunk = () => (dispatch: any, getState: any) => {\n  try {\n    const desktopConnectionDetails = getState().application.desktopConnection;\n    const { protocol, url, port, database, username, password } = desktopConnectionDetails;\n    dispatch(createConnectionThunk(protocol, url, port, database, username, password));\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to establish connection to Neo4j Desktop', e));\n  }\n};\n\n/**\n * Find the active database from Neo4j Desktop.\n * Set global state values to remember the values retrieved from the integration so that we can connect later if possible.\n */\nexport const setDatabaseFromNeo4jDesktopIntegrationThunk = () => (dispatch: any) => {\n  const getActiveDatabase = (context) => {\n    for (let pi = 0; pi < context.projects.length; pi++) {\n      let prj = context.projects[pi];\n      for (let gi = 0; gi < prj.graphs.length; gi++) {\n        let grf = prj.graphs[gi];\n        if (grf.status == 'ACTIVE') {\n          return grf;\n        }\n      }\n    }\n    // No active database found - ask for manual connection details.\n    return null;\n  };\n\n  let promise = window.neo4jDesktopApi && window.neo4jDesktopApi.getContext();\n\n  if (promise) {\n    promise.then((context) => {\n      let neo4j = getActiveDatabase(context);\n      if (neo4j) {\n        dispatch(\n          setDesktopConnectionProperties(\n            neo4j.connection.configuration.protocols.bolt.url.split('://')[0],\n            neo4j.connection.configuration.protocols.bolt.url.split('://')[1].split(':')[0],\n            neo4j.connection.configuration.protocols.bolt.port,\n            undefined,\n            neo4j.connection.configuration.protocols.bolt.username,\n            neo4j.connection.configuration.protocols.bolt.password\n          )\n        );\n      }\n    });\n  }\n};\n\n/**\n * On application startup, check the URL to see if we are loading a shared dashboard.\n * If yes, decode the URL parameters and set the application state accordingly, so that it can be loaded later.\n */\nexport const handleSharedDashboardsThunk = () => (dispatch: any) => {\n  try {\n    const queryString = window.location.search;\n    const urlParams = new URLSearchParams(queryString);\n\n    //  Parse the URL parameters to see if there's any deep linking of parameters.\n    const paramsToSetAfterConnecting = {};\n    Array.from(urlParams.entries()).forEach(([key, value]) => {\n      if (key.startsWith('neodash_')) {\n        paramsToSetAfterConnecting[key] = value;\n      }\n    });\n    if (Object.keys(paramsToSetAfterConnecting).length > 0) {\n      dispatch(setParametersToLoadAfterConnecting(paramsToSetAfterConnecting));\n    }\n\n    if (urlParams.get('share') !== null) {\n      const id = decodeURIComponent(urlParams.get('id'));\n      const type = urlParams.get('type');\n      const standalone = urlParams.get('standalone') == 'Yes';\n      const skipConfirmation = urlParams.get('skipConfirmation') == 'Yes';\n\n      const dashboardDatabase = urlParams.get('dashboardDatabase');\n      if (dashboardDatabase) {\n        dispatch(setStandaloneDashboardDatabase(dashboardDatabase));\n      }\n      if (urlParams.get('credentials')) {\n        setWelcomeScreenOpen(false);\n        const connection = decodeURIComponent(urlParams.get('credentials'));\n        const protocol = connection.split('://')[0];\n        const username = connection.split('://')[1].split(':')[0];\n        const password = connection.split('://')[1].split(':')[1].split('@')[0];\n        const database = connection.split('@')[1].split(':')[0];\n        const url = connection.split('@')[1].split(':')[1];\n        const port = connection.split('@')[1].split(':')[2];\n\n        dispatch(setConnectionModalOpen(false));\n        dispatch(\n          setShareDetailsFromUrl(\n            type,\n            id,\n            standalone,\n            protocol,\n            url,\n            port,\n            database,\n            username,\n            password,\n            dashboardDatabase,\n            skipConfirmation\n          )\n        );\n\n        if (skipConfirmation === true) {\n          dispatch(onConfirmLoadSharedDashboardThunk());\n        }\n        window.history.pushState({}, document.title, window.location.pathname);\n      } else {\n        dispatch(setConnectionModalOpen(false));\n        // dispatch(setWelcomeScreenOpen(false));\n        dispatch(\n          setShareDetailsFromUrl(\n            type,\n            id,\n            standalone,\n            undefined,\n            undefined,\n            undefined,\n            undefined,\n            undefined,\n            undefined,\n            undefined,\n            false\n          )\n        );\n        window.history.pushState({}, document.title, window.location.pathname);\n      }\n    } else {\n      // dispatch(resetShareDetails());\n    }\n  } catch (e) {\n    dispatch(\n      createNotificationThunk(\n        'Unable to load shared dashboard',\n        'You have specified an invalid/incomplete share URL. Try regenerating the share URL from the sharing window.'\n      )\n    );\n  }\n};\n\n/**\n * Confirm that we load a shared dashboard. This requires that the state was previously set in `handleSharedDashboardsThunk()`.\n */\nexport const onConfirmLoadSharedDashboardThunk = () => (dispatch: any, getState: any) => {\n  try {\n    const state = getState();\n    const { shareDetails } = state.application;\n    dispatch(setWelcomeScreenOpen(false));\n    dispatch(setDashboardToLoadAfterConnecting(shareDetails.id));\n\n    if (shareDetails.dashboardDatabase) {\n      dispatch(setStandaloneDashboardDatabase(shareDetails.dashboardDatabase));\n    } else if (!state.application.standaloneDashboardDatabase) {\n      // No standalone dashboard database configured, fall back to default\n      dispatch(setStandaloneDashboardDatabase(shareDetails.database));\n    }\n    if (shareDetails.url) {\n      dispatch(\n        createConnectionThunk(\n          shareDetails.protocol,\n          shareDetails.url,\n          shareDetails.port,\n          shareDetails.database,\n          shareDetails.username,\n          shareDetails.password\n        )\n      );\n    } else {\n      dispatch(setConnectionModalOpen(true));\n    }\n    if (shareDetails.standalone == true) {\n      dispatch(setStandaloneMode(true));\n    }\n    dispatch(resetShareDetails());\n  } catch (e) {\n    dispatch(\n      createNotificationThunk(\n        'Unable to load shared dashboard',\n        'The provided connection or dashboard identifiers are invalid. Try regenerating the share URL from the sharing window.'\n      )\n    );\n  }\n};\n\n/**\n * Initializes the NeoDash application.\n *\n * This is a multi step process, starting with loading the runtime configuration.\n * This is present in the file located at /config.json on the URL where NeoDash is deployed.\n * Note: this does not work in Neo4j Desktop, so we revert to defaults.\n */\nexport const loadApplicationConfigThunk = () => async (dispatch: any, getState: any) => {\n  let config = {\n    ssoEnabled: false,\n    ssoProviders: [],\n    ssoDiscoveryUrl: 'http://example.com',\n    standalone: false,\n    standaloneProtocol: 'neo4j+s',\n    standaloneHost: 'localhost',\n    standalonePort: '7687',\n    standaloneDatabase: 'neo4j',\n    standaloneDashboardName: 'My Dashboard',\n    standaloneDashboardDatabase: 'dashboards',\n    standaloneDashboardURL: '',\n    loggingMode: '0',\n    loggingDatabase: 'logs',\n    logErrorNotification: '3',\n    standaloneAllowLoad: false,\n    standaloneLoadFromOtherDatabases: false,\n    standaloneMultiDatabase: false,\n    standaloneDatabaseList: 'neo4j',\n    customHeader: '',\n    deprecationNotice: false,\n  };\n  try {\n    config = await (await fetch('config.json')).json();\n  } catch (e) {\n    // Config may not be found, for example when we are in Neo4j Desktop.\n    // eslint-disable-next-line no-console\n    console.log('No config file detected. Setting to safe defaults.');\n  }\n\n  try {\n    // Parse the URL parameters to see if there's any deep linking of parameters.\n    const state = getState();\n    const queryString = window.location.search;\n    const urlParams = new URLSearchParams(queryString);\n    if (state.application.waitForSSO) {\n      const paramsBeforeSSO = JSON.parse(sessionStorage.getItem('SSO_PARAMS_BEFORE_REDIRECT') || '{}');\n      Object.entries(paramsBeforeSSO).forEach(([key, value]) => {\n        urlParams.set(key, value);\n      });\n    }\n    const paramsToSetAfterConnecting = {};\n    Array.from(urlParams.entries()).forEach(([key, value]) => {\n      if (key.startsWith('neodash_')) {\n        paramsToSetAfterConnecting[key] = value;\n      }\n    });\n    sessionStorage.getItem('SSO_PARAMS_BEFORE_REDIRECT');\n    const page = urlParams.get('page');\n    if (page !== '' && page !== null) {\n      if (!isNaN(page)) {\n        dispatch(setPageNumberThunk(parseInt(page)));\n      }\n    }\n    dispatch(setSSOEnabled(config.ssoEnabled, state.application.cachedSSODiscoveryUrl));\n    dispatch(setSSOProviders(config.ssoProviders));\n\n    // Check if we are in standalone mode\n    const standalone = config.standalone || urlParams.get('standalone') == 'Yes';\n\n    // if a dashboard database was previously set, remember to use it.\n    const dashboardDatabase = state.application.standaloneDashboardDatabase;\n    dispatch(\n      setStandaloneEnabled(\n        standalone,\n        config.standaloneProtocol,\n        config.standaloneHost,\n        config.standalonePort,\n        config.standaloneDatabase,\n        config.standaloneDashboardName,\n        dashboardDatabase || config.standaloneDashboardDatabase,\n        config.standaloneDashboardURL,\n        config.standaloneUsername,\n        config.standalonePassword,\n        config.standalonePasswordWarningHidden,\n        config.standaloneAllowLoad,\n        config.standaloneLoadFromOtherDatabases,\n        config.standaloneMultiDatabase,\n        config.standaloneDatabaseList\n      )\n    );\n\n    dispatch(setLoggingMode(config.loggingMode));\n    dispatch(setLoggingDatabase(config.loggingDatabase));\n    dispatch(setLogErrorNotification('3'));\n\n    dispatch(setConnectionModalOpen(false));\n    dispatch(setDeprecationNotice(config.deprecationNotice));\n    dispatch(setCustomHeader(config.customHeader));\n\n    // Auto-upgrade the dashboard version if an old version is cached.\n    if (state.dashboard && state.dashboard.version !== NEODASH_VERSION) {\n      // Attempt upgrade if dashboard version is outdated.\n      while (VERSION_TO_MIGRATE[state.dashboard.version]) {\n        const upgradedDashboard = upgradeDashboardVersion(\n          state.dashboard,\n          state.dashboard.version,\n          VERSION_TO_MIGRATE[state.dashboard.version]\n        );\n        dispatch(setDashboard(upgradedDashboard));\n        dispatch(setDraft(true));\n        dispatch(\n          createNotificationThunk(\n            'Successfully upgraded dashboard',\n            `Your old dashboard was migrated to version ${upgradedDashboard.version}. You might need to refresh this page and reactivate extensions.`\n          )\n        );\n      }\n    }\n\n    // SSO - specific case starts here.\n    if (state.application.waitForSSO) {\n      // We just got redirected from the SSO provider. Hide all windows and attempt the connection.\n      dispatch(setAboutModalOpen(false));\n      dispatch(setConnected(false));\n      dispatch(setWelcomeScreenOpen(false));\n      const success = await initializeSSO(state.application.cachedSSODiscoveryUrl, (credentials) => {\n        if (standalone) {\n          // Redirected from SSO and running in viewer mode, merge retrieved config with hardcoded credentials.\n          dispatch(\n            setConnectionProperties(\n              config.standaloneProtocol,\n              config.standaloneHost,\n              config.standalonePort,\n              config.standaloneDatabase,\n              credentials.username,\n              credentials.password\n            )\n          );\n          dispatch(\n            createConnectionThunk(\n              config.standaloneProtocol,\n              config.standaloneHost,\n              config.standalonePort,\n              config.standaloneDatabase,\n              credentials.username,\n              credentials.password\n            )\n          );\n        } else {\n          // Redirected from SSO and running in editor mode, merge retrieved config with existing details.\n          dispatch(\n            setConnectionProperties(\n              state.application.connection.protocol,\n              state.application.connection.url,\n              state.application.connection.port,\n              state.application.connection.database,\n              credentials.username,\n              credentials.password\n            )\n          );\n          dispatch(setConnected(true));\n        }\n\n        if (standalone) {\n          if (urlParams.get('id')) {\n            dispatch(setDashboardToLoadAfterConnecting(urlParams.get('id')));\n          } else if (config.standaloneDashboardURL !== undefined && config.standaloneDashboardURL.length > 0) {\n            dispatch(setDashboardToLoadAfterConnecting(config.standaloneDashboardURL));\n          } else {\n            dispatch(setDashboardToLoadAfterConnecting(`name:${config.standaloneDashboardName}`));\n          }\n          dispatch(setParametersToLoadAfterConnecting(paramsToSetAfterConnecting));\n        }\n        sessionStorage.removeItem('SSO_PARAMS_BEFORE_REDIRECT');\n      });\n\n      dispatch(setWaitForSSO(false));\n      if (!success) {\n        alert('Unable to connect using SSO. See the browser console for more details.');\n        dispatch(\n          createNotificationThunk(\n            'Unable to connect using SSO',\n            'Something went wrong. Most likely your credentials are incorrect...'\n          )\n        );\n      } else {\n        return;\n      }\n    } else if (state.application.ssoEnabled && !state.application.waitForSSO && urlParams) {\n      let paramsToStore = {};\n      urlParams.forEach((value, key) => {\n        paramsToStore[key] = value;\n      });\n      sessionStorage.setItem('SSO_PARAMS_BEFORE_REDIRECT', JSON.stringify(paramsToStore));\n    }\n\n    if (standalone) {\n      dispatch(initializeApplicationAsStandaloneThunk(config, paramsToSetAfterConnecting));\n    } else {\n      dispatch(initializeApplicationAsEditorThunk(config, paramsToSetAfterConnecting));\n    }\n  } catch (e) {\n    console.log(e);\n    dispatch(setWelcomeScreenOpen(false));\n    dispatch(\n      createNotificationThunk(\n        'Unable to load application configuration',\n        'Do you have a valid config.json deployed with your application?'\n      )\n    );\n  }\n};\n\n// Set up NeoDash to run in editor mode.\nexport const initializeApplicationAsEditorThunk = (_, paramsToSetAfterConnecting) => (dispatch: any) => {\n  const clearNotificationAfterLoad = true;\n  dispatch(clearDesktopConnectionProperties());\n  dispatch(setDatabaseFromNeo4jDesktopIntegrationThunk());\n  const old = localStorage.getItem('neodash-dashboard');\n  dispatch(setOldDashboard(old));\n  dispatch(setConnected(false));\n  dispatch(setDashboardToLoadAfterConnecting(null));\n  dispatch(updateGlobalParametersThunk(paramsToSetAfterConnecting));\n  // TODO: this logic around loading/saving/upgrading/migrating dashboards needs a cleanup\n  if (Object.keys(paramsToSetAfterConnecting).length > 0) {\n    dispatch(setParametersToLoadAfterConnecting(null));\n  }\n\n  // Check config to determine which screen is shown by default.\n  if (DEFAULT_SCREEN == Screens.CONNECTION_MODAL) {\n    dispatch(setWelcomeScreenOpen(false));\n    dispatch(setConnectionModalOpen(true));\n  } else if (DEFAULT_SCREEN == Screens.WELCOME_SCREEN) {\n    dispatch(setWelcomeScreenOpen(true));\n  }\n\n  if (clearNotificationAfterLoad) {\n    dispatch(clearNotification());\n  }\n  dispatch(handleSharedDashboardsThunk());\n  dispatch(setReportHelpModalOpen(false));\n  dispatch(setAboutModalOpen(false));\n};\n\n// Set up NeoDash to run in standalone mode.\nexport const initializeApplicationAsStandaloneThunk =\n  (config, paramsToSetAfterConnecting) => (dispatch: any, getState: any) => {\n    const clearNotificationAfterLoad = true;\n    const state = getState();\n    // If we are running in standalone mode, auto-set the connection details that are configured.\n    dispatch(\n      setConnectionProperties(\n        config.standaloneProtocol,\n        config.standaloneHost,\n        config.standalonePort,\n        config.standaloneDatabase,\n        config.standaloneUsername ? config.standaloneUsername : state.application.connection.username,\n        config.standalonePassword ? config.standalonePassword : state.application.connection.password\n      )\n    );\n\n    dispatch(setAboutModalOpen(false));\n    dispatch(setConnected(false));\n    dispatch(setWelcomeScreenOpen(false));\n    if (config.standaloneDashboardURL !== undefined && config.standaloneDashboardURL.length > 0) {\n      dispatch(setDashboardToLoadAfterConnecting(config.standaloneDashboardURL));\n    } else {\n      dispatch(setDashboardToLoadAfterConnecting(`name:${config.standaloneDashboardName}`));\n    }\n    dispatch(setParametersToLoadAfterConnecting(paramsToSetAfterConnecting));\n    dispatch(updateGlobalParametersThunk(paramsToSetAfterConnecting));\n\n    if (clearNotificationAfterLoad) {\n      dispatch(clearNotification());\n    }\n\n    // Override for when username and password are specified in the config - automatically connect to the specified URL.\n    if (config.standaloneUsername && config.standalonePassword) {\n      dispatch(\n        createConnectionThunk(\n          config.standaloneProtocol,\n          config.standaloneHost,\n          config.standalonePort,\n          config.standaloneDatabase,\n          config.standaloneUsername,\n          config.standalonePassword\n        )\n      );\n    } else {\n      dispatch(setConnectionModalOpen(true));\n    }\n    dispatch(handleSharedDashboardsThunk());\n  };\n"
  },
  {
    "path": "src/application/logging/LoggingActions.ts",
    "content": "export const LOGGING_PREFIX = 'APPLICATION/LOGGING/';\n\nexport const SET_LOGGING_MODE = `${LOGGING_PREFIX}/SET_LOGGING_MODE`;\nexport const setLoggingMode = (loggingMode: string) => ({\n  type: SET_LOGGING_MODE,\n  payload: { loggingMode },\n});\n\nexport const SET_LOGGING_DATABASE = `${LOGGING_PREFIX}/SET_LOGGING_DATABASE`;\nexport const setLoggingDatabase = (loggingDatabase: string) => ({\n  type: SET_LOGGING_DATABASE,\n  payload: { loggingDatabase },\n});\n\nexport const SET_LOG_ERROR_NOTIFICATION = `${LOGGING_PREFIX}/SET_LOG_ERROR_NOTIFICATION`;\nexport const setLogErrorNotification = (logErrorNotification: any) => ({\n  type: SET_LOG_ERROR_NOTIFICATION,\n  payload: { logErrorNotification },\n});\n"
  },
  {
    "path": "src/application/logging/LoggingReducer.ts",
    "content": "import { LOGGING_PREFIX, SET_LOGGING_DATABASE, SET_LOGGING_MODE, SET_LOG_ERROR_NOTIFICATION } from './LoggingActions';\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\nexport const LOGGING_INITIAL_STATE = {\n  loggingMode: '0',\n  logErrorNotification: '3',\n  loggingDatabase: undefined,\n};\n\nexport const loggingReducer = (state = LOGGING_INITIAL_STATE, action: { type: any; payload: any }) => {\n  const { type, payload } = action;\n\n  if (!action.type.startsWith(LOGGING_PREFIX)) {\n    return state;\n  }\n\n  // Logging state updates are handled here.\n  switch (type) {\n    case SET_LOGGING_MODE: {\n      const { loggingMode } = payload;\n      state = update(state, { loggingMode: loggingMode });\n      return state;\n    }\n    case SET_LOGGING_DATABASE: {\n      const { loggingDatabase } = payload;\n      state = update(state, { loggingDatabase: loggingDatabase });\n      return state;\n    }\n    case SET_LOG_ERROR_NOTIFICATION: {\n      const { logErrorNotification } = payload;\n      state = update(state, { logErrorNotification: logErrorNotification });\n      return state;\n    }\n    default: {\n      return state;\n    }\n  }\n};\n"
  },
  {
    "path": "src/application/logging/LoggingSelectors.ts",
    "content": "/**\n * Selector function for retrieving logging settings from the application state.\n * @param state - The application state.\n * @returns An object with logging settings.\n */\nexport const applicationGetLoggingSettings = (state: any) => state.application.logging;\n"
  },
  {
    "path": "src/application/logging/LoggingThunk.ts",
    "content": "import { createNotificationThunk } from '../../page/PageThunks';\nimport { runCypherQuery } from '../../report/ReportQueryRunner';\nimport { setLogErrorNotification } from './LoggingActions';\nimport { applicationGetLoggingSettings } from './LoggingSelectors';\nimport { createUUID } from '../../utils/uuid';\n\n// Thunk to handle log events.\n\nexport const createLogThunk =\n  (loggingDriver, loggingDatabase, neodashMode, logUser, logAction, logDatabase, logDashboard = '', logMessage) =>\n  (dispatch: any, getState: any) => {\n    try {\n      const uuid = createUUID();\n      // Generate a cypher query to save the log.\n      const query =\n        'CREATE (n:_Neodash_Log) SET n.uuid = $uuid, n.user = $user, n.date = datetime(), n.neodash_mode = $neodashMode, n.action = $logAction, n.database = $logDatabase, n.dashboard = $logDashboard, n.message = $logMessage RETURN $uuid as uuid';\n\n      const parameters = {\n        uuid: uuid,\n        user: logUser,\n        logAction: logAction,\n        logDatabase: logDatabase,\n        neodashMode: neodashMode,\n        logDashboard: logDashboard,\n        logMessage: logMessage,\n      };\n      runCypherQuery(\n        loggingDriver,\n        loggingDatabase,\n        query,\n        parameters,\n        1,\n        () => {},\n        (records) => {\n          if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) {\n            console.log(`log created: ${uuid}`);\n          } else {\n            // we only show error notification one time\n            const state = getState();\n            const loggingSettings = applicationGetLoggingSettings(state);\n            let LogErrorNotificationNum = Number(loggingSettings.logErrorNotification);\n            console.log(`Error creating log for ${(LogErrorNotificationNum - 4) * -1} times`);\n            if (LogErrorNotificationNum > 0) {\n              dispatch(\n                createNotificationThunk(\n                  'Error creating log',\n                  LogErrorNotificationNum > 1\n                    ? `Please check logging configuration with your Neodash administrator`\n                    : `Please check logging configuration with your Neodash administrator - This message will not be displayed anymore in the current session`\n                )\n              );\n            }\n            LogErrorNotificationNum -= 1;\n            dispatch(setLogErrorNotification(LogErrorNotificationNum.toString()));\n          }\n        }\n      );\n    } catch (e) {\n      // we only show error notification 3 times\n      const state = getState();\n      const loggingSettings = applicationGetLoggingSettings(state);\n      let LogErrorNotificationNum = Number(loggingSettings.logErrorNotification);\n      console.log(`Error creating log for ${(LogErrorNotificationNum - 4) * -1} times`);\n      if (LogErrorNotificationNum > 0) {\n        dispatch(\n          createNotificationThunk(\n            'Error creating log',\n            LogErrorNotificationNum > 1\n              ? `Please check logging configuration with your Neodash administrator`\n              : `Please check logging configuration with your Neodash administrator - This message will not be displayed anymore in the current session`\n          )\n        );\n      }\n      LogErrorNotificationNum -= 1;\n      dispatch(setLogErrorNotification(LogErrorNotificationNum.toString()));\n    }\n  };\n"
  },
  {
    "path": "src/card/Card.tsx",
    "content": "import { Card, Collapse, debounce } from '@mui/material';\nimport React, { useCallback, useContext, useEffect, useState } from 'react';\nimport NeoCardSettings from './settings/CardSettings';\nimport NeoCardView from './view/CardView';\nimport { connect } from 'react-redux';\nimport {\n  updateFieldsThunk,\n  updateSelectionThunk,\n  updateReportQueryThunk,\n  toggleCardSettingsThunk,\n  updateReportSettingThunk,\n  updateReportTitleThunk,\n  updateReportTypeThunk,\n  updateReportDatabaseThunk,\n} from './CardThunks';\nimport { toggleReportSettings } from './CardActions';\nimport { getReportState } from './CardSelectors';\nimport {\n  getDashboardIsEditable,\n  getDatabase,\n  getGlobalParameters,\n  getSessionParameters,\n} from '../settings/SettingsSelectors';\nimport { updateGlobalParameterThunk } from '../settings/SettingsThunks';\nimport useDimensions from 'react-cool-dimensions';\nimport { setReportHelpModalOpen } from '../application/ApplicationActions';\nimport { loadDatabaseListFromNeo4jThunk } from '../dashboard/DashboardThunks';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport { getDashboardExtensions } from '../dashboard/DashboardSelectors';\nimport { downloadComponentAsImage } from '../chart/ChartUtils';\nimport { Dialog } from '@neo4j-ndl/react';\nimport { createNotificationThunk } from '../page/PageThunks';\n\nconst NeoCard = ({\n  id, // id of the card.\n  report, // state of the card, retrieved based on card id.\n  editable, // whether the card is editable.\n  database, // the neo4j database that the card is running against.\n  extensions, // A set of enabled extensions.\n  globalParameters, // Query parameters that are globally set for the entire dashboard.\n  dashboardSettings, // Dictionary of settings for the entire dashboard.\n  onRemovePressed, // action to take when the card is removed. (passed from parent)\n  onClonePressed, // action to take when user presses the clone button\n  onReportHelpButtonPressed, // action to take when someone clicks the 'help' button in the report settings.\n  onTitleUpdate, // action to take when the card title is updated.\n  onTypeUpdate, // action to take when the card report type is updated.\n  onFieldsUpdate, // action to take when the set of returned query fields is updated.\n  onQueryUpdate, // action to take when the card query is updated.\n  onReportSettingUpdate, // action to take when an advanced report setting is updated.\n  onSelectionUpdate, // action to take when the selected visualization fields are updated.\n  onGlobalParameterUpdate, // action to take when a report updates a dashboard parameter.\n  onToggleCardSettings, // action to take when the card settings button is clicked.\n  onToggleReportSettings, // action to take when the report settings (advanced settings) button is clicked.\n  onDatabaseChanged, // action to take when the user changes the database related to the card\n  loadDatabaseListFromNeo4j, // Thunk to get the list of databases\n  createNotification, // Thunk to create a global notification pop-up.\n}) => {\n  // Will be used to fetch the list of current databases\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n\n  const [databaseList, setDatabaseList] = React.useState([database]);\n  const [databaseListLoaded, setDatabaseListLoaded] = React.useState(false);\n\n  const ref = React.useRef();\n\n  // fetching the list of databases from neo4j, filtering out the 'system' db\n  useEffect(() => {\n    if (!databaseListLoaded) {\n      loadDatabaseListFromNeo4j(driver, (result) => {\n        let index = result.indexOf('system');\n        if (index > -1) {\n          // only splice array when item is found\n          result.splice(index, 1); // 2nd parameter means remove one item only\n        }\n        setDatabaseList(result);\n      });\n      setDatabaseListLoaded(true);\n    }\n  }, [report.query]);\n\n  const [settingsOpen, setSettingsOpen] = React.useState(false);\n  const debouncedOnToggleCardSettings = useCallback(debounce(onToggleCardSettings, 500), []);\n  const [collapseTimeout, setCollapseTimeout] = React.useState(report.collapseTimeout);\n\n  const { observe, width, height } = useDimensions({\n    onResize: ({ observe, unobserve }) => {\n      // Triggered whenever the size of the target is changed...\n      unobserve(); // To stop observing the current target element\n      observe(); // To re-start observing the current target element\n    },\n  });\n\n  const [expanded, setExpanded] = useState(false);\n  const onToggleCardExpand = () => {\n    // When we re-minimize a card, close the settings to avoid position issues.\n    if (expanded && settingsOpen) {\n      onToggleCardSettings(id, false);\n    }\n    setExpanded(!expanded);\n  };\n\n  const [active, setActive] = React.useState(\n    report.settings && report.settings.autorun !== undefined ? report.settings.autorun : true\n  );\n\n  useEffect(() => {\n    if (!report.settingsOpen) {\n      setActive(report.settings && report.settings.autorun !== undefined ? report.settings.autorun : true);\n    }\n  }, [report.query]);\n\n  useEffect(() => {\n    setSettingsOpen(report.settingsOpen);\n  }, [report.settingsOpen]);\n\n  useEffect(() => {\n    setCollapseTimeout(report.collapseTimeout);\n  }, [report.collapseTimeout]);\n\n  // TODO - get rid of some of the props-drilling here...\n  const component = (\n    <div\n      ref={observe}\n      className='n-bg-neutral-bg-weak overflow-hidden n-shadow-l4 border-2 border-neutral-border-strong min-w-max rounded-lg px-4 py-5 sm:p-6 n-h-full'\n    >\n      {/* The front of the card, referred to as the 'view' */}\n      <Collapse disablestrictmodecompat='true' in={!settingsOpen} timeout={collapseTimeout} className='n-h-full'>\n        <Card ref={ref} className='n-h-full'>\n          <NeoCardView\n            id={id}\n            settingsOpen={settingsOpen}\n            editable={editable}\n            dashboardSettings={dashboardSettings}\n            extensions={extensions}\n            settings={report.settings ? report.settings : {}}\n            updateReportSetting={(name, value) => onReportSettingUpdate(id, name, value)}\n            createNotification={(title, message) => createNotification(title, message)}\n            type={report.type}\n            database={database}\n            active={active}\n            setActive={setActive}\n            onDownloadImage={() => downloadComponentAsImage(ref)}\n            query={report.query}\n            globalParameters={globalParameters}\n            fields={report.fields ? report.fields : []}\n            selection={report.selection}\n            widthPx={width}\n            heightPx={height}\n            title={report.title}\n            expanded={expanded}\n            onToggleCardExpand={onToggleCardExpand}\n            onGlobalParameterUpdate={onGlobalParameterUpdate}\n            onSelectionUpdate={(selectable, field) => onSelectionUpdate(id, selectable, field)}\n            onTitleUpdate={(title) => onTitleUpdate(id, title)}\n            onFieldsUpdate={(fields) => onFieldsUpdate(id, fields)}\n            onToggleCardSettings={() => {\n              setSettingsOpen(true);\n              setCollapseTimeout('auto');\n              debouncedOnToggleCardSettings(id, true);\n            }}\n          />\n        </Card>\n      </Collapse>\n      {/* The back of the card, referred to as the 'settings' */}\n      <Collapse disablestrictmodecompat='true' in={settingsOpen} timeout={collapseTimeout}>\n        <Card className='n-h-full'>\n          <NeoCardSettings\n            pagenumber={dashboardSettings.pagenumber}\n            reportId={id}\n            settingsOpen={settingsOpen}\n            query={report.query}\n            database={database}\n            databaseList={databaseList}\n            width={report.width}\n            height={report.height}\n            heightPx={height}\n            fields={report.fields}\n            schema={report.schema}\n            type={report.type}\n            expanded={expanded}\n            extensions={extensions}\n            dashboardSettings={dashboardSettings}\n            onToggleCardExpand={onToggleCardExpand}\n            setActive={setActive}\n            reportSettings={report.settings}\n            reportSettingsOpen={report.advancedSettingsOpen}\n            onQueryUpdate={(query) => onQueryUpdate(id, query)}\n            onDatabaseChanged={(database) => onDatabaseChanged(id, database)}\n            onReportSettingUpdate={(setting, value) => onReportSettingUpdate(id, setting, value)}\n            onTypeUpdate={(type) => onTypeUpdate(id, type)}\n            onReportHelpButtonPressed={() => onReportHelpButtonPressed()}\n            onRemovePressed={() => onRemovePressed(id)}\n            onClonePressed={() => onClonePressed(id)}\n            onToggleCardSettings={() => {\n              setSettingsOpen(false);\n              setCollapseTimeout('auto');\n              debouncedOnToggleCardSettings(id, false);\n            }}\n            onToggleReportSettings={() => onToggleReportSettings(id)}\n          />\n        </Card>\n      </Collapse>\n    </div>\n  );\n\n  // If the card is viewed in fullscreen, wrap it in a dialog.\n  // TODO - this causes a re-render (and therefore, a re-run of the report)\n  // Look into React Portals: https://stackoverflow.com/questions/61432878/how-to-render-child-component-outside-of-its-parent-component-dom-hierarchy\n  if (expanded) {\n    return (\n      <Dialog open={expanded} aria-labelledby='form-dialog-title' className='dialog-xxl'>\n        <Dialog.Content style={{ height: document.documentElement.clientHeight - 200 }}>{component}</Dialog.Content>\n      </Dialog>\n    );\n  }\n  return component;\n};\n\nconst mapStateToProps = (state, ownProps) => ({\n  report: getReportState(state, ownProps.id),\n  extensions: getDashboardExtensions(state),\n  editable: getDashboardIsEditable(state),\n  database: getDatabase(\n    state,\n    ownProps && ownProps.dashboardSettings ? ownProps.dashboardSettings.pagenumber : undefined,\n    ownProps.id\n  ),\n  globalParameters: { ...getGlobalParameters(state), ...getSessionParameters(state) },\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  onTitleUpdate: (id: any, title: any) => {\n    dispatch(updateReportTitleThunk(id, title));\n  },\n  onQueryUpdate: (id: any, query: any) => {\n    dispatch(updateReportQueryThunk(id, query));\n  },\n  onTypeUpdate: (id: any, type: any) => {\n    dispatch(updateReportTypeThunk(id, type));\n  },\n  onReportSettingUpdate: (id: any, setting: any, value: any) => {\n    dispatch(updateReportSettingThunk(id, setting, value));\n  },\n  onFieldsUpdate: (id: any, fields: any) => {\n    dispatch(updateFieldsThunk(id, fields));\n  },\n  onGlobalParameterUpdate: (key: any, value: any) => {\n    dispatch(updateGlobalParameterThunk(key, value));\n  },\n  onSelectionUpdate: (id: any, selectable: any, field: any) => {\n    dispatch(updateSelectionThunk(id, selectable, field));\n  },\n  onToggleCardSettings: (id: any, open: any) => {\n    dispatch(toggleCardSettingsThunk(id, open));\n  },\n  onReportHelpButtonPressed: () => {\n    dispatch(setReportHelpModalOpen(true));\n  },\n  onToggleReportSettings: (id: any) => {\n    dispatch(toggleReportSettings(id));\n  },\n  onDatabaseChanged: (id: any, database: any) => {\n    dispatch(updateReportDatabaseThunk(id, database));\n  },\n  createNotification: (title: any, message: any) => {\n    dispatch(createNotificationThunk(title, message));\n  },\n  loadDatabaseListFromNeo4j: (driver, callback) => dispatch(loadDatabaseListFromNeo4jThunk(driver, callback)),\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoCard);\n"
  },
  {
    "path": "src/card/CardActions.ts",
    "content": "/**\n * A list of actions to perform on cards.\n */\n\nexport const TOGGLE_CARD_SETTINGS = 'PAGE/CARD/TOGGLE_CARD_SETTINGS';\nexport const toggleCardSettings = (pagenumber: any, id: any, open: any) => ({\n  type: TOGGLE_CARD_SETTINGS,\n  payload: { pagenumber, id, open },\n});\n\nexport const HARD_RESET_CARD_SETTINGS = 'PAGE/CARD/HARD_RESET_CARD_SETTINGS';\nexport const hardResetCardSettings = (pagenumber: any, id: any) => ({\n  type: HARD_RESET_CARD_SETTINGS,\n  payload: { pagenumber, id },\n});\n\nexport const UPDATE_REPORT_TITLE = 'PAGE/CARD/UPDATE_REPORT_TITLE';\nexport const updateReportTitle = (pagenumber: number, id: number, title: any) => ({\n  type: UPDATE_REPORT_TITLE,\n  payload: { pagenumber, id, title },\n});\n\nexport const UPDATE_REPORT_SIZE = 'PAGE/CARD/UPDATE_REPORT_SIZE';\nexport const updateReportSize = (pagenumber: number, id: number, width: any, height: any) => ({\n  type: UPDATE_REPORT_SIZE,\n  payload: { pagenumber, id, width, height },\n});\n\nexport const UPDATE_REPORT_QUERY = 'PAGE/CARD/UPDATE_REPORT_QUERY';\nexport const updateReportQuery = (pagenumber: number, id: number, query: any) => ({\n  type: UPDATE_REPORT_QUERY,\n  payload: { pagenumber, id, query },\n});\n\nexport const UPDATE_CYPHER_PARAMETERS = 'PAGE/CARD/UPDATE_CYPHER_PARAMETERS';\nexport const updateCypherParameters = (pagenumber: number, id: number, parameters: any) => ({\n  type: UPDATE_CYPHER_PARAMETERS,\n  payload: { pagenumber, id, parameters },\n});\n\nexport const UPDATE_REPORT_TYPE = 'PAGE/CARD/UPDATE_REPORT_TYPE';\nexport const updateReportType = (pagenumber: number, id: number, type: any) => ({\n  type: UPDATE_REPORT_TYPE,\n  payload: { pagenumber, id, type },\n});\n\nexport const UPDATE_FIELDS = 'PAGE/CARD/UPDATE_FIELDS';\nexport const updateFields = (pagenumber: number, id: number, fields: any) => ({\n  type: UPDATE_FIELDS,\n  payload: { pagenumber, id, fields },\n});\n\nexport const UPDATE_SCHEMA = 'PAGE/CARD/UPDATE_SCHEMA';\nexport const updateSchema = (pagenumber: number, id: number, schema: any) => ({\n  type: UPDATE_SCHEMA,\n  payload: { pagenumber, id, schema },\n});\n\nexport const UPDATE_SELECTION = 'PAGE/CARD/UPDATE_SELECTION';\nexport const updateSelection = (pagenumber: number, id: number, selectable: any, field: any) => ({\n  type: UPDATE_SELECTION,\n  payload: { pagenumber, id, selectable, field },\n});\n\nexport const UPDATE_ALL_SELECTIONS = 'PAGE/CARD/UPDATE_ALL_SELECTIONS';\nexport const updateAllSelections = (pagenumber: number, id: number, selections: any) => ({\n  type: UPDATE_ALL_SELECTIONS,\n  payload: { pagenumber, id, selections },\n});\n\nexport const CLEAR_SELECTION = 'PAGE/CARD/CLEAR_SELECTION';\nexport const clearSelection = (pagenumber: number, id: number) => ({\n  type: CLEAR_SELECTION,\n  payload: { pagenumber, id },\n});\n\nexport const UPDATE_REPORT_SETTING = 'PAGE/CARD/UPDATE_REPORT_SETTING';\nexport const updateReportSetting = (pagenumber: number, id: number, setting: any, value: any) => ({\n  type: UPDATE_REPORT_SETTING,\n  payload: { pagenumber, id, setting, value },\n});\n\nexport const TOGGLE_REPORT_SETTINGS = 'PAGE/CARD/TOGGLE_REPORT_SETTINGS';\nexport const toggleReportSettings = (id: any) => ({\n  type: TOGGLE_REPORT_SETTINGS,\n  payload: { id },\n});\n\nexport const UPDATE_REPORT_DATABASE = 'PAGE/CARD/UPDATE_REPORT_DATABASE';\nexport const updateReportDatabase = (pagenumber: number, id: number, database: any) => ({\n  type: UPDATE_REPORT_DATABASE,\n  payload: { pagenumber, id, database },\n});\n"
  },
  {
    "path": "src/card/CardAddButton.tsx",
    "content": "import React from 'react';\nimport { connect } from 'react-redux';\nimport { Card, CardContent } from '@mui/material';\nimport { IconButton } from '@neo4j-ndl/react';\nimport { SquaresPlusIconOutline } from '@neo4j-ndl/react/icons';\n\n/**\n * Button to add a new report to the current page.\n */\nconst NeoAddNewCard = ({ onCreatePressed }) => {\n  return (\n    <div>\n      <Card className='n-bg-dark-neutral-text-weak'>\n        <CardContent style={{ height: '429px' }}>\n          <IconButton\n            aria-label='add report'\n            className='centered'\n            onClick={() => {\n              onCreatePressed();\n            }}\n            size='large'\n            floating\n          >\n            <SquaresPlusIconOutline />\n          </IconButton>\n        </CardContent>\n      </Card>\n    </div>\n  );\n};\n\nconst mapStateToProps = () => ({});\n\nconst mapDispatchToProps = () => ({});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoAddNewCard);\n"
  },
  {
    "path": "src/card/CardReducer.ts",
    "content": "import {\n  CLEAR_SELECTION,\n  HARD_RESET_CARD_SETTINGS,\n  TOGGLE_REPORT_SETTINGS,\n  UPDATE_ALL_SELECTIONS,\n  UPDATE_CYPHER_PARAMETERS,\n  UPDATE_FIELDS,\n  UPDATE_SCHEMA,\n  UPDATE_REPORT_QUERY,\n  UPDATE_REPORT_SETTING,\n  UPDATE_REPORT_SIZE,\n  UPDATE_REPORT_TITLE,\n  UPDATE_REPORT_TYPE,\n  UPDATE_SELECTION,\n  UPDATE_REPORT_DATABASE,\n} from './CardActions';\nimport { TOGGLE_CARD_SETTINGS } from './CardActions';\nimport { createUUID } from '../utils/uuid';\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\n/**\n * State reducers for a single card instance as part of a report.\n */\n\nexport const CARD_INITIAL_STATE = {\n  id: createUUID(),\n  title: '',\n  query: '\\n\\n\\n',\n  settingsOpen: false,\n  advancedSettingsOpen: false,\n  width: 3,\n  height: 3,\n  x: 0,\n  y: 0,\n  type: 'table',\n  fields: [],\n  selection: {},\n  settings: {},\n  collapseTimeout: 'auto',\n};\n\nexport const cardReducer = (state = CARD_INITIAL_STATE, action: { type: any; payload: any }) => {\n  const { type, payload } = action;\n\n  if (!action.type.startsWith('PAGE/CARD/')) {\n    return state;\n  }\n\n  switch (type) {\n    case UPDATE_REPORT_TITLE: {\n      const { title } = payload;\n      state = update(state, { title: title });\n      return state;\n    }\n    case UPDATE_REPORT_SIZE: {\n      const { width, height } = payload;\n      state = update(state, { width: width, height: height });\n      return state;\n    }\n    case UPDATE_REPORT_QUERY: {\n      const { query } = payload;\n      state = update(state, { query: query });\n      return state;\n    }\n    case UPDATE_CYPHER_PARAMETERS: {\n      const { parameters } = payload;\n      state = update(state, { parameters: parameters });\n      return state;\n    }\n    case UPDATE_FIELDS: {\n      const { fields } = payload;\n      state = update(state, { fields: fields });\n      return state;\n    }\n    case UPDATE_SCHEMA: {\n      const { schema } = payload;\n      state = update(state, { schema: schema });\n      return state;\n    }\n    case UPDATE_REPORT_TYPE: {\n      const { type } = payload;\n      state = update(state, { type: type });\n      return state;\n    }\n    case CLEAR_SELECTION: {\n      state = update(state, { selection: {} });\n      return state;\n    }\n    case UPDATE_SELECTION: {\n      const { selectable, field } = payload;\n      const selection = state.selection ? state.selection : {};\n\n      const entry = {};\n      entry[selectable] = field;\n      state = update(state, { selection: update(selection, entry) });\n      return state;\n    }\n\n    case UPDATE_ALL_SELECTIONS: {\n      const { selections } = payload;\n      state = update(state, { selection: selections });\n      return state;\n    }\n\n    case UPDATE_REPORT_SETTING: {\n      const { setting, value } = payload;\n      const settings = state.settings ? state.settings : {};\n      // Javascript is amazing, so \"\" == 0. Instead we check if the string length is zero...\n      if (value == undefined || value.toString().length == 0) {\n        delete settings[setting];\n        update(state, { settings: settings });\n        return state;\n      }\n\n      const entry = {};\n      entry[setting] = value;\n      state = update(state, { settings: update(settings, entry) });\n      return state;\n    }\n    case TOGGLE_CARD_SETTINGS: {\n      const { open } = payload;\n      state = update(state, { settingsOpen: open, collapseTimeout: 'auto' });\n      return state;\n    }\n    case HARD_RESET_CARD_SETTINGS: {\n      state = update(state, { settingsOpen: false, collapseTimeout: 0 });\n      return state;\n    }\n    case TOGGLE_REPORT_SETTINGS: {\n      state = update(state, { advancedSettingsOpen: !state.advancedSettingsOpen });\n      return state;\n    }\n    case UPDATE_REPORT_DATABASE: {\n      const { database } = payload;\n      state = update(state, { database: database });\n      return state;\n    }\n    default: {\n      return state;\n    }\n  }\n};\n\nexport default cardReducer;\n"
  },
  {
    "path": "src/card/CardSelectors.ts",
    "content": "export const getDashboardTitle = (state: any) => state.dashboard.title;\n\nexport const getReportState = (state: any, id: any) => {\n  const { pagenumber } = state.dashboard.settings;\n  return state.dashboard.pages[pagenumber].reports.find((o) => o.id === id);\n};\n"
  },
  {
    "path": "src/card/CardStyle.ts",
    "content": "// TODO We need to refactor styled components\nimport styled from 'styled-components';\n\nexport const ReportItemContainer = styled.div``;\n"
  },
  {
    "path": "src/card/CardThunks.ts",
    "content": "import {\n  updateReportTitle,\n  updateReportQuery,\n  updateSelection,\n  updateCypherParameters,\n  updateFields,\n  updateReportType,\n  updateReportSetting,\n  toggleCardSettings,\n  clearSelection,\n  updateAllSelections,\n  updateReportDatabase,\n  updateSchema,\n} from './CardActions';\nimport { createNotificationThunk } from '../page/PageThunks';\nimport { getReportTypes } from '../extensions/ExtensionUtils';\nimport isEqual from 'lodash.isequal';\nimport { SELECTION_TYPES } from '../config/CardConfig';\nimport { getSelectionBasedOnFields } from '../chart/ChartUtils';\n\nexport const updateReportTitleThunk = (id, title) => (dispatch: any, getState: any) => {\n  try {\n    const state = getState();\n    const { pagenumber } = state.dashboard.settings;\n    dispatch(updateReportTitle(pagenumber, id, title));\n  } catch (e) {\n    dispatch(createNotificationThunk('Cannot update report title', e));\n  }\n};\n\n/*\nThunk used to update the database used from a report\n*/\nexport const updateReportDatabaseThunk = (id, database) => (dispatch: any, getState: any) => {\n  try {\n    const state = getState();\n    const { pagenumber } = state.dashboard.settings;\n    dispatch(updateReportDatabase(pagenumber, id, database));\n  } catch (e) {\n    dispatch(createNotificationThunk('Cannot update report database', e));\n  }\n};\n\nexport const updateReportQueryThunk = (id, query) => (dispatch: any, getState: any) => {\n  try {\n    const state = getState();\n    const { pagenumber } = state.dashboard.settings;\n    dispatch(updateReportQuery(pagenumber, id, query));\n  } catch (e) {\n    dispatch(createNotificationThunk('Cannot update query', e));\n  }\n};\n\nexport const updateCypherParametersThunk = (id, parameters) => (dispatch: any, getState: any) => {\n  try {\n    const state = getState();\n    const { pagenumber } = state.dashboard.settings;\n    dispatch(updateCypherParameters(pagenumber, id, parameters));\n  } catch (e) {\n    dispatch(createNotificationThunk('Cannot update cypher parameters rate', e));\n  }\n};\n\nexport const updateReportTypeThunk = (id, type) => (dispatch: any, getState: any) => {\n  try {\n    const state = getState();\n    const { pagenumber } = state.dashboard.settings;\n\n    dispatch(updateReportType(pagenumber, id, type));\n    dispatch(updateFields(pagenumber, id, []));\n    dispatch(updateSchema(pagenumber, id, []));\n    dispatch(clearSelection(pagenumber, id));\n  } catch (e) {\n    dispatch(createNotificationThunk('Cannot update report type', e));\n  }\n};\n\nexport const updateFieldsThunk =\n  (id, fields, schema = false) =>\n  (dispatch: any, getState: any) => {\n    try {\n      const state = getState();\n      const { pagenumber } = state.dashboard.settings;\n      const extensions = Object.fromEntries(Object.entries(state.dashboard.extensions).filter(([_, v]) => v.active));\n      const oldReport = state.dashboard.pages[pagenumber].reports.find((o) => o.id === id);\n\n      if (!oldReport) {\n        return;\n      }\n      const oldFields = schema ? oldReport.schema : oldReport.fields;\n      const reportType = oldReport.type;\n      const oldSelection = oldReport.selection;\n      const reportTypes = getReportTypes(extensions);\n      const selectableFields = reportTypes[reportType].selection; // The dictionary of selectable fields as defined in the config.\n      const { autoAssignSelectedProperties } = reportTypes[reportType];\n      const selectables = selectableFields ? Object.keys(selectableFields) : [];\n\n      // If the new set of fields is not equal to the current set of fields, we ned to update the field selection.\n      if (!isEqual(oldFields, fields) || Object.keys(oldSelection).length === 0) {\n        selectables.forEach((selection, i) => {\n          if (fields.includes(oldSelection[selection])) {\n            // If the current selection is still present in the new set of fields, no need to reset.\n            // Also we ignore this on a node property selector.\n            /* continue */\n          } else if (selectableFields[selection].optional) {\n            // If the fields change, always set optional selections to none.\n            if (selectableFields[selection].multiple) {\n              dispatch(updateSelection(pagenumber, id, selection, ['(none)']));\n            } else {\n              dispatch(updateSelection(pagenumber, id, selection, '(none)'));\n            }\n          } else if (fields.length > 0) {\n            // For multi selections, select the Nth item of the result fields as a single item array.\n            if (selectableFields[selection].multiple) {\n              // only update if the old selection no longer covers the new set of fields...\n              if (!oldSelection[selection] || !oldSelection[selection].every((v) => fields.includes(v))) {\n                dispatch(updateSelection(pagenumber, id, selection, [fields[Math.min(i, fields.length - 1)]]));\n              }\n            } else if (selectableFields[selection].type == SELECTION_TYPES.NODE_PROPERTIES) {\n              // For node property selections, select the most obvious properties of the node to display.\n              const selection = getSelectionBasedOnFields(fields, oldSelection, autoAssignSelectedProperties);\n              dispatch(updateAllSelections(pagenumber, id, selection));\n            } else {\n              // Else, default the selection to the Nth item of the result set fields.\n              dispatch(updateSelection(pagenumber, id, selection, fields[Math.min(i, fields.length - 1)]));\n            }\n          }\n        });\n        // Set the new set of fields for the report so that we may select them.\n\n        if (schema) {\n          dispatch(updateSchema(pagenumber, id, fields));\n        } else {\n          dispatch(updateFields(pagenumber, id, fields));\n        }\n      }\n    } catch (e) {\n      dispatch(createNotificationThunk('Cannot update report fields', e));\n    }\n  };\n\nexport const updateSelectionThunk = (id, selectable, field) => (dispatch: any, getState: any) => {\n  try {\n    const state = getState();\n    const { pagenumber } = state.dashboard.settings;\n    dispatch(updateSelection(pagenumber, id, selectable, field));\n  } catch (e) {\n    dispatch(createNotificationThunk('Cannot update report selection', e));\n  }\n};\n\nexport const toggleCardSettingsThunk = (id, open) => (dispatch: any, getState: any) => {\n  try {\n    const state = getState();\n    const { pagenumber } = state.dashboard.settings;\n    dispatch(toggleCardSettings(pagenumber, id, open));\n  } catch (e) {\n    dispatch(createNotificationThunk('Cannot open card settings', e));\n  }\n};\n\nexport const updateReportSettingThunk = (id, setting, value) => (dispatch: any, getState: any) => {\n  try {\n    const state = getState();\n    const extensions = Object.fromEntries(Object.entries(state.dashboard.extensions).filter(([_, v]) => v.active));\n    const { pagenumber } = state.dashboard.settings;\n\n    // If we disable optional selections (e.g. grouping), we reset these selections to their none value.\n    if (setting == 'showOptionalSelections' && value == false) {\n      const reportType = state.dashboard.pages[pagenumber].reports.find((o) => o.id === id).type;\n      const reportTypes = getReportTypes(extensions);\n      const selectableFields = reportTypes[reportType].selection;\n      const optionalSelectables = selectableFields\n        ? Object.keys(selectableFields).filter((key) => selectableFields[key].optional)\n        : [];\n      optionalSelectables.forEach((selection) => {\n        dispatch(updateSelection(pagenumber, id, selection, '(none)'));\n      });\n    }\n    dispatch(updateReportSetting(pagenumber, id, setting, value));\n  } catch (e) {\n    dispatch(createNotificationThunk('Error when updating report settings', e));\n  }\n};\n"
  },
  {
    "path": "src/card/settings/CardSettings.tsx",
    "content": "import React from 'react';\nimport { ReportItemContainer } from '../CardStyle';\nimport NeoCardSettingsHeader from './CardSettingsHeader';\nimport NeoCardSettingsContent from './CardSettingsContent';\nimport NeoCardSettingsFooter from './CardSettingsFooter';\nimport { CardContent } from '@mui/material';\nimport { CARD_HEADER_HEIGHT } from '../../config/CardConfig';\n\nconst NeoCardSettings = ({\n  settingsOpen,\n  pagenumber,\n  reportId,\n  query,\n  database, // Current database related to the report\n  databaseList, // List of databases the user can choose from ('system' is filtered out)\n  width,\n  height,\n  type,\n  reportSettings,\n  reportSettingsOpen,\n  fields,\n  schema,\n  heightPx,\n  extensions, // A set of enabled extensions.\n  onQueryUpdate,\n  onDatabaseChanged, // When the database related to a report is changed it must be stored in the report state\n  onRemovePressed,\n  onClonePressed,\n  onReportSettingUpdate,\n  onToggleCardSettings,\n  onTypeUpdate,\n  setActive,\n  onReportHelpButtonPressed,\n  onToggleReportSettings,\n  dashboardSettings,\n  expanded,\n  onToggleCardExpand,\n}) => {\n  const reportHeight = heightPx - CARD_HEADER_HEIGHT + 19;\n\n  const cardSettingsHeader = (\n    <NeoCardSettingsHeader\n      expanded={expanded}\n      onToggleCardExpand={onToggleCardExpand}\n      onRemovePressed={onRemovePressed}\n      onClonePressed={onClonePressed}\n      onReportHelpButtonPressed={onReportHelpButtonPressed}\n      fullscreenEnabled={dashboardSettings.fullscreenEnabled}\n      onToggleCardSettings={(e) => {\n        setActive(reportSettings.autorun !== undefined ? reportSettings.autorun : true);\n        onToggleCardSettings(e);\n      }}\n    />\n  );\n\n  // TODO - instead of hiding everything based on settingsopen, only hide the components that slow down render (cypher editor)\n  const cardSettingsContent = settingsOpen ? (\n    <NeoCardSettingsContent\n      pagenumber={pagenumber}\n      reportId={reportId}\n      query={query}\n      database={database}\n      reportSettings={reportSettings}\n      width={width}\n      height={height}\n      type={type}\n      extensions={extensions}\n      databaseList={databaseList}\n      onDatabaseChanged={onDatabaseChanged}\n      onQueryUpdate={onQueryUpdate}\n      onReportSettingUpdate={onReportSettingUpdate}\n      onTypeUpdate={onTypeUpdate}\n      forceRunQuery={onToggleCardSettings}\n    ></NeoCardSettingsContent>\n  ) : (\n    <CardContent className='n-py-2' />\n  );\n\n  const cardSettingsFooter = settingsOpen ? (\n    <NeoCardSettingsFooter\n      type={type}\n      fields={fields}\n      schema={schema}\n      extensions={extensions}\n      reportSettings={reportSettings}\n      reportSettingsOpen={reportSettingsOpen}\n      onToggleReportSettings={onToggleReportSettings}\n      onReportSettingUpdate={onReportSettingUpdate}\n    ></NeoCardSettingsFooter>\n  ) : (\n    <div></div>\n  );\n\n  return (\n    <div\n      className={`card-view n-bg-palette-neutral-bg-weak n-text-palette-neutral-text-default ${\n        expanded ? 'expanded' : ''\n      } n-overflow-y-auto n-h-full`}\n    >\n      {cardSettingsHeader}\n      <ReportItemContainer style={{ height: reportHeight }} className='-n-mt-2'>\n        {cardSettingsContent}\n        {cardSettingsFooter}\n      </ReportItemContainer>\n    </div>\n  );\n};\n\nexport default NeoCardSettings;\n"
  },
  {
    "path": "src/card/settings/CardSettingsContent.tsx",
    "content": "import React, { useEffect } from 'react';\nimport CardContent from '@mui/material/CardContent';\nimport debounce from 'lodash/debounce';\nimport { useCallback } from 'react';\nimport NeoCodeEditorComponent, {\n  DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE,\n} from '../../component/editor/CodeEditorComponent';\nimport { getReportTypes } from '../../extensions/ExtensionUtils';\nimport { Dropdown } from '@neo4j-ndl/react';\nimport { EXTENSIONS_CARD_SETTINGS_COMPONENT } from '../../extensions/ExtensionConfig';\nimport { objMerge } from '../../utils/ObjectManipulation';\n\nconst NeoCardSettingsContent = ({\n  pagenumber,\n  reportId,\n  query,\n  database, // Current report database\n  databaseList, // List of databases the user can choose from ('system' is filtered out)\n  reportSettings,\n  type,\n  extensions,\n  onQueryUpdate,\n  onReportSettingUpdate,\n  onTypeUpdate,\n  forceRunQuery, // Callback to force close the card settings.\n  onDatabaseChanged, // When the database related to a report is changed it must be stored in the report state\n}) => {\n  // Ensure that we only trigger a text update event after the user has stopped typing.\n  const [queryText, setQueryText] = React.useState(query);\n  const debouncedQueryUpdate = useCallback(debounce(onQueryUpdate, 200), []);\n  // State to manage the current database entry inside the form\n  const [databaseText, setDatabaseText] = React.useState(database);\n  const debouncedDatabaseUpdate = useCallback(debounce(onDatabaseChanged, 200), []);\n\n  useEffect(() => {\n    // Reset text to the dashboard state when the page gets reorganized.\n    if (query !== queryText) {\n      setQueryText(query);\n    }\n  }, [query]);\n\n  const reportTypes = getReportTypes(extensions);\n  const report = reportTypes[type];\n  const SettingsComponent = report?.settingsComponent || {};\n\n  function hasExtensionComponents() {\n    return (\n      Object.keys(EXTENSIONS_CARD_SETTINGS_COMPONENT).filter(\n        (name) => extensions[name] && EXTENSIONS_CARD_SETTINGS_COMPONENT[name]\n      ).length > 0\n    );\n  }\n\n  function updateCypherQuery(value) {\n    debouncedQueryUpdate(value);\n    setQueryText(value);\n  }\n\n  function renderExtensionsComponents() {\n    const res = (\n      <>\n        {Object.keys(EXTENSIONS_CARD_SETTINGS_COMPONENT).map((name) => {\n          const Component = extensions[name] ? EXTENSIONS_CARD_SETTINGS_COMPONENT[name] : '';\n          return Component ? (\n            <Component\n              pagenumber={pagenumber}\n              reportId={reportId}\n              reportType={type}\n              extensions={extensions}\n              onExecute={() => {\n                onQueryUpdate(queryText);\n                forceRunQuery();\n              }}\n              cypherQuery={queryText}\n              updateCypherQuery={updateCypherQuery}\n            />\n          ) : (\n            <></>\n          );\n        })}\n      </>\n    );\n    return res;\n  }\n\n  const defaultQueryBoxComponent = (\n    <>\n      <NeoCodeEditorComponent\n        value={queryText}\n        editable={true}\n        language={report?.inputMode || 'cypher'}\n        onExecute={() => {\n          onQueryUpdate(queryText);\n          forceRunQuery();\n        }}\n        onChange={(value) => {\n          updateCypherQuery(value);\n        }}\n        placeholder={`Enter Cypher here...`}\n      />\n      <div style={DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE}>{report?.helperText || ''}</div>\n    </>\n  );\n\n  return (\n    <CardContent className='n-py-2'>\n      <Dropdown\n        data-test='type-dropdown'\n        id='type'\n        label='Type'\n        type='select'\n        selectProps={{\n          onChange: (newValue) =>\n            newValue && onTypeUpdate(Object.keys(reportTypes).find((key) => reportTypes[key].label === newValue.value)),\n          options: Object.keys(reportTypes).map((option) => ({\n            label: report && reportTypes[option].label,\n            value: report && reportTypes[option].label,\n          })),\n          value: {\n            label: report?.label || '',\n            value: report?.label || '',\n          },\n          menuPortalTarget: document.querySelector('#overlay'),\n        }}\n        fluid\n        style={{ marginLeft: '0px', marginRight: '10px', width: '47%', maxWidth: '200px', display: 'inline-block' }}\n      />\n\n      {report?.disableDatabaseSelector == undefined ? (\n        <Dropdown\n          id='databaseSelector'\n          label='Database'\n          placeholder='neo4j'\n          type='select'\n          selectProps={{\n            onChange: (newValue) => {\n              newValue && setDatabaseText(newValue.value);\n              newValue && debouncedDatabaseUpdate(newValue.value);\n            },\n            options: databaseList.map((database) => ({\n              label: database,\n              value: database,\n            })),\n            value: { label: databaseText, value: databaseText },\n            menuPortalTarget: document.querySelector('#overlay'),\n          }}\n          fluid\n          style={{ marginLeft: '0px', marginRight: '10px', width: '47%', maxWidth: '200px', display: 'inline-block' }}\n        />\n      ) : (\n        <></>\n      )}\n\n      <br />\n      <br />\n      {/* Allow for overriding the code box with a custom component */}\n      {report && report.settingsComponent ? (\n        <SettingsComponent\n          onReportSettingUpdate={onReportSettingUpdate}\n          settings={objMerge({ helperText: report.helperText, inputMode: report.inputMode }, reportSettings)}\n          database={database}\n          query={query}\n          onQueryUpdate={onQueryUpdate}\n          onExecute={() => {\n            onQueryUpdate(queryText);\n            forceRunQuery();\n          }}\n        />\n      ) : (\n        <div>{hasExtensionComponents() ? renderExtensionsComponents() : defaultQueryBoxComponent}</div>\n      )}\n    </CardContent>\n  );\n};\n\nexport default NeoCardSettingsContent;\n"
  },
  {
    "path": "src/card/settings/CardSettingsFooter.tsx",
    "content": "import React, { useEffect } from 'react';\nimport debounce from 'lodash/debounce';\nimport { useCallback } from 'react';\nimport { FormGroup, Tooltip } from '@mui/material';\nimport NeoSetting from '../../component/field/Setting';\nimport {\n  NeoCustomReportStyleModal,\n  RULE_BASED_REPORT_CUSTOMIZATIONS,\n} from '../../extensions/styling/StyleRuleCreationModal';\nimport { getReportTypes } from '../../extensions/ExtensionUtils';\nimport { RULE_BASED_REPORT_ACTIONS_CUSTOMIZATIONS } from '../../extensions/actions/ActionsRuleCreationModal';\nimport NeoCustomReportActionsModal from '../../extensions/actions/ActionsRuleCreationModal';\nimport { AdjustmentsHorizontalIconOutline, SparklesIconOutline } from '@neo4j-ndl/react/icons';\nimport { IconButton, Switch } from '@neo4j-ndl/react';\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\nconst NeoCardSettingsFooter = ({\n  type,\n  fields = [],\n  schema = [],\n  reportSettings,\n  reportSettingsOpen,\n  extensions = {},\n  onToggleReportSettings,\n  onReportSettingUpdate,\n}) => {\n  const [reportSettingsText, setReportSettingsText] = React.useState(reportSettings);\n\n  // Variables related to customizing report settings\n  const [customReportStyleModalOpen, setCustomReportStyleModalOpen] = React.useState(false);\n  const settingToCustomize = 'styleRules';\n\n  // Variables related to customizing report actions\n  const [customReportActionsModalOpen, setCustomReportActionsModalOpen] = React.useState(false);\n  const actionsToCustomize = 'actionsRules';\n\n  const debouncedReportSettingUpdate = useCallback(debounce(onReportSettingUpdate, 250), []);\n\n  const updateSpecificReportSetting = (field: string, value: unknown) => {\n    const entry = {};\n    entry[field] = value;\n    setReportSettingsText(update(reportSettingsText, entry));\n    debouncedReportSettingUpdate(field, value);\n  };\n\n  const reportTypes = getReportTypes(extensions);\n\n  // Contains, for a certain type of chart, its disabling logic\n  const disabledDependency = reportTypes[type] && reportTypes[type].disabledDependency;\n\n  /**\n   * This method manages the disabling logic for all the settings inside the footer.\n   * The logic is based on the disabledDependency param inside the chart's configuration\n   * @param field\n   * @returns\n   */\n  const getDisabled = (field: string) => {\n    // By default an option is enabled\n    let isDisabled = false;\n    let dependencyLogic = disabledDependency[field];\n    if (dependencyLogic != undefined) {\n      // Getting the current parameter defined in the settings of the report\n      // (if undefined, the param will be treated as undefined (boolean false)\n      let currentValue = reportSettingsText[dependencyLogic.dependsOn];\n      if (typeof dependencyLogic.operator === 'boolean') {\n        if (!dependencyLogic.operator) {\n          isDisabled = !currentValue;\n        }\n      }\n      // if the value is in the list of values that enable the option, then enable the option\n      else if (dependencyLogic.operator === 'not in') {\n        isDisabled = !dependencyLogic.values.includes(currentValue);\n      }\n    }\n    return isDisabled;\n  };\n\n  useEffect(() => {\n    // Reset text to the dashboard state when the page gets reorganized.\n    setReportSettingsText(reportSettings);\n  }, [JSON.stringify(reportSettings)]);\n\n  const settings = reportTypes[type] ? reportTypes[type].settings : {};\n\n  // If there are no advanced settings, render nothing.\n  if (Object.keys(settings).length == 0) {\n    return <div></div>;\n  }\n\n  // Else, build the advanced settings view.\n  const advancedReportSettings = (\n    <div style={{ marginLeft: '5px' }}>\n      {Object.keys(settings).map((setting) => {\n        let isDisabled = false;\n        // Adding disabling logic to specific entries but only if the logic is defined inside the configuration\n        if (disabledDependency != undefined) {\n          isDisabled = getDisabled(setting);\n        }\n        return (\n          <NeoSetting\n            key={setting}\n            name={setting}\n            value={reportSettingsText[setting]}\n            type={settings[setting].type}\n            label={settings[setting].label}\n            defaultValue={settings[setting].default}\n            choices={settings[setting].values}\n            disabled={isDisabled}\n            onChange={(e) => updateSpecificReportSetting(setting, e)}\n          />\n        );\n      })}\n    </div>\n  );\n\n  // TODO - Make the extensions more pluggable and dynamic, instead of hardcoded here.\n  // ^ keep modals at a higher level in the object hierarchy instead of injecting in the footer.\n  return (\n    <div>\n      {extensions.styling && extensions.styling.active ? (\n        <NeoCustomReportStyleModal\n          settingName={settingToCustomize}\n          settingValue={reportSettings[settingToCustomize]}\n          type={type}\n          fields={fields}\n          schema={schema}\n          customReportStyleModalOpen={customReportStyleModalOpen}\n          setCustomReportStyleModalOpen={setCustomReportStyleModalOpen}\n          onReportSettingUpdate={onReportSettingUpdate}\n        ></NeoCustomReportStyleModal>\n      ) : (\n        <></>\n      )}\n\n      {extensions.actions && extensions.actions.active ? (\n        <NeoCustomReportActionsModal\n          settingName={actionsToCustomize}\n          settingValue={reportSettings[actionsToCustomize]}\n          type={type}\n          fields={fields}\n          customReportActionsModalOpen={customReportActionsModalOpen}\n          setCustomReportActionsModalOpen={setCustomReportActionsModalOpen}\n          onReportSettingUpdate={onReportSettingUpdate}\n        ></NeoCustomReportActionsModal>\n      ) : (\n        <></>\n      )}\n\n      <table\n        style={{\n          borderTop: '1px dashed lightgrey',\n          width: '100%',\n        }}\n      >\n        <tbody>\n          <tr>\n            <td>\n              <FormGroup className='n-my-2'>\n                <Switch\n                  label='Advanced settings'\n                  checked={reportSettingsOpen}\n                  onChange={onToggleReportSettings}\n                  className='n-ml-2'\n                />\n              </FormGroup>\n            </td>\n            <td>\n              {RULE_BASED_REPORT_CUSTOMIZATIONS[type] && extensions.styling && extensions.styling.active ? (\n                <Tooltip title='Set rule-based styling' aria-label='' disableInteractive>\n                  <IconButton\n                    style={{ float: 'right', marginRight: '10px' }}\n                    aria-label='custom styling'\n                    onClick={() => {\n                      setCustomReportStyleModalOpen(true); // Open the modal.\n                    }}\n                    clean\n                  >\n                    <AdjustmentsHorizontalIconOutline />\n                  </IconButton>\n                </Tooltip>\n              ) : (\n                <></>\n              )}\n              {extensions.actions && extensions.actions.active && RULE_BASED_REPORT_ACTIONS_CUSTOMIZATIONS[type] ? (\n                <Tooltip title='Set report actions' aria-label='' disableInteractive>\n                  <IconButton\n                    style={{ float: 'right' }}\n                    aria-label='custom actions'\n                    clean\n                    onClick={() => {\n                      setCustomReportActionsModalOpen(true); // Open the modal.\n                    }}\n                  >\n                    <SparklesIconOutline />\n                  </IconButton>\n                </Tooltip>\n              ) : (\n                <></>\n              )}\n            </td>\n          </tr>\n          <tr>\n            <td colSpan={2} style={{ maxWidth: '100%' }}>\n              {reportSettingsOpen ? advancedReportSettings : <div></div>}\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    </div>\n  );\n};\n\nexport default NeoCardSettingsFooter;\n"
  },
  {
    "path": "src/card/settings/CardSettingsHeader.tsx",
    "content": "import React from 'react';\nimport { Tooltip, CardHeader } from '@mui/material';\nimport { IconButton } from '@neo4j-ndl/react';\nimport {\n  ExpandIcon,\n  ShrinkIcon,\n  DragIcon,\n  QuestionMarkCircleIconOutline,\n  TrashIconOutline,\n  DocumentDuplicateIconOutline,\n  PlayCircleIconSolid,\n} from '@neo4j-ndl/react/icons';\n\nconst NeoCardSettingsHeader = ({\n  onRemovePressed,\n  onToggleCardSettings,\n  onToggleCardExpand,\n  expanded,\n  fullscreenEnabled,\n  onReportHelpButtonPressed,\n  onClonePressed,\n}) => {\n  const maximizeButton = (\n    <IconButton aria-label='maximize' onClick={onToggleCardExpand}>\n      <ExpandIcon />\n    </IconButton>\n  );\n\n  const unMaximizeButton = (\n    <IconButton aria-label='un-maximize' onClick={onToggleCardExpand}>\n      <ShrinkIcon />\n    </IconButton>\n  );\n\n  return (\n    <CardHeader\n      avatar={\n        <div style={{ marginTop: '-8px', paddingBottom: '1px' }}>\n          <IconButton clean size='medium' aria-label={'Move Report'} className='n-relative -n-left-3 drag-handle'>\n            <DragIcon aria-label={'Move Report'} />\n          </IconButton>\n          <Tooltip title='Help' aria-label='Help' disableInteractive>\n            <IconButton aria-label='Help' onClick={onReportHelpButtonPressed} clean size='medium'>\n              <QuestionMarkCircleIconOutline aria-label={'Help'} />\n            </IconButton>\n          </Tooltip>\n          <Tooltip title='Delete' aria-label='Delete' disableInteractive>\n            <IconButton style={{ color: 'red' }} aria-label='remove' onClick={onRemovePressed} clean size='medium'>\n              <TrashIconOutline aria-label={'Delete'} />\n            </IconButton>\n          </Tooltip>\n          <Tooltip title='Clone' aria-label='Clone' disableInteractive>\n            <IconButton style={{ color: 'green' }} aria-label='Clone' onClick={onClonePressed} clean size='medium'>\n              <DocumentDuplicateIconOutline aria-label={'Clone'} />\n            </IconButton>\n          </Tooltip>\n        </div>\n      }\n      action={\n        <>\n          {fullscreenEnabled ? expanded ? unMaximizeButton : maximizeButton : <></>}\n          <Tooltip title='Run' aria-label='run' disableInteractive>\n            <IconButton\n              aria-label='run'\n              onClick={(e) => {\n                e.preventDefault();\n                onToggleCardSettings();\n              }}\n              clean\n              size='medium'\n            >\n              <PlayCircleIconSolid />\n            </IconButton>\n          </Tooltip>\n        </>\n      }\n      title=''\n      subheader=''\n    />\n  );\n};\n\nexport default NeoCardSettingsHeader;\n"
  },
  {
    "path": "src/card/view/CardView.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { ReportItemContainer } from '../CardStyle';\nimport NeoCardViewHeader from './CardViewHeader';\nimport NeoCardViewFooter from './CardViewFooter';\nimport { CardContent } from '@mui/material';\nimport NeoCodeEditorComponent from '../../component/editor/CodeEditorComponent';\nimport { CARD_FOOTER_HEIGHT, CARD_HEADER_HEIGHT } from '../../config/CardConfig';\nimport { getReportTypes } from '../../extensions/ExtensionUtils';\nimport NeoCodeViewerComponent from '../../component/editor/CodeViewerComponent';\nimport { NeoReportWrapper } from '../../report/ReportWrapper';\nimport { identifyStyleRuleParameters } from '../../extensions/styling/StyleRuleEvaluator';\nimport { IconButton } from '@neo4j-ndl/react';\nimport { PlayCircleIconSolid } from '@neo4j-ndl/react/icons';\nimport { extensionEnabled } from '../../utils/ReportUtils';\nimport { objMerge } from '../../utils/ObjectManipulation';\nimport { REPORT_TYPES } from '../../config/ReportConfig';\n\nconst NeoCardView = ({\n  id,\n  title,\n  database,\n  query,\n  globalParameters,\n  widthPx,\n  heightPx,\n  fields,\n  extensions,\n  active,\n  setActive,\n  onDownloadImage,\n  type,\n  selection,\n  dashboardSettings,\n  settings,\n  updateReportSetting,\n  createNotification,\n  settingsOpen,\n  editable,\n  onGlobalParameterUpdate,\n  onSelectionUpdate,\n  onToggleCardSettings,\n  onTitleUpdate,\n  onFieldsUpdate,\n  expanded,\n  onToggleCardExpand,\n}) => {\n  const reportHeight = heightPx - CARD_FOOTER_HEIGHT - CARD_HEADER_HEIGHT + 20;\n  const cardHeight = heightPx - CARD_FOOTER_HEIGHT + 23;\n  const ref = React.useRef();\n\n  const settingsSelector = Object.keys(\n    Object.fromEntries(Object.entries(REPORT_TYPES[type]?.settings || {}).filter(([_, value]) => value.refresh))\n  ).reduce((obj, key) => {\n    return Object.assign(obj, {\n      [key]: settings[key],\n    });\n  }, {});\n\n  const [lastRunTimestamp, setLastRunTimestamp] = useState(Date.now());\n\n  // TODO : selectorChange should handle every case where query execution needs to be re-executed\n  // e.g. Change of query, type, some advanced settings...\n  const [selectorChange, setSelectorChange] = useState(false);\n\n  const getLocalParameters = (parse_string, drilldown = true): unknown => {\n    if (!parse_string || !globalParameters) {\n      return {};\n    }\n\n    let re = /(?:^|\\W|%20)\\$(\\w+)(?!\\w)/g;\n    let match;\n\n    // If the report styling extension is enabled, extend the list of local (relevant) parameters with those used by the style rules.\n    const styleRules = settings.styleRules ? settings.styleRules : [];\n    const styleParams = extensionEnabled(extensions, 'styling') ? identifyStyleRuleParameters(styleRules) : [];\n\n    // Similarly, if the forms extension is enabled, extract nested parameters used by parameter selectors inside the form.\n    const formFields = settings.formFields ? settings.formFields : [];\n    const formsParams =\n      drilldown && extensionEnabled(extensions, 'forms')\n        ? formFields\n            .map((f) => {\n              return Object.keys(getLocalParameters(f.query, false));\n            })\n            .flat()\n        : [];\n\n    let localQueryVariables: string[] = [...styleParams, ...formsParams];\n    while ((match = re.exec(parse_string))) {\n      localQueryVariables.push(match[1]);\n    }\n\n    let params = Object.fromEntries(\n      Object.entries(globalParameters).filter(([local]) => localQueryVariables.includes(local))\n    );\n\n    return settings.ignoreNonDefinedParams\n      ? objMerge(Object.fromEntries(localQueryVariables.map((name) => [name, null])), params)\n      : params;\n  };\n\n  // @ts-ignore\n  const reportHeader = (\n    <NeoCardViewHeader\n      title={title}\n      editable={editable}\n      description={settings.description}\n      fullscreenEnabled={settings.fullscreenEnabled}\n      downloadImageEnabled={settings.downloadImageEnabled}\n      refreshButtonEnabled={settings.refreshButtonEnabled}\n      onTitleUpdate={onTitleUpdate}\n      onToggleCardSettings={onToggleCardSettings}\n      onManualRefreshCard={() => setLastRunTimestamp(Date.now())}\n      settings={settings}\n      onDownloadImage={onDownloadImage}\n      onToggleCardExpand={onToggleCardExpand}\n      expanded={expanded}\n      parameters={getLocalParameters(title)}\n    ></NeoCardViewHeader>\n  );\n\n  // @ts-ignore\n  const reportFooter = active ? (\n    <NeoCardViewFooter\n      fields={fields}\n      settings={settings}\n      extensions={extensions}\n      selection={selection}\n      type={type}\n      onSelectionUpdate={onSelectionUpdate}\n      showOptionalSelections={settings.showOptionalSelections}\n      dashboardSettings={dashboardSettings}\n    ></NeoCardViewFooter>\n  ) : (\n    <></>\n  );\n\n  const localParameters = { ...getLocalParameters(query), ...getLocalParameters(settings.drilldownLink) };\n  const reportTypes = getReportTypes(extensions);\n  const reportTypeHasNoFooter = reportTypes[type] && reportTypes[type].withoutFooter;\n  const withoutFooter = reportTypeHasNoFooter\n    ? reportTypes[type].withoutFooter\n    : (reportTypes[type] && !reportTypes[type].selection) || (settings && settings.hideSelections);\n\n  const getGlobalParameter = (key: string): unknown => {\n    return globalParameters ? globalParameters[key] : undefined;\n  };\n\n  useEffect(() => {\n    if (!settingsOpen) {\n      setLastRunTimestamp(Date.now());\n    }\n  }, [JSON.stringify(localParameters)]);\n\n  useEffect(() => {\n    if (!settingsOpen && (selectorChange || type === 'select')) {\n      setLastRunTimestamp(Date.now());\n    }\n    setSelectorChange(false);\n  }, [settingsOpen]);\n\n  useEffect(() => {\n    setSelectorChange(true);\n  }, [query, type, database, JSON.stringify(settingsSelector)]);\n\n  // TODO - understand why CardContent is throwing a warning based on this style config.\n  const cardContentStyle = {\n    paddingBottom: '0px',\n    paddingLeft: '0px',\n    paddingRight: '0px',\n    paddingTop: '0px',\n    width: '100%',\n    marginTop: '-9px',\n    height: expanded\n      ? withoutFooter\n        ? '100%'\n        : `calc(100% - ${CARD_FOOTER_HEIGHT}px)`\n      : withoutFooter\n      ? `${reportHeight + CARD_FOOTER_HEIGHT - (reportTypeHasNoFooter ? 0 : 20)}px`\n      : `${reportHeight}px`,\n    overflow: 'auto',\n  };\n  const reportContent = (\n    <CardContent ref={ref} style={cardContentStyle}>\n      {active ? (\n        <NeoReportWrapper\n          id={id}\n          query={query}\n          database={database}\n          parameters={localParameters}\n          lastRunTimestamp={lastRunTimestamp}\n          extensions={extensions}\n          disabled={settingsOpen}\n          selection={selection}\n          fields={fields}\n          settings={settings}\n          expanded={expanded}\n          rowLimit={dashboardSettings.disableRowLimiting ? 1000000 : reportTypes[type] && reportTypes[type].maxRecords}\n          dimensions={{ width: widthPx, height: heightPx }}\n          type={type}\n          ChartType={reportTypes[type] && reportTypes[type].component}\n          setGlobalParameter={onGlobalParameterUpdate}\n          getGlobalParameter={getGlobalParameter}\n          updateReportSetting={updateReportSetting}\n          createNotification={createNotification}\n          queryTimeLimit={dashboardSettings.queryTimeLimit ? dashboardSettings.queryTimeLimit : 20}\n          setFields={onFieldsUpdate}\n        />\n      ) : (\n        <>\n          <IconButton\n            style={{ float: 'right', marginRight: '9px' }}\n            aria-label='run'\n            onClick={() => {\n              setActive(true);\n            }}\n            clean\n          >\n            <PlayCircleIconSolid className='n-w-5 n-h-5' aria-label={'play'} />\n          </IconButton>\n          <NeoCodeEditorComponent\n            value={query}\n            language={'cypher'}\n            editable={false}\n            style={{\n              border: '1px solid lightgray',\n              borderRight: '35px solid #eee',\n              marginTop: '0px',\n              marginLeft: '10px',\n              marginRight: '10px',\n            }}\n            onChange={() => {}}\n            placeholder={'No query specified...'}\n          />\n        </>\n      )}\n    </CardContent>\n  );\n\n  return (\n    <div\n      className={`card-view n-bg-palette-neutral-bg-weak n-text-palette-neutral-text-default ${\n        expanded ? 'expanded' : ''\n      }`}\n      style={settings && settings.backgroundColor ? { backgroundColor: settings.backgroundColor } : {}}\n    >\n      {reportHeader}\n      {/* if there's no selection for this report, we don't have a footer, so the report can be taller. */}\n      <ReportItemContainer\n        style={{ height: expanded ? (withoutFooter ? 'calc(100% - 69px)' : 'calc(100% - 49px)') : cardHeight }}\n      >\n        {reportTypes[type] ? (\n          reportContent\n        ) : (\n          <NeoCodeViewerComponent value={'Invalid report type. Are you missing an extension?'} />\n        )}\n        {reportTypes[type] ? reportFooter : <></>}\n      </ReportItemContainer>\n    </div>\n  );\n};\n\nexport default NeoCardView;\n"
  },
  {
    "path": "src/card/view/CardViewFooter.tsx",
    "content": "import React from 'react';\nimport { CardActions, FormControl, InputLabel, MenuItem, Select } from '@mui/material';\nimport { categoricalColorSchemes } from '../../config/ColorConfig';\nimport { getReportTypes } from '../../extensions/ExtensionUtils';\nimport { SELECTION_TYPES } from '../../config/CardConfig';\nimport { Dropdown, Label } from '@neo4j-ndl/react';\n\nconst NeoCardViewFooter = ({\n  fields,\n  settings,\n  selection,\n  type,\n  extensions,\n  showOptionalSelections,\n  onSelectionUpdate,\n  dashboardSettings,\n}) => {\n  /**\n   * For each selectable field in the visualization, give the user an option to select them from the query output fields.\n   */\n  const reportTypes = getReportTypes(extensions);\n  const selectableFields = reportTypes[type].selection;\n  const selectables = selectableFields ? Object.keys(selectableFields) : [];\n  const nodeColorScheme = settings && settings.nodeColorScheme ? settings.nodeColorScheme : 'neodash';\n  const hideSelections = settings && settings.hideSelections ? settings.hideSelections : false;\n  const { ignoreLabelColors } = reportTypes[type];\n  if (!fields || fields.length == 0 || hideSelections) {\n    return <div></div>;\n  }\n  return (\n    <CardActions\n      style={{\n        position: 'relative',\n        paddingLeft: '15px',\n        overflowX: 'scroll',\n        paddingBottom: '100px',\n      }}\n      disableSpacing\n    >\n      {selectables.map((selectable, index) => {\n        const selectionIsMandatory = !selectableFields[selectable].optional;\n\n        // Creates the component for node property selections.\n        if (selectableFields[selectable].type == SELECTION_TYPES.NODE_PROPERTIES) {\n          // Only show optional selections if we explicitly allow it.\n          if (showOptionalSelections || selectionIsMandatory) {\n            const totalColors = categoricalColorSchemes[nodeColorScheme]\n              ? categoricalColorSchemes[nodeColorScheme].length\n              : 0;\n            const fieldSelections = fields.map((field, i) => {\n              // TODO logically, it should be the last element in the field (node labels) array, as that is typically\n              // the most specific node label when we have multi-labels\n              const nodeLabel = field[0];\n              // TODO this convention that we have for storing node labels and properties in fields should be documented\n              // , and probably even converted to a generic type.\n              const discoveredProperties = field.slice(1);\n              const properties = (discoveredProperties ? [...discoveredProperties].sort() : []).concat([\n                '(label)',\n                '(id)',\n                '(no label)',\n              ]);\n              const color =\n                totalColors > 0 && !ignoreLabelColors\n                  ? categoricalColorSchemes[nodeColorScheme][i % totalColors]\n                  : 'lightgrey';\n              const inputColor =\n                dashboardSettings.theme === 'dark' ? 'var(--palette-dark-neutral-border-strong)' : 'rgba(0, 0, 0, 0.6)';\n              return (\n                <FormControl key={nodeLabel} size={'small'} variant='standard'>\n                  <InputLabel id={nodeLabel} style={{ color: inputColor }}>\n                    {nodeLabel}\n                  </InputLabel>\n                  <Select\n                    labelId={nodeLabel}\n                    id={nodeLabel}\n                    className={'MuiChip-root'}\n                    style={{ backgroundColor: color, paddingLeft: 10, minWidth: 75, marginRight: 5 }}\n                    onChange={(e) => onSelectionUpdate(nodeLabel, e.target.value)}\n                    value={selection && selection[nodeLabel] ? selection[nodeLabel] : ''}\n                  >\n                    {/* Render choices */}\n                    {properties.length &&\n                      properties.map &&\n                      properties.map((field) => {\n                        return (\n                          <MenuItem key={field} value={field}>\n                            {field}\n                          </MenuItem>\n                        );\n                      })}\n                  </Select>\n                </FormControl>\n              );\n            });\n            return fieldSelections;\n          }\n        }\n        // Creates the selection for all other types of components\n        if (\n          selectableFields[selectable].type == SELECTION_TYPES.LIST ||\n          selectableFields[selectable].type == SELECTION_TYPES.NUMBER ||\n          selectableFields[selectable].type == SELECTION_TYPES.NUMBER_OR_DATETIME ||\n          selectableFields[selectable].type == SELECTION_TYPES.TEXT\n        ) {\n          if (selectionIsMandatory || showOptionalSelections) {\n            const sortedFields = fields ? [...fields].sort() : [];\n\n            const fieldsToRender = selectionIsMandatory ? sortedFields : sortedFields.concat(['(none)']);\n            return (\n              <FormControl key={index} size={'small'}>\n                <Dropdown\n                  id={selectable}\n                  label={selectableFields[selectable].label}\n                  type='select'\n                  selectProps={{\n                    onChange: (newValue) =>\n                      (newValue && selectableFields[selectable].multiple\n                        ? onSelectionUpdate(\n                            selectable,\n                            newValue.map((v) => v.value)\n                          )\n                        : onSelectionUpdate(selectable, newValue.value)),\n                    options: fieldsToRender.map((option) => ({ label: option, value: option })),\n                    value: selectableFields[selectable].multiple\n                      ? selection[selectable].map((sel) => ({ label: sel, value: sel }))\n                      : { label: selection[selectable], value: selection[selectable] },\n                    isMulti: selectableFields[selectable].multiple,\n                    isClearable: false,\n                    menuPortalTarget: document.querySelector('#overlay'),\n                  }}\n                  fluid\n                  style={{\n                    minWidth: selectableFields[selectable].multiple ? 170 : 120,\n                    marginRight: 20,\n                    display: 'inline-block',\n                  }}\n                  placeholder={selectableFields[selectable].multiple ? 'Select (multiple)' : 'Select'}\n                ></Dropdown>\n              </FormControl>\n            );\n          }\n        }\n      })}\n    </CardActions>\n  );\n};\n\nexport default NeoCardViewFooter;\n"
  },
  {
    "path": "src/card/view/CardViewHeader.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { Badge, CardHeader, Dialog, DialogContent, DialogTitle, TextField, Tooltip } from '@mui/material';\nimport debounce from 'lodash/debounce';\nimport { useCallback } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport gfm from 'remark-gfm';\nimport { replaceDashboardParameters } from '../../chart/ChartUtils';\n\nimport { IconButton } from '@neo4j-ndl/react';\nimport {\n  DragIcon,\n  EllipsisVerticalIconOutline,\n  ArrowPathIconOutline,\n  ExpandIcon,\n  ShrinkIcon,\n  CameraIconSolid,\n  InformationCircleIconOutline,\n  XMarkIconOutline,\n} from '@neo4j-ndl/react/icons';\nimport { createTheme, ThemeProvider } from '@mui/material/styles';\n\nconst NeoCardViewHeader = ({\n  title,\n  description,\n  editable,\n  onTitleUpdate,\n  fullscreenEnabled,\n  downloadImageEnabled,\n  refreshButtonEnabled,\n  onToggleCardSettings,\n  onManualRefreshCard,\n  onDownloadImage,\n  onToggleCardExpand,\n  expanded,\n  parameters,\n}) => {\n  const [text, setText] = React.useState(title);\n  const [parsedText, setParsedText] = React.useState(title);\n  const [editing, setEditing] = React.useState(false);\n  const [descriptionModalOpen, setDescriptionModalOpen] = React.useState(false);\n\n  function replaceParamsOnString(s, p) {\n    let parsed: string;\n    parsed = replaceDashboardParameters(s, p);\n    return parsed;\n  }\n\n  // Ensure that we only trigger a text update event after the user has stopped typing.\n  const debouncedTitleUpdate = useCallback(debounce(onTitleUpdate, 250), []);\n\n  useEffect(() => {\n    let titleParsed = replaceParamsOnString(`${title}`, parameters);\n    if (!editing) {\n      setParsedText(titleParsed);\n    }\n  }, [editing, parameters]);\n\n  useEffect(() => {\n    // Reset text to the dashboard state when the page gets reorganized.\n    if (text !== title) {\n      setText(title);\n    }\n  }, [title]);\n\n  const theme = createTheme({\n    typography: {\n      fontFamily: \"'Nunito Sans', sans-serif !important\",\n      allVariants: { color: 'rgb(var(--palette-neutral-text-weak))' },\n    },\n    palette: {\n      text: {\n        primary: 'rgb(var(--palette-neutral-text))',\n      },\n      action: {\n        disabled: 'rgb(var(--palette-neutral-text-weak))',\n      },\n    },\n  });\n\n  const cardTitle = (\n    <ThemeProvider theme={theme}>\n      <table style={{ width: '100%' }}>\n        <tbody>\n          <tr>\n            {editable ? (\n              <td>\n                <IconButton\n                  className='n-mb-3 n-relative -n-left-3 drag-handle'\n                  clean\n                  size='medium'\n                  aria-label={'drag'}\n                  onClick={() => {}}\n                >\n                  <DragIcon />\n                </IconButton>\n              </td>\n            ) : (\n              <></>\n            )}\n            <td style={{ width: '100%' }}>\n              <TextField\n                id='standard-outlined'\n                onFocus={() => {\n                  setEditing(true);\n                }}\n                onBlur={() => {\n                  setEditing(false);\n                }}\n                className={'no-underline large'}\n                label=''\n                disabled={!editable}\n                placeholder='Report name...'\n                fullWidth\n                maxRows={4}\n                value={editing ? text : parsedText !== ' ' ? parsedText : ''}\n                onChange={(event) => {\n                  setText(event.target.value);\n                  debouncedTitleUpdate(event.target.value);\n                }}\n                size={'small'}\n                style={{ paddingTop: '0px important!' }}\n                variant={'standard'}\n                sx={{\n                  '& .MuiInputBase-input.Mui-disabled': {\n                    WebkitTextFillColor: 'inherit',\n                  },\n                }}\n              />\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    </ThemeProvider>\n  );\n\n  const descriptionEnabled = description && description.length > 0;\n\n  // TODO: all components like buttons should probably be seperate files\n  const settingsButton = (\n    <Tooltip title='Settings' aria-label='settings' disableInteractive>\n      <IconButton aria-label='settings' onClick={onToggleCardSettings} clean size='medium'>\n        <EllipsisVerticalIconOutline />\n      </IconButton>\n    </Tooltip>\n  );\n\n  const refreshButton = (\n    <Tooltip title='Refresh' aria-label='refresh' disableInteractive>\n      <IconButton aria-label='refresh' onClick={onManualRefreshCard} clean size='medium'>\n        <ArrowPathIconOutline />\n      </IconButton>\n    </Tooltip>\n  );\n\n  const maximizeButton = (\n    <Tooltip title='Maximize' aria-label='maximize' disableInteractive>\n      <IconButton aria-label='maximize' onClick={onToggleCardExpand} clean size='medium'>\n        <ExpandIcon />\n      </IconButton>\n    </Tooltip>\n  );\n\n  const unMaximizeButton = (\n    <IconButton aria-label='un-maximize' onClick={onToggleCardExpand} clean size='medium'>\n      <ShrinkIcon />\n    </IconButton>\n  );\n\n  const downloadImageButton = (\n    <Tooltip title='Download as Image' aria-label='download' disableInteractive>\n      <IconButton onClick={onDownloadImage} aria-label='download csv' clean size='medium'>\n        <CameraIconSolid />\n      </IconButton>\n    </Tooltip>\n  );\n\n  const descriptionButton = (\n    <Tooltip title='Details' aria-label='details' disableInteractive>\n      <IconButton onClick={() => setDescriptionModalOpen(true)} aria-label='details' clean size='medium'>\n        <InformationCircleIconOutline />\n      </IconButton>\n    </Tooltip>\n  );\n\n  return (\n    <>\n      <Dialog\n        maxWidth={'lg'}\n        open={descriptionModalOpen}\n        onClose={() => setDescriptionModalOpen(false)}\n        aria-labelledby='form-dialog-title'\n      >\n        <DialogTitle id='form-dialog-title'>\n          {title}\n          <IconButton\n            onClick={() => setDescriptionModalOpen(false)}\n            style={{ padding: '3px', float: 'right' }}\n            aria-label={'rect badge'}\n            clean\n          >\n            <XMarkIconOutline />\n          </IconButton>\n        </DialogTitle>\n        <DialogContent style={{ minWidth: '400px' }}>\n          <div>\n            <base target='_blank' /> <ReactMarkdown plugins={[gfm]} children={description} />\n          </div>\n        </DialogContent>\n      </Dialog>\n      <CardHeader\n        style={{ height: '72px' }}\n        action={\n          <>\n            {downloadImageEnabled ? downloadImageButton : <></>}\n            {fullscreenEnabled ? expanded ? unMaximizeButton : maximizeButton : <></>}\n            {descriptionEnabled ? descriptionButton : <></>}\n            {refreshButtonEnabled ? refreshButton : <></>}\n            {editable ? settingsButton : <></>}\n          </>\n        }\n        title={cardTitle}\n      />\n    </>\n  );\n};\n\nexport default NeoCardViewHeader;\n"
  },
  {
    "path": "src/chart/Chart.ts",
    "content": "import { Record as Neo4jRecord } from 'neo4j-driver';\n\n/**\n * Interface for all charts that NeoDash can render.\n * When you extend NeoDash, make sure that your component implements this interface.\n */\nexport interface ChartProps {\n  records: Neo4jRecord[]; // Query output, Neo4j records as returned from the driver.\n  extensions?: Record<string, any>; // A dictionary of enabled extensions.\n  selection?: Record<string, any>; // A dictionary with the selection made in the report footer.\n  settings?: Record<string, any>; // A dictionary with the 'advanced settings' specified through the NeoDash interface.\n  dimensions?: Record<string, number>; // a dictionary with the dimensions of the report (likely not needed, charts automatically fill up space).\n  fullscreen?: boolean; // flag indicating whether the report is rendered in a fullscreen view.\n  parameters?: Record<string, any>; // A dictionary with the global dashboard parameters.\n  query?: string; // The original query that was used to populate the `records`.\n  queryCallback?: (query: string | undefined, parameters: Record<string, any>, setRecords: any) => void; // Callback to query the database with a given set of parameters. Calls 'setReccords' upon completion.\n  createNotification?: (title: string, message: string) => void; // Callback to create a notification that overlays the entire application.\n  setGlobalParameter?: (name: string, value: any) => void; // Allows a chart to update a global dashboard parameter to be used in Cypher queries for other reports.\n  getGlobalParameter?: (name) => string; // Allows a chart to get a global dashboard parameter.\n  updateReportSetting?: (name, value) => void; // Callback to update a setting for this report.\n  fields: (fields) => string[]; // List of fields (return values) available for the report.\n  setFields?: (fields) => void; // Update the list of fields for this report.\n  theme?: string; // Dashboard theme value.\n}\n\n/**\n * A simplified schema of the Neo4j database.\n */\nexport interface Neo4jSchema {\n  nodeLabels: string[]; // list of node labels.\n  relationshipTypes: string[]; // list of relationship types.\n  setPageNumber?: (index: number) => void; // Callback to update the currently selected page of the dashboard.\n}\n"
  },
  {
    "path": "src/chart/ChartUtils.ts",
    "content": "import domtoimage from 'dom-to-image';\nimport { Date as Neo4jDate } from 'neo4j-driver-core/lib/temporal-types.js';\n\n/**\n * Converts a neo4j record entry to a readable string representation.\n */\nexport const convertRecordObjectToString = (entry) => {\n  if (entry == null || entry == undefined) {\n    return entry;\n  }\n  const className = entry.__proto__.constructor.name;\n  if (className == 'String') {\n    return entry;\n  } else if (valueIsNode(entry)) {\n    return convertNodeToString(entry);\n  } else if (valueIsRelationship(entry)) {\n    return convertRelationshipToString(entry);\n  } else if (valueIsPath(entry)) {\n    return convertPathToString(entry);\n  }\n  return entry.toString();\n};\n\n/**\n * Converts a neo4j node record entry to a readable string representation.\n * if it's a fieldType ==\"Node\"\n * Then, return\n * 1. 'name' property, if it exists,\n * 2. the 'title' property, if it exists,\n * 3. the 'id' property, if it exists...\n * 4. the 'uid' property, if it exists..\n * 5. the ({labels}}, if they exist,\n * 6. Node(id).\n */\nconst convertNodeToString = (nodeEntry) => {\n  if (nodeEntry.properties.name) {\n    return `${nodeEntry.labels}(${nodeEntry.properties.name})`;\n  }\n  if (nodeEntry.properties.title) {\n    return `${nodeEntry.labels}(${nodeEntry.properties.title})`;\n  }\n  if (nodeEntry.properties.id) {\n    return `${nodeEntry.labels}(${nodeEntry.properties.id})`;\n  }\n  if (nodeEntry.properties.uid) {\n    return `${nodeEntry.labels}(${nodeEntry.properties.uid})`;\n  }\n  return `${nodeEntry.labels}(` + `_id=${nodeEntry.identity})`;\n};\n\n// if it's a fieldType == \"Relationship\"\nconst convertRelationshipToString = (relEntry) => {\n  return relEntry.toString();\n};\n\n// if it's a fieldType == \"Path\"\nconst convertPathToString = (pathEntry) => {\n  return pathEntry.toString();\n};\n// Anything else, return the string representation of the object.\n\n/* HELPER FUNCTIONS FOR DETERMINING TYPE OF FIELD RETURNED FROM NEO4J */\nexport function valueIsArray(value) {\n  const className = value !== undefined && value.__proto__.constructor.name;\n  return className == 'Array';\n}\n\nexport function valueIsNode(value) {\n  // const className = value.__proto__.constructor.name;\n  // return className == \"Node\";\n  return value && value.labels && value.identity && value.properties;\n}\n\nexport function valueIsRelationship(value) {\n  // const className = value.__proto__.constructor.name;\n  // return className == \"Relationship\";\n  return value && value.type && value.start && value.end && value.identity && value.properties;\n}\n\nexport function valueIsPath(value) {\n  // const className = value.__proto__.constructor.name;\n  // return className == \"Path\"\n  return value && value.start && value.end && value.segments && value.length;\n}\n\nexport function valueisPoint(value) {\n  // Look at the properties and identify the type.\n  return value && value.x && value.y && value.srid;\n}\n\nexport function valueIsObject(value) {\n  // TODO - this will not work in production builds. Need alternative.\n  const className = value.__proto__.constructor.name;\n  return className == 'Object';\n}\n\nexport function toNumber(ref) {\n  if (ref === undefined || typeof ref === 'number') {\n    return ref;\n  }\n  let { low, high } = ref;\n  let res = high;\n\n  for (let i = 0; i < 32; i++) {\n    res *= 2;\n  }\n\n  return low + res;\n}\n\nexport function getRecordType(value) {\n  // mui data-grid native column types are: 'string' (default),\n  // 'number', 'date', 'dateTime', 'boolean' and 'singleSelect'\n  // https://v4.mui.com/components/data-grid/columns/#column-types\n  // Type singleSelect is not implemented here\n  if (value === true || value === false) {\n    return 'boolean';\n  } else if (value === undefined) {\n    return 'undefined';\n  } else if (value === null) {\n    return 'null';\n  } else if (value.__isInteger__) {\n    return 'integer';\n  } else if (typeof value == 'number') {\n    return 'number';\n  } else if (value.__isDate__) {\n    return 'date';\n  } else if (value.__isDateTime__) {\n    return 'dateTime';\n  } else if (valueIsNode(value)) {\n    return 'node';\n  } else if (valueIsRelationship(value)) {\n    return 'relationship';\n  } else if (valueIsPath(value)) {\n    return 'path';\n  } else if (valueIsArray(value)) {\n    return 'array';\n  } else if (valueIsObject(value)) {\n    if (!isNaN(toNumber(value))) {\n      return 'objectNumber';\n    }\n    return 'object';\n  } else if (typeof value === 'string' || value instanceof String) {\n    if (value.startsWith('http') || value.startsWith('https')) {\n      return 'link';\n    }\n    return 'string';\n  }\n\n  // Use string as default type\n  return 'string';\n}\n\n/**\n * Basic function to convert a table row output to a CSV file, and download it.\n * TODO: Make this more robust. Probably the commas should be escaped to ensure the CSV is always valid.\n */\nexport const downloadCSV = (rows) => {\n  const element = document.createElement('a');\n  let csv = '';\n  const headers = Object.keys(rows[0]).slice(1);\n  csv += `${headers.join(', ')}\\n`;\n  rows.forEach((row) => {\n    headers.forEach((header) => {\n      // Parse value\n      let value = row[header];\n      if (value?.low !== undefined) {\n        value = value.low;\n      }\n      csv += `${JSON.stringify(value)}`;\n      csv += headers.indexOf(header) < headers.length - 1 ? ',' : '';\n    });\n    csv += '\\n';\n  });\n  const file = new Blob([`\\ufeff${csv}`], { type: 'text/plain;charset=utf8' });\n  element.href = URL.createObjectURL(file);\n  element.download = 'table.csv';\n  document.body.appendChild(element); // Required for this to work in FireFox\n  element.click();\n};\n\n/**\n * Replaces all global dashboard parameters inside a string with their values.\n * @param str The string to replace the parameters in.\n * @param parameters The parameters to replace.\n */\nexport function replaceDashboardParameters(str, parameters) {\n  if (!str) {\n    return '';\n  }\n  let rx = /`.([^`]*)`/g;\n  let regexSquareBrackets = /\\[(.*?)\\]/g;\n  let rxSimple = /\\$neodash_\\w*/g;\n\n  /**\n   * Define function to access elements in an array/object type dashboard parameter.\n   * @param _ needed for str.replace(), unused.\n   * @param p1 - the original string.\n   * @returns an updated markdown with injected parameters.\n   */\n  const parameterElementReplacer = (_, p1) => {\n    // Find (in the markdown) occurences of the parameter `$neodash_movie_title[index]` or  `$neodash_movie_title[key]`.\n    let matches = p1.match(regexSquareBrackets);\n    let param = p1.split('[')[0].replace(`$`, '').trim();\n    let val = parameters?.[param] || null;\n\n    // Inject the element at that index/key into the markdown as text.\n    matches?.forEach((m) => {\n      let i = m.replace(/[[\\]']+/g, '');\n      i = isNaN(i) ? i.replace(/['\"']+/g, '') : Number(i);\n      val = val ? val[i] : null;\n    });\n\n    return RenderSubValue(val);\n  };\n\n  const parameterSimpleReplacer = (_) => {\n    let param = _.replace(`$`, '').trim();\n    let val = parameters?.[param] || null;\n    let type = getRecordType(val);\n\n    // Arrays weren't playing nicely with RenderSubValue(). Each object would be passed separately and return [oject Object].\n    if (type === 'string' || type == 'link') {\n      return val;\n    } else if (type === 'array') {\n      return RenderSubValue(val.join(', '));\n    }\n    return RenderSubValue(val);\n  };\n\n  let newString = str.replace(rx, parameterElementReplacer).replace(rxSimple, parameterSimpleReplacer);\n\n  return newString;\n}\n\nexport function replaceDashboardParametersInString(str, parameters) {\n  Object.keys(parameters).forEach((key) => {\n    str = str.replaceAll(`$${key}`, parameters[key]);\n  });\n  return str;\n}\n\n/**\n * Downloads a screenshot of the element reference passed to it.\n * @param ref The reference to the element to download as an image.\n */\nexport const downloadComponentAsImage = (ref) => {\n  const element = ref.current;\n\n  domtoimage.toPng(element, { bgcolor: 'white' }).then((dataUrl) => {\n    const link = document.createElement('a');\n    link.download = 'image.png';\n    link.href = dataUrl;\n    link.click();\n  });\n};\n\nimport { QueryResult, Record as Neo4jRecord } from 'neo4j-driver';\nimport { RenderSubValue } from '../report/ReportRecordProcessing';\n\n/**\n * Function to cast a value received from the Neo4j Driver to its TS native type\n * @param input Value to cast\n * @returns Value casted to it's native type\n */\nexport function recordToNative(input: any): any {\n  if (!input && input !== false) {\n    return null;\n  } else if (typeof input.keys === 'object' && typeof input.get === 'function') {\n    return Object.fromEntries(input.keys.map((key) => [key, recordToNative(input.get(key))]));\n  } else if (typeof input.toNumber === 'function') {\n    return input.toNumber();\n  } else if (Array.isArray(input)) {\n    return (input as Array<any>).map((item) => recordToNative(item));\n  } else if (typeof input === 'object') {\n    const converted = Object.entries(input).map(([key, value]) => [key, recordToNative(value)]);\n\n    return Object.fromEntries(converted);\n  }\n\n  return input;\n}\n\nexport function resultToNative(result: QueryResult): Record<string, any> {\n  if (!result) {\n    return {};\n  }\n\n  return result.records.map((row) => recordToNative(row));\n}\nexport function checkResultKeys(first: Neo4jRecord, keys: string[]) {\n  const missing = keys.filter((key) => !first.keys.includes(key));\n\n  if (missing.length > 0) {\n    return new Error(\n      `The query is missing the following key${missing.length > 1 ? 's' : ''}: ${missing.join(\n        ', '\n      )}.  The expected keys are: ${keys.join(', ')}`\n    );\n  }\n\n  return false;\n}\n\n/**\n * For hierarchical data structures, recursively search for a key property that must have a given value.\n * If none can be found, return null.\n */\nexport const search = (tree, value, key = 'id', reverse = false) => {\n  if (tree.length == 0) {\n    return null;\n  }\n  const stack = Array.isArray(tree) ? [...tree] : [tree];\n  while (stack.length) {\n    const node = stack[reverse ? 'pop' : 'shift']();\n    if (node[key] && node[key] === value) {\n      return node;\n    }\n    if (node.children) {\n      stack.push(...node.children);\n    }\n  }\n  return null;\n};\n\n/**\n * For hierarchical data, we remove all intermediate node prefixes generate by `processHierarchyFromRecords`.\n * This ensures that the visualization itself shows the 'real' names, and not the intermediate ones.\n */\nexport const mutateName = (currentNode) => {\n  if (currentNode.name) {\n    const s = currentNode.name.split('_');\n    currentNode.name = s.length > 0 ? s.slice(1).join('_') : s[0];\n  }\n\n  if (currentNode.children) {\n    currentNode.children.forEach((n) => mutateName(n));\n  }\n};\n\nexport const findObject = (data, name) => data.find((searchedName) => searchedName.name === name);\n\nexport const flatten = (data) =>\n  data.reduce((acc, item) => {\n    if (item.children) {\n      return [...acc, item, ...flatten(item.children)];\n    }\n    return [...acc, item];\n  }, []);\n\n/**\n * Converts a list of Neo4j records into a hierarchy structure for hierarchical data visualizations.\n */\n// TODO: this needs docs\nexport const processHierarchyFromRecords = (records: Record<string, any>[], selection: any) => {\n  return records.reduce((data: Record<string, any>, row: Record<string, any>) => {\n    try {\n      const index = recordToNative(row.get(selection.index));\n      // const idx = data.findIndex(item => item.index === index)\n      // const key = selection['key'] !== \"(none)\" ? recordToNative(row.get(selection['key'])) : selection['value'];\n      const value = recordToNative(row.get(selection.value));\n      if (!Array.isArray(index) || isNaN(value)) {\n        throw 'Invalid data format selected for hierarchy report.';\n      }\n      let holder = data;\n      for (let [idx, val] of index.entries()) {\n        // Add a level prefix to each item to avoid duplicates\n        val = `lvl${idx}_${val}`;\n        const obj = search(holder, val, 'name');\n        const entry = { name: val };\n        if (obj) {\n          holder = obj;\n        } else {\n          if (Array.isArray(holder)) {\n            holder.push(entry);\n            // eslint-disable-next-line no-prototype-builtins\n          } else if (holder.hasOwnProperty('children')) {\n            holder.children.push(entry);\n          } else {\n            holder.children = [entry];\n          }\n\n          holder = search(holder, val, 'name');\n        }\n      }\n      holder.loc = value;\n      return data;\n    } catch (e) {\n      // eslint-disable-next-line no-console\n      console.error(e);\n      return [];\n    }\n  }, []);\n};\n\n/**\n * Wrapper for empty check logic, to prevent calling writing the same code too many times\n * @param obj\n * @returns  Returns True if the input is null, undefined or an empty object\n */\nexport const isEmptyObject = (obj: object) => {\n  if (obj == undefined) {\n    return true;\n  }\n  return Object.keys(obj).length == 0;\n};\n\n/**\n * Checks that the value in input can be casted to Neo4j Bolt Driver Date\n * @param value\n * @returns True if it's an object castable to date\n */\nexport function isCastableToNeo4jDate(value: object) {\n  if (value == null || value == undefined) {\n    return false;\n  }\n  let keys = Object.keys(value);\n  return keys.includes('day') && keys.includes('month') && keys.includes('year');\n}\n\n/**\n * Casts value in input to Neo4j Date bolt driver. If can't cast, it will throw an error\n * @param value\n * @returns Casted value to Neo4j Bolt Driver Date\n */\nexport function castToNeo4jDate(value: object) {\n  if (isCastableToNeo4jDate(value)) {\n    return new Neo4jDate(toNumber(value.year), toNumber(value.month), toNumber(value.day));\n  }\n  throw new Error(`Invalid input for castToNeo4jDate: ${value}`);\n}\n\n/**\n * Creates a default selection config for a node-property based chart footer.\n */\nexport function getSelectionBasedOnFields(fields, oldSelection = {}, autoAssignSelectedProperties = true) {\n  const selection = {};\n  fields.forEach((nodeLabelAndProperties) => {\n    const label = nodeLabelAndProperties[0];\n    const properties = nodeLabelAndProperties.slice(1);\n    let selectedProp = oldSelection[label] ? oldSelection[label] : undefined;\n    if (autoAssignSelectedProperties) {\n      DEFAULT_NODE_LABELS.forEach((prop) => {\n        if (properties.indexOf(prop) !== -1) {\n          if (selectedProp == undefined) {\n            selectedProp = prop;\n          }\n        }\n      });\n      selection[label] = selectedProp ? selectedProp : '(label)';\n    } else {\n      selection[label] = selectedProp ? selectedProp : '(no label)';\n    }\n  });\n  return selection;\n}\n\nexport const DEFAULT_NODE_LABELS = ['name', 'title', 'label', 'id', 'uid', '(label)'];\n"
  },
  {
    "path": "src/chart/SettingsUtils.ts",
    "content": "import { getReportTypes } from '../extensions/ExtensionUtils';\nimport { useStyleRules } from '../extensions/styling/StyleRuleEvaluator';\nimport { extensionEnabled } from '../utils/ReportUtils';\n\n/**\n * Gets the user specified settings and merges it with the defaults from ReportConfig.tsx.\n * @param userSettings the user specified settings for the report.\n * @param extensions the extensions enabled for the dashboard.\n * @param getGlobalParameter a callback to get global parameters for the dashboard.\n * @returns  a merged list of user settings and defaults as provided in the configuration.\n */\nexport const getSettings = (\n  userSettings: Record<string, any> | undefined,\n  extensions: Record<string, any> | undefined,\n  getGlobalParameter: any\n) => {\n  const settings: Record<string, any> = {};\n  const config: Record<string, any> = getReportTypes(extensions).graph.settings;\n\n  if (userSettings == undefined) {\n    return {};\n  }\n\n  Object.keys(config).map((key) => {\n    settings[key] = userSettings[key] !== undefined ? userSettings[key] : config[key].default;\n  });\n\n  settings.styleRules = useStyleRules(\n    extensionEnabled(extensions, 'styling'),\n    userSettings && userSettings.styleRules,\n    getGlobalParameter\n  );\n  settings.actionsRules =\n    extensionEnabled(extensions, 'actions') && settings && userSettings.actionsRules ? userSettings.actionsRules : [];\n  return settings;\n};\n"
  },
  {
    "path": "src/chart/Utils.ts",
    "content": "import { tokens } from '@neo4j-ndl/base';\nimport { QueryResult, Record as Neo4jRecord } from 'neo4j-driver';\nexport function recordToNative(input: any): any {\n  if (!input && input !== false) {\n    return null;\n  } else if (typeof input.keys === 'object' && typeof input.get === 'function') {\n    return Object.fromEntries(input.keys.map((key) => [key, recordToNative(input.get(key))]));\n  } else if (typeof input.toNumber === 'function') {\n    return input.toNumber();\n  } else if (Array.isArray(input)) {\n    return (input as Array<any>).map((item) => recordToNative(item));\n  } else if (typeof input === 'object') {\n    const converted = Object.entries(input).map(([key, value]) => [key, recordToNative(value)]);\n\n    return Object.fromEntries(converted);\n  }\n\n  return input;\n}\n\nexport function resultToNative(result: QueryResult): Record<string, any> {\n  if (!result) {\n    return {};\n  }\n\n  return result.records.map((row) => recordToNative(row));\n}\nexport function checkResultKeys(first: Neo4jRecord, keys: string[]) {\n  const missing = keys.filter((key) => !first.keys.includes(key));\n\n  if (missing.length > 0) {\n    return new Error(\n      `The query is missing the following key${missing.length > 1 ? 's' : ''}: ${missing.join(\n        ', '\n      )}.  The expected keys are: ${keys.join(', ')}`\n    );\n  }\n\n  return false;\n}\n\n/**\n * For hierarchical data structures, recursively search for a key property that must have a given value.\n * If none can be found, return null.\n */\nexport const search = (tree, value, key = 'id', reverse = false) => {\n  if (tree.length == 0) {\n    return null;\n  }\n  const stack = Array.isArray(tree) ? [...tree] : [tree];\n  while (stack.length) {\n    const node = stack[reverse ? 'pop' : 'shift']();\n    if (node[key] && node[key] === value) {\n      return node;\n    }\n    if (node.children) {\n      stack.push(...node.children);\n    }\n  }\n  return null;\n};\n\n/**\n * For hierarchical data, we remove all intermediate node prefixes generate by `processHierarchyFromRecords`.\n * This ensures that the visualization itself shows the 'real' names, and not the intermediate ones.\n */\nexport const mutateName = (currentNode) => {\n  if (currentNode.name) {\n    const s = currentNode.name.split('_');\n    currentNode.name = s.length > 0 ? s.slice(1).join('_') : s[0];\n  }\n\n  if (currentNode.children) {\n    currentNode.children.forEach((n) => mutateName(n));\n  }\n};\n\nexport const findObject = (data, name) => data.find((searchedName) => searchedName.name === name);\n\nexport const flatten = (data) =>\n  data.reduce((acc, item) => {\n    if (item.children) {\n      return [...acc, item, ...flatten(item.children)];\n    }\n    return [...acc, item];\n  }, []);\n\nexport const rgbaToHex = (color: string): string => {\n  let rgba;\n  if (/^rgb/.test(color)) {\n    rgba = color.replace(/^rgba?\\(|\\s+|\\)$/g, '').split(',');\n  } else {\n    rgba = color.split(',');\n  }\n\n  if (rgba) {\n    // rgb to hex\n    // eslint-disable-next-line no-bitwise\n    let hex = `#${((1 << 24) + (parseInt(rgba[0], 10) << 16) + (parseInt(rgba[1], 10) << 8) + parseInt(rgba[2], 10))\n      .toString(16)\n      .slice(1)}`;\n\n    // added alpha param if exists\n    if (rgba[4]) {\n      const alpha = Math.round(0o1 * 255);\n      const hexAlpha = (alpha + 0x10000).toString(16).substr(-2).toUpperCase();\n      hex += hexAlpha;\n    }\n\n    return hex;\n  }\n  return color;\n};\n\nexport enum EntityType {\n  Node,\n  Relationship,\n}\n\nexport const themeNivo = {\n  textColor: 'rgb(var(--palette-neutral-text-default))',\n  text: { fill: 'rgb(var(--palette-neutral-text-default))' },\n  axis: {\n    ticks: { text: { fill: 'rgb(var(--palette-neutral-text-default))' } },\n    legend: { text: { fill: 'rgb(var(--palette-neutral-text-default))' } },\n  },\n  legends: {\n    text: { fill: 'rgb(var(--palette-neutral-text-default))' },\n    title: { text: { fill: 'rgb(var(--palette-neutral-text-default))' } },\n    ticks: { text: { fill: 'rgb(var(--palette-neutral-text-default))' } },\n    hidden: { text: { fill: 'rgb(var(--palette-neutral-text-default))' } },\n  },\n  markers: {\n    text: { fill: 'rgb(var(--palette-neutral-text-default))' },\n  },\n  labels: {\n    text: { fill: 'rgb(var(--palette-neutral-text-default))' },\n  },\n  annotations: {\n    text: { fill: 'rgb(var(--palette-neutral-text-default))' },\n  },\n  tooltip: {\n    container: {\n      fill: 'rgb(var(--palette-neutral-text-default))',\n      background: 'rgb(var(--palette-neutral-bg-strong))',\n    },\n  },\n};\n\nexport const themeNivoCanvas = (theme) => {\n  let baseDefault =\n    theme === 'light' ? tokens.palette.light.neutral.text.default : tokens.palette.dark.neutral.text.default;\n  let baseWeak = theme === 'light' ? tokens.palette.light.neutral.text.weak : tokens.palette.dark.neutral.text.weak;\n  return {\n    // textColor: 'rgb(var(--palette-neutral-text-default))',\n    text: { fill: baseDefault },\n    axis: {\n      ticks: { text: { fill: baseDefault } },\n      legend: { text: { fill: baseDefault } },\n    },\n    legends: {\n      text: { fill: baseWeak },\n      title: { text: { fill: baseWeak } },\n      ticks: { text: { fill: baseWeak } },\n      hidden: { text: { fill: baseWeak } },\n    },\n    markers: {\n      text: { fill: baseDefault },\n    },\n    labels: {\n      text: { fill: baseDefault },\n    },\n    annotations: {\n      text: { fill: baseDefault },\n    },\n    tooltip: {\n      container: {\n        fill: 'rgb(var(--palette-neutral-text-default))',\n        background: 'rgb(var(--palette-neutral-bg-strong))',\n      },\n    },\n  };\n};\n"
  },
  {
    "path": "src/chart/bar/BarChart.tsx",
    "content": "import { ResponsiveBar, ResponsiveBarCanvas } from '@nivo/bar';\nimport React, { useEffect } from 'react';\nimport { NoDrawableDataErrorMessage } from '../../component/editor/CodeViewerComponent';\nimport { getD3ColorsByScheme } from '../../config/ColorConfig';\nimport { evaluateRulesOnDict, useStyleRules } from '../../extensions/styling/StyleRuleEvaluator';\nimport { ChartProps } from '../Chart';\nimport { convertRecordObjectToString, recordToNative } from '../ChartUtils';\nimport { themeNivo, themeNivoCanvas } from '../Utils';\nimport { extensionEnabled } from '../../utils/ReportUtils';\nimport { getPageNumbersAndNamesList, getRule, performActionOnElement } from '../../extensions/advancedcharts/Utils';\nimport { getOriginalRecordForNivoClickEvent } from './util';\n\nconst NeoBarChart = (props: ChartProps) => {\n  const { records, selection } = props;\n\n  const [keys, setKeys] = React.useState<string[]>([]);\n  const [data, setData] = React.useState<Record<string, any>[]>([]);\n  const settings = props.settings ? props.settings : {};\n  const marginRight = settings.marginRight ? settings.marginRight : 24;\n  const marginLeft = settings.marginLeft ? settings.marginLeft : 50;\n  const customDimensions = settings.customDimensions ? settings.customDimensions : false;\n  const legendWidth = settings.legendWidth ? settings.legendWidth : 128;\n  const marginTop = settings.marginTop ? settings.marginTop : 24;\n  const marginBottom = settings.marginBottom ? settings.marginBottom : 30;\n  const legend = settings.legend ? settings.legend : false;\n  const labelRotation = settings.labelRotation != undefined ? settings.labelRotation : 45;\n  const barWidth = settings.barWidth ? settings.barWidth : 10;\n  const padding = settings.padding ? settings.padding : 0.25;\n  const innerPadding = settings.innerPadding ? settings.innerPadding : 0;\n  const expandHeightForLegend = settings.expandHeightForLegend ? settings.expandHeightForLegend : false;\n  const displayYAxis = settings.displayYAxis ?? true;\n  const displayYGridLines = settings.displayYGridLines ?? true;\n\n  const actionsRules =\n    extensionEnabled(props.extensions, 'actions') && props.settings && props.settings.actionsRules\n      ? props.settings.actionsRules\n      : [];\n  const pageNames = getPageNumbersAndNamesList();\n\n  const legendPosition = settings.legendPosition ? settings.legendPosition : 'Vertical';\n\n  const labelSkipWidth = settings.labelSkipWidth ? settings.labelSkipWidth : 0;\n  const labelSkipHeight = settings.labelSkipHeight ? settings.labelSkipHeight : 0;\n  const enableLabel = settings.barValues ? settings.barValues : false;\n  const positionLabel = settings.positionLabel ? settings.positionLabel : 'off';\n\n  // TODO: we should make all these defaults be loaded from the config file.\n  const layout = settings.layout ? settings.layout : 'vertical';\n  const colorScheme = settings.colors ? settings.colors : 'set2';\n  const groupMode = settings.groupMode ? settings.groupMode : 'stacked';\n  const valueScale = settings.valueScale ? settings.valueScale : 'linear';\n  const minValue = settings.minValue ? settings.minValue : 'auto';\n  const maxValue = settings.maxValue ? settings.maxValue : 'auto';\n  const styleRules = useStyleRules(\n    extensionEnabled(props.extensions, 'styling'),\n    settings.styleRules,\n    props.getGlobalParameter\n  );\n  // For adaptable item length in the legend\n\n  // Populates data with record information\n  useEffect(() => {\n    let newKeys = {};\n    let newData: Record<string, unknown>[] = records\n      .reduce((data: Record<string, unknown>[], row: Record<string, any>) => {\n        try {\n          if (!selection || !selection.index || !selection.value) {\n            return data;\n          }\n          const index = convertRecordObjectToString(row.get(selection.index));\n          const idx = data.findIndex((item) => item.index === index);\n\n          const key = selection.key !== '(none)' ? recordToNative(row.get(selection.key)) : selection.value;\n          const rawValue = recordToNative(row.get(selection.value));\n          const value = rawValue !== null ? rawValue : 0.0000001;\n          if (isNaN(value)) {\n            return data;\n          }\n          newKeys[key] = true;\n\n          if (idx > -1) {\n            data[idx][key] = value;\n          } else {\n            data.push({ index, [key]: value });\n          }\n\n          return data;\n        } catch (e) {\n          // eslint-disable-next-line no-console\n          console.error(e);\n          return [];\n        }\n      }, [])\n      .map((row) => {\n        Object.keys(newKeys).forEach((key) => {\n          // eslint-disable-next-line no-prototype-builtins\n          if (!row.hasOwnProperty(key)) {\n            row[key] = 0;\n          }\n        });\n        return row;\n      });\n    setKeys(Object.keys(newKeys));\n    setData(newData);\n  }, [selection]);\n\n  if (!selection || props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  // Function to calculate the conditional margin bottom\n  function calculateMarginBottom(legendPosition, showLegend, legendWidth, marginBottom) {\n    // Check if legendPosition is 'Horizontal'\n    if (legendPosition === 'Horizontal') {\n      // Calculate margin based on whether the legend is shown\n      return showLegend ? legendWidth * 0.3 + marginBottom + 50 : legendWidth * 0.3 + marginBottom;\n    }\n    // Return the default marginBottom if legendPosition is not 'Horizontal'\n    return marginBottom;\n  }\n\n  // Using the function in your code\n  const conditionalMarginBottom = calculateMarginBottom(legendPosition, settings.legend, legendWidth, marginBottom);\n\n  // Function to call from BarComponent. Conducts necessary logic for Report Action.\n  const handleBarClick = (e) => {\n    // Get the original record that was used to draw this bar (or a group in a bar).\n    const record = getOriginalRecordForNivoClickEvent(e, records, selection);\n\n    // If there's a record, check if there are any rules assigned to each of the fields (columns).\n    if (record) {\n      Object.keys(record).forEach((key) => {\n        let rules = getRule({ field: key, value: record[key] }, actionsRules, 'Click');\n        // If there is a rule assigned, run the rule with the specified field and value retrieved from the record.\n        rules?.forEach((rule) => {\n          const ruleField = rule.field;\n          const ruleValue = record[rule.value];\n          performActionOnElement(\n            { field: ruleField, value: ruleValue },\n            actionsRules,\n            { ...props, pageNames: pageNames },\n            'Click',\n            'bar'\n          );\n        });\n      });\n    }\n  };\n\n  // Function to calculate the right margin\n  function calculateRightMargin(legendPosition, legend, legendWidth, marginRight) {\n    if (legendPosition === 'Vertical') {\n      return legend ? legendWidth + marginRight : marginRight;\n    }\n    return marginRight;\n  }\n\n  // Original margin function, refactored\n  const margin = () => {\n    return {\n      top: marginTop,\n      right: calculateRightMargin(legendPosition, legend, legendWidth, marginRight),\n      bottom: conditionalMarginBottom,\n      left: marginLeft,\n    };\n  };\n\n  const chartColorsByScheme = getD3ColorsByScheme(colorScheme);\n  // Compute bar color based on rules - overrides default color scheme completely.\n  const getBarColor = (bar) => {\n    let { index: colorIndex } = bar;\n    if (colorIndex >= chartColorsByScheme.length) {\n      colorIndex %= chartColorsByScheme.length;\n    }\n\n    if (!props.selection) {\n      return chartColorsByScheme[colorIndex];\n    }\n\n    const dict = {};\n    dict[selection.index] = bar.indexValue;\n    dict[selection.value] = bar.value;\n    dict[selection.key] = bar.id;\n\n    const validRuleIndex = evaluateRulesOnDict(dict, styleRules, ['bar color']);\n    if (validRuleIndex !== -1) {\n      return styleRules[validRuleIndex].customizationValue;\n    }\n\n    return chartColorsByScheme[colorIndex];\n  };\n\n  function calculateLabelPosition(bar, positionLabel, layout) {\n    let x = bar.width ? bar.width / 2 : 0;\n    let y = bar.height ? bar.height / 2 : 0;\n\n    if (positionLabel === 'top') {\n      if (layout === 'vertical') {\n        y = -10;\n      } else {\n        x = bar.width + 10;\n      }\n    } else if (positionLabel === 'bottom') {\n      if (layout === 'vertical') {\n        y = bar.height + 10;\n      } else {\n        x = -10;\n      }\n    }\n\n    return { x, y };\n  }\n\n  // Used instead of BarChartComponent when Position Label !== 'off'\n  const BarComponent = ({ bar, borderColor, onClick }) => {\n    let shade = false;\n    let darkTop = false;\n    let includeIndex = false;\n    let textAnchor = 'middle';\n    const { x, y } = calculateLabelPosition(bar, positionLabel, layout);\n\n    return (\n      <g\n        transform={`translate(${bar.x},${bar.y})`}\n        // onClick event to trigger event to pass value with report action\n        onClick={(event) => onClick(bar.data, event)}\n        style={{ cursor: 'pointer' }}\n      >\n        {shade ? <rect x={-3} y={7} width={bar.width} height={bar.height} fill='rgba(0, 0, 0, .07)' /> : <></>}\n        <rect width={bar.width} height={bar.height} fill={bar.color} />\n        {darkTop ? (\n          <rect x={bar.width - 5} width={5} height={bar.height} fill={borderColor} fillOpacity={0.2} />\n        ) : (\n          <></>\n        )}\n        {includeIndex ? (\n          <text\n            x={bar.width - 16}\n            y={bar.height / 2}\n            textAnchor='end'\n            dominantBaseline='central'\n            fill='black'\n            style={{\n              fontWeight: 900,\n              fontSize: 30,\n            }}\n          >\n            {bar.data.indexValue}\n          </text>\n        ) : (\n          <></>\n        )}\n        {enableLabel ? (\n          <text\n            x={x}\n            y={y}\n            textAnchor={textAnchor}\n            dominantBaseline='central'\n            fill={borderColor}\n            style={{\n              fontWeight: 100,\n              fontSize: 10,\n            }}\n          >\n            {bar.data.value}\n          </text>\n        ) : (\n          <></>\n        )}\n      </g>\n    );\n  };\n\n  // Fixing canvas bug, from https://github.com/plouc/nivo/issues/2162\n  // SVGGraphicsElement.getBBox\n  HTMLCanvasElement.prototype.getBBox = function tooltipMapper() {\n    return { width: this.offsetWidth, height: this.offsetHeight };\n  };\n\n  const extraProperties = positionLabel !== 'off' ? { barComponent: BarComponent } : {};\n  const canvas = data.length > 30;\n  const BarChartComponent = canvas ? ResponsiveBarCanvas : ResponsiveBar;\n\n  // Creates enough width to ensure chart doesn't get cut off\n  const adaptableWidth =\n    marginLeft +\n    marginRight +\n    data.length * barWidth * 4 +\n    (data.length - 1) * 4 +\n    (data.length - 1) * innerPadding * 4;\n\n  // Legend F\n  const calculateLegendConfig = () => {\n    if (!legend) {\n      return []; // No legend required\n    }\n\n    if (legendPosition === 'Horizontal') {\n      return [\n        {\n          dataFrom: 'keys',\n          anchor: 'bottom',\n          direction: 'row',\n          justify: false,\n          translateX: 0,\n          translateY: legendWidth,\n          itemsSpacing: 2,\n          itemWidth: legendWidth,\n          itemHeight: 20,\n          itemDirection: 'left-to-right',\n          itemOpacity: 0.85,\n          symbolSize: 20,\n          effects: [\n            {\n              on: 'hover',\n              style: {\n                itemOpacity: 1,\n              },\n            },\n          ],\n        },\n      ];\n    }\n    // Vertical legend\n    return [\n      {\n        dataFrom: 'keys',\n        anchor: 'bottom-right',\n        direction: 'column',\n        justify: false,\n        translateX: legendWidth + 10,\n        translateY: 0,\n        itemsSpacing: 1,\n        itemWidth: legendWidth,\n        itemHeight: 20,\n        itemDirection: 'left-to-right',\n        itemOpacity: 0.85,\n        symbolSize: 15,\n        effects: [\n          {\n            on: 'hover',\n            style: {\n              itemOpacity: 1,\n            },\n          },\n        ],\n      },\n    ];\n  };\n\n  // Height of each legend item\n  const itemHeight = 24.5;\n\n  // Function to handle width logic, including scrollbar logic\n  function calculateWidth(customDimensions, legendPosition, adaptableWidth, legendWidth, data, barWidth) {\n    if (!customDimensions) {\n      return '100%';\n    }\n\n    if (legendPosition === 'Horizontal') {\n      const horizontalLegendWidth = legendWidth * data.length + 200;\n      return adaptableWidth > horizontalLegendWidth ? adaptableWidth : horizontalLegendWidth;\n    }\n\n    return barWidth * 5 * data.length + legendWidth;\n  }\n\n  // Container to make the chart scroll horizontally\n  const scrollableWrapperStyle: React.CSSProperties = {\n    width: calculateWidth(customDimensions, legendPosition, adaptableWidth, legendWidth, data, barWidth),\n    height: expandHeightForLegend ? itemHeight * data.length + conditionalMarginBottom : '100%',\n    whiteSpace: 'nowrap',\n  };\n\n  // Container for scrolling container to scroll in\n  const barChartStyle: React.CSSProperties = customDimensions\n    ? {\n        width: '100%',\n        overflowX: 'auto',\n        overflowY: 'auto',\n        height: '100%',\n      }\n    : {\n        width: '100%',\n        height: '100%',\n        overflowY: 'auto',\n      };\n\n  const chart = (\n    <div style={barChartStyle}>\n      <div style={scrollableWrapperStyle}>\n        <BarChartComponent\n          theme={canvas ? themeNivoCanvas(props.theme) : themeNivo}\n          data={data}\n          key={`${selection.index}___${selection.value}`}\n          layout={layout}\n          groupMode={groupMode == 'stacked' ? 'stacked' : 'grouped'}\n          enableLabel={enableLabel}\n          onClick={handleBarClick}\n          keys={keys}\n          indexBy='index'\n          margin={margin()}\n          valueScale={{ type: valueScale }}\n          padding={padding}\n          innerPadding={innerPadding}\n          minValue={minValue}\n          maxValue={maxValue}\n          colors={getBarColor}\n          axisTop={null}\n          axisRight={null}\n          axisBottom={{\n            tickSize: 5,\n            tickPadding: 5,\n            tickRotation: labelRotation,\n          }}\n          axisLeft={\n            displayYAxis\n              ? {\n                  tickSize: 5,\n                  tickPadding: 5,\n                  tickRotation: 0,\n                }\n              : null\n          }\n          enableGridY={displayYGridLines}\n          labelSkipWidth={labelSkipWidth}\n          labelSkipHeight={labelSkipHeight}\n          labelTextColor={{ from: 'color', modifiers: [['darker', 1.6]] }}\n          {...extraProperties}\n          legends={calculateLegendConfig()}\n          animate={false}\n        />\n      </div>\n    </div>\n  );\n\n  return chart;\n};\n\nexport default NeoBarChart;\n"
  },
  {
    "path": "src/chart/bar/util.ts",
    "content": "/**\n * Utility function to reverse engineer, from an event on a Nivo bar chart, what the original Neo4j record was the data came from.\n * Once we have this record, we can pass it to the action rule handler, so that users can define report actions on any variable\n * in their return statement.\n * @param e the click event on the bar chart.\n * @param records the Neo4j records used to build the visualization\n * @param selection the selection made by the user (category, index, group*) - where group is optional.\n * @returns\n */\nexport function getOriginalRecordForNivoClickEvent(e, records, selection) {\n  // TODO - rewrite this to be more optimal (using list comprehensions, etc.)\n  const usesGroups = Object.keys(e.data).length > 2;\n  const group = e.id;\n  const { value } = e;\n  const category = e.indexValue;\n\n  // Go through all records and find the first record `r` where the event's values match exactly.\n  for (const i in records) {\n    const r = records[i];\n    const categoryIndex = r._fieldLookup[selection.index];\n    const groupIndex = r._fieldLookup[selection.key];\n    const valueIndex = r._fieldLookup[selection.value];\n    const recordCategory = r._fields[categoryIndex];\n    const recordGroup = r._fields[groupIndex];\n    const recordValue = r._fields[valueIndex];\n\n    if (usesGroups) {\n      if (recordCategory == category && recordGroup == group && recordValue == value) {\n        const dict = {};\n        for (const i in Object.keys(r._fieldLookup)) {\n          const key = Object.keys(r._fieldLookup)[i];\n          dict[key] = r._fields[r._fieldLookup[key]];\n        }\n        return dict;\n      }\n    } else if (recordCategory == category && recordValue == value) {\n      const dict = {};\n      for (const i in Object.keys(r._fieldLookup)) {\n        const key = Object.keys(r._fieldLookup)[i];\n        dict[key] = r._fields[r._fieldLookup[key]];\n      }\n      return dict;\n    }\n  }\n}\n"
  },
  {
    "path": "src/chart/graph/GraphChart.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport useDimensions from 'react-cool-dimensions';\nimport { ChartProps } from '../Chart';\nimport { NeoGraphChartInspectModal } from './component/GraphChartInspectModal';\nimport { NeoGraphChartVisualization2D } from './GraphChartVisualization2D';\nimport { NeoGraphChartDeepLinkButton } from './component/button/GraphChartDeepLinkButton';\nimport { NeoGraphChartCanvas } from './component/GraphChartCanvas';\nimport { NeoGraphChartLockButton } from './component/button/GraphChartLockButton';\nimport { NeoGraphChartFitViewButton } from './component/button/GraphChartFitViewButton';\nimport { buildGraphVisualizationObjectFromRecords } from './util/RecordUtils';\nimport { parseNodeIconConfig } from './util/NodeUtils';\nimport { GraphChartVisualizationProps, Link, layouts } from './GraphChartVisualization';\nimport { handleExpand } from './util/ExplorationUtils';\nimport { categoricalColorSchemes } from '../../config/ColorConfig';\nimport { IconButtonArray, IconButton } from '@neo4j-ndl/react';\nimport { Tooltip } from '@mui/material';\nimport { downloadCSV } from '../ChartUtils';\nimport { generateSafeColumnKey } from '../table/TableChart';\nimport { GraphChartContextMenu } from './component/GraphChartContextMenu';\nimport { getSettings } from '../SettingsUtils';\nimport { getPageNumbersAndNamesList } from '../../extensions/advancedcharts/Utils';\nimport { CloudArrowDownIconOutline } from '@neo4j-ndl/react/icons';\n\nexport interface GraphChartProps extends ChartProps {\n  lockable?: boolean;\n  component?: any;\n}\n\nconst DEFAULT_VISUALIZATION_COMPONENT = NeoGraphChartVisualization2D;\n\n/**\n * Draws graph data using a force-directed-graph visualization.\n * This visualization is powered by `react-force-graph`.\n * See https://github.com/vasturiano/react-force-graph for examples on customization.\n */\nconst NeoGraphChart = (props: GraphChartProps) => {\n  if (props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <>No data, re-run the report.</>;\n  }\n  const Visualization = props.component ? props.component : DEFAULT_VISUALIZATION_COMPONENT;\n\n  // Retrieve config from advanced settings\n  const settings = getSettings(props.settings, props.extensions, props.getGlobalParameter);\n  const lockable = props.lockable !== undefined ? props.lockable : true;\n  const linkDirectionalParticles = props.settings && props.settings.relationshipParticles ? 5 : undefined;\n  const arrowLengthProp = props?.settings?.arrowLengthProp ?? 3;\n  let nodePositions = props.settings && props.settings.nodePositions ? props.settings.nodePositions : {};\n  const parameters = props.parameters ? props.parameters : {};\n\n  const setNodePositions = (positions) =>\n    props.updateReportSetting && props.updateReportSetting('nodePositions', positions);\n  const handleEntityClick = (item) => {\n    setSelectedEntity(item);\n    setContextMenuOpen(false);\n    if (item !== undefined && settings.showPropertiesOnClick) {\n      setInspectModalOpen(true);\n    }\n  };\n\n  const handleEntityRightClick = (item, event) => {\n    setSelectedEntity(item);\n    setContextMenuOpen(true);\n    setClickPosition({\n      x: event.clientX,\n      y: event.clientY,\n    });\n  };\n  const frozen: boolean = props.settings && props.settings.frozen !== undefined ? props.settings.frozen : false;\n  const [inspectModalOpen, setInspectModalOpen] = useState(false);\n  const [selectedEntity, setSelectedEntity] = useState(undefined);\n  const [contextMenuOpen, setContextMenuOpen] = useState(false);\n  const [clickPosition, setClickPosition] = useState({ x: 0, y: 0 });\n  const [recenterAfterEngineStop, setRecenterAfterEngineStop] = useState(true);\n  const [cooldownTicks, setCooldownTicks] = useState(100);\n\n  let [nodeLabels, setNodeLabels] = useState({});\n  let [linkTypes, setLinkTypes] = useState({});\n  const [data, setData] = useState({ nodes: [] as Node[], links: [] as Link[] });\n\n  const setLayoutFrozen = (value) => {\n    if (value == false) {\n      setCooldownTicks(100);\n      setRecenterAfterEngineStop(true);\n      setNodePositions({});\n    }\n    props.updateReportSetting && props.updateReportSetting('frozen', value);\n  };\n\n  const setGraph = (nodes, links) => {\n    setData({ nodes: nodes, links: links });\n  };\n  const setNodes = (nodes) => {\n    setData({ nodes: nodes, links: data.links });\n  };\n  const setLinks = (links) => {\n    setData({ nodes: data.nodes, links: links });\n  };\n\n  let icons = parseNodeIconConfig(settings.iconStyle);\n  const colorScheme = categoricalColorSchemes[settings.nodeColorScheme];\n  const { theme } = props;\n\n  const generateVisualizationDataGraph = (records, _) => {\n    let nodes: Record<string, any>[] = [];\n    let links: Record<string, any>[] = [];\n    const extractedGraphFromRecords = buildGraphVisualizationObjectFromRecords(\n      records,\n      nodes,\n      links,\n      nodeLabels,\n      linkTypes,\n      colorScheme,\n      props.fields,\n      settings.nodeColorProp,\n      settings.defaultNodeColor,\n      settings.nodeSizeProp,\n      settings.defaultNodeSize,\n      settings.relWidthProp,\n      settings.defaultRelWidth,\n      settings.relColorProp,\n      settings.defaultRelColor,\n      settings.styleRules,\n      nodePositions,\n      frozen\n    );\n    setData(extractedGraphFromRecords);\n  };\n\n  const { observe, width, height } = useDimensions({\n    onResize: ({ observe, unobserve }) => {\n      unobserve(); // To stop observing the current target element\n      observe(); // To re-start observing the current target element\n    },\n  });\n\n  const pageNames = getPageNumbersAndNamesList();\n  const chartProps: GraphChartVisualizationProps = {\n    data: {\n      nodes: data.nodes,\n      nodeLabels: nodeLabels,\n      links: data.links,\n      linkTypes: linkTypes,\n      parameters: parameters,\n      setGraph: setGraph,\n      setNodes: setNodes,\n      setLinks: setLinks,\n      setNodeLabels: setNodeLabels,\n      setLinkTypes: setLinkTypes,\n    },\n    style: {\n      width: width,\n      height: height,\n      backgroundColor: theme == 'dark' && settings.backgroundColor == '#fafafa' ? '#040404' : settings.backgroundColor, // Temporary fix for default color adjustment in dark mode\n      linkDirectionalParticles: linkDirectionalParticles,\n      linkDirectionalArrowLength: arrowLengthProp,\n      linkDirectionalParticleSpeed: settings.linkDirectionalParticleSpeed,\n      nodeLabelFontSize: settings.nodeLabelFontSize,\n      nodeLabelColor: theme == 'dark' && settings.nodeLabelColor == 'black' ? 'white' : settings.nodeLabelColor, // Temporary fix for default color adjustment in dark mode\n      relLabelFontSize: settings.relLabelFontSize,\n      relLabelColor: settings.relLabelColor,\n      defaultNodeSize: settings.defaultNodeSize,\n      nodeIcons: icons,\n      colorScheme: colorScheme,\n      nodeColorProp: settings.nodeColorProp,\n      defaultNodeColor: settings.defaultNodeColor,\n      nodeSizeProp: settings.nodeSizeProp,\n      relWidthProp: settings.relWidthProp,\n      defaultRelWidth: settings.defaultRelWidth,\n      relColorProp: settings.relColorProp,\n      defaultRelColor: settings.defaultRelColor,\n      theme: theme,\n    },\n    engine: {\n      layout: layouts[settings.layout],\n      graphDepthSep: settings.graphDepthSep,\n      queryCallback: props.queryCallback,\n      cooldownTicks: cooldownTicks,\n      setCooldownTicks: setCooldownTicks,\n      selection: props.selection,\n      setSelection: () => {\n        throw 'NotImplemented';\n      },\n      fields: props.fields,\n      setFields: props.setFields,\n      recenterAfterEngineStop: recenterAfterEngineStop,\n      setRecenterAfterEngineStop: setRecenterAfterEngineStop,\n    },\n    interactivity: {\n      enableExploration: settings.enableExploration,\n      enableEditing: settings.enableEditing,\n      layoutFrozen: frozen,\n      setLayoutFrozen: setLayoutFrozen,\n      nodePositions: nodePositions,\n      setNodePositions: setNodePositions,\n      showPropertiesOnHover: settings.showPropertiesOnHover,\n      showPropertiesOnClick: settings.showPropertiesOnClick,\n      showPropertyInspector: inspectModalOpen,\n      setPropertyInspectorOpen: setInspectModalOpen,\n      fixNodeAfterDrag: settings.fixNodeAfterDrag,\n      handleExpand: handleExpand,\n      setGlobalParameter: props.setGlobalParameter,\n      setPageNumber: props.setPageNumber,\n      pageNames: pageNames,\n      onNodeClick: (item) => handleEntityClick(item),\n      onNodeRightClick: (item, event) => handleEntityRightClick(item, event),\n      onRelationshipClick: (item) => handleEntityClick(item),\n      onRelationshipRightClick: (item, event) => handleEntityRightClick(item, event),\n      drilldownLink: settings.drilldownLink,\n      selectedEntity: selectedEntity,\n      setSelectedEntity: setSelectedEntity,\n      contextMenuOpen: contextMenuOpen,\n      setContextMenuOpen: setContextMenuOpen,\n      clickPosition: clickPosition,\n      setClickPosition: setClickPosition,\n      createNotification: props.createNotification,\n    },\n    extensions: {\n      styleRules: settings.styleRules,\n      actionsRules: settings.actionsRules,\n    },\n  };\n\n  // When data is refreshed, rebuild the visualization data.\n  useEffect(() => {\n    generateVisualizationDataGraph(props.records, chartProps);\n  }, [props.records]);\n\n  return (\n    <div ref={observe} style={{ width: '100%', height: '100%' }}>\n      <NeoGraphChartCanvas>\n        <IconButtonArray\n          aria-label={'graph icon'}\n          floating\n          orientation='horizontal'\n          className='n-z-10 n-absolute n-bottom-2 n-right-4'\n        >\n          <GraphChartContextMenu {...chartProps} />\n          <NeoGraphChartFitViewButton {...chartProps} />\n          {lockable && settings.lockable ? <NeoGraphChartLockButton {...chartProps} /> : <></>}\n          {settings.drilldownLink ? <NeoGraphChartDeepLinkButton {...chartProps} /> : <></>}\n        </IconButtonArray>\n        <Visualization {...chartProps} />\n        <NeoGraphChartInspectModal {...chartProps}></NeoGraphChartInspectModal>\n        {settings.allowDownload && props.records && props.records.length > 0 ? (\n          <IconButtonArray floating orientation='horizontal' className='n-z-10 n-absolute n-bottom-2 n-left-4'>\n            <Tooltip title='Download CSV.' aria-label={'download csv'} disableInteractive>\n              <IconButton aria-label='download csv' size='small' clean grouped>\n                <CloudArrowDownIconOutline\n                  onClick={() => {\n                    const rows = props.records.map((record, rownumber) => {\n                      return Object.assign(\n                        { id: rownumber },\n                        ...record._fields.map((field, i) => ({ [generateSafeColumnKey(record.keys[i])]: field }))\n                      );\n                    });\n                    downloadCSV(rows);\n                  }}\n                />\n              </IconButton>\n            </Tooltip>\n          </IconButtonArray>\n        ) : (\n          <></>\n        )}\n      </NeoGraphChartCanvas>\n    </div>\n  );\n};\n\nexport default NeoGraphChart;\n"
  },
  {
    "path": "src/chart/graph/GraphChartVisualization.ts",
    "content": "/**\n * A mapping between human-readable layout names, and the ones used by the library.\n */\nexport const layouts = {\n  'force-directed': undefined,\n  'tree-top-down': 'td',\n  'tree-bottom-up': 'bu',\n  'tree-left-right': 'lr',\n  'tree-right-left': 'rl',\n  radial: 'radialout',\n  tree: 'td',\n};\n\ntype Layout = 'td' | 'bu' | 'lr' | 'rl' | 'radialout' | 'radialin';\n\nexport const defaultNodeColor = 'lightgrey'; // Color of nodes without labels\n\n/**\n * A node or relationship as selected in the graph.\n */\nexport interface GraphEntity {\n  properties: any;\n  id: string;\n}\n\nexport interface Node extends GraphEntity {\n  labels: string[];\n  mainLabel: string;\n  x?: number;\n  y?: number;\n  fx?: number;\n  fy?: number;\n}\n\nexport interface Link extends GraphEntity {\n  type: string;\n  width?: number;\n  source?: Node;\n  target?: Node;\n  color?: string;\n}\n\n/**\n * The set of properties a graph visualization component (and its peripheral components) expects.\n * objects implementing this interface are passed around the different utility functions for the graph visualization.\n * TODO: Split the `GraphChartVisualizationProps` into sub-interfaces that can be passed down individually.\n */\nexport interface GraphChartVisualizationProps {\n  /**\n   * entries in 'data' are related to anything relevant for rendering the graph.\n   * These are the nodes/relationships, but also their labels and types.\n   * The data dictionary can be updated by calling any of the functions in the data entry.\n   */\n  data: {\n    nodes: Node[];\n    nodeLabels: Record<string, any>;\n    links: Link[];\n    linkTypes: Record<string, any>;\n    parameters: Record<string, any>;\n    setGraph: (nodes, links) => void;\n    setNodes: (nodes) => void;\n    setLinks: (links) => void;\n    setNodeLabels: (labels) => void;\n    setLinkTypes: (types) => void;\n  };\n  /**\n   * The properties relevant for styling the graph.\n   * Style is applied at the moment the data dictionary is generated.\n   */\n  style: {\n    width: number;\n    height: number;\n    backgroundColor: any;\n    linkDirectionalParticles?: number;\n    linkDirectionalParticleSpeed: number;\n    linkDirectionalArrowLength: number;\n    nodeLabelFontSize: number;\n    nodeLabelColor: string;\n    relLabelFontSize: number;\n    relLabelColor: string;\n    defaultNodeSize: number;\n    nodeIcons: Record<string, any>;\n    colorScheme: Record<string, any>;\n    nodeColorProp: string;\n    defaultNodeColor: string;\n    nodeSizeProp: string;\n    relWidthProp: string;\n    defaultRelWidth: number;\n    relColorProp: string;\n    defaultRelColor: string;\n    theme?: string;\n  };\n  /**\n   * The keys in 'engine' are related to the graph rendering engine (force-directed layout) or the NeoDash query engine.\n   */\n  engine: {\n    layout: Layout;\n    graphDepthSep: number;\n    queryCallback: (query: string, parameters: Record<string, any>, setRecords: any) => void;\n    cooldownTicks: number;\n    setCooldownTicks: (ticks: number) => void;\n    selection: Record<string, any> | undefined;\n    setSelection: (selection: Record<string, any>) => void;\n    fields: any;\n    setFields: ((fields: any) => void) | undefined;\n    recenterAfterEngineStop: boolean;\n    setRecenterAfterEngineStop: (value: boolean) => void;\n  };\n  /**\n   * The entries in 'interactivity' handle the interactive elements of the visualization.\n   * This includes handling click events, showing pop-ups, and more.\n   * TODO: Split up interactivity user-settings and interactivity callbacks/functional variables.\n   */\n  interactivity: {\n    enableExploration: boolean;\n    enableEditing: boolean;\n    layoutFrozen: boolean;\n    setLayoutFrozen: React.Dispatch<React.SetStateAction<boolean>>;\n    nodePositions: Record<string, any>;\n    setNodePositions: (positions: any[]) => void;\n    showPropertiesOnHover: boolean;\n    showPropertiesOnClick: boolean;\n    showPropertyInspector: boolean;\n    setPropertyInspectorOpen: React.Dispatch<React.SetStateAction<boolean>>;\n    createNotification: ((title: string, message: string) => void) | undefined;\n    fixNodeAfterDrag: boolean;\n    onNodeClick: (node) => void;\n    onNodeRightClick: (node, event) => void;\n    onRelationshipClick: (rel) => void;\n    onRelationshipRightClick: (rel, event) => void;\n    setGlobalParameter?: (name: string, value: string) => void;\n    handleExpand: (id, type, dir, properties) => void;\n    zoomToFit: () => void;\n    drilldownLink: string;\n    selectedEntity?: GraphEntity;\n    setSelectedEntity: (entity) => void;\n    contextMenuOpen: boolean;\n    setContextMenuOpen: (boolean) => void;\n    clickPosition: Record<string, any>;\n    setClickPosition: (pos) => void;\n    setPageNumber: any;\n    pageNames: [];\n    customTablePropertiesOfModal: any[];\n    pageIdAndParameterName: string;\n  };\n  /**\n   * A set of configuration parameters used for the visualization engine.\n   */\n  config?: {\n    graphComponent: any;\n    cooldownAfterengineStop: number;\n    nodeCanvasObjectMode?: () => void;\n    nodeCanvasObject?: (node: any, ctx: any) => void;\n    linkCanvasObjectMode?: () => void;\n    linkCanvasObject?: (link: any, ctx: any) => void;\n    nodeThreeObjectExtend?: boolean;\n    nodeThreeObject?: (node) => void;\n    linkThreeObjectExtend?: boolean;\n    linkThreeObject?: (link) => void;\n    linkPositionUpdate?: (sprite: any, { start, end }: { start: any; end: any }, link: any, ref: any) => void;\n  };\n  /**\n   * entries in 'extensions' let users plug in extra functionality into the visualization based on enabled plugins.\n   */\n  extensions: {\n    styleRules: any[];\n    actionsRules: any[];\n  };\n}\n"
  },
  {
    "path": "src/chart/graph/GraphChartVisualization2D.tsx",
    "content": "import React, { useRef } from 'react';\nimport ForceGraph2D from 'react-force-graph-2d';\nimport { executeActionRule, getRuleWithFieldPropertyName } from '../../extensions/advancedcharts/Utils';\nimport { getTooltip } from './component/GraphChartTooltip';\nimport { GraphChartVisualizationProps } from './GraphChartVisualization';\nimport { generateNodeCanvasObject } from './util/NodeUtils';\nimport { generateRelCanvasObject } from './util/RelUtils';\nimport { NeoGraphChartVisualizationBase } from './GraphChartVisualizationBase';\n\n/*\n *\n */\nexport const NeoGraphChartVisualization2D = (props: GraphChartVisualizationProps) => {\n  const config2d = {\n    graphComponent: ForceGraph2D,\n    cooldownAfterengineStop: 0,\n    nodeCanvasObjectMode: () => 'after',\n    nodeCanvasObject: (node: any, ctx: any) =>\n      generateNodeCanvasObject(\n        node,\n        ctx,\n        props.style.nodeIcons,\n        props.interactivity.layoutFrozen,\n        props.interactivity.nodePositions,\n        props.style.nodeLabelFontSize,\n        props.style.defaultNodeSize,\n        props.style.nodeLabelColor,\n        props.extensions.styleRules,\n        props.engine.selection\n      ),\n    linkCanvasObjectMode: () => 'after',\n    linkCanvasObject: (link: any, ctx: any) =>\n      generateRelCanvasObject(link, ctx, props.style.relLabelFontSize, props.style.relLabelColor),\n  };\n  const props2d = { ...props, config: config2d };\n  return <NeoGraphChartVisualizationBase {...props2d} />;\n};\n"
  },
  {
    "path": "src/chart/graph/GraphChartVisualizationBase.tsx",
    "content": "import React, { useRef } from 'react';\nimport { executeActionRule, getRuleWithFieldPropertyName } from '../../extensions/advancedcharts/Utils';\nimport { getTooltip } from './component/GraphChartTooltip';\nimport { GraphChartVisualizationProps } from './GraphChartVisualization';\n\nexport const NeoGraphChartVisualizationBase = (props: GraphChartVisualizationProps) => {\n  const fgRef: React.MutableRefObject<any> = useRef();\n  const GraphComponent = props.config?.graphComponent;\n  if (!props.style.width || !props.style.height) {\n    return <></>;\n  }\n  props.interactivity.zoomToFit = () => fgRef.current && fgRef.current.zoomToFit(400);\n  return (\n    <GraphComponent\n      ref={fgRef}\n      width={props.style.width - 20}\n      height={props.style.height}\n      linkCurvature='curvature'\n      backgroundColor={props.style.backgroundColor}\n      linkDirectionalArrowLength={props.style.linkDirectionalArrowLength}\n      linkDirectionalArrowRelPos={1}\n      dagMode={props.engine.layout}\n      dagLevelDistance={props.engine.graphDepthSep}\n      linkWidth={(link: any) => link.width}\n      linkLabel={(link: any) => (props.interactivity.showPropertiesOnHover ? `<div>${getTooltip(link)}</div>` : '')}\n      nodeLabel={(node: any) => (props.interactivity.showPropertiesOnHover ? `<div>${getTooltip(node)}</div>` : '')}\n      nodeVal={(node: any) => node.size}\n      onNodeClick={(item) => {\n        let rules = getRuleWithFieldPropertyName(item, props.extensions.actionsRules, 'onNodeClick', 'labels');\n        rules != null\n          ? rules.forEach((rule) => executeActionRule(rule, item, { ...props.interactivity }))\n          : props.interactivity.onNodeClick(item);\n      }}\n      onLinkClick={(item) => {\n        let rules = getRuleWithFieldPropertyName(item, props.extensions.actionsRules, 'onLinkClick', 'type');\n        rules != null\n          ? rules.forEach((rule) => executeActionRule(rule, item, props.interactivity.setGlobalParameter))\n          : props.interactivity.onRelationshipClick(item);\n      }}\n      onNodeRightClick={(node, event) => props.interactivity.onNodeRightClick(node, event)}\n      onLinkRightClick={(link, event) => props.interactivity.onRelationshipRightClick(link, event)}\n      onBackgroundClick={() => props.interactivity.onNodeClick(undefined)}\n      onBackgroundRightClick={() => props.interactivity.onNodeClick(undefined)}\n      linkLineDash={(link) => (link.new ? [2, 1] : null)}\n      linkDirectionalParticles={props.style.linkDirectionalParticles}\n      linkDirectionalParticleSpeed={props.style.linkDirectionalParticleSpeed}\n      cooldownTicks={props.engine.cooldownTicks}\n      onEngineStop={() => {\n        props.engine.setCooldownTicks(props.config.cooldownAfterengineStop);\n        if (props.engine.recenterAfterEngineStop) {\n          fgRef.current.zoomToFit(400);\n          props.engine.setRecenterAfterEngineStop(false);\n        }\n      }}\n      onZoom={() => {\n        props.interactivity.setContextMenuOpen(false);\n      }}\n      onNodeDrag={() => {\n        props.interactivity.setContextMenuOpen(false);\n        props.engine.setCooldownTicks(1);\n        props.engine.setRecenterAfterEngineStop(false);\n      }}\n      onNodeDragEnd={(node) => {\n        props.engine.setCooldownTicks(props.config.cooldownAfterengineStop);\n        if (props.interactivity.fixNodeAfterDrag) {\n          node.fx = node.x;\n          node.fy = node.y;\n          if (node.z !== undefined) {\n            node.fz = node.z;\n          }\n        }\n        // TODO - Frozen layout only works in 2D\n        if (props.interactivity.layoutFrozen) {\n          const key = node.id;\n          const val = [node.x, node.y];\n          const old = { ...props.interactivity.nodePositions };\n          old[key] = val;\n          props.interactivity.setNodePositions(old);\n        }\n      }}\n      // 2D-specific config settings\n      nodeCanvasObjectMode={props.config?.nodeCanvasObjectMode}\n      nodeCanvasObject={props.config?.nodeCanvasObject}\n      linkCanvasObjectMode={props.config?.linkCanvasObjectMode}\n      linkCanvasObject={props.config?.linkCanvasObject}\n      // 3D-specific config settings\n      nodeThreeObjectExtend={props.config?.nodeThreeObjectExtend}\n      nodeThreeObject={props.config?.nodeThreeObject}\n      linkThreeObjectExtend={props.config?.linkThreeObjectExtend}\n      linkThreeObject={props.config?.linkThreeObject}\n      linkPositionUpdate={(sprite, { start, end }, link) =>\n        props.config?.linkPositionUpdate(sprite, { start, end }, link, fgRef)\n      }\n      // Data to populate graph\n      graphData={props.style.width ? { nodes: props.data.nodes, links: props.data.links } : { nodes: [], links: [] }}\n    />\n  );\n};\n"
  },
  {
    "path": "src/chart/graph/component/GraphChartCanvas.tsx",
    "content": "import React from 'react';\n\nconst canvasStyle = {\n  paddingLeft: '10px',\n  marginBottom: 5,\n  position: 'relative',\n  overflow: 'hidden',\n  width: '100%',\n  height: '100%',\n};\n\n/**\n * Renders the canvas on which the graph visualization is projected.\n */\nexport const NeoGraphChartCanvas = ({ children }) => {\n  return (\n    <div className='graph-chart-canvas' style={canvasStyle}>\n      {children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/chart/graph/component/GraphChartContextMenu.tsx",
    "content": "import * as React from 'react';\nimport MenuItem from '@mui/material/MenuItem';\nimport { GraphChartVisualizationProps } from '../GraphChartVisualization';\nimport { Card, CardHeader } from '@mui/material';\nimport { IconButton } from '@neo4j-ndl/react';\nimport { MagnifyingGlassCircleIconOutline, PencilIconOutline, XMarkIconOutline } from '@neo4j-ndl/react/icons';\nimport { NestedMenuItem, IconMenuItem } from 'mui-nested-menu';\nimport { RenderNode, RenderNodeChip, RenderRelationshipChip } from '../../../report/ReportRecordProcessing';\nimport { getNodeLabel } from '../util/NodeUtils';\nimport { EditAction, EditType, GraphChartEditModal } from './GraphChartEditModal';\nimport { handleExpand, handleGetNodeRelTypes } from '../util/ExplorationUtils';\nimport { useEffect } from 'react';\nimport { mergeDatabaseStatCountsWithCountsInView } from '../util/ExplorationUtils';\nimport { createPortal } from 'react-dom';\n\n/**\n * Renders the context menu that is present when a user right clicks on a node or relationship in the graph.\n * The context menu can be used to inspect and edit nodes/relationships, or explore the graph.\n */\nexport const GraphChartContextMenu = (props: GraphChartVisualizationProps) => {\n  const [dialogOpen, setDialogOpen] = React.useState(false);\n  const [editableEntity, setEditableEntity] = React.useState(undefined);\n  const [editableEntityType, setEditableEntityType] = React.useState(EditType.Node);\n  const [action, setAction] = React.useState(EditAction.Create);\n  const [neighbourRelCounts, setNeighbourRelCounts] = React.useState([]);\n  const handleClose = () => {\n    props.interactivity.setContextMenuOpen(false);\n  };\n  const dialogProps = { ...props, selectedNode: editableEntity, dialogOpen: dialogOpen, setDialogOpen: setDialogOpen };\n  const expandable = props.interactivity.selectedEntity && props.interactivity.selectedEntity.labels !== undefined;\n  const [cachedNeighbours, setCachedNeighbours] = React.useState(false);\n  // Clear neighbour cache when selection changes.\n  useEffect(() => {\n    setCachedNeighbours(false);\n  }, [props.interactivity.selectedEntity]);\n\n  const menu = (\n    <div\n      className='n-absolute n-z-60'\n      style={{\n        top: props.interactivity.clickPosition.y,\n        left: props.interactivity.clickPosition.x,\n      }}\n    >\n      <Card id='basic-menu'>\n        <CardHeader\n          className='-n-mx-1'\n          style={{ color: 'black' }}\n          action={\n            <IconButton aria-label='close' className='n-p-1 n-ml-5 n-mt-1' onClick={handleClose} clean>\n              <XMarkIconOutline />\n            </IconButton>\n          }\n          titleTypographyProps={{ variant: 'h6' }}\n          title={\n            props.interactivity.selectedEntity\n              ? expandable\n                ? props.interactivity.selectedEntity.labels.join(', ')\n                : props.interactivity.selectedEntity.type\n              : ''\n          }\n        />\n        <IconMenuItem\n          rightIcon={<MagnifyingGlassCircleIconOutline className='btn-icon-base-r' />}\n          label='Inspect'\n          onClick={() => {\n            props.interactivity.setContextMenuOpen(false);\n            props.interactivity.setPropertyInspectorOpen(true);\n          }}\n        ></IconMenuItem>\n        {props.interactivity.enableEditing ? (\n          <IconMenuItem\n            rightIcon={<PencilIconOutline className='btn-icon-base-r' />}\n            label='Edit'\n            onClick={() => {\n              setEditableEntityType(expandable ? EditType.Node : EditType.Relationship);\n              setAction(EditAction.Edit);\n              props.interactivity.setContextMenuOpen(false);\n              setDialogOpen(true);\n            }}\n          ></IconMenuItem>\n        ) : (\n          <></>\n        )}\n\n        {props.interactivity.enableExploration && expandable ? (\n          <NestedMenuItem\n            label='Expand...'\n            nonce={undefined}\n            parentMenuOpen={true}\n            onMouseOver={() => {\n              if (!cachedNeighbours) {\n                setCachedNeighbours(true);\n                const id = props.interactivity.selectedEntity?.id;\n                // Virtual relationships do not have any neighbours\n                if (id < 0) {\n                  setNeighbourRelCounts([]);\n                  return;\n                }\n                handleGetNodeRelTypes(id, props.engine, (records) =>\n                  setNeighbourRelCounts(mergeDatabaseStatCountsWithCountsInView(id, records, props.data.links))\n                );\n              }\n            }}\n          >\n            <div style={{ maxHeight: '400px', overflowY: 'auto', overflowX: 'hidden' }}>\n              <table>\n                <tbody>\n                  {neighbourRelCounts.length == 0 ? (\n                    <tr key={'ctxMenuItemTr1Default'}>\n                      <td style={{ paddingLeft: 15, minWidth: '250px' }}> No relationships...</td>\n                    </tr>\n                  ) : (\n                    <></>\n                  )}\n                  {neighbourRelCounts.length > 0 &&\n                    neighbourRelCounts.map((item, index) => {\n                      const dir = item[1] == 'any' ? undefined : item[1] == 'out';\n                      return (\n                        <tr key={`ctxMenuItemTr1-${index}`}>\n                          <MenuItem\n                            key={`ctxMenuItem1-${index}`}\n                            onClick={() => {\n                              props.interactivity.setContextMenuOpen(false);\n                              handleExpand(props.interactivity.selectedEntity.id, item[0], item[1], props);\n                              setDialogOpen(false);\n                              setCachedNeighbours(false);\n                            }}\n                          >\n                            <td style={{ minWidth: '250px', overflow: 'hidden' }}>\n                              {RenderNodeChip(props.interactivity.selectedEntity.labels, '#fff', '1px solid lightgrey')}\n                              &nbsp;\n                              {RenderRelationshipChip(item[0], dir, '#dedede')}\n                              &nbsp;\n                              {RenderNodeChip('...', '#fff', '1px solid lightgrey')}\n                            </td>\n                            <td style={{ width: 'auto', marginLeft: '15px' }}>{item[2]}</td>\n                          </MenuItem>\n                        </tr>\n                      );\n                    })}\n                </tbody>\n              </table>\n            </div>\n          </NestedMenuItem>\n        ) : (\n          <></>\n        )}\n\n        {props.interactivity.enableEditing && expandable ? (\n          <NestedMenuItem label='Create relationship...' nonce={undefined} parentMenuOpen={true}>\n            <div style={{ maxHeight: '400px', overflowY: 'auto', overflowX: 'hidden' }}>\n              <table>\n                <tbody>\n                  {props.data &&\n                    props.data.nodes.map((node, index) => (\n                      <tr key={`ctxMenuItemTr2-${index}`}>\n                        <MenuItem\n                          key={`ctxMenuItem2-${index}`}\n                          onClick={() => {\n                            setEditableEntityType(EditType.Relationship);\n                            setAction(EditAction.Create);\n                            setEditableEntity(node);\n                            props.interactivity.setContextMenuOpen(false);\n                            setDialogOpen(true);\n                          }}\n                        >\n                          <td style={{ width: '150px', overflow: 'hidden' }}>{RenderNode(node, false)}</td>\n                          <td style={{ width: 'auto', marginLeft: '15px' }}>\n                            {props.engine.selection[node.mainLabel] ? getNodeLabel(props.engine.selection, node) : ''}\n                          </td>\n                        </MenuItem>\n                      </tr>\n                    ))}\n                </tbody>\n              </table>\n            </div>\n          </NestedMenuItem>\n        ) : (\n          <></>\n        )}\n      </Card>\n    </div>\n  );\n\n  return (\n    <>\n      <GraphChartEditModal type={editableEntityType} action={action} {...dialogProps} />\n      {props.interactivity.contextMenuOpen ? createPortal(menu, document.body) : <></>}\n    </>\n  );\n};\n"
  },
  {
    "path": "src/chart/graph/component/GraphChartEditModal.tsx",
    "content": "import { GraphChartVisualizationProps } from '../GraphChartVisualization';\nimport React, { useEffect } from 'react';\nimport { Dialog, DialogContent, DialogContentText, DialogTitle } from '@mui/material';\nimport { Button } from '@mui/material';\nimport { TextField, Typography } from '@mui/material';\n\nimport { PlusIconOutline, XMarkIconOutline, PlayIconOutline } from '@neo4j-ndl/react/icons';\nimport { IconButton } from '@neo4j-ndl/react';\nimport { LabelTypeAutocomplete } from './autocomplete/LabelTypeAutocomplete';\nimport { DeletePropertyButton } from './button/modal/DeletePropertyButton';\nimport {\n  handleNodeCreate,\n  handleNodeDelete,\n  handleNodeEdit,\n  handleRelationshipCreate,\n  handleRelationshipDelete,\n  handleRelationshipEdit,\n} from '../util/EditUtils';\nimport { PropertyNameAutocomplete } from './autocomplete/PropertyNameAutocomplete';\n\nexport enum EditType {\n  Node = 0,\n  Relationship = 1,\n}\n\nexport enum EditAction {\n  Create = 0,\n  Edit = 1,\n  Delete = 2,\n}\n\ninterface GraphChartEditorVisualizationProps extends GraphChartVisualizationProps {\n  type: EditType;\n  action: EditAction;\n  selectedNode: any;\n  dialogOpen: any;\n  setDialogOpen: any;\n}\n\n/**\n * The edit modal is a pop-up window that lets users change a node or relationship in the graph.\n * This is a generic component, that can be used for creating/editing/deleting either nodes or relationships, and their properties.\n */\nexport const GraphChartEditModal = (props: GraphChartEditorVisualizationProps) => {\n  const [properties, setProperties] = React.useState([{ name: '', value: '' }]);\n  const [labelRecords, setLabelRecords] = React.useState([]);\n  const [labelInputText, setLabelInputText] = React.useState('');\n  const [propertyRecords, setPropertyRecords] = React.useState([]);\n  const [propertyInputTexts, setPropertyInputTexts] = React.useState([]);\n  const [label, setLabel] = React.useState(undefined);\n\n  // When the dialog gets opened, and we are editing, prepopulate the fields with current node/rel data in the database.\n  useEffect(() => {\n    if (\n      props.dialogOpen &&\n      props.interactivity.selectedEntity &&\n      props.type == EditType.Node &&\n      props.action == EditAction.Edit\n    ) {\n      const label = props.interactivity.selectedEntity.labels ? props.interactivity.selectedEntity.labels[0] : '';\n      setLabelInputText(label);\n      setLabel(label);\n      const selectedProps = Object.keys(props.interactivity.selectedEntity.properties).map((prop) => {\n        return { name: prop, value: props.interactivity.selectedEntity.properties[prop] };\n      });\n      setProperties(selectedProps);\n      setPropertyInputTexts(selectedProps.map((p) => p.name));\n    } else if (\n      props.dialogOpen &&\n      props.interactivity.selectedEntity &&\n      props.type == EditType.Relationship &&\n      props.action == EditAction.Edit\n    ) {\n      const { type } = props.interactivity.selectedEntity;\n      setLabelInputText(type);\n      setLabel(type);\n      const selectedProps = Object.keys(props.interactivity.selectedEntity.properties).map((prop) => {\n        return { name: prop, value: props.interactivity.selectedEntity.properties[prop] };\n      });\n      setProperties(selectedProps);\n      setPropertyInputTexts(selectedProps.map((p) => p.name));\n    } else if (props.dialogOpen) {\n      setLabelInputText('');\n      setLabel('');\n    }\n  }, [props.dialogOpen]);\n\n  return (\n    <Dialog\n      maxWidth={'lg'}\n      open={props.dialogOpen}\n      onClose={() => {\n        props.setDialogOpen(false);\n      }}\n      aria-labelledby='form-dialog-title'\n    >\n      <DialogTitle id='form-dialog-title'>\n        {props.action == EditAction.Create ? 'Create' : 'Edit'} a{' '}\n        {props.type == EditType.Node ? 'Node' : 'Relationship'}\n        <IconButton\n          onClick={() => {\n            props.interactivity.setContextMenuOpen(false);\n            props.setDialogOpen(false);\n            setProperties([{ name: '', value: '' }]);\n          }}\n          style={{ marginLeft: '40px', padding: '3px', float: 'right' }}\n          clean\n        >\n          <XMarkIconOutline />\n        </IconButton>\n      </DialogTitle>\n\n      <DialogContent style={{ minWidth: '300px' }}>\n        <DialogContentText>\n          <LabelTypeAutocomplete\n            records={labelRecords}\n            setRecords={setLabelRecords}\n            value={label}\n            setValue={setLabel}\n            queryCallback={props.engine.queryCallback}\n            type={props.type}\n            input={labelInputText}\n            setInput={setLabelInputText}\n            disabled={props.type == EditType.Relationship && props.action == EditAction.Edit}\n          />\n          <h4>Properties</h4>\n\n          <table>\n            <tbody>\n              {properties.map((property, index) => {\n                const disabled = !(\n                  typeof property.value == 'string' ||\n                  typeof property.value == 'number' ||\n                  property.value.toNumber !== undefined\n                );\n\n                return (\n                  <tr key={`trEditProp${index}`} style={{ height: 40 }}>\n                    <td style={{ paddingLeft: '2px', paddingRight: '2px' }}>\n                      <span style={{ color: 'black', width: '50px' }}>{index + 1}.</span>\n                    </td>\n                    <td style={{ paddingLeft: '5px', paddingRight: '5px' }}>\n                      <PropertyNameAutocomplete\n                        records={propertyRecords}\n                        setRecords={setPropertyRecords}\n                        values={properties}\n                        setValues={setProperties}\n                        queryCallback={props.engine.queryCallback}\n                        index={index}\n                        inputs={propertyInputTexts}\n                        setInputs={setPropertyInputTexts}\n                        disabled={disabled}\n                      />\n                    </td>\n                    <td style={{ paddingLeft: '5px', paddingRight: '5px' }}>\n                      <TextField\n                        key={`txtFieldEditProp${index}`}\n                        style={{ width: '100%' }}\n                        placeholder='Value...'\n                        disabled={disabled}\n                        value={property.value}\n                        onChange={(e) => {\n                          const newProperties = [...properties];\n                          newProperties[index].value = e.target.value;\n                          setProperties(newProperties);\n                        }}\n                      ></TextField>\n                    </td>\n\n                    <td>\n                      <DeletePropertyButton\n                        key={`deletePropBtn${index}`}\n                        onClick={() => {\n                          setProperties([...properties.slice(0, index), ...properties.slice(index + 1)]);\n                          setPropertyInputTexts([\n                            ...propertyInputTexts.slice(0, index),\n                            ...propertyInputTexts.slice(index + 1),\n                          ]);\n                        }}\n                      />\n                    </td>\n                  </tr>\n                );\n              })}\n\n              <tr key={'trEditButtons'}>\n                <td style={{ minWidth: '450px' }} colSpan={4}>\n                  <Typography variant='h3' color='primary' style={{ textAlign: 'center', marginBottom: '5px' }}>\n                    <IconButton\n                      key={'btnAddProp'}\n                      size='small'\n                      aria-label='add'\n                      style={{ background: 'white', color: 'black' }}\n                      onClick={() => {\n                        const newProperty = { name: '', value: '' };\n                        setProperties(properties.concat(newProperty));\n                      }}\n                    >\n                      <PlusIconOutline />\n                    </IconButton>\n                  </Typography>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n          <Button\n            key={'btnEditProp'}\n            style={{ marginBottom: '10px' }}\n            disabled={label === undefined || label == '' || labelInputText !== label}\n            onClick={() => {\n              const newProperties = {};\n\n              properties.map((prop) => {\n                if (prop.name !== '' && prop.value !== '') {\n                  newProperties[prop.name] = prop.value;\n                }\n              });\n\n              if (props.action == EditAction.Create && props.type == EditType.Node) {\n                handleNodeCreate();\n              } else if (props.action == EditAction.Create && props.type == EditType.Relationship) {\n                handleRelationshipCreate(\n                  props.interactivity.selectedEntity,\n                  label,\n                  newProperties,\n                  props.selectedNode,\n                  props.engine,\n                  props.interactivity,\n                  props.data\n                );\n              } else if (props.action == EditAction.Edit && props.type == EditType.Node) {\n                const labels = label.split(',').map((l) => l.trim());\n                handleNodeEdit(props.interactivity.selectedEntity, labels, newProperties, props);\n              } else if (props.action == EditAction.Edit && props.type == EditType.Relationship) {\n                handleRelationshipEdit(props.interactivity.selectedEntity, newProperties, props);\n              } else if (props.action == EditAction.Delete && props.type == EditType.Node) {\n                handleNodeDelete();\n              } else if (props.action == EditAction.Delete && props.type == EditType.Relationship) {\n                handleRelationshipDelete();\n              }\n              props.setDialogOpen(false);\n              setProperties([{ name: '', value: '' }]);\n            }}\n            style={{ float: 'right', marginBottom: 15 }}\n            variant='contained'\n            size='medium'\n            endIcon={<PlayIconOutline className='btn-icon-base-r' />}\n          >\n            {props.action == EditAction.Create ? 'Create' : 'Save'}\n          </Button>\n        </DialogContentText>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "src/chart/graph/component/GraphChartInspectModal.tsx",
    "content": "import React from 'react';\nimport { GraphChartVisualizationProps } from '../GraphChartVisualization';\nimport { getEntityHeader } from '../util/NodeUtils';\nimport { Dialog } from '@neo4j-ndl/react';\nimport GraphEntityInspectionTable from './GraphEntityInspectionTable';\n\n/**\n * Renders a pop-up window to inspect a node/relationship properties in a read-only table.\n */\nexport const NeoGraphChartInspectModal = (props: GraphChartVisualizationProps) => {\n  return (\n    <div>\n      <Dialog\n        size='large'\n        open={props.interactivity.showPropertyInspector}\n        onClose={() => props.interactivity.setPropertyInspectorOpen(false)}\n        aria-labelledby='form-dialog-title'\n      >\n        <Dialog.Header id='form-dialog-title'>\n          {props.interactivity.selectedEntity\n            ? getEntityHeader(props.interactivity.selectedEntity, props?.engine?.selection)\n            : ''}\n        </Dialog.Header>\n        <Dialog.Content>\n          <GraphEntityInspectionTable\n            entity={props.interactivity.selectedEntity}\n            theme={props.style.theme}\n          ></GraphEntityInspectionTable>\n        </Dialog.Content>\n      </Dialog>\n    </div>\n  );\n};\n\nexport default NeoGraphChartInspectModal;\n"
  },
  {
    "path": "src/chart/graph/component/GraphChartTooltip.tsx",
    "content": "import React from 'react';\nimport { Table, TableBody, TableCell, TableContainer, TableRow, Card } from '@mui/material';\nimport ReactDOMServer from 'react-dom/server';\nimport { GraphEntity, Link } from '../GraphChartVisualization';\nimport { Node } from '../GraphChartVisualization';\n\n/**\n * Renders a tooltip above the user's cursor showing information on the selected node/relationship.\n */\nexport function getTooltip(entity: Node | Link) {\n  const tooltip = (\n    <Card>\n      <b style={{ padding: '10px' }}>\n        {entity.labels ? (entity.labels.length > 0 ? entity.labels.join(', ') : 'Node') : entity.type}\n      </b>\n      {Object.keys(entity.properties).length == 0 ? (\n        <i>\n          <br />\n          (No properties)\n        </i>\n      ) : (\n        <TableContainer>\n          <Table size='small'>\n            <TableBody>\n              {Object.keys(entity.properties)\n                .sort()\n                .map((key) => (\n                  <TableRow key={key}>\n                    <TableCell component='th' scope='row' style={{ padding: '3px', paddingLeft: '8px' }}>\n                      {key}\n                    </TableCell>\n                    <TableCell align={'left'} style={{ padding: '3px', paddingLeft: '8px' }}>\n                      {entity.properties[key].toString().length <= 30\n                        ? entity.properties[key].toString()\n                        : `${entity.properties[key].toString().substring(0, 40)}...`}\n                    </TableCell>\n                  </TableRow>\n                ))}\n            </TableBody>\n          </Table>\n        </TableContainer>\n      )}\n    </Card>\n  );\n  return ReactDOMServer.renderToString(tooltip);\n}\n"
  },
  {
    "path": "src/chart/graph/component/GraphEntityInspectionTable.tsx",
    "content": "import React from 'react';\nimport ShowMoreText from 'react-show-more-text';\nimport { Checkbox, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';\nimport { TextLink } from '@neo4j-ndl/react';\n// import DOMPurify from 'dompurify'; \n\nexport const formatProperty = (property) => {\n  const str = property?.toString() || '';\n  if (str.startsWith('http://') || str.startsWith('https://')) {\n    return (\n      <TextLink externalLink href={str}>\n        {str}\n      </TextLink>\n    );\n  }\n  return str; \n};\n\n/**\n * Component to render node/relationship properties in a table format\n */\nexport const GraphEntityInspectionTable = ({\n  entity,\n  theme,\n  setSelectedParameters = (_value) => {\n    console.log('undefined function in GraphEntityInspectionTable');\n  },\n  checklistEnabled = false,\n}) => {\n  const [checkedParameters, setCheckedParameters] = React.useState<string[]>([]);\n  const hasPropertyToShow = Object.keys(entity.properties).length > 0;\n  if (!entity) {\n    return <></>;\n  }\n\n  /**\n   * Function to manage the click\n   * @param parameter\n   * @param checked\n   */\n  function handleCheckboxClick(parameter, checked) {\n    let newCheckedParameters = [...checkedParameters];\n    if (checked) {\n      newCheckedParameters.push(parameter);\n    } else {\n      const index = newCheckedParameters.indexOf(parameter);\n      if (index > -1) {\n        newCheckedParameters.splice(index, 1);\n      }\n    }\n    if (setSelectedParameters) {\n      setCheckedParameters(newCheckedParameters);\n      setSelectedParameters(newCheckedParameters);\n    }\n  }\n\n  const tableTextColor = theme === 'dark' ? 'var(--palette-dark-neutral-border-strong)' : 'rgba(0, 0, 0, 0.6)';\n\n  return (\n    <TableContainer>\n      <Table size='small'>\n        {hasPropertyToShow ? (\n          <TableHead>\n            <TableRow>\n              <TableCell align='left' style={{ color: tableTextColor }}>\n                Property\n              </TableCell>\n              <TableCell align='left' style={{ color: tableTextColor }}>\n                Value\n              </TableCell>\n              {checklistEnabled ? <TableCell align='center'>Select Property</TableCell> : <></>}\n            </TableRow>\n          </TableHead>\n        ) : (\n          <></>\n        )}\n        <TableBody>\n          {!hasPropertyToShow ? (\n            <TableRow key='empty-row'>\n              <TableCell component='th' scope='row'>\n                (No properties)\n              </TableCell>\n            </TableRow>\n          ) : (\n            Object.keys(entity.properties)\n              .sort()\n              .map((key) => (\n                <TableRow key={key}>\n                  <TableCell component='th' scope='row' style={{ color: tableTextColor }}>\n                    {key}\n                  </TableCell>\n                  <TableCell align={'left'} style={{ color: tableTextColor }}>\n                    <ShowMoreText lines={2}>{formatProperty(entity?.properties[key])}</ShowMoreText> \n                  </TableCell>\n                  {checklistEnabled ? (\n                    <TableCell align={'center'}>\n                      <Checkbox\n                        color='default'\n                        onChange={(event) => {\n                          handleCheckboxClick(key, event.target.checked);\n                        }}\n                      />\n                    </TableCell>\n                  ) : (\n                    <></>\n                  )}\n                </TableRow>\n              ))\n          )}\n        </TableBody>\n      </Table>\n    </TableContainer>\n  );\n};\n\nexport default GraphEntityInspectionTable;\n"
  },
  {
    "path": "src/chart/graph/component/autocomplete/LabelTypeAutocomplete.tsx",
    "content": "import React from 'react';\nimport Autocomplete from '@mui/material/Autocomplete';\nimport { TextField } from '@mui/material';\nimport { EditType } from '../GraphChartEditModal';\n\n/**\n * Renders an auto-complete text field that uses either:\n * - The labels from the active Neo4j database.\n * - The relationship types from the active Neo4j database.\n * TODO - check that the same database is used that the component has selected.\n */\nexport const LabelTypeAutocomplete = ({\n  type,\n  disabled,\n  input,\n  setInput,\n  value,\n  setValue,\n  records,\n  setRecords,\n  queryCallback,\n}) => {\n  return (\n    <Autocomplete\n      id='autocomplete-label-type'\n      disabled={disabled}\n      options={records.map((r) => (r._fields ? r._fields[0] : '(no data)'))}\n      getOptionLabel={(option) => option || ''}\n      style={{ width: '100%', marginLeft: '5px', marginTop: '5px' }}\n      inputValue={input}\n      onInputChange={(event, value) => {\n        setInput(value);\n        if (type == EditType.Node) {\n          queryCallback(\n            'CALL db.labels() YIELD label WITH label as nodeLabel WHERE toLower(nodeLabel) CONTAINS toLower($input) RETURN DISTINCT nodeLabel LIMIT 5',\n            { input: value },\n            setRecords\n          );\n        } else {\n          queryCallback(\n            'CALL db.relationshipTypes() YIELD relationshipType WITH relationshipType as relType WHERE toLower(relType) CONTAINS toLower($input) RETURN DISTINCT relType LIMIT 5',\n            { input: value },\n            setRecords\n          );\n        }\n      }}\n      value={value}\n      onChange={(event, newValue) => setValue(newValue)}\n      renderInput={(params) => (\n        <TextField\n          {...params}\n          placeholder='Start typing...'\n          InputLabelProps={{ shrink: true }}\n          label={type == EditType.Relationship ? 'Type' : 'Label'}\n        />\n      )}\n    />\n  );\n};\n"
  },
  {
    "path": "src/chart/graph/component/autocomplete/PropertyNameAutocomplete.tsx",
    "content": "import React from 'react';\nimport Autocomplete from '@mui/material/Autocomplete';\nimport { TextField } from '@mui/material';\n\n/**\n * Renders an auto-complete text field for property identifiers.\n * TODO - check that the same database is used that the component has selected.\n */\nexport const PropertyNameAutocomplete = ({\n  disabled,\n  index,\n  inputs,\n  setInputs,\n  values,\n  setValues,\n  records,\n  setRecords,\n  queryCallback,\n}) => {\n  return (\n    <Autocomplete\n      id='autocomplete-property'\n      disabled={disabled}\n      options={records.map((r) => (r._fields ? r._fields[0] : '(no data)'))}\n      getOptionLabel={(option) => option || ''}\n      style={{ display: 'inline-block', width: 170, marginLeft: '5px', marginTop: '0px' }}\n      inputValue={inputs[index]}\n      onInputChange={(event, value) => {\n        const newPropertyInputTexts = [...inputs];\n        newPropertyInputTexts[index] = value;\n        setInputs(newPropertyInputTexts);\n        queryCallback(\n          'CALL db.propertyKeys() YIELD propertyKey as propertyName WITH propertyName WHERE toLower(propertyName) CONTAINS toLower($input) RETURN DISTINCT propertyName LIMIT 5',\n          { input: value },\n          setRecords\n        );\n      }}\n      value={values[index].name}\n      onChange={(e, val) => {\n        const newProperties = [...values];\n        newProperties[index].name = val;\n        setValues(newProperties);\n      }}\n      renderInput={(params) => <TextField {...params} placeholder='Name...' InputLabelProps={{ shrink: true }} />}\n    />\n  );\n};\n"
  },
  {
    "path": "src/chart/graph/component/button/GraphChartDeepLinkButton.tsx",
    "content": "import React from 'react';\nimport { Tooltip } from '@mui/material';\nimport { replaceDashboardParametersInString } from '../../../ChartUtils';\nimport { GraphChartVisualizationProps } from '../../GraphChartVisualization';\nimport { IconButton } from '@neo4j-ndl/react';\nimport { MagnifyingGlassIconOutline } from '@neo4j-ndl/react/icons';\n\n/**\n * If a deep-link URL is specified in the advanced settings, renders an icon at the top right of the graph visualization that redirects to the link.\n */\nexport const NeoGraphChartDeepLinkButton = (props: GraphChartVisualizationProps) => {\n  return (\n    <IconButton\n      aria-label='investigate graph'\n      size='small'\n      clean\n      grouped\n      href={replaceDashboardParametersInString(props.interactivity.drilldownLink, props.data.parameters)}\n      target='_blank'\n    >\n      <Tooltip title='Investigate' disableInteractive>\n        <MagnifyingGlassIconOutline />\n      </Tooltip>\n    </IconButton>\n  );\n};\n"
  },
  {
    "path": "src/chart/graph/component/button/GraphChartFitViewButton.tsx",
    "content": "import React from 'react';\nimport { Tooltip } from '@mui/material';\nimport { GraphChartVisualizationProps } from '../../GraphChartVisualization';\nimport { IconButton } from '@neo4j-ndl/react';\nimport { FitToScreenIcon } from '@neo4j-ndl/react/icons';\n\n/**\n * Renders an icon on the bottom-right of the graph visualization to fit the current graph to the user's view.\n */\nexport const NeoGraphChartFitViewButton = (props: GraphChartVisualizationProps) => {\n  return (\n    <Tooltip title='Fit graph to view.' aria-label={'fit to screen'} disableInteractive>\n      <IconButton\n        aria-label='fit graph to view'\n        size='small'\n        onClick={() => {\n          props.interactivity.zoomToFit();\n        }}\n        clean\n        grouped\n      >\n        <FitToScreenIcon\n          onClick={() => {\n            props.interactivity.zoomToFit();\n          }}\n        />\n      </IconButton>\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "src/chart/graph/component/button/GraphChartLockButton.tsx",
    "content": "import React from 'react';\nimport { GraphChartVisualizationProps } from '../../GraphChartVisualization';\nimport { Tooltip } from '@mui/material';\nimport { IconButton } from '@neo4j-ndl/react';\nimport { LockOpenIconSolid, LockClosedIconSolid } from '@neo4j-ndl/react/icons';\n\n/**\n * Renders a button that can be used to 'lock' = freeze the current graph layout by disabling the force layout.\n */\nexport const NeoGraphChartLockButton = (props: GraphChartVisualizationProps) => {\n  return (\n    <IconButton aria-label='Lock graph layout icon' size='small' clean grouped>\n      {props.interactivity.layoutFrozen ? (\n        <Tooltip title='Toggle dynamic graph layout.' aria-label='unlock graph layout' disableInteractive>\n          <LockClosedIconSolid\n            onClick={() => {\n              props.interactivity.setLayoutFrozen(false);\n            }}\n          />\n        </Tooltip>\n      ) : (\n        <Tooltip title='Toggle fixed graph layout.' aria-label='lock graph layout' disableInteractive>\n          <LockOpenIconSolid\n            onClick={() => {\n              if (props.interactivity.nodePositions == undefined) {\n                props.interactivity.nodePositions = {};\n              }\n              props.interactivity.setLayoutFrozen(true);\n            }}\n          />\n        </Tooltip>\n      )}\n    </IconButton>\n  );\n};\n"
  },
  {
    "path": "src/chart/graph/component/button/modal/DeletePropertyButton.tsx",
    "content": "import React from 'react';\nimport { XMarkIconOutline } from '@neo4j-ndl/react/icons';\nimport { IconButton } from '@neo4j-ndl/react';\n\n/**\n * Returns a button to delete a property entry from the table inside the GraphChartEditModal.\n */\nexport const DeletePropertyButton = ({ onClick, key }) => {\n  return (\n    <IconButton\n      key={key}\n      size='medium'\n      aria-label='remove'\n      style={{\n        background: 'white',\n        color: 'grey',\n        marginTop: '-6px',\n        marginLeft: '20px',\n        width: '26px',\n        height: '26px',\n        minHeight: '26px',\n      }}\n      onClick={onClick}\n      clean\n    >\n      <XMarkIconOutline key={`icon${key}`} />\n    </IconButton>\n  );\n};\n"
  },
  {
    "path": "src/chart/graph/util/EditUtils.ts",
    "content": "import { GraphChartVisualizationProps, Link, Node } from '../GraphChartVisualization';\nimport { injectNewRecordsIntoGraphVisualization } from './RecordUtils';\nimport { recomputeCurvatures } from './RelUtils';\n\nexport const handleNodeCreate = () => {\n  throw 'Not Implemented';\n};\n\nexport const handleNodeEdit = (\n  node: Node,\n  labels: string[],\n  properties: Record<string, any>,\n  props: GraphChartVisualizationProps\n) => {\n  // Cast properties to numbers if they are castable as such...\n  Object.keys(properties).forEach((key) => {\n    const value = properties[key];\n    if (!Number.isNaN(parseFloat(value))) {\n      properties[key] = parseFloat(value);\n    }\n  });\n\n  const oldLabels = node.labels.join(':');\n  const newLabels = labels.join(':');\n\n  props.engine.queryCallback(\n    `MATCH (n) WHERE id(n) = $id REMOVE n:${oldLabels} SET n:${newLabels} SET n = $properties RETURN n`,\n    {\n      id: node.id,\n      properties: properties,\n    },\n    (records) => {\n      if (records && records[0] && records[0].error) {\n        props.interactivity.createNotification('Error', records[0].error);\n        return;\n      }\n      // const updatedNode = records[0]._fields[0];\n      const { nodes, links, nodesMap, linksMap } = injectNewRecordsIntoGraphVisualization(records, props);\n      const newNodes = [...props.data.nodes];\n\n      // Iterate over the old nodes, and override the nodes object if it was changed.\n      newNodes.forEach((n, i) => {\n        nodes\n          .filter((x) => x.id == n.id)\n          .forEach((match) => {\n            newNodes[i].labels = match.labels;\n            newNodes[i].mainLabel = match.mainLabel;\n            newNodes[i].color = match.color;\n            newNodes[i].size = match.size;\n            newNodes[i].properties = match.properties;\n          });\n      });\n      props.data.setNodes(newNodes);\n      props.interactivity.createNotification('Node Updated', 'The node details were updated successfully.');\n    }\n  );\n};\n\nexport const handleNodeDelete = () => {\n  throw 'Not Implemented';\n};\n\nexport const handleRelationshipCreate = (\n  start: Node,\n  type: string,\n  properties: Record<string, any>,\n  end: Node,\n  engine,\n  interactivity,\n  data\n) => {\n  engine.queryCallback(\n    `MATCH (n), (m) WHERE id(n) = $start AND id(m) = $end CREATE (n)-[r:${type}]->(m) SET r = $properties RETURN r`,\n    {\n      start: start.id,\n      type: type,\n      properties: properties,\n      end: end.id,\n    },\n    (records) => {\n      if (records && records[0] && records[0].error) {\n        interactivity.createNotification('Error', records[0].error);\n        return;\n      }\n\n      const id = records[0]._fields[0].identity;\n\n      // Clean up properties for displaying in the visualization. This has to do with the visualization using 'name' as an override label.\n      Object.keys(properties).map((prop) => {\n        if (prop == 'name') {\n          properties[' name'] = properties[prop];\n          delete properties[prop];\n        }\n      });\n\n      const { links } = data;\n      links.push({\n        id: id,\n        width: 2,\n        color: 'grey',\n        type: type,\n        new: true,\n        properties: properties,\n        source: start,\n        target: end,\n      });\n\n      // Recompute curvature for all links, because a new link was added.\n      data.setLinks(recomputeCurvatures(links));\n      interactivity.createNotification('Relationship Created', 'The new relationship was added successfully.');\n      interactivity.setContextMenuOpen(false);\n    }\n  );\n};\n\nexport const handleRelationshipEdit = (\n  link: Link,\n  properties: Record<string, any>,\n  props: GraphChartVisualizationProps\n) => {\n  // Cast properties to numbers if they are castable as such...\n  Object.keys(properties).forEach((key) => {\n    const value = properties[key];\n    if (!Number.isNaN(parseFloat(value))) {\n      properties[key] = parseFloat(value);\n    }\n  });\n\n  props.engine.queryCallback(\n    `MATCH ()-[r]->()  WHERE id(r) = $id SET r = $properties RETURN r`,\n    {\n      id: link.id,\n      properties: properties,\n    },\n    (records) => {\n      if (records && records[0] && records[0].error) {\n        props.interactivity.createNotification('Error', records[0].error);\n        return;\n      }\n\n      const { nodes, links, nodesMap, linksMap } = injectNewRecordsIntoGraphVisualization(records, props);\n      const newLinks = [...props.data.links];\n      // Iterate over the old links, and override the links object if it was changed.\n      newLinks.forEach((n, i) => {\n        links\n          .filter((x) => x.id == n.id)\n          .forEach((match) => {\n            newLinks[i].color = match.color;\n            newLinks[i].width = match.width;\n            newLinks[i].properties = match.properties;\n          });\n      });\n\n      props.data.setLinks(newLinks);\n      props.interactivity.createNotification(\n        'Relationship Updated',\n        'The relationship details were updated successfully.'\n      );\n    }\n  );\n};\n\nexport const handleRelationshipDelete = () => {\n  throw 'Not Implemented';\n};\n"
  },
  {
    "path": "src/chart/graph/util/ExplorationUtils.ts",
    "content": "import { GraphChartVisualizationProps } from '../GraphChartVisualization';\nimport { injectNewRecordsIntoGraphVisualization } from './RecordUtils';\nimport { recomputeCurvatures } from './RelUtils';\n\nexport const getNodeRelationshipCountsQuery = `MATCH (b)\nWHERE id(b) = $id\nWITH b, apoc.node.relationship.types(b) as types\nUNWIND types as type\nWITH type, apoc.node.degree.in(b,type) as in, apoc.node.degree.out(b,type) AS out\nUNWIND [\"in\", \"out\",\"any\"] as direction\nWITH type, direction, in, out\nWHERE (in <> 0 AND direction = \"in\") OR (out <> 0 AND direction = \"out\") OR direction=\"any\"\nRETURN type, direction, \n    CASE WHEN direction = \"in\" THEN in \n         WHEN direction = \"out\" THEN out\n        ELSE in+out END as value\nORDER BY type, direction\n`;\n\nexport const getNodeRelationshipCountsQueryWithoutApoc = `\nMATCH (b)\nWHERE id(b) = $id\nMATCH (b)-[r]-()\nWITH type(r) as type, CASE WHEN startNode(r) = b THEN \"out\" ELSE \"in\" END as dir, COUNT(*) as value\nUNWIND [\"in\", \"out\",\"any\"] as direction\nWITH *\nWHERE (direction = dir) OR direction=\"any\"\nRETURN type, direction, sum(value) as value\nORDER BY type, direction\n`;\n\nexport const handleGetNodeRelTypes = (id: number, engine: any, callback: any) => {\n  engine.queryCallback(getNodeRelationshipCountsQuery, { id: id }, (records) => {\n    if (records && records[0] && records[0].error) {\n      handleGetNodeRelTypesWithoutApoc(id, engine, callback);\n    } else {\n      callback(records);\n    }\n  });\n};\n\nconst handleGetNodeRelTypesWithoutApoc = (id: number, engine: any, callback: any) => {\n  engine.queryCallback(getNodeRelationshipCountsQueryWithoutApoc, { id: id }, (records) => {\n    callback(records);\n  });\n};\n\nexport const handleExpand = (id: number, type: string, dir: string, props: GraphChartVisualizationProps) => {\n  const query = `\n    MATCH (n)\n    WHERE id(n) = $id \n    MATCH (n)${dir == 'in' ? '<' : ''}-[r${type !== '...' ? `:\\`${type}\\`` : ''}]-${dir == 'out' ? '>' : ''}(m)\n    RETURN n, r, m\n    `;\n\n  props.engine.queryCallback(query, { id: id }, (records) => {\n    if (records && records[0] && records[0].error) {\n      props.interactivity.createNotification('Error', records[0].error);\n      return;\n    }\n    const { nodes, links, nodesMap, linksMap } = injectNewRecordsIntoGraphVisualization(records, props);\n\n    const newNodes = [...props.data.nodes];\n    nodes.forEach((n) => {\n      if (nodesMap[n.id] === undefined) {\n        nodesMap[n.id] = n; // do not double push\n        newNodes.push(n);\n      }\n    });\n\n    const newLinks = [...props.data.links];\n    links.forEach((n) => {\n      if (linksMap[n.id] === undefined) {\n        if (n.target.id === undefined) {\n          n.target = nodesMap[n.target];\n        }\n        if (n.source.id === undefined) {\n          n.source = nodesMap[n.source];\n        }\n        linksMap[n.id] = n; // do not double push\n        newLinks.push(n);\n      }\n    });\n    props.data.setGraph(newNodes, recomputeCurvatures(newLinks));\n    props.engine.setCooldownTicks(50);\n  });\n};\n\n// Combines the database statistic on relationship frequencies with those in the current view.\nexport const mergeDatabaseStatCountsWithCountsInView = (id, stats, links) => {\n  const directions = ['out', 'in', 'any'];\n  const mergedRelCounts = {};\n  directions.map((d) => {\n    mergedRelCounts[`...` + `___${d}`] = 0;\n  });\n  stats.forEach((item) => {\n    const entry = `${item._fields[0]}___${item._fields[1]}`;\n    if (mergedRelCounts[entry] === undefined) {\n      mergedRelCounts[entry] = 0;\n    }\n    mergedRelCounts[entry] += parseInt(item._fields[2]);\n    mergedRelCounts[`...` + `___${item._fields[1]}`] += parseInt(item._fields[2]);\n  });\n  // Subtract if we find links in the view that are already visible...\n  links.forEach((item) => {\n    if (item.source.id == id) {\n      mergedRelCounts[`${item.type}___` + `out`] -= 1;\n      mergedRelCounts[`${item.type}___` + `any`] -= 1;\n      mergedRelCounts['...' + '___' + 'out'] -= 1;\n      mergedRelCounts['...' + '___' + 'any'] -= 1;\n    }\n    if (item.target.id == id) {\n      mergedRelCounts[`${item.type}___` + `in`] -= 1;\n      mergedRelCounts[`${item.type}___` + `any`] -= 1;\n      mergedRelCounts['...' + '___' + 'in'] -= 1;\n      mergedRelCounts['...' + '___' + 'any'] -= 1;\n    }\n  });\n  const mergedCountsList = Object.keys(mergedRelCounts).map((key) => {\n    const [type, direction] = key.split('___');\n    const value = mergedRelCounts[key];\n    if (value !== 0) {\n      return [type, direction, value];\n    }\n    return undefined;\n  });\n  return mergedCountsList.filter((v) => v !== undefined);\n};\n"
  },
  {
    "path": "src/chart/graph/util/NodeUtils.ts",
    "content": "import { evaluateRulesOnNode } from '../../../extensions/styling/StyleRuleEvaluator';\nimport { GraphEntity } from '../GraphChartVisualization';\n\nexport const getNodeLabel = (selection, node) => {\n  const selectedProp = selection && selection[node.mainLabel];\n  if (selectedProp == '(id)') {\n    return node.id;\n  }\n  if (selectedProp == '(label)') {\n    return node.labels;\n  }\n  if (selectedProp == '(no label)') {\n    return '';\n  }\n  return node.properties[selectedProp] ? node.properties[selectedProp] : '';\n};\n\nexport const parseNodeIconConfig = (iconStyle) => {\n  try {\n    return iconStyle ? JSON.parse(iconStyle) : undefined;\n  } catch (error) {\n    // Unable to parse node icon definition as specified by the user.\n    console.log(error);\n  }\n};\n\nconst getSelectedNodeProperty = (entity: any, sourceOrTarget: string, propertySelections: any) => {\n  const selection = propertySelections[entity[sourceOrTarget]?.mainLabel];\n  switch (selection) {\n    case '(label)':\n      return entity[sourceOrTarget]?.mainLabel;\n    case '(id)':\n      return entity[sourceOrTarget]?.id;\n    default:\n      return entity[sourceOrTarget]?.properties[selection];\n  }\n};\n\nconst getRelPatternString = (entity: any, selection: any) => {\n  const sourceTitle = getSelectedNodeProperty(entity, 'source', selection);\n  const targetTitle = getSelectedNodeProperty(entity, 'target', selection);\n  return `(${sourceTitle ? sourceTitle : '[no value]'} --> ${targetTitle ? targetTitle : '[no value]'})`;\n};\n\nexport const getEntityHeader = (entity: any, selection: any) => {\n  return entity.labels?.join(', ') || `${entity.type} ${getRelPatternString(entity, selection)}`;\n};\n\nexport const drawDataURIOnCanvas = (node, strDataURI, canvas, defaultNodeSize) => {\n  let img = new Image();\n  let prop = defaultNodeSize * 6;\n  img.src = strDataURI;\n  canvas.drawImage(img, node.x - prop / 2, node.y - prop / 2, prop, prop);\n};\n\nexport const generateNodeCanvasObject = (\n  node: GraphEntity,\n  ctx: any,\n  icons: any,\n  frozen: boolean,\n  nodePositions: Record<string, any>,\n  nodeLabelFontSize: number,\n  defaultNodeSize: any,\n  nodeLabelColor: any,\n  styleRules: any,\n  selection: any\n) => {\n  if (icons && icons[node.mainLabel]) {\n    drawDataURIOnCanvas(node, icons[node.mainLabel], ctx, defaultNodeSize);\n  } else {\n    const label = selection && selection[node.mainLabel] ? getNodeLabel(selection, node) : '';\n    const fontSize = nodeLabelFontSize;\n    ctx.font = `${fontSize}px Sans-Serif`;\n    ctx.fillStyle = evaluateRulesOnNode(node, 'node label color', nodeLabelColor, styleRules);\n    ctx.textAlign = 'center';\n    ctx.fillText(label, node.x ? node.x : 0, node.y ? node.y + 1 : 0);\n    if (frozen && !node.fx && !node.fy && nodePositions) {\n      node.fx = node.x;\n      node.fy = node.y;\n      nodePositions[`${node.id}`] = [node.x, node.y];\n    }\n    if (!frozen && node.fx && node.fy && nodePositions && nodePositions[node.id]) {\n      nodePositions[node.id] = undefined;\n      node.fx = undefined;\n      node.fy = undefined;\n    }\n  }\n};\n"
  },
  {
    "path": "src/chart/graph/util/RecordUtils.ts",
    "content": "import { evaluateRulesOnNode, evaluateRulesOnLink } from '../../../extensions/styling/StyleRuleEvaluator';\nimport { extractNodePropertiesFromRecords, mergeNodePropsFieldsLists } from '../../../report/ReportRecordProcessing';\nimport { valueIsArray, valueIsNode, valueIsRelationship, valueIsPath, toNumber } from '../../ChartUtils';\nimport { GraphChartVisualizationProps } from '../GraphChartVisualization';\nimport { assignCurvatureToLink } from './RelUtils';\nimport { isNode } from 'neo4j-driver-core/lib/graph-types.js';\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\n// Gets all graphy entities (nodes/relationships) from the complete set of return values.\nfunction extractGraphEntitiesFromField(\n  value,\n  nodes: Record<string, any>[],\n  links: Record<string, any>[],\n  nodeLabels: Record<string, any>,\n  linkTypes: Record<string, any>,\n  frozen: boolean,\n  nodeSizeProperty: string,\n  defaultNodeSize: number,\n  relWidthProperty: string,\n  defaultRelWidth: number,\n  relColorProperty: string,\n  defaultRelColor: string,\n  nodePositions: Record<string, any>[]\n) {\n  if (value == undefined) {\n    return;\n  }\n  if (valueIsArray(value)) {\n    value.forEach((v) =>\n      extractGraphEntitiesFromField(\n        v,\n        nodes,\n        links,\n        nodeLabels,\n        linkTypes,\n        frozen,\n        nodeSizeProperty,\n        defaultNodeSize,\n        relWidthProperty,\n        defaultRelWidth,\n        relColorProperty,\n        defaultRelColor,\n        nodePositions\n      )\n    );\n  } else if (valueIsNode(value)) {\n    value.labels.forEach((l) => (nodeLabels[l] = true));\n    nodes[value.identity.low] = {\n      id: value.identity.low,\n      labels: value.labels,\n      size: !Number.isNaN(value.properties[nodeSizeProperty])\n        ? toNumber(value.properties[nodeSizeProperty])\n        : defaultNodeSize,\n      properties: value.properties,\n      mainLabel: value.labels[value.labels.length - 1],\n    };\n    if (frozen && nodePositions && nodePositions[value.identity.low]) {\n      nodes[value.identity.low].fx = nodePositions[value.identity.low][0];\n      nodes[value.identity.low].fy = nodePositions[value.identity.low][1];\n    }\n  } else if (valueIsRelationship(value)) {\n    if (links[`${value.start.low},${value.end.low}`] == undefined) {\n      links[`${value.start.low},${value.end.low}`] = [];\n    }\n    const addItem = (arr, item) => arr.find((x) => x.id === item.id) || arr.push(item);\n    addItem(links[`${value.start.low},${value.end.low}`], {\n      id: value.identity.low,\n      source: value.start.low,\n      target: value.end.low,\n      type: value.type,\n      width:\n        value.properties[relWidthProperty] !== undefined && !Number.isNaN(value.properties[relWidthProperty])\n          ? toNumber(value.properties[relWidthProperty])\n          : defaultRelWidth,\n      color: value.properties[relColorProperty] ? value.properties[relColorProperty] : defaultRelColor,\n      properties: value.properties,\n    });\n  } else if (valueIsPath(value)) {\n    value.segments.map((segment) => {\n      extractGraphEntitiesFromField(\n        segment.start,\n        nodes,\n        links,\n        nodeLabels,\n        linkTypes,\n        frozen,\n        nodeSizeProperty,\n        defaultNodeSize,\n        relWidthProperty,\n        defaultRelWidth,\n        relColorProperty,\n        defaultRelColor,\n        nodePositions\n      );\n      extractGraphEntitiesFromField(\n        segment.relationship,\n        nodes,\n        links,\n        nodeLabels,\n        linkTypes,\n        frozen,\n        nodeSizeProperty,\n        defaultNodeSize,\n        relWidthProperty,\n        defaultRelWidth,\n        relColorProperty,\n        defaultRelColor,\n        nodePositions\n      );\n      extractGraphEntitiesFromField(\n        segment.end,\n        nodes,\n        links,\n        nodeLabels,\n        linkTypes,\n        frozen,\n        nodeSizeProperty,\n        defaultNodeSize,\n        relWidthProperty,\n        defaultRelWidth,\n        relColorProperty,\n        defaultRelColor,\n        nodePositions\n      );\n    });\n  }\n}\n\nconst isValidLink = (link, nodes) => {\n  if (nodes[link.source] == null || nodes[link.target] == null) {\n    return false;\n  }\n  return true;\n};\nexport function buildGraphVisualizationObjectFromRecords(\n  records: any[], // Neo4jRecord[],\n  nodes: Record<string, any>[],\n  links: Record<string, any>[],\n  nodeLabels: Record<string, any>,\n  linkTypes: Record<string, any>,\n  colorScheme: any,\n  fields: any,\n  nodeColorProperty: any,\n  defaultNodeColor: any,\n  nodeSizeProperty: any,\n  defaultNodeSize: any,\n  relWidthProperty: any,\n  defaultRelWidth: any,\n  relColorProperty: any,\n  defaultRelColor: any,\n  styleRules: any,\n  nodePositions: any = {},\n  frozen: any = false\n) {\n  // Extract graph objects from result set.\n  records.forEach((record) => {\n    record._fields.forEach((field) => {\n      extractGraphEntitiesFromField(\n        field,\n        nodes,\n        links,\n        nodeLabels,\n        linkTypes,\n        frozen,\n        nodeSizeProperty,\n        defaultNodeSize,\n        relWidthProperty,\n        defaultRelWidth,\n        relColorProperty,\n        defaultRelColor,\n        nodePositions\n      );\n    });\n  });\n\n  // Assign proper curvatures and colors to relationships.\n  // Assigning curvature is needed for pairs of nodes that have multiple relationships between them, or self-loops.\n  const linksList = Object.values(links).map((linkArray) => {\n    return linkArray.map((link, i) => {\n      let defaultColor = link.color;\n\n      // Assign color from json based on style rule evaluation if specified\n      let evaluatedColor = evaluateRulesOnLink(link, 'relationship color', defaultColor, styleRules);\n      link.color = evaluatedColor;\n      const mirroredNodePair = links[`${link.target},${link.source}`];\n      return assignCurvatureToLink(link, i, linkArray.length, mirroredNodePair ? mirroredNodePair.length : 0);\n    });\n  });\n\n  linksList.forEach((link, idx, object) => {\n    if (!isValidLink(link[0], nodes)) {\n      object.splice(idx, 1);\n    }\n  });\n\n  // Assign proper colors to nodes.\n  const totalColors = colorScheme ? colorScheme.length : 0;\n  const nodeLabelsList = fields.map((e) => e[0]);\n  const nodesList = Object.values(nodes).map((node) => {\n    // First try to assign a node a color if it has a property specifying the color.\n    let assignedColor = node.properties[nodeColorProperty]\n      ? node.properties[nodeColorProperty]\n      : totalColors > 0\n      ? colorScheme[nodeLabelsList.indexOf(node.mainLabel) % totalColors]\n      : defaultNodeColor;\n    // Next, evaluate the custom styling rules to see if there's a rule-based override\n    assignedColor = evaluateRulesOnNode(node, 'node color', assignedColor, styleRules);\n    return update(node, { color: assignedColor ? assignedColor : defaultNodeColor });\n  });\n\n  // Set the data dictionary that is read by the visualization.\n  return {\n    nodes: nodesList,\n    links: linksList.flat(),\n  };\n}\n\n/**\n * Utility function to inject new records into an existing visualization while it already exists.\n * This is used to enable graph interactivity (e.g. exploration, editing).\n * @param records a new set of Neo4j records.\n * @param props properties of the existing graph visualization.\n */\nexport function injectNewRecordsIntoGraphVisualization(\n  records: any[], // Neo4jRecord[],\n  props: GraphChartVisualizationProps\n) {\n  // We should probably just maintain these in the state...\n  const nodesMap = {};\n  props.data.nodes.forEach((node) => {\n    nodesMap[node.id] = node;\n  });\n  const linksMap = {};\n  props.data.links.forEach((link) => {\n    linksMap[link.id] = link;\n  });\n  const newFields = extractNodePropertiesFromRecords(records);\n  const mergedFields = mergeNodePropsFieldsLists(props.engine.fields, newFields);\n  props.engine.setFields(mergedFields);\n\n  const { nodes, links } = buildGraphVisualizationObjectFromRecords(\n    records,\n    { ...nodesMap },\n    {},\n    props.data.nodeLabels,\n    props.data.linkTypes,\n    props.style.colorScheme,\n    mergedFields,\n    props.style.nodeColorProp,\n    props.style.defaultNodeColor,\n    props.style.nodeSizeProp,\n    props.style.defaultNodeSize,\n    props.style.relWidthProp,\n    props.style.defaultRelWidth,\n    props.style.relColorProp,\n    props.style.defaultRelColor,\n    props.extensions.styleRules,\n    props.interactivity.nodePositions,\n    props.interactivity.layoutFrozen\n  );\n\n  return { nodes, links, nodesMap, linksMap };\n}\n\n/**\n * TODO: generalize and fix to be consistent with other parts of the code.\n * TODO: maybe we shouldn't check if all records are nodes, but instead extract nodes from the records dynamically as the graph chart deos.\n * @param records List of records got back from the Driver\n * @param fieldIndex index of the field i want to check that is just nodes\n * @returns True if all the records are Node Objects\n */\nexport function checkIfAllRecordsAreNodes(records, fieldIndex) {\n  try {\n    let res = records.every((record) => {\n      return record._fields && isNode(record._fields[fieldIndex]);\n    });\n    return res;\n  } catch (error) {\n    // In any case of error, log and continue with false\n    console.error(error);\n    return false;\n  }\n}\n\n/**\n * TODO - this functionality is duplicated in the graph chart logic.\n * Ideally, we want to have a Node/Relationship representation indipendent from the return\n * that the driver gets back.\n * @param records List of records got from the driver\n * @returns List of Object that are parsed from the Node object received from the driver\n */\nexport function parseNodeRecordsToDictionaries(records, fieldIndex = 0) {\n  let res = records.map((record) => {\n    let { identity, labels, properties } = record._fields[fieldIndex];\n    // Preventing high/low fields by casting to its primitive type\n    identity = identity.toNumber();\n    return { id: identity, labels: labels, properties: properties };\n  });\n  return res;\n}\n"
  },
  {
    "path": "src/chart/graph/util/RelUtils.ts",
    "content": "export enum Direction {\n  Incoming,\n  Outgoing,\n}\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\n/**\n * Assigns a computed curvature value to a link in the visualization.\n * @param link the link object (n)-[e]->(n2)\n * @param index the index of the link in the list between a pair of nodes.\n * @param nodePairListLength  the amount of links between (n) and (n2) in the same direction.\n * @param mirroredNodePairListLength the amount of links between (n) and (n2) in the opposite direction.\n * @returns the link with an assigned curvature value.\n */\nexport function assignCurvatureToLink(link, index, nodePairListLength, mirroredNodePairListLength) {\n  if (link.source == link.target) {\n    // Self-loop\n    return update(link, { curvature: 0.4 + index / 8 });\n  }\n  // If we have edges from the target to the source, adjust curvatures accordingly.\n  const totalRelsBetweenPair = nodePairListLength + mirroredNodePairListLength;\n  return update(link, {\n    curvature:\n      link.source > link.target\n        ? getCurvature(index, totalRelsBetweenPair)\n        : -getCurvature(index + mirroredNodePairListLength, totalRelsBetweenPair),\n  });\n}\n\n// Function to manually compute edge curvatures for dense node pairs.\nexport function getCurvature(index, total) {\n  if (total <= 6) {\n    // Precomputed edge curvatures for nodes with multiple edges in between.\n    const curvatures = {\n      0: 0,\n      1: 0,\n      2: [-0.5, 0.5], // 2 = Math.floor(1/2) + 1\n      3: [-0.5, 0, 0.5], // 2 = Math.floor(3/2) + 1\n      4: [-0.66666, -0.33333, 0.33333, 0.66666], // 3 = Math.floor(4/2) + 1\n      5: [-0.66666, -0.33333, 0, 0.33333, 0.66666], // 3 = Math.floor(5/2) + 1\n      6: [-0.75, -0.5, -0.25, 0.25, 0.5, 0.75], // 4 = Math.floor(6/2) + 1\n      7: [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75], // 4 = Math.floor(7/2) + 1\n    };\n    return curvatures[total][index];\n  }\n\n  if (isNaN(total)) {\n    return 0;\n  }\n  // @ts-ignore\n  const arr1 = [...Array(Math.floor(total / 2)).keys()].map((i) => {\n    return (i + 1) / (Math.floor(total / 2) + 1);\n  });\n  const arr2 = total % 2 == 1 ? [0] : [];\n  // @ts-ignore\n  const arr3 = [...Array(Math.floor(total / 2)).keys()].map((i) => {\n    return (i + 1) / -(Math.floor(total / 2) + 1);\n  });\n  return arr1.concat(arr2).concat(arr3)[index];\n}\n\nexport const selfLoopRotationDegrees = 45;\n\nexport const generateRelCanvasObject = (link: any, ctx: any, relLabelFontSize: any, relLabelColor: any) => {\n  const label = link.properties.name || link.type || link.id;\n  const fontSize = relLabelFontSize;\n  const { source } = link;\n  const { target } = link;\n  ctx.font = `${fontSize}px Sans-Serif`;\n  ctx.fillStyle = relLabelColor;\n  if (link.target != link.source) {\n    const lenX = target.x - source.x;\n    const lenY = target.y - source.y;\n    const posX = target.x - lenX / 2;\n    const posY = target.y - lenY / 2;\n    const length = Math.sqrt(lenX * lenX + lenY * lenY);\n    const angle = Math.atan(lenY / lenX);\n    ctx.save();\n    ctx.translate(posX, posY);\n    ctx.rotate(angle);\n    // Mirrors the curvatures when the label is upside down.\n    const mirror = link.source.x > link.target.x ? 1 : -1;\n    ctx.textAlign = 'center';\n    if (link.curvature) {\n      ctx.fillText(label, 0, mirror * length * link.curvature * 0.5);\n    } else {\n      ctx.fillText(label, 0, 0);\n    }\n    ctx.restore();\n  } else {\n    ctx.save();\n    ctx.translate(link.source.x, link.source.y);\n    ctx.rotate((Math.PI * selfLoopRotationDegrees) / 180);\n    ctx.textAlign = 'center';\n    ctx.fillText(label, 0, -18.7 + -37.1 * (link.curvature - 0.5));\n    ctx.restore();\n  }\n};\n\n/**\n * Recompute curvatures for all links in the visualization.\n * This is needed when new relationships are added by exploration or graph editing.\n * TODO - this could be optimized by caching a dictionary instead of transforming the list here...\n */\nexport function recomputeCurvatures(links) {\n  const linksMap = {};\n  links.forEach((link) => {\n    if (linksMap[`${link.source.id},${link.target.id}`] == undefined) {\n      linksMap[`${link.source.id},${link.target.id}`] = [];\n    }\n    linksMap[`${link.source.id},${link.target.id}`].push(link);\n  });\n  const linksList = Object.values(linksMap).map((linkArray) => {\n    return linkArray.map((link, i) => {\n      const mirroredNodePair = linksMap[`${link.target.id},${link.source.id}`];\n      return assignCurvatureToLink(link, i, linkArray.length, mirroredNodePair ? mirroredNodePair.length : 0);\n    });\n  });\n  return linksList.flat();\n}\n\n/**\n * Merges two lists of (potententially duplicate) links.\n */\nexport function mergeLinksLists(oldLinks, newLinks) {\n  const links = {};\n  oldLinks.forEach((link) => {\n    links[link.id] = link;\n  });\n  newLinks.forEach((link) => {\n    links[link.id] = link;\n  });\n  return Object.values(links);\n}\n"
  },
  {
    "path": "src/chart/iframe/IFrameChart.tsx",
    "content": "import React from 'react';\nimport { ChartProps } from '../Chart';\nimport { replaceDashboardParameters } from '../ChartUtils';\n\n/**\n * Renders an iFrame of the URL provided by the user.\n */\nconst NeoIFrameChart = (props: ChartProps) => {\n  // Records are overridden to be a single element array with a field called 'input'.\n  const { records } = props;\n  const parameters = props.parameters ? props.parameters : {};\n  const passGlobalParameters =\n    props.settings && props.settings.passGlobalParameters ? props.settings.passGlobalParameters : false;\n  const replaceGlobalParameters =\n    props.settings && props.settings.replaceGlobalParameters !== undefined\n      ? props.settings.replaceGlobalParameters\n      : true;\n  const url = records[0].input.trim();\n  const mapParameters = records[0].parameters || {};\n  const queryString = Object.keys(mapParameters)\n    .map((key) => `${key}=${mapParameters[key]}`)\n    .join('&');\n  const modifiedUrl =\n    (replaceGlobalParameters ? replaceDashboardParameters(url, parameters) : url) +\n    (passGlobalParameters ? `#${queryString}` : '');\n\n  if (!modifiedUrl || !(modifiedUrl.startsWith('http://') || modifiedUrl.startsWith('https://'))) {\n    return (\n      <p style={{ margin: '15px' }}>\n        Invalid iFrame URL. Make sure your url starts with <code>http://</code> or <code>https://</code>.\n      </p>\n    );\n  }\n\n  return (\n    <iframe\n      style={{ width: '100%', border: 'none', marginBottom: '-5px', height: '100%', overflow: 'hidden' }}\n      src={modifiedUrl}\n    />\n  );\n};\n\nexport default NeoIFrameChart;\n"
  },
  {
    "path": "src/chart/json/JSONChart.tsx",
    "content": "import React from 'react';\nimport { ChartProps } from '../Chart';\nimport { TextareaAutosize } from '@mui/material';\nimport YAML from 'yaml';\n\n/**\n * Renders Neo4j records as their JSON representation.\n */\nconst NeoJSONChart = (props: ChartProps) => {\n  const { records, settings } = props;\n  const type = settings && settings.format ? settings.format : 'json';\n  const value = type == 'json' ? JSON.stringify(records, null, 2) : YAML.stringify(records, null, 2);\n  return (\n    <div style={{ marginTop: '0px' }}>\n      <TextareaAutosize\n        style={{ width: '100%', border: '1px solid lightgray' }}\n        className={'textinput-linenumbers'}\n        value={value}\n        aria-label=''\n        placeholder='Query output should be rendered here.'\n      />\n    </div>\n  );\n};\n\nexport default NeoJSONChart;\n"
  },
  {
    "path": "src/chart/line/LineChart.tsx",
    "content": "import { ResponsiveLine } from '@nivo/line';\nimport React, { useEffect } from 'react';\nimport { NoDrawableDataErrorMessage } from '../../component/editor/CodeViewerComponent';\nimport { evaluateRulesOnDict, useStyleRules } from '../../extensions/styling/StyleRuleEvaluator';\nimport { ChartProps } from '../Chart';\nimport { recordToNative, toNumber } from '../ChartUtils';\nimport { themeNivo } from '../Utils';\nimport { extensionEnabled } from '../../utils/ReportUtils';\n\ninterface LineChartData {\n  id: string;\n  data: Record<any, any>[];\n}\n\n/**\n * Embeds a LineReport (from Charts) into NeoDash.\n */\nconst NeoLineChart = (props: ChartProps) => {\n  const POSSIBLE_TIME_FORMATS = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'];\n\n  if (props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <>No data, re-run the report.</>;\n  }\n  const { records, selection } = props;\n\n  if (!selection || !selection.value || selection.value.length == 0) {\n    return <div style={{ margin: '15px' }}>No y-axis selected. To view the report, select a value below. </div>;\n  }\n\n  const [isTimeChart, setIsTimeChart] = React.useState(false);\n  const [validSelection, setValidSelection] = React.useState(true);\n\n  const [parseFormat, setParseFormat] = React.useState('%Y-%m-%dT%H:%M:%SZ');\n  const [data, setData] = React.useState([]);\n\n  const settings = props.settings ? props.settings : {};\n\n  const colorScheme = settings.colors ? settings.colors : 'set2';\n  const xScale = settings.xScale ? settings.xScale : 'linear';\n  const yScale = settings.yScale ? settings.yScale : 'linear';\n  const xScaleLogBase = settings.xScaleLogBase ? settings.xScaleLogBase : 10;\n  const yScaleLogBase = settings.yScaleLogBase ? settings.yScaleLogBase : 10;\n  const minXValue = settings.minXValue ? settings.minXValue : 'auto';\n  const maxXValue = settings.maxXValue ? settings.maxXValue : 'auto';\n  const minYValue = settings.minYValue ? settings.minYValue : 'auto';\n  const maxYValue = settings.maxYValue ? settings.maxYValue : 'auto';\n  const legend = settings.legend != undefined ? settings.legend : false;\n  const legendWidth = settings.legendWidth ? settings.legendWidth : 70;\n  const curve = settings.curve ? settings.curve : 'linear';\n  const marginRight = settings.marginRight ? settings.marginRight : 24;\n  const marginLeft = settings.marginLeft ? settings.marginLeft : 36;\n  const marginTop = settings.marginTop ? settings.marginTop : 24;\n  const marginBottom = settings.marginBottom ? settings.marginBottom : 40;\n  const lineWidth = settings.type == 'scatter' ? 0 : settings.lineWidth || 2;\n  const pointSize = settings.pointSize ? settings.pointSize : 10;\n  const showGrid = settings.showGrid != undefined ? settings.showGrid : true;\n  const xTickValues = settings.xTickValues != undefined ? settings.xTickValues : undefined;\n  const xTickTimeValues = settings.xTickTimeValues != undefined ? settings.xTickTimeValues : 'every 1 years';\n  const xAxisTimeFormat = settings.xAxisTimeFormat != undefined ? settings.xAxisTimeFormat : '%Y-%m-%dT%H:%M:%SZ';\n  const xAxisFormat = settings.xAxisFormat != undefined ? settings.xAxisFormat : undefined;\n\n  const xTickRotationAngle = settings.xTickRotationAngle != undefined ? settings.xTickRotationAngle : 0;\n  const yTickRotationAngle = settings.yTickRotationAngle != undefined ? settings.yTickRotationAngle : 0;\n  const styleRules = useStyleRules(\n    extensionEnabled(props.extensions, 'styling'),\n    props.settings.styleRules,\n    props.getGlobalParameter\n  );\n\n  // Compute line color based on rules - overrides default color scheme completely.\n  // For line charts, the line color is overridden if at least one value meets the criteria.\n  const getLineColors = (line) => {\n    const xFieldName = props.selection && props.selection.x;\n    const yFieldName = line.id;\n    let color = 'black';\n    for (const entry of line.data) {\n      const data = {};\n      data[xFieldName] = entry.x;\n      data[yFieldName] = entry.y;\n      const validRuleIndex = evaluateRulesOnDict(data, styleRules, ['line color']);\n      if (validRuleIndex !== -1) {\n        color = styleRules[validRuleIndex].customizationValue;\n        break;\n      }\n    }\n    return color;\n  };\n\n  if (!selection.value.length) {\n    return <p></p>;\n  }\n\n  const isDate = (x) => {\n    return x.__isDate__;\n  };\n\n  const isDateTime = (x) => {\n    return x.__isDateTime__;\n  };\n\n  const isDateTimeOrDate = (x) => {\n    return isDate(x) || isDateTime(x) || x instanceof Date;\n  };\n\n  useEffect(() => {\n    const dataRaw: LineChartData[] = selection.value.map((key) => ({\n      id: key as string,\n      data: [],\n    }));\n\n    records.forEach((row) => {\n      selection.value.forEach((key) => {\n        const index = dataRaw.findIndex((item) => (item as Record<string, any>).id === key);\n        let x: any = row.get(selection.x) || 0;\n        const y: any = recordToNative(row.get(key)) || 0;\n        if (dataRaw[index] && !isNaN(y)) {\n          if (isDate(x)) {\n            dataRaw[index].data.push({ x, y });\n          } else if (isDateTime(x)) {\n            x = new Date(x.toString());\n            dataRaw[index].data.push({ x, y });\n          } else {\n            dataRaw[index].data.push({ x, y });\n          }\n        }\n      });\n    });\n\n    setData(dataRaw);\n  }, [records, selection]);\n\n  useEffect(() => {\n    let validSelectionRaw = true;\n    data.forEach((selected) => {\n      if (selected.data.length == 0) {\n        validSelectionRaw = false;\n      }\n    });\n    setValidSelection(validSelectionRaw);\n\n    let timeRef = data[0]?.data[0]?.x || undefined;\n    timeRef = !isNaN(toNumber(timeRef)) ? toNumber(timeRef) : timeRef;\n    const chartIsTimeChart = timeRef !== undefined && isDateTimeOrDate(timeRef);\n    if (isTimeChart !== chartIsTimeChart) {\n      const p = chartIsTimeChart ? (isDateTime(timeRef) ? '%Y-%m-%dT%H:%M:%SZ' : '%Y-%m-%d') : '';\n\n      setParseFormat(p);\n      setIsTimeChart(chartIsTimeChart);\n    }\n  }, [data]);\n\n  // TODO - Nivo has a bug that, when we switch from a time-axis to a number axis, the visualization breaks.\n  // Therefore, we now require a manual refresh.\n  let timeRef = data[0]?.data[0]?.x || undefined;\n  const chartIsTimeChart = timeRef !== undefined && isDateTimeOrDate(timeRef);\n  if (isTimeChart !== chartIsTimeChart) {\n    if (!chartIsTimeChart) {\n      return (\n        <div style={{ margin: '15px' }}>\n          Line chart switched from time-axis to number-axis. Please re-run the report to see your changes.\n        </div>\n      );\n    }\n  }\n\n  if (!validSelection) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  const validateXTickTimeValues = xTickTimeValues.split(' ');\n  if (\n    validateXTickTimeValues.length != 3 ||\n    validateXTickTimeValues[0] != 'every' ||\n    !Number.isInteger(parseFloat(validateXTickTimeValues[1])) ||\n    parseFloat(validateXTickTimeValues[1]) <= 0 ||\n    !POSSIBLE_TIME_FORMATS.includes(validateXTickTimeValues[2])\n  ) {\n    return (\n      <code style={{ margin: '10px' }}>\n        Invalid tick size specification for time chart. Parameter value must be set to \"every [number] ['years',\n        'months', 'weeks', 'days', 'hours', 'seconds', 'milliseconds']\".\n      </code>\n    );\n  }\n\n  // T18:40:32.142+0100\n  // %Y-%m-%dT%H:%M:%SZ\n  const lineViz = (\n    <div className='n-h-full n-w-full overflow-hidden'>\n      <ResponsiveLine\n        theme={themeNivo}\n        data={data}\n        xScale={\n          isTimeChart\n            ? { format: parseFormat, type: 'time' }\n            : xScale == 'linear'\n            ? { type: xScale, min: minXValue, max: maxXValue, stacked: false, reverse: false }\n            : { type: xScale, min: minXValue, max: maxXValue, constant: xScaleLogBase, base: xScaleLogBase }\n        }\n        xFormat={isTimeChart ? `time:${xAxisTimeFormat}` : xAxisFormat}\n        margin={{ top: marginTop, right: marginRight, bottom: marginBottom, left: marginLeft }}\n        yScale={\n          yScale == 'linear'\n            ? { type: yScale, min: minYValue, max: maxYValue, stacked: false, reverse: false }\n            : { type: yScale, min: minYValue, max: maxYValue, constant: xScaleLogBase, base: yScaleLogBase }\n        }\n        curve={curve}\n        enableGridX={showGrid}\n        enableGridY={showGrid}\n        axisTop={null}\n        axisRight={null}\n        axisBottom={\n          isTimeChart\n            ? {\n                tickValues: xTickTimeValues,\n                tickSize: 5,\n                tickPadding: 5,\n                tickRotation: xTickRotationAngle,\n                format: xAxisTimeFormat,\n                legend: 'Time',\n                legendOffset: 36,\n                legendPosition: 'middle',\n              }\n            : {\n                orient: 'bottom',\n                tickSize: 6,\n                tickValues: xTickValues,\n                format: xAxisFormat,\n                tickRotation: xTickRotationAngle,\n                tickPadding: 12,\n              }\n        }\n        axisLeft={{\n          tickSize: 6,\n          tickPadding: 12,\n          tickRotation: yTickRotationAngle,\n        }}\n        pointSize={pointSize}\n        lineWidth={lineWidth}\n        lineColor='black'\n        pointColor='white'\n        colors={styleRules.length >= 1 ? getLineColors : { scheme: colorScheme }}\n        pointBorderWidth={2}\n        pointBorderColor={{ from: 'serieColor' }}\n        pointLabelYOffset={-12}\n        useMesh={true}\n        legends={\n          legend\n            ? [\n                {\n                  anchor: 'top-right',\n                  direction: 'row',\n                  justify: false,\n                  translateX: -10,\n                  translateY: -20,\n                  itemsSpacing: 0,\n                  itemDirection: 'right-to-left',\n                  itemWidth: legendWidth,\n                  itemHeight: 20,\n                  itemOpacity: 0.75,\n                  symbolSize: 6,\n                  symbolShape: 'circle',\n                  symbolBorderColor: 'rgba(0, 0, 0, .5)',\n                  effects: [\n                    {\n                      on: 'hover',\n                      style: {\n                        itemBackground: 'rgba(0, 0, 0, .03)',\n                        itemOpacity: 1,\n                      },\n                    },\n                  ],\n                },\n              ]\n            : []\n        }\n      />\n    </div>\n  );\n  return lineViz;\n};\n\nexport default NeoLineChart;\n"
  },
  {
    "path": "src/chart/map/MapChart.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { ChartProps } from '../Chart';\nimport { categoricalColorSchemes } from '../../config/ColorConfig';\nimport { valueIsArray, valueIsNode, valueIsRelationship, valueIsPath, valueIsObject } from '../../chart/ChartUtils';\nimport { MapContainer, TileLayer } from 'react-leaflet';\nimport 'leaflet/dist/leaflet.css';\nimport { evaluateRulesOnNode, useStyleRules } from '../../extensions/styling/StyleRuleEvaluator';\nimport { createHeatmap } from './layers/HeatmapLayer';\nimport { createMarkers } from './layers/MarkerLayer';\nimport { createLines } from './layers/LineLayer';\nimport { extensionEnabled } from '../../utils/ReportUtils';\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\n/**\n * Renders Neo4j records as their JSON representation.\n */\nconst NeoMapChart = (props: ChartProps) => {\n  // Retrieve config from advanced settings\n  const layerType = props.settings && props.settings.layerType ? props.settings.layerType : 'markers';\n  const nodeColorProp = props.settings && props.settings.nodeColorProp ? props.settings.nodeColorProp : 'color';\n  const defaultNodeSize = props.settings && props.settings.defaultNodeSize ? props.settings.defaultNodeSize : 'large';\n  const relWidthProp = props.settings && props.settings.relWidthProp ? props.settings.relWidthProp : 'width';\n  const relColorProp = props.settings && props.settings.relColorProp ? props.settings.relColorProp : 'color';\n  const defaultRelWidth = props.settings && props.settings.defaultRelWidth ? props.settings.defaultRelWidth : 3.5;\n  const defaultRelColor = props.settings && props.settings.defaultRelColor ? props.settings.defaultRelColor : '#666';\n  const nodeColorScheme = props.settings && props.settings.nodeColorScheme ? props.settings.nodeColorScheme : 'neodash';\n  const styleRules = useStyleRules(\n    extensionEnabled(props.extensions, 'styling'),\n    props.settings.styleRules,\n    props.getGlobalParameter\n  );\n  const defaultNodeColor = 'grey'; // Color of nodes without labels\n  const dimensions = props.dimensions ? props.dimensions : { width: 100, height: 100 };\n  const mapProviderURL =\n    props.settings && props.settings.providerUrl\n      ? props.settings.providerUrl\n      : 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';\n  const attribution =\n    props.settings && props.settings.attribution\n      ? props.settings.attribution\n      : '&copy; <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors';\n\n  const actionsRules =\n    extensionEnabled(props.extensions, 'actions') && props.settings && props.settings.actionsRules\n      ? props.settings.actionsRules\n      : [];\n\n  const [data, setData] = React.useState({ nodes: [], links: [], zoom: 0, centerLatitude: 0, centerLongitude: 0 });\n\n  // Per pixel, scaling factors for the latitude/longitude mapping function.\n  const widthScale = 8.55;\n  const heightScale = 6.7;\n\n  let key = `${dimensions.width},${dimensions.height},${data.centerLatitude},${data.centerLongitude},${props.fullscreen}`;\n  useEffect(() => {\n    `${data.centerLatitude},${data.centerLongitude},${props.fullscreen}`;\n  }, [props.fullscreen]);\n\n  useEffect(() => {\n    buildVisualizationDictionaryFromRecords(props.records);\n  }, []);\n\n  let nodes = {};\n  let nodeLabels = {};\n  let links = {};\n  let linkTypes = {};\n\n  // Gets all graphy objects (nodes/relationships) from the complete set of return values.\n  // TODO this should be in Utils.ts\n  function extractGraphEntitiesFromField(value) {\n    if (value == undefined) {\n      return;\n    }\n    if (valueIsArray(value)) {\n      value.forEach((v) => extractGraphEntitiesFromField(v));\n    } else if (valueIsObject(value)) {\n      if (value.label && value.id) {\n        // Override for adding point nodes using a manually constructed dict.\n        nodeLabels[value.label] = true;\n        nodes[value.id] = {\n          id: value.id,\n          labels: [value.label],\n          size: defaultNodeSize,\n          properties: value,\n          firstLabel: value.label,\n        };\n      } else if (value.type && value.id && value.start && value.end) {\n        // Override for adding relationships using a manually constructed dict.\n        if (links[`${value.start},${value.end}`] == undefined) {\n          links[`${value.start},${value.end}`] = [];\n        }\n        const addItem = (arr, item) => arr.find((x) => x.id === item.id) || arr.push(item);\n        addItem(links[`${value.start},${value.end}`], {\n          id: value.id,\n          source: value.start,\n          target: value.end,\n          type: value.type,\n          width: value[relWidthProp] ? value[relWidthProp] : defaultRelWidth,\n          color: value[relColorProp] ? value[relColorProp] : defaultRelColor,\n          properties: value,\n        });\n      }\n    } else if (valueIsNode(value)) {\n      value.labels.forEach((l) => (nodeLabels[l] = true));\n      nodes[value.identity.low] = {\n        id: value.identity.low,\n        labels: value.labels,\n        size: defaultNodeSize,\n        properties: value.properties,\n        firstLabel: value.labels[0],\n      };\n    } else if (valueIsRelationship(value)) {\n      if (links[`${value.start.low},${value.end.low}`] == undefined) {\n        links[`${value.start.low},${value.end.low}`] = [];\n      }\n      const addItem = (arr, item) => arr.find((x) => x.id === item.id) || arr.push(item);\n      addItem(links[`${value.start.low},${value.end.low}`], {\n        id: value.identity.low,\n        source: value.start.low,\n        target: value.end.low,\n        type: value.type,\n        width: value.properties[relWidthProp] ? value.properties[relWidthProp] : defaultRelWidth,\n        color: value.properties[relColorProp] ? value.properties[relColorProp] : defaultRelColor,\n        properties: value.properties,\n      });\n    } else if (valueIsPath(value)) {\n      value.segments.map((segment) => {\n        extractGraphEntitiesFromField(segment.start);\n        extractGraphEntitiesFromField(segment.relationship);\n        extractGraphEntitiesFromField(segment.end);\n      });\n    }\n  }\n\n  // TODO this should be in Utils.ts\n  function buildVisualizationDictionaryFromRecords(records) {\n    // Extract graph objects from result set.\n    records.forEach((record) => {\n      record._fields &&\n        record._fields.forEach((field) => {\n          extractGraphEntitiesFromField(field);\n        });\n    });\n\n    // Assign proper colors & coordinates to nodes.\n    const totalColors = categoricalColorSchemes[nodeColorScheme].length;\n    const nodeLabelsList = Object.keys(nodeLabels);\n    const nodesList = Object.values(nodes).map((node) => {\n      const assignPosition = (node) => {\n        if (node.properties.latitude && node.properties.longitude) {\n          nodes[node.id].pos = [parseFloat(node.properties.latitude), parseFloat(node.properties.longitude)];\n          return nodes[node.id].pos;\n        }\n        if (node.properties.lat && node.properties.long) {\n          nodes[node.id].pos = [parseFloat(node.properties.lat), parseFloat(node.properties.long)];\n          return nodes[node.id].pos;\n        }\n        Object.values(node.properties).forEach((p) => {\n          if (p != null && p.srid != null && p.x != null && p.y != null) {\n            if (!isNaN(p.x) && !isNaN(p.y)) {\n              nodes[node.id].pos = [p.y, p.x];\n              return [p.y, p.x];\n            }\n          }\n        });\n      };\n\n      let assignedColor = node.properties[nodeColorProp]\n        ? node.properties[nodeColorProp]\n        : categoricalColorSchemes[nodeColorScheme][nodeLabelsList.indexOf(node.firstLabel) % totalColors];\n\n      assignedColor = evaluateRulesOnNode(node, 'marker color', assignedColor, styleRules);\n      const assignedPos = assignPosition(node);\n      return update(node, {\n        pos: node.pos ? node.pos : assignedPos,\n        color: assignedColor ? assignedColor : defaultNodeColor,\n      });\n    });\n\n    // Assign proper curvatures to relationships.\n    const linksList = Object.values(links)\n      .map((nodePair) => {\n        return nodePair.map((link) => {\n          if (nodes[link.source] && nodes[link.source].pos && nodes[link.target] && nodes[link.target].pos) {\n            return update(link, { start: nodes[link.source].pos, end: nodes[link.target].pos });\n          }\n        });\n      })\n      .flat();\n\n    // Calculate center latitude and center longitude:\n\n    const latitudes = nodesList.reduce((a, b) => {\n      if (b.pos == undefined) {\n        return a;\n      }\n      a.push(b.pos[0]);\n      return a;\n    }, []);\n    const longitudes = nodesList.reduce((a, b) => {\n      if (b.pos == undefined) {\n        return a;\n      }\n      a.push(b.pos[1]);\n      return a;\n    }, []);\n    const maxLat = Math.max(...latitudes);\n    const minLat = Math.min(...latitudes);\n    const avgLat = maxLat - (maxLat - minLat) / 2.0;\n\n    let latWidthScaleFactor = (dimensions.width ? dimensions.width : 300) / widthScale;\n    let latDiff = maxLat - avgLat;\n    let latProjectedWidth = latDiff / latWidthScaleFactor;\n    let latZoomFit = Math.ceil(Math.log2(1.0 / latProjectedWidth));\n\n    const maxLong = Math.min(...longitudes);\n    const minLong = Math.min(...longitudes);\n    const avgLong = maxLong - (maxLong - minLong) / 2.0;\n\n    let longHeightScaleFactor = (dimensions.height ? dimensions.height : 300) / heightScale;\n    let longDiff = maxLong - avgLong;\n    let longProjectedHeight = longDiff / longHeightScaleFactor;\n    let longZoomFit = Math.ceil(Math.log2(1.0 / longProjectedHeight));\n    // Set data based on result values.\n    setData({\n      zoom: Math.min(latZoomFit, longZoomFit),\n      centerLatitude: latitudes ? latitudes.reduce((a, b) => a + b, 0) / latitudes.length : 0,\n      centerLongitude: longitudes ? longitudes.reduce((a, b) => a + b, 0) / longitudes.length : 0,\n      nodes: nodesList,\n      links: linksList,\n    });\n  }\n\n  // TODO this should definitely be refactored as an if/case statement.\n  const markers = layerType == 'markers' ? createMarkers(data, props) : '';\n  const lines = layerType == 'markers' ? createLines(data) : '';\n  const heatmap = layerType == 'heatmap' ? createHeatmap(data, props) : '';\n  // Draw the component.\n  // Ideally, we want to have one component for each layer on the map, different files\n  // https://stackoverflow.com/questions/69751481/i-want-to-use-useref-to-access-an-element-in-a-reat-leaflet-and-use-the-flyto\n  return (\n    <MapContainer\n      key={key}\n      style={{ width: '100%', height: '100%' }}\n      center={[data.centerLatitude ? data.centerLatitude : 0, data.centerLongitude ? data.centerLongitude : 0]}\n      zoom={data.zoom ? data.zoom : 0}\n      maxZoom={18}\n      scrollWheelZoom={false}\n    >\n      {heatmap}\n      <TileLayer attribution={attribution} url={mapProviderURL ? mapProviderURL : ''} />\n      {markers}\n      {lines}\n    </MapContainer>\n  );\n};\n\nexport default NeoMapChart;\n"
  },
  {
    "path": "src/chart/map/MapUtils.ts",
    "content": "/**\n * Function to make a abbreviate a number (EX: abbreviateNumber(1230,2) -> ->1,23K)\n * @param number Number to abbreviate\n * @param decPlaces Number of desired decimal places\n * @returns Abbreviated version of the number in input\n */\nexport const abbreviateNumber = (number, decPlaces) => {\n  // 2 decimal places => 100, 3 => 1000, etc\n  decPlaces = Math.pow(10, decPlaces);\n\n  // Enumerate number abbreviations\n  let abbrev = ['K', 'M', 'B', 'T', 'Qd', 'Qn', 'Sx'];\n\n  // Go through the array backwards, so we do the largest first\n  for (let i = abbrev.length - 1; i >= 0; i--) {\n    // Convert array index to \"1000\", \"1000000\", etc\n    let size = Math.pow(10, (i + 1) * 3);\n\n    // If the number is bigger or equal do the abbreviation\n    if (size <= number) {\n      // Here, we multiply by decPlaces, round, and then divide by decPlaces.\n      // This gives us nice rounding to a particular decimal place.\n      number = Math.round((number * decPlaces) / size) / decPlaces;\n\n      // Handle special case where we round up to the next abbreviation\n      if (number == 1000 && i < abbrev.length - 1) {\n        number = 1;\n        i += 1;\n      }\n\n      // Add the letter for the abbreviation\n      number += abbrev[i];\n\n      // We are done... stop\n      break;\n    }\n  }\n\n  return number;\n};\n"
  },
  {
    "path": "src/chart/map/layers/HeatmapLayer.tsx",
    "content": "import { HeatmapLayer } from 'react-leaflet-heatmap-layer-v3';\nimport 'leaflet/dist/leaflet.css';\nimport React from 'react';\n\n/**\n *  Create Heatmap layer to add on top of the map\n */\nexport function createHeatmap(data, props) {\n  /**\n   *  Extract the intensity property from a node.\n   */\n  const extractIntensityProperty = (node, intensityProp) => {\n    //\n    if (node.properties[intensityProp]) {\n      // Parse int from Neo4j Integer type if it has this type\n      // Or return plain value if already parsed as Integer or Float\n      return node.properties[intensityProp].low ?? node.properties[intensityProp];\n    }\n    return 0;\n  };\n  const intensityProp = props.settings && props.settings.intensityProp ? props.settings.intensityProp : '';\n\n  let points = data.nodes\n    .filter((node) => node.pos && !isNaN(node.pos[0]) && !isNaN(node.pos[1]))\n    .map((node) => [node.pos[0], node.pos[1], intensityProp == '' ? 1 : extractIntensityProperty(node, intensityProp)]);\n\n  return (\n    <HeatmapLayer\n      fitBoundsOnLoad\n      fitBoundsOnUpdate\n      points={points}\n      longitudeExtractor={(m) => m[1]}\n      latitudeExtractor={(m) => m[0]}\n      intensityExtractor={(m) => parseFloat(m[2])}\n    />\n  );\n}\n"
  },
  {
    "path": "src/chart/map/layers/LineLayer.tsx",
    "content": "import React from 'react';\nimport { Polyline, Popup } from 'react-leaflet';\nimport 'leaflet/dist/leaflet.css';\nimport { Typography } from '@neo4j-ndl/react';\n\nexport function createLines(data) {\n  function createPopupFromRelProperties(value) {\n    return (\n      <Popup className={'leaflet-custom-rel-popup'}>\n        <Typography variant='h4'>\n          <b>{value.type}</b>\n        </Typography>\n        <table>\n          <tbody>\n            {Object.keys(value.properties).length == 0 ? (\n              <tr>\n                <td>(No properties)</td>\n              </tr>\n            ) : (\n              Object.keys(value.properties).map((k, i) => (\n                <tr key={i}>\n                  <td style={{ marginRight: '10px' }} key={0}>\n                    {k.toString()}:\n                  </td>\n                  <td key={1}>{value.properties[k].toString()}</td>\n                </tr>\n              ))\n            )}\n          </tbody>\n        </table>\n      </Popup>\n    );\n  }\n\n  // Create lines to plot on the map.\n  return data.links\n    .filter((link) => link)\n    .map((rel, i) => {\n      if (rel.start && rel.end) {\n        return (\n          <Polyline weight={rel.width} key={i} positions={[rel.start, rel.end]} color={rel.color}>\n            {createPopupFromRelProperties(rel)}\n          </Polyline>\n        );\n      }\n    });\n}\n"
  },
  {
    "path": "src/chart/map/layers/MarkerLayer.tsx",
    "content": "import React from 'react';\nimport Marker from 'react-leaflet-enhanced-marker';\nimport MarkerClusterGroup from 'react-leaflet-cluster';\nimport { MapPinIconSolid } from '@neo4j-ndl/react/icons';\nimport 'leaflet/dist/leaflet.css';\nimport { Popup, Tooltip } from 'react-leaflet';\nimport { Button, Typography } from '@neo4j-ndl/react';\nimport { getRule } from '../../../extensions/advancedcharts/Utils';\nimport { extensionEnabled } from '../../../utils/ReportUtils';\n\nexport function createMarkers(data, props) {\n  const clusterMarkers = props.settings?.clusterMarkers ? props.settings.clusterMarkers : false;\n  const separateOverlappingMarkers = props.settings?.separateOverlappingMarkers\n    ? props.settings.separateOverlappingMarkers\n    : false;\n\n  const defaultNodeSize = props.settings && props.settings.defaultNodeSize ? props.settings.defaultNodeSize : 'large';\n  const actionsRules =\n    extensionEnabled(props.extensions, 'actions') && props.settings && props.settings.actionsRules\n      ? props.settings.actionsRules\n      : [];\n\n  let markerMarginTop;\n  let markerIconClass;\n  let markerMarginLeft;\n  // Render a node label tooltip\n  switch (defaultNodeSize) {\n    case 'large':\n      markerMarginTop = '-20px';\n      markerMarginLeft = '0px';\n      markerIconClass = '';\n      break;\n    case 'medium':\n      markerMarginTop = '-5px';\n      markerMarginLeft = '2px';\n      markerIconClass = 'btn-icon-lg-r';\n      break;\n    default:\n      markerMarginTop = '6px';\n      markerMarginLeft = '10px';\n      markerIconClass = 'btn-icon-base-r';\n      break;\n  }\n\n  function createPopupFromNodeProperties(value) {\n    return (\n      <Popup className={'leaflet-custom-node-popup'}>\n        <Typography variant='h4'>\n          <b>{value.labels.length > 0 ? value.labels.map((b) => `${b} `) : '(No labels)'}</b>\n        </Typography>\n        <table>\n          <tbody>\n            {Object.keys(value.properties).length == 0 ? (\n              <tr>\n                <td>(No properties)</td>\n              </tr>\n            ) : (\n              Object.keys(value.properties).map((k, i) => {\n                // TODO MOVE THIS DEPENDENCY OUT OF THE TOOLTIP GENERATION\n                let rule = getRule(\n                  { field: k.toString(), value: value.properties[k].toString() },\n                  actionsRules,\n                  'Click'\n                );\n                let execRule =\n                  rule !== null &&\n                  rule[0] !== null &&\n                  rule[0].customization == 'set variable' &&\n                  props &&\n                  props.setGlobalParameter;\n\n                return (\n                  <tr\n                    key={i}\n                    onClick={() => {\n                      if (execRule) {\n                        // call thunk for $neodash_customizationValue\n                        props.setGlobalParameter(`neodash_${rule.customizationValue}`, value.properties[k].toString());\n                      }\n                    }}\n                  >\n                    <td style={{ marginRight: '10px' }} key={0}>\n                      {k.toString()}:\n                    </td>\n                    <td key={1}>\n                      {execRule ? (\n                        <Button\n                          style={{ width: '100%', marginLeft: '10px', marginRight: '10px' }}\n                          color='primary'\n                          onClick={() => {\n                            if (execRule) {\n                              // call thunk for $neodash_customizationValue\n                              props.setGlobalParameter(\n                                `neodash_${rule[0].customizationValue}`,\n                                value.properties[k].toString()\n                              );\n                            }\n                          }}\n                        >{`${value.properties[k].toString()}`}</Button>\n                      ) : (\n                        value.properties[k].toString()\n                      )}\n                    </td>\n                  </tr>\n                );\n              })\n            )}\n          </tbody>\n        </table>\n      </Popup>\n    );\n  }\n\n  const renderNodeLabel = (node) => {\n    const selectedProp = props.selection && props.selection[node.firstLabel];\n    if (selectedProp == '(id)') {\n      return node.id;\n    }\n    if (selectedProp == '(label)') {\n      return node.labels;\n    }\n    if (selectedProp == '(no label)') {\n      return '';\n    }\n    return node.properties[selectedProp] ? node.properties[selectedProp].toString() : '';\n  };\n\n  // Create markers to plot on the map\n  let markers = data.nodes\n    .filter((node) => node.pos && !isNaN(node.pos[0]) && !isNaN(node.pos[1]))\n    .map((node, i) => (\n      <Marker\n        position={node.pos}\n        key={i}\n        icon={\n          <div\n            style={{ color: node.color, textAlign: 'center', marginTop: markerMarginTop, marginLeft: markerMarginLeft }}\n          >\n            <MapPinIconSolid className={markerIconClass} />\n          </div>\n        }\n      >\n        {props.selection && props.selection[node.firstLabel] && props.selection[node.firstLabel] != '(no label)' ? (\n          <Tooltip direction='bottom' permanent className={'leaflet-custom-tooltip'} disableInteractive>\n            {renderNodeLabel(node)}\n          </Tooltip>\n        ) : (\n          <></>\n        )}\n        {createPopupFromNodeProperties(node)}\n      </Marker>\n    ));\n  if (clusterMarkers) {\n    markers = <MarkerClusterGroup chunkedLoading>{markers}</MarkerClusterGroup>;\n  } else {\n    markers = (\n      <MarkerClusterGroup chunkedLoading maxClusterRadius={separateOverlappingMarkers ? 5 : 0}>\n        {markers}\n      </MarkerClusterGroup>\n    );\n  }\n  return markers;\n}\n"
  },
  {
    "path": "src/chart/markdown/MarkdownChart.tsx",
    "content": "import React from 'react';\nimport { ChartProps } from '../Chart';\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport URI from 'urijs';\nimport { replaceDashboardParameters } from '../ChartUtils';\n\n// Sanitizes URIs\nconst transformUri = (uri: string): string | undefined => {\n  const parsedUri = URI(uri);\n  if (parsedUri.protocol() === 'http' || parsedUri.protocol() === 'https') {\n    return parsedUri.toString(); // Convert URI object back to string\n  }\n  return undefined; // Return undefined to skip rendering of potentially unsafe URLs\n};\n\n// Define custom components for Markdown elements\nconst CustomTable = ({ _, ...props }) => <table {...props} className='markdown-table' />;\nconst CustomTh = ({ _, ...props }) => <th {...props} className='markdown-th' />;\nconst CustomTd = ({ _, ...props }) => <td {...props} className='markdown-td' />;\nconst CustomATag = ({ _, href, ...props }) => (\n  // Apply URI transformation right in the anchor element for additional security\n  <a href={href ? transformUri(href) : undefined} {...props} rel='noopener noreferrer' target='_blank' />\n);\n\n/**\n * Renders Markdown text provided by the user.\n */\nconst NeoMarkdownChart = (props: ChartProps) => {\n  // Define custom components for Markdown elements\n  const components = {\n    table: CustomTable,\n    th: CustomTh,\n    td: CustomTd,\n    a: CustomATag,\n  };\n\n  // Records are overridden to be a single element array with a field called 'input'.\n  const { records } = props;\n  const parameters = props.parameters ? props.parameters : {};\n  const replaceGlobalParameters =\n    props.settings && props.settings.replaceGlobalParameters !== undefined\n      ? props.settings.replaceGlobalParameters\n      : true;\n  const markdown = records[0].input;\n  const modifiedMarkdown = replaceGlobalParameters ? replaceDashboardParameters(markdown, parameters) : markdown;\n  return (\n    <div\n      className='markdown-widget'\n      style={{ marginTop: '0px', marginLeft: '15px', marginRight: '15px', marginBottom: '0px' }}\n    >\n      <base target='_blank' />\n      <ReactMarkdown\n        children={modifiedMarkdown}\n        remarkPlugins={[remarkGfm]}\n        components={components}\n        transformLinkUri={transformUri}\n      />\n    </div>\n  );\n};\n\nexport default NeoMarkdownChart;\n"
  },
  {
    "path": "src/chart/parameter/ParameterSelectCardSettings.tsx",
    "content": "// TODO: this file (in a way) belongs to chart/parameter/ParameterSelectionChart. It would make sense to move it there\n\nimport React, { useCallback, useContext, useEffect } from 'react';\nimport { RUN_QUERY_DELAY_MS } from '../../config/ReportConfig';\nimport { QueryStatus, runCypherQuery } from '../../report/ReportQueryRunner';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport { Autocomplete, debounce, TextField } from '@mui/material';\nimport NeoField from '../../component/field/Field';\nimport { Dropdown } from '@neo4j-ndl/react';\nimport NeoCodeEditorComponent from '../../component/editor/CodeEditorComponent';\n\ntype ParameterId = string | undefined | null;\n\nconst ParameterSelectCardSettings = ({ query, database, settings, onReportSettingUpdate, onQueryUpdate }) => {\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n  if (!driver) {\n    throw new Error(\n      '`driver` not defined. Have you added it into your app as <Neo4jContext.Provider value={{driver}}> ?'\n    );\n  }\n\n  const [queryText, setQueryText] = React.useState(query);\n  const debouncedQueryUpdate = useCallback(debounce(onQueryUpdate, 250), []);\n  const debouncedRunCypherQuery = useCallback(debounce(runCypherQuery, RUN_QUERY_DELAY_MS), []);\n\n  const { manualPropertyNameSpecification } = settings;\n  const [labelInputText, setLabelInputText] = React.useState(settings.entityType);\n  const [labelRecords, setLabelRecords] = React.useState([]);\n  const [propertyInputText, setPropertyInputText] = React.useState(settings.propertyType);\n  const [propertyInputDisplayText, setPropertyInputDisplayText] = React.useState(\n    settings.propertyTypeDisplay || settings.propertyType\n  );\n  const [propertyRecords, setPropertyRecords] = React.useState([]);\n  let { parameterName } = settings;\n\n  // When certain settings are updated, a re-generated search query is needed.\n  useEffect(() => {\n    updateReportQuery(settings.entityType, settings.propertyType, settings.propertyTypeDisplay);\n  }, [settings.suggestionLimit, settings.deduplicateSuggestions, settings.searchType, settings.caseSensitive]);\n\n  useEffect(() => {\n    setLabelRecords([]);\n    setPropertyRecords([]);\n  }, [database]);\n\n  const cleanParameter = (parameter: string) => parameter.replaceAll(' ', '_').replaceAll('-', '_').toLowerCase();\n  const formatParameterId = (id: ParameterId) => {\n    const cleanedId = id ?? '';\n    return cleanedId == '' || cleanedId.startsWith('_') ? cleanedId : `_${cleanedId}`;\n  };\n\n  if (settings.type == undefined) {\n    onReportSettingUpdate('type', 'Node Property');\n  }\n\n  if (!parameterName && settings.entityType && settings.propertyType) {\n    const entityAndPropertyType = `neodash_${settings.entityType}_${settings.propertyType}`;\n    const formattedParameterId = formatParameterId(settings.id);\n    const parameterName = cleanParameter(entityAndPropertyType + formattedParameterId);\n\n    onReportSettingUpdate('parameterName', parameterName);\n  }\n  // Define query callback to allow reports to get extra data on interactions.\n  const queryCallback = useCallback(\n    (query, parameters, setRecords) => {\n      debouncedRunCypherQuery(\n        driver,\n        database,\n        query,\n        parameters,\n        10,\n        (status) => {\n          status == QueryStatus.NO_DATA ? setRecords([]) : () => {};\n        },\n        (result) => setRecords(result),\n        () => {}\n      );\n    },\n    [database]\n  );\n\n  function handleParameterTypeUpdate(newValue) {\n    onReportSettingUpdate('entityType', undefined);\n    onReportSettingUpdate('propertyType', undefined);\n    onReportSettingUpdate('propertyTypeDisplay', undefined);\n    onReportSettingUpdate('id', undefined);\n    onReportSettingUpdate('parameterName', undefined);\n    onReportSettingUpdate('type', newValue);\n  }\n\n  function handleNodeLabelSelectionUpdate(newValue) {\n    setPropertyInputText('');\n    setPropertyInputDisplayText('');\n    onReportSettingUpdate('entityType', newValue);\n    onReportSettingUpdate('propertyType', undefined);\n    onReportSettingUpdate('propertyTypeDisplay', undefined);\n    onReportSettingUpdate('parameterName', undefined);\n  }\n\n  function handleFreeTextNameSelectionUpdate(newValue) {\n    if (newValue) {\n      const new_parameter_name = cleanParameter(`neodash_${newValue}`);\n      handleReportQueryUpdate(new_parameter_name, newValue, undefined, undefined);\n    } else {\n      onReportSettingUpdate('parameterName', undefined);\n    }\n  }\n\n  function handlePropertyNameSelectionUpdate(newValue) {\n    onReportSettingUpdate('propertyType', newValue);\n    onReportSettingUpdate('propertyTypeDisplay', newValue);\n    if (newValue && settings.entityType) {\n      const newParameterName = `neodash_${settings.entityType}_${newValue}`;\n      const formattedParameterId = formatParameterId(settings.id);\n      const cleanedParameter = cleanParameter(newParameterName + formattedParameterId);\n\n      handleReportQueryUpdate(cleanedParameter, settings.entityType, newValue, newValue);\n    } else {\n      onReportSettingUpdate('parameterName', undefined);\n    }\n  }\n\n  function handlePropertyDisplayNameSelectionUpdate(newValue) {\n    onReportSettingUpdate('propertyTypeDisplay', newValue);\n    if (newValue && settings.entityType) {\n      updateReportQuery(settings.entityType, settings.propertyType, newValue);\n    } else {\n      onReportSettingUpdate('parameterName', undefined);\n    }\n  }\n\n  function handleIdSelectionUpdate(value) {\n    const newValue = value || '';\n    onReportSettingUpdate('id', `${newValue}`);\n    if (settings.propertyType && settings.entityType) {\n      const newParameterName = `neodash_${settings.entityType}_${settings.propertyType}`;\n      const formattedParameterId = formatParameterId(`${newValue}`);\n      const cleanedParameter = cleanParameter(newParameterName + formattedParameterId);\n\n      handleReportQueryUpdate(\n        cleanedParameter,\n        settings.entityType,\n        settings.propertyType,\n        settings.propertyTypeDisplay\n      );\n    }\n  }\n\n  function handleReportQueryUpdate(new_parameter_name, entityType, propertyType, propertyTypeDisplay) {\n    onReportSettingUpdate('parameterName', new_parameter_name);\n    updateReportQuery(entityType, propertyType, propertyTypeDisplay);\n  }\n\n  function updateReportQuery(entityType, propertyType, propertyTypeDisplay) {\n    const propertyTypeDisplaySanitized = propertyTypeDisplay || propertyType;\n    const limit = settings.suggestionLimit ? settings.suggestionLimit : 5;\n    const deduplicate = settings.deduplicateSuggestions !== undefined ? settings.deduplicateSuggestions : true;\n    const searchType = settings.searchType ? settings.searchType : 'CONTAINS';\n    const caseSensitive = settings.caseSensitive !== undefined ? settings.caseSensitive : false;\n    if (settings.type == 'Node Property') {\n      const newQuery =\n        `MATCH (n:\\`${entityType}\\`) \\n` +\n        `WHERE ${caseSensitive ? '' : 'toLower'}(toString(n.\\`${propertyTypeDisplaySanitized}\\`)) ${searchType} ${\n          caseSensitive ? '' : 'toLower'\n        }($input) \\n` +\n        `RETURN ${deduplicate ? 'DISTINCT' : ''} n.\\`${propertyType}\\` as value, ` +\n        ` n.\\`${propertyTypeDisplaySanitized}\\` as display ` +\n        `ORDER BY size(toString(value)) ASC LIMIT ${limit}`;\n      onQueryUpdate(newQuery);\n    } else if (settings.type == 'Relationship Property') {\n      const newQuery =\n        `MATCH ()-[n:\\`${entityType}\\`]->() \\n` +\n        `WHERE ${caseSensitive ? '' : 'toLower'}(toString(n.\\`${propertyTypeDisplaySanitized}\\`)) ${searchType} ${\n          caseSensitive ? '' : 'toLower'\n        }($input) \\n` +\n        `RETURN ${deduplicate ? 'DISTINCT' : ''} n.\\`${propertyType}\\` as value, ` +\n        ` n.\\`${propertyTypeDisplaySanitized}\\` as display ` +\n        `ORDER BY size(toString(value)) ASC LIMIT ${limit}`;\n      onQueryUpdate(newQuery);\n    } else if (settings.type == 'Custom Query') {\n      const newQuery = query;\n      onQueryUpdate(newQuery);\n    } else {\n      onQueryUpdate('RETURN true;');\n    }\n  }\n\n  // TODO: since this component is only rendered for parameter select, this is technically not needed\n  const parameterSelectTypes = ['Node Property', 'Relationship Property', 'Free Text', 'Custom Query', 'Date Picker'];\n  const selectedType = settings.type ? settings.type : 'Node Property';\n  const helperText = settings?.helperText || '';\n  const inputMode = settings?.inputMode || 'cypher';\n  const overridePropertyDisplayName =\n    settings.overridePropertyDisplayName !== undefined ? settings.overridePropertyDisplayName : false;\n\n  // If the override is off, and the two values differ, set the display value to the original one again.\n  if (!overridePropertyDisplayName && propertyInputText !== propertyInputDisplayText) {\n    onReportSettingUpdate('propertyTypeDisplay', settings.propertyType);\n    setPropertyInputDisplayText(propertyInputText);\n    updateReportQuery(settings.entityType, settings.propertyType, settings.propertyType);\n  }\n\n  return (\n    <div>\n      <p style={{ color: 'grey', fontSize: 12, paddingLeft: '5px', border: '1px solid lightgrey', marginTop: '0px' }}>\n        {helperText}\n      </p>\n      <Dropdown\n        id='type'\n        selectProps={{\n          onChange: (newValue) => newValue && handleParameterTypeUpdate(newValue.value),\n          options: parameterSelectTypes.map((option) => ({ label: option, value: option })),\n          value: { label: selectedType, value: selectedType },\n          menuPlacement: 'bottom',\n          menuPortalTarget: document.querySelector('#overlay'),\n        }}\n        label='Selection Type'\n        type='select'\n        fluid\n        autoFocus\n        style={{ marginTop: '5px' }}\n      />\n\n      {settings.type == 'Free Text' || settings.type == 'Date Picker' ? (\n        <NeoField\n          label={'Name'}\n          key={'freetext'}\n          value={settings.entityType ? settings.entityType : ''}\n          defaultValue={''}\n          placeholder={'Enter a parameter name here...'}\n          style={{}}\n          onChange={(value) => {\n            setLabelInputText(value);\n            handleNodeLabelSelectionUpdate(value);\n            handleFreeTextNameSelectionUpdate(value);\n          }}\n        />\n      ) : settings.type == 'Custom Query' ? (\n        <>\n          <div>\n            <NeoField\n              label={'Name'}\n              key={'query'}\n              value={settings?.entityType || ''}\n              defaultValue={''}\n              placeholder={'Enter a parameter name here...'}\n              style={{}}\n              onChange={(value) => {\n                setLabelInputText(value);\n                handleNodeLabelSelectionUpdate(value);\n                handleFreeTextNameSelectionUpdate(value);\n              }}\n            />\n            <br />\n            <div style={{ display: labelInputText ? 'inherit' : 'none' }}>\n              <NeoCodeEditorComponent\n                value={queryText}\n                editable={true}\n                language={inputMode}\n                onChange={(value) => {\n                  debouncedQueryUpdate(value);\n                  setQueryText(value);\n                }}\n                placeholder={'Enter Cypher here...'}\n              />\n              <p\n                style={{\n                  color: 'grey',\n                  fontSize: 12,\n                  paddingLeft: '5px',\n                  borderBottom: '1px solid lightgrey',\n                  borderLeft: '1px solid lightgrey',\n                  borderRight: '1px solid lightgrey',\n                  marginTop: '0px',\n                }}\n              >\n                {\n                  'Specify a query that takes a parameter $input (the user typed text) and return a number of rows with a field called `value` (the suggestions).'\n                }\n              </p>\n            </div>\n          </div>\n        </>\n      ) : (\n        <>\n          <Autocomplete\n            id='autocomplete-label-type'\n            options={\n              manualPropertyNameSpecification\n                ? [settings.entityType]\n                : labelRecords.map((r) => (r._fields ? r._fields[0] : '(no data)'))\n            }\n            getOptionLabel={(option) => option || ''}\n            style={{ marginTop: '13px' }}\n            inputValue={labelInputText}\n            onInputChange={(event, value) => {\n              setLabelInputText(value);\n              if (manualPropertyNameSpecification) {\n                handleNodeLabelSelectionUpdate(value);\n              } else if (settings.type == 'Node Property') {\n                queryCallback(\n                  'CALL db.labels() YIELD label WITH label as nodeLabel WHERE toLower(nodeLabel) CONTAINS toLower($input) RETURN DISTINCT nodeLabel ORDER BY size(nodeLabel) LIMIT 5',\n                  { input: value },\n                  setLabelRecords\n                );\n              } else {\n                queryCallback(\n                  'CALL db.relationshipTypes() YIELD relationshipType WITH relationshipType as relType WHERE toLower(relType) CONTAINS toLower($input) RETURN DISTINCT relType ORDER BY size(relType) LIMIT 5',\n                  { input: value },\n                  setLabelRecords\n                );\n              }\n            }}\n            size={'small'}\n            value={settings.entityType ? settings.entityType : undefined}\n            onChange={(event, newValue) => handleNodeLabelSelectionUpdate(newValue)}\n            renderInput={(params) => (\n              <TextField\n                {...params}\n                placeholder='Start typing...'\n                InputLabelProps={{ shrink: true }}\n                label={settings.type == 'Node Property' ? 'Node Label' : 'Relationship Type'}\n              />\n            )}\n          />\n          {/* Draw the property name & id selectors only after a label/type has been selected. */}\n          {settings.entityType ? (\n            <>\n              <Autocomplete\n                id='autocomplete-property'\n                options={\n                  manualPropertyNameSpecification\n                    ? [settings.propertyType]\n                    : propertyRecords.map((r) => (r._fields ? r._fields[0] : '(no data)'))\n                }\n                getOptionLabel={(option) => option || ''}\n                style={{ display: 'inline-block', width: '65%', marginTop: '13px', marginRight: '5%' }}\n                inputValue={propertyInputText}\n                onInputChange={(event, value) => {\n                  setPropertyInputText(value);\n                  setPropertyInputDisplayText(value);\n                  if (manualPropertyNameSpecification) {\n                    handlePropertyNameSelectionUpdate(value);\n                  } else {\n                    queryCallback(\n                      'CALL db.propertyKeys() YIELD propertyKey as propertyName WITH propertyName WHERE toLower(propertyName) CONTAINS toLower($input) RETURN DISTINCT propertyName ORDER BY size(propertyName) LIMIT 5',\n                      { input: value },\n                      setPropertyRecords\n                    );\n                  }\n                }}\n                size={'small'}\n                value={settings.propertyType}\n                onChange={(event, newValue) => handlePropertyNameSelectionUpdate(newValue)}\n                renderInput={(params) => (\n                  <TextField\n                    {...params}\n                    placeholder='Start typing...'\n                    InputLabelProps={{ shrink: true }}\n                    label={'Property Name'}\n                  />\n                )}\n              />\n              {overridePropertyDisplayName ? (\n                <Autocomplete\n                  id='autocomplete-property-display'\n                  size={'small'}\n                  options={\n                    manualPropertyNameSpecification\n                      ? [settings.propertyTypeDisplay || settings.propertyType]\n                      : propertyRecords.map((r) => (r._fields ? r._fields[0] : '(no data)'))\n                  }\n                  getOptionLabel={(option) => option || ''}\n                  style={{ display: 'inline-block', width: '65%', marginTop: '13px', marginRight: '5%' }}\n                  inputValue={propertyInputDisplayText}\n                  onInputChange={(event, value) => {\n                    setPropertyInputDisplayText(value);\n                    if (manualPropertyNameSpecification) {\n                      handlePropertyDisplayNameSelectionUpdate(value);\n                    } else {\n                      queryCallback(\n                        'CALL db.propertyKeys() YIELD propertyKey as propertyName WITH propertyName WHERE toLower(propertyName) CONTAINS toLower($input) RETURN DISTINCT propertyName ORDER BY size(propertyName) LIMIT 5',\n                        { input: value },\n                        setPropertyRecords\n                      );\n                    }\n                  }}\n                  value={settings.propertyTypeDisplay || settings.propertyType}\n                  onChange={(event, newValue) => handlePropertyDisplayNameSelectionUpdate(newValue)}\n                  renderInput={(params) => (\n                    <TextField\n                      {...params}\n                      placeholder='Start typing...'\n                      InputLabelProps={{ shrink: true }}\n                      label={'Property Display Name'}\n                    />\n                  )}\n                />\n              ) : (\n                <></>\n              )}\n              <TextField\n                placeholder='number'\n                label='Number (optional)'\n                disabled={!settings.propertyType}\n                value={settings.id}\n                style={{ width: '30%', display: 'inline-block', marginTop: '13px' }}\n                onChange={(e) => {\n                  handleIdSelectionUpdate(e.target.value);\n                }}\n                size={'small'}\n              />\n            </>\n          ) : (\n            <></>\n          )}\n        </>\n      )}\n      {parameterName ? (\n        <p>\n          Use <b>${parameterName}</b> in a query to use the parameter.\n        </p>\n      ) : (\n        <></>\n      )}\n    </div>\n  );\n};\n\nexport default ParameterSelectCardSettings;\n"
  },
  {
    "path": "src/chart/parameter/ParameterSelectionChart.tsx",
    "content": "import React from 'react';\nimport { ChartProps } from '../Chart';\nimport DatePickerParameterSelectComponent from './component/DateParameterSelect';\nimport NodePropertyParameterSelectComponent from './component/NodePropertyParameterSelect';\nimport RelationshipPropertyParameterSelectComponent from './component/RelationshipPropertyParameterSelect';\nimport FreeTextParameterSelectComponent from './component/FreeTextParameterSelect';\nimport QueryParameterSelectComponent from './component/QueryParameterSelect';\nimport { createTheme, ThemeProvider } from '@mui/material/styles';\n\n/**\n * A special chart type to define global dashboard parameters that are injected as query parameters into each report.\n */\nexport const NeoParameterSelectionChart = (props: ChartProps) => {\n  const query = props.records[0].input ? props.records[0].input : undefined;\n  const parameterName = props.settings && props.settings.parameterName ? props.settings.parameterName : undefined;\n  const parameterDisplayName = `${parameterName}_display`;\n  const type = props.settings && props.settings.type ? props.settings.type : undefined;\n  const queryCallback = props.queryCallback ? props.queryCallback : () => {};\n  const setGlobalParameter = props.setGlobalParameter ? props.setGlobalParameter : () => {};\n  const parameterValue =\n    props.getGlobalParameter && props.getGlobalParameter(parameterName) ? props.getGlobalParameter(parameterName) : '';\n  const parameterDisplayValue =\n    props.getGlobalParameter &&\n    props.getGlobalParameter(parameterDisplayName) &&\n    props.settings.overridePropertyDisplayName\n      ? props.getGlobalParameter(parameterDisplayName)\n      : parameterValue;\n  const setParameterValue = (value) => setGlobalParameter(parameterName, value);\n  const setParameterDisplayValue = (value) => setGlobalParameter(parameterDisplayName, value);\n  const allParameters = props.parameters;\n  const multiSelector = props?.settings?.multiSelector;\n  const multiline = props?.settings?.multiline;\n  const manualParameterSave = props?.settings?.manualParameterSave;\n  // in NeoDash 2.2.1 or earlier, there was no means to have a different display value in the selector. This condition handles that.\n  const compatibilityMode = !query?.toLowerCase().includes('as display') || false;\n\n  if (!query || query.trim().length == 0) {\n    return <p style={{ margin: '15px' }}>No selection specified.</p>;\n  }\n\n  const theme = createTheme({\n    typography: {\n      fontFamily: \"'Nunito Sans', sans-serif !important\",\n      allVariants: { color: 'rgb(var(--palette-neutral-text))' },\n    },\n    palette: {\n      text: {\n        primary: 'rgb(var(--palette-neutral-text))',\n      },\n      background: {\n        paper: 'rgb(var(--palette-neutral-bg-weak))',\n      },\n    },\n  });\n\n  const content = () => {\n    if (type == 'Free Text') {\n      return (\n        <FreeTextParameterSelectComponent\n          parameterName={parameterName}\n          parameterDisplayName={parameterName}\n          parameterValue={parameterValue}\n          parameterDisplayValue={parameterDisplayValue}\n          setParameterValue={(value) => {\n            setParameterValue(value);\n            props.updateReportSetting && props.updateReportSetting('typing', undefined);\n          }}\n          setParameterDisplayValue={setParameterDisplayValue}\n          query={query}\n          queryCallback={queryCallback}\n          onInputChange={() => {\n            props.updateReportSetting && props.updateReportSetting('typing', true);\n          }}\n          settings={props.settings}\n          allParameters={allParameters}\n          compatibilityMode={compatibilityMode}\n          manualParameterSave={manualParameterSave}\n          multiline={multiline}\n        />\n      );\n    } else if (type == 'Node Property') {\n      return (\n        <NodePropertyParameterSelectComponent\n          parameterName={parameterName}\n          parameterDisplayName={parameterName}\n          parameterValue={parameterValue}\n          parameterDisplayValue={parameterDisplayValue}\n          setParameterValue={setParameterValue}\n          setParameterDisplayValue={setParameterDisplayValue}\n          query={query}\n          queryCallback={queryCallback}\n          settings={props.settings}\n          allParameters={allParameters}\n          compatibilityMode={compatibilityMode}\n          multiSelector={multiSelector}\n          manualParameterSave={manualParameterSave}\n          autoSort={true}\n        />\n      );\n    } else if (type == 'Relationship Property') {\n      return (\n        <RelationshipPropertyParameterSelectComponent\n          parameterName={parameterName}\n          parameterDisplayName={parameterName}\n          parameterValue={parameterValue}\n          parameterDisplayValue={parameterDisplayValue}\n          setParameterValue={setParameterValue}\n          setParameterDisplayValue={setParameterDisplayValue}\n          query={query}\n          queryCallback={queryCallback}\n          settings={props.settings}\n          allParameters={allParameters}\n          compatibilityMode={compatibilityMode}\n          multiSelector={multiSelector}\n          manualParameterSave={manualParameterSave}\n          autoSort={true}\n        />\n      );\n    } else if (type == 'Date Picker') {\n      return (\n        <DatePickerParameterSelectComponent\n          parameterName={parameterName}\n          parameterDisplayName={parameterName}\n          parameterValue={parameterValue}\n          parameterDisplayValue={parameterDisplayValue}\n          setParameterValue={setParameterValue}\n          setParameterDisplayValue={setParameterDisplayValue}\n          query={query}\n          queryCallback={queryCallback}\n          settings={props.settings}\n          allParameters={allParameters}\n          compatibilityMode={compatibilityMode}\n          manualParameterSave={manualParameterSave}\n        />\n      );\n    } else if (type == 'Custom Query') {\n      return (\n        <QueryParameterSelectComponent\n          parameterName={parameterName}\n          parameterDisplayName={parameterName}\n          parameterValue={parameterValue}\n          parameterDisplayValue={parameterDisplayValue}\n          setParameterValue={setParameterValue}\n          setParameterDisplayValue={setParameterDisplayValue}\n          query={query}\n          queryCallback={queryCallback}\n          settings={props.settings}\n          allParameters={allParameters}\n          compatibilityMode={compatibilityMode}\n          multiSelector={multiSelector}\n          manualParameterSave={manualParameterSave}\n          autoSort={false}\n        />\n      );\n    }\n    return <div>Invalid Parameter Selector Type.</div>;\n  };\n  return <ThemeProvider theme={theme}>{content()}</ThemeProvider>;\n};\n\nexport default NeoParameterSelectionChart;\n"
  },
  {
    "path": "src/chart/parameter/component/DateParameterSelect.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { ParameterSelectProps } from './ParameterSelect';\nimport NeoDatePicker from '../../../component/field/DateField';\nimport dayjs from 'dayjs';\nimport { Date as Neo4jDate } from 'neo4j-driver-core/lib/temporal-types.js';\nimport { isCastableToNeo4jDate, isEmptyObject } from '../../ChartUtils';\n\nfunction castPropsToBoltDate(dict) {\n  if (isEmptyObject(dict)) {\n    return undefined;\n  }\n  return new Neo4jDate(dict.year, dict.month, dict.day);\n}\n\nfunction castPropsToJsDate(dict) {\n  if (isEmptyObject(dict)) {\n    return dayjs();\n  }\n  return dayjs(new Date(dict.year, dict.month - 1, dict.day));\n}\n\nconst DatePickerParameterSelectComponent = (props: ParameterSelectProps) => {\n  const defaultValue =\n    props.settings && props.settings.defaultValue && props.settings.defaultValue.length > 0\n      ? props.settings.defaultValue\n      : dayjs();\n\n  const [inputDate, setInputDate] = React.useState(castPropsToJsDate(props.parameterValue));\n  const label = props?.settings?.entityType ? props.settings.entityType : '';\n  const helperText = props?.settings?.helperText ? props.settings.helperText : '';\n  const clearParameterOnFieldClear =\n    props.settings && props.settings.clearParameterOnFieldClear ? props.settings.clearParameterOnFieldClear : false;\n  const disabled = props?.settings?.disabled ? props.settings.disabled : false;\n\n  const setParameterValue = (value) => {\n    props.setParameterValue(castPropsToBoltDate(value));\n  };\n  //\n  useEffect(() => {\n    setInputDate(castPropsToJsDate(props.parameterValue));\n  }, [props.parameterValue]);\n\n  // If the user hasn't typed, and the parameter value mismatches the input value --> it was changed externally --> refresh the input value.\n  if (inputDate && isCastableToNeo4jDate(inputDate) && !inputDate.isSame(castPropsToJsDate(props.parameterValue))) {\n    setInputDate(castPropsToJsDate(props.parameterValue));\n  }\n\n  return (\n    <div style={{ width: '100%' }}>\n      <NeoDatePicker\n        label={helperText ? helperText : label}\n        value={inputDate}\n        disabled={disabled}\n        onChange={(newValue) => {\n          setInputDate(newValue);\n\n          // Check whether the user has inputted a valid year. If not, do not update the parameter.\n          if (!newValue || isNaN(newValue.$y) || isNaN(newValue.$m) || isNaN(newValue.$d)) {\n            return;\n          }\n          if (newValue == null && clearParameterOnFieldClear) {\n            setParameterValue(Neo4jDate.fromStandardDate(defaultValue.toDate()));\n          } else if (newValue == null) {\n            setParameterValue(undefined);\n          } else if (newValue.isValid()) {\n            setParameterValue(Neo4jDate.fromStandardDate(newValue.toDate()));\n          }\n        }}\n      />\n    </div>\n  );\n};\n\nexport default DatePickerParameterSelectComponent;\n"
  },
  {
    "path": "src/chart/parameter/component/FreeTextParameterSelect.tsx",
    "content": "import { debounce, CircularProgress } from '@mui/material';\nimport React, { useCallback, useEffect } from 'react';\nimport { ParameterSelectProps } from './ParameterSelect';\nimport NeoField from '../../../component/field/Field';\nimport { SelectionConfirmationButton } from './SelectionConfirmationButton';\n\nconst FreeTextParameterSelectComponent = (props: ParameterSelectProps) => {\n  const { manualParameterSave } = props;\n  const setParameterTimeout =\n    props.settings && props.settings.setParameterTimeout ? props.settings.setParameterTimeout : 1000;\n  const defaultValue =\n    props.settings && props.settings.defaultValue && props.settings.defaultValue.length > 0\n      ? props.settings.defaultValue\n      : '';\n  const [inputText, setInputText] = React.useState(props.parameterValue);\n  const label = props.settings && props.settings.entityType ? props.settings.entityType : '';\n  const property = props.settings && props.settings.propertyType ? props.settings.propertyType : '';\n  const helperText = props.settings && props.settings.helperText ? props.settings.helperText : '';\n  const clearParameterOnFieldClear =\n    props.settings && props.settings.clearParameterOnFieldClear ? props.settings.clearParameterOnFieldClear : false;\n  const disabled = props?.settings?.disabled ? props.settings.disabled : false;\n  const [running, setRunning] = React.useState(false);\n  const [paramValueLocal, setParamValueLocal] = React.useState(null);\n\n  const setParameterValue = (value) => {\n    setRunning(false);\n    props.setParameterValue(value);\n  };\n  const debouncedSetParameterValue = useCallback(debounce(setParameterValue, setParameterTimeout), []);\n\n  const manualHandleParametersUpdate = () => {\n    handleParametersUpdate(paramValueLocal, false);\n  };\n\n  const handleParametersUpdate = (value, manual = false) => {\n    setParamValueLocal(value);\n\n    if (manual) {\n      return;\n    }\n\n    if (value == '') {\n      if (clearParameterOnFieldClear) {\n        debouncedSetParameterValue(undefined);\n      } else {\n        debouncedSetParameterValue(defaultValue);\n      }\n    } else {\n      debouncedSetParameterValue(value);\n    }\n  };\n\n  // If the user hasn't typed, and the parameter value mismatches the input value --> it was changed externally --> refresh the input value.\n  if (running == false && inputText !== props.parameterValue) {\n    setInputText(props.parameterValue);\n  }\n\n  return (\n    <div className={'n-flex n-flex-row n-flex-wrap n-items-center'} style={{ width: '100%', marginTop: '5px' }}>\n      <NeoField\n        key={'freetext'}\n        label={helperText ? helperText : `${label} ${property}`}\n        defaultValue={defaultValue}\n        value={inputText}\n        variant='outlined'\n        multiline={props.multiline}\n        placeholder={'Enter text here...'}\n        style={{\n          marginBottom: '20px',\n          marginRight: '10px',\n          marginLeft: '15px',\n          minWidth: `calc(100% - ${manualParameterSave ? '80' : '30'}px)`,\n          maxWidth: 'calc(100% - 30px)',\n        }}\n        disabled={disabled}\n        onChange={(newValue) => {\n          setRunning(true);\n          setInputText(newValue);\n          props.onInputChange && props.onInputChange(newValue);\n          handleParametersUpdate(newValue, manualParameterSave);\n        }}\n      />\n      {manualParameterSave ? <SelectionConfirmationButton onClick={() => manualHandleParametersUpdate()} /> : <></>}\n      {running && !manualParameterSave ? (\n        <CircularProgress size={18} style={{ position: 'absolute', right: '20px' }} />\n      ) : (\n        <></>\n      )}\n    </div>\n  );\n};\n\nexport default FreeTextParameterSelectComponent;\n"
  },
  {
    "path": "src/chart/parameter/component/NodePropertyParameterSelect.tsx",
    "content": "import React, { useCallback, useEffect } from 'react';\nimport { debounce, TextField } from '@mui/material';\nimport Autocomplete from '@mui/material/Autocomplete';\nimport { ParameterSelectProps } from './ParameterSelect';\nimport { RenderSubValue } from '../../../report/ReportRecordProcessing';\nimport { SelectionConfirmationButton } from './SelectionConfirmationButton';\nimport NeoCodeViewerComponent from '../../../component/editor/CodeViewerComponent';\nimport { getRecordType, toNumber } from '../../ChartUtils';\n\nconst NodePropertyParameterSelectComponent = (props: ParameterSelectProps) => {\n  const suggestionsUpdateTimeout =\n    props.settings && props.settings.suggestionsUpdateTimeout ? props.settings.suggestionsUpdateTimeout : 250;\n  const defaultValue =\n    props.settings && props.settings.defaultValue && props.settings.defaultValue.length > 0\n      ? props.settings.defaultValue\n      : '';\n\n  const disabled = props?.settings?.disabled ? props.settings.disabled : false;\n  const getInitialValue = (value, multi) => {\n    if (value && Array.isArray(value)) {\n      return multi ? value : null;\n    } else if (value) {\n      return multi ? [value] : value;\n    }\n    return multi ? [] : value;\n  };\n  const { multiSelector, manualParameterSave } = props;\n  const allParameters = props.allParameters ? props.allParameters : {};\n  const [extraRecords, setExtraRecords] = React.useState([]);\n\n  const [inputDisplayText, setInputDisplayText] = React.useState(\n    props.parameterDisplayValue && multiSelector ? '' : props.parameterDisplayValue\n  );\n  const [inputValue, setInputValue] = React.useState(getInitialValue(props.parameterDisplayValue, multiSelector));\n\n  const [paramValueLocal, setParamValueLocal] = React.useState(props.parameterValue);\n  const [paramValueDisplayLocal, setParamValueDisplayLocal] = React.useState(props.parameterDisplayValue);\n\n  const debouncedQueryCallback = useCallback(debounce(props.queryCallback, suggestionsUpdateTimeout), []);\n  const label = props.settings && props.settings.entityType ? props.settings.entityType : '';\n  const multiSelectLimit = props.settings && props.settings.multiSelectLimit ? props.settings.multiSelectLimit : 5;\n  const propertyType = props.settings && props.settings.propertyType ? props.settings.propertyType : '';\n  const helperText = props.settings && props.settings.helperText ? props.settings.helperText : '';\n  const clearParameterOnFieldClear =\n    props.settings && props.settings.clearParameterOnFieldClear ? props.settings.clearParameterOnFieldClear : false;\n  const autoSelectFirstValue =\n    props.settings && props.settings.autoSelectFirstValue ? props.settings.autoSelectFirstValue : false;\n\n  // index of the display value in the resulting extra records retrieved by the component when the user types. equals '1' for NeoDash 2.2.2 and later.\n  const displayValueRowIndex = props.compatibilityMode\n    ? 0\n    : extraRecords[0]?.keys?.findIndex((e) => e.toLowerCase() == 'display') || 0;\n\n  const realValueRowIndex = props.compatibilityMode ? 0 : 1 - displayValueRowIndex;\n\n  const manualHandleParametersUpdate = () => {\n    handleParametersUpdate(paramValueLocal, paramValueDisplayLocal, false);\n  };\n  const handleParametersUpdate = (value, displayValue, manual = false) => {\n    setParamValueLocal(value);\n    setParamValueDisplayLocal(displayValue);\n\n    if (manual) {\n      return;\n    }\n\n    props.setParameterValue(value);\n    props.setParameterDisplayValue(displayValue);\n  };\n  const handleCrossClick = (isMulti, value) => {\n    if (isMulti) {\n      if (value !== null && value.length == 0 && clearParameterOnFieldClear) {\n        setInputValue([]);\n        handleParametersUpdate(undefined, undefined, manualParameterSave);\n        return true;\n      }\n      if (value !== null && value.length == 0) {\n        setInputValue([]);\n        handleParametersUpdate([], [], manualParameterSave);\n        return true;\n      }\n    } else {\n      if (value && clearParameterOnFieldClear) {\n        setInputValue(null);\n        handleParametersUpdate(undefined, undefined, manualParameterSave);\n        return true;\n      }\n      if (value == null) {\n        setInputValue(null);\n        handleParametersUpdate(defaultValue, defaultValue, manualParameterSave);\n        return true;\n      }\n      return false;\n    }\n  };\n  const propagateSelection = (event, newDisplay) => {\n    const isMulti = Array.isArray(newDisplay);\n    if (handleCrossClick(isMulti, newDisplay)) {\n      return;\n    }\n    let newValue;\n    let valReference = manualParameterSave ? paramValueLocal : props.parameterValue;\n    let valDisplayReference = manualParameterSave ? paramValueDisplayLocal : props.parameterDisplayValue;\n    // Multiple and new entry\n    if (isMulti && inputValue !== null && newDisplay !== null && inputValue.length < newDisplay.length) {\n      newValue = Array.isArray(valReference)\n        ? [...valReference]\n        : valReference && valReference !== null\n        ? [valReference]\n        : [];\n      const newDisplayValue = [...newDisplay].slice(-1)[0];\n      let val = extraRecords.filter((r) => r._fields[displayValueRowIndex].toString() == newDisplayValue)[0]._fields[\n        realValueRowIndex\n      ];\n      if (newValue.low) {\n        newValue.push(toNumber(val));\n      } else {\n        newValue.push(RenderSubValue(val));\n      }\n    } else if (!isMulti) {\n      // if records are toStringed before comparison, toString the comparing variable\n      const newDisplay2 = typeof newDisplay === 'boolean' ? newDisplay.toString() : newDisplay;\n      newValue = extraRecords.filter((r) => (r?._fields?.[displayValueRowIndex]?.toString() || null) == newDisplay2)[0]\n        ._fields[realValueRowIndex];\n\n      newValue =\n        (newValue.low && newValue.low != null) || newValue.low === 0 ? toNumber(newValue) : RenderSubValue(newValue);\n    } else {\n      let ele = valDisplayReference.filter((x) => !newDisplay.includes(x))[0];\n      newValue = [...valReference];\n      newValue.splice(valDisplayReference.indexOf(ele), 1);\n    }\n\n    newDisplay = newDisplay.low ? toNumber(newDisplay) : RenderSubValue(newDisplay);\n    setInputDisplayText(isMulti ? '' : newDisplay);\n    setInputValue(newDisplay);\n    handleParametersUpdate(newValue, newDisplay, manualParameterSave);\n  };\n\n  // If we don't have an error message, render the selector:\n  useEffect(() => {\n    // Handle external updates of parameter values, with varying value types and parameter selector types.\n    // Handles multiple scenarios if an external parameter changes type from value to lists.\n    const isArray = Array.isArray(props.parameterDisplayValue);\n    if (multiSelector) {\n      if (isArray) {\n        setInputDisplayText(props.parameterDisplayValue);\n        setInputValue(props.parameterDisplayValue);\n      } else if (props.parameterDisplayValue !== '') {\n        setInputDisplayText([props.parameterDisplayValue]);\n        setInputValue([props.parameterDisplayValue]);\n      } else {\n        setInputDisplayText('');\n        setInputValue([]);\n      }\n    } else {\n      setInputDisplayText(props.parameterDisplayValue);\n      setInputValue(props.parameterDisplayValue);\n    }\n  }, [props.parameterDisplayValue]);\n\n  // The query used to populate the selector is invalid.\n  if (extraRecords && extraRecords[0] && extraRecords[0].error) {\n    return (\n      <NeoCodeViewerComponent\n        value={`The parameter value retrieval query is invalid: \\n${props.query}\\n\\nError message:\\n${extraRecords[0].error}`}\n      />\n    );\n  }\n\n  // \"false\" will not be mapped to \"(no data)\"\n  let options = extraRecords\n    ?.map((r) => r?._fields?.[displayValueRowIndex])\n    .map((f) => (f === undefined || f === null ? '(no data)' : f));\n  options = props.autoSort ? options.sort() : options;\n\n  return (\n    <div className={'n-flex n-flex-row n-flex-wrap n-items-center'}>\n      <Autocomplete\n        id='autocomplete'\n        multiple={multiSelector}\n        options={options}\n        disabled={disabled}\n        limitTags={multiSelectLimit}\n        style={{\n          maxWidth: 'calc(100% - 40px)',\n          minWidth: `calc(100% - ${manualParameterSave ? '60' : '30'}px)`,\n          marginLeft: '15px',\n          marginTop: '5px',\n        }}\n        inputValue={inputDisplayText.toString() || ''}\n        onInputChange={(event, value) => {\n          setInputDisplayText(value);\n          debouncedQueryCallback(props.query, { input: `${value}`, ...allParameters }, setExtraRecords);\n        }}\n        isOptionEqualToValue={(option, value) => {\n          return (option && option.toString()) === (value && value.toString());\n        }}\n        onOpen={() => {\n          if (extraRecords && extraRecords.length == 0) {\n            debouncedQueryCallback(props.query, { input: `${inputDisplayText}`, ...allParameters }, setExtraRecords);\n          }\n        }}\n        onBlur={() => {\n          // If the user loses focus of the selector, and nothing is selected\n          // We may want to auto-select the first value produced by the selector query (`autoSelectFirstValue == true`)\n          if (autoSelectFirstValue && paramValueDisplayLocal == '') {\n            debouncedQueryCallback(props.query, { input: '', ...allParameters }, (records) => {\n              if (records && records.length > 0 && records[0] && records[0]._fields) {\n                let values = records?.map((r) => r?._fields?.[displayValueRowIndex] || '(no data)');\n                values = props.autoSort ? values.sort() : values;\n                setExtraRecords(records);\n                propagateSelection(undefined, values[0]);\n              }\n            });\n          }\n        }}\n        value={inputValue || ''}\n        onChange={propagateSelection}\n        renderInput={(params) => (\n          <TextField\n            {...params}\n            InputLabelProps={{ shrink: true }}\n            placeholder='Start typing...'\n            label={helperText ? helperText : `${label} ${propertyType}`}\n            variant='outlined'\n          />\n        )}\n        getOptionLabel={(option) => option?.toString() || ''}\n      />\n      {manualParameterSave ? <SelectionConfirmationButton onClick={() => manualHandleParametersUpdate()} /> : <></>}\n    </div>\n  );\n};\n\nexport default NodePropertyParameterSelectComponent;\n"
  },
  {
    "path": "src/chart/parameter/component/ParameterSelect.ts",
    "content": "/**\n * Interface for all parameter selector components to implement.\n */\nexport interface ParameterSelectProps {\n  /**\n   * Name of the parameter (e.g. neodash_person_name)\n   */\n  parameterName: string;\n  /**\n   * Display name of the parameter (e.g. neodash_person_name_display) - used by the NeoDash engine exclusively.\n   */\n  parameterDisplayName: string;\n  /**\n   * Parameter value as defined in the global state. (e.g. \"Alfredo\" or 1234)\n   */\n  parameterValue: string | number | null;\n  /**\n   * The parameter value ***displayed*** in the selector when selecting the actual parameterValue.\n   */\n  parameterDisplayValue: string | number;\n  /**\n   * Callback to update the value in the global state.\n   */\n  setParameterValue: (value) => void;\n  /**\n   * Callback to update the display value in the global state.\n   */\n  setParameterDisplayValue: (value) => void;\n  /**\n   * Callback for when any character is typed, without neccesary saving / submission.\n   */\n  onInputChange?: (value) => void;\n  /**\n   * The query that can be used to retrieve parameter value suggestions from the database.\n   */\n  query: string | undefined;\n  /**\n   * Callback to query the database with a given set of parameters. Calls 'setRecords' upon completion.\n   */\n  queryCallback: (query: string | undefined, parameters: Record<string, any>, setRecords: any) => void;\n  /**\n   * The advanced settings for the parameter selector component.\n   */\n  settings: Record<string, any> | undefined;\n  /**\n   * A dictionary of all global dashboard parameters.\n   */\n  allParameters: Record<string, any> | undefined;\n  /**\n   * Create the parameter selector in compatibility mode for NeoDash 2.2.1 or earlier.\n   */\n  compatibilityMode: boolean;\n  /**\n   * Add the possibility for multiple selections\n   */\n  multiSelector?: boolean;\n  /**\n   * Add the possibility for users to insert multiple lines (freetext only)\n   */\n  multiline?: boolean;\n  /**\n   * Add the possibility for manual selection confirmation\n   */\n  manualParameterSave?: boolean;\n  /**\n   * Pass true if results should be sorted automatically\n   */\n  autoSort?: boolean;\n}\n"
  },
  {
    "path": "src/chart/parameter/component/QueryParameterSelect.tsx",
    "content": "import React from 'react';\nimport NodePropertyParameterSelectComponent from './NodePropertyParameterSelect';\nimport { ParameterSelectProps } from './ParameterSelect';\n\nconst QueryParameterSelectComponent = (props: ParameterSelectProps) => {\n  return (\n    <NodePropertyParameterSelectComponent\n      parameterName={props.parameterName}\n      parameterDisplayName={props.parameterDisplayName}\n      parameterValue={props.parameterValue}\n      parameterDisplayValue={props.parameterDisplayValue}\n      setParameterValue={props.setParameterValue}\n      setParameterDisplayValue={props.setParameterDisplayValue}\n      query={props.query}\n      queryCallback={props.queryCallback}\n      settings={props.settings}\n      allParameters={props.allParameters}\n      compatibilityMode={props.compatibilityMode}\n      multiSelector={props.multiSelector}\n      autoSort={props.autoSort}\n    />\n  );\n};\n\nexport default QueryParameterSelectComponent;\n"
  },
  {
    "path": "src/chart/parameter/component/RelationshipPropertyParameterSelect.tsx",
    "content": "import React from 'react';\nimport NodePropertyParameterSelectComponent from './NodePropertyParameterSelect';\nimport { ParameterSelectProps } from './ParameterSelect';\n\n/**\n * At the moment relationship property selectors are identical to node property selectors.\n * Therefore, just return the node component.\n */\nconst RelationshipPropertyParameterSelectComponent = (props: ParameterSelectProps) => {\n  return (\n    <NodePropertyParameterSelectComponent\n      parameterName={props.parameterName}\n      parameterDisplayName={props.parameterDisplayName}\n      parameterValue={props.parameterValue}\n      parameterDisplayValue={props.parameterDisplayValue}\n      setParameterValue={props.setParameterValue}\n      setParameterDisplayValue={props.setParameterDisplayValue}\n      query={props.query}\n      queryCallback={props.queryCallback}\n      settings={props.settings}\n      allParameters={props.allParameters}\n      compatibilityMode={props.compatibilityMode}\n      multiSelector={props.multiSelector}\n      manualParameterSave={props.manualParameterSave}\n    />\n  );\n};\n\nexport default RelationshipPropertyParameterSelectComponent;\n"
  },
  {
    "path": "src/chart/parameter/component/SelectionConfirmationButton.tsx",
    "content": "import React from 'react';\nimport { Tooltip } from '@mui/material';\nimport { PlayIconOutline } from '@neo4j-ndl/react/icons';\nimport { IconButton } from '@neo4j-ndl/react';\n/**\n * Returns a button to confirm a selection entry from the parameter selector.\n */\nexport const SelectionConfirmationButton = ({ onClick }) => {\n  return (\n    <Tooltip title={'Confirm'} disableInteractive>\n      <IconButton className='logo-btn n-p-1' aria-label={'btb-confirmation'} size='large' onClick={onClick} clean>\n        <PlayIconOutline className='header-icon' type='outline' />\n      </IconButton>\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "src/chart/pie/PieChart.tsx",
    "content": "import { ResponsivePie } from '@nivo/pie';\nimport React, { useEffect } from 'react';\nimport { NoDrawableDataErrorMessage } from '../../component/editor/CodeViewerComponent';\nimport { getD3ColorsByScheme } from '../../config/ColorConfig';\nimport { evaluateRulesOnDict, useStyleRules } from '../../extensions/styling/StyleRuleEvaluator';\nimport { ChartProps } from '../Chart';\nimport { convertRecordObjectToString, recordToNative } from '../ChartUtils';\nimport { themeNivo } from '../Utils';\nimport { extensionEnabled } from '../../utils/ReportUtils';\nimport { objMerge } from '../../utils/ObjectManipulation';\n\n/**\n * Embeds a PieChart (from Nivo) into NeoDash.\n */\nconst NeoPieChart = (props: ChartProps) => {\n  const { records } = props;\n  const { selection } = props;\n\n  if (!selection || props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  const buildFromRecords = (records) => {\n    let keys = {};\n    let dataRaw = records\n      .reduce((dataR: Record<string, any>[], row: Record<string, any>) => {\n        try {\n          if (!selection || !selection.index || !selection.value) {\n            return dataR;\n          }\n\n          const index = convertRecordObjectToString(row.get(selection.index));\n          const idx = dataR.findIndex((item) => item.index === index);\n          const key = selection.key !== '(none)' ? recordToNative(row.get(selection.key)) : selection.value;\n          const value = recordToNative(row.get(selection.value));\n\n          if (isNaN(value)) {\n            return dataR;\n          }\n          keys[key] = true;\n\n          if (idx > -1) {\n            data[idx][key] = value;\n          } else {\n            data.push({ id: index, label: index, value: value });\n          }\n\n          return data;\n        } catch (e) {\n          // eslint-disable-next-line no-console\n          console.error(e);\n          return [];\n        }\n      }, [])\n      .map((row) => {\n        Object.keys(keys).forEach((key) => {\n          // eslint-disable-next-line no-prototype-builtins\n          if (!row.hasOwnProperty(key)) {\n            row[key] = 0;\n          }\n        });\n\n        return row;\n      });\n    return dataRaw;\n  };\n\n  const [data, setData] = React.useState([]);\n\n  useEffect(() => {\n    setData(buildFromRecords(records));\n  }, [selection, records]);\n\n  const settings = props.settings ? props.settings : {};\n  const legendHeight = 20;\n  // TODO to retrieve all defaults from the ReportConfig.ts file instead of hardcoding them in the file\n  const marginRight = settings.marginRight ? settings.marginRight : 50;\n  const marginLeft = settings.marginLeft ? settings.marginLeft : 50;\n  const marginTop = settings.marginTop ? settings.marginTop : 50;\n  const marginBottom = settings.marginBottom ? settings.marginBottom : 50;\n  const sortByValue = settings.sortByValue ? settings.sortByValue : false;\n  const enableArcLabels = settings.enableArcLabels !== undefined ? settings.enableArcLabels : true;\n  const enableArcLinkLabels = settings.enableArcLinkLabels !== undefined ? settings.enableArcLinkLabels : true;\n  const interactive = settings.interactive ? settings.interactive : true;\n  const innerRadius = settings.innerRadius ? settings.innerRadius : 0;\n  const padAngle = settings.padAngle ? settings.padAngle : 0;\n  const borderWidth = settings.borderWidth ? settings.borderWidth : 0;\n  const activeOuterRadiusOffset = settings.activeOuterRadiusOffset ? settings.activeOuterRadiusOffset : 8;\n  const arcLinkLabelsOffset = settings.arcLinkLabelsOffset ? settings.arcLinkLabelsOffset : 15;\n  const arcLinkLabelsSkipAngle = settings.arcLinkLabelsSkipAngle ? settings.arcLinkLabelsSkipAngle : 1;\n  const cornerRadius = settings.cornerRadius ? settings.cornerRadius : 1;\n  const arcLabelsSkipAngle = settings.arcLabelsSkipAngle ? settings.arcLabelsSkipAngle : 10;\n\n  const arcLabelsFontSize = settings.arcLabelsFontSize ? settings.arcLabelsFontSize : 13;\n\n  const legend = settings.legend ? settings.legend : false;\n  const colorScheme = settings.colors ? settings.colors : 'set2';\n  const styleRules = useStyleRules(\n    extensionEnabled(props.extensions, 'styling'),\n    props.settings.styleRules,\n    props.getGlobalParameter\n  );\n\n  const chartColorsByScheme = getD3ColorsByScheme(colorScheme);\n\n  // Compute chart colors, based on default scheme and on styling rules\n  const computedChartColors = data.map((value, index) => {\n    let colorIndex = index;\n    if (index >= chartColorsByScheme.length) {\n      colorIndex = index % chartColorsByScheme.length;\n    }\n\n    const dict = {};\n    if (!props.selection) {\n      return chartColorsByScheme[colorIndex];\n    }\n\n    dict[props.selection.value] = value.value;\n    dict[props.selection.index] = value.id;\n    const validRuleIndex = evaluateRulesOnDict(dict, styleRules, ['slice color']);\n\n    if (validRuleIndex !== -1) {\n      return styleRules[validRuleIndex].customizationValue;\n    }\n\n    return chartColorsByScheme[colorIndex];\n  });\n\n  const getArcLabel = (item) => {\n    return `${((item.arc.angleDeg * 100) / 360).toFixed(2).toString()}%`;\n  };\n\n  if (data.length == 0) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  const theme = objMerge(themeNivo, {\n    labels: {\n      text: { fontSize: arcLabelsFontSize },\n    },\n  });\n  return (\n    <ResponsivePie\n      theme={theme}\n      data={data}\n      sortByValue={sortByValue}\n      enableArcLabels={enableArcLabels}\n      enableArcLinkLabels={enableArcLinkLabels}\n      isInteractive={interactive}\n      innerRadius={innerRadius}\n      padAngle={padAngle}\n      borderWidth={borderWidth}\n      activeOuterRadiusOffset={activeOuterRadiusOffset}\n      cornerRadius={cornerRadius}\n      arcLinkLabelsSkipAngle={arcLinkLabelsSkipAngle}\n      arcLinkLabelsOffset={arcLinkLabelsOffset}\n      arcLabelsSkipAngle={arcLabelsSkipAngle}\n      margin={{\n        top: marginTop,\n        right: marginRight,\n        bottom: legend ? legendHeight + marginBottom : marginBottom,\n        left: marginLeft,\n      }}\n      colors={computedChartColors}\n      legends={\n        legend\n          ? [\n              {\n                anchor: 'bottom',\n                direction: 'row',\n                justify: false,\n                translateX: 0,\n                translateY: 50,\n                itemsSpacing: 0,\n                itemWidth: 100,\n                itemHeight: 18,\n                itemDirection: 'left-to-right',\n                itemOpacity: 1,\n                symbolSize: 18,\n                symbolShape: 'circle',\n                effects: [\n                  {\n                    on: 'hover',\n                    style: {\n                      itemTextColor: '#000',\n                    },\n                  },\n                ],\n              },\n            ]\n          : []\n      }\n      animate={true}\n      // TODO : Needs to be set dynamic (default true on percentage)\n      arcLabel={getArcLabel}\n    />\n  );\n};\n\nexport default NeoPieChart;\n"
  },
  {
    "path": "src/chart/scatter/ScatterPlotChart.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { NoDrawableDataErrorMessage } from '../../component/editor/CodeViewerComponent';\nimport { ChartProps } from '../Chart';\nimport { recordToNative } from '../ChartUtils';\nimport { ResponsiveScatterPlot, ResponsiveScatterPlotCanvas } from '@nivo/scatterplot';\nimport { animated } from '@react-spring/web';\nimport chroma from 'chroma-js';\nimport { themeNivo } from '../Utils';\n\n/**\n * Embeds a Nivo ResponsiveScatterPlot and a ResponsiveScatterPlotCanvas into NeoDash.\n */\nconst NeoScatterPlot = (props: ChartProps) => {\n  const POSSIBLE_TIME_FORMATS = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'];\n\n  if (props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <>No data, re-run the report.</>;\n  }\n\n  const { records, selection } = props;\n\n  if (!selection || !selection.value || selection.value.length == 0) {\n    return <div style={{ margin: '15px' }}>No y-axis selected. To view the report, select a value below. </div>;\n  }\n\n  if (!selection.value.length) {\n    return <p></p>;\n  }\n\n  const [keepLegend, setKeepLegend] = React.useState(false);\n  const [isTimeChart, setIsTimeChart] = React.useState(false);\n  const [parseFormat, setParseFormat] = React.useState('%Y-%m-%dT%H:%M:%SZ');\n  const [validSelection, setValidSelection] = React.useState(true);\n  const [data, setData] = React.useState({\n    id: selection.value as string,\n    data: [] as any[],\n  });\n\n  const [intensities, setIntensities] = React.useState<number[]>([]);\n  const [legendRange, setLegendRange] = React.useState({ min: 1, max: 2 });\n\n  const settings = props.settings ? props.settings : {};\n\n  const colorIntensityProp = settings.colorIntensityProp != undefined ? settings.colorIntensityProp : 'intensity';\n  const labelProp = settings.labelProp != undefined ? settings.labelProp : 'label';\n  const colorScale = chroma.scale('Spectral');\n\n  const pointSize = settings.pointSize ? settings.pointSize : 10;\n  const showGrid = settings.showGrid != undefined ? settings.showGrid : true;\n  const showLegend = settings.legend !== undefined ? settings.legend : false;\n  const legendWidth = settings.legendWidth !== undefined ? settings.legendWidth : 20;\n  const xScale = settings.xScale ? settings.xScale : 'linear';\n  const yScale = settings.yScale ? settings.yScale : 'linear';\n\n  const xScaleLogBase = settings.xScaleLogBase ? settings.xScaleLogBase : 10;\n  const yScaleLogBase = settings.yScaleLogBase ? settings.yScaleLogBase : 10;\n\n  const minXValue = settings.minXValue ? settings.minXValue : 'auto';\n  const maxXValue = settings.maxXValue ? settings.maxXValue : 'auto';\n  const minYValue = settings.minYValue ? settings.minYValue : 'auto';\n  const maxYValue = settings.maxYValue ? settings.maxYValue : 'auto';\n\n  const marginRight = settings.marginRight ? settings.marginRight : 24;\n  const marginLeft = settings.marginLeft ? settings.marginLeft : 36;\n  const marginTop = settings.marginTop ? settings.marginTop : 24;\n  const marginBottom = settings.marginBottom ? settings.marginBottom : 40;\n\n  const xTickValues = settings.xTickValues != undefined ? settings.xTickValues : undefined;\n  const xTickTimeValues = settings.xTickTimeValues != undefined ? settings.xTickTimeValues : 'every 1 years';\n  const xAxisTimeFormat = settings.xAxisTimeFormat != undefined ? settings.xAxisTimeFormat : '%Y-%m-%dT%H:%M:%SZ';\n  const xAxisFormat = settings.xAxisFormat != undefined ? settings.xAxisFormat : undefined;\n  const xTickRotationAngle = settings.xTickRotationAngle != undefined ? settings.xTickRotationAngle : 0;\n  const yTickRotationAngle = settings.yTickRotationAngle != undefined ? settings.yTickRotationAngle : 0;\n\n  const isDate = (x) => {\n    return x.__isDate__;\n  };\n\n  const isDateTime = (x) => {\n    return x.__isDateTime__;\n  };\n\n  const isDateTimeOrDate = (x) => {\n    return isDate(x) || isDateTime(x) || x instanceof Date;\n  };\n\n  // Effect used to recalculate the values at each selection change.\n  // This prevents the app doing computations on each re-render.\n  useEffect(() => {\n    let key = selection.value as string;\n    let processed = {\n      id: key,\n      data: [] as any[],\n    };\n\n    let newIntensities: any[] = [];\n\n    records.forEach((row) => {\n      const intensity: any = row.keys.includes(colorIntensityProp)\n        ? recordToNative(row.get(colorIntensityProp))\n        : undefined;\n\n      const label: any = row.keys.includes(labelProp) ? recordToNative(row.get(labelProp)) : undefined;\n\n      if (intensity) {\n        newIntensities.push(intensity);\n        if (!keepLegend) {\n          setKeepLegend(true);\n        }\n      }\n\n      let x: number | Date = row.get(selection.x) || 0;\n      let y: any = recordToNative(row.get(key)) || 0;\n      if (!isNaN(y)) {\n        if (isDateTime(x)) {\n          x = new Date(x.toString());\n        }\n        processed.data.push({ x, y, intensity, label });\n      }\n    });\n\n    setData(processed);\n    setValidSelection(processed.data.length > 0);\n    setIntensities(newIntensities);\n  }, [records, selection]);\n\n  // Resetting the legend range\n  useEffect(() => {\n    let sortedIntensities = [...intensities].sort((a, b) => {\n      return a - b;\n    });\n\n    setLegendRange({ min: sortedIntensities[0], max: sortedIntensities.slice(-1)[0] });\n  }, [intensities]);\n\n  // Post-processing validation on the data --> confirm only numeric data was selected by the user.\n  if (!validSelection) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  // TODO - Nivo has a bug that, when we switch from a time-axis to a number axis, the visualization breaks.\n  // Therefore, we now require a manual refresh.\n  // TODO - check if this is still an issue with latest Nivo version.\n\n  const chartIsTimeChart =\n    data !== undefined &&\n    data.data !== undefined &&\n    data.data[0] !== undefined &&\n    data.data[0].x !== undefined &&\n    isDateTimeOrDate(data.data[0].x);\n\n  if (isTimeChart !== chartIsTimeChart) {\n    if (!chartIsTimeChart) {\n      return (\n        <div style={{ margin: '15px' }}>\n          Line chart switched from time-axis to number-axis. Please re-run the report to see your changes.\n        </div>\n      );\n    }\n\n    const p = chartIsTimeChart ? (isDateTime(data.data[0].x) ? '%Y-%m-%dT%H:%M:%SZ' : '%Y-%m-%d') : '';\n    setParseFormat(p);\n    setIsTimeChart(chartIsTimeChart);\n  }\n\n  const validateXTickTimeValues = xTickTimeValues.split(' ');\n  if (\n    validateXTickTimeValues.length != 3 ||\n    validateXTickTimeValues[0] != 'every' ||\n    !Number.isInteger(parseFloat(validateXTickTimeValues[1])) ||\n    parseFloat(validateXTickTimeValues[1]) <= 0 ||\n    !POSSIBLE_TIME_FORMATS.includes(validateXTickTimeValues[2])\n  ) {\n    return (\n      <code style={{ margin: '10px' }}>\n        Invalid tick size specification for time chart. Parameter value must be set to \"every [number] ['years',\n        'months', 'weeks', 'days', 'hours', 'seconds', 'milliseconds']\".\n      </code>\n    );\n  }\n\n  /**\n   * Color gradient showing the possible colors binded to the viz\n   * @returns Legend based on the intensity\n   */\n  const generateColorLegend = () => {\n    return (\n      <div\n        style={{\n          backgroundImage: `linear-gradient(0deg, ${[...Array(11)].map((_, i) => colorScale(i / 10)).join(', ')})`,\n          height: props.dimensions.height - marginBottom - marginTop - 100,\n          marginTop: 20,\n          marginBottom: 50,\n          marginRight: 10,\n          width: legendWidth,\n          float: 'right',\n        }}\n      ></div>\n    );\n  };\n\n  /**\n   * Given a point in input, gets a color using the colorPicker function based on chroma.js.\n   * @param node A point inside the chart\n   * @returns A color based on the intensity value on the node\n   */\n  const getNodeColor = (node) => {\n    let { intensity } = node.data;\n    if (isNaN(intensity)) {\n      return 'green';\n    }\n    // The input is normalized before passing it inside the colorPicker function\n    let value =\n      legendRange.max === legendRange.min ? 0.3 : (intensity - legendRange.min) / (legendRange.max - legendRange.min);\n\n    return colorScale(value).toString();\n  };\n\n  /**\n   * Component to render a custom node (necessary to bind colors to each node)\n   * @param node\n   * @returns Custom circle representing a node\n   */\n  const getCanvasNode = (node) => {\n    return (\n      <animated.circle\n        cx={node.style.x}\n        cy={node.style.y}\n        r={node.style.size.to((size) => size / 2)}\n        fill={getNodeColor(node.node)}\n      />\n    );\n  };\n\n  /**\n   * Used to render a node\n   * @param ctx\n   * @param node\n   */\n  const renderNode = (ctx, node) => {\n    ctx.beginPath();\n    ctx.arc(node.x, node.y, node.size / 2, 0, 2 * Math.PI);\n    ctx.fillStyle = getNodeColor(node);\n    ctx.fill();\n  };\n\n  /**\n   * Component to render a custom tooltip\n   * @param node Current selected node\n   * @returns Tooltip generated for that node\n   */\n  const generateTooltip = (node) => {\n    return (\n      <div style={{ color: 'black', background: 'white', border: '1px solid black', padding: '12px 16px' }}>\n        <strong>{node.data.label ? `${labelProp}: ${node.data.label} ` : ''}</strong>\n        <br />\n        <strong>{node.data.intensity ? `${colorIntensityProp}: ${node.data.intensity}` : ''}</strong>\n        {node.data.label ? <br /> : <></>}\n        {`x: ${node.formattedX}`}\n        <br />\n        {`y: ${node.formattedY}`}\n        <br />\n      </div>\n    );\n  };\n\n  // Fixing canvas bug, from https://github.com/plouc/nivo/issues/2162\n  HTMLCanvasElement.prototype.getBBox = function tooltipMapper() {\n    return { width: this.offsetWidth, height: this.offsetHeight };\n  };\n\n  // If the query returns too many nodes, pass to a Canvas verison of the chart (scales easier than a normal plot)\n  const ComponentType = data.data.length <= 50 ? ResponsiveScatterPlot : ResponsiveScatterPlotCanvas;\n\n  const scatterplot = (\n    <div\n      style={{\n        width: !keepLegend ? '100%' : props.dimensions.width - legendWidth - 10,\n        height: '100%',\n        float: 'left',\n        display: 'flex',\n      }}\n    >\n      <ComponentType\n        theme={themeNivo}\n        data={[data]}\n        key={`${selection.value}`}\n        xScale={\n          isTimeChart\n            ? { format: parseFormat, type: 'time' }\n            : xScale == 'linear'\n            ? { type: xScale, min: minXValue, max: maxXValue, stacked: false, reverse: false }\n            : { type: xScale, min: minXValue, max: maxXValue, constant: xScaleLogBase, base: xScaleLogBase }\n        }\n        xFormat={isTimeChart ? `time:${xAxisTimeFormat}` : xAxisFormat}\n        margin={{ top: marginTop, right: marginRight, bottom: marginBottom, left: marginLeft }}\n        yScale={\n          yScale == 'linear'\n            ? { type: yScale, min: minYValue, max: maxYValue, stacked: false, reverse: false }\n            : { type: yScale, min: minYValue, max: maxYValue, constant: xScaleLogBase, base: yScaleLogBase }\n        }\n        enableGridX={showGrid}\n        enableGridY={showGrid}\n        axisTop={null}\n        axisRight={null}\n        indexBy={'y'}\n        axisBottom={\n          isTimeChart\n            ? {\n                tickValues: xTickTimeValues,\n                tickSize: 5,\n                tickPadding: 5,\n                tickRotation: xTickRotationAngle,\n                format: xAxisTimeFormat,\n                legend: 'Time',\n                legendOffset: 36,\n                legendPosition: 'middle',\n              }\n            : {\n                orient: 'bottom',\n                tickSize: 6,\n                tickValues: xTickValues,\n                format: xAxisFormat,\n                tickRotation: xTickRotationAngle,\n                tickPadding: 12,\n              }\n        }\n        axisLeft={{\n          tickSize: 6,\n          tickPadding: 12,\n          tickRotation: yTickRotationAngle,\n        }}\n        nodeComponent={getCanvasNode}\n        nodeSize={pointSize}\n        pointBorderWidth={2}\n        pointBorderColor={{ from: 'serieColor' }}\n        pointLabelYOffset={-12}\n        tooltip={(node) => generateTooltip(node.node)}\n        renderNode={renderNode}\n      />\n    </div>\n  );\n\n  const visualization = (\n    <>\n      {scatterplot}\n      {showLegend && keepLegend ? generateColorLegend() : <></>}\n    </>\n  );\n\n  return visualization;\n};\n\nexport default NeoScatterPlot;\n"
  },
  {
    "path": "src/chart/single/SingleValueChart.tsx",
    "content": "import React from 'react';\nimport { ChartProps } from '../Chart';\nimport { renderValueByType } from '../../report/ReportRecordProcessing';\nimport { evaluateRulesOnNeo4jRecord } from '../../extensions/styling/StyleRuleEvaluator';\nimport YAML from 'yaml';\nimport { extensionEnabled } from '../../utils/ReportUtils';\n\n/**\n * Renders Neo4j records as their JSON representation.\n */\nconst NeoSingleValueChart = (props: ChartProps) => {\n  const { records } = props;\n  const fontSize = props.settings && props.settings.fontSize ? props.settings.fontSize : 64;\n  const color = props.settings && props.settings.color ? props.settings.color : 'NO-OP';\n  const format = props.settings && props.settings.format ? props.settings.format : 'auto';\n  const textAlign = props.settings && props.settings.textAlign ? props.settings.textAlign : 'left';\n  const verticalAlign = props.settings && props.settings.verticalAlign ? props.settings.verticalAlign : 'top';\n  const monospace = props.settings && props.settings.monospace !== undefined ? props.settings.monospace : false;\n  const styleRules =\n    extensionEnabled(props.extensions, 'styling') && props.settings && props.settings.styleRules\n      ? props.settings.styleRules\n      : [];\n\n  const dimensions = props.dimensions ? props.dimensions : { width: 100, height: 100 };\n  const reportHeight = dimensions.height - fontSize;\n\n  const value = records && records[0] && records[0]._fields && records[0]._fields[0] ? records[0]._fields[0] : '';\n\n  const createDisplayValue = (value) => {\n    if (format == 'json') {\n      return JSON.stringify(value, null, 2);\n    }\n    if (format == 'yml') {\n      return YAML.stringify(value, null, 2);\n    }\n    return renderValueByType(value);\n  };\n\n  return (\n    <div\n      style={{\n        height: reportHeight,\n        lineHeight: `${reportHeight}px`,\n        position: 'relative',\n        textAlign: textAlign,\n        marginLeft: '15px',\n        marginRight: '15px',\n      }}\n    >\n      <span\n        style={{\n          display: 'inline-block',\n          verticalAlign: verticalAlign,\n          whiteSpace: 'pre',\n          marginTop: verticalAlign == 'middle' ? '-72px' : '0px', // go to a \"true middle\", subtract header height.\n          fontSize: fontSize,\n          fontFamily: monospace ? 'monospace' : 'inherit',\n          lineHeight: `${fontSize + 8}px`,\n          color: evaluateRulesOnNeo4jRecord(records[0], 'text color', color, styleRules),\n        }}\n      >\n        {createDisplayValue(value)}\n      </span>\n    </div>\n  );\n};\n\nexport default NeoSingleValueChart;\n"
  },
  {
    "path": "src/chart/table/TableActionsHelper.ts",
    "content": "export const hasCheckboxes = (actionsRules) => {\n  let rules = actionsRules.filter((rule) => rule.condition && rule.condition == 'rowCheck');\n  return rules.length > 0;\n};\n\nexport const getCheckboxes = (actionsRules, rows, getGlobalParameter) => {\n  let rules = actionsRules.filter((rule) => rule.condition && rule.condition == 'rowCheck');\n  const params = rules.map((rule) => `neodash_${rule.customizationValue}`);\n  // See if any of the rows should be checked. This is the case when a parameter is already in the list of checked values.\n  let selection: number[] = [];\n  params.forEach((parameter, index) => {\n    const fieldName = rules[index].value;\n    const values = getGlobalParameter(parameter);\n\n    // If the parameter is an array (to be expected), iterate over it to find the rows to check.\n    if (Array.isArray(values)) {\n      values.forEach((value) => {\n        rows.forEach((row) => {\n          if (row[fieldName] == value) {\n            selection.push(row.id);\n          }\n        });\n      });\n    } else {\n      // Else (special case), still check the row if it's a single value parameter.\n      rows.forEach((row) => {\n        if (row[fieldName] == values) {\n          selection.push(row.id);\n        }\n      });\n    }\n  });\n  return [...new Set(selection)];\n};\n\nexport const updateCheckBoxes = (actionsRules, rows, selection, setGlobalParameter) => {\n  if (hasCheckboxes(actionsRules)) {\n    const selectedRows = rows.filter((row) => selection.includes(row.id));\n    console.log(selectedRows);\n    let rules = actionsRules.filter((rule) => rule.condition && rule.condition == 'rowCheck');\n    rules.forEach((rule) => {\n      const parameterValues = selectedRows.map((row) => row[rule.value]).filter((v) => v !== undefined);\n      setGlobalParameter(`neodash_${rule.customizationValue}`, parameterValues);\n      setGlobalParameter(`neodash_${rule.customizationValue}_display`, parameterValues);\n    });\n  }\n};\n"
  },
  {
    "path": "src/chart/table/TableChart.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { DataGrid, GridColumnVisibilityModel } from '@mui/x-data-grid';\nimport { ChartProps } from '../Chart';\nimport {\n  evaluateRulesOnDict,\n  evaluateSingleRuleOnDict,\n  generateClassDefinitionsBasedOnRules,\n  useStyleRules,\n} from '../../extensions/styling/StyleRuleEvaluator';\nimport { Tooltip, Snackbar } from '@mui/material';\nimport { downloadCSV } from '../ChartUtils';\nimport { getRendererForValue, rendererForType, RenderSubValue } from '../../report/ReportRecordProcessing';\n\nimport {\n  getRule,\n  executeActionRule,\n  getPageNumbersAndNamesList,\n  performActionOnElement,\n} from '../../extensions/advancedcharts/Utils';\n\nimport { IconButton } from '@neo4j-ndl/react';\nimport { CloudArrowDownIconOutline, XMarkIconOutline } from '@neo4j-ndl/react/icons';\nimport { ThemeProvider, createTheme } from '@mui/material/styles';\nimport Button from '@mui/material/Button';\nimport { extensionEnabled } from '../../utils/ReportUtils';\nimport { getCheckboxes, hasCheckboxes, updateCheckBoxes } from './TableActionsHelper';\n\nconst TABLE_ROW_HEIGHT = 52;\nconst HIDDEN_COLUMN_PREFIX = '__';\nconst theme = createTheme({\n  typography: {\n    fontFamily: \"'Nunito Sans', sans-serif !important\",\n    allVariants: { color: 'rgb(var(--palette-neutral-text-default))' },\n  },\n});\nconst fallbackRenderer = (value) => {\n  return JSON.stringify(value);\n};\n\nfunction renderAsButtonWrapper(renderer) {\n  return function renderAsButton(value) {\n    const outputValue = renderer(value, true);\n    // If there's nothing to be rendered, there's no button needed.\n    if (outputValue == '') {\n      return <></>;\n    }\n    return (\n      <Button style={{ width: '100%', marginLeft: '5px', marginRight: '5px' }} variant='contained' color='primary'>\n        {outputValue}\n      </Button>\n    );\n  };\n}\n\nfunction ApplyColumnType(column, value, asAction) {\n  const renderer = getRendererForValue(value);\n  const renderCell = asAction ? renderAsButtonWrapper(renderer.renderValue) : renderer.renderValue;\n  const columnProperties = renderer\n    ? { type: renderer.type, renderCell: renderCell ? renderCell : fallbackRenderer }\n    : rendererForType.string;\n  if (columnProperties) {\n    column = { ...column, ...columnProperties };\n  }\n  return column;\n}\n\nexport const generateSafeColumnKey = (key) => {\n  return key != 'id' ? key : `${key} `;\n};\n\nexport const NeoTableChart = (props: ChartProps) => {\n  const transposed = props.settings && props.settings.transposed ? props.settings.transposed : false;\n  const wrapContent = props.settings && props.settings.wrapContent ? props.settings.wrapContent : false;\n  const allowDownload =\n    props.settings && props.settings.allowDownload !== undefined ? props.settings.allowDownload : false;\n\n  const actionsRules =\n    extensionEnabled(props.extensions, 'actions') && props.settings && props.settings.actionsRules\n      ? props.settings.actionsRules\n      : [];\n  const compact = props.settings && props.settings.compact !== undefined ? props.settings.compact : false;\n  const styleRules = useStyleRules(\n    extensionEnabled(props.extensions, 'styling'),\n    props.settings.styleRules,\n    props.getGlobalParameter\n  );\n\n  const [notificationOpen, setNotificationOpen] = React.useState(false);\n  const [columnVisibilityModel, setColumnVisibilityModel] = React.useState<GridColumnVisibilityModel>({});\n\n  const useStyles = generateClassDefinitionsBasedOnRules(styleRules);\n  const classes = useStyles();\n  const tableRowHeight = compact ? TABLE_ROW_HEIGHT / 2 : TABLE_ROW_HEIGHT;\n\n  const columnWidthsType =\n    props.settings && props.settings.columnWidthsType ? props.settings.columnWidthsType : 'Relative (%)';\n  let columnWidths = null;\n  try {\n    columnWidths = props.settings && props.settings.columnWidths && JSON.parse(props.settings.columnWidths);\n  } catch (e) {\n    // do nothing\n  } finally {\n    // do nothing\n  }\n\n  const { records } = props;\n\n  const generateSafeColumnKey = (key) => {\n    return key != 'id' ? key : `${key} `;\n  };\n\n  const actionableFields = actionsRules.filter((r) => r.condition !== 'rowCheck').map((r) => r.field);\n  const columns = transposed\n    ? [records[0].keys[0]].concat(records.map((record) => record._fields[0]?.toString() || '')).map((key, i) => {\n        const uniqueKey = `${String(key)}_${i}`;\n        return ApplyColumnType(\n          {\n            key: `col-key-${i}`,\n            field: generateSafeColumnKey(uniqueKey),\n            headerName: generateSafeColumnKey(key),\n            headerClassName: 'table-small-header',\n            disableColumnSelector: true,\n            flex: columnWidths && i < columnWidths.length ? columnWidths[i] : 1,\n            disableClickEventBubbling: true,\n          },\n          key,\n          actionableFields.includes(key)\n        );\n      })\n    : records[0] &&\n      records[0].keys &&\n      records[0].keys.map((key, i) => {\n        const value = records[0].get(key);\n        if (columnWidthsType == 'Relative (%)') {\n          return ApplyColumnType(\n            {\n              key: `col-key-${i}`,\n              field: generateSafeColumnKey(key),\n              headerName: generateSafeColumnKey(key),\n              headerClassName: 'table-small-header',\n              disableColumnSelector: true,\n              flex: columnWidths && i < columnWidths.length ? columnWidths[i] : 1,\n              disableClickEventBubbling: true,\n            },\n            value,\n            actionableFields.includes(key)\n          );\n        }\n        return ApplyColumnType(\n          {\n            key: `col-key-${i}`,\n            field: generateSafeColumnKey(key),\n            headerName: generateSafeColumnKey(key),\n            headerClassName: 'table-small-header',\n            disableColumnSelector: true,\n            width: columnWidths && i < columnWidths.length ? columnWidths[i] : 100,\n            disableClickEventBubbling: true,\n          },\n          value,\n          actionableFields.includes(key)\n        );\n      });\n\n  useEffect(() => {\n    const hiddenColumns = Object.assign(\n      {},\n      ...columns.filter((x) => x.field.startsWith(HIDDEN_COLUMN_PREFIX)).map((x) => ({ [x.field]: false }))\n    );\n    setColumnVisibilityModel(hiddenColumns);\n  }, [records]);\n\n  if (props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <>No data, re-run the report.</>;\n  }\n  const getTransposedRows = (records) => {\n    // Skip first key\n    const rowKeys = [...records[0].keys];\n    rowKeys.shift();\n\n    // Add values in rows\n    const rowsWithValues = rowKeys.map((key, i) =>\n      Object.assign(\n        { id: i, Field: key },\n        ...records.map((record, j) => ({\n          // Note the true here is for the rendered to know we are inside a transposed table\n          // It will be needed for rendering the records properly, if they are arrays\n          [`${record._fields[0]}_${j + 1}`]: RenderSubValue(record._fields[i + 1], true),\n        }))\n      )\n    );\n\n    // Add field in rows\n    const rowsWithFieldAndValues = rowsWithValues.map((row, i) => ({\n      ...row,\n      [`${records[0].keys[0]}_${0}`]: rowKeys[i],\n    }));\n\n    return rowsWithFieldAndValues;\n  };\n\n  const rows = transposed\n    ? getTransposedRows(records)\n    : records.map((record, rownumber) => {\n        return Object.assign(\n          { id: rownumber },\n          ...record._fields.map((field, i) => ({ [generateSafeColumnKey(record.keys[i])]: field }))\n        );\n      });\n\n  const pageNames = getPageNumbersAndNamesList();\n  const customStyles = { '&.MuiDataGrid-root .MuiDataGrid-footerContainer > div': { marginTop: '0px' } };\n\n  const commonGridProps = {\n    key: 'tableKey',\n    columnHeaderHeight: 32,\n    rowHeight: tableRowHeight,\n    rows: rows,\n    columns: columns,\n    pageSizeOptions: [5, 10, 25, 50, 100],\n    initialState: {\n      pagination: {\n        paginationModel: {\n          pageSize: 5,\n          pageIndex: 0,\n        },\n      },\n    },\n    columnVisibilityModel: columnVisibilityModel,\n    onColumnVisibilityModelChange: (newModel) => setColumnVisibilityModel(newModel),\n    onCellClick: (e) => performActionOnElement(e, actionsRules, { ...props, pageNames: pageNames }, 'Click', 'Table'),\n    onCellDoubleClick: (e) => {\n      let rules = getRule(e, actionsRules, 'doubleClick');\n      if (rules !== null) {\n        rules.forEach((rule) => executeActionRule(rule, e, { ...props, pageNames: pageNames }, 'table'));\n      } else {\n        setNotificationOpen(true);\n        navigator.clipboard.writeText(e.value);\n      }\n    },\n    checkboxSelection: hasCheckboxes(actionsRules),\n    rowSelectionModel: getCheckboxes(actionsRules, rows, props.getGlobalParameter),\n    onRowSelectionModelChange: (selection) => updateCheckBoxes(actionsRules, rows, selection, props.setGlobalParameter),\n    disableRowSelectionOnClick: true,\n    components: {\n      ColumnSortedDescendingIcon: () => <></>,\n      ColumnSortedAscendingIcon: () => <></>,\n    },\n    getRowClassName: (params) => {\n      return ['row color', 'row text color']\n        .map((e) => {\n          return `rule${evaluateRulesOnDict(params.row, styleRules, [e])}`;\n        })\n        .join(' ');\n    },\n    getCellClassName: (params) => {\n      return ['cell color', 'cell text color']\n        .map((e) => {\n          let validRuleClass = '';\n          for (const [index, rule] of styleRules.entries()) {\n            let ruleClass = '';\n            // If the rule target is not the current cell\n            if (rule.targetField) {\n              if (rule.targetField === params.field) {\n                ruleClass = `rule${evaluateSingleRuleOnDict({ [rule.field]: params.row[rule.field] }, rule, index, [\n                  e,\n                ])}`;\n              }\n            }\n            // If the rule target is the current cell\n            else {\n              ruleClass = `rule${evaluateSingleRuleOnDict({ [params.field]: params.value }, rule, index, [e])}`;\n            }\n            // If rule class is valid (rule-1 means rule check has failed)\n            if (ruleClass && ruleClass !== 'rule-1') {\n              validRuleClass = ruleClass;\n              break;\n            }\n          }\n          return validRuleClass;\n        })\n        .join(' ');\n    },\n  };\n\n  return (\n    <ThemeProvider theme={theme}>\n      <div className={classes.root} style={{ height: '100%', width: '100%', position: 'relative' }}>\n        <Snackbar\n          anchorOrigin={{\n            vertical: 'bottom',\n            horizontal: 'center',\n          }}\n          open={notificationOpen}\n          autoHideDuration={2000}\n          onClose={() => setNotificationOpen(false)}\n          message='Value copied to clipboard.'\n          action={\n            <React.Fragment>\n              <IconButton\n                size='small'\n                aria-label='close'\n                color='inherit'\n                onClick={() => setNotificationOpen(false)}\n                clean\n              >\n                <XMarkIconOutline />\n              </IconButton>\n            </React.Fragment>\n          }\n        />\n\n        {allowDownload && rows && rows.length > 0 ? (\n          <Tooltip title='Download CSV' aria-label='' disableInteractive>\n            <IconButton\n              onClick={() => {\n                downloadCSV(rows);\n              }}\n              aria-label='download csv'\n              className='n-absolute n-z-10 n-bottom-2 n-left-1'\n              clean\n            >\n              <CloudArrowDownIconOutline />\n            </IconButton>\n          </Tooltip>\n        ) : (\n          <></>\n        )}\n\n        {wrapContent ? (\n          <DataGrid\n            {...commonGridProps}\n            getRowHeight={() => 'auto'}\n            sx={{\n              ...customStyles,\n              '&.MuiDataGrid-root .MuiDataGrid-cell': { wordBreak: 'break-word' },\n            }}\n          />\n        ) : (\n          <DataGrid {...commonGridProps} sx={customStyles} />\n        )}\n      </div>\n    </ThemeProvider>\n  );\n};\n\nexport default NeoTableChart;\n"
  },
  {
    "path": "src/component/editor/CodeEditorComponent.tsx",
    "content": "import React from 'react';\nimport { CypherEditor, CypherEditorProps } from '@neo4j-cypher/react-codemirror';\nimport { markdown, markdownLanguage } from '@codemirror/lang-markdown';\n\nexport const DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE = {\n  color: 'grey',\n  fontSize: 12,\n  paddingLeft: '5px',\n  borderBottom: '1px solid lightgrey',\n  borderLeft: '1px solid lightgrey',\n  borderRight: '1px solid lightgrey',\n  marginTop: '0px',\n};\n\nconst markdownExtensions = [\n  markdown({\n    base: markdownLanguage, // Support GFM\n    // codeLanguages: languages\n  }),\n];\n\nconst NeoCodeEditorComponent = ({\n  value,\n  onChange,\n  placeholder,\n  editable = true,\n  language = 'cypher',\n  onExecute = () => {},\n  style = { border: '1px solid lightgray' },\n}) => {\n  const [keys, setKeys] = React.useState({});\n\n  const editorProps: CypherEditorProps = {\n    cypherLanguage: language === 'cypher',\n    readOnly: !editable,\n    placeholder: placeholder,\n    preExtensions: language === 'markdown' ? markdownExtensions : [],\n    value: value,\n\n    // This is a check to discover whether a user wants to run the report with a shortcut (CTRL/CMD + Enter)\n    onKeyDown: (e) => {\n      const newKeys = keys;\n      newKeys[e.key] = true;\n      setKeys(newKeys);\n      if ((newKeys.Control && newKeys.Enter) || (newKeys.Meta && newKeys.Enter)) {\n        onExecute();\n        setKeys({});\n      }\n      return undefined;\n    },\n    onKeyUp: (e) => {\n      const newKeys = keys;\n      delete newKeys[e.key];\n      setKeys(newKeys);\n      return undefined;\n    },\n    onValueChanged: (val) => {\n      if (editable && onChange) {\n        onChange(val);\n      }\n    },\n  };\n\n  // className 'ReactCodeMirror', only used by integration tests\n  return (\n    <div style={style}>\n      <CypherEditor className='ndl-cypher-editor ReactCodeMirror' {...editorProps} />\n    </div>\n  );\n};\n\nexport default NeoCodeEditorComponent;\n"
  },
  {
    "path": "src/component/editor/CodeViewerComponent.tsx",
    "content": "import { TextareaAutosize } from '@mui/material';\nimport React from 'react';\n\n/**\n * Returns a static code block, without line numbers.\n */\nconst NeoCodeViewerComponent = ({ value = '', placeholder = '' }) => {\n  return (\n    <div\n      className={'n-text-palette-neutral-text-default'}\n      style={{ overflowY: 'auto', marginLeft: '10px', marginRight: '10px', height: '100%' }}\n    >\n      <TextareaAutosize\n        style={{\n          width: '100%',\n          overflowY: 'hidden',\n          scrollbarWidth: 'auto',\n          paddingLeft: '10px',\n          background: 'none',\n          overflow: 'scroll !important',\n          marginTop: '5px',\n          border: '1px solid lightgray',\n        }}\n        className={'textinput-linenumbers'}\n        aria-label=''\n        value={value}\n        placeholder={placeholder}\n      />\n    </div>\n  );\n};\n\nexport const NoDrawableDataErrorMessage = () => {\n  return (\n    <NeoCodeViewerComponent\n      value={\n        'Data was returned, but it can not be visualized.\\n\\n' +\n        'This could have one of the following causes:\\n' +\n        '- a numeric value field was selected, but no numeric values were returned. \\n' +\n        '- a numeric value field was selected, but only zeroes were returned.\\n' +\n        '- an array field was selected, but no array was returned.\\n' +\n        '- Your visualization expects nodes/relationships, but none were returned.\\n\\n' +\n        'View the NeoDash documentation for more information.'\n      }\n    />\n  );\n};\n\nexport default NeoCodeViewerComponent;\n"
  },
  {
    "path": "src/component/field/ColorPicker.tsx",
    "content": "import React from 'react';\nimport { ColorPicker } from 'mui-color';\n\nconst NeoColorPicker = ({ value, onChange, defaultValue, label, style }) => {\n  return (\n    <div style={style}>\n      <div className='ndl-form-item ndl-type-text ndl-small'>\n        <label htmlFor={label} className='ndl-form-item-label ndl-fluid'>\n          <div className='ndl-input-wrapper'>\n            <ColorPicker\n              key={label}\n              defaultValue={defaultValue}\n              value={value}\n              onChange={(e) => {\n                if (e?.css?.backgroundColor) {\n                  onChange(e?.css?.backgroundColor);\n                }\n                if (typeof e === 'string' || e instanceof String) {\n                  onChange(e);\n                }\n              }}\n            />\n          </div>\n          <div className='ndl-form-item-wrapper'>\n            <span className='ndl-form-label-text'>{label}</span>\n          </div>\n        </label>\n      </div>\n    </div>\n  );\n};\n\nexport default NeoColorPicker;\n"
  },
  {
    "path": "src/component/field/DateField.tsx",
    "content": "import React from 'react';\nimport TextField from '@mui/material/TextField';\nimport { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';\nimport { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';\nimport { DesktopDatePicker } from '@mui/x-date-pickers';\n\nconst NeoDatePicker = ({ label, value, onChange, disabled = false }) => {\n  return (\n    <LocalizationProvider dateAdapter={AdapterDayjs}>\n      <DesktopDatePicker\n        label={label}\n        inputFormat='YYYY-MM-DD'\n        value={value}\n        disabled={disabled}\n        onChange={(event) => {\n          onChange(event);\n        }}\n        maxDate={new Date('9999-12-31')}\n        renderInput={(params) => (\n          <TextField\n            variant='outlined'\n            style={{ width: 'calc(100% - 30px)', marginLeft: '15px', marginTop: '5px' }}\n            {...params}\n          />\n        )}\n      />\n    </LocalizationProvider>\n  );\n};\n\nexport default NeoDatePicker;\n"
  },
  {
    "path": "src/component/field/Field.tsx",
    "content": "import React from 'react';\nimport { Dropdown, TextInput, Textarea } from '@neo4j-ndl/react';\n\nconst textFieldStyle = { width: '155px', marginBottom: '10px', marginRight: '10px', marginLeft: '10px' };\n\nconst NeoField = ({\n  label,\n  valueLabel,\n  value,\n  style = textFieldStyle,\n  choices = [],\n  onChange,\n  onClick = () => {},\n  numeric = false,\n  select = false,\n  disabled = undefined,\n  variant = undefined,\n  password = false,\n  helperText = undefined,\n  defaultValueLabel = undefined,\n  defaultValue = undefined,\n  multiline = false,\n  placeholder = '',\n  size = 'small',\n}) => {\n  return select === true ? (\n    <div style={style}>\n      <Dropdown\n        label={label}\n        type='select'\n        selectProps={{\n          options: choices,\n          onChange: (newValue) => onChange(newValue.value),\n          value:\n            value != null ? { label: valueLabel, value: value } : { label: defaultValueLabel, value: defaultValue },\n          menuPlacement: 'auto',\n          isDisabled: disabled,\n          menuPortalTarget: document.querySelector('#overlay'),\n        }}\n        helpText={helperText}\n        placeholder={placeholder}\n        size={size}\n      ></Dropdown>\n    </div>\n  ) : multiline === true ? (\n    <div style={style}>\n      <Textarea\n        key={label}\n        variant={variant}\n        label={label}\n        helpText={helperText}\n        disabled={disabled}\n        value={value != null ? value : defaultValue}\n        fluid\n        onClick={(e) => {\n          onClick(e.target.textContent);\n        }}\n        onChange={(event) => {\n          if (!numeric) {\n            onChange(event.target.value);\n          } else if (\n            event.target.value.toString().length == 0 ||\n            event.target.value.endsWith('.') ||\n            event.target.value.startsWith('-')\n          ) {\n            onChange(event.target.value);\n          } else if (!isNaN(event.target.value)) {\n            onChange(Number(event.target.value));\n          }\n        }}\n        placeholder={placeholder}\n        size={size}\n      ></Textarea>\n    </div>\n  ) : (\n    <div style={style}>\n      <TextInput\n        key={label}\n        variant={variant}\n        label={label}\n        helpText={helperText}\n        type={password ? 'password' : 'text'}\n        disabled={disabled}\n        value={value != null ? value : defaultValue}\n        fluid\n        onClick={(e) => {\n          onClick(e.target.textContent);\n        }}\n        onChange={(event) => {\n          if (!numeric) {\n            onChange(event.target.value);\n          } else if (\n            event.target.value.toString().length == 0 ||\n            event.target.value.endsWith('.') ||\n            event.target.value.startsWith('-')\n          ) {\n            onChange(event.target.value);\n          } else if (!isNaN(event.target.value)) {\n            onChange(Number(event.target.value));\n          }\n        }}\n        placeholder={placeholder}\n        size={size}\n      ></TextInput>\n    </div>\n  );\n};\n\nexport default NeoField;\n"
  },
  {
    "path": "src/component/field/Setting.tsx",
    "content": "import React from 'react';\nimport NeoField from './Field';\nimport { categoricalColorSchemes } from '../../config/ColorConfig';\nimport NeoColorPicker from './ColorPicker';\nimport { SELECTION_TYPES } from '../../config/CardConfig';\nimport { Label } from '@neo4j-ndl/react';\n\nconst generateListItem = (label, option) => {\n  if (typeof option === 'boolean') {\n    return { label: option ? 'on' : 'off', value: Boolean(option) };\n  }\n  if (label == 'Color Scheme' || label == 'Node Color Scheme') {\n    const colorsFull = categoricalColorSchemes[option];\n    const colors = Array.isArray(colorsFull)\n      ? Array.isArray(colorsFull.slice(-1)[0])\n        ? colorsFull.slice(-1)[0]\n        : colorsFull\n      : colorsFull;\n    return {\n      label: Array.isArray(colors) ? (\n        <>\n          {colors.map((element) => {\n            return (\n              <span\n                key={element}\n                style={{\n                  display: 'inline-block',\n                  background: element,\n                  width: '18px',\n                  height: '18px',\n                  position: 'relative',\n                  top: '3px',\n                }}\n              ></span>\n            );\n          })}\n          <Label color='info' fill='outlined' className='n-inline-block n-ml-2'>\n            {option}\n          </Label>\n        </>\n      ) : (\n        `${option}`\n      ),\n      value: option,\n    };\n  }\n  return { label: option, value: option };\n};\n\nconst generateValueLabel = (option) => {\n  if (typeof option === 'boolean') {\n    return option ? 'on' : 'off';\n  }\n  return option;\n};\n\nconst generateValue = (option) => {\n  if (typeof option === 'boolean') {\n    return Boolean(option);\n  }\n  return option;\n};\n\n/**\n * A setting is a generic React component that is rendered dynamically based on the 'type'.\n */\nconst NeoSetting = ({\n  value,\n  choices,\n  type,\n  label,\n  defaultValue,\n  disabled = undefined,\n  helperText = undefined,\n  password = false,\n  onChange,\n  onClick = () => {},\n  style = { width: '100%', marginBottom: '10px', marginRight: '10px', marginLeft: '10px' },\n}) => {\n  switch (type) {\n    case SELECTION_TYPES.NUMBER:\n      return (\n        <div key={label} style={{ width: '100%', paddingRight: '28px' }}>\n          <NeoField\n            label={label}\n            numeric={true}\n            key={label}\n            value={value}\n            disabled={disabled}\n            helperText={helperText}\n            defaultValue={''}\n            placeholder={`${defaultValue}`}\n            style={style}\n            onClick={(val) => onClick(val)}\n            onChange={(val) => onChange(val)}\n          />\n        </div>\n      );\n    case SELECTION_TYPES.TEXT:\n      return (\n        <div key={label} style={{ width: '100%', paddingRight: '28px' }}>\n          <NeoField\n            label={label}\n            key={label}\n            disabled={disabled}\n            helperText={helperText}\n            value={value}\n            password={password}\n            defaultValue={''}\n            placeholder={`${defaultValue}`}\n            style={style}\n            onClick={(val) => onClick(val)}\n            onChange={(val) => onChange(val)}\n          />\n        </div>\n      );\n    case SELECTION_TYPES.MULTILINE_TEXT:\n      return (\n        <div key={label} style={{ width: '100%', paddingRight: '28px' }}>\n          <NeoField\n            label={label}\n            key={label}\n            disabled={disabled}\n            helperText={helperText}\n            value={value}\n            defaultValue={''}\n            placeholder={`${defaultValue}`}\n            multiline={true}\n            style={style}\n            onClick={(val) => onClick(val)}\n            onChange={(val) => onChange(val)}\n          />\n        </div>\n      );\n    case SELECTION_TYPES.DICTIONARY:\n      return (\n        <div key={label} style={{ width: '100%', paddingRight: '28px' }}>\n          <NeoField\n            label={label}\n            key={label}\n            disabled={disabled}\n            helperText={helperText}\n            value={JSON.stringify(value)}\n            defaultValue={''}\n            placeholder={defaultValue ? `${JSON.stringify(defaultValue)}` : '{}'}\n            style={style}\n            onClick={(val) => onClick(val)}\n            onChange={(val) => onChange(val)}\n          />\n        </div>\n      );\n    case SELECTION_TYPES.LIST:\n      return (\n        <div key={label} style={{ width: '100%', paddingRight: '28px' }}>\n          <NeoField\n            select\n            label={label}\n            disabled={disabled}\n            helperText={helperText}\n            key={label}\n            valueLabel={generateValueLabel(value)}\n            value={generateValue(value)}\n            defaultValueLabel={generateValueLabel(defaultValue)}\n            defaultValue={generateValue(defaultValue)}\n            style={style}\n            choices={choices.map((option) => generateListItem(label, option))}\n            onClick={(val) => onClick(val)}\n            onChange={(val) => onChange(val)}\n          />\n        </div>\n      );\n    case SELECTION_TYPES.COLOR:\n      return (\n        <div key={label} style={{ width: '100%', paddingRight: '28px' }}>\n          <NeoColorPicker\n            label={label}\n            key={label}\n            disabled={disabled}\n            defaultValue={defaultValue}\n            value={value}\n            onClick={(val) => onClick(val)}\n            onChange={(val) => onChange(val)}\n            style={style}\n          ></NeoColorPicker>\n        </div>\n      );\n    default:\n      return <div key={label}></div>;\n  }\n};\n\nexport default NeoSetting;\n"
  },
  {
    "path": "src/component/misc/DashboardConnectionUpdateHandler.tsx",
    "content": "import { useConnection } from 'use-neo4j';\nimport React from 'react';\nimport isEqual from 'lodash.isequal';\n/**\n * Updates the Neo4j context when noticing an update in the global connection state.\n * TODO - there's probably a better way to do this, but I'm not sure how at the moment.\n */\nconst NeoDashboardConnectionUpdateHandler = ({ pagenumber, connection, onConnectionUpdate }) => {\n  const [existingConnection, setExistingConnection] = React.useState(null);\n  if (!isEqual(connection, existingConnection)) {\n    // Only trigger connection settings refreshes if the connection was once set before.\n    if (existingConnection != null) {\n      useConnection(connection.protocol, connection.url, connection.port, connection.username, connection.password);\n      onConnectionUpdate(pagenumber);\n    }\n    setExistingConnection(connection);\n  }\n  return <div></div>;\n};\n\nexport default NeoDashboardConnectionUpdateHandler;\n"
  },
  {
    "path": "src/component/sso/SSOLoginButton.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { SSOProviderOriginal, authRequestForSSO } from 'neo4j-client-sso';\nimport { getDiscoveryDataInfo } from './SSOUtils';\nimport { ShieldCheckIconOutline } from '@neo4j-ndl/react/icons';\nimport { Button, IconButton } from '@neo4j-ndl/react';\n\nexport const SSOLoginButton = ({ discoveryAPIUrl, hostname, port, onSSOAttempt, onClick, providers }) => {\n  const [savedSSOProviders, setSSOProviders] = useState([]);\n  const [discoveryUrlValidated, setDiscoveryUrlValidated] = useState<string | undefined>(undefined);\n\n  const filterByProvidersList = (discoveredProviders, validProviders) => {\n    return validProviders == null || validProviders.length == 0\n      ? discoveredProviders\n      : discoveredProviders.filter((p) => validProviders.includes(p.id));\n  };\n  const attemptManualSSOProviderRetrieval = () => {\n    // Do an extra check to see if the hostname provides some SSO provider configuration.\n    const protocol = isLocalhost(hostname) ? 'http' : 'https';\n    const discoveryUrl = `${protocol}://${hostname}:${port}`;\n    getDiscoveryDataInfo(discoveryUrl)\n      .then((mergedSSOProviders) => {\n        setSSOProviders(filterByProvidersList(mergedSSOProviders, providers));\n        if (mergedSSOProviders.length == 0) {\n          setDiscoveryUrlValidated(undefined);\n        } else {\n          setDiscoveryUrlValidated(discoveryUrl);\n        }\n      })\n      // eslint-disable-next-line no-console\n      .catch((err) => console.error('Error in getDiscoveryDataInfo of Login component', err));\n  };\n\n  function isLocalhost(hostname) {\n    const localhostNames = ['localhost', '127.0.0.1', '::1'];\n    return localhostNames.includes(hostname);\n  }\n\n  useEffect(() => {\n    // First, try to get the SSO discovery URL from the config.json configuration file and see if it contains anything.\n    getDiscoveryDataInfo(discoveryAPIUrl)\n      .then((mergedSSOProviders) => {\n        setSSOProviders(filterByProvidersList(mergedSSOProviders, providers));\n        if (mergedSSOProviders.length == 0) {\n          attemptManualSSOProviderRetrieval();\n        } else {\n          setDiscoveryUrlValidated(discoveryAPIUrl);\n        }\n      })\n      // eslint-disable-next-line no-console\n      .catch((err) => console.error('Error in getDiscoveryDataInfo of Login component', err));\n  }, [hostname]);\n\n  return (\n    <>\n      {savedSSOProviders?.length ? (\n        savedSSOProviders.map((provider) => (\n          <Button\n            floating\n            aria-label={'sso select'}\n            style={{ float: 'right' }}\n            onClick={() => {\n              // TODO - if we have SSO credentials cached, try and use those first, if fail, do a call to the SSO provider.\n              const selectedSSOProvider = savedSSOProviders.find(({ id }) => id === provider.id);\n              onClick();\n              onSSOAttempt(discoveryUrlValidated);\n              authRequestForSSO(selectedSSOProvider);\n            }}\n          >\n            Sign in\n            <ShieldCheckIconOutline className='btn-icon-base-r' aria-label={'Shield'} />\n          </Button>\n        ))\n      ) : (\n        <div>\n          {!discoveryUrlValidated ? (\n            <p style={{ fontSize: 11, color: 'grey', marginTop: 5 }}>\n              No default SSO providers found for the database {hostname}. Deploy this application with https and/or set\n              a manual SSO provider inside config.json.\n              <div className=''></div>\n            </p>\n          ) : (\n            <></>\n          )}\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "src/component/sso/SSOUtils.ts",
    "content": "import {\n  authLog,\n  getSSOServerIdIfShouldRedirect,\n  authRequestForSSO,\n  handleAuthFromRedirect,\n  removeSearchParamsInBrowserHistory,\n  restoreSearchAndHashParams,\n  wasRedirectedBackFromSSOServer,\n  defaultSearchParamsToRemoveAfterAutoRedirect,\n  getInitialisationParameters,\n  fetchDiscoveryDataFromUrl,\n  Success,\n  NoProviderError,\n} from 'neo4j-client-sso';\n\nexport const getDiscoveryDataInfo = async (discoveryAPIurl) => {\n  //\n  // These are the three different \"sources\" that we (potentially) fetch\n  // the discovery data from. fallbackEndpoints is mostly/only relevant for Bloom\n  //\n  const fallbackEndpoints = ['/', '/discovery.json']; // Might only be relevant for Bloom\n  const { discoveryURL } = getInitialisationParameters();\n\n  const _handleFetchingDiscoveryURL = () => {\n    authLog(`Attempting to load SSO providers from endpoint: ${discoveryURL}`);\n    return fetchDiscoveryDataFromUrl(discoveryURL);\n  };\n\n  const _handleDiscoveryAPI = () => {\n    authLog(`Attempting to load SSO providers from Discovery API: ${discoveryAPIurl}`);\n    return fetchDiscoveryDataFromUrl(discoveryAPIurl);\n  };\n\n  const _handleFallbackDiscovery = async (urls) => {\n    authLog(`Attempting to load SSO providers from fallback endpoints: ${urls}`);\n    let result = { status: Success, message: Success, host: null, SSOProviders: [] };\n    for (const url of urls) {\n      result = await fetchDiscoveryDataFromUrl(url);\n      if (result.status === Success || result.status === NoProviderError) {\n        break;\n      }\n    }\n    return result;\n  };\n\n  const _handleLocalDiscovery = async () => {\n    if (!discoveryAPIurl) {\n      // For Bloom: Don't pass a discoveryAPIurl if dbms < 4.4\n      return _handleFallbackDiscovery(fallbackEndpoints);\n    }\n\n    let localDiscoveryData = await _handleDiscoveryAPI();\n    if (!localDiscoveryData?.SSOProviders.length) {\n      localDiscoveryData = await _handleFallbackDiscovery(fallbackEndpoints);\n    }\n    return localDiscoveryData;\n  };\n\n  const _handleDiscoveryURL = async () => {\n    const discoveryURLData = await (discoveryURL\n      ? _handleFetchingDiscoveryURL()\n      : Promise.resolve({ SSOProviders: [] }));\n    return discoveryURLData;\n  };\n\n  // Note here that the \"local\" discovery and the DiscoveryURL fetching run in parallel.\n  const [localDiscoveryData, discoveryURLData] = await Promise.all([_handleLocalDiscovery(), _handleDiscoveryURL()]);\n\n  const newProvidersFromLocalDiscovery = localDiscoveryData.SSOProviders.filter(\n    (providerFromLocalDisc) =>\n      !discoveryURLData.SSOProviders.find((provider) => providerFromLocalDisc.id === provider.id)\n  );\n\n  const mergedSSOProviders = discoveryURLData.SSOProviders.concat(newProvidersFromLocalDiscovery);\n  authLog(`Discovery data yielded SSO providers with ids: ${mergedSSOProviders.map((p) => p.id).join(', ') || '-'}`);\n  return mergedSSOProviders;\n};\n\nexport const initializeSSO = async (cachedSSODiscoveryUrl, _setCredentials) => {\n  const SSORedirectId = getSSOServerIdIfShouldRedirect();\n\n  if (SSORedirectId) {\n    // _setIsProcessing(true)\n\n    authLog(`Initialised with sso_redirect value: \"${SSORedirectId}\"`);\n\n    removeSearchParamsInBrowserHistory(defaultSearchParamsToRemoveAfterAutoRedirect);\n\n    try {\n      const mergedSSOProviders = await getDiscoveryDataInfo(cachedSSODiscoveryUrl);\n\n      // _setIsProcessing(false)\n\n      if (!mergedSSOProviders.length) {\n        authLog('Discovery data fetching after auto-redirect failed', 'warn');\n      }\n      const selectedSSOProvider = mergedSSOProviders.find(({ id }) => id === SSORedirectId);\n      authRequestForSSO(selectedSSOProvider);\n      return true;\n    } catch (error) {\n      // _setIsProcessing(false)\n      alert(error);\n      return false;\n    }\n  } else if (wasRedirectedBackFromSSOServer()) {\n    // _setIsProcessing(true)\n    authLog('Handling auth_flow_step redirect');\n    restoreSearchAndHashParams(['connectURL', 'discoveryURL'], false);\n\n    try {\n      const mergedSSOProviders = await getDiscoveryDataInfo(cachedSSODiscoveryUrl);\n      const credentials = await handleAuthFromRedirect(mergedSSOProviders);\n      // _setIsProcessing(false)\n\n      //\n      // \"Hook-in\" point.\n      //\n      // Successful credentials retrieval.\n      // Log in at the Neo4j dbms now using the Neo4j (js) driver.\n      //\n      _setCredentials(credentials);\n\n      // Exemplifying retrieval of stored URL paramenters\n      _retrieveAdditionalURLParameters();\n      return true;\n    } catch (error) {\n      // _setIsProcessing(false)\n      alert(error);\n      authLog(`Handling auth_flow_step redirect failed. err: ${error}`, 'warn');\n    }\n  } else {\n    return false;\n  }\n  return false;\n};\n\n/**\n * Neo4j Bloom uses so called deep links and the arguments (URL paramenters)\n * needed to be temporarly stored due to the redirect to the identity provider.\n * This method shall exemplify this.\n */\nconst _retrieveAdditionalURLParameters = () => {\n  const storedDeepLinkArgs = restoreSearchAndHashParams(['search', 'perspective', 'run']);\n  if (storedDeepLinkArgs) {\n    // eslint-disable-next-line no-console\n    console.log('Time to apply the deep link args now...');\n  }\n};\n"
  },
  {
    "path": "src/component/theme/Themes.tsx",
    "content": "import { createTheme } from '@mui/material/styles';\nexport const lightTheme = createTheme({\n  palette: {\n    mode: 'light',\n  },\n  typography: {\n    fontFamily: \"'Nunito Sans', sans-serif !important\",\n  },\n  breakpoints: {\n    values: {\n      xs: 0,\n      sm: 600,\n      md: 1200,\n      lg: 1536,\n      xl: 1920,\n    },\n  },\n});\n\nexport const darkHeaderTheme = createTheme({\n  palette: {\n    text: {\n      primary: '#ffffff',\n      secondary: '#ffffff',\n    },\n  },\n  breakpoints: {\n    values: {\n      xs: 0,\n      sm: 600,\n      md: 1200,\n      lg: 1536,\n      xl: 1920,\n    },\n  },\n});\n\nexport const luma = (colorString) => {\n  // TODO - we are not able to handle color strings that do not start with '#'.\n  // If we encouter such a color, for now, assume it's always light.\n  if (colorString[0] !== '#') {\n    return 100;\n  }\n\n  let color = colorString.substring(1); // strip #\n  let rgb = parseInt(color, 16); // convert rrggbb to decimal\n  let r = (rgb >> 16) & 0xff; // extract red\n  let g = (rgb >> 8) & 0xff; // extract green\n  let b = (rgb >> 0) & 0xff; // extract blue\n  return 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709\n};\n\nexport default lightTheme;\n"
  },
  {
    "path": "src/config/ApplicationConfig.ts",
    "content": "import StyleConfig from './StyleConfig';\n\nexport const enum Screens {\n  WELCOME_SCREEN,\n  CONNECTION_MODAL,\n}\n\nconst styleConfig = await StyleConfig.getInstance();\n\nexport const DEFAULT_SCREEN = Screens.WELCOME_SCREEN; // WELCOME_SCREEN\nexport const DEFAULT_NEO4J_URL = 'localhost'; // localhost\nexport const DEFAULT_DASHBOARD_TITLE = 'New dashboard';\n\nexport const DASHBOARD_HEADER_COLOR = styleConfig?.style?.DASHBOARD_HEADER_COLOR || '#0B297D'; // '#0B297D'\n\nexport const DASHBOARD_HEADER_BUTTON_COLOR = styleConfig?.style?.DASHBOARD_HEADER_BUTTON_COLOR || null; // '#FFFFFF22'\n\nexport const DASHBOARD_HEADER_TITLE_COLOR = styleConfig?.style?.DASHBOARD_HEADER_TITLE_COLOR || '#FFFFFF'; // '#FFFFFF'\n\nexport const DASHBOARD_HEADER_BRAND_LOGO =\n  styleConfig?.style?.DASHBOARD_HEADER_BRAND_LOGO || 'neo4j-icon-color-full.png';\n\nexport const IS_CUSTOM_LOGO = Boolean(styleConfig?.style?.DASHBOARD_HEADER_BRAND_LOGO);\n\nexport const CUSTOM_CONNECTION_FOOTER_TEXT = ''; // ''\n"
  },
  {
    "path": "src/config/CardConfig.ts",
    "content": "export const CARD_FOOTER_HEIGHT = 88;\nexport const CARD_HEADER_HEIGHT = 72;\n\nexport const enum SELECTION_TYPES {\n  NUMBER,\n  NUMBER_OR_DATETIME,\n  LIST,\n  TEXT,\n  MULTILINE_TEXT,\n  DICTIONARY,\n  COLOR,\n  NODE_PROPERTIES,\n}\n"
  },
  {
    "path": "src/config/ColorConfig.ts",
    "content": "import {\n  schemeCategory10,\n  schemeAccent,\n  schemeDark2,\n  schemePaired,\n  schemePastel1,\n  schemePastel2,\n  schemeSet1,\n  schemeSet2,\n  schemeSet3,\n  schemeYlOrRd,\n  schemeGreens,\n  schemeBrBG,\n  schemeRdYlGn,\n} from 'd3-scale-chromatic';\n\nexport const categoricalColorSchemes = {\n  neodash: [\n    '#588c7e',\n    '#f2e394',\n    '#f2ae72',\n    '#d96459',\n    '#5b9aa0',\n    '#d6d4e0',\n    '#b8a9c9',\n    '#622569',\n    '#ddd5af',\n    '#d9ad7c',\n    '#a2836e',\n    '#674d3c',\n  ],\n  nivo: ['#e8c1a0', '#f47560', '#f1e15b', '#e8a838', '#61cdbb', '#97e3d5'],\n  category10: schemeCategory10,\n  accent: schemeAccent,\n  dark2: schemeDark2,\n  paired: schemePaired,\n  pastel1: schemePastel1,\n  pastel2: schemePastel2,\n  set1: schemeSet1,\n  set2: schemeSet2,\n  set3: schemeSet3,\n  BrBG: schemeBrBG,\n  RdYlGn: schemeRdYlGn,\n  YlOrRd: schemeYlOrRd,\n  greens: schemeGreens,\n};\n\nexport const getD3ColorsByScheme = (scheme) => categoricalColorSchemes[scheme];\n"
  },
  {
    "path": "src/config/DashboardConfig.ts",
    "content": "import { SELECTION_TYPES } from './CardConfig';\n\nexport const DASHBOARD_SETTINGS = {\n  editable: {\n    label: 'Editable',\n    type: SELECTION_TYPES.LIST,\n    values: [true, false],\n    default: true,\n    helperText:\n      'This controls whether users can edit your dashboard. Disable this to turn the dashboard into presentation mode.',\n  },\n  queryTimeLimit: {\n    label: 'Maximum Query Time (seconds)',\n    type: SELECTION_TYPES.NUMBER,\n    default: 45,\n    helperText: 'The maximum time a report is allowed to run before automatically aborted.',\n  },\n  downloadImageEnabled: {\n    label: 'Enable Image Download',\n    type: SELECTION_TYPES.LIST,\n    values: [true, false],\n    default: false,\n    helperText: 'Shows a button in the dashboard header that lets users capture their dashboard as an image.',\n  },\n  disableRowLimiting: {\n    label: 'Disable Row Limiting ⚠️',\n    type: SELECTION_TYPES.LIST,\n    values: [true, false],\n    default: false,\n    helperText:\n      'This disables the automatic row limiting feature. When disabled, always ensure your queries are not returning too many rows.',\n  },\n  resizing: {\n    label: 'Resize Mode',\n    type: SELECTION_TYPES.LIST,\n    values: ['bottom-right', 'all'],\n    default: 'bottom-right',\n    helperText: 'These are the resize handle options shared across all reports. ',\n  },\n  darkLuma: {\n    label: 'Luma Threshold',\n    type: SELECTION_TYPES.NUMBER,\n    default: 25,\n    helperText: 'Background colors under this threshold will be considered as dark',\n  },\n  pagenumber: {\n    label: 'Page Number',\n    type: SELECTION_TYPES.NUMBER,\n    disabled: true,\n    helperText: 'This is the number of the currently selected page.',\n  },\n  parameters: {\n    label: 'Global Parameters',\n    type: SELECTION_TYPES.DICTIONARY,\n    disabled: true,\n    helperText:\n      \"These are the query parameters shared across all reports. You can set these using a 'property select' report.\",\n  },\n  extensions: {\n    label: 'Extensions',\n    type: SELECTION_TYPES.LIST,\n    multiple: true,\n    values: ['actions'],\n    default: false,\n    hidden: true,\n  },\n};\n"
  },
  {
    "path": "src/config/ExampleConfig.ts",
    "content": "import NeoBarChart from '../chart/bar/BarChart';\nimport NeoGraphChart from '../chart/graph/GraphChart';\nimport NeoIFrameChart from '../chart/iframe/IFrameChart';\nimport NeoLineChart from '../chart/line/LineChart';\nimport NeoMapChart from '../chart/map/MapChart';\nimport NeoPieChart from '../chart/pie/PieChart';\nimport NeoTableChart from '../chart/table/TableChart';\n\nexport const EXAMPLE_REPORTS = [\n  {\n    title: 'Table',\n    description:\n      'A table will return any data from Neo4j, including values, nodes, relationships and paths.\\nClick the table headers to sort/filter results.',\n    exampleQuery:\n      'MATCH path=\\n (p:Person)-[r:RATES]->(m:Movie)\\nRETURN path as Path,\\n       p.name as Person,\\n       r.rating as Rating,\\n       m.title as Movie',\n    syntheticQuery: `\n        WITH [\n            {\n                path: {  start: {identity: 1},  end:  {identity: 10},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 10, identity: 10001, properties: {rating: 4.5}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Jim\", movie: \"The Matrix\", rating: 4.5\n            },\n            {\n                path: {  start: {identity: 2},  end:  {identity: 10},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}}, relationship: {type: \"RATES\", start: 2, end: 10, identity: 10002, properties: {rating: 3.8}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Mike\", movie: \"The Matrix\", rating: 3.8\n            },\n            {\n                path: {  start: {identity: 3},  end:  {identity: 10},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 10, identity: 10003, properties: {rating: 5.0}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Sarah\", movie: \"The Matrix\", rating: 5.0\n            },\n            {\n                path: {  start: {identity: 1},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 11, identity: 10004, properties: {rating: 3.5}}, end: {labels: [\"Movie\"], identity: 12,properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Jim\", movie: \"The Matrix - Reloaded\", rating: 3.5\n            },\n            {\n                path: {  start: {identity: 3},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 11, identity: 10005, properties: {rating: 2.7}}, end: {labels: [\"Movie\"], identity: 12,properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Sarah\", movie: \"The Matrix - Reloaded\", rating: 2.7\n            },\n            {\n                path: {  start: {identity: 4},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}}, relationship: {type: \"RATES\", start: 4, end: 11, identity: 10006, properties: {rating: 4.1}}, end: {labels: [\"Movie\"], identity: 12,properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Anna\", movie: \"The Matrix - Reloaded\", rating: 4.1\n            },\n            {\n                path: {  start: {identity: 1},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 12, identity: 10007, properties: {rating: 4.9}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Jim\", movie: \"The Matrix - Revolutions\", rating: 4.9\n            },\n            {\n                path: {  start: {identity: 2},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}}, relationship: {type: \"RATES\", start: 2, end: 12, identity: 10008, properties: {rating: 4.8}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Mike\", movie: \"The Matrix - Revolutions\", rating: 4.8\n            },\n            {\n                path: {  start: {identity: 3},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 12, identity: 10009, properties: {rating: 4.0}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Sarah\", movie: \"The Matrix - Revolutions\", rating: 4.0\n            },\n            {\n                path: {  start: {identity: 4},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}}, relationship: {type: \"RATES\", start: 4, end: 12, identity: 10010, properties: {rating: 4.0}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 2003}} } ] }, person: \"Anna\", movie: \"The Matrix - Revolutions\", rating: 4.0\n            }\n          ] as data\n          UNWIND data as row\n          RETURN row.path as Path, row.person as Person, row.rating as Rating, row.movie as Movie\n        `,\n    settings: { columnWidths: '[2,1,1,1]' },\n    fields: [],\n    selection: {},\n    type: 'table',\n    chartType: NeoTableChart,\n  },\n  {\n    title: 'Graph',\n    description: 'A graph visualization will draw all returned nodes, relationships and paths.',\n    exampleQuery: 'MATCH (p:Person)-[r:RATES]->(m:Movie)\\nRETURN p, r, m',\n    syntheticQuery: `\n        WITH [\n            {\n                path: {  start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 11, identity: 10001, properties: {rating: 4.5}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Jim\", movie: \"The Matrix\", rating: 4.5\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}}, relationship: {type: \"RATES\", start: 2, end: 11, identity: 10002, properties: {rating: 3.8}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Mike\", movie: \"The Matrix\", rating: 3.8\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 11, identity: 10003, properties: {rating: 5.0}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Sarah\", movie: \"The Matrix\", rating: 5.0\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 12, identity: 10004, properties: {rating: 3.5}}, end: {labels: [\"Movie\"], identity: 12, properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Jim\", movie: \"The Matrix - Reloaded\", rating: 3.5\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 12, identity: 10005, properties: {rating: 2.7}}, end: {labels: [\"Movie\"], identity: 12,properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Sarah\", movie: \"The Matrix - Reloaded\", rating: 2.7\n            },\n            {\n                path: { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}}, relationship: {type: \"RATES\", start: 4, end: 12, identity: 10006, properties: {rating: 4.1}}, end: {labels: [\"Movie\"], identity: 12,properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Anna\", movie: \"The Matrix - Reloaded\", rating: 4.1\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 13, identity: 10007, properties: {rating: 4.9}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Jim\", movie: \"The Matrix - Revolutions\", rating: 4.9\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}}, relationship: {type: \"RATES\", start: 2, end: 13, identity: 10008, properties: {rating: 4.8}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Mike\", movie: \"The Matrix - Revolutions\", rating: 4.8\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 13, identity: 10009, properties: {rating: 4.0}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Sarah\", movie: \"The Matrix - Revolutions\", rating: 4.0\n            },\n            {\n                path: { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}}, relationship: {type: \"RATES\", start: 4, end: 13, identity: 10010, properties: {rating: 4.0}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 2003}} } ] }, person: \"Anna\", movie: \"The Matrix - Revolutions\", rating: 4.0\n            }\n          ] as data\n          UNWIND data as row\n          RETURN row.path as Path\n        `,\n    settings: { lockable: false, enableExploration: false, enableEditing: false },\n    fields: [],\n    selection: {\n      Person: 'name',\n      Movie: 'title',\n    },\n    type: 'graph',\n    chartType: NeoGraphChart,\n  },\n  {\n    title: 'Bar Chart',\n    description: 'A bar chart needs a category and a numeric value field.',\n    exampleQuery: 'MATCH (n:Movie)' + '\\n' + 'RETURN n.genre as Genre, \\n       count(n) as Movies',\n    syntheticQuery:\n      'UNWIND [[\"Action\", 9],[\"Comedy\", 12],[\"Drama\", 8],[\"Thriller\",6],[\"Sci-Fi\",10],[\"Fantasy\", 7]] as X RETURN X[0] as Genre, X[1] as Movies',\n    settings: {},\n    selection: { index: 'Genre', value: 'Movies', key: '(none)' },\n    fields: ['Genre', 'Movies'],\n    type: 'bar',\n    chartType: NeoBarChart,\n  },\n  {\n    title: 'Bar Chart (Grouped/Stacked)',\n    description: 'Enable grouping in the advanced report settings to create a grouped/stacked bar chart.',\n    exampleQuery:\n      'MATCH (n:Movie)' + '\\n' + 'RETURN n.genre as Genre, \\n       count(n) as Movies,\\n       n.released as Year',\n    syntheticQuery:\n      'UNWIND [[\"Action\", 4, 2019],[\"Action\", 2, 2020],[\"Action\", 3, 2021],[\"Comedy\", 3, 2019],[\"Comedy\", 3, 2020],[\"Comedy\", 6, 2021],[\"Drama\", 2, 2019],[\"Drama\", 1, 2020],[\"Drama\", 5, 2021],[\"Thriller\",2,2020],[\"Thriller\",4,2021],[\"Sci-Fi\",7,2019],[\"Sci-Fi\",2,2020],[\"Sci-Fi\",1,2021],[\"Fantasy\", 3,2019],[\"Fantasy\", 3,2020],[\"Fantasy\", 2,2021]] as X RETURN X[0] as Genre, X[1] as Movies, X[2] as Year',\n    settings: { legend: true },\n    selection: { index: 'Genre', value: 'Movies', key: 'Year' },\n    fields: ['Genre', 'Movies', 'Year'],\n    type: 'bar',\n    chartType: NeoBarChart,\n  },\n  {\n    title: 'Pie Charts',\n    description: 'Pie charts can be used to visualize categories and numeric values.',\n    exampleQuery:\n      '// How much fruit is in stock?' +\n      '\\n' +\n      'MATCH (p:Product) \\nRETURN p.name as Product,\\n       p.quantity as Quantity',\n    syntheticQuery:\n      'WITH [[\"Apple\",10], [\"Banana\",20], [\"Coconut\",20], [\"Pear\",40] ] as array UNWIND array as row RETURN row[0] as Product, row[1] as Quantity',\n    settings: {},\n    selection: { index: 'Product', value: 'Quantity', key: 'Product' },\n    fields: ['Product', 'Quantity'],\n    type: 'pie',\n    chartType: NeoPieChart,\n  },\n  {\n    title: 'Line Chart',\n    description: 'A line chart can plot multiple values against a horizontal axis.',\n    exampleQuery:\n      'MATCH (n:Year)' + '\\n' + 'RETURN n.year as Year, \\n       n.revenue as Revenue, \\n       n.profit as Profit',\n    syntheticQuery: `\n        WITH [\n            [2011, 5.6, 2.3],\n            [2012, 6.1, 2.6],\n            [2013, 6.3, 2.8],\n            [2014, 6.7, 3.3],\n            [2015, 7.3, 3.5],\n            [2016, 7.6, 3.9],\n            [2017, 8.1, 4.1],\n            [2018, 8.3, 4.5],\n            [2019, 8.9, 4.7],\n            [2020, 9.2, 5.1]\n            ] as data\n            UNWIND data as row\n            RETURN row[0] as Year, row[1] as Revenue, row[2] as Profit\n        `,\n    settings: {\n      legend: true,\n      legendWidth: 100,\n      marginTop: 32,\n    },\n    selection: {\n      x: 'Year',\n      value: ['Revenue', 'Profit'],\n    },\n    fields: ['Year', 'Revenue', 'Profit'],\n    type: 'line',\n    chartType: NeoLineChart,\n  },\n  {\n    title: 'Map',\n    description: 'A map report visualizes nodes and relationships with spatial (geographical) properties.',\n    exampleQuery:\n      '// Find all routes between cinemas.\\n // Each cinema node has a point property.\\nMATCH (c:Cinema),\\n      (c)-[r:ROUTE_TO]->(c2:Cinema)\\nRETURN c, r, c2',\n    syntheticQuery: `\n        UNWIND [{id: \"Tilburg\", label: \"Cinema\", point: point({latitude:51.59444886664065 , longitude:5.088862976119185})},\n{id: \"Antwerp\", label: \"Cinema\", point: point({latitude:51.22065200961528  , longitude:4.414094044161085})},\n{id: \"Brussels\", label: \"Cinema\", point: point({latitude:50.854284724408664, longitude:4.344177490986771})},\n{id: \"Cologne\", label: \"Cinema\", point: point({latitude:50.94247712506476  , longitude:6.9699327434361855 })},\n{id: \"Nijmegen\", label: \"Cinema\", point: point({latitude:51.81283449474347 , longitude:5.866804797140869})},\n{start: \"Tilburg\", end: \"Antwerp\", type: \"ROUTE_TO\", distance: \"125km\", id: 100},\n{start: \"Antwerp\", end: \"Brussels\", type: \"ROUTE_TO\", distance: \"70km\", id: 101},\n{start: \"Brussels\", end: \"Cologne\", type: \"ROUTE_TO\", distance: \"259km\", id: 102},\n{start: \"Cologne\", end: \"Nijmegen\", type: \"ROUTE_TO\", distance: \"180km\", id: 103},\n{start: \"Nijmegen\", end: \"Tilburg\", type: \"ROUTE_TO\", distance: \"92km\", id: 104}\n] as value\nRETURN value\n        `,\n    settings: {},\n    fields: [],\n    selection: {},\n    type: 'map',\n    chartType: NeoMapChart,\n  },\n  {\n    title: 'Map (from properties)',\n    description: 'Use dictionaries to visualize entities that are not real nodes and relationships.',\n    exampleQuery: `// Plot an artificial relationship.\\nMATCH (l1:Location)<--(a:Person),\\n      (a:Person)-[:KNOWS]-(b:Person),\\n      (b:Person)-->(l2:Location)\nRETURN {id: a.name, label: \"Person\", point: l1.point},\n       {id: b.name, label: \"Person\", point: l2.point},\n       {start: a.name, end: b.name, type: \"KNOWS\", id: 1}\n`,\n    syntheticQuery: `\n        UNWIND [{id: \"Dwight\", label: \"Person\", point: point({latitude:41.45954418871592, longitude:-75.75265878192192})},\n{id: \"Jim\", label: \"Person\", point: point({latitude:41.41492119160039,longitude: -75.6470002887925})},\n{start: \"Dwight\", end: \"Jim\", type: \"KNOWS\", id: 1}\n] as value\nRETURN value\n        `,\n    settings: {},\n    fields: [],\n    selection: {},\n    type: 'map',\n    chartType: NeoMapChart,\n  },\n  {\n    title: 'iFrame',\n    description:\n      'You can iFrame other webpages inside a dashboard, and dynamically pass in your dashboard parameters into the URL.',\n    exampleQuery: `https://neodash.graphapp.io/embed-test.html`,\n    syntheticQuery: `https://neodash.graphapp.io/embed-test.html`,\n    settings: { passGlobalParameters: true },\n    fields: [],\n    globalParameters: { neodash_person_name: 'Keanu', neodash_movie_title: 'The Matrix' },\n    selection: {},\n    type: 'iframe',\n    chartType: NeoIFrameChart,\n  },\n];\n\nexport default EXAMPLE_REPORTS;\n"
  },
  {
    "path": "src/config/PageConfig.ts",
    "content": "export const GRID_COMPACTION_TYPE = 'vertical'; // Can be set to vertical or horizontal or none.\n"
  },
  {
    "path": "src/config/ReportConfig.tsx",
    "content": "import React from 'react';\nimport ParameterSelectCardSettings from '../chart/parameter/ParameterSelectCardSettings';\nimport NeoBarChart from '../chart/bar/BarChart';\nimport NeoGraphChart from '../chart/graph/GraphChart';\nimport NeoIFrameChart from '../chart/iframe/IFrameChart';\nimport NeoJSONChart from '../chart/json/JSONChart';\nimport NeoMapChart from '../chart/map/MapChart';\nimport NeoPieChart from '../chart/pie/PieChart';\nimport NeoTableChart from '../chart/table/TableChart';\nimport NeoSingleValueChart from '../chart/single/SingleValueChart';\nimport NeoParameterSelectionChart from '../chart/parameter/ParameterSelectionChart';\nimport NeoMarkdownChart from '../chart/markdown/MarkdownChart';\nimport { SELECTION_TYPES } from './CardConfig';\nimport NeoLineChart from '../chart/line/LineChart';\nimport NeoScatterPlot from '../chart/scatter/ScatterPlotChart';\nimport { objMerge, objectMap } from '../utils/ObjectManipulation';\n\n// TODO: make the reportConfig a interface with not self-documented code\n// Use Neo4j 4.0 subqueries to limit the number of rows returned by overriding the query.\nexport const HARD_ROW_LIMITING = false;\n\n// A small delay (for UX reasons) between when to run the query after saving a report.\nexport const RUN_QUERY_DELAY_MS = 300;\n\n// The default number of rows to process in a visualization.\nexport const DEFAULT_ROW_LIMIT = 100;\n\n// A dictionary of available reports (visualizations).\nconst _REPORT_TYPES = {\n  table: {\n    label: 'Table',\n    helperText: 'A table will contain all returned data.',\n    component: NeoTableChart,\n    useReturnValuesAsFields: true,\n    maxRecords: 1000,\n    settings: {\n      transposed: {\n        label: 'Transpose Rows & Columns',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      compact: {\n        label: 'Compact Table',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      wrapContent: {\n        label: 'Wrap overflowing content',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      columnWidthsType: {\n        label: 'Column Widths Specification',\n        type: SELECTION_TYPES.LIST,\n        values: ['Relative (%)', 'Fixed (px)'],\n        default: 'Relative (%)',\n      },\n      columnWidths: {\n        label: 'Relative/Fixed Column Sizes',\n        type: SELECTION_TYPES.TEXT,\n        default: '[1, 1, 1, ...]',\n      },\n      allowDownload: {\n        label: 'Enable CSV Download',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n    },\n  },\n  graph: {\n    label: 'Graph',\n    helperText: 'A graph visualization will draw all returned nodes, relationships and paths.',\n    selection: {\n      properties: {\n        label: 'Node Properties',\n        type: SELECTION_TYPES.NODE_PROPERTIES,\n      },\n    },\n    useNodePropsAsFields: true,\n    autoAssignSelectedProperties: true,\n    component: NeoGraphChart,\n    maxRecords: 1000,\n    // The idea is to match a setting to its dependency, the operator represents the kind of relationship\n    // between the different options (EX: if operator is false, then it must be the opposite of the setting it depends on)\n    disabledDependency: { relationshipParticleSpeed: { dependsOn: 'relationshipParticles', operator: false } },\n    settings: {\n      nodeColorScheme: {\n        label: 'Node Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: [\n          'neodash',\n          'nivo',\n          'category10',\n          'accent',\n          'dark2',\n          'paired',\n          'pastel1',\n          'pastel2',\n          'set1',\n          'set2',\n          'set3',\n        ],\n        default: 'neodash',\n      },\n      nodeLabelColor: {\n        label: 'Node Label Color',\n        type: SELECTION_TYPES.COLOR,\n        default: 'black',\n      },\n      nodeLabelFontSize: {\n        label: 'Node Label Font Size',\n        type: SELECTION_TYPES.NUMBER,\n        default: 3.5,\n      },\n      defaultNodeSize: {\n        label: 'Node Size',\n        type: SELECTION_TYPES.NUMBER,\n        default: 2,\n      },\n      nodeSizeProp: {\n        label: 'Node Size Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'size',\n      },\n      nodeColorProp: {\n        label: 'Node Color Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'color',\n      },\n      defaultRelColor: {\n        label: 'Relationship Color',\n        type: SELECTION_TYPES.TEXT,\n        default: '#a0a0a0',\n      },\n      defaultRelWidth: {\n        label: 'Relationship Width',\n        type: SELECTION_TYPES.NUMBER,\n        default: 1,\n      },\n      relLabelColor: {\n        label: 'Relationship Label Color',\n        type: SELECTION_TYPES.TEXT,\n        default: '#a0a0a0',\n      },\n      relLabelFontSize: {\n        label: 'Relationship Label Font Size',\n        type: SELECTION_TYPES.NUMBER,\n        default: 2.75,\n      },\n      relColorProp: {\n        label: 'Relationship Color Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'color',\n      },\n      relWidthProp: {\n        label: 'Relationship Width Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'width',\n      },\n      relationshipParticles: {\n        label: 'Animated particles on Relationships',\n        type: SELECTION_TYPES.LIST,\n        default: false,\n        values: [false, true],\n      },\n      relationshipParticleSpeed: {\n        label: 'Speed of the particle animation',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0.005,\n      },\n      arrowLengthProp: {\n        label: 'Arrow head size',\n        type: SELECTION_TYPES.NUMBER,\n        default: 3,\n      },\n      layout: {\n        label: 'Graph Layout (experimental)',\n        type: SELECTION_TYPES.LIST,\n        values: ['force-directed', 'tree-top-down', 'tree-bottom-up', 'tree-left-right', 'tree-right-left', 'radial'],\n        default: 'force-directed',\n      },\n      graphDepthSep: {\n        label: 'Tree layout level distance',\n        type: SELECTION_TYPES.NUMBER,\n        default: 30,\n      },\n      enableExploration: {\n        label: 'Enable graph exploration',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      enableEditing: {\n        label: 'Enable graph editing',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      showPropertiesOnHover: {\n        label: 'Show pop-up on Hover',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      showPropertiesOnClick: {\n        label: 'Show properties on Click',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      fixNodeAfterDrag: {\n        label: 'Fix node positions after Drag',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      drilldownLink: {\n        label: 'Drilldown Icon Link',\n        type: SELECTION_TYPES.TEXT,\n        placeholder: 'https://bloom.neo4j.io',\n        default: '',\n      },\n      allowDownload: {\n        label: 'Enable CSV Download',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      hideSelections: {\n        label: 'Hide Property Selection',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      lockable: {\n        label: 'Enable locking node positions',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      iconStyle: {\n        label: 'Node Label images',\n        type: SELECTION_TYPES.TEXT,\n        placeholder: '{label : url}',\n        default: '',\n      },\n      rightClickToExpandNodes: {\n        label: 'Right Click to Expand Nodes',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n    },\n  },\n  bar: {\n    label: 'Bar Chart',\n    component: NeoBarChart,\n    useReturnValuesAsFields: true,\n    helperText: (\n      <div>\n        A bar chart expects two fields: a <code>category</code> and a <code>value</code>.\n      </div>\n    ),\n    selection: {\n      index: {\n        label: 'Category',\n        type: SELECTION_TYPES.TEXT,\n      },\n      value: {\n        label: 'Value',\n        type: SELECTION_TYPES.NUMBER,\n        key: true,\n      },\n      key: {\n        label: 'Group',\n        type: SELECTION_TYPES.TEXT,\n        optional: true,\n      },\n    },\n    maxRecords: 250,\n    disabledDependency: { barWidth: { dependsOn: 'customDimensions', operator: false } },\n    settings: {\n      legend: {\n        label: 'Show Legend',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      showOptionalSelections: {\n        label: 'Grouping',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      valueScale: {\n        label: 'Value Scale',\n        type: SELECTION_TYPES.LIST,\n        values: ['linear', 'symlog'],\n        default: 'linear',\n      },\n      minValue: {\n        label: 'Min Value',\n        type: SELECTION_TYPES.NUMBER,\n        default: 'auto',\n      },\n      maxValue: {\n        label: 'Max Value',\n        type: SELECTION_TYPES.NUMBER,\n        default: 'auto',\n      },\n      groupMode: {\n        label: 'Group Mode',\n        type: SELECTION_TYPES.LIST,\n        values: ['grouped', 'stacked'],\n        default: 'stacked',\n      },\n      layout: {\n        label: 'Layout',\n        type: SELECTION_TYPES.LIST,\n        values: ['horizontal', 'vertical'],\n        default: 'vertical',\n      },\n      colors: {\n        label: 'Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: ['nivo', 'category10', 'accent', 'dark2', 'paired', 'pastel1', 'pastel2', 'set1', 'set2', 'set3'],\n        default: 'set2',\n      },\n      barValues: {\n        label: 'Show Values On Bars',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      customDimensions: {\n        label: 'Custom Dimensions',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      barWidth: {\n        label: 'Bar Width',\n        type: SELECTION_TYPES.NUMBER,\n        default: 10,\n      },\n      labelSkipWidth: {\n        label: 'Skip label if Bar Width < Xpx',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      labelSkipHeight: {\n        label: 'Skip label if Bar Height < Xpx',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      positionLabel: {\n        label: 'Custom label position',\n        type: SELECTION_TYPES.LIST,\n        values: ['off', 'top', 'bottom'],\n        default: 'off',\n      },\n      labelRotation: {\n        label: 'Label Rotation (degrees)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 45,\n      },\n      marginLeft: {\n        label: 'Margin Left',\n        type: SELECTION_TYPES.NUMBER,\n        default: 50,\n      },\n      marginRight: {\n        label: 'Margin Right',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginTop: {\n        label: 'Margin Top',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginBottom: {\n        label: 'Margin Bottom',\n        type: SELECTION_TYPES.NUMBER,\n        default: 30,\n      },\n      legendWidth: {\n        label: 'Legend Width',\n        type: SELECTION_TYPES.NUMBER,\n        default: 128,\n      },\n      hideSelections: {\n        label: 'Hide Selections',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      expandHeightForLegend: {\n        label: 'Expand Height For Legend',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      innerPadding: {\n        label: 'Inner Padding',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      legendPosition: {\n        label: 'Legend Position',\n        type: SELECTION_TYPES.LIST,\n        values: ['Horizontal', 'Vertical'],\n        default: 'Vertical',\n      },\n      padding: {\n        label: 'Padding',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0.25,\n      },\n      displayYAxis: {\n        label: 'Display Y axis',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      displayYGridLines: {\n        label: 'Display Y grid lines',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n    },\n  },\n  pie: {\n    label: 'Pie Chart',\n    component: NeoPieChart,\n    useReturnValuesAsFields: true,\n    helperText: (\n      <div>\n        A pie chart expects two fields: a <code>category</code> and a <code>value</code>.\n      </div>\n    ),\n    selection: {\n      index: {\n        label: 'Category',\n        type: SELECTION_TYPES.TEXT,\n      },\n      value: {\n        label: 'Value',\n        type: SELECTION_TYPES.NUMBER,\n        key: true,\n      },\n      key: {\n        label: 'Group',\n        type: SELECTION_TYPES.TEXT,\n        optional: true,\n      },\n    },\n    maxRecords: 250,\n    settings: {\n      legend: {\n        label: 'Show Legend',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      sortByValue: {\n        label: 'Auto-sort slices by value',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      enableArcLabels: {\n        label: 'Show Values in slices',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      arcLabelsSkipAngle: {\n        label: \"Skip label if corresponding arc's angle is lower than provided value\",\n        type: SELECTION_TYPES.NUMBER,\n        default: 10,\n      },\n      arcLabelsFontSize: {\n        label: 'Labels font Size',\n        type: SELECTION_TYPES.NUMBER,\n        default: 13,\n      },\n      enableArcLinkLabels: {\n        label: 'Show categories next to slices',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      interactive: {\n        label: 'Enable interactivity',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      colors: {\n        label: 'Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: ['nivo', 'category10', 'accent', 'dark2', 'paired', 'pastel1', 'pastel2', 'set1', 'set2', 'set3'],\n        default: 'set2',\n      },\n      innerRadius: {\n        label: 'Pie Inner Radius (between 0 and 1)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      padAngle: {\n        label: 'Slice padding angle (degrees)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      borderWidth: {\n        label: 'Slice border width (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      activeOuterRadiusOffset: {\n        label: 'Extends active slice outer radius',\n        type: SELECTION_TYPES.NUMBER,\n        default: 8,\n      },\n      arcLinkLabelsOffset: {\n        label: 'Link offset from pie outer radius, useful to have links overlapping pie slices',\n        type: SELECTION_TYPES.NUMBER,\n        default: 15,\n      },\n      arcLinkLabelsSkipAngle: {\n        label: \"Skip label if corresponding slice's angle is lower than provided value\",\n        type: SELECTION_TYPES.NUMBER,\n        default: 1,\n      },\n      cornerRadius: {\n        label: 'Slice Corner Radius',\n        type: SELECTION_TYPES.NUMBER,\n        default: 1,\n      },\n      marginLeft: {\n        label: 'Margin Left (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 50,\n      },\n      marginRight: {\n        label: 'Margin Right (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 50,\n      },\n      marginTop: {\n        label: 'Margin Top (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 50,\n      },\n      marginBottom: {\n        label: 'Margin Bottom (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 50,\n      },\n    },\n  },\n  line: {\n    label: 'Line Chart',\n    component: NeoLineChart,\n    useReturnValuesAsFields: true,\n    helperText: (\n      <div>\n        A line chart expects two fields: an <code>x</code> value and a <code>y</code> value. The <code>x</code> value\n        can be a number or a Neo4j datetime object. Values are automatically selected from your query results.\n      </div>\n    ),\n    selection: {\n      x: {\n        label: 'X-value',\n        type: SELECTION_TYPES.NUMBER_OR_DATETIME,\n      },\n      value: {\n        label: 'Y-value',\n        type: SELECTION_TYPES.NUMBER,\n        key: true,\n        multiple: true,\n      },\n    },\n    maxRecords: 250,\n    settings: {\n      legend: {\n        label: 'Show Legend',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      colors: {\n        label: 'Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: ['nivo', 'category10', 'accent', 'dark2', 'paired', 'pastel1', 'pastel2', 'set1', 'set2', 'set3'],\n        default: 'set2',\n      },\n      xScale: {\n        label: 'X Scale',\n        type: SELECTION_TYPES.LIST,\n        values: ['linear', 'log', 'point'],\n        default: 'linear',\n      },\n      yScale: {\n        label: 'Y Scale',\n        type: SELECTION_TYPES.LIST,\n        values: ['linear', 'log'],\n        default: 'linear',\n      },\n      minXValue: {\n        label: 'Min X Value',\n        type: SELECTION_TYPES.NUMBER,\n        default: 'auto',\n      },\n      maxXValue: {\n        label: 'Max X Value',\n        type: SELECTION_TYPES.NUMBER,\n        default: 'auto',\n      },\n      minYValue: {\n        label: 'Min Y Value',\n        type: SELECTION_TYPES.NUMBER,\n        default: 'auto',\n      },\n      maxYValue: {\n        label: 'Max Y Value',\n        type: SELECTION_TYPES.NUMBER,\n        default: 'auto',\n      },\n      xTickValues: {\n        label: 'X-axis Tick Count (Approximate)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 'auto',\n      },\n      xAxisTimeFormat: {\n        label: 'X-axis Format (Time chart)',\n        type: SELECTION_TYPES.TEXT,\n        default: '%Y-%m-%dT%H:%M:%SZ',\n      },\n      xTickTimeValues: {\n        label: 'X-axis Tick Size (Time chart)',\n        type: SELECTION_TYPES.TEXT,\n        default: 'every 1 year',\n      },\n      xTickRotationAngle: {\n        label: 'X-axis Tick Rotation (Degrees)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      yTickRotationAngle: {\n        label: 'Y-axis Tick Rotation (Degrees)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      curve: {\n        label: 'Line Smoothing',\n        type: SELECTION_TYPES.LIST,\n        values: ['linear', 'basis', 'cardinal', 'step'],\n        default: 'linear',\n      },\n      showGrid: {\n        label: 'Show Grid',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      pointSize: {\n        label: 'Point Radius (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 10,\n      },\n      lineWidth: {\n        label: 'Line Width (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 2,\n      },\n      marginLeft: {\n        label: 'Margin Left (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 50,\n      },\n      marginRight: {\n        label: 'Margin Right (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginTop: {\n        label: 'Margin Top (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginBottom: {\n        label: 'Margin Bottom (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 40,\n      },\n      legendWidth: {\n        label: 'Legend Label Width (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 100,\n      },\n      hideSelections: {\n        label: 'Hide Property Selection',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n    },\n  },\n  // TODO - move to advanced visualization.\n  // scatterPlot: {\n  //   label: 'Scatter Plot',\n  //   component: NeoScatterPlot,\n  //   useReturnValuesAsFields: true,\n  //   helperText: (\n  //     <div>\n  //       A Scatter plot chart expects two fields: an <code>x</code> value and a <code>y</code> value. The <code>x</code>\n  //       value can be a number or a Neo4j datetime object. Values are automatically selected from your query results.\n  //     </div>\n  //   ),\n  //   selection: {\n  //     x: {\n  //       label: 'X-value',\n  //       type: SELECTION_TYPES.NUMBER_OR_DATETIME,\n  //     },\n  //     value: {\n  //       label: 'Y-value',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       key: true,\n  //     },\n  //   },\n  //   maxRecords: 2000,\n  //   settings: {\n  //     backgroundColor: {\n  //       label: 'Background Color',\n  //       type: SELECTION_TYPES.COLOR,\n  //       default: '#fafafa',\n  //     },\n  //     colorIntensityProp: {\n  //       label: 'Intensity value field',\n  //       type: SELECTION_TYPES.TEXT,\n  //       default: 'intensity',\n  //     },\n  //     labelProp: {\n  //       label: 'Point label field',\n  //       type: SELECTION_TYPES.TEXT,\n  //       default: 'label',\n  //     },\n  //     legend: {\n  //       label: 'Show Legend',\n  //       type: SELECTION_TYPES.LIST,\n  //       values: [true, false],\n  //       default: false,\n  //     },\n  //     legendWidth: {\n  //       label: 'Legend Width (px)',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 20,\n  //     },\n  //     xScale: {\n  //       label: 'X Scale',\n  //       type: SELECTION_TYPES.LIST,\n  //       values: ['linear', 'log'],\n  //       default: 'linear',\n  //     },\n  //     yScale: {\n  //       label: 'Y Scale',\n  //       type: SELECTION_TYPES.LIST,\n  //       values: ['linear', 'log'],\n  //       default: 'linear',\n  //     },\n  //     minXValue: {\n  //       label: 'Min X Value',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 'auto',\n  //     },\n  //     maxXValue: {\n  //       label: 'Max X Value',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 'auto',\n  //     },\n  //     minYValue: {\n  //       label: 'Min Y Value',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 'auto',\n  //     },\n  //     maxYValue: {\n  //       label: 'Max Y Value',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 'auto',\n  //     },\n  //     xTickValues: {\n  //       label: 'X-axis Tick Count (Approximate)',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 'auto',\n  //     },\n  //     xAxisTimeFormat: {\n  //       label: 'X-axis Format (Time chart)',\n  //       type: SELECTION_TYPES.TEXT,\n  //       default: '%Y-%m-%dT%H:%M:%SZ',\n  //     },\n  //     xTickTimeValues: {\n  //       label: 'X-axis Tick Size (Time chart)',\n  //       type: SELECTION_TYPES.TEXT,\n  //       default: 'every 1 year',\n  //     },\n  //     xTickRotationAngle: {\n  //       label: 'X-axis Tick Rotation (Degrees)',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 0,\n  //     },\n  //     yTickRotationAngle: {\n  //       label: 'Y-axis Tick Rotation (Degrees)',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 0,\n  //     },\n  //     showGrid: {\n  //       label: 'Show Grid',\n  //       type: SELECTION_TYPES.LIST,\n  //       values: [true, false],\n  //       default: true,\n  //     },\n  //     pointSize: {\n  //       label: 'Point Radius (px)',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 9,\n  //     },\n  //     marginLeft: {\n  //       label: 'Margin Left (px)',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 50,\n  //     },\n  //     marginRight: {\n  //       label: 'Margin Right (px)',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 24,\n  //     },\n  //     marginTop: {\n  //       label: 'Margin Top (px)',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 24,\n  //     },\n  //     marginBottom: {\n  //       label: 'Margin Bottom (px)',\n  //       type: SELECTION_TYPES.NUMBER,\n  //       default: 40,\n  //     },\n  //     hideSelections: {\n  //       label: 'Hide Property Selection',\n  //       type: SELECTION_TYPES.LIST,\n  //       values: [true, false],\n  //       default: false,\n  //     },\n  //   },\n  // },\n  map: {\n    label: 'Map',\n    helperText: 'A map will draw all nodes and relationships with spatial properties.',\n    selection: {\n      properties: {\n        label: 'Node Properties',\n        type: SELECTION_TYPES.NODE_PROPERTIES,\n      },\n    },\n    useNodePropsAsFields: true,\n    component: NeoMapChart,\n    maxRecords: 1000,\n    settings: {\n      layerType: {\n        label: 'Layer Type',\n        type: SELECTION_TYPES.LIST,\n        values: ['markers', 'heatmap'],\n        default: 'markers',\n      },\n      clusterMarkers: {\n        label: 'Cluster Markers',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      separateOverlappingMarkers: {\n        label: 'Seperate Overlapping Markers',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      nodeColorScheme: {\n        label: 'Node Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: [\n          'neodash',\n          'nivo',\n          'category10',\n          'accent',\n          'dark2',\n          'paired',\n          'pastel1',\n          'pastel2',\n          'set1',\n          'set2',\n          'set3',\n        ],\n        default: 'neodash',\n      },\n      defaultNodeSize: {\n        label: 'Node Marker Size',\n        type: SELECTION_TYPES.LIST,\n        values: ['small', 'medium', 'large'],\n        default: 'large',\n      },\n      nodeColorProp: {\n        label: 'Node Color Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'color',\n      },\n      defaultRelColor: {\n        label: 'Relationship Color',\n        type: SELECTION_TYPES.TEXT,\n        default: '#a0a0a0',\n      },\n      defaultRelWidth: {\n        label: 'Relationship Width',\n        type: SELECTION_TYPES.NUMBER,\n        default: 1,\n      },\n      relColorProp: {\n        label: 'Relationship Color Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'color',\n      },\n      relWidthProp: {\n        label: 'Relationship Width Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'width',\n      },\n      providerUrl: {\n        label: 'Map Provider URL',\n        type: SELECTION_TYPES.TEXT,\n        default: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n      },\n      intensityProp: {\n        label: 'Intensity Property (for heatmap)',\n        type: SELECTION_TYPES.TEXT,\n        default: 'intensity',\n      },\n      hideSelections: {\n        label: 'Hide Property Selection',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n    },\n  },\n  value: {\n    label: 'Single Value',\n    helperText: 'This report will show only the first value of the first row returned.',\n    component: NeoSingleValueChart,\n    maxRecords: 1,\n    settings: {\n      fontSize: {\n        label: 'Font Size',\n        type: SELECTION_TYPES.NUMBER,\n        default: 64,\n      },\n      color: {\n        label: 'Color',\n        type: SELECTION_TYPES.TEXT,\n        default: 'rgba(0, 0, 0, 0.87)',\n      },\n      format: {\n        label: 'Display format',\n        type: SELECTION_TYPES.LIST,\n        values: ['auto', 'json', 'yml'],\n        default: 'auto',\n      },\n      monospace: {\n        label: 'Use monospace font',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      textAlign: {\n        label: 'Horizontal Align',\n        type: SELECTION_TYPES.LIST,\n        values: ['left', 'center', 'right'],\n        default: 'left',\n      },\n      verticalAlign: {\n        label: 'Vertical Align',\n        type: SELECTION_TYPES.LIST,\n        values: ['bottom', 'middle', 'top'],\n        default: 'top',\n      },\n    },\n  },\n  json: {\n    label: 'Raw JSON',\n    helperText: 'This report will render the raw data returned by Neo4j.',\n    component: NeoJSONChart,\n    allowScrolling: true,\n    maxRecords: 500,\n    settings: {\n      format: {\n        label: 'Format',\n        type: SELECTION_TYPES.LIST,\n        values: ['json', 'yml'],\n        default: 'json',\n      },\n    },\n  },\n  select: {\n    label: 'Parameter Select',\n    helperText:\n      'This report will let users interactively select Cypher parameters that are available globally, in all reports. A parameter can either be a node property, relationship property, or a free text field.',\n    component: NeoParameterSelectionChart,\n    settingsComponent: ParameterSelectCardSettings,\n    disableCypherParameters: true,\n    textOnly: true,\n    maxRecords: 100,\n    settings: {\n      multiSelector: {\n        label: 'Multiple Selection',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      multiline: {\n        label: 'Multiline',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      manualParameterSave: {\n        label: 'Manual Parameter Save',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      overridePropertyDisplayName: {\n        label: 'Property Display Name Override',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      suggestionLimit: {\n        label: 'Value Suggestion Limit',\n        type: SELECTION_TYPES.NUMBER,\n        default: 5,\n      },\n      searchType: {\n        label: 'Search Type',\n        type: SELECTION_TYPES.LIST,\n        values: ['CONTAINS', 'STARTS WITH', 'ENDS WITH'],\n        default: 'CONTAINS',\n      },\n      disabled: {\n        label: 'Disable the field',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      caseSensitive: {\n        label: 'Case Sensitive Search',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      deduplicateSuggestions: {\n        label: 'Deduplicate Suggestion Values',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      defaultValue: {\n        label: 'Default Value (Override)',\n        type: SELECTION_TYPES.TEXT,\n        default: '',\n      },\n      clearParameterOnFieldClear: {\n        label: 'Clear Parameter on Field Reset',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      autoSelectFirstValue: {\n        label: 'Auto-select first value on no selection',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      manualPropertyNameSpecification: {\n        label: 'Manual Label/Property Name Specification',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      multiSelectLimit: {\n        label: 'Multiselect Value Limit',\n        type: SELECTION_TYPES.NUMBER,\n        default: 5,\n      },\n      helperText: {\n        label: 'Helper Text (Override)',\n        type: SELECTION_TYPES.TEXT,\n        default: 'Enter a custom helper text here...',\n      },\n      suggestionsUpdateTimeout: {\n        label: 'Timeout for value suggestions (ms)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 250,\n      },\n      setParameterTimeout: {\n        label: 'Timeout for value updates (ms)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 1000,\n      },\n    },\n  },\n  iframe: {\n    label: 'iFrame',\n    helperText:\n      'iFrame reports let you embed external webpages into your dashboard. Enter an URL in the query box above to embed it as an iFrame.',\n    textOnly: true, // this makes sure that no query is executed, input of the report gets passed directly to the renderer.\n    disableDatabaseSelector: true,\n    component: NeoIFrameChart,\n    inputMode: 'url',\n    maxRecords: 1,\n    allowScrolling: true,\n    settings: {\n      replaceGlobalParameters: {\n        label: 'Replace global parameters in URL',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      passGlobalParameters: {\n        label: 'Append global parameters to iFrame URL',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n    },\n  },\n  text: {\n    label: 'Markdown',\n    helperText: 'Markdown text specified above will be rendered in the report.',\n    component: NeoMarkdownChart,\n    inputMode: 'markdown',\n    textOnly: true, // this makes sure that no query is executed, input of the report gets passed directly to the renderer.\n    disableDatabaseSelector: true,\n    maxRecords: 1,\n    allowScrolling: true,\n    settings: {\n      replaceGlobalParameters: {\n        label: 'Replace global parameters in Markdown',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n    },\n  },\n};\n\nexport const COMMON_REPORT_SETTINGS = {\n  backgroundColor: {\n    label: 'Background Color',\n    type: SELECTION_TYPES.COLOR,\n    default: '#fafafa',\n  },\n  description: {\n    label: 'Selector Description',\n    type: SELECTION_TYPES.MULTILINE_TEXT,\n    default: 'Enter markdown here...',\n  },\n  ignoreNonDefinedParams: {\n    label: 'Ignore undefined parameters',\n    type: SELECTION_TYPES.LIST,\n    values: [true, false],\n    default: false,\n    refresh: true,\n  },\n  refreshButtonEnabled: {\n    label: 'Refreshable',\n    type: SELECTION_TYPES.LIST,\n    values: [true, false],\n    default: false,\n  },\n  fullscreenEnabled: {\n    label: 'Fullscreen enabled',\n    type: SELECTION_TYPES.LIST,\n    values: [true, false],\n    default: false,\n  },\n  downloadImageEnabled: {\n    label: 'Download Image enabled',\n    type: SELECTION_TYPES.LIST,\n    values: [true, false],\n    default: false,\n  },\n  autorun: {\n    label: 'Auto-run query',\n    type: SELECTION_TYPES.LIST,\n    values: [true, false],\n    default: true,\n  },\n  refreshRate: {\n    label: 'Refresh rate (seconds)',\n    type: SELECTION_TYPES.NUMBER,\n    default: '0 (No refresh)',\n  },\n  noDataMessage: {\n    label: 'Override no data message',\n    type: SELECTION_TYPES.TEXT,\n    default: 'Query returned no data.',\n  },\n};\n\nexport const REPORT_TYPES = objectMap(_REPORT_TYPES, (value: any) => {\n  return objMerge({ settings: COMMON_REPORT_SETTINGS }, value);\n});\n"
  },
  {
    "path": "src/config/StyleConfig.tsx",
    "content": "import { rgbaToHex } from '../chart/Utils';\n\nexport default class StyleConfig {\n  private static instance: StyleConfig;\n\n  protected style: any;\n\n  private constructor() {}\n\n  public static async getInstance(): Promise<StyleConfig> {\n    if (!this.instance) {\n      this.instance = await this.create();\n    }\n    return this.instance;\n  }\n\n  async initialize() {\n    try {\n      await (await fetch('style.config.json')).json().then((json) => {\n        this.style = json;\n      });\n    } catch (e) {\n      this.style = {};\n    }\n  }\n\n  static async create() {\n    const o = new StyleConfig();\n    await o.initialize();\n    o.applyCSS();\n    return o;\n  }\n\n  public applyCSS() {\n    const rules = this.style?.style || {};\n    for (const [key, value] of Object.entries(rules)) {\n      document.documentElement.style.setProperty(key, value);\n    }\n  }\n\n  public complementColor(color: string) {\n    const hexColor = rgbaToHex(document.documentElement.style.getPropertyValue(color));\n    const complementColor = (0xffffff - parseInt(hexColor.replace('#', ''), 16)).toString(16);\n    return `#${complementColor}`;\n  }\n}\n"
  },
  {
    "path": "src/dashboard/Dashboard.tsx",
    "content": "import React from 'react';\nimport NeoPage from '../page/Page';\nimport NeoDashboardHeader from './header/DashboardHeader';\nimport NeoDashboardTitle from './header/DashboardTitle';\nimport NeoDashboardHeaderPageList from './header/DashboardHeaderPageList';\nimport { createDriver, Neo4jProvider } from 'use-neo4j';\nimport { applicationGetConnection, applicationGetStandaloneSettings } from '../application/ApplicationSelectors';\nimport { connect } from 'react-redux';\nimport NeoDashboardConnectionUpdateHandler from '../component/misc/DashboardConnectionUpdateHandler';\nimport { forceRefreshPage } from '../page/PageActions';\nimport { getPageNumber } from '../settings/SettingsSelectors';\nimport { createNotificationThunk } from '../page/PageThunks';\nimport { version } from '../modal/AboutModal';\nimport NeoDashboardSidebar from './sidebar/DashboardSidebar';\n\nconst Dashboard = ({\n  pagenumber,\n  connection,\n  standaloneSettings,\n  onConnectionUpdate,\n  onDownloadDashboardAsImage,\n  onAboutModalOpen,\n  resetApplication,\n}) => {\n  const [driver, setDriver] = React.useState(undefined);\n\n  // If no driver is yet instantiated, create a new one.\n  if (driver == undefined) {\n    const newDriver = createDriver(\n      connection.protocol,\n      connection.url,\n      connection.port,\n      connection.username,\n      connection.password,\n      { userAgent: `neodash/v${version}` }\n    );\n    setDriver(newDriver);\n  }\n  const content = (\n    <Neo4jProvider driver={driver}>\n      <NeoDashboardConnectionUpdateHandler\n        pagenumber={pagenumber}\n        connection={connection}\n        onConnectionUpdate={onConnectionUpdate}\n      />\n\n      {/* Navigation Bar */}\n      <div\n        className='n-w-screen n-flex n-flex-row n-items-center n-bg-neutral-bg-weak n-border-b'\n        style={{ borderColor: 'lightgrey' }}\n      >\n        <NeoDashboardHeader\n          connection={connection}\n          onDownloadImage={onDownloadDashboardAsImage}\n          onAboutModalOpen={onAboutModalOpen}\n          resetApplication={resetApplication}\n        ></NeoDashboardHeader>\n      </div>\n      {/* Main Page */}\n      <div\n        style={{\n          display: 'flex',\n          height: 'calc(40vh - 32px)',\n          minHeight: window.innerHeight - 62,\n          overflow: 'hidden',\n          position: 'relative',\n        }}\n      >\n        {!standaloneSettings.standalone || (standaloneSettings.standalone && standaloneSettings.standaloneAllowLoad) ? (\n          <NeoDashboardSidebar />\n        ) : (\n          <></>\n        )}\n        <div className='n-w-full n-h-full n-flex n-flex-col n-items-center n-justify-center n-rounded-md'>\n          <div className='n-w-full n-h-full n-overflow-y-scroll n-flex n-flex-row'>\n            {/* Main Content */}\n            <main className='n-flex-1 n-relative n-z-0 n-scroll-smooth n-w-full'>\n              <div className='n-absolute n-inset-0 page-spacing'>\n                <div className='page-spacing-overflow'>\n                  {/* The main content of the page */}\n\n                  <div>\n                    {standaloneSettings.standalonePassword &&\n                    standaloneSettings.standalonePasswordWarningHidden !== true ? (\n                      <div style={{ textAlign: 'center', color: 'red', paddingTop: 60, marginBottom: -50 }}>\n                        Warning: NeoDash is running with a plaintext password in config.json.\n                      </div>\n                    ) : (\n                      <></>\n                    )}\n                    <NeoDashboardTitle />\n                    <NeoDashboardHeaderPageList />\n                    <NeoPage></NeoPage>\n                  </div>\n                </div>\n              </div>\n            </main>\n          </div>\n        </div>\n      </div>\n    </Neo4jProvider>\n  );\n  return content;\n};\n\nconst mapStateToProps = (state) => ({\n  connection: applicationGetConnection(state),\n  pagenumber: getPageNumber(state),\n  standaloneSettings: applicationGetStandaloneSettings(state),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  onConnectionUpdate: (pagenumber) => {\n    dispatch(\n      createNotificationThunk(\n        'Connection Updated',\n        'You have updated your Neo4j connection, your reports have been reloaded.'\n      )\n    );\n    dispatch(forceRefreshPage(pagenumber));\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(Dashboard);\n"
  },
  {
    "path": "src/dashboard/DashboardActions.ts",
    "content": "export const RESET_DASHBOARD_STATE = 'DASHBOARD/RESET_DASHBOARD_STATE';\nexport const resetDashboardState = () => ({\n  type: RESET_DASHBOARD_STATE,\n  payload: {},\n});\n\nexport const SET_DASHBOARD = 'DASHBOARD/SET_DASHBOARD';\nexport const setDashboard = (dashboard: any) => ({\n  type: SET_DASHBOARD,\n  payload: { dashboard },\n});\n\nexport const SET_DASHBOARD_UUID = 'DASHBOARD/SET_DASHBOARD_UUID';\nexport const setDashboardUuid = (uuid: any) => ({\n  type: SET_DASHBOARD_UUID,\n  payload: { uuid },\n});\n\nexport const SET_DASHBOARD_TITLE = 'DASHBOARD/SET_DASHBOARD_TITLE';\nexport const setDashboardTitle = (title: any) => ({\n  type: SET_DASHBOARD_TITLE,\n  payload: { title },\n});\n\nexport const CREATE_PAGE = 'DASHBOARD/CREATE_PAGE';\nexport const addPage = () => ({\n  type: CREATE_PAGE,\n  payload: {},\n});\n\nexport const REMOVE_PAGE = 'DASHBOARD/REMOVE_PAGE';\nexport const removePage = (number: any) => ({\n  type: REMOVE_PAGE,\n  payload: { number },\n});\n\nexport const MOVE_PAGE = 'DASHBOARD/MOVE_PAGE';\nexport const movePage = (oldIndex: any, newIndex: any) => ({\n  type: MOVE_PAGE,\n  payload: { oldIndex, newIndex },\n});\n\nexport const SET_EXTENSION_ENABLED = 'DASHBOARD/SET_EXTENSION_ENABLED';\nexport const setExtensionEnabled = (name: string, enabled: boolean) => ({\n  type: SET_EXTENSION_ENABLED,\n  payload: { name, enabled },\n});\n"
  },
  {
    "path": "src/dashboard/DashboardReducer.ts",
    "content": "/**\n * Reducers define changes to the application state when a given action\n */\n\nimport { DEFAULT_DASHBOARD_TITLE } from '../config/ApplicationConfig';\nimport { extensionsReducer, INITIAL_EXTENSIONS_STATE } from '../extensions/state/ExtensionReducer';\nimport { PAGE_EXAMPLE_STATE, pageReducer, PAGE_EMPTY_STATE } from '../page/PageReducer';\nimport { settingsReducer, SETTINGS_INITIAL_STATE } from '../settings/SettingsReducer';\n\nimport {\n  CREATE_PAGE,\n  REMOVE_PAGE,\n  SET_DASHBOARD_TITLE,\n  RESET_DASHBOARD_STATE,\n  SET_DASHBOARD,\n  MOVE_PAGE,\n  SET_EXTENSION_ENABLED,\n  SET_DASHBOARD_UUID,\n} from './DashboardActions';\n\nexport const NEODASH_VERSION = '2.4';\nexport const VERSION_TO_MIGRATE = {\n  '1.1': '2.0',\n  '2.0': '2.1',\n  '2.1': '2.2',\n  '2.2': '2.3',\n  '2.3': '2.4',\n};\n\nexport const initialState = {\n  title: DEFAULT_DASHBOARD_TITLE,\n  version: NEODASH_VERSION,\n  settings: SETTINGS_INITIAL_STATE,\n  pages: [PAGE_EXAMPLE_STATE],\n  parameters: {},\n  extensions: INITIAL_EXTENSIONS_STATE,\n};\n\nexport const emptyDashboardState = {\n  title: DEFAULT_DASHBOARD_TITLE,\n  version: NEODASH_VERSION,\n  settings: SETTINGS_INITIAL_STATE,\n  pages: [PAGE_EMPTY_STATE],\n  parameters: {},\n  extensions: INITIAL_EXTENSIONS_STATE,\n};\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\nexport const dashboardReducer = (state = initialState, action: { type: any; payload: any }) => {\n  const { type, payload } = action;\n\n  // Page-specific updates are deferred to the page reducer.\n  if (action.type.startsWith('PAGE/')) {\n    const { pagenumber = state.settings.pagenumber } = payload;\n    return {\n      ...state,\n      pages: [\n        ...state.pages.slice(0, pagenumber),\n        pageReducer(state.pages[pagenumber], action),\n        ...state.pages.slice(pagenumber + 1),\n      ],\n    };\n  }\n\n  // Settings-specific updates are deferred to the settings reducer.\n  if (action.type.startsWith('SETTINGS/')) {\n    const enrichedPayload = update(payload, { dashboard: state });\n    const enrichedAction = { type, payload: enrichedPayload };\n    return {\n      ...state,\n      settings: settingsReducer(state, enrichedAction),\n    };\n  }\n\n  // Extensions-specific updates are deferred to the extensions reducer.\n  if (action.type.startsWith('DASHBOARD/EXTENSIONS')) {\n    return {\n      ...state,\n      extensions: extensionsReducer(state.extensions, action),\n    };\n  }\n\n  // Global dashboard updates are handled here.\n  switch (type) {\n    case RESET_DASHBOARD_STATE: {\n      return { ...emptyDashboardState };\n    }\n    case SET_DASHBOARD: {\n      const { dashboard } = payload;\n      return { ...dashboard };\n    }\n    case SET_DASHBOARD_UUID: {\n      const { uuid } = payload;\n      return { uuid: uuid, ...state };\n    }\n    case SET_DASHBOARD_TITLE: {\n      const { title } = payload;\n      return { ...state, title: title };\n    }\n    case SET_EXTENSION_ENABLED: {\n      const { name, enabled } = payload;\n      const extensions = state.extensions ? { ...state.extensions } : {};\n      // If the extension was enabled before, remember the old settings and toggle the 'active' switch.\n      extensions[name] = extensions[name] == undefined ? { active: enabled } : { ...extensions[name], active: enabled };\n      return { ...state, extensions: extensions };\n    }\n    case CREATE_PAGE: {\n      return { ...state, pages: [...state.pages, PAGE_EMPTY_STATE] };\n    }\n    case REMOVE_PAGE: {\n      // Removes the card at a given index on a selected page number.\n      const { number } = payload;\n      const pagesInFront = state.pages.slice(0, number);\n      const pagesBehind = state.pages.slice(number + 1);\n\n      return {\n        ...state,\n        pages: pagesInFront.concat(pagesBehind),\n      };\n    }\n    case MOVE_PAGE: {\n      // Moves a page from a given index to a new index.\n      const { oldIndex, newIndex } = payload;\n\n      const element = state.pages.splice(oldIndex, 1)[0];\n      state.pages.splice(newIndex, 0, element);\n\n      return {\n        ...state,\n        pages: state.pages,\n      };\n    }\n    default: {\n      return state;\n    }\n  }\n};\n"
  },
  {
    "path": "src/dashboard/DashboardSelectors.ts",
    "content": "export const getDashboardUuid = (state: any) => state.dashboard.uuid;\n\nexport const getDashboardTitle = (state: any) => state.dashboard.title;\n\nexport const getDashboardSettings = (state: any) => state.dashboard.settings;\n\nexport const getDashboardTheme = (state: any) => state?.dashboard?.settings?.theme ?? 'light';\n\nexport const getDashboardExtensions = (state: any) => {\n  const { extensions } = state.dashboard;\n  if (!extensions) {\n    return {};\n  }\n\n  return Object.fromEntries(Object.entries(extensions).filter(([_, v]) => v.active));\n};\n\nexport const getPages = (state: any) => state.dashboard.pages;\n\nexport const getPageNumbersAndNames = (state: any) => {\n  let pageNames = state.dashboard.pages.reduce((acc, page, idx) => {\n    acc.push(`${idx}/${page.title}`);\n    return acc;\n  }, []);\n  return pageNames;\n};\n"
  },
  {
    "path": "src/dashboard/DashboardThunks.ts",
    "content": "import { createNotificationThunk } from '../page/PageThunks';\nimport { updateDashboardSetting } from '../settings/SettingsActions';\nimport { addPage, movePage, removePage, resetDashboardState, setDashboard, setDashboardUuid } from './DashboardActions';\nimport { QueryStatus, runCypherQuery } from '../report/ReportQueryRunner';\nimport { setDraft, setParametersToLoadAfterConnecting, setWelcomeScreenOpen } from '../application/ApplicationActions';\nimport { updateGlobalParametersThunk,setPageNumberThunk } from '../settings/SettingsThunks';\nimport { createUUID } from '../utils/uuid';\nimport { createLogThunk } from '../application/logging/LoggingThunk';\nimport { applicationGetConnectionUser, applicationIsStandalone } from '../application/ApplicationSelectors';\nimport { applicationGetLoggingSettings } from '../application/logging/LoggingSelectors';\nimport { NEODASH_VERSION, VERSION_TO_MIGRATE } from './DashboardReducer';\n\nexport const removePageThunk = (number) => (dispatch: any, getState: any) => {\n  try {\n    const numberOfPages = getState().dashboard.pages.length;\n    const pageIndex = getState().dashboard.settings.pagenumber;\n    if (numberOfPages == 1) {\n      dispatch(createNotificationThunk('Cannot remove page', \"You can't remove the only page of a dashboard.\"));\n      return;\n    }\n\n    dispatch(removePage(number));\n\n    if (number <= pageIndex) {\n      dispatch(updateDashboardSetting('pagenumber', Math.max(pageIndex - 1)));\n    }\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to remove page', e));\n  }\n};\n\nexport const addPageThunk = () => (dispatch: any, getState: any) => {\n  try {\n    const numberOfPages = getState().dashboard.pages.length;\n    dispatch(addPage());\n    dispatch(updateDashboardSetting('pagenumber', numberOfPages));\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to create page', e));\n  }\n};\n\nexport const movePageThunk = (oldIndex: number, newIndex: number) => (dispatch: any, getState: any) => {\n  try {\n    const pageIndex = getState().dashboard.settings.pagenumber;\n    if (pageIndex == oldIndex) {\n      dispatch(updateDashboardSetting('pagenumber', newIndex));\n    } else if (oldIndex > pageIndex && pageIndex >= newIndex) {\n      dispatch(updateDashboardSetting('pagenumber', pageIndex + 1));\n    } else if (oldIndex < pageIndex && pageIndex <= newIndex) {\n      dispatch(updateDashboardSetting('pagenumber', pageIndex - 1));\n    }\n    dispatch(movePage(oldIndex, newIndex));\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to move page', e));\n  }\n};\n\nexport const loadDashboardThunk = (uuid, text) => (dispatch: any, getState: any) => {\n  try {\n    if (text.length == 0) {\n      throw 'No dashboard file specified. Did you select a file?';\n    }\n    if (text.trim() == '{}') {\n      dispatch(resetDashboardState());\n      return;\n    }\n    let dashboard = JSON.parse(text);\n\n    // If we load a debug report, take out the 'dashboard' value and set it to safe values.\n    if (dashboard._persist && dashboard.application && dashboard.dashboard) {\n      dispatch(\n        createNotificationThunk('Loaded a Debug Report', \"Recovery-mode active. All report types were set to 'table'.\")\n      );\n      dashboard.dashboard.pages.map((p) => {\n        p.reports.map((r) => {\n          r.type = 'table';\n        });\n      });\n      dashboard = dashboard.dashboard;\n    }\n\n    let patched;\n    [dashboard, patched] = patchDashboardVersion(dashboard, dashboard.version);\n    if (patched) {\n      dispatch(\n        createNotificationThunk(\n          'Successfully patched dashboard',\n          `Your old dashboard has been patched. You might need to refresh this page and reactivate extensions.`\n        )\n      );\n    }\n\n    // Attempt upgrade if dashboard version is outdated.\n    while (VERSION_TO_MIGRATE[dashboard.version]) {\n      const upgradedDashboard = upgradeDashboardVersion(\n        dashboard,\n        dashboard.version,\n        VERSION_TO_MIGRATE[dashboard.version]\n      );\n      dispatch(setDashboard(upgradedDashboard));\n      dispatch(setWelcomeScreenOpen(false));\n      dispatch(setDraft(true));\n      dispatch(\n        createNotificationThunk(\n          'Successfully upgraded dashboard',\n          `Your old dashboard was migrated to version ${upgradedDashboard.version}. You might need to refresh this page and reactivate extensions.`\n        )\n      );\n    }\n\n    if (dashboard.version !== NEODASH_VERSION) {\n      throw `Invalid dashboard version: ${dashboard.version}. Try restarting the application, or retrieve your cached dashboard using a debug report.`;\n    }\n\n    // Reverse engineer the minimal set of fields from the selection loaded.\n    dashboard.pages.forEach((p) => {\n      p.reports.forEach((r) => {\n        if (r.selection) {\n          r.fields = [];\n          Object.keys(r.selection).forEach((f) => {\n            r.fields.push([f, r.selection[f]]);\n          });\n        }\n      });\n    });\n\n    dispatch(setDashboard(dashboard));\n    \n    // Check if we have to navigate to a page\n    const queryString = window.location.search;\n    const urlParams = new URLSearchParams(queryString);\n    const pageToSet = urlParams.get('page');\n    if (pageToSet !== '' && pageToSet !== null) {\n      if (!isNaN(pageToSet)) {\n        dispatch(setPageNumberThunk(pageToSet));\n      }\n    }\n\n\n    const { application } = getState();\n\n    dispatch(updateGlobalParametersThunk(application.parametersToLoadAfterConnecting));\n    dispatch(updateGlobalParametersThunk(dashboard.settings.parameters));\n    dispatch(setParametersToLoadAfterConnecting(null));\n    // Pre-2.3.4 dashboards might now always have a UUID. Set it if not present.\n    if (!dashboard.uuid) {\n      dispatch(setDashboardUuid(uuid));\n    }\n  } catch (e) {\n    console.log(e);\n    dispatch(createNotificationThunk('Unable to load dashboard', e));\n  }\n};\n\nexport const saveDashboardToNeo4jThunk =\n  (driver, database, dashboard, date, user, onSuccess) => (dispatch: any, getState: any) => {\n    const state = getState();\n    const loggingSettings = applicationGetLoggingSettings(state);\n    const loguser = applicationGetConnectionUser(state);\n    const neodashMode = applicationIsStandalone(state) ? 'Standalone' : 'Editor';\n\n    try {\n      let { uuid } = dashboard;\n\n      // Dashboards pre-2.3.4 may not always have a UUID. If this is the case, generate one just before we save.\n      if (!dashboard.uuid) {\n        uuid = createUUID();\n        dashboard.uuid = uuid;\n        dispatch(setDashboardUuid(uuid));\n        createUUID();\n      }\n\n      const { title, version } = dashboard;\n\n      // Generate a cypher query to save the dashboard.\n      const query =\n        'MERGE (n:_Neodash_Dashboard {uuid: $uuid }) SET n.title = $title, n.version = $version, n.user = $user, n.content = $content, n.date = datetime($date) RETURN $uuid as uuid';\n\n      const parameters = {\n        uuid: uuid,\n        title: title,\n        version: version,\n        user: user,\n        content: JSON.stringify(dashboard, null, 2),\n        date: date,\n      };\n      runCypherQuery(\n        driver,\n        database,\n        query,\n        parameters,\n        1,\n        () => {},\n        (records) => {\n          if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) {\n            dispatch(createNotificationThunk('🎉 Success!', 'Your current dashboard was saved to Neo4j.'));\n            onSuccess(uuid);\n            if (loggingSettings.loggingMode > '1') {\n              dispatch(\n                createLogThunk(\n                  driver,\n                  loggingSettings.loggingDatabase,\n                  neodashMode,\n                  loguser,\n                  'INF - save dashboard',\n                  database,\n                  `Name:${title}`,\n                  `User ${loguser} saved dashboard to Neo4J in ${neodashMode} mode at ${Date(Date.now()).substring(\n                    0,\n                    33\n                  )}`\n                )\n              );\n            }\n          } else {\n            dispatch(\n              createNotificationThunk(\n                'Unable to save dashboard',\n                `Do you have write access to the '${database}' database?`\n              )\n            );\n            if (loggingSettings.loggingMode > '1') {\n              dispatch(\n                createLogThunk(\n                  driver,\n                  loggingSettings.loggingDatabase,\n                  neodashMode,\n                  loguser,\n                  'ERR - save dashboard',\n                  database,\n                  `Name:${title}`,\n                  `Error while trying to save dashboard to Neo4J in ${neodashMode} mode at ${Date(Date.now()).substring(\n                    0,\n                    33\n                  )}`\n                )\n              );\n            }\n          }\n        }\n      );\n    } catch (e) {\n      dispatch(createNotificationThunk('Unable to save dashboard to Neo4j', e));\n      if (loggingSettings.loggingMode > '1') {\n        dispatch(\n          createLogThunk(\n            driver,\n            loggingSettings.loggingDatabase,\n            neodashMode,\n            loguser,\n            'ERR - save dashboard',\n            database,\n            'Name:Not fetched',\n            `Error while trying to save dashboard to Neo4J in ${neodashMode} mode at ${Date(Date.now()).substring(\n              0,\n              33\n            )}`\n          )\n        );\n      }\n    }\n  };\n\nexport const deleteDashboardFromNeo4jThunk = (driver, database, uuid, onSuccess) => (dispatch: any) => {\n  try {\n    // Generate a cypher query to save the dashboard.\n    const query = 'MATCH (n:_Neodash_Dashboard {uuid: $uuid }) DETACH DELETE n RETURN $uuid as uuid';\n\n    const parameters = {\n      uuid: uuid,\n    };\n    runCypherQuery(\n      driver,\n      database,\n      query,\n      parameters,\n      1,\n      () => {},\n      (records) => {\n        if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) {\n          onSuccess(uuid);\n        } else {\n          dispatch(\n            createNotificationThunk(\n              'Unable to delete dashboard',\n              `Do you have write access to the '${database}' database?`\n            )\n          );\n        }\n      }\n    );\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to delete dashboard from Neo4j', e));\n  }\n};\n\nexport const loadDashboardFromNeo4jThunk = (driver, database, uuid, callback) => (dispatch: any, getState: any) => {\n  const state = getState();\n  const loggingSettings = applicationGetLoggingSettings(state);\n  const loguser = applicationGetConnectionUser(state);\n  const neodashMode = applicationIsStandalone(state) ? 'Standalone' : 'Editor';\n\n  try {\n    const query = 'MATCH (n:_Neodash_Dashboard) WHERE n.uuid = $uuid RETURN n.content as dashboard';\n    runCypherQuery(\n      driver,\n      database,\n      query,\n      { uuid: uuid },\n      1,\n      (status) => {\n        if (status == QueryStatus.NO_DATA) {\n          dispatch(\n            createNotificationThunk(\n              `Unable to load dashboard from database '${database}'.`,\n              `A dashboard with UUID '${uuid}' does not exist.`\n            )\n          );\n        }\n      },\n      (records) => {\n        if (!records[0]._fields) {\n          dispatch(\n            createNotificationThunk(\n              `Unable to load dashboard from database '${database}'.`,\n              `A dashboard with UUID '${uuid}' could not be loaded.`\n            )\n          );\n          if (loggingSettings.loggingMode > '1') {\n            dispatch(\n              createLogThunk(\n                driver,\n                loggingSettings.loggingDatabase,\n                neodashMode,\n                loguser,\n                'ERR - load dashboard',\n                database,\n                `UUID:${uuid}`,\n                `Error while trying to load dashboard by UUID in ${neodashMode} mode at ${Date(Date.now()).substring(\n                  0,\n                  33\n                )}`\n              )\n            );\n          }\n        } else {\n          callback(records[0]._fields[0]);\n          if (loggingSettings.loggingMode > '1') {\n            const dashboard = JSON.parse(records[0]._fields[0]);\n            dispatch(\n              createLogThunk(\n                driver,\n                loggingSettings.loggingDatabase,\n                neodashMode,\n                loguser,\n                'INF - load dashboard',\n                database,\n                `Name:${dashboard.title}`,\n                `User ${loguser} Loaded dashboard by UUID in ${neodashMode} mode at ${Date(Date.now()).substring(\n                  0,\n                  33\n                )}`\n              )\n            );\n          }\n        }\n      }\n    );\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to load dashboard to Neo4j', e));\n    if (loggingSettings.loggingMode > '1') {\n      dispatch(\n        createLogThunk(\n          driver,\n          loggingSettings.loggingDatabase,\n          neodashMode,\n          loguser,\n          'ERR - load dashboard',\n          database,\n          `UUID:${uuid}`,\n          `Error while trying to load dashboard by UUID in ${neodashMode} mode at ${Date(Date.now()).substring(0, 33)}`\n        )\n      );\n    }\n  }\n};\n\nexport const loadDashboardFromNeo4jByNameThunk =\n  (driver, database, name, callback) => (dispatch: any, getState: any) => {\n    const loggingState = getState();\n    const loggingSettings = applicationGetLoggingSettings(loggingState);\n    const loguser = applicationGetConnectionUser(loggingState);\n    const neodashMode = applicationIsStandalone(loggingState) ? 'Standalone' : 'Editor';\n    try {\n      const query =\n        'MATCH (d:_Neodash_Dashboard) WHERE d.title = $name RETURN d.content as dashboard ORDER by d.date DESC LIMIT 1';\n      runCypherQuery(\n        driver,\n        database,\n        query,\n        { name: name },\n        1,\n        (status) => {\n          if (status == QueryStatus.NO_DATA) {\n            dispatch(\n              createNotificationThunk(\n                'Unable to load dashboard.',\n                'A dashboard with the provided name could not be found.'\n              )\n            );\n          }\n        },\n        (records) => {\n          if (records.length == 0) {\n            dispatch(\n              createNotificationThunk(\n                `Unable to load dashboard \"${name}\".`,\n                'A dashboard with the provided name could not be found.'\n              )\n            );\n            if (loggingSettings.loggingMode > '1') {\n              dispatch(\n                createLogThunk(\n                  driver,\n                  loggingSettings.loggingDatabase,\n                  neodashMode,\n                  loguser,\n                  'ERR - load dashboard',\n                  database,\n                  `Name:${name}`,\n                  `Error while trying to load dashboard by Name in ${neodashMode} mode at ${Date(Date.now()).substring(\n                    0,\n                    33\n                  )}`\n                )\n              );\n            }\n            return;\n          }\n\n          if (records[0].error) {\n            dispatch(createNotificationThunk(`Unable to load dashboard \"${name}\".`, records[0].error));\n            if (loggingSettings.loggingMode > '1') {\n              dispatch(\n                createLogThunk(\n                  driver,\n                  loggingSettings.loggingDatabase,\n                  neodashMode,\n                  loguser,\n                  'ERR - load dashboard',\n                  database,\n                  `Name:${name}`,\n                  `Error while trying to load dashboard by Name in ${neodashMode} mode at ${Date(Date.now()).substring(\n                    0,\n                    33\n                  )}`\n                )\n              );\n            }\n            return;\n          }\n\n          if (loggingSettings.loggingMode > '1') {\n            dispatch(\n              createLogThunk(\n                driver,\n                loggingSettings.loggingDatabase,\n                neodashMode,\n                loguser,\n                'INF - load dashboard',\n                database,\n                `Name:${name}`,\n                `User ${loguser} Loaded dashboard by UUID in ${neodashMode} mode at ${Date(Date.now()).substring(\n                  0,\n                  33\n                )}`\n              )\n            );\n          }\n          callback(records[0]._fields[0]);\n        }\n      );\n    } catch (e) {\n      dispatch(createNotificationThunk('Unable to load dashboard from Neo4j', e));\n    }\n  };\n\nexport const loadDashboardListFromNeo4jThunk = (driver, database, callback) => (dispatch: any) => {\n  function setStatus(status) {\n    if (status == QueryStatus.NO_DATA) {\n      runCallback([]);\n    }\n  }\n  function runCallback(records) {\n    if (!records || !records[0] || !records[0]._fields) {\n      callback([]);\n      return;\n    }\n    const result = records.map((r, index) => {\n      return {\n        uuid: r._fields[0],\n        title: r._fields[1],\n        date: r._fields[2],\n        author: r._fields[3],\n        version: r._fields[4],\n        index: index,\n      };\n    });\n    callback(result);\n  }\n  try {\n    runCypherQuery(\n      driver,\n      database,\n      'MATCH (n:_Neodash_Dashboard) RETURN n.uuid as uuid, n.title as title, toString(n.date) as date,  n.user as author, n.version as version ORDER BY toLower(n.title) ASC',\n      {},\n      1000,\n      (status) => setStatus(status),\n      (records) => runCallback(records)\n    );\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to load dashboard list from Neo4j', e));\n  }\n};\n\nexport const loadDatabaseListFromNeo4jThunk = (driver, callback) => (dispatch: any) => {\n  try {\n    runCypherQuery(\n      driver,\n      'system',\n      'SHOW DATABASES yield name, currentStatus WHERE currentStatus = \"online\" RETURN DISTINCT name',\n      {},\n      1000,\n      () => {},\n      (records) => {\n        const result = records.map((r) => {\n          return r._fields && r._fields[0];\n        });\n        callback(result);\n      }\n    );\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to list databases from Neo4j', e));\n  }\n};\n\nexport const assignDashboardUuidIfNotPresentThunk = () => (dispatch: any, getState: any) => {\n  const { uuid } = getState().dashboard;\n  if (!uuid) {\n    dispatch(setDashboardUuid(createUUID()));\n  }\n};\nexport function patchDashboardVersion(dashboard: any, version: any) {\n  let patched = false;\n  if (version == '2.4') {\n    dashboard.pages.forEach((p) => {\n      p.reports.forEach((r) => {\n        if (r.type == 'graph' || r.type == 'map' || r.type == 'graph3d') {\n          r.settings?.actionsRules?.forEach((rule) => {\n            if (\n              rule?.field &&\n              (rule?.condition === 'onNodeClick' || rule?.condition == 'Click') &&\n              rule.value.includes('.')\n            ) {\n              let val = rule.value.split('.');\n              rule.value = val[val.length - 1] || rule.value;\n              patched = true;\n            }\n          });\n        }\n      });\n    });\n  }\n  return [dashboard, patched];\n}\n\nexport function upgradeDashboardVersion(dashboard: any, origin: string, target: string) {\n  if (origin == '2.3' && target == '2.4') {\n    dashboard.pages.forEach((p) => {\n      p.reports.forEach((r) => {\n        r.x *= 2;\n        r.y *= 2;\n        r.width *= 2;\n        r.height *= 2;\n\n        if (r.type == 'graph' || r.type == 'map' || r.type == 'graph3d') {\n          r.settings?.actionsRules?.forEach((rule) => {\n            if (\n              rule?.field &&\n              (rule?.condition === 'onNodeClick' || rule?.condition == 'Click') &&\n              rule.value.includes('.')\n            ) {\n              let val = rule.value.split('.');\n              rule.value = val[val.length - 1] || rule.value;\n            }\n          });\n        }\n      });\n    });\n    dashboard.version = '2.4';\n    return dashboard;\n  }\n  // In 2.3 uuids were created, as well as a new format for specificing extensions.\n  if (origin == '2.2' && target == '2.3') {\n    dashboard.pages.forEach((p) => {\n      p.reports.forEach((r) => {\n        r.id = createUUID();\n      });\n    });\n\n    dashboard.extensions = {\n      'advanced-charts': {\n        active: true,\n      },\n      styling: {\n        active: true,\n      },\n      active: true,\n      activeReducers: [],\n    };\n    dashboard.version = '2.3';\n    return dashboard;\n  }\n  if (origin == '2.1' && target == '2.2') {\n    // In 2.1, extensions were enabled by default. Therefore if we migrate, enable them.\n    dashboard.extensions = {\n      'advanced-charts': true,\n      styling: true,\n    };\n    dashboard.version = '2.2';\n    return dashboard;\n  }\n  if (origin == '2.0' && target == '2.1') {\n    dashboard.pages.forEach((p, i) => {\n      // From v2.1 onwards, reports will have their x,y positions explicitly specified.\n      // v2.0 dashboards do not have this, therefore we must assign them.\n      // Additionally we divide the old report height by 1.5 (adjusted vertical scaling factor).\n\n      let xPos = 0;\n      let yPos = 0;\n      let rowHeight = 1;\n      p.reports.forEach((r, j) => {\n        const reportWidth = parseInt(r.width);\n        const reportHeight = parseInt(r.height);\n        dashboard.pages[i].reports[j] = { x: xPos, y: yPos, ...dashboard.pages[i].reports[j] };\n        dashboard.pages[i].reports[j].height = reportHeight / 1.5;\n        xPos += reportWidth;\n        rowHeight = Math.max(reportHeight / 1.5, rowHeight);\n        if (xPos >= 12) {\n          xPos = 0;\n          yPos += rowHeight;\n          rowHeight = 1;\n        }\n      });\n    });\n    dashboard.version = '2.1';\n    return dashboard;\n  }\n  if (origin == '1.1' && target == '2.0') {\n    const upgradedDashboard = {};\n    upgradedDashboard.title = dashboard.title;\n    upgradedDashboard.version = '2.0';\n    upgradedDashboard.settings = {\n      pagenumber: dashboard.pagenumber,\n      editable: dashboard.editable,\n    };\n    const upgradedDashboardPages = [];\n    dashboard.pages.forEach((p) => {\n      const newPage = {};\n      newPage.title = p.title;\n      const newPageReports = [];\n      p.reports.forEach((r) => {\n        // only migrate value report types.\n        if (\n          ['table', 'graph', 'bar', 'line', 'map', 'value', 'json', 'select', 'iframe', 'text'].indexOf(r.type) == -1\n        ) {\n          return;\n        }\n        if (r.type == 'select') {\n          r.query = '';\n        }\n        const newPageReport = {\n          title: r.title,\n          width: r.width,\n          height: r.height * 0.75,\n          type: r.type,\n          parameters: r.parameters,\n          query: r.query,\n          selection: {},\n          settings: {},\n        };\n\n        newPageReports.push(newPageReport);\n      });\n      newPage.reports = newPageReports;\n      upgradedDashboardPages.push(newPage);\n    });\n    upgradedDashboard.pages = upgradedDashboardPages;\n    return upgradedDashboard;\n  }\n  throw new Error(`Invalid upgrade path: ${origin} --> ${target}`);\n}\n"
  },
  {
    "path": "src/dashboard/header/DashboardHeader.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { connect } from 'react-redux';\nimport { setDashboardTitle } from '../DashboardActions';\nimport { getDashboardSettings, getDashboardTheme, getDashboardTitle, getPages } from '../DashboardSelectors';\nimport { setConnectionModalOpen } from '../../application/ApplicationActions';\nimport { applicationGetStandaloneSettings, applicationGetCustomHeader } from '../../application/ApplicationSelectors';\nimport { getDashboardIsEditable, getPageNumber } from '../../settings/SettingsSelectors';\nimport { NeoDashboardHeaderLogo } from './DashboardHeaderLogo';\nimport NeoAboutButton from './DashboardHeaderAboutButton';\nimport { NeoLogoutButton } from './DashboardHeaderLogoutButton';\nimport { NeoDashboardHeaderDownloadImageButton } from './DashboardHeaderDownloadImageButton';\nimport { updateDashboardSetting } from '../../settings/SettingsActions';\nimport { DarkModeSwitch } from 'react-toggle-dark-mode';\nimport { DASHBOARD_HEADER_BUTTON_COLOR } from '../../config/ApplicationConfig';\nimport { Tooltip } from '@mui/material';\n\nexport const NeoDashboardHeader = ({\n  standaloneSettings,\n  dashboardTitle,\n  customHeader,\n  connection,\n  settings,\n  onConnectionModalOpen,\n  onDownloadImage,\n  onAboutModalOpen,\n  resetApplication,\n  themeMode,\n  setTheme,\n}) => {\n  const downloadImageEnabled = settings ? settings.downloadImageEnabled : false;\n  const [dashboardTitleText, setDashboardTitleText] = React.useState(dashboardTitle);\n\n  const [isDarkMode, setDarkMode] = React.useState(themeMode !== 'light');\n\n  const toggleDarkMode = (checked: boolean) => {\n    setDarkMode(checked);\n  };\n\n  useEffect(() => {\n    // Reset text to the dashboard state when the page gets reorganized.\n    if (dashboardTitle !== dashboardTitleText) {\n      setDashboardTitleText(dashboardTitle);\n    }\n  }, [dashboardTitle]);\n\n  useEffect(() => {\n    setTheme(isDarkMode ? 'dark' : 'light');\n  }, [isDarkMode]);\n  const content = (\n    <div className='n-relative n-bg-palette-neutral-bg-weak n-w-full'>\n      <div className='n-min-w-full'>\n        <div className='n-flex n-justify-between n-h-16 n-items-center n-py-6 md:n-justify-start md:n-space-x-10 n-mx-4'>\n          <NeoDashboardHeaderLogo resetApplication={resetApplication} />\n          <nav className='n-items-center n-justify-center n-flex n-flex-1 n-w-full n-font-semibold'>\n            {customHeader && customHeader.length > 0\n              ? `${customHeader}`\n              : `${connection.protocol}://${connection.url}:${connection.port}`}\n          </nav>\n          <div className='sm:n-flex n-items-center n-justify-end md:n-flex-1 lg:n-w-0 n-gap-6'>\n            <div className='n-flex n-flex-row n-gap-x-2'>\n              <Tooltip title={'Change Theme'} disableInteractive>\n                <div>\n                  <DarkModeSwitch\n                    className={'ndl-icon-btn n-p-2 ndl-large ndl-clean'}\n                    style={{}}\n                    checked={isDarkMode}\n                    onChange={toggleDarkMode}\n                    size={24}\n                    sunColor={DASHBOARD_HEADER_BUTTON_COLOR || '#000000'}\n                    moonColor={'#ff0000'}\n                  />\n                </div>\n              </Tooltip>\n\n              {downloadImageEnabled && <NeoDashboardHeaderDownloadImageButton onDownloadImage={onDownloadImage} />}\n              <NeoAboutButton connection={connection} onAboutModalOpen={onAboutModalOpen} />\n              <NeoLogoutButton standaloneSettings={standaloneSettings} onConnectionModalOpen={onConnectionModalOpen} />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n  return content;\n};\n\nconst mapStateToProps = (state) => ({\n  dashboardTitle: getDashboardTitle(state),\n  standaloneSettings: applicationGetStandaloneSettings(state),\n  customHeader: applicationGetCustomHeader(state),\n  pages: getPages(state),\n  settings: getDashboardSettings(state),\n  editable: getDashboardIsEditable(state),\n  pagenumber: getPageNumber(state),\n  themeMode: getDashboardTheme(state),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  setDashboardTitle: (title: any) => {\n    dispatch(setDashboardTitle(title));\n  },\n\n  setTheme: (theme: string) => {\n    dispatch(updateDashboardSetting('theme', theme));\n  },\n\n  onConnectionModalOpen: () => {\n    dispatch(setConnectionModalOpen(true));\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoDashboardHeader);\n"
  },
  {
    "path": "src/dashboard/header/DashboardHeaderAboutButton.tsx",
    "content": "import React, { useState } from 'react';\nimport { connect } from 'react-redux';\nimport { IconButton, Menu, MenuItems, MenuItem } from '@neo4j-ndl/react';\nimport {\n  QuestionMarkCircleIconOutline,\n  BookOpenIconOutline,\n  InformationCircleIconOutline,\n} from '@neo4j-ndl/react/icons';\nimport { Tooltip } from '@mui/material';\n\nimport { DASHBOARD_HEADER_BUTTON_COLOR } from '../../config/ApplicationConfig';\nimport StyleConfig from '../../config/StyleConfig';\nimport { getDashboardExtensions } from '../DashboardSelectors';\nimport { getExampleReports } from '../../extensions/ExtensionUtils';\nimport { NeoReportExamplesModal } from '../../modal/ReportExamplesModal';\nimport { enterHandler, openTab } from '../../utils/accessibility';\n\ntype HelpMenuOpenEvent = React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>;\n\nawait StyleConfig.getInstance();\n\nexport const NeoAboutButton = ({ connection, onAboutModalOpen, extensions }) => {\n  const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);\n  const handleHelpMenuOpen = (event: HelpMenuOpenEvent) => {\n    setAnchorEl(event.currentTarget);\n  };\n  const handleHelpMenuClose = () => {\n    setAnchorEl(null);\n  };\n  const menuOpen = Boolean(anchorEl);\n\n  const menuAboutHandler = (e) => {\n    onAboutModalOpen(e);\n    handleHelpMenuClose();\n  };\n\n  return (\n    <>\n      <Tooltip title={'Help and documentation'} disableInteractive>\n        <IconButton\n          className='logo-btn n-p-1'\n          aria-label={'help'}\n          style={DASHBOARD_HEADER_BUTTON_COLOR ? { color: DASHBOARD_HEADER_BUTTON_COLOR } : {}}\n          size='large'\n          onClick={handleHelpMenuOpen}\n          clean\n        >\n          <QuestionMarkCircleIconOutline className='header-icon' type='outline' />\n        </IconButton>\n      </Tooltip>\n      <Menu\n        anchorOrigin={{\n          horizontal: 'right',\n          vertical: 'bottom',\n        }}\n        transformOrigin={{\n          horizontal: 'right',\n          vertical: 'top',\n        }}\n        anchorEl={anchorEl}\n        open={menuOpen}\n        onClose={handleHelpMenuClose}\n        size='large'\n      >\n        <MenuItems>\n          <NeoReportExamplesModal\n            extensions={extensions}\n            examples={getExampleReports(extensions)}\n            database={connection.database}\n          ></NeoReportExamplesModal>\n          <MenuItem\n            onKeyDown={(e) =>\n              enterHandler(e, () =>\n                openTab(\n                  'https://github.com/neo4j-labs/neodash/tree/master/docs/modules/ROOT/pages/user-guide/index.adoc'\n                )\n              )\n            }\n            onClick={() =>\n              openTab('https://github.com/neo4j-labs/neodash/tree/master/docs/modules/ROOT/pages/user-guide/index.adoc')\n            }\n            title={'Documentation'}\n            icon={<BookOpenIconOutline />}\n          />\n          <MenuItem\n            title={'About'}\n            onClick={menuAboutHandler}\n            onKeyDown={(e) => enterHandler(e, menuAboutHandler)}\n            icon={<InformationCircleIconOutline />}\n          />\n        </MenuItems>\n      </Menu>\n    </>\n  );\n};\n\nconst mapStateToProps = (state) => ({\n  extensions: getDashboardExtensions(state),\n});\n\nexport default connect(mapStateToProps, null)(NeoAboutButton);\n"
  },
  {
    "path": "src/dashboard/header/DashboardHeaderDownloadImageButton.tsx",
    "content": "import React from 'react';\nimport { IconButton } from '@neo4j-ndl/react';\nimport { Tooltip } from '@mui/material';\nimport { CameraIconSolid } from '@neo4j-ndl/react/icons';\n\nimport { DASHBOARD_HEADER_BUTTON_COLOR } from '../../config/ApplicationConfig';\nimport StyleConfig from '../../config/StyleConfig';\n\nawait StyleConfig.getInstance();\n\nexport const NeoDashboardHeaderDownloadImageButton = (onDownloadImage) => {\n  const content = (\n    <Tooltip title={'Download Dashboard as Image'} disableInteractive>\n      <IconButton\n        aria-label={'camera'}\n        style={DASHBOARD_HEADER_BUTTON_COLOR ? { color: DASHBOARD_HEADER_BUTTON_COLOR } : {}}\n        onClick={() => onDownloadImage()}\n        size='large'\n        clean\n      >\n        <CameraIconSolid aria-label={'camera icon'} />\n      </IconButton>\n    </Tooltip>\n  );\n\n  return content;\n};\n\nexport default NeoDashboardHeaderDownloadImageButton;\n"
  },
  {
    "path": "src/dashboard/header/DashboardHeaderLogo.tsx",
    "content": "import React from 'react';\n\nimport { DASHBOARD_HEADER_BRAND_LOGO, IS_CUSTOM_LOGO } from '../../config/ApplicationConfig';\nimport StyleConfig from '../../config/StyleConfig';\nimport { Typography } from '@neo4j-ndl/react';\n\nawait StyleConfig.getInstance();\n\nexport const NeoDashboardHeaderLogo = ({ resetApplication }) => {\n  const content = (\n    <div className='n-items-center sm:n-flex md:n-flex-1 n-justify-start'>\n      <a className='n-cursor-pointer'>\n        <img onClick={resetApplication} className='n-h-6 n-w-auto n-m-2' src={DASHBOARD_HEADER_BRAND_LOGO} alt='Logo' />\n      </a>\n      {IS_CUSTOM_LOGO ? <></> : <Typography variant='h6'>Labs</Typography>}\n    </div>\n  );\n\n  return content;\n};\n\nexport default NeoDashboardHeaderLogo;\n"
  },
  {
    "path": "src/dashboard/header/DashboardHeaderLogoutButton.tsx",
    "content": "import React from 'react';\nimport { connect } from 'react-redux';\nimport { IconButton } from '@neo4j-ndl/react';\nimport { Tooltip } from '@mui/material';\n\nimport { DASHBOARD_HEADER_BUTTON_COLOR } from '../../config/ApplicationConfig';\nimport StyleConfig from '../../config/StyleConfig';\nimport { ArrowRightOnRectangleIconOutline } from '@neo4j-ndl/react/icons';\n\nawait StyleConfig.getInstance();\n\nexport const NeoLogoutButton = ({ standaloneSettings, onConnectionModalOpen }) => {\n  return standaloneSettings.standalone && !standaloneSettings.standaloneMultiDatabase ? (\n    <></>\n  ) : (\n    <Tooltip title={'Log out'} disableInteractive>\n      <IconButton\n        className='logo-btn n-p-1'\n        aria-label={'connection '}\n        style={DASHBOARD_HEADER_BUTTON_COLOR ? { color: DASHBOARD_HEADER_BUTTON_COLOR } : {}}\n        onClick={() => {\n          onConnectionModalOpen();\n        }}\n        size='large'\n        clean\n      >\n        <ArrowRightOnRectangleIconOutline className='header-icon' type='outline' />\n      </IconButton>\n    </Tooltip>\n  );\n};\n\nconst mapStateToProps = () => ({});\n\nconst mapDispatchToProps = () => ({});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoLogoutButton);\n"
  },
  {
    "path": "src/dashboard/header/DashboardHeaderPageList.tsx",
    "content": "import React, { useCallback } from 'react';\nimport { connect } from 'react-redux';\nimport { setDashboardTitle } from '../DashboardActions';\nimport { getPages } from '../DashboardSelectors';\nimport debounce from 'lodash/debounce';\nimport { addPageThunk, movePageThunk } from '../DashboardThunks';\nimport { setConnectionModalOpen } from '../../application/ApplicationActions';\nimport { setPageNumberThunk } from '../../settings/SettingsThunks';\nimport { getDashboardIsEditable, getPageNumber } from '../../settings/SettingsSelectors';\nimport { applicationIsStandalone } from '../../application/ApplicationSelectors';\nimport { Tabs, IconButton } from '@neo4j-ndl/react';\nimport { PlusIconOutline } from '@neo4j-ndl/react/icons';\nimport DashboardHeaderPageTitle from './DashboardHeaderPageTitle';\nimport { DndContext, useSensor, useSensors } from '@dnd-kit/core';\nimport { SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable';\nimport { KeyboardSensor, MouseSensor } from '../../utils/accessibility';\n\n/**\n * The component responsible for rendering the list of pages, as well as the logic for adding, removing, selecting and updating pages.\n */\nexport const NeoDashboardHeaderPageList = ({\n  // standalone,\n  editable,\n  pages,\n  pagenumber,\n  addPage,\n  movePage,\n  selectPage,\n}) => {\n  const [canSwitchPages, setCanSwitchPages] = React.useState(true);\n\n  // We debounce several state changes to improve user experience.\n  const debouncedSetCanSwitchPages = useCallback(debounce(setCanSwitchPages, 50), []);\n\n  const pageAddButton = (\n    <IconButton aria-label={'add page'} className='n-relative -n-top-1' size='large' onClick={addPage} clean>\n      <PlusIconOutline />\n    </IconButton>\n  );\n\n  function handleDragEnd(event) {\n    const { active, over } = event;\n\n    if (!over || !editable) {\n      return;\n    }\n    if (active.id !== over.id) {\n      const oldIndex = parseInt(active.id.split('_')[1]);\n      const newIndex = parseInt(over.id.split('_')[1]);\n      movePage(oldIndex, newIndex);\n    }\n\n    debouncedSetCanSwitchPages(true);\n  }\n\n  const mouseSensor = useSensor(MouseSensor, {\n    activationConstraint: {\n      distance: 5, // Enable sort function when dragging 10px\n    },\n  });\n\n  const keySensor = useSensor(KeyboardSensor, {\n    keyboardCodes: {\n      start: ['Space'],\n      cancel: ['Escape'],\n      end: ['Space'],\n    },\n  });\n\n  const sensors = useSensors(mouseSensor, keySensor);\n\n  const content = (\n    <div className='n-flex n-flex-row n-w-full'>\n      <Tabs fill='underline' onChange={(tabId) => (canSwitchPages ? selectPage(tabId) : null)} value={pagenumber}>\n        <DndContext onDragEnd={handleDragEnd} sensors={sensors}>\n          <SortableContext items={pages} strategy={horizontalListSortingStrategy}>\n            {pages.map((page, i) => (\n              <DashboardHeaderPageTitle\n                title={page.title}\n                tabIndex={i}\n                key={`DashboardHeaderPageTitle_${i}`}\n                disabled={!editable}\n              />\n            ))}\n          </SortableContext>\n        </DndContext>\n      </Tabs>\n      {editable && pageAddButton}\n    </div>\n  );\n  return content;\n};\n\nconst mapStateToProps = (state) => ({\n  standalone: applicationIsStandalone(state),\n  pages: getPages(state),\n  editable: getDashboardIsEditable(state),\n  pagenumber: getPageNumber(state),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  setDashboardTitle: (title: any) => {\n    dispatch(setDashboardTitle(title));\n  },\n  selectPage: (number: any) => {\n    dispatch(setPageNumberThunk(number));\n  },\n  addPage: () => {\n    dispatch(addPageThunk());\n  },\n  movePage: (oldIndex: number, newIndex: number) => {\n    dispatch(movePageThunk(oldIndex, newIndex));\n  },\n  onConnectionModalOpen: () => {\n    dispatch(setConnectionModalOpen(true));\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoDashboardHeaderPageList);\n"
  },
  {
    "path": "src/dashboard/header/DashboardHeaderPageTitle.tsx",
    "content": "import React, { useState, useCallback, useEffect } from 'react';\nimport { connect } from 'react-redux';\nimport classnames from 'classnames';\nimport debounce from 'lodash/debounce';\nimport { setPageTitle } from '../../page/PageActions';\nimport { removePageThunk } from '../DashboardThunks';\nimport { Tab, Menu, MenuItems, MenuItem, IconButton } from '@neo4j-ndl/react';\nimport { EllipsisHorizontalIconOutline, PencilIconOutline, TrashIconOutline } from '@neo4j-ndl/react/icons';\nimport { NeoDeletePageModal } from '../../modal/DeletePageModal';\nimport { useSortable } from '@dnd-kit/sortable';\nimport { CSS } from '@dnd-kit/utilities';\n\ntype MenuEditEvent = React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>;\n\nexport const DashboardHeaderPageTitle = ({ title, tabIndex, removePage, setPageTitle, disabled = false }) => {\n  const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);\n  const menuOpen = Boolean(anchorEl);\n  const [deleteModalOpen, setDeleteModalOpen] = React.useState(false);\n  const [editing, setEditing] = React.useState(false);\n  const [inputWidth, setInputWidth] = React.useState(125);\n  const handleMenuEditClick = (event: MenuEditEvent) => {\n    event.preventDefault();\n    setEditing(!editing);\n    setAnchorEl(null);\n  };\n\n  const handleDeleteModalClose = () => {\n    setAnchorEl(null);\n    setDeleteModalOpen(false);\n  };\n\n  const { attributes, listeners, setNodeRef, transform, transition, isDragging, isSorting } = useSortable({\n    id: `tab_${tabIndex}`,\n  });\n\n  useEffect(() => {\n    // Reset text to the dashboard state when the page gets reorganized.\n    if ((isDragging || isSorting) && editing) {\n      setEditing(false);\n    }\n    setAnchorEl(null);\n  }, [isDragging, isSorting]);\n\n  const style = {\n    transform: CSS.Transform.toString(transform),\n    transition,\n  };\n\n  const debouncedSetPageTitle = useCallback(debounce(setPageTitle, 200), []);\n\n  const [titleText, setTitleText] = React.useState(title);\n  useEffect(() => {\n    // Reset text to the dashboard state when the page gets reorganized.\n    if (titleText !== title) {\n      setTitleText(title);\n    }\n  }, [title]);\n\n  const content = (\n    <div className='n-inline-flex' ref={setNodeRef} style={style} id={`tab_${tabIndex}`} {...attributes} {...listeners}>\n      <Tab tabId={tabIndex} key={tabIndex}>\n        {!editing ? (\n          title ? (\n            title\n          ) : (\n            '(no title)'\n          )\n        ) : (\n          <form\n            onSubmit={(event) => {\n              if (editing) {\n                handleMenuEditClick(event);\n              }\n            }}\n          >\n            <input\n              data-no-dnd='true'\n              autoFocus={true}\n              value={titleText}\n              className=''\n              onBlur={(event) => {\n                if (editing) {\n                  handleMenuEditClick(event);\n                }\n              }}\n              onChange={(event) => {\n                const { target } = event;\n                target.style.width = '125px';\n                setInputWidth(target.scrollWidth);\n\n                if (disabled) {\n                  return;\n                }\n                setTitleText(event.target.value);\n                debouncedSetPageTitle(tabIndex, event.target.value);\n              }}\n              style={{\n                height: '1.9rem',\n                marginBottom: -5,\n                width: inputWidth,\n                paddingLeft: 5,\n                paddingRight: 5,\n              }}\n              placeholder='Page name...'\n            />\n          </form>\n        )}\n        {!disabled && !editing && (\n          <>\n            <IconButton\n              aria-label='Page actions'\n              className={classnames('n-relative n-top-1 visible-on-tab-hover', {\n                'open-menu': menuOpen,\n              })}\n              style={{ height: '1.1rem' }}\n              onClick={(e) => {\n                e.stopPropagation();\n                setAnchorEl(e.currentTarget);\n              }}\n              size='small'\n              clean\n            >\n              <EllipsisHorizontalIconOutline />\n            </IconButton>\n            <Menu anchorEl={anchorEl} open={menuOpen} onClose={() => setAnchorEl(null)}>\n              <MenuItems>\n                <MenuItem\n                  icon={<PencilIconOutline />}\n                  title={'Edit name'}\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    if (editing) {\n                      debouncedSetPageTitle(tabIndex, titleText);\n                    }\n                    !disabled && handleMenuEditClick(e);\n                  }}\n                />\n                <MenuItem\n                  className='n-text-palette-danger-text'\n                  icon={<TrashIconOutline />}\n                  title='Delete'\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    !disabled && setDeleteModalOpen(true);\n                  }}\n                />\n              </MenuItems>\n            </Menu>\n          </>\n        )}\n      </Tab>\n      <NeoDeletePageModal\n        modalOpen={deleteModalOpen}\n        onRemove={() => removePage(tabIndex)}\n        handleClose={handleDeleteModalClose}\n      />\n    </div>\n  );\n\n  return content;\n};\n\nconst mapDispatchToProps = (dispatch) => ({\n  setPageTitle: (number: any, title: any) => {\n    dispatch(setPageTitle(number, title));\n  },\n  removePage: (index: any) => {\n    dispatch(removePageThunk(index));\n  },\n});\n\nexport default connect(null, mapDispatchToProps)(DashboardHeaderPageTitle);\n"
  },
  {
    "path": "src/dashboard/header/DashboardTitle.tsx",
    "content": "import React, { Suspense, useCallback, useEffect, useState } from 'react';\nimport debounce from 'lodash/debounce';\nimport { connect } from 'react-redux';\nimport { setDashboardTitle } from '../DashboardActions';\nimport { applicationGetConnection, applicationGetStandaloneSettings } from '../../application/ApplicationSelectors';\nimport { getDashboardTitle, getDashboardExtensions, getDashboardSettings } from '../DashboardSelectors';\nimport { getDashboardIsEditable } from '../../settings/SettingsSelectors';\nimport { updateDashboardSetting } from '../../settings/SettingsActions';\nimport { Typography, IconButton, Menu, MenuItems, TextInput } from '@neo4j-ndl/react';\nimport {\n  CheckBadgeIconOutline,\n  CheckIconOutline,\n  EllipsisHorizontalIconOutline,\n  PencilSquareIconOutline,\n} from '@neo4j-ndl/react/icons';\nimport NeoSettingsModal from '../../settings/SettingsModal';\nimport NeoExtensionsModal from '../../extensions/ExtensionsModal';\nimport { EXTENSIONS_DRAWER_BUTTONS } from '../../extensions/ExtensionConfig';\nimport { Tooltip } from '@mui/material';\nimport NeoExportModal from '../../modal/ExportModal';\nimport { setDraft } from '../../application/ApplicationActions';\n\ntype SettingsMenuOpenEvent = React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>;\n\nexport const NeoDashboardTitle = ({\n  dashboardTitle,\n  setDashboardTitle,\n  editable,\n  standaloneSettings,\n  dashboardSettings,\n  extensions,\n  updateDashboardSetting,\n  connection,\n}) => {\n  const [dashboardTitleText, setDashboardTitleText] = React.useState(dashboardTitle);\n  const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);\n  const [editing, setEditing] = React.useState(false);\n  const debouncedDashboardTitleUpdate = useCallback(debounce(setDashboardTitle, 250), []);\n  const [inputWidth, setInputWidth] = React.useState(350);\n  const handleSettingsMenuOpen = (event: SettingsMenuOpenEvent) => {\n    setAnchorEl(event.currentTarget);\n  };\n  const handleSettingsMenuClose = () => {\n    setAnchorEl(null);\n  };\n  const menuOpen = Boolean(anchorEl);\n\n  /**\n   * Function to render dynamically the buttons in the drawer related to all the extension that\n   * are enabled and present a button (EX: node-sidebar)\n   * @returns JSX element containing all the buttons related to their enabled extensions\n   */\n  function renderExtensionsButtons() {\n    const res = (\n      <>\n        {Object.keys(EXTENSIONS_DRAWER_BUTTONS).map((name) => {\n          const Component = extensions[name] ? EXTENSIONS_DRAWER_BUTTONS[name] : '';\n          return Component ? <Component key={`ext-${name}`} database={connection.database} /> : <></>;\n        })}\n      </>\n    );\n    return res;\n  }\n\n  useEffect(() => {\n    document.title = dashboardTitle ? `NeoDash - ${dashboardTitle}` : 'NeoDash - Neo4j Dashboard Builder';\n    // Reset text to the dashboard state when the page gets reorganized.\n    if (dashboardTitle !== dashboardTitleText) {\n      setDashboardTitleText(dashboardTitle);\n    }\n  }, [dashboardTitle]);\n  return (\n    <div className='n-flex n-flex-row n-flex-wrap n-justify-between n-items-center'>\n      {/* TODO : Replace with editable field if dashboard is editable */}\n      {/* only allow edit title if dashboard is not standalone - here we are in Title edit mode*/}\n      {editing && !standaloneSettings.standalone ? (\n        <div className={'n-flex n-flex-row n-flex-wrap n-justify-between n-items-center'}>\n          <form\n            onSubmit={() => {\n              if (editing) {\n                setEditing(false);\n              }\n            }}\n          >\n            <input\n              autoFocus={true}\n              value={dashboardTitleText}\n              style={{\n                height: '1.9rem',\n                fontSize: '1.875rem', // h3\n                fontWeight: 700, // h3\n                padding: 10,\n                width: inputWidth,\n              }}\n              placeholder='Dashboard name...'\n              onBlur={() => {\n                if (editing) {\n                  setEditing(false);\n                }\n              }}\n              onChange={(event) => {\n                if (editable) {\n                  const { target } = event;\n                  target.style.width = '350px';\n                  setInputWidth(target.scrollWidth);\n                  setDashboardTitleText(event.target.value);\n                  debouncedDashboardTitleUpdate(event.target.value);\n                }\n              }}\n            />\n          </form>\n          <Tooltip title={'Stop Editing'} disableInteractive>\n            <IconButton\n              className='logo-btn n-p-1'\n              aria-label={'stop-editing'}\n              size='large'\n              onClick={() => setEditing(false)}\n              clean\n            >\n              <CheckIconOutline className='header-icon' type='outline' />\n            </IconButton>\n          </Tooltip>\n        </div>\n      ) : !standaloneSettings.standalone /* out of edit mode - if Not Standalone we display the edit button */ ? (\n        <div className={'n-flex n-flex-row n-flex-wrap n-justify-between n-items-center'}>\n          <Typography variant='h3'>{dashboardTitle ? dashboardTitle : '(no title)'}</Typography>\n          <Tooltip title={'Edit'} disableInteractive>\n            {editable ? (\n              <IconButton\n                className='logo-btn n-p-1'\n                aria-label={'edit'}\n                size='large'\n                onClick={() => setEditing(true)}\n                clean\n              >\n                <PencilSquareIconOutline className='header-icon' type='outline' />\n              </IconButton>\n            ) : (\n              <></>\n            )}\n          </Tooltip>\n        </div>\n      ) : (\n        /* if we are in Standalone just title is displayed with no edit button */\n        <div className={'n-flex n-flex-row n-flex-wrap n-justify-between n-items-center'}>\n          <Typography variant='h3'>{dashboardTitle}</Typography>\n        </div>\n      )}\n      {/* If the app is not running in standalone mode (i.e. in edit mode) always show dashboard settings. */}\n      {!standaloneSettings.standalone ? (\n        <div className='flex flex-row flex-wrap items-center gap-2'>\n          {editable ? renderExtensionsButtons() : <></>}\n          <NeoSettingsModal dashboardSettings={dashboardSettings} updateDashboardSetting={updateDashboardSetting} />\n          {editable ? <NeoExportModal /> : <></>}\n          {editable ? <NeoExtensionsModal closeMenu={handleSettingsMenuClose} /> : <></>}\n        </div>\n      ) : (\n        <></>\n      )}\n    </div>\n  );\n};\n\nconst mapStateToProps = (state) => ({\n  dashboardTitle: getDashboardTitle(state),\n  editable: getDashboardIsEditable(state),\n  standaloneSettings: applicationGetStandaloneSettings(state),\n  dashboardSettings: getDashboardSettings(state),\n  extensions: getDashboardExtensions(state),\n  connection: applicationGetConnection(state),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  setDashboardTitle: (title: any) => {\n    dispatch(setDashboardTitle(title));\n  },\n  updateDashboardSetting: (setting, value) => {\n    dispatch(setDraft(true));\n    dispatch(updateDashboardSetting(setting, value));\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoDashboardTitle);\n"
  },
  {
    "path": "src/dashboard/placeholder/DashboardPlaceholder.tsx",
    "content": "import React from 'react';\nimport { LoadingSpinner } from '@neo4j-ndl/react';\nimport { BoltIconSolid } from '@neo4j-ndl/react/icons';\nimport { NeoDashboardHeaderLogo } from '../header/DashboardHeaderLogo';\n\nexport const NeoDashboardPlaceholder = () => {\n  return (\n    <>\n      <div className='n-w-screen n-flex n-flex-row n-items-center n-bg-neutral-bg-weak n-border-b n-border-neutral-border-weak'>\n        <div className='n-relative n-bg-neutral-bg-weak n-w-full'>\n          <div className='n-min-w-full'>\n            <div className='n-flex n-justify-between n-h-16 n-items-center n-py-6 md:n-justify-start md:n-space-x-10 n-mx-4'>\n              <NeoDashboardHeaderLogo />\n              <nav className='n-items-center n-justify-center n-flex n-flex-1 n-w-full'>\n                NeoDash <BoltIconSolid className='icon-base' color='gold' />\n              </nav>\n              <div className='sm:n-flex n-items-center n-justify-end md:n-flex-1 lg:n-w-0 n-gap-6'></div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div className='n-w-full n-h-full n-overflow-y-scroll n-flex n-flex-row'>\n        <div className='n-flex-1 n-relative n-z-0  n-scroll-smooth n-w-full'>\n          <div className='n-absolute n-inset-0 page-spacing'>\n            <div className='page-spacing-overflow'>\n              <div className='n-absolute n-w-full n-h-full'>\n                <LoadingSpinner size='large' className='centered' />\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n};\n\nexport default NeoDashboardPlaceholder;\n"
  },
  {
    "path": "src/dashboard/sidebar/DashboardSidebar.tsx",
    "content": "import React, { useContext, useState } from 'react';\nimport { connect } from 'react-redux';\nimport { getDashboardIsEditable, getPageNumber } from '../../settings/SettingsSelectors';\nimport { getDashboardSettings, getDashboardTitle } from '../DashboardSelectors';\nimport { Button, SideNavigation, SideNavigationGroupHeader, SideNavigationList, TextInput } from '@neo4j-ndl/react';\nimport { removeReportThunk } from '../../page/PageThunks';\nimport {\n  PlusIconOutline,\n  MagnifyingGlassIconOutline,\n  CircleStackIconOutline,\n  ArrowPathIconOutline,\n} from '@neo4j-ndl/react/icons';\n\nimport Tooltip from '@mui/material/Tooltip';\nimport { DashboardSidebarListItem } from './DashboardSidebarListItem';\nimport {\n  applicationGetConnection,\n  applicationGetConnectionDatabase,\n  applicationGetStandaloneSettings,\n  applicationIsStandalone,\n  dashboardIsDraft,\n} from '../../application/ApplicationSelectors';\nimport { setDraft } from '../../application/ApplicationActions';\nimport NeoDashboardSidebarLoadModal from './modal/DashboardSidebarLoadModal';\nimport { resetDashboardState } from '../DashboardActions';\nimport NeoDashboardSidebarCreateModal from './modal/DashboardSidebarCreateModal';\nimport NeoDashboardSidebarDatabaseMenu from './menu/DashboardSidebarDatabaseMenu';\nimport NeoDashboardSidebarDashboardMenu from './menu/DashboardSidebarDashboardMenu';\nimport {\n  deleteDashboardFromNeo4jThunk,\n  loadDashboardFromNeo4jThunk,\n  loadDashboardListFromNeo4jThunk,\n  loadDashboardThunk,\n  loadDatabaseListFromNeo4jThunk,\n  saveDashboardToNeo4jThunk,\n} from '../DashboardThunks';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport NeoDashboardSidebarSaveModal from './modal/DashboardSidebarSaveModal';\nimport { getDashboardJson } from '../../modal/ModalSelectors';\nimport NeoDashboardSidebarCreateMenu from './menu/DashboardSidebarCreateMenu';\nimport NeoDashboardSidebarImportModal from './modal/DashboardSidebarImportModal';\nimport { createUUID } from '../../utils/uuid';\nimport NeoDashboardSidebarExportModal from './modal/DashboardSidebarExportModal';\nimport NeoDashboardSidebarDeleteModal from './modal/DashboardSidebarDeleteModal';\nimport NeoDashboardSidebarInfoModal from './modal/DashboardSidebarInfoModal';\nimport NeoDashboardSidebarShareModal from './modal/DashboardSidebarShareModal';\nimport NeoDashboardSidebarAccessModal from './modal/DashboardSidebarAccessModal';\nimport LegacyShareModal from './modal/legacy/LegacyShareModal';\nimport { NEODASH_VERSION } from '../DashboardReducer';\n\n// Which (small) pop-up menu is currently open for the sidebar.\nenum Menu {\n  DASHBOARD = 0,\n  DATABASE = 1,\n  CREATE = 2,\n  NONE = 3,\n}\n\n// Which (large) pop-up modal is currently open for the sidebar.\nenum Modal {\n  CREATE = 0,\n  IMPORT = 1,\n  EXPORT = 2,\n  DELETE = 3,\n  SHARE = 4,\n  SHARE_LEGACY = 5,\n  INFO = 6,\n  LOAD = 7,\n  SAVE = 8,\n  NONE = 9,\n  ACCESS = 10,\n}\n\n// We use \"index = -1\" to represent a non-saved draft dashboard in the sidebar's dashboard list.\nconst UNSAVED_DASHBOARD_INDEX = -1;\n\n/**\n * A component responsible for rendering the sidebar on the left of the screen.\n */\nexport const NeoDashboardSidebar = ({\n  database,\n  connection,\n  title,\n  readonly,\n  draft,\n  setDraft,\n  dashboard,\n  resetLocalDashboard,\n  loadDashboard,\n  loadDatabaseListFromNeo4j,\n  loadDashboardListFromNeo4j,\n  loadDashboardFromNeo4j,\n  saveDashboardToNeo4j,\n  deleteDashboardFromNeo4j,\n  standaloneSettings,\n}) => {\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n  const [expanded, setOnExpanded] = useState(false);\n  const [selectedDashboardIndex, setSelectedDashboardIndex] = React.useState(UNSAVED_DASHBOARD_INDEX);\n  const [dashboardDatabase, setDashboardDatabase] = React.useState(database ? database : 'neo4j');\n  const [databases, setDatabases] = useState([]);\n  const [inspectedIndex, setInspectedIndex] = useState(UNSAVED_DASHBOARD_INDEX);\n  const [searchText, setSearchText] = useState('');\n  const [menuAnchor, setMenuAnchor] = useState<HTMLElement | null>(null);\n  const [menuOpen, setMenuOpen] = useState(Menu.NONE);\n  const [modalOpen, setModalOpen] = useState(Modal.NONE);\n  const [dashboards, setDashboards] = React.useState([]);\n  const [cachedDashboard, setCachedDashboard] = React.useState('');\n\n  const getDashboardListFromNeo4j = () => {\n    // Retrieves list of all dashboards stored in a given database.\n    loadDashboardListFromNeo4j(driver, dashboardDatabase, (list) => {\n      setDashboards(list);\n\n      // Update the UI to reflect the currently selected dashboard.\n      if (dashboard && dashboard.uuid) {\n        const index = list.findIndex((element) => element.uuid == dashboard.uuid);\n        setSelectedDashboardIndex(index);\n        if (index == UNSAVED_DASHBOARD_INDEX) {\n          // If we can't find the currently dashboard in the database, we are drafting a new one.\n          setDraft(true);\n        }\n      }\n    });\n  };\n\n  function createDashboard() {\n    // Creates new dashboard in draft state (not yet saved to Neo4j)\n    resetLocalDashboard();\n    setDraft(true);\n  }\n\n  function deleteDashboard(uuid) {\n    // Creates new dashboard in draft state (not yet saved to Neo4j)\n    deleteDashboardFromNeo4j(driver, dashboardDatabase, uuid, () => {\n      if (uuid == dashboard.uuid) {\n        setSelectedDashboardIndex(UNSAVED_DASHBOARD_INDEX);\n        resetLocalDashboard();\n        loadDashboardListFromNeo4j();\n        setDraft(true);\n      }\n      setTimeout(() => {\n        getDashboardListFromNeo4j();\n      }, 100);\n    });\n  }\n\n  return (\n    <div>\n      <NeoDashboardSidebarSaveModal\n        open={modalOpen == Modal.SAVE}\n        onConfirm={() => {\n          saveDashboardToNeo4j(\n            driver,\n            dashboardDatabase,\n            dashboard,\n            new Date().toISOString(),\n            connection.username,\n            () => {\n              // After saving successfully, refresh the list after a small delay.\n              // The new dashboard will always be on top (the latest), so we select index 0.\n              setDashboards([]);\n              setTimeout(() => {\n                getDashboardListFromNeo4j();\n                setSelectedDashboardIndex(0);\n                setDraft(false);\n              }, 100);\n            }\n          );\n        }}\n        overwrite={selectedDashboardIndex >= 0}\n        handleClose={() => setModalOpen(Modal.NONE)}\n      />\n\n      <NeoDashboardSidebarLoadModal\n        open={modalOpen == Modal.LOAD}\n        onConfirm={() => {\n          if (inspectedIndex == UNSAVED_DASHBOARD_INDEX) {\n            // Someone attempted to load the unsaved draft dashboard... this isn't possible, we create a fresh one.\n            setSelectedDashboardIndex(UNSAVED_DASHBOARD_INDEX);\n            createDashboard();\n          } else {\n            // Load one of the dashboards from the database.\n            setModalOpen(Modal.LOAD);\n            const { uuid } = dashboards[inspectedIndex];\n            loadDashboardFromNeo4j(driver, dashboardDatabase, uuid, (file) => {\n              setDraft(false);\n              loadDashboard(uuid, file);\n              setSelectedDashboardIndex(inspectedIndex);\n            });\n          }\n        }}\n        handleClose={() => setModalOpen(Modal.NONE)}\n      />\n\n      <NeoDashboardSidebarShareModal\n        connection={connection}\n        uuid={dashboards[inspectedIndex] && dashboards[inspectedIndex].uuid}\n        dashboardDatabase={dashboardDatabase}\n        open={modalOpen == Modal.SHARE}\n        onConfirm={() => {\n          setModalOpen(Modal.NONE);\n        }}\n        onLegacyShareClicked={() => setModalOpen(Modal.SHARE_LEGACY)}\n        handleClose={() => setModalOpen(Modal.NONE)}\n      />\n\n      <LegacyShareModal open={modalOpen == Modal.SHARE_LEGACY} handleClose={() => setModalOpen(Modal.NONE)} />\n\n      <NeoDashboardSidebarCreateModal\n        open={modalOpen == Modal.CREATE}\n        onConfirm={() => {\n          setModalOpen(Modal.NONE);\n          createDashboard();\n          setSelectedDashboardIndex(UNSAVED_DASHBOARD_INDEX);\n        }}\n        handleClose={() => setModalOpen(Modal.NONE)}\n      />\n\n      <NeoDashboardSidebarDeleteModal\n        open={modalOpen == Modal.DELETE}\n        title={dashboards[inspectedIndex] && dashboards[inspectedIndex].title}\n        onConfirm={() => {\n          setModalOpen(Modal.NONE);\n          if (dashboards[inspectedIndex]) {\n            deleteDashboard(dashboards[inspectedIndex].uuid);\n          }\n        }}\n        handleClose={() => setModalOpen(Modal.NONE)}\n      />\n\n      <NeoDashboardSidebarImportModal\n        open={modalOpen == Modal.IMPORT}\n        onImport={(text) => {\n          setModalOpen(Modal.NONE);\n          setDraft(true);\n          setSelectedDashboardIndex(UNSAVED_DASHBOARD_INDEX);\n          loadDashboard(createUUID(), text);\n        }}\n        handleClose={() => setModalOpen(Modal.NONE)}\n      />\n\n      <NeoDashboardSidebarInfoModal\n        open={modalOpen == Modal.INFO}\n        dashboard={dashboards[inspectedIndex]}\n        handleClose={() => {\n          setModalOpen(Modal.NONE);\n          setCachedDashboard('');\n        }}\n      />\n\n      <NeoDashboardSidebarExportModal\n        open={modalOpen == Modal.EXPORT}\n        dashboard={cachedDashboard}\n        handleClose={() => {\n          setModalOpen(Modal.NONE);\n          setCachedDashboard('');\n        }}\n      />\n\n      <NeoDashboardSidebarAccessModal\n        open={modalOpen == Modal.ACCESS}\n        database={dashboardDatabase}\n        dashboard={dashboards[inspectedIndex]}\n        handleClose={() => {\n          setModalOpen(Modal.NONE);\n          setCachedDashboard('');\n        }}\n      />\n\n      <SideNavigation\n        position='left'\n        type='overlay'\n        expanded={expanded}\n        onExpandedChange={(open) => {\n          setOnExpanded(open);\n          if (open) {\n            getDashboardListFromNeo4j();\n          }\n          // Wait until the sidebar has fully opened. Then trigger a resize event to align the grid layout.\n          const timeout = setTimeout(() => {\n            window.dispatchEvent(new Event('resize'));\n          }, 300);\n        }}\n      >\n        <SideNavigationList>\n          <NeoDashboardSidebarDatabaseMenu\n            databases={databases}\n            selected={dashboardDatabase}\n            setSelected={(newDatabase) => {\n              setDashboardDatabase(newDatabase);\n              // We changed the active dashboard database, reload the list in the sidebar.\n              loadDashboardListFromNeo4j(driver, newDatabase, (list) => {\n                setDashboards(list);\n                if (!readonly) {\n                  setDraft(true);\n                }\n              });\n            }}\n            open={menuOpen == Menu.DATABASE}\n            anchorEl={menuAnchor}\n            handleClose={() => {\n              setMenuOpen(Menu.NONE);\n              setMenuAnchor(null);\n            }}\n          />\n          <NeoDashboardSidebarDashboardMenu\n            draft={draft && selectedDashboardIndex == inspectedIndex}\n            open={menuOpen == Menu.DASHBOARD}\n            anchorEl={menuAnchor}\n            handleInfoClicked={() => {\n              setMenuOpen(Menu.NONE);\n              const d = dashboards[inspectedIndex];\n              loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (text) => {\n                setCachedDashboard(JSON.parse(text));\n              });\n              setModalOpen(Modal.INFO);\n            }}\n            handleDiscardClicked={() => {\n              setMenuOpen(Menu.NONE);\n              setModalOpen(Modal.LOAD);\n            }}\n            handleSaveClicked={() => {\n              setMenuOpen(Menu.NONE);\n              setModalOpen(Modal.SAVE);\n            }}\n            handleLoadClicked={() => {\n              setMenuOpen(Menu.NONE);\n              if (draft) {\n                setModalOpen(Modal.LOAD);\n              } else {\n                const d = dashboards[inspectedIndex];\n                loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (file) => {\n                  loadDashboard(d.uuid, file);\n                  setSelectedDashboardIndex(inspectedIndex);\n                });\n              }\n            }}\n            handleExportClicked={() => {\n              setMenuOpen(Menu.NONE);\n              const d = dashboards[inspectedIndex];\n              loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (text) => {\n                setCachedDashboard(JSON.parse(text));\n              });\n              setModalOpen(Modal.EXPORT);\n            }}\n            handleShareClicked={() => {\n              setMenuOpen(Menu.NONE);\n              setModalOpen(Modal.SHARE);\n            }}\n            handleAccessClicked={() => {\n              setMenuOpen(Menu.NONE);\n              setModalOpen(Modal.ACCESS);\n            }}\n            handleDeleteClicked={() => {\n              setMenuOpen(Menu.NONE);\n              setModalOpen(Modal.DELETE);\n            }}\n            handleClose={() => {\n              setMenuOpen(Menu.NONE);\n              setMenuAnchor(null);\n            }}\n          />\n\n          <NeoDashboardSidebarCreateMenu\n            open={menuOpen == Menu.CREATE}\n            anchorEl={menuAnchor}\n            handleNewClicked={() => {\n              setMenuOpen(Menu.NONE);\n              if (draft) {\n                setModalOpen(Modal.CREATE);\n              } else {\n                setSelectedDashboardIndex(UNSAVED_DASHBOARD_INDEX);\n                createDashboard();\n              }\n            }}\n            handleImportClicked={() => {\n              setMenuOpen(Menu.NONE);\n              setModalOpen(Modal.IMPORT);\n            }}\n            handleClose={() => {\n              setMenuOpen(Menu.NONE);\n              setMenuAnchor(null);\n            }}\n          />\n\n          <SideNavigationGroupHeader>\n            <div style={{ display: 'inline-block', width: '100%' }}>\n              <span className='n-text-palette-neutral-text-weak' style={{ lineHeight: '28px' }}>\n                Dashboards\n              </span>\n              <Tooltip title='Refresh' aria-label='refresh' disableInteractive>\n                <Button\n                  aria-label={'refresh'}\n                  fill='text'\n                  size='small'\n                  color='neutral'\n                  style={{\n                    float: 'right',\n                    marginLeft: '3px',\n                    marginRight: '12px',\n                    paddingLeft: 0,\n                    paddingRight: '3px',\n                  }}\n                  onClick={() => {\n                    getDashboardListFromNeo4j();\n                    // When reloading, if the dashboard is not in DRAFT mode, we can directly refresh it.\n                    if (!draft) {\n                      const d = dashboards[selectedDashboardIndex];\n                      loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (file) => {\n                        loadDashboard(d.uuid, file);\n                      });\n                    }\n                  }}\n                >\n                  <ArrowPathIconOutline className='btn-icon-base-r-m' />\n                </Button>\n              </Tooltip>\n              {/* Only let users create dashboards and change database when running in editor mode. */}\n              {!readonly || (readonly && standaloneSettings.standaloneLoadFromOtherDatabases) ? (\n                <>\n                  <Tooltip title='Database' aria-label='database' disableInteractive>\n                    <Button\n                      aria-label={'settings'}\n                      fill='text'\n                      size='small'\n                      color='neutral'\n                      style={{\n                        float: 'right',\n                        marginLeft: '0px',\n                        marginRight: '3px',\n                        paddingLeft: 0,\n                        paddingRight: '3px',\n                      }}\n                      onClick={(event) => {\n                        setMenuOpen(Menu.DATABASE);\n                        // Only when not yet retrieved, and needed, get the list of databases from Neo4j.\n                        if (databases.length == 0) {\n                          loadDatabaseListFromNeo4j(driver, (result) => {\n                            if (\n                              readonly &&\n                              standaloneSettings.standaloneMultiDatabase &&\n                              standaloneSettings.standaloneDatabaseList\n                            ) {\n                              let tmp = standaloneSettings.standaloneDatabaseList.split(',').map((x) => x.trim());\n                              result = result.filter((value) => tmp.includes(value));\n                            }\n                            setDatabases(result);\n                          });\n                        }\n                        setMenuAnchor(event.currentTarget);\n                      }}\n                    >\n                      <CircleStackIconOutline className='btn-icon-base-r' />\n                    </Button>\n                  </Tooltip>\n\n                  {!readonly ? (\n                    <Tooltip title='Create' aria-label='create' disableInteractive>\n                      <Button\n                        aria-label={'new dashboard'}\n                        fill='text'\n                        size='small'\n                        color='neutral'\n                        style={{\n                          float: 'right',\n                          marginLeft: '0px',\n                          marginRight: '5px',\n                          paddingLeft: 0,\n                          paddingRight: '3px',\n                        }}\n                        onClick={(event) => {\n                          setMenuAnchor(event.currentTarget);\n                          setMenuOpen(Menu.CREATE);\n                        }}\n                      >\n                        <PlusIconOutline className='btn-icon-base-r' />\n                      </Button>\n                    </Tooltip>\n                  ) : (\n                    <></>\n                  )}\n                </>\n              ) : (\n                <></>\n              )}\n            </div>\n          </SideNavigationGroupHeader>\n        </SideNavigationList>\n        <SideNavigationList>\n          <SideNavigationGroupHeader style={{ marginBottom: '10px' }}>\n            <TextInput\n              fluid\n              size='small'\n              leftIcon={<MagnifyingGlassIconOutline style={{ height: 16, marginTop: '2px' }} />}\n              className='n-w-full n-mr-2'\n              placeholder='Search...'\n              aria-label='Search'\n              value={searchText}\n              onChange={(e) => setSearchText(e.target.value)}\n            />\n          </SideNavigationGroupHeader>\n          {draft && selectedDashboardIndex == UNSAVED_DASHBOARD_INDEX && !readonly ? (\n            <DashboardSidebarListItem\n              version={NEODASH_VERSION}\n              selected={draft}\n              title={title}\n              saved={false}\n              onSelect={() => {}}\n              onSettingsOpen={(event) => {\n                setInspectedIndex(UNSAVED_DASHBOARD_INDEX);\n                setMenuOpen(Menu.DASHBOARD);\n                setMenuAnchor(event.currentTarget);\n              }}\n            />\n          ) : (\n            <></>\n          )}\n          {dashboards\n            .filter((d) => d.title.toLowerCase().includes(searchText.toLowerCase()))\n            .map((d) => {\n              // index stored in list\n              return (\n                <DashboardSidebarListItem\n                  selected={selectedDashboardIndex == d.index}\n                  title={draft && selectedDashboardIndex == d.index ? title : d.title}\n                  version={d.version}\n                  saved={!(draft && selectedDashboardIndex == d.index)}\n                  readonly={readonly}\n                  onSelect={() => {\n                    if (draft && d.index !== selectedDashboardIndex) {\n                      setInspectedIndex(d.index);\n                      setModalOpen(Modal.LOAD);\n                    } else {\n                      loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (file) => {\n                        loadDashboard(d.uuid, file);\n                        setSelectedDashboardIndex(d.index);\n                      });\n                    }\n                  }}\n                  onSettingsOpen={(event) => {\n                    setInspectedIndex(d.index);\n                    setMenuOpen(Menu.DASHBOARD);\n                    setMenuAnchor(event.currentTarget);\n                  }}\n                />\n              );\n            })}\n        </SideNavigationList>\n      </SideNavigation>\n    </div>\n  );\n};\n\nconst mapStateToProps = (state) => ({\n  readonly: applicationIsStandalone(state),\n  connection: applicationGetConnection(state),\n  pagenumber: getPageNumber(state),\n  title: getDashboardTitle(state),\n  editable: getDashboardIsEditable(state),\n  draft: dashboardIsDraft(state),\n  dashboard: getDashboardJson(state),\n  dashboardSettings: getDashboardSettings(state),\n  database: applicationGetConnectionDatabase(state),\n  standaloneSettings: applicationGetStandaloneSettings(state),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  onRemovePressed: (id) => dispatch(removeReportThunk(id)),\n  resetLocalDashboard: () => dispatch(resetDashboardState()),\n  setDraft: (draft) => dispatch(setDraft(draft)),\n  loadDashboard: (uuid, text) => dispatch(loadDashboardThunk(uuid, text)),\n  loadDatabaseListFromNeo4j: (driver, callback) => dispatch(loadDatabaseListFromNeo4jThunk(driver, callback)),\n  loadDashboardFromNeo4j: (driver, database, uuid, callback) =>\n    dispatch(loadDashboardFromNeo4jThunk(driver, database, uuid, callback)),\n  loadDashboardListFromNeo4j: (driver, database, callback) =>\n    dispatch(loadDashboardListFromNeo4jThunk(driver, database, callback)),\n  saveDashboardToNeo4j: (driver: any, database: string, dashboard: any, date: any, user: any, onSuccess) => {\n    dispatch(saveDashboardToNeo4jThunk(driver, database, dashboard, date, user, onSuccess));\n  },\n  deleteDashboardFromNeo4j: (driver: any, database: string, uuid: string, onSuccess) => {\n    dispatch(deleteDashboardFromNeo4jThunk(driver, database, uuid, onSuccess));\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoDashboardSidebar);\n"
  },
  {
    "path": "src/dashboard/sidebar/DashboardSidebarListItem.tsx",
    "content": "import { Button, IconButton, SideNavigationGroupHeader } from '@neo4j-ndl/react';\nimport React from 'react';\nimport { CloudArrowUpIconOutline, EllipsisVerticalIconOutline } from '@neo4j-ndl/react/icons';\nimport Tooltip from '@mui/material/Tooltip';\nimport { NEODASH_VERSION } from '../DashboardReducer';\n\nexport const DashboardSidebarListItem = ({ title, selected, readonly, saved, version, onSelect, onSettingsOpen }) => {\n  return (\n    <SideNavigationGroupHeader>\n      <div style={{ display: 'contents', width: '100%' }}>\n        <Tooltip\n          title={version !== NEODASH_VERSION ? `Old version: v${version}` : ''}\n          aria-label='old version'\n          disableInteractive\n        >\n          <Button\n            aria-label={'dashboard'}\n            fill={selected == true ? 'outlined' : 'text'}\n            size='medium'\n            color={selected == true ? (saved == true ? 'primary' : 'warning') : 'neutral'}\n            style={{\n              width: '240px',\n              whiteSpace: 'nowrap',\n              overflowX: 'clip',\n              justifyContent: 'left',\n              marginRight: '10px',\n              paddingLeft: '5px',\n              paddingRight: '5px',\n            }}\n            onClick={() => {\n              onSelect();\n            }}\n          >\n            {saved == false ? <b>(Draft)</b> : <></>}\n            {title ? title : '(no title)'}\n          </Button>\n        </Tooltip>\n        {readonly !== true ? (\n          <IconButton\n            aria-label={'new dashboard'}\n            clean\n            size='small'\n            color={'neutral'}\n            style={{\n              justifyContent: 'left',\n              paddingLeft: '0px',\n              marginRight: '10px',\n            }}\n            onClick={(event) => {\n              saved == false ? onSettingsOpen(event) : onSettingsOpen(event);\n            }}\n          >\n            {saved == true ? (\n              <Tooltip title='Settings' aria-label='settings' disableInteractive>\n                <EllipsisVerticalIconOutline\n                  style={{ float: 'right', marginRight: '-6px' }}\n                  className='btn-icon-base-r'\n                />\n              </Tooltip>\n            ) : (\n              <Tooltip title='Settings' aria-label='settings' disableInteractive>\n                <EllipsisVerticalIconOutline\n                  color='rgb(var(--palette-warning-text))'\n                  style={{ float: 'right', marginRight: '-6px' }}\n                  className='btn-icon-base-r'\n                />\n              </Tooltip>\n            )}\n          </IconButton>\n        ) : (\n          <></>\n        )}\n      </div>\n    </SideNavigationGroupHeader>\n  );\n};\n"
  },
  {
    "path": "src/dashboard/sidebar/menu/DashboardSidebarCreateMenu.tsx",
    "content": "import React from 'react';\nimport { Menu, MenuItem, MenuItems } from '@neo4j-ndl/react';\nimport { DocumentTextIconOutline, PlusCircleIconOutline } from '@neo4j-ndl/react/icons';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoDashboardSidebarCreateMenu = ({\n  anchorEl,\n  open,\n  handleNewClicked,\n  handleImportClicked,\n  handleClose,\n}) => {\n  return (\n    <Menu\n      anchorOrigin={{\n        horizontal: 'left',\n        vertical: 'bottom',\n      }}\n      transformOrigin={{\n        horizontal: 'left',\n        vertical: 'top',\n      }}\n      anchorEl={anchorEl}\n      open={open}\n      onClose={handleClose}\n      size='small'\n    >\n      <MenuItems>\n        <MenuItem onClick={handleNewClicked} title='New' />\n        <MenuItem onClick={handleImportClicked} title='Import' />\n      </MenuItems>\n    </Menu>\n  );\n};\n\nexport default NeoDashboardSidebarCreateMenu;\n"
  },
  {
    "path": "src/dashboard/sidebar/menu/DashboardSidebarDashboardMenu.tsx",
    "content": "import React from 'react';\nimport { Menu, MenuItem, MenuItems } from '@neo4j-ndl/react';\nimport {\n  ArchiveBoxIconOutline,\n  ArrowUturnLeftIconOutline,\n  CloudArrowUpIconOutline,\n  DocumentDuplicateIconOutline,\n  DocumentTextIconOutline,\n  InformationCircleIconOutline,\n  ShareIconOutline,\n  FingerPrintIconOutline,\n  TrashIconOutline,\n  XMarkIconOutline,\n} from '@neo4j-ndl/react/icons';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoDashboardSidebarDashboardMenu = ({\n  anchorEl,\n  draft,\n  open,\n  handleInfoClicked,\n  handleSaveClicked,\n  handleDiscardClicked,\n  handleLoadClicked,\n  handleExportClicked,\n  handleShareClicked,\n  handleAccessClicked,\n  handleDeleteClicked,\n  handleClose,\n}) => {\n  return (\n    <Menu\n      anchorOrigin={{\n        horizontal: 'left',\n        vertical: 'bottom',\n      }}\n      transformOrigin={{\n        horizontal: 'left',\n        vertical: 'top',\n      }}\n      anchorEl={anchorEl}\n      open={open}\n      onClose={handleClose}\n      size='small'\n    >\n      {!draft ? (\n        <MenuItems>\n          <MenuItem onClick={handleInfoClicked} icon={<InformationCircleIconOutline />} title='Info' />\n          <MenuItem onClick={handleLoadClicked} icon={<CloudArrowUpIconOutline />} title='Load' />\n          {/* <MenuItem onClick={() => {}} icon={<DocumentDuplicateIconOutline />} title='Clone' /> */}\n          <MenuItem onClick={handleExportClicked} icon={<DocumentTextIconOutline />} title='Export' />\n          <MenuItem onClick={handleAccessClicked} icon={<FingerPrintIconOutline />} title='Access' />\n          <MenuItem onClick={handleShareClicked} icon={<ShareIconOutline />} title='Share' />\n          <MenuItem onClick={handleDeleteClicked} icon={<TrashIconOutline />} title='Delete' />\n        </MenuItems>\n      ) : (\n        <MenuItems>\n          <MenuItem onClick={handleSaveClicked} icon={<CloudArrowUpIconOutline />} title='Save' />\n          <MenuItem onClick={handleDiscardClicked} icon={<ArrowUturnLeftIconOutline />} title='Discard Draft' />\n        </MenuItems>\n      )}\n    </Menu>\n  );\n};\n\nexport default NeoDashboardSidebarDashboardMenu;\n"
  },
  {
    "path": "src/dashboard/sidebar/menu/DashboardSidebarDatabaseMenu.tsx",
    "content": "import React from 'react';\nimport { Button, Dialog, Menu, MenuItem, MenuItems } from '@neo4j-ndl/react';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoDashboardSidebarDatabaseMenu = ({ anchorEl, open, handleClose, databases, selected, setSelected }) => {\n  return (\n    <Menu\n      anchorOrigin={{\n        horizontal: 'left',\n        vertical: 'bottom',\n      }}\n      transformOrigin={{\n        horizontal: 'left',\n        vertical: 'top',\n      }}\n      anchorEl={anchorEl}\n      open={open}\n      onClose={handleClose}\n      size='small'\n    >\n      <MenuItems>\n        {databases.map((d) => {\n          return (\n            <MenuItem\n              onClick={() => {\n                setSelected(d);\n              }}\n              title={d}\n              style={\n                d == selected\n                  ? {\n                      borderWidth: '1px',\n                      borderStyle: 'solid',\n                      color: 'rgb(var(--palette-primary-bg-strong))',\n                      borderColor: 'rgb(var(--palette-primary-bg-strong))',\n                      borderRadius: '8px',\n                    }\n                  : {}\n              }\n            />\n          );\n        })}\n      </MenuItems>\n    </Menu>\n  );\n};\n\nexport default NeoDashboardSidebarDatabaseMenu;\n"
  },
  {
    "path": "src/dashboard/sidebar/modal/DashboardSidebarAccessModal.tsx",
    "content": "import React, { useEffect, useState, useContext } from 'react';\nimport { IconButton, Button, Dialog, TextInput } from '@neo4j-ndl/react';\nimport { Menu, MenuItem, Chip } from '@mui/material';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport { PlusCircleIconOutline } from '@neo4j-ndl/react/icons';\nimport { QueryStatus, runCypherQuery } from '../../../report/ReportQueryRunner';\nimport { createNotificationThunk } from '../../../page/PageThunks';\nimport { useDispatch } from 'react-redux';\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n * @param open - Whether the modal is open or not.\n * @param database - The current Neo4j database.\n * @param dashboard - The current dashboard.\n * @param handleClose - The function to close the modal.\n */\nexport const NeoDashboardSidebarAccessModal = ({ open, database, dashboard, handleClose }) => {\n  const [anchorEl, setAnchorEl] = useState(null);\n  const [selectedLabels, setSelectedLabels] = useState([]);\n  const [allLabels, setAllLabels] = useState([]);\n  const [neo4jLabels, setNeo4jLabels] = useState([]);\n  const [newLabel, setNewLabel] = useState('');\n  const INITIAL_LABEL = '_Neodash_Dashboard';\n  const [feedback, setFeedback] = useState('');\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    if (!open) {\n      return;\n    }\n    runCypherQuery(\n      driver,\n      database,\n      'CALL db.labels()',\n      {},\n      1000,\n      () => {},\n      (records) => setNeo4jLabels(records.map((record) => record.get('label')))\n    );\n\n    const query = `\n    MATCH (d:${INITIAL_LABEL} {uuid: \"${dashboard.uuid}\"})\n    RETURN labels(d) as labels\n    `;\n    runCypherQuery(\n      driver,\n      database,\n      query,\n      {},\n      1000,\n      (error) => {\n        console.error(error);\n      },\n      (records) => {\n        // Set the selectedLabels state to the labels of the dashboard\n        setSelectedLabels(records[0].get('labels'));\n        setAllLabels(records[0].get('labels'));\n      }\n    );\n    setFeedback('');\n    setNewLabel('');\n  }, [open]);\n\n  useEffect(() => {\n    setAllLabels([INITIAL_LABEL]);\n    setSelectedLabels([INITIAL_LABEL]);\n  }, []);\n\n  const handleOpenMenu = (event) => {\n    setAnchorEl(event.currentTarget);\n  };\n\n  const handleCloseMenu = () => {\n    setAnchorEl(null);\n  };\n\n  const handleLabelSelect = (label) => {\n    if (!selectedLabels.includes(label) && label !== INITIAL_LABEL) {\n      setSelectedLabels([...selectedLabels, label]);\n    }\n    handleCloseMenu();\n  };\n\n  const handleDeleteLabel = (label) => {\n    if (label !== INITIAL_LABEL) {\n      const updatedLabels = selectedLabels.filter((selectedLabel) => selectedLabel !== label);\n      setSelectedLabels(updatedLabels);\n    }\n  };\n\n  const handleAddNewLabel = (e) => {\n    if (e.key === 'Enter' && newLabel.trim() !== '') {\n      if (selectedLabels.includes(newLabel)) {\n        setFeedback('Label already exists. Please enter a unique label.');\n        handleCloseMenu();\n      } else {\n        setSelectedLabels([...selectedLabels, newLabel]);\n        handleLabelSelect(newLabel);\n        setNewLabel('');\n        handleCloseMenu();\n        setFeedback('');\n      }\n    }\n  };\n\n  const handleSave = () => {\n    // Finding the difference between what is stored and what has been selected in the UI\n    let toDelete = allLabels.filter((item) => selectedLabels.indexOf(item) < 0);\n\n    const query = `\n    MATCH (d:${INITIAL_LABEL} {uuid: \"${dashboard.uuid}\"})\n    SET d:${selectedLabels.join(':')}\n    ${toDelete.length > 0 ? `REMOVE d:${toDelete.join(':')}` : ''}\n    RETURN 1;\n    `;\n\n    runCypherQuery(\n      driver,\n      database,\n      query,\n      { selectedLabels: selectedLabels },\n      1000,\n      (status) => {\n        if (status == QueryStatus.COMPLETE) {\n          dispatch(\n            createNotificationThunk(\n              '🎉 Success!',\n              'Selected Labels have successfully been added to the dashboard node.'\n            )\n          );\n          handleClose();\n        } else {\n          dispatch(\n            createNotificationThunk(\n              'Unable to save dashboard',\n              `Do you have write access to the '${database}' database?`\n            )\n          );\n        }\n      },\n      () => {}\n    );\n  };\n\n  return (\n    <Dialog size='small' open={open} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>Dasboard Access Control - '{dashboard?.title}'</Dialog.Header>\n      <Dialog.Content>\n        Welcome to the Dashboard Access settings!\n        <br />\n        In this modal, you can select the labels that you want to add to the current dashboard node.\n        <br />\n        For more information, please refer to the{' '}\n        <a\n          href='https://github.com/neo4j-labs/neodash/tree/master/docs/modules/ROOT/pages/user-guide/access-control-management.adoc'\n          target='_blank'\n          rel='noopener noreferrer'\n          style={{ color: 'blue', textDecoration: 'underline' }}\n        >\n          documentation\n        </a>\n        .\n      </Dialog.Content>\n      <div>\n        <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleCloseMenu}>\n          {/* Fetch labels dynamically from Neo4j and map to menu items */}\n          {neo4jLabels\n            .filter((e) => !selectedLabels.includes(e))\n            .map((label) => (\n              <MenuItem key={label} onClick={() => handleLabelSelect(label)}>\n                {label}\n              </MenuItem>\n            ))}\n          <MenuItem>\n            <TextInput\n              value={newLabel}\n              onChange={(e) => setNewLabel(e.target.value)}\n              onKeyDown={(e: KeyboardEvent) => {\n                handleAddNewLabel(e);\n                e.stopPropagation();\n              }}\n              errorText={feedback}\n              placeholder='Create New label'\n              autoComplete='off'\n            />\n          </MenuItem>\n        </Menu>\n        <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', marginTop: '10px' }}>\n          {selectedLabels.map((label) => (\n            <Chip\n              key={label}\n              label={label}\n              variant='outlined'\n              onDelete={label === INITIAL_LABEL ? undefined : () => handleDeleteLabel(label)}\n              style={{ marginRight: '5px', marginBottom: '5px' }}\n            />\n          ))}\n          <IconButton title='Add Label' size='large' clean style={{ marginBottom: '5px' }} onClick={handleOpenMenu}>\n            <PlusCircleIconOutline color='#018BFF' />\n          </IconButton>\n        </div>\n      </div>\n      <Dialog.Actions>\n        <Button onClick={handleClose} style={{ float: 'right' }} fill='outlined' floating>\n          Cancel\n        </Button>\n        <Button onClick={handleSave} color='primary' style={{ float: 'right', marginRight: '10px' }} floating>\n          Save\n        </Button>\n      </Dialog.Actions>\n    </Dialog>\n  );\n};\n\nexport default NeoDashboardSidebarAccessModal;\n"
  },
  {
    "path": "src/dashboard/sidebar/modal/DashboardSidebarCreateModal.tsx",
    "content": "import React from 'react';\nimport { Button, Dialog } from '@neo4j-ndl/react';\nimport { BackspaceIconOutline, ExclamationTriangleIconOutline } from '@neo4j-ndl/react/icons';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoDashboardSidebarCreateModal = ({ open, onConfirm, handleClose }) => {\n  return (\n    <Dialog size='small' open={open == true} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>Discard Draft?</Dialog.Header>\n      <Dialog.Subtitle>\n        Creating a new dashboard will delete your current draft. Save the draft first to ensure your dashboard is\n        stored.\n      </Dialog.Subtitle>\n      <Dialog.Actions>\n        <Button\n          onClick={() => {\n            handleClose();\n          }}\n          style={{ float: 'right' }}\n        >\n          <BackspaceIconOutline className='btn-icon-base-l' />\n          Cancel\n        </Button>\n        <Button\n          onClick={() => {\n            onConfirm();\n            handleClose();\n          }}\n          color='danger'\n          fill='outlined'\n          style={{ float: 'right', marginRight: '5px' }}\n        >\n          Continue\n          <ExclamationTriangleIconOutline className='btn-icon-base-r' />\n        </Button>\n      </Dialog.Actions>\n    </Dialog>\n  );\n};\n\nexport default NeoDashboardSidebarCreateModal;\n"
  },
  {
    "path": "src/dashboard/sidebar/modal/DashboardSidebarDeleteModal.tsx",
    "content": "import React from 'react';\nimport { Button, Dialog } from '@neo4j-ndl/react';\nimport { BackspaceIconOutline, TrashIconSolid } from '@neo4j-ndl/react/icons';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoDashboardSidebarDeleteModal = ({ open, title, onConfirm, handleClose }) => {\n  return (\n    <Dialog size='small' open={open == true} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>Delete Dashboard '{title}'?</Dialog.Header>\n      <Dialog.Subtitle>\n        Are you sure you want to delete this dashboard? <br /> This action cannot be undone.\n      </Dialog.Subtitle>\n      <Dialog.Actions>\n        <Button\n          fill='outlined'\n          onClick={() => {\n            handleClose();\n          }}\n          style={{ float: 'right' }}\n        >\n          <BackspaceIconOutline className='btn-icon-base-l' />\n          Cancel\n        </Button>\n        <Button\n          onClick={() => {\n            onConfirm();\n            handleClose();\n          }}\n          color='danger'\n          fill='filled'\n          style={{ float: 'right', marginRight: '5px' }}\n        >\n          Delete\n          <TrashIconSolid className='btn-icon-base-r' />\n        </Button>\n      </Dialog.Actions>\n    </Dialog>\n  );\n};\n\nexport default NeoDashboardSidebarDeleteModal;\n"
  },
  {
    "path": "src/dashboard/sidebar/modal/DashboardSidebarExportModal.tsx",
    "content": "import React from 'react';\nimport { DocumentArrowDownIconOutline } from '@neo4j-ndl/react/icons';\nimport { Button, Dialog } from '@neo4j-ndl/react';\nimport { valueIsArray, valueIsObject } from '../../../chart/ChartUtils';\nimport { TextareaAutosize } from '@mui/material';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoDashboardSidebarExportModal = ({ open, dashboard, handleClose }) => {\n  /**\n   * Removes the specified set of keys from the nested dictionary.\n   */\n  const filterNestedDict = (value: any, removedKeys: any[]) => {\n    if (value == undefined) {\n      return value;\n    }\n\n    if (valueIsArray(value)) {\n      return value.map((v) => filterNestedDict(v, removedKeys));\n    }\n\n    if (valueIsObject(value)) {\n      const newValue = {};\n      Object.keys(value).forEach((k) => {\n        if (removedKeys.indexOf(k) != -1) {\n          newValue[k] = undefined;\n        } else {\n          newValue[k] = filterNestedDict(value[k], removedKeys);\n        }\n      });\n      return newValue;\n    }\n    return value;\n  };\n\n  const filteredDashboard = filterNestedDict(dashboard, [\n    'fields',\n    'settingsOpen',\n    'advancedSettingsOpen',\n    'collapseTimeout',\n    'apiKey', // Added for query-translator extension\n  ]);\n\n  const dashboardString = JSON.stringify(filteredDashboard, null, 2);\n  const downloadDashboard = () => {\n    const element = document.createElement('a');\n    const file = new Blob([dashboardString], { type: 'text/plain' });\n    element.href = URL.createObjectURL(file);\n    element.download = 'dashboard.json';\n    document.body.appendChild(element); // Required for this to work in FireFox\n    element.click();\n  };\n\n  return (\n    <Dialog size='large' open={open} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>Export Dashboard</Dialog.Header>\n      <Dialog.Content>\n        Export your dashboard as a JSON file, or copy-paste the file from here.\n        <br />\n        <Button onClick={downloadDashboard} fill='outlined' color='neutral' floating>\n          Save to file\n          <DocumentArrowDownIconOutline className='btn-icon-base-r' aria-label={'save arrow'} />\n        </Button>\n        <br />\n        <br />\n        <TextareaAutosize\n          style={{ minHeight: '500px', width: '100%', border: '1px solid lightgray' }}\n          className={'textinput-linenumbers'}\n          value={dashboardString}\n          aria-label=''\n          placeholder='Your dashboard JSON should be displayed here.'\n        />\n      </Dialog.Content>\n    </Dialog>\n  );\n};\n\nexport default NeoDashboardSidebarExportModal;\n"
  },
  {
    "path": "src/dashboard/sidebar/modal/DashboardSidebarImportModal.tsx",
    "content": "import React, { useRef } from 'react';\nimport { PlayIconSolid, DocumentPlusIconOutline } from '@neo4j-ndl/react/icons';\nimport { Button, Checkbox, Dialog, Dropdown } from '@neo4j-ndl/react';\nimport TextareaAutosize from '@mui/material/TextareaAutosize';\n\nexport const NeoDashboardSidebarImportModal = ({ open, onImport, handleClose }) => {\n  const [text, setText] = React.useState('');\n  const loadFromFile = useRef(null);\n\n  const onSelectFileClick = async (e: React.ChangeEvent<HTMLInputElement>) => {\n    e.preventDefault();\n\n    if (e.target.files == null) {\n      return;\n    }\n\n    const file = e.target.files[0];\n    const text = await file.text();\n\n    setText(text);\n  };\n\n  return (\n    <Dialog size='large' open={open} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>Import Dashboard</Dialog.Header>\n      <Dialog.Content>\n        Import your dashboard from a JSON file, or copy-paste the save file here.\n        <br />\n        <b>Importing will discard your current draft, if any.</b>\n        <br /> <br />\n      </Dialog.Content>\n      <TextareaAutosize\n        style={{ minHeight: '200px', width: '100%', border: '1px solid lightgray' }}\n        className={'textinput-linenumbers'}\n        onChange={(e) => setText(e.target.value)}\n        value={text}\n        aria-label=''\n        placeholder='Paste a dashboard JSON file here...'\n      />\n      <Dialog.Actions>\n        <Button\n          onClick={() => {\n            loadFromFile.current.click();\n          }}\n          fill='outlined'\n          color='neutral'\n          style={{ marginLeft: '10px' }}\n          floating\n        >\n          <input value='' type='file' ref={loadFromFile} onChange={onSelectFileClick} hidden />\n          Select From File\n          <DocumentPlusIconOutline className='btn-icon-base-r' />\n        </Button>\n        <Button\n          onClick={() => {\n            onImport(text);\n            setText('');\n            handleClose();\n          }}\n          color={text.length > 0 ? 'success' : 'neutral'}\n          disabled={text.length == 0}\n          style={{ float: 'right', marginRight: '10px' }}\n          floating\n        >\n          Import\n          <PlayIconSolid className='btn-icon-base-r' />\n        </Button>\n      </Dialog.Actions>\n    </Dialog>\n  );\n};\n\nexport default NeoDashboardSidebarImportModal;\n"
  },
  {
    "path": "src/dashboard/sidebar/modal/DashboardSidebarInfoModal.tsx",
    "content": "import React from 'react';\nimport { Dialog } from '@neo4j-ndl/react';\nimport { DataGrid } from '@mui/x-data-grid';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoDashboardSidebarInfoModal = ({ open, dashboard, handleClose }) => {\n  const columns = [\n    { field: 'field', headerName: 'Field', width: 150 },\n    { field: 'value', headerName: 'Value', width: 600 },\n  ];\n\n  const rows = dashboard\n    ? [\n        { id: 0, field: 'ID', value: dashboard.uuid },\n        { id: 1, field: 'Title', value: dashboard.title },\n        { id: 2, field: 'Last Modified', value: dashboard.date },\n        { id: 3, field: 'Author', value: dashboard.author },\n        { id: 4, field: 'Version', value: dashboard.version },\n      ]\n    : [];\n\n  return (\n    <Dialog size='large' open={open} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>About '{dashboard && dashboard.title}'</Dialog.Header>\n      <Dialog.Content>\n        <div style={{ height: '300px' }}>\n          <DataGrid\n            rows={rows}\n            columns={columns}\n            pageSize={5}\n            rowsPerPageOptions={[5]}\n            disableSelectionOnClick\n            headerHeight={0}\n            hideFooter={true}\n            components={{\n              ColumnSortedDescendingIcon: () => <></>,\n              ColumnSortedAscendingIcon: () => <></>,\n            }}\n          />\n        </div>\n      </Dialog.Content>\n    </Dialog>\n  );\n};\n\nexport default NeoDashboardSidebarInfoModal;\n"
  },
  {
    "path": "src/dashboard/sidebar/modal/DashboardSidebarLoadModal.tsx",
    "content": "import React from 'react';\nimport { Button, Dialog } from '@neo4j-ndl/react';\nimport { BackspaceIconOutline, ExclamationTriangleIconOutline, TrashIconOutline } from '@neo4j-ndl/react/icons';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoDashboardSidebarLoadModal = ({ open, onConfirm, handleClose }) => {\n  return (\n    <Dialog size='small' open={open == true} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>Discard Draft?</Dialog.Header>\n      <Dialog.Subtitle>\n        You are discarding your current draft dashboard.\n        <br />\n        <b>Your draft will not be recoverable.</b>\n      </Dialog.Subtitle>\n      <Dialog.Actions>\n        <Button\n          onClick={() => {\n            handleClose();\n          }}\n          fill='outlined'\n          style={{ float: 'right' }}\n        >\n          <BackspaceIconOutline className='btn-icon-base-l' />\n          Keep\n        </Button>\n        <Button\n          onClick={() => {\n            onConfirm();\n            handleClose();\n          }}\n          color='danger'\n          style={{ float: 'right', marginRight: '5px' }}\n        >\n          Discard\n          <TrashIconOutline className='btn-icon-base-r' />\n        </Button>\n      </Dialog.Actions>\n    </Dialog>\n  );\n};\n\nexport default NeoDashboardSidebarLoadModal;\n"
  },
  {
    "path": "src/dashboard/sidebar/modal/DashboardSidebarSaveModal.tsx",
    "content": "import React from 'react';\nimport { DatabaseAddCircleIcon, BackspaceIconOutline } from '@neo4j-ndl/react/icons';\nimport { Button, Dialog } from '@neo4j-ndl/react';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoDashboardSidebarSaveModal = ({ open, onConfirm, handleClose, overwrite }) => {\n  return (\n    <Dialog size='small' open={open} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>Save to Neo4j</Dialog.Header>\n      <Dialog.Content>\n        This will <b>{overwrite ? 'overwrite' : 'save'}</b> your current draft as a node in your Neo4j database.\n        <br />\n        Ensure you have write permissions to the database to use this feature.\n      </Dialog.Content>\n      <Dialog.Actions>\n        <Button onClick={handleClose} style={{ float: 'right' }} fill='outlined' floating>\n          <BackspaceIconOutline className='btn-icon-base-l' aria-label={'save back'} />\n          Cancel\n        </Button>\n        <Button\n          onClick={() => {\n            onConfirm();\n            handleClose();\n          }}\n          color='success'\n          style={{ float: 'right', marginRight: '10px' }}\n          floating\n        >\n          Save\n          <DatabaseAddCircleIcon className='btn-icon-base-r' />\n        </Button>\n      </Dialog.Actions>\n    </Dialog>\n  );\n};\n\nexport default NeoDashboardSidebarSaveModal;\n"
  },
  {
    "path": "src/dashboard/sidebar/modal/DashboardSidebarShareModal.tsx",
    "content": "import React from 'react';\nimport { Checkbox, Dialog, TextLink } from '@neo4j-ndl/react';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoDashboardSidebarShareModal = ({\n  uuid,\n  dashboardDatabase,\n  connection,\n  open,\n  onLegacyShareClicked,\n  handleClose,\n}) => {\n  const shareBaseURL = 'http://neodash.graphapp.io';\n  const shareBaseURLAlternative = 'https://neodash.graphapp.io';\n  const shareLocalURL = window.location.origin.startsWith('file') ? shareBaseURL : window.location.origin;\n  const [selfHosted, setSelfHosted] = React.useState(false);\n  const [standalone, setStandalone] = React.useState(false);\n  const [includeCredentials, setIncludeCredentials] = React.useState(false);\n\n  function getShareURL() {\n    const prefix = selfHosted ? shareLocalURL : shareBaseURL;\n    const id = encodeURIComponent(uuid);\n    const db = encodeURIComponent(dashboardDatabase);\n    const suffix1 = includeCredentials\n      ? `&credentials=${encodeURIComponent(\n          `${connection.protocol}://${connection.username}:${connection.password}@${connection.database}:${connection.url}:${connection.port}`\n        )}`\n      : '';\n    const suffix2 = standalone ? `&standalone=Yes` : '';\n    return `${prefix}/?share&type=database&id=${id}&dashboardDatabase=${db}${suffix1}${suffix2}`;\n  }\n\n  return (\n    <Dialog size='small' open={open == true} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>Share Dashboard</Dialog.Header>\n      <Dialog.Subtitle>\n        This screen lets you create a one-off, direct link for your dashboard. Click{' '}\n        <TextLink onClick={onLegacyShareClicked}>here</TextLink> to use legacy file-sharing instead.\n      </Dialog.Subtitle>\n      <Dialog.Content>\n        {shareLocalURL !== shareBaseURL && shareLocalURL !== shareBaseURLAlternative ? (\n          <Checkbox\n            label='Self-hosted'\n            style={{ fontSize: 'small' }}\n            checked={selfHosted}\n            name='enable'\n            onClick={() => {\n              setSelfHosted(!selfHosted);\n            }}\n          />\n        ) : (\n          <></>\n        )}\n        <Checkbox\n          label='Hide Editor UI'\n          style={{ fontSize: 'small' }}\n          checked={standalone}\n          name='enable'\n          onClick={() => {\n            setStandalone(!standalone);\n          }}\n        />\n\n        <Checkbox\n          label={'Include credentials ⚠️'}\n          style={{ fontSize: 'small' }}\n          checked={includeCredentials}\n          name='enable'\n          onClick={() => {\n            setIncludeCredentials(!includeCredentials);\n          }}\n        />\n\n        <br />\n        <span>Your Temporary Link:</span>\n        <br />\n        <span style={{ display: 'block', width: '100%', overflow: 'hidden' }}>\n          <TextLink href={getShareURL()} externalLink>\n            {' '}\n            {getShareURL()}{' '}\n          </TextLink>\n          <br />\n          {includeCredentials ? <i>Caution: this link embeds your current database credentials.</i> : <></>}\n        </span>\n      </Dialog.Content>\n    </Dialog>\n  );\n};\n\nexport default NeoDashboardSidebarShareModal;\n"
  },
  {
    "path": "src/dashboard/sidebar/modal/legacy/LegacyShareModal.tsx",
    "content": "import React, { useContext } from 'react';\n\nimport { connect } from 'react-redux';\nimport { DataGrid } from '@mui/x-data-grid';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport NeoSetting from '../../../../component/field/Setting';\nimport { applicationGetConnection } from '../../../../application/ApplicationSelectors';\nimport { SELECTION_TYPES } from '../../../../config/CardConfig';\nimport { MenuItem, Button, Dialog, Dropdown, TextLink } from '@neo4j-ndl/react';\nimport {\n  ShareIconOutline,\n  PlayIconSolid,\n  DocumentCheckIconOutline,\n  DatabaseAddCircleIcon,\n} from '@neo4j-ndl/react/icons';\n\nconst shareBaseURL = 'http://neodash.graphapp.io';\nconst shareLocalURL = window.location.origin.startsWith('file') ? shareBaseURL : window.location.origin;\n\nexport const NeoShareModal = ({ open, handleClose, connection }) => {\n  const [loadFromNeo4jModalOpen, setLoadFromNeo4jModalOpen] = React.useState(false);\n  const [loadFromFileModalOpen, setLoadFromFileModalOpen] = React.useState(false);\n  const [rows, setRows] = React.useState([]);\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n\n  // One of [null, database, file]\n  const shareType = 'url';\n  const [shareID, setShareID] = React.useState(null);\n  const [shareName, setShareName] = React.useState(null);\n  const [shareConnectionDetails, setShareConnectionDetails] = React.useState('No');\n  const [shareStandalone, setShareStandalone] = React.useState('No');\n  const [selfHosted, setSelfHosted] = React.useState('No');\n  const [shareLink, setShareLink] = React.useState(null);\n\n  const [dashboardDatabase, setDashboardDatabase] = React.useState('neo4j');\n\n  const columns = [\n    { field: 'uuid', hide: true, headerName: 'ID', width: 150 },\n    { field: 'date', headerName: 'Date', width: 200 },\n    { field: 'title', headerName: 'Title', width: 370 },\n    { field: 'author', headerName: 'Author', width: 160 },\n    {\n      field: 'load',\n      headerName: ' ',\n      renderCell: (c) => {\n        return (\n          <Button\n            onClick={() => {\n              setShareID(c.uuid);\n              setShareName(c.row.title);\n              setShareType('database');\n              setLoadFromNeo4jModalOpen(false);\n            }}\n            style={{ float: 'right' }}\n            fill='outlined'\n            color='neutral'\n            floating\n          >\n            Select\n            <PlayIconSolid className='btn-icon-base-r' />\n          </Button>\n        );\n      },\n      width: 130,\n    },\n  ];\n\n  return (\n    <Dialog key={1} size='large' open={open} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>\n        <ShareIconOutline className='icon-base icon-inline text-r' />\n        Share Dashboard File\n      </Dialog.Header>\n      <Dialog.Content>\n        This window lets you create a temporary share link for your dashboard. Keep in mind that share links are not\n        intended as a way to publish your dashboard for users, see the&nbsp;\n        <TextLink\n          externalLink\n          href='https://github.com/neo4j-labs/neodash/tree/master/docs/modules/ROOT/pages/user-guide/publishing.adoc'\n        >\n          documentation\n        </TextLink>{' '}\n        for more on publishing.\n        <br />\n        <hr />\n        <br />\n        To share a dashboard file directly, make it accessible{' '}\n        <TextLink externalLink target='_blank' href='https://gist.github.com/'>\n          online\n        </TextLink>\n        .<br /> Then, paste the direct link here:\n        <NeoSetting\n          key={'url'}\n          name={'url'}\n          value={shareID}\n          style={{ marginLeft: '0px', width: '100%', marginBottom: '10px' }}\n          type={SELECTION_TYPES.TEXT}\n          helperText={'Make sure the URL starts with http:// or https://.'}\n          label={''}\n          defaultValue='https://gist.githubusercontent.com/username/0a78d80567f23072f06e03005cf53bce/raw/f97cc...'\n          onChange={(e) => {\n            setShareLink(null);\n            setShareID(e);\n          }}\n        />\n        {shareID ? (\n          <>\n            <br />\n            <NeoSetting\n              key={'credentials'}\n              name={'credentials'}\n              value={shareConnectionDetails}\n              type={SELECTION_TYPES.LIST}\n              style={{ marginLeft: '0px', width: '100%', marginBottom: '10px' }}\n              helperText={'Share the dashboard including your Neo4j credentials.'}\n              label={'Include Connection Details'}\n              defaultValue={'No'}\n              choices={['Yes', 'No']}\n              onChange={(e) => {\n                if (e == 'No' && shareStandalone == 'Yes') {\n                  return;\n                }\n                setShareLink(null);\n                setShareConnectionDetails(e);\n              }}\n            />\n            {shareLocalURL != shareBaseURL ? (\n              <NeoSetting\n                key={'standalone'}\n                name={'standalone'}\n                value={shareStandalone}\n                style={{ marginLeft: '0px', width: '100%', marginBottom: '10px' }}\n                type={SELECTION_TYPES.LIST}\n                helperText={'Share the dashboard as a standalone webpage, without the NeoDash editor.'}\n                label={'Standalone Dashboard'}\n                defaultValue={'No'}\n                choices={['Yes', 'No']}\n                onChange={(e) => {\n                  setShareLink(null);\n                  setShareStandalone(e);\n                  if (e == 'Yes') {\n                    setShareConnectionDetails('Yes');\n                  }\n                }}\n              />\n            ) : (\n              <></>\n            )}\n            <NeoSetting\n              key={'selfHosted'}\n              name={'selfHosted'}\n              value={selfHosted}\n              style={{ marginLeft: '0px', width: '100%', marginBottom: '10px' }}\n              type={SELECTION_TYPES.LIST}\n              helperText={'Share the dashboard using self Hosted Neodash, otherwise neodash.graphapp.io will be used'}\n              label={'Self Hosted Dashboard'}\n              defaultValue={'No'}\n              choices={['Yes', 'No']}\n              onChange={(e) => {\n                setShareLink(null);\n                setSelfHosted(e);\n              }}\n            />\n            <Button\n              onClick={() => {\n                setShareLink(\n                  `${\n                    selfHosted == 'Yes' ? shareLocalURL : shareBaseURL\n                  }/?share&type=${shareType}&id=${encodeURIComponent(shareID)}&dashboardDatabase=${encodeURIComponent(\n                    dashboardDatabase\n                  )}${\n                    shareConnectionDetails == 'Yes'\n                      ? `&credentials=${encodeURIComponent(\n                          `${connection.protocol}://${connection.username}:${connection.password}@${connection.database}:${connection.url}:${connection.port}`\n                        )}`\n                      : ''\n                  }${shareStandalone == 'Yes' ? `&standalone=${shareStandalone}` : ''}`\n                );\n              }}\n              fill='outlined'\n              color='neutral'\n              floating\n            >\n              Generate Link\n              <ShareIconOutline className='btn-icon-base-r' />\n            </Button>\n          </>\n        ) : (\n          <></>\n        )}\n        {shareLink ? (\n          <>\n            <br />\n            Use the generated link to view the dashboard:\n            <br />\n            <TextLink externalLink href={shareLink} target='_blank'>\n              {shareLink}\n            </TextLink>\n            <br />\n          </>\n        ) : (\n          <></>\n        )}\n      </Dialog.Content>\n    </Dialog>\n  );\n};\n\nconst mapStateToProps = (state) => ({\n  connection: applicationGetConnection(state),\n});\n\nconst mapDispatchToProps = () => ({});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoShareModal);\n"
  },
  {
    "path": "src/extensions/ExtensionConfig.tsx",
    "content": "import React from 'react';\nimport { QUERY_TRANSLATOR_ACTION_PREFIX } from './text2cypher/state/QueryTranslatorActions';\nimport { queryTranslatorReducer } from './text2cypher/state/QueryTranslatorReducer';\nimport NeoOverrideCardQueryEditor from './text2cypher/component/OverrideCardQueryEditor';\nimport { translateQuery } from './text2cypher/util/Util';\nimport { GPT_LOADING_ICON } from './text2cypher/component/LoadingIcon';\nimport QueryTranslatorButton from './text2cypher/component/QueryTranslatorButton';\nimport RBACManagementLabelButton from './rbac/RBACManagementLabelButton';\n\n// TODO: continue documenting interface\ninterface Extension {\n  name: string;\n  label: string;\n  author: string;\n  image: string;\n  enabled: boolean;\n  description: string;\n  link: string;\n  reducerPrefix?: string;\n  reducerObject?: any;\n  settingsMenuButton?: JSX.Element;\n  cardSettingsComponent?: JSX.Element;\n  settingsModal?: JSX.Element;\n  prepopulateReportFunction?: any; // function\n  customLoadingIcon?: JSX.Element;\n}\n\n// TODO: define extension config interface\nexport const EXTENSIONS: Record<string, Extension> = {\n  'advanced-charts': {\n    name: 'advanced-charts',\n    label: 'Advanced Visualizations',\n    author: 'Neo4j Labs',\n    image: 'advanced-visualizations.png',\n    enabled: true,\n    description:\n      'Advanced visualizations let you take your dashboard to the next level. This extension adds a sankey chart to visualize flows, three charts to plot hierarchical data (Sunburst, Circle Packing, Treemap). A Gauge Chart to show percentages, a Radar chart to show radial data, and an Area map to visualize country-data.',\n    link: 'https://github.com/neo4j-labs/neodash/tree/master/docs/modules/ROOT/pages/user-guide/index.adoc',\n  },\n  'rule-based-styling': {\n    name: 'styling',\n    label: 'Rule-Based Styling',\n    author: 'Neo4j Labs',\n    image: 'rule-based-styling.png',\n    enabled: true,\n    description:\n      \"The rule-based styling extension allows users to dynamically color elements in a visualization based on output values. This can be applied to tables, graphs, bar charts, line charts, and more. To use the extension, click on the 'rule-based styling' icon inside the settings of a report.\",\n    link: 'https://github.com/neo4j-labs/neodash/tree/master/docs/modules/ROOT/pages/user-guide/index.adoc',\n  },\n  'report-actions': {\n    name: 'actions',\n    label: 'Report Actions',\n    author: 'Neo4j Professional Services',\n    image: 'report-actions.png',\n    enabled: true,\n    description:\n      'Report actions let dashboard builders add extra interactivity into dashboards. For example, setting parameter values when a cell in a table or a node in a graph is clicked.',\n    link: 'https://neo4j.com/professional-services/',\n  },\n  'query-translator': {\n    name: 'query-translator',\n    label: 'Text2Cypher: Natural Language Queries',\n    author: 'Neo4j Professional Services',\n    image: 'translator.png',\n    enabled: true,\n    reducerPrefix: QUERY_TRANSLATOR_ACTION_PREFIX,\n    reducerObject: queryTranslatorReducer,\n    cardSettingsComponent: NeoOverrideCardQueryEditor,\n    prepopulateReportFunction: translateQuery,\n    customLoadingIcon: GPT_LOADING_ICON,\n    settingsMenuButton: QueryTranslatorButton,\n    description:\n      'Use natural language to generate Cypher queries in NeoDash. Connect to an LLM through an API, and let NeoDash use your database schema + the report types to generate queries automatically. This extension requires APOC Core installed inside Neo4j.',\n    link: 'https://neo4j.com/professional-services/',\n  },\n  forms: {\n    name: 'forms',\n    label: 'Forms',\n    author: 'Neo4j Professional Services',\n    image: 'form.png',\n    enabled: true,\n    description:\n      'Forms let you craft Cypher queries with multiple inputs, that are fired on demand. Using parameters from the dashboard, or form specific input, you will be able to trigger custom logic with forms.',\n    link: 'https://neo4j.com/professional-services/',\n  },\n  'access-control-management': {\n    name: 'access-control-management',\n    label: 'Access Control Management',\n    author: 'Neo4j Professional Services',\n    image: 'accesscontrol2.jpg',\n    enabled: true,\n    description:\n      'This extension lets you manage access control, letting you assign users to roles, as well as controlling which node labels can be read by a user.',\n    link: 'https://neo4j.com/professional-services/',\n    settingsMenuButton: RBACManagementLabelButton,\n  },\n};\n\n/**\n * At the start of the application, we want to collect programmatically the mapping an extension and its reducer.\n * @returns\n */\nfunction getExtensionReducers() {\n  let reducers = {};\n  Object.values(EXTENSIONS).forEach((extension) => {\n    try {\n      if (extension.reducerPrefix && extension.reducerObject) {\n        let reducer = { name: extension.name, reducer: extension.reducerObject };\n        reducers[extension.reducerPrefix] = reducer;\n      }\n    } catch (e) {\n      console.log(`Something wrong happened while loading the Extension Reducer: ${e}`);\n    }\n  });\n  return reducers;\n}\n\n/**\n * At the start of the application, we want to collect programmatically the buttons that will be in the drawer.\n * @returns\n */\nfunction getExtensionDrawerButtons() {\n  let buttons = {};\n  Object.values(EXTENSIONS).forEach((extension) => {\n    try {\n      if (extension.settingsMenuButton) {\n        buttons[extension.name] = extension.settingsMenuButton;\n      }\n    } catch (e) {\n      console.log(`Something wrong happened while loading the drawer extension : ${e}`);\n    }\n  });\n  return buttons;\n}\n\n/**\n * Gets components to inject inside the card settings content, before the Cypher query box.\n */\nexport function getExtensionCardSettingsComponents() {\n  let components = {};\n  Object.values(EXTENSIONS).forEach((extension) => {\n    try {\n      if (extension.cardSettingsComponent) {\n        components[extension.name] = extension.cardSettingsComponent;\n      }\n    } catch (e) {\n      console.log(`Something wrong happened while loading the extension components. : ${e}`);\n    }\n  });\n  return components;\n}\n\n/**\n * At the start of the application, we want to collect programmatically the extensions that need to be added inside the SettingsModal.\n * @returns\n */\nfunction getExtensionSettingsModal() {\n  let modals = {};\n  Object.values(EXTENSIONS).forEach((extension) => {\n    try {\n      if (extension.settingsModal) {\n        modals[extension.name] = extension.settingsModal;\n      }\n    } catch (e) {\n      console.log(`Something wrong happened while loading the Extensions settings modals  : ${e}`);\n    }\n  });\n  return modals;\n}\n\n/**\n * At the start of the application, we want to collect programmatically the extensions that need to be added inside the SettingsModal.\n * @returns\n */\nfunction getExtensionPrepopulateReportFunction() {\n  let prepopulateFunctions = {};\n  Object.values(EXTENSIONS).forEach((extension) => {\n    try {\n      if (extension.prepopulateReportFunction) {\n        prepopulateFunctions[extension.name] = extension.prepopulateReportFunction;\n      }\n    } catch (e) {\n      console.log(`Something wrong happened while loading the Extensions Prepolulation Report functions  : ${e}`);\n    }\n  });\n  return prepopulateFunctions;\n}\n\nexport const EXTENSIONS_REDUCERS = getExtensionReducers();\nexport const EXTENSIONS_DRAWER_BUTTONS = getExtensionDrawerButtons();\nexport const EXTENSIONS_SETTINGS_MODALS = getExtensionSettingsModal();\nexport const EXTENSIONS_CARD_SETTINGS_COMPONENT = getExtensionCardSettingsComponents();\nexport const EXTENSION_PREPOPULATE_REPORT_FUNCTION = getExtensionPrepopulateReportFunction();\n"
  },
  {
    "path": "src/extensions/ExtensionUtils.ts",
    "content": "import { EXAMPLE_REPORTS } from '../config/ExampleConfig';\nimport { REPORT_TYPES } from '../config/ReportConfig';\n\nimport { ADVANCED_REPORT_TYPES } from './advancedcharts/AdvancedChartsReportConfig';\nimport { EXAMPLE_ADVANCED_REPORTS } from './advancedcharts/AdvancedChartsExampleConfig';\nimport { FORMS } from './forms/FormsReportConfig';\nimport { EXAMPLE_FORMS } from './forms/FormsExampleConfig';\n// Components can call this to check if any extension is enabled. For example, to decide whether to all rule-based styling.\nexport const extensionEnabled = (extensions, name) => {\n  return extensions[name]?.active;\n};\n\n// Tell the application what charts are available, dynmically, based on the selected extensions.\nexport const getReportTypes = (extensions) => {\n  let charts = { ...REPORT_TYPES };\n  if (extensions['advanced-charts']?.active) {\n    charts = { ...charts, ...ADVANCED_REPORT_TYPES };\n  }\n  if (extensions?.forms?.active) {\n    charts = { ...charts, ...FORMS };\n  }\n  return charts;\n};\n\n// Tell the application what examples are available, dynmically, based on the selected extensions.\nexport const getExampleReports = (extensions) => {\n  let examples = [...EXAMPLE_REPORTS];\n  if (extensions['advanced-charts']?.active) {\n    examples = [...examples, ...EXAMPLE_ADVANCED_REPORTS];\n  }\n  if (extensions?.forms?.active) {\n    examples = [...examples, ...EXAMPLE_FORMS];\n  }\n  return examples;\n};\n"
  },
  {
    "path": "src/extensions/ExtensionsModal.tsx",
    "content": "import React from 'react';\nimport { EXTENSIONS } from './ExtensionConfig';\nimport { connect } from 'react-redux';\nimport { createNotificationThunk } from '../page/PageThunks';\nimport { getDashboardExtensions } from '../dashboard/DashboardSelectors';\nimport { setExtensionEnabled } from '../dashboard/DashboardActions';\nimport { setExtensionReducerEnabled } from './state/ExtensionActions';\nimport { Dialog, Label, MenuItem, TextLink, Typography, Checkbox, IconButton } from '@neo4j-ndl/react';\nimport { PuzzlePieceIconSolid } from '@neo4j-ndl/react/icons';\nimport { Section, SectionContent } from '../modal/ModalUtils';\nimport Tooltip from '@mui/material/Tooltip/Tooltip';\n\nconst NeoExtensionsModal = ({\n  extensions,\n  setExtensionEnabled,\n  onExtensionUnavailableTriggered, // Action to take when the user tries to enable a disabled extension.\n  setExtensionReducerEnabled,\n  closeMenu,\n}) => {\n  const [open, setOpen] = React.useState(false);\n\n  const handleClickOpen = () => {\n    setOpen(true);\n  };\n\n  const handleClose = () => {\n    setOpen(false);\n    closeMenu();\n  };\n\n  return (\n    <>\n      <Tooltip title='Extensions' aria-label='extensions' disableInteractive>\n        <IconButton className='n-mx-1' aria-label='Extensions' onClick={handleClickOpen}>\n          <PuzzlePieceIconSolid />\n        </IconButton>\n      </Tooltip>\n\n      {open ? (\n        <Dialog size='large' open={open} onClose={handleClose} aria-labelledby='form-dialog-title'>\n          <Dialog.Header id='form-dialog-title'>\n            <PuzzlePieceIconSolid className='icon-base icon-inline text-r' />\n            Extensions\n          </Dialog.Header>\n          <Dialog.Content>\n            <div className='n-flex n-flex-col n-gap-token-4 n-divide-y n-divide-neutral-border-strong'>\n              <Section>\n                <SectionContent>\n                  <TextLink\n                    externalLink\n                    target='_blank'\n                    href='https://github.com/neo4j-labs/neodash/tree/master/docs/modules/ROOT/pages/user-guide/extensions.adoc'\n                  >\n                    Extensions\n                  </TextLink>\n                  &nbsp;are a way of extending the core functionality of NeoDash with custom logic. This can be a new\n                  visualization, extra styling options for an existing visualization, or even a completely new logic for\n                  the dashboarding engine.\n                </SectionContent>\n              </Section>\n\n              {Object.values(EXTENSIONS).map((e, key) => {\n                return (\n                  <Section key={key}>\n                    <SectionContent>\n                      <div style={{ opacity: e.enabled ? 1.0 : 0.6 }}>\n                        <table>\n                          <tbody>\n                            <tr>\n                              <td>\n                                <div className='n-flex n-flex-row n-gap-token-4 n-items-center'>\n                                  <Typography variant='h5'>{e.label}</Typography>\n                                  {e.enabled && e.author == 'Neo4j Professional Services' && (\n                                    <Label color='info' fill='outlined'>\n                                      Expert\n                                    </Label>\n                                  )}\n                                </div>\n                              </td>\n                              <td style={{ width: 50 }}></td>\n                              <td style={{ float: 'right' }}>\n                                <Tooltip title='Enable the extension' aria-label='' disableInteractive>\n                                  <Checkbox\n                                    id={`checkbox-${e.name}`}\n                                    label='Active'\n                                    disabled={!e.enabled}\n                                    style={{ fontSize: 'small' }}\n                                    checked={extensions[e.name]}\n                                    name='enable'\n                                    onClick={() => {\n                                      let active = extensions[e.name] == undefined ? true : undefined;\n                                      if (e.enabled) {\n                                        setExtensionEnabled(e.name, active);\n\n                                        // Subscribing the reducer binded to the newly enabled extension\n                                        // to the extensionReducer\n                                        if (e.reducerPrefix) {\n                                          setExtensionReducerEnabled(e.reducerPrefix, active);\n                                        }\n                                      } else {\n                                        onExtensionUnavailableTriggered(e.label);\n                                        // If an extension presents a reducer, we need to unbind it from the extension reducer\n                                        if (e.reducerPrefix) {\n                                          setExtensionReducerEnabled(e.reducerPrefix, active);\n                                        }\n                                      }\n                                    }}\n                                  />\n                                </Tooltip>\n                              </td>\n                            </tr>\n                            <tr>\n                              <td valign='top'>\n                                <br />\n                                <p>{e.description}</p>\n                                <br />\n                                <p>\n                                  Author:{' '}\n                                  <TextLink externalLink href={e.link}>\n                                    {e.author}\n                                  </TextLink>\n                                </p>\n                              </td>\n                              <td></td>\n                              <td style={{ width: 300 }}>\n                                <br />\n                                <img src={e.image} style={{ border: '1px solid grey', width: '100%' }}></img>\n                              </td>\n                            </tr>\n                          </tbody>\n                        </table>\n                      </div>\n                    </SectionContent>\n                  </Section>\n                );\n              })}\n            </div>\n          </Dialog.Content>\n        </Dialog>\n      ) : (\n        <></>\n      )}\n    </>\n  );\n};\n\nconst mapStateToProps = (state) => ({\n  extensions: getDashboardExtensions(state),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  setExtensionEnabled: (name, enabled) => dispatch(setExtensionEnabled(name, enabled)),\n  setExtensionReducerEnabled: (name, enabled) => dispatch(setExtensionReducerEnabled(name, enabled)),\n  onExtensionUnavailableTriggered: (name) =>\n    dispatch(\n      createNotificationThunk(\n        `Extension '${name}' Unavailable`,\n        // eslint-disable-next-line no-multi-str\n        'This extension is not available in this version of NeoDash.\\n  \\\n     To learn more about expert extensions, check out the project documentation.'\n      )\n    ),\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoExtensionsModal);\n"
  },
  {
    "path": "src/extensions/actions/ActionsRuleCreationModal.tsx",
    "content": "import React, { useEffect } from 'react';\nimport {\n  AdjustmentsHorizontalIconOutline,\n  XMarkIconOutline,\n  PlusIconOutline,\n  PlayIconSolid,\n  SparklesIconOutline,\n} from '@neo4j-ndl/react/icons';\nimport { getPageNumbersAndNamesList } from '../advancedcharts/Utils';\nimport { IconButton, Button, Dialog, Dropdown, TextInput } from '@neo4j-ndl/react';\nimport { Autocomplete, TextField } from '@mui/material';\n\n// The set of conditional checks that are included in the rule specification.\nconst RULE_CONDITIONS = {\n  table: [\n    {\n      value: 'Click',\n      label: 'Cell Click',\n      default: true,\n    },\n    {\n      value: 'doubleClick',\n      label: 'Cell Double Click',\n    },\n    {\n      value: 'rowCheck',\n      label: 'Row Checked',\n      disableFieldSelection: true,\n      multiple: true,\n    },\n  ],\n  bar: [\n    {\n      value: 'Click',\n      label: 'Click',\n      default: true,\n    },\n  ],\n  map: [\n    {\n      value: 'Click',\n      label: 'Click on Tooltip',\n      default: true,\n    },\n  ],\n  graph: [\n    {\n      value: 'onNodeClick',\n      label: 'Node Click',\n      default: true,\n    },\n    {\n      value: 'onLinkClick',\n      label: 'Link Click',\n    },\n  ],\n  graph3d: [\n    {\n      value: 'onNodeClick',\n      label: 'Node Click',\n      default: true,\n    },\n    {\n      value: 'onLinkClick',\n      label: 'Link Click',\n    },\n  ],\n  gantt: [\n    {\n      value: 'onTaskClick',\n      label: 'Task Click',\n      default: true,\n    },\n  ],\n};\n\n// For each report type, the customizations that can be specified using rules.\nexport const RULE_BASED_REPORT_ACTIONS_CUSTOMIZATIONS = {\n  table: [\n    {\n      value: 'set variable',\n      label: 'Parameter',\n    },\n    {\n      value: 'set page',\n      label: 'Page',\n    },\n  ],\n  bar: [\n    {\n      value: 'set variable',\n      label: 'Parameter',\n    },\n    {\n      value: 'set page',\n      label: 'Page',\n    },\n  ],\n  map: [\n    {\n      value: 'set variable',\n      label: 'Parameter',\n    },\n    {\n      value: 'set page',\n      label: 'Page',\n    },\n  ],\n  graph: [\n    {\n      value: 'set variable',\n      label: 'Parameter',\n    },\n    {\n      value: 'set page',\n      label: 'Page',\n    },\n  ],\n  graph3d: [\n    {\n      value: 'set variable',\n      label: 'Parameter',\n    },\n    {\n      value: 'set page',\n      label: 'Page',\n    },\n  ],\n  gantt: [\n    {\n      value: 'set variable',\n      label: 'Parameter',\n    },\n    {\n      value: 'set page',\n      label: 'Page',\n    },\n  ],\n};\n\n// Get the default rule structure to append when a rule gets added to the list.\nconst getDefaultRule = (type) => {\n  let rule = RULE_CONDITIONS[type].filter((e) => e.default !== undefined && e.default);\n  rule = rule.length > 0 ? rule[0] : RULE_CONDITIONS[type][0];\n  let customization = RULE_BASED_REPORT_ACTIONS_CUSTOMIZATIONS[type].filter(\n    (e) => e.default !== undefined && e.default\n  );\n  customization = customization.length > 0 ? customization[0] : RULE_BASED_REPORT_ACTIONS_CUSTOMIZATIONS[type][0];\n  return {\n    condition: rule.value,\n    field: '',\n    value: '',\n    customization: customization.value,\n    customizationValue: '',\n  };\n};\n\n/**\n * The pop-up window used to build and specify custom styling rules for reports.\n */\nexport const NeoCustomReportActionsModal = ({\n  customReportActionsModalOpen,\n  settingName,\n  settingValue,\n  type,\n  fields,\n  setCustomReportActionsModalOpen,\n  onReportSettingUpdate,\n}) => {\n  // The rule set defined in this modal is updated whenever the setting value is externally changed.\n  const [rules, setRules] = React.useState([]);\n  useEffect(() => {\n    if (settingValue) {\n      setRules(settingValue);\n    }\n  }, [settingValue]);\n\n  const pageNames = getPageNumbersAndNamesList();\n  const handleClose = () => {\n    // If no rules are specified, clear the special report setting that holds the customization rules.\n    if (rules.length == 0) {\n      onReportSettingUpdate(settingName, undefined);\n    } else {\n      onReportSettingUpdate(settingName, rules);\n    }\n    setCustomReportActionsModalOpen(false);\n  };\n\n  // Update a single field in one of the rules in the rule array.\n  const updateRuleField = (ruleIndex, ruleField, ruleFieldValue) => {\n    let newRules = [...rules]; // Deep copy\n    newRules[ruleIndex][ruleField] = ruleFieldValue;\n    setRules(newRules);\n  };\n\n  /**\n   * Create the list of suggestions used in the autocomplete box of the rule specification window.\n   * This will be dynamic based on the type of report we are customizing.\n   */\n  const createFieldVariableSuggestions = (c, labels, labelRel) => {\n    if (!fields) {\n      return [];\n    }\n    if (type == 'graph' || type == 'map' || type == 'gantt' || type == 'graph3d') {\n      return fields\n        .map((node, index) => {\n          if (!Array.isArray(node)) {\n            return undefined;\n          }\n          return fields[index].map((property, propertyIndex) => {\n            if (!['Click', 'onNodeClick', 'onTaskClick'].includes(c)) {\n              return undefined;\n            }\n\n            if (labels) {\n              if (propertyIndex > 0) {\n                return undefined;\n              }\n              return fields[index][0];\n            }\n\n            if (propertyIndex == 0) {\n              return undefined;\n            }\n\n            return `${fields[index][0]}.${property}`;\n          });\n        })\n        .flat()\n        .filter((e) => e !== undefined)\n        .filter((e) => labelRel == null || e.startsWith(labelRel));\n    }\n    if (type == 'bar' || type == 'line' || type == 'pie' || type == 'table' || type == 'value') {\n      return fields;\n    }\n    return [];\n  };\n\n  const createFieldVariableSuggestionsFromRule = (rule, skipRuleFieldCheck) => {\n    let suggestions: string[];\n    if (skipRuleFieldCheck) {\n      suggestions = createFieldVariableSuggestions(rule.condition, true, null).filter((e) =>\n        e.toLowerCase().startsWith(rule.field.toLowerCase())\n      );\n    } else if (rule.customization == 'set page' && pageNames) {\n      suggestions = pageNames;\n    } else {\n      suggestions = createFieldVariableSuggestions(rule.condition, false, rule.field).filter((e) =>\n        e.toLowerCase().startsWith(rule.value.toLowerCase())\n      );\n    }\n    // When we are accessing node properties (not page names), parse the node label + property pair to only show properties.\n    // Fields for graph and map reports are structured differently than regular reports (table, bar, etc.), so we access suggestions differently.\n    if (rule.customization !== 'set page' && (type == 'graph' || type == 'map' || type == 'graph3d')) {\n      suggestions = suggestions.map((e) => e.split('.')[1] || e);\n    }\n    return suggestions;\n  };\n\n  const handleOnInputchange = (customization, index, value) => {\n    updateRuleField(index, 'value', value);\n    if (type == 'bar' && customization !== 'set page') {\n      // For bar charts, duplicate the value to rule.field\n      updateRuleField(index, 'field', value);\n    }\n  };\n\n  const handleOnChange = (customization, index, newValue) => {\n    updateRuleField(index, 'value', newValue);\n  };\n\n  const actionHelperClass = 'n-w-2/3 n-inline-flex';\n  const spanClass = 'n-align-middle ';\n  const textInputClass = 'font-bold n-ml-2 n-mt-[1px] n-float-right n-w-full';\n\n  // Sets parameter value\n  const getActionHelper = (rule, index, customization) => {\n    if (customization == 'set variable') {\n      return (\n        <div className={actionHelperClass}>\n          <div style={{ marginLeft: 10, display: 'inline' }} className={spanClass}>\n            <span\n              style={{\n                height: '2.25rem',\n                paddingTop: '0.5rem',\n                paddingBottom: '0.5rem',\n                fontSize: 'var(--font-size-body-medium)',\n                fontWeight: '700',\n                letterSpacing: '0.016rem',\n                lineHeight: '37px',\n              }}\n            >\n              $neodash_\n            </span>\n          </div>\n          <TextInput\n            className={textInputClass}\n            aria-label='Choose variable'\n            fluid\n            style={{ fontWeight: 700 }}\n            placeholder=''\n            value={rule.customizationValue}\n            onChange={(e) => updateRuleField(index, 'customizationValue', e.target.value)}\n          ></TextInput>\n        </div>\n      );\n    } else if (customization == 'set page') {\n      return (\n        <>\n          <div style={{ marginLeft: 15, display: 'inline-block' }}>\n            <span\n              style={{\n                height: '2.25rem',\n                paddingTop: '0.5rem',\n                paddingBottom: '0.5rem',\n                fontSize: 'var(--font-size-body-medium)',\n                fontWeight: '700',\n                letterSpacing: '0.016rem',\n                lineHeight: '36px',\n              }}\n            >\n              index/name\n            </span>\n          </div>\n        </>\n      );\n    }\n    return undefined;\n  };\n\n  // Naming convention: td2 = table data 2. Conditional styling was implemented for each table data depending on whether the chart type == bar or not.\n  // Styling was then extracted into functions outside of the html components, hence the naming.\n  const td2Styling = (type) => ({ width: type === 'bar' ? '15%' : '30%' });\n  const td2DropdownClassname = (type) => `n-align-middle n-pr-1 ${type === 'bar' ? 'n-w-full' : 'n-w-2/5'}`;\n  const td2Autocomplete = (type, index, rule) =>\n    (type !== 'bar' && rule.condition !== 'rowCheck' ? (\n      <Autocomplete\n        className='n-align-middle n-inline-block n-w-/5'\n        disableClearable={true}\n        id='autocomplete-label-type'\n        size='small'\n        noOptionsText='*Specify an exact field name'\n        options={createFieldVariableSuggestionsFromRule(rule, true)}\n        value={rule.field ? rule.field : ''}\n        inputValue={rule.field ? rule.field : ''}\n        popupIcon={<></>}\n        style={{\n          minWidth: 125,\n        }}\n        onInputChange={(event, value) => {\n          updateRuleField(index, 'field', value);\n        }}\n        onChange={(event, newValue) => {\n          updateRuleField(index, 'field', newValue);\n        }}\n        renderInput={(params) => (\n          <TextField\n            {...params}\n            placeholder='Field name...'\n            style={{ padding: 0 }}\n            InputLabelProps={{ shrink: true }}\n          />\n        )}\n      />\n    ) : (\n      <></>\n    ));\n  const td4Styling = (type) => ({ width: type === 'bar' ? '45%' : '40%' });\n  const td4DropdownClassname = 'n-align-middle, n-w-1/3';\n  const td6Styling = (type) => ({ width: type === 'bar' ? '30%' : '20%' });\n\n  return (\n    <div>\n      {customReportActionsModalOpen ? (\n        <Dialog\n          className='dialog-xl'\n          open={customReportActionsModalOpen == true}\n          onClose={handleClose}\n          style={{ overflow: 'inherit', overflowY: 'inherit' }}\n          aria-labelledby='form-dialog-title'\n        >\n          <Dialog.Header id='form-dialog-title'>\n            <SparklesIconOutline className='icon-base icon-inline text-r' aria-label={'Adjust'} />\n            Report Actions\n          </Dialog.Header>\n          <Dialog.Content style={{ overflow: 'inherit' }}>\n            <p>You can define actions for the report here. </p>\n            <p>\n              Report actions enable you to create conditional logic in the dashboard, for example, setting a parameter\n              dynamically based on a 'node click' or 'table click'.\n            </p>\n            <p>For more on report actions, see the documentation.</p>\n            <div>\n              <hr></hr>\n\n              <table style={{ width: '100%' }}>\n                {rules.map((rule, index) => {\n                  const ruleType = RULE_BASED_REPORT_ACTIONS_CUSTOMIZATIONS[type].find(\n                    (el) => el.value === rule.customization\n                  );\n                  const ruleTrigger = RULE_CONDITIONS[type].find((el) => el.value === rule.condition);\n\n                  return (\n                    <>\n                      <tr>\n                        <td width='5%' className='n-pr-1'>\n                          <span className='n-pr-1'>{index + 1}.</span>\n                          <span className='n-font-bold'>&nbsp;ON</span>\n                        </td>\n\n                        {/* <--      td2 (table data 2)      -->*/}\n\n                        <td style={td2Styling(type)}>\n                          <div style={{ border: '2px dashed grey' }} className='n-p-1'>\n                            <Dropdown\n                              type='select'\n                              className={td2DropdownClassname(type)}\n                              style={{\n                                minWidth: '140px',\n                                width: ruleTrigger?.disableFieldSelection === true ? '100%' : '140px',\n                                display: 'inline-block',\n                              }}\n                              selectProps={{\n                                onChange: (newValue) => updateRuleField(index, 'condition', newValue.value),\n                                options:\n                                  RULE_CONDITIONS[type] &&\n                                  RULE_CONDITIONS[type].map((option) => ({\n                                    label: option.label,\n                                    value: option.value,\n                                  })),\n                                value: { label: ruleTrigger ? ruleTrigger.label : '', value: rule.condition },\n                              }}\n                            ></Dropdown>\n                            {td2Autocomplete(type, index, rule)}\n                          </div>\n                        </td>\n\n                        {/* <--      td3 (table data 3)      -->*/}\n\n                        <td style={{ width: '6%' }} className='n-text-center'>\n                          <span style={{ fontWeight: 'bold', color: 'black', marginLeft: 5, marginRight: 5 }}>\n                            {!ruleTrigger?.multiple ? 'SET' : 'APPEND'}\n                          </span>\n                        </td>\n\n                        {/* <--      td4 (table data 4)      -->*/}\n\n                        <td style={td4Styling(type)}>\n                          <div style={{ border: '2px dashed grey' }} className='n-p-1'>\n                            <Dropdown\n                              type='select'\n                              className={td4DropdownClassname}\n                              style={{ minWidth: 130, display: 'inline-block' }}\n                              fluid\n                              selectProps={{\n                                onChange: (newValue) => updateRuleField(index, 'customization', newValue.value),\n                                options:\n                                  RULE_BASED_REPORT_ACTIONS_CUSTOMIZATIONS[type] &&\n                                  RULE_BASED_REPORT_ACTIONS_CUSTOMIZATIONS[type].map((option) => ({\n                                    label: option.label,\n                                    value: option.value,\n                                  })),\n                                value: { label: ruleType ? ruleType.label : '', value: rule.customization },\n                              }}\n                            ></Dropdown>\n                            {getActionHelper(rule, index, rules[index].customization)}\n                          </div>\n                        </td>\n\n                        {/* <--      td5 (table data 5)       -->*/}\n\n                        <td width='5%' className='n-text-center'>\n                          <span style={{ fontWeight: 'bold', color: 'black', marginLeft: 5, marginRight: 5 }}>\n                            {!ruleTrigger?.multiple ? 'TO' : 'WITH'}\n                          </span>\n                        </td>\n\n                        {/* <--      td6 (table data 6)       -->*/}\n\n                        <td style={td6Styling(type)}>\n                          <div style={{ border: '2px dashed grey' }} className='n-p-1'>\n                            <Autocomplete\n                              disableClearable={true}\n                              size='small'\n                              className='n-align-middle n-inline-block n-w-full'\n                              id='autocomplete-label-type'\n                              noOptionsText='*Specify an exact field name'\n                              options={createFieldVariableSuggestionsFromRule(rule, false)}\n                              value={rule.value || ''}\n                              inputValue={rule.value || ''}\n                              popupIcon={<></>}\n                              style={{ minWidth: 250 }}\n                              onInputChange={(e, value) => handleOnInputchange(rule.customization, index, value)}\n                              onChange={(e, newValue) => handleOnChange(rule.customization, index, newValue)}\n                              renderInput={(params) => (\n                                <TextField {...params} placeholder='Value name...' InputLabelProps={{ shrink: true }} />\n                              )}\n                            />\n                          </div>\n                        </td>\n\n                        <td width='5%'>\n                          <IconButton\n                            aria-label='remove rule'\n                            size='medium'\n                            style={{ marginLeft: 10 }}\n                            floating\n                            onClick={() => {\n                              setRules([...rules.slice(0, index), ...rules.slice(index + 1)]);\n                            }}\n                          >\n                            <XMarkIconOutline />\n                          </IconButton>\n                        </td>\n                      </tr>\n                    </>\n                  );\n                })}\n\n                <tr>\n                  <td colSpan={7}>\n                    <div className='n-text-center n-mt-1'>\n                      <IconButton\n                        aria-label='add'\n                        size='medium'\n                        floating\n                        onClick={() => {\n                          const newRule = getDefaultRule(type);\n                          setRules(rules.concat(newRule));\n                        }}\n                      >\n                        <PlusIconOutline />\n                      </IconButton>\n                    </div>\n                  </td>\n                </tr>\n              </table>\n            </div>\n          </Dialog.Content>\n          <Dialog.Actions>\n            <Button\n              onClick={() => {\n                handleClose();\n              }}\n              size='large'\n              floating\n            >\n              Save\n              <SparklesIconOutline className='btn-icon-lg-r' />\n            </Button>\n          </Dialog.Actions>\n        </Dialog>\n      ) : (\n        <></>\n      )}\n    </div>\n  );\n};\n\nexport default NeoCustomReportActionsModal;\n"
  },
  {
    "path": "src/extensions/advancedcharts/AdvancedChartsExampleConfig.ts",
    "content": "import NeoChoroplethMapChart from './chart/choropleth/ChoroplethMapChart';\nimport NeoCirclePackingChart from './chart/circlepacking/CirclePackingChart';\nimport NeoGaugeChart from './chart/gauge/GaugeChart';\nimport NeoSankeyChart from './chart/sankey/SankeyChart';\nimport NeoSunburstChart from './chart/sunburst/SunburstChart';\nimport NeoTreeMapChart from './chart/treemap/TreeMapChart';\nimport NeoRadarChart from './chart/radar/RadarChart';\nimport NeoAreaMapChart from './chart/areamap/AreaMapChart';\nimport NeoGanttChart from './chart/gantt/GanttChart';\nimport NeoGraphChart3D from './chart/graph3d/GraphChart3D';\n\nexport const EXAMPLE_ADVANCED_REPORTS = [\n  {\n    title: 'Graph 3D',\n    description:\n      'A 3D graph visualization will draw all returned nodes, relationships and paths... in three dimensions!',\n    exampleQuery: 'MATCH (p:Person)-[r:RATES]->(m:Movie)\\nRETURN p, r, m',\n    syntheticQuery: `\n        WITH [\n            {\n                path: {  start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 11, identity: 10001, properties: {rating: 4.5}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Jim\", movie: \"The Matrix\", rating: 4.5\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}}, relationship: {type: \"RATES\", start: 2, end: 11, identity: 10002, properties: {rating: 3.8}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Mike\", movie: \"The Matrix\", rating: 3.8\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 11, identity: 10003, properties: {rating: 5.0}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Sarah\", movie: \"The Matrix\", rating: 5.0\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 12, identity: 10004, properties: {rating: 3.5}}, end: {labels: [\"Movie\"], identity: 12, properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Jim\", movie: \"The Matrix - Reloaded\", rating: 3.5\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 12, identity: 10005, properties: {rating: 2.7}}, end: {labels: [\"Movie\"], identity: 12,properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Sarah\", movie: \"The Matrix - Reloaded\", rating: 2.7\n            },\n            {\n                path: { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}}, relationship: {type: \"RATES\", start: 4, end: 12, identity: 10006, properties: {rating: 4.1}}, end: {labels: [\"Movie\"], identity: 12,properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Anna\", movie: \"The Matrix - Reloaded\", rating: 4.1\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 13, identity: 10007, properties: {rating: 4.9}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Jim\", movie: \"The Matrix - Revolutions\", rating: 4.9\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}}, relationship: {type: \"RATES\", start: 2, end: 13, identity: 10008, properties: {rating: 4.8}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Mike\", movie: \"The Matrix - Revolutions\", rating: 4.8\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 13, identity: 10009, properties: {rating: 4.0}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Sarah\", movie: \"The Matrix - Revolutions\", rating: 4.0\n            },\n            {\n                path: { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}}, relationship: {type: \"RATES\", start: 4, end: 13, identity: 10010, properties: {rating: 4.0}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 2003}} } ] }, person: \"Anna\", movie: \"The Matrix - Revolutions\", rating: 4.0\n            }\n          ] as data\n          UNWIND data as row\n          RETURN row.path as Path\n        `,\n    settings: { lockable: false, enableExploration: false, enableEditing: false },\n    fields: [],\n    selection: {\n      Person: 'name',\n      Movie: 'title',\n    },\n    type: 'graph3d',\n    chartType: NeoGraphChart3D,\n  },\n  {\n    title: 'Sunburst Chart',\n    description: 'Sunburst charts can be used to visualize hierarchical data, where each leaf has a numeric value.',\n    exampleQuery:\n      '// How are people distributed in the company?\\n' +\n      \"MATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department)\\n\" +\n      'WITH nodes(path) as no\\n' +\n      'WITH no, last(no) as leaf\\n' +\n      'WITH  [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val\\n' +\n      'RETURN result, val',\n    syntheticQuery:\n      'UNWIND [\\n' +\n      '{path: [\"NeoDash\", \"North\"], value: 3},\\n' +\n      '{path: [\"NeoDash\", \"Center\"], value: 5},\\n' +\n      '{path: [\"NeoDash\", \"South\", \"South 1\"], value: 2},\\n' +\n      '{path: [\"NeoDash\", \"South\", \"South 2\", \"South 2.1\"], value: 1},\\n' +\n      '{path: [\"NeoDash\", \"South\", \"South 2\", \"South 2.2\"], value: 3}\\n' +\n      '] as x\\n' +\n      'RETURN x.path as path, x.value as value',\n    settings: {},\n    selection: { index: 'path', value: 'value', key: 'path' },\n    fields: ['path', 'value'],\n    type: 'sunburst',\n    chartType: NeoSunburstChart,\n  },\n  {\n    title: 'Circle Packing Chart',\n    description:\n      'Circle Packing charts can be used to visualize hierarchical data, where each leaf has a numeric value.',\n    exampleQuery:\n      '// How are people distributed in the company?\\n' +\n      \"MATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department)\\n\" +\n      'WITH nodes(path) as no\\n' +\n      'WITH no, last(no) as leaf\\n' +\n      'WITH  [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val\\n' +\n      'RETURN result, val',\n    syntheticQuery:\n      'UNWIND [\\n' +\n      '{path: [\"NeoDash\", \"North\"], value: 3},\\n' +\n      '{path: [\"NeoDash\", \"Center\"], value: 5},\\n' +\n      '{path: [\"NeoDash\", \"South\", \"South 1\"], value: 2},\\n' +\n      '{path: [\"NeoDash\", \"South\", \"South 2\", \"South 2.1\"], value: 1},\\n' +\n      '{path: [\"NeoDash\", \"South\", \"South 2\", \"South 2.2\"], value: 3}\\n' +\n      '] as x\\n' +\n      'RETURN x.path as path, x.value as value',\n    settings: {},\n    selection: { index: 'path', value: 'value', key: 'path' },\n    fields: ['path', 'value'],\n    type: 'circlePacking',\n    chartType: NeoCirclePackingChart,\n  },\n  {\n    title: 'Treemap Chart',\n    description: 'Treemap charts can be used to visualize hierarchical data, where each leaf has a numeric value.',\n    exampleQuery:\n      '// How are people distributed in the company?\\n' +\n      \"MATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department)\\n\" +\n      'WITH nodes(path) as no\\n' +\n      'WITH no, last(no) as leaf\\n' +\n      'WITH  [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val\\n' +\n      'RETURN result, val',\n    syntheticQuery:\n      'UNWIND [\\n' +\n      '{path: [\"NeoDash\", \"North\"], value: 3},\\n' +\n      '{path: [\"NeoDash\", \"Center\"], value: 5},\\n' +\n      '{path: [\"NeoDash\", \"South\", \"South 1\"], value: 2},\\n' +\n      '{path: [\"NeoDash\", \"South\", \"South 2\", \"South 2.1\"], value: 1},\\n' +\n      '{path: [\"NeoDash\", \"South\", \"South 2\", \"South 2.2\"], value: 3}\\n' +\n      '] as x\\n' +\n      'RETURN x.path as path, x.value as value',\n    settings: {},\n    selection: { index: 'path', value: 'value', key: 'path' },\n    fields: ['path', 'value'],\n    type: 'treeMap',\n    chartType: NeoTreeMapChart,\n  },\n\n  {\n    title: 'Sankey',\n    description:\n      'A Sankey visualization will compute a diagram from nodes and links. Beware that cyclic dependencies are not supported.',\n    exampleQuery: 'MATCH (p:Person)-[r:RATES]->(m:Movie)\\nRETURN p, r, m',\n    syntheticQuery: `\n        WITH [\n            {\n                path: {  start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 11, identity: 10001, properties: {rating: 4.5}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Jim\", movie: \"The Matrix\", rating: 4.5\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}}, relationship: {type: \"RATES\", start: 2, end: 11, identity: 10002, properties: {rating: 3.8}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Mike\", movie: \"The Matrix\", rating: 3.8\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}},  end:  {identity: 11},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 11, identity: 10003, properties: {rating: 5.0}}, end: {labels: [\"Movie\"], identity: 11,properties: {title: \"The Matrix\", released: 1999}} } ] }, person: \"Sarah\", movie: \"The Matrix\", rating: 5.0\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 12, identity: 10004, properties: {rating: 3.5}}, end: {labels: [\"Movie\"], identity: 12, properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Jim\", movie: \"The Matrix - Reloaded\", rating: 3.5\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 12, identity: 10005, properties: {rating: 2.7}}, end: {labels: [\"Movie\"], identity: 12,properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Sarah\", movie: \"The Matrix - Reloaded\", rating: 2.7\n            },\n            {\n                path: { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}},  end:  {identity: 12},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}}, relationship: {type: \"RATES\", start: 4, end: 12, identity: 10006, properties: {rating: 4.1}}, end: {labels: [\"Movie\"], identity: 12,properties: {title: \"The Matrix - Reloaded\", released: 2003}} } ] }, person: \"Anna\", movie: \"The Matrix - Reloaded\", rating: 4.1\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 1, properties: {name: \"Jim\"}}, relationship: {type: \"RATES\", start: 1, end: 13, identity: 10007, properties: {rating: 4.9}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Jim\", movie: \"The Matrix - Revolutions\", rating: 4.9\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 2, properties: {name: \"Mike\"}}, relationship: {type: \"RATES\", start: 2, end: 13, identity: 10008, properties: {rating: 4.8}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Mike\", movie: \"The Matrix - Revolutions\", rating: 4.8\n            },\n            {\n                path: {  start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 3, properties: {name: \"Sarah\"}}, relationship: {type: \"RATES\", start: 3, end: 13, identity: 10009, properties: {rating: 4.0}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 1999}} } ] }, person: \"Sarah\", movie: \"The Matrix - Revolutions\", rating: 4.0\n            },\n            {\n                path: { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}},  end:  {identity: 13},  length: 1, segments: [ { start: {labels: [\"Person\"], identity: 4, properties: {name: \"Anna\"}}, relationship: {type: \"RATES\", start: 4, end: 13, identity: 10010, properties: {rating: 4.0}}, end: {labels: [\"Movie\"], identity: 13,properties: {title: \"The Matrix - Revolutions\", released: 2003}} } ] }, person: \"Anna\", movie: \"The Matrix - Revolutions\", rating: 4.0\n            }\n          ] as data\n          UNWIND data as row\n          RETURN row.path as Path\n        `,\n    settings: { labelPosition: 'outside', labelProperty: 'rating', layout: 'vertical' },\n    fields: [],\n    selection: {\n      Person: 'name',\n      Movie: 'title',\n    },\n    type: 'sankey',\n    chartType: NeoSankeyChart,\n  },\n  {\n    title: 'Choropleth Chart',\n    description: 'Choropleth charts can be used to render geographical based information on geoJson polygons.',\n    exampleQuery:\n      '// How are people distributed in the company per country?\\n' +\n      \"MATCH (:Company{name:'NeoDash'})-[:HAS_DEPARTMENT]->(:Department)<-[:IN_DEPARTMENT]-(e:Employee),\\n\" +\n      '(e)-[:LIVES_IN]->(c:Country)\\n' +\n      'WITH c.code as code, count(e) as value\\n' +\n      'RETURN code, value',\n    syntheticQuery:\n      'UNWIND [\\n' +\n      '{id: \"ARG\", value: 23},\\n' +\n      '{id: \"BOL\", value: 2},\\n' +\n      '{id: \"CAN\", value: 100},\\n' +\n      '{id: \"COL\", value: 5},\\n' +\n      '{id: \"FRA\", value: 40},\\n' +\n      '{id: \"USA\", value: 156}\\n' +\n      '] as x \\n' +\n      'RETURN x.id as code, x.value as value',\n    settings: { colors: 'nivo' },\n    selection: { index: 'code', value: 'value', key: 'code' },\n    fields: ['code', 'value'],\n    type: 'choropleth',\n    chartType: NeoChoroplethMapChart,\n  },\n  {\n    title: 'Radar Chart',\n    description:\n      'Radar charts can be used to render multivariate data from an array of nodes into the form of a two dimensional chart of three or more quantitative variables.',\n    exampleQuery:\n      '// What are FIFA22 players stats?\\n' +\n      'MATCH (s:Skill),\\n' +\n      \"(:Player{name:'Messi'})-[h1]->(s),\\n\" +\n      \"(:Player{name:'Mbappe'})-[h2]->(s),\\n\" +\n      \"(:Player{name:'Benzema'})-[h3]->(s),\\n\" +\n      \"(:Player{name:'Ronaldo'})-[h4]->(s),\\n\" +\n      \"(:Player{name:'Lewandowski'})-[h5]->(s)\\n\" +\n      'RETURN s.name as Skill, \\n h1.value as Messi,\\nh2.value as Mbappe,\\n h3.value as Benzema,\\n' +\n      'h4.value as `Ronaldo`,\\n h5.value as Lewandowski',\n    syntheticQuery:\n      'UNWIND [' +\n      '{Skill: \"PACE\", Lewandowski: 78, Messi: 83, Ronaldo: 85, Benzema: 80, Mbappé: 97},' +\n      '   {Skill: \"SHOOTING\", Lewandowski: 92, Messi: 90, Ronaldo: 93, Benzema: 88, Mbappé: 88},' +\n      '   {Skill: \"PASSING\", Lewandowski: 79, Messi: 91, Ronaldo: 80, Benzema: 83, Mbappé: 80},' +\n      '   {Skill: \"DRIBBLING\", Lewandowski: 86, Messi: 95, Ronaldo: 86, Benzema: 87, Mbappé: 92},' +\n      '   {Skill: \"DEFENDING\", Lewandowski: 44, Messi: 34, Ronaldo: 34, Benzema: 39, Mbappé: 36},' +\n      '   {Skill: \"PHYSICAL\", Lewandowski: 82, Messi: 64, Ronaldo: 75, Benzema: 78, Mbappé: 77}' +\n      '   ] as data ' +\n      '   RETURN data.Skill as Skill, data.Lewandowski as Lewandowski, data.Messi as Messi, data.Ronaldo as Ronaldo, data.Benzema as Benzema ,data.Mbappé as Mbappé',\n    settings: { colors: 'set3' },\n    selection: {\n      index: 'Skill',\n      values: ['Lewandowski', 'Benzema', 'Mbappé', 'Messi', 'Ronaldo'],\n    },\n    fields: ['Skill', 'Lewandowski', 'Messi', 'Ronaldo', 'Benzema', 'Mbappé'],\n    type: 'radar',\n    chartType: NeoRadarChart,\n  },\n  {\n    title: 'Area Map',\n    description:\n      \"The Area Map charts can be used to render geographical based information on geoJson polygons. It's possible to click a polygon to visualize its regions and their related data.\",\n    exampleQuery: `\nMATCH (:Company{name:'NeoDash'})-[:HAS_DEPARTMENT]->(:Department)<-[:IN_DEPARTMENT]-(e:Employee),\n(e)-[:LIVES]->(city:City)-[:IN_COUNTRY]->(country:Country)\nWITH city, country\nCALL {\n    WITH country\n    RETURN country.countryCode as code, count(*) as value\n    UNION\n    WITH city\n    RETURN city.countryCode as code, count(*) as value\n}\nWITH code, sum(value) as totalCount\nRETURN code,totalCount\n    `,\n    syntheticQuery: `\n        UNWIND [[\"FR\", 1], [\"IT\", 3], [\"FR.32\", 1], [\"IT.02\", 2], [\"IT.01\", 1]] as v\n        RETURN v[0] as code, v[1] as value \n        `,\n    settings: { mapDrillDown: true, showLegend: false },\n    fields: [],\n    selection: { index: 'code', value: 'value' },\n    type: 'map',\n    chartType: NeoAreaMapChart,\n  },\n  {\n    title: 'Gauge Chart',\n    description: 'Gauge charts can be used to visualize a single numeric value (0-100) as a reading on a dial',\n    exampleQuery:\n      '// How many story points has been closed during this sprint (based on the total)?\\n' +\n      \"MATCH (:Sprint{name:'Sprint 2'})-[:HAS_STORY]->(s:Story)\\n\" +\n      'WITH collect(s) as Stories\\n' +\n      'WITH  reduce(t = 0, n IN Stories | t + n.points) as Total, reduce(t = 0, n IN [story in Stories where story.closed = true ] | t + n.points) as TotalClosed\\n' +\n      'RETURN toFloat(TotalClosed*100)/Total',\n    syntheticQuery: 'RETURN rand()*100',\n    settings: {},\n    selection: {},\n    fields: [],\n    type: 'gauge',\n    chartType: NeoGaugeChart,\n  },\n  {\n    title: 'Gantt',\n    description:\n      'A Gantt chart models nodes as tasks, and relationships as dependencies between them. A relationship property can be used to display different dependency types.',\n    exampleQuery: 'MATCH (a:Activity)-[r:NEXT]->(a2:Activity)\\nRETURN a, r, a2',\n    syntheticQuery: `\n        WITH [\n            {\n                path: {  start: {labels: [\"Task\"], identity: 1, properties: {name: \"Task A\", startDate: \"2023-10-10\",  endDate: \"2023-10-11\"}},  end:  {identity: 2},  length: 1, segments: [ { start: {labels: [\"Task\"], identity: 1, properties: {name: \"Task A\", startDate: \"2023-10-10\",  endDate: \"2023-10-11\"}}, relationship: {type: \"NEXT\", start: 1, end: 2, identity: 10001, properties: {rel_type: 'ES'}}, end: {labels: [\"Task\"], identity: 2,properties: {name: \"Task B\", startDate: \"2023-10-11\",  endDate: \"2023-10-12\"}} } ] }, person: \"Jim\", movie: \"The Matrix\", rating: 4.5\n            },\n            {\n                path: {  start: {labels: [\"Task\"], identity: 2, properties: {name: \"Task B\", startDate: \"2023-10-11\",  endDate: \"2023-10-12\"}},  end:  {identity: 3},  length: 1, segments: [ { start: {labels: [\"Task\"], identity: 2, properties: {name: \"Task B\", startDate: \"2023-10-11\",  endDate: \"2023-10-12\"}}, relationship: {type: \"NEXT\", start: 2, end: 3, identity: 10002, properties: {rel_type: 'EE'}}, end: {labels: [\"Task\"], identity: 3,properties: {name: \"Task C\", startDate: \"2023-10-12\",  endDate: \"2023-10-13\"}} } ] }, person: \"Mike\", movie: \"The Matrix\", rating: 3.8\n            },\n            {\n                path: {  start: {labels: [\"Task\"], identity: 3, properties: {name: \"Task C\", startDate: \"2023-10-12\",  endDate: \"2023-10-13\"}},  end:  {identity: 4},  length: 1, segments: [ { start: {labels: [\"Task\"], identity: 3, properties: {name: \"Task C\", startDate: \"2023-10-12\",  endDate: \"2023-10-13\"}}, relationship: {type: \"NEXT\", start: 3, end: 4, identity: 10003, properties: {rel_type: 'SE'}}, end: {labels: [\"Task\"], identity: 4,properties: {name: \"Task D\", startDate: \"2023-10-13\",  endDate: \"2023-10-14\"}} } ] }, person: \"Sarah\", movie: \"The Matrix\", rating: 5.0\n            },\n            {\n                path: {  start: {labels: [\"Task\"], identity: 4, properties: {name: \"Task D\", startDate: \"2023-10-13\",  endDate: \"2023-10-14\"}},  end:  {identity: 5},  length: 1, segments: [ { start: {labels: [\"Task\"], identity: 4, properties: {name: \"Task D\", startDate: \"2023-10-13\",  endDate: \"2023-10-14\"}}, relationship: {type: \"NEXT\", start: 4, end: 5, identity: 10004, properties: {rel_type: 'SS'}}, end: {labels: [\"Task\"], identity: 5, properties: {name: \"Task E\", startDate: \"2023-10-14\",  endDate: \"2023-10-15\"}} } ] }, person: \"Jim\", movie: \"The Matrix - Reloaded\", rating: 3.5\n            }\n          ] as data\n          UNWIND data as row\n          RETURN row.path as Path\n        `,\n    settings: {},\n    fields: [],\n    dimensions: {\n      height: 420,\n    },\n    selection: {},\n    type: 'gantt',\n    chartType: NeoGanttChart,\n  },\n];\n"
  },
  {
    "path": "src/extensions/advancedcharts/AdvancedChartsReportConfig.tsx",
    "content": "import React from 'react';\nimport { SELECTION_TYPES } from '../../config/CardConfig';\nimport NeoChoroplethMapChart from './chart/choropleth/ChoroplethMapChart';\nimport NeoCirclePackingChart from './chart/circlepacking/CirclePackingChart';\nimport NeoGaugeChart from './chart/gauge/GaugeChart';\nimport NeoSankeyChart from './chart/sankey/SankeyChart';\nimport NeoSunburstChart from './chart/sunburst/SunburstChart';\nimport NeoTreeMapChart from './chart/treemap/TreeMapChart';\nimport NeoRadarChart from './chart/radar/RadarChart';\nimport NeoAreaMapChart from './chart/areamap/AreaMapChart';\nimport NeoGanttChart from './chart/gantt/GanttChart';\nimport NeoGraphChart3D from './chart/graph3d/GraphChart3D';\nimport { objectMap, objMerge } from '../../utils/ObjectManipulation';\nimport { COMMON_REPORT_SETTINGS } from '../../config/ReportConfig';\n\nexport const _ADVANCED_REPORT_TYPES = {\n  graph3d: {\n    label: '3D Graph',\n    helperText:\n      'A 3D graph visualization will draw all returned nodes, relationships and paths... in three dimensions!',\n    selection: {\n      properties: {\n        label: 'Node Properties',\n        type: SELECTION_TYPES.NODE_PROPERTIES,\n      },\n    },\n    useNodePropsAsFields: true,\n    autoAssignSelectedProperties: true,\n    component: NeoGraphChart3D,\n    maxRecords: 1000,\n    // The idea is to match a setting to its dependency, the operator represents the kind of relationship\n    // between the different options (EX: if operator is false, then it must be the opposite of the setting it depends on)\n    disabledDependency: { relationshipParticleSpeed: { dependsOn: 'relationshipParticles', operator: false } },\n    settings: {\n      nodeColorScheme: {\n        label: 'Node Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: [\n          'neodash',\n          'nivo',\n          'category10',\n          'accent',\n          'dark2',\n          'paired',\n          'pastel1',\n          'pastel2',\n          'set1',\n          'set2',\n          'set3',\n        ],\n        default: 'neodash',\n      },\n      nodeLabelColor: {\n        label: 'Node Label Color',\n        type: SELECTION_TYPES.COLOR,\n        default: 'black',\n      },\n      nodeLabelFontSize: {\n        label: 'Node Label Font Size',\n        type: SELECTION_TYPES.NUMBER,\n        default: 3.5,\n      },\n      defaultNodeSize: {\n        label: 'Node Size',\n        type: SELECTION_TYPES.NUMBER,\n        default: 2,\n      },\n      nodeSizeProp: {\n        label: 'Node Size Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'size',\n      },\n      nodeColorProp: {\n        label: 'Node Color Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'color',\n      },\n      defaultRelColor: {\n        label: 'Relationship Color',\n        type: SELECTION_TYPES.TEXT,\n        default: '#a0a0a0',\n      },\n      defaultRelWidth: {\n        label: 'Relationship Width',\n        type: SELECTION_TYPES.NUMBER,\n        default: 1,\n      },\n      relLabelColor: {\n        label: 'Relationship Label Color',\n        type: SELECTION_TYPES.TEXT,\n        default: '#a0a0a0',\n      },\n      relLabelFontSize: {\n        label: 'Relationship Label Font Size',\n        type: SELECTION_TYPES.NUMBER,\n        default: 2.75,\n      },\n      relColorProp: {\n        label: 'Relationship Color Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'color',\n      },\n      relWidthProp: {\n        label: 'Relationship Width Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'width',\n      },\n      relationshipParticles: {\n        label: 'Animated particles on Relationships',\n        type: SELECTION_TYPES.LIST,\n        default: false,\n        values: [false, true],\n      },\n      relationshipParticleSpeed: {\n        label: 'Speed of the particle animation',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0.005,\n      },\n      arrowLengthProp: {\n        label: 'Arrow head size',\n        type: SELECTION_TYPES.NUMBER,\n        default: 3,\n      },\n      layout: {\n        label: 'Graph Layout (experimental)',\n        type: SELECTION_TYPES.LIST,\n        values: ['force-directed', 'tree-top-down', 'tree-bottom-up', 'tree-left-right', 'tree-right-left', 'radial'],\n        default: 'force-directed',\n      },\n      graphDepthSep: {\n        label: 'Tree layout level distance',\n        type: SELECTION_TYPES.NUMBER,\n        default: 30,\n      },\n      enableExploration: {\n        label: 'Enable graph exploration',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      enableEditing: {\n        label: 'Enable graph editing',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      showPropertiesOnHover: {\n        label: 'Show pop-up on Hover',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      showPropertiesOnClick: {\n        label: 'Show properties on Click',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      drilldownLink: {\n        label: 'Drilldown Icon Link',\n        type: SELECTION_TYPES.TEXT,\n        placeholder: 'https://bloom.neo4j.io',\n        default: '',\n      },\n      allowDownload: {\n        label: 'Enable CSV Download',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      hideSelections: {\n        label: 'Hide Property Selection',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      rightClickToExpandNodes: {\n        label: 'Right Click to Expand Nodes',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n    },\n  },\n  gauge: {\n    label: 'Gauge Chart',\n    component: NeoGaugeChart,\n    helperText: (\n      <div>\n        A gauge chart expects a single <code>value</code>.\n      </div>\n    ),\n    maxRecords: 1,\n    selection: {\n      value: {\n        label: 'Value',\n        type: SELECTION_TYPES.NUMBER,\n        key: true,\n      },\n    },\n    withoutFooter: true,\n    settings: {\n      backgroundColor: {\n        label: 'Background Color',\n        type: SELECTION_TYPES.COLOR,\n        default: '#fafafa',\n      },\n      nrOfLevels: {\n        label: 'Number of levels',\n        type: SELECTION_TYPES.NUMBER,\n        default: 3,\n      },\n      arcsLength: {\n        label: 'Comma-separated length of each arc',\n        type: SELECTION_TYPES.TEXT,\n        default: '0.15, 0.55, 0.3',\n      },\n      arcPadding: {\n        label: 'Arc padding',\n        type: SELECTION_TYPES.TEXT,\n        default: '0.02',\n      },\n      colors: {\n        label: 'Comma-separated arc colors',\n        type: SELECTION_TYPES.TEXT,\n        default: '#5BE12C, #F5CD19, #EA4228',\n      },\n      textColor: {\n        label: 'Color of the text',\n        type: SELECTION_TYPES.TEXT,\n        default: 'black',\n      },\n      animDelay: {\n        label: 'Delay in ms before needle animation',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      animateDuration: {\n        label: 'Duration in ms for needle animation',\n        type: SELECTION_TYPES.NUMBER,\n        default: 2000,\n      },\n      marginLeft: {\n        label: 'Margin Left (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginRight: {\n        label: 'Margin Right (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginTop: {\n        label: 'Margin Top (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 40,\n      },\n      marginBottom: {\n        label: 'Margin Bottom (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 40,\n      },\n    },\n  },\n  sunburst: {\n    label: 'Sunburst Chart',\n    component: NeoSunburstChart,\n    useReturnValuesAsFields: true,\n    helperText: (\n      <div>\n        A Sunburst chart expects two fields: a <code>path</code> (list of strings) and a <code>value</code>.\n      </div>\n    ),\n    selection: {\n      index: {\n        label: 'Path',\n        type: SELECTION_TYPES.LIST,\n      },\n      value: {\n        label: 'Value',\n        type: SELECTION_TYPES.NUMBER,\n        key: true,\n      },\n      key: {\n        label: 'Inline',\n        type: SELECTION_TYPES.LIST,\n        optional: true,\n      },\n    },\n    maxRecords: 3000,\n    settings: {\n      backgroundColor: {\n        label: 'Background Color',\n        type: SELECTION_TYPES.COLOR,\n        default: '#fafafa',\n      },\n      enableArcLabels: {\n        label: 'Show Values on Arcs',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      interactive: {\n        label: 'Enable interactivity',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      colors: {\n        label: 'Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: ['nivo', 'category10', 'accent', 'dark2', 'paired', 'pastel1', 'pastel2', 'set1', 'set2', 'set3'],\n        default: 'set2',\n      },\n      borderWidth: {\n        label: 'Arc border width (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      marginLeft: {\n        label: 'Margin Left (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginRight: {\n        label: 'Margin Right (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginTop: {\n        label: 'Margin Top (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginBottom: {\n        label: 'Margin Bottom (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 40,\n      },\n      arcLabelsSkipAngle: {\n        label: 'Minimum Arc Angle for Label (degrees)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 10,\n      },\n      cornerRadius: {\n        label: 'Slice Corner Radius',\n        type: SELECTION_TYPES.NUMBER,\n        default: 3,\n      },\n      inheritColorFromParent: {\n        label: 'Inherit color from parent',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n    },\n  },\n  circlePacking: {\n    label: 'Circle Packing',\n    component: NeoCirclePackingChart,\n    useReturnValuesAsFields: true,\n    helperText: (\n      <div>\n        A circle packing chart expects two fields: a <code>path</code> (list of strings) and a <code>value</code>.\n      </div>\n    ),\n    selection: {\n      index: {\n        label: 'Path',\n        type: SELECTION_TYPES.LIST,\n      },\n      value: {\n        label: 'Value',\n        type: SELECTION_TYPES.NUMBER,\n        key: true,\n      },\n      key: {\n        label: 'Inline',\n        type: SELECTION_TYPES.LIST,\n        optional: true,\n      },\n    },\n    maxRecords: 3000,\n    settings: {\n      backgroundColor: {\n        label: 'Background Color',\n        type: SELECTION_TYPES.COLOR,\n        default: '#fafafa',\n      },\n      interactive: {\n        label: 'Enable interactivity',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      colors: {\n        label: 'Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: ['nivo', 'category10', 'accent', 'dark2', 'paired', 'pastel1', 'pastel2', 'set1', 'set2', 'set3'],\n        default: 'set2',\n      },\n      showLabels: {\n        label: 'Show Circle Labels',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      borderWidth: {\n        label: 'Circle border width (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      marginLeft: {\n        label: 'Margin Left (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginRight: {\n        label: 'Margin Right (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginTop: {\n        label: 'Margin Top (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginBottom: {\n        label: 'Margin Bottom (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 40,\n      },\n    },\n  },\n  treeMap: {\n    label: 'Treemap',\n    component: NeoTreeMapChart,\n    useReturnValuesAsFields: true,\n    helperText: (\n      <div>\n        A Tree Map chart expects two fields: a <code>path</code> (list of strings) and a <code>value</code>.\n      </div>\n    ),\n    selection: {\n      index: {\n        label: 'Path',\n        type: SELECTION_TYPES.LIST,\n      },\n      value: {\n        label: 'Value',\n        type: SELECTION_TYPES.NUMBER,\n        key: true,\n      },\n      key: {\n        label: 'Inline',\n        type: SELECTION_TYPES.LIST,\n        optional: true,\n      },\n    },\n    maxRecords: 3000,\n    settings: {\n      backgroundColor: {\n        label: 'Background Color',\n        type: SELECTION_TYPES.COLOR,\n        default: '#fafafa',\n      },\n      interactive: {\n        label: 'Enable interactivity',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      colors: {\n        label: 'Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: ['nivo', 'category10', 'accent', 'dark2', 'paired', 'pastel1', 'pastel2', 'set1', 'set2', 'set3'],\n        default: 'set2',\n      },\n      borderWidth: {\n        label: 'Rectangle border width (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      marginLeft: {\n        label: 'Margin Left (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginRight: {\n        label: 'Margin Right (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginTop: {\n        label: 'Margin Top (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginBottom: {\n        label: 'Margin Bottom (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 40,\n      },\n    },\n  },\n  sankey: {\n    label: 'Sankey Chart',\n    component: NeoSankeyChart,\n    useNodePropsAsFields: true,\n    autoAssignSelectedProperties: true,\n    ignoreLabelColors: true,\n    helperText: (\n      <div>\n        A Sankey chart expects Neo4j <code>nodes</code> and <code>weighted relationships</code>.\n      </div>\n    ),\n    selection: {\n      nodeProperties: {\n        label: 'Node Properties',\n        type: SELECTION_TYPES.NODE_PROPERTIES,\n      },\n    },\n    maxRecords: 250,\n    settings: {\n      backgroundColor: {\n        label: 'Background Color',\n        type: SELECTION_TYPES.COLOR,\n        default: '#fafafa',\n      },\n      legend: {\n        label: 'Show legend',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      interactive: {\n        label: 'Enable interactivity',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      layout: {\n        label: 'Sankey layout',\n        type: SELECTION_TYPES.LIST,\n        values: ['horizontal', 'vertical'],\n        default: 'horizontal',\n      },\n      colors: {\n        label: 'Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: ['nivo', 'category10', 'accent', 'dark2', 'paired', 'pastel1', 'pastel2', 'set1', 'set2', 'set3'],\n        default: 'set2',\n      },\n      labelPosition: {\n        label: 'Control sankey label position',\n        type: SELECTION_TYPES.LIST,\n        values: ['inside', 'outside'],\n        default: 'inside',\n      },\n      labelOrientation: {\n        label: 'Control sankey label orientation',\n        type: SELECTION_TYPES.LIST,\n        values: ['horizontal', 'vertical'],\n        default: 'horizontal',\n      },\n      nodeBorderWidth: {\n        label: 'Node border width (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      marginLeft: {\n        label: 'Margin Left (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginRight: {\n        label: 'Margin Right (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginTop: {\n        label: 'Margin Top (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginBottom: {\n        label: 'Margin Bottom (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 40,\n      },\n      labelProperty: {\n        label: 'Relationship value Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'value',\n      },\n      nodeThickness: {\n        label: 'Node thickness (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 18,\n      },\n      nodeSpacing: {\n        label: 'Spacing between nodes at an identical level (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 18,\n      },\n    },\n  },\n  choropleth: {\n    label: 'Choropleth Map',\n    component: NeoChoroplethMapChart,\n    useReturnValuesAsFields: true,\n    helperText: (\n      <div>\n        A Choropleth Map chart expects two fields: a <code>country code</code> (three-letter code) and a\n        <code>value</code>.\n      </div>\n    ),\n    selection: {\n      index: {\n        label: 'Code',\n        type: SELECTION_TYPES.TEXT,\n      },\n      value: {\n        label: 'Value',\n        type: SELECTION_TYPES.NUMBER,\n        key: true,\n      },\n      key: {\n        label: 'code',\n        type: SELECTION_TYPES.TEXT,\n        optional: true,\n      },\n    },\n    maxRecords: 300,\n    settings: {\n      matchAccessor: {\n        label: 'Country code format',\n        type: SELECTION_TYPES.LIST,\n        values: ['iso_a3', 'iso_a2', 'iso_n3'],\n        default: 'iso_a3',\n      },\n      backgroundColor: {\n        label: 'Background Color',\n        type: SELECTION_TYPES.COLOR,\n        default: '#fafafa',\n      },\n      interactive: {\n        label: 'Enable Interactivity',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      legend: {\n        label: 'Show Legend',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      colors: {\n        label: 'Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: ['nivo', 'BrBG', 'RdYlGn', 'YlOrRd', 'greens'],\n        default: 'nivo',\n      },\n      borderWidth: {\n        label: 'Polygon Border Width (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0,\n      },\n      marginLeft: {\n        label: 'Margin Left (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginRight: {\n        label: 'Margin Right (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginTop: {\n        label: 'Margin Top (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginBottom: {\n        label: 'Margin Bottom (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 40,\n      },\n      projectionScale: {\n        label: 'Projection Scale',\n        type: SELECTION_TYPES.NUMBER,\n        default: 100,\n      },\n      projectionTranslationX: {\n        label: 'Projection X translation',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0.5,\n      },\n      projectionTranslationY: {\n        label: 'Projection Y translation',\n        type: SELECTION_TYPES.NUMBER,\n        default: 0.5,\n      },\n      labelProperty: {\n        label: 'Tooltip Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'properties.name',\n      },\n    },\n  },\n  radar: {\n    label: 'Radar Chart',\n    component: NeoRadarChart,\n    useReturnValuesAsFields: true,\n    helperText: (\n      <div>\n        A radar chart expects two advanced configurations: a <code>Quantitative Variables</code> and an\n        <code>Index Property</code>.\n      </div>\n    ),\n    selection: {\n      index: {\n        label: 'Index',\n        type: SELECTION_TYPES.TEXT,\n        key: true,\n      },\n      values: {\n        label: 'Value',\n        type: SELECTION_TYPES.NUMBER,\n        multiple: true,\n        key: true,\n      },\n    },\n    maxRecords: 250,\n    settings: {\n      backgroundColor: {\n        label: 'Background Color',\n        type: SELECTION_TYPES.COLOR,\n        default: '#fafafa',\n      },\n      legend: {\n        label: 'Show Legend',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      interactive: {\n        label: 'Enable interactivity',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      animate: {\n        label: 'Enable transitions',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      colors: {\n        label: 'Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: ['nivo', 'category10', 'accent', 'dark2', 'paired', 'pastel1', 'pastel2', 'set1', 'set2', 'set3'],\n        default: 'set2',\n      },\n      marginLeft: {\n        label: 'Margin Left (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginRight: {\n        label: 'Margin Right (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 24,\n      },\n      marginTop: {\n        label: 'Margin Top (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 40,\n      },\n      marginBottom: {\n        label: 'Margin Bottom (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 40,\n      },\n      dotSize: {\n        label: 'Size of the dots (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 10,\n      },\n      dotBorderWidth: {\n        label: 'Width of the dots border (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 2,\n      },\n      gridLevels: {\n        label: 'Number of levels to display for grid',\n        type: SELECTION_TYPES.NUMBER,\n        default: 5,\n      },\n      gridLabelOffset: {\n        label: 'Label offset from outer radius (px)',\n        type: SELECTION_TYPES.NUMBER,\n        default: 16,\n      },\n      blendMode: {\n        label: 'Blend Mode',\n        type: SELECTION_TYPES.LIST,\n        values: [\n          'normal',\n          'multiply',\n          'screen',\n          'overlay',\n          'darken',\n          'lighten',\n          'color-dodge',\n          'color-burn',\n          'hard-light',\n          'soft-light',\n          'difference',\n          'exclusion',\n          'hue',\n          'saturation',\n          'color',\n          'luminosity',\n        ],\n        default: 'normal',\n      },\n      motionConfig: {\n        label: 'Motion Configuration',\n        type: SELECTION_TYPES.LIST,\n        values: ['default', 'gentle', 'wobbly', 'stiff', 'slow', 'molasses'],\n        default: 'gentle',\n      },\n      curve: {\n        label: 'Curve interpolation',\n        type: SELECTION_TYPES.LIST,\n        values: ['basicClosed', 'cardinalClosed', 'catmullRomClosed', 'linearClosed'],\n        default: 'linearClosed',\n      },\n    },\n  },\n  //\n  /** *\n   * * TODO: An idea here:\n    For the level zero layers, perhaps we can make the component work agnostically of whether the user is using two or three level country codes.\n    E.g. it can apply colouring to germany based on \"DE\" or \"GER\", whatever it picks up. That would be a lot easier than providing an advanced setting for it. In the rare case that the user returns both (this will probably never happen), we just choose either.\n    I'm also thinking about adding three-letter country code support since that is what the choropleth used, so it will make migrating from choropleth to areamap a lot easier for users.\n   */\n  areamap: {\n    label: 'Area Map',\n    helperText: (\n      <div>\n        An Area Map expects two fields: a <code>country code / region code</code> (three-letter code) and a\n        <code>value</code>.\n      </div>\n    ),\n    useReturnValuesAsFields: true,\n    maxRecords: 300,\n    component: NeoAreaMapChart,\n    selection: {\n      index: {\n        label: 'Code',\n        type: SELECTION_TYPES.TEXT,\n      },\n      value: {\n        label: 'Value',\n        type: SELECTION_TYPES.NUMBER,\n        key: true,\n      },\n    },\n    settings: {\n      providerUrl: {\n        label: 'Map Provider URL',\n        type: SELECTION_TYPES.TEXT,\n        default: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n      },\n      colors: {\n        label: 'Color Scheme',\n        type: SELECTION_TYPES.LIST,\n        values: ['nivo', 'BrBG', 'RdYlGn', 'YlOrRd', 'greens'],\n        default: 'YlOrRd',\n      },\n      countryCodeFormat: {\n        label: 'Country Code Format',\n        type: SELECTION_TYPES.LIST,\n        values: ['Alpha-2', 'Alpha-3'],\n        default: 'Alpha-2',\n      },\n      showLegend: {\n        label: 'Color Legend',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      mapDrillDown: {\n        label: 'Drilldown Enabled',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n    },\n  },\n  gantt: {\n    label: 'Gantt Chart',\n    helperText: <div>A gantt chart requires nodes (events) and relationships (dependencies).</div>,\n    maxRecords: 300,\n    component: NeoGanttChart,\n    withoutFooter: true,\n    useNodePropsAsFields: true,\n    autoAssignSelectedProperties: true,\n    selection: {\n      properties: {\n        label: 'Node Properties',\n        type: SELECTION_TYPES.NODE_PROPERTIES,\n      },\n    },\n    settings: {\n      barColor: {\n        label: 'Bar Color',\n        type: SELECTION_TYPES.TEXT,\n        default: '#a3a3ff',\n      },\n      nameProperty: {\n        label: 'Task Label Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'name',\n      },\n      startDateProperty: {\n        label: 'Task Start Date Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'startDate',\n      },\n      endDateProperty: {\n        label: 'Task End Date Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'endDate',\n      },\n      orderProperty: {\n        label: 'Task Ordering Property',\n        type: SELECTION_TYPES.TEXT,\n        default: '(auto)',\n      },\n      dependencyTypeProperty: {\n        label: 'Dependency Type Property',\n        type: SELECTION_TYPES.TEXT,\n        default: 'rel_type',\n      },\n      viewMode: {\n        label: 'View mode',\n        type: SELECTION_TYPES.LIST,\n        values: ['auto', 'Half Day', 'Day', 'Week', 'Month', 'Quarter', 'Year'],\n        default: 'auto',\n      },\n    },\n  },\n};\n\nexport const ADVANCED_REPORT_TYPES = objectMap(_ADVANCED_REPORT_TYPES, (value: any) => {\n  return objMerge({ settings: COMMON_REPORT_SETTINGS }, value);\n});\n"
  },
  {
    "path": "src/extensions/advancedcharts/Utils.ts",
    "content": "import { valueIsArray } from '../../chart/ChartUtils';\nimport { useSelector } from 'react-redux';\nimport { getPageNumbersAndNames } from '../../dashboard/DashboardSelectors';\n\nexport const getRule = (e, rules, type) => {\n  let r = getRuleWithFieldPropertyName(e, rules, type, null);\n  return r || null;\n};\n\nexport const getRuleWithFieldPropertyName = (e, rules, type, fieldPropertyName) => {\n  let f = fieldPropertyName == null ? 'field' : fieldPropertyName;\n  let r = rules.filter((rule) => rule.condition && rule.condition == type && ruleFieldCheck(rule.field, e[f]));\n  if (r.length == 0) {\n    return null;\n  }\n  return r;\n};\n\nconst ruleFieldCheck = (ruleValue, value) => {\n  // if the field is unspecified, always return true\n  if (ruleValue == '') {\n    return true;\n  }\n  if (valueIsArray(value)) {\n    return value.includes(ruleValue);\n  }\n  return value.trim() == ruleValue.trim();\n};\n\n/**\n * Returns a list of pairs, where each pair represents a page number and a name.\n */\nexport const getPageNumbersAndNamesList = () => {\n  return useSelector(getPageNumbersAndNames);\n};\n\n/**\n * Get the relevant page for a specific action rule.\n */\nexport const getPageFromPageNames = (pageNames, ruleValue) => {\n  // TODO - handle renames and reorders automatically by updating the action logic.\n  let page = pageNames.filter((pageNew) => pageNew.split('/')[1] == ruleValue.split('/')[1]);\n  if (page.length == 1) {\n    return page[0];\n  }\n  page = pageNames.filter((pageNew) => pageNew == ruleValue);\n  if (page.length == 1) {\n    return page[0];\n  }\n  return null;\n};\n\n/**\n *\n * @param rule  - an action rule as specified by the user {\"condition\", \"field\", \"value\",  \"customization\", \"customizationValue\"}\n * @param e - element to execute the rule on.\n * @param props - ReportProps object to get callback from to update the state.\n * @param type - type of rule, currently unused.\n */\nexport const executeActionRule = (rule, e, props, _type = 'default') => {\n  if (rule !== null) {\n    if (rule.customization == 'set variable' && props && props.setGlobalParameter) {\n      // call thunk for $neodash_customizationValue\n      let rValue = rule.value == 'id' ? 'id ' : rule.value;\n      if (rValue != '' && e.row && e.row[rValue]) {\n        props.setGlobalParameter(`neodash_${rule.customizationValue}`, e.row[rule.value]);\n      } else if (rule.value != '' && e.properties && e.properties[rule.value]) {\n        props.setGlobalParameter(`neodash_${rule.customizationValue}`, e.properties[rule.value]);\n      } else {\n        props.setGlobalParameter(`neodash_${rule.customizationValue}`, e.value);\n      }\n    } else if (rule.customization == 'set page' && props.setPageNumber && props.pageNames) {\n      let page = getPageFromPageNames(props.pageNames, rule.value);\n      if (page) {\n        props.setPageNumber(page.split('/')[0]);\n      } else {\n        props.setPageNumber(undefined);\n      }\n    }\n  }\n};\n\n/**\n * Evaluates and runs actions based on an element based on the rule set defined in the settings.\n * @param e - the element --> should be a dictionary with two entries {field, value}, the category and the attached value.\n * @param actionsRules - the list of rules.\n * @param props - ChartProps object with callbacks to execute rule.\n * @param action - the type of action to perform.\n * @param type - the rule type.\n */\nexport const performActionOnElement = (e: { field; value }, actionsRules, props, action, type = 'default') => {\n  let rules = getRule(e, actionsRules, action);\n  if (rules !== null) {\n    rules.forEach((rule) => executeActionRule(rule, e, props, type));\n  }\n};\n\nexport const unassign = (target, source) => {\n  Object.keys(source).forEach((key) => {\n    delete target[key];\n  });\n};\n\nexport const merge = (oldData, newData, operation) => {\n  if (operation) {\n    return Object.assign({}, newData, oldData);\n  }\n  unassign(oldData, newData);\n  return oldData;\n};\n\nexport const update = (state, mutations) => Object.assign({}, state, mutations);\n\nfunction isCyclicUtil(i, visited, recStack, adj) {\n  // Mark the current node as visited and\n  // part of recursion stack\n  if (recStack.get(i)) {\n    return true;\n  }\n\n  if (visited.get(i)) {\n    return false;\n  }\n\n  visited.set(i, true);\n  recStack.set(i, true);\n\n  let childrens = adj.get(i);\n\n  for (const children in childrens) {\n    if (isCyclicUtil(childrens[children], visited, recStack, adj)) {\n      return true;\n    }\n  }\n\n  recStack.set(i, false);\n\n  return false;\n}\n\nexport const isCyclic = (graph) => {\n  let visited = new Map();\n  let recStack = new Map();\n  let adj = new Map();\n\n  graph.nodes.forEach((node) => {\n    visited.set(node.id, false);\n    recStack.set(node.id, false);\n    adj.set(node.id, []);\n  });\n\n  graph.links.forEach((link) => {\n    adj.get(link.source).push(link.target);\n  });\n\n  for (const idx in graph.nodes) {\n    if (isCyclicUtil(graph.nodes[idx].id, visited, recStack, adj)) {\n      return true;\n    }\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/areamap/AreaMapChart.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { ChartProps } from '../../../../chart/Chart';\nimport { MapContainer, TileLayer } from 'react-leaflet';\nimport 'leaflet/dist/leaflet.css';\nimport { MapBoundary } from './PolygonLayer';\nimport { recordToNative } from '../../../../chart/ChartUtils';\nimport { NoDrawableDataErrorMessage } from '../../../../component/editor/CodeViewerComponent';\nimport { keyLengthToKeyName, regionCodeName } from './constants';\n\n/**\n * Method used to extract geographic data from the records got back by the query\n * @param records List of records returned from the query\n * @param selection Selection defined by the user to map the query result to the map\n * @returns Dictionary that assigns, to each geoCode, its value\n */\nfunction createGeoDictionary(records, selection) {\n  let data = {};\n\n  records.forEach((row: Record<string, any>) => {\n    try {\n      const index = recordToNative(row.get(selection.index));\n      const value = recordToNative(row.get(selection.value));\n      if (!index || value == undefined || isNaN(value) || typeof index !== 'string') {\n        return;\n        // throw \"Invalid selection for area map chart. Ensure a three letter country code is retrieved together with a value.\"\n      }\n      data[index] = value;\n    } catch (e) {\n      // eslint-disable-next-line no-console\n      console.error(e);\n    }\n  });\n  return data;\n}\n\n/**\n * To speed up the binding process, let's reduce the list into a object to use the index access\n * @param features\n * @param desiredLevel\n * @returns\n */\nfunction fromFeatureListToObject(features, desiredLevel) {\n  let res = {};\n  features.forEach((feature) => {\n    let code = feature.properties[desiredLevel];\n    if (code != undefined) {\n      res[code] = feature;\n    }\n  });\n  return res;\n}\n\n/**\n * Renders Neo4j Records inside a GeoJSON map.\n */\nconst NeoAreaMapChart = (props: ChartProps) => {\n  // Retrieve config from advanced settings\n  const { records } = props;\n  const { selection } = props;\n  const dimensions = props.dimensions ? props.dimensions : { width: 100, height: 100 };\n  const keyLength = props.settings && props.settings.countryCodeFormat ? props.settings.countryCodeFormat : 'Alpha-2';\n\n  // Key used to refresh the visualization\n  let key = `${dimensions.width},${dimensions.height},${props.fullscreen}`;\n  const [data, setData] = useState({});\n\n  // Two feature levels (ideally we can extend this too)\n  const [featureLevel0, setFeatureLevel0] = useState({});\n  const [featureLevel1, setFeatureLevel1] = useState({});\n\n  const [isReady, setIsReady] = useState(false);\n  const mapProviderURL =\n    props.settings && props.settings.providerUrl\n      ? props.settings.providerUrl\n      : 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';\n  const attribution =\n    props.settings && props.settings.attribution\n      ? props.settings.attribution\n      : '&copy; <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors';\n\n  // Extracting the geoData from the records\n  useEffect(() => {\n    setData(createGeoDictionary(records, selection));\n  }, [records, selection]);\n\n  useEffect(() => {\n    fetch('https://raw.githubusercontent.com/neo4j-labs/neodash-static/main/world_polymap_level_0_entities.json')\n      .then((res) => res.json())\n      .then((matched) => {\n        let tmp = fromFeatureListToObject(matched.features, keyLengthToKeyName[keyLength]);\n        setFeatureLevel0(tmp);\n        setIsReady(true);\n      });\n  }, [keyLength]);\n\n  useEffect(() => {\n    fetch('https://raw.githubusercontent.com/neo4j-labs/neodash-static/main/world_polymap_level_1_entities.json')\n      .then((res) => res.json())\n      .then((matched) => {\n        let tmp = fromFeatureListToObject(matched.features, regionCodeName);\n        setFeatureLevel1(tmp);\n      });\n  }, []);\n\n  if (\n    Object.keys(data).length == 0 ||\n    !selection ||\n    selection.index == selection.value ||\n    props.records == null ||\n    props.records.length == 0 ||\n    props.records[0].keys == null\n  ) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  // Rendering the map only when all the data is ready\n  if (isReady) {\n    return (\n      <MapContainer\n        key={key}\n        style={{ width: '100%', height: '100%' }}\n        center={[0, 0]}\n        zoom={0.5}\n        maxZoom={18}\n        scrollWheelZoom={false}\n      >\n        <TileLayer attribution={attribution} url={mapProviderURL} />\n        {\n          <MapBoundary\n            data={data}\n            props={props}\n            featureLevel0={featureLevel0}\n            featureLevel1={featureLevel1}\n            dimensions={dimensions}\n          />\n        }\n      </MapContainer>\n    );\n  }\n  return <></>;\n};\n\nexport default NeoAreaMapChart;\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/areamap/PolygonLayer.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { useMap, GeoJSON } from 'react-leaflet';\nimport 'leaflet/dist/leaflet.css';\nimport { Button } from '@mui/material';\nimport './styles/PolygonStyle.css';\nimport { categoricalColorSchemes } from '../../../../config/ColorConfig';\nimport { abbreviateNumber } from '../../../../chart/map/MapUtils';\n\n/**\n * Creates the list of values that will be used for the legend\n * @param min Minimum of the legend\n * @param max Maximum of the legend\n * @param lenRange Number of buckets\n * @returns List of numbers\n */\nfunction getLegendRange(min, max, legendLength) {\n  let diff = (max - min) / legendLength;\n  // If the difference is 0 or less (strange that can be less, but you never know)\n  // Return only one value\n  if (diff <= 0) {\n    return [Math.round(min * 100) / 100];\n  }\n  return [...Array(legendLength).keys()].map((i) => {\n    return Math.round((min + diff * i) * 100) / 100;\n  });\n}\n\n/**\n * Function to return a random string, right now useful to retrigger the GeoJson rendering\n * @returns A random string\n */\nfunction randomString() {\n  return `${Math.random().toString(36)}00000000000000000`.slice(2, 16);\n}\n\n/**\n * Function responsible of getting the regions inside a country\n * @param geoJson geoJson clicked on the UI, if we can't find anything we still show the clicked geoJson\n * @param features Object that matches an id to it's polygon\n * @param key Name of the parameter that will be used to match a polygon adn it's components\n * @returns feature object filtered of the useless objects\n */\nfunction getDrillDown(geoJson, features, key, keepConflict = false) {\n  let id = geoJson.properties[key];\n  let polygonsToKeep = Object.keys(features).filter((polygonId) => {\n    let tmp = features[polygonId];\n\n    // Some regions can be disputed between two or more countries\n    let toDrillDown =\n      key === 'SHORT_COUNTRY_CODE' && tmp.properties.POSSIBLE_COUNTRIES && keepConflict\n        ? tmp.properties.POSSIBLE_COUNTRIES.includes(id)\n        : tmp.properties[key] === id;\n    return toDrillDown;\n  });\n\n  // We don't want to re-render if there is no data in the drillDown\n  if (polygonsToKeep.length == 0) {\n    return [geoJson];\n  }\n\n  // Creating a dictionary that is just a filtered version of  features\n  let res = {};\n  polygonsToKeep.map((id) => {\n    res[id] = features[id];\n  });\n  return res;\n}\n\n/**\n * Function used to bind the data from the query with each geojson\n * @param geoData Data parsed from the query results\n * @param geoJsonData geoJson data that needs to be joined with the query results\n * @returns\n */\nfunction bindDataToMap(geoData, geoJsonData) {\n  let newValues = {};\n  let listValues: any[] = [];\n  Object.keys(geoData).forEach((key) => {\n    let tmp;\n    if (geoJsonData[key] != undefined) {\n      tmp = Object.assign({}, geoJsonData[key]);\n      tmp.properties.neo_value = geoData[key];\n      listValues.push(geoData[key]);\n      newValues[key] = tmp;\n    } else {\n      // console.log(`Missing key in Polygon Map :  ${key} with value ${geoData[key]}`);\n    }\n  });\n\n  // Deepcopying data to prevent strange behaviors\n  let geoJsonData_copy = Object.assign(geoJsonData, {});\n  let res = Object.assign(geoJsonData_copy, newValues);\n\n  // Sorting the values to get min and max\n  listValues.sort((a, b) => {\n    return a - b;\n  });\n  // Returning the list of features\n  return [Object.values(res), listValues[0], listValues.slice(-1)[0]];\n}\n\n// Wraps the boundary logic function\nexport const MapBoundary = ({ dimensions, data, props, featureLevel0, featureLevel1 }) => {\n  // Keys used for the drill down\n  // (right now only 0 is useful but important to keep in mind that we can add more levels)\n  const level0key = 'SHORT_COUNTRY_CODE';\n  const level1key = 'isoCode';\n  const isLegendEnabled = props.settings && props.settings.showLegend != undefined ? props.settings.showLegend : true;\n  const isDrillDownEnabled = props.settings && props.settings.mapDrillDown ? props.settings.mapDrillDown : false;\n  // Getting the list of colors from the scheme (for now, starting from a scheme, we always get the last color of the scheme)\n  const colorScheme =\n    props.settings && props.settings.colors\n      ? categoricalColorSchemes[props.settings.colors]\n      : categoricalColorSchemes.YlOrRd;\n  const listColors = Array.isArray(colorScheme)\n    ? Array.isArray(colorScheme.slice(-1)[0])\n      ? colorScheme.slice(-1)[0]\n      : colorScheme\n    : colorScheme;\n\n  // Final polygon data\n  const [polygonData, setPolygonData] = useState([]);\n\n  // Key of the current geoJson viz and currently selected geoJson\n  const [state, setState] = useState({ key: 'firstAll', geoJson: undefined });\n  // Key used to prevent race condition\n  const [key, setKey] = React.useState('firstAll');\n\n  const [legendRange, setLegendRange] = React.useState([]);\n  const [rangeValues, setRangeValues] = React.useState({ min: NaN, max: NaN });\n\n  useEffect(() => {\n    let currentKey =\n      state.geoJson != undefined && Object.keys(state.geoJson.properties).includes(level1key) ? level1key : level0key;\n    let tmp = state.geoJson === undefined ? featureLevel0 : getDrillDown(state.geoJson, featureLevel1, currentKey);\n\n    // Getting the new data and the distribution of the values\n    let [newData, minValue, maxValue] = bindDataToMap(data, tmp);\n\n    // Saving the new polygon data and their value range\n    setPolygonData(newData);\n    setRangeValues({ min: minValue, max: maxValue });\n\n    // Getting the new legend numbers\n    let legendRange = !isNaN(minValue) && !isNaN(maxValue) ? getLegendRange(minValue, maxValue, listColors.length) : [];\n    setLegendRange(legendRange);\n\n    // Synchronizing the key with the state key to retrigger rendering\n    setKey(state.key);\n  }, [state]);\n\n  // Getting the upper level Map\n  const map = useMap();\n\n  function onDrillDown(e) {\n    setState({ key: randomString(), geoJson: e.target.feature });\n    map.fitBounds(e.target.getBounds());\n  }\n\n  function onEachFeature(_, layer) {\n    if (isDrillDownEnabled) {\n      layer.on({\n        click: onDrillDown,\n      });\n    }\n  }\n\n  /**\n   * For each polygon, we have a defined style that we want to apply\n   * @param _feature : Features of the current polygon\n   * @returns A style that will be applied to its polygon\n   */\n  const geoJSONStyle = (_feature) => {\n    /**\n     * A polygon has a fill color based on its value\n     * @param weight value inside the polygon\n     * @param legendRange List that is used to define the legend range\n     * @param listColors List of colors in the palette\n     * @returns color assigned to the polygon based on it's position in the legend\n     */\n    function getColor(weight, legendRange, listColors) {\n      let index = legendRange.findIndex((number) => {\n        return number >= weight;\n      });\n      return listColors.slice(index)[0];\n    }\n    let color = !isNaN(_feature.properties.neo_value)\n      ? getColor(_feature.properties.neo_value, legendRange, listColors)\n      : 'gray';\n    let style = {\n      color: '#1f2021',\n      weight: 1,\n      fillOpacity: 0.8,\n      fillColor: color,\n    };\n    return style;\n  };\n\n  /**\n   * Functional component for a button to reset the visualization\n   * @returns button component binded to the current map\n   */\n  function ResetButton() {\n    // Binding the button to the map\n    const map = useMap();\n    function onClick() {\n      setState({ key: randomString(), geoJson: undefined });\n      map.setZoom(1);\n      return null;\n    }\n\n    return (\n      <div className='btnWrapper' style={{ zIndex: 1001, position: 'absolute', top: 10, right: 10 }}>\n        <Button variant='contained' onClick={onClick}>\n          Reset View\n        </Button>\n      </div>\n    );\n  }\n\n  const resetButton = isDrillDownEnabled ? ResetButton() : <></>;\n\n  /**\n   * Creates Legend component\n   * @param colors Color palette selected defined in the ReportSettings\n   * @param legendRange List of numbers that will be used to define the legend buckets\n   * @param dimensions dimensions object passed down from AreaMapChart\n   * @returns Legend component\n   */\n  function Legend(colors, legendRange, dimensions) {\n    return (\n      <div\n        className='info legend'\n        style={{\n          zIndex: 1001,\n          position: 'absolute',\n          bottom: 30,\n          right: 30,\n          minWidth: 110,\n          maxWidth: 180,\n          width: dimensions.width * 0.25,\n        }}\n      >\n        <table>\n          {legendRange.map((from, i, legendRange) => {\n            let to = legendRange[i + 1];\n            return (\n              <tr>\n                <td>\n                  <i style={{ background: colors[i] }}> &nbsp; </i>\n                </td>\n                <td>\n                  <p style={{ fontSize: 'small', margin: 0, marginTop: 2, overflow: 'hidden', height: 20 }}>\n                    {''}\n                    {abbreviateNumber(from, 2)}\n                    {!isNaN(to) ? ` - ${abbreviateNumber(to, 2)}` : i > 0 ? '+' : ''}{' '}\n                  </p>\n                </td>\n              </tr>\n            );\n          })}\n        </table>\n      </div>\n    );\n  }\n\n  // Create a legend only if the values are ready\n  const legend = !(isNaN(rangeValues.min) && isNaN(rangeValues.min)) ? (\n    Legend(listColors, legendRange, dimensions)\n  ) : (\n    <></>\n  );\n\n  // We need data or synchronized keys to enable re-render\n  if (polygonData.length == 0 || key !== state.key) {\n    return <></>;\n  }\n\n  const geoJsonLayer = (\n    <div>\n      {isDrillDownEnabled && state.geoJson !== undefined ? resetButton : <></>}\n      {isLegendEnabled ? legend : <></>}\n      <GeoJSON\n        id='polygonId'\n        key={state.key}\n        data={polygonData}\n        style={geoJSONStyle}\n        onEachFeature={onEachFeature}\n      ></GeoJSON>\n    </div>\n  );\n\n  return geoJsonLayer;\n};\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/areamap/constants.ts",
    "content": "export const keyLengthToKeyName = { 'Alpha-2': 'SHORT_COUNTRY_CODE', 'Alpha-3': 'COUNTRY_CODE' };\nexport const regionCodeName = 'code';\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/areamap/styles/PolygonStyle.css",
    "content": "body {\n  margin: 0px;\n}\n\n.info {\n  padding: 6px 8px;\n  font: 14px/16px Arial, Helvetica, sans-serif;\n  background: white;\n  background: rgba(255, 255, 255, 0.8);\n  box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);\n  border-radius: 5px;\n}\n\n.info h4 {\n  margin: 0 0 5px;\n  color: #777;\n}\n\n.legend {\n  text-align: left;\n  line-height: 18px;\n  color: #555;\n}\n\n.legend i {\n  width: 18px;\n  height: 18px;\n  float: left;\n  margin-right: 8px;\n  opacity: 0.7;\n}"
  },
  {
    "path": "src/extensions/advancedcharts/chart/choropleth/ChoroplethMapChart.tsx",
    "content": "import React from 'react';\nimport { ChartProps } from '../../../../chart/Chart';\nimport { ResponsiveChoropleth } from '@nivo/geo';\nimport { useState, useEffect } from 'react';\nimport { recordToNative } from '../../../../chart/ChartUtils';\nimport { NoDrawableDataErrorMessage } from '../../../../component/editor/CodeViewerComponent';\n\n/**\n * Embeds a NeoChoroplethMapChart (from Charts) into NeoDash.\n */\nconst NeoChoroplethMapChart = (props: ChartProps) => {\n  const { records } = props;\n  const { selection } = props;\n\n  // TODO Apply certain logic to determine different map features to display\n  // const feature = globeFeature;\n  const [feature, setFeature] = useState({ features: [] });\n  // TODO Think of a way to make it configurable to fetch vector data.\n  // It makes sense to ship this JSON with NeoDash deployments that are behind some firewall and can't access this site.\n  useEffect(() => {\n    fetch('https://raw.githubusercontent.com/neo4j-labs/neodash-static/main/world_polymap.json')\n      .then((res) => res.json())\n      .then((matched) => setFeature(matched));\n  }, []);\n\n  if (!selection || props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  const settings = props.settings ? props.settings : {};\n  const marginRight = settings.marginRight ? settings.marginRight : 24;\n  const marginLeft = settings.marginLeft ? settings.marginLeft : 24;\n  const marginTop = settings.marginTop ? settings.marginTop : 24;\n  const marginBottom = settings.marginBottom ? settings.marginBottom : 40;\n  const interactive = settings.interactive !== undefined ? settings.interactive : true;\n  const borderWidth = settings.borderWidth ? settings.borderWidth : 0;\n  const legend = settings.legend !== undefined ? settings.legend : true;\n  const colorScheme = settings.colors ? settings.colors : 'nivo';\n  const projectionScale = settings.projectionScale ? settings.projectionScale : 100;\n  const projectionTranslationX = settings.projectionTranslationX ? settings.projectionTranslationX : 0.5;\n  const projectionTranslationY = settings.projectionTranslationY ? settings.projectionTranslationY : 0.5;\n  const labelProperty = settings.labelProperty ? settings.labelProperty : 'properties.name';\n  const matchAccessor = settings.matchAccessor ? `properties.${settings.matchAccessor}` : 'properties.iso_a3';\n\n  let data = records.reduce((data: Record<string, any>, row: Record<string, any>) => {\n    try {\n      const index = recordToNative(row.get(selection.index));\n      const value = recordToNative(row.get(selection.value));\n\n      if (!index || isNaN(value)) {\n        return data;\n        // throw \"Invalid selection for choropleth chart. Ensure a three letter country code is retrieved together with a value.\"\n      }\n      data.push({ [matchAccessor]: index, value: value });\n      return data;\n    } catch (e) {\n      // eslint-disable-next-line no-console\n      console.error(e);\n      return [];\n    }\n  }, []);\n  let m = Math.max(...data.map((o) => o.value));\n\n  if (!data || data.length == 0) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  return (\n    <>\n      <div style={{ position: 'relative', overflow: 'hidden', width: '100%', height: '100%' }}>\n        <ResponsiveChoropleth\n          data={data}\n          isInteractive={interactive}\n          features={feature.features}\n          domain={[0, m]}\n          margin={{ top: marginTop, right: marginRight, bottom: marginBottom, left: marginLeft }}\n          colors={colorScheme}\n          unknownColor='#666666'\n          label={labelProperty}\n          valueFormat='.2s'\n          match={matchAccessor}\n          projectionScale={projectionScale}\n          projectionTranslation={[projectionTranslationX, projectionTranslationY]}\n          projectionRotation={[0, 0, 0]}\n          enableGraticule={true}\n          graticuleLineColor='#dddddd'\n          borderWidth={borderWidth}\n          borderColor='#152538'\n          legends={\n            legend\n              ? [\n                  {\n                    anchor: 'bottom-left',\n                    direction: 'column',\n                    justify: true,\n                    translateX: 20,\n                    translateY: -100,\n                    itemsSpacing: 0,\n                    itemWidth: 94,\n                    itemHeight: 18,\n                    itemDirection: 'left-to-right',\n                    itemOpacity: 0.85,\n                    symbolSize: 18,\n                    effects: [\n                      {\n                        on: 'hover',\n                        style: {\n                          itemTextColor: '#000000',\n                          itemOpacity: 1,\n                        },\n                      },\n                    ],\n                  },\n                ]\n              : []\n          }\n        />\n      </div>\n    </>\n  );\n};\n\nexport default NeoChoroplethMapChart;\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/circlepacking/CirclePackingChart.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { ResponsiveCirclePacking } from '@nivo/circle-packing';\nimport { useState } from 'react';\nimport { ChartProps } from '../../../../chart/Chart';\nimport { NoDrawableDataErrorMessage } from '../../../../component/editor/CodeViewerComponent';\nimport { mutateName, processHierarchyFromRecords, findObject, flatten } from '../../../../chart/ChartUtils';\nimport { themeNivo } from '../../../../chart/Utils';\nimport RefreshButton from '../../component/RefreshButton';\n\n/**\n * Embeds a CirclePackaging (from Charts) into NeoDash.\n */\nconst NeoCirclePackingChart = (props: ChartProps) => {\n  if (props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <>No data, re-run the report.</>;\n  }\n  const { records } = props;\n  const { selection } = props;\n  const [data, setData] = useState(undefined);\n  const [commonProperties, setCommonProperties] = useState({ data: { name: 'Total', children: [] } });\n  const [refreshable, setRefreshable] = useState(false);\n\n  if (!selection || props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  useEffect(() => {\n    let dataPre = processHierarchyFromRecords(records, selection);\n    dataPre.forEach((currentNode) => mutateName(currentNode));\n    setCommonProperties({ data: dataPre.length == 1 ? dataPre[0] : { name: 'Total', children: dataPre } });\n  }, [records]);\n\n  useEffect(() => {\n    setData(commonProperties.data);\n  }, [props.selection, commonProperties]);\n\n  // Where a user give us the hierarchy with a common root, in that case we can push the entire tree.\n  // Where a user give us just the tree starting one hop away from the root.\n  // as Nivo needs a common root, so in that case, we create it for them.\n  if (data == undefined) {\n    setData(commonProperties.data);\n  }\n\n  const settings = props.settings ? props.settings : {};\n  const legendHeight = 20;\n  const marginRight = settings.marginRight ? settings.marginRight : 24;\n  const marginLeft = settings.marginLeft ? settings.marginLeft : 24;\n  const marginTop = settings.marginTop ? settings.marginTop : 24;\n  const marginBottom = settings.marginBottom ? settings.marginBottom : 40;\n  const interactive = settings.interactive ? settings.interactive : true;\n  const borderWidth = settings.borderWidth ? settings.borderWidth : 0;\n  const legend = settings.legend ? settings.legend : false;\n  const colorScheme = settings.colors ? settings.colors : 'nivo';\n  const showLabels = settings.showLabels !== undefined ? settings.showLabels : true;\n\n  /**\n   * Helper function to decide whether to show labels for a specific node in the hierarchy,\n   * @param n - the entity\n   * @returns a boolean\n   */\n  const showLabelsForNode = (n) => {\n    return n.node.height == 0;\n  };\n\n  if (!data || !data.children || data.children.length == 0) {\n    return <NoDrawableDataErrorMessage />;\n  }\n  return (\n    <>\n      <div style={{ position: 'relative', overflow: 'hidden', width: '100%', height: '100%' }}>\n        {refreshable ? (\n          <RefreshButton\n            onClick={() => {\n              setData(commonProperties.data);\n              setRefreshable(false);\n            }}\n          ></RefreshButton>\n        ) : (\n          <div></div>\n        )}\n        <ResponsiveCirclePacking\n          {...commonProperties}\n          theme={themeNivo}\n          id='name'\n          value='loc'\n          data={data}\n          onClick={(clickedData) => {\n            const foundObject = findObject(flatten(data.children), clickedData.id);\n            if (foundObject && foundObject.children) {\n              setData(foundObject);\n              setRefreshable(true);\n            }\n          }}\n          isInteractive={interactive}\n          borderWidth={borderWidth}\n          margin={{\n            top: marginTop,\n            right: marginRight,\n            bottom: legend ? legendHeight + marginBottom : marginBottom,\n            left: marginLeft,\n          }}\n          animate={true}\n          enableLabels={showLabels}\n          labelsFilter={showLabelsForNode}\n          colors={{ scheme: colorScheme }}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default NeoCirclePackingChart;\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/gantt/GanttChart.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { ChartProps } from '../../../../chart/Chart';\nimport { NoDrawableDataErrorMessage } from '../../../../component/editor/CodeViewerComponent';\nimport { CARD_HEADER_HEIGHT } from '../../../../config/CardConfig';\nimport { extractNodePropertiesFromRecords } from '../../../../report/ReportRecordProcessing';\nimport { extensionEnabled } from '../../../../utils/ReportUtils';\nimport {\n  createDependenciesDirectionsMap,\n  createDependenciesMap,\n  createTasksList,\n  generateVisualizationDataGraph,\n} from './Utils';\nimport ReactGantt from './frappe/GanttVisualization';\nimport { createUUID } from '../../../../utils/uuid';\nimport NeoGraphChartInspectModal from '../../../../chart/graph/component/GraphChartInspectModal';\nimport { executeActionRule, getRuleWithFieldPropertyName } from '../../Utils';\nimport { useStyleRules } from '../../../styling/StyleRuleEvaluator';\n\n/**\n * A Gantt Chart plots activities (nodes) with dependencies (relationships) on a timeline.\n * A start date and end date property on the nodes is required.\n */\nconst NeoGanttChart = (props: ChartProps) => {\n  const { records } = props;\n  const { selection } = props;\n  const settings = props.settings ? props.settings : {};\n\n  /**\n   * This is where we store an in-memory graph from the Neo4j results.\n   * We are essentially reconstructing the graph from the set of links, nodes, and paths returned in Cypher.\n   */\n  const [data, setData] = useState({ nodes: [] as any[], links: [] as any[] });\n  const [tasks, setTasks] = useState([]);\n  const [key, setKey] = useState(createUUID());\n  const [selectedTask, setSelectedTask] = useState(undefined);\n  //\n  const startDateProperty = settings.startDateProperty ? settings.startDateProperty : 'startDate';\n  const endDateProperty = settings.endDateProperty ? settings.endDateProperty : 'endDate';\n  const nameProperty = settings.nameProperty ? settings.nameProperty : 'name';\n  const orderProperty = settings.orderProperty ? settings.orderProperty : startDateProperty;\n  const viewModeSetting = settings.viewMode ? settings.viewMode : 'auto';\n  const dependencyTypeProperty = settings.dependencyTypeProperty ? settings.dependencyTypeProperty : 'rel_type';\n  const barColor = settings.barColor ? settings.barColor : '#a3a3ff';\n\n  let nodeLabels = {};\n  let linkTypes = {};\n\n  // Get the set of report actions defined for the report.\n  const actionsRules =\n    extensionEnabled(props.extensions, 'actions') && props.settings && props.settings.actionsRules\n      ? props.settings.actionsRules\n      : [];\n\n  const styleRules = useStyleRules(\n    extensionEnabled(props.extensions, 'styling'),\n    props.settings.styleRules,\n    props.getGlobalParameter\n  );\n\n  // When data is refreshed, rebuild the data graph used for visualization.\n  useEffect(() => {\n    const settings = {\n      styleRules: styleRules,\n      defaultNodeColor: barColor,\n    };\n    const newData = generateVisualizationDataGraph(props.records, nodeLabels, linkTypes, [], props.fields, settings);\n    setData(newData);\n    const newFields = extractNodePropertiesFromRecords(records);\n    props.setFields && props.setFields(newFields);\n\n    // Build visualization-specific objects.\n    const dependencies = createDependenciesMap(newData.links);\n    const dependencyDirections = createDependenciesDirectionsMap(newData.links, dependencyTypeProperty);\n    let tasks = createTasksList(\n      newData.nodes,\n      dependencies,\n      dependencyDirections,\n      startDateProperty,\n      endDateProperty,\n      nameProperty\n    );\n\n    // Sort tasks by the user's specified property.\n    tasks = tasks.sort((a, b) => {\n      if (a.properties[orderProperty] > b.properties[orderProperty]) {\n        return 1;\n      }\n      if (a.properties[orderProperty] < b.properties[orderProperty]) {\n        return -1;\n      }\n      return 0;\n    });\n    setTasks(tasks);\n  }, [props.records]);\n\n  // If no data is present, return an error message.\n  if (!selection || props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  // If no tasks can be parsed, also return an error message.\n  if (!tasks || tasks.length == 0) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  // Find the earliest task in the view.\n  let minDate = tasks\n    .map((t) => t.end)\n    .reduce((a, b) => {\n      return a < b ? a : b;\n    });\n\n  // Find the latest task in the view.\n  let maxDate = tasks\n    .map((t) => t.end)\n    .reduce((a, b) => {\n      return a > b ? a : b;\n    });\n\n  // Determine view mode based on the range between the minimum and maximum dates.\n  let dateDiff = (maxDate - minDate) / (1000 * 60 * 60 * 24);\n  let viewMode = viewModeSetting; // default\n\n  if (viewMode == 'auto') {\n    switch (true) {\n      case dateDiff < 7:\n        viewMode = 'Quarter Day';\n        break;\n      case dateDiff < 14:\n        viewMode = 'Half Day';\n        break;\n      case dateDiff < 30:\n        viewMode = 'Day';\n        break;\n      case dateDiff < 120:\n        viewMode = 'Week';\n        break;\n      case dateDiff < 1.8 * 365:\n        viewMode = 'Month';\n        break;\n      case dateDiff < 3 * 365:\n        viewMode = 'Quarter';\n        break;\n      default:\n        viewMode = 'Year';\n        break;\n    }\n  }\n\n  return (\n    <div>\n      <NeoGraphChartInspectModal\n        style={{ theme: props.theme }}\n        interactivity={{\n          selectedEntity: selectedTask,\n          showPropertyInspector: selectedTask !== undefined,\n          setPropertyInspectorOpen: function update(_) {\n            setSelectedTask(undefined);\n          },\n        }}\n      />\n      <div\n        className='gantt-wrapper'\n        key={key}\n        id={key}\n        style={{ height: props.dimensions?.height - CARD_HEADER_HEIGHT + 7, overflow: 'scroll' }}\n      >\n        <ReactGantt\n          tasks={tasks}\n          height={props.dimensions?.height}\n          viewMode={viewMode}\n          minBarHeight={15}\n          maxBarHeight={100}\n          onBarRightClick={(e) => {\n            setSelectedTask(e);\n          }}\n          onBarSelect={(e) => {\n            if (actionsRules) {\n              let rules = getRuleWithFieldPropertyName(e, actionsRules, 'onTaskClick', 'labels');\n              if (rules) {\n                rules.forEach((rule) => executeActionRule(rule, e, { ...props }));\n              } else {\n                setSelectedTask(e);\n              }\n            } else {\n              setSelectedTask(e);\n            }\n          }}\n        />\n      </div>\n    </div>\n  );\n};\n\nexport default NeoGanttChart;\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/gantt/Utils.ts",
    "content": "import { toNumber } from '../../../../chart/ChartUtils';\nimport { buildGraphVisualizationObjectFromRecords } from '../../../../chart/graph/util/RecordUtils';\nimport date_utils from './frappe/lib/date_utils';\n\n// Helper function to extract Neo4j types (nodes and relationships) from a records object.\nexport const generateVisualizationDataGraph = (records, nodeLabels, linkTypes, colorScheme, fields, settings) => {\n  let nodes: Record<string, any>[] = [];\n  let links: Record<string, any>[] = [];\n  const extractedGraphFromRecords = buildGraphVisualizationObjectFromRecords(\n    records,\n    nodes,\n    links,\n    nodeLabels,\n    linkTypes,\n    colorScheme,\n    fields,\n    settings.nodeColorProp,\n    settings.defaultNodeColor,\n    settings.nodeSizeProp,\n    settings.defaultNodeSize,\n    settings.relWidthProp,\n    settings.defaultRelWidth,\n    settings.relColorProp,\n    settings.defaultRelColor,\n    settings.styleRules\n  );\n  return extractedGraphFromRecords;\n};\n\n// Helper function to extract a dependency map from the parsed relationships.\nexport function createDependenciesMap(links) {\n  const dependencies = {};\n  links.forEach((l) => {\n    if (!dependencies[`${l.target}`]) {\n      dependencies[`${l.target}`] = [];\n    }\n    dependencies[`${l.target}`].push(`${l.source}`);\n  });\n  return dependencies;\n}\n\n// Helper function to extract a dependency map from the parsed relationships.\nexport function createDependenciesDirectionsMap(links, direction_property) {\n  const directions = {};\n  links.forEach((l) => {\n    if (!directions[`${l.target}`]) {\n      directions[`${l.target}`] = [];\n    }\n    directions[`${l.target}`].push(`${l.properties[direction_property]}`);\n  });\n  return directions;\n}\n\n// Helper function to extract a list of task objects from the parsed nodes.\nexport function createTasksList(\n  nodes,\n  dependencies,\n  dependencyDirections,\n  startDateProperty,\n  endDateProperty,\n  nameProperty\n) {\n  return nodes\n    .map((n) => {\n      let neoStartDate = n.properties[startDateProperty];\n      let neoEndDate = n.properties[endDateProperty];\n      const name = n.properties[nameProperty];\n\n      // Two cases:\n      // 1. The date returned is a valid neo4j date object. We do nothing.\n      // 2. The date returned is not a valid Neo4j date but a string representing one. We try to parse it as one, and set the date accordingly.\n      // Fallback - we skip the current entry altogether.\n      if (\n        !neoStartDate?.year ||\n        !neoStartDate?.month ||\n        !neoStartDate?.day ||\n        !neoEndDate?.year ||\n        !neoEndDate?.month ||\n        !neoEndDate?.day\n      ) {\n        // Not a valid Neo4j date, try to parse it as one...\n        const parsedStartDate = date_utils.parse(neoStartDate);\n        if (parsedStartDate) {\n          neoStartDate = {};\n          neoStartDate.year = parsedStartDate.getFullYear();\n          neoStartDate.month = parsedStartDate.getMonth();\n          neoStartDate.day = parsedStartDate.getDay();\n        }\n        const parsedEndDate = date_utils.parse(neoEndDate);\n        if (parsedEndDate) {\n          neoEndDate = {};\n          neoEndDate.year = parsedEndDate.getFullYear();\n          neoEndDate.month = parsedEndDate.getMonth();\n          neoEndDate.day = parsedEndDate.getDay();\n        }\n        if (!parsedEndDate) {\n          // Fallback scenario, parsing has failed\n          return undefined;\n        }\n      }\n      let res = {\n        start: new Date(toNumber(neoStartDate.year), toNumber(neoStartDate.month), toNumber(neoStartDate.day)),\n        end: new Date(toNumber(neoEndDate.year), toNumber(neoEndDate.month), toNumber(neoEndDate.day)),\n        name: name || '(undefined)',\n        labels: n.labels,\n        dependencies: dependencies[n.id],\n        dependencyDirections: dependencyDirections[n.id],\n        id: `${n.id}`,\n        properties: n.properties,\n        type: 'task',\n        color: n.color,\n        progress: 100,\n        isDisabled: true,\n        styles: { progressColor: '#ffbb54', progressSelectedColor: '#ff9e0d' },\n      };\n      return res;\n    })\n    .filter((i) => i !== undefined);\n}\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/gantt/frappe/GanttVisualization.tsx",
    "content": "/**\n * Code transpiled and extended from https://github.com/hustcc/gantt-for-react\n * (MIT License)\n */\nimport React, { Component } from 'react';\nimport Gantt from './lib';\nimport { createUUID } from '../../../../../utils/uuid';\n\ntype GantRef = SVGSVGElement | undefined;\n\nconst TASK_PADDING = 9;\nconst HEADER_HEIGHT = 50;\nconst STEP_SIZE = 8;\nconst COLUMN_WIDTH = 30;\nconst VIEW_MODE = 'Day';\n\n/**\n * React wrapper for the modified Frappe Gannt library.\n */\nexport default class ReactGantt extends Component {\n  props: any;\n\n  ganttRef: GantRef = undefined;\n\n  key: any;\n\n  ganttInstance: any;\n\n  /**\n   * Maps the NeoDash configuration into the configuration dictionary expected by the Gantt chart library.\n   * @returns Frappe Gantt chart configuration dictionary.\n   */\n  getOptions() {\n    const barHeight = (this.props.height - 10 - TASK_PADDING * 2 * this.props.tasks.length) / this.props.tasks.length;\n    return {\n      on_click: this.props.onBarSelect,\n      on_right_click: this.props.onBarRightClick,\n      on_date_change: this.props.onDateChange,\n      on_progress_change: this.props.onProgressChange,\n      on_view_change: this.props.onViewChange,\n      custom_popup_html: this.props.customPopupHtml || null,\n      header_height: HEADER_HEIGHT,\n      // column_width: 30,\n      step: STEP_SIZE,\n      draggable: false,\n      // view_modes: [...Object.values(VIEW_MODE)],\n      bar_height: Math.min(Math.max(this.props.minBarHeight, barHeight), this.props.maxBarHeight),\n      // bar_corner_radius: 3,\n      // arrow_curve: 5,\n      padding: TASK_PADDING,\n      view_mode: this.props.viewMode,\n      // date_format: 'YYYY-MM-DD',\n      // popup_trigger: 'click',\n      // custom_popup_html: null,\n      // language: 'en',\n    };\n  }\n\n  /**\n   * Instantiate the Gantt chart when the React component mounts.\n   */\n  componentDidMount() {\n    if (this.ganttInstance) {\n      this.key = createUUID();\n      return this.ganttInstance;\n    }\n\n    this.ganttInstance = new Gantt(this.ganttRef, this.props.tasks, this.getOptions());\n    this.ganttInstance.change_view_mode(this.props.viewMode);\n    return this.ganttInstance;\n  }\n\n  /**\n   * Update instance variables when the properties of the React component change.\n   */\n  componentDidUpdate(prevProps, _) {\n    if (this.ganttInstance) {\n      if (this.props.tasks !== prevProps.tasks || this.props.height !== prevProps.height) {\n        this.ganttInstance.refresh(this.props.tasks);\n        this.ganttInstance.setup_options(this.getOptions());\n      }\n      if (this.props.viewMode !== prevProps.viewMode) {\n        this.ganttInstance.change_view_mode(this.props.viewMode);\n      }\n    }\n  }\n\n  /**\n   * Clear reference when the component unmounts.\n   */\n  componentWillUnmount() {\n    this.ganttRef = undefined;\n    this.ganttInstance = undefined;\n  }\n\n  // Render the component as an SVG.\n  render() {\n    return (\n      <svg\n        key={this.key}\n        style={{ height: '100%' }}\n        ref={(node) => {\n          this.ganttRef = node;\n        }}\n      />\n    );\n  }\n}\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/gantt/frappe/lib/arrow.ts",
    "content": "/**\n * Code transpiled and extended from https://github.com/frappe/gantt/.\n * (MIT License)\n */\n\nimport { createSVG } from './svg_utils';\n\nexport const DependencyDirection = {\n  SS: 0,\n  SF: 1,\n  FS: 2,\n  FF: 3,\n};\n\nconst reverse_arrow_path = `\nh 5\nm 0 -5\nl -5 5\nl 5 5\nm -5 -5\n`;\n\nconst arrow_path = `\nm -5 -5\nl 5 5\nl -5 5\nm 5 -5\n`;\n\nexport default class Arrow {\n  gantt: any;\n\n  from_task: any;\n\n  to_task: any;\n\n  path: string | undefined;\n\n  element: any;\n\n  direction = DependencyDirection.SF;\n\n  constructor(gantt, from_task, to_task, direction) {\n    this.gantt = gantt;\n    this.from_task = from_task;\n    this.to_task = to_task;\n    this.direction = direction || DependencyDirection.FS;\n    this.calculate_path(this.direction);\n    this.draw();\n  }\n\n  calculate_path(direction) {\n    // Start in the horizontal center of the 'from' bar...\n    // let start_x = this.from_task.$bar.getX() + this.from_task.$bar.getWidth() / 2;\n    const start_x = this.from_task.$bar.getX();\n    const start_y =\n      this.gantt.options.header_height +\n      this.gantt.options.bar_height / 2 +\n      (this.gantt.options.padding + this.gantt.options.bar_height) * this.from_task.task._index +\n      this.gantt.options.padding;\n\n    // End X position is a small margin (padding) before the 'to' bar starts.\n    const end_x = this.to_task.$bar.getX();\n    // if (direction === DependencyDirection['EE'] || direction === DependencyDirection['SE']) {\n    //     end_x = end_x + this.to_task.$bar.getWidth();\n    // }\n    // End Y position is exactly in the middle of the 'to' bar.\n    const end_y =\n      this.gantt.options.header_height +\n      this.gantt.options.bar_height / 2 +\n      (this.gantt.options.padding + this.gantt.options.bar_height) * this.to_task.task._index +\n      this.gantt.options.padding;\n\n    const from_is_below_to = this.from_task.task._index > this.to_task.task._index;\n    const from_end_is_before_to_start =\n      this.to_task.$bar.getX() >\n      this.from_task.$bar.getX() + this.from_task.$bar.getWidth() + this.gantt.options.padding;\n    const from_end_is_before_to_end =\n      this.to_task.$bar.getX() + this.to_task.$bar.getWidth() >\n      this.from_task.$bar.getX() + this.from_task.$bar.getWidth() + this.gantt.options.padding;\n    const from_start_is_before_to_start = this.to_task.$bar.getX() > this.from_task.$bar.getX();\n    const from_start_is_before_to_end =\n      this.to_task.$bar.getX() + this.to_task.$bar.getWidth() + this.gantt.options.padding > this.from_task.$bar.getX();\n    const curve = this.gantt.options.arrow_curve;\n    const clockwise = from_is_below_to ? 1 : 0;\n    const counter_clockwise = from_is_below_to ? 0 : 1;\n    const { padding } = this.gantt.options;\n    const curve_y = from_is_below_to ? -curve : curve;\n    const offset = from_is_below_to ? end_y + this.gantt.options.arrow_curve : end_y - this.gantt.options.arrow_curve;\n    const down_1 = (this.gantt.options.padding / 2 - curve) * (from_is_below_to ? 1 : -1);\n    const down_2 = this.to_task.$bar.getY() + this.to_task.$bar.getHeight() / 2 - curve_y;\n    const left = this.to_task.$bar.getX() - this.gantt.options.padding;\n    const bar_height = this.to_task.$bar.getHeight() * (from_is_below_to ? 1 : -1);\n\n    if (direction == DependencyDirection.FS) {\n      if (from_end_is_before_to_start) {\n        this.path = `\n                    M ${start_x + this.from_task.$bar.getWidth()} ${start_y}\n                    h ${padding}\n                    a ${curve} ${curve} 0 0 ${counter_clockwise} ${curve} ${curve_y}\n                    V ${offset}\n                    a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}\n                    L ${end_x} ${end_y}\n                    ${arrow_path}\n                `;\n      } else {\n        this.path = `\n                    M ${start_x + this.from_task.$bar.getWidth()} ${start_y}\n                    h ${padding}\n                    a ${curve} ${curve} 0 0 ${counter_clockwise} ${curve} ${curve_y}\n                    v ${-bar_height / 2.5}\n                    a ${curve} ${curve} 0 0 ${counter_clockwise} -${curve} ${curve_y}\n                    H ${left}\n                    a ${curve} ${curve} 0 0 ${clockwise} -${curve} ${curve_y}\n                    V ${down_2}\n                    a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}\n                    L ${end_x} ${end_y}\n                    ${arrow_path}`;\n      }\n    }\n    if (direction == DependencyDirection.FF) {\n      if (from_end_is_before_to_end) {\n        this.path = `\n                        M ${start_x + this.from_task.$bar.getWidth()} ${start_y}\n                        H ${end_x + this.to_task.$bar.getWidth() + padding}\n                        a ${curve} ${curve} 0 0 ${counter_clockwise} ${curve} ${curve_y}\n                        V ${offset}\n                        a ${curve} ${curve} 0 0 ${counter_clockwise} -${curve} ${curve_y}\n                        L ${end_x + this.to_task.$bar.getWidth()} ${end_y}\n                        ${reverse_arrow_path}\n                    `;\n      } else {\n        this.path = `\n                    M ${start_x + this.from_task.$bar.getWidth()} ${start_y}\n                    h ${padding}\n                    a ${curve} ${curve} 0 0 ${counter_clockwise} ${curve} ${curve_y}\n                    V ${offset}\n                    a ${curve} ${curve} 0 0 ${counter_clockwise} -${curve} ${curve_y}\n                    L ${end_x + this.to_task.$bar.getWidth()} ${end_y}\n                    ${reverse_arrow_path}\n            `;\n      }\n    }\n    if (direction == DependencyDirection.SS) {\n      if (from_start_is_before_to_start) {\n        this.path = `\n                    M ${start_x} ${start_y}\n                    h ${-padding}\n                    a ${curve} ${curve} 0 0 ${clockwise} -${curve} ${curve_y}\n                    V ${offset}\n                    a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}\n                    L ${end_x} ${end_y}\n                    ${arrow_path}\n                `;\n      } else {\n        this.path = `\n                    M ${start_x} ${start_y}\n                    H ${end_x - padding}\n                    a ${curve} ${curve} 0 0 ${clockwise} -${curve} ${curve_y}\n                    V ${offset}\n                    a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}\n                    L ${end_x} ${end_y}\n                    ${arrow_path}\n                `;\n      }\n    }\n    if (direction == DependencyDirection.SF) {\n      if (from_start_is_before_to_end) {\n        this.path = `\n                        M ${start_x} ${start_y}\n                        h ${-padding}\n                        a ${curve} ${curve} 0 0 ${clockwise} -${curve} ${curve_y}\n                        V ${offset + bar_height / 2.5}\n                        a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}\n                        L ${end_x + this.to_task.$bar.getWidth() + padding} ${end_y + bar_height / 2}\n                        a ${curve} ${curve} 0 0 ${counter_clockwise} ${curve} ${curve_y}\n                        v ${-bar_height / 2}\n                        a ${curve} ${curve} 0 0 ${counter_clockwise} -${curve} ${curve_y}\n                        h ${-padding}\n                        ${reverse_arrow_path}\n                    `;\n      } else {\n        this.path = `\n                    M ${start_x} ${start_y}\n                    h ${-padding}\n                    a ${curve} ${curve} 0 0 ${clockwise} -${curve} ${curve_y}\n                    V ${offset}\n                    a ${curve} ${curve} 0 0 ${counter_clockwise} -${curve} ${curve_y}\n                    L ${end_x + this.to_task.$bar.getWidth()} ${end_y}\n                    ${reverse_arrow_path}\n            `;\n      }\n    }\n  }\n\n  draw() {\n    this.element = createSVG('path', {\n      d: this.path,\n      'data-from': this.from_task.task.id,\n      'data-to': this.to_task.task.id,\n    });\n  }\n\n  update() {\n    this.calculate_path(this.direction);\n    this.element.setAttribute('d', this.path);\n  }\n}\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/gantt/frappe/lib/bar.ts",
    "content": "/**\n * Code transpiled and extended from https://github.com/frappe/gantt/.\n * (MIT License)\n */\n\nimport date_utils from './date_utils';\nimport { $, createSVG, animateSVG } from './svg_utils';\n\nexport default class Bar {\n  action_completed: boolean | undefined;\n\n  gantt: any;\n\n  task: any;\n\n  invalid: any;\n\n  height: any;\n\n  x: number | undefined;\n\n  y: number | undefined;\n\n  corner_radius: any;\n\n  duration: number | undefined;\n\n  width: number | undefined;\n\n  progress_width: number | undefined;\n\n  group: any;\n\n  bar_group: any;\n\n  handle_group: any;\n\n  $bar: any;\n\n  $bar_progress: any;\n\n  arrows: any;\n\n  constructor(gantt, task) {\n    this.set_defaults(gantt, task);\n    this.prepare();\n    this.draw();\n    this.bind();\n  }\n\n  set_defaults(gantt, task) {\n    this.action_completed = false;\n    this.gantt = gantt;\n    this.task = task;\n  }\n\n  prepare() {\n    this.prepare_values();\n    this.prepare_helpers();\n  }\n\n  prepare_values() {\n    this.invalid = this.task.invalid;\n    this.height = this.gantt.options.bar_height;\n    this.x = this.compute_x();\n    this.y = this.compute_y();\n    this.corner_radius = this.gantt.options.bar_corner_radius;\n    this.duration = date_utils.diff(this.task._end, this.task._start, 'hour') / this.gantt.options.step;\n    this.width = Math.max(this.gantt.options.min_bar_width, this.gantt.options.column_width * this.duration);\n    this.progress_width = Math.max(\n      this.gantt.options.min_bar_width,\n      this.gantt.options.column_width * this.duration * (this.task.progress / 100) || 0\n    );\n    this.group = createSVG('g', {\n      class: `bar-wrapper ${this.task.custom_class || ''}`,\n      'data-id': this.task.id,\n    });\n    this.bar_group = createSVG('g', {\n      class: 'bar-group',\n      append_to: this.group,\n    });\n    this.handle_group = createSVG('g', {\n      class: 'handle-group',\n      append_to: this.group,\n    });\n  }\n\n  prepare_helpers() {\n    SVGElement.prototype.getX = function getX() {\n      return Number(this.getAttribute('x'));\n    };\n    SVGElement.prototype.getY = function getY() {\n      return Number(this.getAttribute('y'));\n    };\n    SVGElement.prototype.getWidth = function getWidth() {\n      return Number(this.getAttribute('width'));\n    };\n    SVGElement.prototype.getHeight = function getHeight() {\n      return Number(this.getAttribute('height'));\n    };\n    SVGElement.prototype.getEndX = function getEndX() {\n      return this.getX() + this.getWidth();\n    };\n  }\n\n  draw() {\n    this.draw_bar();\n    this.draw_progress_bar();\n    this.draw_label();\n    if (this.gantt.options.draggable) {\n      this.draw_resize_handles();\n    }\n  }\n\n  draw_bar() {\n    this.$bar = createSVG('rect', {\n      x: this.x,\n      y: this.y,\n      width: this.width,\n      height: this.height,\n      rx: this.corner_radius,\n      ry: this.corner_radius,\n      class: 'bar',\n      append_to: this.bar_group,\n    });\n\n    animateSVG(this.$bar, 'width', 0, this.width);\n\n    if (this.invalid) {\n      this.$bar.classList.add('bar-invalid');\n    }\n  }\n\n  draw_progress_bar() {\n    if (this.invalid) {\n      return;\n    }\n    this.$bar_progress = createSVG('rect', {\n      x: this.x,\n      y: this.y,\n      width: this.progress_width,\n      height: this.height,\n      rx: this.corner_radius,\n      ry: this.corner_radius,\n      // class: 'bar-progress',\n      fill: this.task.color,\n      append_to: this.bar_group,\n    });\n\n    animateSVG(this.$bar_progress, 'width', 0, this.progress_width);\n  }\n\n  draw_label() {\n    createSVG('text', {\n      x: this.x + this.width + 15,\n      y: this.y + this.height / 2,\n      innerHTML: this.task.name,\n      class: 'bar-label',\n      append_to: this.bar_group,\n    });\n    // labels get BBox in the next tick\n    requestAnimationFrame(() => this.update_label_position());\n  }\n\n  draw_resize_handles() {\n    if (this.invalid) {\n      return;\n    }\n\n    const bar = this.$bar;\n    const handle_width = 8;\n\n    createSVG('rect', {\n      x: bar.getX() + bar.getWidth() - 9,\n      y: bar.getY() + 1,\n      width: handle_width,\n      height: this.height - 2,\n      rx: this.corner_radius,\n      ry: this.corner_radius,\n      class: 'handle right',\n      append_to: this.handle_group,\n    });\n\n    createSVG('rect', {\n      x: bar.getX() + 1,\n      y: bar.getY() + 1,\n      width: handle_width,\n      height: this.height - 2,\n      rx: this.corner_radius,\n      ry: this.corner_radius,\n      class: 'handle left',\n      append_to: this.handle_group,\n    });\n\n    if (this.task.progress && this.task.progress < 100) {\n      this.$handle_progress = createSVG('polygon', {\n        points: this.get_progress_polygon_points().join(','),\n        class: 'handle progress',\n        append_to: this.handle_group,\n      });\n    }\n  }\n\n  get_progress_polygon_points() {\n    const bar_progress = this.$bar_progress;\n    return [\n      bar_progress.getEndX() - 5,\n      bar_progress.getY() + bar_progress.getHeight(),\n      bar_progress.getEndX() + 5,\n      bar_progress.getY() + bar_progress.getHeight(),\n      bar_progress.getEndX(),\n      bar_progress.getY() + bar_progress.getHeight() - 8.66,\n    ];\n  }\n\n  bind() {\n    if (this.invalid) {\n      return;\n    }\n    this.setup_click_event();\n  }\n\n  setup_click_event() {\n    $.on(this.group, `focus ${this.gantt.options.popup_trigger}`, (_) => {\n      if (this.action_completed) {\n        // just finished a move action, wait for a few seconds\n        return;\n      }\n      this.gantt.trigger_event('click', [this.task]);\n      this.show_popup();\n      this.gantt.unselect_all();\n      this.group.classList.add('active');\n    });\n\n    $.on(this.group, 'auxclick', (e) => {\n      e.preventDefault();\n      this.gantt.trigger_event('right_click', [this.task]);\n    });\n\n    $.on(this.group, 'contextmenu', (e) => {\n      e.preventDefault();\n      this.gantt.trigger_event('right_click', [this.task]);\n    });\n\n    $.on(this.group, 'dblclick', (_) => {\n      if (this.action_completed) {\n        // just finished a move action, wait for a few seconds\n        return;\n      }\n\n      this.gantt.trigger_event('click', [this.task]);\n    });\n  }\n\n  show_popup() {\n    if (this.gantt.bar_being_dragged) {\n      return;\n    }\n\n    const start_date = date_utils.format(this.task._start, 'D MMM YYYY', this.gantt.options.language);\n    const end_date = date_utils.format(\n      date_utils.add(this.task._end, -1, 'second'),\n      'D MMM YYYY',\n      this.gantt.options.language\n    );\n    const subtitle = `${start_date} - ${end_date}`;\n\n    this.gantt.show_popup({\n      target_element: this.$bar,\n      title: this.task.name,\n      subtitle: subtitle,\n      task: this.task,\n    });\n  }\n\n  update_bar_position({ x = null, width = null }) {\n    const bar = this.$bar;\n    if (x) {\n      // get all x values of parent task\n      // const xs = this.task.dependencies.map((dep) => {\n      //     return this.gantt.get_bar(dep).$bar.getX();\n      // });\n      // child task must not go before parent\n      // const valid_x = xs.reduce((prev, curr) => {\n      //     return x >= curr;\n      // }, x);\n      // if (!valid_x) {\n      //     width = null;\n      //     return;\n      // }\n      this.update_attr(bar, 'x', x);\n    }\n    if (width && width >= this.gantt.options.column_width) {\n      this.update_attr(bar, 'width', width);\n    }\n    this.update_label_position();\n    this.update_handle_position();\n    this.update_progressbar_position();\n    this.update_arrow_position();\n  }\n\n  date_changed() {\n    let changed = false;\n    const { new_start_date, new_end_date } = this.compute_start_end_date();\n\n    if (Number(this.task._start) !== Number(new_start_date)) {\n      changed = true;\n      this.task._start = new_start_date;\n    }\n\n    if (Number(this.task._end) !== Number(new_end_date)) {\n      changed = true;\n      this.task._end = new_end_date;\n    }\n\n    if (!changed) {\n      return;\n    }\n\n    this.gantt.trigger_event('date_change', [this.task, new_start_date, date_utils.add(new_end_date, -1, 'second')]);\n  }\n\n  progress_changed() {\n    const new_progress = this.compute_progress();\n    this.task.progress = new_progress;\n    this.gantt.trigger_event('progress_change', [this.task, new_progress]);\n  }\n\n  set_action_completed() {\n    this.action_completed = true;\n    setTimeout(() => (this.action_completed = false), 1000);\n  }\n\n  compute_start_end_date() {\n    const bar = this.$bar;\n    const x_in_units = bar.getX() / this.gantt.options.column_width;\n    const new_start_date = date_utils.add(this.gantt.gantt_start, x_in_units * this.gantt.options.step, 'hour');\n    const width_in_units = bar.getWidth() / this.gantt.options.column_width;\n    const new_end_date = date_utils.add(new_start_date, width_in_units * this.gantt.options.step, 'hour');\n\n    return { new_start_date, new_end_date };\n  }\n\n  compute_progress() {\n    const progress = (this.$bar_progress.getWidth() / this.$bar.getWidth()) * 100;\n    return parseInt(progress, 10);\n  }\n\n  compute_x() {\n    const { step, column_width } = this.gantt.options;\n    const task_start = this.task._start;\n    const { gantt_start } = this.gantt;\n\n    const diff = date_utils.diff(task_start, gantt_start, 'hour');\n    let x = (diff / step) * column_width;\n\n    if (this.gantt.view_is('Month')) {\n      const diff = date_utils.diff(task_start, gantt_start, 'day');\n      x = (diff * column_width) / 30;\n    }\n    return x;\n  }\n\n  compute_y() {\n    return (\n      this.gantt.options.header_height +\n      this.gantt.options.padding +\n      this.task._index * (this.height + this.gantt.options.padding)\n    );\n  }\n\n  get_snap_position(dx) {\n    let odx = dx;\n    let rem;\n    let position;\n\n    if (this.gantt.view_is('Week')) {\n      rem = dx % (this.gantt.options.column_width / 7);\n      position = odx - rem + (rem < this.gantt.options.column_width / 14 ? 0 : this.gantt.options.column_width / 7);\n    } else if (this.gantt.view_is('Month')) {\n      rem = dx % (this.gantt.options.column_width / 30);\n      position = odx - rem + (rem < this.gantt.options.column_width / 60 ? 0 : this.gantt.options.column_width / 30);\n    } else {\n      rem = dx % this.gantt.options.column_width;\n      position = odx - rem + (rem < this.gantt.options.column_width / 2 ? 0 : this.gantt.options.column_width);\n    }\n    return position;\n  }\n\n  update_attr(element, attr, value) {\n    value = Number(value);\n    if (!isNaN(value)) {\n      element.setAttribute(attr, value);\n    }\n    return element;\n  }\n\n  update_progressbar_position() {\n    if (this.invalid) {\n      return;\n    }\n    this.$bar_progress.setAttribute('x', this.$bar.getX());\n    this.$bar_progress.setAttribute('width', this.$bar.getWidth() * (this.task.progress / 100));\n  }\n\n  update_label_position() {\n    const bar = this.$bar;\n    const label = this.group.querySelector('.bar-label');\n\n    if (label.getBBox().width > bar.getWidth()) {\n      label.classList.add('big');\n      label.setAttribute('x', bar.getX() + bar.getWidth() + 15);\n    } else {\n      label.classList.remove('big');\n      label.setAttribute('x', bar.getX() + bar.getWidth() + 15);\n    }\n  }\n\n  update_handle_position() {\n    if (this.invalid) {\n      return;\n    }\n    const bar = this.$bar;\n    this.handle_group.querySelector('.handle.left').setAttribute('x', bar.getX() + 1);\n    this.handle_group.querySelector('.handle.right').setAttribute('x', bar.getEndX() - 9);\n    const handle = this.group.querySelector('.handle.progress');\n    handle && handle.setAttribute('points', this.get_progress_polygon_points());\n  }\n\n  update_arrow_position() {\n    this.arrows = this.arrows || [];\n    for (let arrow of this.arrows) {\n      arrow.update();\n    }\n  }\n}\n\nfunction isFunction(functionToCheck) {\n  let getType = {};\n  return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';\n}\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/gantt/frappe/lib/date_utils.js",
    "content": "/**\n * Code transpiled and extended from https://github.com/frappe/gantt/.\n * (MIT License)\n */\n\nconst YEAR = 'year';\nconst MONTH = 'month';\nconst DAY = 'day';\nconst HOUR = 'hour';\nconst MINUTE = 'minute';\nconst SECOND = 'second';\nconst MILLISECOND = 'millisecond';\n\nexport default {\n  /**\n   * Parse an object (formatted string or date) to a JS date.\n   */\n  parse(date, date_separator = '-', time_separator = /[.:]/) {\n    if (date instanceof Date) {\n      return date;\n    }\n    if (typeof date === 'string') {\n      let date_parts, time_parts;\n      const parts = date.split(' ');\n\n      date_parts = parts[0].split(date_separator).map((val) => parseInt(val, 10));\n      time_parts = parts[1] && parts[1].split(time_separator);\n\n      // month is 0 indexed\n      date_parts[1] = date_parts[1] - 1;\n\n      let vals = date_parts;\n\n      if (time_parts && time_parts.length) {\n        if (time_parts.length == 4) {\n          time_parts[3] = '0.' + time_parts[3];\n          time_parts[3] = parseFloat(time_parts[3]) * 1000;\n        }\n        vals = vals.concat(time_parts);\n      }\n\n      return new Date(...vals);\n    }\n  },\n\n  /**\n   * Formats a date object into a string representation.\n   */\n  to_string(date, with_time = false) {\n    if (!(date instanceof Date)) {\n      throw new TypeError('Invalid argument type');\n    }\n    const vals = this.get_date_values(date).map((val, i) => {\n      if (i === 1) {\n        // add 1 for month\n        val = val + 1;\n      }\n\n      if (i === 6) {\n        return padStart(val + '', 3, '0');\n      }\n\n      return padStart(val + '', 2, '0');\n    });\n    const date_string = `${vals[0]}-${vals[1]}-${vals[2]}`;\n    const time_string = `${vals[3]}:${vals[4]}:${vals[5]}.${vals[6]}`;\n\n    return date_string + (with_time ? ' ' + time_string : '');\n  },\n\n  /**\n   * Format a date to a complex string format.\n   */\n  format(date, format_string = 'YYYY-MM-DD HH:mm:ss.SSS', lang = 'en') {\n    const dateTimeFormat = new Intl.DateTimeFormat(lang, {\n      month: 'long',\n    });\n    const month_name = dateTimeFormat.format(date);\n    const month_name_capitalized = month_name.charAt(0).toUpperCase() + month_name.slice(1);\n\n    const values = this.get_date_values(date).map((d) => padStart(d, 2, 0));\n    const format_map = {\n      YYYY: values[0],\n      MM: padStart(+values[1] + 1, 2, 0),\n      DD: values[2],\n      HH: values[3],\n      mm: values[4],\n      ss: values[5],\n      SSS: values[6],\n      D: values[2],\n      MMMM: month_name_capitalized,\n      MMM: month_name_capitalized,\n    };\n\n    let str = format_string;\n    const formatted_values = [];\n\n    Object.keys(format_map)\n      .sort((a, b) => b.length - a.length) // big string first\n      .forEach((key) => {\n        if (str.includes(key)) {\n          str = str.replace(key, `$${formatted_values.length}`);\n          formatted_values.push(format_map[key]);\n        }\n      });\n\n    formatted_values.forEach((value, i) => {\n      str = str.replace(`$${i}`, value);\n    });\n\n    return str;\n  },\n\n  diff(date_a, date_b, scale = DAY) {\n    let milliseconds, seconds, hours, minutes, days, months, years;\n\n    milliseconds = date_a - date_b;\n    seconds = milliseconds / 1000;\n    minutes = seconds / 60;\n    hours = minutes / 60;\n    days = hours / 24;\n    months = days / 30;\n    years = months / 12;\n\n    if (!scale.endsWith('s')) {\n      scale += 's';\n    }\n\n    return Math.floor(\n      {\n        milliseconds,\n        seconds,\n        minutes,\n        hours,\n        days,\n        months,\n        years,\n      }[scale]\n    );\n  },\n\n  today() {\n    const vals = this.get_date_values(new Date()).slice(0, 3);\n    return new Date(...vals);\n  },\n\n  now() {\n    return new Date();\n  },\n\n  add(date, qty, scale) {\n    qty = parseInt(qty, 10);\n    const vals = [\n      date.getFullYear() + (scale === YEAR ? qty : 0),\n      date.getMonth() + (scale === MONTH ? qty : 0),\n      date.getDate() + (scale === DAY ? qty : 0),\n      date.getHours() + (scale === HOUR ? qty : 0),\n      date.getMinutes() + (scale === MINUTE ? qty : 0),\n      date.getSeconds() + (scale === SECOND ? qty : 0),\n      date.getMilliseconds() + (scale === MILLISECOND ? qty : 0),\n    ];\n    return new Date(...vals);\n  },\n\n  start_of(date, scale) {\n    const scores = {\n      [YEAR]: 6,\n      [MONTH]: 5,\n      [DAY]: 4,\n      [HOUR]: 3,\n      [MINUTE]: 2,\n      [SECOND]: 1,\n      [MILLISECOND]: 0,\n    };\n\n    function should_reset(_scale) {\n      const max_score = scores[scale];\n      return scores[_scale] <= max_score;\n    }\n\n    const vals = [\n      date.getFullYear(),\n      should_reset(YEAR) ? 0 : date.getMonth(),\n      should_reset(MONTH) ? 1 : date.getDate(),\n      should_reset(DAY) ? 0 : date.getHours(),\n      should_reset(HOUR) ? 0 : date.getMinutes(),\n      should_reset(MINUTE) ? 0 : date.getSeconds(),\n      should_reset(SECOND) ? 0 : date.getMilliseconds(),\n    ];\n\n    return new Date(...vals);\n  },\n\n  clone(date) {\n    return new Date(...this.get_date_values(date));\n  },\n\n  get_date_values(date) {\n    return [\n      date.getFullYear(),\n      date.getMonth(),\n      date.getDate(),\n      date.getHours(),\n      date.getMinutes(),\n      date.getSeconds(),\n      date.getMilliseconds(),\n    ];\n  },\n\n  get_days_in_month(date) {\n    const no_of_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n\n    const month = date.getMonth();\n\n    if (month !== 1) {\n      return no_of_days[month];\n    }\n\n    // Feb\n    const year = date.getFullYear();\n    if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {\n      return 29;\n    }\n    return 28;\n  },\n};\n\n// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart\nfunction padStart(str, targetLength, padString) {\n  str = str + '';\n  targetLength = targetLength >> 0;\n  padString = String(typeof padString !== 'undefined' ? padString : ' ');\n  if (str.length > targetLength) {\n    return String(str);\n  } else {\n    targetLength = targetLength - str.length;\n    if (targetLength > padString.length) {\n      padString += padString.repeat(targetLength / padString.length);\n    }\n    return padString.slice(0, targetLength) + String(str);\n  }\n}\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/gantt/frappe/lib/gantt.css",
    "content": "/** \n * Code transpiled and extended from https://github.com/frappe/gantt/.\n * (MIT License) \n */\n\n.gantt .grid-background {\n    fill: none;\n}\n\n.gantt .grid-header {\n    fill: #ffffff;\n    stroke: #e0e0e0;\n    stroke-width: 1.4;\n}\n\n.gantt .grid-row {\n    fill: #ffffff;\n}\n\n.gantt .grid-row:nth-child(even) {\n    fill: #f5f5f5;\n}\n\n.gantt .row-line {\n    stroke: #ebeff2;\n}\n\n.gantt .tick {\n    stroke: #e0e0e0;\n    stroke-width: 0.2;\n\n    &.thick {\n        stroke-width: 0.4;\n    }\n}\n\n.gantt .today-highlight {\n    fill: #fcf8e3;\n    opacity: 0.5;\n}\n\n.gantt .arrow {\n    fill: none;\n    stroke: #666;\n    stroke-width: 1.4;\n}\n\n.gantt .bar {\n    fill: #b8c2cc;\n    stroke: #8D99A6;\n    stroke-width: 0;\n    transition: stroke-width .3s ease;\n    user-select: none;\n}\n\n.gantt .bar-progress {\n    fill: #a3a3ff;\n}\n\n.gantt .bar-invalid {\n    fill: transparent;\n    stroke: #8D99A6;\n    stroke-width: 1;\n    stroke-dasharray: 5;\n\n\n}\n\n.gantt .bar-invalid .bar-label {\n    fill: '#555';\n}\n\n.gantt .bar-label {\n    fill: #000;\n    dominant-baseline: central;\n    text-anchor: start;\n    font-size: 12px;\n    font-weight: normal;\n\n}\n\n.gantt .bar-label .big {\n    fill: #555;\n    text-anchor: start;\n}\n\n.gantt .handle {\n    fill: #ddd;\n    cursor: ew-resize;\n    opacity: 0;\n    visibility: hidden;\n    transition: opacity .3s ease;\n}\n\n.gantt .bar-wrapper {\n    cursor: pointer;\n    outline: none;\n}\n\n\n.gantt .bar-wrapper:hover .bar {\n    fill: darken('#b8c2cc ', 5);\n}\n\n.gantt .bar-wrapper:hover .bar-progress {\n    fill: darken('#a3a3ff ', 5);\n}\n\n.gantt .bar-wrapper:hover .handle {\n    visibility: visible;\n    opacity: 1;\n}\n\n\n\n.gantt .bar-wrapper:active .bar {\n    fill: darken('#b8c2cc ', 5);\n}\n\n.gantt .bar-wrapper:active .bar-progress {\n    fill: darken('#a3a3ff ', 5);\n}\n\n\n.gantt .lower-text,\n.gantt .upper-text {\n    font-size: 12px;\n    text-anchor: middle;\n}\n\n.gantt .upper-text {\n    fill: #555;\n}\n\n.gantt .lower-text {\n    fill: #333;\n}\n\n.hide {\n    display: none;\n}\n\n\n.gantt-container {\n    /* position: relative; */\n    /* overflow: auto; */\n    font-size: 12px;\n}\n\n.gantt-container .popup-wrapper {\n    position: relative;\n    width: 200;\n    left: 0;\n    background: rgba(0, 0, 0, 0.8);\n    padding: 0;\n    color: #959da5;\n    border-radius: 3px;\n}\n\n.gantt-container .popup-wrapper .title {\n    border-bottom: 3px solid #a3a3ff;\n    padding: 10px;\n}\n\n.gantt-container .popup-wrapper .subtitle {\n    padding: 10px;\n    color: #dfe2e5;\n}\n\n.gantt-container .popup-wrapper .pointer {\n    position: absolute;\n    height: 5px;\n    margin: 0 0 0 -5px;\n    border: 5px solid transparent;\n    border-top-color: rgba(0, 0, 0, 0.8);\n}"
  },
  {
    "path": "src/extensions/advancedcharts/chart/gantt/frappe/lib/index.ts",
    "content": "/**\n * Code transpiled and extended from https://github.com/frappe/gantt/.\n * (MIT License)\n */\n\nimport date_utils from './date_utils';\nimport { $, createSVG } from './svg_utils';\nimport Bar from './bar';\nimport Arrow, { DependencyDirection } from './arrow';\nimport Popup from './popup';\n\nimport './gantt.css';\n\nconst VIEW_MODE = {\n  QUARTER_DAY: 'Quarter Day',\n  HALF_DAY: 'Half Day',\n  DAY: 'Day',\n  WEEK: 'Week',\n  MONTH: 'Month',\n  QUARTER: 'Quarter',\n  YEAR: 'Year',\n};\n\nexport default class Gantt {\n  $svg: any;\n\n  $container: any;\n\n  popup_wrapper: HTMLDivElement;\n\n  options: any;\n\n  tasks: any;\n\n  dependency_map: object;\n\n  gantt_start: null;\n\n  gantt_end: null;\n\n  dates: never[];\n\n  layers: object;\n\n  /**\n   * List of arrows (dependencies between the tasks)\n   */\n  arrows: Arrow[] = [];\n\n  /**\n   * List of horizontal bars representing a task.\n   */\n  bars: Bar[] = [];\n\n  bar_being_dragged: null;\n\n  popup: Popup;\n\n  static VIEW_MODE: {\n    QUARTER_DAY: string;\n    HALF_DAY: string;\n    DAY: string;\n    WEEK: string;\n    MONTH: string;\n    QUARTER: string;\n    YEAR: string;\n  };\n\n  /**\n   * Sets up the Gantt chart visualization\n   * @param wrapper\n   * @param tasks\n   * @param options configuration options for styling and handling events.\n   */\n  constructor(wrapper, tasks, options) {\n    this.setup_wrapper(wrapper);\n    this.setup_options(options);\n    this.setup_tasks(tasks);\n    // initialize with default view mode\n    this.change_view_mode();\n    this.bind_events();\n  }\n\n  setup_wrapper(element) {\n    let svg_element;\n    let wrapper_element;\n\n    // CSS Selector is passed\n    if (typeof element === 'string') {\n      element = document.querySelector(element);\n    }\n\n    // get the SVGElement\n    if (element instanceof HTMLElement) {\n      wrapper_element = element;\n      svg_element = element.querySelector('svg');\n    } else if (element instanceof SVGElement) {\n      svg_element = element;\n    } else {\n      throw new TypeError(\n        \"Frappé Gantt only supports usage of a string CSS selector. HTML DOM element or SVG DOM element for the 'element' parameter\"\n      );\n    }\n\n    // svg element\n    if (!svg_element) {\n      // create it\n      this.$svg = createSVG('svg', {\n        append_to: wrapper_element,\n        class: 'gantt',\n      });\n    } else {\n      this.$svg = svg_element;\n      this.$svg.classList.add('gantt');\n    }\n\n    // wrapper element\n    this.$container = document.createElement('div');\n    this.$container.classList.add('gantt-container');\n\n    const parent_element = this.$svg.parentElement;\n    parent_element.appendChild(this.$container);\n    this.$container.appendChild(this.$svg);\n\n    // popup wrapper\n    this.popup_wrapper = document.createElement('div');\n    this.popup_wrapper.style.marginTop = '-500px';\n    this.popup_wrapper.classList.add('popup-wrapper');\n    this.$container.appendChild(this.popup_wrapper);\n  }\n\n  setup_options(options) {\n    const default_options = {\n      header_height: 50,\n      column_width: 30,\n      min_bar_width: 10,\n      step: 24,\n      view_modes: [...Object.values(VIEW_MODE)],\n      bar_height: 20,\n      bar_corner_radius: 3,\n      arrow_curve: 5,\n      padding: 9,\n      view_mode: 'Month',\n      date_format: 'YYYY-MM-DD',\n      popup_trigger: 'click',\n      custom_popup_html: null,\n      language: 'en',\n    };\n    this.options = Object.assign({}, default_options, options);\n  }\n\n  setup_tasks(tasks) {\n    // prepare tasks\n    this.tasks = tasks.map((task, i) => {\n      // convert to Date objects\n      task._start = date_utils.parse(task.start);\n      task._end = date_utils.parse(task.end);\n\n      // make task invalid if duration too large\n      if (date_utils.diff(task._end, task._start, 'year') > 10) {\n        task.end = null;\n      }\n\n      // cache index\n      task._index = i;\n\n      // invalid dates\n      if (!task.start && !task.end) {\n        const today = date_utils.today();\n        task._start = today;\n        task._end = date_utils.add(today, 2, 'day');\n      }\n\n      if (!task.start && task.end) {\n        task._start = date_utils.add(task._end, -2, 'day');\n      }\n\n      if (task.start && !task.end) {\n        task._end = date_utils.add(task._start, 2, 'day');\n      }\n\n      // if hours is not set, assume the last day is full day\n      // e.g: 2018-09-09 becomes 2018-09-09 23:59:59\n      const task_end_values = date_utils.get_date_values(task._end);\n      if (task_end_values.slice(3).every((d) => d === 0)) {\n        task._end = date_utils.add(task._end, 24, 'hour');\n      }\n\n      // invalid flag\n      if (!task.start || !task.end) {\n        task.invalid = true;\n      }\n\n      // dependencies\n      if (typeof task.dependencies === 'string' || !task.dependencies) {\n        let deps = [];\n        if (task.dependencies) {\n          deps = task.dependencies.map((d) => d.trim()).filter((d) => d);\n        }\n        task.dependencies = deps;\n      }\n\n      // uids\n      if (!task.id) {\n        task.id = generate_id(task);\n      }\n\n      return task;\n    });\n\n    this.setup_dependencies();\n  }\n\n  setup_dependencies() {\n    this.dependency_map = {};\n    for (let t of this.tasks) {\n      for (let d of t.dependencies) {\n        this.dependency_map[d] = this.dependency_map[d] || [];\n        this.dependency_map[d].push(t.id);\n      }\n    }\n  }\n\n  refresh(tasks) {\n    this.setup_tasks(tasks);\n    this.change_view_mode();\n    // this.setup_dates();\n    // this.setup_date_values();\n\n    // this.render();\n\n    // fire viewmode_change event\n    // this.trigger_event('view_change', [this.options.view_mode]);\n  }\n\n  change_view_mode(mode = this.options.view_mode) {\n    this.update_view_scale(mode);\n    this.setup_dates();\n    this.render();\n    // fire viewmode_change event\n    this.trigger_event('view_change', [mode]);\n  }\n\n  update_view_scale(view_mode) {\n    this.options.view_mode = view_mode;\n\n    if (view_mode === VIEW_MODE.DAY) {\n      this.options.step = 24;\n      this.options.column_width = 38;\n    } else if (view_mode === VIEW_MODE.HALF_DAY) {\n      this.options.step = 24 / 2;\n      this.options.column_width = 38;\n    } else if (view_mode === VIEW_MODE.QUARTER_DAY) {\n      this.options.step = 24 / 4;\n      this.options.column_width = 38;\n    } else if (view_mode === VIEW_MODE.WEEK) {\n      this.options.step = 24 * 7;\n      this.options.column_width = 140;\n    } else if (view_mode === VIEW_MODE.MONTH) {\n      this.options.step = 24 * 30;\n      this.options.column_width = 120;\n    } else if (view_mode === VIEW_MODE.QUARTER) {\n      this.options.step = 24 * 90;\n      this.options.column_width = 200;\n    } else if (view_mode === VIEW_MODE.YEAR) {\n      this.options.step = 24 * 365;\n      this.options.column_width = 120;\n    }\n  }\n\n  setup_dates() {\n    this.setup_gantt_dates();\n    this.setup_date_values();\n  }\n\n  setup_gantt_dates() {\n    this.gantt_start = null;\n    this.gantt_end = null;\n\n    for (let task of this.tasks) {\n      // set global start and end date\n      if (!this.gantt_start || task._start < this.gantt_start) {\n        this.gantt_start = task._start;\n      }\n      if (!this.gantt_end || task._end > this.gantt_end) {\n        this.gantt_end = task._end;\n      }\n    }\n\n    // this.gantt_start = date_utils.start_of(this.gantt_start, 'day');\n    // this.gantt_end = date_utils.start_of(this.gantt_end, 'day');\n\n    // add date padding on both sides\n    if (this.view_is([VIEW_MODE.QUARTER_DAY, VIEW_MODE.HALF_DAY, VIEW_MODE.DAY])) {\n      this.gantt_start = date_utils.add(this.gantt_start, -2, 'day');\n      this.gantt_end = date_utils.add(this.gantt_end, 2, 'day');\n    } else if (this.view_is([VIEW_MODE.WEEK, VIEW_MODE.MONTH, VIEW_MODE.QUARTER])) {\n      this.gantt_start = date_utils.add(this.gantt_start, -14, 'day');\n      this.gantt_end = date_utils.add(this.gantt_end, 14, 'day');\n    } else if (this.view_is(VIEW_MODE.YEAR)) {\n      this.gantt_start = date_utils.add(this.gantt_start, -90, 'day');\n      this.gantt_end = date_utils.add(this.gantt_end, 90, 'day');\n    } else {\n      this.gantt_start = date_utils.add(this.gantt_start, -1, 'month');\n      this.gantt_end = date_utils.add(this.gantt_end, 1, 'month');\n    }\n  }\n\n  setup_date_values() {\n    this.dates = [];\n    let cur_date = null;\n\n    while (cur_date === null || cur_date < this.gantt_end) {\n      if (!cur_date) {\n        cur_date = date_utils.clone(this.gantt_start);\n      } else if (this.view_is(VIEW_MODE.YEAR)) {\n        cur_date = date_utils.add(cur_date, 1, 'year');\n      } else if (this.view_is(VIEW_MODE.MONTH)) {\n        cur_date = date_utils.add(cur_date, 1, 'month');\n      } else if (this.view_is(VIEW_MODE.QUARTER)) {\n        cur_date = date_utils.add(cur_date, 3, 'month');\n      } else {\n        cur_date = date_utils.add(cur_date, this.options.step, 'hour');\n      }\n      this.dates.push(cur_date);\n    }\n  }\n\n  bind_events() {\n    this.bind_grid_click();\n    this.bind_bar_events();\n  }\n\n  render() {\n    this.clear();\n    this.setup_layers();\n    this.make_grid();\n    this.make_dates();\n    this.make_bars();\n    this.make_arrows();\n    this.map_arrows_on_bars();\n    this.set_width();\n    this.set_scroll_position();\n  }\n\n  setup_layers() {\n    this.layers = {};\n    const layers = ['grid', 'date', 'arrow', 'progress', 'bar', 'details'];\n    // make group layers\n    for (let layer of layers) {\n      this.layers[layer] = createSVG('g', {\n        class: layer,\n        append_to: this.$svg,\n      });\n    }\n  }\n\n  make_grid() {\n    this.make_grid_background();\n    this.make_grid_rows();\n    this.make_grid_header();\n    this.make_grid_ticks();\n    this.make_grid_highlights();\n  }\n\n  make_grid_background() {\n    const grid_width = this.dates.length * this.options.column_width;\n    const grid_height =\n      // this.options.header_height +\n      -this.options.padding + (this.options.bar_height + this.options.padding) * this.tasks.length;\n\n    createSVG('rect', {\n      x: 0,\n      y: 0,\n      width: grid_width,\n      height: grid_height,\n      class: 'grid-background',\n      append_to: this.layers.grid,\n    });\n\n    $.attr(this.$svg, {\n      height: grid_height + this.options.padding + 50,\n      width: '100%',\n    });\n  }\n\n  make_grid_rows() {\n    const rows_layer = createSVG('g', { append_to: this.layers.grid });\n    const lines_layer = createSVG('g', { append_to: this.layers.grid });\n\n    const row_width = this.dates.length * this.options.column_width;\n    const row_height = this.options.bar_height + this.options.padding;\n\n    let row_y = this.options.header_height + this.options.padding / 2;\n\n    for (let task of this.tasks) {\n      createSVG('rect', {\n        x: 0,\n        y: row_y,\n        width: row_width,\n        height: row_height,\n        class: 'grid-row',\n        append_to: rows_layer,\n      });\n\n      createSVG('line', {\n        x1: 0,\n        y1: row_y + row_height,\n        x2: row_width,\n        y2: row_y + row_height,\n        class: 'row-line',\n        append_to: lines_layer,\n      });\n\n      row_y += this.options.bar_height + this.options.padding;\n    }\n  }\n\n  make_grid_header() {\n    const header_width = this.dates.length * this.options.column_width;\n    const header_height = this.options.header_height + 10;\n    createSVG('rect', {\n      x: 0,\n      y: 0,\n      width: header_width,\n      height: header_height,\n      class: 'grid-header',\n      append_to: this.layers.grid,\n    });\n  }\n\n  make_grid_ticks() {\n    let tick_x = 0;\n    let tick_y = this.options.header_height + this.options.padding / 2;\n    let tick_height = (this.options.bar_height + this.options.padding) * this.tasks.length;\n\n    for (let date of this.dates) {\n      let tick_class = 'tick';\n      // thick tick for monday\n      if (this.view_is(VIEW_MODE.DAY) && date.getDate() === 1) {\n        tick_class += ' thick';\n      }\n      // thick tick for first week\n      if (this.view_is(VIEW_MODE.WEEK) && date.getDate() >= 1 && date.getDate() < 8) {\n        tick_class += ' thick';\n      }\n      // thick ticks for quarters\n      if (this.view_is(VIEW_MODE.MONTH) && date.getMonth() % 3 === 0) {\n        tick_class += ' thick';\n      }\n      // thick ticks for half years\n      if (this.view_is(VIEW_MODE.QUARTER) && date.getMonth() % 6 === 0) {\n        tick_class += ' thick';\n      }\n\n      createSVG('path', {\n        d: `M ${tick_x} ${tick_y} v ${tick_height}`,\n        class: tick_class,\n        append_to: this.layers.grid,\n      });\n\n      if (this.view_is(VIEW_MODE.MONTH)) {\n        tick_x += (date_utils.get_days_in_month(date) * this.options.column_width) / 30;\n      } else if (this.view_is(VIEW_MODE.QUARTER)) {\n        tick_x += (date_utils.get_days_in_month(date) * this.options.column_width) / 90;\n      } else {\n        tick_x += this.options.column_width;\n      }\n    }\n  }\n\n  make_grid_highlights() {\n    // highlight today's date\n    if (this.view_is(VIEW_MODE.DAY)) {\n      const x =\n        (date_utils.diff(date_utils.today(), this.gantt_start, 'hour') / this.options.step) * this.options.column_width;\n      const y = 0;\n\n      const width = this.options.column_width;\n      const height =\n        (this.options.bar_height + this.options.padding) * this.tasks.length +\n        this.options.header_height +\n        this.options.padding / 2;\n\n      createSVG('rect', {\n        x,\n        y,\n        width,\n        height,\n        class: 'today-highlight',\n        append_to: this.layers.grid,\n      });\n    }\n  }\n\n  make_dates() {\n    for (let date of this.get_dates_to_draw()) {\n      createSVG('text', {\n        x: date.lower_x,\n        y: date.lower_y,\n        innerHTML: date.lower_text,\n        class: 'lower-text',\n        append_to: this.layers.date,\n      });\n\n      if (date.upper_text) {\n        const $upper_text = createSVG('text', {\n          x: date.upper_x,\n          y: date.upper_y,\n          innerHTML: date.upper_text,\n          class: 'upper-text',\n          append_to: this.layers.date,\n        });\n\n        // remove out-of-bound dates\n        if ($upper_text.getBBox().x2 > this.layers.grid.getBBox().width) {\n          $upper_text.remove();\n        }\n      }\n    }\n  }\n\n  get_dates_to_draw() {\n    let last_date = null;\n    const dates = this.dates.map((date, i) => {\n      const d = this.get_date_info(date, last_date, i);\n      last_date = date;\n      return d;\n    });\n    return dates;\n  }\n\n  get_date_info(date, last_date, i) {\n    if (!last_date) {\n      last_date = date_utils.add(date, 1, 'year');\n    }\n    const date_text = {\n      'Quarter Day_lower': date_utils.format(date, 'HH', this.options.language),\n      'Half Day_lower': date_utils.format(date, 'HH', this.options.language),\n      Day_lower: date.getDate() !== last_date.getDate() ? date_utils.format(date, 'D', this.options.language) : '',\n      Week_lower:\n        date.getMonth() !== last_date.getMonth()\n          ? date_utils.format(date, 'D MMM', this.options.language)\n          : date_utils.format(date, 'D', this.options.language),\n      Month_lower: date_utils.format(date, 'MMMM', this.options.language),\n      Quarter_lower: date_utils.format(date, 'MMMM', this.options.language),\n      Year_lower: date_utils.format(date, 'YYYY', this.options.language),\n      'Quarter Day_upper':\n        date.getDate() !== last_date.getDate() ? date_utils.format(date, 'D MMM', this.options.language) : '',\n      'Half Day_upper':\n        date.getDate() !== last_date.getDate()\n          ? date.getMonth() !== last_date.getMonth()\n            ? date_utils.format(date, 'D MMM', this.options.language)\n            : date_utils.format(date, 'D', this.options.language)\n          : '',\n      Day_upper: date.getMonth() !== last_date.getMonth() ? date_utils.format(date, 'MMMM', this.options.language) : '',\n      Week_upper:\n        date.getMonth() !== last_date.getMonth() ? date_utils.format(date, 'MMMM', this.options.language) : '',\n      Month_upper:\n        date.getFullYear() !== last_date.getFullYear() ? date_utils.format(date, 'YYYY', this.options.language) : '',\n      Quarter_upper:\n        date.getFullYear() !== last_date.getFullYear() ? date_utils.format(date, 'YYYY', this.options.language) : '',\n      Year_upper:\n        date.getFullYear() !== last_date.getFullYear() ? date_utils.format(date, 'YYYY', this.options.language) : '',\n    };\n\n    const base_pos = {\n      x: i * this.options.column_width,\n      lower_y: this.options.header_height,\n      upper_y: this.options.header_height - 25,\n    };\n\n    const x_pos = {\n      'Quarter Day_lower': (this.options.column_width * 4) / 2,\n      'Quarter Day_upper': 0,\n      'Half Day_lower': (this.options.column_width * 2) / 2,\n      'Half Day_upper': 0,\n      Day_lower: this.options.column_width / 2,\n      Day_upper: (this.options.column_width * 30) / 2,\n      Week_lower: 0,\n      Week_upper: (this.options.column_width * 4) / 2,\n      Month_lower: this.options.column_width / 2,\n      Quarter_lower: this.options.column_width / 2 / 3,\n      Month_upper: (this.options.column_width * 12) / 2,\n      Quarter_upper: this.options.column_width / 2 / 3,\n      Year_lower: this.options.column_width / 2,\n      Year_upper: (this.options.column_width * 30) / 2,\n    };\n\n    return {\n      upper_text: date_text[`${this.options.view_mode}_upper`],\n      lower_text: date_text[`${this.options.view_mode}_lower`],\n      upper_x: base_pos.x + x_pos[`${this.options.view_mode}_upper`],\n      upper_y: base_pos.upper_y,\n      lower_x: base_pos.x + x_pos[`${this.options.view_mode}_lower`],\n      lower_y: base_pos.lower_y,\n    };\n  }\n\n  make_bars() {\n    this.bars = this.tasks.map((task) => {\n      const bar = new Bar(this, task);\n      this.layers.bar.appendChild(bar.group);\n      return bar;\n    });\n  }\n\n  make_arrows() {\n    this.arrows = [];\n    for (let task of this.tasks) {\n      let arrows = [];\n      arrows = task.dependencies\n        .map((task_id, index) => {\n          const dependency = this.get_task(task_id);\n          if (!dependency) {\n            return;\n          }\n          const arrow = new Arrow(\n            this,\n            this.bars[dependency._index], // from_task\n            this.bars[task._index], // to_task\n            DependencyDirection[task.dependencyDirections[index]]\n          );\n          this.layers.arrow.appendChild(arrow.element);\n          return arrow;\n        })\n        .filter(Boolean); // filter falsy values\n      this.arrows = this.arrows.concat(arrows);\n    }\n  }\n\n  map_arrows_on_bars() {\n    for (let bar of this.bars) {\n      bar.arrows = this.arrows.filter((arrow) => {\n        return arrow.from_task.task.id === bar.task.id || arrow.to_task.task.id === bar.task.id;\n      });\n    }\n  }\n\n  set_width() {\n    const cur_width = this.$svg.getBoundingClientRect().width;\n    const actual_width = this.$svg.querySelector('.grid .grid-row').getAttribute('width');\n    if (cur_width < actual_width) {\n      this.$svg.setAttribute('width', actual_width);\n    }\n  }\n\n  set_scroll_position() {\n    const parent_element = this.$svg.parentElement;\n    if (!parent_element) {\n      return;\n    }\n\n    const hours_before_first_task = date_utils.diff(this.get_oldest_starting_date(), this.gantt_start, 'hour');\n\n    const scroll_pos =\n      (hours_before_first_task / this.options.step) * this.options.column_width - this.options.column_width;\n\n    parent_element.scrollLeft = scroll_pos;\n  }\n\n  bind_grid_click() {\n    $.on(this.$svg, this.options.popup_trigger, '.grid-row, .grid-header', () => {\n      this.unselect_all();\n      this.hide_popup();\n    });\n  }\n\n  bind_bar_events() {\n    let is_dragging = false;\n    let x_on_start = 0;\n    let y_on_start = 0;\n    let is_resizing_left = false;\n    let is_resizing_right = false;\n    let parent_bar_id = null;\n    let bars = []; // instanceof Bar\n    this.bar_being_dragged = null;\n\n    function action_in_progress() {\n      return is_dragging || is_resizing_left || is_resizing_right;\n    }\n\n    $.on(this.$svg, 'mousedown', '.bar-wrapper, .handle', (e, element) => {\n      if (!this.options.draggable) {\n        return;\n      }\n\n      const bar_wrapper = $.closest('.bar-wrapper', element);\n\n      if (element.classList.contains('left')) {\n        is_resizing_left = true;\n      } else if (element.classList.contains('right')) {\n        is_resizing_right = true;\n      } else if (element.classList.contains('bar-wrapper')) {\n        is_dragging = true;\n      }\n\n      bar_wrapper.classList.add('active');\n\n      x_on_start = e.offsetX;\n      y_on_start = e.offsetY;\n\n      parent_bar_id = bar_wrapper.getAttribute('data-id');\n      const ids = [parent_bar_id, ...this.get_all_dependent_tasks(parent_bar_id)];\n      bars = ids.map((id) => this.get_bar(id));\n\n      this.bar_being_dragged = parent_bar_id;\n\n      bars.forEach((bar) => {\n        if (bar) {\n          const { $bar } = bar;\n          $bar.ox = $bar.getX();\n          $bar.oy = $bar.getY();\n          $bar.owidth = $bar.getWidth();\n          $bar.finaldx = 0;\n        }\n      });\n    });\n\n    $.on(this.$svg, 'mousemove', (e) => {\n      if (!action_in_progress()) {\n        return;\n      }\n      const dx = e.offsetX - x_on_start;\n      const dy = e.offsetY - y_on_start;\n\n      bars.forEach((bar) => {\n        if (bar) {\n          const { $bar } = bar;\n          $bar.finaldx = this.get_snap_position(dx);\n          this.hide_popup();\n          if (is_resizing_left) {\n            if (parent_bar_id === bar.task.id) {\n              bar.update_bar_position({\n                x: $bar.ox + $bar.finaldx,\n                width: $bar.owidth - $bar.finaldx,\n              });\n            } else {\n              bar.update_bar_position({\n                x: $bar.ox + $bar.finaldx,\n              });\n            }\n          } else if (is_resizing_right) {\n            if (parent_bar_id === bar.task.id) {\n              bar.update_bar_position({\n                width: $bar.owidth + $bar.finaldx,\n              });\n            }\n          } else if (is_dragging) {\n            bar.update_bar_position({ x: $bar.ox + $bar.finaldx });\n          }\n        }\n      });\n    });\n\n    document.addEventListener('mouseup', (_) => {\n      if (is_dragging || is_resizing_left || is_resizing_right) {\n        bars.forEach((bar) => bar && bar.group.classList.remove('active'));\n      }\n\n      is_dragging = false;\n      is_resizing_left = false;\n      is_resizing_right = false;\n    });\n\n    $.on(this.$svg, 'mouseup', (_) => {\n      this.bar_being_dragged = null;\n      bars.forEach((bar) => {\n        if (bar) {\n          const { $bar } = bar;\n          if (!$bar.finaldx) {\n            return;\n          }\n          bar.date_changed();\n          bar.set_action_completed();\n        }\n      });\n    });\n\n    this.bind_bar_progress();\n  }\n\n  bind_bar_progress() {\n    let x_on_start = 0;\n    let y_on_start = 0;\n    let is_resizing = null;\n    let bar = null;\n    let $bar_progress = null;\n    let $bar = null;\n\n    $.on(this.$svg, 'mousedown', '.handle.progress', (e, handle) => {\n      is_resizing = true;\n      x_on_start = e.offsetX;\n      y_on_start = e.offsetY;\n\n      const $bar_wrapper = $.closest('.bar-wrapper', handle);\n      const id = $bar_wrapper.getAttribute('data-id');\n      bar = this.get_bar(id);\n\n      $bar_progress = bar.$bar_progress;\n      $bar = bar.$bar;\n\n      $bar_progress.finaldx = 0;\n      $bar_progress.owidth = $bar_progress.getWidth();\n      $bar_progress.min_dx = -$bar_progress.getWidth();\n      $bar_progress.max_dx = $bar.getWidth() - $bar_progress.getWidth();\n    });\n\n    $.on(this.$svg, 'mousemove', (e) => {\n      if (!is_resizing) {\n        return;\n      }\n      let dx = e.offsetX - x_on_start;\n      let dy = e.offsetY - y_on_start;\n\n      if (dx > $bar_progress.max_dx) {\n        dx = $bar_progress.max_dx;\n      }\n      if (dx < $bar_progress.min_dx) {\n        dx = $bar_progress.min_dx;\n      }\n\n      const $handle = bar.$handle_progress;\n      $.attr($bar_progress, 'width', $bar_progress.owidth + dx);\n      $.attr($handle, 'points', bar.get_progress_polygon_points());\n      $bar_progress.finaldx = dx;\n    });\n\n    $.on(this.$svg, 'mouseup', () => {\n      is_resizing = false;\n      if (!($bar_progress && $bar_progress.finaldx)) {\n        return;\n      }\n      bar.progress_changed();\n      bar.set_action_completed();\n    });\n  }\n\n  get_all_dependent_tasks(task_id) {\n    let out = [];\n    let to_process = [task_id];\n    while (to_process.length) {\n      const to_process_copy = [...to_process];\n      const deps = to_process_copy.reduce((acc, curr) => {\n        acc = acc.concat(this.dependency_map[curr]);\n        return acc;\n      }, []);\n\n      out = out.concat(deps);\n      to_process = deps.filter((d) => !to_process_copy.includes(d));\n    }\n\n    return out.filter(Boolean);\n  }\n\n  get_snap_position(dx) {\n    let odx = dx;\n    let rem;\n    let position;\n\n    if (this.view_is(VIEW_MODE.WEEK)) {\n      rem = dx % (this.options.column_width / 7);\n      position = odx - rem + (rem < this.options.column_width / 14 ? 0 : this.options.column_width / 7);\n    } else if (this.view_is(VIEW_MODE.MONTH)) {\n      rem = dx % (this.options.column_width / 30);\n      position = odx - rem + (rem < this.options.column_width / 60 ? 0 : this.options.column_width / 30);\n    } else if (this.view_is(VIEW_MODE.QUARTER)) {\n      rem = dx % (this.options.column_width / 30);\n      position = odx - rem + (rem < this.options.column_width / 60 ? 0 : this.options.column_width / 30);\n    } else {\n      rem = dx % this.options.column_width;\n      position = odx - rem + (rem < this.options.column_width / 2 ? 0 : this.options.column_width);\n    }\n    return position;\n  }\n\n  unselect_all() {\n    [...this.$svg.querySelectorAll('.bar-wrapper')].forEach((el) => {\n      el.classList.remove('active');\n    });\n  }\n\n  view_is(modes) {\n    if (typeof modes === 'string') {\n      return this.options.view_mode === modes;\n    }\n\n    if (Array.isArray(modes)) {\n      return modes.some((mode) => this.options.view_mode === mode);\n    }\n\n    return false;\n  }\n\n  get_task(id) {\n    return this.tasks.find((task) => {\n      return task.id === id;\n    });\n  }\n\n  get_bar(id) {\n    return this.bars.find((bar) => {\n      return bar.task.id === id;\n    });\n  }\n\n  show_popup(options) {\n    if (options.popup_enabled) {\n      if (!this.popup) {\n        this.popup = new Popup(this.popup_wrapper, this.options.custom_popup_html);\n      }\n      this.popup.show(options);\n    }\n  }\n\n  hide_popup() {\n    this.popup && this.popup.hide();\n  }\n\n  trigger_event(event, args) {\n    if (this.options[`on_${event}`]) {\n      this.options[`on_${event}`].apply(null, args);\n    }\n  }\n\n  /**\n   * Gets the oldest starting date from the list of tasks\n   *\n   * @returns Date\n   * @memberof Gantt\n   */\n  get_oldest_starting_date() {\n    return this.tasks\n      .map((task) => task._start)\n      .reduce((prev_date, cur_date) => (cur_date <= prev_date ? cur_date : prev_date));\n  }\n\n  /**\n   * Clear all elements from the parent svg element\n   *\n   * @memberof Gantt\n   */\n  clear() {\n    this.$svg.innerHTML = '';\n  }\n}\n\nGantt.VIEW_MODE = VIEW_MODE;\n\nfunction generate_id(task) {\n  return `${task.name}_${Math.random().toString(36).slice(2, 12)}`;\n}\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/gantt/frappe/lib/popup.ts",
    "content": "/**\n * Code transpiled and extended from https://github.com/frappe/gantt/.\n * (MIT License)\n */\n\nexport default class Popup {\n  parent: any;\n\n  custom_html: any;\n\n  title: any;\n\n  subtitle: any;\n\n  pointer: any;\n\n  constructor(parent, custom_html) {\n    this.parent = parent;\n    this.custom_html = custom_html;\n    this.make();\n  }\n\n  make() {\n    this.parent.innerHTML = `\n            <div class=\"title\"></div>\n            <div class=\"subtitle\"></div>\n            <div class=\"pointer\"></div>\n        `;\n\n    this.hide();\n\n    this.title = this.parent.querySelector('.title');\n    this.subtitle = this.parent.querySelector('.subtitle');\n    this.pointer = this.parent.querySelector('.pointer');\n  }\n\n  show(options) {\n    if (!options.target_element) {\n      throw new Error('target_element is required to show popup');\n    }\n    if (!options.position) {\n      options.position = 'left';\n    }\n    const { target_element } = options;\n\n    if (this.custom_html) {\n      let html = this.custom_html(options.task);\n      html += '<div class=\"pointer\"></div>';\n      this.parent.innerHTML = html;\n      this.pointer = this.parent.querySelector('.pointer');\n    } else {\n      // set data\n      this.title.innerHTML = options.title;\n      this.subtitle.innerHTML = options.subtitle;\n      this.parent.style.width = `${300}px`;\n      // this.parent.style.width = this.parent.clientWidth + 'px';\n    }\n\n    // set position\n    let position_meta;\n    if (target_element instanceof HTMLElement) {\n      position_meta = target_element.getBoundingClientRect();\n    } else if (target_element instanceof SVGElement) {\n      position_meta = options.target_element.getBBox();\n    }\n\n    if (options.position === 'left') {\n      this.parent.style.left = `${position_meta.x + (position_meta.width + 10)}px`;\n      const bar_height = 63;\n      const popup_height = 79;\n      const bar_index = options.task._index;\n      this.parent.style.top = `${20 + (position_meta.height + 18) * bar_index - position_meta.height * 2}px`;\n      this.pointer.style.transform = 'rotateZ(90deg)';\n      this.pointer.style.left = '-7px';\n      this.pointer.style.top = '2px';\n    }\n\n    // show\n    this.parent.style.opacity = 1;\n  }\n\n  hide() {\n    this.parent.style.opacity = 0;\n    this.parent.style.left = 0;\n  }\n}\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/gantt/frappe/lib/svg_utils.ts",
    "content": "/**\n * Code transpiled and extended from https://github.com/frappe/gantt/.\n * (MIT License)\n */\n\nexport function $(expr, con) {\n  return typeof expr === 'string' ? (con || document).querySelector(expr) : expr || null;\n}\n\nexport function createSVG(tag, attrs) {\n  const elem = document.createElementNS('http://www.w3.org/2000/svg', tag);\n  for (let attr in attrs) {\n    if (attr === 'append_to') {\n      const parent = attrs.append_to;\n      parent.appendChild(elem);\n    } else if (attr === 'innerHTML') {\n      elem.innerHTML = attrs.innerHTML;\n    } else {\n      elem.setAttribute(attr, attrs[attr]);\n    }\n  }\n  return elem;\n}\n\nexport function animateSVG(svgElement, attr, from, to) {\n  const animatedSvgElement = getAnimationElement(svgElement, attr, from, to);\n\n  if (animatedSvgElement === svgElement) {\n    // triggered 2nd time programmatically\n    // trigger artificial click event\n    const event = document.createEvent('HTMLEvents');\n    event.initEvent('click', true, true);\n    event.eventName = 'click';\n    animatedSvgElement.dispatchEvent(event);\n  }\n}\n\nfunction getAnimationElement(svgElement, attr, from, to, dur = '0.4s', begin = '0.1s') {\n  const animEl = svgElement.querySelector('animate');\n  if (animEl) {\n    $.attr(animEl, {\n      attributeName: attr,\n      from,\n      to,\n      dur,\n      begin: `click + ${begin}`, // artificial click\n    });\n    return svgElement;\n  }\n\n  const animateElement = createSVG('animate', {\n    attributeName: attr,\n    from,\n    to,\n    dur,\n    begin,\n    calcMode: 'spline',\n    values: `${from};${to}`,\n    keyTimes: '0; 1',\n    keySplines: cubic_bezier('ease-out'),\n  });\n  svgElement.appendChild(animateElement);\n\n  return svgElement;\n}\n\nfunction cubic_bezier(name) {\n  return {\n    ease: '.25 .1 .25 1',\n    linear: '0 0 1 1',\n    'ease-in': '.42 0 1 1',\n    'ease-out': '0 0 .58 1',\n    'ease-in-out': '.42 0 .58 1',\n  }[name];\n}\n\n$.on = (element, event, selector, callback) => {\n  if (!callback) {\n    callback = selector;\n    $.bind(element, event, callback);\n  } else {\n    $.delegate(element, event, selector, callback);\n  }\n};\n\n$.off = (element, event, handler) => {\n  element.removeEventListener(event, handler);\n};\n\n$.bind = (element, event, callback) => {\n  event.split(/\\s+/).forEach((event) => {\n    element.addEventListener(event, callback);\n  });\n};\n\n$.delegate = (element, event, selector, callback) => {\n  element.addEventListener(event, function addListener(e) {\n    const delegatedTarget = e.target.closest(selector);\n    if (delegatedTarget) {\n      e.delegatedTarget = delegatedTarget;\n      callback.call(this, e, delegatedTarget);\n    }\n  });\n};\n\n$.closest = (selector, element) => {\n  if (!element) {\n    return null;\n  }\n\n  if (element.matches(selector)) {\n    return element;\n  }\n\n  return $.closest(selector, element.parentNode);\n};\n\n$.attr = (element, attr, value) => {\n  if (!value && typeof attr === 'string') {\n    return element.getAttribute(attr);\n  }\n\n  if (typeof attr === 'object') {\n    for (let key in attr) {\n      $.attr(element, key, attr[key]);\n    }\n    return;\n  }\n\n  element.setAttribute(attr, value);\n};\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/gauge/GaugeChart.tsx",
    "content": "import React from 'react';\nimport GaugeChart from 'react-gauge-chart';\nimport { ChartProps } from '../../../../chart/Chart';\nimport { NoDrawableDataErrorMessage } from '../../../../component/editor/CodeViewerComponent';\nimport { createUUID } from '../../../../utils/uuid';\n\n/**\n * Based on https://github.com/dekelpaz PR https://github.com/neo4j-labs/neodash/pull/191\n */\nconst NeoGaugeChart = (props: ChartProps) => {\n  const { records } = props;\n  const { selection } = props;\n  const settings = props.settings ? props.settings : {};\n\n  if (!selection || props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <NoDrawableDataErrorMessage />;\n  }\n  /**\n   * This visualization was extracted from https://github.com/Martin36/react-gauge-chart.\n   */\n\n  const nrOfLevels = settings.nrOfLevels ? settings.nrOfLevels : 3;\n  const arcsLength = settings.arcsLength ? settings.arcsLength : '0.15, 0.55, 0.3';\n  const arcPadding = settings.arcPadding ? settings.arcPadding : 0.02;\n  const colors = settings.colors ? settings.colors : '#5BE12C, #F5CD19, #EA4228';\n  const textColor = settings.textColor ? settings.textColor : 'black';\n  const animDelay = settings.animDelay ? settings.animDelay : 0;\n  const animateDuration = settings.animateDuration ? settings.animateDuration : 2000;\n  const marginRight = settings.marginRight ? settings.marginRight : 24;\n  const marginLeft = settings.marginLeft ? settings.marginLeft : 24;\n  const marginTop = settings.marginTop ? settings.marginTop : 40;\n  const marginBottom = settings.marginBottom ? settings.marginBottom : 40;\n\n  let arcsLengthN = arcsLength.split(',').map((e) => parseFloat(e.trim()));\n\n  if (arcsLengthN.filter((e) => isNaN(e)).length > 0 || arcsLengthN.length != nrOfLevels) {\n    arcsLengthN = Array(nrOfLevels).fill(1);\n  }\n  const sumArcs = arcsLengthN.reduce((previousValue, currentValue) => previousValue + currentValue, 0);\n  arcsLengthN = arcsLengthN.map((e) => e / sumArcs);\n\n  const chartId = createUUID();\n  let score = records && records[0] && records[0]._fields && records[0]._fields[0] ? records[0]._fields[0] : '';\n\n  if (isNaN(score)) {\n    return <NoDrawableDataErrorMessage />;\n  }\n  if (score.low != undefined) {\n    score = score.low;\n  }\n  if (score >= 0) {\n    score /= 100;\n  } // supporting older versions of Neo4j which don't support round to 2 decimal points\n\n  return (\n    <div style={{ position: 'relative', top: '40%', transform: 'translateY(-50%)' }}>\n      {typeof score == 'number' ? (\n        <GaugeChart\n          id={chartId}\n          nrOfLevels={nrOfLevels}\n          percent={score}\n          arcsLength={arcsLengthN}\n          arcPadding={arcPadding}\n          colors={colors.split(', ')}\n          textColor={textColor}\n          style={{ marginTop: marginTop, marginRight: marginRight, marginBottom: marginBottom, marginLeft: marginLeft }}\n          animDelay={animDelay}\n          animateDuration={animateDuration}\n        />\n      ) : (\n        <></>\n      )}\n    </div>\n  );\n};\n\nexport default NeoGaugeChart;\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/graph3d/GraphChart3D.tsx",
    "content": "import React from 'react';\nimport { ChartProps } from '../../../../chart/Chart';\nimport { NeoGraphChartVisualization3D } from './GraphChartVisualization3D';\nimport NeoGraphChart from '../../../../chart/graph/GraphChart';\n\n/**\n * This is a 3D visualization renderer, powered by react-force-graph-3d.\n * We can re-use the existing 2D graph visualization, but override the visualization component with the 3D version.\n */\nconst NeoGraphChart3D = (props: ChartProps) => {\n  return <NeoGraphChart component={NeoGraphChartVisualization3D} lockable={false} {...props} />;\n};\n\nexport default NeoGraphChart3D;\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/graph3d/GraphChartVisualization3D.tsx",
    "content": "import React, { useRef } from 'react';\nimport ForceGraph3D from 'react-force-graph-3d';\nimport { GraphChartVisualizationProps } from '../../../../chart/graph/GraphChartVisualization';\nimport { getNodeLabel } from '../../../../chart/graph/util/NodeUtils';\nimport SpriteText from 'three-spritetext';\nimport { evaluateRulesOnNode } from '../../../styling/StyleRuleEvaluator';\nimport { NeoGraphChartVisualizationBase } from '../../../../chart/graph/GraphChartVisualizationBase';\nimport * as THREE from 'three';\n\n/*\n *\n */\nexport const NeoGraphChartVisualization3D = (props: GraphChartVisualizationProps) => {\n  const config3d = {\n    graphComponent: ForceGraph3D,\n    cooldownAfterengineStop: 1,\n    nodeThreeObjectExtend: true,\n    nodeThreeObject: (node) => {\n      const label =\n        props.engine.selection && props.engine.selection[node.mainLabel]\n          ? getNodeLabel(props.engine.selection, node)\n          : '';\n      const sprite = new SpriteText(label);\n      sprite.color = evaluateRulesOnNode(\n        node,\n        'node label color',\n        props.style.nodeLabelColor,\n        props.extensions.styleRules\n      );\n      sprite.textHeight = props.style.nodeLabelFontSize;\n      return sprite;\n    },\n    linkThreeObjectExtend: true,\n    linkThreeObject: (link) => {\n      // extend link with text sprite\n      const label = link.properties.name || link.type || link.id;\n      const fontSize = props.style.relLabelFontSize;\n      const sprite = new SpriteText(label);\n      sprite.color = props.style.relLabelColor;\n      sprite.textHeight = fontSize;\n      return sprite;\n    },\n    linkPositionUpdate: (sprite, { start, end }, link, _) => {\n      if (link.source.id !== link.target.id) {\n        // If this is a relationship with a different start and end node...\n        const middle = Object.assign(\n          ...['x', 'y', 'z'].map((c) => ({\n            [c]: start[c] + (end[c] - start[c]) / 2, // calc middle point\n          }))\n        );\n        if (!link.curvature) {\n          // Simple case - no curvature assigned, we position the label in the middle of the two nodes.\n          Object.assign(sprite.position, middle);\n        } else {\n          // Complex case, multiple rels between a pair of nodes. Adjust the position to match each rel's curvature.\n          let vector = new THREE.Vector3(end.x - start.x, end.y - start.y, end.z - start.z);\n          let length = vector.length();\n          let axis = new THREE.Vector3(0, 0, 1);\n          let angle = -Math.PI / 2;\n          vector = vector.applyAxisAngle(axis, angle);\n          vector.multiplyScalar(0.5 * link.curvature * Math.pow(length / 30.0, 0.01));\n          const translated = { x: middle.x + vector.x, y: middle.y + vector.y, z: middle.z + vector.z };\n          Object.assign(sprite.position, translated);\n        }\n      } else {\n        // If this is a relationship with an identical start and end node...\n        const vector = { x: 26 * link.curvature, y: 26 * link.curvature, z: 0 };\n\n        const translated = { x: start.x + vector.x, y: start.y + vector.y, z: start.z + vector.z };\n        Object.assign(sprite.position, translated);\n      }\n    },\n  };\n  const props3d = { ...props, config: config3d };\n  return <NeoGraphChartVisualizationBase {...props3d} />;\n};\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/radar/RadarChart.tsx",
    "content": "import React from 'react';\nimport { ChartProps } from '../../../../chart/Chart';\nimport { ResponsiveRadar } from '@nivo/radar';\nimport { evaluateRulesOnDict, useStyleRules } from '../../../styling/StyleRuleEvaluator';\nimport { NoDrawableDataErrorMessage } from '../../../../component/editor/CodeViewerComponent';\nimport { themeNivo } from '../../../../chart/Utils';\nimport { extensionEnabled } from '../../../../utils/ReportUtils';\n\n/**\n * Embeds a RadarChart (from Charts) into NeoDash.\n */\nconst NeoRadarChart = (props: ChartProps) => {\n  if (\n    !props.selection ||\n    props.selection.values == undefined ||\n    props.records == null ||\n    props.records.length == 0 ||\n    props.records[0].keys == null\n  ) {\n    return <NoDrawableDataErrorMessage />;\n  }\n  const { records } = props;\n  const selection = props.selection ? props.selection : {};\n  const settings = props.settings ? props.settings : {};\n  const legendHeight = 20;\n  const legendWidth = 20;\n  const marginRight = settings.marginRight ? settings.marginRight : 24;\n  const marginLeft = settings.marginLeft ? settings.marginLeft : 24;\n  const marginTop = settings.marginTop ? settings.marginTop : 40;\n  const marginBottom = settings.marginBottom ? settings.marginBottom : 40;\n  const dotSize = settings.dotSize ? settings.dotSize : 10;\n  const dotBorderWidth = settings.dotBorderWidth ? settings.dotBorderWidth : 2;\n  const gridLabelOffset = settings.gridLabelOffset ? settings.gridLabelOffset : 16;\n  const gridLevels = settings.gridLevels ? settings.gridLevels : 5;\n  const interactive = settings.interactive !== undefined ? settings.interactive : true;\n  const animate = settings.animate !== undefined ? settings.animate : true;\n  const legend = settings.legend !== undefined ? settings.legend : false;\n  const colorScheme = settings.colors ? settings.colors : 'set2';\n  const blendMode = settings.blendMode ? settings.blendMode : 'normal';\n  const motionConfig = settings.motionConfig ? settings.motionConfig : 'gentle';\n  const curve = settings.curve ? settings.curve : 'linearClosed';\n  const styleRules = useStyleRules(\n    extensionEnabled(props.extensions, 'styling'),\n    props.settings.styleRules,\n    props.getGlobalParameter\n  );\n\n  const keys = selection.values;\n\n  // Compute slice color based on rules - overrides default color scheme completely.\n  const getCircleColor = (slice) => {\n    const data = {};\n    if (!props.selection) {\n      return 'grey';\n    }\n    data[props.selection.value] = slice.value;\n    data[props.selection.index] = slice.id;\n    const validRuleIndex = evaluateRulesOnDict(data, styleRules, ['slice color']);\n    if (validRuleIndex !== -1) {\n      return styleRules[validRuleIndex].customizationValue;\n    }\n    return 'grey';\n  };\n\n  let valid = true;\n  const data = records.map((r) => {\n    const entry = {};\n    selection.values.concat([selection.index]).forEach((k) => {\n      const fieldIndex = r._fieldLookup[k];\n      if (k !== selection.index && isNaN(r._fields[fieldIndex])) {\n        valid = false;\n      }\n      entry[k] = `${r._fields[fieldIndex]}`;\n    });\n    return entry;\n  });\n\n  // If we find inconsitent data, return an error/\n  if (!valid) {\n    return <NoDrawableDataErrorMessage />;\n  }\n  return (\n    <ResponsiveRadar\n      theme={themeNivo}\n      data={data}\n      isInteractive={interactive}\n      animate={animate}\n      margin={{\n        top: legend ? legendHeight + marginTop : marginTop,\n        right: legend ? legendWidth + marginRight : marginRight,\n        bottom: marginBottom,\n        left: legend ? legendHeight + marginLeft : marginLeft,\n      }}\n      gridLevels={gridLevels}\n      keys={keys}\n      indexBy={selection.index}\n      valueFormat='>-.2f'\n      borderColor={{ from: 'color' }}\n      gridLabelOffset={gridLabelOffset}\n      dotSize={dotSize}\n      dotColor={{ theme: 'background' }}\n      dotBorderWidth={dotBorderWidth}\n      // colors={styleRules.length >= 1 ? getCircleColor : { scheme: colorScheme }}\n      colors={{ scheme: colorScheme }}\n      blendMode={blendMode}\n      motionConfig={motionConfig}\n      curve={curve}\n      legends={\n        legend\n          ? [\n              {\n                anchor: 'top-left',\n                direction: 'column',\n                translateX: 0,\n                translateY: -40,\n                itemWidth: 100,\n                itemHeight: 14,\n                symbolSize: 14,\n                symbolShape: 'circle',\n                effects: [\n                  {\n                    on: 'hover',\n                    style: {\n                      itemTextColor: '#000',\n                    },\n                  },\n                ],\n              },\n            ]\n          : []\n      }\n    />\n  );\n};\n\nexport default NeoRadarChart;\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/sankey/SankeyChart.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { ResponsiveSankey } from '@nivo/sankey';\nimport { ChartProps } from '../../../../chart/Chart';\nimport { valueIsArray, valueIsNode, valueIsPath, valueIsRelationship } from '../../../../chart/ChartUtils';\nimport { categoricalColorSchemes } from '../../../../config/ColorConfig';\nimport { evaluateRulesOnDict, evaluateRulesOnNode, useStyleRules } from '../../../styling/StyleRuleEvaluator';\nimport NeoCodeViewerComponent from '../../../../component/editor/CodeViewerComponent';\nimport { isCyclic } from '../../Utils';\nimport { themeNivo } from '../../../../chart/Utils';\nimport { extensionEnabled } from '../../../../utils/ReportUtils';\n\nconst UNWEIGHTED_SANKEY_PROPERTY = 'SANKEY_UNWEIGHTED';\n\n/**\n * Embeds a SankeyChart (from Charts) into NeoDash.\n */\nconst NeoSankeyChart = (props: ChartProps) => {\n  const settings = props.settings ? props.settings : {};\n  const legendHeight = 20;\n  const legendWidth = 120;\n  const marginRight = settings.marginRight ? settings.marginRight : 24;\n  const marginLeft = settings.marginLeft ? settings.marginLeft : 24;\n  const marginTop = settings.marginTop ? settings.marginTop : 24;\n  const marginBottom = settings.marginBottom ? settings.marginBottom : 40;\n\n  const interactive = settings.interactive !== undefined ? settings.interactive : true;\n  const nodeBorderWidth = settings.nodeBorderWidth ? settings.nodeBorderWidth : 0;\n\n  const legend = settings.legend !== undefined ? settings.legend : false;\n  const colorScheme = settings.colors ? settings.colors : 'set2';\n  const labelProperty = settings.labelProperty ? settings.labelProperty : 'value';\n  const layout = settings.layout ? settings.layout : 'horizontal';\n  const labelPosition = settings.labelPosition ? settings.labelPosition : 'inside';\n  const labelOrientation = settings.labelOrientation ? settings.labelOrientation : 'horizontal';\n  const nodeThickness = settings.nodeThickness ? settings.nodeThickness : 12;\n  const nodeSpacing = settings.nodeSpacing ? settings.nodeSpacing : 12;\n\n  const styleRules = useStyleRules(\n    extensionEnabled(props.extensions, 'styling'),\n    props.settings.styleRules,\n    props.getGlobalParameter\n  );\n\n  // TODO this line is duplicated in a lot of places, should be in an utils file\n  const update = (state, mutations) => Object.assign({}, state, mutations);\n\n  const [data, setData] = useState({ nodes: [], links: [] });\n\n  useEffect(() => {\n    buildVisualizationDictionaryFromRecords(props.records);\n  }, [props.records]);\n\n  if (props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <>No data, re-run the report.</>;\n  }\n  let nodes = {};\n  let nodeLabels = {};\n  let links = {};\n  let linkTypes = {};\n\n  function extractGraphEntitiesFromField(value) {\n    if (value == undefined) {\n      return;\n    }\n    if (valueIsArray(value)) {\n      value.forEach((v) => extractGraphEntitiesFromField(v));\n    } else if (valueIsNode(value)) {\n      value.labels.forEach((l) => (nodeLabels[l] = true));\n      nodes[value.identity.low] = {\n        id: value.identity.low,\n        labels: value.labels,\n        properties: value.properties,\n        lastLabel: value.labels[value.labels.length - 1],\n      };\n    } else if (valueIsRelationship(value)) {\n      if (links[`${value.start.low},${value.end.low}`] == undefined) {\n        links[`${value.start.low},${value.end.low}`] = [];\n      }\n      const addItem = (arr, item) => arr.find((x) => x.id === item.id) || arr.push(item);\n      if (labelProperty === UNWEIGHTED_SANKEY_PROPERTY) {\n        addItem(links[`${value.start.low},${value.end.low}`], {\n          id: value.identity.low,\n          source: value.start.low,\n          target: value.end.low,\n          type: value.type,\n          properties: value.properties,\n          value: 1,\n        });\n      } else if (value.properties[labelProperty] !== undefined && !isNaN(value.properties[labelProperty])) {\n        addItem(links[`${value.start.low},${value.end.low}`], {\n          id: value.identity.low,\n          source: value.start.low,\n          target: value.end.low,\n          type: value.type,\n          properties: value.properties,\n          value: value.properties[labelProperty],\n        });\n      }\n    } else if (valueIsPath(value)) {\n      value.segments.map((segment) => {\n        extractGraphEntitiesFromField(segment.start);\n        extractGraphEntitiesFromField(segment.relationship);\n        extractGraphEntitiesFromField(segment.end);\n      });\n    }\n  }\n\n  function buildVisualizationDictionaryFromRecords(records) {\n    // Extract graph objects from result set.\n    records.forEach((record) => {\n      record._fields.forEach((field) => {\n        extractGraphEntitiesFromField(field);\n      });\n    });\n    // Assign proper curvatures to relationships.\n    // This is needed for pairs of nodes that have multiple relationships between them, or self-loops.\n    const linksList = Object.values(links).map((nodePair) => {\n      return nodePair;\n    });\n\n    // Assign proper colors to nodes.\n    const totalColors = categoricalColorSchemes[colorScheme] ? categoricalColorSchemes[colorScheme].length : 0;\n    const nodeLabelsList = Object.keys(nodeLabels);\n    const nodesList = Object.values(nodes).map((node) => {\n      // First try to assign a node a color if it has a property specifying the color.\n      let assignedColor =\n        totalColors > 0\n          ? categoricalColorSchemes[colorScheme][nodeLabelsList.indexOf(node.lastLabel) % totalColors]\n          : 'grey';\n      // Next, evaluate the custom styling rules to see if there's a rule-based override\n      assignedColor = evaluateRulesOnNode(node, 'node color', assignedColor, styleRules);\n      return update(node, { nodeColor: assignedColor ? assignedColor : 'grey' });\n    });\n\n    // Set the data dictionary that is read by the visualization.\n    setData({\n      nodes: nodesList,\n      links: linksList.flat(),\n    });\n  }\n\n  // Compute slice color based on rules - overrides default color scheme completely.\n  const getSliceColor = (slice) => {\n    const data = {};\n    if (!props.selection) {\n      return 'grey';\n    }\n    data[props.selection.value] = slice.value;\n    data[props.selection.index] = slice.id;\n    const validRuleIndex = evaluateRulesOnDict(data, styleRules, ['slice color']);\n    if (validRuleIndex !== -1) {\n      return styleRules[validRuleIndex].customizationValue;\n    }\n    return 'grey';\n  };\n\n  const getArcLabel = (item) => {\n    let lbl = '';\n\n    switch (props.selection[item.lastLabel]) {\n      case '(id)':\n        lbl = item.id;\n        break;\n      case '(label)':\n        lbl = item.lastLabel;\n        break;\n      default:\n        lbl = item.properties[props.selection[item.lastLabel]];\n    }\n\n    return typeof lbl === 'object' ? lbl.low : lbl;\n  };\n\n  if (data && data.links && data.links.length == 0) {\n    return (\n      <NeoCodeViewerComponent\n        value={\n          \"No relationship weights found. \\nDefine a numeric 'Relationship Property' in the \\nreport's advanced settings to view the sankey diagram.\"\n        }\n      ></NeoCodeViewerComponent>\n    );\n  }\n\n  if (data && data.nodes && data.links && isCyclic(data)) {\n    return (\n      <NeoCodeViewerComponent\n        value={'Please be careful with the data you use for this chart as it does not support cyclic dependencies.'}\n      ></NeoCodeViewerComponent>\n    );\n  }\n\n  return (\n    <ResponsiveSankey\n      theme={themeNivo}\n      data={data}\n      margin={{\n        top: marginTop,\n        right: legend ? legendWidth + marginRight : marginRight,\n        bottom: legend ? legendHeight + marginBottom : marginBottom,\n        left: marginLeft,\n      }}\n      isInteractive={interactive}\n      layout={layout}\n      label={getArcLabel}\n      nodeBorderWidth={nodeBorderWidth}\n      align='justify'\n      nodeOpacity={1}\n      nodeHoverOthersOpacity={0.35}\n      nodeThickness={nodeThickness}\n      nodeSpacing={nodeSpacing}\n      nodeBorderColor={{\n        from: 'color',\n        modifiers: [['darker', 0.8]],\n      }}\n      nodeBorderRadius={3}\n      linkOpacity={0.5}\n      linkHoverOthersOpacity={0.1}\n      linkContract={3}\n      enableLinkGradient={true}\n      labelPosition={labelPosition}\n      labelOrientation={labelOrientation}\n      labelPadding={16}\n      labelTextColor={{\n        from: 'color',\n        modifiers: [['darker', 1]],\n      }}\n      colors={styleRules.length >= 1 ? getSliceColor : { scheme: colorScheme }}\n      legends={\n        legend\n          ? [\n              {\n                anchor: 'bottom-right',\n                direction: 'column',\n                translateX: 120,\n                itemWidth: 100,\n                itemHeight: 14,\n                itemDirection: 'right-to-left',\n                itemsSpacing: 2,\n                symbolSize: 14,\n                effects: [\n                  {\n                    on: 'hover',\n                    style: {\n                      itemTextColor: '#000',\n                    },\n                  },\n                ],\n              },\n            ]\n          : []\n      }\n      animate={true}\n    />\n  );\n};\n\nexport default NeoSankeyChart;\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/sunburst/SunburstChart.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { processHierarchyFromRecords, findObject, flatten, mutateName } from '../../../../chart/ChartUtils';\nimport { ResponsiveSunburst } from '@nivo/sunburst';\nimport { ChartProps } from '../../../../chart/Chart';\nimport { NoDrawableDataErrorMessage } from '../../../../component/editor/CodeViewerComponent';\nimport { themeNivo } from '../../../../chart/Utils';\nimport RefreshButton from '../../component/RefreshButton';\n/**\n * Embeds a SunburstChart (from Charts) into NeoDash.\n */\nconst NeoSunburstChart = (props: ChartProps) => {\n  if (props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <>No data, re-run the report.</>;\n  }\n  const { records } = props;\n  const { selection } = props;\n  const [data, setData] = useState(undefined);\n  const [commonProperties, setCommonProperties] = useState({ data: { name: 'Total', children: [] } });\n  const [refreshable, setRefreshable] = useState(false);\n\n  if (!selection || props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  useEffect(() => {\n    let dataPre = processHierarchyFromRecords(records, selection);\n    dataPre.forEach((currentNode) => mutateName(currentNode));\n    setCommonProperties({ data: dataPre.length == 1 ? dataPre[0] : { name: 'Total', children: dataPre } });\n  }, [records]);\n\n  useEffect(() => {\n    setData(commonProperties.data);\n  }, [props.selection, commonProperties]);\n\n  // Where a user give us the hierarchy with a common root, in that case we can push the entire tree.\n  // Where a user give us just the tree starting one hop away from the root.\n  // as Nivo needs a common root, so in that case, we create it for them.\n  if (data == undefined) {\n    setData(commonProperties.data);\n  }\n\n  const settings = props.settings ? props.settings : {};\n  const legendHeight = 20;\n  const marginRight = settings.marginRight ? settings.marginRight : 24;\n  const marginLeft = settings.marginLeft ? settings.marginLeft : 24;\n  const marginTop = settings.marginTop ? settings.marginTop : 24;\n  const marginBottom = settings.marginBottom ? settings.marginBottom : 40;\n  const enableArcLabels = settings.enableArcLabels !== undefined ? settings.enableArcLabels : true;\n  const interactive = settings.interactive ? settings.interactive : true;\n  const borderWidth = settings.borderWidth ? settings.borderWidth : 0;\n  const legend = settings.legend !== undefined ? settings.legend : false;\n  const arcLabelsSkipAngle = settings.arcLabelsSkipAngle ? settings.arcLabelsSkipAngle : 10;\n  const cornerRadius = settings.cornerRadius ? settings.cornerRadius : 3;\n  const colorScheme = settings.colors ? settings.colors : 'nivo';\n  const inheritColorFromParent = settings.inheritColorFromParent !== undefined ? settings.inheritColorFromParent : true;\n\n  if (!data || !data.children || data.children.length == 0) {\n    return <NoDrawableDataErrorMessage />;\n  }\n  return (\n    <>\n      <div style={{ position: 'relative', overflow: 'hidden', width: '100%', height: '100%' }}>\n        {refreshable ? (\n          <RefreshButton\n            onClick={() => {\n              setData(commonProperties.data);\n              setRefreshable(false);\n            }}\n          ></RefreshButton>\n        ) : (\n          <div></div>\n        )}\n        <ResponsiveSunburst\n          {...commonProperties}\n          theme={themeNivo}\n          id='name'\n          value='loc'\n          data={data}\n          transitionMode={'pushIn'}\n          isInteractive={interactive}\n          onClick={(clickedData) => {\n            const foundObject = findObject(flatten(data.children), clickedData.id);\n            if (foundObject && foundObject.children) {\n              setData(foundObject);\n              setRefreshable(true);\n            }\n          }}\n          enableArcLabels={enableArcLabels}\n          borderWidth={borderWidth}\n          cornerRadius={cornerRadius}\n          inheritColorFromParent={inheritColorFromParent}\n          margin={{\n            top: marginTop,\n            right: marginRight,\n            bottom: legend ? legendHeight + marginBottom : marginBottom,\n            left: marginLeft,\n          }}\n          childColor={{\n            from: 'color',\n            modifiers: [['brighter', 0.4]],\n          }}\n          animate={true}\n          arcLabelsSkipAngle={arcLabelsSkipAngle}\n          colors={{ scheme: colorScheme }}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default NeoSunburstChart;\n"
  },
  {
    "path": "src/extensions/advancedcharts/chart/treemap/TreeMapChart.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { ResponsiveTreeMap } from '@nivo/treemap';\nimport { mutateName, processHierarchyFromRecords, findObject, flatten } from '../../../../chart/ChartUtils';\nimport { useState } from 'react';\n\nimport { ChartProps } from '../../../../chart/Chart';\nimport { NoDrawableDataErrorMessage } from '../../../../component/editor/CodeViewerComponent';\nimport { themeNivo } from '../../../../chart/Utils';\nimport RefreshButton from '../../component/RefreshButton';\n\n/**\n * Embeds a TreeMap (from Charts) into NeoDash.\n */\nconst NeoTreeMapChart = (props: ChartProps) => {\n  if (props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <>No data, re-run the report.</>;\n  }\n  const { records } = props;\n  const { selection } = props;\n  const [data, setData] = useState(undefined);\n  const [commonProperties, setCommonProperties] = useState({ data: { name: 'Total', children: [] } });\n  const [refreshable, setRefreshable] = useState(false);\n\n  if (!selection || props.records == null || props.records.length == 0 || props.records[0].keys == null) {\n    return <NoDrawableDataErrorMessage />;\n  }\n\n  useEffect(() => {\n    let dataPre = processHierarchyFromRecords(records, selection);\n    dataPre.forEach((currentNode) => mutateName(currentNode));\n    setCommonProperties({ data: dataPre.length == 1 ? dataPre[0] : { name: 'Total', children: dataPre } });\n  }, [records]);\n\n  useEffect(() => {\n    setData(commonProperties.data);\n  }, [props.selection, commonProperties]);\n\n  // Where a user give us the hierarchy with a common root, in that case we can push the entire tree.\n  // Where a user give us just the tree starting one hop away from the root.\n  // as Nivo needs a common root, so in that case, we create it for them.\n  if (data == undefined) {\n    setData(commonProperties.data);\n  }\n\n  const settings = props.settings ? props.settings : {};\n  const legendHeight = 20;\n  const marginRight = settings.marginRight ? settings.marginRight : 24;\n  const marginLeft = settings.marginLeft ? settings.marginLeft : 24;\n  const marginTop = settings.marginTop ? settings.marginTop : 24;\n  const marginBottom = settings.marginBottom ? settings.marginBottom : 40;\n  const interactive = settings.interactive ? settings.interactive : true;\n  const borderWidth = settings.borderWidth ? settings.borderWidth : 0;\n  const legend = settings.legend ? settings.legend : false;\n  const colorScheme = settings.colors ? settings.colors : 'nivo';\n\n  /**\n   * Helper function to determine which label to draw on a node in the hierarchy.\n   * @param n  the node.\n   * @returns a string (label).\n   */\n  const getLabelForNode = (n) => {\n    return n.formattedValue;\n  };\n\n  // Final sanity check - only draw the visualization if we are sure the data is there and formatted correctly.\n  if (!data || !data.children || data.children.length == 0) {\n    return <NoDrawableDataErrorMessage />;\n  }\n  return (\n    <>\n      <div style={{ position: 'relative', overflow: 'hidden', width: '100%', height: '100%' }}>\n        {refreshable ? (\n          <RefreshButton\n            onClick={() => {\n              setData(commonProperties.data);\n              setRefreshable(false);\n            }}\n          ></RefreshButton>\n        ) : (\n          <div></div>\n        )}\n        <ResponsiveTreeMap\n          {...commonProperties}\n          theme={themeNivo}\n          identity='name'\n          value='loc'\n          data={data}\n          onClick={(clickedData) => {\n            const foundObject = findObject(flatten(data.children), clickedData.id);\n            if (foundObject && foundObject.children) {\n              setData(foundObject);\n              setRefreshable(true);\n            }\n          }}\n          isInteractive={interactive}\n          borderWidth={borderWidth}\n          margin={{\n            top: marginTop,\n            right: marginRight,\n            bottom: legend ? legendHeight + marginBottom : marginBottom,\n            left: marginLeft,\n          }}\n          animate={true}\n          colors={{ scheme: colorScheme }}\n          label={getLabelForNode}\n        />\n      </div>\n    </>\n  );\n};\n\nexport default NeoTreeMapChart;\n"
  },
  {
    "path": "src/extensions/advancedcharts/component/RefreshButton.tsx",
    "content": "import { IconButton } from '@neo4j-ndl/react';\nimport { ArrowPathIconOutline } from '@neo4j-ndl/react/icons';\nimport React from 'react';\nimport { Tooltip } from '@mui/material';\n\nconst RefreshButton = ({ onClick }) => (\n  <Tooltip title='Reset' aria-label='reset' disableInteractive>\n    <IconButton\n      onClick={() => onClick()}\n      className='n-z-10'\n      style={{\n        opacity: 0.6,\n        bottom: 12,\n        right: 12,\n        position: 'absolute',\n        borderRadius: '12px',\n      }}\n      size={'small'}\n    >\n      <ArrowPathIconOutline />\n    </IconButton>\n  </Tooltip>\n);\n\nexport default RefreshButton;\n"
  },
  {
    "path": "src/extensions/forms/FormsExampleConfig.tsx",
    "content": "export const EXAMPLE_FORMS = [];\n"
  },
  {
    "path": "src/extensions/forms/FormsReportConfig.tsx",
    "content": "import React from 'react';\nimport { SELECTION_TYPES } from '../../config/CardConfig';\nimport NeoForm from './chart/NeoForm';\nimport NeoFormCardSettings from './settings/NeoFormCardSettings';\n\nexport const FORMS = {\n  forms: {\n    label: 'Form',\n    component: NeoForm,\n    settingsComponent: NeoFormCardSettings,\n    textOnly: true, // this makes sure that no query is executed, input of the report gets passed directly to the renderer.\n    helperText: (\n      <div>\n        A form lets users specify multiple parameters, which can then be used to run a custom Cypher query on demand.\n      </div>\n    ),\n    maxRecords: 1,\n    settings: {\n      backgroundColor: {\n        label: 'Background Color',\n        type: SELECTION_TYPES.COLOR,\n        default: '#fafafa',\n      },\n      runButtonText: {\n        label: 'Form Button Text',\n        type: SELECTION_TYPES.TEXT,\n        default: 'Submit',\n      },\n      confirmationMessage: {\n        label: 'Confirmation Message',\n        type: SELECTION_TYPES.MULTILINE_TEXT,\n        default: 'Form submitted.',\n      },\n      resetButtonText: {\n        label: 'Reset Button Text',\n        type: SELECTION_TYPES.TEXT,\n        default: 'Reset Form',\n      },\n      hasSubmitButton: {\n        label: 'Has Submit Button',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      clearParametersAfterSubmit: {\n        label: 'Clear parameters after submit',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      hasResetButton: {\n        label: 'Has Reset Button',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      hasSubmitMessage: {\n        label: 'Has Submit Message',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: true,\n      },\n      refreshButtonEnabled: {\n        label: 'Refreshable',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      fullscreenEnabled: {\n        label: 'Fullscreen enabled',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      downloadImageEnabled: {\n        label: 'Download Image enabled',\n        type: SELECTION_TYPES.LIST,\n        values: [true, false],\n        default: false,\n      },\n      description: {\n        label: 'Report Description',\n        type: SELECTION_TYPES.MULTILINE_TEXT,\n        default: 'Enter markdown here...',\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "src/extensions/forms/chart/NeoForm.tsx",
    "content": "import React, { useCallback, useEffect } from 'react';\nimport { ChartProps } from '../../../chart/Chart';\nimport { Button } from '@neo4j-ndl/react';\nimport { PlayIconSolid } from '@neo4j-ndl/react/icons';\nimport NeoCodeViewerComponent from '../../../component/editor/CodeViewerComponent';\nimport { REPORT_LOADING_ICON } from '../../../report/Report';\nimport debounce from 'lodash/debounce';\nimport { RUN_QUERY_DELAY_MS } from '../../../config/ReportConfig';\nimport NeoParameterSelectionChart from '../../../chart/parameter/ParameterSelectionChart';\nimport { checkParametersNameInGlobalParameter, extractAllParameterNames } from '../../../utils/parameterUtils';\n\nenum FormStatus {\n  DATA_ENTRY = 0, // The user is filling in the form.\n  RUNNING = 1, // The form is running.\n  SUBMITTED = 2, // The form was successfully submitted.\n  ERROR = 3, // Submitting the form has failed.\n}\n\n/**\n * Renders a form.\n */\nconst NeoForm = (props: ChartProps) => {\n  const { settings } = props;\n  const buttonText = settings?.runButtonText ?? 'Submit';\n  const confirmationMessage = settings?.confirmationMessage ?? 'Form Submitted.';\n  const resetButtonText = settings?.resetButtonText ?? 'Reset Form';\n  const hasResetButton = settings?.hasResetButton ?? true;\n  const hasSubmitButton = settings?.hasSubmitButton ?? true;\n  const hasSubmitMessage = settings?.hasSubmitMessage ?? true;\n  const clearParametersAfterSubmit = settings?.clearParametersAfterSubmit ?? true;\n  const [submitButtonActive, setSubmitButtonActive] = React.useState(true);\n  const [status, setStatus] = React.useState(FormStatus.DATA_ENTRY);\n  const [formResults, setFormResults] = React.useState([]);\n  const debouncedRunCypherQuery = useCallback(debounce(props.queryCallback, RUN_QUERY_DELAY_MS), []);\n\n  // Helper function to force a refresh on all reports that depend on the form.\n  // All reports that use one or more parameters used in the form will be refreshed.\n  function forceRefreshDependentReports() {\n    const paramCache = { ...props.parameters };\n    Object.keys(paramCache).forEach((key) => {\n      props.setGlobalParameter && props.setGlobalParameter(key, undefined);\n      props.setGlobalParameter && props.setGlobalParameter(key, paramCache[key]);\n    });\n  }\n\n  const isParametersDefined = (cypherQuery: string | undefined) => {\n    const parameterNames = extractAllParameterNames(cypherQuery);\n    if (props.parameters) {\n      return checkParametersNameInGlobalParameter(parameterNames, props.parameters);\n    }\n    return false;\n  };\n\n  useEffect(() => {\n    // If the parameters change after the form is completed, reset it, as there might be another submission.\n    if (status == FormStatus.SUBMITTED) {\n      setStatus(FormStatus.DATA_ENTRY);\n    }\n  }, [JSON.stringify(props)]);\n\n  if (status == FormStatus.DATA_ENTRY) {\n    return (\n      <div>\n        {settings?.formFields?.map((field) => (\n          <div style={{ marginBottom: 10 }}>\n            <NeoParameterSelectionChart\n              records={[{ input: field.query }]}\n              settings={field.settings}\n              parameters={props.parameters}\n              queryCallback={props.queryCallback}\n              updateReportSetting={(key, value) => {\n                // If anyone of the fields is in a loading state (debounce / waiting for input) we disable submission temporarily.\n                if (key == 'typing' && value == true) {\n                  setSubmitButtonActive(false);\n                }\n                if (key == 'typing' && value == undefined) {\n                  setSubmitButtonActive(true);\n                }\n              }}\n              setGlobalParameter={props.setGlobalParameter}\n              getGlobalParameter={props.getGlobalParameter}\n            />\n          </div>\n        ))}\n        {hasSubmitButton ? (\n          <Button\n            style={{ marginLeft: 15 }}\n            id='form-submit'\n            disabled={!submitButtonActive || isParametersDefined(props.query)}\n            onClick={() => {\n              if (!props.query || !props.query.trim()) {\n                props.createNotification(\n                  'No query specified',\n                  'There is no query defined to run on submission. Specify one in the report settings.'\n                );\n                return;\n              }\n              setStatus(FormStatus.RUNNING);\n              debouncedRunCypherQuery(props.query, props.parameters, (records) => {\n                setFormResults(records);\n                if (records && records[0] && records[0].error) {\n                  setStatus(FormStatus.ERROR);\n                } else {\n                  forceRefreshDependentReports();\n                  if (clearParametersAfterSubmit) {\n                    const formFields = props?.settings?.formFields;\n                    if (formFields) {\n                      const entries = formFields.map((f) => f.settings);\n                      entries.forEach((entry) => {\n                        if (entry.disabled !== true) {\n                          if (entry.multiSelector) {\n                            props.setGlobalParameter && props.setGlobalParameter(entry.parameterName, []);\n                          } else {\n                            props.setGlobalParameter && props.setGlobalParameter(entry.parameterName, '');\n                          }\n                        }\n                      });\n                    }\n                  }\n                  if (hasSubmitMessage) {\n                    setStatus(FormStatus.SUBMITTED);\n                  } else {\n                    setStatus(FormStatus.DATA_ENTRY);\n                  }\n                }\n              });\n            }}\n          >\n            {buttonText}\n            <PlayIconSolid className='btn-icon-base-r' />\n          </Button>\n        ) : (\n          <></>\n        )}\n      </div>\n    );\n  }\n\n  // The form is running\n  if (status == FormStatus.RUNNING) {\n    return (\n      <div\n        className='n-col-span-2'\n        style={{\n          margin: '10px',\n          height: '355px',\n          overflow: 'hidden',\n        }}\n      >\n        {REPORT_LOADING_ICON}\n      </div>\n    );\n  }\n\n  const resetButton = (\n    <div>\n      <Button\n        color='neutral'\n        onClick={() => {\n          setStatus(FormStatus.DATA_ENTRY);\n        }}\n      >\n        {resetButtonText}\n      </Button>\n    </div>\n  );\n\n  // The user has succesfully completed the form\n  if (status == FormStatus.SUBMITTED) {\n    return (\n      <div className='content-center form-submitted-message' style={{ margin: '10px' }}>\n        {confirmationMessage}\n        {hasResetButton ? resetButton : <></>}\n      </div>\n    );\n  }\n\n  // The form query has failed, display the error\n  if (status == FormStatus.ERROR) {\n    return (\n      <div>\n        <div className='content-center' style={{ margin: '10px' }}>\n          Unable to submit form. A query error has occurred:\n        </div>\n        <NeoCodeViewerComponent\n          value={formResults && formResults[0] && formResults[0].error && formResults[0].error}\n          placeholder={'Unknown query error, check the browser console.'}\n        />\n        <div className='content-center' style={{ margin: '10px' }}>\n          {hasResetButton ? resetButton : <></>}\n        </div>\n      </div>\n    );\n  }\n};\n\nexport default NeoForm;\n"
  },
  {
    "path": "src/extensions/forms/settings/NeoFormCardSettings.tsx",
    "content": "// TODO: this file (in a way) belongs to chart/parameter/ParameterSelectionChart. It would make sense to move it there\n\nimport React, { useCallback, useContext, useEffect } from 'react';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport NeoCodeEditorComponent, {\n  DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE,\n} from '../../../component/editor/CodeEditorComponent';\nimport debounce from 'lodash/debounce';\nimport { Banner, IconButton } from '@neo4j-ndl/react';\nimport { PencilIconOutline, PlusIconOutline, XMarkIconOutline } from '@neo4j-ndl/react/icons';\nimport NeoFormCardSettingsModal from './NeoFormCardSettingsModal';\nimport { SortableList } from './list/NeoFormSortableList';\n\nconst NeoFormCardSettings = ({ query, database, settings, extensions, onReportSettingUpdate, onQueryUpdate }) => {\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n  if (!driver) {\n    throw new Error(\n      '`driver` not defined. Have you added it into your app as <Neo4jContext.Provider value={{driver}}> ?'\n    );\n  }\n  // Ensure that we only trigger a text update event after the user has stopped typing.\n  const [queryText, setQueryText] = React.useState(query);\n  const debouncedQueryUpdate = useCallback(debounce(onQueryUpdate, 250), []);\n  const formFields = settings.formFields ? settings.formFields : [];\n  const [selectedFieldIndex, setSelectedFieldIndex] = React.useState(-1);\n  const [fieldModalOpen, setFieldModalOpen] = React.useState(false);\n  const [indexedFormFields, setIndexedFormFields] = React.useState([]);\n\n  function updateCypherQuery(value) {\n    debouncedQueryUpdate(value);\n    setQueryText(value);\n  }\n\n  function updateFormFields(newFormFields) {\n    onReportSettingUpdate('formFields', newFormFields);\n  }\n\n  const addFieldButton = (\n    <div style={{ width: '100%', display: 'flex' }}>\n      <IconButton\n        className='form-add-parameter'\n        style={{ marginLeft: 'auto', marginRight: 'auto', marginTop: 5, marginBottom: 5 }}\n        aria-label='add'\n        size='medium'\n        floating\n        onClick={() => {\n          const newField = { type: 'Node Property', settings: {}, query: '' };\n          const newIndex = formFields.length;\n          updateFormFields(formFields.concat(newField));\n          setSelectedFieldIndex(newIndex);\n          setFieldModalOpen(true);\n        }}\n      >\n        <PlusIconOutline />\n      </IconButton>\n    </div>\n  );\n\n  useEffect(() => {\n    if (formFields && !(formFields.length == 0 && indexedFormFields.length == 0)) {\n      setIndexedFormFields(\n        formFields.map((f, index) => {\n          return { ...f, id: index + 1 };\n        })\n      );\n    }\n  }, [formFields]);\n\n  return (\n    <div>\n      <NeoFormCardSettingsModal\n        open={fieldModalOpen}\n        setOpen={setFieldModalOpen}\n        index={selectedFieldIndex}\n        formFields={formFields}\n        setFormFields={updateFormFields}\n        database={database}\n        extensions={extensions}\n      />\n\n      <div style={{ borderTop: '1px dashed lightgrey', width: '100%' }}>\n        <span>Fields:</span>\n        <div style={{ position: 'relative' }}>\n          <SortableList\n            items={indexedFormFields}\n            onChange={(e) => {\n              setIndexedFormFields([]);\n              updateFormFields(e);\n            }}\n            renderItem={(item, index) => (\n              <SortableList.Item id={index + 1}>\n                <Banner\n                  key={index + 1}\n                  id={`list${index}`}\n                  description={\n                    <div>\n                      <span style={{ lineHeight: '32px' }}>\n                        <SortableList.DragHandle />{' '}\n                        {formFields[index]?.settings?.parameterName\n                          ? `$${formFields[index].settings.parameterName}`\n                          : '(undefined)'}\n                      </span>\n                      <IconButton\n                        className='n-float-right'\n                        aria-label='remove field'\n                        size='small'\n                        onClick={() => {\n                          updateFormFields([...formFields.slice(0, index), ...formFields.slice(index + 1)]);\n                        }}\n                      >\n                        <XMarkIconOutline />\n                      </IconButton>\n                      <IconButton\n                        className='n-float-right'\n                        aria-label='edit field'\n                        size='small'\n                        onClick={() => {\n                          setSelectedFieldIndex(index);\n                          setFieldModalOpen(true);\n                        }}\n                      >\n                        <PencilIconOutline />\n                      </IconButton>\n                    </div>\n                  }\n                  style={{ width: '100%' }}\n                ></Banner>\n              </SortableList.Item>\n            )}\n          />\n          {addFieldButton}\n        </div>\n        <div style={{ borderTop: '1px dashed lightgrey', width: '100%' }}>\n          <span>Form Submission Query:</span>\n          <NeoCodeEditorComponent\n            value={queryText}\n            editable={true}\n            language={'cypher'}\n            onChange={(value) => {\n              updateCypherQuery(value);\n            }}\n            placeholder={`Enter Cypher here...`}\n          />\n          <div style={DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE}>\n            This query is executed when the user submits the form.\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default NeoFormCardSettings;\n"
  },
  {
    "path": "src/extensions/forms/settings/NeoFormCardSettingsModal.tsx",
    "content": "// TODO: this file (in a way) belongs to chart/parameter/ParameterSelectionChart. It would make sense to move it there\n\nimport React from 'react';\nimport { Button, Dialog } from '@neo4j-ndl/react';\nimport ParameterSelectCardSettings from '../../../chart/parameter/ParameterSelectCardSettings';\nimport NeoCardSettingsFooter from '../../../card/settings/CardSettingsFooter';\nimport { objMerge } from '../../../utils/ObjectManipulation';\n\nconst NeoFormCardSettingsModal = ({ open, setOpen, index, formFields, setFormFields, database, extensions }) => {\n  const [advancedSettingsOpen, setAdvancedSettingsOpen] = React.useState(false);\n\n  return (\n    <Dialog\n      className='dialog-l'\n      open={open}\n      onClose={() => setOpen(false)}\n      style={{ overflow: 'inherit', overflowY: 'auto' }}\n      aria-labelledby='form-dialog-title'\n    >\n      <Dialog.Header id='form-dialog-title'>Editing Form Field #{index + 1}</Dialog.Header>\n      <Dialog.Content style={{ overflow: 'inherit' }}>\n        {formFields[index] ? (\n          <>\n            <ParameterSelectCardSettings\n              query={formFields[index].query}\n              type={'select'}\n              database={database}\n              settings={objMerge({ inputMode: 'cypher' }, formFields[index].settings)}\n              extensions={extensions}\n              onReportSettingUpdate={(key, value) => {\n                const newFormFields = [...formFields];\n                newFormFields[index].settings[key] = value;\n                if (key == 'type') {\n                  newFormFields[index].type = value;\n                }\n                setFormFields(newFormFields);\n              }}\n              onQueryUpdate={(query) => {\n                const newFormFields = [...formFields];\n\n                newFormFields[index].query = query;\n                setFormFields(newFormFields);\n              }}\n            />\n\n            <Button\n              onClick={() => {\n                setOpen(false);\n              }}\n              size='medium'\n              floating\n              style={{ float: 'right' }}\n            >\n              Save\n            </Button>\n            <br />\n            <br />\n            <NeoCardSettingsFooter\n              type={'select'}\n              reportSettings={formFields[index].settings}\n              reportSettingsOpen={advancedSettingsOpen}\n              onToggleReportSettings={() => setAdvancedSettingsOpen(!advancedSettingsOpen)}\n              onReportSettingUpdate={(key, value) => {\n                const newFormFields = [...formFields];\n                newFormFields[index].settings[key] = value;\n                setFormFields(newFormFields);\n              }}\n            />\n          </>\n        ) : (\n          <></>\n        )}\n      </Dialog.Content>\n    </Dialog>\n  );\n};\n\nexport default NeoFormCardSettingsModal;\n"
  },
  {
    "path": "src/extensions/forms/settings/list/NeoFormSortableItem.tsx",
    "content": "import React, { createContext, useContext, useMemo } from 'react';\nimport type { CSSProperties, PropsWithChildren } from 'react';\nimport type { DraggableSyntheticListeners, UniqueIdentifier } from '@dnd-kit/core';\nimport { useSortable } from '@dnd-kit/sortable';\nimport { CSS } from '@dnd-kit/utilities';\n\ninterface Props {\n  id: UniqueIdentifier;\n}\n\ninterface Context {\n  attributes: Record<string, any>;\n  listeners: DraggableSyntheticListeners;\n  ref(node: HTMLElement | null): void;\n}\n\nconst SortableItemContext = createContext<Context>({\n  attributes: {},\n  listeners: undefined,\n  ref() {},\n});\n\nexport function SortableItem({ children, id }: PropsWithChildren<Props>) {\n  const { attributes, isDragging, listeners, setNodeRef, setActivatorNodeRef, transform, transition } = useSortable({\n    id,\n  });\n  const context = useMemo(\n    () => ({\n      attributes,\n      listeners,\n      ref: setActivatorNodeRef,\n    }),\n    [attributes, listeners, setActivatorNodeRef]\n  );\n  const style: CSSProperties = {\n    opacity: isDragging ? 0.4 : undefined,\n    transform: CSS.Translate.toString(transform),\n    transition,\n  };\n\n  return (\n    <SortableItemContext.Provider value={context}>\n      <div className='SortableItem' ref={setNodeRef} style={style}>\n        {children}\n      </div>\n    </SortableItemContext.Provider>\n  );\n}\n\nexport function DragHandle() {\n  const { attributes, listeners, ref } = useContext(SortableItemContext);\n\n  return (\n    <button className='DragHandle' {...attributes} {...listeners} ref={ref}>\n      <svg viewBox='0 0 20 20' width='12'>\n        <path d='M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z'></path>\n      </svg>\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/extensions/forms/settings/list/NeoFormSortableList.tsx",
    "content": "import React, { useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';\nimport type { Active, UniqueIdentifier } from '@dnd-kit/core';\nimport { SortableContext, arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable';\nimport { DragHandle, SortableItem } from './NeoFormSortableItem';\nimport { SortableOverlay } from './NeoFormSortableOverlay';\nimport { createPortal } from 'react-dom';\n\ninterface BaseItem {\n  id: UniqueIdentifier;\n}\n\ninterface Props<T extends BaseItem> {\n  items: T[];\n  onChange(items: T[]): void;\n  renderItem(item: T, index: number): ReactNode;\n}\n\nexport function SortableList<T extends BaseItem>({ items, onChange, renderItem }: Props<T>) {\n  const [active, setActive] = useState<Active | null>(null);\n  const activeItem = useMemo(() => items.find((item) => item.id === active?.id), [active, items]);\n  const sensors = useSensors(\n    useSensor(PointerSensor),\n    useSensor(KeyboardSensor, {\n      coordinateGetter: sortableKeyboardCoordinates,\n    })\n  );\n\n  return (\n    <DndContext\n      sensors={sensors}\n      onDragStart={({ active }) => {\n        setActive(active);\n      }}\n      onDragEnd={({ active, over }) => {\n        if (over && active.id !== over?.id) {\n          const activeIndex = items.findIndex(({ id }) => id === active.id);\n          const overIndex = items.findIndex(({ id }) => id === over.id);\n\n          onChange(arrayMove(items, activeIndex, overIndex));\n        }\n        setActive(null);\n      }}\n      onDragCancel={() => {\n        setActive(null);\n      }}\n    >\n      <SortableContext items={items}>\n        <div className='SortableList' role='application'>\n          {items.map((item, index) => (\n            <React.Fragment key={index}>{renderItem(item, index)}</React.Fragment>\n          ))}\n        </div>\n      </SortableContext>\n      {createPortal(\n        <SortableOverlay>\n          {activeItem !== undefined ? renderItem(activeItem, items.indexOf(activeItem)) : null}\n        </SortableOverlay>,\n        document.body\n      )}\n    </DndContext>\n  );\n}\n\nSortableList.Item = SortableItem;\nSortableList.DragHandle = DragHandle;\n"
  },
  {
    "path": "src/extensions/forms/settings/list/NeoFormSortableOverlay.tsx",
    "content": "import type { PropsWithChildren } from 'react';\nimport { DragOverlay, defaultDropAnimationSideEffects } from '@dnd-kit/core';\nimport type { DropAnimation } from '@dnd-kit/core';\nimport React from 'react';\n\nconst dropAnimationConfig: DropAnimation = {\n  sideEffects: defaultDropAnimationSideEffects({\n    styles: {\n      active: {\n        opacity: '0.4',\n      },\n    },\n  }),\n};\n\nexport function SortableOverlay({ children }: PropsWithChildren) {\n  return <DragOverlay dropAnimation={dropAnimationConfig}>{children}</DragOverlay>;\n}\n"
  },
  {
    "path": "src/extensions/query-translator/component/OverrideCardQueryEditor.tsx",
    "content": "import React, { useCallback, useContext, useEffect } from 'react';\nimport { connect } from 'react-redux';\nimport { Button, Switch } from '@neo4j-ndl/react';\nimport NeoCodeEditorComponent, {\n  DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE,\n} from '../../../component/editor/CodeEditorComponent';\nimport { getReportTypes } from '../../ExtensionUtils';\nimport { queryTranslationThunk } from '../state/QueryTranslatorThunks';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport debounce from 'lodash/debounce';\nimport { updateLastMessage } from '../state/QueryTranslatorActions';\nimport { createNotification } from '../../../application/ApplicationActions';\nimport { getLastMessage, QUERY_TRANSLATOR_EXTENSION_NAME } from '../state/QueryTranslatorSelector';\nimport { GPT_LOADING_ICON } from './LoadingIcon';\nimport {\n  deleteSessionStoragePrepopulationReportFunction,\n  setSessionStoragePrepopulationReportFunction,\n} from '../../state/ExtensionActions';\nimport { getPrepopulateReportExtension } from '../../state/ExtensionSelectors';\n\n// TODO: right now if we change the database in the cardSelector, it should forgot the card history\nexport const NeoOverrideCardQueryEditor = ({\n  pagenumber,\n  reportId,\n  cypherQuery,\n  extensions,\n  reportType,\n  updateCypherQuery,\n  lastMessage,\n  prepopulateExtensionName,\n  onExecute,\n  translateQuery,\n  updateEnglishQuery,\n  displayError,\n  setPrepopulationReportFunction,\n  deletePrepopulationReportFunction,\n}) => {\n  enum Language {\n    ENGLISH = 0,\n    CYPHER = 1,\n  }\n\n  const [language, setLanguage] = React.useState(Language.CYPHER);\n  const [runningTranslation, setRunningTranslation] = React.useState(false);\n  const [englishQuestion, setEnglishQuestion] = React.useState('');\n  const debouncedEnglishQuestionUpdate = useCallback(debounce(updateEnglishQuery, 250), []);\n\n  useEffect(() => {\n    // Reset text to the dashboard state when the page gets reorganized.\n    if (lastMessage !== englishQuestion) {\n      setEnglishQuestion(lastMessage);\n    }\n  }, [lastMessage]);\n\n  const reportTypes = getReportTypes(extensions);\n\n  const cypherEditor = (\n    <NeoCodeEditorComponent\n      value={cypherQuery}\n      editable={true}\n      onExecute={onExecute}\n      language={\n        reportTypes[reportType] && reportTypes[reportType].inputMode ? reportTypes[reportType].inputMode : 'cypher'\n      }\n      onChange={(value) => updateCypherQuery(value)}\n      placeholder={`Enter Cypher here...`}\n    />\n  );\n\n  function updateEnglishQuestion(value) {\n    debouncedEnglishQuestionUpdate(pagenumber, reportId, value);\n    setEnglishQuestion(value);\n  }\n\n  // To prevent a bug with the code editor component, we wrap it in an extra enclosing bracket.\n  const englishEditor = (\n    <div>\n      <NeoCodeEditorComponent\n        value={englishQuestion}\n        editable={true}\n        language={'markdown'}\n        onChange={(value) => {\n          setPrepopulationReportFunction(reportId);\n          updateEnglishQuestion(value);\n        }}\n        style={{ border: '1px dashed darkgrey' }}\n        placeholder={`Enter English here...`}\n      />\n    </div>\n  );\n\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n\n  function triggerTranslation() {\n    setRunningTranslation(true);\n    translateQuery(\n      pagenumber,\n      reportId,\n      englishQuestion,\n      reportType,\n      driver,\n      () => {\n        setRunningTranslation(false);\n      },\n      (e) => {\n        setRunningTranslation(false);\n        displayError(e);\n      }\n    );\n  }\n\n  return (\n    <div>\n      {runningTranslation ? (\n        <div style={{ height: 150, border: '1px dashed grey', position: 'relative' }}>{GPT_LOADING_ICON}</div>\n      ) : (\n        <>\n          <table style={{ marginBottom: 5, width: '100%' }}>\n            <tr>\n              <td style={{ width: 50 }}>Cypher</td>\n              <td style={{ width: 50 }}>\n                <Switch\n                  style={{ backgroundColor: 'grey' }}\n                  checked={language == Language.ENGLISH}\n                  onChange={() => {\n                    if (language == Language.ENGLISH) {\n                      setLanguage(Language.CYPHER);\n                      deletePrepopulationReportFunction(reportId);\n                    } else {\n                      setLanguage(Language.ENGLISH);\n                    }\n                  }}\n                  className='n-ml-2'\n                />\n              </td>\n              <td style={{ width: 70 }}>&nbsp; English</td>\n              <td style={{ width: '100px', float: 'right' }}>\n                {/* Only show translation button if there's something new to translate */}\n                {language == Language.ENGLISH ? (\n                  <Button\n                    fill='outlined'\n                    disabled={prepopulateExtensionName == undefined}\n                    style={{ float: 'right' }}\n                    onClick={() => {\n                      if (prepopulateExtensionName !== undefined) {\n                        triggerTranslation();\n                        setLanguage(Language.CYPHER);\n                        deletePrepopulationReportFunction(reportId);\n                      }\n                    }}\n                  >\n                    Translate\n                  </Button>\n                ) : (\n                  <></>\n                )}\n              </td>\n            </tr>\n          </table>\n          {language == Language.CYPHER ? cypherEditor : englishEditor}\n          <div\n            style={\n              language == Language.CYPHER\n                ? DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE\n                : {\n                    ...DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE,\n                    // color: '#006FD6',\n                    borderBottom: '1px dashed darkgrey',\n                    borderLeft: '1px dashed darkgrey',\n                    borderRight: '1px dashed darkgrey',\n                  }\n            }\n          >\n            {language == Language.ENGLISH ? (\n              <>\n                For best results, use a descriptive question. See also the{' '}\n                <a\n                  target='_blank'\n                  style={{ textDecoration: 'underline' }}\n                  href='https://github.com/neo4j-labs/neodash/tree/master/docs/modules/ROOT/pages/user-guide/extensions/natural-language-queries.adoc'\n                >\n                  documentation\n                </a>\n                .\n              </>\n            ) : (\n              reportTypes[reportType] && reportTypes[reportType].helperText\n            )}\n          </div>\n        </>\n      )}\n    </div>\n  );\n};\n\nconst mapStateToProps = (state, ownProps) => ({\n  lastMessage: getLastMessage(state, ownProps.pagenumber, ownProps.reportId),\n  prepopulateExtensionName: getPrepopulateReportExtension(state, ownProps.reportId),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  translateQuery: (pagenumber, reportId, text, reportType, driver, onComplete, onError, onRetry) => {\n    dispatch(queryTranslationThunk(pagenumber, reportId, text, reportType, driver, onComplete, onError, onRetry));\n  },\n  updateEnglishQuery: (pagenumber, reportId, message) => {\n    dispatch(updateLastMessage(message, pagenumber, reportId));\n  },\n  displayError: (message) => {\n    dispatch(createNotification('Error when translating the natural language query', message));\n  },\n  setPrepopulationReportFunction: (reportId) => {\n    dispatch(setSessionStoragePrepopulationReportFunction(reportId, QUERY_TRANSLATOR_EXTENSION_NAME));\n  },\n  deletePrepopulationReportFunction: (reportId) => {\n    dispatch(deleteSessionStoragePrepopulationReportFunction(reportId));\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoOverrideCardQueryEditor);\n"
  },
  {
    "path": "src/extensions/rbac/RBACManagementLabelButton.tsx",
    "content": "import React from 'react';\nimport { connect } from 'react-redux';\nimport { IconButton, MenuItem } from '@neo4j-ndl/react';\nimport { UserCircleIconOutline } from '@neo4j-ndl/react/icons';\nimport { RBACManagementMenu } from './RBACManagementMenu';\n\nimport Tooltip from '@mui/material/Tooltip/Tooltip';\nimport { createNotificationThunk } from '../../page/PageThunks';\n\nconst RBACManagementLabelButton = ({ createNotification }) => {\n  const [MenuOpen, setMenuOpen] = React.useState(false);\n  const [anchorEl, setAnchorEl] = React.useState(null);\n\n  const handleButtonClick = (event) => {\n    setMenuOpen(true);\n    setAnchorEl(event.currentTarget);\n  };\n\n  const handleClose = () => {\n    setMenuOpen(false);\n  };\n\n  const button = (\n    <Tooltip title='Access Control' aria-label='Access Control' disableInteractive>\n      <IconButton className='n-mx-1' aria-label='Access Control' onClick={handleButtonClick}>\n        <UserCircleIconOutline />\n      </IconButton>\n    </Tooltip>\n  );\n\n  return (\n    <div style={{ display: 'inline' }}>\n      {button}\n      <RBACManagementMenu\n        anchorEl={anchorEl}\n        MenuOpen={MenuOpen}\n        handleClose={handleClose}\n        createNotification={createNotification}\n      />\n    </div>\n  );\n};\n\nconst mapStateToProps = () => ({});\n\nconst mapDispatchToProps = (dispatch) => ({\n  createNotification: (title: any, message: any) => {\n    dispatch(createNotificationThunk(title, message));\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(RBACManagementLabelButton);\n"
  },
  {
    "path": "src/extensions/rbac/RBACManagementMenu.tsx",
    "content": "import React, { useEffect, useState, useContext } from 'react';\nimport { Menu, MenuItem, MenuItems } from '@neo4j-ndl/react';\nimport { UserIconOutline } from '@neo4j-ndl/react/icons';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport { QueryStatus, runCypherQuery } from '../../report/ReportQueryRunner';\nimport RBACManagementModal from './RBACManagementModal';\n\n/**\n * Component for providing a menu of all the roles in the neo4j database to the user whenever they press on the\n * RBACManagementLabelButton.\n */\nexport const RBACManagementMenu = ({ anchorEl, MenuOpen, handleClose, createNotification }) => {\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n  const [roles, setRoles] = useState([]);\n  const [selectedRole, setSelectedRole] = useState(null);\n  const [isModalOpen, setIsModalOpen] = useState(false);\n\n  useEffect(() => {\n    if (!MenuOpen) {\n      return;\n    }\n    const query = `SHOW PRIVILEGES YIELD role, action WHERE role <> \"PUBLIC\" RETURN role, 'dbms_actions' in collect(action)`;\n    runCypherQuery(\n      driver,\n      'system',\n      query,\n      {},\n      1000,\n      () => {},\n      (records) => {\n        if (records[0].error) {\n          createNotification('Unable to retrieve roles', records[0].error);\n          return;\n        }\n        // Only display roles which are not able to do 'dbms_actions', i.e. they are not admins.\n        setRoles(records.filter((r) => r._fields[1] == false).map((record) => record._fields[0]));\n      }\n    );\n  }, [MenuOpen]);\n\n  if (roles.length == 0) {\n    return <></>;\n  }\n\n  const handleRoleClicked = (role) => {\n    handleClose();\n    setSelectedRole(role);\n    setIsModalOpen(true);\n  };\n\n  return (\n    <>\n      <Menu\n        anchorOrigin={{\n          horizontal: 'right',\n          vertical: 'bottom',\n        }}\n        transformOrigin={{\n          horizontal: 'right',\n          vertical: 'top',\n        }}\n        anchorEl={anchorEl}\n        open={MenuOpen}\n        onClose={handleClose}\n        size='small'\n      >\n        <MenuItems className='n-overflow-y-scroll n-h-44'>\n          {roles.map((role) => (\n            <MenuItem key={role} onClick={() => handleRoleClicked(role)} icon={<UserIconOutline />} title={role} />\n          ))}\n        </MenuItems>\n      </Menu>\n\n      <RBACManagementModal\n        open={isModalOpen}\n        handleClose={() => {\n          setIsModalOpen(false);\n        }}\n        currentRole={selectedRole}\n        createNotification={createNotification}\n      />\n    </>\n  );\n};\n\nexport default RBACManagementMenu;\n"
  },
  {
    "path": "src/extensions/rbac/RBACManagementModal.tsx",
    "content": "import React, { useEffect, useState, useContext } from 'react';\nimport { Button, Dialog, Dropdown } from '@neo4j-ndl/react';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport {\n  Operation,\n  retrieveAllowAndDenyLists,\n  retrieveDatabaseList,\n  retrieveLabelsList,\n  retrieveNeo4jUsers,\n  updatePrivileges,\n  updateUsers,\n} from './RBACUtils';\n/**\n * Configures RBAC Access Control Management for a certain role on certain labels and attaches the roles to specific users.\n * @param open - Whether the modal is open or not.\n * @param currentRole - The currently selected role.\n * @param handleClose - The function to close the modal.\n */\nexport const RBACManagementModal = ({ open, handleClose, currentRole, createNotification }) => {\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n  const [neo4jUsers, setNeo4jUsers] = useState([]);\n  const [selectedUsers, setSelectedUsers] = useState([]);\n  const [selectedDatabase, setSelectedDatabase] = useState('');\n  const [databases, setDatabases] = useState([]);\n  const [loaded, setLoaded] = useState(false);\n  const [labels, setLabels] = useState([]);\n  const [allowList, setAllowList] = useState([]);\n  const [denyList, setDenyList] = useState([]);\n  const [fixedAllowList, setFixedAllowList] = useState([]);\n  const [fixedDenyList, setFixedDenyList] = useState([]);\n  const [denyCompleted, setDenyCompleted] = useState(false);\n  const [allowCompleted, setAllowCompleted] = useState(false);\n  const [usersCompleted, setUsersCompleted] = useState(false);\n  const [failed, setFailed] = useState(false);\n  const [isDatabaseEmpty, setIsDatabaseEmpty] = useState(false);\n\n  useEffect(() => {\n    if (!open) {\n      setSelectedUsers([]);\n      setAllowList([]);\n      setDenyList([]);\n      setSelectedDatabase('');\n      return;\n    }\n    setDenyCompleted(false);\n    setAllowCompleted(false);\n    setUsersCompleted(false);\n    setFailed(false);\n    retrieveDatabaseList(driver, setDatabases);\n    retrieveNeo4jUsers(driver, currentRole, setNeo4jUsers, setSelectedUsers);\n  }, [open]);\n\n  useEffect(() => {\n    if (failed !== false) {\n      createNotification('Unable to update privileges', `${failed}`);\n    } else if (denyCompleted && allowCompleted && usersCompleted) {\n      createNotification('Success', `Access for role '${currentRole}' updated.`);\n    }\n  }, [denyCompleted, allowCompleted, usersCompleted, failed]);\n\n  const parseLabelsList = (database, records) => {\n    const allLabels = records.map((record) => record._fields[0]).filter((l) => l !== '_Neodash_Dashboard');\n    retrieveAllowAndDenyLists(\n      driver,\n      database,\n      currentRole,\n      allLabels,\n      setLabels,\n      setAllowList,\n      setDenyList,\n      setFixedAllowList,\n      setFixedDenyList,\n      setLoaded\n    );\n  };\n\n  const handleDatabaseSelect = (selectedOption) => {\n    setSelectedDatabase(selectedOption.value);\n    setLabels([]);\n    setAllowList([]);\n    setDenyList([]);\n    retrieveLabelsList(driver, selectedOption.value, (records) => {\n      if (records.length === 0) {\n        setIsDatabaseEmpty(true);\n      } else {\n        parseLabelsList(selectedOption.value, records);\n        setIsDatabaseEmpty(false);\n      }\n    });\n  };\n\n  const handleSave = async () => {\n    createNotification('Updating', `Access for role '${currentRole}' is being updated, please wait...`);\n    try {\n      await updateUsers(\n        driver,\n        currentRole,\n        neo4jUsers,\n        selectedUsers,\n        () => setUsersCompleted(true),\n        (failReason) => setFailed(`Operation 'ROLE-USER ASSIGNMENT' failed.\\n Reason: ${failReason}`)\n      );\n\n      if (selectedDatabase && labels.length > 0) {\n        // Check if there are labels to update\n        const nonFixedDenyList = denyList.filter((n) => !fixedDenyList.includes(n));\n        const nonFixedAllowList = allowList.filter((n) => !fixedDenyList.includes(n));\n\n        await updatePrivileges(\n          driver,\n          selectedDatabase,\n          currentRole,\n          labels,\n          nonFixedDenyList,\n          Operation.DENY,\n          () => setDenyCompleted(true),\n          (failReason) => setFailed(`Operation 'DENY LABEL ACCESS' failed.\\n Reason: ${failReason}`)\n        );\n\n        await updatePrivileges(\n          driver,\n          selectedDatabase,\n          currentRole,\n          labels,\n          nonFixedAllowList,\n          Operation.GRANT,\n          () => setAllowCompleted(true),\n          (failReason) => setFailed(`Operation 'ALLOW LABEL ACCESS' failed.\\n Reason: ${failReason}`)\n        );\n      } else {\n        // Since there is no database or labels selected, we don't run the DENY/ALLOW queries.\n        // We just mark them as completed so the success message shows up.\n        setDenyCompleted(true);\n        setAllowCompleted(true);\n      }\n    } catch (error) {\n      // Handle any errors that occur during the update process\n      createNotification('error', `An error occurred: ${error.message}`);\n    } finally {\n      handleClose();\n    }\n  };\n\n  return (\n    <Dialog size='large' open={open} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>Access Control - '{currentRole}'</Dialog.Header>\n      <Dialog.Content>\n        This screen lets you handle user assignment and access control for a specific role.\n        <br />\n        For more information, please refer to the{' '}\n        <a\n          href='https://github.com/neo4j-labs/neodash/tree/master/docs/modules/ROOT/pages/user-guide/extensions/access-control-management.adoc'\n          target='_blank'\n          rel='noopener noreferrer'\n          style={{ color: 'blue', textDecoration: 'underline' }}\n        >\n          documentation\n        </a>\n        .\n        <br />\n        <div>\n          <br />\n          <h5>Manage Users</h5>\n          <p>Select a list of users to assign to the current role.</p>\n          <Dropdown\n            type='select'\n            selectProps={{\n              value: selectedUsers.map((user) => ({ value: user, label: user })),\n              options: neo4jUsers.map((user) => ({ value: user, label: user })),\n              isMulti: true,\n              onChange: (val) => setSelectedUsers(val.map((v) => v.value)),\n            }}\n          />\n        </div>\n        <div>\n          <br />\n          <h5>Label Access</h5>\n          <p>For a given database, control what labels the role is or is not allowed to see.</p>\n          <Dropdown\n            type='select'\n            label='Database'\n            // errorText={!selectedDatabase && 'Please choose a database in order to proceed'}\n            selectProps={{\n              value: { value: selectedDatabase, label: selectedDatabase },\n              placeholder: 'Select a database',\n              options: databases\n                .filter((database) => database !== 'system')\n                .map((database) => ({ value: database, label: database })),\n              onChange: handleDatabaseSelect,\n            }}\n          />\n          {selectedDatabase && isDatabaseEmpty && (\n            <p style={{ color: 'red' }}>\n              This database is currently empty. Please select a different database or add labels to manage access.\n            </p>\n          )}\n        </div>\n        {selectedDatabase && !isDatabaseEmpty && loaded && (\n          <>\n            <br />\n            <div style={{ display: 'flex', justifyContent: 'space-between' }}>\n              <div style={{ width: '45%' }}>\n                <Dropdown\n                  type='select'\n                  label='Allow List'\n                  helpText={\n                    allowList.find((i) => i == '*') &&\n                    'Selecting (*) grants access to all labels, overriding other selections.'\n                  }\n                  selectProps={{\n                    placeholder: 'Select labels',\n                    isClearable: false,\n                    value: allowList.map((nodelabel) => ({ value: nodelabel, label: nodelabel })),\n                    options: labels.map((nodelabel) => ({ value: nodelabel, label: nodelabel })),\n                    isMulti: true,\n                    onChange: (val) => {\n                      // Make sure that only database-specific label access rules can be changed from this UI.\n                      if (fixedAllowList.every((v) => val.map((selected) => selected.value).includes(v))) {\n                        setAllowList(val.map((v) => v.value));\n                      } else {\n                        createNotification(\n                          'Label cannot be removed',\n                          'The selected label is allowed access across all databases. You cannot remove this privilege using this interface.'\n                        );\n                      }\n                    },\n                  }}\n                />\n              </div>\n              <div style={{ width: '45%' }}>\n                <Dropdown\n                  type='select'\n                  label='Deny List'\n                  helpText={\n                    denyList.find((i) => i == '*') &&\n                    'Selecting (*) denies access to all labels, overriding other selections.'\n                  }\n                  selectProps={{\n                    placeholder: 'Select labels',\n                    isClearable: false,\n                    value: denyList.map((nodelabel) => ({ value: nodelabel, label: nodelabel })),\n                    options: labels\n                      .filter((l) => l !== '*')\n                      .map((nodelabel) => ({ value: nodelabel, label: nodelabel })),\n                    isMulti: true,\n                    onChange: (val) => {\n                      // Make sure that only database-specific label access rules can be changed from this UI.\n                      if (fixedDenyList.every((v) => val.map((selected) => selected.value).includes(v))) {\n                        setDenyList(val.map((v) => v.value));\n                      } else {\n                        createNotification(\n                          'Label cannot be removed',\n                          'The selected label is denied access across all databases. You cannot remove this privilege using this interface.'\n                        );\n                      }\n                    },\n                  }}\n                />\n              </div>\n            </div>\n          </>\n        )}\n      </Dialog.Content>\n      <Dialog.Actions>\n        <Button onClick={handleClose} style={{ float: 'right' }} fill='outlined' floating>\n          Cancel\n        </Button>\n        <Button onClick={handleSave} color='primary' style={{ float: 'right', marginRight: '10px' }} floating>\n          Save\n        </Button>\n      </Dialog.Actions>\n    </Dialog>\n  );\n};\n\nexport default RBACManagementModal;\n"
  },
  {
    "path": "src/extensions/rbac/RBACUtils.ts",
    "content": "import { QueryStatus, runCypherQuery } from '../../report/ReportQueryRunner';\n\nexport enum Operation {\n  GRANT,\n  DENY,\n}\n\n/**\n * Sets the privileges for a role to a new list provided by the user.\n * This involves wiping old privileges, including a special case for '*' privileges.\n * @param driver the Neo4j driver.\n * @param database a database name for which Privileges must be changed.\n * @param role role for which privileges are updated.\n * @param allLabels list of all labels in the given database.\n * @param newLabels list of new labels in the database, for which priveleges are changed.\n * @param operation The operation, either 'GRANT' or 'DENY'\n */\nexport async function updatePrivileges(\n  driver,\n  database,\n  role,\n  allLabels,\n  newLabels,\n  operation: Operation,\n  onSuccess,\n  onFail\n) {\n  // TODO - should we also drop cross-database DENYs (`ON GRAPH *`) to catch the true full set?\n  // TODO - there\n  // 1. Special case for '*'. Create it if needed to be there, otherwise revoke it.\n  runCypherQuery(\n    driver,\n    'system',\n    buildAccessQuery(database, role, ['*'], operation, !newLabels.includes('*')),\n    {},\n    1000,\n    (status) => {\n      if (status == QueryStatus.NO_DATA || QueryStatus.COMPLETE) {\n        // 2. Build the query that revokes all possible priveleges, returning to a 'blank slate'\n        runCypherQuery(\n          driver,\n          'system',\n          buildAccessQuery(\n            database,\n            role,\n            allLabels.filter((l) => l !== '*'),\n            operation,\n            true\n          ),\n          {},\n          1000,\n          (status) => {\n            if (status == QueryStatus.NO_DATA || status == QueryStatus.COMPLETE) {\n              //  TODO: Neo4j is very slow in updating after the previous query, even though it is technically a finished query.\n              // We build in an artificial delay...\n              const timeout = setTimeout(() => {\n                // 3. Create the new privileges as specified in the `newLabels` list by the user.\n                if (newLabels.filter((l) => l !== '*').length > 0) {\n                  runCypherQuery(\n                    driver,\n                    'system',\n                    buildAccessQuery(\n                      database,\n                      role,\n                      newLabels.filter((l) => l !== '*'),\n                      operation,\n                      false\n                    ),\n                    {},\n                    1000,\n                    (status) => {\n                      if (status == QueryStatus.NO_DATA || status == QueryStatus.COMPLETE) {\n                        onSuccess();\n                      }\n                    },\n                    (records) => {\n                      if (records && records[0] && records[0].error) {\n                        onFail(records[0].error);\n                      }\n                    }\n                  );\n                } else {\n                  onSuccess();\n                }\n              }, 1000);\n            }\n          },\n          (records) => {\n            if (records && records[0] && records[0].error) {\n              onFail(records[0].error);\n            }\n          }\n        );\n      }\n    },\n    (records) => {\n      if (records && records[0] && records[0].error) {\n        onFail(records[0].error);\n      }\n    }\n  );\n}\n\n/**\n * Generic query builder for adding/removing grants/denies for a list of labels.\n * @param database the database to grant/deny on.\n * @param role the role to create access rules for.\n * @param labels a list of node labels\n * @param access the access type. Can be \"GRANT\" or \"DENY\"\n * @param revoke Whether to revoke access or not.\n * @returns\n */\nfunction buildAccessQuery(database, role, labels, operation: Operation, revoke: boolean): string {\n  const query = `${revoke ? 'REVOKE' : ''} \n            ${operation == Operation.DENY ? 'DENY' : 'GRANT'} \n            MATCH {*} ON GRAPH ${database} \n            NODES ${labels.join(',')} \n            ${revoke ? 'FROM' : 'TO'} ${role}`;\n  return query;\n}\n\n/**\n * Retrieve allow and deny lists for a selected role, and a given database.\n * @param driver Neo4j driver object.\n * @param database the user's selected database.\n * @param currentRole the user's selected role.\n * @param allLabels list of all labels in the database (retrieved seperately)\n * @param setLabels callback to update the list of all labels with any more that may only exist in priveleges\n * @param setAllowList callback to update the allow list retrieved from the database.\n * @param setDenyList callback to update the deny list retrieved from the database.\n * @param setLoaded callback to indicate the retrieval is completed.\n */\nexport const retrieveAllowAndDenyLists = (\n  driver,\n  database,\n  currentRole,\n  allLabels,\n  setLabels,\n  setAllowList,\n  setDenyList,\n  setFixedAllowList,\n  setFixedDenyList,\n  setLoaded\n) => {\n  runCypherQuery(\n    driver,\n    'system',\n    `SHOW PRIVILEGES\n      YIELD graph, role, access, action, segment\n      WHERE (graph = $database OR graph = '*')\n      AND role = $rolename\n      AND action = 'match' \n      AND segment STARTS WITH 'NODE('\n      RETURN access, collect(substring(segment, 5, size(segment)-6)) as nodes, graph = \"*\" as fixed`,\n    { rolename: currentRole, database: database },\n    1000,\n    (status) => {\n      if (status == QueryStatus.NO_DATA) {\n        setLabels(['*'].concat(allLabels));\n        setLoaded(true);\n      }\n    },\n    (records) => {\n      // Extract granted and denied label list from the result of the SHOW PRIVILEGES query\n      const grants = records.filter((r) => r._fields[0] == 'GRANTED' && r._fields[2] == false);\n      const denies = records.filter((r) => r._fields[0] == 'DENIED' && r._fields[2] == false);\n      const grantedLabels = grants[0] ? [...new Set(grants[0]._fields[1])] : [];\n      const deniedLabels = denies[0] ? [...new Set(denies[0]._fields[1])] : [];\n\n      // Do the same for fixed grants (those stored under the '*' graph permission)\n      const fixedGrants = records.filter((r) => r._fields[0] == 'GRANTED' && r._fields[2] == true);\n      const fixedDenies = records.filter((r) => r._fields[0] == 'DENIED' && r._fields[2] == true);\n      const fixedGrantedLabels = fixedGrants[0] ? [...new Set(fixedGrants[0]._fields[1])] : [];\n      const fixedDeniedLabels = fixedDenies[0] ? [...new Set(fixedDenies[0]._fields[1])] : [];\n\n      setAllowList([...new Set(grantedLabels.concat(fixedGrantedLabels))]);\n      setDenyList([...new Set(deniedLabels.concat(fixedDeniedLabels))]);\n      setFixedAllowList(fixedGrantedLabels);\n      setFixedDenyList(fixedDeniedLabels);\n\n      // Here we build a set of all POSSIBLE labels, that includes the list in the database, plus those in denies and grants.\n      const possibleLabels = [...new Set(allLabels.concat(grantedLabels).concat(deniedLabels))];\n      // Add '*' as an extra option.\n      setLabels(['*'].concat(possibleLabels));\n      setLoaded(true);\n    }\n  );\n};\n\n/**\n * Retrieve the set of all users from the database.\n * @param driver Neo4j driver object with active session.\n * @param  currentRole selected role.\n * @param setNeo4jUsers callback to update the list of all users.\n * @param setRoleUsers callback to update the list of role-specific users.\n */\nexport const retrieveNeo4jUsers = (driver, currentRole, setNeo4jUsers, setRoleUsers) => {\n  runCypherQuery(\n    driver,\n    'system',\n    'SHOW users yield user, roles return user, roles',\n    {},\n    1000,\n    () => {},\n    (records) => {\n      const roleRecords = records.filter((r) => r._fields[1].includes(currentRole));\n      setRoleUsers(roleRecords.map((record) => record._fields[0]));\n      setNeo4jUsers(records.map((record) => record._fields[0]));\n    }\n  );\n};\n\n/**\n * retrieve the list of labels in a given database from the dbms.\n * @param driver Neo4j driver object.\n * @param database selected database.\n * @param setLabels callback to update the list of labels.\n */\nexport function retrieveLabelsList(driver, database: any, setLabels: (records: any) => void) {\n  let labelsSet = false; // Flag to track if setLabels was called\n\n  // Wrapper around the original setLabels to set the flag when called\n  const wrappedSetLabels = (records) => {\n    labelsSet = true;\n    setLabels(records);\n  };\n\n  runCypherQuery(driver, database, 'CALL db.labels()', {}, 1000, () => {}, wrappedSetLabels)\n    .then(() => {\n      if (!labelsSet) {\n        setLabels([]);\n      }\n    })\n    .catch((error) => {\n      console.error('Error retrieving labels:', error);\n      setLabels([]);\n    });\n}\n\n/**\n * retrieve the list of databases in a DBMS.\n * @param driver Neo4j driver with active session\n * @param setDatabases callback to update the list of databases.\n */\nexport function retrieveDatabaseList(driver, setDatabases: React.Dispatch<React.SetStateAction<never[]>>) {\n  runCypherQuery(\n    driver,\n    'system',\n    'SHOW DATABASES yield name return distinct name',\n    {},\n    1000,\n    () => {},\n    (records) => {\n      setDatabases(records.map((record) => record._fields[0]));\n    }\n  );\n}\n\n/**\n * Updates the list of users for a given role.\n * This is a two step operation: clear the users assigned to the role currently, and recreate them with a new list.\n * @param driver Neo4j driver with active session.\n * @param currentRole selected role\n * @param allUsers list of all users.\n * @param selectedUsers list of users to have the role after the operation completes.\n */\nexport async function updateUsers(driver, currentRole, allUsers, selectedUsers, onSuccess, onFail) {\n  // 1. Build the query that removes all users from the role.\n  let globalStatus = -1;\n  const escapedAllUsers = allUsers.map((user) => `\\`${user}\\``).join(',');\n  await runCypherQuery(\n    driver,\n    'system',\n    `REVOKE ROLE ${currentRole} FROM ${escapedAllUsers}`,\n    {},\n    1000,\n    async (status) => {\n      globalStatus = status;\n      if (globalStatus == QueryStatus.NO_DATA || globalStatus == QueryStatus.COMPLETE) {\n        //  TODO: Neo4j is very slow in updating after the previous query, even though it is technically a finished query.\n        // We build in an artificial delay... This must be improved the future.\n        setTimeout(async () => {\n          if (selectedUsers.length > 0) {\n            const escapedSelectedUsers = selectedUsers.map((user) => `\\`${user}\\``).join(',');\n            await runCypherQuery(\n              driver,\n              'system',\n              `GRANT ROLE ${currentRole} TO ${escapedSelectedUsers};`,\n              {},\n              1000,\n              (status) => {\n                if (status == QueryStatus.NO_DATA || QueryStatus.COMPLETE) {\n                  onSuccess();\n                }\n              }\n            );\n          } else {\n            onSuccess();\n          }\n        }, 2000);\n      }\n    },\n    (records) => {\n      if (records && records[0] && records[0].error) {\n        onFail(records[0].error);\n      }\n    }\n  );\n}\n"
  },
  {
    "path": "src/extensions/state/ExtensionActions.ts",
    "content": "import { deleteSessionStorageValue, setSessionStorageValue } from '../../sessionStorage/SessionStorageActions';\nimport { getPrepopulationReportExtensionSessionStorageKey } from './ExtensionSelectors';\n\n/**\n * We want to register new reducers to the extension reducer but only if\n * that extension is enabled\n */\nexport const SET_EXTENSION_REDUCER_ENABLED = 'DASHBOARD/EXTENSIONS/SET_EXTENSION_REDUCER_ENABLED';\nexport const setExtensionReducerEnabled = (name: string, enabled: boolean) => ({\n  type: SET_EXTENSION_REDUCER_ENABLED,\n  payload: { name, enabled },\n});\n\nexport const setSessionStoragePrepopulationReportFunction = (reportId, extensionName) =>\n  setSessionStorageValue(getPrepopulationReportExtensionSessionStorageKey(reportId), extensionName);\n\nexport const deleteSessionStoragePrepopulationReportFunction = (reportId) =>\n  deleteSessionStorageValue(getPrepopulationReportExtensionSessionStorageKey(reportId));\n"
  },
  {
    "path": "src/extensions/state/ExtensionReducer.ts",
    "content": "import { EXTENSIONS_REDUCERS } from '../ExtensionConfig';\nimport { SET_EXTENSION_REDUCER_ENABLED } from './ExtensionActions';\n\nexport const INITIAL_EXTENSIONS_STATE = {\n  active: true,\n  activeReducers: [],\n};\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\nexport const extensionsReducer = (state = INITIAL_EXTENSIONS_STATE, action: { type: any; payload: any }) => {\n  const { type, payload } = action;\n\n  if (!action.type.startsWith('DASHBOARD/EXTENSIONS')) {\n    return state;\n  }\n\n  // Checking if we are receiving an action from an enabled extension\n  if (state.activeReducers && state.activeReducers.some((prefix) => type.startsWith(prefix))) {\n    let currentPrefix = state.activeReducers.find((prefix) => type.startsWith(prefix));\n    let { name, reducer } = EXTENSIONS_REDUCERS[currentPrefix];\n    let newState = {\n      ...state,\n    };\n    newState[name] = reducer(state[name], action);\n    return newState;\n  }\n\n  switch (type) {\n    case SET_EXTENSION_REDUCER_ENABLED: {\n      const { name, enabled } = payload;\n      const newState = {\n        ...state,\n      };\n      if (enabled) {\n        newState.activeReducers.push(name);\n      } else {\n        const index = newState.activeReducers.indexOf(name);\n        if (index > -1) {\n          // only splice array when item is found\n          newState.activeReducers.splice(index, 1); // 2nd parameter means remove one item only\n        }\n      }\n      return newState;\n    }\n    default: {\n      return state;\n    }\n  }\n};\n"
  },
  {
    "path": "src/extensions/state/ExtensionSelectors.ts",
    "content": "import { getSessionStorageValue } from '../../sessionStorage/SessionStorageSelectors';\n\nexport const getPrepopulationReportExtensionSessionStorageKey = (cardId) => `prepopulation_report_extension__${cardId}`;\n\n/**\n * An Extension can define a function to run before (prepopulate) the report itself\n * @param state State of the application\n * @param cardId Unique Id of the card running the report\n * @returns Name of the Extension to use to fetch the prepopulation function\n */\nexport const getPrepopulateReportExtension = (state: any, cardId: string) => {\n  return getSessionStorageValue(state, getPrepopulationReportExtensionSessionStorageKey(cardId));\n};\nexport const getExtensionActiveReducers = (state: any) => {\n  return state.dashboard.extensions && state.dashboard.extensions.activeReducers;\n};\n\nexport const getExtensionSettings = (state: any, name: string) => {\n  let res = state.dashboard.extensions && state.dashboard.extensions[name];\n  return res != undefined && res.settings ? res.settings : {};\n};\n"
  },
  {
    "path": "src/extensions/styling/StyleRuleCreationModal.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { Autocomplete, TextField, MenuItem } from '@mui/material';\nimport NeoColorPicker from '../../component/field/ColorPicker';\nimport { IconButton, Button, Dialog, Dropdown, TextInput } from '@neo4j-ndl/react';\nimport {\n  AdjustmentsHorizontalIconOutline,\n  XMarkIconOutline,\n  PlusIconOutline,\n  PlayIconSolid,\n} from '@neo4j-ndl/react/icons';\n\n// The set of conditional checks that are included in the rule specification.\nconst RULE_CONDITIONS = [\n  {\n    value: '=',\n    label: '=',\n  },\n  {\n    value: '!=',\n    label: '!=',\n  },\n  {\n    value: '>',\n    label: '>',\n  },\n  {\n    value: '>=',\n    label: '>=',\n  },\n  {\n    value: '<',\n    label: '<',\n  },\n  {\n    value: '<=',\n    label: '<=',\n  },\n  {\n    value: 'contains',\n    label: 'contains',\n  },\n];\n\n// For each report type, the customizations that can be specified using rules.\nexport const RULE_BASED_REPORT_CUSTOMIZATIONS = {\n  graph: [\n    {\n      value: 'node color',\n      label: 'Node Color',\n    },\n    {\n      value: 'node label color',\n      label: 'Node Label Color',\n    },\n    {\n      value: 'relationship color',\n      label: 'Relationship Color',\n      on: 'relationship',\n    },\n  ],\n  graph3d: [\n    {\n      value: 'node color',\n      label: 'Node Color',\n    },\n    {\n      value: 'node label color',\n      label: 'Node Label Color',\n    },\n    {\n      value: 'relationship color',\n      label: 'Relationship Color',\n      on: 'relationship',\n    },\n  ],\n  map: [\n    {\n      value: 'marker color',\n      label: 'Marker color',\n    },\n  ],\n  bar: [\n    {\n      value: 'bar color',\n      label: 'Bar Color',\n    },\n  ],\n  line: [\n    {\n      value: 'line color',\n      label: 'Line Color',\n    },\n  ],\n  pie: [\n    {\n      value: 'slice color',\n      label: 'Slice Color',\n    },\n  ],\n  gantt: [\n    {\n      value: 'node color',\n      label: 'Task Color',\n    },\n  ],\n  value: [\n    {\n      value: 'text color',\n      label: 'Text Color',\n    },\n  ],\n  table: [\n    {\n      value: 'row color',\n      label: 'Row Background Color',\n    },\n    {\n      value: 'row text color',\n      label: 'Row Text Color',\n    },\n    {\n      value: 'cell color',\n      label: 'Cell Background Color',\n    },\n    {\n      value: 'cell text color',\n      label: 'Cell Text Color',\n    },\n  ],\n};\n\n// Get the default rule structure to append when a rule gets added to the list.\nconst getDefaultRule = (customization) => {\n  return {\n    field: '',\n    condition: '=',\n    value: '',\n    customization: customization,\n    customizationValue: 'black',\n  };\n};\n\n/**\n * The pop-up window used to build and specify custom styling rules for reports.\n */\nexport const NeoCustomReportStyleModal = ({\n  customReportStyleModalOpen,\n  settingName,\n  settingValue,\n  type,\n  fields,\n  schema,\n  setCustomReportStyleModalOpen,\n  onReportSettingUpdate,\n}) => {\n  // The rule set defined in this modal is updated whenever the setting value is externally changed.\n  const [rules, setRules] = React.useState([]);\n  useEffect(() => {\n    if (settingValue) {\n      setRules(settingValue);\n    }\n  }, [settingValue]);\n\n  const handleClose = () => {\n    // If no rules are specified, clear the special report setting that holds the customization rules.\n    if (rules.length == 0) {\n      onReportSettingUpdate(settingName, undefined);\n    } else {\n      onReportSettingUpdate(settingName, rules);\n    }\n    setCustomReportStyleModalOpen(false);\n  };\n\n  // Update a single field in one of the rules in the rule array.\n  const updateRuleField = (ruleIndex, ruleField, ruleFieldValue) => {\n    let newRules = [...rules]; // Deep copy\n    newRules[ruleIndex][ruleField] = ruleFieldValue;\n    setRules(newRules);\n  };\n\n  /**\n   * Create the list of suggestions used in the autocomplete box of the rule specification window.\n   * This will be dynamic based on the type of report we are customizing.\n   */\n  const createFieldVariableSuggestions = () => {\n    if (!schema && !fields) {\n      return [];\n    }\n    if (type == 'graph' || type == 'map' || type == 'gantt' || type == 'graph3d') {\n      return schema\n        .map((node, index) => {\n          if (!Array.isArray(node)) {\n            return undefined;\n          }\n          return schema[index].map((property, propertyIndex) => {\n            if (propertyIndex == 0) {\n              return undefined;\n            }\n            return `${schema[index][0]}.${property}`;\n          });\n        })\n        .flat()\n        .filter((e) => e !== undefined);\n    }\n    if (type == 'bar' || type == 'line' || type == 'pie' || type == 'table' || type == 'value') {\n      return fields;\n    }\n    return [];\n  };\n\n  return (\n    <div>\n      {customReportStyleModalOpen ? (\n        <Dialog\n          className='dialog-xl'\n          open={customReportStyleModalOpen == true}\n          onClose={handleClose}\n          aria-labelledby='form-dialog-title'\n        >\n          <Dialog.Header id='form-dialog-title'>\n            <AdjustmentsHorizontalIconOutline className='icon-base icon-inline text-r' aria-label={'Adjust'} />\n            Rule-Based Styling\n          </Dialog.Header>\n          <Dialog.Content style={{ overflow: 'inherit' }}>\n            <p>\n              You can define rule-based styling for the report here. <br />\n              Style rules are checked in-order and override the default behaviour - if no rules are valid, no style is\n              applied.\n              <br />\n              {type == 'graph' || type == 'map' || type == 'gantt' || type == 'graph3d' ? (\n                <p>\n                  For <b>{type}</b> reports, the field name should be specified in the format <code>label.name</code>,\n                  for example: <code>Person.age</code>. This is case-sensitive.\n                </p>\n              ) : (\n                <></>\n              )}\n              {type == 'line' || type == 'value' || type == 'bar' || type == 'pie' || type == 'table' ? (\n                <p>\n                  For <b>{type}</b> reports, the field name should be the exact name of the returned field. <br />\n                  For example, if your query is <code>MATCH (n:Movie) RETURN n.rating as Rating</code>, your field name\n                  is <code>Rating</code>.\n                </p>\n              ) : (\n                <></>\n              )}\n            </p>\n            <div>\n              <hr></hr>\n\n              <table>\n                <tbody>\n                  {rules.map((rule, index) => {\n                    const ruleType = RULE_BASED_REPORT_CUSTOMIZATIONS[type].find(\n                      (el) => el.value === rule.customization\n                    );\n                    return (\n                      <>\n                        <tr>\n                          <td width='2.5%' className='n-pr-1'>\n                            <span className='n-pr-1'>{index + 1}.</span>\n                            <span className='n-font-bold'>IF</span>\n                          </td>\n                          <td width='45%'>\n                            <div style={{ border: '2px dashed grey' }} className='n-p-1'>\n                              <Autocomplete\n                                className='n-align-middle n-inline-block n-w-5/12 n-pr-1'\n                                disableClearable={true}\n                                id={`autocomplete-label-type${index}`}\n                                size='small'\n                                noOptionsText='*Specify an exact field name'\n                                options={createFieldVariableSuggestions().filter((e) =>\n                                  e.toLowerCase().includes(rule.field.toLowerCase())\n                                )}\n                                value={rule.field ? rule.field : ''}\n                                inputValue={rule.field ? rule.field : ''}\n                                popupIcon={<></>}\n                                style={{ minWidth: 125 }}\n                                onInputChange={(event, value) => {\n                                  updateRuleField(index, 'field', value);\n                                }}\n                                onChange={(event, newValue) => {\n                                  updateRuleField(index, 'field', newValue);\n                                }}\n                                renderInput={(params) => (\n                                  <TextField\n                                    {...params}\n                                    placeholder='Field name...'\n                                    InputLabelProps={{ shrink: true }}\n                                    style={{ padding: '6px 0 7px' }}\n                                    size={'small'}\n                                  />\n                                )}\n                              />\n                              <Dropdown\n                                type='select'\n                                className='n-align-middle n-w-2/12 n-pr-1'\n                                selectProps={{\n                                  onChange: (newValue) => updateRuleField(index, 'condition', newValue.value),\n                                  options: RULE_CONDITIONS.map((option) => ({\n                                    label: option.label,\n                                    value: option.value,\n                                  })),\n                                  value: { label: rule.condition, value: rule.condition },\n                                }}\n                                style={{ minWidth: 70, display: 'inline-block' }}\n                                fluid\n                              />\n                              <TextInput\n                                className='n-align-middle n-inline-block n-w-5/12'\n                                style={{ minWidth: 100 }}\n                                placeholder='Value...'\n                                value={rule.value}\n                                onChange={(e) => updateRuleField(index, 'value', e.target.value)}\n                                fluid\n                              ></TextInput>\n                            </div>\n                          </td>\n                          <td width='5%' className='n-text-center'>\n                            <span style={{ fontWeight: 'bold', color: 'black' }}>THEN</span>\n                          </td>\n                          <td width='45%'>\n                            <div style={{ border: '2px dashed grey' }} className='n-p-1'>\n                              <Dropdown\n                                type='select'\n                                className='n-align-middle n-w-5/12 n-pr-1'\n                                style={{ minWidth: 100, display: 'inline-block' }}\n                                selectProps={{\n                                  onChange: (newValue) => updateRuleField(index, 'customization', newValue.value),\n                                  options: RULE_BASED_REPORT_CUSTOMIZATIONS[type].map((option) => ({\n                                    label: option.label,\n                                    value: option.value,\n                                  })),\n                                  value: {\n                                    label: ruleType ? ruleType.label : '',\n                                    value: rule.customization,\n                                  },\n                                }}\n                                fluid\n                              />\n                              <Autocomplete\n                                className='n-align-middle n-inline-block n-w-5/12 n-pr-1'\n                                disableClearable={true}\n                                id={`autocomplete-label-type${index}`}\n                                size='small'\n                                noOptionsText='*Specify an exact field name'\n                                options={createFieldVariableSuggestions().filter((e) =>\n                                  e.toLowerCase().includes(rule.targetField)\n                                )}\n                                value={rule.targetField ? rule.targetField : rule.field ? rule.field : ''}\n                                inputValue={rule.targetField ? rule.targetField : rule.field ? rule.field : ''}\n                                popupIcon={<></>}\n                                style={{\n                                  minWidth: 125,\n                                  visibility: rule.customization.includes('cell') ? 'visible' : 'hidden',\n                                  display: rule.customization.includes('cell') ? '' : 'none',\n                                }}\n                                onInputChange={(event, value) => {\n                                  updateRuleField(index, 'targetField', value);\n                                }}\n                                onChange={(event, newValue) => {\n                                  updateRuleField(index, 'targetField', newValue);\n                                }}\n                                renderInput={(params) => (\n                                  <TextField\n                                    {...params}\n                                    placeholder='Target field name...'\n                                    InputLabelProps={{ shrink: true }}\n                                    style={{ padding: '6px 0 7px' }}\n                                    size={'small'}\n                                  />\n                                )}\n                              />\n                              <TextInput\n                                className='n-align-middle n-inline-block n-w-1/12 n-pr-1'\n                                style={{ minWidth: 30 }}\n                                disabled={true}\n                                value={'='}\n                                fluid\n                              ></TextInput>\n                              <div className='n-align-middle n-w-6/12 n-inline-block'>\n                                <NeoColorPicker\n                                  style={{ minWidth: 125 }}\n                                  label=''\n                                  defaultValue='#ffffff'\n                                  key={undefined}\n                                  value={rule.customizationValue}\n                                  onChange={(value) => updateRuleField(index, 'customizationValue', value)}\n                                ></NeoColorPicker>\n                              </div>\n                            </div>\n                          </td>\n                          <td width='2.5%'>\n                            <IconButton\n                              aria-label='remove rule'\n                              size='medium'\n                              floating\n                              onClick={() => {\n                                setRules([...rules.slice(0, index), ...rules.slice(index + 1)]);\n                              }}\n                            >\n                              <XMarkIconOutline />\n                            </IconButton>\n                          </td>\n                        </tr>\n                      </>\n                    );\n                  })}\n\n                  <tr>\n                    <td colSpan={5}>\n                      <div className='n-text-center n-mt-1'>\n                        <IconButton\n                          aria-label='add'\n                          size='medium'\n                          floating\n                          onClick={() => {\n                            const newRule = getDefaultRule(RULE_BASED_REPORT_CUSTOMIZATIONS[type][0].value);\n                            setRules(rules.concat(newRule));\n                          }}\n                        >\n                          <PlusIconOutline />\n                        </IconButton>\n                      </div>\n                    </td>\n                  </tr>\n                </tbody>\n              </table>\n            </div>\n          </Dialog.Content>\n          <Dialog.Actions>\n            <Button\n              onClick={() => {\n                handleClose();\n              }}\n              size='large'\n              floating\n            >\n              Save\n              <AdjustmentsHorizontalIconOutline className='btn-icon-lg-r' />\n            </Button>\n          </Dialog.Actions>\n        </Dialog>\n      ) : (\n        <></>\n      )}\n    </div>\n  );\n};\n\nexport default NeoCustomReportStyleModal;\n"
  },
  {
    "path": "src/extensions/styling/StyleRuleEvaluator.ts",
    "content": "import { makeStyles } from '@mui/styles';\nimport { EntityType } from '../../chart/Utils';\n\n/**\n * Evaluates the specified rule set on a row returned by the Neo4j driver.\n * @param record - a single result row produced by the Neo4j driver.\n * @param customization - the target customization (e.g. \"text color\")\n * @param defaultValue - the value to default to if no rules are met.\n * @param rules - a list of rules as produced by the rule-based styling screen.\n * @returns a user-defined value if a rule is met, or the default value if none are.\n */\nexport const evaluateRulesOnNeo4jRecord = (record, customization, defaultValue, rules) => {\n  if (!record || !customization || !rules) {\n    return defaultValue;\n  }\n  for (const [index, rule] of rules.entries()) {\n    // Only look at rules relevant to the target customization.\n    if (rule.customization == customization) {\n      // if the row contains the specified field...\n      if (record._fieldLookup[rule.field] !== undefined) {\n        const val = record._fields[record._fieldLookup[rule.field]];\n        const realValue = val && val.low ? val.low : val;\n        const ruleValue = rule.value;\n        if (evaluateCondition(realValue, rule.condition, ruleValue)) {\n          return rule.customizationValue;\n        }\n      }\n    }\n  }\n  // If the rules have determined a value, return it, otherwise, return the default.\n  return defaultValue;\n};\n\n/**\n * @deprecated - to be removed together with record mapper.\n * We translate the 'mapped' record back into its original using the mapping specified by the user.\n */\n// TODO this function is no longer needed as the record mapper is gone since 2.2.\nexport const evaluateRulesOnMappedNeo4jRecord = (record, mapping, customization, defaultValue, rules) => {\n  const tempRecord = {};\n\n  tempRecord._fields = record._fields;\n  tempRecord._fieldLookup = {};\n  tempRecord._fieldLookup[mapping.index] = record._fieldLookup.index;\n  tempRecord._fieldLookup[mapping.value] = record._fieldLookup.value;\n  tempRecord._fieldLookup[mapping.key] = record._fieldLookup.key;\n  tempRecord.keys = Object.values(mapping);\n  return evaluateRulesOnNeo4jRecord(tempRecord, customization, defaultValue, rules);\n};\n\n/**\n * Evaluates the specified rule set on a dictionary of key/value pairs.\n * Returns the `index` of the rule that is satisfied.\n *\n * @param dict - a dictionary of key/value pairs.\n * @param rules - a list of rules as produced by the rule-based styling screen.\n * @param customizations - a list of customizations to look for.\n * @returns the index of the rule that is satisfied.\n */\nexport const evaluateRulesOnDict = (dict, rules, customizations) => {\n  if (!dict || !rules) {\n    return -1;\n  }\n  for (const [index, rule] of rules.entries()) {\n    // Only check customizations that are specified\n    const evaluationResult = evaluateSingleRuleOnDict(dict, rule, index, customizations);\n    if (evaluationResult !== -1) {\n      return evaluationResult;\n    }\n  }\n  // If no rules are met, return not found (index=-1)\n  return -1;\n};\n\nexport const evaluateSingleRuleOnDict = (dict, rule, ruleIndex, customizations) => {\n  if (customizations.includes(rule.customization)) {\n    // if the row contains the specified field...\n    if (dict[rule.field] !== undefined && dict[rule.field] !== null) {\n      const realValue = dict[rule.field].low ? dict[rule.field].low : dict[rule.field];\n      const ruleValue = rule.value;\n      if (evaluateCondition(realValue, rule.condition, ruleValue)) {\n        return ruleIndex;\n      }\n    }\n  }\n  return -1;\n};\n\n/**\n *  Evaluates the specified rule set on a node object returned by the Neo4j driver.\n * @param node - the node representation returned by the Neo4j driver.\n * @param customization - the target customization (e.g. \"node label color\")\n * @param defaultValue - the value to default to if no rules are met.\n * @param rules - a list of rules as produced by the rule-based styling screen.\n * @returns a user-defined value if a rule is met, or the default value if none are.\n */\nexport const evaluateRulesOnNode = (node, customization, defaultValue, rules) => {\n  return evaluateRules(node, customization, defaultValue, rules, EntityType.Node);\n};\n\nexport const evaluateRulesOnLink = (link, customization, defaultValue, rules) => {\n  return evaluateRules(link, customization, defaultValue, rules, EntityType.Relationship);\n};\n\nexport const evaluateRules = (entity, customization, defaultValue, rules, entityType) => {\n  if (!entity || !customization || !rules) {\n    return defaultValue;\n  }\n\n  for (const [index, rule] of rules.entries()) {\n    // Only look at rules relevant to the target customization.\n    if (rule.customization == customization) {\n      // if the row contains the specified field...\n      const typeOrLabel = rule.field.split('.')[0];\n      const property = rule.field.split('.')[1];\n\n      if (\n        (entityType === EntityType.Node && entity.labels.includes(typeOrLabel)) ||\n        (entityType === EntityType.Relationship && entity.type == typeOrLabel)\n      ) {\n        const realValue = entity?.properties?.[property] || '';\n        const ruleValue = rule.value;\n        if (evaluateCondition(realValue, rule.condition, ruleValue)) {\n          return rule.customizationValue;\n        }\n      }\n    }\n  }\n  return defaultValue;\n};\n\n/**\n * @param realValue the value found in the real data returned by the query\n * @param ruleValue the value specified in the rule.\n * @returns whether the condition is met.\n */\nconst isLooselyEqual = (realValue, ruleValue) => {\n  // In order to avoid having '5' <> {low: 5, high: 0} OR '5' <> 5\n  const sensitiveTypes = ['string', 'number', 'object'];\n  if (sensitiveTypes.includes(typeof realValue) && sensitiveTypes.includes(typeof ruleValue)) {\n    return realValue == ruleValue;\n  }\n  return realValue === ruleValue;\n};\n\n/**\n * @param realValue the value found in the real data returned by the query\n * @param condition the condition, one of [=,!=,<,<=,>,>=,contains].\n * @param ruleValue the value specified in the rule.\n * @return whether the condition is met.\n */\nconst evaluateCondition = (realValue, condition, ruleValue) => {\n  if (!ruleValue || !condition || !realValue) {\n    // If something is null, rules are never met.\n    return false;\n  }\n  if (condition == '=') {\n    return isLooselyEqual(realValue, ruleValue);\n  }\n  if (condition == '!=') {\n    return !isLooselyEqual(realValue, ruleValue);\n  }\n  if (!isNaN(Number(ruleValue))) {\n    ruleValue = Number(ruleValue);\n  }\n  if (condition == '<=') {\n    return realValue <= ruleValue;\n  }\n  if (condition == '<') {\n    return realValue < ruleValue;\n  }\n  if (condition == '>=') {\n    return realValue >= ruleValue;\n  }\n  if (condition == '>') {\n    return realValue > ruleValue;\n  }\n  if (condition == 'contains') {\n    return realValue.toString().includes(ruleValue.toString());\n  }\n  return false;\n};\n\n/**\n * Uses the mui `makeStyles` functionality to generate classes for each of the rules.\n * This is used for styling table rows and columns.\n */\nexport const generateClassDefinitionsBasedOnRules = (rules) => {\n  const classes = {};\n  rules.forEach((rule, i) => {\n    if (rule.customization == 'cell color') {\n      classes[`& .rule${i}`] = {\n        backgroundColor: rule.customizationValue,\n      };\n    }\n    if (rule.customization == 'cell text color') {\n      classes[`& .rule${i}`] = {\n        color: rule.customizationValue,\n        fontWeight: 'bold',\n      };\n    }\n    if (rule.customization == 'row color') {\n      classes[`& .rule${i}`] = {\n        backgroundColor: rule.customizationValue,\n      };\n    }\n    if (rule.customization == 'row text color') {\n      classes[`& .rule${i}`] = {\n        color: rule.customizationValue,\n        fontWeight: 'bold',\n      };\n    }\n  });\n  return makeStyles({\n    root: classes,\n  });\n};\n\nexport const identifyStyleRuleParameters = (rules) => {\n  return rules.reduce((acc, rule) => {\n    if (rule.value.startsWith('$neodash_')) {\n      acc.push(rule.value.substring(1).trim());\n    }\n    return acc;\n  }, []);\n};\n\nexport const styleRulesReplaceParams = (rules, getGlobalParameter) => {\n  return rules.reduce((acc, rule) => {\n    let r = Object.assign({}, rule);\n    if (r.value.startsWith('$neodash_')) {\n      r.value = getGlobalParameter(r.value.substring(1).trim())\n        ? getGlobalParameter(r.value.substring(1).trim())\n        : r.value;\n    }\n    acc.push(r);\n    return acc;\n  }, []);\n};\n\nexport function useStyleRules(enabled, rules, callback) {\n  if (!enabled || !rules) {\n    return [];\n  }\n  const styleParamsCalc = identifyStyleRuleParameters(rules);\n  let styleRules = styleParamsCalc;\n  styleRules = styleRulesReplaceParams(rules, callback);\n\n  return styleRules;\n}\n"
  },
  {
    "path": "src/extensions/text2cypher/QueryTranslatorConfig.ts",
    "content": "import { SELECTION_TYPES } from '../../config/CardConfig';\nimport { ModelClient } from './clients/ModelClient';\nimport { OpenAiClient } from './clients/OpenAi/OpenAiClient';\n\nimport { AzureOpenAiClient } from './clients/AzureOpenAi/AzureOpenAiClient';\n\ninterface ClientSettingEntry {\n  label: string;\n  type: SELECTION_TYPES;\n  default: any;\n  password?: boolean;\n  authentication?: boolean; // Required for authentication, the user should insert all the required fields before trying to authenticate\n  hasAuthButton?: boolean; // Append a button at the end of the selector to trigger an auth request.\n  methodFromClient?: string; // String that contains the name of the client function to call to retrieve the data needed to fill the option\n}\n\ninterface ClientSettings {\n  apiKey: ClientSettingEntry;\n  modelType: ClientSettingEntry;\n  region?: ClientSettingEntry;\n}\n\ninterface ClientConfig {\n  clientName: string;\n  clientClass: ModelClient;\n  clientSettingsModal: JSX.Element;\n  settings: ClientSettings;\n}\n\ninterface AvailableClients {\n  OpenAI: ClientConfig;\n  vertexAi: ClientConfig;\n}\n\ninterface QueryTranslatorConfig {\n  availableClients: AvailableClients;\n}\n\nexport const QUERY_TRANSLATOR_CONFIG: QueryTranslatorConfig = {\n  availableClients: {\n    OpenAI: {\n      clientName: 'OpenAI',\n      clientClass: OpenAiClient,\n      settings: {\n        apiKey: {\n          label: 'OpenAI API Key',\n          type: SELECTION_TYPES.TEXT,\n          default: '',\n          password: true,\n          hasAuthButton: true,\n          authentication: true,\n        },\n        modelType: {\n          label: 'Model',\n          type: SELECTION_TYPES.LIST,\n          methodFromClient: 'getListModels',\n          default: '',\n          authentication: false,\n        },\n      },\n    },\n    AzureOpenAI: {\n      clientName: 'AzureOpenAI',\n      clientClass: AzureOpenAiClient,\n      settings: {\n        endpoint: {\n          label: 'Azure OpenAI EndPoint',\n          type: SELECTION_TYPES.TEXT,\n          default: '',\n          hasAuthButton: false,\n          authentication: true,\n        },\n        apiKey: {\n          label: 'Subscription Key',\n          type: SELECTION_TYPES.TEXT,\n          default: '',\n          password: true,\n          hasAuthButton: true,\n          authentication: true,\n        },\n        modelType: {\n          label: 'Model',\n          type: SELECTION_TYPES.LIST,\n          methodFromClient: 'getListModels',\n          default: '',\n          authentication: false,\n        },\n      },\n    },\n  },\n};\n\n/**\n * Function to get the extension config\n * @param extensionName Name of the desired extension\n * @returns Predefined fields of configuration for an extension\n */\nexport function getQueryTranslatorDefaultConfig(providerName) {\n  return QUERY_TRANSLATOR_CONFIG?.availableClients[providerName]?.settings || {};\n}\n\n/**\n * Given the provider and the settings in input, return the related client object\n * @param modelProvider Name of the provider (for example: OpenAi)\n * @param settings Dictionary that will be unpacked by the client itself\n * @returns Client object related to the provider\n */\nexport function getModelClientObject(modelProvider, settings) {\n  let providerDetails = QUERY_TRANSLATOR_CONFIG.availableClients[modelProvider];\n  if (providerDetails === undefined) {\n    throw Error(`Invalid provider name${modelProvider}`);\n  }\n  let modelProviderClass = providerDetails.clientClass;\n  return new modelProviderClass(settings);\n}\n"
  },
  {
    "path": "src/extensions/text2cypher/clients/AzureOpenAi/AzureOpenAiClient.ts",
    "content": "import { AzureKeyCredential, OpenAIClient } from '@azure/openai';\n\nimport { OpenAiClient } from '../OpenAi/OpenAiClient';\n\nconst consoleLogAsync = async (message: string, other?: any) => {\n  await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other));\n};\n\nexport class AzureOpenAiClient extends OpenAiClient {\n  modelType: string | undefined;\n\n  createSystemMessage: any;\n\n  modelClient!: OpenAIClient;\n\n  driver: any;\n\n  constructor(settings) {\n    super(settings);\n  }\n\n  /**\n   * Function used to create the OpenAiApi object.\n   * */\n  setModelClient() {\n    if (typeof this.endpoint === 'string') {\n      this.modelClient = new OpenAIClient(this.endpoint, new AzureKeyCredential(this.apiKey));\n    }\n  }\n\n  async getListModels() {\n    let res;\n    try {\n      if (!this.modelClient) {\n        throw new Error('no client defined');\n      }\n\n      const response = await fetch(\n        `${this.endpoint + (this.endpoint?.endsWith('/') ? '' : '/')}openai/deployments?api-version=2023-03-15-preview`,\n        {\n          method: 'GET',\n          mode: 'cors',\n          headers: {\n            'Api-Key': this.apiKey,\n          },\n        }\n      );\n      const req = await response.json();\n      res = req.data.filter((x) => x.model.startsWith('gpt-')).map((x) => x.id);\n    } catch (e) {\n      consoleLogAsync('Error while loading the model list: ', e);\n      res = [];\n    }\n    this.setListAvailableModels(res);\n    return res;\n  }\n\n  async chatCompletion(history) {\n    let completion;\n    if (typeof this.modelType === 'string') {\n      completion = await this.modelClient.getChatCompletions(this.modelType, history);\n    }\n    // If the status is correct\n    if (completion?.choices?.[0]?.message) {\n      let { message } = completion.choices[0];\n      return message;\n    }\n    throw Error(`Request returned with status: ${completion.id}`);\n  }\n}\n"
  },
  {
    "path": "src/extensions/text2cypher/clients/ModelClient.ts",
    "content": "import {\n  MAX_NUM_VALIDATION,\n  nodePropsQuery,\n  relPropsQuery,\n  relQuery,\n  schemaSamplingQuery,\n  SCHEMA_SAMPLING_NUMBER,\n  QUERY_TRANSLATOR_TASK,\n} from './const';\n\nconst notImplementedError = (functionName) => {\n  throw new Error(`Not Implemented: ${functionName}`);\n};\nconst consoleLogAsync = async (message: string, other?: any) => {\n  await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other));\n};\n\n// A model client should just handle the communication\nexport abstract class ModelClient {\n  apiKey: string;\n\n  modelType: string | undefined;\n\n  listAvailableModels: string[];\n\n  createSystemMessage: any;\n\n  modelClient: any;\n\n  driver: any;\n\n  endpoint: string | undefined;\n\n  constructor(settings) {\n    this.apiKey = settings.apiKey;\n    this.modelType = settings.modelType;\n    this.listAvailableModels = [];\n    this.endpoint = settings.endpoint;\n    this.setModelClient();\n  }\n\n  setModelClient() {\n    notImplementedError('setModelClient');\n  }\n\n  /**\n   * Function used to create a schema representation to send to the model\n   * @param nodeProps Labels and properties of the nodes in the database\n   * @param relProps Properties of the relationships in the database\n   * @param rels Patterns existing in the database\n   * @returns Message representing the schema\n   */\n  createSchemaText(nodeProps, relProps, rels) {\n    if (nodeProps.length == 0 && relProps.length == 0 && rels.length == 0) {\n      throw Error(`Looks like there is no schema to fetch, are you sure this database is not empty?`);\n    }\n    let nodes = JSON.stringify(nodeProps);\n    let relationshipsProps = JSON.stringify(relProps);\n    let relationships = JSON.stringify(rels);\n    return `\n    This is the schema representation of the Neo4j database.\n    Node properties are the following:\n    ${nodes}\n    Relationship properties are the following:\n    ${relationshipsProps}\n    Relationship point from source to target nodes\n    ${relationships}\n    Make sure to respect relationship types and directions\n    `;\n  }\n\n  /**\n   * Creates the schema message for the model in sampling mode (faster but less accurate)\n   * @param database Name of the database which will provide the schema\n   * @returns Message representing the schema\n   */\n  async generateSchemaSample(database) {\n    let sample = await this.queryDatabase(schemaSamplingQuery, database, false, { sample: SCHEMA_SAMPLING_NUMBER });\n    let { relationships, nodes, patterns } = sample[0];\n\n    let nodesText = nodes ? nodes.split('\\n').join(',') : '';\n    let relText = relationships ? relationships.split('\\n').join(',') : '';\n    let patternsText = patterns ? patterns.split('\\n').join(',') : '';\n\n    let res = this.createSchemaText(nodesText, relText, patternsText);\n    return res;\n  }\n\n  /**\n   * Creates the schema message for the model in full reading mode (slower but 100% accurate)\n   * @param database Name of the database which will provide the schema\n   * @returns Message representing the schema\n   */\n  async generateSchema(database) {\n    try {\n      let nodeProps = await this.queryDatabase(nodePropsQuery, database);\n      let relProps = await this.queryDatabase(relPropsQuery, database);\n      let rels = await this.queryDatabase(relQuery, database);\n\n      let schema = this.createSchemaText(nodeProps, relProps, rels);\n      return schema;\n    } catch (e) {\n      throw Error(`Couldn't generate schema due to: ${e.message}`);\n    }\n  }\n\n  getTaskDefinition(schemaText) {\n    return `${QUERY_TRANSLATOR_TASK}\n      Schema:\n      ${schemaText}\n    `;\n  }\n\n  setDriver(driver: any) {\n    this.driver = driver;\n  }\n\n  getMessageContent(_message: any) {\n    notImplementedError('getMessageContent');\n    return '';\n  }\n\n  getExamplePrompt(examples) {\n    let res = `Here are some examples of questions and their answers: \\n`;\n    let tmp = examples.map((ex) => `Question: ${ex.question} \\nAnswer: ${ex.answer} \\n`);\n    return res + tmp.join('');\n  }\n\n  async manageMessageHistory(database, schema, schemaSampling, inputMessage, history, reportType, examples) {\n    // If empty, the first message will be the task definition\n    if (history.length == 0) {\n      // The schema can be fetched in full or in sample mode (the second one is faster but less accurate)\n      schema = schemaSampling ? await this.generateSchemaSample(database) : await this.generateSchema(database);\n      history.push(this.addSystemMessage(this.getTaskDefinition(schema)));\n    }\n    // The Examples are always refreshed and always in second position\n    if (examples.length > 0) {\n      history[1] = this.addSystemMessage(this.getExamplePrompt(examples));\n    } else {\n      history[1] = this.addSystemMessage('There are no examples provided.');\n    }\n    history.push(this.addUserMessage(inputMessage, reportType, true));\n    return history;\n  }\n\n  /**\n   * Method responsible to ask the model to translate the message.\n   * @param inputMessage\n   * @param history History of messages exchanged between a card and the model client\n   * @param database Databased used from the report, it will be used to fetch the schema\n   * @param reportType Type of report asking that requires the translation\n   * @param setValidationStep Function to set the current validation step outside the function\n   * @returns The new history to assign to the card. If there was no possibility of validating the query, the\n   * method will return the same history passed in input\n   */\n  async queryTranslation(\n    inputMessage,\n    history,\n    database,\n    reportType,\n    examples,\n    onRetry = () => {\n      // console.log(value);\n    },\n    schemaSampling = true // By default we create the schema message using apoc.meta.data in sampling mode\n  ) {\n    // Creating a copy of the history\n    let newHistory = [...history];\n\n    // Creating a tmp history to prevent updating the history with erroneous messages\n    let tmpHistory = [...newHistory];\n    let schema = '';\n    let query = '';\n    let modelAnswer = { role: '', content: '' };\n    try {\n      tmpHistory = await this.manageMessageHistory(\n        database,\n        schema,\n        schemaSampling,\n        inputMessage,\n        tmpHistory,\n        reportType,\n        examples\n      );\n\n      let retries = 0;\n      let isValidated = false;\n      let errorMessage = '';\n\n      // While is not validated and we didn't exceed the maximum retry number\n      while (!isValidated && retries < MAX_NUM_VALIDATION) {\n        retries += 1;\n        onRetry(retries);\n\n        // Get the answer to the question from the model\n        modelAnswer = await this.chatCompletion(tmpHistory);\n        tmpHistory.push(modelAnswer);\n\n        // and try to validate it\n        let validationResult = await this.validateQuery(modelAnswer, database);\n        isValidated = validationResult[0];\n        errorMessage = validationResult[1];\n\n        // If you can't validate the query, send the model a message to try to fix it\n        if (!isValidated) {\n          tmpHistory.push(this.addErrorMessage(errorMessage));\n        } else {\n          newHistory = await this.manageMessageHistory(\n            database,\n            schema,\n            schemaSampling,\n            inputMessage,\n            newHistory,\n            reportType,\n            examples\n          );\n          newHistory.push(modelAnswer);\n          query = this.getMessageContent(modelAnswer);\n        }\n      }\n      if (!isValidated) {\n        throw Error(\n          `The model could not translate your question to valid Cypher: '${inputMessage}'. \\n\n           The result from the model was: '${modelAnswer?.content ? modelAnswer.content : ''}'. \\n\n           Try writing a more descriptive question, explicitly calling out the node labels, relationship types, and property names. `\n        );\n      }\n    } catch (error) {\n      await consoleLogAsync('Error during query', error);\n      throw error;\n    }\n    return [query, newHistory];\n  }\n\n  /**\n   * Function to query the db directly from the client\n   * @param query Query to run\n   * @param database Selected database\n   * @returns The records results if the query runs correctly, otherwise the function will throw an error\n   */\n  async queryDatabase(query, database, getFirstColumnOnly = true, parameters = {}) {\n    if (this.driver) {\n      const session = this.driver.session({ database: database });\n      const transaction = session.beginTransaction({ timeout: 20 * 1000, connectionTimeout: 2000 });\n\n      let res = await transaction\n        .run(query, parameters)\n        .then((res) => {\n          const { records } = res;\n          let elems = records.map((elem) => {\n            return getFirstColumnOnly ? elem.toObject()[elem.keys[0]] : elem.toObject();\n          });\n          records.length > 0 ?? elems.unshift(records[0].keys);\n          transaction.commit();\n          return elems;\n        })\n        .catch(async (e) => {\n          throw e;\n        });\n      return res;\n    }\n    throw new Error('Driver not present');\n  }\n\n  async validateQuery(_message, _database) {\n    notImplementedError('validateQuery');\n  }\n\n  async chatCompletion(_history) {\n    notImplementedError('chatCompletion');\n  }\n\n  addUserMessage(_content, _reportType, _plain = false) {\n    notImplementedError('addUserMessage');\n  }\n\n  addSystemMessage(_content) {\n    notImplementedError('addSystemMessage');\n  }\n\n  addAssistantMessage(_content) {\n    notImplementedError('addAssistantMessage');\n  }\n\n  addErrorMessage(_error) {\n    notImplementedError('addErrorMessage');\n  }\n}\n\n// to see if i need this\nexport enum ModelConnectionState {\n  RUNNING,\n  DONE,\n  ERROR,\n}\n"
  },
  {
    "path": "src/extensions/text2cypher/clients/OpenAi/OpenAiClient.ts",
    "content": "import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum, Configuration, OpenAIApi } from 'openai';\nimport { reportTypesToDesc, reportExampleQueries } from '../const';\nimport { ModelClient } from '../ModelClient';\nimport { Status } from '../../util/Status';\n\nconst consoleLogAsync = async (message: string, other?: any) => {\n  await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other));\n};\n\nexport class OpenAiClient extends ModelClient {\n  modelType: string | undefined;\n\n  createSystemMessage: any;\n\n  modelClient!: OpenAIApi;\n\n  driver: any;\n\n  constructor(settings) {\n    super(settings);\n  }\n\n  async validateQuery(message, database) {\n    let query = message.content;\n    let isValid = false;\n    let errorMessage = '';\n    try {\n      await this.queryDatabase(`EXPLAIN ${query}`, database);\n      isValid = true;\n    } catch (e) {\n      isValid = false;\n      errorMessage = e.message;\n    }\n    return [isValid, errorMessage];\n  }\n\n  /**\n   * Function used to create the OpenAiApi object.\n   * */\n  setModelClient() {\n    const configuration = new Configuration({\n      apiKey: this.apiKey,\n    });\n    this.modelClient = new OpenAIApi(configuration);\n  }\n\n  /**\n   *\n   * @param setIsAuthenticated If defined, is a function used to set the authentication result (for example, set function of a state variable)\n   * @returns True if we client can authenticate, False otherwise\n   */\n  async authenticate(\n    setIsAuthenticated = () => {\n      // console.log(boolean);\n    }\n  ) {\n    try {\n      let tmp = await this.getListModels();\n      // Can be used in async mode without awaiting\n      // by passing down a function to set the authentication result\n      setIsAuthenticated(tmp.length > 0 ? Status.AUTHENTICATED : Status.ERROR);\n      return tmp.length > 0;\n    } catch (e) {\n      consoleLogAsync('Authentication went wrong: ', e);\n      return false;\n    }\n  }\n\n  /**\n   *  Used also to check authentication\n   * @returns list of models available for this client\n   */\n  async getListModels() {\n    let res;\n    try {\n      if (!this.modelClient) {\n        throw new Error('no client defined');\n      }\n      let req = await this.modelClient.listModels();\n      // Extracting the names\n      res = req.data.data.map((x) => x.id).filter((x) => x.includes('gpt-'));\n    } catch (e) {\n      consoleLogAsync('Error while loading the model list: ', e);\n      res = [];\n    }\n    this.setListAvailableModels(res);\n    return res;\n  }\n\n  setApiKey(apiKey) {\n    this.apiKey = apiKey;\n    const configuration = new Configuration({\n      apiKey: apiKey,\n    });\n    this.modelClient = new OpenAIApi(configuration);\n  }\n\n  setDriver(driver) {\n    this.driver = driver;\n  }\n\n  setListAvailableModels(listModels) {\n    this.listAvailableModels = listModels;\n  }\n\n  setModelType(modelType) {\n    this.modelType = modelType;\n  }\n\n  /**\n   * Function used to create a message sent from the user to the model.\n   * @param content Content of the message (the message wrote from the UI)\n   * @param reportType Type of report needed\n   * @param plain If True, return content itself, otherwise the message with all the prompting needed.\n   * @returns\n   */\n  addUserMessage(content, reportType, plain = false) {\n    let queryExample = reportExampleQueries[reportType];\n    let finalMessage = `${content}. Please respect the structure of the result based on this description: ${reportTypesToDesc[reportType]}.\n  Here an example of a query: ${queryExample}.\n  Remember that every $ prefixed word is a parameter.`;\n    return { role: ChatCompletionRequestMessageRoleEnum.User, content: plain ? content : finalMessage };\n  }\n\n  addSystemMessage(content) {\n    return { role: ChatCompletionRequestMessageRoleEnum.System, content: content };\n  }\n\n  addAssistantMessage(content) {\n    return { role: ChatCompletionRequestMessageRoleEnum.Assistant, content: content };\n  }\n\n  addErrorMessage(error) {\n    let finalMessage = `Error: ${error}. Please correct the query based on the provided error message. Ensure the query follows the expected format, adheres to the schema, and does not contain any comments, explanations, or unnecessary symbols. Please remove any comments or explanations from the query result.`;\n    return { role: ChatCompletionRequestMessageRoleEnum.User, content: finalMessage };\n  }\n\n  getMessageContent(message: ChatCompletionRequestMessage) {\n    return message.content;\n  }\n\n  async chatCompletion(history) {\n    const completion = await this.modelClient.createChatCompletion({\n      model: this.modelType,\n      messages: history,\n    });\n    // If the status is correct\n    if (completion.status == 200 && completion.data && completion.data.choices && completion.data.choices[0].message) {\n      let { message } = completion.data.choices[0];\n      return message;\n    }\n    throw Error(`Request returned with status: ${completion.status}`);\n  }\n}\n"
  },
  {
    "path": "src/extensions/text2cypher/clients/VertexAiClient.ts",
    "content": "import { ModelClient } from './ModelClient';\n\nconst consoleLogAsync = async (message: string, other?: any) => {\n  await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other));\n};\n\nexport class VertexAiClient extends ModelClient {}\n"
  },
  {
    "path": "src/extensions/text2cypher/clients/const.ts",
    "content": "export const nodePropsQuery = `CALL apoc.meta.data()\nYIELD label, other, elementType, type, property\nWHERE NOT type = \"RELATIONSHIP\" AND elementType = \"node\"\nWITH label AS nodeLabels, collect(property) AS properties\nRETURN {labels: nodeLabels, properties: properties} AS output\n`;\n\nexport const relPropsQuery = `\nCALL apoc.meta.data()\nYIELD label, other, elementType, type, property\nWHERE NOT type = \"RELATIONSHIP\" AND elementType = \"relationship\"\nWITH label AS nodeLabels, collect(property) AS properties\nRETURN {type: nodeLabels, properties: properties} AS output\n`;\n\nexport const relQuery = `\nCALL apoc.meta.data()\nYIELD label, other, elementType, type, property\nWHERE type = \"RELATIONSHIP\" AND elementType = \"node\"\nRETURN {source: label, relationship: property, target: other} AS output\n`;\n\nexport const QUERY_TRANSLATOR_TASK = `\nYou are an expert Neo4j Cypher translator who understands the question in english and convert to Cypher strictly based on the Neo4j Schema provided and following the instructions below:\n1. Generate Cypher query compatible ONLY for Neo4j Version 5\n2. Do not use EXISTS, SIZE keywords in the cypher. Use alias when using the WITH keyword\n3. Please do not use same variable names for different nodes and relationships in the query.\n4. Use only Nodes and relationships mentioned in the schema\n5. Always do a case-insensitive and fuzzy search for any properties related search. Eg: to search for a Company name use \"toLower(c.name) contains 'neo4j'\"\n6. Always use aliases to refer the node in the query\n7. 'Answer' is NOT a Cypher keyword. Answer should never be used in a query.\n8. Please generate only one Cypher query per question. \n9. Cypher is NOT SQL. So, do not mix and match the syntaxes.\n10. Every Cypher query always starts with a MATCH keyword.\n11. Do not response with any explanation or any other information except the Cypher query.\n12. Respect the provided schema.`;\n\nexport const schemaSamplingQuery = `\nWITH coalesce($sample,(count(*)/1000)+1) as sample\ncall apoc.meta.data({maxRels: 10, sample:toInteger(sample) })\nYIELD label, other, elementType, type, property\nWITH label, elementType,\napoc.text.join(collect(case when NOT type = \"RELATIONSHIP\" then property+\": \"+type else null end),\", \") AS properties,\ncollect(case when type = \"RELATIONSHIP\" AND elementType = \"node\" then \"(:\" + label + \")-[:\" + property + \"]->(:\" + toString(other[0]) + \")\" else null end) as patterns\nwith elementType as type,\napoc.text.join(collect(\":\"+label+\" {\"+properties+\"}\"),\"\\\\n\") as entities,\napoc.text.join(apoc.coll.flatten(collect(coalesce(patterns,[]))),\"\\\\n\") as patterns\nreturn collect(case type when \"relationship\" then entities end)[0] as relationships,\ncollect(case type when \"node\" then entities end)[0] as nodes,\ncollect(case type when \"node\" then patterns end)[0]  as patterns\n`;\n\nexport const SCHEMA_SAMPLING_NUMBER = 10000;\n\nexport const reportTypesToDesc = {\n  table: 'Multiple variables representing property values of nodes and relationships.',\n  graph:\n    'Multiple variables representing nodes objects and relationships objects inside the graph. Please return also the relationship objects.',\n  'Bar Chart': 'Two variables named category(a String value) and value(numeric value).',\n  'Line Chart': 'Two numeric variables named x and y.',\n  sunburst: 'Two variables named Path(list of strings) and value(a numerical value).',\n  'Circle Packing': 'Two variables named Path(a list of strings) and value(a numerical value).',\n  choropleth: 'Two variables named code(a String value) and value(a numerical value).',\n  'Area Map': 'Two variables named code(a String value) and value(a numerical value).',\n  treemap: 'Two variables named Path(a list of strings) and value(a numerical value).',\n  'Radar Chart': 'Multiple variables representing property values of nodes and relationships.',\n  'Sankey Chart':\n    'Three variables, two being a node object (and not a property value) and one representing a relationship object (and not a property value).',\n  map: 'multiple variables representing nodes objects(should contain spatial propeties) and relationship objects.',\n  'Single Value': 'A single value of a single variable.',\n  'Gauge Chart': 'A single value of a single variable.',\n  'Raw JSON': 'The Cypher query must return a JSON object that will be displayed as raw JSON data.',\n  'Pie Chart': 'Two variables named category and value.',\n};\n\nexport const reportExampleQueries = {\n  table: 'MATCH (n:Movie)<-[:ACTED_IN]-(p:Person) RETURN n.title, n.released, count(p) as actors',\n  graph: `MATCH (p:Person)-[a:ACTED_IN]->(m:Movie) WHERE m.title = 'The Matrix' RETURN p, a, m`,\n  'Bar Chart': 'MATCH (p:Person)-[e]->(m:Movie) RETURN m.title as Title, COUNT(p) as People',\n  'Line Chart': 'MATCH (p:Person) RETURN (p.born/10)*10 as Decade, COUNT(p) as People ORDER BY Decade ASC',\n  sunburst: `MATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department) WITH nodes(path) as no WITH no, last(no) as leaf WITH  [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val RETURN result, val`,\n  'Circle Packing': `MATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department) WITH nodes(path) as no WITH no, last(no) as leaf WITH  [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val RETURN result, val`,\n  choropleth: `MATCH (:Company{name:'NeoDash'})-[:HAS_DEPARTMENT]->(:Department)<-[:IN_DEPARTMENT]-(e:Employee),(e)-[:LIVES_IN]->(c:Country) WITH c.code as code, count(e) as value RETURN code, value`,\n  'Area Map': `MATCH (:Company{name:'NeoDash'})-[:HAS_DEPARTMENT]->(:Department)<-[:IN_DEPARTMENT]-(e:Employee),\n  (e)-[:LIVES]->(city:City)-[:IN_COUNTRY]->(country:Country)\n  WITH city, country\n  CALL {\n      WITH country\n      RETURN country.countryCode as code, count(*) as value\n      UNION\n      WITH city\n      RETURN city.countryCode as code, count(*) as value\n  }\n  WITH code, sum(value) as totalCount\n  RETURN code,totalCount`,\n  treemap: `MATCH path=(:Company{name:'NeoDash'})-[:HAS_DEPARTMENT*]->(:Department) WITH nodes(path) as no WITH no, last(no) as leaf WITH  [n IN no[..-1] | n.name] AS result, sum(leaf.employees) as val RETURN result, val`,\n  'Radar Chart': `MATCH (s:Skill) \n  MATCH (:Player{name:\"Messi\"})-[h1:HAS_SKILL]->(s) \n  MATCH (:Player{name:\"Mbappe\"})-[h2:HAS_SKILL]->(s) \n  MATCH (:Player{name:\"Benzema\"})-[h3:HAS_SKILL]->(s) \n  MATCH (:Player{name:\"C Ronaldo\"})-[h4:HAS_SKILL]->(s) \n  MATCH (:Player{name:\"Lewandowski\"})-[h5:HAS_SKILL]->(s) \n  RETURN s.name as Skill, h1.value as Messi, h2.value as Mbappe, h3.value as Benzema,  h4.value as Ronaldo, h5.value as Lewandowski`,\n  'Sankey Chart': 'MATCH (p:Person)-[r:RATES]->(m:Movie) RETURN p, r, m',\n  map: 'MATCH (b:Brewery) RETURN b',\n  'Single Value': 'MATCH (n) RETURN COUNT(n)',\n  'Gauge Chart': 'MATCH (c:CPU) WHERE c.id = 1 RETURN c.load_percentage * 100',\n  'Raw JSON': 'MATCH (n) RETURN COUNT(n)',\n  'Pie Chart': 'Match (p:Person)-[e]->(m:Movie) RETURN m.title as Title, COUNT(p) as People LIMIT 10',\n};\n\nexport const MAX_NUM_VALIDATION = 1;\n"
  },
  {
    "path": "src/extensions/text2cypher/component/ClientSettings.tsx",
    "content": "import React, { useCallback, useEffect } from 'react';\nimport { connect } from 'react-redux';\nimport { debounce, List, ListItem } from '@mui/material';\nimport { getModelClientObject, getQueryTranslatorDefaultConfig } from '../QueryTranslatorConfig';\nimport {\n  QUERY_TRANSLATOR_HISTORY_PREFIX,\n  getHistory,\n  getModelClientSessionStorageKey,\n  getQueryTranslatorSettings,\n} from '../state/QueryTranslatorSelector';\nimport NeoSetting from '../../../component/field/Setting';\nimport {\n  deleteAllMessageHistory,\n  setClientSettings,\n  setGlobalModelClient,\n  setModelProvider,\n} from '../state/QueryTranslatorActions';\nimport {\n  CheckCircleIconSolid,\n  ExclamationTriangleIconSolid,\n  PlayCircleIconSolid,\n  PlayIconSolid,\n  PencilSquareIconOutline,\n} from '@neo4j-ndl/react/icons';\nimport { Button, IconButton } from '@neo4j-ndl/react';\nimport { modelClientInitializationThunk } from '../state/QueryTranslatorThunks';\nimport { Status } from '../util/Status';\nimport {\n  getSessionStorage,\n  getSessionStorageValue,\n  getSessionStorageValuesWithPrefix,\n} from '../../../sessionStorage/SessionStorageSelectors';\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\nexport const ClientSettings = ({\n  modelProvider,\n  settingState,\n  setSettingsState,\n  authenticate,\n  updateModelProvider,\n  updateClientSettings,\n  messageHistory,\n  deleteAllMessageHistory,\n  handleClose,\n  handleOpenEditSolutions,\n}) => {\n  const defaultSettings = getQueryTranslatorDefaultConfig(modelProvider);\n  const requiredSettings = Object.keys(defaultSettings).filter((setting) => defaultSettings[setting].required);\n  const [localSettings, setLocalSettings] = React.useState(settingState);\n  const [status, setStatus] = React.useState(\n    settingState.modelType == undefined ? Status.NOT_AUTHENTICATED : Status.AUTHENTICATED\n  );\n  const [settingChoices, setSettingChoices] = React.useState({});\n  /**\n   * Method used to update a certain field inside a state object.\n   * @param field Name of the field to update\n   * @param value Value to set for the specified field\n   * @param stateObj Object to update\n   * @param setFunction Function used to update stateObj\n   */\n  const updateSpecificFieldInStateObject = (field: string, value: any, stateObj, setFunction) => {\n    const entry = {};\n    entry[field] = value;\n    setFunction(update(stateObj, entry));\n  };\n\n  const debouncedUpdateSpecificFieldInStateObject = useCallback(debounce(updateSpecificFieldInStateObject, 500), []);\n\n  /**\n   * Function used from each setting to understand if it needs to be disabled\n   * @param setting Name of the setting to check\n   * @returns False if not disabled, otherwise True\n   */\n  function checkIfDisabled(setting) {\n    let tmp = defaultSettings[setting];\n    if (tmp.required || status == Status.AUTHENTICATED) {\n      return false;\n    }\n    return !requiredSettings.every((e) => settingState[e]);\n  }\n\n  // Effect used to trigger the population of the settings when the user inserts a correct apiKey\n  useEffect(() => {\n    let localClientTmp = getModelClientObject(modelProvider, settingState);\n    if (status == Status.AUTHENTICATED) {\n      setGlobalModelClient(localClientTmp);\n    } else {\n      setGlobalModelClient(undefined);\n    }\n    Object.keys(defaultSettings).forEach((setting) => {\n      setChoices(setting, localClientTmp);\n    });\n  }, [status]);\n\n  /**\n   * Function used to handle the definition of the choices param inside the settings form.\n   * If needed, it will get the choices from the client\n   * @param setting Name of the setting that we need to populate\n   * @param modelClient Client to call the AI model\n   */\n  function setChoices(setting, modelClient) {\n    let choices = defaultSettings[setting].values ? defaultSettings[setting].values : [];\n    let { methodFromClient } = defaultSettings[setting];\n    if (methodFromClient && status == Status.AUTHENTICATED) {\n      modelClient[methodFromClient]().then((value) => {\n        updateSpecificFieldInStateObject(setting, value, settingChoices, setSettingChoices);\n      });\n    } else {\n      updateSpecificFieldInStateObject(setting, choices, settingChoices, setSettingChoices);\n    }\n  }\n\n  const getBackgroundColor = (status) => {\n    if (status == Status.AUTHENTICATED) {\n      return 'green';\n    } else if (status == Status.NOT_AUTHENTICATED) {\n      return 'orange';\n    }\n    return 'red';\n  };\n\n  // Prevent authentication if all required fields are not full (EX: look at checkIfDisabled)\n  const authButton = (\n    <IconButton\n      key={'auth-setting'}\n      aria-label='connect'\n      onClick={(e) => {\n        e.preventDefault();\n        updateModelProvider(modelProvider);\n        updateClientSettings(settingState);\n        authenticate(setStatus);\n      }}\n      clean\n      style={{\n        marginTop: 24,\n        marginRight: 28,\n        color: 'white',\n        backgroundColor: getBackgroundColor(status),\n      }}\n      size='medium'\n    >\n      {status == Status.AUTHENTICATED ? (\n        <CheckCircleIconSolid />\n      ) : status == Status.NOT_AUTHENTICATED ? (\n        <PlayCircleIconSolid color='white' />\n      ) : (\n        <ExclamationTriangleIconSolid />\n      )}\n    </IconButton>\n  );\n\n  const component = (\n    <List style={{ marginLeft: 0, marginRight: 0 }}>\n      {/* Only render the base settings (required for auth) if no authentication is available. */}\n      {Object.keys(defaultSettings)\n        .filter((setting) => defaultSettings[setting].authentication == true || status == Status.AUTHENTICATED)\n        .map((setting) => {\n          let disabled = checkIfDisabled(setting);\n          return (\n            <ListItem key={`list-${setting}`} style={{ padding: 0 }}>\n              <NeoSetting\n                key={setting}\n                style={{ marginLeft: 0, marginRight: 0 }}\n                name={setting}\n                value={localSettings[setting]}\n                disabled={disabled}\n                password={defaultSettings[setting].password}\n                type={defaultSettings[setting].type}\n                label={defaultSettings[setting].label}\n                defaultValue={defaultSettings[setting].default}\n                choices={settingChoices[setting] ? settingChoices[setting] : []}\n                onChange={(e) => {\n                  updateSpecificFieldInStateObject(setting, e, localSettings, setLocalSettings);\n                  debouncedUpdateSpecificFieldInStateObject(setting, e, settingState, setSettingsState);\n\n                  if (defaultSettings[setting].hasAuthButton) {\n                    setStatus(Status.NOT_AUTHENTICATED);\n                  }\n                }}\n              />\n              {defaultSettings[setting]?.hasAuthButton ? authButton : <></>}\n            </ListItem>\n          );\n        })}\n      <br />\n      {status == Status.AUTHENTICATED && Object.keys(defaultSettings).every((n) => localSettings[n] !== undefined) ? (\n        <div className='n-flex n-justify-between'>\n          <Button floating onClick={handleOpenEditSolutions}>\n            Tweak Prompts\n            <PencilSquareIconOutline className='btn-icon-base-r' />\n          </Button>\n          {Object.keys(messageHistory).length > 0 ? (\n            <Button fill='outlined' onClick={() => deleteAllMessageHistory()}>\n              Delete Model History\n            </Button>\n          ) : (\n            <></>\n          )}\n          <Button\n            style={{ marginRight: '30px' }}\n            onClick={() => {\n              handleClose();\n            }}\n            floating\n          >\n            Start Querying\n            <PlayIconSolid className='btn-icon-base-r' />\n          </Button>\n        </div>\n      ) : (\n        <></>\n      )}\n    </List>\n  );\n  return component;\n};\n\nconst mapStateToProps = (state) => ({\n  settings: getQueryTranslatorSettings(state),\n  messageHistory: getSessionStorageValuesWithPrefix(state, QUERY_TRANSLATOR_HISTORY_PREFIX),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  setGlobalModelClient: (modelClient) => {\n    dispatch(setGlobalModelClient(modelClient));\n  },\n  authenticate: (setIsAuthenticated) => {\n    dispatch(modelClientInitializationThunk(setIsAuthenticated));\n  },\n  updateModelProvider: (modelProviderState) => {\n    dispatch(setModelProvider(modelProviderState));\n  },\n  updateClientSettings: (settingState) => {\n    dispatch(setClientSettings(settingState));\n  },\n  deleteAllMessageHistory: () => {\n    dispatch(deleteAllMessageHistory());\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(ClientSettings);\n"
  },
  {
    "path": "src/extensions/text2cypher/component/LoadingIcon.tsx",
    "content": "import React from 'react';\nimport logo from '../clients/OpenAi/OpenAiLogo.png';\n\nexport const GPT_LOADING_ICON = (\n  <div className='centered' style={{ textAlign: 'center' }}>\n    <br />\n    <img\n      style={{ width: 40, animation: 'pulse 2s infinite', marginTop: 'auto', marginLeft: 'auto', marginRight: 'auto' }}\n      src={logo}\n    ></img>\n    <br />\n    <span style={{ fontSize: 12 }}>Calling OpenAI...</span>\n  </div>\n);\n"
  },
  {
    "path": "src/extensions/text2cypher/component/OverrideCardQueryEditor.tsx",
    "content": "import React, { useCallback, useContext, useEffect } from 'react';\nimport { connect } from 'react-redux';\nimport { Button, Switch } from '@neo4j-ndl/react';\nimport NeoCodeEditorComponent, {\n  DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE,\n} from '../../../component/editor/CodeEditorComponent';\nimport { getReportTypes } from '../../ExtensionUtils';\nimport { queryTranslationThunk } from '../state/QueryTranslatorThunks';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport debounce from 'lodash/debounce';\nimport { updateLastMessage } from '../state/QueryTranslatorActions';\nimport { createNotification } from '../../../application/ApplicationActions';\nimport { getLastMessage, QUERY_TRANSLATOR_EXTENSION_NAME } from '../state/QueryTranslatorSelector';\nimport { GPT_LOADING_ICON } from './LoadingIcon';\nimport {\n  deleteSessionStoragePrepopulationReportFunction,\n  setSessionStoragePrepopulationReportFunction,\n} from '../../state/ExtensionActions';\nimport { getPrepopulateReportExtension } from '../../state/ExtensionSelectors';\n\n// TODO: right now if we change the database in the cardSelector, it should forgot the card history\nexport const NeoOverrideCardQueryEditor = ({\n  pagenumber,\n  reportId,\n  cypherQuery,\n  extensions,\n  reportType,\n  updateCypherQuery,\n  lastMessage,\n  prepopulateExtensionName,\n  translateQuery,\n  updateEnglishQuery,\n  displayError,\n  setPrepopulationReportFunction,\n  deletePrepopulationReportFunction,\n}) => {\n  enum Language {\n    ENGLISH,\n    CYPHER,\n  }\n\n  // States\n  const [language, setLanguage] = React.useState(Language.CYPHER);\n  const [runningTranslation, setRunningTranslation] = React.useState(false);\n  const [englishQuestion, setEnglishQuestion] = React.useState('');\n  const [needsUpdate, setNeedsUpdate] = React.useState(true);\n  const debouncedEnglishQuestionUpdate = useCallback(debounce(updateEnglishQuery, 250), []);\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n\n  useEffect(() => {\n    // Reset text to the dashboard state when the page gets reorganized.\n    if (lastMessage !== englishQuestion) {\n      setEnglishQuestion(lastMessage);\n    }\n  }, [lastMessage]);\n\n  const reportTypes = getReportTypes(extensions);\n\n  const cypherEditor = (\n    <NeoCodeEditorComponent\n      value={cypherQuery}\n      editable={true}\n      language={reportTypes[reportType]?.inputMode || 'cypher'}\n      onChange={(value) => updateCypherQuery(value)}\n      placeholder={`Enter Cypher here...`}\n    />\n  );\n\n  function updateEnglishQuestion(value) {\n    debouncedEnglishQuestionUpdate(pagenumber, reportId, value);\n    setEnglishQuestion(value);\n    if (needsUpdate) {\n      updateCypherQuery(`${cypherQuery} `);\n      setNeedsUpdate(false);\n    }\n  }\n\n  // To prevent a bug with the code editor component, we wrap it in an extra enclosing bracket.\n  const englishEditor = (\n    <div>\n      <NeoCodeEditorComponent\n        value={englishQuestion}\n        editable={true}\n        language={'markdown'}\n        onChange={(value) => {\n          setPrepopulationReportFunction(reportId);\n          updateEnglishQuestion(value);\n        }}\n        style={{ border: '1px dashed darkgrey' }}\n        placeholder={`Enter English here...`}\n      />\n    </div>\n  );\n\n  function triggerTranslation() {\n    setRunningTranslation(true);\n    translateQuery(\n      pagenumber,\n      reportId,\n      englishQuestion,\n      reportType,\n      driver,\n      () => {\n        setRunningTranslation(false);\n      },\n      (e) => {\n        setRunningTranslation(false);\n        console.log(e);\n        displayError(e);\n      }\n    );\n  }\n\n  return (\n    <div>\n      {runningTranslation ? (\n        <div style={{ height: 150, border: '1px dashed grey', position: 'relative' }}>{GPT_LOADING_ICON}</div>\n      ) : (\n        <>\n          <table style={{ marginBottom: 5, width: '100%' }}>\n            <tr style={{ display: 'block', width: '100%' }}>\n              <td style={{ marginBottom: 5, width: '100%' }}>\n                <div style={{ float: 'left', display: 'flex', justifyContent: 'flex-end' }}></div>\n              </td>\n            </tr>\n            <tr style={{ display: 'inline', width: '100%' }}>\n              <td style={{ width: 50, textAlign: 'right', paddingTop: '6px' }}>Cypher</td>\n              <td style={{ width: 50, textAlign: 'left', paddingTop: '8px' }}>\n                <Switch\n                  style={{ backgroundColor: 'grey' }}\n                  checked={language == Language.ENGLISH}\n                  onChange={() => {\n                    if (language == Language.ENGLISH) {\n                      setLanguage(Language.CYPHER);\n                      deletePrepopulationReportFunction(reportId);\n                    } else {\n                      setLanguage(Language.ENGLISH);\n                    }\n                  }}\n                  className='n-ml-2'\n                />\n              </td>\n              <td style={{ width: 50, textAlign: 'left', paddingTop: '6px' }}>&nbsp;English&nbsp;</td>\n              <td style={{ float: 'right' }}>\n                {/* Only show translation button if there's something new to translate */}\n                {language == Language.ENGLISH ? (\n                  <div style={{ display: 'flex', justifyContent: 'flex-end' }}>\n                    <div>\n                      <Button\n                        fill='outlined'\n                        disabled={prepopulateExtensionName == undefined}\n                        style={{ marginLeft: '8px' }}\n                        onClick={() => {\n                          if (prepopulateExtensionName !== undefined) {\n                            triggerTranslation();\n                            setLanguage(Language.CYPHER);\n                            deletePrepopulationReportFunction(reportId);\n                          }\n                        }}\n                      >\n                        Translate\n                      </Button>\n                    </div>\n                  </div>\n                ) : (\n                  <></>\n                )}\n              </td>\n            </tr>\n          </table>\n          {language == Language.CYPHER ? cypherEditor : englishEditor}\n          <div\n            style={\n              language == Language.CYPHER\n                ? DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE\n                : {\n                    ...DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE,\n                    // color: '#006FD6',\n                    borderBottom: '1px dashed darkgrey',\n                    borderLeft: '1px dashed darkgrey',\n                    borderRight: '1px dashed darkgrey',\n                  }\n            }\n          >\n            {language == Language.ENGLISH ? (\n              <>\n                For best results, use a descriptive question. See also the{' '}\n                <a\n                  target='_blank'\n                  style={{ textDecoration: 'underline' }}\n                  href='https://github.com/neo4j-labs/neodash/tree/master/docs/modules/ROOT/pages/user-guide/extensions/natural-language-queries.adoc'\n                >\n                  documentation\n                </a>\n                .\n              </>\n            ) : (\n              reportTypes[reportType]?.helperText\n            )}\n          </div>\n        </>\n      )}\n    </div>\n  );\n};\n\nconst mapStateToProps = (state, ownProps) => ({\n  lastMessage: getLastMessage(state, ownProps.pagenumber, ownProps.reportId),\n  prepopulateExtensionName: getPrepopulateReportExtension(state, ownProps.reportId),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  translateQuery: (pagenumber, reportId, text, reportType, driver, onComplete, onError, onRetry) => {\n    dispatch(queryTranslationThunk(pagenumber, reportId, text, reportType, driver, onComplete, onError, onRetry));\n  },\n  updateEnglishQuery: (pagenumber, reportId, message) => {\n    dispatch(updateLastMessage(message, pagenumber, reportId));\n  },\n  displayError: (message) => {\n    dispatch(createNotification('Error when translating the natural language query', message));\n  },\n  setPrepopulationReportFunction: (reportId) => {\n    dispatch(setSessionStoragePrepopulationReportFunction(reportId, QUERY_TRANSLATOR_EXTENSION_NAME));\n  },\n  deletePrepopulationReportFunction: (reportId) => {\n    dispatch(deleteSessionStoragePrepopulationReportFunction(reportId));\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoOverrideCardQueryEditor);\n"
  },
  {
    "path": "src/extensions/text2cypher/component/QueryTranslatorButton.tsx",
    "content": "import React from 'react';\nimport { connect } from 'react-redux';\nimport { IconButton, MenuItem } from '@neo4j-ndl/react';\nimport QueryTranslatorSettingsModal from './QueryTranslatorSettingsModal';\nimport { ExclamationTriangleIconSolid, LanguageIconSolid } from '@neo4j-ndl/react/icons';\nimport { getModelProvider } from '../state/QueryTranslatorSelector';\nimport Tooltip from '@mui/material/Tooltip/Tooltip';\n\nconst QueryTranslatorButton = (active) => {\n  const [open, setOpen] = React.useState(false);\n  const button = (\n    <Tooltip title='Text2Cypher' aria-label='text2cypher' disableInteractive>\n      <IconButton className='n-mx-1' aria-label='Text2Cypher' onClick={() => setOpen(true)}>\n        <LanguageIconSolid />\n        {active.active == '' || active.active == undefined ? (\n          <ExclamationTriangleIconSolid color='red' className='-n-mt-1 n-ml-3 n-w-4/5'></ExclamationTriangleIconSolid>\n        ) : (\n          <></>\n        )}\n      </IconButton>\n    </Tooltip>\n  );\n\n  const component = (\n    <div style={{ display: 'inline' }}>\n      {button}\n      {open ? <QueryTranslatorSettingsModal open={open} setOpen={setOpen} /> : <></>}\n    </div>\n  );\n\n  return component;\n};\n\nconst mapStateToProps = (state) => ({\n  active: getModelProvider(state),\n});\n\nconst mapDispatchToProps = (_dispatch) => ({});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(QueryTranslatorButton);\n"
  },
  {
    "path": "src/extensions/text2cypher/component/QueryTranslatorSettingsModal.tsx",
    "content": "import React from 'react';\nimport { connect } from 'react-redux';\nimport { setClientSettings, setModelProvider } from '../state/QueryTranslatorActions';\nimport { getQueryTranslatorSettings, getModelProvider } from '../state/QueryTranslatorSelector';\nimport { SELECTION_TYPES } from '../../../config/CardConfig';\nimport NeoSetting from '../../../component/field/Setting';\nimport { QUERY_TRANSLATOR_CONFIG } from '../QueryTranslatorConfig';\nimport ClientSettings from './ClientSettings';\nimport { Dialog } from '@neo4j-ndl/react';\nimport { modelClientInitializationThunk } from '../state/QueryTranslatorThunks';\nimport QueryTranslatorSettingsModelExamples from './model-examples/QueryTranslatorSettingsModelExamples';\n\nconst QueryTranslatorSettingsModal = ({\n  open,\n  setOpen,\n  modelProvider,\n  clientSettings,\n  updateClientSettings,\n  updateModelProvider,\n  initializeModelClient,\n}) => {\n  const [modelProviderState, setModelProviderState] = React.useState(modelProvider);\n  const [settingsState, setSettingsState] = React.useState(clientSettings);\n  const [editDialogIsOpen, setEditDialogIsOpen] = React.useState(false);\n\n  const handleCloseWithSave = () => {\n    updateModelProvider(modelProviderState);\n    updateClientSettings(settingsState);\n    setOpen(false);\n    initializeModelClient();\n  };\n\n  const handleCloseWithoutSave = () => {\n    setOpen(false);\n  };\n\n  const handleOpenEditSolutions = () => {\n    setEditDialogIsOpen(true);\n  };\n\n  const handleCloseEditSolutions = () => {\n    setEditDialogIsOpen(false);\n  };\n\n  if (!editDialogIsOpen) {\n    return (\n      <Dialog size='large' open={open} onClose={handleCloseWithoutSave} aria-labelledby='form-dialog-title'>\n        <Dialog.Header id='form-dialog-title'>Text2Cypher Configuration</Dialog.Header>\n        <Dialog.Content>\n          This extensions lets you create reports with natural language. Your queries (in English) are translated to\n          Cypher by a LLM provider of your choice.\n          <br />\n          <br />\n          Keep in mind that the following data will be sent to a external API:\n          <ul>\n            <li>- Your database schema, including label names, relationship types, and property keys.</li>\n            <li>- Any natural language question that a user writes.</li>\n          </ul>\n          <br />\n          <br />\n          <NeoSetting\n            style={{ marginLeft: '0', marginRight: '0' }}\n            key={'Model Provider'}\n            name={'Model Provider'}\n            label={'Model Provider'}\n            value={modelProviderState}\n            type={SELECTION_TYPES.LIST}\n            choices={Object.keys(QUERY_TRANSLATOR_CONFIG.availableClients)}\n            onChange={(e) => setModelProviderState(e)}\n          />\n          {modelProviderState ? (\n            <ClientSettings\n              handleOpenEditSolutions={handleOpenEditSolutions}\n              handleClose={handleCloseWithSave}\n              modelProvider={modelProviderState}\n              settingState={settingsState}\n              setSettingsState={setSettingsState}\n            />\n          ) : (\n            <>Select one of the available clients.</>\n          )}\n        </Dialog.Content>\n      </Dialog>\n    );\n  }\n  return (\n    <QueryTranslatorSettingsModelExamples\n      handleCloseEditSolutions={handleCloseEditSolutions}\n      open={open}\n      handleCloseWithoutSave={handleCloseWithoutSave}\n    ></QueryTranslatorSettingsModelExamples>\n  );\n};\n\nconst mapStateToProps = (state) => ({\n  clientSettings: getQueryTranslatorSettings(state),\n  modelProvider: getModelProvider(state),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  updateClientSettings: (settings) => dispatch(setClientSettings(settings)),\n  updateModelProvider: (modelProvider) => dispatch(setModelProvider(modelProvider)),\n  initializeModelClient: (setIsAuthenticated) => {\n    dispatch(modelClientInitializationThunk(setIsAuthenticated));\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(QueryTranslatorSettingsModal);\n"
  },
  {
    "path": "src/extensions/text2cypher/component/model-examples/ExampleDisplayTable.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport {\n  TrashIconOutline,\n  PencilSquareIconOutline,\n  ChevronDoubleLeftIconOutline,\n  ChevronLeftIconOutline,\n  ChevronDoubleRightIconOutline,\n  ChevronRightIconOutline,\n} from '@neo4j-ndl/react/icons';\nimport ShowMoreText from 'react-show-more-text';\nimport {\n  createColumnHelper,\n  flexRender,\n  getCoreRowModel,\n  getPaginationRowModel,\n  useReactTable,\n} from '@tanstack/react-table';\nimport { IconButton } from '@neo4j-ndl/react';\nimport { getModelExamples } from '../../state/QueryTranslatorSelector';\nimport { deleteModelExample } from '../../state/QueryTranslatorActions';\nimport { connect } from 'react-redux';\n\ntype Example = {\n  question: string;\n  answer: string;\n};\n\nconst RemoveButton = ({ onClick }) => (\n  <IconButton\n    className='n-float-right n-text-right'\n    style={{ color: 'red' }}\n    aria-label='remove'\n    onClick={onClick}\n    size='medium'\n    clean\n  >\n    <TrashIconOutline aria-label={'remove'} />\n  </IconButton>\n);\n\nconst EditButton = ({ onClick }) => (\n  <IconButton className='n-float-right n-text-right' onClick={onClick} aria-label={'edit'} size='medium' clean>\n    <PencilSquareIconOutline aria-label={'edit'} />\n  </IconButton>\n);\n\nfunction ExampleDisplayTable({ examples, deleteModelExample, handleEdit }) {\n  const columnHelper = createColumnHelper<Example>();\n\n  // Buttons that will be used inside the table\n  const RowButtons = (index) => (\n    <div className='n-float-right n-text-right n-w-[100px]'>\n      <RemoveButton onClick={() => deleteModelExample(index)} />\n      <EditButton onClick={() => handleEdit(index)} />\n    </div>\n  );\n\n  const columns = React.useMemo(\n    () => [\n      columnHelper.accessor('question', {\n        cell: (info) => <ShowMoreText lines={3}>{info.getValue()}</ShowMoreText>,\n        header: () => 'Question',\n      }),\n      columnHelper.accessor('answer', {\n        cell: (info) => (\n          <div>\n            <ShowMoreText lines={3}>{info.getValue()}</ShowMoreText>\n          </div>\n        ),\n        header: 'Answer',\n      }),\n      {\n        header: '',\n        id: 'actions',\n        cell: ({ row }) => RowButtons(row.index),\n      },\n    ],\n    []\n  );\n\n  const data = React.useMemo(() => examples, [examples]);\n\n  const table = useReactTable({\n    data,\n    columns,\n    getCoreRowModel: getCoreRowModel(),\n    getPaginationRowModel: getPaginationRowModel(),\n    initialState: {\n      pagination: {\n        pageSize: 5,\n      },\n    },\n  });\n\n  const [cellWidth, setCellWidth] = useState('600px');\n\n  // For screens with 1080 x pixels or less\n  useEffect(() => {\n    const updateCellWidth = () => {\n      if (window.innerWidth <= 1080) {\n        // Example breakpoint for smaller screens\n        setCellWidth('463px');\n      } else {\n        setCellWidth('600px');\n      }\n    };\n\n    window.addEventListener('resize', updateCellWidth);\n    updateCellWidth(); // Initialize on component mount\n\n    return () => window.removeEventListener('resize', updateCellWidth);\n  }, []);\n\n  return (\n    <div className='n-flex n-flex-col n-gap-2 n-mb-[15px]'>\n      <div className='ndl-table-root n-rounded-lg' style={{ maxWidth: '100%', overflowX: 'auto' }}>\n        <table className='ndl-div-table'>\n          <thead className='ndl-table-thead'>\n            {table.getHeaderGroups().map((headerGroup) => (\n              <tr\n                className='ndl-table-tr'\n                style={{\n                  display: 'grid',\n                  gridTemplateColumns: '1fr 1fr 120px',\n                  textAlign: 'left', // Aligns text to the left\n                }}\n                key={headerGroup.id}\n              >\n                {headerGroup.headers.map((header) => (\n                  <th\n                    className='ndl-table-th ndl-focusable-cell ndl-header-group ndl-header-cell'\n                    key={header.id}\n                    style={{\n                      wordBreak: 'break-word',\n                      overflowWrap: 'break-word',\n                      borderBottom: '1px solid #ccc', // Adds bottom border to each cell\n                      padding: '8px', // Adds padding for readability\n                      maxWidth: cellWidth,\n                    }}\n                  >\n                    {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}\n                  </th>\n                ))}\n              </tr>\n            ))}\n          </thead>\n          <tbody className='ndl-table-tbody'>\n            {table.getRowModel().rows.map((row) => (\n              <tr\n                className='ndl-table-tr'\n                style={{\n                  display: 'grid',\n                  gridTemplateColumns: '1fr 1fr 120px',\n                }}\n                key={row.id}\n              >\n                {row.getVisibleCells().map((cell) => (\n                  <td\n                    className='ndl-table-td ndl-focusable-cell'\n                    key={cell.id}\n                    style={{\n                      wordBreak: 'break-word',\n                      overflowWrap: 'break-word',\n                      padding: '8px', // Adds padding for readability\n                      maxWidth: cellWidth,\n                    }}\n                  >\n                    {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                  </td>\n                ))}\n              </tr>\n            ))}\n          </tbody>\n        </table>\n        <div className='paginaton n-flex n-place-content-center n-my-[14px] n-pt-[14px]'>\n          <IconButton\n            className='n-place-content-left'\n            onClick={() => table.setPageIndex(0)}\n            aria-label={'edit'}\n            size='medium'\n            clean\n            disabled={!table.getCanPreviousPage()}\n          >\n            <ChevronDoubleLeftIconOutline className='n-py-0' aria-label={'firstPage'} />\n          </IconButton>\n\n          <IconButton\n            className='n-place-content-left'\n            onClick={() => table.previousPage()}\n            aria-label={'previousPage'}\n            size='medium'\n            clean\n            disabled={!table.getCanPreviousPage()}\n          >\n            <ChevronLeftIconOutline className='n-py-0' aria-label={'firstPage'} />\n          </IconButton>\n\n          <span className='n-mt-[6px]'>\n            &nbsp;Page&nbsp;\n            <strong>\n              {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}\n            </strong>\n            &nbsp;\n          </span>\n\n          <IconButton\n            className='n-place-content-center'\n            onClick={() => table.nextPage()}\n            aria-label={'nextPage'}\n            size='medium'\n            clean\n            disabled={!table.getCanNextPage()}\n          >\n            <ChevronRightIconOutline className='n-py-0' aria-label={'firstPage'} />\n          </IconButton>\n\n          <IconButton\n            className='n- n-place-content-center n-mr-[3px]'\n            onClick={() => table.setPageIndex(table.getPageCount() - 1)}\n            aria-label={'lastPage'}\n            size='medium'\n            clean\n            disabled={!table.getCanNextPage()}\n          >\n            <ChevronDoubleRightIconOutline className='n-py-0' aria-label={'firstPage'} />\n          </IconButton>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nconst mapStateToProps = (state) => ({\n  examples: getModelExamples(state),\n});\n\n// Function to launch an action to modify the state\nconst mapDispatchToProps = (dispatch) => ({\n  deleteModelExample: (index) => dispatch(deleteModelExample(index)),\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(ExampleDisplayTable);\n"
  },
  {
    "path": "src/extensions/text2cypher/component/model-examples/ExampleEditorModal.tsx",
    "content": "import React, { useState, useContext } from 'react';\nimport { connect } from 'react-redux';\nimport { Dialog, Button, Textarea } from '@neo4j-ndl/react';\nimport { addModelExample, updateModelExample } from '../../state/QueryTranslatorActions';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport { getDatabase } from '../../../../settings/SettingsSelectors';\nimport { checkModelExampleAndSubmit } from './utils';\nimport NeoCodeEditorComponent from '../../../../component/editor/CodeEditorComponent';\n\nconst ExampleEditorModal = ({\n  index,\n  question,\n  answer,\n  exampleEditorIsOpen,\n  setExampleEditorIsOpen,\n  database,\n  updateModelExample,\n  addModelExample,\n}) => {\n  // States\n  const [questionState, setQuestionState] = useState(question);\n  const [answerState, setAnswerState] = useState(answer);\n  const [questionErrorMessage, setQuestionErrorMessage] = useState('');\n  const [errorMessage, setErrorMessage] = useState('');\n  const [isSaving, setIsSaving] = useState(false); // Add isSaving state\n\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n\n  const handleCloseEditor = () => {\n    setExampleEditorIsOpen(false);\n  };\n\n  async function handleSubmit(e) {\n    e.preventDefault();\n\n    // Disable the \"Save\" button and show \"Saving...\" message while saving\n    setIsSaving(true);\n\n    // Passing on props to utils.ts\n    await checkModelExampleAndSubmit(\n      questionState,\n      setQuestionState,\n      answerState,\n      setAnswerState,\n      driver,\n      database,\n      handleFormSubmit,\n      setQuestionErrorMessage,\n      setErrorMessage\n    );\n\n    // Re-enable the \"Save\" button after saving is complete\n    setIsSaving(false);\n  }\n\n  // Function to handle form submission\n  const handleFormSubmit = (question, answer) => {\n    /* If the current index state has a value, use it \n    to update the Q&A, otherwise, create a new one*/\n    index != null && index >= 0 ? updateModelExample(index, question, answer) : addModelExample(question, answer);\n    // Reset the form and hide it\n    setExampleEditorIsOpen(false);\n  };\n\n  const handleFormClose = () => {\n    // Reset the form and hide it\n    setExampleEditorIsOpen(false);\n  };\n\n  const cypherEditor = (\n    <NeoCodeEditorComponent\n      value={answerState}\n      editable={true}\n      onChange={(e) => setAnswerState(e)}\n      placeholder={`Enter Cypher here...`}\n    />\n  );\n\n  return (\n    <Dialog size='large' aria-labelledby='form-dialog-title' open={exampleEditorIsOpen} onClose={handleCloseEditor}>\n      <Dialog.Header> {index != null && index >= 0 ? 'Edit' : 'Create New'} Example </Dialog.Header>\n      <Dialog.Content>\n        {index != null && index >= 0 ? 'Edit' : 'Create new'} prompt and cypher query examples for the purposes of\n        training LLM prediction performance.\n        <br />\n        <br />\n        <div\n          style={{\n            backgroundColor: '#fff',\n            width: '100%',\n            margin: '10px auto',\n          }}\n        >\n          <form onSubmit={handleSubmit}>\n            <div className='n-mb-6'>\n              <label>Prompt</label>\n              <Textarea\n                className='n-mt-1'\n                errorText={questionErrorMessage}\n                fluid\n                size='small'\n                value={questionState}\n                onChange={(e) => setQuestionState(e.target.value)}\n                label=''\n                placeholder='Enter prompt here ...'\n              />\n            </div>\n            <div className=' n-mb-1 '>Cypher Query</div>\n            {/* Cypher editor */}\n            <div className='n-mb-6'>{cypherEditor}</div>\n            <p className='n-text-palette-danger-text'> {errorMessage}</p>\n\n            {/* Save and close buttons  */}\n            <div className='n-text-right n-t-2'>\n              {/* Changes button to loading button is isSaving state=true */}\n              {isSaving ? (\n                <Button className='n-m-1' loading>\n                  Saving\n                </Button>\n              ) : (\n                <Button type='submit' className='n-m-1' disabled={isSaving}>\n                  {' '}\n                  {/* Disable the button while saving */}\n                  Save\n                </Button>\n              )}\n              <Button fill='outlined' type='button' onClick={handleFormClose} className='n-m-1'>\n                Close\n              </Button>\n            </div>\n          </form>\n        </div>\n      </Dialog.Content>\n    </Dialog>\n  );\n};\n\n// Function to access the state and pass to the component some parameters\nconst mapStateToProps = (state, ownProps) => ({\n  database: getDatabase(state, ownProps.pagenumber, ownProps.reportId),\n});\n\n// Function to launch an action to modify the state\nconst mapDispatchToProps = (dispatch) => ({\n  updateModelExample: (index, question, answer) => dispatch(updateModelExample(index, question, answer)),\n  addModelExample: (question, answer) => dispatch(addModelExample(question, answer)),\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(ExampleEditorModal);\n"
  },
  {
    "path": "src/extensions/text2cypher/component/model-examples/QueryTranslatorSettingsModelExamples.tsx",
    "content": "import React, { useState } from 'react';\nimport { connect } from 'react-redux';\nimport { getModelExamples } from '../../state/QueryTranslatorSelector';\nimport { Dialog, Button } from '@neo4j-ndl/react';\nimport ExampleEditorModal from './ExampleEditorModal';\nimport ExampleDisplayTable from './ExampleDisplayTable';\n\nconst QueryTranslatorSettingsModelExamples = ({ handleCloseEditSolutions, examples, open, handleCloseWithoutSave }) => {\n  // States\n  const [exampleEditorIsOpen, setExampleEditorIsOpen] = useState(false);\n  const [index, setIndex] = useState<number | null>(0);\n\n  // Function for edit button being pressed\n  const handleEdit = (index: number) => {\n    setIndex(index);\n    setExampleEditorIsOpen(true);\n  };\n\n  // Function for AddQ&A button being pressed\n  const handleAdd = () => {\n    setIndex(null);\n    setExampleEditorIsOpen(true);\n  };\n\n  // Returns viewer or editor depending on exampleEditorIsOpen state\n  if (!exampleEditorIsOpen) {\n    return (\n      <Dialog\n        className='dialog-xl n-bg-palette-neutral-bg-default ndl-theme-light'\n        open={open}\n        onClose={handleCloseWithoutSave}\n        aria-labelledby='form-dialog-title'\n      >\n        <Dialog.Header className='n-ml-2' id='form-dialog-title'>\n          LLM Examples\n        </Dialog.Header>\n        <Dialog.Content className='n-ml-2'>\n          You can define custom English to Cypher translation examples here. <br /> Adding your own examples will\n          improve the accuracy of translating English to Cypher in your dashboard.\n          <br />\n          <br />\n          <ExampleDisplayTable handleEdit={handleEdit} />\n          <div>\n            <Button className='n-float-left' onClick={handleAdd}>\n              Create New\n            </Button>\n            <Button className='n-float-right' onClick={handleCloseEditSolutions}>\n              Save\n            </Button>\n          </div>\n        </Dialog.Content>\n      </Dialog>\n    );\n  }\n  return (\n    <ExampleEditorModal\n      index={index}\n      // checks if index exists, and if it does, it passes the question and answer props over to the component, otherwise is empty\n      question={index !== null && examples[index].question ? examples[index].question : ''}\n      answer={index !== null && examples[index].answer ? examples[index].answer : ''}\n      exampleEditorIsOpen={exampleEditorIsOpen}\n      setExampleEditorIsOpen={setExampleEditorIsOpen}\n    ></ExampleEditorModal>\n  );\n};\n\n// Function to access the state and pass to the component some parameters\nconst mapStateToProps = (state) => ({\n  examples: getModelExamples(state),\n});\n\nexport default connect(mapStateToProps)(QueryTranslatorSettingsModelExamples);\n"
  },
  {
    "path": "src/extensions/text2cypher/component/model-examples/utils.ts",
    "content": "import { validateQuery } from '../../../../utils/ReportUtils';\n\nexport async function checkModelExampleAndSubmit(\n  question,\n  setQuestion,\n  answer,\n  setAnswer,\n  driver,\n  database,\n  handleFormSubmit,\n  setQuestionErrorMessage,\n  setErrorMessage\n) {\n  // If both fields are filled, reset the error message\n  setQuestionErrorMessage('');\n  setErrorMessage('');\n\n  // Check if either question or answer is empty\n  if (!question && !answer) {\n    setErrorMessage('Both fields must be filled');\n    return;\n  }\n  if (!question) {\n    setQuestionErrorMessage('Field must be filled');\n    return; // Don't proceed with submission\n  }\n  if (!answer) {\n    setErrorMessage('Answer field must be filled');\n    return; // Don't proceed with submission\n  }\n\n  // Proceed with submission\n  let isValid = await validateQuery(answer, driver, database);\n\n  if (!isValid) {\n    setErrorMessage('The answer is not a valid Cypher query.');\n    return; // Don't proceed with submission\n  }\n\n  // Submit question and answer and then reset question and answer states\n  handleFormSubmit(question, answer);\n  setQuestion('');\n  setAnswer('');\n}\n"
  },
  {
    "path": "src/extensions/text2cypher/state/QueryTranslatorActions.ts",
    "content": "import {\n  deleteAllKeysInSessionStorageWithPrefix,\n  deleteSessionStorageValue,\n  SESSION_STORAGE_PREFIX,\n  setSessionStorageValue,\n} from '../../../sessionStorage/SessionStorageActions';\nimport {\n  getModelClientSessionStorageKey,\n  getSessionStorageHistoryKey,\n  QUERY_TRANSLATOR_HISTORY_PREFIX,\n} from './QueryTranslatorSelector';\n\nexport const QUERY_TRANSLATOR_ACTION_PREFIX = 'DASHBOARD/EXTENSIONS/QUERY_TRANSLATOR/';\nexport const QUERY_TRANSLATOR_SESSION_STORAGE_ACTION_PREFIX = `DASHBOARD/EXTENSIONS/QUERY_TRANSLATOR/${SESSION_STORAGE_PREFIX}/`;\n\nexport const SET_MODEL_PROVIDER = `${QUERY_TRANSLATOR_ACTION_PREFIX}SET_MODEL_PROVIDER`;\nexport const setModelProvider = (modelProvider) => ({\n  type: SET_MODEL_PROVIDER,\n  payload: { modelProvider },\n});\n\nexport const SET_CLIENT_SETTINGS = `${QUERY_TRANSLATOR_ACTION_PREFIX}SET_CLIENT_SETTINGS`;\nexport const setClientSettings = (settings) => ({\n  type: SET_CLIENT_SETTINGS,\n  payload: { settings },\n});\n\nexport const SET_GLOBAL_MODEL_CLIENT = `${QUERY_TRANSLATOR_ACTION_PREFIX}SET_GLOBAL_MODEL_CLIENT`;\nexport const setGlobalModelClient = (modelClient) =>\n  setSessionStorageValue(getModelClientSessionStorageKey(), modelClient);\n\nexport const UPDATE_LAST_MESSAGE = `${QUERY_TRANSLATOR_ACTION_PREFIX}UPDATE_LAST_MESSAGE`;\n/**\n * Action to store the last message sent between a user and the query translator\n * @param message History of messages between a card and the model\n * @param pagenumber Index of the page related to the card\n * @param cardId Id of the card inside the page\n * @returns\n */\nexport const updateLastMessage = (message: string, pagenumber: number, cardId: string) => ({\n  type: UPDATE_LAST_MESSAGE,\n  payload: { message, pagenumber, cardId },\n});\n\nexport const ADD_EXAMPLE = `${QUERY_TRANSLATOR_ACTION_PREFIX}ADD_EXAMPLE`;\n/**\n * Action to add an example to enhance model capabilities\n * @param question Question to ask\n * @param answer Answer to the question\n * @returns\n */\nexport const addModelExample = (question: string, answer: string) => ({\n  type: ADD_EXAMPLE,\n  payload: { question, answer },\n});\n\nexport const UPDATE_EXAMPLE = `${QUERY_TRANSLATOR_ACTION_PREFIX}UPDATE_EXAMPLE`;\n/**\n * Action to update an existing example\n * @param index Index of the QA in its list\n * @param question Question to ask\n * @param answer Answer to the question\n * @returns\n */\nexport const updateModelExample = (index: number, question: string, answer: string) => ({\n  type: UPDATE_EXAMPLE,\n  payload: { index, question, answer },\n});\n\nexport const DELETE_EXAMPLE = `${QUERY_TRANSLATOR_ACTION_PREFIX}DELETE_EXAMPLE`;\n/**\n * Action to delete an existing example\n * @param index Index of the QA in its list\n * @returns\n */\nexport const deleteModelExample = (index: number) => ({\n  type: DELETE_EXAMPLE,\n  payload: { index },\n});\n\n/**\n * Action to add a new message to the history\n * @param history History of messages between a card and the model\n * @param pagenumber Index of the page related to the card\n * @param cardId Id of the card inside the page\n * @returns\n */\nexport const updateMessageHistory = (cardHistory: any[], pagenumber: number, cardId: string) =>\n  setSessionStorageValue(getSessionStorageHistoryKey(pagenumber, cardId), cardHistory);\n\nexport const deleteMessageHistory = (pagenumber: number, cardId: string) =>\n  deleteSessionStorageValue(getSessionStorageHistoryKey(pagenumber, cardId));\n\nexport const deleteAllMessageHistory = () => deleteAllKeysInSessionStorageWithPrefix(QUERY_TRANSLATOR_HISTORY_PREFIX);\n"
  },
  {
    "path": "src/extensions/text2cypher/state/QueryTranslatorReducer.ts",
    "content": "/**\n * Reducers define changes to the application state when a given action\n */\n\nimport {\n  SET_MODEL_PROVIDER,\n  SET_CLIENT_SETTINGS,\n  UPDATE_LAST_MESSAGE,\n  UPDATE_EXAMPLE,\n  DELETE_EXAMPLE,\n  ADD_EXAMPLE,\n} from './QueryTranslatorActions';\n\nexport const INITIAL_EXTENSION_STATE = {\n  modelProvider: '', // Name of the provider (defined in the config)\n  history: {}, // Objects that keeps, for every card, their history (to move to session store)\n  modelClient: '', // Object to connect with the model API (to move to session store)\n  settings: {}, // Settings needed by the client to operate\n  lastMessages: {},\n  examples: [], // User can pass down to the model different examples of QAs\n};\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\nexport const queryTranslatorReducer = (state = INITIAL_EXTENSION_STATE, action: { type: any; payload: any }) => {\n  const { type, payload } = action;\n\n  switch (type) {\n    case SET_MODEL_PROVIDER: {\n      const { modelProvider } = payload;\n      state = update(state, { modelProvider: modelProvider });\n      return state;\n    }\n    case SET_CLIENT_SETTINGS: {\n      const { settings } = payload;\n      state = update(state, { settings: settings });\n      return state;\n    }\n    case UPDATE_LAST_MESSAGE: {\n      const { message, pagenumber, cardId } = payload;\n      let newLastMessages = { ...state.lastMessages };\n      if (newLastMessages && !newLastMessages[pagenumber]) {\n        newLastMessages[pagenumber] = {};\n        newLastMessages[pagenumber][cardId] = message;\n      } else {\n        newLastMessages[pagenumber][cardId] = message;\n      }\n      state = update(state, { lastMessages: newLastMessages });\n      return state;\n    }\n    case ADD_EXAMPLE: {\n      const { question, answer } = payload;\n      let currentExamples = state.examples ? state.examples : [];\n      let newExamples: object[] = [...currentExamples];\n      newExamples.push({ question: question, answer: answer });\n      state = update(state, { examples: newExamples });\n      return state;\n    }\n    case UPDATE_EXAMPLE: {\n      const { index, question, answer } = payload;\n      let newExamples: object[] = [...state.examples];\n      newExamples[index] = { question: question, answer: answer };\n      state = update(state, { examples: newExamples });\n      return state;\n    }\n    case DELETE_EXAMPLE: {\n      const { index } = payload;\n      let newExamples: object[] = [...state.examples];\n      // Removing the element at the specified index\n      newExamples.splice(index, 1);\n      state = update(state, { examples: newExamples });\n      return state;\n    }\n    default: {\n      return state;\n    }\n  }\n};\n"
  },
  {
    "path": "src/extensions/text2cypher/state/QueryTranslatorSelector.ts",
    "content": "import { getSessionStorageValue } from '../../../sessionStorage/SessionStorageSelectors';\n\nexport const QUERY_TRANSLATOR_EXTENSION_NAME = 'query-translator';\nexport const QUERY_TRANSLATOR_HISTORY_PREFIX = `${QUERY_TRANSLATOR_EXTENSION_NAME}_history__`;\n\n/**\n * Creates a new composite key for RW operations against the SessionStorage.\n */\nexport const getSessionStorageHistoryKey = (pagenumber, cardId) => {\n  return `${QUERY_TRANSLATOR_HISTORY_PREFIX}__${pagenumber}__${cardId}`;\n};\n/**\n * Returns a key for RW operations against the SessionStorage.\n */\nexport const getModelClientSessionStorageKey = () => 'query_translator_model_client_tmp';\n\nconst checkExtensionConfig = (state: any) => {\n  return state?.dashboard?.extensions[QUERY_TRANSLATOR_EXTENSION_NAME];\n};\n\nexport const getHistory = (state: any) => {\n  let history = checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].history;\n  return history != undefined && history ? history : {};\n};\n\nexport const getLastMessages = (state: any) => {\n  let lastMessages =\n    checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].lastMessages;\n  return lastMessages != undefined && lastMessages ? lastMessages : {};\n};\n\nexport const getQueryTranslatorSettings = (state: any) => {\n  let clientSettings =\n    checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].settings;\n  return clientSettings != undefined && clientSettings ? clientSettings : {};\n};\n\nexport const getModelProvider = (state: any) => {\n  let modelProvider =\n    checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].modelProvider;\n  return modelProvider != undefined && modelProvider ? modelProvider : '';\n};\n\n/**\n * The extension keeps, during one session, the client to connect to the model API.\n * The client is kept only during the session, so every refresh it is deleted.\n * @param state Current state of the session\n * @returns Current model client\n */\nexport const getModelClient = (state: any) => {\n  let modelClient = getSessionStorageValue(state, getModelClientSessionStorageKey());\n  return modelClient != undefined && modelClient ? modelClient : undefined;\n};\n\n/**\n * The extension keeps, during one session, the history of messages between a user and a model.\n * The history is kept only during the session, so every refresh it is deleted.\n * @param state Current state of the session\n * @param pagenumber Index of the page where the card lives\n * @param cardId Index that identifies the card inside the page\n * @returns history of messages between the user and the model within the context of that card (defaulted to [])\n */\nexport const getHistoryPerCard = (state: any, pagenumber, cardId) => {\n  let sessionStorageKey = getSessionStorageHistoryKey(pagenumber, cardId);\n  let cardHistory = getSessionStorageValue(state, sessionStorageKey);\n  return cardHistory != undefined && cardHistory ? cardHistory : [];\n};\n\n/**\n * We persist the last message sent from the user to the model.\n * @param state State of the application\n * @param pagenumber Number of the page where the card lives\n * @param id Unique identifier of the card\n * @returns\n */\nexport const getLastMessage = (state: any, pagenumber, id) => {\n  let messages = getLastMessages(state);\n  let lastMessage = messages[pagenumber] && messages[pagenumber][id];\n  return lastMessage !== undefined ? lastMessage : '';\n};\n\nexport const getApiKey = (state: any) => {\n  let settings = getQueryTranslatorSettings(state);\n  return settings.apiKey != undefined && settings.apiKey ? settings.apiKey : '';\n};\n\n/**\n * Method to retrieve the examples provided by the user in the shape {question, answer}\n * @param state State of the application\n * @returns List of examples provided by the user\n */\nexport const getModelExamples = (state: any) => {\n  let examples = checkExtensionConfig(state) && state.dashboard.extensions[QUERY_TRANSLATOR_EXTENSION_NAME].examples;\n  return examples != undefined && examples ? examples : [];\n};\n"
  },
  {
    "path": "src/extensions/text2cypher/state/QueryTranslatorThunks.ts",
    "content": "import { updateReportQueryThunk } from '../../../card/CardThunks';\nimport { getDatabase } from '../../../settings/SettingsSelectors';\nimport { ModelClient } from '../clients/ModelClient';\nimport { getModelClientObject } from '../QueryTranslatorConfig';\nimport { setGlobalModelClient, updateLastMessage, updateMessageHistory } from './QueryTranslatorActions';\nimport {\n  getQueryTranslatorSettings,\n  getHistoryPerCard,\n  getModelClient,\n  getModelProvider,\n  getModelExamples,\n} from './QueryTranslatorSelector';\nimport { Status } from '../util/Status';\n\nconst consoleLogAsync = async (message: string, other?: any) => {\n  await new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.info(message, other));\n};\n\n/**\n * Thunk used to initialize the client model. To inizialize the client, we need to check that\n * it can authenticate to it's service by calling its authenticate function\n * @returns True if the client is created, otherwise False\n */\nexport const modelClientInitializationThunk =\n  (\n    setIsAuthenticated = () => {\n      return Status.ERROR;\n    }\n  ) =>\n  async (dispatch: any, getState: any) => {\n    const state = getState();\n\n    // Fetching the client properties from the state\n    let modelProvider = getModelProvider(state);\n    let settings = getQueryTranslatorSettings(state);\n\n    if (modelProvider && settings) {\n      // Getting the correct ModelClient object\n      let tmpClient = getModelClientObject(modelProvider, settings);\n\n      // Try authentication\n      let isAuthenticated = await tmpClient.authenticate(setIsAuthenticated);\n\n      // If the authentication runs smoothly, store the client inside the application state\n      if (isAuthenticated) {\n        dispatch(setGlobalModelClient(tmpClient));\n        return tmpClient;\n      }\n    }\n    return undefined;\n  };\n\n/**\n * Wrapper to get the model client from the state if already exists, otherwise it will recreate it and check that\n * the authentication still works\n * @returns An instance of the model client\n */\nconst getModelClientThunk = () => async (dispatch: any, getState: any) => {\n  const state = getState();\n  let modelClient = getModelClient(state);\n\n  // If not persisted in the current session, try to initialize a new model\n  if (!modelClient) {\n    let newClient = await dispatch(modelClientInitializationThunk());\n    if (newClient) {\n      return newClient;\n    }\n  }\n  return modelClient;\n};\n\n/**\n * Thunk used to handle the request to a model client for a query translation.\n * @param pagenumber Index of the page where the card lives\n * @param cardId Index that identifies a card inside its page\n * @param message Message inserted by the user\n * @param reportType Type of report used by the card calling the thunk\n * @param driver Neo4j Driver used to fetch the schema from the database\n * @param onComplete Function used to bring the query back to the calling component\n * @param onError Function used to bring the error back to the calling component\n * @param onRetry  Function used to bring the current validation step counter\n *  back to the calling component\n */\nexport const queryTranslationThunk =\n  (\n    pagenumber,\n    cardId,\n    message,\n    reportType,\n    driver,\n    onComplete = (e) => {\n      console.log(e);\n    },\n    onError = (e) => {\n      console.log(e);\n    },\n    onRetry = (e) => {\n      console.log(e);\n    }\n  ) =>\n  async (dispatch: any, getState: any) => {\n    let query;\n    try {\n      const state = getState();\n      const database = getDatabase(state, pagenumber, cardId);\n      const examples = getModelExamples(state);\n      // Storing the message that will be sent to the model\n      dispatch(updateLastMessage(message, pagenumber, cardId));\n\n      // Retrieving the model client from the state\n      let client: ModelClient = await dispatch(getModelClientThunk());\n      if (client) {\n        // If missing, pass down the driver to persist it inside the client\n        if (!client.driver) {\n          client.setDriver(driver);\n        }\n        const messageHistory = getHistoryPerCard(state, pagenumber, cardId);\n        let translationRes = await client.queryTranslation(\n          message,\n          messageHistory,\n          database,\n          reportType,\n          examples,\n          onRetry\n        );\n        query = translationRes[0];\n        let newHistory = translationRes[1];\n        // The history will be updated only if the length is different (otherwise, it's the same history)\n        if (messageHistory.length < newHistory.length && query) {\n          dispatch(updateMessageHistory(newHistory, pagenumber, cardId));\n          dispatch(updateReportQueryThunk(cardId, query));\n          onComplete(query);\n        }\n      } else {\n        throw new Error(\n          'Could not start client for the natural language translation, please check that you have an API key configured in the extension settings.'\n        );\n      }\n    } catch (e) {\n      await consoleLogAsync(\n        `Something wrong happened while calling the model client for the card number ${cardId} inside the page ${pagenumber}: \\n`,\n        { e }\n      );\n      onError(e);\n    }\n  };\n"
  },
  {
    "path": "src/extensions/text2cypher/util/Status.ts",
    "content": "export enum Status {\n  NOT_AUTHENTICATED,\n  AUTHENTICATED,\n  ERROR,\n}\n"
  },
  {
    "path": "src/extensions/text2cypher/util/Util.ts",
    "content": "import { createNotification } from '../../../application/ApplicationActions';\nimport { toggleCardSettingsThunk } from '../../../card/CardThunks';\nimport { QUERY_TRANSLATOR_EXTENSION_NAME } from '../state/QueryTranslatorSelector';\nimport { queryTranslationThunk } from '../state/QueryTranslatorThunks';\n\n/**\n * This function translates a query using the specified driver and dispatches actions accordingly.\n * It checks for the last message associated with the given pagenumber and ID from the QUERY_TRANSLATOR_EXTENSION_NAME extension.\n * If a last message exists, it dispatches the queryTranslationThunk action with the necessary parameters.\n * The result is set using the provided setResult function.\n * If an error occurs during translation, a notification with an error message is dispatched.\n * @param driver Neo4j driver\n * @param dispatch Dispatch function to call actions\n * @param pagenumber number of the page where the card lives\n * @param id id of the card\n * @param reportType Type of the report (needed for prompting logic)\n * @param extensions Extension state\n * @param setResult Functions to set the results back to the calling component\n */\nexport function translateQuery(driver, dispatch, pagenumber, id, reportType, extensions, setResult) {\n  const messages = extensions[QUERY_TRANSLATOR_EXTENSION_NAME].lastMessages;\n  let lastMessage = messages && messages[pagenumber] && messages[pagenumber][id] ? messages[pagenumber][id] : false;\n\n  if (lastMessage) {\n    dispatch(\n      queryTranslationThunk(\n        pagenumber,\n        id,\n        lastMessage,\n        reportType,\n        driver,\n        (result) => {\n          setResult(result);\n        },\n        (error) => {\n          dispatch(createNotification('Error when translating the natural language query', error));\n          dispatch(toggleCardSettingsThunk(id, true));\n        }\n      )\n    );\n  }\n}\n"
  },
  {
    "path": "src/index.pcss",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* Create CSS class based components */\n/* https://tailwindcss.com/docs/reusing-styles#extracting-classes-with-apply */\n@layer components {\n  /* Page structure */\n  .page-spacing {\n    @apply n-p-2 sm:n-p-3 md:n-p-4;\n  }\n\n  .page-spacing-overflow {\n    @apply n-pb-2 sm:n-pb-3 md:n-pb-4;\n  }\n\n  /* Tabs */\n  .visible-on-tab-hover {\n    display: none;\n  }\n\n  .ndl-tab:hover .visible-on-tab-hover,\n  .ndl-tab .visible-on-tab-hover.open-menu {\n    display: inline-flex;\n  }\n\n  /* Override Mui default z-index */\n  .MuiAppBar-root.n-z-20 {\n    z-index: 20 !important;\n  }\n\n  /* Utility functions for buttons */\n  .btn-icon-sm-r {\n    @apply n-ml-token-2 n-w-4 n-h-4;\n  }\n  .btn-icon-sm-l {\n    @apply n-mr-token-2 n-w-4 n-h-4;\n  }\n\n  .btn-icon-base-r-m {\n    @apply n-ml-token-3 n-w-5 n-h-5;\n  }\n\n  .btn-icon-base-r {\n    @apply n-ml-token-3 n-w-6 n-h-6;\n  }\n\n  .btn-icon-base-l {\n    @apply n-mr-token-3 n-w-6 n-h-6;\n  }\n\n  .btn-icon-lg-r {\n    @apply n-ml-token-4 n-w-8 n-h-8;\n  }\n\n  .btn-icon-lg-l {\n    @apply n-mr-token-4 n-w-8 n-h-8;\n  }\n\n  /* Utility functions for icons */\n  .icon-base {\n    @apply n-w-6 n-h-6;\n  }\n\n  .icon-inline {\n    @apply n-inline n-mb-1;\n  }\n\n  .icon-inline.text-r {\n    @apply n-mr-token-3;\n  }\n\n  .icon-inline.text-l {\n    @apply n-ml-token-3;\n  }\n\n  /* DataGrid overrides */\n  .MuiDataGrid-footerContainer > div {\n    @apply n-mt-0;\n  }\n\n  /* Make bullet list points in Markdown card view */\n  .card-view ul {\n    @apply n-list-disc n-ml-token-7;\n  }\n\n  .ndl-dialog.dialog-xl {\n    max-width: 75%;\n  }\n\n  .ndl-dialog.dialog-xxl {\n    max-width: 90%;\n  }\n\n  .n-bg-dark-neutral-text-weak {\n    background-color: rgb(196 200 205 / var(--tw-bg-opacity)) !important;\n  }\n\n  /* Markdown table styles */\n  .markdown-table {\n    width: 100%;\n    border-collapse: collapse;\n  }\n\n  .markdown-table th,\n  .markdown-table td {\n    border: 1px solid #ddd; /* Light gray border */\n    padding: 8px; /* Padding around text */\n    text-align: left; /* Align text to the left */\n  }\n\n  .markdown-table th {\n    background-color: #f4f4f4; /* Light gray background for header */\n    color: #333; /* Dark text color for contrast */\n  }\n}\n"
  },
  {
    "path": "src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Provider as ReduxProvider } from 'react-redux';\nimport { configureStore } from './store';\nimport { persistStore } from 'redux-persist';\nimport { PersistGate } from 'redux-persist/lib/integration/react';\nimport Application from './application/Application';\nimport '/node_modules/react-grid-layout/css/styles.css';\nimport '/node_modules/react-resizable/css/styles.css';\nimport './index.pcss';\nimport StyleConfig from './config/StyleConfig';\nimport * as Sentry from '@sentry/react';\n\nif (window.location.href.includes('//neodash.graphapp.io/')) {\n  Sentry.init({\n    dsn: 'https://25edb17cc4c14c8cb726e7ac1ff74e3b@o110884.ingest.sentry.io/4505397810167808',\n    allowUrls: [/^https:\\/\\/neodash\\.graphapp\\.io/, /^http:\\/\\/neodash\\.graphapp\\.io/],\n    integrations: [\n      new Sentry.BrowserTracing({\n        // Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled\n        tracePropagationTargets: [/^https:\\/\\/neodash\\.graphapp\\.io/, /^http:\\/\\/neodash\\.graphapp\\.io/],\n      }),\n      new Sentry.Replay({\n        networkDetailAllowUrls: [/^https:\\/\\/neodash\\.graphapp\\.io/, /^http:\\/\\/neodash\\.graphapp\\.io/],\n      }),\n    ],\n    // Performance Monitoring\n    tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!\n    // Session Replay\n    replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.\n    replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.\n  });\n}\n/**\n * Set up the NeoDash application and wrap it in the needed providers.\n */\nconst store = configureStore();\n\n// @ts-ignore - persist state in browser cache.\nconst persister = persistStore(store);\n\nawait StyleConfig.getInstance();\n\n/** Wrap the application in a redux provider / browser cache persistance gate **/\nconst provider = (\n  <ReduxProvider store={store}>\n    <PersistGate persistor={persister} loading={<div>Loading NeoDash...</div>}>\n      <Application />\n    </PersistGate>\n  </ReduxProvider>\n);\n\nReactDOM.render(<React.StrictMode>{provider}</React.StrictMode>, document.getElementById('root'));\n"
  },
  {
    "path": "src/modal/AboutModal.tsx",
    "content": "import React from 'react';\nimport { Button, Dialog, TextLink } from '@neo4j-ndl/react';\nimport { BookOpenIconOutline, BeakerIconOutline } from '@neo4j-ndl/react/icons';\nimport { Section, SectionTitle, SectionContent } from './ModalUtils';\n\nexport const version = '2.4.11-labs';\n\nexport const NeoAboutModal = ({ open, handleClose, getDebugState }) => {\n  const downloadDebugFile = () => {\n    const element = document.createElement('a');\n    const state = getDebugState();\n    state.version = version;\n    const file = new Blob([JSON.stringify(state, null, 2)], { type: 'text/plain' });\n    element.href = URL.createObjectURL(file);\n    element.download = 'neodash-debug-state.json';\n    document.body.appendChild(element); // Required for this to work in FireFox\n    element.click();\n  };\n\n  return (\n    <>\n      <Dialog onClose={handleClose} open={open} aria-labelledby='form-dialog-title' size='large'>\n        <Dialog.Header>About NeoDash</Dialog.Header>\n        <Dialog.Content>\n          <div className='n-flex n-flex-col n-gap-token-4 n-divide-y n-divide-neutral-border-strong'>\n            <Section>\n              <SectionContent>\n                NeoDash is a dashboard builder for the Neo4j graph database. With NeoDash, all you need to do is write\n                Cypher queries, and you can build a dashboard in minutes.\n              </SectionContent>\n            </Section>\n            <Section>\n              <SectionTitle>Core Features</SectionTitle>\n              <SectionContent>\n                <ul className='n-list-disc n-pl-token-8'>\n                  <li>\n                    An editor to write and execute&nbsp;\n                    <TextLink externalLink target='_blank' href='https://neo4j.com/developer/cypher/'>\n                      Cypher\n                    </TextLink>\n                    &nbsp;queries.\n                  </li>\n                  <li>\n                    Use results of your Cypher queries to create tables, bar charts, graph visualizations, and more.\n                  </li>\n                  <li>Style your reports, group them together in pages, and add interactivity between reports.</li>\n                  <li>Save and share your dashboards with your friends.</li>\n                </ul>\n                No connectors or data pre-processing needed, it works directly with Neo4j!\n              </SectionContent>\n            </Section>\n            <Section>\n              <SectionTitle>Getting Started</SectionTitle>\n              <SectionContent>\n                You will automatically start with an empty dashboard when starting up NeoDash for this first time.\n                <br />\n                Click the{' '}\n                <strong>\n                  (<BookOpenIconOutline className='icon-base icon-inline text-r' /> Documentation)\n                </strong>\n                &nbsp;button to see some example queries and visualizations.\n              </SectionContent>\n            </Section>\n            <Section>\n              <SectionTitle>Extending NeoDash</SectionTitle>\n              <SectionContent>\n                NeoDash is built with React and&nbsp;\n                <TextLink target='_blank' href='https://github.com/adam-cowley/use-neo4j'>\n                  use-neo4j\n                </TextLink>\n                , It uses{' '}\n                <TextLink target='_blank' href='https://github.com/neo4j-labs/charts'>\n                  charts\n                </TextLink>{' '}\n                to power some of the visualizations, and&nbsp;\n                <TextLink target='_blank' href='https://www.openstreetmap.org/'>\n                  openstreetmap\n                </TextLink>{' '}\n                for the map view. You can also extend NeoDash with your own visualizations. Check out the developer\n                guide in the{' '}\n                <TextLink target='_blank' href='https://github.com/neo4j-labs/neodash/'>\n                  project repository\n                </TextLink>\n                .\n              </SectionContent>\n            </Section>\n            <Section>\n              <SectionTitle>Contact</SectionTitle>\n              <SectionContent>\n                For suggestions, feature requests and other feedback: create an issue on the&nbsp;\n                <TextLink target='_blank' href='https://github.com/neo4j-labs/neodash'>\n                  GitHub repository\n                </TextLink>{' '}\n                , or the{' '}\n                <TextLink\n                  href={\n                    'https://community.neo4j.com/t5/forums/filteredbylabelpage/board-id/integrations/label-name/neodash'\n                  }\n                >\n                  Neo4j Community Forums\n                </TextLink>\n                .\n              </SectionContent>\n            </Section>\n          </div>\n          <div className='n-flex n-flex-row n-justify-between n-mt-token-8'>\n            <div>\n              <Button onClick={downloadDebugFile} fill='outlined' color='neutral' size='small'>\n                Debug Report\n                <BeakerIconOutline className='btn-icon-sm-r' />\n              </Button>\n            </div>\n            <div>\n              <i style={{ float: 'right', fontSize: '11px' }}>v{version}</i>\n            </div>\n          </div>\n        </Dialog.Content>\n      </Dialog>\n    </>\n  );\n};\n\nexport default NeoAboutModal;\n"
  },
  {
    "path": "src/modal/ConnectionModal.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { SSOLoginButton } from '../component/sso/SSOLoginButton';\nimport { Button, Dialog, Switch, TextInput, Dropdown, TextLink, IconButton } from '@neo4j-ndl/react';\nimport { PlayIconOutline, ArrowLeftIconOutline } from '@neo4j-ndl/react/icons';\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport default function NeoConnectionModal({\n  connected,\n  open,\n  standalone,\n  standaloneSettings,\n  ssoSettings,\n  connection,\n  dismissable,\n  createConnection,\n  setConnectionProperties,\n  onConnectionModalClose,\n  onSSOAttempt,\n  setWelcomeScreenOpen,\n}) {\n  const protocols = ['neo4j', 'neo4j+s', 'neo4j+ssc', 'bolt', 'bolt+s', 'bolt+ssc'];\n  const [ssoVisible, setSsoVisible] = React.useState(ssoSettings.ssoEnabled);\n  const [protocol, setProtocol] = React.useState(connection.protocol);\n  const [url, setUrl] = React.useState(connection.url);\n  const [port, setPort] = React.useState(connection.port);\n  const [username, setUsername] = React.useState(connection.username);\n  const [password, setPassword] = React.useState(connection.password);\n  const [database, setDatabase] = React.useState(connection.database);\n\n  // Make sure local vars are updated on external connection updates.\n  useEffect(() => {\n    setProtocol(connection.protocol);\n    setUrl(connection.url);\n    setUsername(connection.username);\n    setPassword(connection.password);\n    setPort(connection.port);\n    setDatabase(connection.database);\n  }, [JSON.stringify(connection)]);\n\n  useEffect(() => {\n    setSsoVisible(ssoSettings.ssoEnabled);\n  }, [JSON.stringify(ssoSettings)]);\n\n  const discoveryAPIUrl = ssoSettings && ssoSettings.ssoDiscoveryUrl;\n\n  // since config is loaded asynchronously, value may not be yet defined when this runs for first time\n  let standaloneDatabaseList = [standaloneSettings.standaloneDatabase];\n  try {\n    standaloneDatabaseList = standaloneSettings.standaloneDatabaseList\n      ? standaloneSettings.standaloneDatabaseList.split(',')\n      : standaloneDatabaseList;\n  } catch (e) {\n    console.log(e);\n  }\n\n  return (\n    <>\n      <Dialog\n        size='small'\n        open={open}\n        onClose={() => {\n          onConnectionModalClose();\n          if (!connected) {\n            setWelcomeScreenOpen(true);\n          }\n        }}\n        aria-labelledby='form-dialog-title'\n        disableCloseButton={!dismissable}\n      >\n        <Dialog.Header id='form-dialog-title'>{standalone ? 'Connect to Dashboard' : 'Connect to Neo4j'}</Dialog.Header>\n        <Dialog.Content className='n-flex n-flex-col n-gap-token-4'>\n          <div className='n-flex n-flex-row n-flex-wrap'>\n            <Dropdown\n              id='protocol'\n              label='Protocol'\n              type='select'\n              selectProps={{\n                isDisabled: standalone,\n                onChange: (newValue) => newValue && setProtocol(newValue.value),\n                options: protocols.map((option) => ({ label: option, value: option })),\n                value: { label: protocol, value: protocol },\n              }}\n              style={{ width: '25%', display: 'inline-block' }}\n              fluid\n            />\n            <div style={{ marginLeft: '2.5%', width: '55%', marginRight: '2.5%', display: 'inline-block' }}>\n              <TextInput\n                id='url'\n                value={url}\n                disabled={standalone}\n                onChange={(e) => {\n                  // Help the user here a bit by extracting the hostname if they copy paste things in\n                  const input = e.target.value;\n                  const splitted = input.split('://');\n                  const host = splitted[splitted.length - 1].split(':')[0].split('/')[0];\n                  setUrl(host);\n                }}\n                label='Hostname'\n                placeholder='localhost'\n                autoFocus\n                fluid\n              />\n            </div>\n            <div style={{ width: '15%', display: 'inline-block' }}>\n              <TextInput\n                id='port'\n                value={port}\n                disabled={standalone}\n                onChange={(event) => {\n                  if (event.target.value.toString().length == 0) {\n                    setPort(event.target.value);\n                  } else if (!isNaN(event.target.value)) {\n                    setPort(Number(event.target.value));\n                  }\n                }}\n                label='Port'\n                placeholder='7687'\n                fluid\n              />\n            </div>\n          </div>\n\n          {window.location.href.startsWith('https') && !(protocol.endsWith('+s') || protocol.endsWith('+scc')) ? (\n            <div>\n              You're running NeoDash from a secure (https) webpage. You can't connect to a Neo4j database with an\n              unencrypted protocol. Change the protocol, or use NeoDash using http instead: &nbsp;\n              <TextLink href={window.location.href.replace('https://', 'http://')}>\n                {window.location.href.replace('https://', 'http://')}\n              </TextLink>\n              .\n            </div>\n          ) : null}\n          {url == 'localhost' && (protocol.endsWith('+s') || protocol.endsWith('+scc')) && (\n            <div>\n              A local host with an encrypted connection will likely not work - try an unencrypted protocol instead.\n            </div>\n          )}\n          {url.endsWith('neo4j.io') && !protocol.endsWith('+s') ? (\n            <div>\n              Neo4j Aura databases require a <code>neo4j+s</code> protocol. Your current configuration may not work.\n            </div>\n          ) : null}\n          {!standalone ? (\n            <TextInput\n              id='database'\n              value={database}\n              disabled={standalone}\n              onChange={(e) => setDatabase(e.target.value)}\n              label='Database (optional)'\n              placeholder='neo4j'\n              fluid\n            />\n          ) : (\n            <Dropdown\n              id='database'\n              label='Database'\n              type='select'\n              selectProps={{\n                onChange: (newValue) => {\n                  setDatabase(newValue.value);\n                },\n                options: standaloneDatabaseList.map((option) => ({\n                  label: option,\n                  value: option,\n                })),\n                value: { label: database, value: database },\n                menuPlacement: 'auto',\n              }}\n              fluid\n            ></Dropdown>\n          )}\n\n          {!ssoVisible ? (\n            <TextInput\n              id='dbusername'\n              value={username}\n              onChange={(e) => setUsername(e.target.value)}\n              label='Username'\n              placeholder='neo4j'\n              fluid\n            />\n          ) : null}\n          <form\n            onSubmit={(e) => {\n              e.preventDefault();\n              onConnectionModalClose();\n              createConnection(protocol, url, port, database, username, password);\n            }}\n          >\n            {!ssoVisible ? (\n              <TextInput\n                id='dbpassword'\n                value={password}\n                onChange={(e) => setPassword(e.target.value)}\n                label='Password'\n                placeholder='neo4j'\n                type='password'\n                fluid\n              />\n            ) : null}\n            {ssoSettings.ssoEnabled ? (\n              <div style={{ marginTop: 10 }}>\n                <Switch\n                  label='Use SSO'\n                  checked={ssoVisible}\n                  onChange={() => setSsoVisible(!ssoVisible)}\n                  style={{ marginLeft: '5px' }}\n                />\n              </div>\n            ) : (\n              <></>\n            )}\n            {ssoVisible ? (\n              <SSOLoginButton\n                hostname={url}\n                port={port}\n                discoveryAPIUrl={discoveryAPIUrl}\n                onSSOAttempt={onSSOAttempt}\n                onClick={() => {\n                  // Remember credentials on click\n                  setConnectionProperties(protocol, url, port, database, '', '');\n                }}\n                providers={ssoSettings.ssoProviders}\n              />\n            ) : (\n              <Button\n                type='submit'\n                onClick={(e) => {\n                  e.preventDefault();\n                  onConnectionModalClose();\n                  createConnection(protocol, url, port, database, username, password);\n                }}\n                style={{ float: 'right', marginTop: '20px', marginBottom: '20px' }}\n                size='large'\n              >\n                Connect\n                <PlayIconOutline className='btn-icon-base-r' />\n              </Button>\n            )}\n          </form>\n        </Dialog.Content>\n        <Dialog.Actions\n          style={{\n            background: '#555',\n            marginLeft: '-3rem',\n            marginRight: '-3rem',\n            marginBottom: '-3rem',\n            padding: '3rem',\n          }}\n        >\n          {standalone ? (\n            <div style={{ color: 'lightgrey' }}>\n              {standaloneSettings.standaloneDashboardURL === '' ? (\n                <>\n                  Sign in to continue. You will be connected to Neo4j, and load a dashboard called&nbsp;\n                  <b>{standaloneSettings.standaloneDashboardName}</b>.\n                </>\n              ) : (\n                <> Sign in to continue. You will be connected to Neo4j, and load a dashboard.</>\n              )}\n            </div>\n          ) : (\n            <div style={{ color: 'white' }}>\n              Enter your Neo4j database credentials to start. Don't have a Neo4j database yet? Create your own in&nbsp;\n              <TextLink externalLink className='n-text-neutral-text-inverse' href='https://neo4j.com/download/'>\n                Neo4j Desktop\n              </TextLink>\n              , or try the&nbsp;\n              <TextLink externalLink className='n-text-neutral-text-inverse' href='https://console.neo4j.io/'>\n                Neo4j Aura\n              </TextLink>\n              &nbsp;free tier.\n            </div>\n          )}\n        </Dialog.Actions>\n      </Dialog>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/modal/DeletePageModal.tsx",
    "content": "import React from 'react';\nimport { Button, Dialog } from '@neo4j-ndl/react';\nimport { BackspaceIconOutline, TrashIconSolid } from '@neo4j-ndl/react/icons';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoDeletePageModal = ({ modalOpen, onRemove, handleClose }) => {\n  return (\n    <Dialog size='small' open={modalOpen == true} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>Delete page?</Dialog.Header>\n      <Dialog.Subtitle>Are you sure you want to remove this page? This cannot be undone.</Dialog.Subtitle>\n      <Dialog.Actions>\n        <Button\n          onClick={() => {\n            handleClose();\n          }}\n          fill='outlined'\n          style={{ float: 'right' }}\n        >\n          <BackspaceIconOutline className='btn-icon-base-l' />\n          Cancel\n        </Button>\n        <Button\n          onClick={() => {\n            onRemove();\n            handleClose();\n          }}\n          color='danger'\n          style={{ float: 'right', marginRight: '5px' }}\n        >\n          Remove\n          <TrashIconSolid className='btn-icon-base-r' />\n        </Button>\n      </Dialog.Actions>\n    </Dialog>\n  );\n};\n\nexport default NeoDeletePageModal;\n"
  },
  {
    "path": "src/modal/ExportModal.tsx",
    "content": "import React from 'react';\nimport { connect } from 'react-redux';\nimport { IconButton, MenuItem } from '@neo4j-ndl/react';\nimport NeoDashboardSidebarExportModal from '../dashboard/sidebar/modal/DashboardSidebarExportModal';\nimport { getDashboardJson } from './ModalSelectors';\nimport { DocumentArrowDownIconOutline, DocumentTextIconOutline } from '@neo4j-ndl/react/icons';\nimport Tooltip from '@mui/material/Tooltip/Tooltip';\n/**\n * A modal to save a dashboard as a JSON text string.\n * The button to open the modal is intended to use in a drawer at the side of the page.\n */\nexport const NeoExportModal = ({ dashboard }) => {\n  const [open, setOpen] = React.useState(false);\n  return (\n    <>\n      <Tooltip title='Export' aria-label='export' disableInteractive>\n        <IconButton className='n-mx-1' onClick={() => setOpen(true)} aria-label='Export'>\n          <DocumentArrowDownIconOutline />\n        </IconButton>\n      </Tooltip>\n      <NeoDashboardSidebarExportModal\n        open={open}\n        dashboard={dashboard}\n        handleClose={() => {\n          setOpen(false);\n        }}\n      />\n    </>\n  );\n};\n\nconst mapStateToProps = (state) => ({\n  dashboard: getDashboardJson(state),\n});\n\nconst mapDispatchToProps = () => ({});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoExportModal);\n"
  },
  {
    "path": "src/modal/LoadSharedDashboardModal.tsx",
    "content": "import React from 'react';\nimport { connect } from 'react-redux';\nimport { Button, Dialog } from '@neo4j-ndl/react';\nimport { PlayIconSolid, AdjustmentsVerticalIconOutline, BackspaceIconOutline } from '@neo4j-ndl/react/icons';\n\n/**\n * A modal to save a dashboard as a JSON text string.\n * The button to open the modal is intended to use in a drawer at the side of the page.\n */\n\nexport const NeoLoadSharedDashboardModal = ({ shareDetails, onResetShareDetails, onConfirmLoadSharedDashboard }) => {\n  const handleClose = () => {\n    onResetShareDetails();\n  };\n\n  return (\n    <div>\n      <Dialog\n        size='large'\n        open={shareDetails !== undefined && shareDetails.skipConfirmation === false}\n        aria-labelledby='form-dialog-title'\n      >\n        <Dialog.Header id='form-dialog-title'>\n          <AdjustmentsVerticalIconOutline\n            className='icon-base icon-inline text-r'\n            style={{ display: 'inline', marginRight: '5px', marginBottom: '5px' }}\n          />\n          Loading Dashboard\n        </Dialog.Header>\n        <Dialog.Content>\n          {shareDetails !== undefined ? (\n            <>\n              You are loading a Neo4j dashboard.\n              <br />\n              {shareDetails && shareDetails.url ? (\n                <>\n                  You will be connected to <b>{shareDetails && shareDetails.url}</b>.\n                </>\n              ) : (\n                <>You will still need to specify a connection manually.</>\n              )}\n              <br /> <br />\n              This will override your current dashboard (if any). Continue?\n            </>\n          ) : (\n            <>\n              <br />\n              <br />\n              <br />\n            </>\n          )}\n        </Dialog.Content>\n        <Dialog.Actions>\n          <Button\n            onClick={() => {\n              handleClose();\n            }}\n            fill='outlined'\n            style={{ float: 'right' }}\n          >\n            <BackspaceIconOutline className='btn-icon-base-l' />\n            Cancel\n          </Button>\n          <Button\n            onClick={() => {\n              onConfirmLoadSharedDashboard();\n            }}\n            style={{ float: 'right', marginRight: '5px' }}\n            color='success'\n          >\n            Continue\n            <PlayIconSolid className='btn-icon-base-r' />\n          </Button>\n        </Dialog.Actions>\n      </Dialog>\n    </div>\n  );\n};\n\nconst mapStateToProps = () => ({});\n\nconst mapDispatchToProps = () => ({});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoLoadSharedDashboardModal);\n"
  },
  {
    "path": "src/modal/ModalSelectors.tsx",
    "content": "export const getDashboardJson = (state: any) => state.dashboard;\n"
  },
  {
    "path": "src/modal/ModalUtils.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { Typography } from '@neo4j-ndl/react';\n\nexport const Section = ({ children }: PropsWithChildren) => (\n  <div className='n-py-4 n-flex n-flex-col n-gap-token-4'>{children}</div>\n);\n\nexport const SectionTitle = ({ children }: PropsWithChildren) => <Typography variant='h5'>{children}</Typography>;\n\nexport const SectionContent = ({ children }: PropsWithChildren) => (\n  <Typography variant='body-medium'>{children}</Typography>\n);\n"
  },
  {
    "path": "src/modal/NotificationModal.tsx",
    "content": "import React from 'react';\nimport { connect } from 'react-redux';\nimport {\n  applicationHasNotification,\n  applicationHasWelcomeScreenOpen,\n  applicationIsConnected,\n  getNotification,\n  getNotificationIsDismissable,\n  getNotificationTitle,\n} from '../application/ApplicationSelectors';\nimport { clearNotification, setConnectionModalOpen } from '../application/ApplicationActions';\nimport { Dialog } from '@neo4j-ndl/react';\n\n/**\n * A modal to save a dashboard as a JSON text string.\n * The button to open the modal is intended to use in a drawer at the side of the page.\n */\nexport const NeoNotificationModal = ({\n  open,\n  title,\n  text,\n  dismissable,\n  openConnectionModalOnClose,\n  setConnectionModalOpen,\n  onNotificationClose,\n}) => {\n  return (\n    <div>\n      <Dialog\n        size='large'\n        open={open}\n        onClose={() => {\n          if (dismissable) {\n            onNotificationClose();\n            if (openConnectionModalOnClose) {\n              setConnectionModalOpen();\n            }\n          }\n        }}\n        aria-labelledby='form-dialog-title'\n        disableCloseButton={!dismissable}\n      >\n        <Dialog.Header id='form-dialog-title'>{title}</Dialog.Header>\n\n        <Dialog.Content style={{ minWidth: '300px' }}>{text && text.toString()}</Dialog.Content>\n      </Dialog>\n    </div>\n  );\n};\n\nconst mapStateToProps = (state) => ({\n  open: applicationHasNotification(state),\n  openConnectionModalOnClose: !applicationIsConnected(state) && !applicationHasWelcomeScreenOpen(state),\n  title: getNotificationTitle(state),\n  text: getNotification(state),\n  dismissable: getNotificationIsDismissable(state),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  onNotificationClose: () => dispatch(clearNotification()),\n  setConnectionModalOpen: () => dispatch(setConnectionModalOpen(true)),\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoNotificationModal);\n"
  },
  {
    "path": "src/modal/ReportExamplesModal.tsx",
    "content": "import React from 'react';\nimport NeoCodeEditorComponent from '../component/editor/CodeEditorComponent';\nimport NeoReport from '../report/Report';\nimport { Dialog, MenuItem } from '@neo4j-ndl/react';\nimport { ChartBarIconSolid } from '@neo4j-ndl/react/icons';\nimport { Section, SectionTitle, SectionContent } from '../modal/ModalUtils';\nimport { enterHandler } from '../utils/accessibility';\n\nexport const NeoReportExamplesModal = ({ database, examples, extensions }) => {\n  const [open, setOpen] = React.useState(false);\n\n  const handleClickOpen = () => {\n    setOpen(true);\n  };\n\n  const handleClose = () => {\n    setOpen(false);\n  };\n\n  return (\n    <>\n      <MenuItem\n        title='Examples'\n        onClick={handleClickOpen}\n        onKeyDown={(e) => enterHandler(e, handleClickOpen)}\n        icon={<ChartBarIconSolid />}\n      />\n\n      <Dialog open={open} onClose={handleClose} aria-labelledby='form-dialog-title' className='dialog-xl'>\n        <Dialog.Header id='form-dialog-title'>\n          <ChartBarIconSolid className='icon-base icon-inline text-r' />\n          Report Examples\n        </Dialog.Header>\n        <Dialog.Content>\n          <div className='n-flex n-flex-col n-gap-token-4 n-divide-y n-divide-neutral-border-strong'>\n            {examples.map((example, index) => {\n              return (\n                <Section key={`example-${index}`}>\n                  <SectionTitle>{example.title}</SectionTitle>\n                  <SectionContent>\n                    <div className='n-grid n-grid-cols-3 n-gap-8'>\n                      <div className='n-col-span-3'>{example.description}</div>\n                      <div className='n-col-span-1'>\n                        <NeoCodeEditorComponent\n                          editable={false}\n                          placeholder=''\n                          value={example.exampleQuery}\n                          language={example.type == 'iframe' ? 'url' : 'cypher'}\n                        ></NeoCodeEditorComponent>\n                      </div>\n\n                      <div\n                        className='n-col-span-2'\n                        style={{\n                          height: '355px',\n                          overflow: 'hidden',\n                          border: '1px solid lightgrey',\n                        }}\n                      >\n                        <NeoReport\n                          id={index}\n                          query={example.syntheticQuery}\n                          database={database}\n                          disabled={!open}\n                          extensions={extensions}\n                          selection={example.selection}\n                          parameters={example.globalParameters}\n                          settings={example.settings}\n                          fields={example.fields}\n                          dimensions={example.dimensions}\n                          ChartType={example.chartType}\n                          type={example.type}\n                        />\n                      </div>\n                    </div>\n                  </SectionContent>\n                </Section>\n              );\n            })}\n          </div>\n        </Dialog.Content>\n      </Dialog>\n    </>\n  );\n};\n\nexport default NeoReportExamplesModal;\n"
  },
  {
    "path": "src/modal/ReportHelpModal.tsx",
    "content": "import React from 'react';\nimport { Dialog, TextLink } from '@neo4j-ndl/react';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoReportHelpModal = ({ open, handleClose }) => {\n  return (\n    <Dialog size='large' open={open} onClose={handleClose} aria-labelledby='form-dialog-title'>\n      <Dialog.Header id='form-dialog-title'>About Reports</Dialog.Header>\n      <Dialog.Content>\n        {' '}\n        A report is the smallest building block of your dashboard. Each report runs a single Cypher query that loads\n        data from your database. By changing the report type, different visualizations can be created for the data. See\n        the{' '}\n        <TextLink\n          externalLink\n          href='https://github.com/neo4j-labs/neodash/tree/master/docs/modules/ROOT/pages/user-guide/reports/index.adoc'\n          target='_blank'\n        >\n          Documentation\n        </TextLink>\n        for more on reports.\n        <br></br>\n        <br></br>\n        <table>\n          <tr>\n            <td>\n              <b>Moving Reports</b>\n              <img src='movereport.gif' style={{ width: '100%' }}></img>\n            </td>\n            <td>\n              <b>Resizing Reports</b>\n              <img src='resizereport.gif' style={{ width: '100%' }}></img>\n            </td>\n          </tr>\n        </table>\n      </Dialog.Content>\n    </Dialog>\n  );\n};\n\nexport default NeoReportHelpModal;\n"
  },
  {
    "path": "src/modal/UpgradeOldDashboardModal.tsx",
    "content": "import React from 'react';\nimport { TextareaAutosize } from '@mui/material';\nimport { Button, Dialog } from '@neo4j-ndl/react';\nimport { TrashIconOutline, PlayIconSolid } from '@neo4j-ndl/react/icons';\nimport { createUUID } from '../utils/uuid';\n\nexport const NeoUpgradeOldDashboardModal = ({ open, text, clearOldDashboard, loadDashboard }) => {\n  return (\n    <div>\n      <Dialog size='large' open={open} aria-labelledby='form-dialog-title'>\n        <Dialog.Header id='form-dialog-title'>Old Dashboard Found</Dialog.Header>\n        <Dialog.Content>\n          We've found a dashboard built with an old version of NeoDash. Would you like to attempt an upgrade, or start\n          from scratch?\n          <br />\n          <b>Make sure you back up this dashboard first!</b>\n          <div style={{ marginTop: '20px', marginBottom: '20px' }}>\n            <Button\n              onClick={() => {\n                localStorage.removeItem('neodash-dashboard');\n                clearOldDashboard();\n              }}\n              style={{ marginRight: '20px' }}\n              color='danger'\n              floating\n            >\n              Delete old dashboard\n              <TrashIconOutline className='btn-icon-base-r' />\n            </Button>\n            <Button\n              onClick={() => {\n                localStorage.removeItem('neodash-dashboard');\n                loadDashboard(createUUID(), text);\n                clearOldDashboard();\n              }}\n              style={{ marginRight: '6px' }}\n              color='success'\n              size='large'\n              floating\n            >\n              Upgrade\n              <PlayIconSolid className='btn-icon-base-r' />\n            </Button>\n          </div>\n          <TextareaAutosize\n            style={{ minHeight: '200px', width: '100%', border: '1px solid lightgray' }}\n            className={'textinput-linenumbers'}\n            onChange={() => {}}\n            value={text ? text : ''}\n            aria-label=''\n            placeholder=''\n          />\n        </Dialog.Content>\n      </Dialog>\n    </div>\n  );\n};\n\nexport default NeoUpgradeOldDashboardModal;\n"
  },
  {
    "path": "src/modal/WelcomeScreenModal.tsx",
    "content": "import React from 'react';\nimport { Tooltip } from '@mui/material';\nimport { Button, Dialog, TextLink } from '@neo4j-ndl/react';\nimport {\n  BoltIconSolid,\n  ExclamationTriangleIconSolid,\n  BackspaceIconOutline,\n  PlayIconSolid,\n} from '@neo4j-ndl/react/icons';\n\n/**\n * Configures setting the current Neo4j database connection for the dashboard.\n */\nexport const NeoWelcomeScreenModal = ({\n  welcomeScreenOpen,\n  setWelcomeScreenOpen,\n  hasCachedDashboard,\n  hasNeo4jDesktopConnection,\n  createConnectionFromDesktopIntegration,\n  resetDashboard,\n  onConnectionModalOpen,\n  onAboutModalOpen,\n}) => {\n  const [promptOpen, setPromptOpen] = React.useState(false);\n  const handleOpen = () => {\n    setWelcomeScreenOpen(true);\n  };\n  const handleClose = () => {\n    setWelcomeScreenOpen(false);\n  };\n  const handlePromptOpen = () => {\n    setPromptOpen(true);\n  };\n  const handlePromptClose = () => {\n    setPromptOpen(false);\n  };\n\n  return (\n    <div>\n      <Dialog size='small' open={welcomeScreenOpen} aria-labelledby='form-dialog-title' disableCloseButton>\n        <Dialog.Header id='form-dialog-title'>\n          NeoDash - Neo4j Dashboard Builder\n          <BoltIconSolid className='icon-base' color='gold' style={{ float: 'right' }} />\n        </Dialog.Header>\n        <Dialog.Content>\n          <Tooltip title='Connect to Neo4j and create a new dashboard.' aria-label='create' disableInteractive>\n            <Button\n              onClick={() => {\n                if (hasCachedDashboard) {\n                  handlePromptOpen();\n                  handleClose();\n                } else {\n                  onConnectionModalOpen();\n                  handleClose();\n                }\n              }}\n              style={{ marginTop: '10px', width: '100%' }}\n              fill='outlined'\n              color='primary'\n              size='large'\n            >\n              New Dashboard\n            </Button>\n          </Tooltip>\n\n          <Tooltip title='Load the existing dashboard from cache (if it exists).' aria-label='load' disableInteractive>\n            {hasCachedDashboard ? (\n              <Button\n                onClick={() => {\n                  handleClose();\n                  onConnectionModalOpen();\n                }}\n                style={{ marginTop: '10px', width: '100%' }}\n                fill='outlined'\n                color='primary'\n                size='large'\n              >\n                Existing Dashboard\n              </Button>\n            ) : (\n              <Button\n                disabled\n                style={{ marginTop: '10px', width: '100%' }}\n                fill='outlined'\n                color='neutral'\n                size='large'\n              >\n                Existing Dashboard\n              </Button>\n            )}\n          </Tooltip>\n          {hasNeo4jDesktopConnection ? (\n            <Tooltip title='Connect to an active database in Neo4j Desktop.' aria-label='connect' disableInteractive>\n              <Button\n                onClick={() => {\n                  handleClose();\n                  createConnectionFromDesktopIntegration();\n                }}\n                style={{ marginTop: '10px', width: '100%' }}\n                fill='outlined'\n                color='neutral'\n                size='large'\n              >\n                Connect to Neo4j Desktop\n              </Button>\n            </Tooltip>\n          ) : (\n            <Button\n              disabled\n              onClick={handleClose}\n              style={{ marginTop: '10px', width: '100%' }}\n              fill='outlined'\n              color='neutral'\n              size='large'\n            >\n              Connect to Neo4j Desktop\n            </Button>\n          )}\n\n          <Tooltip title='View a gallery of live examples.' aria-label='demo' disableInteractive>\n            <Button\n              target='_blank'\n              href='https://neodash-gallery.graphapp.io'\n              style={{ marginTop: '10px', width: '100%' }}\n              fill='outlined'\n              color='neutral'\n              size='large'\n            >\n              Try a Demo\n            </Button>\n          </Tooltip>\n\n          <Tooltip title='Show information about this application.' aria-label='' disableInteractive>\n            <Button\n              onClick={onAboutModalOpen}\n              style={{ marginTop: '10px', width: '100%' }}\n              fill='outlined'\n              color='neutral'\n              size='large'\n            >\n              {/**/}\n              About\n            </Button>\n          </Tooltip>\n        </Dialog.Content>\n        <Dialog.Actions\n          style={{\n            background: '#555',\n            marginLeft: '-3rem',\n            marginRight: '-3rem',\n            marginBottom: '-3rem',\n            padding: '3rem',\n          }}\n        >\n          <div style={{ color: 'white' }}>\n            This version of NeoDash is not actively maintained. Visit the&nbsp;\n            <TextLink\n              href='https://github.com/neo4j-labs/neodash'\n              className='n-text-neutral-text-inverse'\n              target='_blank'\n              style={{ color: 'white' }}\n            >\n              repository\n            </TextLink>\n            &nbsp; to learn more.\n          </div>\n        </Dialog.Actions>\n      </Dialog>\n\n      {/* Prompt when creating new dashboard with existing cache */}\n      <Dialog size='small' open={promptOpen == true} aria-labelledby='form-dialog-title'>\n        <Dialog.Header id='form-dialog-title'>\n          Create New Dashboard\n          {/* <ExclamationTriangleIconSolid className='icon-base' color='orange' style={{ float: 'right' }} /> */}\n        </Dialog.Header>\n        <Dialog.Content>\n          Are you sure you want to create a new dashboard? This will remove your currently cached dashboard.\n        </Dialog.Content>\n        <Dialog.Actions>\n          <Button\n            onClick={() => {\n              handleOpen();\n              handlePromptClose();\n            }}\n            style={{ marginTop: '10px', float: 'right' }}\n            color='primary'\n            fill='outlined'\n          >\n            <BackspaceIconOutline className='btn-icon-base-l' />\n            No\n          </Button>\n          <Button\n            onClick={() => {\n              handleClose();\n              handlePromptClose();\n              resetDashboard();\n              onConnectionModalOpen();\n            }}\n            style={{ marginTop: '10px', float: 'right', marginRight: '5px' }}\n            color='primary'\n          >\n            Yes\n            <PlayIconSolid className='btn-icon-base-r' />\n          </Button>\n        </Dialog.Actions>\n      </Dialog>\n    </div>\n  );\n};\n\nexport default NeoWelcomeScreenModal;\n"
  },
  {
    "path": "src/page/Page.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { connect } from 'react-redux';\nimport NeoAddCard from '../card/CardAddButton';\nimport NeoCard from '../card/Card';\nimport { getReports } from './PageSelectors';\nimport { addReportThunk, removeReportThunk, updatePageLayoutThunk, cloneReportThunk } from './PageThunks';\nimport Grid from '@mui/material/Grid';\nimport { getDashboardIsEditable, getPageNumber } from '../settings/SettingsSelectors';\nimport { getDashboardSettings } from '../dashboard/DashboardSelectors';\nimport { Responsive, WidthProvider } from 'react-grid-layout';\nimport { GRID_COMPACTION_TYPE } from '../config/PageConfig';\n\nconst ResponsiveGridLayout = WidthProvider(Responsive);\n\n/**\n * A component responsible for rendering the **current** page, a collection of reports.\n */\nexport const NeoPage = ({\n  editable = true, // Whether the page is editable.\n  dashboardSettings, // global settings for the entire dashboard.\n  pagenumber, // The page number to render.\n  reports = [], // list of reports as defined in the dashboard state.\n  onCreatePressed = () => {}, // callback for when the user wants to add a new report.\n  onClonePressed = () => {}, // callback/action to take when a user wants to clone a report\n  onRemovePressed = () => {}, // action to take when a report gets removed.\n  onPageLayoutUpdate = () => {}, // action to take when the page layout is updated.\n}) => {\n  const getReportKey = (pagenumber: number, id: string) => {\n    return `${pagenumber}:${id}`;\n  };\n\n  const defaultLayouts = {\n    lg: [\n      {\n        x: 0,\n        y: 0,\n        i: getReportKey(pagenumber, '999999'),\n        w: 6,\n        h: 3,\n        isDraggable: false,\n      },\n    ],\n  };\n\n  const [isDragging, setIsDragging] = React.useState(false);\n  const [layouts, setLayouts] = React.useState(defaultLayouts);\n  const [lastElement, setLastElement] = React.useState(<div key={getReportKey(pagenumber, '999999')}></div>);\n  const [animated, setAnimated] = React.useState(false); // To turn off animations when cards are dragged around.\n\n  const availableHandles = () => {\n    if (dashboardSettings.resizing && dashboardSettings.resizing == 'all') {\n      return ['s', 'w', 'e', 'sw', 'se'];\n    }\n    return ['se'];\n  };\n\n  /**\n   * Based on the current layout, determine where the 'add report' card should be placed.\n   * The position here is the 'first available (2x2) spot' on a row, starting from the top.\n   * @returns the position (x,y) of the add card button.\n   */\n  const getAddCardButtonPosition = () => {\n    // Find all reports that touch on a specific y-level.\n    if (reports.length == 0) {\n      return { x: 0, y: 0 };\n    }\n\n    const maxY = Math.max.apply(\n      Math,\n      reports.map((o) => {\n        return o.y + o.height;\n      })\n    );\n    const maxXbyYLevel = {}; // The max x value for each y-level.\n    for (let i = 0; i < maxY; i++) {\n      maxXbyYLevel[i] = Math.max(\n        0,\n        Math.max.apply(\n          Math,\n          reports\n            .filter((report) => report.y + report.height > i && report.y <= i)\n            .map((o) => {\n              return o.x + o.width;\n            })\n        )\n      );\n    }\n\n    for (let level = 0; level < maxY; level++) {\n      if (maxXbyYLevel[level] <= 18 && (maxXbyYLevel[level + 1] === undefined || maxXbyYLevel[level + 1] <= 18)) {\n        return { x: maxXbyYLevel[level] !== undefined ? maxXbyYLevel[level] : 0, y: level };\n      }\n    }\n    return { x: 0, y: maxY };\n  };\n  /**\n   * Recompute the layout of the page buttons.This is called whenever the pages get reorganized.\n   */\n  const recomputeLayout = () => {\n    const { x, y } = getAddCardButtonPosition();\n    setLayouts({\n      // @ts-ignore\n      lg: [\n        ...reports.map((report) => {\n          return {\n            x: report.x || 0,\n            y: report.y || 0,\n            i: getReportKey(pagenumber, report.id),\n            w: Math.max(parseInt(report.width), 4) || 4,\n            h: Math.max(parseInt(report.height), 2) || 2,\n            minW: 4,\n            minH: 2,\n            resizeHandles: availableHandles(),\n            isDraggable: true,\n          };\n        }),\n        {\n          x: x,\n          y: y,\n          i: getReportKey(pagenumber, '999999'),\n          w: 6,\n          h: 4,\n          minW: 6,\n          minH: 4,\n          isDraggable: false,\n          isResizable: false,\n        },\n      ],\n    });\n    setLastElement(\n      <Grid style={{ paddingBottom: '6px' }} key={getReportKey(pagenumber, '999999')}>\n        <NeoAddCard\n          onCreatePressed={() => {\n            const { x, y } = getAddCardButtonPosition();\n            onCreatePressed(x, y, 6, 4);\n          }}\n        />\n      </Grid>\n    );\n  };\n\n  useEffect(() => {\n    setAnimated(false);\n    recomputeLayout();\n  }, [reports, dashboardSettings.resizing, pagenumber]);\n\n  const content = (\n    <div className='n-pt-3'>\n      <ResponsiveGridLayout\n        draggableHandle='.drag-handle'\n        layouts={layouts}\n        className={`layout neodash-card-editable-${editable} ${animated ? 'animated' : 'not-animated'}`}\n        breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}\n        cols={{ lg: 24, md: 20, sm: 12, xs: 8, xxs: 4 }}\n        rowHeight={100}\n        compactType={GRID_COMPACTION_TYPE}\n        onDrag={() => {\n          if (!isDragging) {\n            setAnimated(true);\n            setIsDragging(true);\n            recomputeLayout();\n          }\n        }}\n        onDragStop={(newLayout) => {\n          if (isDragging) {\n            onPageLayoutUpdate(newLayout);\n            setIsDragging(false);\n          }\n        }}\n        onResize={() => {\n          setIsDragging(true);\n          if (!animated) {\n            setAnimated(true);\n          }\n        }}\n        onResizeStop={(newLayout) => {\n          setIsDragging(false);\n          onPageLayoutUpdate(newLayout);\n        }}\n      >\n        {reports.map((report) => {\n          const w = 24;\n          const { id } = report;\n          // @ts-ignore\n          return (\n            <Grid\n              key={getReportKey(pagenumber, id)}\n              style={{ paddingBottom: '6px' }}\n              item\n              xs={Math.min(w * 4, 24)}\n              sm={Math.min(w * 2, 24)}\n              md={Math.min(w * 2, 24)}\n              lg={Math.min(w, 24)}\n              xl={Math.min(w, 24)}\n            >\n              <NeoCard\n                id={id}\n                key={getReportKey(pagenumber, id)}\n                dashboardSettings={dashboardSettings}\n                onRemovePressed={onRemovePressed}\n                onClonePressed={(id) => {\n                  const { x, y } = getAddCardButtonPosition();\n                  onClonePressed(id, x, y);\n                }}\n              />\n            </Grid>\n          );\n        })}\n        {editable && !isDragging ? lastElement : <div key={getReportKey(pagenumber, '999999')}></div>}\n      </ResponsiveGridLayout>\n    </div>\n  );\n  return content;\n};\n\nconst mapStateToProps = (state) => ({\n  pagenumber: getPageNumber(state),\n  editable: getDashboardIsEditable(state),\n  dashboardSettings: getDashboardSettings(state),\n  reports: getReports(state),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  onRemovePressed: (id) => dispatch(removeReportThunk(id)),\n  onClonePressed: (id, x, y) => dispatch(cloneReportThunk(id, x, y)),\n  onCreatePressed: (x, y, width, height) => dispatch(addReportThunk(x, y, width, height, undefined)),\n  onPageLayoutUpdate: (layout) => dispatch(updatePageLayoutThunk(layout)),\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoPage);\n"
  },
  {
    "path": "src/page/PageActions.ts",
    "content": "export const CREATE_REPORT = 'PAGE/CREATE_REPORT';\nexport const createReport = (pagenumber: number, report: any) => ({\n  type: CREATE_REPORT,\n  payload: { pagenumber, report },\n});\n\nexport const REMOVE_REPORT = 'PAGE/REMOVE_REPORT';\nexport const removeReport = (pagenumber: number, id: any) => ({\n  type: REMOVE_REPORT,\n  payload: { pagenumber, id },\n});\n\nexport const UPDATE_ALL_CARD_POSITIONS_IN_PAGE = 'PAGE/UPDATE_ALL_CARD_POSITIONS_IN_PAGE';\nexport const updateAllCardPositionsInPage = (pagenumber: number, positions: any) => ({\n  type: UPDATE_ALL_CARD_POSITIONS_IN_PAGE,\n  payload: { pagenumber, positions },\n});\n\nexport const SET_PAGE_TITLE = 'PAGE/SET_TITLE';\nexport const setPageTitle = (pagenumber: number, title: any) => ({\n  type: SET_PAGE_TITLE,\n  payload: { pagenumber, title },\n});\n\nexport const FORCE_REFRESH_PAGE = 'PAGE/FORCE_REFRESH_PAGE';\nexport const forceRefreshPage = (pagenumber: number) => ({\n  type: FORCE_REFRESH_PAGE,\n  payload: { pagenumber },\n});\n"
  },
  {
    "path": "src/page/PageReducer.ts",
    "content": "import cardReducer from '../card/CardReducer';\nimport {\n  CREATE_REPORT,\n  REMOVE_REPORT,\n  SET_PAGE_TITLE,\n  FORCE_REFRESH_PAGE,\n  UPDATE_ALL_CARD_POSITIONS_IN_PAGE,\n} from './PageActions';\nimport { createUUID } from '../utils/uuid';\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\n// TODO : Alfredo: this should source the card config defined inside the reducer and then define the first page initial state\nexport const PAGE_EXAMPLE_STATE = {\n  title: 'Main Page',\n  reports: [\n    {\n      id: createUUID(),\n      title: 'Hi there 👋',\n      query:\n        '**This is your first dashboard!** \\n \\nYou can click (⋮) to edit this report, or add a new report to get started. You can run any Cypher query directly from each report and render data in a variety of formats. \\n \\nTip: try _renaming_ this report by editing the title text. You can also edit the dashboard header at the top of the screen.\\n\\n\\n',\n      width: 6,\n      height: 4,\n      x: 0,\n      y: 0,\n      type: 'text',\n      selection: {},\n      settings: {},\n    },\n    {\n      id: createUUID(),\n      title: '',\n      query: 'MATCH (n)-[e]->(m) RETURN n,e,m LIMIT 20\\n\\n\\n',\n      width: 6,\n      height: 4,\n      x: 6,\n      y: 0,\n      type: 'graph',\n      selection: {},\n      settings: {},\n    },\n  ],\n};\n\nexport const PAGE_EMPTY_STATE = {\n  title: 'New page',\n  reports: [],\n};\n\n/**\n * Reducers define changes to the application state when a given action.\n * This reducer handles updates to a single page of the dashboard.\n * TODO - pagenumbers can be cut from here with new reducer architecture.\n */\nexport const pageReducer = (state = PAGE_EMPTY_STATE, action: { type: any; payload: any }) => {\n  const { type, payload } = action;\n\n  if (!action.type.startsWith('PAGE/')) {\n    return state;\n  }\n  // Updates a report at a given page and index.\n  if (action.type.startsWith('PAGE/CARD/')) {\n    const { id } = payload;\n    const index = state.reports.findIndex((o) => o.id === id);\n    return {\n      ...state,\n      reports: [\n        ...state.reports.slice(0, index),\n        cardReducer(state.reports[index], action),\n        ...state.reports.slice(index + 1),\n      ],\n    };\n  }\n\n  // Else, deal with page-level operations.\n  switch (type) {\n    case CREATE_REPORT: {\n      // Adds a new card at the end of the page with selected page number.\n      const { report } = payload;\n      return {\n        ...state,\n        reports: state.reports.concat(report),\n      };\n    }\n    case REMOVE_REPORT: {\n      // Removes the card at a given index on a selected page number.\n      const { id } = payload;\n      let cards = state.reports.filter((o) => o.id !== id);\n      // cards.forEach(c => c.collapseTimeout = 0 );\n      return {\n        ...state,\n        reports: cards,\n      };\n    }\n    case UPDATE_ALL_CARD_POSITIONS_IN_PAGE: {\n      // Updates the layout for the entire page (all positions of all cards in that page).\n      const { positions } = payload;\n      const newReports = state.reports.map((report: object, index) => {\n        return {\n          ...report,\n          x: positions[index].x,\n          y: positions[index].y,\n          width: positions[index].w,\n          height: positions[index].h,\n        };\n      });\n      return {\n        ...state,\n        reports: newReports,\n      };\n    }\n    case SET_PAGE_TITLE: {\n      // Moves a card right (swaps it with the next card)\n      const { title } = payload;\n      return {\n        ...state,\n        title: title,\n      };\n    }\n    case FORCE_REFRESH_PAGE: {\n      // We force a page refresh by resetting the field set for each report. (workaround)\n      return {\n        ...state,\n        reports: state.reports.map((report) => update(report, { fields: report.fields.concat(['']) })),\n      };\n    }\n    default: {\n      return state;\n    }\n  }\n};\n"
  },
  {
    "path": "src/page/PageSelectors.ts",
    "content": "import { createSelector } from 'reselect';\nexport const getReports = (state: any) => {\n  const { pagenumber } = state.dashboard.settings;\n  return state.dashboard.pages[pagenumber] ? state.dashboard.pages[pagenumber].reports : [];\n};\nexport const getReportsLoading = (state: any) => state.dashboard.isLoading;\n\n// TODO: Investigate to define what is the expected behavior => current filter is useless\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport const getCurrentReports = createSelector(getReports, (reports) => reports.filter((report) => true));\n"
  },
  {
    "path": "src/page/PageThunks.ts",
    "content": "import { createNotification } from '../application/ApplicationActions';\nimport { CARD_INITIAL_STATE } from '../card/CardReducer';\nimport { createReport, removeReport, updateAllCardPositionsInPage } from './PageActions';\nimport { createUUID } from '../utils/uuid';\n\nexport const createNotificationThunk = (title: any, message: any) => (dispatch: any) => {\n  dispatch(createNotification(title, message));\n};\n\nexport const addReportThunk =\n  (x: number, y: number, width: number, height: number, data: any) => (dispatch: any, getState: any) => {\n    try {\n      const initialState = data !== undefined ? data : CARD_INITIAL_STATE;\n      const report = { ...initialState, x: x, y: y, width: width, height: height, id: createUUID() };\n      const state = getState();\n      const { pagenumber } = state.dashboard.settings;\n      dispatch(createReport(pagenumber, report));\n    } catch (e) {\n      dispatch(createNotificationThunk('Cannot create report', e));\n    }\n  };\n\nexport const removeReportThunk = (id: string) => (dispatch: any, getState: any) => {\n  try {\n    const state = getState();\n    const { pagenumber } = state.dashboard.settings;\n    dispatch(removeReport(pagenumber, id));\n  } catch (e) {\n    dispatch(createNotificationThunk('Cannot remove report', e));\n  }\n};\n\nexport const cloneReportThunk = (id: string, x: number, y: number) => (dispatch: any, getState: any) => {\n  try {\n    const state = getState();\n    const { pagenumber } = state.dashboard.settings;\n    const data = { ...state.dashboard.pages[pagenumber].reports.find((o) => o.id === id) };\n    data.settingsOpen = false;\n    dispatch(addReportThunk(x, y, data.width, data.height, data));\n  } catch (e) {\n    dispatch(createNotificationThunk('Cannot clone report', e));\n  }\n};\n\nexport const updatePageLayoutThunk = (layout: any) => (dispatch: any, getState: any) => {\n  try {\n    const { pagenumber } = getState().dashboard.settings;\n    const cardPositions = layout.slice(0, layout.length - 1);\n    dispatch(updateAllCardPositionsInPage(pagenumber, cardPositions));\n  } catch (e) {\n    dispatch(createNotificationThunk('Cannot update page layout', e));\n  }\n};\n"
  },
  {
    "path": "src/report/Report.tsx",
    "content": "import { Chip, Tooltip } from '@mui/material';\nimport React, { useState, useEffect } from 'react';\nimport { QueryStatus, runCypherQuery } from './ReportQueryRunner';\nimport debounce from 'lodash/debounce';\nimport { useCallback } from 'react';\nimport NeoCodeViewerComponent, { NoDrawableDataErrorMessage } from '../component/editor/CodeViewerComponent';\nimport { DEFAULT_ROW_LIMIT, HARD_ROW_LIMITING, RUN_QUERY_DELAY_MS } from '../config/ReportConfig';\nimport { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';\nimport { useContext } from 'react';\nimport NeoTableChart from '../chart/table/TableChart';\nimport { getReportTypes } from '../extensions/ExtensionUtils';\nimport { SELECTION_TYPES } from '../config/CardConfig';\nimport { LoadingSpinner } from '@neo4j-ndl/react';\nimport { EllipsisVerticalIconOutline, ExclamationTriangleIconSolid } from '@neo4j-ndl/react/icons';\nimport { connect } from 'react-redux';\nimport { setPageNumberThunk } from '../settings/SettingsThunks';\nimport { EXTENSIONS } from '../extensions/ExtensionConfig';\nimport { getPageNumber } from '../settings/SettingsSelectors';\nimport { getPrepopulateReportExtension } from '../extensions/state/ExtensionSelectors';\nimport { deleteSessionStoragePrepopulationReportFunction } from '../extensions/state/ExtensionActions';\nimport { updateFieldsThunk } from '../card/CardThunks';\nimport { getDashboardTheme } from '../dashboard/DashboardSelectors';\n\nexport const REPORT_LOADING_ICON = <LoadingSpinner size='large' className='centered' style={{ marginTop: '-30px' }} />;\n\nexport const NeoReport = ({\n  pagenumber = '', // page number that the report is on.\n  id = '', // ID of the report / card.\n  database = 'neo4j', // The Neo4j database to run queries onto.\n  query = '', // The Cypher query used to populate the report.\n  lastRunTimestamp = 0, // Timestamp of the last query run for this report.\n  parameters = {}, // A dictionary of parameters to pass into the query.\n  disabled = false, // Whether to disable query execution.\n  selection = {}, // A selection of return fields to send to the report.\n  fields = [], // A list of the return data fields that the query produces.\n  settings = {}, // An optional dictionary of customization settings to pass to the report.\n  setFields = (f) => {\n    fields = f;\n  }, // The callback to update the set of query fields after query execution.\n  setSchemaDispatch,\n  setGlobalParameter = () => {}, // callback to update global (dashboard) parameters.\n  getGlobalParameter = (_: string) => {\n    return '';\n  }, // function to get global (cypher) parameters.\n  updateReportSetting = () => {},\n  createNotification = () => {},\n  setPageNumber = () => {}, // Callback to update the current page number selected by the user.\n  dimensions = { width: 300, height: 300 }, // Size of the report in pixels.\n  rowLimit = DEFAULT_ROW_LIMIT, // The maximum number of records to render.\n  queryTimeLimit = 20, // Time limit for queries before automatically aborted.\n  type = 'table', // The type of report as a string.\n  expanded = false, // whether the report is visualized in a fullscreen view.\n  extensions = {}, // A set of enabled extensions.\n  getCustomDispatcher = () => {},\n  ChartType = NeoTableChart, // The report component to render with the query results.\n  prepopulateExtensionName,\n  deletePrepopulationReportFunction,\n  theme,\n}) => {\n  const [records, setRecords] = useState(null);\n  const [timer, setTimer] = useState(null);\n  const [status, setStatus] = useState(QueryStatus.NO_QUERY);\n  const { driver } = useContext<Neo4jContextState>(Neo4jContext);\n  const [loadingIcon, setLoadingIcon] = React.useState(REPORT_LOADING_ICON);\n  if (!driver) {\n    throw new Error(\n      '`driver` not defined. Have you added it into your app as <Neo4jContext.Provider value={{driver}}> ?'\n    );\n  }\n  const debouncedRunCypherQuery = useCallback(debounce(runCypherQuery, RUN_QUERY_DELAY_MS), []);\n\n  const setSchema = (id, schema) => {\n    if (type === 'graph' || type === 'map' || type === 'gantt' || type === 'graph3d') {\n      setSchemaDispatch(id, schema);\n    }\n  };\n  const populateReport = (debounced = true) => {\n    // If this is a 'text-only' report, no queries are ran, instead we pass the input directly to the report.\n    const reportTypes = getReportTypes(extensions);\n\n    if (reportTypes[type].textOnly) {\n      setStatus(QueryStatus.COMPLETE);\n      setRecords([{ input: query, parameters: parameters }]);\n      return;\n    }\n\n    // Reset the report records before we run the query.\n    setRecords([]);\n\n    // Determine the set of fields from the configurations.\n    let numericFields =\n      reportTypes[type].selection && fields\n        ? Object.keys(reportTypes[type].selection).filter(\n            (field) =>\n              reportTypes[type].selection[field].type == SELECTION_TYPES.NUMBER &&\n              !reportTypes[type].selection[field].multiple\n          )\n        : [];\n    // Take care of multi select fields, they need to be added to the numeric fields too.\n    if (reportTypes[type].selection) {\n      Object.keys(reportTypes[type].selection).forEach((field) => {\n        if (reportTypes[type].selection[field].multiple && selection[field]) {\n          selection[field].forEach((f) => numericFields.push(`${field}(${f})`));\n        }\n      });\n    }\n\n    const useNodePropsAsFields = reportTypes[type].useNodePropsAsFields == true;\n    const useReturnValuesAsFields = reportTypes[type].useReturnValuesAsFields == true;\n\n    // Logic to run a query\n    const executeQuery = (newQuery) => {\n      setLoadingIcon(REPORT_LOADING_ICON);\n      if (debounced) {\n        debouncedRunCypherQuery(\n          driver,\n          database,\n          newQuery,\n          parameters,\n          rowLimit,\n          setStatus,\n          setRecords,\n          setFields,\n          fields,\n          useNodePropsAsFields,\n          useReturnValuesAsFields,\n          HARD_ROW_LIMITING,\n          queryTimeLimit,\n          (schema) => {\n            setSchema(id, schema);\n          }\n        );\n      } else {\n        runCypherQuery(\n          driver,\n          database,\n          newQuery,\n          parameters,\n          rowLimit,\n          setStatus,\n          setRecords,\n          setFields,\n          fields,\n          useNodePropsAsFields,\n          useReturnValuesAsFields,\n          HARD_ROW_LIMITING,\n          queryTimeLimit,\n          (schema) => {\n            setSchema(id, schema);\n          }\n        );\n      }\n    };\n\n    setStatus(QueryStatus.RUNNING);\n\n    // If a custom prepopulating function is present in the session storage...\n    //  ... Await for the prepopulating function to complete before running the (normal) query logic.\n    // Else just run the normal query.\n    // Finally, remove the prepopulating function from session storage.\n    if (prepopulateExtensionName) {\n      setLoadingIcon(EXTENSIONS[prepopulateExtensionName].customLoadingIcon);\n      EXTENSIONS[prepopulateExtensionName].prepopulateReportFunction(\n        driver,\n        getCustomDispatcher(),\n        pagenumber,\n        id,\n        type,\n        extensions,\n        (result) => {\n          executeQuery(result);\n        }\n      );\n      deletePrepopulationReportFunction(id);\n    } else {\n      executeQuery(query);\n    }\n  };\n\n  // When report parameters are changed, re-run the report.\n  useEffect(() => {\n    if (timer) {\n      // @ts-ignore\n      clearInterval(timer);\n    }\n    if (!disabled) {\n      if (query.trim() == '') {\n        setStatus(QueryStatus.NO_QUERY);\n      }\n      populateReport();\n      // If a refresh rate was specified, set up an interval for re-running the report. (max 24 hrs)\n      if (settings.refreshRate && settings.refreshRate > 0) {\n        // @ts-ignore\n        setTimer(\n          setInterval(() => {\n            populateReport(false);\n          }, Math.min(settings.refreshRate, 86400) * 1000.0)\n        );\n      }\n    }\n  }, [lastRunTimestamp]);\n\n  // Define query callback to allow reports to get extra data on interactions.\n  // Can retrieve a maximum of 1000 rows at a time.\n  const queryCallback = useCallback(\n    (query, parameters, setRecords) => {\n      runCypherQuery(\n        driver,\n        database,\n        query,\n        parameters,\n        1000,\n        (status) => {\n          status == QueryStatus.NO_DATA ? setRecords([]) : () => {};\n        },\n        (result) => setRecords(result),\n        () => {},\n        fields,\n        false,\n        false,\n        HARD_ROW_LIMITING,\n        queryTimeLimit,\n        (schema) => {\n          setSchema(id, schema);\n        }\n      );\n    },\n    [database]\n  );\n\n  const reportTypes = getReportTypes(extensions);\n\n  // Draw the report based on the query status.\n  if (disabled) {\n    return <div></div>;\n  } else if (status == QueryStatus.NO_QUERY) {\n    return (\n      <div className={'n-text-palette-neutral-text-default'} style={{ padding: 15 }}>\n        No query specified. <br /> Use the &nbsp;\n        <Chip\n          style={{ backgroundColor: '#dddddd' }}\n          size='small'\n          icon={<EllipsisVerticalIconOutline className='btn-icon-base-r' />}\n          label='Report Settings'\n        />{' '}\n        button to get started.\n      </div>\n    );\n  } else if (status == QueryStatus.RUNNING) {\n    return loadingIcon;\n  } else if (status == QueryStatus.NO_DATA) {\n    return <NeoCodeViewerComponent value={settings?.noDataMessage || 'Query returned no data.'} />;\n  } else if (status == QueryStatus.NO_DRAWABLE_DATA) {\n    return <NoDrawableDataErrorMessage />;\n  } else if (status == QueryStatus.COMPLETE) {\n    if (records == null || records.length == 0) {\n      return <div>Loading...</div>;\n    }\n    return (\n      <div\n        className={'n-text-palette-neutral-text-default'}\n        style={{ height: '100%', marginTop: '0px', overflow: reportTypes[type].allowScrolling ? 'auto' : 'hidden' }}\n      >\n        <ChartType\n          setPageNumber={setPageNumber}\n          records={records}\n          extensions={extensions}\n          selection={selection}\n          settings={settings}\n          fullscreen={expanded}\n          dimensions={dimensions}\n          parameters={parameters}\n          query={query}\n          queryCallback={queryCallback}\n          createNotification={createNotification}\n          setGlobalParameter={setGlobalParameter}\n          getGlobalParameter={getGlobalParameter}\n          updateReportSetting={updateReportSetting}\n          fields={fields}\n          setFields={setFields}\n          theme={theme}\n        />\n      </div>\n    );\n  } else if (status == QueryStatus.COMPLETE_TRUNCATED) {\n    if (records == null || records.length == 0) {\n      return <div>Loading...</div>;\n    }\n    return (\n      <div style={{ height: '100%', marginTop: '0px', overflow: reportTypes[type].allowScrolling ? 'auto' : 'hidden' }}>\n        <div style={{ marginBottom: '-31px' }}>\n          <div style={{ display: 'flex' }}>\n            <Tooltip\n              title={`Over ${rowLimit} row(s) were returned, results have been truncated.`}\n              placement='left'\n              aria-label='host'\n              disableInteractive\n            >\n              <ExclamationTriangleIconSolid\n                aria-label={'Exclamation'}\n                className='icon-base n-z-10'\n                style={{ marginTop: '2px', marginRight: '20px', marginLeft: 'auto', color: 'orange' }}\n              />\n            </Tooltip>\n          </div>\n        </div>\n        <ChartType\n          setPageNumber={setPageNumber}\n          records={records}\n          extensions={extensions}\n          selection={selection}\n          settings={settings}\n          fullscreen={expanded}\n          dimensions={dimensions}\n          parameters={parameters}\n          queryCallback={queryCallback}\n          createNotification={createNotification}\n          setGlobalParameter={setGlobalParameter}\n          getGlobalParameter={getGlobalParameter}\n          updateReportSetting={updateReportSetting}\n          fields={fields}\n          setFields={setFields}\n        />\n      </div>\n    );\n  } else if (status == QueryStatus.TIMED_OUT) {\n    return (\n      <NeoCodeViewerComponent\n        value={\n          `Query was aborted - it took longer than ${queryTimeLimit}s to run. \\n` +\n          `Consider limiting your returned query rows,\\nor increase the maximum query time.`\n        }\n      />\n    );\n  }\n  return (\n    <NeoCodeViewerComponent\n      value={records && records[0] && records[0].error && records[0].error}\n      placeholder={'Unknown query error, check the browser console.'}\n    />\n  );\n};\n\nconst mapStateToProps = (state, ownProps) => ({\n  pagenumber: getPageNumber(state),\n  prepopulateExtensionName: getPrepopulateReportExtension(state, ownProps.id),\n  theme: getDashboardTheme(state),\n});\n\nconst mapDispatchToProps = (dispatch) => ({\n  setPageNumber: (index: number) => {\n    dispatch(setPageNumberThunk(index));\n  },\n  deletePrepopulationReportFunction: (id) => {\n    dispatch(deleteSessionStoragePrepopulationReportFunction(id));\n  },\n  getCustomDispatcher: () => {\n    return dispatch;\n  },\n  setSchemaDispatch: (id: any, schema: any) => {\n    dispatch(updateFieldsThunk(id, schema, true));\n  },\n});\n\nexport default connect(mapStateToProps, mapDispatchToProps)(NeoReport);\n"
  },
  {
    "path": "src/report/ReportQueryRunner.ts",
    "content": "import { extractNodePropertiesFromRecords, extractNodeAndRelPropertiesFromRecords } from './ReportRecordProcessing';\nimport isEqual from 'lodash.isequal';\n\nexport enum QueryStatus {\n  NO_QUERY, // No query specified\n  NO_DATA, // No data was returned, therefore we can't draw it.\n  NO_DRAWABLE_DATA, // There is data returned, but we can't draw it\n  WAITING, // The report is waiting for custom logic to be executed.\n  RUNNING, // The report query is running.\n  TIMED_OUT, // Query has reached the time limit.\n  COMPLETE, // There is data returned, and we can visualize it all.\n  COMPLETE_TRUNCATED, // There is data returned, but it's too much so we truncate it.\n  ERROR, // Something broke, likely the cypher query is invalid.\n}\n\n// TODO: create a readOnly version of this method or inject a property\n/**\n * Runs a Cypher query using the specified driver.\n * @param driver - an instance of a Neo4j driver.\n * @param database - optionally, the Neo4j database to run the query against.\n * @param query - the cypher query to run.\n * @param parameters - an optional set of query parameters.\n * @param rowLimit - optionally, the maximum number of rows to retrieve.\n * @param setStatus - callback to retrieve query status.\n * @param setRecords  - callback to retrieve query records.\n * @param setFields - callback to set list of returned query fields.\n * @param queryTimeLimit - maximum query time in seconds.\n * @returns\n */\nexport async function runCypherQuery(\n  driver,\n  database = '',\n  query = '',\n  parameters = {},\n  rowLimit = 1000,\n  setStatus = (status) => {\n    // eslint-disable-next-line no-console\n    console.log(`Query runner attempted to set status: ${JSON.stringify(status)}`);\n  },\n  setRecords = (records) => {\n    // eslint-disable-next-line no-console\n    console.log(`Query runner attempted to set records: ${JSON.stringify(records)}`);\n  },\n  setFields = (fields) => {\n    // eslint-disable-next-line no-console\n    console.log(`Query runner attempted to set fields: ${JSON.stringify(fields)}`);\n  },\n  fields = [],\n  useNodePropsAsFields = false,\n  useReturnValuesAsFields = false,\n  useHardRowLimit = false,\n  queryTimeLimit = 20,\n  setSchema = () => {\n    // eslint-disable-next-line no-console\n    // console.log(`Query runner attempted to set schema: ${JSON.stringify(schema)}`);\n  }\n) {\n  // If no query specified, we don't do anything.\n  if (query.trim() == '') {\n    setFields([]);\n    setStatus(QueryStatus.NO_QUERY);\n    return;\n  }\n  if (!driver) {\n    setStatus(QueryStatus.ERROR);\n    return;\n  }\n\n  const session = database ? driver.session({ database: database }) : driver.session();\n  const transaction = session.beginTransaction({ timeout: queryTimeLimit * 1000, connectionTimeout: 2000 });\n\n  // For usuability reasons, we can set a hard cap on the query result size by wrapping it a subquery (Neo4j 4.0 and later).\n  // This unfortunately does not preserve ordering on the return fields.\n  // If we are on Neo4j 4.0 or later, we can use subqueries to smartly limit the result set size based on report type.\n  if (useHardRowLimit && Object.values(driver._connectionProvider._openConnections).length > 0) {\n    // @ts-ignore\n    const dbVersion = Object.values(driver._connectionProvider._openConnections)[0]._server.version;\n    if (!dbVersion.startsWith('Neo4j/3.')) {\n      query = `CALL { ${query}} RETURN * LIMIT ${rowLimit + 1}`;\n    }\n  }\n\n  await transaction\n    .run(query, parameters)\n    .then((res) => {\n      // @ts-ignore\n      const { records } = res;\n      // TODO - check query summary to ensure that no writes are made in safe-mode.\n      if (records.length == 0) {\n        setStatus(QueryStatus.NO_DATA);\n        // console.log(\"TODO remove this - QUERY RETURNED NO DATA!\")\n        transaction.commit();\n        return;\n      }\n\n      if (useReturnValuesAsFields) {\n        // Send a deep copy of the returned record keys as the set of fields.\n        const newFields = records && records[0] && records[0].keys ? records[0].keys.slice() : [];\n\n        if (!isEqual(newFields, fields)) {\n          setFields(newFields);\n        }\n      } else if (useNodePropsAsFields) {\n        // If we don't use dynamic field mapping, but we do have a selection, use the discovered node properties as fields.\n        const nodePropsAsFields = extractNodePropertiesFromRecords(records);\n        setFields(nodePropsAsFields);\n      }\n\n      setSchema(extractNodeAndRelPropertiesFromRecords(records));\n\n      if (records == null) {\n        setStatus(QueryStatus.NO_DRAWABLE_DATA);\n        // console.log(\"TODO remove this - QUERY RETURNED NO DRAWABLE DATA!\")\n        transaction.commit();\n        return;\n      } else if (records.length > rowLimit) {\n        setStatus(QueryStatus.COMPLETE_TRUNCATED);\n        setRecords(records.slice(0, rowLimit));\n        // console.log(\"TODO remove this - QUERY RETURNED WAS TRUNCTURED!\")\n        transaction.commit();\n        return;\n      }\n      setStatus(QueryStatus.COMPLETE);\n      setRecords(records);\n      // console.log(\"TODO remove this - QUERY WAS EXECUTED SUCCESFULLY!\")\n\n      transaction.commit();\n    })\n    .catch((e) => {\n      // setFields([]);\n\n      // Process timeout errors.\n      if (\n        e.message.startsWith(\n          'The transaction has been terminated. ' +\n            'Retry your operation in a new transaction, and you should see a successful result. ' +\n            'The transaction has not completed within the specified timeout (dbms.transaction.timeout).'\n        )\n      ) {\n        setStatus(QueryStatus.TIMED_OUT);\n        setRecords([{ error: e.message }]);\n        transaction.rollback();\n        return e.message;\n      }\n\n      setStatus(QueryStatus.ERROR);\n      // Process other errors.\n      if (setRecords) {\n        setRecords([{ error: e.message }]);\n      }\n      transaction.rollback();\n      return e.message;\n    });\n}\n"
  },
  {
    "path": "src/report/ReportRecordProcessing.tsx",
    "content": "import React from 'react';\nimport { Chip, Tooltip } from '@mui/material';\nimport { GraphLabel, TextLink } from '@neo4j-ndl/react';\nimport { withStyles } from '@mui/styles';\nimport {\n  getRecordType,\n  toNumber,\n  valueIsArray,\n  valueIsNode,\n  valueIsPath,\n  valueIsRelationship,\n} from '../chart/ChartUtils';\n// import DOMPurify from 'dompurify';\n\n/**\n * Collects all node labels and node properties in a set of Neo4j records.\n * @param records : a list of Neo4j records.\n * @returns a list of lists, where each inner list is [NodeLabel] + [prop1, prop2, prop3]...\n */\nexport function extractNodePropertiesFromRecords(records: any) {\n  const fieldsDict = {};\n  records.forEach((record) => {\n    record._fields.forEach((field) => {\n      saveNodePropertiesToDictionary(field, fieldsDict);\n    });\n  });\n  const fields = Object.keys(fieldsDict).map((label) => {\n    return [label].concat(Object.values(fieldsDict[label]));\n  });\n  return fields.length > 0 ? fields : [];\n}\n\n/**\n * Collects all node labels and node properties in a set of Neo4j records.\n * @param records : a list of Neo4j records.\n * @returns a list of lists, where each inner list is [NodeLabel] + [prop1, prop2, prop3]...\n */\nexport function extractNodeAndRelPropertiesFromRecords(records: any) {\n  const fieldsDict = {};\n  records.forEach((record) => {\n    record._fields.forEach((field) => {\n      saveNodeAndRelPropertiesToDictionary(field, fieldsDict);\n    });\n  });\n  const fields = Object.keys(fieldsDict).map((label) => {\n    return [label].concat(Object.values(fieldsDict[label]));\n  });\n  return fields.length > 0 ? fields : [];\n}\n\n/**\n * Merges an existing set of fields (node labels and their properties) with a new one.\n * This is used when we explore the graph and want to update the report footer.\n * @param oldFields a list of string[].\n * @param newFields  a list of string[].\n * @returns a list of string[].\n */\nexport function mergeNodePropsFieldsLists(oldFields: any[], newFields: any[]) {\n  const fields = [...oldFields];\n  newFields.forEach((newEntry) => {\n    const label = newEntry[0];\n    const existingEntry = fields.filter((f) => f[0] == label)[0];\n    if (!existingEntry) {\n      fields.push(newEntry);\n    } else {\n      newEntry.slice(1).forEach((element) => {\n        if (!element in existingEntry) {\n          existingEntry.push(element);\n        }\n      });\n    }\n  });\n  return fields;\n}\n\nexport function saveNodePropertiesToDictionary(field, fieldsDict) {\n  // TODO - instead of doing this discovery ad-hoc, we could also use CALL db.schema.nodeTypeProperties().\n  if (field == undefined) {\n    return;\n  }\n  if (valueIsArray(field)) {\n    field.forEach((v) => saveNodePropertiesToDictionary(v, fieldsDict));\n  } else if (valueIsNode(field)) {\n    field.labels.forEach((l) => {\n      fieldsDict[l] = fieldsDict[l]\n        ? [...new Set(fieldsDict[l].concat(Object.keys(field.properties)))]\n        : Object.keys(field.properties);\n    });\n  } else if (valueIsPath(field)) {\n    field.segments.forEach((segment) => {\n      saveNodePropertiesToDictionary(segment.start, fieldsDict);\n      saveNodePropertiesToDictionary(segment.end, fieldsDict);\n    });\n  }\n}\n\nexport function saveNodeAndRelPropertiesToDictionary(field, fieldsDict) {\n  // TODO - instead of doing this discovery ad-hoc, we could also use CALL db.schema.nodeTypeProperties().\n  if (field == undefined) {\n    return;\n  }\n  if (valueIsArray(field)) {\n    field.forEach((v) => saveNodeAndRelPropertiesToDictionary(v, fieldsDict));\n  } else if (valueIsNode(field)) {\n    field.labels.forEach((l) => {\n      fieldsDict[l] = fieldsDict[l]\n        ? [...new Set(fieldsDict[l].concat(Object.keys(field.properties)))]\n        : Object.keys(field.properties);\n    });\n  } else if (valueIsRelationship(field)) {\n    let l = field.type;\n    fieldsDict[l] = fieldsDict[l]\n      ? [...new Set(fieldsDict[l].concat(Object.keys(field.properties)))]\n      : Object.keys(field.properties);\n  } else if (valueIsPath(field)) {\n    field.segments.forEach((segment) => {\n      saveNodeAndRelPropertiesToDictionary(segment.start, fieldsDict);\n      saveNodeAndRelPropertiesToDictionary(segment.end, fieldsDict);\n    });\n  }\n}\n\n/* HELPER FUNCTIONS FOR RENDERING A FIELD BASED ON TYPE */\nconst HtmlTooltip = withStyles(() => ({\n  tooltip: {\n    color: 'white',\n    fontSize: 12,\n    border: '1px solid #fcfffa',\n  },\n}))(Tooltip);\n\nfunction addDirection(relationship, start) {\n  relationship.direction = relationship.start.low == start.identity.low;\n  return relationship;\n}\n\nconst rightRelationship =\n  'polygon(10px 0%, calc(100% - 10px) 0%, 100% 50%, 100% calc(100% - 50%), calc(100% - 10px) 100%, 0px 100%, 0% calc(100% - 0px), 0% 0px)';\nconst leftRelationship =\n  'polygon(10px 0%, calc(100% - 0%) 0%, 100% 10px, 100% calc(100% - 10px), calc(100% - 0%) 100%, 10px 100%, 0% calc(100% - 50%), 0% 50%)';\n\nexport function RenderNode(value, hoverable = true) {\n  const chip = RenderNodeChip(value.labels.length > 0 ? value.labels.join(', ') : 'Node');\n  if (!hoverable) {\n    return chip;\n  }\n  return (\n    <HtmlTooltip\n      key={`${0}-${value.identity}`}\n      arrow\n      title={\n        <div>\n          <b> {value.labels.length > 0 ? value.labels.join(', ') : 'Node'}</b>\n          <table>\n            <tbody>\n              {Object.keys(value.properties).length == 0 ? (\n                <tr>\n                  <td>(No properties)</td>\n                </tr>\n              ) : (\n                Object.keys(value.properties)\n                  .sort()\n                  .map((k, i) => (\n                    <tr key={i}>\n                      <td key={0}>{k.toString()}:</td>\n                      <td key={1}>{value.properties[k].toString()}</td>\n                    </tr>\n                  ))\n              )}\n            </tbody>\n          </table>\n        </div>\n      }\n    >\n      {chip}\n    </HtmlTooltip>\n  );\n}\n\nexport function RenderNodeChip(text, color = 'lightgrey', border = '0px') {\n  return (\n    <GraphLabel type='node' color={color} style={{ border: border }}>\n      {text}\n    </GraphLabel>\n  );\n}\n\nexport function RenderRelationshipChip(text, direction = undefined, color = 'lightgrey') {\n  return (\n    <Chip\n      style={{\n        background: color,\n        borderRadius: 0,\n        paddingRight: 5,\n        height: 21,\n        paddingLeft: 5,\n        clipPath: direction == undefined ? 'none' : direction ? rightRelationship : leftRelationship,\n      }}\n      label={text}\n    />\n  );\n}\n\nfunction RenderRelationship(value, key = 0) {\n  return (\n    <HtmlTooltip\n      key={`${key}-${value.identity}`}\n      arrow\n      title={\n        <div>\n          <b> {value.type}</b>\n          <table>\n            <tbody>\n              {Object.keys(value.properties).length == 0 ? (\n                <tr>\n                  <td>(No properties)</td>\n                </tr>\n              ) : (\n                Object.keys(value.properties)\n                  .sort()\n                  .map((k, i) => (\n                    <tr key={i}>\n                      <td key={0}>{k.toString()}:</td>\n                      <td key={1}>{value.properties[k].toString()}</td>\n                    </tr>\n                  ))\n              )}\n            </tbody>\n          </table>\n        </div>\n      }\n    >\n      {RenderRelationshipChip(value.type, value.direction)}\n    </HtmlTooltip>\n  );\n}\n\nfunction RenderPath(value) {\n  return value.segments.map((segment, i) => {\n    return RenderSubValue(\n      i < value.segments.length - 1\n        ? [segment.start, addDirection(segment.relationship, segment.start)]\n        : [segment.start, addDirection(segment.relationship, segment.start), segment.end],\n      i\n    );\n  });\n}\n\n/**\n * Renders an array of values.\n *\n * @param value - The array of values to render.\n * @param transposedTable - Optional. Specifies whether the table should be transposed. Default is false.\n * @returns The rendered array of values.\n */\nfunction RenderArray(value, transposedTable = false) {\n  let mapped = [];\n  // If the first value is neither a Node nor a Relationship object\n  // It is safe to assume that all values should be renedered as strings\n  if (value.length > 0 && !valueIsNode(value[0]) && !valueIsRelationship(value[0])) {\n    // If this request comes up from a transposed table\n    // The returned value must be a single value, not an array\n    // Otherwise, it will cast to [Object object], [Object object]\n    if (transposedTable) {\n      return RenderString(value.join(', '));\n    }\n    // Nominal case of a list of values renderable as strings\n    // These should be joined by commas, and not inside <span> tags\n    mapped = value.map((v, i) => {\n      return RenderSubValue(v) + (i < value.length - 1 ? ', ' : '');\n    });\n  } else {\n    // Render Node and Relationship objects, which will look like a Path\n    mapped = value.map((v, i) => {\n      return (\n        <span key={String(`k${i}`) + v}>\n          {RenderSubValue(v)}\n          {i < value.length - 1 && !valueIsNode(v) && !valueIsRelationship(v) ? <span>, </span> : <></>}\n        </span>\n      );\n    });\n  }\n  return mapped;\n}\n\nfunction RenderString(value) {\n  const str = value?.toString() || '';\n  if (str.startsWith('http') || str.startsWith('https')) {\n    return (\n      <TextLink key={value} externalLink target='_blank' href={str}>\n        {str}\n      </TextLink>\n    );\n  }\n  return str;\n}\n\nfunction RenderLink(value, disabled = false) {\n  // If the link is embedded in a button (disabled), it's not a hyperlink, so we just return the string.\n  if (disabled) {\n    return value;\n  }\n  // Else, it's a 'real' link, and return a React object\n  return (\n    <TextLink key={value} externalLink target='_blank' href={value}>\n      {value}\n    </TextLink>\n  );\n}\n\nfunction RenderPoint(value) {\n  return (\n    <HtmlTooltip\n      key={value.toString()}\n      title={\n        <div>\n          <b>\n            Point x={value.x} y={value.y}\n          </b>\n        </div>\n      }\n    >\n      <Chip label={'📍'} />\n    </HtmlTooltip>\n  );\n}\n\nfunction RenderInteger(value) {\n  // if we cannot cast to integer, use the generic number renderer.\n  if (!value || !value.toInt) {\n    return RenderNumber(value);\n  }\n  const integer = value.toNumber().toLocaleString('en-US');\n  return integer;\n}\n\nfunction RenderNumber(value) {\n  if (value === null || !value.toLocaleString) {\n    return 'null';\n  }\n  const number = value.toLocaleString('en-US');\n  return number;\n}\n\nexport function RenderSubValue(value, transposedTable = false) {\n  if (value == undefined) {\n    return '';\n  }\n  const type = getRecordType(value);\n  const columnProperties = rendererForType[type];\n  if (columnProperties) {\n    if (columnProperties.renderValue) {\n      return columnProperties.renderValue({ value: value, transposedTable: transposedTable });\n    } else if (columnProperties.valueGetter) {\n      return columnProperties.valueGetter({ value: value });\n    }\n  }\n\n  return RenderString(value);\n}\n\nexport const rendererForType: any = {\n  node: {\n    type: 'string',\n    renderValue: (c) => RenderNode(c.value),\n  },\n  relationship: {\n    type: 'string',\n    renderValue: (c) => RenderRelationship(c.value),\n  },\n  path: {\n    type: 'string',\n    renderValue: (c) => RenderPath(c.value),\n  },\n  point: {\n    type: 'string',\n    renderValue: (c) => RenderPoint(c.value),\n  },\n  object: {\n    type: 'string',\n    // valueGetter enables sorting and filtering on string values inside the object\n    valueGetter: (c) => {\n      return JSON.stringify(c.value);\n    },\n    renderValue: (c) => {\n      return JSON.stringify(c.value);\n    },\n  },\n  array: {\n    type: 'string',\n    renderValue: (c) => RenderArray(c.value, c.transposedTable),\n  },\n  string: {\n    type: 'string',\n    renderValue: (c) => RenderString(c.value),\n  },\n  integer: {\n    type: 'number',\n    renderValue: (c) => RenderInteger(c.value),\n  },\n  number: {\n    type: 'number',\n    renderValue: (c) => RenderNumber(c.value),\n  },\n  objectNumber: {\n    type: 'number',\n    renderValue: (c) => RenderNumber(toNumber(c.value)),\n  },\n  null: {\n    type: 'string',\n    renderValue: (c) => RenderString(c.value),\n  },\n  undefined: {\n    type: 'string',\n    renderValue: (c) => RenderString(c.value),\n  },\n  boolean: {\n    type: 'string',\n    renderValue: (c) => RenderString(c.value),\n  },\n  link: {\n    type: 'link',\n    renderValue: (c, disabled = false) => RenderLink(c.value, disabled),\n  },\n};\n\nexport function getRendererForValue(value) {\n  const type = getRecordType(value);\n  return rendererForType[type] == null ? rendererForType.undefined : rendererForType[type];\n}\n\nexport function renderValueByType(value) {\n  const renderer = getRendererForValue(value);\n  if (renderer) {\n    return renderer.renderValue({ value: value });\n  }\n  return value.toString();\n}\n"
  },
  {
    "path": "src/report/ReportWrapper.tsx",
    "content": "import React, { useEffect } from 'react';\nimport NeoReport from './Report';\nimport { withErrorBoundary, useErrorBoundary } from 'react-use-error-boundary';\nimport NeoCodeViewerComponent from '../component/editor/CodeViewerComponent';\n\n/**\n * Error boundary wrapping the report object, to ensure that unexpected errors are handled at the report level.\n */\nconst ErrorBoundary = withErrorBoundary(({ children, resetTrigger }) => {\n  const [error, resetError] = useErrorBoundary();\n\n  useEffect(() => {\n    if (resetTrigger && error) {\n      resetError();\n    }\n  }, [resetTrigger]);\n\n  if (error) {\n    return (\n      <NeoCodeViewerComponent\n        value={`An unexpected error occurred. Try refreshing the component. \\n\\n${error.stack}`}\n      />\n    );\n  }\n  return children;\n});\n\nexport const NeoReportWrapper = ({\n  id,\n  database,\n  query,\n  lastRunTimestamp,\n  parameters,\n  disabled,\n  selection,\n  fields,\n  settings,\n  setFields,\n  setGlobalParameter,\n  getGlobalParameter,\n  updateReportSetting,\n  createNotification,\n  dimensions,\n  rowLimit,\n  queryTimeLimit,\n  type,\n  expanded,\n  extensions,\n  ChartType,\n}) => {\n  return (\n    <ErrorBoundary resetTrigger={disabled}>\n      <NeoReport\n        id={id}\n        database={database}\n        query={query}\n        lastRunTimestamp={lastRunTimestamp}\n        parameters={parameters}\n        disabled={disabled}\n        selection={selection}\n        fields={fields}\n        settings={settings}\n        setFields={setFields}\n        setGlobalParameter={setGlobalParameter}\n        getGlobalParameter={getGlobalParameter}\n        updateReportSetting={updateReportSetting}\n        createNotification={createNotification}\n        dimensions={dimensions}\n        rowLimit={rowLimit}\n        queryTimeLimit={queryTimeLimit}\n        type={type}\n        expanded={expanded}\n        extensions={extensions}\n        ChartType={ChartType}\n      />\n    </ErrorBoundary>\n  );\n};\n"
  },
  {
    "path": "src/sessionStorage/SessionStorageActions.ts",
    "content": "export const SESSION_STORAGE_PREFIX = 'NEODASH_SESSION_STORAGE';\n\nexport const RESET_STATE = `${SESSION_STORAGE_PREFIX}/RESET_STATE`;\nexport const resetSessionStorage = () => ({\n  type: RESET_STATE,\n  payload: {},\n});\n\nexport const STORE_VALUE_SESSION_STORAGE = `${SESSION_STORAGE_PREFIX}/STORE_VALUE`;\n/**\n *  Sets a value with the key passed in input\n * @param key Key to use to access the SessionStorage\n * @param value Value to add inside the SessionStorage\n */\nexport const setSessionStorageValue = (key, value) => ({\n  type: STORE_VALUE_SESSION_STORAGE,\n  payload: { key, value },\n});\n\n/**\n * Deletes a key from the SessionStorage\n * @param key Key to use to access the SessionStorage\n */\nexport const DELETE_VALUE_SESSION_STORAGE = `${SESSION_STORAGE_PREFIX}/DELETE_VALUE`;\nexport const deleteSessionStorageValue = (key) => ({\n  type: DELETE_VALUE_SESSION_STORAGE,\n  payload: { key },\n});\n\n/**\n * Deletes all the keys that start with the prefix passed in input\n * @param prefix Prefix used to match the keys inside the SessionStorage\n */\nexport const DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE = `${SESSION_STORAGE_PREFIX}/DELETE_ALL_KEYS_WITH_PREFIX`;\nexport const deleteAllKeysInSessionStorageWithPrefix = (prefix) => ({\n  type: DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE,\n  payload: { prefix },\n});\n"
  },
  {
    "path": "src/sessionStorage/SessionStorageReducer.ts",
    "content": "/**\n * Reducers define changes to the application state when a given action\n */\n\nimport {\n  DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE,\n  DELETE_VALUE_SESSION_STORAGE,\n  RESET_STATE,\n  STORE_VALUE_SESSION_STORAGE,\n} from './SessionStorageActions';\n\nexport const initialState = {};\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\nexport const sessionStorageReducer = (state = initialState, action: { type: any; payload: any }) => {\n  const { type, payload } = action;\n\n  switch (type) {\n    case RESET_STATE: {\n      return {};\n    }\n    case STORE_VALUE_SESSION_STORAGE: {\n      const { key, value } = payload;\n      let newValue = {};\n      newValue[key] = value;\n      return update(state, newValue);\n    }\n\n    case DELETE_VALUE_SESSION_STORAGE: {\n      const { key } = payload;\n      let newState = { ...state };\n      delete newState[key];\n      return newState;\n    }\n    case DELETE_ALL_KEYS_WITH_PREFIX_SESSION_STORAGE: {\n      const { prefix } = payload;\n      let newState = { ...state };\n      // Deleting all the values that elements that present that\n      Object.keys(newState).map((key) => {\n        if (key.startsWith(prefix)) {\n          delete newState[key];\n        }\n      });\n      return newState;\n    }\n    default: {\n      return state;\n    }\n  }\n};\n"
  },
  {
    "path": "src/sessionStorage/SessionStorageSelectors.ts",
    "content": "export const getSessionStorage = (state: any) => state.sessionStorage;\n\nexport const getSessionStorageValue = (state: any, key: any) => {\n  const sessionStorage = getSessionStorage(state);\n  return sessionStorage[key] ? sessionStorage[key] : undefined;\n};\n\nexport const getSessionStorageValuesWithPrefix = (state: any, prefix: any) => {\n  const sessionStorage = getSessionStorage(state);\n  let filtered = Object.fromEntries(Object.entries(sessionStorage).filter(([k, _]) => k.startsWith(prefix)));\n  return filtered;\n};\n"
  },
  {
    "path": "src/settings/SettingsActions.ts",
    "content": "export const UPDATE_DASHBOARD_SETTING = 'SETTINGS/UPDATE_DASHBOARD_SETTING';\nexport const updateDashboardSetting = (setting: string, value: any) => ({\n  type: UPDATE_DASHBOARD_SETTING,\n  payload: { setting, value },\n});\n"
  },
  {
    "path": "src/settings/SettingsModal.tsx",
    "content": "import React from 'react';\nimport NeoSetting from '../component/field/Setting';\nimport { DASHBOARD_SETTINGS } from '../config/DashboardConfig';\nimport { Dialog, IconButton, MenuItem } from '@neo4j-ndl/react';\nimport { Cog6ToothIconOutline } from '@neo4j-ndl/react/icons';\nimport Tooltip from '@mui/material/Tooltip/Tooltip';\n\nexport const NeoSettingsModal = ({ dashboardSettings, updateDashboardSetting }) => {\n  const [open, setOpen] = React.useState(false);\n\n  const handleClickOpen = () => {\n    setOpen(true);\n  };\n\n  const handleClose = () => {\n    setOpen(false);\n  };\n\n  const settings = DASHBOARD_SETTINGS;\n  // Else, build the advanced settings view.\n  const advancedDashboardSettings = (\n    <div style={{ marginLeft: '-10px' }}>\n      {Object.keys(settings)\n        .filter((setting) => !settings[setting].hidden)\n        .map((setting) => (\n          <div key={setting}>\n            <NeoSetting\n              key={setting}\n              value={dashboardSettings[setting]}\n              type={settings[setting].type}\n              disabled={settings[setting].disabled}\n              helperText={settings[setting].helperText}\n              label={settings[setting].label}\n              defaultValue={settings[setting].default}\n              choices={settings[setting].values}\n              onChange={(e) => updateDashboardSetting(setting, e)}\n            />\n            <br />\n          </div>\n        ))}\n    </div>\n  );\n\n  return (\n    <>\n      <Tooltip title='Settings' aria-label='settings' disableInteractive>\n        <IconButton className='n-mx-1' onClick={handleClickOpen} aria-label='Settings'>\n          <Cog6ToothIconOutline />\n        </IconButton>\n      </Tooltip>\n\n      <Dialog size='large' open={open} onClose={handleClose} aria-labelledby='form-dialog-title'>\n        <Dialog.Header id='form-dialog-title'>\n          <Cog6ToothIconOutline className='icon-base icon-inline text-r' />\n          Dashboard Settings\n        </Dialog.Header>\n        <Dialog.Content>\n          You can modify settings for your entire dashboard here.\n          <br />\n          <br />\n          {advancedDashboardSettings}\n          <br />\n        </Dialog.Content>\n      </Dialog>\n    </>\n  );\n};\n\nexport default NeoSettingsModal;\n"
  },
  {
    "path": "src/settings/SettingsReducer.ts",
    "content": "import { UPDATE_DASHBOARD_SETTING } from './SettingsActions';\n\nconst update = (state, mutations) => Object.assign({}, state, mutations);\n\nexport const SETTINGS_INITIAL_STATE = {\n  pagenumber: 0,\n  editable: true,\n  fullscreenEnabled: false,\n  parameters: {},\n};\n\n/**\n * Reducers define changes to the application state when a given action.\n * This reducer handles updates to a single page of the dashboard.\n * TODO - pagenumbers can be cut from here with new reducer architecture.\n */\nexport const settingsReducer = (state = SETTINGS_INITIAL_STATE, action: { type: any; payload: any }) => {\n  const { type, payload } = action;\n\n  if (!action.type.startsWith('SETTINGS/')) {\n    return state;\n  }\n\n  // Else, deal with page-level operations.\n  switch (type) {\n    case UPDATE_DASHBOARD_SETTING: {\n      const { setting, value } = payload;\n      const settings = state.settings ? state.settings : {};\n\n      // Javascript is amazing, so \"\" == 0. Instead we check if the string length is zero...\n      if (value.toString().length == 0) {\n        const entry = {};\n        entry[setting] = undefined;\n        return update(settings, entry);\n      }\n\n      const entry = {};\n      entry[setting] = value;\n      return update(settings, entry);\n    }\n    default: {\n      return state;\n    }\n  }\n};\n"
  },
  {
    "path": "src/settings/SettingsSelectors.ts",
    "content": "export const getPageNumber = (state: any) => state.dashboard.settings.pagenumber;\n\nexport const getDashboardIsEditable = (state: any) =>\n  state.dashboard.settings.editable && !state.application.standalone;\n\nexport const getGlobalParameters = (state: any) => state.dashboard.settings.parameters;\n\nexport const getSessionParameters = (state: any) => state.application.sessionParameters;\n\n/*\nThe database related to a card is, at its start, the same as the one defined inside the application connection field, however\na user can modify the database that is used by a card with a new option inside the card itself.\nTODO: too overloaded, define two different functions based on the scope (global db or card specific db)\n */\nexport const getDatabase = (state: any, pagenumber: number, id: number) => {\n  if (state == undefined || pagenumber == undefined || id == undefined) {\n    // TODO - use DMBS default database instead of neo4j.\n    return 'neo4j';\n  }\n  if (\n    state.dashboard.pages[pagenumber] == undefined ||\n    state.dashboard.pages[pagenumber].reports.find((o) => o.id === id) == undefined\n  ) {\n    // TODO - use DMBS default database instead of neo4j.\n    return 'neo4j';\n  }\n  const reportDatabase = state.dashboard.pages[pagenumber].reports.find((o) => o.id === id).database;\n  if (reportDatabase !== undefined) {\n    return reportDatabase;\n  }\n  return state.application.connection.database ? state.application.connection.database : 'neo4j';\n};\n"
  },
  {
    "path": "src/settings/SettingsThunks.ts",
    "content": "import { setSessionParameters } from '../application/ApplicationActions';\nimport { hardResetCardSettings } from '../card/CardActions';\nimport { castToNeo4jDate, isCastableToNeo4jDate, toNumber, valueIsNode } from '../chart/ChartUtils';\nimport { createNotificationThunk } from '../page/PageThunks';\nimport { updateDashboardSetting } from './SettingsActions';\n\nexport const setPageNumberThunk = (number) => (dispatch: any, getState: any) => {\n  try {\n    if (number == undefined) {\n      throw 'The specified page could not be found, was it moved, removed, or renamed?';\n    }\n    const { pages } = getState().dashboard;\n    // Make sure the page number is within bounds.\n    number = Math.max(0, Math.min(pages.length - 1, number));\n    dispatch(updateDashboardSetting('pagenumber', number));\n    // Make sure that we don't have weird transitions with the settings popups.\n\n    const page = pages[number];\n    page.reports.map((report) => {\n      dispatch(hardResetCardSettings(number, report.id));\n    });\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to set page number', e));\n  }\n};\n\nexport const updateGlobalParameterThunk = (key, value) => (dispatch: any, getState: any) => {\n  try {\n    const { settings } = getState().dashboard;\n    const parameters = settings.parameters ? settings.parameters : {};\n    if (value !== undefined) {\n      let valueFinal = valueIsNode(value) ? Object.assign({}, value) : value;\n      parameters[key] = valueFinal;\n    } else {\n      delete parameters[key];\n    }\n\n    dispatch(updateDashboardSetting('parameters', { ...parameters }));\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to update global parameter', e));\n  }\n};\n\nexport const updateSessionParameterThunk = (key, value) => (dispatch: any, getState: any) => {\n  try {\n    const { application } = getState();\n    const parameters = application.sessionParameters ? application.sessionParameters : {};\n    if (value !== undefined) {\n      parameters[key] = value;\n    } else {\n      delete parameters[key];\n    }\n\n    dispatch(setSessionParameters({ ...parameters }));\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to update session parameter', e));\n  }\n};\n\nexport const updateGlobalParametersThunk = (newParameters) => (dispatch: any, getState: any) => {\n  try {\n    const { settings } = getState().dashboard;\n    const parameters = settings.parameters ? settings.parameters : {};\n    // if new parameters are set...\n    if (newParameters) {\n      // iterate over the key value pairs in parameters\n      Object.keys(newParameters).forEach((key) => {\n        if (newParameters[key] !== undefined) {\n          parameters[key] = newParameters[key];\n        } else {\n          delete parameters[key];\n        }\n      });\n      dispatch(updateDashboardSetting('parameters', { ...parameters }));\n      dispatch(updateParametersToNeo4jTypeThunk());\n    }\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to update global parameters', e));\n  }\n};\n\n/**\n * Casting complex params to Neo4j type (right now just dates)\n */\nexport const updateParametersToNeo4jTypeThunk = () => (dispatch: any, getState: any) => {\n  try {\n    const { settings } = getState().dashboard;\n    const parameters = settings.parameters ? settings.parameters : {};\n    // if new parameters are set...\n    // iterate over the key value pairs in parameters\n    Object.keys(parameters).forEach((key) => {\n      if (isCastableToNeo4jDate(parameters[key])) {\n        parameters[key] = castToNeo4jDate(parameters[key]);\n      } else if (\n        parameters[key] &&\n        !isNaN(toNumber(parameters[key])) &&\n        typeof toNumber(parameters[key]) === 'number'\n      ) {\n        parameters[key] = toNumber(parameters[key]);\n      } else if (parameters[key] == undefined) {\n        delete parameters[key];\n      }\n    });\n    dispatch(updateDashboardSetting('parameters', { ...parameters }));\n  } catch (e) {\n    dispatch(createNotificationThunk('Unable to update cached parameters to Neo4j types', e));\n  }\n};\n"
  },
  {
    "path": "src/store.ts",
    "content": "import { createStore, combineReducers, applyMiddleware } from 'redux';\nimport { persistReducer } from 'redux-persist';\nimport storage from 'redux-persist/lib/storage';\nimport autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';\nimport thunk from 'redux-thunk';\nimport { composeWithDevTools } from '@redux-devtools/extension';\nimport { dashboardReducer } from './dashboard/DashboardReducer';\nimport { applicationReducer } from './application/ApplicationReducer';\nimport { sessionStorageReducer } from './sessionStorage/SessionStorageReducer';\n\n/**\n * Set up the store (browser cache), as well as the reducers that can update application state.\n */\nconst persistConfig = {\n  key: 'root',\n  storage,\n  stateReconciler: autoMergeLevel2,\n};\n\nconst reducers = {\n  dashboard: dashboardReducer,\n  application: applicationReducer,\n  sessionStorage: sessionStorageReducer,\n};\nconst rootReducer = combineReducers(reducers);\n\n// @ts-ignore\nconst persistedReducer = persistReducer(persistConfig, rootReducer);\n\nexport const configureStore = () => createStore(persistedReducer, composeWithDevTools(applyMiddleware(thunk)));\n"
  },
  {
    "path": "src/utils/ObjectManipulation.ts",
    "content": "import merge from 'lodash.merge';\n\n/**\n * Merges two objects using lodash.merge and returns the result.\n * @param a - The first object to merge.\n * @param b - The second object to merge.\n * @returns A new object representing the merged result.\n */\nexport const objMerge = (a, b) => {\n  return merge({}, a, b);\n};\n\n/**\n * Returns a new object with values at each key mapped using the provided map function.\n * @param object - The original object to map.\n * @param mapFn - The mapping function to apply to each value.\n * @returns A new object with mapped values.\n */\nexport const objectMap = (object, mapFn) => {\n  return Object.keys(object).reduce((result, key) => {\n    result[key] = mapFn(object[key]);\n    return result;\n  }, {});\n};\n"
  },
  {
    "path": "src/utils/ReportUtils.ts",
    "content": "// Components can call this to check if any extension is enabled. For example, to decide whether to all rule-based styling.\nexport const extensionEnabled = (extensions, name) => {\n  return extensions && extensions[name] && extensions[name].active;\n};\n\nexport async function validateQuery(query, driver, database) {\n  /**\n   * This function validates the query in input and returns True if valid, otherwise False\n   */\n  try {\n    const toValidate = `EXPLAIN ${query}`;\n    const session = driver.session({ database: database });\n    const transaction = session.beginTransaction({ timeout: 20 * 1000, connectionTimeout: 2000 });\n    await transaction.run(toValidate);\n    return true;\n  } catch (e) {\n    return false;\n  }\n}\n"
  },
  {
    "path": "src/utils/accessibility.ts",
    "content": "import React from 'react';\nimport type { MouseEvent, KeyboardEvent } from 'react';\nimport { MouseSensor as LibMouseSensor, KeyboardSensor as LibKeyboardSensor } from '@dnd-kit/core';\n\nexport class MouseSensor extends LibMouseSensor {\n  static activators = [\n    {\n      eventName: 'onMouseDown' as const,\n      handler: ({ nativeEvent: event }: MouseEvent) => {\n        return shouldHandleEvent(event.target as HTMLElement);\n      },\n    },\n  ];\n}\n\nexport class KeyboardSensor extends LibKeyboardSensor {\n  static activators = [\n    {\n      eventName: 'onKeyDown' as const,\n      handler: ({ nativeEvent: event }: KeyboardEvent<Element>) => {\n        return shouldHandleEvent(event.target as HTMLElement);\n      },\n    },\n  ];\n}\n\nfunction shouldHandleEvent(element: HTMLElement | null) {\n  let cur = element;\n\n  while (cur) {\n    if (cur?.dataset?.noDnd) {\n      return false;\n    }\n    cur = cur.parentElement;\n  }\n\n  return true;\n}\n\nexport const enterHandler = (event: React.KeyboardEvent<HTMLElement>, callback) => {\n  if (event.key === 'Enter') {\n    // 👇 Get input value\n    callback(event);\n    event.stopPropagation();\n    event.preventDefault();\n  }\n};\n\nexport const openTab = (url, target = '_blank') => {\n  window.open(url, target);\n};\n"
  },
  {
    "path": "src/utils/parameterUtils.ts",
    "content": "/**\n * Extracts all parameter names from a given Cypher query string.\n *\n * @param {string} cypherQuery The Cypher query string to extract parameter names from.\n * @returns {string[]} An array containing all extracted parameter names.\n */\nexport const extractAllParameterNames = (cypherQuery: string): string[] => {\n  // A regular expression pattern to match parameter names following '$'\n  const pattern = /\\$([A-Za-z_]\\w*)/g;\n\n  const parameterNames: string[] = [];\n  let match: any;\n\n  while ((match = pattern.exec(cypherQuery)) !== null) {\n    parameterNames.push(match[1]);\n  }\n\n  return parameterNames;\n};\n\n/**\n * Checks if all parameter names are present in the global parameter names.\n *\n * @param {string[]} parameterNames An array of parameter names to be checked.\n * @param {object} globalParameterNames The object containing global parameter names to compare against.\n * @returns {boolean} A boolean indicating whether all parameters are present in the global parameters.\n */\nexport const checkParametersNameInGlobalParameter = (parameterNames: string[], globalParameterNames: any): boolean => {\n  for (const key of parameterNames) {\n    if (\n      !globalParameterNames[key] ||\n      (Array.isArray(globalParameterNames[key]) && globalParameterNames[key].length === 0) ||\n      globalParameterNames[key] === ''\n    ) {\n      return true;\n    }\n  }\n  return false;\n};\n"
  },
  {
    "path": "src/utils/uuid.ts",
    "content": "// TODO move this to a generic utils file\nexport function createUUID() {\n  let dt = new Date().getTime();\n  let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n    let r = (dt + Math.random() * 16) % 16 | 0;\n    dt = Math.floor(dt / 16);\n    return (c == 'x' ? r : (r & 0x3) | 0x8).toString(16);\n  });\n  return uuid;\n}\n"
  },
  {
    "path": "ssl/Dockerfile",
    "content": "# build stage\nFROM neo4jlabs/neodash:latest AS neodash\n\nENV NGINX_HTTPS_PORT=5443\n\nUSER root\n\nRUN mkdir -p /etc/nginx/certs\n\nRUN --mount=type=secret,id=NEODASH_SSL_KEY \\\n    base64 -d /run/secrets/NEODASH_SSL_KEY > /etc/nginx/certs/key.pem\n\nRUN --mount=type=secret,id=NEODASH_SSL_CERT \\\n    base64 -d /run/secrets/NEODASH_SSL_CERT > /etc/nginx/certs/cert.pem\n\nCOPY default.conf /etc/nginx/templates/default.conf.template\nCOPY default.conf /etc/nginx/conf.d/\n\nRUN chown -R nginx:nginx /etc/nginx\n\nUSER nginx\nEXPOSE $NGINX_HTTPS_PORT\n\nHEALTHCHECK CMD curl --fail \"https://localhost:$NGINX_HTTPS_PORT\" || exit 1\nLABEL version=\"1.0\""
  },
  {
    "path": "ssl/default.conf",
    "content": "server {\n    listen       ${NGINX_PORT};\n    server_name  localhost;\n    include      mime.types;\n    location / {\n        root   /usr/share/nginx/html;\n        try_files $uri $uri/ /index.html;\n        index  index.html index.htm;\n    }\n    # redirect server error pages to the static page /50x.html\n    # Note: This is optional, depending on the implementation in React\n    error_page   500 502 503 504  /50x.html;\n    location = /50x.html {\n        root   /usr/share/nginx/html;\n    }\n}\nserver {\n    listen 5443 ssl;\n    ssl_certificate /etc/nginx/certs/cert.pem;\n    ssl_certificate_key /etc/nginx/certs/key.pem;\n    server_name localhost;\n    include      mime.types;\n    location / {\n        root   /usr/share/nginx/html;\n        try_files $uri $uri/ /index.html;\n        index  index.html index.htm;\n    }\n    # redirect server error pages to the static page /50x.html\n    # Note: This is optional, depending on the implementation in React\n    error_page   500 502 503 504  /50x.html;\n    location = /50x.html {\n        root   /usr/share/nginx/html;\n    }\n}"
  },
  {
    "path": "tailwind.config.js",
    "content": "module.exports = {\n  content: ['./src/**/*.{js,jsx,ts,tsx}'],\n  presets: [require('@neo4j-ndl/base').tailwindConfig],\n  corePlugins: {\n    preflight: false,\n  },\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react\",\n    \"noImplicitAny\": false,\n    \"useDefineForClassFields\": false,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "webpack.config.js",
    "content": "const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');\nconst { sentryWebpackPlugin } = require('@sentry/webpack-plugin');\nconst CircularDependencyPlugin = require('circular-dependency-plugin');\n\nconst circularPlugin = new CircularDependencyPlugin({\n  // exclude detection of files based on a RegExp\n  exclude: /a\\.js|node_modules/,\n  // add errors to webpack instead of warnings\n  failOnError: false,\n  // allow import cycles that include an asyncronous import,\n  // e.g. via import(/* webpackMode: \"weak\" */ './file.js')\n  allowAsyncCycles: false,\n  // set the current working directory for displaying module paths\n  cwd: process.cwd(),\n});\n\nconst circularValidation = false;\n\nconst rules = [\n  {\n    test: /\\.(js|jsx|ts|tsx)$/,\n    exclude: /(node_modules)/,\n    loader: 'babel-loader',\n    options: {\n      presets: ['@babel/env'],\n      //plugins: [isDevelopment && require.resolve('react-refresh/babel')].filter(Boolean),\n    },\n  },\n  {\n    test: /\\.css$/,\n    use: ['style-loader', 'css-loader'],\n  },\n  {\n    test: /\\.pcss$/,\n    use: ['style-loader', 'css-loader', 'postcss-loader'],\n  },\n  {\n    test: /\\.js$/,\n    exclude: /(node_modules\\/react-leaflet-heatmap-layer-v3)/,\n    enforce: 'pre',\n    use: ['source-map-loader'],\n  },\n  {\n    test: /.(png|svg|jpe?g|gif|woff2?|ttf|eot)$/,\n    use: ['file-loader'],\n  },\n];\n\n// TODO - move this config to a dedicated environment file.\n// TODO - use process.env.NODE_ENV which will directly return the environment string \"development\", \"production\".\nmodule.exports = (env) => {\n  const production = env.production;\n  return {\n    experiments: {\n      topLevelAwait: true,\n    },\n    entry: ['./src/index.tsx'],\n    mode: production ? 'production' : 'development',\n    devtool: production ? 'source-map' : 'eval-cheap-module-source-map',\n    module: {\n      rules: rules,\n    },\n    resolve: { extensions: ['*', '.js', '.jsx', '.ts', '.tsx'] },\n    output: {\n      filename: 'bundle.js',\n    },\n    devServer: {\n      port: 3000,\n      hot: true,\n      compress: true,\n      client: {\n        overlay: {\n          warnings: false,\n        },\n      },\n    },\n    plugins: production\n      ? [\n          sentryWebpackPlugin({\n            authToken: process.env.SENTRY_AUTH_TOKEN,\n            org: 'neo4j-inc',\n            project: 'neodash',\n          }),\n        ]\n      : [new ReactRefreshWebpackPlugin(), ...(circularValidation ? [circularPlugin] : [])],\n    ignoreWarnings: [/Failed to parse source map/],\n  };\n};\n"
  }
]