Repository: nielsdejong/neodash Branch: master Commit: 97c0be0b63b7 Files: 384 Total size: 1.5 MB Directory structure: gitextract_84dlt97i/ ├── .babelrc ├── .eslintrc.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ └── feature-request.md │ ├── dependabot.yml │ └── workflows/ │ ├── develop-deployment.yml │ ├── develop-test.yml │ ├── master-deployment.yml │ └── master-test.yml ├── .gitignore ├── .husky/ │ ├── common.sh │ └── pre-commit ├── .lintstagedrc.json ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── Dockerfile ├── LICENSE ├── README.md ├── about.md ├── changelog.md ├── compose.yaml ├── conf/ │ └── default.conf.template ├── cypress/ │ ├── Page.js │ ├── e2e/ │ │ ├── charts/ │ │ │ ├── array.cy.js │ │ │ ├── bar.cy.js │ │ │ └── table.cy.js │ │ └── start_page.cy.js │ ├── fixtures/ │ │ └── cypher_queries.js │ ├── index.js │ ├── plugins/ │ │ └── index.js │ └── support/ │ ├── commands.js │ └── e2e.ts ├── cypress.config.ts ├── docs/ │ ├── README.md │ ├── antora.yml │ ├── modules/ │ │ └── ROOT/ │ │ ├── nav.adoc │ │ └── pages/ │ │ ├── banner.adoc │ │ ├── developer-guide/ │ │ │ ├── adding-visualizations.adoc │ │ │ ├── build-and-run.adoc │ │ │ ├── component-overview.adoc │ │ │ ├── configuration.adoc │ │ │ ├── contributing.adoc │ │ │ ├── deploy-a-build.adoc │ │ │ ├── design.adoc │ │ │ ├── index.adoc │ │ │ ├── session-storage.adoc │ │ │ ├── standalone-mode.adoc │ │ │ ├── state-management.adoc │ │ │ ├── style-configuration.adoc │ │ │ └── testing.adoc │ │ ├── index.adoc │ │ ├── quickstart.adoc │ │ └── user-guide/ │ │ ├── access-control.adoc │ │ ├── bloom-integration.adoc │ │ ├── dashboards.adoc │ │ ├── extensions/ │ │ │ ├── access-control-management.adoc │ │ │ ├── advanced-visualizations.adoc │ │ │ ├── forms.adoc │ │ │ ├── index.adoc │ │ │ ├── natural-language-queries.adoc │ │ │ ├── report-actions.adoc │ │ │ ├── rule-based-styling.adoc │ │ │ └── workflows.adoc │ │ ├── faq.adoc │ │ ├── index.adoc │ │ ├── pages.adoc │ │ ├── publishing.adoc │ │ └── reports/ │ │ ├── areamap.adoc │ │ ├── bar-chart.adoc │ │ ├── choropleth.adoc │ │ ├── circle-packing.adoc │ │ ├── form.adoc │ │ ├── gantt.adoc │ │ ├── gauge-chart.adoc │ │ ├── graph.adoc │ │ ├── graph3d.adoc │ │ ├── iframe.adoc │ │ ├── index.adoc │ │ ├── line-chart.adoc │ │ ├── map.adoc │ │ ├── markdown.adoc │ │ ├── parameter-select.adoc │ │ ├── pie-chart.adoc │ │ ├── radar.adoc │ │ ├── raw-json.adoc │ │ ├── sankey.adoc │ │ ├── single-value.adoc │ │ ├── sunburst.adoc │ │ ├── table.adoc │ │ └── treemap.adoc │ ├── package.json │ ├── preview.yml │ └── server.js ├── gallery/ │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── dashboards/ │ │ ├── assessment.json │ │ ├── bom-english.json │ │ ├── bom.json │ │ ├── citation.json │ │ ├── domains.json │ │ ├── fraud.json │ │ ├── jokes.json │ │ ├── movies.json │ │ ├── recommendations.json │ │ ├── twitter.json │ │ └── wine.json │ ├── package.json │ ├── postcss.config.js │ ├── public/ │ │ ├── index.html │ │ ├── manifest.json │ │ └── robots.txt │ ├── setup.md │ ├── src/ │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ └── setupTests.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── k8s-deploy/ │ ├── neodash/ │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── templates/ │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ ├── deployment.yaml │ │ │ ├── hpa.yaml │ │ │ ├── ingress.yaml │ │ │ ├── service.yaml │ │ │ ├── serviceaccount.yaml │ │ │ └── tests/ │ │ │ └── test-connection.yaml │ │ └── values.yaml │ └── sample-k8s-yamls/ │ ├── deployment.yaml │ └── service.yaml ├── package.json ├── postcss.config.js ├── public/ │ ├── README.md │ ├── config.json │ ├── embed-test.html │ ├── index.html │ ├── manifest.json │ ├── style.config.json │ └── style.css ├── release-notes.md ├── scripts/ │ ├── config-entrypoint.sh │ ├── docker-neo4j-initializer/ │ │ ├── docker-neo4j.sh │ │ ├── movies.cypher │ │ └── start-movies-db.sh │ └── message-entrypoint.sh ├── src/ │ ├── application/ │ │ ├── Application.tsx │ │ ├── ApplicationActions.ts │ │ ├── ApplicationReducer.ts │ │ ├── ApplicationSelectors.ts │ │ ├── ApplicationThunks.ts │ │ └── logging/ │ │ ├── LoggingActions.ts │ │ ├── LoggingReducer.ts │ │ ├── LoggingSelectors.ts │ │ └── LoggingThunk.ts │ ├── card/ │ │ ├── Card.tsx │ │ ├── CardActions.ts │ │ ├── CardAddButton.tsx │ │ ├── CardReducer.ts │ │ ├── CardSelectors.ts │ │ ├── CardStyle.ts │ │ ├── CardThunks.ts │ │ ├── settings/ │ │ │ ├── CardSettings.tsx │ │ │ ├── CardSettingsContent.tsx │ │ │ ├── CardSettingsFooter.tsx │ │ │ └── CardSettingsHeader.tsx │ │ └── view/ │ │ ├── CardView.tsx │ │ ├── CardViewFooter.tsx │ │ └── CardViewHeader.tsx │ ├── chart/ │ │ ├── Chart.ts │ │ ├── ChartUtils.ts │ │ ├── SettingsUtils.ts │ │ ├── Utils.ts │ │ ├── bar/ │ │ │ ├── BarChart.tsx │ │ │ └── util.ts │ │ ├── graph/ │ │ │ ├── GraphChart.tsx │ │ │ ├── GraphChartVisualization.ts │ │ │ ├── GraphChartVisualization2D.tsx │ │ │ ├── GraphChartVisualizationBase.tsx │ │ │ ├── component/ │ │ │ │ ├── GraphChartCanvas.tsx │ │ │ │ ├── GraphChartContextMenu.tsx │ │ │ │ ├── GraphChartEditModal.tsx │ │ │ │ ├── GraphChartInspectModal.tsx │ │ │ │ ├── GraphChartTooltip.tsx │ │ │ │ ├── GraphEntityInspectionTable.tsx │ │ │ │ ├── autocomplete/ │ │ │ │ │ ├── LabelTypeAutocomplete.tsx │ │ │ │ │ └── PropertyNameAutocomplete.tsx │ │ │ │ └── button/ │ │ │ │ ├── GraphChartDeepLinkButton.tsx │ │ │ │ ├── GraphChartFitViewButton.tsx │ │ │ │ ├── GraphChartLockButton.tsx │ │ │ │ └── modal/ │ │ │ │ └── DeletePropertyButton.tsx │ │ │ └── util/ │ │ │ ├── EditUtils.ts │ │ │ ├── ExplorationUtils.ts │ │ │ ├── NodeUtils.ts │ │ │ ├── RecordUtils.ts │ │ │ └── RelUtils.ts │ │ ├── iframe/ │ │ │ └── IFrameChart.tsx │ │ ├── json/ │ │ │ └── JSONChart.tsx │ │ ├── line/ │ │ │ └── LineChart.tsx │ │ ├── map/ │ │ │ ├── MapChart.tsx │ │ │ ├── MapUtils.ts │ │ │ └── layers/ │ │ │ ├── HeatmapLayer.tsx │ │ │ ├── LineLayer.tsx │ │ │ └── MarkerLayer.tsx │ │ ├── markdown/ │ │ │ └── MarkdownChart.tsx │ │ ├── parameter/ │ │ │ ├── ParameterSelectCardSettings.tsx │ │ │ ├── ParameterSelectionChart.tsx │ │ │ └── component/ │ │ │ ├── DateParameterSelect.tsx │ │ │ ├── FreeTextParameterSelect.tsx │ │ │ ├── NodePropertyParameterSelect.tsx │ │ │ ├── ParameterSelect.ts │ │ │ ├── QueryParameterSelect.tsx │ │ │ ├── RelationshipPropertyParameterSelect.tsx │ │ │ └── SelectionConfirmationButton.tsx │ │ ├── pie/ │ │ │ └── PieChart.tsx │ │ ├── scatter/ │ │ │ └── ScatterPlotChart.tsx │ │ ├── single/ │ │ │ └── SingleValueChart.tsx │ │ └── table/ │ │ ├── TableActionsHelper.ts │ │ └── TableChart.tsx │ ├── component/ │ │ ├── editor/ │ │ │ ├── CodeEditorComponent.tsx │ │ │ └── CodeViewerComponent.tsx │ │ ├── field/ │ │ │ ├── ColorPicker.tsx │ │ │ ├── DateField.tsx │ │ │ ├── Field.tsx │ │ │ └── Setting.tsx │ │ ├── misc/ │ │ │ └── DashboardConnectionUpdateHandler.tsx │ │ ├── sso/ │ │ │ ├── SSOLoginButton.tsx │ │ │ └── SSOUtils.ts │ │ └── theme/ │ │ └── Themes.tsx │ ├── config/ │ │ ├── ApplicationConfig.ts │ │ ├── CardConfig.ts │ │ ├── ColorConfig.ts │ │ ├── DashboardConfig.ts │ │ ├── ExampleConfig.ts │ │ ├── PageConfig.ts │ │ ├── ReportConfig.tsx │ │ └── StyleConfig.tsx │ ├── dashboard/ │ │ ├── Dashboard.tsx │ │ ├── DashboardActions.ts │ │ ├── DashboardReducer.ts │ │ ├── DashboardSelectors.ts │ │ ├── DashboardThunks.ts │ │ ├── header/ │ │ │ ├── DashboardHeader.tsx │ │ │ ├── DashboardHeaderAboutButton.tsx │ │ │ ├── DashboardHeaderDownloadImageButton.tsx │ │ │ ├── DashboardHeaderLogo.tsx │ │ │ ├── DashboardHeaderLogoutButton.tsx │ │ │ ├── DashboardHeaderPageList.tsx │ │ │ ├── DashboardHeaderPageTitle.tsx │ │ │ └── DashboardTitle.tsx │ │ ├── placeholder/ │ │ │ └── DashboardPlaceholder.tsx │ │ └── sidebar/ │ │ ├── DashboardSidebar.tsx │ │ ├── DashboardSidebarListItem.tsx │ │ ├── menu/ │ │ │ ├── DashboardSidebarCreateMenu.tsx │ │ │ ├── DashboardSidebarDashboardMenu.tsx │ │ │ └── DashboardSidebarDatabaseMenu.tsx │ │ └── modal/ │ │ ├── DashboardSidebarAccessModal.tsx │ │ ├── DashboardSidebarCreateModal.tsx │ │ ├── DashboardSidebarDeleteModal.tsx │ │ ├── DashboardSidebarExportModal.tsx │ │ ├── DashboardSidebarImportModal.tsx │ │ ├── DashboardSidebarInfoModal.tsx │ │ ├── DashboardSidebarLoadModal.tsx │ │ ├── DashboardSidebarSaveModal.tsx │ │ ├── DashboardSidebarShareModal.tsx │ │ └── legacy/ │ │ └── LegacyShareModal.tsx │ ├── extensions/ │ │ ├── ExtensionConfig.tsx │ │ ├── ExtensionUtils.ts │ │ ├── ExtensionsModal.tsx │ │ ├── actions/ │ │ │ └── ActionsRuleCreationModal.tsx │ │ ├── advancedcharts/ │ │ │ ├── AdvancedChartsExampleConfig.ts │ │ │ ├── AdvancedChartsReportConfig.tsx │ │ │ ├── Utils.ts │ │ │ ├── chart/ │ │ │ │ ├── areamap/ │ │ │ │ │ ├── AreaMapChart.tsx │ │ │ │ │ ├── PolygonLayer.tsx │ │ │ │ │ ├── constants.ts │ │ │ │ │ └── styles/ │ │ │ │ │ └── PolygonStyle.css │ │ │ │ ├── choropleth/ │ │ │ │ │ └── ChoroplethMapChart.tsx │ │ │ │ ├── circlepacking/ │ │ │ │ │ └── CirclePackingChart.tsx │ │ │ │ ├── gantt/ │ │ │ │ │ ├── GanttChart.tsx │ │ │ │ │ ├── Utils.ts │ │ │ │ │ └── frappe/ │ │ │ │ │ ├── GanttVisualization.tsx │ │ │ │ │ └── lib/ │ │ │ │ │ ├── arrow.ts │ │ │ │ │ ├── bar.ts │ │ │ │ │ ├── date_utils.js │ │ │ │ │ ├── gantt.css │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── popup.ts │ │ │ │ │ └── svg_utils.ts │ │ │ │ ├── gauge/ │ │ │ │ │ └── GaugeChart.tsx │ │ │ │ ├── graph3d/ │ │ │ │ │ ├── GraphChart3D.tsx │ │ │ │ │ └── GraphChartVisualization3D.tsx │ │ │ │ ├── radar/ │ │ │ │ │ └── RadarChart.tsx │ │ │ │ ├── sankey/ │ │ │ │ │ └── SankeyChart.tsx │ │ │ │ ├── sunburst/ │ │ │ │ │ └── SunburstChart.tsx │ │ │ │ └── treemap/ │ │ │ │ └── TreeMapChart.tsx │ │ │ └── component/ │ │ │ └── RefreshButton.tsx │ │ ├── forms/ │ │ │ ├── FormsExampleConfig.tsx │ │ │ ├── FormsReportConfig.tsx │ │ │ ├── chart/ │ │ │ │ └── NeoForm.tsx │ │ │ └── settings/ │ │ │ ├── NeoFormCardSettings.tsx │ │ │ ├── NeoFormCardSettingsModal.tsx │ │ │ └── list/ │ │ │ ├── NeoFormSortableItem.tsx │ │ │ ├── NeoFormSortableList.tsx │ │ │ └── NeoFormSortableOverlay.tsx │ │ ├── query-translator/ │ │ │ └── component/ │ │ │ └── OverrideCardQueryEditor.tsx │ │ ├── rbac/ │ │ │ ├── RBACManagementLabelButton.tsx │ │ │ ├── RBACManagementMenu.tsx │ │ │ ├── RBACManagementModal.tsx │ │ │ └── RBACUtils.ts │ │ ├── state/ │ │ │ ├── ExtensionActions.ts │ │ │ ├── ExtensionReducer.ts │ │ │ └── ExtensionSelectors.ts │ │ ├── styling/ │ │ │ ├── StyleRuleCreationModal.tsx │ │ │ └── StyleRuleEvaluator.ts │ │ └── text2cypher/ │ │ ├── QueryTranslatorConfig.ts │ │ ├── clients/ │ │ │ ├── AzureOpenAi/ │ │ │ │ └── AzureOpenAiClient.ts │ │ │ ├── ModelClient.ts │ │ │ ├── OpenAi/ │ │ │ │ └── OpenAiClient.ts │ │ │ ├── VertexAiClient.ts │ │ │ └── const.ts │ │ ├── component/ │ │ │ ├── ClientSettings.tsx │ │ │ ├── LoadingIcon.tsx │ │ │ ├── OverrideCardQueryEditor.tsx │ │ │ ├── QueryTranslatorButton.tsx │ │ │ ├── QueryTranslatorSettingsModal.tsx │ │ │ └── model-examples/ │ │ │ ├── ExampleDisplayTable.tsx │ │ │ ├── ExampleEditorModal.tsx │ │ │ ├── QueryTranslatorSettingsModelExamples.tsx │ │ │ └── utils.ts │ │ ├── state/ │ │ │ ├── QueryTranslatorActions.ts │ │ │ ├── QueryTranslatorReducer.ts │ │ │ ├── QueryTranslatorSelector.ts │ │ │ └── QueryTranslatorThunks.ts │ │ └── util/ │ │ ├── Status.ts │ │ └── Util.ts │ ├── index.pcss │ ├── index.tsx │ ├── modal/ │ │ ├── AboutModal.tsx │ │ ├── ConnectionModal.tsx │ │ ├── DeletePageModal.tsx │ │ ├── ExportModal.tsx │ │ ├── LoadSharedDashboardModal.tsx │ │ ├── ModalSelectors.tsx │ │ ├── ModalUtils.tsx │ │ ├── NotificationModal.tsx │ │ ├── ReportExamplesModal.tsx │ │ ├── ReportHelpModal.tsx │ │ ├── UpgradeOldDashboardModal.tsx │ │ └── WelcomeScreenModal.tsx │ ├── page/ │ │ ├── Page.tsx │ │ ├── PageActions.ts │ │ ├── PageReducer.ts │ │ ├── PageSelectors.ts │ │ └── PageThunks.ts │ ├── report/ │ │ ├── Report.tsx │ │ ├── ReportQueryRunner.ts │ │ ├── ReportRecordProcessing.tsx │ │ └── ReportWrapper.tsx │ ├── sessionStorage/ │ │ ├── SessionStorageActions.ts │ │ ├── SessionStorageReducer.ts │ │ └── SessionStorageSelectors.ts │ ├── settings/ │ │ ├── SettingsActions.ts │ │ ├── SettingsModal.tsx │ │ ├── SettingsReducer.ts │ │ ├── SettingsSelectors.ts │ │ └── SettingsThunks.ts │ ├── store.ts │ └── utils/ │ ├── ObjectManipulation.ts │ ├── ReportUtils.ts │ ├── accessibility.ts │ ├── parameterUtils.ts │ └── uuid.ts ├── ssl/ │ ├── Dockerfile │ └── default.conf ├── tailwind.config.js ├── tsconfig.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["@babel/preset-env", "@babel/preset-react","@babel/preset-typescript"], "plugins": ["@babel/plugin-transform-runtime", "istanbul"] } ================================================ FILE: .eslintrc.json ================================================ { "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint", "react"], "extends": ["eslint:recommended", "prettier", "plugin:@typescript-eslint/recommended"], // this is optional "env": { "browser": true, "node": true, "jest": true }, "settings": { "react": { "version": "detect" } }, "ignorePatterns": ["node_modules/**", "packages/**/dist/**", "packages/**/coverage/**"], "rules": { "@typescript-eslint/no-explicit-any": "off", // Off for v1 "@typescript-eslint/ban-ts-comment": "off", // Off for v1 "@typescript-eslint/no-empty-function": "off", // Off for v1 "@typescript-eslint/no-unused-vars": [ "error", { "vars": "all", "varsIgnorePattern": "^_*", "args": "after-used", "argsIgnorePattern": "^_" } ], "array-callback-return": "off", // Off for v1 "arrow-body-style": "off", "block-scoped-var": "error", "camelcase": "off", // Off for v1 "consistent-return": "off", // Off for v1 "consistent-this": ["error", "self"], "constructor-super": "error", "curly": ["error", "all"], "default-case": "error", "default-param-last": "off", // Off for v1 "dot-notation": "error", "eqeqeq": "off", // Off for v1 "func-names": "error", "func-style": [ "error", "declaration", { "allowArrowFunctions": true } ], "grouped-accessor-pairs": "error", "line-comment-position": "off", // Off for v1 "lines-between-class-members": "error", "max-depth": "error", "max-len": [ "off", // Off for v1 { "code": 120, "comments": 120, "ignoreUrls": true, "ignoreTemplateLiterals": true } ], "max-lines-per-function": ["off"], "max-nested-callbacks": ["error", 5], "max-statements": ["off"], "max-statements-per-line": "error", "no-alert": "off", // Off for v1 "no-array-constructor": "error", "no-await-in-loop": "off", // Off for v1 "no-buffer-constructor": "error", "no-caller": "error", "no-confusing-arrow": "error", "no-console": "warn", "no-constructor-return": "error", "no-constant-condition": "error", "no-debugger": "warn", "no-dupe-else-if": "error", "no-else-return": "error", "no-empty-function": [ "off", // Off for v1 { "allow": ["constructors"] } ], "no-eq-null": "off", // Off for V1 "no-eval": "error", "no-extend-native": "error", "no-extra-bind": "error", "no-extra-label": "error", "no-implicit-coercion": "error", "no-implicit-globals": "error", "no-implied-eval": "error", "no-import-assign": "error", "no-invalid-this": "off", "no-iterator": "error", "no-labels": "error", "no-lone-blocks": "error", "no-lonely-if": "error", "no-loop-func": "error", "no-magic-numbers": "off", "no-multi-assign": "error", "no-multi-str": "error", "no-nested-ternary": "off", // Off for v1 "no-new": "error", "no-new-func": "error", "no-new-object": "error", "no-new-wrappers": "error", "no-octal-escape": "error", "no-param-reassign": "off", // Off for v1 "no-path-concat": "error", "no-plusplus": [ "error", { "allowForLoopAfterthoughts": true } ], "no-proto": "off", // Off for v1 "no-restricted-globals": "error", "no-return-assign": "error", "no-return-await": "error", "no-self-compare": "error", "no-sequences": "error", "no-setter-return": "error", "no-sync": "error", "no-tabs": "error", "no-template-curly-in-string": "error", "no-underscore-dangle": "off", // Off for v1 "no-unmodified-loop-condition": "error", "no-unneeded-ternary": "error", "no-unreachable": "error", "no-unused-expressions": "off", // Off for v1 "no-useless-call": "error", "no-useless-computed-key": "error", "no-useless-concat": "off", // Off for v1 "no-useless-rename": "error", "no-useless-return": "error", "no-var": "error", "no-void": ["error", { "allowAsStatement": true }], "one-var": ["error", "never"], "operator-assignment": "error", "padding-line-between-statements": "error", "prefer-arrow-callback": "warn", "prefer-const": "off", // Off for v1 "prefer-destructuring": [ // Off for v1 "warn", { "VariableDeclarator": { "array": true, "object": true }, "AssignmentExpression": { "array": false, "object": false } } ], "prefer-numeric-literals": "warn", "prefer-promise-reject-errors": "warn", "prefer-rest-params": "warn", "prefer-spread": "warn", "prefer-template": "warn", "radix": "off", // Off for v1 "require-atomic-updates": "error", "require-await": "warn", // Warn for v1 "sort-keys": "off", "spaced-comment": [ "warn", "always", { "markers": ["/"] } ], "symbol-description": "error", "yoda": "error" }, "globals": { "cy": "readonly", "Cypress": "readonly" } } ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- ## Guidelines Please note that GitHub issues are only meant for bug reports/feature requests. Before 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. To help us understand your issue, please specify important details, primarily: - NeoDash version: X.Y.Z - Neo4j Database version: X.Y.Z (Community/Enterprise/Aura). - **Steps to reproduce** - Expected behavior - Actual behavior Additionally, include (as appropriate) screenshots, drawings, etc. ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: feature assignees: '' --- ## Guidelines Please note that GitHub issues are only meant for bug reports/feature requests. ## Feature request template **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: 'npm' directory: '/' schedule: interval: 'weekly' target-branch: 'develop' - package-ecosystem: 'npm' directory: '/gallery' schedule: interval: 'weekly' target-branch: 'develop' - package-ecosystem: 'npm' directory: '/docs' schedule: interval: 'weekly' target-branch: 'develop' ================================================ FILE: .github/workflows/develop-deployment.yml ================================================ name: Test/Deploy Develop on: push: branches: [develop] jobs: build-test: if: github.event.pull_request.draft == false runs-on: neodash-runners strategy: matrix: node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Creating Neo4j Container run: | chmod +x ./scripts/docker-neo4j-initializer/docker-neo4j.sh ./scripts/docker-neo4j-initializer/docker-neo4j.sh sleep 30s chmod +x ./scripts/docker-neo4j-initializer/start-movies-db.sh ./scripts/docker-neo4j-initializer/start-movies-db.sh - run: yarn install - name: Eslint check run: yarn run lint - name: Cypress run uses: cypress-io/github-action@v4 with: build: yarn run build start: yarn run prod wait-on: "http://localhost:3000" browser: chrome build-s3: needs: build-test runs-on: neodash-runners strategy: matrix: node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: yarn install - run: yarn run build-minimal - name: Set AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-west-1 - run: curl ${{ secrets.INDEX_HTML_DEPLOYMENT_URL }} > dist/index.html - run: aws s3 rm s3://neodash-test.graphapp.io/ --recursive && aws s3 sync dist s3://neodash-test.graphapp.io/ --acl public-read ================================================ FILE: .github/workflows/develop-test.yml ================================================ name: Test Develop on: pull_request: branches: [develop] jobs: build-test: if: github.event.pull_request.draft == false runs-on: neodash-runners strategy: matrix: node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Creating Neo4j Container run: | chmod +x ./scripts/docker-neo4j-initializer/docker-neo4j.sh ./scripts/docker-neo4j-initializer/docker-neo4j.sh sleep 30s chmod +x ./scripts/docker-neo4j-initializer/start-movies-db.sh ./scripts/docker-neo4j-initializer/start-movies-db.sh - run: yarn install - name: Eslint check run: yarn run lint - name: Cypress run uses: cypress-io/github-action@v4 with: build: yarn run build start: yarn run prod wait-on: 'http://localhost:3000' browser: chrome - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 ================================================ FILE: .github/workflows/master-deployment.yml ================================================ name: Test/Deploy Master on: push: branches: [master] jobs: build-test: runs-on: neodash-runners strategy: matrix: node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Creating Neo4j Container run: | chmod +x ./scripts/docker-neo4j-initializer/docker-neo4j.sh ./scripts/docker-neo4j-initializer/docker-neo4j.sh sleep 30s chmod +x ./scripts/docker-neo4j-initializer/start-movies-db.sh ./scripts/docker-neo4j-initializer/start-movies-db.sh - run: yarn install - name: Eslint check run: yarn run lint - name: Cypress run uses: cypress-io/github-action@v4 with: build: yarn run build start: yarn run dev wait-on: 'http://localhost:3000' browser: chrome build-s3: needs: build-test runs-on: neodash-runners strategy: matrix: node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: rm -rf docs - run: yarn install - run: yarn run build-minimal - name: Set AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-west-1 - run: curl ${{ secrets.INDEX_HTML_DEPLOYMENT_URL }} > dist/index.html # - run: curl https://gist.githubusercontent.com/nielsdejong/944d8f8f30dd2719f9b275e31df22f92/raw/f363cf5280eb5095e12e56a278f6616b6220adcf/config.json > dist/config.json - run: aws s3 rm s3://neodash.graphapp.io/ --recursive && aws s3 sync dist s3://neodash.graphapp.io/ --acl public-read build-docker: needs: build-test runs-on: neodash-runners strategy: matrix: node-version: [18.x] steps: - name: run Docker uses: actions/checkout@v2 - run: rm -rf docs - name: Login to Docker Hub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_HUB_LABS_USERNAME }} password: ${{ secrets.DOCKER_HUB_LABS_ACCESS_TOKEN }} - name: Set up Docker Build uses: docker/setup-buildx-action@v1 - name: Build and push uses: docker/build-push-action@v2 with: context: . file: ./Dockerfile push: true tags: ${{ secrets.DOCKER_HUB_LABS_USERNAME }}/neodash:latest,${{ secrets.DOCKER_HUB_LABS_USERNAME }}/neodash:2.4.11 build-docker-legacy: needs: build-test runs-on: neodash-runners strategy: matrix: node-version: [18.x] steps: - name: run Docker uses: actions/checkout@v2 - run: rm -rf docs - name: Login to Docker Hub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Build and push uses: docker/build-push-action@v2 with: context: . file: ./Dockerfile push: true tags: ${{ secrets.DOCKER_HUB_USERNAME }}/neodash:latest,${{ secrets.DOCKER_HUB_USERNAME }}/neodash:2.4.11 deploy-gallery: runs-on: neodash-runners strategy: matrix: node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: cd gallery && yarn install - run: cd gallery && yarn run build - name: Set AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-west-1 - run: aws s3 rm s3://neodash-gallery.graphapp.io/ --recursive && aws s3 sync gallery/build s3://neodash-gallery.graphapp.io/ --acl public-read deploy-docs: needs: build-test runs-on: neodash-runners steps: - name: Trigger Developer Event uses: peter-evans/repository-dispatch@main with: token: ${{ secrets.DOCS_REFRESH_TOKEN }} repository: neo4j-documentation/docs-refresh event-type: labs build-npm: needs: build-test runs-on: neodash-runners strategy: matrix: node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: rm -rf docs - run: yarn install - run: PRODUCTION=true && yarn run build-minimal - run: curl https://gist.githubusercontent.com/nielsdejong/944d8f8f30dd2719f9b275e31df22f92/raw/f363cf5280eb5095e12e56a278f6616b6220adcf/config.json > dist/config.json - run: curl ${{ secrets.INDEX_HTML_DEPLOYMENT_URL }} > dist/index.html - run: npm pack - run: rm -rf target - run: mkdir target - run: mv *.tgz target/ - run: tar -xvf target/*.tgz - run: rm -f target/*.tgz - run: cp package/dist/favicon.ico package/favicon.ico - run: echo "${{ secrets.NEO4J_LABS_APP_KEY }}" > neo4j-labs-app.pem - run: echo "${{ secrets.NEO4J_LABS_APP_CERTIFICATE }}" > neo4j-labs-app.cert - run: npx @neo4j/code-signer --app ./package --private-key neo4j-labs-app.pem --cert neo4j-labs-app.cert --passphrase ${{ secrets.NEO4J_DESKTOP_PASSPHRASE }} - run: echo "${{ secrets.NEO4J_DESKTOP_CERTIFICATE }}" > neo4j_desktop.cert - run: npx @neo4j/code-signer --verify --app ./package --root-cert neo4j_desktop.cert - run: cd package && npm pack - run: mv package/*.tgz . - run: rm -rf package - run: tar xvf *.tgz package - run: npx @neo4j/code-signer --verify --app ./package --root-cert neo4j_desktop.cert - run: rm -rf package - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc - name: Publish package to NPM 📦 run: npm publish --access public neodash*.tgz ================================================ FILE: .github/workflows/master-test.yml ================================================ name: Test Master on: pull_request: branches: [master] jobs: build-test: runs-on: neodash-runners strategy: matrix: node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Creating Neo4j Container run: | chmod +x ./scripts/docker-neo4j-initializer/docker-neo4j.sh ./scripts/docker-neo4j-initializer/docker-neo4j.sh sleep 30s chmod +x ./scripts/docker-neo4j-initializer/start-movies-db.sh ./scripts/docker-neo4j-initializer/start-movies-db.sh - run: yarn install - name: Eslint check run: yarn run lint - name: Cypress run uses: cypress-io/github-action@v4 with: build: yarn run build start: yarn run dev wait-on: "http://localhost:3000" browser: chrome ================================================ FILE: .gitignore ================================================ .idea *.iml *.pem *.cert *.passphrase node_modules build target /node_modules /.pnp .pnp.js .vscode # testing /coverage /.nyc_output cypress/videos cypress/screenshots # production /build /dist # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local yarn-debug.log* yarn-error.log* # Ignore builds *.tgz # package directories node_modules jspm_packages # Serverless directories .serverless # Sentry Auth Token .env.sentry-build-plugin ================================================ FILE: .husky/common.sh ================================================ command_exists () { command -v "$1" >/dev/null 2>&1 } # Workaround for Windows 10, Git Bash and Yarn if command_exists winpty && test -t 1; then exec < /dev/tty fi ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/common.sh" yarn run lint-staged ================================================ FILE: .lintstagedrc.json ================================================ { "*.ts": ["prettier --write", "eslint --fix"], "*.tsx": ["prettier --write", "eslint --fix"], "*.json": ["prettier --write"], "*.js": ["prettier --write"] } ================================================ FILE: .npmignore ================================================ public src .env .env.hosted .gitignore tsconfig.json yarn.lock node_modules *.tgz desktop.passphrase *.config.js desktop-signer.sh ================================================ FILE: .prettierignore ================================================ coverage dist node_modules docs ================================================ FILE: .prettierrc.json ================================================ { "printWidth": 120, "semi": true, "singleQuote": true, "jsxSingleQuote": true, "useTabs": false, "tabWidth": 2, "arrowParens": "always", "trailingComma": "es5", "bracketSpacing": true, "endOfLine": "lf" } ================================================ FILE: Dockerfile ================================================ # build stage FROM node:lts-alpine3.18 AS build-stage RUN yarn global add typescript jest WORKDIR /usr/local/src/neodash # Pull source code if you have not cloned the repository #RUN apk add --no-cache git #RUN git clone https://github.com/neo4j-labs/neodash.git /usr/local/src/neodash # Copy sources and install/build COPY ./package.json /usr/local/src/neodash/package.json COPY ./yarn.lock /usr/local/src/neodash/yarn.lock RUN yarn install COPY ./ /usr/local/src/neodash RUN yarn run build-minimal # production stage FROM nginx:alpine3.18 AS neodash RUN apk upgrade ENV NGINX_PORT=5005 COPY --from=build-stage /usr/local/src/neodash/dist /usr/share/nginx/html COPY ./conf/default.conf.template /etc/nginx/templates/ COPY ./scripts/config-entrypoint.sh /docker-entrypoint.d/config-entrypoint.sh COPY ./scripts/message-entrypoint.sh /docker-entrypoint.d/message-entrypoint.sh RUN chown -R nginx:nginx /var/cache/nginx && \ chown -R nginx:nginx /var/log/nginx && \ chown -R nginx:nginx /etc/nginx/conf.d && \ chown -R nginx:nginx /etc/nginx/templates && \ chown -R nginx:nginx /docker-entrypoint.d/config-entrypoint.sh && \ chmod +x /docker-entrypoint.d/config-entrypoint.sh && \ chmod +x /docker-entrypoint.d/message-entrypoint.sh RUN touch /var/run/nginx.pid && \ chown -R nginx:nginx /var/run/nginx.pid RUN chown -R nginx:nginx /usr/share/nginx/html/ ## Launch webserver as non-root user. USER nginx EXPOSE $NGINX_PORT HEALTHCHECK cmd curl --fail "http://localhost:$NGINX_PORT" || exit 1 LABEL version="2.4.11" ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2023 Niels de Jong Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ ## NeoDash Labs NeoDash is a dashboard builder for Neo4j, letting you build a graph dashboard in minutes. **This project is no longer maintained, use at your own risk.** If you'd like to continue building dashboards, you have the following options: 1. For the best experience, [upgrade](https://console-preview.neo4j.io/tools/dashboards) to Dashboards in the Neo4j Console (free for everyone). 2. 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). 3. 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. ================================================ FILE: about.md ================================================ ## About NeoDash Labs > NeoDash Labs is an unmaintained and unsupported tool. Use at your own risk! NeoDash is a web-based tool for visualizing your Neo4j data. It lets you group visualizations together as dashboards, and allow for interactions between reports. Neodash 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. ## Try NeoDash Labs You can build NeoDash yourself, or pull the latest Docker image from Docker Hub. ``` # Run the application on http://localhost:5005 docker pull neo4jlabs/neodash:latest docker run -it --rm -p 5005:5005 neo4jlabs/neodash ``` > Windows users may need to prefix the `docker run` command with `winpty`. ## Build and Run This project uses `yarn` to install, run, build prettify and apply linting to the code. To install dependencies: ``` yarn install ``` To run the application in development mode: ``` yarn run dev ``` To build the app for deployment: ``` yarn run build ``` To manually prettify all the project `.ts` and `.tsx` files, run: ``` yarn run format ``` To manually run linting of all your .ts and .tsx files, run: ``` yarn run lint ``` To manually run linting of all your .ts and .tsx staged files, run: ``` yarn run lint-staged ``` See the [Developer Guide](https://neo4j.com/labs/neodash/2.3/developer-guide/) for more on installing, building, and running the application. ### Pre-Commit Hook While 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. There is also a dedicated linting step in the Github project pipeline in order to catch each potential inconsistency. > Don't hesitate to setup your IDE formatting feature to use the Prettier module and our defined rules (.prettierrc.json). ## User Guide NeoDash 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]( https://neo4j.com/labs/neodash/2.3/user-guide/). ## Publish Dashboards After 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. > NeoDash Labs is a free and open-source tool developed by the Neo4j community - not an official Neo4j product. Use at your own risk! ================================================ FILE: changelog.md ================================================ ## NeoDash 2.4.10 - Community contributions This is a minor release containing bug fixes and improvements contributed by the NeoDash community. - [#1039](https://github.com/neo4j-labs/neodash/pull/1039) - Fix default color scheme for bar charts - [#1038](https://github.com/neo4j-labs/neodash/pull/1038) - Fix rule-based styling for line charts - [#1036](https://github.com/neo4j-labs/neodash/pull/1036) - Fix table cell rule-based styling - [#1029](https://github.com/neo4j-labs/neodash/pull/1029) - Fix rule-based styling for numeric values - [#1028](https://github.com/neo4j-labs/neodash/pull/1028) - Fix OpenStreetMap leaflet display - [#1020](https://github.com/neo4j-labs/neodash/pull/1020) - Fix boolean handling in parameter selection - [#1014](https://github.com/neo4j-labs/neodash/pull/1014) - Remove autoPageSize flag (defaults to 0) - [#1009](https://github.com/neo4j-labs/neodash/pull/1009) - Fix SSO parameters lost on browser redirect - [#1008](https://github.com/neo4j-labs/neodash/pull/1008) - Fix existence check for `value.low` - [#1005](https://github.com/neo4j-labs/neodash/pull/1005) - Replace Neo4j Logo - [#1002](https://github.com/neo4j-labs/neodash/pull/1002) - Patch FAQ on supportability - [#999](https://github.com/neo4j-labs/neodash/pull/999) - Fix dark mode table header styling - [#956](https://github.com/neo4j-labs/neodash/pull/956) - Change default protocol to `neo4j+s` ## NeoDash 2.4.9 This release adds some minor changes to documentation and implements some community contributions. - Added notice about project evolution: [#967](https://github.com/neo4j-labs/neodash/pull/967) - Added community contributions and bug fixes: [#967](https://github.com/neo4j-labs/neodash/pull/967) [#894](https://github.com/neo4j-labs/neodash/pull/894) [#822](https://github.com/neo4j-labs/neodash/pull/822) [#951](https://github.com/neo4j-labs/neodash/pull/951) [#946](https://github.com/neo4j-labs/neodash/pull/946) [#944](https://github.com/neo4j-labs/neodash/pull/944) [#943](https://github.com/neo4j-labs/neodash/pull/943) [#938](https://github.com/neo4j-labs/neodash/pull/938) [#935](https://github.com/neo4j-labs/neodash/pull/935) [#918](https://github.com/neo4j-labs/neodash/pull/918) [#908](https://github.com/neo4j-labs/neodash/pull/908) [#906](https://github.com/neo4j-labs/neodash/pull/906) [#902](https://github.com/neo4j-labs/neodash/pull/902) [#895](https://github.com/neo4j-labs/neodash/pull/895) [#893](https://github.com/neo4j-labs/neodash/pull/893) ## NeoDash 2.4.8 This is a minor release containing an important fix and other minor fixes: - Fixed a bug where loading a dashboard would reset parameters to null ([887](https://github.com/neo4j-labs/neodash/pull/887)). - Fix relationship width parameter for Graph report ([889](https://github.com/neo4j-labs/neodash/pull/889)). Thanks to all the contributors for this release: - [alfredorubin96](https://github.com/alfredorubin96), - [nielsdejong](https://github.com/nielsdejong). ## NeoDash 2.4.7 This is a minor release containing a few critical fixes and general code quality improvements: - Fix multiple parameter select ([881](https://github.com/neo4j-labs/neodash/pull/881)). - Fix parameter casting error when loading dashboards([874](https://github.com/neo4j-labs/neodash/pull/874)). - Fix the fraud demo in the [Example Gallery](https://neodash-gallery.graphapp.io/). Thanks to all the contributors for this release: - [alfredorubin96](https://github.com/alfredorubin96), - [MariusC](https://github.com/mariusconjeaud), - [elizarp](https://github.com/elizarp). ## NeoDash 2.4.6 This is a minor release containing a few critical fixes and some extra style customizations: - Fix bad text wrapping for arrays in tables ([868](https://github.com/neo4j-labs/neodash/pull/868)). - Make wrapping in table optional, disabled by default ([872](https://github.com/neo4j-labs/neodash/pull/872)). - Fixed issues where cross database dashboard sharing always reverted back to the default database ([873](https://github.com/neo4j-labs/neodash/pull/873)). - Added option to define style config using environment variables for the Docker image ([876](https://github.com/neo4j-labs/neodash/pull/876)). ## NeoDash 2.4.5 This is a small release containing a few fixes: - Fixed rendering of string arrays inside tables, report titles, and report action buttons [849](https://github.com/neo4j-labs/neodash/pull/849) - Allowed text to wrap in tables, preserving the number of rows [852](https://github.com/neo4j-labs/neodash/pull/852) - 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) - Updated role selector menu, and made user updates more robust [854](https://github.com/neo4j-labs/neodash/pull/854) Thanks to all the contributors for this release: - [MariusC](https://github.com/mariusconjeaud), - [LiamEdwardsLamarche](https://github.com/LiamEdwardsLamarche), - [AleSim94](https://github.com/AleSim94) ## NeoDash 2.4.4 This is a hotfix release fixing some breaking issues in the 2.4.3: - Fixed number parsing using newer versions of the Neo4j driver. [811](https://github.com/neo4j-labs/neodash/pull/811) - Reverted new connection handler for auto-renewed SSO sessions. [815](https://github.com/neo4j-labs/neodash/pull/815) - Improved handling of parameters in form extension, resolved local state issues. [813](https://github.com/neo4j-labs/neodash/pull/813) - 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) If you are currently using NeoDash version 2.4.3, we recommend updating as soon as possible. ## NeoDash 2.4.3 This release contains several improvements and additions to multi-dashboard management, as well as a bug fixes and a variety of quality-of-life improvements: Dashboard management and access control: - Added a UI for handling dashboard access using RBAC, as well as a new extension to simply access control. - Added button to sidebar to refresh the list of dashboards saved in the database. - Improved handling and detection of draft dashboards in the dashboard sidebar. Other improvements: - Changed CSV export functionality for tables to use UTF-8 format. - Various improvements / fixes to the documentation to include new images, and up-to-date functionality. - Added logic for handling refresh tokens when connected to NeoDash via SSO. - Incorporated tooltips for bar charts with and without custom labels. Bug fixes and testing: - Implemented bug fixes on type casting for numeric parameter selectors. - Fixed issue with report actions not functioning properly on node click events. - Extended test suite with Cypress tests for advanced settings in the bar chart. Thanks to all the contributors for this release: - [OskarDamkjaer](https://github.com/OskarDamkjaer) - [alfredorubin96](https://github.com/alfredorubin96), - [AleSim94](https://github.com/AleSim94), - [BennuFire](https://github.com/BennuFire), - [jacobbleakley-neo4j](https://github.com/jacobbleakley-neo4j), - [josepmonclus](https://github.com/josepmonclus) - [nielsdejong](https://github.com/nielsdejong) ## NeoDash 2.4.2 This is a release with a large amount of quality of life improvements, as well as some new features: - Visualize graphs in 3D with the new 3D graph report. [#737](https://github.com/neo4j-labs/neodash/pull/737) - Improved dashboard management sidebar and handling of drafts. [#734](https://github.com/neo4j-labs/neodash/pull/734) - Added parameter select setting for autopopulating first selector value. [#746](https://github.com/neo4j-labs/neodash/pull/746) - Improved UX for editing page names & dashboard titles. [#743](https://github.com/neo4j-labs/neodash/pull/743) - Unified common settings for each report type. [#724](https://github.com/neo4j-labs/neodash/pull/724) - 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) - Fixed issue where invisible table columns were not handled correctly. [#695](https://github.com/neo4j-labs/neodash/pull/695) - Miscellaneous bug fixes, style improvements & stability fixes. [#744](https://github.com/neo4j-labs/neodash/pull/744) ## NeoDash 2.4.1 This 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. Included: - Improvements to customizability of the bar chart (styling, legend customization, report actions). [#689](https://github.com/neo4j-labs/neodash/pull/689) - Improved dashboard settings interface, fixed alignment for table download button. [#729](https://github.com/neo4j-labs/neodash/pull/729) - Adjusted ordering of suggested labels/properties for parameter selectors. [#728](https://github.com/neo4j-labs/neodash/pull/728) - Better handling of date parameters when saving/loading dashboards. [#727](https://github.com/neo4j-labs/neodash/pull/727) - Fixed incorrect z-index issue for form creation modals. [#726](https://github.com/neo4j-labs/neodash/pull/726) - Adjusted filtering tooltip on tables to avoid hiding result data. [#712](https://github.com/neo4j-labs/neodash/pull/712) - Fixed uncontrolled component issue for dashboard import modal. [#711](https://github.com/neo4j-labs/neodash/pull/711) - Adjusted font color of graph context popups to use theme colors. [#699](https://github.com/neo4j-labs/neodash/pull/699) - Adjust sidebar database selector to only show active databases. [#698](https://github.com/neo4j-labs/neodash/pull/698) - Incorporated logging functionality for self-hosted NeoDash deployments. [#705](https://github.com/neo4j-labs/neodash/pull/705) - Improved dashboard management in standalone-mode deployments. [#705](https://github.com/neo4j-labs/neodash/pull/705) - Added Docker parameter for overriding the app's logo & custom header. [#705](https://github.com/neo4j-labs/neodash/pull/705) - 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) - Docker: Updated Alpine base image to mitigate CVE-2023-38039 & CVE-2023-4863. [#705](https://github.com/neo4j-labs/neodash/pull/705) ## NeoDash 2.4.0 NeoDash 2.4 is out! 🎂 This release packs a ton of new features, as well as improvements to the existing visualizations. Key new features: - A new sidebar with support for managing, save and load multiple dashboards directly from the UI. [#657](https://github.com/neo4j-labs/neodash/pull/657) - 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) - Added a new advanced visualization type: Gantt charts. [#684](https://github.com/neo4j-labs/neodash/pull/684) - Doubled the grid resolution for dashboards, giving you more freedom to arrange visualizations. [#682](https://github.com/neo4j-labs/neodash/pull/682) - Several improvements for the natural language queries extension - including customizable prompting, and faster schema retrieval. [#600](https://github.com/neo4j-labs/neodash/pull/600) Other improvements: - Support for multiselect checkboxes as a report action for tables. [#688](https://github.com/neo4j-labs/neodash/pull/688/commits) - Added keyboard shortcuts (CMD/CTRL+Enter) for running Cypher queries from the editor. [#694](https://github.com/neo4j-labs/neodash/pull/694/) - Added new experimental graph layouts (trees in various directions), with customizable level distance. [#690](https://github.com/neo4j-labs/neodash/pull/690) - Increased customizability for the Pie chart's styling. [#638](https://github.com/neo4j-labs/neodash/pull/638/) - Fixed issues with parameter selector: Better handling of integer / long parameters and processing external updates. [#641](https://github.com/neo4j-labs/neodash/pull/641/) - Improvements on text readability for the experimental dark mode. [#668](https://github.com/neo4j-labs/neodash/pull/668/) - UX improvements on database connection interface. [#675](https://github.com/neo4j-labs/neodash/pull/675/) - Added option to provide a custom message when no data is returned by a report. [#683](https://github.com/neo4j-labs/neodash/pull/683/) - Fixed issue where column names were not hidden correctly. [#685](https://github.com/neo4j-labs/neodash/pull/685/commits) Thanks to all the contributors for this release: [alfredorubin96](https://github.com/alfredorubin96), [AleSim94](https://github.com/AleSim94), [BennuFire](https://github.com/BennuFire), [jacobbleakley-neo4j](https://github.com/jacobbleakley-neo4j), [hugorplobo](https://github.com/hugorplobo), [brahmprakashMishra](https://github.com/brahmprakashMishra), [m-o-n-i-s-h](https://github.com/m-o-n-i-s-h), [JonanOribe](https://github.com/JonanOribe), [nielsdejong](https://github.com/nielsdejong) ## NeoDash 2.3.5 This is a bugfix / stability release directly following 2.3.4. Improvements: - Fixed issue where orphan relationships prevented graph charts from working ([@BennuFire](https://github.com/BennuFire), [#586](https://github.com/neo4j-labs/neodash/pull/586)) - 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)) - 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 - 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)) - 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)) - Dark theme tweaks. ([@BennuFire](https://github.com/BennuFire), [#585](https://github.com/neo4j-labs/neodash/pull/585)) - 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)) - 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)) - 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)) ## NeoDash 2.3.3 & 2.3.4 This is a bugfix / stability release directly following 2.3.2. Improvements: - 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)) - Migrated all icons from Material UI to Needle icons. ([@BennuFire](https://github.com/BennuFire), [#545](https://github.com/neo4j-labs/neodash/pull/571)) - Improved contrast for light and dark theme. ([@nielsdejong](https://github.com/nielsdejong), [#545](https://github.com/neo4j-labs/neodash/pull/566)) - 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)) - 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)) - 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)) - 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)) ## NeoDash 2.3.2 What'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/) ! - Key Features: - 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)) - *Experimental* Support for **Dark Mode**. - Parameter Selector Chart - 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)) - Fix delete button leading to inconsistent values on click. ([@BennuFire](https://github.com/bennufire), [#545](https://github.com/neo4j-labs/neodash/pull/545)) - Fix search on numbers not being triggered. ([@BennuFire](https://github.com/bennufire), [#545](https://github.com/neo4j-labs/neodash/pull/545)) - Others - Fix performance degradation on schema calculation ([@BennuFire](https://github.com/bennufire), [#555](https://github.com/neo4j-labs/neodash/pull/555)) - 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)) - Added Sentry Support on https://neodash.graphapp.io ([@mariusconjeaud](https://github.com/mariusconjeaud), [#546](https://github.com/neo4j-labs/neodash/pull/546)) - Fix SSO redirection on editor mode ([@BennuFire](https://github.com/bennufire), [#543](https://github.com/neo4j-labs/neodash/pull/543)) ## NeoDash 2.3.1 What'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! - Natural language queries - **Support of Azure Open AI** ([@BennuFire](https://github.com/bennufire), [#515](https://github.com/neo4j-labs/neodash/pull/515)) - Support parameters on natural language queries ([@BennuFire](https://github.com/bennufire), [#514](https://github.com/neo4j-labs/neodash/pull/514)) - Graph Visualization - 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)) - Table Chart - 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)) - Fix falsy boolean display on table ([@bastienhubert](https://github.com/bastienhubert), [#536](https://github.com/neo4j-labs/neodash/pull/536)) - Report Actions - 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)) - Others - 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)) - 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)) - 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)) ## NeoDash 2.3.0 NeoDash 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). Key features: - 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. - UI updated to use the **[Neo4j Design Language](https://www.neo4j.design/)**, giving NeoDash a similar look-and-feel to other Neo4j tools. - Customize branding, colors dynamically with a new [Style Configuration File](https://neo4j.com/labs/neodash/2.3/developer-guide/style-configuration). Other changes: - Fixed issues with date picker / free-text parameter sometimes not initializing. - Improved documentation by fixing broken links, and adding more details around complex concepts. - **Pro Extensions have evolved to open Expert Extensions.** - Fixed issue where deep-linked parameters were not set from the URL. - Added option to specify absolute width for table columns (in pixels or as percentages). - Fixed map charts to auto-cluster markers when they collide, or are too close together. - ... and dozens of other improvements! Contributors to this release: - [Alfredo Rubin](https://github.com/alfredorubin96) - [Harold Agudelo](https://github.com/BennuFire) - [Aleksandar Simeunovic](https://github.com/AleSim94) - [Marius Conjeaud](https://github.com/mariusconjeaud) - [Brahm Prakash Mishra](https://github.com/brahmprakashMishra) - [Pierre Martignon](https://github.com/pierremartignon) - [Kim Zachariassen](https://github.com/KiZach) - [Paolo Baldini](https://github.com/8Rav3n) - [Niels de Jong](https://github.com/nielsdejong/) ## NeoDash 2.2.5 This is a minor release with some small bug fixes, directly following the 2.2.4 release. - Fixed replacement rules for parameters in iFrames/Markdown reports. [#417](https://github.com/neo4j-labs/neodash/pull/417) - Added automatic header text color switch for reports with a dark background [#420](https://github.com/neo4j-labs/neodash/pull/420) - Fixed handling right click events (for graph exploration) in Neo4j Desktop [#415](https://github.com/neo4j-labs/neodash/pull/415). - Added support for unweighted Sankey charts [#419](https://github.com/neo4j-labs/neodash/pull/419) ## NeoDash 2.2.4 This 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. - Area Map - **New!** - 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)) - Assign color scale automatically based on numeric values. - Assign colors to countries based on Alpha-2 and Alpha-3 codes, and area codes by ISO 3166 code. - Interactive drilldown by clicking on regions in a country. - Graph Visualization - 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)) - 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)) - Fixed incorrect assignment of chip colors in graph visualization footer. ([@BennuFire](https://github.com/bennufire), [#296](https://github.com/neo4j-labs/neodash/issues/296)) - 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)) - 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)) - 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)) - Single Value Chart - 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)) - Choropleth Map - Added polygon information for missing countries: France, Kosovo, and others. ([@BennuFire](https://github.com/bennufire), [#357](https://github.com/neo4j-labs/neodash/issues/357)) - Parameter Selector - 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)) - 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)) - 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)) - Table Chart - Added support for customizing the seperator in csv exports. ([@nielsdejong](https://github.com/nielsdejong), [#337](https://github.com/neo4j-labs/neodash/issues/337)) - Others - 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)) - 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)) - 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)) - 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)) - Fixed hot-module replacement inside webpack configuration. ([@konsalex](https://github.com/konsalex), [#396](https://github.com/neo4j-labs/neodash/pull/396)) - 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)) - 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)) ## NeoDash 2.2.3 This releases fixes a small set of bugs that slipped through the 2.2.3 release, and adds some minor features: - Added support for scatter plots by overriding a parameter in the line chart. - Added the ability to use dashboard parameter as filters in custom parameter selector queries. - Fixed breaking bug in parameter selector settings causing a white-screen error. - Fixed auto-coloring of bar charts (resolved back to logic of 2.2.1 and earlier). - Added a quick fix for automatically resetting the parameter display value when the property display override is toggled. - Upversioned outdated dashboards and in the NeoDash Gallery. ## NeoDash 2.2.2 The NeoDash 2.2.2 release is packed with a bunch of new usability features: - Changed the built-in Cypher editor to a brand-new [CodeMirror Editor](https://github.com/neo4j-contrib/cypher-editor). - Rebuilt the **Parameter Select** component from scratch for improved stability, performance and extendability: - Added an optional setting to the parameter selector to display a different property from the one that is set by the selector. - Use this to - for example - let users choose a name and set an ID for use by other reports. - Fields no longer reset randomly when parameters are changed. - Freetext fields are no longer slow - perform as fast as the other selectors. - Add the option to use rule-based styling based on dashboard parameters. - Changed rule-based styling on bar and pie charts to override color scheme instead of clear the scheme. - Extended the [Example Gallery](https://neodash-gallery.graphapp.io/) with several new demos. - Adding intermediate report error boundaries for improved app stability. - Changed docker image name to `neo4jlabs/neodash`. - Improved documementation for developers. - Fixed inconsistent styling between different pop-up screens, and fixed report title placeholders. ## NeoDash 2.2.1 This update provides a number of usability improves over the 2.2.0 release. In addition, it entails various improvements to the codebase, including security patches on the dependencies. Table: - Column names prefixed with `__` are now hidden in the table view. Map: - Added documentation for adding a custom map provider. Parameter selector: - Added support for boolean parameters. Editor: - Parameters are now automatically replaced **inside report titles**. - Image downloads now include the report title alongside the visualization. Others: - Applied security patches for dependencies. - Set test container for release pipeline to fixed version of Neo4j. - Aligned code style / linting with Neo4j product standards. - Updated Docker setup to inject `standaloneDashboardURL` into the application config. ## NeoDash 2.2.0 This 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! NeoDash 2.2 comes with three in-built extensions. - **Rule-Based Styling** - **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). - **Report Actions**: Which let you create interactivity in dashboards, using the output of one report as input for another visualization. (Expert Extension) You can enable extensions by clicking the 🧩 icon on the left sidebar of the screen. Other changes include: - New example dashboards available in the [Dashboard Gallery](https://neodash-gallery.graphapp.io). - Customizable background colors for all report types. - Fixing a bug where the Choropleth map chart was unable to parse country-codes. ## NeoDash 2.1.10 This is a minor update which adds some operational/styling improvements, and a bug fix for line charts. Changes: - Added customizable label positions for bar charts. - Fixed bug where datetimes were not handled correctly by line charts. (https://github.com/neo4j-labs/neodash/issues/243) - Added **session parameters**, set automatically and available to Cypher queries ([Documentation](https://neo4j.com/labs/neodash/2.1/user-guide/reports/)). - Added option to restore debug reports in recovery mode. - Added option to share dashboards from self-hosted deployments. ## NeoDash 2.1.8 & 2.1.9 New features: - Added the [Dashboard Gallery](https://neodash-gallery.graphapp.io), a live gallery of example NeoDash dashboards. - Added **Gauge Charts**, a contribution of the [BlueHound](https://github.com/zeronetworks/BlueHound) fork. - Updated testing pipeline to work as an independent procedure. - Added option to select a different Neo4j database for each report. ([#188](https://github.com/neo4j-labs/neodash/issues/118)) - 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)) Bug fixes: - Fixed issue preventing dashboards to be shared with a non-standard database name. - Fixed table chart breaking when returning a property called 'id' with a null value. - Fixed bug not allowing users to select a different database when loading/saving a dashboard. - **Added error handler for database list race condition in Neo4j Desktop**. ## NeoDash 2.1.6 & 2.1.7 New features: - Added *Radar Charts/Spider Charts*. - Added optional markdown description for each report, to be displayed via the header. Extensions: - Added option to provide a custom map provider for map charts. - Added support for default values in parameter selectors. - Added documentation on deep-linking into NeoDash. - Added tick-rotation customization for line charts. - Added option to have children in the sunburst chart inherit colors from their parents. Improvements: - Rewiring of the internal query/rendering engine - resulting in far fewer query executions and a smoother UX. - Changed package manager from `npm` to `yarn`, and bumped node version to 18. Cleaned up `package.json`. - Reduced flaky behaviour in parameter selectors. - Added cycle-detection logic for sankey charts. - Fixed report documentation pop-up to open link in a new window. ## NeoDash 2.1.5 Added *New* Sankey charts: - Visualize nodes and relationships as a flow diagram. - Select a customizable flow value from relationship properties. - Configure a variety of style customizations. Parameter select: - Fixed bug where values would randomly be deleted after changing the parameter. - Added option to customize the number of suggested values when a user enters (part of) a property value. - Added option to customize search type (CONTAINS, STARTS WITH, or ENDS WITH). - Added option to enable/disable case-sensitive search. - Added option to enable/disable removing duplicate suggestions. Miscellaneous: - Extended documentation with examples on running NeoDash in Kubernetes. - Fixed issue where duplicate database names were visible when running NeoDash on an on-prem Neo4j cluster. ## NeoDash 2.1.4 Added hotfix for missing function in map visualization (https://github.com/neo4j-labs/neodash/issues/183). ## NeoDash 2.1.3 The 2.1.3 release contains updates to the map visualization, as well as a new Choropleth map report type. Several usability improvements were also added, including fixing all links into the new documentation pages. - Extended the map visualization with a heatmap mode & marker clustering. - Added a Choropleth map visualization report type. - Added support for auto-linking into a predefined database from https://tools.neo4jlabs.com/. - Added optional background color setting for reports. - Added a new 'resize mode' for page layout creation. - Added support for drawing dates on a time chart (in addition to existing datetime types). - Fixed broken links in the documentation portal, all in-app links now point to this portal as well. ## NeoDash 2.1.2 The 2.1.2 release contains some bug fixes and minor improvements to the application. Application changes: - Added button to clone (duplicate) a report inside a dashboard. - Added option to show/hide labels inside circle packing charts. - Changed dashboard layout compaction strategy to be more natural. - Fixed card headers not rendering correctly in read-only mode. - Fixed rendering issues for table columns containing null values. Operational changes: - Added support for username/password environment variables in Docker. ## NeoDash 2.1.0, NeoDash 2.1.1 The 2.1 release is a major update to the NeoDash application. Main updates: - Added new drag-and-drop dashboard layout - reports can be **moved** and **resized** freely within the dashboard. - Updated dashboard file format for new layout (2.0 dashboards are automatically migrated). - Pages can now be reordered by dragging and dropping. - Added three new hierarchical report types: - Treemaps - Sunburst Charts - Circle Packing Charts - Styling/usability improvements for pie charts. - Improved image download (screenshot functionality) for all report types. - Parameter select reports now resize the selector to fit the available space. Other changes: - Added continuous integration and deployment workflows. - Created a new [User Guide](https://github.com/neo4j-labs/neodash/wiki/User-Guide) with documentation on all report customizations is available. - Added a new [Developer Guide](https://github.com/neo4j-labs/neodash/wiki/Developer-Guide) with info on installing, building and extending the application. ## NeoDash 2.0.15 This is the final minor update before the 2.1 release. Changes: - Several stability improvements before the 2.1 release. - Updated Dockerfile to make better use of caching, and pick up environment variables at run time. - Added option to replace dashboard parameters in Markdown/iFrames to make them dynamic. - Removed unneeded index column from the CSV download for tables. - Added optional dashboard setting to enable image downloads for reports/the entire dashboard. ## NeoDash 2.0.14 Report features: - Added optional "Download as CSV" button to table reports. - Dashboard parameters can now be used in iFrames/Graph drilldown links, and they are automatically replaced when parameters get updated. - Updating a dashboard parameter now only refreshes the reports that use the parameter. Standalone mode: - Enabled deploying standalone dashboards with a direct URL to the dashboard. - Added functionality to deep link into a NeoDash dashboard with dashboard parameters (use ?neodash_variable_name=value in the URL). Miscellaneous Bug fixes and improvements: - Resolved crash caused by invalid geospatial properties in a Map visualization. - Saving a dashboard now lets users override an existing dashboard with the same name (enabled by default). - Increased the default row limits for line/bar/pie charts to 250. Added option to override the row limiter in the dashboard settings. - Updated project README file to refer to the correct port number on Docker deployments. - Enabled a configurable timeout for parameter selection reports, both a timeout for the suggestion retrieval and a timeout for updating the parameters. - Fixed dependency issues when installing the application on Windows systems. Bumped suggested npm version to 8.6. ## NeoDash 2.0.13 This is a bug fix/minor usability update. Changes: - Resolved error where the float value 0.0 was rendered as 'null' in tables. - Added alphabetical sorting to all node/relationship inspection pop-ups & parameter select reports. - Resolved bug where switching pages quickly resulting in an error message. - Resolved bug where rule-based styling would break on null values. - Replaced margin-based styling on single value reports with a vertical alignment option. ## NeoDash 2.0.12 Added **rule-based styling**: - Use the card settings to specify styling rules for tables, graphs, bar/pie/line charts and single values. - Conditional rules are evaluated on each report render in order of priority. - Rules can customize colors in tables, node colors & dynamically set the colors of components in your chart. Minor improvements: - Better handling of null values in tables. - Tweaking/reorganization of the Docker file and deployment scripts. - Renaming/restructuring of source code. ## NeoDash 2.0.8 / 2.0.9 / 2.0.10 / 2.0.11 Stability fixes to supplement 2.0.7: - Hotfix for missing config file in Neo4j Desktop causing startup issue. - Hotfix for application crashes caused by rendering custom data types in transposed table views. - Hotfix for object rendering in tables & line-chart type detection. - Fix for rendering dictionaries in tables/single value charts. - Added resize handler for fullscreen map views. - Added missing auto-run config to pie charts. - Fixed broken value scale parameter for bar charts. ## NeoDash 2.0.7 Application functionality: - Added standalone 'dashboard viewer' mode. - Added option to save/load dashboards from other Neo4j databases. Reports/Visualizations: - Fixed bug in creating line charts. - Added support for datetime axis in line charts. - Added auto-locale formatting to number values in single value / table reports. - Added unified renderer for value types. - Updated default font size for single value reports. - Added optional deep-link button for graph visualizations. - Added option to disable auto-running a report, to let users explore the query first. - Minor styling tweaks to the graph views. For Developers: - Added more documentation on extending the app. - New security-vetted docker image available on Docker hub. ## NeoDash 2.0.6 Major version updates to all internal dependencies. NeoDash 2.0.6 uses Node 17+, react 17+ and recent versions of all visualization libraries. Visualizations: - Added pie charts (Including examples and new demo dashboard). - Added setting to transpose table rows and columns. - Improved styling on graph pop-up windows. - Graph visualizations now auto-fit to the report size. - Added button to reset the zoom on a graph report. Parameter selection: - Added relationship property / free text selection options. Editor: - Improved performance of inbuilt Cypher editor. - Added button to maximize cards while in edit-mode. - All reports are now maximizable by default. - Added tiny report sizes. - Added option to override the default query timeout of twenty seconds. Other: - Updated docker image build scripts. - Fixed share link geneneration incorrectly removing capitals from usernames/passwords. ## NeoDash 2.0.5 Graph report: - Fixed node position after dragging nodes. - Added option to 'lock' graph views, storing the current positions of the nodes in the graph. - Added experimental graph layouts. Table: - Fixed bug where the report freezes for very wide tables. - Added support for rendering native/custom Neo4j types in the table. Parameter select: - Fixed issue where the dashboard crashes for slow connections. Editor: - Added button to create a debug file from the 'About' screen. ## NeoDash 2.0.4 New features: - Added option dashboard setting to let users view reports in a fullscreen pop-up. - Added inspection pop-up for graph visualizations. - Added option to manually specify node labels/property names in parameter selection reports (for large databases). - Added example of how to user map visualizations from derived properties. - Added button to return to the welcome screen. - iFrames can now take live parameter selections in the hash-part of the URL. Bug fixes: - Dashboards will now remember the active selection(s) made in parameter select reports. - Graph visualizations will no longer draw overlapping lines when a pair of nodes shares bidirectional relationships. - connection screen is now dismissable if an existing connection exists. Special thanks to @JipSogeti for their contributions to this release. ## NeoDash 2.0.3 UX improvements + bug fixes. - Parameter selection report: - fixed bug to allow for selecting properties from nodes with >5 distinct properties. - Added support for nodes and properties with spaces in their name. - Sharing: - Removed persisted URL in share links to avoid getting stuck on shared dashboards - Table: - Added option to specify relative column sizes - Graph: - Changed node styling to use the last (most specific label) for applying customizations - Fixed error where incorrect properties were extracted from graphs with multi-labeled nodes - Fixed node display to hide "undefined" when a non-existing property is selected for that node. ## NeoDash 2.0.0, 2.0.1 & 2.0.2 **New & Improved Dashboard Editor** - Added new Cypher editor with syntax highlighting / live syntax validation. - Redesigned Cypher query runner to be 2x more performant. - Easy custom styling of reports with the "advanced report settings" window. - Added in-built documentation with example queries and visualizations. - Updated dashboard layout to better use screen real estate. **Visualizations** - Table View - New table view with post-query sorting and filtering, and highlighting of native Neo4j types. - Fixed array property display in table reports. - Added automatic link generation from URL properties in the table report. - Graph View - Updated graph visualization library to a canvas-based renderer, handling 4x larger graphs. - Added custom node/relationship styling with custom colors, width, and font-size. - Better property display on graph visualization hover. - Bar/Line Chart - New bar/line chart visualizations based on the Charts graph app. - Added support for multi-line charts, stacked/grouped bar charts. - Added log scale + explicit limit setting to bar/line charts. - Line chart hover values are no longer rounded and incorrectly stacked. - Map View - Added custom styling options to map visualizations. - Added dictionary-based point property rendering on maps. - Stability improvement of map views for offline deployments. - Single Value Report - Improved single value report. - Custom styling (text alignment) of single value reports. - Property Selection: - Improved property selection documentation. - Added optional "clear parameter" setting to parameter selection report. - property selector now uses the filter to gather more results. **Saving, loading and sharing** - Added setting to turn entire dashboard into 'Standalone mode' from a share link. - Added option to save/load dashboards from both files and text. - New "Try a demo" button on the welcome screen. - added save/load to Neo4j database feature. - Auto-convert older versions of NeoDash on load. ================================================ FILE: compose.yaml ================================================ services: neodash: build: context: . dockerfile: Dockerfile ports: - "5005:5005" environment: - NGINX_PORT=5005 ================================================ FILE: conf/default.conf.template ================================================ server { listen ${NGINX_PORT}; server_name localhost; include mime.types; location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; index index.html index.htm; } # redirect server error pages to the static page /50x.html # Note: This is optional, depending on the implementation in React error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } ================================================ FILE: cypress/Page.js ================================================ const DB_URL = 'localhost'; const DB_USERNAME = 'neo4j'; const DB_PASSWORD = 'test1234'; export class Page { constructor(cardSelector) { this.cardSelector = cardSelector; } init() { cy.viewport(1920, 1080); cy.visit('/', { onBeforeLoad(win) { win.localStorage.clear(); }, }); return this; } createNewDashboard() { cy.get('#form-dialog-title').then(($div) => { const text = $div.text(); if (text == 'NeoDash - Neo4j Dashboard Builder') { cy.wait(100); // Create new dashboard cy.contains('New Dashboard').click(); } }); return this; } connectToNeo4j() { cy.get('#form-dialog-title', { timeout: 20000 }).should('contain', 'Connect to Neo4j'); cy.get('#protocol').type('neo4j{enter}'); cy.get('#url').clear().type(DB_URL); cy.get('#dbusername').clear().type(DB_USERNAME); cy.get('#dbpassword').type(DB_PASSWORD); cy.get('button').contains('Connect').click(); cy.wait(100); return this; } enableReportActions() { cy.get('main button[aria-label="Extensions').should('be.visible').click(); cy.get('#checkbox-actions').scrollIntoView(); cy.get('#checkbox-actions').should('be.visible').click(); cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click(); cy.wait(100); return this; } enableAdvancedVisualizations() { cy.get('main button[aria-label="Extensions').should('be.visible').click(); cy.get('#checkbox-advanced-charts').should('be.visible').click(); cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click(); cy.wait(100); return this; } enableFormsExtension() { cy.get('main button[aria-label="Extensions').should('be.visible').click(); cy.get('#checkbox-forms').scrollIntoView(); cy.get('#checkbox-forms').should('be.visible').click(); cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click(); cy.wait(100); return this; } selectReportOfType(type) { cy.get('main .react-grid-item button[aria-label="add report"]').should('be.visible').click(); cy.get('main .react-grid-item') .contains('No query specified.') .parentsUntil('.react-grid-item') .find('button[aria-label="settings"]', { timeout: 2000 }) .should('be.visible') .click(); cy.get(`${this.cardSelector} #type`, { timeout: 2000 }).should('be.visible').click(); cy.contains(type).click(); cy.wait(100); return this; } createReportOfType(type, query, fast = false, run = true) { this.selectReportOfType(type); if (fast) { cy.get(`${this.cardSelector} .ReactCodeMirror`).type(query, { delay: 1, parseSpecialCharSequences: false, }); } else { cy.get(`${this.cardSelector} .ReactCodeMirror`).type(query, { parseSpecialCharSequences: false }); } cy.wait(400); if (run) { this.closeSettings(); } cy.wait(100); return this; } openSettings() { cy.get(this.cardSelector).find('button[aria-label="settings"]', { WAITING_TIME: 2000 }).click(); cy.wait(100); return this; } closeSettings() { cy.get(`${this.cardSelector} button[aria-label="run"]`).click(); cy.wait(100); return this; } openAdvancedSettings() { this.openSettings(); cy.get(this.cardSelector).contains('Advanced settings').click(); cy.wait(100); return this; } closeAdvancedSettings() { cy.get(this.cardSelector).contains('Advanced settings').click(); this.closeSettings(); return this; } openReportActionsMenu() { this.openSettings(); cy.get(this.cardSelector).find('button[aria-label="custom actions"]').click(); cy.wait(100); return this; } updateDropdownAdvancedSetting(settingLabel, targetValue) { this.openAdvancedSettings(); cy.get(`${this.cardSelector} .ndl-dropdown`).contains(settingLabel).siblings('div').click(); cy.contains(targetValue).click(); this.closeAdvancedSettings(); return this; } updateChartQuery(query) { this.openSettings(); cy.get(this.cardSelector) .find('.ndl-cypher-editor div[role="textbox"]') .should('be.visible') .click() .clear() .type(query); cy.wait(100); this.closeSettings(); return this; } } ================================================ FILE: cypress/e2e/charts/array.cy.js ================================================ import { stringArrayCypherQuery, intArrayCypherQuery, pathArrayCypherQuery } from '../../fixtures/cypher_queries'; import { Page } from '../../Page'; const CARD_SELECTOR = 'main .react-grid-item:eq(2)'; const page = new Page(CARD_SELECTOR); // Ignore warnings that may appear when using the Cypress dev server Cypress.on('uncaught:exception', (err, runnable) => { console.log(err, runnable); return false; }); describe('Testing array rendering', () => { beforeEach('open neodash', () => { page.init().createNewDashboard().connectToNeo4j(); cy.wait(100); }); it('creates a table that contains string arrays', () => { cy.checkInitialState(); page.enableReportActions(); page.createReportOfType('Table', stringArrayCypherQuery, true, true); // Standard array, displays strings joined with comma and whitespace cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', 'initial, list'); cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', 'other, list'); // Now, transpose the table page.updateDropdownAdvancedSetting('Transpose Rows & Columns', 'on'); cy.get(`${CARD_SELECTOR} .MuiDataGrid-columnHeaderTitle:eq(1)`).should('have.text', 'initial,list'); cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', 'other, list'); // Transpose back // And add a report action page.updateDropdownAdvancedSetting('Transpose Rows & Columns', 'off'); page.openReportActionsMenu(); cy.get('.ndl-modal').find('button[aria-label="add"]').click(); cy.get('.ndl-modal').find('input:eq(2)').type('column'); cy.get('.ndl-modal').find('input:eq(5)').type('test_param'); cy.get('.ndl-modal').find('input:eq(6)').type('column'); cy.get('.ndl-modal').find('button').contains('Save').click(); page.closeSettings(); cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`) .find('button') .should('be.visible') .should('have.text', 'initial, list') .click(); // Previous step's click set a parameter from the array // Test that parameter rendering works cy.get(`${CARD_SELECTOR} .MuiCardHeader-root`).find('input').type('$neodash_test_param').blur(); cy.get(`${CARD_SELECTOR} .MuiCardHeader-root`).find('input').should('have.value', 'initial, list'); }); it('creates a table that contains int arrays', () => { cy.checkInitialState(); page.createReportOfType('Table', intArrayCypherQuery, true, true); // Standard array, displays strings joined with comma and whitespace cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', '1, 2'); cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', '3, 4'); // Now, transpose the table page.updateDropdownAdvancedSetting('Transpose Rows & Columns', 'on'); cy.get(`${CARD_SELECTOR} .MuiDataGrid-columnHeaderTitle:eq(1)`).should('have.text', '1,2'); cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', '3, 4'); }); it('creates a table that contains nodes and rels', () => { cy.checkInitialState(); page.createReportOfType('Table', pathArrayCypherQuery, true, true); // Standard array, displays a path with two nodes and a relationship cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', 'PersonACTED_INMovie'); cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button`).should('have.length', 2); cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button:eq(0)`).should('have.text', 'Person'); cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button:eq(1)`).should('have.text', 'Movie'); cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) .MuiChip-root`).should('have.length', 1); cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) .MuiChip-root`).should('have.text', 'ACTED_IN'); }); it('creates a single value report which is an array', () => { cy.checkInitialState(); page.createReportOfType('Single Value', stringArrayCypherQuery, true, true); cy.get(CARD_SELECTOR).should('have.text', 'initial, list'); }); it('creates a multi parameter select', () => { cy.checkInitialState(); page.selectReportOfType('Parameter Select'); cy.get('main .react-grid-item:eq(2) label[for="Selection Type"]').siblings('div').click(); // Set up the parameter select cy.contains('Node Property').click(); cy.wait(100); cy.contains('Node Label').click(); cy.contains('Node Label').siblings('div').find('input').type('Movie'); cy.wait(1000); cy.get('.MuiAutocomplete-popper').contains('Movie').click(); cy.contains('Property Name').click(); cy.contains('Property Name').siblings('div').find('input').type('title'); cy.wait(1000); cy.get('.MuiAutocomplete-popper').contains('title').click(); // Enable multiple selection page.closeSettings(); page.updateDropdownAdvancedSetting('Multiple Selection', 'on'); // Finally, select a few values in the parameter select cy.get(CARD_SELECTOR).contains('Movie title').click(); cy.get(CARD_SELECTOR).contains('Movie title').siblings('div').find('input').type('a'); cy.get('.MuiAutocomplete-popper').contains('Apollo 13').click(); cy.get(CARD_SELECTOR).contains('Movie title').siblings('div').find('input').type('t'); cy.get('.MuiAutocomplete-popper').contains('The Matrix').click(); cy.get(CARD_SELECTOR).contains('Apollo 13').should('be.visible'); cy.get(CARD_SELECTOR).contains('The Matrix').should('be.visible'); }); }); ================================================ FILE: cypress/e2e/charts/bar.cy.js ================================================ import { barChartCypherQuery } from '../../fixtures/cypher_queries'; import { Page } from '../../Page'; const CARD_SELECTOR = '.react-grid-layout:eq(0) .MuiGrid-root:eq(2)'; const page = new Page(CARD_SELECTOR); // Ignore warnings that may appear when using the Cypress dev server Cypress.on('uncaught:exception', (err, runnable) => { console.log(err, runnable); return false; }); describe('Testing bar chart', () => { beforeEach('open neodash', () => { page.init().createNewDashboard().connectToNeo4j().createReportOfType('Bar Chart', barChartCypherQuery); }); it('Checking Colour Picker settings', () => { //Opens advanced settings cy.get('.react-grid-layout') .first() .within(() => { //Finds the 2nd card cy.get('.MuiGrid-root:eq(2)').within(() => { // Access advanced settings cy.get('button').eq(1).click(); cy.get('[role="switch"]').click(); cy.wait(200); // Changing setting for colour picker cy.get('[data-testid="colorpicker-input"]').find('input').click().type('{selectall}').type('red'); cy.get('button[aria-label="run"]').click(); // Checking that colour picker was applied correctly cy.get('.card-view').should('have.css', 'background-color', 'rgb(255, 0, 0)'); cy.wait(200); // Changing colour back to white cy.get('button').eq(1).click(); cy.get('[data-testid="colorpicker-input"]').find('input').click().type('{selectall}').type('white'); cy.get('button[aria-label="run"]').click(); // Checking colour has been set back to white cy.wait(200); cy.get('.card-view').should('have.css', 'background-color', 'rgb(255, 255, 255)'); }); }); }); it('Checking Selector Description', () => { //Opens first 2nd card cy.get('.react-grid-layout:eq(0) .MuiGrid-root:eq(2)').within(() => { // Access advanced settings cy.get('button').eq(1).click(); cy.get('[role="switch"]').click(); cy.wait(200); // Changing Selector Description to 'Test' cy.get('.ndl-textarea').contains('span', 'Selector Description').click().type('Test'); cy.get('button[aria-label="run"]').click(); // Pressing Selector Description button cy.get('button[aria-label="details"]').click(); }); // Checking that Selector Description is behaving as expected cy.get('.MuiDialog-paper').should('be.visible').and('contain.text', 'Test'); cy.wait(1000); // Click elsewhere on the page to close dialog box cy.get('div[role="dialog"]').parent().click(-100, -100, { force: true }); }); it('Checking full screen bar chart setting', () => { page.updateDropdownAdvancedSetting('Fullscreen enabled', 'on'); cy.get('button[aria-label="maximize"]').click(); // Checking existence of full-screen modal cy.get('.dialog-xxl').should('be.visible'); // Action to close full-screen modal cy.get('button[aria-label="un-maximize"]').click(); // Checking that fullscreen has un-maximized // Check that the div is no longer in the DOM cy.get('div[data-focus-lock-disabled="false"]').should('not.exist'); }); it('Checking "Autorun Query" works as intended', () => { page.updateDropdownAdvancedSetting('Auto-run query', 'off'); cy.get('.MuiCardContent-root').find('.ndl-cypher-editor').should('be.visible'); cy.get('.MuiCardContent-root').find('g').should('not.exist'); cy.wait(100); cy.get('.MuiCardContent-root').find('button[aria-label="run"]').filter(':visible').click(); cy.get('g').should('exist'); }); it('Checking Legend integration works as intended', () => { page.updateDropdownAdvancedSetting('Show Legend', 'on'); // Checking that legend matches value specified: in the case - 'count' cy.get('svg g g text').last().contains(/count/i); page.updateDropdownAdvancedSetting('Show Legend', 'off'); cy.get('svg g g text').last().contains(/count/i).should('not.exist'); }); it('Checking the stacked grouping function works as intended', () => { const TRANSLATE_REGEXP = /translate\(([0-9]{1,3}), [0-9]{1,3}\)/; page .updateChartQuery( 'MATCH (p:Person)-[:DIRECTED]->(n:Movie) RETURN n.released AS released, p.name AS Director, count(n.title) AS count LIMIT 5' ) .updateDropdownAdvancedSetting('Grouping', 'on'); cy.get('.MuiGrid-root:eq(2)') .find('.ndl-dropdown:contains("Group")') .find('svg') .parent() .click() .type('Director{enter}'); // Checking that the groups are stacked cy.get('.MuiGrid-root:eq(2)') .find('g') .children('g') .eq(3) // Get the fourth g element (index starts from 0) .invoke('attr', 'transform') .then((transformValue) => { // 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 // if transformValue is translate(100,200), then it will produce an array like ["translate(100,200)", "100"], const match = transformValue.match(TRANSLATE_REGEXP); if (match?.[1]) { const xValue = match[1]; // Now find sibling g elements with the same x transform value cy.get('.MuiCardContent-root') .find('g') .children('g') .filter((_, element) => { const siblingTransform = Cypress.$(element).attr('transform'); return siblingTransform?.includes(`translate(${xValue},`); }) .should('have.length', 3); // Check that there's at least one element } else { throw new Error('Transform attribute not found or invalid format'); } }); cy.get('.ndl-dropdown:contains("Group")').find('svg').parent().click().type('(none){enter}'); //Checking that the stacked grouped elements do not exist cy.get('.MuiCardContent-root') .find('g') .children('g') .eq(3) // Get the fourth g element (index starts from 0) .invoke('attr', 'transform') .then((transformValue) => { // 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 // if transformValue is translate(100,200), then it will produce an array like ["translate(100,200)", "100"], const match = transformValue.match(TRANSLATE_REGEXP); if (match?.[1]) { const xValue = match[1]; // Now find sibling g elements with the same x transform value cy.get('.MuiCardContent-root') .find('g') .children('g') .filter((_, element) => { const siblingTransform = Cypress.$(element).attr('transform'); return siblingTransform?.includes(`translate(${xValue},`); }) .should('have.length', 1); // Check that there are no matching elements } else { throw new Error('Transform attribute not found or invalid format'); } }); }); // How to properly test this? it.skip('Testing grouped grouping mode', () => { page .updateChartQuery( 'MATCH (p:Person)-[:DIRECTED]->(n:Movie) RETURN n.released AS released, p.name AS Director, count(n.title) AS count LIMIT 5' ) .updateDropdownAdvancedSetting('Grouping', 'on') .updateDropdownAdvancedSetting('Group Mode', 'grouped'); cy.get('.ndl-dropdown:contains("Group")').find('svg').parent().click().type('Director{enter}'); }); it('Testing "Show Value on Bars"', () => { page.updateDropdownAdvancedSetting('Show Values On Bars', 'on'); cy.get('.react-grid-layout:eq(0) .MuiGrid-root:eq(2)').find('div svg > g > g > text').should('have.length', 5); page.updateDropdownAdvancedSetting('Show Values On Bars', 'off'); cy.get('.react-grid-layout:eq(0) .MuiGrid-root:eq(2)').find('div svg > g > g > text').should('not.exist'); }); describe('Y axis display', () => { it('Checking Y axis is displayed', () => { page.updateDropdownAdvancedSetting('Display Y axis', 'on'); cy.get('.MuiCardContent-root svg > g > g:nth-child(3)') .invoke('attr', 'transform') .should('eq', 'translate(0,0)'); }); it('Checking Y axis is hidden', () => { page.updateDropdownAdvancedSetting('Display Y axis', 'off'); cy.get('.MuiCardContent-root svg > g > g:nth-child(3)') .invoke('attr', 'transform') .should('not.eq', 'translate(0,0)'); }); }); describe('Y grid lines display', () => { it('Checking Y grid lines are displayed', () => { page.updateDropdownAdvancedSetting('Display Y grid lines', 'on'); cy.get('.MuiCardContent-root svg g > g > line').invoke('attr', 'stroke').should('eq', '#dddddd'); }); it('Checking Y grid lines are hidden', () => { page.updateDropdownAdvancedSetting('Display Y grid lines', 'off'); cy.get('.MuiCardContent-root svg g > g > line').invoke('attr', 'stroke').should('not.eq', '#dddddd'); }); }); }); ================================================ FILE: cypress/e2e/charts/table.cy.js ================================================ import { tableCypherQuery } from '../../fixtures/cypher_queries'; import { Page } from '../../Page'; const page = new Page(); // Ignore warnings that may appear when using the Cypress dev server Cypress.on('uncaught:exception', (err, runnable) => { console.log(err, runnable); return false; }); describe('Testing table', () => { beforeEach('open neodash', () => { page.init().createNewDashboard().connectToNeo4j(); cy.wait(100); }); it.skip('create a table', () => { //Opens the div containing all report cards cy.get('.react-grid-layout:eq(0)') .first() .within(() => { //Finds the 2nd card cy.get('.MuiGrid-root') .eq(1) .within(() => { //Clicks the 2nd button (opens settings) cy.get('button').eq(1).click(); // cy.get('div[role="textbox"') }); }); cy.get('.react-grid-layout') .first() .within(() => { //Finds the 2nd card cy.get('.MuiGrid-root') .eq(1) .within(() => { //Opens the drop down cy.getDataTest('type-dropdown').click(); }); }); // Selects the Table option cy.get('[id^="react-select-5-option"]').contains(/Table/).should('be.visible').click({ force: true }); cy.get('.react-grid-layout .MuiGrid-root:eq(1) #type input[name="Type"]').should('have.value', 'Table'); //Removes text in cypher editor and types new query cy.get('.react-grid-layout') .first() .within(() => { //Finds the 2nd card cy.get('.MuiGrid-root') .eq(1) .within(() => { //Replaces default query with new query cy.get('.ndl-cypher-editor div[role="textbox"]').clear().type(tableCypherQuery); cy.get('button[aria-label="run"]').click(); }); }); }); }); ================================================ FILE: cypress/e2e/start_page.cy.js ================================================ import { tableCypherQuery, barChartCypherQuery, mapChartCypherQuery, sunburstChartCypherQuery, iFrameText, markdownText, loadDashboardURL, sankeyChartCypherQuery, gaugeChartCypherQuery, formCypherQuery, } from '../fixtures/cypher_queries'; import { Page } from '../Page'; const CARD_SELECTOR = 'main .react-grid-item:eq(2)'; const page = new Page(CARD_SELECTOR); // Ignore warnings that may appear when using the Cypress dev server Cypress.on('uncaught:exception', (err, runnable) => { console.log(err, runnable); return false; }); describe('NeoDash E2E Tests', () => { beforeEach(() => { page.init().createNewDashboard().connectToNeo4j(); cy.wait(100); }); it('initializes the dashboard', () => { cy.checkInitialState(); }); it('creates a new card', () => { cy.checkInitialState(); cy.createCard(); }); // Test each type of card it('creates a table report', () => { cy.checkInitialState(); cy.get('main .react-grid-item button[aria-label="add report"]').should('be.visible').click(); cy.get('main .react-grid-item') .contains('No query specified.') .parentsUntil('.react-grid-item') .find('button[aria-label="settings"]', { timeout: 2000 }) .should('be.visible') .click(); cy.get('main .react-grid-item:eq(2) #type input[name="Type"]').should('have.value', 'Table'); cy.get('main .react-grid-item:eq(2) .ReactCodeMirror').type(tableCypherQuery); cy.wait(400); cy.get('main .react-grid-item:eq(2)').contains('Advanced settings').click(); cy.get('main .react-grid-item:eq(2) button[aria-label="run"]').click(); cy.get('main .react-grid-item:eq(2) .MuiDataGrid-columnHeaders') .should('contain', 'title') .and('contain', 'released') .and('not.contain', '__id'); // cy.get('main .react-grid-item:eq(2) .MuiDataGrid-virtualScroller .MuiDataGrid-row').should('have.length', 8); // cy.get('main .react-grid-item:eq(2) .MuiDataGrid-footerContainer').should('contain', '1–8 of 8'); // cy.get('main .react-grid-item:eq(2) .MuiDataGrid-footerContainer button[aria-label="Go to next page"]').click(); // cy.get('main .react-grid-item:eq(2) .MuiDataGrid-virtualScroller .MuiDataGrid-row').should('have.length', 3); // cy.get('main .react-grid-item:eq(2) .MuiDataGrid-footerContainer').should('contain', '6–8 of 8'); }); it('creates a bar chart report', () => { cy.checkInitialState(); page.createReportOfType('Bar Chart', barChartCypherQuery); cy.get('main .react-grid-item:eq(2) #index input[name="Category"]').should('have.value', 'released'); cy.get('main .react-grid-item:eq(2) #value input[name="Value"]').should('have.value', 'count'); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g').should('have.length', 8); }); it('creates a pie chart report', () => { cy.checkInitialState(); page.createReportOfType('Pie Chart', barChartCypherQuery); cy.get('main .react-grid-item:eq(2) #index input[name="Category"]').should('have.value', 'released'); cy.get('main .react-grid-item:eq(2) #value input[name="Value"]').should('have.value', 'count'); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g').should('have.length', 3); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g > path').should('have.length', 5); }); it('creates a line chart report', () => { cy.checkInitialState(); page.createReportOfType('Line Chart', barChartCypherQuery); cy.get('main .react-grid-item:eq(2) #x input[name="X-value"]').should('have.value', 'released'); cy.get('main .react-grid-item:eq(2) #value input[name="Y-value"]').should('have.value', 'count'); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g').should('have.length', 6); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g:nth-child(2) > line').should( 'have.length', 11 ); }); it('creates a map chart report', () => { cy.checkInitialState(); page.createReportOfType('Map', mapChartCypherQuery, true); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > path').should('have.length', 5); }); it('creates a single value report', () => { cy.checkInitialState(); page.createReportOfType('Single Value', barChartCypherQuery); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root > div > div:nth-child(2) > span') .invoke('text') .then((text) => { expect(text).to.be.oneOf(['1999', '1,999', '1 999']); }); }); it.skip('creates a gauge chart report', () => { page.enableAdvancedVisualizations(); cy.checkInitialState(); page.createReportOfType('Gauge Chart', gaugeChartCypherQuery); cy.get('.text-group > text').contains('69'); }); it('creates a sunburst chart report', () => { page.enableAdvancedVisualizations(); cy.checkInitialState(); page.createReportOfType('Sunburst Chart', sunburstChartCypherQuery); cy.get('main .react-grid-item:eq(2) #index input[name="Path"]').should('have.value', 'x.path'); cy.get('main .react-grid-item:eq(2) #value input[name="Value"]').should('have.value', 'x.value'); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g:nth-child(1) > path').should('have.length', 5); }); it('creates a circle packing report', () => { page.enableAdvancedVisualizations(); cy.checkInitialState(); page.createReportOfType('Circle Packing', sunburstChartCypherQuery); cy.get('main .react-grid-item:eq(2) #index input[name="Path"]').should('have.value', 'x.path'); cy.get('main .react-grid-item:eq(2) #value input[name="Value"]').should('have.value', 'x.value'); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > circle').should('have.length', 6); }); it('creates a tree map report', () => { page.enableAdvancedVisualizations(); cy.checkInitialState(); page.createReportOfType('Treemap', sunburstChartCypherQuery); cy.get('main .react-grid-item:eq(2) #index input[name="Path"]').should('have.value', 'x.path'); cy.get('main .react-grid-item:eq(2) #value input[name="Value"]').should('have.value', 'x.value'); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > g').should('have.length', 6); }); it('creates a sankey chart report', () => { page.enableAdvancedVisualizations(); cy.checkInitialState(); page.createReportOfType('Sankey Chart', sankeyChartCypherQuery, true); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root svg > g > path').should('have.attr', 'fill-opacity', 0.5); }); it('creates a raw json report', () => { cy.checkInitialState(); page.createReportOfType('Raw JSON', barChartCypherQuery); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root textarea:nth-child(1)', { timeout: 45000 }).should( ($div) => { const text = $div.text(); expect(text.length).to.eq(1387); } ); }); it('creates a parameter select report', () => { cy.checkInitialState(); page.selectReportOfType('Parameter Select'); cy.wait(500); cy.get('#autocomplete-label-type').type('Movie'); cy.get('#autocomplete-label-type-option-0').click(); cy.wait(500); cy.get('#autocomplete-property').type('title'); cy.get('#autocomplete-property-option-0').click(); cy.get('main .react-grid-item:eq(2) button[aria-label="run"]').click(); cy.get('#autocomplete').type('The Matrix'); cy.get('#autocomplete-option-0').click(); }); it('creates an iframe report', () => { cy.checkInitialState(); page.createReportOfType('iFrame', iFrameText); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root iframe', { timeout: 45000 }).should('be.visible'); }); it('creates a markdown report', () => { cy.checkInitialState(); page.createReportOfType('Markdown', markdownText); cy.get('main .react-grid-item:eq(2) .MuiCardContent-root h1', { timeout: 45000 }).should('have.text', 'Hello'); }); it.skip('creates a form report', () => { page.enableFormsExtension(); cy.checkInitialState(); page.createReportOfType('Form', formCypherQuery, true, false); cy.get('main .react-grid-item:eq(2) .form-add-parameter').click(); cy.wait(200); cy.get('#autocomplete-label-type').type('Movie'); cy.get('#autocomplete-label-type-option-0').click(); cy.wait(200); cy.get('#autocomplete-property').type('title'); cy.get('#autocomplete-property-option-0').click(); cy.get('.ndl-dialog-close').click(); cy.get('main .react-grid-item:eq(2) button[aria-label="run"]').scrollIntoView().should('be.visible').click(); cy.wait(500); cy.get('#form-submit').should('be.disabled'); cy.get('#autocomplete').type('The Matrix'); cy.get('#autocomplete-option-0').click(); cy.get('#form-submit').should('not.be.disabled'); cy.get('#form-submit').click(); cy.wait(500); cy.get('.form-submitted-message').should('have.text', 'Form Submitted.Reset Form'); }); // Test load stress-test dashboard from file // TODO - this test is flaky, especially in GitHub actions environment. it.skip('test load dashboard from file and stress test report customizations', () => { try { const NUMBER_OF_PAGES_IN_STRESS_TEST_DASHBOARD = 5; const file = cy.request(loadDashboardURL).should((response) => { cy.get('#root .MuiDrawer-root .MuiIconButton-root:eq(2)').click(); cy.get('.MuiDialog-root .MuiPaper-root .MuiDialogContent-root textarea:eq(0)') .invoke('val', response.body) .trigger('change'); cy.get('.MuiDialog-root .MuiPaper-root .MuiDialogContent-root textarea:eq(0)').type(' '); cy.get('.MuiDialog-root .MuiDialogContent-root .MuiButtonBase-root:eq(2)').click(); cy.wait(2500); // Click on each page and wait ~3 seconds for it to load completely for (let i = 1; i < NUMBER_OF_PAGES_IN_STRESS_TEST_DASHBOARD; i++) { cy.get('.MuiAppBar-root .react-grid-item:eq(' + i + ')').click(); cy.wait(3000); } }); } catch (e) { console.log('Unable to fetch test dashboard. Skipping test.'); } }); }); ================================================ FILE: cypress/fixtures/cypher_queries.js ================================================ // Cypher queries - for component testing export const defaultCypherQuery = 'MATCH (n) RETURN n LIMIT 25'; export const tableCypherQuery = 'MATCH (n:Movie) RETURN n.title AS title, n.released AS released, id(n) AS __id LIMIT 8'; export const barChartCypherQuery = 'MATCH (n:Movie) RETURN n.released AS released, count(n.title) AS count LIMIT 5'; export const mapChartCypherQuery = "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" + "{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" + "{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" + "{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//"; export const sunburstChartCypherQuery = "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"; export const sankeyChartCypherQuery = "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"; export const gaugeChartCypherQuery = 'RETURN 69'; export const formCypherQuery = 'MATCH (n:Movie) WHERE n.title = $neodash_movie_title SET n.rating = 92'; // Cypher queries - for renderer testing export const stringArrayCypherQuery = "RETURN ['initial', 'list'] AS column, ['other', 'list'] AS otherColumn"; export const intArrayCypherQuery = 'RETURN [1, 2] AS column, [3, 4] AS otherColumn'; export const pathArrayCypherQuery = 'MATCH p=(:Person)-[:ACTED_IN]->(:Movie) WITH p LIMIT 1 RETURN p'; // Other content fixtures export const iFrameText = 'https://www.wikipedia.org/'; export const markdownText = '# Hello'; export const loadDashboardURL = 'https://gist.githubusercontent.com/nielsdejong/ee33245256b471f92901ca4073b16ec1/raw/cfaae47e0fcdf430a5de6d0d8e3ac13cfd97742e/dashboard-cypress.json'; ================================================ FILE: cypress/index.js ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** // Import commands.js using ES2015 syntax: import './support/commands'; // Alternatively you can use CommonJS syntax: // require('./commands') ================================================ FILE: cypress/plugins/index.js ================================================ module.exports = (on, config) => { require('@cypress/code-coverage/task')(on, config); //Used to instrument code ran like unit tests on('file:preprocessor', require('@cypress/code-coverage/use-babelrc')); on('before:browser:launch', (browser, launchOptions) => { if (browser.family === 'chromium') { console.log('Adding Chrome flag: --disable-dev-shm-usage'); launchOptions.args.push('--disable-dev-shm-usage'); } return launchOptions; }); return config; }; ================================================ FILE: cypress/support/commands.js ================================================ // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite // existing commands. // // For more comprehensive examples of custom // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** // // // -- This is a parent command -- // Cypress.Commands.add('login', (email, password) => { ... }) // // // -- This is a child command -- // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) // // // -- This is a dual command -- // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) // // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) Cypress.Commands.add('getDataTest', (dataTestSelector) => { return cy.get(`[data-test="${dataTestSelector}"]`); }); /** * Function to interact with a specific element and execute additional custom commands. * @param {Function} customAction - A callback function containing custom Cypress commands. */ //Used in start_page.cy.js Cypress.Commands.add('checkInitialState', () => { // Check the starter cards cy.get('main .react-grid-item:eq(0)').should('contain', 'This is your first dashboard!'); cy.get('main .react-grid-item:eq(1) .force-graph-container canvas').should('be.visible'); cy.get('main .react-grid-item:eq(2) button').should('have.attr', 'aria-label', 'add report'); }); // Creates a card const WAITING_TIME = 20000; Cypress.Commands.add('createCard', () => { // Check the starter cards cy.get('main .react-grid-item button[aria-label="add report"]', { timeout: WAITING_TIME }) .should('be.visible') .click(); cy.wait(1000); cy.get('main .react-grid-item:eq(2)').should('contain', 'No query specified.'); }); ================================================ FILE: cypress/support/e2e.ts ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** // Import commands.js using ES2015 syntax: import './commands'; // Alternatively you can use CommonJS syntax: // require('./commands') import '@cypress/code-coverage/support'; ================================================ FILE: cypress.config.ts ================================================ /* eslint @typescript-eslint/no-var-requires: "off" */ import { defineConfig } from 'cypress'; export default defineConfig({ projectId: 'a8nh14', video: false, e2e: { defaultCommandTimeout: 20000, experimentalMemoryManagement: true, numTestsKeptInMemory: 0, baseUrl: 'http://localhost:3000', setupNodeEvents(on, config) { return require('./cypress/plugins/index.js')(on, config); }, retries: { runMode: 2, openMode: 2, }, }, env: { codeCoverage: { exclude: ['cypress/**/*.*'], }, }, }); ================================================ FILE: docs/README.md ================================================ # NeoDash Documentation This folder contains the documentation for the NeoDash project. The pages are written in AsciiDoc, and generated into webpages by Antora. An external workflow picks up this directory, embeds it into the Neo4j docs, and makes sure generated files are automatically deployed to: ``` https://neo4j.com/labs/neodash/{version} ``` For example: https://neo4j.com/labs/neodash/2.4 ## Local Build To compile and view the documentation locally, navigate to this (`./docs`) folder and run: ``` yarn install yarn start ``` Then, open your browser and navigate to http://localhost:8000/. ================================================ FILE: docs/antora.yml ================================================ name: neodash version: 2.4 title: NeoDash start_page: ROOT:index.adoc nav: - modules/ROOT/nav.adoc asciidoc: attributes: docs-version: 2.4 page-product: NeoDash page-type: NeoDash Manual page-canonical-root: /labs ================================================ FILE: docs/modules/ROOT/nav.adoc ================================================ * xref:index.adoc[Introduction] * xref:quickstart.adoc[Quickstart] * xref:user-guide/index.adoc[User Guide] ** xref:user-guide/dashboards.adoc[Dashboards] ** xref:user-guide/pages.adoc[Pages] ** xref:user-guide/reports/index.adoc[Reports] *** xref:user-guide/reports/table.adoc[Table] *** xref:user-guide/reports/graph.adoc[Graph] *** xref:user-guide/reports/bar-chart.adoc[Bar Chart] *** xref:user-guide/reports/pie-chart.adoc[Pie Chart] *** xref:user-guide/reports/line-chart.adoc[Line Chart] *** xref:user-guide/reports/graph3d.adoc[3D Graph] *** xref:user-guide/reports/sunburst.adoc[Sunburst] *** xref:user-guide/reports/circle-packing.adoc[Circle Packing] *** xref:user-guide/reports/choropleth.adoc[Choropleth] *** xref:user-guide/reports/areamap.adoc[Area Map] *** xref:user-guide/reports/treemap.adoc[Treemap] *** xref:user-guide/reports/radar.adoc[Radar Chart] *** xref:user-guide/reports/sankey.adoc[Sankey Chart] *** xref:user-guide/reports/gantt.adoc[Gantt Chart] *** xref:user-guide/reports/map.adoc[Map] *** xref:user-guide/reports/single-value.adoc[Single Value] *** xref:user-guide/reports/gauge-chart.adoc[Gauge Chart] *** xref:user-guide/reports/raw-json.adoc[Raw JSON] *** xref:user-guide/reports/parameter-select.adoc[Parameter Select] *** xref:user-guide/reports/form.adoc[Form] *** xref:user-guide/reports/iframe.adoc[iFrame] *** xref:user-guide/reports/markdown.adoc[Markdown] ** xref:user-guide/publishing.adoc[Publishing] ** xref:user-guide/bloom-integration.adoc[Bloom Integration] ** xref:user-guide/extensions/index.adoc[Extensions] *** xref:user-guide/extensions/advanced-visualizations.adoc[Advanced Visualizations] *** xref:user-guide/extensions/rule-based-styling.adoc[Rule-Based Styling] *** xref:user-guide/extensions/report-actions.adoc[Report Actions] *** xref:user-guide/extensions/natural-language-queries.adoc[Text2Cypher - Natural Language Queries] *** xref:user-guide/extensions/forms.adoc[Forms] *** xref:user-guide/extensions/access-control-management.adoc[Access Control Management] ** xref:user-guide/faq.adoc[FAQ] * xref:developer-guide/index.adoc[Developer Guide] ** xref:developer-guide/build-and-run.adoc[Build & Run] ** xref:developer-guide/deploy-a-build.adoc[Deploy a Build] ** xref:developer-guide/configuration.adoc[Configuration] ** xref:developer-guide/standalone-mode.adoc[Standalone Mode] ** xref:developer-guide/component-overview.adoc[Component Overview] ** xref:developer-guide/design.adoc[Design] ** xref:developer-guide/style-configuration.adoc[Style Configuration] ** xref:developer-guide/adding-visualizations.adoc[Adding Visualizations] ** xref:developer-guide/state-management.adoc[State Management] ** xref:developer-guide/session-storage.adoc[Session Storage] ** xref:developer-guide/testing.adoc[Testing] ** xref:developer-guide/contributing.adoc[Contributing] ================================================ FILE: docs/modules/ROOT/pages/banner.adoc ================================================ [NOTE] ==== This documentation pertains to the unsupported version of NeoDash, as part of Neo4j Labs. For users of the supported NeoDash offering, refer to https://neo4j.com/docs/neodash-commercial/[NeoDash commercial]. ==== ================================================ FILE: docs/modules/ROOT/pages/developer-guide/adding-visualizations.adoc ================================================ include::../banner.adoc[] = Adding Visualizations include::../banner.adoc[] You can extend NeoDash with your own visualizations without diving deep into the core application. Likewise, adding a new customization to an existing report requires minimal changes. == Add a Visualization You can add a new chart to NeoDash in three steps: [arabic] . Make sure you have a local copy of NeoDash installed and running: .... git clone git@github.com:neo4j-labs/neodash.git git checkout develop yarn install yarn run dev .... [arabic, start=2] . Create a new file `src/charts/ABCChart.tsx`. In here, add a new object that implements the `ChartProps` interface: .... export interface ChartProps { records: Neo4jRecord[]; // Query output, Neo4j records as returned from the driver. selection?: Record; // A dictionary with the selection made in the report footer. settings?: Record; // A dictionary with the 'advanced settings' specified through the NeoDash interface. dimensions?: Number[]; // a 2D array with the dimensions of the report (likely not needed, charts automatically fill up space). fullscreen?: boolean; // flag indicating whether the report is rendered in a fullscreen view. queryCallback?: (query: string, parameters: Record, records: Neo4jRecord[]) => null; // Optionally, a way for the report to read more data from Neo4j. setGlobalParameter?: (name: string, value: string) => void; // Allows a chart to update a global dashboard parameter to be used in Cypher queries for other reports. getGlobalParameter?: (name) => string; // Allows a chart to get a global dashboard parameter. } .... Note that the only mandatory property is `records`. This contains a list of https://neo4j.com/docs/api/javascript-driver/current/class/lib6/record.js~Record.html[records] returned from the Cypher query specified by the user. For inspiration, below is a basic example of a component that renders all returned data as a list: .... import React from 'react'; import { ChartProps } from './Chart'; import { renderValueByType } from '../report/ReportRecordProcessing'; const NeoListReport = (props: ChartProps) => { const records = props.records; return records.map(r => { return
{ r["_fields"].map(value => { return <>{renderValueByType(value)}, })}
}) } export default NeoListReport; .... [arabic, start=3] . Make your component selectable. Now that you’ve created a new chart type, you need to tell the card settings window that it can be chosen by a user. To accomplish this, open `config/ReportConfig.tsx`. Add a new entry to the `REPORT_TYPES` dictionary: .... export const REPORT_TYPES = { ... "list": { label: "List", helperText: "I'm a list", component: NeoListReport, maxRecords: 10, settings: {} }, ... } .... Inspect the other entries for examples of the fields that each entry can have. Restart the application, and you should be able to select your new chart type. Finally, *Cypress* can be used to develop an end-to-end test for your component in a matter of minutes. See Testing for more on Cypress testing. ____ After you added a visualization or a new customization, consider contributing it to the NeoDash project by creating a https://github.com/neo4j-labs/neodash/pulls[Pull Request]. ____ ================================================ FILE: docs/modules/ROOT/pages/developer-guide/build-and-run.adoc ================================================ include::../banner.adoc[] = Build & Run include::../banner.adoc[] To start developing the application, you will need to set up the development environment. == Run & Build using yarn NeoDash is built with React. You will need `yarn` installed to run the web app. ____ Use a recent version of `yarn` and `node` to build NeoDash. The application has been tested with yarn 1.22.17 & node v18.8.0. ____ To run the application in development mode: - https://github.com/neo4j-labs/neodash[clone this repository.] - open a terminal and navigate to the directory you just cloned. - run `yarn install` to install the necessary dependencies. - run `yarn run dev` to run the app in development mode. - the application should be available at http://localhost:3000. To build the app for production: - follow the steps above to clone the repository and install dependencies. - execute `yarn run build`. This will create a `build` folder in your project directory. - deploy the contents of the build folder to a web server. You should then be able to run the web app. == Run locally with Docker Pull the latest image from Docker Hub to run the application locally: .... # Run the application on http://localhost:5005 docker pull neo4jlabs/neodash:latest docker run -it --rm -p 5005:5005 neo4jlabs/neodash # If you want to run on a custom port, set an environment variable export NGINX_PORT=5008 docker run -it --rm -e NGINX_PORT=5008 -p 5008:5008 neo4jlabs/neodash .... ____ Windows users may need to prefix the `docker run` command with `winpty`. ____ == Build Docker image A pre-built Docker image is available https://hub.docker.com/r/neo4jlabs/neodash[on DockerHub]. This image is built using the default configuration (running in editor mode, without SSO). === To build the image yourself: Make sure you have a recent version of `docker` installed to build the multi-stage NeoDash image and run it. On Unix (Mac/Linux) systems: .... docker build . -t neodash .... If you use Windows, you might need to prefix the command with `winpty`: .... winpty docker build . -t neodash .... After building, you can run the image with: .... docker run -it –rm -p 5005:5005 neodash .... == Run on Kubernetes === To deploy using YAML files YAML 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: .... apiVersion: v1 kind: Pod metadata: name: neodash labels: project: neodash spec: containers: - name: neodash image: neo4jlabs/neodash:latest ports: - containerPort: 5005 .... Creating a Kubernetes service to expose the application: .... apiVersion: v1 kind: Service metadata: name: neodash-svc spec: type: LoadBalancer ports: - port: 5005 targetPort: 5005 selector: project: neodash .... === To deploy using a Helm Charts A 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, .... # Name override or full name override nameOverride: '' fullnameOverride: neodash-test # Number of pods replicaCount: 1 # Image Details image: repository: neo4jlabs/neodash pullPolicy: IfNotPresent tag: 'latest' imagePullSecrets: [] # Image pull secret if any # Pod annotations, labels and security context podAnnotations: {} podLabels: {} podSecurityContext: {} # Mode configuration using environment variables # Set reader mode environment variables when enable_reader_mode is true enable_reader_mode: true env: - name: "ssoEnabled" value: "false" - name: "standalone" value: "true" - name: "standaloneProtocol" value: "neo4j+s" - name: "standaloneHost" value: "localhost" - name: "standalonePort" value: "7687" - name: "standaloneDatabase" value: neo4j - name: "standaloneDashboardName" value: "test" - name: "standaloneDashboardDatabase" value: neo4j - name: "standaloneAllowLoad" value: "false" - name: "standaloneLoadFromOtherDatabases" value: "false" - name: "standaloneMultiDatabase" value: "false" # Environment variable from secret envFromSecrets: [] # standaloneUsername: # secretName: "neo4j-connection-secrets" # key: "username" # standalonePassword: # secretName: "neo4j-connection-secrets" # key: "password" # Service details service: type: LoadBalancer # Can also be ClusterIP or NodePort port: 5005 # For the service to listen in for Traffic targetPort: 5005 # Target port is the container port annotations: {} # Service annotations for the LoadBalance # Ingress ingress: enabled: false # Enable Kubernetes Ingress className: 'alb' # Class Name annotations: {} # Cloud LoadBalancer annotations hosts: [] # - host: neodash.example.com # paths: # - path: '/' # pathType: Prefix tls: [] # Pod resources request, limits and health check resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "128Mi" cpu: "500m" livenessProbe: httpGet: path: /* port: 5005 readinessProbe: httpGet: path: /* port: 5005 # Pod Autoscaler autoscaling: enabled: false # minReplicas: 1 # maxReplicas: 100 # targetCPUUtilizationPercentage: 80 # Pod Volumes volumes: [] volumeMounts: [] # Service Account serviceAccount: create: true automount: true # annotations: {} # name: '' .... ================================================ FILE: docs/modules/ROOT/pages/developer-guide/component-overview.adoc ================================================ include::../banner.adoc[] = Component Overview include::../banner.adoc[] The image below contains a high-level overview of the component hierarchy within the application. The following conceptual building blocks are used to create the interface: image::component-hierarchy.png[NeoDash Component Hierarchy] * *Application* - highest level in the component structure. Handles all application-level logic (e.g. initalizing the app). * *Modals* - all pop-up windows used by the tool. (Connection modal, save-dashboard modal, errors/warnings, etc.) * *Drawer* - the sidebar on the left side of the screen. Contains buttons to perform application-level actions. * *The Dashboard* - Main dashboard component. Renders components dynamically based on the current state. * *Dashboard Header* - the textbox at the top of the screen that lets you set a title for the dashboard, plus the page selector. * *Pages* - a dashboard has one or more pages, each of which can have a list of cards. * *Cards* - a `block' inside a dashboard. Each card contains a `view' window, and a `settings' window. * *Card View* - the front of the card containing the selected report. * *Card Settings* - the back of the card, containing the cypher editor and advanced settings for the report. * *Card View Header* - the header of the card, containing a text box that acts as the name of the report. * *Report* - the component inside the card view that handles query execution and result parsing. Contains a single chart (visualization) * *Card View Footer* - The footer of the card view. Depending on the type, contains several `selectors' that modify the visualization. * *Card Settings Header* - Header of the card settings, used for moving/deleting the card. * *Card Settings Content* - the component containing the main content of the report. This is most often the Cypher query editor. * *Card Settings Footer* - the `footer' of the card. This contains the `advanced settings' window for reports. * *Charts* - the different visualizations used by the application: bar charts, tables, graphs, etc. == A note on Cards v.s. Reports Whereas a user might associate a Card in NeoDash to a report directly, the application has a more nuanced seggration of responsibilities: * The *Card* is responsible for positioning the component in a page. * The *Card Content* is the core element of the card (exclusive of the title header and any optional footer). * A *Report* sits inside the card content, and handles the running of queries and displaying errors. * A *Chart* is rendered by the report and is solely responsible for rendering a specific visualization. ================================================ FILE: docs/modules/ROOT/pages/developer-guide/configuration.adoc ================================================ include::../banner.adoc[] = Configuration include::../banner.adoc[] When using a custom NeoDash deployment, there are several settings that can be configured. These mostly relate to link:../standalone-mode[Standalone Mode] and SSO configurations. For a simple (non-Dockerized) deployment, these configuration parameters can be changed by modifying `dist/config.json` after you have built the application. When Docker image, these can be passed as environment variables. See link:../standalone-mode[Standalone Mode] for more on Docker deployments. An example configuration for NeoDash (default, running in editor mode) will look like this: .... { "ssoEnabled": false, "ssoProviders": [], "ssoDiscoveryUrl": "https://example.com", "standalone": false, "standaloneProtocol": "neo4j+s", "standaloneHost": "localhost", "standalonePort": "7687", "standaloneDatabase": "neo4j", "standaloneDashboardName": "My Dashboard", "standaloneDashboardDatabase": "dashboards", "standaloneDashboardURL": "", "standaloneAllowLoad": false, "standaloneLoadFromOtherDatabases": false, "standaloneMultiDatabase": false, "standaloneDatabaseList": "neo4j" "loggingMode": "0", "loggingDatabase": "logs", "customHeader": "", } .... == Configuration Options [width="100%",cols="19%,17%,26%,38%",options="header",] |=== |Name |Type |Default Value |Description |ssoEnabled |boolean |false |If enabled, lets users connect to Neo4j using SSO. This requires a valid ssoDiscoveryUrl to be set. |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. |ssoDiscoveryUrl |string |https://example.com |If ssoEnabled is true & standalone mode is enabled, the URL to retrieve SSO auth config from. |standalone |boolean |false |Determines whether to run NeoDash in editor mode (false), or reader mode (true). The terms ``Reader mode'' and ``Standalone mode'' are used interchangibly. |standaloneProtocol |string |neo4j |When running in standalone mode, the protocol to used for the Neo4j driver. This should be set to one of `neo4j`, `neo4j+s`, `neo4j+ssc`, `bolt`, `bolt+s`, or `bolt+ssc`. |standaloneHost |string |localhost |When running in standalone mode, the hostname to connect to. This should be *just* the hostname, no protocols or ports. |standalonePort |string |7687 |When running in standalone mode, the Bolt port to connect to. |standaloneDatabase |string |neo4j |When running in standalone mode, the database to use for reporting. Cypher queries used in reports will read data from this database. |standaloneUsername ⚠️ |string |… |A hidden config parameter enables you to set the username for standalone mode by default. Keep in mind this is a security risk, as it exposes the Neo4j username to anyone who can access the NeoDash deployment. |standalonePassword ⚠️ |string |… |A hidden config parameter enables you to set the password for standalone mode by default. If this value is set connections are also made automatically. Keep in mind this is a security risk, as it exposes the Neo4j username to anyone who can access the NeoDash deployment. |standaloneDashboardName |string |My Dashboard |The exact name (case-sensitive) of the dashboard to be loaded when running in standalone mode. This must be a dashboard that is saved as a node in the graph. |standaloneDashboardDatabase |string |neo4j |The name of the Neo4j database that contains the saved dashboard node. This is neo4j by default, _unless you are using Neo4j Enterprise Edition_, which lets you use multiple databases. |standaloneDashboardURL |string |neo4j |If you do not save a dashboard inside Neo4j and would like to run a standalone mode deployment with a dashboard from a URL, set this parameter to the complete URL pointing to the dashboard JSON. |standaloneAllowLoad |boolean |false |If set to yes the "Load Dashboard" button will be enabled in standalone mode, allowing users to load additional dashboards from Neo4J. This parameter is false by default _unless you are using Neo4j Enterprise Edition_, which lets you use multiple databases. *NOTE*: when Load is enabled in standalone mode, only Database is available as a source, not file. |standaloneLoadFromOtherDatabases |boolean |false |If _standaloneAllowLoad_ is set to true, this parmeter enables or not users to load dashboards from other databases than the one deifned in _standaloneDashboardDatabase_. If _standaloneAllowLoad_ is set to false this parameters has no effect. |standaloneMultiDatabase |boolean |false |If this parameter set to true, the standalone configuration will ignore the _standaloneDatabase_ parameter and allow users to choose which database to connect to in the login screen, among the ones provided in _standaloneDatabaseList_, with a dropdown list. This parameter is false by default _unless you are using Neo4j Enterprise Edition_, which lets you use multiple databases. |standaloneDatabaseList |string |neo4j |If _standaloneMultiDatabase_ is set to true, this parmeter must contain a comma separated list of database names that will be displayed as options in the Database dropdown at user login (e.g. 'neo4j,database1,database2' will populate the database dropdown with the values 'neo4j','database1' and 'database2' in the connection screen). If _standaloneMultiDatabase_ is set to false this parameters has no effect. |loggingMode |string |none |Determines whether neodash should create any user activity logs. possible values include: `0` (no log is created), `1` (user login are tracked), `2` (tracks when a specific dashboard is accessed/loaded or saved by a user*). ⚠️ Logs are created in Neo4J DB using the current user credentials (or standaloneUsername if configured); write access to the log database must be granted to enble any user to create logs. ⚠️ * Load/Save from/to file are not logged (only from/to Database) |loggingDatabase |string |logs |When loggingMode is set to anything else than '0', the database to use for logging. Log records (nodes) will be created in this database. |customHeader |string |none |When set the dashboard header will display the prameter value as a fixed string, otherwise it will display the host and port of current connection. |=== == Configuring SSO NeoDash can use SSO as an alternative for password-based sign-in, if your Neo4j database is enabled to use single sign on. To enable SSO, set `ssoEnabled` to `true`. Then, set `ssoDiscoveryUrl` to the place where your `discovery.json` is located (This will often be the hostname of your database, appended by `/discovery.json`). ____ Note that SSO is only available when Standalone Mode is enabled. ____ == Auth Provider To set up NeoDash to use an external identity provider, you can add a /auth_provider resource to nginx (in `/conf/default.conf`): .... location /auth_provider { default_type application/json; return 200 '{ "auth_config" : { "oidc_providers" : [ ... ] } }'; } .... For basic deployments it might suffice to route requests to `/auth_provider` on the https port of the neo4j database. == Configuring Standalone Mode Standalone mode, or reader-mode, overrides the functionality of NeoDash, allowing you to deploy a fixed dashboard to users. Standalone mode can be enabled by changing the `standalone` config parameter: * If standalone mode is `false`, all other configuration parameters are ignored. NeoDash will run in Editor mode, and require a manual sign-in. * If standalone mode is `true`, NeoDash will read all configuration parameters. A *predefined dashboard* will be auto-loaded, and no changes to the dashboard can be made. There are two types of valid standalone deployments: ** A standalone deployment that *reads the fixed dashboard from Neo4j*. The `standaloneDashboardName` and `standaloneDashboardDatabase` config parameters are used to define these. ** A standalone deployment that *reads the fixed dashboard from a URL*. The `standaloneDashboardURL` config parameter is used to define this. * Standalone mode can also be configured to allow users load a different dashboard after the predefined one is loaded (a `Load Dashboard` button will be displayed on the right side of dashboard title). The `standaloneAllowLoad` and `standaloneLoadFromOtherDatabases` are used to define this. * When allowing users to load dashboards dyamically in standalone mode, they may also need to connect to different databases, depending on the specific dashboard bing loaded. this can be enabled setting `standaloneMultiDatabase` to true and providing a comma separated list of the allowed database names in the`standaloneDatabaseList` parameter. ================================================ FILE: docs/modules/ROOT/pages/developer-guide/contributing.adoc ================================================ include::../banner.adoc[] = Contributing include::../banner.adoc[] Contributions to the project are highly welcomed. Please consider creating a https://github.com/neo4j-labs/neodash/pulls[Pull Request]. Ensure you start from the `develop` branch, and set the merge base to `develop` as well. For your feature to be accepted, ensure: 1. The component is tested (if relevant, see Testing). 2. Your code is aligned with https://www.w3.org/wiki/JavaScript_best_practices[JS Best Practices]. 3. The component is well documented in the documentation portal (if applicable). == Feature Requests / Bugs If you have a request for a feature, or have found a bug, consider creating an https://github.com/neo4j-labs/neodash/issues[issue] on GitHub. Please include a link:./testing#debug-report[Debug Report] if available. ================================================ FILE: docs/modules/ROOT/pages/developer-guide/deploy-a-build.adoc ================================================ = Deploy a Build include::../banner.adoc[] If you have a pre-built NeoDash application, you can easily deploy it on an any webserver. A NeoDash build is "just" a collection of HTML, CSS and JavaScript files, so it can run virtually anywhere. This guide walks you through the process of deploying a NeoDash build onto your own webserver. == 1. Prepare the files First, check that you have the correct files. We typically provide builds as either a zip file or tarball with the following naming convention: `neodash-2.X.X.zip` or `neodash-2.X.X.tar.gz`. For zip files, open up the terminal and run: ```bash unzip neodash-2.X.X.zip ``` For tar.gz files, open up the terminal and run: ```bash tar -xf neodash-2.X.X.tar.gz ``` After running either of these, you should now have a folder `neodash-2.X.X` in the current directory. == 2. Edit Configuration (Optional) This is an optional step if you want to configure optional settings for your NeoDash deployment (e.g. SSO or standalone mode). 1. Inside the folder you just unzipped, open up `config.json`. 2. Edit this file to modify your link:../configuration[Configuration] settings. 3. Save the file. 4. Inside the folder you just unzipped, open up `style.config.json`. 5. Edit this file to modify your link:../styleConfiguration[Style Configuration] settings. 6. Save the file == 3. Move the tarball/zip to your webserver Finally, copy the files to the correct folder on your webserver. Depending on the webserver type and version, this could be different directory. As an example - to copy the files to an nginx webserver using `scp`: ```bash scp neodash-2.4.11-labs username@host:/usr/share/nginx/html ``` NeoDash should now be visible by visiting your (sub)domain in the browser. Can't see the application? Check that the webserver user has read-permissions on the files you copied into the HTML directory. ================================================ FILE: docs/modules/ROOT/pages/developer-guide/design.adoc ================================================ include::../banner.adoc[] = Design include::../banner.adoc[] This page contains some key guidelines for design of the application. This entails code architecture, as well as UX/UI design. == File Structure The source code of NeoDash is organized as a flat file structure based on components. Given a component `ABC` is to be added, you should create a directory called `abc` with the following files: * `ABC.tsx` (component renderer) * `ABCActions.ts` (objects defining state manipulation) * `ABCReducer.ts` (handling state changes based on actions) * `ABCSelectors.ts` (used by components to retrieve part of the state) * `ABCThunks.ts` (Complex state handling logic, to fire one or more actions) === Structure of the other folders .... conf: nginx configuration for Docker image. dist: directory for generated webpack files. node_modules: downloaded dependencies public: style files/images. Runtime app config. scripts: utility scripts for deployment. src: source code. target: compiled package as tgz file. .babelrc: javascript compiled settings. .gitignore: gitignore files. Dockerfile: docker image definition. .... == UX Design At it’s core, NeoDash aims to be a tool that is _easy to learn, but hard to master_. This translates into the following five design principles in mind: [arabic] . Use a limited set of core visualizations, with high customizability. . It should be easy to get started without reading documentation. . The tool should be self-documenting. . Complex data transformations should be done by dashboard builders in Cypher, and not by the application. . The tool should be easy to extend with custom visualizations. ================================================ FILE: docs/modules/ROOT/pages/developer-guide/index.adoc ================================================ include::../banner.adoc[] = Developer Guide include::../banner.adoc[] This guide contains information for developers looking to deploy NeoDash, or extend it for their own needs. - link:build-and-run[Build & Run] - link:configuration[Configuration] - link:standalone-mode[Standalone Mode] - link:component-overview[Component Overview] - link:design[Design] - link:style-configuration[Style Configuration] - link:adding-visualizations[Adding Visualizations] - link:state-management[State Management] - link:testing[Testing] - link:contributing[Contributing] == Prerequisites for extending NeoDash NeoDash is a web application written in TypeScript. Knowledge of React & Redux is also highly recommended when extending the application. Concretely, the following languages and frameworks make up the core of NeoDash: - https://reactjs.org/[React] - https://redux.js.org/[Redux] - https://redux.js.org/usage/writing-logic-thunks[Redux Thunks] - https://www.cypress.io/[Cypress] - https://mui.com/[Material UI] - https://webpack.js.org/[Webpack] The following core libraries are used to build the visualizations for reports: - https://github.com/vasturiano/react-force-graph[react-force-graph (Graph)] - https://mui.com/components/data-grid/[@mui/datagrid (Table)] - https://nivo.rocks/[@nivo (Bar, Line, Pie charts)] - https://leafletjs.com/[leaflet (Map)] - https://github.com/remarkjs/react-markdown[react-markdown (Markdown)] ================================================ FILE: docs/modules/ROOT/pages/developer-guide/session-storage.adoc ================================================ include::../banner.adoc[] = Session Storage include::../banner.adoc[] This reducer serves only to store data that we want to reset at each new session. To connect to it, just define a key and use the predefined actions to set a new pair (key,value) inside of it. Inside 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). ================================================ FILE: docs/modules/ROOT/pages/developer-guide/standalone-mode.adoc ================================================ include::../banner.adoc[] = Standalone Mode include::../banner.adoc[] Next to being a dashboard editor, NeoDash can be deployed in a `standalone mode' - allowing you set up a architecture to publish and read dashboards. Running in standalone modec mode will: - Disable all editing options - Have a hardcoded Neo4j URL and database name - Load a dashboard from Neo4j with a fixed name. The diagram below illustrates how NeoDash standalone mode can be deployed next to a standard `Editor Mode' instance: image:standalone-architecture.png[image] == Option 1 - Standard Deployment (Non-Docker) First, build NeoDash as described link:../build-and-run[here]. After building, you’ll have a `dist` directory that you can deploy to a web server. To configure the app to run in standalone mode, you’ll need to edit `dist/config.json` and change the `standalone` property to `true`. The other variables inside `config.json` should also be configured to match the hostname, port and database name of your Neo4j instance. See Configuration for more on configuration variables. As `config.json` gets picked up at runtime by the application, users viewing the application will now access the dashboard in standalone mode. == Option 2 - Docker Deployment You can configure the app to run in standalone by passing environment variables to Docker: .... docker run -it --rm -p 5005:5005 \ -e ssoEnabled=false \ -e ssoProviders=[] \ -e ssoDiscoveryUrl="https://example.com" \ -e standalone=true \ -e standaloneProtocol="neo4j+s" \ -e standaloneHost="localhost" \ -e standalonePort="7687" \ -e standaloneDatabase="neo4j" \ -e standaloneDashboardName="My Dashboard" \ -e standaloneDashboardDatabase="dashboards" \ -e standaloneDashboardURL="dashboards" \ -e standaloneAllowLoad=false \ -e standaloneLoadFromOtherDatabases=false \ -e standaloneMultiDatabase=false \ -e standaloneDatabaseList="neo4j" \ neo4jlabs/neodash .... Make sure that all of the environment variables are set to the correct values. This is described in more detail link:../configuration[here]. ____ Alternatively, environment variables from docker compose or a kubernetes deployment can be used. ____ == Deep Linking To dynamically view a deployed NeoDash dashboard, you can deep-link into a deployed dashboard. the following deeplinking options are available via URL parameters: - Appending `?page=1` to the URL will open up a dashboard at a given page. (Starting at zero). - Appending `?neodash_person_name=Tom` to the URL will set a dashboard parameter as a default for the entire dashboard. Multiple parameters can be used in a deep-link by concatinating them: .... https://myneodashdeployment.com/?page=1&neodash_person_name=Tom&neodash_movie_name=The%20Matrix .... ================================================ FILE: docs/modules/ROOT/pages/developer-guide/state-management.adoc ================================================ include::../banner.adoc[] = State Management include::../banner.adoc[] NeoDash is an application with a complex internal state. If you are planning to extend the application state in some way, make sure you are familiar with https://redux.js.org/[Redux] design patterns. The app’s entire state object is encapsulated in the following JSON structure: .... { "dashboard": { "title": "My Dashboard Name", "version": "2.4", "settings": { "pagenumber": 0, "editable": true, ... "parameters": { ... } }, "pages": [ ... ] }, "application": { ... }, "version": "2.1.0" } .... At the highest level, this object consists of three entries: - `dashboard`: all state related to the currently active dashboard. This is changed when a dashboard gets loaded, modified or removed. - `application`: all state related to the application itself. This describes which windows are open, what database you are connected to, etc. - `version`: the version of NeoDash that is running. Note that these are complete version numbers (of the shape X.Y.Z), unlike dashboard versions, which have a different versioning scheme. ____ Want to see the complete state object for your application? Generate a *Debug Report* from the About window. ____ == Dashboard State The dashboard entry contains the entire state of the currently loaded dashboard. Take the following simple dashboard as an example. .... { "dashboard": { "title": "A Simple Dashboard", "version": "2.4", "settings": { "pagenumber": 0, "editable": true, "fullscreenEnabled": true, "parameters": { "neodash_person_name": "Bob" } }, "pages": [ { title: “My Page” reports: [ { "title": "My Report", "query": "MATCH (n)-[e]->(m) RETURN n,e,m", "type": "graph", "x": "1", "y": "2", "width": "6", "height": "3", "settings": { "nodeColorSchmeme": "blue" } } ] } ] } } .... Key entries of the object are: - `title`: the title of the dashboard. This is displayed on the top of the window. - `version`: _Main_ version of the dashboard that is loaded. - `settings`: contains settings for the dashboard. This includes the current page number, whether the dashboard is editable, whether the dashboard is in fullscreen mode, and the dashboard parameters that are currently set. - `pages`: contains the list of all pages in the dashboard. Each page has a title and a list of reports. == Application State The application state is a flat dictionary of values that determine what the user’s window looks like (which windows are open?) as well as the current database connection, and whether the app is running in standalone mode. .... "application": { "notificationTitle": null, "notificationMessage": null, "connectionModalOpen": false, "welcomeScreenOpen": true, "aboutModalOpen": true, "connection": { "protocol": "neo4j+s", "url": "localhost", "port": "", "database": "", "username": "neo4j", "password": "************" }, "desktopConnection": null, "connected": false, "dashboardToLoadAfterConnecting": null, "waitForSSO": false, "standalone": false, "oldDashboard": null, "ssoEnabled": false, "ssoProviders": [], "ssoDiscoveryUrl": "https://example.com", "standaloneProtocol": "neo4j+s", "standaloneHost": "localhost", "standalonePort": "7687", "standaloneDatabase": "neo4j", "standaloneDashboardName": "My Dashboard", "standaloneDashboardDatabase": "dashboards", "standaloneDashboardURL": "dashboards", "loggingMode": "0", "loggingDatabase": "logging", "standaloneAllowLoad": false, "standaloneLoadFromOtherDatabases ": false, "standaloneMultiDatabase": false, "standaloneDatabaseList": "neo4j", "notificationIsDismissable": null } .... ================================================ FILE: docs/modules/ROOT/pages/developer-guide/style-configuration.adoc ================================================ include::../banner.adoc[] = Style Configuration include::../banner.adoc[] When using a custom NeoDash deployment, there are several theme variables that can be configured. These mostly relate to css tokens for link:https://cdn.jsdelivr.net/npm/@neo4j-ndl/base@1.4.0/lib/tokens/css/tokens.css[Needle] and some other brand specific options. For a simple (non-Dockerized) deployment, these configuration parameters can be changed by modifying `dist/style.config.json` after you have built the application. When using the NeoDash Docker image, these can be passed as environment variables. For example: .... docker run -p 5005:5005 \ -e DASHBOARD_HEADER_BRAND_LOGO=https://picsum.photos/500/100 \ neo4jlabs/neodash .... An example configuration for NeoDash .... { "DASHBOARD_HEADER_BRAND_LOGO": "logo_lightsand.png", "DASHBOARD_HEADER_COLOR" : "#F3F3F0", "DASHBOARD_HEADER_BUTTON_COLOR" : "#009999", "DASHBOARD_HEADER_TITLE_COLOR" : "#00C1B6", "DASHBOARD_PAGE_LIST_COLOR" : "#F3F3F0", "DASHBOARD_PAGE_LIST_ACTIVE_COLOR": "#009999", "style": { "--palette-light-neutral-bg-weak" : "243, 243, 240" } } .... == Configuration Options [width="100%",cols="19%,17%,26%,38%",options="header",] |=== |Name |Type |Default Value |Description |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. |DASHBOARD_HEADER_COLOR |string |#0B297D |Determines the color of the header. |DASHBOARD_HEADER_BUTTON_COLOR |string |#FFFFFF22 |Determines the color of the header buttons. |DASHBOARD_HEADER_TITLE_COLOR |string |#FFFFFF |Determines the color of the header title. |DASHBOARD_PAGE_LIST_COLOR |string |#F0F0F0 |Determines the color of the page selector tabs. |DASHBOARD_PAGE_LIST_ACTIVE_COLOR |string |#FFFFFF |Determines the color of the page selector active tabs. |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") |=== ================================================ FILE: docs/modules/ROOT/pages/developer-guide/testing.adoc ================================================ include::../banner.adoc[] = Testing include::../banner.adoc[] NeoDash uses *Cypress* for automated testing. To install Cypress, check out the official https://docs.cypress.io/guides/getting-started/installing-cypress#What-you-ll-learn[installation instructions]. After cypress is installed, you can use: .... yarn run test .... To open the Cypress GUN. Alternatively, use: .... yarn run test-headless .... To run Cypress from the UI. Before starting the tests, make sure you have a local instance of NeoDash running at `http://localhost:3000` using `yarn run dev`. image:cypress.png[Cypress] Above: a screenshot of the Cypress GUI. == Debug Report For ad-hoc testing, a debug report can be generated by NeoDash. This report contains a JSON representation of the current state of the NeoDash application. To generate a debug report, open the `About' screen. Then, click the 'Debug Report' button in the bottom left corner. image::about.png[About] ================================================ FILE: docs/modules/ROOT/pages/index.adoc ================================================ include::/banner.adoc[] = Introduction This portal contains information on getting started with NeoDash - A Low-Code Dashboard Builder for Neo4j. NeoDash is an open source tool for visualizing your Neo4j data. It lets you group visualizations together as dashboards, and allow for interactions between reports. image::dashboard.png[Dashboard] Neodash 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. - To get started, see the link:quickstart[Quickstart] page. - For more on building dashboards, visit the link:user-guide[User Guide]. - For deploying, configuring and extending NeoDash, check out the link:developer-guide[Developer Guide]. ================================================ FILE: docs/modules/ROOT/pages/quickstart.adoc ================================================ include::/banner.adoc[] = Quickstart include::/banner.adoc[] There are three easy ways to run NeoDash and start dashboarding your Neo4j data: . The latest version is always available online: https://neodash.graphapp.io. . Neo4j Desktop: Install it from the https://install.graphapp.io[Graph App Gallery]. . Using Docker: ``` docker pull neo4jlabs/neodash:latest docker run -it --rm -p 5005:5005 neo4jlabs/neodash ``` Or, build it yourself: ``` git clone https://github.com/neo4j-labs/neodash yarn install yarn run dev ``` NeoDash connects to any recent version of the Neo4j database. (Neo4j 4.0 or later). The quickest way to get started is to create a free cloud database on https://console.neo4j.io[Neo4j Aura]. To get started with building your own dashboard, see the Dashboards page. == NeoDash in Five Minutes See the video below for tips on how to get started with NeoDash in 5 minutes: https://www.youtube.com/watch?v=Ygzj0Y4cYm4[image:https://img.youtube.com/vi/Ygzj0Y4cYm4/0.jpg[Youtube Video]] See also the link:../user-guide/faq#1-how-can-i-learn-more-about-neodash[list of blog posts] in the FAQ. ================================================ FILE: docs/modules/ROOT/pages/user-guide/access-control.adoc ================================================ include::../banner.adoc[] = Access Control include::../banner.adoc[] The 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. == How it Works Navigate 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/). ================================================ FILE: docs/modules/ROOT/pages/user-guide/bloom-integration.adoc ================================================ include::../banner.adoc[] = Bloom Integration include::../banner.adoc[] NeoDash can be linked to Neo4j Bloom perspectives by using https://neo4j.com/docs/bloom-user-guide/current/bloom-tutorial/deep-links/[Bloom Deep Links]. This functionality allows you to combine the power of graph reporting (NeoDash) with intuitive graph exploration (Bloom). == Bloom Deep-Linking To link NeoDash to a Bloom perspective, you will need to: 1. Create a Neo4j Bloom https://neo4j.com/docs/bloom-user-guide/current/bloom-perspectives/bloom-perspectives/[perspective]. 2. Define a https://neo4j.com/docs/bloom-user-guide/current/bloom-tutorial/search-phrases-advanced/[Bloom Search Phrase] for the perspective. 3. Generate a https://neo4j.com/docs/bloom-user-guide/current/bloom-tutorial/deep-links/#_server_hosted_bloom[Deep Link] for your perspective and respective search phrase. This requires that you have a https://neo4j.com/docs/bloom-user-guide/current/bloom-installation/installation-activation/#installing-server-plugin[Server-hosted Bloom installation] running. 4. Use the deep link you created in either: - an iFrame Report (optionally passing in the dashboard parameters into the search phrase). - a Graph Report (Adding your deep link inside the `Drilldown Link' field under advanced settings): image::graphdrilldown.png[Graph Drilldown] ================================================ FILE: docs/modules/ROOT/pages/user-guide/dashboards.adoc ================================================ include::../banner.adoc[] = Dashboards include::../banner.adoc[] In NeoDash, a dashboard consists of several pages, each of which can consist of multiple reports. image::dashboardnew.png[Dashboard] As an example: The screenshot above shows a dashboard with three pages: `Breweries`, `Beer Ratings` and `Styles`. The dashboard title `My Beer Database Dashboard 🍺` is displayed on the top of the window. The first page is selected, and contains three reports, a table, a graph and a map. Each report can be given their own name, and has exactly one Cypher query used to populate the report. See Reports for more info on how reports work. == Dashboard Management After startup up NeoDash, you will be given the choice to create a new dashboard or open an existing one (if available). After being connected, the buttons on the sidebar can be used to save, load or share a dashboard. image::dashboardnewsettings.png[Save/Load/Share Button] === Save a Dashboard A NeoDash dashboard is, simply put, a JSON file. As an example, the default dashboard has the following structure: .... { "title": "", "version": "2.0", "settings": { "pagenumber": 0, "editable": true, "fullscreenEnabled": true, "parameters": {} }, "pages": [ { "title": "Main Page", "reports": [ { "title": "Hi there 👋", "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", "width": 3, "type": "text", "height": 3, "selection": {}, "settings": {} }, { "title": "", "query": "MATCH (n)-[e]->(m) RETURN n,e,m LIMIT 20\n\n\n", "width": 3, "type": "graph", "height": 3, "selection": { "Movie": "title", "Genre": "name" }, "settings": { "nodePositions": {} } } ] } ] } .... After opening the save dialog, there are three options for saving your dashboard: 1. Save as a file. This triggers a download of the current dashboard as `.json` file. 2. Save inside Neo4j. This stores a stringified representation of the dashboard as a node in the database. When using Neo4j multi-database, you will be given the choice of which database to save the dashboard in. 3. Copy-paste the JSON file directly. > Keep in mind that your currently active dashboard is stored in the browser cache. If you clear your cache (cookies), the dashboard is gone. === Load a Dashboard Just like in the save screen, a dashboard can be loaded in one of three ways: 1. Load from a file. This requires you to select a `.json` somewhere on your computer. 2. Load from Neo4j. This requires you to select a dashboard node stored in the database. When loading from Neo4j, you will be presented with the list of dashboards in reverse chronological order. 3. Loading a JSON file by pasting it directly into the editor. === Share a Dashboard A dashboard can be shared with other users by generating a direct link to it. This link will contain: - A link to the dashboard (either a direct URL or the name of the dashboard inside Neo4j). - (Optionally), the credentials of the database that the dashboard is reporting on. *Be warned*, when using this feature, the share link will contain the database credentials, which can be a security risk. - 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. When creating a NeoDash deployment on a production database, it is not recommended to use the `Share' feature. Rather, set up a dedicated standalone deployment of NeoDash. See Publishing for more infomation. === Dashboard Access Control With 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. You 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. > 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. image::dashboardaccesscontrol.png[Dashboard Access Control] == Dashboard Settings Settings for the entire dashboard can be accessed by clicking the *Settings ⚙️* button in the dashboard sidebar. image::dashboardsettings.png[Dashboard Settings] This window can be used to control the followng settings: [width="100%",cols="19%,17%,26%,38%",options="header",] |=== |Name |Changeable |Default Value |Description |Editable |Yes |on |If enabled, show the dashboard in `editing mode'. If not, show it in `view mode'. In view mode, all editing is disabled, pages and reports can not be moved, edited or renamed. |Enable Fullscreen Report Views |Yes |on |If enabled, show the *🔳 Fullscreen* button on the top-right of a report, letting users maximize a visualization. |Maximum Query Time (seconds) |Yes |20 |The maximum time is a query is allowed to take before being cancelled automatically. Increase this if you have complex analytical queries. |Disable Row Limiting |Yes |off |If enabled, the automatic link:reports#row-limiting[row limiting] feature of dashboards is disabled. |Page Number |No |0 |The current page number of the dashboard being viewed. This can only be changed by switching pages in the dashboard header. |Global Parameters |No | {} |The global parameters that are shared among all reports in the dashboard. See the next section for more on global parameters. |=== == Parameters Dashboard parameters are key-value pairs that can be used inside the queries of reports. A convention is that a dashboard parameter in NeoDash will always start with `$neodash_`. Parameters can only be set (and unset) using the link:../reports/parameter-select[Parameter Select] reports. After setting a parameter, it will be available to all reports in the dashboard. A query that uses a dashboard parameter will look like this: .... MATCH (m:Movie)<-[a:ACTED_IN]-(p:Person) WHERE m.title = $neodash_movie_title RETURN m, a, p .... === Deep-Linking Parameters For browser-based NeoDash deployments, you set NeoDash parameters by means of URL parameters. For example, when a user visits the following URL: .... https://neodash.graphapp.io/?neodash_person_name=Adam .... This will set the parameter `$neodash_person_name` to `Adam` after loading the dashboard. ================================================ FILE: docs/modules/ROOT/pages/user-guide/extensions/access-control-management.adoc ================================================ = Access Control Management include::../../banner.adoc[] This 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. This 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. == Using the Extension == If 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. image::rolesmenu.png[Role menu] The user can then click on any role and a window will appear with the role's context: * 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. * 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) * 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) Finally 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. image::rolelabelmodal.png[Role modal] > 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. ================================================ FILE: docs/modules/ROOT/pages/user-guide/extensions/advanced-visualizations.adoc ================================================ = Advanced Visualizations include::../../banner.adoc[] Advanced visualizations let you extend your dashboard with complex, powerful visualizations beyond the standard visualizations. For specific use-cases, these visualizations may convey information that a simple visualization cannot. To use advanced visualizations, enable them in the **Extensions Window**. This makes them selectable inside reports, as well as add examples to the Example window. The following visualizations are part of this extension: - A link:../../reports/graph3d[3D Graph] to visualize a graph in three dimensions. - A link:../../reports/sankey[Sankey Chart] to visualize flows. - Three charts to plot hierarchical data (link:../../reports/sunburst[Sunburst], link:../../reports/circle-packing[Circle Packing], link:../../reports/treemap[Treemap]) - A link:../../reports/gauge-chart[Gauge Chart] to show percentages. - An link:../../reports/choropleth[Choropleth] to visualize numeric, country-data. - An link:../../reports/areamap[Area Map] to show an interactive world map, annotated with numeric country / region values. - A link:../../reports/gantt[Gantt] chart to visualize dependencies between tasks. - A link:../../reports/radar[Radar Chart] to create a radial view of multiple categoric values. image::advanced-visualizations.png[Advanced Visualizations] ================================================ FILE: docs/modules/ROOT/pages/user-guide/extensions/forms.adoc ================================================ = Forms include::../../banner.adoc[] The 'forms' extension lets you combine different parameter selectors to update / modify your graph data. Update queries are predefined by the dashboard builder, and the user is limited to specifying the parameters for the query only. See link:../../reports/form[Form] on how to create, configure, and use forms. > 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. image::forms.png[Forms] ================================================ FILE: docs/modules/ROOT/pages/user-guide/extensions/index.adoc ================================================ = Extensions include::../../banner.adoc[] Extensions provide a way to expand the basic functionality of NeoDash with extra features. To enable an extension, open up the extensions window by clicking the puzzle piece icon in the left-sidebar of the screen. This will open up the **Extensions Window**, which lets you toggle active extensions for the current dashboard. image::extensions.png[The Extensions Window] The following types of functionality can be added through NeoDash extensions: - A new type of visualization. - A more customizable version of an existing visualization. - New core features, such as rule-based styling or interactive reports. The currently available extensions in NeoDash are: - link:advanced-visualizations[Advanced Visualizations] - link:rule-based-styling[Rule-based Styling] - link:report-actions[Report Actions] - link:natural-language-queries[Text2Cypher - Natural Language Queries] - link:forms[Forms] - link:access-control-management[Access Control Management] == Types of Extensions === 1. Core Extensions Core Extensions are available as part of the open-source NeoDash project. These are available to use for free anywhere - Neo4j Desktop, public NeoDash deployments, and self-hosted NeoDash deployments. === 2. Expert Extensions Expert Extensions are built by the Neo4j Professional Services team. These extensions push NeoDash to the next level, by providing extra functionality to create interactive graph applications. Reach 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. === 3. Custom Extensions Custom Extensions are self-built extensions that you can plug into the project. To learn about how to fork and extend NeoDash, check out the link:../../developer-guide[Developer Guide]. ================================================ FILE: docs/modules/ROOT/pages/user-guide/extensions/natural-language-queries.adoc ================================================ = Text2Cypher - Natural Language Queries include::../../banner.adoc[] 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. == How it works This extension feature allows users to interact with NeoDash using natural language to generate Cypher queries for querying Neo4j graph databases. This integration leverages Language Models (LLMs) to interpret user inputs and generate Cypher queries based on the provided schema definition. == Configuration To enable Natural Language Queries in NeoDash, follow these configuration steps: 1. Open NeoDash and navigate to the "Extensions" section in the left sidebar. 2. Locate the "Text2Cypher" extension and click on it to activate it. 3. Once activated, a new button will appear on top of the screen, with a red exclamation mark (⚠️). Click this button. 4. 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. 5. After providing the required information, click on the "Start Querying" button to finalize the configuration. image::llmconfiguration.png[Configuration settings for the Natural Language Queries extension] == Usage Once the extension is configured, you can start using it in your NeoDash reports. Here's how: 1. Open the Report settings for the desired report. 2. 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. 3. Since you have enabled the extension and authenticated by providing your API key, you can switch to the English language mode. 4. Start formulating your queries in plain English, using natural language expressions to describe the data you want to retrieve. 5. After composing your query, you have two options for further actions: * 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. This allows you to review and modify the generated Cypher query before execution. * 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. image::englisheditor.png[Example of the English editor in NeoDash] == Improving Accuracy with Custom Prompting To boost the accuracy of the language model, you can provide your own example queries to be fed into the prompt. Specifying queries specific to your data model & use-cases can significantly improve the quality of Text2Cypher translations. To access the model examples screen, open up the settings for the extensions. After specifying the provider and model, click the "Tweak Prompts" button on the bottom-left of the window. This leads you to the example interface: image::llm-examples.png[Custom Examples for your prompt] In this interface, you can specify one or more examples that are sent to the language model. An example consists of both a Cypher query, and a natural language equivalent of that query. You can create as many examples as you want, but keeping them close to your user queries will yield best results. == Underlying Functionality * 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. * 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. * 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. * 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. == Prompting Tips When using Natural Language Queries in NeoDash, keep the following tips in mind to enhance your experience: 1. 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. 2. Use keywords and phrases: Incorporate relevant keywords and phrases that are commonly used in the context of your data to improve query accuracy. 3. 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?" 4. Experiment with different phrasings: If you're not getting the desired results, try rephrasing your query using synonyms or alternative expressions. 5. Avoid ambiguous queries: Ambiguous or vague queries may yield unexpected results. Make sure to provide sufficient context and clarify any ambiguities. 6. Validate and review generated queries: Always review the generated Cypher queries to ensure they accurately represent your intent and produce the expected results. == Important Considerations When using Natural Language Queries with Language Models, it's important to be aware of the following considerations: 1. 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. 2. 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. 3. 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. ================================================ FILE: docs/modules/ROOT/pages/user-guide/extensions/report-actions.adoc ================================================ = Report Actions include::../../banner.adoc[] link:../#_2_pro_extensions[label:Pro Extension[]] Report actions let dashboard builders add interactivity into dashboards. Actions can be used to achieve: 1. Cross-report filtering. 2. Using the output of one report in another report. 3. Providing users with more parameterized control beyond parameter selectors. The image below displays an example of two reports interacting using report actions: - 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`**. - 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. image::report-actions.png[Report Actions] == Configuration First, ensure that the extension is enabled. Then, 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): image::reportactionsbutton.png[Report] This will open up the rule definition window. Inside this screen, a list of 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: IF [CONDITION] SET [OBJECT] TO [VALUE] image::reportactions.png[Report] == Supported Visualizations Report Actions are available for the following report types: - Tables - Graphs - Maps ================================================ FILE: docs/modules/ROOT/pages/user-guide/extensions/rule-based-styling.adoc ================================================ = Rule-Based Styling include::../../banner.adoc[] 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. image::rule-based-styling.png[Rule-Based Styling] == Configuration First, ensure that the extension is enabled. Then, on several report types, rule-based styling can be applied to the visualization. To do this, open up the report settings, Then, click the *Rule-Based Styling* button on the bottom right (marked with the red circle): image::rulebasedstylingbutton.png[Report] This will open up the action definition window. Inside this screen, a list of action rules can be defined. Each rule will have the following structure: IF [CONDITION] THEN [STYLE] image::rulebasedstyling.png[Report] Conditions are always based on one of the return fields of the query. This can be a simple field (text, number) or a node property. Style rules are (as of NeoDash 2.1) always color-based. For example, the following rule will set the color of all `Warning` nodes to red: `IF Warning.level = "critical" THEN 'Node Color' = "red"` Ultimately, it is important to understand that the order of the rules is important. If a node matches multiple rules, the first rule that matches will be used. If no rules are matched, the default style will be used. == Supported Visualizations Rule-Based Styling is available for the following report types: - Tables - Bar Charts - Line Charts - Pie Charts - Graphs - Maps - Single Values ================================================ FILE: docs/modules/ROOT/pages/user-guide/extensions/workflows.adoc ================================================ = Workflows include::../../banner.adoc[] Introducing 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. == Enable the extension == Create a Workflow == Create a new step in the workflow == Check status ================================================ FILE: docs/modules/ROOT/pages/user-guide/faq.adoc ================================================ include::../banner.adoc[] = FAQ include::../banner.adoc[] == 1. How can I learn more about NeoDash? To learn more, check out the following list of resources (blogs, videos and sites): - https://www.youtube.com/watch?v=Ygzj0Y4cYm4[NeoDash 2.0 in Five Minutes] - https://www.youtube.com/watch?v=vjZ9M7JpExA[NeoDash 2.0 - Hands On at Neo4j Live] - https://medium.com/p/ddc938ff82fa[Investigating Supply Chains with NeoDash] - https://thatdavestevens.medium.com/social-recommendations-slack-neo4j-and-neodash-fe916588e65b[Social Recommendations with Neo4j & NeoDash] - https://neo4j.com/developer-blog/bitcoin-transactions-dashboard-neo4j-neodash/[Real-Time Dashboard of Bitcoin Transactions With Neo4j and NeoDash] - https://medium.com/@a.emrevarol/european-natural-gas-network-via-knowledge-graph-3c3decb5f2ec[European Natural Gas Pipelines] - http://blog.bruggen.com/2020/11/exporting-spotify-playlists-into-neo4j.html[Exporting Spotify Playlists into Neo4j] - https://nielsdejong.nl/neo4j%20projects/2021/12/14/neodash-2.0-a-brand-new-way-of-visualizing-neo4j-data.html[NeoDash 2.0 Release Overview] - https://nielsdejong.nl/neo4j%20projects/2021/06/06/neodash-1.1-extensible-interactive-dashboards.html[NeoDash 1.1 Release Overview] - https://nielsdejong.nl/neo4j%20projects/2020/11/16/neodash[NeoDash 1.0 Release Overview] _Have a blog post about NeoDash you’d like to share? Let us know and we can add it to this list!_ == 2. Is NeoDash free to use? NeoDash 2.X is available under the https://www.apache.org/licenses/LICENSE-2.0[Apache 2.0 license], which means you can use it for free for with your project. ____ Keep in mind! As NeoDash is a https://neo4j.com/labs/[Neo4j Labs] project, it is not part of the official Neo4j product suite, and is not supported as part of a Neo4j license. (See also question #8 below…) ____ == 3. Can I publish the dashboard that I built? When you’re done building a dashboard and want to show to others as a read-only web page, you can set up a link:standalone%20mode[Standalone Mode] deployment of NeoDash. If you need help setting this up, please contact the NeoDash team. == 4. Is NeoDash Production Ready? NeoDash Labs is an experimental tool without official support. For production-grade usage with Neo4j Enterprise Edition, we recommend a `NeoDash commercial` license. == 5. Can I use NeoDash with Neo4j Community Edition? Yes, NeoDash can be used with any type of Neo4j deployment (On-premise, cloud, or fully managed in Neo4j Aura). We do however recommend that you use the Enterprise version, as it lets you create a dedicated read-only user for reporting. == 6. I’m missing a feature. Can I ask for help? Feature requests and bug reports are more than welcome. Please open an issue on https://github.com/neo4j-labs/neodash/issues[Github]. Issues will be addressed on a best-effort basis. If you’re looking for a specific feature with high priority, you can reach out to the Neo4j team. == 7. How can I contribute to NeoDash myself? NeoDash is an open-source tool that can be extended by anyone. If you are interested in contributing, please check out the https://github.com/neo4j-labs/neodash[Github repository]. Aside from code contributions, we are also very happy to hear about how you use NeoDash. If you have a blog post, podcast or video of your graph dashboard, let us know! == 8. Can I get professional help with NeoDash? If you are interested in a services agreement to support your NeoDash deployment, please reach out to the https://neo4j.com/professional-services/[Neo4j Services Team]. ================================================ FILE: docs/modules/ROOT/pages/user-guide/index.adoc ================================================ include::../banner.adoc[] = User Guide include::../banner.adoc[] The following pages contain everything you need to get started with NeoDash. * link:../quickstart[Quickstart] tells you how to get started with NeoDash right away. * link:dashboards[Dashboards] elaborates on how dashboards are created and used in NeoDash. * link:pages[Pages] explains how to manage pages inside a dashboard. * link:reports[Reports] contains information on how a report works, and lists the different types that can be used. * link:publishing[Publishing] explains how to publish a dashboard for others to view. * link:extensions[Extensions] lists the different extensions available for NeoDash. ================================================ FILE: docs/modules/ROOT/pages/user-guide/pages.adoc ================================================ include::../banner.adoc[] = Pages include::../banner.adoc[] A page is a collection of link:../reports[reports] that can be viewed at the same time. Each page can have an unlimited number of reports in it, and will switch to a scrollable view when the number of reports do not fit in the user’s window. An example of a dashboard with 4 pages can be seen below: image::page.png[Page] A dashboard may have as many pages as required by the dashboard builder. To switch pages, simply click on the page title in the dashboard header. == Editing Pages Pages can be added, renamed, and deleted by using the buttons in the dashboard header (if editing is enabled). Pages can additionally be *re-ordered* by dragging and dropping them in the header. ================================================ FILE: docs/modules/ROOT/pages/user-guide/publishing.adoc ================================================ include::../banner.adoc[] = Publishing include::../banner.adoc[] When you are done building a dashboard, you may want to *publish* that dashboard for others to view. The workflow for a continuous dashboarding cycle may look something like this: image::publish.png[Workflow] Keep in mind that the purpose of an application in the `View' phase is very different from the `Build' phase: 1. A dashboard cannot be edited after it has been published. 2. A fixed dashboard must be loaded and a fixed database must be connected to. 3. Users in the `View' phase should not see the Cypher queries configuration powering the visualizations. == Architecture NeoDash enables the Build, Publish, View workflow by having two seperate deployments of the NeoDash application: 1. An `editor` deployment for the build phase. 2. A `viewer` deployment for the view phase. The *editor* deployment is the app you are using from Neo4j Desktop, from https://neodash.graphapp.io, or from your own deployment. The *viewer* deployment will require some configuration to be set up. These three configurations must be set for NeoDash to be able to run in `View' mode: 1. A flag telling the app to disable all editing features. 2. A hardcoded Neo4j database to connect to. 3. A hardcoded dashboard to load. Technical details on setting this up are documented in the link:../../developer-guide/standalone-mode[Standalone Mode] page. ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/areamap.adoc ================================================ = Area Map include::../../banner.adoc[] link:../../extensions/advanced-visualizations[label:Advanced Visualization[]] 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. It takes two fields: - *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. - *value*: Number. Cardinal data to be used on the chart. == Examples === Basic Area Map [source,cypher] ---- MATCH (:Company{name:'NeoDash'})-[:HAS_DEPARTMENT]->(:Department)<-[:IN_DEPARTMENT]-(e:Employee), (e)-[:LIVES]->(city:City)-[:IN_COUNTRY]->(country:Country) WITH city, country CALL { WITH country RETURN country.countryCode as code, count(*) as value UNION WITH city RETURN city.countryCode as code, count(*) as value } WITH code, sum(value) as totalCount RETURN code,totalCount ---- image::areamap-countries.png[Country Level Visualization] image::areamap-regions.png[Example of Drill Down inside a Country] == Advanced Settings [width="100%",cols="15%,2%,26%,57%",options="header",] |=== |Name |Type |Default Value |Description |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. |Color Scheme |List | |The color scheme to use for the area map. Country colors will vary according their min to max ratio. |Country Code Format |List |Alpha-2 |Type of the country code (two/three letters). |Color Legend|on/off |on |Option to show color legend. |Drilldown Enabled |on/off |off |Enable map drilldown to visualize regional data. |Refreshable |on/off |off |Enables a refresh button for the report. |Fullscreen enabled |on/off |off |Enables a fullscreen view button for the report. |Download image enabled |on/off |off |Enables an image download button for the report. |Auto-run query |on/off |on |When activated, automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/bar-chart.adoc ================================================ = Bar Chart include::../../banner.adoc[] A bar chart will draw categories and values in a familiar bar-layout. The bar chart will require you to choose the following selections: * *Category*: a text field. These will be the labels on the bars. * *Value*: a numeric field. This will be the height of the bars. * *Group*: Optionally, a second textual field. When ``Grouping'' is enabled in the advanced settings, the group can be used to draw a stacked bar chart, with several groups per category. == Examples === Simple Bar Chart [source,cypher] ---- MATCH (p:Person)-[e]->(m:Movie) RETURN m.title as Title, COUNT(p) as People ---- image::bar.png[Basic Table] === Stacked Bar Chart [source,cypher] ---- Match (p:Person)-[e]->(m:Movie) RETURN m.title AS Title, COUNT(p) as People, type(e) as Role ---- image::barstacked.png[Basic Table] == Advanced Settings [width="100%",cols="19%,17%,26%,38%",options="header",] |=== |Name |Type |Default Value |Description |Show Legend |on/off |off |If enabled, shows a legend at the top right of the visualization. |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. |Value Scale |List |linear |When set to symlog, uses a Symmetric logarithmic scale instead of the default linear scale. |Min Value |Number |auto |If not set to ``auto'', this variable is minimum value for the bar chart. |Max Value |Number |auto |If not set to ``auto'', this variable is the maximum value for the bar chart. |Group Mode |List |stacked |This setting determines how different groups are visualized when grouping is enabled. If set to stacked, different groups of the same category are stacked on top of each other. If set to grouped, they are placed alongside each other. |Layout |List |vertical |Whether to use a vertical or horizontal bar chart layout. |Color Scheme |List | |The color scheme to use for the category groups. Colors are assigned automatically (consequitevely) to the different groups returned by the Cypher query. |Show Values on Bars |on/off |off |If enabled, shows the category value inside the respective bar. |Skip label on width (px) |number |0 |Skip label if bar width is lower than provided value, ignored if 0. |Skip label on height (px) |number |0 |Skip label if bar height is lower than provided value, ignored if 0. |Custom label position |off/top/bottom |off | Allow user to place label out of the bar. This will override any other label configuration. |Label Rotation (degrees) |number |45 |the angle at which the bar labels are rotated. |Margin Left (px) |number |50 |The margin in pixels on the left side of the visualization. |Margin Right (px) |number |24 |The margin in pixels on the right side of the visualization. |Margin Top (px) |number |24 |The margin in pixels on the top side of the visualization. |Margin Bottom (px) |number |40 |The margin in pixels on the bottom side of the visualization. |Legend Width (px) |number |128 |The width in pixels of each legend label on top of the visualization (if enabled). |Hide Selections |on/off |off |If enabled, hides the property selector (footer of the visualization). |Auto-run query |on/off |on |when activated automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |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. |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. |Inner Padding |number |0 |When specified, will add padding between any grouped elements. |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. |=== == Rule-Based Styling Using the link:../#_rule_based_styling[Rule-Based Styling] menu, the following style rules can be applied to the bar chart: - The color of the bar. ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/choropleth.adoc ================================================ = Choropleth include::../../banner.adoc[] link:../../extensions/advanced-visualizations[label:Advanced Visualization[]] A Choropleth chart will render geographical data in geoJson polygons layout. It takes two fields: - *code*: String. This represents the Alpha-3 country code of region to be used. Alpha-2 it's not supported. - *value*: Number. Cardinal data to be used on the chart. == Examples === Basic Choropleth [source,cypher] ---- MATCH p=(n:Wine)-[:IS_FROM|PART_OF*]->(c:Country) WITH DISTINCT c.iso3 as country, count(DISTINCT n) as wines RETURN country, wines ---- image::choropleth.png[Choropleth Chart] == Advanced Settings [width="100%",cols="15%,2%,26%,57%",options="header",] |=== |Name |Type |Default Value |Description |Enable interactivity |on/off |on |If enabled, turn on animations when a user hovers over a polygon. |Color Scheme |List | |The color scheme to use for the choropleth. Country colors will vary according their min to max ratio. |Polygon border width (px) |number |0 |The width of the border of each rectangle. |Margin Left (px) |number |24 |The margin in pixels on the left side of the visualization. |Margin Right (px) |number |24 |The margin in pixels on the right side of the visualization. |Margin Top (px) |number |24 |The margin in pixels on the top side of the visualization. |Margin Bottom (px) |number |40 |The margin in pixels on the bottom side of the visualization. |Country Code Format |List |iso_a3 |ISO Standard used on country codes. |Projection Scale |number |100 |Projection Scale of the visualization |Projection x translation |number |0.5 |This parameter will move the center of the visualization on the x axis |Projection y translation |number |0.5 |This parameter will move the center of the visualization on the y axis |Auto-run query |on/off |on |When activated, automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/circle-packing.adoc ================================================ = Circle Packing include::../../banner.adoc[] link:../../extensions/advanced-visualizations[label:Advanced Visualization[]] A circle packing chart will render hierarchical data in a group of nested circles. It takes two fields: - *Path*: a list of strings. This represents the hierarchy (from highest to lowest level). - *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. == Examples === Basic Circle Packing Chart [source,cypher] ---- 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 ---- image::circlepacking.png[Circle Packing Chart] == Advanced Settings [width="100%",cols="13%,3%,26%,58%",options="header",] |=== |Name |Type |Default Value |Description |Enable interactivity |on/off |on |If enabled, turn on animations when a user hovers over an circle. |Color Scheme |List | |The color scheme to use for the circles. Colors are assigned automatically for each of the sub-hierarchies. |Circle border width (px) |number |0 |The width of the border of each circle. |Margin Left (px) |number |24 |The margin in pixels on the left side of the visualization. |Margin Right (px) |number |24 |The margin in pixels on the right side of the visualization. |Margin Top (px) |number |24 |The margin in pixels on the top side of the visualization. |Margin Bottom (px) |number |40 |The margin in pixels on the bottom side of the visualization. |Auto-run query |on/off |on |When activated, automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/form.adoc ================================================ = Form include::../../banner.adoc[] A form is a special type of report that lets users run predefined, parameterized queries. A single form can consist of: - Zero or more link:../parameter-select[parameter selectors]. - A button that triggers submitting the form. When creating a form, you write the Cypher query that is called when the submit button is clicked. This 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. image::createform.png[Complex Form] == Examples === Simple Button A form without parameters is a button that runs a specified query. One or more buttons can be used to perform simple operations in the graph. Below is an example of a simple button form. On submitting, the following query is executed: [source,cypher] ---- MERGE (c:Counter) SET c.count = c.count+1 ---- image::formbutton.png[Button Form] === A Parameter and a Button To create a form with dynamic input, use both a parameter and a button. Below is an example of a form that deletes nodes from the graph. On submit, the following query is executed: [source,cypher] ---- MATCH (p:Person) WHERE p.name = $neodash_person_name DETACH DELETE p ---- image::formsimple.png[Simple Form] === Parameters Only By hiding the submit button, a form can also be used as a space-efficient way to embed multiple parameter selectors. Disable `Has Submit Button` in the report's advanced settings, and add two or more parameter selectors to the form. image::formselector.png[Parameter-only Form] == Advanced Settings [width="100%",cols="19%,17%,26%,38%",options="header",] |=== |Name |Type |Default Value |Description |Form Button Text |text |Submit |Text displayed on the button that submits the form. |Reset Button Text | text |Reset Form |Text displayed on the button that resets the form to data entry mode. |Confirmation Message | multiline text |Form submitted. |Text displayed to the user after the form is submitted successfully. |Clear parameters after submit |on/off |on | Clears all dashboard parameters in the form after submitting. |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. |Has Reset Button |on/off |on |When enabled, lets the user reset the form to enter more data. |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. |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. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/gantt.adoc ================================================ = Gantt Chart include::../../banner.adoc[] link:../../extensions/advanced-visualizations[label:Advanced Visualization[]] A Gantt chart can be used to visualize tasks on a timeline, as well as their dependencies. The NeoDash Gantt chart views your tasks are nodes in the graph, and your relationships are dependencies between them. To use the Sankey chart, nodes must have at least three properties on them: - A `startDate` - An `endDate` - A `title` In 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: - `**SE**`: The dependency is from the **S**tart of the origin task, to the **E**nd of the next task. - `**SS**`: The dependency is from the **S**tart of the origin task, to the **S**start of the next task. - `**ES**`: The dependency is from the **E**nd of the origin task, to the **S**start of the next task. - `**EE**`: The dependency is from the **E**nd of the origin task, to the **E**nd of the next task. == Examples === Gantt Chart Return nodes and relationships to be visualized in the chart. It is mandatory to specify the three node properties (start date, end date and title) in the report's advanced settings. [source,cypher] ---- MATCH (a:Activity)-[r:FOLLOWS]->(a2:Activity) RETURN a, r, a2 ---- image::gantt.png[Gantt Chart] == Advanced Settings [width="100%",cols="15%,2%,6%,77%",options="header",] |=== |Name |Type |Default Value |Description | Bar Color | string | '#a3a3ff' | Default color for the task bars (with no style rules applied.) | Task Label Property | string | 'activityName' | Node property to display on the task bar. | Task Start Date Property | string | 'startDate' | Node property to use as a start date for the task. | Task End Date Property | string | 'endDate' | Node property to use as an end date for the task. | Task Ordering Property | string | (auto) | Custom ordering of the tasks. Defaults to use the start date property. | Dependency Type Property | string | 'rel_type' | The relationship property that stores the dependency type. Property values must be one of `['SS', 'SE', 'ES', 'EE']` | View mode | string | 'auto' | Zoom level of the chart. one of `['auto', 'Half Day', 'Day', 'Week', 'Month', 'Year']`. |Margin Left (px) |number |24 |The margin in pixels on the left side of the visualization. |Margin Right (px) |number |24 |The margin in pixels on the right side of the visualization. |Margin Top (px) |number |24 |The margin in pixels on the top side of the visualization. |Margin Bottom (px) |number |40 |The margin in pixels on the bottom side of the visualization. |Auto-run query |on/off |on |when activated automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== == Rule-Based Styling Using the link:../#_rule_based_styling[Rule-Based Styling] menu, the following style rules can be applied to the Gantt chart: - The color of a task bar. ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/gauge-chart.adoc ================================================ = Gauge Chart include::../../banner.adoc[] link:../../extensions/advanced-visualizations[label:Advanced Visualization[]] A gauge chart takes a single numeric value, and plots it on an animated gauge: - The value returned should be in the range of 0 to 100. - The gauge chart can be customized with different colors and levels (arc segments). == Examples === Basic Gauge Chart [source,cypher] ---- MATCH (c:CPU) WHERE c.id = 1 RETURN c.load_percentage * 100 ---- image::gauge.png[Gauge Chart] == Advanced Settings [width="100%",cols="15%,2%,6%,77%",options="header",] |=== |Name |Type |Default Value |Description |Number of levels | number | 3 | The number of distinct colored levels in the gauge. | 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. | Comma-separated arc colors | List | "#5BE12C, #F5CD19, #EA4228" | The HEX color values to assign to each arc. | Color of the text | string | black | The color of the number on the gauge. | Delay in ms before needle animation | number | 0 | Delay in milliseconds before starting the animation. | Duration in ms for needle animation | number | 2000 | The duration of the moving needle animation when the chart renders. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/graph.adoc ================================================ = Graph include::../../banner.adoc[] The graph report will render all returned nodes, relationships and paths in a force-directed graph layout. This includes collections (lists) of these objects. The library `react-force-graph` is used to create the visualizations. Depending on your browser, the visualization should be able to handle drawing 1000-3000 nodes/relationships with custom styling options. The graph layout contains an extensive set of features, including: - Drag and drop nodes. - Custom node/relationship styling. - Tooltips/inspect window on nodes/relationships. == Examples === Basic Graph .... MATCH (p:Person)-[a:ACTED_IN]->(m:Movie) WHERE m.title = 'The Matrix' RETURN p, a, m .... image::graph.png[Basic Graph] == Virtual Graph .... MATCH (p:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person) WHERE m.title = "The Matrix" RETURN p, p2, apoc.create.vRelationship(p, "KNOWS", {}, p2) .... image::graph2.png[Virtual Graph] == Advanced Settings [width="100%",cols="12%,2%,3%,83%",options="header",] |=== |Name |Type |Default Value |Description |Node Color Scheme |List |neodash |The color scheme to use for the node labels. Colors are assigned automatically (consequitevely) to the different labels returned by the Cypher query. |Node Label Color |Text |black |The color of the labels drawn on the nodes. |Node Label Font Size |Number |3.5 |Size of the labels drawn on the nodes. |Node Size |Number |2 |Default size of a node in the graph visualization. This size is applied if no custom size styling is defined and no Rule-Based styling is active. |Node Size Property |Text |size |Optionally, the name of the node property to map to the node size. This lets you define sizes on a node-specific level, if you have a property that directly maps to the numeric size value. |Node Color Property |Text |color |Optionally, the name of the node property to map to the node color. This lets you define colors on a node-specific level, if you have a property that directly maps to the HTML color value. |Relationship Color |Text |#a0a0a0 |The color used for drawing the relationship arrows in the visualization. |Relationship Width |Text |1 |The (default) width of the relationship arrows in the visualization. |Relationship Label Color |Text |#a0a0a0 |The color of the labels (relationship type) drawn next to the relationship arrows. |Relationship Label Font Size |Text |2.75 |The font size of the labels (relationship type) drawn next to the relationship arrows. |Relationship Color Property |Text |color |Optionally, the name of the relationship property to map to the arrow color. This lets you define colors on a relationship-specific level, if you have a property that directly maps to the HTML color value. |Relationship Width Property |Text |width |Optionally, the name of the relationship property to map to the arrow width. This lets you define widths on a relationship-specific level, if you have a property that directly maps to the width value. |Animated Particles on Relationships |on/off |off |If enabled, draw relationships with animated particles on them, moving in the direction of the relationship. |Arrow head size |Number |3 |Use this to set the length of the arrow head, size is adjusted automatically. If 0, no arrow will be drawn. |Background Color |Text |#fafafa |The background color of the visualization. |Layout (experimental) |List |force-directed |tree-top-down |tree-bottom-up |tree-left-right |tree-right-left |radial | Use this to switch from the main (force-directed) layout to one of the experimental layouts (tree, radial). For the experimental layouts, make sure your graph is a DAG (directed acyclic graph). | Graph Depth Separation | Number | 30 | Specify the level distance for the tree layout. This 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. |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. |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. |Show pop-up on Hover |on/off |on |if enabled, shows a pop-up when a user hovers over one of the nodes/relationships in the visualization. The pop-up contains the label and properties of the node/relationship. |Show properties on Click |on/off |on |if enabled, opens up a window when a user clicks on one of the nodes/relationships in the visualization. The window contains the label and properties of the node/relationship. |Fix node positions after drag |on/off |on |If enabled, locks in (freezes) the node positions after a user drags them. |Drilldown Link |Text (URL) |(no value) |Specifying a URL here will display a floating button on the top right of the visualization. This button can be used to drilldown into a different tool (e.g. Bloom) so that the graph can be explored further. Dynamic Dashboard Parameters (e.g. $neodash_person_name) can be used in these links as well. |Hide Selections |on/off |off |If enabled, hides the property selector (footer of the visualization). |Override no data message |Text |Query returned no data. |Override the message displayed to the user when their query returns no data. |Auto-run query |on/off |on |when activated automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== == Rule-Based Styling Using the link:../#_rule_based_styling[Rule-Based Styling] menu, the following style rules can be applied to the graph: - The background color of a node. - The label color of a node. ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/graph3d.adoc ================================================ = 3D Graph include::../../banner.adoc[] link:../../extensions/advanced-visualizations[label:Advanced Visualization[]] The 3D graph report extends the default graph visualization with another dimension. It supports most of the features & customizations for the regular (2D) graph, including rule-based styling and report actions. Users can explore the 3D graph by zooming and panning through 3D space. == Examples === Basic Graph .... MATCH (p:Person)-[a:ACTED_IN]->(m:Movie) WHERE m.title = 'The Matrix' RETURN p, a, m .... image::graph3d.png[Basic 3D Graph] == Virtual Graph .... MATCH (p:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person) WHERE m.title = "The Matrix" RETURN p, p2, apoc.create.vRelationship(p, "KNOWS", {}, p2) .... image::graph3dvirtual.png[Virtual 3D Graph] == Advanced Settings [width="100%",cols="12%,2%,3%,83%",options="header",] |=== |Name |Type |Default Value |Description |Node Color Scheme |List |neodash |The color scheme to use for the node labels. Colors are assigned automatically (consequitevely) to the different labels returned by the Cypher query. |Node Label Color |Text |black |The color of the labels drawn on the nodes. |Node Label Font Size |Number |3.5 |Size of the labels drawn on the nodes. |Node Size |Number |2 |Default size of a node in the graph visualization. This size is applied if no custom size styling is defined and no Rule-Based styling is active. |Node Size Property |Text |size |Optionally, the name of the node property to map to the node size. This lets you define sizes on a node-specific level, if you have a property that directly maps to the numeric size value. |Node Color Property |Text |color |Optionally, the name of the node property to map to the node color. This lets you define colors on a node-specific level, if you have a property that directly maps to the HTML color value. |Relationship Color |Text |#a0a0a0 |The color used for drawing the relationship arrows in the visualization. |Relationship Width |Text |1 |The (default) width of the relationship arrows in the visualization. |Relationship Label Color |Text |#a0a0a0 |The color of the labels (relationship type) drawn next to the relationship arrows. |Relationship Label Font Size |Text |2.75 |The font size of the labels (relationship type) drawn next to the relationship arrows. |Relationship Color Property |Text |color |Optionally, the name of the relationship property to map to the arrow color. This lets you define colors on a relationship-specific level, if you have a property that directly maps to the HTML color value. |Relationship Width Property |Text |width |Optionally, the name of the relationship property to map to the arrow width. This lets you define widths on a relationship-specific level, if you have a property that directly maps to the width value. |Animated Particles on Relationships |on/off |off |If enabled, draw relationships with animated particles on them, moving in the direction of the relationship. |Arrow head size |Number |3 |Use this to set the length of the arrow head, size is adjusted automatically. If 0, no arrow will be drawn. |Background Color |Text |#fafafa |The background color of the visualization. |Layout (experimental) |List |force-directed |tree-top-down |tree-bottom-up |tree-left-right |tree-right-left |radial | Use this to switch from the main (force-directed) layout to one of the experimental layouts (tree, radial). For the experimental layouts, make sure your graph is a DAG (directed acyclic graph). | Graph Depth Separation | Number | 30 | Specify the level distance for the tree layout. This 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. |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. |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. |Show pop-up on Hover |on/off |on |if enabled, shows a pop-up when a user hovers over one of the nodes/relationships in the visualization. The pop-up contains the label and properties of the node/relationship. |Show properties on Click |on/off |on |if enabled, opens up a window when a user clicks on one of the nodes/relationships in the visualization. The window contains the label and properties of the node/relationship. |Drilldown Link |Text (URL) |(no value) |Specifying a URL here will display a floating button on the top right of the visualization. This button can be used to drilldown into a different tool (e.g. Bloom) so that the graph can be explored further. Dynamic Dashboard Parameters (e.g. $neodash_person_name) can be used in these links as well. |Hide Selections |on/off |off |If enabled, hides the property selector (footer of the visualization). |Override no data message |Text |Query returned no data. |Override the message displayed to the user when their query returns no data. |Auto-run query |on/off |on |when activated automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== == Rule-Based Styling Using the link:../#_rule_based_styling[Rule-Based Styling] menu, the following style rules can be applied to the graph: - The background color of a node. - The label color of a node. ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/iframe.adoc ================================================ = iFrame include::../../banner.adoc[] An iFrame report lets you embed a webpage inside your NeoDash dashboard. The page can be loaded from any web address starting with `http://` or `https://`, with some exceptions*. ____ The webpage must not explicitly disallow itself to be embedded, such as https://google.com. ____ To render iFrames interactively based on the dashboard state, your global dashboard parameters can be passed into it dynamically. See the *Advanced Settings* below for more information. == Examples === Basic iFrame image::iframe.png[Basic iFrame] === Dynamic iFrame image::iframe2.png[Dynamic iFrame] == Advanced Settings [width="100%",cols="19%,17%,26%,38%",options="header",] |=== |Name |Type |Default Value |Description |Replace global parameters in URL |on/off |on |If enabled, replaces all instances of query parameters (e.g. $neodash_person_name) inside the iFrame URL. |Append global parameters to iFrame URL |on/off |off |If enabled, appends the full list of global parameters as URL parameters to the specified URL. |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. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/index.adoc ================================================ = Reports include::../../banner.adoc[] A report is the smallest building build of your dashboard. Each report will have a single Cypher query behind it that is used to populate the report. Reports can be of several types (graph, table, bar chart, etc.), each of which expect different types of data. See the relevant documentation pages for more information. A report can be given a title, which will be displayed in the dashboard header. To change the query of a report, open the settings by clicking the (⋮) icon on the top right of the report. image::report.gif[Report] The settings window additionally allows you to change the type of report, the refresh rate of the report, and a number of *Advanced Settings*. The advanced settings differ between the different report types, and can be viewed by toggling the switch on the bottom left of the settings page. == Create and Delete Reports A new report can be added to a page by clicking the large (+) button at the end of the page. By default, a report will have nothing defined, so you will need to set the query before any data is visualized. Reports can be deleted by opening the report settings, and clicking the 🗑️ icon in the report header. == Re-order Reports As of NeoDash 2.1, reports can be re-ordered by dragging and dropping them around the page. To move a report, grab it by the handle (top left corner), and drag it to the desired location. image::movereport.gif[Report] == Resize Reports As of NeoDash 2.1, reports can be resized by grabbing their bottom-right corner, and dragging your mouse to the desired size. image::resizereport.gif[Report] == Writing Queries A single Cypher query is used to populate each report. As any Cypher syntax is supported, this includes https://neo4j.com/developer/neo4j-apoc/[APOC], https://neo4j.com/docs/graph-data-science/current/[GDS], and even https://neo4j.com/docs/operations-manual/current/fabric/queries/[Fabric]! Keep the following best practises in mind when writing your Cypher queries: 1. Always use a `LIMIT` in your query to keep the result size manageable. 2. Ensure you return the right data types for the right report type. For example, a graph report expects nodes and relationships, whereas a line chart expects numbers. == Row Limiting NeoDash has a built-in post-query *row limiter*. This means that results are truncated to a maximum number of rows, depending on the report type. The row limiter is in place to ensure that visualizations do not become too complex for the browser to handle. Note that even though the row limiter is enabled by default, rows are only limited after the query is executed. For this reason, it is recommended to use the `LIMIT` clause in your query at all times. == Parameters Parameters 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. In addition, **session parameters** are available based on the currently active database connection. |=== |Parameter | Description | $session_uri | The URI of the current active database connection. | $session_database | The Neo4j database that was connected to when the user logged in. | $session_username | The username used to authenticate to Neo4j. |=== == Report Types To learn more about a specific report type, see one of the following pages: - link:table[Table] - link:graph[Graph] - link:bar-chart[Bar Chart] - link:pie-chart[Pie Chart] - link:line-chart[Line Chart] - link:graph3d[3D Graph] - link:sunburst[Sunburst] - link:circle-packing[Circle Packing] - link:treemap[Treemap] - link:radar[Radar Chart] - link:map[Map] - link:choropleth[Choropleth Chart] - link:areamap[Area Map] - link:single-value[Single Value] - link:sankey[Sankey Chart] - link:gantt[Gantt Chart] - link:gauge[Gauge Chart] - link:raw-json[Raw JSON] - link:parameter-select[Parameter Select] - link:form[Form] - link:iframe[iFrame] - link:markdown[Markdown] ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/line-chart.adoc ================================================ = Line Chart include::../../banner.adoc[] A line chart can be used to draw one or more lines in a two-dimensional plane. It requires two numeric fields: * *X-value*: a numeric field. These will be the values used as an X-coordinate. * *Y-Value*: a numeric field. These will be the values used as an Y-coordinate. Always ensure that the X-value are sorted in ascending order. If not, the chart will not be displayed correctly. The line chart supports plotting both simple numbers and time values on the x-axis. If you select a Neo4j datetime property on the x-axis, the chart will automatically be drawn as a time series. == Examples === Basic Line Chart (Actors born by decade) .... MATCH (p:Person) RETURN (p.born/10)*10 as Decade, COUNT(p) as People ORDER BY Decade ASC .... image::line1.png[Basic Line Chart] === Multi-Line Chart (Actors born & movies released by decade) .... MATCH (p:Person) WITH (p.born/10)*10 as Decade, COUNT(p) as People ORDER BY Decade ASC MATCH (m:Movie) WHERE (m.released/10)*10 = Decade RETURN Decade, People, COUNT(DISTINCT m) as Movies .... image::line2.png[Multi Line Chart] == Advanced Settings [width="100%",cols="13%,2%,6%,79%",options="header",] |=== |Name |Type |Default Value |Description |Plot Type |List |line | Whether to use a line plot (with connections) or a scatter plot of disjointed points |Show Legend |on/off |off |If enabled, shows a legend at the top right of the visualization. |Color Scheme |List |neodash |The color scheme to use for the lines. Colors are assigned automatically to the different fields selected in the report footer. |X Scale |List |linear |How to scale the values on the x-axis. Can be either linear, logarithmic or point. Use point for categorical data. |Y Scale |List |linear |How to scale the values on the y-axis. Can be either linear or logarithmic. |Min X Value |Number |auto |If not set to ``auto'', this variable is the minimum value on the x-axis. |Max X Value |Number |auto |If not set to ``auto'', this variable is the maximum value on the x-axis. |Min Y Value |Number |auto |If not set to ``auto'', this variable is the minimum value on the y-axis. |Max Y Value |Number |auto |If not set to ``auto'', this variable is the maximum value on the y-axis. |X-axis Tick Count |Number |auto |If not set to ``auto'', the number of ticks to be made on the x-axis. This is an approximate number that the visualization tries to adhere to (numeric X-axis only) |X-axis Format (Time chart) |Text |%Y-%m-%dT%H:%M:%SZ |When using a time chart, this setting lets you override how time values are rendered on the x-axis. This uses the ISO 8601 time notations. |X-axis Tick Size (Time chart) |Text |every 1 year |When using a time chart, this setting helps you set the frequency of ticks. The text format should look like this: `"every [number] ['years', 'months', 'weeks', 'days', 'hours', 'seconds', 'milliseconds']"`. |Line Smoothing |List |linear |Determines how the lines in the chart are smoothened. One of linear (no smoothing), basis (interpolating), cardinal (through each point) and step (step-based interpolation). |X-axis Tick Rotation (Degrees) |number |0 | The angle at which the tick labels on the x-axis are rotated. |Y-axis Tick Rotation (Degrees) |number |0 | The angle at which the tick labels on the y-axis are rotated. |Show Grid |on/off |on |If enabled, shows a grid in the line chart that intersects at the axis ticks. |Point Radius (px) |number |10 |The size of a point on each line. |Line Width (px) |number |2 |The width (in pixels) of each line in the chart. |Margin Left (px) |number |50 |The margin in pixels on the left side of the visualization. |Margin Right (px) |number |24 |The margin in pixels on the right side of the visualization. |Margin Top (px) |number |24 |The margin in pixels on the top side of the visualization. |Margin Bottom (px) |number |40 |The margin in pixels on the bottom side of the visualization. |Legend Width (px) |number |128 |The width in pixels of each legend label on top of the visualization (if enabled). |Hide Property Selection |on/off |off |If enabled, hides the property selector (footer of the visualization). |Auto-run query |on/off |on |when activated automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== == Rule-Based Styling Using the link:../#_rule_based_styling[Rule-Based Styling] menu, the following style rules can be applied to the line chart: - The color of the line. ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/map.adoc ================================================ = Map include::../../banner.adoc[] The map report will render all returned nodes, relationships and paths on a geomap. https://www.openstreetmap.org[Open Street Map] is used to visualize the data on the map. Map visualizations work best with https://neo4j.com/docs/cypher-manual/current/syntax/spatial/#cypher-spatial-specifying-spatial-instants[Neo4j Spatial Data]. Make sure that the nodes in your database have their locations stored as a spatial property. Customizations are available to change several parts of the visualization, including the label for each node as well as the colors and sizes of the markers/lines. The nodes can also automatically cluster together and expand depending on the level of zoom. A heatmap mode is also available. == Examples === Nodes on a map ____ Note that the nodes returned here have https://neo4j.com/docs/cypher-manual/current/syntax/spatial/[spatial] properties on them, so they can be visualized on a map. ____ .... MATCH (b:Brewery) RETURN b .... image::map.png[Basic Map] === Nodes and relationships on a map .... MATCH (b:Brewery)-[e]->(b2:Brewery) RETURN b, e, b2 .... image::map2.png[Relationships on a Map] === Clustered nodes on a map .... MATCH (b:Brewery) RETURN b .... image::map_cluster.png[Clustered nodes on a map] === Heatmap .... MATCH (b:Brewery) RETURN b .... image::map_heatmap.png[Heatmap] === Artificial map data By returning a dictionary instead of a node directly, you can work around the visualization expecting nodes and relationships directly. .... MATCH (l1:Location)<--(a:Person), (a:Person)-[:KNOWS]-(b:Person), (b:Person)-->(l2:Location) RETURN {id: a.name, label: "Person", point: l1.point}, {id: b.name, label: "Person", point: l2.point}, {start: a.name, end: b.name, type: "KNOWS", id: 1} .... image::map3.png[Artificial Map Data] == Advanced Settings [width="100%",cols="19%,17%,26%,38%",options="header",] |=== |Name |Type |Default Value |Description |Layer Type |List |markers |Allows you to choose between the standard map with markers, or a heatmap. |Cluster markers |on/off |off |Whether to automatically cluster and expand the markers on the map. |Node Color Scheme |List |neodash |The color scheme to use for the node labels. Colors are assigned automatically (consequitevely) to the different labels returned by the Cypher query. |Node Marker Size |List |large |The size of the markers for the nodes on the map. One of [small, medium, large]. |Node Color Property |Text |color |Optionally, the name of the node property to map to the node color. This lets you define colors on a node-specific level, if you have a property that directly maps to the HTML color value. |Relationship Color |Text |#a0a0a0 |The color used for drawing the relationships on the map. |Relationship Width |Text |1 |The (default) width of the relationships on the map. |Relationship Color Property |Text |color |Optionally, the name of the relationship property to map to the relationship color. This lets you define colors on a relationship-specific level, if you have a property that directly maps to the HTML color value. |Relationship Width Property |Text |width |Optionally, the name of the relationship property to map to the arrow width. This lets you define widths on a relationship-specific level, if you have a property that directly maps to the width value. |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. |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. |Hide Property Selection |on/off |off |If enabled, hides the property selector (footer of the visualization). |Auto-run query |on/off |on |when activated automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== == Rule-Based Styling Using the link:../#_rule_based_styling[Rule-Based Styling] menu, the following style rules can be applied to the map: - The color of a node marker. ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/markdown.adoc ================================================ = Markdown include::../../banner.adoc[] Markdown reports let you specify https://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] text, to be renderer as rich HTML. This lets you turn your dashboards into a storybook with textual descriptions, hyperlinks, images and videos. To use dashboard parameters in Markdown, turn on the `Replace global parameters in Markdown` setting. Then, include a variable surrounded by backticks inside the markdown string. For example: ``` == This is a title My variable is equal to `$neodash_person_object['name']` ``` == Examples === Basic Markdown .... ## Hello there! I'm a **Markdown** file. Check out this cool image: ![image](https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/how-to-keep-ducks-call-ducks-1615457181.jpg?resize=240:*) .... image::markdown.png[Basic Markdown] == Advanced Settings [width="100%",cols="19%,17%,26%,38%",options="header",] |=== |Name |Type |Default Value |Description |Replace global parameters in Markdown |on/off |on |If enabled, replaces all instances of query parameters (e.g. $neodash_person_name) inside the markdown source. |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. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/parameter-select.adoc ================================================ = Parameter Select include::../../banner.adoc[] Parameter select reports provide you with an easy way to add interactivity into your dashboards. Simply put, a parameter select report lets users set a Neo4j query parameter (e.g. *$neodash_person_name*) dynamically. This means that your reports can be created to show different data depending on the value of a parameter. There are five types of parameter select reports: - Node property-based selections - Relationship property-based selections - Free text selections - Date picker selections - Custom query selections == Examples === Node Property Select A node property selector lets users choose a property from a node with a given label, to be used as a parameter in the dashboard. image::select.png[Node Property Select] === Relationship Property Select A relationship property selector lets users choose a property from a relationship with a given type, to be used as a parameter in the dashboard. image::select2.png[Relationship Property Select] === Free Text Select A free text selectors lets users enter any string value, which can then be used as a parameter inside dashboard queries. image::select3.png[Free Text Select] == Date Select A date selector lets users specify dates using a calendar widget, or by entering a date format. image::select4.png[Free Text Select] == Custom Query Select A 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. image::select5.png[Custom Query Select] == Advanced Settings [width="100%",cols="19%,17%,26%,38%",options="header",] |=== |Name |Type |Default Value |Description |Clear Parameter on Field Reset |on/off |off |If enabled, removes the global parameter completely when the field is cleared. This may break some visualizations. If disabled, sets the parameter value to “” (empty string) when the input field is cleared. |Multiple Selection |on/off |off |If enabled, allows user to select multiple choices. Parameter will be then an array of selections. |Manual Parameter Save |on/off |off |If enabled, adds a confirmation button in order to propagate the selection into the dashboard parameter. |Enable Manual Label/Property Name Specification |on/off |off |If enabled, does not enforce you to select a node label/property using an auto-complete field, instead, you can enter any value. This is useful for large datasets where the autocomplete field is too slow to render. |Helper Text (Override) |Text |(none) |Text to show above the user input field. This will override the autogenerated text from the node/relationship property pair. |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. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/pie-chart.adoc ================================================ = Pie Chart include::../../banner.adoc[] A pie chart will draw categories and values in a circular disc layout. The pie chart will require you to choose the following selections: * *Category*: a text field. These will be the labels on the pie slices. * *Value*: a numeric field. This will be the size of the slices. The pie chart can be additionally be customized to become a donut chart, show categories as a legend, and to show the percentage of the total value of the pie. See *Advanced Settings* for more information. == Examples === Basic Pie Chart [source,cypher] ---- Match (p:Person)-[e]->(m:Movie) RETURN m.title as Title, COUNT(p) as People LIMIT 8 ---- image::pie.png[Pie Chart] === Donut Chart [source,cypher] ---- MATCH (p:Person)-[e]->(m:Movie) WHERE m.title = "Cloud Atlas" WITH TYPE(e) as Role RETURN Role, COUNT(Role) as Count ---- image::piedonut.png[Donut Chart] == Advanced Settings [width="100%",cols="15%,2%,6%,77%",options="header",] |=== |Name |Type |Default Value |Description |Show Legend |on/off |off |If enabled, shows a legend on the bottom of the visualization. |Auto-sort slices by value |on/off |off |If enabled, automatically sorts the pie slices in order of size. |Show Values in Slices |on/off |off |If enabled, show the category values inside the pie slices. |Labels font Size |Number |13 |Define the size of the font for internal and external labels on the pie. |Show categories next to Slices |on/off |off |If enabled, show the category values next to the pie slices. |Enable interactivity |on/off |on |If enabled, turn on animations when a user hovers over a pie slice. |Color Scheme |List | |The color scheme to use for the slices. Colors are assigned automatically (consequitevely) to the different categories returned by the Cypher query. |Pie inner radius |Number |0 |The radius of the ``donut hole'' inside the pie. When set to zero, no hole is present, when set to 0.99, the pie will be almost completely visualized as a thin disc. |Slice padding angle (degrees) |number |0 |the angle between each pie slice reserved for white space. For example, when set to 3.6, there will be 1/100th of the total circle space reserved between each slice. |Slice border with (px) |number |0 |The width of the border of each slice. |Margin Left (px) |number |50 |The margin in pixels on the left side of the visualization. |Margin Right (px) |number |50 |The margin in pixels on the right side of the visualization. |Margin Top (px) |number |50 |The margin in pixels on the top side of the visualization. |Margin Bottom (px) |number |50 |The margin in pixels on the bottom side of the visualization. |Hide Selections |on/off |off |If enabled, hides the property selector (footer of the visualization). |Auto-run query |on/off |on |when activated automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== == Rule-Based Styling Using the link:../#_rule_based_styling[Rule-Based Styling] menu, the following style rules can be applied to the pie chart: - The background color of a pie slice. ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/radar.adoc ================================================ = Radar Chart include::../../banner.adoc[] link:../../extensions/advanced-visualizations[label:Advanced Visualization[]] A Radar chart 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. A radar chart expects a single index, to which a list of numeric fields can be linked. == Examples === Basic Radar [source,cypher] ---- MATCH (s:Skill) MATCH (:Player{name:"Messi"})-[h1:HAS_SKILL]->(s) MATCH (:Player{name:"Mbappe"})-[h2:HAS_SKILL]->(s) MATCH (:Player{name:"Benzema"})-[h3:HAS_SKILL]->(s) MATCH (:Player{name:"C Ronaldo"})-[h4:HAS_SKILL]->(s) MATCH (:Player{name:"Lewandowski"})-[h5:HAS_SKILL]->(s) RETURN s.name as Skill, h1.value as Messi, h2.value as Mbappe, h3.value as Benzema, h4.value as `C Ronaldo`, h5.value as Lewandowski ---- image::radar.png[Radar Chart] == Advanced Settings [width="100%",cols="15%,2%,26%,57%",options="header",] |=== |Name |Type |Default Value |Description |Enable interactivity |on/off |on |If enabled, turn on animations when a user hovers over a layer. |Show Legend |on/off |off |If enabled, shows a legend on the bottom of the visualization. |Color Scheme |List | |The color scheme to use for the Radar. Each polygon will have a color from the list. |Margin Left (px) |number |24 |The margin in pixels on the left side of the visualization. |Margin Right (px) |number |24 |The margin in pixels on the right side of the visualization. |Margin Top (px) |number |24 |The margin in pixels on the top side of the visualization. |Margin Bottom (px) |number |40 |The margin in pixels on the bottom side of the visualization. |Dot Size |number |10 |Size of the dots (px). |Dot Border Width |number |2 |Width of the dots border (px). |Grid Levels |number |5 |Number of levels to display for grid. |Grid Label Offset (px) |number |16 |Label offset from outer radius (px) |Blend Mode |List |normal |This will define CSS mix-blend-mode for layers |Motion Configuration |List |gentle |This parameter will select the motion config for react-spring. |Curve |List |linearClosed |This parameter will select the type of curve interpolation. |Auto-run query |on/off |on |When activated, automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/raw-json.adoc ================================================ = Raw JSON include::../../banner.adoc[] The Raw JSON report renders the JSON response received from Neo4j directly. This is typically used for debugging queries, or, understanding the exact data types being returned from Neo4j. == Examples === Raw JSON .... MATCH (n) RETURN COUNT(n) .... image::json.png[Basic Value] == Advanced Settings [width="100%",cols="19%,17%,26%,38%",options="header",] |=== |Name |Type |Default Value |Description |Auto-run query |on/off |on |when activated automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/sankey.adoc ================================================ = Sankey Chart include::../../banner.adoc[] link:../../extensions/advanced-visualizations[label:Advanced Visualization[]] A Sankey visualization will generate a flow diagram from nodes and links. Beware that cyclic dependencies are not supported. == Examples === Basic Sankey Chart For a sankey chart to use the correct relationship weights - it is mandatory to set a 'Relationship Property' in the report's advanced settings. [source,cypher] ---- MATCH (p:Person)-[r:RATES]->(m:Movie) RETURN p, r, m ---- image::sankey.png[Sankey Chart] == Advanced Settings [width="100%",cols="15%,2%,6%,77%",options="header",] |=== |Name |Type |Default Value |Description |Show Legend |on/off |off |If enabled, shows a legend on the bottom of the visualization. |Enable interactivity |on/off |on |If enabled, turn on animations when a user hovers over a node or link. |Relationship Property |text | value | Name of the property with an integer value that is going to be used to determine thickness of links. Using 'SANKEY_UNWEIGHTED', this sankey will assume every relationship with a weight of 1. |Color Scheme |List | |The color scheme to use for the slices. Colors are assigned automatically (consecutively) to the different categories returned by the Cypher query. |Layout |List |horizontal |Sankey layout direction. |Label Position |List |inside |Control sankey label position. |Label Orientation |List |horizontal |Control sankey label orientation. |Node Border Width (px) |number |0 |Controls Node border width. |Node Spacing (px) |number |18 |Controls spacing between nodes at an identical level (px). |Node thickness (px) |number |18 |Controls Node thickness. |Margin Left (px) |number |24 |The margin in pixels on the left side of the visualization. |Margin Right (px) |number |24 |The margin in pixels on the right side of the visualization. |Margin Top (px) |number |24 |The margin in pixels on the top side of the visualization. |Margin Bottom (px) |number |40 |The margin in pixels on the bottom side of the visualization. |Auto-run query |on/off |on |when activated automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/single-value.adoc ================================================ = Single Value include::../../banner.adoc[] A single value report will render the first column of the first row returned by the Cypher query. Single value reports are typically used for key metrics: - The total number of nodes - The total number of data integrity violations - The name of a node or relationship that is standing out in the data. == Examples === Number value .... MATCH (n) RETURN COUNT(n) .... image::value.png[Basic Value] === Text value with custom styling .... // Who's the biggest Fraudster? MATCH (n:Person)-[:CREATED]->(t:Transaction{fraud:true}) RETURN n.name, COUNT(t) ORDER BY COUNT(t) DESC .... image::value2.png[Styled Value] == Advanced Settings [width="100%",cols="10%,3%,29%,58%",options="header",] |=== |Name |Type |Default Value |Description |Font Size |Number |64 |The font size of the value text. |Color |Text |rgba(0, 0, 0, 0.87) |The HTML color value of the text. |Background Color |Text | white |The HTML color value of the background of the report. |Horizontal Align |List |left |The horizontal alignment of the text. |Vertical Align |List |top |The vertical alignment of the text. |Auto-run query |on/off |on |when activated automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== == Rule-Based Styling Using the link:../#_rule_based_styling[Rule-Based Styling] menu, the following style rules can be applied to the map: - The color of the text. ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/sunburst.adoc ================================================ = Sunburst include::../../banner.adoc[] link:../../extensions/advanced-visualizations[label:Advanced Visualization[]] A sunburst chart will render hierarchical data in a multi-level pie visualization. It takes two fields: - *Path*: a list of strings. This represents the hierarchy (from highest to lowest level). - *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. == Examples === Basic Sunburst Chart [source,cypher] ---- 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 ---- image::sunburst.png[Sunburst Chart] == Advanced Settings [width="100%",cols="19%,2%,26%,53%",options="header",] |=== |Name |Type |Default Value |Description |Show Values on Arcs |on/off |off |If enabled, show the category values inside the sunburst arcs. |Enable interactivity |on/off |on |If enabled, turn on animations when a user hovers over an arc. |Color Scheme |List | |The color scheme to use for the arcs. Colors are assigned automatically for each of the sub-hierarchies. |Arc border width (px) |number |0 |The width of the border of each arc. |Margin Left (px) |number |24 |The margin in pixels on the left side of the visualization. |Margin Right (px) |number |24 |The margin in pixels on the right side of the visualization. |Margin Top (px) |number |24 |The margin in pixels on the top side of the visualization. |Margin Bottom (px) |number |40 |The margin in pixels on the bottom side of the visualization. |Minimum Arc Angle for Label (degrees) |number |10 |The minimum angle of an arc needed to display a label (if labels are enabled). |Slice Corner Radius |number |3 |The rounding angle of each of the arcs in the visualization. |Inherit color from parent |on/off |on |If enabled, starting from level 2, each level will inherit the same color of his parent. If disabled, color will be randomly assigned based on the color scheme. |Auto-run query |on/off |on |When activated, automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/table.adoc ================================================ = Table include::../../banner.adoc[] The most common report in a dashboard is often a simple table view. NeoDash contains a powerful table component that can render all the data returned by a Cypher query. This includes simple data like numbers or text, but also Neo4j native data like nodes, relationships, and paths. The table report supports the following additional features: - automatic pagination of results. - Sorting/filtering by clicking on the table headers. - Prefixing a column header with __ (double underscore) will make the column hidden - Downloading your data as a CSV file. Double-clicking on a table cell will copy that cell's value to the user's clipboard. == Examples === Basic Table .... MATCH (n:Movie)<-[:ACTED_IN]-(p:Person) RETURN n.title AS Title, n.released AS Released, count(p) as Actors .... image::table1.png[Basic Table] === Table with nodes / collections .... MATCH (n:Movie)<-[:ACTED_IN]-(p:Person) RETURN n, collect(p.name) as actors LIMIT 200 .... image::table2.png[Table with nodes / collections] == Advanced Settings [width="100%",cols="12%,6%,26%,56%",options="header",] |=== |Name |Type |Default Value |Description |Transpose Rows & Columns |on/off |off |when activated, transposes the rows and columns of the table. This means that each of the returned rows from Neo4j will be shown as a column instead of a row. |Compact Table |on/off |off |When activated, makes the rows half height and increase the number of rows per page accordingly. |Relative Column Sizes |List of numbers |[1, 1, 1, …] |The relative width between each of the columns in the table. For example, if the first column should be twice the width of the 2nd and 3rd, this will be set to ``[2, 1, 1]''. |Enable CSV Download |on/off |off |when activated, displays a button on the bottom right of the table footer. This button lets the user download the complete set of table results (all pages) as a CSV file. |Override no data message |Text |Query returned no data. |Override the message displayed to the user when their query returns no data. |Auto-run query |on/off |on |when activated automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== == Rule-Based Styling Using the link:../#_rule_based_styling[Rule-Based Styling] menu, the following style rules can be applied to the table: - The background color of an entire row in a table. - The text color of an entire row in a table. - The background color of a single cell in the table. - The text color of a single cell in the table. If a column is hidden (header prefixed with __ double underscore), it can still be used as an entry point for a styling rule. == Report Actions With the link:../../extensions/report-actions[Report Actions] extension, tables can be turned into interactive components that set parameters. Two flavours of report actions for tables exist: === 1. Select a value from a row Adding a **Cell Click** action to a table column, turns the values in that row into clickable buttons. When the user clicks on the button, a predefined parameter is set to one of the columns in that row. image::select-single-table.png[Select a value from a table to be used as a parameter] === 2. Select multiple from a row Adding a **Row Clicked** action to a table prepends each row with a checkbox. The user can then check one or more boxes to update a dashboard parameter. > 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. image::select-multiple-table.png[Select multiple values to be used as a parameter] ================================================ FILE: docs/modules/ROOT/pages/user-guide/reports/treemap.adoc ================================================ = Treemap include::../../banner.adoc[] link:../../extensions/advanced-visualizations[label:Advanced Visualization[]] A treemap chart will render hierarchical data in a nested rectangle layout. It takes two fields: - *Path*: a list of strings. This represents the hierarchy (from highest to lowest level). - *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. == Examples === Basic Treemap [source,cypher] ---- 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 ---- image::treemap.png[Treemap Chart] == Advanced Settings [width="100%",cols="15%,2%,26%,57%",options="header",] |=== |Name |Type |Default Value |Description |Enable interactivity |on/off |on |If enabled, turn on animations when a user hovers over a rectangle. |Color Scheme |List | |The color scheme to use for the rectangle. Colors are assigned automatically for each of the sub-hierarchies. |Rectangle border width (px) |number |0 |The width of the border of each rectangle. |Margin Left (px) |number |24 |The margin in pixels on the left side of the visualization. |Margin Right (px) |number |24 |The margin in pixels on the right side of the visualization. |Margin Top (px) |number |24 |The margin in pixels on the top side of the visualization. |Margin Bottom (px) |number |40 |The margin in pixels on the bottom side of the visualization. |Auto-run query |on/off |on |When activated, automatically runs the query when the report is displayed. When set to `off', the query is displayed and will need to be executed manually. |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. |=== ================================================ FILE: docs/package.json ================================================ { "name": "docs", "version": "1.0.0", "description": "", "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "npm run dev", "dev": "node server.js & npm-watch preview", "preview": "antora preview.yml", "publish": "git push origin HEAD:publish" }, "watch": { "preview": { "patterns": [ "modules" ], "extensions": "adoc" } }, "dependencies": { "@antora/cli": "^3.1.1", "@antora/site-generator-default": "^3.1.1", "@neo4j-documentation/macros": "^1.0.0", "@neo4j-documentation/remote-include": "^1.0.0", "express": "^4.17.1", "npm-watch": "^0.11.0" } } ================================================ FILE: docs/preview.yml ================================================ site: title: NeoDash content: sources: - url: ../ start_path: docs branches: HEAD exclude: - '!**/_includes/*' - '!**/readme.adoc' - '!**/README.adoc' ui: bundle: url: https://static-content.neo4j.com/build/ui-bundle-latest.zip snapshot: true urls: html_extension_style: indexify asciidoc: extensions: - "@neo4j-documentation/remote-include" - "@neo4j-documentation/macros" attributes: page-theme: labs ================================================ FILE: docs/server.js ================================================ const express = require('express') const app = express() const version = "2.4" app.use(express.static('./build/site')) app.get('/', (req, res) => res.redirect('neodash/' + version)) app.listen(8000, () => console.log('📘 http://localhost:8000')) ================================================ FILE: gallery/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: gallery/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2023 Niels de Jong Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: gallery/README.md ================================================ # NeoDash Dashboard Gallery 🎨 This is the source code for the NeoDash dashboard gallery located at [https://neodash-gallery.graphapp.io](https://neodash-gallery.graphapp.io). ## Run the app locally This app is built with React, Tailwind and the Neo4j Design Language. - `yarn install` installs the dependencies. - `yarn start` runs the app in development mode. - `yarn build` builds the app for production. ## Contribute to the Gallery Want to add a dashboard to the gallery? Create an [issue on GitHub](https://github.com/neo4j-labs/neodash/issues) with the following information: - Your name. - A URL to your page (GitHub, LinkedIn, Personal Website, ...) - The name of your dashboard. - A one sentence description for the dashboard. - A screenshot of the main page. - A data dump of the Neo4j database populating the dashboard. - A list of 5 keywords. Keep in mind that the data you provide needs to be public data, as it will be accessible by anyone. ================================================ FILE: gallery/dashboards/assessment.json ================================================ { "title": "Graph Assessment", "version": "2.1", "settings": { "pagenumber": 1, "editable": true, "fullscreenEnabled": false, "parameters": { "neodash_customer_name": "Black Mesa" }, "extensions": ["core", "actions"] }, "pages": [ { "title": "Main Page", "reports": [ { "title": "Neo4j Graph Framework", "query": "https://dist.neo4j.com/wp-content/uploads/20220812112237/FrameworkBrighter.jpg\n\n", "width": 6, "height": 3, "x": 6, "y": 0, "type": "iframe", "selection": {}, "settings": {} }, { "title": "Neo4j Graph Framework", "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", "width": 6, "height": 3, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": {} } ] }, { "title": "Customer Summary", "reports": [ { "title": "Select Customer", "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", "width": 3, "height": 1, "x": 5, "y": 0, "type": "select", "selection": {}, "settings": { "type": "Node Property", "entityType": "Customer", "propertyType": "name", "parameterName": "neodash_customer_name" } }, { "title": "Overall Summary", "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", "width": 4, "height": 2, "x": 8, "y": 0, "type": "radar", "selection": { "index": "pillar", "values": ["current", "target"] }, "settings": {} }, { "title": "Use Case Assessment", "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", "width": 6, "height": 2, "x": 0, "y": 2, "type": "radar", "selection": { "index": "Topic", "values": ["current", "target"] }, "settings": {} }, { "title": "Operations", "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", "width": 6, "height": 2, "x": 6, "y": 2, "type": "radar", "selection": { "index": "Topic", "values": ["current", "target"] }, "settings": {} }, { "title": "Graph Readiness", "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", "width": 6, "height": 2, "x": 6, "y": 4, "type": "radar", "selection": { "index": "Topic", "values": ["current", "target"] }, "settings": {} }, { "title": "Number of responses", "query": "MATCH (c:Customer)-[:HAS]->(pr:Project)<-[r:ASSOCIATED]-(pa:ProjectAssessment)\nwhere c.name=$neodash_customer_name\nreturn count (r)\n\n\n", "width": 3, "height": 1, "x": 5, "y": 1, "type": "value", "selection": {}, "settings": {} }, { "title": "Customer List", "query": "match (c:Customer)-[:HAS]->(p:Project)\nreturn c.name as Customer, p.name as Project\norder by c.name\n\n\n\n", "width": 5, "height": 2, "x": 0, "y": 0, "type": "table", "selection": {}, "settings": { "actionsRules": [ { "condition": "Click", "field": "Customer", "value": "Customer", "customization": "set variable", "customizationValue": "customer_name" } ] } }, { "title": "Graph Development", "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", "width": 6, "height": 2, "x": 0, "y": 4, "type": "radar", "selection": { "index": "Topic", "values": ["current", "target"] }, "settings": {} } ] } ], "parameters": {} } ================================================ FILE: gallery/dashboards/bom-english.json ================================================ { "title": "BOM - Bill of Material", "version": "2.1", "settings": { "pagenumber": 0, "editable": true, "fullscreenEnabled": true, "parameters": { "neodash_supplier_name": "Audio Wizardry", "neodash_model_number": null, "neodash_model_name": "EveryRoad GPS Car Navigation Unit - Model 300 - US Edition", "neodash_model_name_1": "EveryRoad GPS Car Navigation Unit - Model 300 - US Edition", "neodash_model_name_2": "EveryRoad GPS Car Navigation Unit - Model 500 - UK Edition" } }, "pages": [ { "title": "Suppliers", "reports": [ { "x": 0, "y": 0, "title": "Data model", "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", "width": "6", "type": "graph", "height": 2, "selection": { "Model": "name", "Supplier": "name", "Component": "name" }, "settings": { "nodePositions": {} } }, { "x": 6, "y": 0, "title": "Suppliers", "query": "MATCH (s:Supplier) RETURN s.name AS `supplier name`\n\n\n", "width": 3, "type": "table", "height": 2, "selection": {}, "settings": { "nodePositions": {} } }, { "x": 9, "y": 0, "title": "Pick a supplier", "query": "MATCH (n:`Supplier`) \nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`name` as value LIMIT 5", "width": "3", "type": "select", "height": 2, "selection": {}, "settings": { "type": "Node Property", "entityType": "Supplier", "propertyType": "name", "parameterName": "neodash_supplier_name" } }, { "x": 0, "y": 2, "title": "Model connected to supplier", "query": "MATCH p=(m:Model)-[*]->(s:Supplier {name: $neodash_supplier_name}) return p\n\n\n", "width": "12", "type": "graph", "height": 2, "selection": { "Model": "name", "Component": "name", "Supplier": "name" }, "settings": { "nodePositions": {} } } ] }, { "title": "BOM models", "reports": [ { "x": 0, "y": 0, "title": "Models", "query": "MATCH (m:Model) RETURN m.number AS ID, m.name AS name\n\n\n", "width": "6", "type": "table", "height": 2, "selection": {}, "settings": { "nodePositions": {} } }, { "x": 6, "y": 0, "title": "Pick a model", "query": "MATCH (n:`Model`) \nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`name` as value LIMIT 5", "width": "6", "type": "select", "height": 2, "selection": {}, "settings": { "nodePositions": {}, "type": "Node Property", "entityType": "Model", "propertyType": "name", "parameterName": "neodash_model_name" } }, { "x": 0, "y": 2, "title": "BOM", "query": "MATCH path = (m:Model {name: $neodash_model_name})-[:HAS*]->(:Component)\nRETURN path\n\n", "width": "6", "type": "graph", "height": 2, "selection": { "Model": "name", "Component": "number" }, "settings": { "nodePositions": {}, "nodeColorScheme": "neodash", "layout": "tree" } }, { "x": 6, "y": 2, "title": "Prices", "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", "width": "6", "type": "table", "height": 2, "selection": {}, "settings": { "nodePositions": {} } } ] }, { "title": " Model vs Model", "reports": [ { "x": 0, "y": 0, "title": "Models List", "query": "MATCH (m:Model) RETURN m.name AS name", "width": 7, "type": "table", "height": 2, "selection": {}, "settings": { "type": "Node Property", "entityType": "Model", "propertyType": "name", "parameterName": "neodash_model_name" } }, { "x": 7, "y": 0, "title": "Model 1", "query": "MATCH (n:`Model`) \nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`name` as value LIMIT 5", "width": 2, "type": "select", "height": 2, "selection": {}, "settings": { "type": "Node Property", "entityType": "Model", "propertyType": "name", "parameterName": "neodash_model_name_1", "id": "1" } }, { "x": 9, "y": 0, "title": "Model 2", "query": "MATCH (n:`Model`) \nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`name` as value LIMIT 5", "width": 2, "type": "select", "height": 2, "selection": {}, "settings": { "type": "Node Property", "entityType": "Model", "propertyType": "name", "parameterName": "neodash_model_name_2", "id": "2" } }, { "x": 7, "y": 2, "title": "Components in both", "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", "width": 4, "type": "table", "height": 2, "selection": {}, "settings": { "nodePositions": {} } }, { "x": 0, "y": 2, "title": "Components only in first", "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", "width": 4, "type": "table", "height": 2, "selection": {}, "settings": { "nodePositions": {} } }, { "x": 4, "y": 2, "title": "Similarity", "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", "width": 3, "type": "pie", "height": 2, "selection": { "index": "name", "value": "cardinality", "key": "(none)" }, "settings": { "nodePositions": {} } } ] } ], "parameters": {} } ================================================ FILE: gallery/dashboards/bom.json ================================================ { "title": "BOM - Lista de materiales", "version": "2.1", "settings": { "pagenumber": 0, "editable": true, "fullscreenEnabled": true, "parameters": { "neodash_supplier_name": "Manchester Manufacturing", "neodash_model_number": null, "neodash_model_name": "EveryRoad GPS Car Navigation Unit - Model 500 - UK Edition", "neodash_model_name_1": "EveryRoad GPS Car Navigation Unit - Model 300 - US Edition", "neodash_model_name_2": "EveryRoad GPS Car Navigation Unit - Model 500 - UK Edition" } }, "pages": [ { "title": "Provedores", "reports": [ { "x": 0, "y": 0, "title": "Data Model", "query": "CALL db.schema.visualization();\n\n\n", "width": 6, "type": "graph", "height": 2, "selection": { "Model": "name", "Supplier": "name", "Component": "name" }, "settings": { "nodePositions": {} } }, { "x": 6, "y": 0, "title": "Provedores", "query": "MATCH (s:Supplier) RETURN s.name\n\n\n", "width": 3, "type": "table", "height": 2, "selection": {}, "settings": { "nodePositions": {} } }, { "x": 9, "y": 0, "title": "Elija un provedor", "query": "MATCH (n:`Supplier`) \nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`name` as value LIMIT 5", "width": 3, "type": "select", "height": 2, "selection": {}, "settings": { "type": "Node Property", "entityType": "Supplier", "propertyType": "name", "parameterName": "neodash_supplier_name" } }, { "x": 0, "y": 2, "title": "Modelos conectados a provedor", "query": "MATCH p=(m:Model)-[*]->(s:Supplier {name: $neodash_supplier_name}) return p\n\n\n", "width": 12, "type": "graph", "height": 2, "selection": { "Model": "name", "Component": "name", "Supplier": "name" }, "settings": { "nodePositions": {} } } ] }, { "title": "BOM Modelos", "reports": [ { "x": 0, "y": 0, "title": "Modelos", "query": "MATCH (m:Model) RETURN m.number AS ID, m.name AS name\n\n\n", "width": 6, "type": "table", "height": 2, "selection": {}, "settings": { "nodePositions": {} } }, { "x": 6, "y": 0, "title": "Elija un modelo de producto", "query": "MATCH (n:`Model`) \nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`name` as value LIMIT 5", "width": 6, "type": "select", "height": 2, "selection": {}, "settings": { "nodePositions": {}, "type": "Node Property", "entityType": "Model", "propertyType": "name", "parameterName": "neodash_model_name" } }, { "x": 6, "y": 2, "title": "BOM", "query": "MATCH path = (m:Model {name: $neodash_model_name})-[:HAS*]->(:Component)\nRETURN path\n\n", "width": 6, "type": "graph", "height": 2, "selection": { "Model": "name", "Component": "number" }, "settings": { "nodePositions": {}, "nodeColorScheme": "neodash", "layout": "tree" } }, { "x": 0, "y": 2, "title": "Precios", "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", "width": 6, "type": "table", "height": 2, "selection": {}, "settings": { "nodePositions": {} } } ] }, { "title": "Comparar Modelos", "reports": [ { "x": 0, "y": 0, "title": "Lista de Modelos", "query": "MATCH (m:Model) RETURN m.name AS name", "width": 3, "type": "table", "height": 2, "selection": {}, "settings": { "type": "Node Property", "entityType": "Model", "propertyType": "name", "parameterName": "neodash_model_name" } }, { "x": 3, "y": 0, "title": "Modelo 1", "query": "MATCH (n:`Model`) \nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`name` as value LIMIT 5", "width": 4, "type": "select", "height": 2, "selection": {}, "settings": { "type": "Node Property", "entityType": "Model", "propertyType": "name", "parameterName": "neodash_model_name_1", "id": "1" } }, { "x": 7, "y": 0, "title": "Modelo 2", "query": "MATCH (n:`Model`) \nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`name` as value LIMIT 5", "width": 5, "type": "select", "height": 2, "selection": {}, "settings": { "type": "Node Property", "entityType": "Model", "propertyType": "name", "parameterName": "neodash_model_name_2", "id": "2" } }, { "x": 7, "y": 2, "title": "Components en ambos", "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", "width": 5, "type": "table", "height": 2, "selection": {}, "settings": { "nodePositions": {} } }, { "x": 0, "y": 2, "title": "Components en primero solo", "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", "width": 4, "type": "table", "height": 2, "selection": {}, "settings": { "nodePositions": {} } }, { "x": 4, "y": 2, "title": "Similaridad", "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", "width": 3, "type": "pie", "height": 2, "selection": { "index": "name", "value": "cardinality", "key": "(none)" }, "settings": { "nodePositions": {} } } ] } ] } ================================================ FILE: gallery/dashboards/citation.json ================================================ { "title": "Citation graph - Topic extraction, recommendation, and Bloom exploration", "version": "2.2", "settings": { "pagenumber": 0, "editable": true, "fullscreenEnabled": false, "parameters": { "neodash_community_property": "2892", "neodash_title": "Modeling of architectures with UML panel" }, "extensions": ["core", "actions"] }, "pages": [ { "title": "Overview", "reports": [ { "title": "Summary", "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.", "width": 3, "height": 2, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": {} }, { "title": "A sample of the graph with some citations", "query": "MATCH p=(n)-[e]->(m) RETURN n,e,m LIMIT 18\n\n\n", "width": 6, "height": 2, "x": 6, "y": 0, "type": "graph", "selection": { "Article": "title", "Author": "name", "Venue": "name" }, "settings": { "nodePositions": {} } }, { "title": "Data model", "query": "CALL db.schema.visualization()", "width": 3, "height": 2, "x": 3, "y": 0, "type": "graph", "selection": { "Venue": "name", "Article": "name", "Author": "name" }, "settings": { "nodePositions": {} } }, { "title": "Some articles", "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", "width": 9, "height": 2, "x": 3, "y": 2, "type": "table", "selection": {}, "settings": {} }, { "title": "Number of articles", "query": "MATCH (n:Article) RETURN count(n)\n\n", "width": 3, "height": 1, "x": 0, "y": 2, "type": "value", "selection": {}, "settings": {} }, { "title": "Number of citations", "query": "MATCH ()-[rel:CITED]->()\nRETURN count(rel)\n\n\n", "width": 3, "height": 1, "x": 0, "y": 3, "type": "value", "selection": {}, "settings": {} } ] }, { "title": "Do articles cluster together ?", "reports": [ { "title": "Article clusters in Bloom - set username \"citation\" and password \"citation\" to view the perspective.", "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", "width": 9, "height": 4, "x": 0, "y": 0, "type": "iframe", "selection": {}, "settings": {} }, { "title": "# of distinct clusters", "query": "MATCH (n:Article)\nWITH n.wcc as community, count(n) as communitySize\nWHERE communitySize > 1\nRETURN count(DISTINCT community)", "width": 3, "height": 1, "x": 9, "y": 2, "type": "value", "selection": {}, "settings": {} }, { "title": "What's next?", "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", "width": 3, "height": 2, "x": 9, "y": 0, "type": "text", "selection": {}, "settings": {} }, { "title": "Biggest community has :", "query": "MATCH (n:Article)\nWITH n.wcc as community, count(n) as communitySize\nRETURN toString(max(communitySize)) + ' Articles'\n\n\n", "width": 3, "height": 1, "x": 9, "y": 3, "type": "value", "selection": {}, "settings": { "fontSize": 56 } } ] }, { "title": "Topic extraction", "reports": [ { "title": "Topic extraction", "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", "width": 3, "height": 2, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": {} }, { "title": "Louvain detected :", "query": "MATCH (n:Article)\nWHERE EXISTS(n.louvain)\nRETURN toString(count(DISTINCT n.louvain)) + \" communities\"\n", "width": 3, "height": 2, "x": 0, "y": 2, "type": "value", "selection": {}, "settings": { "fontSize": 56 } }, { "title": "You might see \"disconnected islands\" because not all nodes and relationships are displayed.", "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", "width": 9, "height": 4, "x": 3, "y": 0, "type": "iframe", "selection": {}, "settings": { "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." } } ] }, { "title": "Topic extraction - 2", "reports": [ { "title": "Click on a community number to update view", "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", "width": 4, "height": 2, "x": 0, "y": 2, "type": "table", "selection": {}, "settings": { "actionsRules": [ { "condition": "Click", "field": "community", "value": "community", "customization": "set variable", "customizationValue": "community_property" } ], "columnWidths": "[0.5,2]" } }, { "title": "Top 10 topics", "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", "width": 4, "height": 2, "x": 0, "y": 0, "type": "pie", "selection": { "index": "summary", "value": "count", "key": "(none)" }, "settings": { "marginTop": 88, "marginBottom": 88, "marginRight": 88, "marginLeft": 88 } }, { "title": "Size indicates an Article's influence - Biggest node for a given community = topic", "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", "width": 8, "height": 4, "x": 4, "y": 0, "type": "iframe", "selection": {}, "settings": { "passGlobalParameters": false, "replaceGlobalParameters": true, "description": "Set username \"citation\" and password \"citation\" to view the perspective." } } ] }, { "title": "Recommendation", "reports": [ { "title": "Pick an article you like (a random one is also fine)", "query": "MATCH (n:Article WHERE EXISTS(n.louvain))\nRETURN \"Click me\" AS click, n.title AS title LIMIT 100", "width": 8, "height": 2, "x": 0, "y": 0, "type": "table", "selection": {}, "settings": { "actionsRules": [ { "condition": "Click", "field": "click", "value": "title", "customization": "set variable", "customizationValue": "title" } ], "type": "Node Property", "entityType": "Article", "propertyType": "title", "parameterName": "neodash_article_title", "columnWidths": "[1,3]" } }, { "title": "Recommended articles - Same topic", "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`", "width": 6, "height": 2, "x": 0, "y": 2, "type": "table", "selection": {}, "settings": {} }, { "title": "Some explanations", "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", "width": 4, "height": 2, "x": 8, "y": 0, "type": "text", "selection": {}, "settings": {} }, { "title": "Recommended articles - Related topic", "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", "width": 6, "height": 2, "x": 6, "y": 2, "type": "table", "selection": {}, "settings": {} } ] } ], "parameters": {}, "extensions": { "advanced-charts": true, "styling": true, "actions": true } } ================================================ FILE: gallery/dashboards/domains.json ================================================ { "title": "New Caledonia Domains Dashboard", "version": "2.1", "settings": { "pagenumber": 0, "editable": true, "fullscreenEnabled": false, "parameters": { "neodash_gestionnaires_name": "", "neodash_gestionnaires_id": "" } }, "pages": [ { "title": "Overview", "reports": [ { "title": "New Caledonia Domain Names", "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.", "width": 3, "height": 3, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": {} }, { "title": "Graph Schema", "query": "CALL db.schema.visualization()\n\n", "width": 3, "height": 3, "x": 3, "y": 0, "type": "graph", "selection": { "Site": "name", "DNS": "name", "Gestionnaires": "name", "Beneficiaires": "name" }, "settings": { "nodeColorScheme": "nivo" } }, { "title": "Total nodes", "query": "MATCH (n)\nRETURN COUNT(n) as Nodes\n\n\n", "width": 3, "height": 1, "x": 6, "y": 0, "type": "value", "selection": {}, "settings": {} }, { "title": "Total relationships", "query": "MATCH ()-[e]->()\nRETURN count(e) as rels\n\n\n", "width": 3, "height": 1, "x": 9, "y": 0, "type": "value", "selection": {}, "settings": {} }, { "title": "Site Status", "query": "MATCH (n:Site)\nRETURN n as node, n.id as id, n.status as status\nSKIP 1025 LIMIT 1000", "width": 3, "height": 2, "x": 0, "y": 3, "type": "table", "selection": {}, "settings": { "styleRules": [ { "field": "status", "condition": "=", "value": "green", "customization": "cell color", "customizationValue": "#00e300" }, { "field": "status", "condition": "=", "value": "orange", "customization": "cell color", "customizationValue": "orange" }, { "field": "status", "condition": "=", "value": "red", "customization": "cell color", "customizationValue": "red" } ], "description": "Click the column headers to sort the website by status code." } }, { "title": "A sample of the data", "query": "MATCH (s:Site)\nWHERE s.id in [\"1013\",\"1050\",\"1016\"]\nMATCH p=(s)--()\nRETURN p", "width": 6, "height": 2, "x": 6, "y": 1, "type": "graph", "selection": { "Site": "id", "DNS": "id", "Beneficiaires": "id", "Gestionnaires": "id" }, "settings": { "nodeColorScheme": "pastel1" } }, { "title": "DNS'es", "query": "MATCH (n:DNS)\nRETURN n as node, n.id as name\nLIMIT 1000\n\n\n", "width": 3, "height": 2, "x": 3, "y": 3, "type": "table", "selection": {}, "settings": { "columnWidths": "[1,2]" } }, { "title": "Gestionnaires (Managers)", "query": "MATCH (n:Gestionnaires)\nRETURN n as node, n.id as name\nLIMIT 1000\n\n\n", "width": 3, "height": 2, "x": 6, "y": 3, "type": "table", "selection": {}, "settings": { "columnWidths": "[1,2]" } }, { "title": "Beneficiaires (Clients)", "query": "MATCH (n:Beneficiaires)\nRETURN n as node, n.id as name\nLIMIT 1000\n\n\n\n\n\n", "width": 3, "height": 2, "x": 9, "y": 3, "type": "table", "selection": {}, "settings": { "columnWidths": "[1,2]" } }, { "title": "Domains created by year", "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", "width": 6, "height": 2, "x": 0, "y": 5, "type": "line", "selection": { "x": "year", "value": [ "number" ] }, "settings": {} }, { "title": "Gestionnaires with most clients", "query": "MATCH (g:Gestionnaires)<-[:CLIENTDE]-(b:Beneficiaires)\nRETURN g.id as Gestionnaires, COUNT(b) as clients\nORDER BY clients DESC LIMIT 20\n\n", "width": 6, "height": 2, "x": 6, "y": 5, "type": "bar", "selection": { "index": "Gestionnaires", "value": "clients", "key": "(none)" }, "settings": { "marginBottom": 120 } } ] }, { "title": "Drilldown", "reports": [ { "title": "Select a Gestionnaire", "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", "width": 3, "height": 1, "x": 0, "y": 1, "type": "select", "selection": {}, "settings": { "type": "Node Property", "entityType": "Gestionnaires", "propertyType": "id", "parameterName": "neodash_gestionnaires_id" } }, { "title": "Drilldown page", "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:", "width": 3, "height": 1, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": {} }, { "title": "Clients and number of domains for the selected provider", "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", "width": 9, "height": 4, "x": 3, "y": 0, "type": "bar", "selection": { "index": "Client", "value": "Domains", "key": "(none)" }, "settings": { "marginBottom": 160 } }, { "title": "Graph view", "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", "width": 3, "height": 2, "x": 0, "y": 4, "type": "graph", "selection": { "Gestionnaires": "id", "Beneficiaires": "id", "Site": "id" }, "settings": {} } ] } ], "parameters": {} } ================================================ FILE: gallery/dashboards/fraud.json ================================================ { "uuid": "b3236f88-ff8b-492d-8a84-d620a3dd629d", "title": "Financial Crimes Enforcement Dashboard 🕵️", "version": "2.4", "settings": { "pagenumber": 0, "editable": true, "parameters": { "neodash_entity_name": null, "neodash_country_name": null }, "fullscreenEnabled": true }, "pages": [ { "title": "Countries", "reports": [ { "x": 0, "y": 0, "title": "About this dashboard", "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.", "width": 8, "type": "text", "height": 4, "selection": {}, "settings": {}, "id": "bd17ccad-c12e-4e5a-8e45-48504b071698" }, { "x": 8, "y": 0, "title": "How much does each entity benefit in total? (Hint: try clicking the table headers to sort/filter data)", "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", "width": 16, "type": "table", "height": 4, "selection": {}, "settings": {}, "id": "6db3061c-12c5-4a92-a1a1-bf7e40c068a4" }, { "x": 0, "y": 4, "title": "Where in Europe does the Netherlands send money to?", "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) ", "width": 12, "type": "map", "height": 4, "selection": { "Country": "(no label)", "TRANSFER": "(label)" }, "settings": { "defaultRelColor": "rgba(120,120,120,0.5)", "defaultRelWidth": 5, "defaultNodeSize": "medium", "nodeColorScheme": "category10" }, "id": "5484e81c-52b2-416d-8b7f-fa112887fbec", "schema": [ ["Country", "code", "name", "location", "tld"], ["TRANSFER", "amount"] ] }, { "x": 12, "y": 4, "title": "Which entities are involved?", "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]", "width": 12, "type": "graph", "height": 4, "selection": { "Country": "name", "TRANSFER": "(label)", "Entity": "name" }, "settings": { "nodePositions": {} }, "id": "a4f84cff-996b-4bfd-b5de-2d8f0c9aa8b1", "schema": [ ["Country", "code", "name", "location", "tld"], ["TRANSFER", "amount"], ["Entity", "name", "location", "id", "country"] ] } ] }, { "title": "Entities", "reports": [ { "x": 0, "y": 0, "title": "Entity Investigator 🔎", "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", "width": 6, "type": "text", "height": 4, "selection": {}, "settings": {}, "id": "e33482d3-a7a2-4090-8868-0ed3931bc99e" }, { "x": 6, "y": 0, "title": "Select an entity to view reports", "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", "width": 5, "type": "select", "height": 4, "selection": {}, "settings": { "type": "Node Property", "entityType": "Entity", "propertyType": "name", "parameterName": "neodash_entity_name" }, "id": "5a5a46cb-d586-4831-88bc-b193edfa9e9c" }, { "x": 11, "y": 0, "title": "Details ", "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", "width": 7, "type": "table", "height": 4, "selection": {}, "settings": { "compact": false }, "id": "6e2e57b7-09c8-46cf-b33e-19fd3645693b", "schema": [] }, { "x": 18, "y": 0, "title": "Entity interactions", "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", "width": 6, "type": "map", "height": 4, "selection": { "Entity": "(no label)", "INTERACTS": "(label)" }, "settings": { "hideSelections": true }, "id": "6070f205-23ce-42bb-abc1-96ec3839e531", "schema": [["Entity", "name", "location", "id", "country"], ["INTERACTS"]] }, { "x": 0, "y": 4, "title": "Who receives most money from this entity?", "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", "width": 12, "type": "bar", "height": 4, "selection": { "index": "Other", "value": "Amount", "key": "(none)" }, "settings": { "valueScale": "linear", "marginLeft": 90, "marginBottom": 100, "marginRight": 50, "colors": "paired", "groupMode": "grouped" }, "id": "7023ae0c-68af-4fa6-8c59-3ba91d7980aa" }, { "x": 12, "y": 4, "title": "Details on a filing by the entity", "query": "MATCH path=(e:Entity)<--(f:Filing)\nWHERE e.name = $neodash_entity_name\nRETURN f LIMIT 1\n", "width": 6, "type": "json", "height": 4, "selection": {}, "settings": {}, "id": "ceb4d37e-8d9c-45c9-9062-485d40bed6cd" }, { "x": 18, "y": 4, "title": "Number of Filings", "query": "MATCH (e:Entity)--(:Filing)\nWHERE e.name = $neodash_entity_name\nRETURN COUNT(*)\n\n", "width": 6, "type": "value", "height": 4, "selection": {}, "settings": { "fontSize": 80 }, "id": "0efecd19-10ea-4475-b649-19b3b1b1e511" } ] }, { "title": "Statistics", "reports": [ { "x": 0, "y": 0, "title": "Total number of nodes", "query": "MATCH (n)\nRETURN COUNT(n)", "width": 6, "type": "value", "height": 4, "selection": {}, "settings": { "textAlign": "center", "fontSize": 80, "marginTop": 50 }, "id": "073fc2f0-c1a8-4206-ac48-53171ed98696" }, { "x": 6, "y": 0, "title": "Total number of relationships", "query": "MATCH (n)-[e]->(m)\nRETURN COUNT(e)\n\n\n", "width": 6, "type": "value", "height": 4, "selection": {}, "settings": { "fontSize": 80, "marginTop": 50, "textAlign": "center" }, "id": "294a7cc4-bd1e-4d6f-b440-16e9ad9a95f8" }, { "x": 12, "y": 0, "title": "Number of nodes by label", "query": "MATCH (n)\nRETURN labels(n), count(*) as count\nORDER BY count ASC\n\n\n", "width": 6, "type": "pie", "height": 4, "selection": { "index": "labels(n)", "value": "count", "key": "(none)" }, "settings": { "colors": "pastel1", "marginBottom": 60 }, "id": "bb995076-94b4-4e58-822b-19d4f8c62ac8" }, { "x": 18, "y": 0, "title": "Number of relationship types", "query": "MATCH (n)-[e]->(m)\nRETURN type(e),count(*) as count\nORDER BY count ASC\n\n\n\n\n\n\n", "width": 6, "type": "pie", "height": 4, "selection": { "index": "type(e)", "value": "count", "key": "(none)" }, "settings": { "colors": "pastel1", "marginBottom": 60, "marginLeft": 120, "marginRight": 120 }, "id": "6b604fad-6104-49e7-9aea-0a878a887e49" }, { "x": 0, "y": 4, "title": "Number of filing per year", "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", "width": 12, "type": "line", "height": 4, "selection": { "x": "Year", "value": ["Total"] }, "settings": { "marginLeft": 60 }, "id": "93ce2eab-4f7e-4e6e-a71e-257ebf5f68a9" }, { "x": 12, "y": 4, "title": "Example: using iFrames to embed custom visualizations (3D graph)", "query": "https://vasturiano.github.io/react-force-graph/example/basic/", "width": 12, "type": "iframe", "height": 4, "selection": {}, "settings": {}, "id": "94463630-ed46-4cbe-b140-316151e23ed1" } ] } ], "extensions": { "advanced-charts": { "active": true }, "styling": { "active": true }, "active": true, "activeReducers": [] } } ================================================ FILE: gallery/dashboards/jokes.json ================================================ { "title": "Dad Jokes Dashboard", "version": "2.2", "settings": { "pagenumber": 0, "editable": true, "fullscreenEnabled": false, "parameters": { "neodash_entity_text": "bananas" }, "downloadImageEnabled": false, "resizing": "bottom-right" }, "pages": [ { "title": "Start", "reports": [ { "title": "Let's start this crazy trip down the dadjoke rathole!", "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", "width": 12, "height": 2, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": { "nodePositions": {} } } ] }, { "title": "Tweets and Handles ", "reports": [ { "title": "Handles tweeting the same text", "query": "MATCH (t1:Tweet), (t2:Tweet)\nWHERE t1 <> t2\nAND t1.Text = t2.Text\nRETURN t1.`Screen Name`, t2.`Screen Name`, t1.Text;\n", "width": 12, "height": 2, "x": 0, "y": 0, "type": "table", "selection": {}, "settings": {} } ] }, { "title": "Jeff Bezos and his Pijamas", "reports": [ { "title": "Jokes about Jeff Bezos", "query": "MATCH path = (dj:Dadjoke)-[*..2]-(conn)\nWHERE dj.Text CONTAINS \"pyjamazon\"\n RETURN path\nlimit 10;\n\n\n", "width": 12, "height": 3, "x": 0, "y": 0, "type": "graph", "selection": { "Dadjoke": "Text" }, "settings": { "nodePositions": {} } }, { "title": "With Entities (after NLP)", "query": "\nMATCH path = (e:Entity)--(dj:Dadjoke)-[REFERENCES_DADJOKE]-(t:Tweet)--(h:Handle)\nWHERE dj.Text CONTAINS \"amazon\"\n RETURN path;\n\n", "width": 12, "height": 2, "x": 0, "y": 3, "type": "graph", "selection": { "Entity": "text", "Person": "text", "Dadjoke": "Text", "Tweet": "Text", "Handle": "name", "Other": "text" }, "settings": { "nodePositions": {} } } ] }, { "title": "String Metrics", "reports": [ { "title": "Levenshtein and Sørensen-Dice Similarity", "query": "\nMATCH (dj1:Dadjoke), (dj2:Dadjoke)\n WHERE id(dj1) 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", "width": 12, "height": 4, "x": 0, "y": 0, "type": "table", "selection": {}, "settings": { "nodePositions": {}, "autorun": true } } ] }, { "title": "Jokes about cars", "reports": [ { "title": "Jokes with \"cars\" in the text", "query": "MATCH (dj:Dadjoke) WHERE dj.Text CONTAINS \"car\" RETURN dj.Text as Dadjoke LIMIT 10;\n\n\n", "width": 6, "height": 2, "x": 0, "y": 0, "type": "table", "selection": {}, "settings": { "nodePositions": {} } }, { "title": "Jokes with \"car\" in the assosiated entity", "query": "MATCH (e:Entity)--(dj:Dadjoke) WHERE e.text CONTAINS \"car\" RETURN e.text as Entity, dj.Text as Dadjoke LIMIT 10;", "width": 6, "height": 2, "x": 6, "y": 0, "type": "table", "selection": {}, "settings": {} }, { "title": "Dadjoke with entity equal to \"car\"", "query": "MATCH p = (e:Entity)--(dj:Dadjoke) WHERE e.text = \"car\" RETURN dj.Text as Dadjoke LIMIT 10;\n\n", "width": 12, "height": 2, "x": 0, "y": 3, "type": "table", "selection": {}, "settings": {} } ] }, { "title": "Jokes about spaghetti", "reports": [ { "title": "Spaghetti jokes", "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", "width": 12, "height": 3, "x": 0, "y": 0, "type": "graph", "selection": { "Handle": "name", "Tweet": "Tweet Id", "Dadjoke": "Text" }, "settings": { "nodePositions": {} } } ] }, { "title": "Jokes about entities", "reports": [ { "title": "Select the Entity", "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", "width": 3, "height": 3, "x": 0, "y": 0, "type": "select", "selection": {}, "settings": { "nodePositions": {}, "type": "Node Property", "entityType": "Entity", "propertyType": "text", "parameterName": "neodash_entity_text" } }, { "title": "", "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", "width": 12, "height": 3, "x": 0, "y": 3, "type": "graph", "selection": { "Handle": "name", "Tweet": "(label)", "Dadjoke": "(label)", "Entity": "(label)", "ConsumerGood": "(label)", "Other": "(label)" }, "settings": { "nodePositions": {} } }, { "title": "Graph of jokes about the Entity selected", "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", "width": 9, "height": 3, "x": 3, "y": 0, "type": "graph", "selection": { "Handle": "name", "Tweet": "Tweet Id", "Dadjoke": "Text", "Entity": "text", "Other": "text" }, "settings": { "nodePositions": {} } } ] }, { "title": "Dadjoke Twitterverse", "reports": [ { "title": "Twitterverse cliques", "query": "\nMATCH path = (h1:Handle)-[*2..2]->(dj:Dadjoke)<-[*2..2]-(h2:Handle)\nWHERE id(h1)(b)\nRETURN *", "width": 6, "height": 3, "x": 6, "y": 1, "type": "graph", "selection": { "Person": "name", "Movie": "title" }, "settings": {} }, { "title": "Total movies", "query": "MATCH (n)\nRETURN COUNT(n) as Total", "width": 2, "height": 1, "x": 6, "y": 0, "type": "value", "selection": {}, "settings": {} }, { "title": "Total people", "query": "MATCH (n:Person)\nRETURN COUNT(n)\n\n\n", "width": 2, "height": 1, "x": 8, "y": 0, "type": "value", "selection": {}, "settings": {} }, { "title": "Relationships", "query": "MATCH ()-[r]->()\nRETURN count(r)\n\n\n", "width": 2, "height": 1, "x": 10, "y": 0, "type": "value", "selection": {}, "settings": {} }, { "title": "Relationship types", "query": "MATCH ()-[r]->()\nRETURN type(r) as Relationship, COUNT(r) as Total\n\n", "width": 3, "height": 2, "x": 0, "y": 2, "type": "bar", "selection": { "index": "Relationship", "value": "Total", "key": "(none)" }, "settings": { "marginBottom": 80 } }, { "title": "Browse the movies", "query": "MATCH (m:Movie)\nRETURN m.title as Movie, m.released as Year\n\n\n", "width": 3, "height": 2, "x": 6, "y": 4, "type": "table", "selection": {}, "settings": {} }, { "title": "Movies with most actors", "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", "width": 6, "height": 2, "x": 0, "y": 4, "type": "pie", "selection": { "index": "Movie", "value": "Actors", "key": "(none)" }, "settings": { "marginRight": 60, "marginLeft": 60, "marginTop": 40, "marginBottom": 60 } }, { "title": "Movies by decade", "query": "MATCH (m:Movie)\nRETURN toInteger(m.released/10)*10 as Year, COUNT(m) as Total\nORDER BY Year ASC\n\n\n", "width": 3, "height": 2, "x": 3, "y": 2, "type": "line", "selection": { "x": "Year", "value": [ "Total" ] }, "settings": { "curve": "cardinal", "marginTop": 50 } }, { "title": "Customized, grouped bar chart", "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", "width": 3, "height": 2, "x": 9, "y": 4, "type": "bar", "selection": { "index": "Movie", "value": "People", "key": "Role" }, "settings": { "showOptionalSelections": true, "legend": true, "marginBottom": 100 } } ] }, { "title": "Actor View", "reports": [ { "title": "Select a person", "query": "MATCH (p:Person)--(m:Movie)\nRETURN p.name as Person, COUNT(m) as Movies \nORDER BY Movies DESC\n\n\n", "width": 4, "height": 2, "x": 2, "y": 0, "type": "table", "selection": {}, "settings": { "actionsRules": [ { "condition": "Click", "field": "Person", "value": "Person", "customization": "set variable", "customizationValue": "person_name" } ] } }, { "title": "Graph view for this person", "query": "MATCH path=(p:Person)--()\nWHERE p.name = $neodash_person_name\nRETURN path\n\n", "width": 6, "height": 2, "x": 6, "y": 0, "type": "graph", "selection": { "Person": "name", "Movie": "title" }, "settings": { "nodeColorScheme": "paired" } }, { "title": "About this page", "query": "On this page, you can select a person from a table, and dynamically view other visualizations update.\n\n", "width": 2, "height": 1, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": { "replaceGlobalParameters": true } }, { "title": "Movies for the selected person", "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", "width": 5, "height": 2, "x": 0, "y": 3, "type": "table", "selection": {}, "settings": {} }, { "title": "The highest voted movies for this person", "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", "width": 7, "height": 2, "x": 5, "y": 2, "type": "sankey", "selection": { "Person": "name", "Movie": "title" }, "settings": { "labelProperty": "weight" } }, { "title": "Selected person", "query": "RETURN $neodash_person_name\n\n\n", "width": 2, "height": 1, "x": 0, "y": 2, "type": "value", "selection": {}, "settings": { "fontSize": 26 } } ] } ], "parameters": {} } ================================================ FILE: gallery/dashboards/recommendations.json ================================================ { "title": "NeoDash Recommendations Dashboard 🎬", "version": "2.1", "settings": { "pagenumber": 0, "editable": true, "fullscreenEnabled": false, "parameters": { "neodash_person_name": "Woody Allen" }, "extensions": [ "core", "actions" ] }, "pages": [ { "title": "Overview", "reports": [ { "title": "The Reccomendations Dashboard", "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.", "width": 6, "height": 2, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": {} }, { "title": "Total movies", "query": "MATCH (n)\nRETURN COUNT(n) as Total", "width": 3, "height": 1, "x": 6, "y": 0, "type": "value", "selection": {}, "settings": {} }, { "title": "Total people", "query": "MATCH (n:Person)\nRETURN COUNT(n)\n\n\n", "width": 3, "height": 1, "x": 9, "y": 0, "type": "value", "selection": {}, "settings": {} }, { "title": "Relationships", "query": "MATCH ()-[r]->()\nRETURN count(r)\n\n\n", "width": 6, "height": 1, "x": 6, "y": 1, "type": "value", "selection": {}, "settings": {} }, { "title": "Relationship types", "query": "MATCH ()-[r]->()\nRETURN type(r) as Relationship, COUNT(r) as Total\n\n", "width": 3, "height": 2, "x": 0, "y": 2, "type": "bar", "selection": { "index": "Relationship", "value": "Total", "key": "(none)" }, "settings": { "marginBottom": 80 } }, { "title": "Browse the movies", "query": "MATCH (m:Movie)\nRETURN m.title as Movie, m.released as Year\n\n\n", "width": 3, "height": 2, "x": 6, "y": 6, "type": "table", "selection": {}, "settings": {} }, { "title": "Movies with most actors", "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", "width": 6, "height": 2, "x": 0, "y": 4, "type": "pie", "selection": { "index": "Movie", "value": "Actors", "key": "(none)" }, "settings": { "marginRight": 60, "marginLeft": 60, "marginTop": 40, "marginBottom": 60 } }, { "title": "Movies by decade", "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", "width": 3, "height": 2, "x": 3, "y": 2, "type": "line", "selection": { "x": "Year", "value": [ "Total" ] }, "settings": { "curve": "cardinal", "marginTop": 50 } }, { "title": "Customized, grouped bar chart", "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", "width": 3, "height": 2, "x": 6, "y": 2, "type": "bar", "selection": { "index": "Movie", "value": "People", "key": "Role" }, "settings": { "showOptionalSelections": true, "legend": true, "marginBottom": 100 } } ] }, { "title": "Actor View", "reports": [ { "title": "Select a person", "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", "width": 4, "height": 2, "x": 2, "y": 0, "type": "table", "selection": {}, "settings": { "actionsRules": [ { "condition": "Click", "field": "Person", "value": "Person", "customization": "set variable", "customizationValue": "person_name" } ] } }, { "title": "Graph view for this person", "query": "MATCH path=(p:Person)--()\nWHERE p.name = $neodash_person_name\nRETURN path\n\n", "width": 6, "height": 2, "x": 6, "y": 0, "type": "graph", "selection": { "Actor": "name", "Director": "name", "Person": "name", "Movie": "title" }, "settings": { "nodeColorScheme": "paired" } }, { "title": "About this page", "query": "On this page, you can select a person from a table, and dynamically view other visualizations update.\n\n", "width": 2, "height": 1, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": { "replaceGlobalParameters": true } }, { "title": "Movies for the selected person", "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", "width": 5, "height": 2, "x": 0, "y": 3, "type": "table", "selection": {}, "settings": {} }, { "title": "The highest voted movies for this person", "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", "width": 7, "height": 2, "x": 5, "y": 2, "type": "sankey", "selection": { "Actor": "name", "Director": "name", "Person": "name", "Movie": "title" }, "settings": { "labelProperty": "weight" } }, { "title": "Selected person", "query": "RETURN $neodash_person_name\n\n\n", "width": 2, "height": 1, "x": 0, "y": 2, "type": "value", "selection": {}, "settings": { "fontSize": 26 } } ] } ], "parameters": {} } ================================================ FILE: gallery/dashboards/twitter.json ================================================ { "title": "NeoDash Twitter Dashboard 📲", "version": "2.1", "settings": { "pagenumber": 0, "editable": true, "fullscreenEnabled": false, "parameters": { "neodash_person_name": "Florent Biville", "neodash_tweet_info": { "low": -1013501951, "high": 311222644 }, "neodash_tweet_url": "https://twitter.com/i/web/status/1352796210650886145", "neodash_user_pic": "http://pbs.twimg.com/profile_images/792577726230237184/8ZSDZEvI_normal.jpg" }, "extensions": [ "core", "actions" ] }, "pages": [ { "title": "Overview", "reports": [ { "title": "The Twitter Dashboard", "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.", "width": 6, "height": 2, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": {} }, { "title": "The entire graph", "query": "// It's the entire graph!\nMATCH (u:User)-[r:FOLLOWS]->(b)\nRETURN *", "width": 6, "height": 3, "x": 6, "y": 2, "type": "graph", "selection": { "User": "name", "Me": "name" }, "settings": {} }, { "title": "Total Tweets", "query": "MATCH (n:Tweet)\nRETURN COUNT(n) as Total", "width": 2, "height": 1, "x": 6, "y": 0, "type": "value", "selection": {}, "settings": {} }, { "title": "Total Users", "query": "MATCH (n:User)\nRETURN COUNT(n)\n\n\n", "width": 2, "height": 1, "x": 8, "y": 0, "type": "value", "selection": {}, "settings": {} }, { "title": "Relationships", "query": "MATCH ()-[r]->()\nRETURN count(r)\n\n\n", "width": 2, "height": 1, "x": 10, "y": 0, "type": "value", "selection": {}, "settings": {} }, { "title": "Relationship types", "query": "MATCH ()-[r]->()\nRETURN type(r) as Relationship, COUNT(r) as Total\n\n", "width": 3, "height": 2, "x": 0, "y": 2, "type": "bar", "selection": { "index": "Relationship", "value": "Total", "key": "(none)" }, "settings": { "marginBottom": 80 } }, { "title": "Browse the movies", "query": "MATCH (m:Tweet)\nRETURN m.id_str as idx, m.text as Text\n\n\n", "width": 6, "height": 2, "x": 6, "y": 5, "type": "table", "selection": {}, "settings": {} }, { "title": "Users with most tweets", "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", "width": 6, "height": 2, "x": 0, "y": 4, "type": "pie", "selection": { "index": "User", "value": "Tweets", "key": "(none)" }, "settings": { "marginRight": 60, "marginLeft": 60, "marginTop": 40, "marginBottom": 60 } }, { "title": "Tweets by Month", "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", "width": 3, "height": 2, "x": 3, "y": 2, "type": "line", "selection": { "x": "nMonth", "value": [ "Total" ] }, "settings": { "curve": "cardinal", "marginTop": 50 } }, { "title": "Total Sources", "query": "MATCH (n:Source)\nRETURN COUNT(n) as Sources", "width": 2, "height": 1, "x": 6, "y": 1, "type": "value", "selection": {}, "settings": {} }, { "title": "Total Hashtags", "query": "MATCH (n:Hashtag)\nRETURN COUNT(n) as Hashtags", "width": 2, "height": 1, "x": 8, "y": 1, "type": "value", "selection": {}, "settings": {} }, { "title": "Total Links", "query": "MATCH (n:Link)\nRETURN COUNT(n) as Links", "width": 2, "height": 1, "x": 10, "y": 1, "type": "value", "selection": {}, "settings": {} }, { "title": "Tweets distribution by Hasgtag", "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", "width": 6, "height": 2, "x": 0, "y": 6, "type": "pie", "selection": { "index": "Hashtag", "value": "Tweets", "key": "(none)" }, "settings": { "marginTop": 45 } } ] }, { "title": "User View", "reports": [ { "title": "Select an user", "query": "MATCH (t:Tweet)<-[:POSTS]-(u:User)\nRETURN u.name as User, COUNT(t) as Tweets\nORDER BY Tweets desc\n\n", "width": 4, "height": 2, "x": 3, "y": 0, "type": "table", "selection": {}, "settings": { "actionsRules": [ { "condition": "Click", "field": "User", "value": "User", "customization": "set variable", "customizationValue": "person_name" } ] } }, { "title": "Graph view for this user follow relationships", "query": "MATCH path=(p:User)-[:FOLLOWS]-()\nWHERE p.name = $neodash_person_name\nRETURN path\n\n", "width": 5, "height": 2, "x": 7, "y": 0, "type": "graph", "selection": { "User": "name", "Me": "name" }, "settings": { "nodeColorScheme": "paired" } }, { "title": "About this page", "query": "On this page, you can select a person from a table, and dynamically view other visualizations update.\n\n", "width": 3, "height": 1, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": { "replaceGlobalParameters": true } }, { "title": "Tweets for the selected user", "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", "width": 7, "height": 2, "x": 0, "y": 2, "type": "table", "selection": {}, "settings": { "columnWidths": "[3,1,11]" } }, { "title": "The highest tweeted hashtags by this user", "query": "MATCH path=(p)-->(m:Tweet)-[:TAGS]->(t)\nWHERE p.name = $neodash_person_name\nRETURN t.name as HashTag, count(*) as Total\n\n", "width": 5, "height": 2, "x": 7, "y": 3, "type": "table", "selection": {}, "settings": { "labelProperty": "weight" } }, { "title": "Selected User", "query": "RETURN $neodash_person_name\n\n\n", "width": 3, "height": 1, "x": 0, "y": 1, "type": "value", "selection": {}, "settings": { "fontSize": 26 } } ] }, { "title": "Tweet View", "reports": [ { "title": "About this page", "query": "On this page, you can select a tweet from a table, and dynamically view other visualizations update.\n\n\n\n\n", "width": 3, "height": 1, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": {} }, { "title": "Selected User", "query": "RETURN $neodash_person_name\n\n\n\n\n\n", "width": 3, "height": 1, "x": 0, "y": 1, "type": "value", "selection": {}, "settings": { "fontSize": 26 } }, { "title": "Graph of Tweets for this User. (Click on a Tweet for more info)", "query": "MATCH path=(p)-->(m:Tweet)\nWHERE p.name = $neodash_person_name\nRETURN path\n\n", "width": 5, "height": 2, "x": 3, "y": 0, "type": "graph", "selection": { "User": "name", "Tweet": "id" }, "settings": { "actionsRules": [ { "condition": "onNodeClick", "field": "Tweet", "value": "id", "customization": "set variable", "customizationValue": "tweet_info" }, { "condition": "onNodeClick", "field": "User", "value": "profile_image_url", "customization": "set variable", "customizationValue": "user_pic" } ] } }, { "title": "Tweet Info", "query": "MATCH path = (a:Tweet)-[b]-(c)\nWHERE a.id = $neodash_tweet_info\nRETURN path\n\n\n", "width": 3, "height": 2, "x": 8, "y": 0, "type": "graph", "selection": { "Tweet": "id", "User": "name" }, "settings": {} } ] } ], "parameters": {} } ================================================ FILE: gallery/dashboards/wine.json ================================================ { "title": "Revue de vins par Winemag", "version": "2.2", "settings": { "pagenumber": 0, "editable": true, "fullscreenEnabled": false, "parameters": { "neodash_wine": "DFJ Vinhos 2012 Paxis Red (Lisboa)" }, "extensions": ["core", "actions"], "disableRowLimiting": true }, "pages": [ { "title": "Aperçu", "reports": [ { "title": "Bonjour !", "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", "width": 3, "height": 2, "x": 0, "y": 0, "type": "text", "selection": {}, "settings": {} }, { "title": "Modèle de données", "query": "CALL db.schema.visualization()\n\n", "width": 6, "height": 2, "x": 3, "y": 0, "type": "graph", "selection": { "Wine": "(label)", "Winery": "(label)", "Designation": "(label)", "Country": "(label)", "Region": "(label)", "Review": "(label)", "Variety": "(label)", "Province": "(label)", "Taster": "(label)" }, "settings": { "nodePositions": {} } }, { "title": "Répartition des vins par pays", "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", "width": 6, "height": 2, "x": 3, "y": 2, "type": "choropleth", "selection": { "index": "country", "value": "wines", "key": "(none)" }, "settings": {} }, { "title": "Nombre de vins dégustés", "query": "MATCH (n:Wine)\nRETURN count(n)\n\n", "width": 3, "height": 1, "x": 0, "y": 2, "type": "value", "selection": {}, "settings": {} }, { "title": "Nombre de dégustations", "query": "MATCH (n:Review)\nRETURN count(n)\n\n", "width": 3, "height": 1, "x": 0, "y": 3, "type": "value", "selection": {}, "settings": {} }, { "title": "Nombre de vignobles", "query": "MATCH (n:Winery)\nRETURN count(n)\n", "width": 3, "height": 1, "x": 9, "y": 0, "type": "value", "selection": {}, "settings": {} }, { "title": "Exemples de vins", "query": "MATCH (w:Wine)\nRETURN w.title as Désignation\nLIMIT 50\n", "width": 3, "height": 3, "x": 9, "y": 1, "type": "table", "selection": {}, "settings": {} } ] }, { "title": "Répartition des notes", "reports": [ { "title": "Note moyenne", "query": "MATCH (n:Review)\nRETURN avg(n.points)\n\n", "width": 3, "height": 2, "x": 0, "y": 0, "type": "gauge", "selection": {}, "settings": {} }, { "title": "Vins les mieux notés", "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", "width": 6, "height": 2, "x": 3, "y": 0, "type": "table", "selection": {}, "settings": {} }, { "title": "Note moyenne par pays (nombre de notes > 100)", "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", "width": 6, "height": 2, "x": 0, "y": 2, "type": "table", "selection": {}, "settings": {} }, { "title": "Cépages les mieux notés", "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", "width": 3, "height": 2, "x": 9, "y": 0, "type": "table", "selection": {}, "settings": {} }, { "title": "Evolution de la note moyenne en fonction du prix", "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", "width": 6, "height": 2, "x": 6, "y": 3, "type": "line", "selection": { "x": "Prix", "value": ["Note moyenne"] }, "settings": {} } ] }, { "title": "Vins recommandés", "reports": [ { "title": "Meilleur rapport qualité prix (note >= 90)", "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", "width": 5, "height": 2, "x": 0, "y": 0, "type": "table", "selection": {}, "settings": { "actionsRules": [ { "condition": "Click", "field": "Sélectionner", "value": "Vin", "customization": "set variable", "customizationValue": "wine" } ] } }, { "title": "Pays", "query": "MATCH (n:Wine)-[:IS_FROM|PART_OF*]->(c:Country)\nWHERE n.title=$neodash_wine\nRETURN DISTINCT c.name\n\n", "width": 2, "height": 1, "x": 5, "y": 0, "type": "value", "selection": {}, "settings": { "fontSize": 32 } }, { "title": "Région", "query": "MATCH (n:Wine)-[:IS_FROM]->(p)\nWHERE n.title=$neodash_wine\nRETURN p.name\n\n", "width": 2, "height": 1, "x": 7, "y": 0, "type": "value", "selection": {}, "settings": { "fontSize": 32 } }, { "title": "Plus d'informations sur ce vin / Acheter", "query": "https://www.winemag.com/?s=$neodash_wine\n\n", "width": 7, "height": 3, "x": 5, "y": 1, "type": "iframe", "selection": {}, "settings": { "replaceGlobalParameters": true, "passGlobalParameters": false } }, { "title": "Type de vin", "query": "MATCH (n:Wine)-->(d:Designation)\nWHERE n.title=$neodash_wine\nRETURN d.title\nLIMIT 1\n", "width": 3, "height": 1, "x": 9, "y": 0, "type": "value", "selection": {}, "settings": { "fontSize": 32 } }, { "title": "Commentaires - $neodash_wine", "query": "MATCH (n:Wine)<-[:ON]-(r:Review)\nWHERE n.title=$neodash_wine\nRETURN r.content AS commentaire\n", "width": 5, "height": 2, "x": 0, "y": 2, "type": "value", "selection": {}, "settings": { "fontSize": 24 } } ] } ], "parameters": {}, "extensions": { "advanced-charts": true, "styling": true, "actions": true } } ================================================ FILE: gallery/package.json ================================================ { "name": "neodash-gallery", "version": "0.2", "private": true, "dependencies": { "@neo4j-ndl/base": "^0.8.3", "@neo4j-ndl/react": "^0.8.3", "@types/react-dom": "^18.0.10", "neo4j-driver": "^5.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "^5.0.1", "typescript": "^4.4.2", "web-vitals": "^2.1.0" }, "scripts": { "start": "DISABLE_ESLINT_PLUGIN=true react-scripts start", "build": "DISABLE_ESLINT_PLUGIN=true react-scripts build", "test": "DISABLE_ESLINT_PLUGIN=true react-scripts test", "eject": "DISABLE_ESLINT_PLUGIN=true react-scripts eject" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "devDependencies": { "autoprefixer": "^10.4.12", "postcss": "^8.4.31", "tailwindcss": "^3.1.8" } } ================================================ FILE: gallery/postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: gallery/public/index.html ================================================ NeoDash Dashboard Gallery
================================================ FILE: gallery/public/manifest.json ================================================ { "short_name": "NeoDash Dashboard Gallery", "name": "NeoDash Dashboard Gallery", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "favicon.png", "type": "image/png", "sizes": "192x192" }, { "src": "favicon.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: gallery/public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: gallery/setup.md ================================================ ## Setting up the gallery back-end The NeoDash gallery is powered by a Neo4j Aura Enterprise instance, available at `acb5b6ae.databases.neo4j.io`. A set ### Create a read-only user to list dashboards. ``` :use system; CREATE USER gallery SET PASSWORD 'gallery' CHANGE NOT REQUIRED; CREATE ROLE gallery; GRANT ROLE gallery TO gallery; GRANT ACCESS ON DATABASE neo4j TO `gallery`; GRANT MATCH {*} ON GRAPH neo4j NODE `_Neodash_Dashboard` TO gallery; ``` ### Create read-only users for each of the use-cases. Bill of Materials: ``` :use system; CREATE USER bom SET PASSWORD 'bom' CHANGE NOT REQUIRED; CREATE ROLE bom; GRANT ROLE bom TO bom; GRANT ACCESS ON DATABASE neo4j TO `bom`; GRANT MATCH {*} ON GRAPH neo4j NODE Component TO `bom`; GRANT MATCH {*} ON GRAPH neo4j NODE Model TO `bom`; GRANT MATCH {*} ON GRAPH neo4j NODE Supplier TO `bom`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP HAS TO `bom`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP SUPPLIED_BY TO `bom`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP SIMILAR TO `bom`; ``` Graph Assessment: ``` :use system; CREATE USER assessment SET PASSWORD 'assessment' CHANGE NOT REQUIRED; CREATE ROLE assessment; GRANT ROLE assessment TO assessment; GRANT ACCESS ON DATABASE neo4j TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j NODE GraphAssessment TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j NODE Pillar TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j NODE Topic TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j NODE Person TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j NODE ProjectAssessment TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j NODE Customer TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j NODE Project TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j NODE Model TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j NODE Component TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP CONSISTS_OF TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP CONTAINS TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP ASSESSED TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP COMPLETED TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP EMPLOYEES TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP MEMBEROF TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP ASSOCIATED TO `assessment`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP HAS TO `assessment`; ``` Domain Names: ``` :use system; CREATE USER domains SET PASSWORD 'domains' CHANGE NOT REQUIRED; CREATE ROLE domains; GRANT ROLE domains TO domains; GRANT ACCESS ON DATABASE neo4j TO `domains`; GRANT MATCH {*} ON GRAPH neo4j NODE DNS TO `domains`; GRANT MATCH {*} ON GRAPH neo4j NODE Site TO `domains`; GRANT MATCH {*} ON GRAPH neo4j NODE Gestionnaires TO `domains`; GRANT MATCH {*} ON GRAPH neo4j NODE Beneficiaires TO `domains`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP HEBERGESUR TO `domains`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP GERE TO `domains`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP CLIENTDE TO `domains`; GRANT MATCH {*} ON GRAPH neo4j RELATIONSHIP POSSEDE TO `domains`; ``` Bitcoin: ``` :use system; CREATE USER bitcoin SET PASSWORD 'bitcoin' CHANGE NOT REQUIRED; CREATE ROLE bitcoin; GRANT ROLE bitcoin TO bitcoin; GRANT ACCESS ON DATABASE neo4j TO `bitcoin`; ``` ================================================ FILE: gallery/src/App.css ================================================ ================================================ FILE: gallery/src/App.tsx ================================================ import React from 'react'; import './App.css'; import { Button, TextInput, HeroIcon, Tag, Alert } from '@neo4j-ndl/react'; // These are the read-only credentials of the public database where the gallery exists. const uri = 'neo4j+s://acb5b6ae.databases.neo4j.io'; const user = 'gallery'; const password = 'gallery'; async function loadDashboards(setResults: any) { // eslint-disable-next-line @typescript-eslint/no-var-requires const neo4j = require('neo4j-driver'); const driver = neo4j.driver(uri, neo4j.auth.basic(user, password)); const session = driver.session(); try { const result = await session.run( 'MATCH (n:_Neodash_Dashboard) RETURN properties(n) as entry ORDER BY entry.index ASC' ); setResults( result.records.map((r: { _fields: any }) => { return r._fields[0]; }) ); } finally { await session.close(); } await driver.close(); } function App() { const [searchText, setSearchText] = React.useState(''); const [list, setList] = React.useState([]); if (list.length == 0) { loadDashboards(setList); } const filteredList = list.filter( (item: { title: string; author: string; description: string; keywords: any }) => item.keywords && `${item.title} ${item.author} ${item.description} ${item.keywords}` .toLowerCase() .includes(searchText.toLowerCase()) ); return (
{/* setBannerOpen(false)} > This app will no longer be available in the near future.   Migrate  your dashboards to the Neo4j Console, or{' '} visit {' '} the NeoDash repository to run NeoDash yourself. */} {/* Header */}

NeoDash Dashboard Gallery 🎨

This page contains a set of sample NeoDash dashboards built on public data.

This gallery is created and maintained by the NeoDash community.

setSearchText(e.target.value)} leftIcon={} placeholder='Filter Dashboards...' rightIcon={} />
{/* Grid */}
{filteredList.map((item: Record) => { return (

{item.language} {item.logo ? ( ) : ( <> )}

{item.title}

{item.description}
Author:{' '} {item.author}

{`${item.keywords}`.split(' ').map((k) => ( {k} ))}
); })}
{list.length == 0 ?

Loading...

: <>} {list.length != 0 && filteredList.length == 0 ? (

No results.

) : ( <> )}
{/* Footer */}

Want to add a dashboard to this gallery? Check out the

on GitHub.


{'-- neodash-gallery v0.2 --'}
); } export default App; ================================================ FILE: gallery/src/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; body { margin: 0; height: 100%; background-color: white; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } ================================================ FILE: gallery/src/index.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; // We need to include the base CSS in the root of // the app so all of our components can inherit the styles import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); ================================================ FILE: gallery/src/react-app-env.d.ts ================================================ /// ================================================ FILE: gallery/src/reportWebVitals.ts ================================================ import { ReportHandler } from 'web-vitals'; const reportWebVitals = (onPerfEntry?: ReportHandler) => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); }); } }; export default reportWebVitals; ================================================ FILE: gallery/src/setupTests.ts ================================================ // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; ================================================ FILE: gallery/tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./src/**/*.{js,jsx,ts,tsx}", ], theme: { extend: {}, }, plugins: [], presets: [require('@neo4j-ndl/base/lib/optimised.config')], prefix: '', corePlugins: { preflight: false, }, } ================================================ FILE: gallery/tsconfig.json ================================================ { "compilerOptions": { "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": ["src"] } ================================================ FILE: k8s-deploy/neodash/.helmignore ================================================ # Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store # Common VCS dirs .git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/ # Common backup files *.swp *.bak *.tmp *.orig *~ # Various IDEs .project .idea/ *.tmproj .vscode/ ================================================ FILE: k8s-deploy/neodash/Chart.yaml ================================================ apiVersion: v2 name: neodash description: A NeoDash Helm chart for Kubernetes # A chart can be either an 'application' or a 'library' chart. # # Application charts are a collection of templates that can be packaged into versioned archives # to be deployed. # # Library charts provide useful utilities or functions for the chart developer. They're included as # a dependency of application charts to inject those utilities and functions into the rendering # pipeline. Library charts do not define any templates and therefore cannot be deployed. type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) version: 1.0.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "2.4.10" ================================================ FILE: k8s-deploy/neodash/README.md ================================================ # NeoDash ![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) A NeoDash Helm chart for Kubernetes ## Resources Following are the Kubernetes resources utilized for the NeoDash. - Deployment - Service - Ingress - Service Account - Horizontal Pod Autoscalar (HPA) ## Values Configuration | Key | Type | Default | Description | |-----|------|---------|-------------| | autoscaling.enabled | bool | `false` | Enable/disable Autoscaling | | enable_reader_mode | bool | `true` | Enable/disable Reader mode | | envFromSecrets | list | `[]` | Environment variables from secrets | | fullnameOverride | string | `"neodash-test"` | Name override applies to all resources | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"neo4jlabs/neodash"` | Image repository and Image name | | image.tag | string | `"latest"` | Image version | | imagePullSecrets | list | `[]` | Image pull secrets if any | | podAnnotations | object | `{}` | Pod annotations | | podLabels | object | `{}` | Additional labels | | podSecurityContext | object | `{}` | Security Context if any | | ingress.annotations | object | `{}` | Ingress Annotations for load balancers | | ingress.className | string | `"alb"` | Ingress Class | | ingress.enabled | bool | `false` | Enable/disable Ingress | | ingress.hosts | list | `[]` | Host Details | | ingress.tls | list | `[]` | TLS details | | livenessProbe.httpGet.path | string | `"/*"` | LivenessProbe path | | livenessProbe.httpGet.port | int | `5005` | LivenessProbe port | | readinessProbe.httpGet.path | string | `"/*"` | Readiness path | | readinessProbe.httpGet.port | int | `5005` | Readiness port | | replicaCount | int | `1` | Replica count | | resources.limits.cpu | string | `"500m"` | CPU limit | | resources.limits.memory | string | `"128Mi"` | Memory limit | | resources.requests.cpu | string | `"250m"` | CPU request | | resources.requests.memory | string | `"64Mi"` | Memory request | | service.annotations | object | `{}` | Service annotations | | service.port | int | `5005` | Service port | | service.targetPort | int | `5005` | Service target port | | service.type | string | `"LoadBalancer"` | Type of service, other options are `ClusterIP` or `NodePort` | | serviceAccount.automount | bool | `true` | Enable/disable service account auto mount to pod | | serviceAccount.create | bool | `true` | Enable/disable service account | | volumeMounts | list | `[]` | Volume mounts on pod | | volumes | list | `[]` | Volumes for pod | | env | list |
- name: "ssoEnabled" 
  value: "false" 
- name: "standalone" 
  value: "true" 
- name: "standaloneProtocol" 
  value: "neo4j+s" 
- name: "standaloneHost" 
  value: "localhost" 
- name: "standalonePort" 
  value: "7687" 
- name: "standaloneDatabase" 
  value: "neo4j" 
- name: "standaloneDashboardName" 
  value: "test" 
- name: "standaloneDashboardDatabase" 
  value: "neo4j" 
- name: "standaloneAllowLoad" 
  value: "false" 
- name: "standaloneLoadFromOtherDatabases" 
  value: "false" 
- name: "standaloneMultiDatabase" 
  value: "false" 
| Env variables for reader mode | ## Usage - To install this helm chart run the following command, ```bash helm install ./neodash -n ``` - To upgrade the release run the following command, ```bash helm upgrade ./neodash -n ``` - To uninstall the release run the following command, ```bash helm uninstall -n ``` > **Note:** To use custom values files, pass `-f .yaml` for the above command. > **Note:** To use custom values, pass `--set param=value` for the above command. For example, to install neodash and set the service type to NodePort, run: `helm install ./neodash -n --set service.type=NodePort` ================================================ FILE: k8s-deploy/neodash/templates/NOTES.txt ================================================ The NeoDash application has been successfully deployed, here is the application URL: {{- if .Values.ingress.enabled }} {{- range $host := .Values.ingress.hosts }} {{- range .paths }} http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} {{- end }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} Run the following command to retrieve the IP address: export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "neodash.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. You can watch the status of the LoadBalancer by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "neodash.fullname" . }}' Once available, run the following command to retrieve the IP address: export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "neodash.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") echo http://$SERVICE_IP:{{ .Values.service.port }} {{- else if contains "ClusterIP" .Values.service.type }} Run the following command to retrieve the IP address: 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}") export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT {{- end }} ================================================ FILE: k8s-deploy/neodash/templates/_helpers.tpl ================================================ {{/* Expand the name of the chart. */}} {{- define "neodash.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "neodash.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define "neodash.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} {{- define "neodash.labels" -}} helm.sh/chart: {{ include "neodash.chart" . }} {{ include "neodash.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "neodash.selectorLabels" -}} app.kubernetes.io/name: {{ include "neodash.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} {{- define "neodash.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "neodash.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} ================================================ FILE: k8s-deploy/neodash/templates/deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "neodash.fullname" . }} labels: {{- include "neodash.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: {{- include "neodash.selectorLabels" . | nindent 6 }} template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "neodash.labels" . | nindent 8 }} {{- with .Values.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "neodash.serviceAccountName" . }} automountServiceAccountToken: false containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- with .Values.podSecurityContext }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 12 }} {{- end }} ports: - name: http containerPort: {{ .Values.service.targetPort }} protocol: TCP env: {{- if ne 5005 (int .Values.service.targetPort) }} - name: NGINX_PORT value: {{ .Values.service.port | quote }} {{- end }} {{- if .Values.enable_reader_mode}} {{- with .Values.env }} {{- toYaml . | nindent 12 }} {{- end }} {{- if .Values.envFromSecrets }} {{- range $key, $value := .Values.envFromSecrets }} - name: {{ $key }} valueFrom: secretKeyRef: name: {{ $value.secretName }} key: {{ $value.key }} {{- end }} {{- end }} {{- end }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: {{- toYaml .Values.readinessProbe | nindent 12 }} resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.volumeMounts }} volumeMounts: {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.volumes }} volumes: {{- toYaml . | nindent 8 }} {{- end }} ================================================ FILE: k8s-deploy/neodash/templates/hpa.yaml ================================================ {{- if .Values.autoscaling.enabled }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: {{ include "neodash.fullname" . }} labels: {{- include "neodash.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: {{ include "neodash.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - type: Resource resource: name: cpu target: type: Utilization averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} {{- end }} {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - type: Resource resource: name: memory target: type: Utilization averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- end }} {{- end }} ================================================ FILE: k8s-deploy/neodash/templates/ingress.yaml ================================================ {{- if .Values.ingress.enabled -}} {{- $fullName := include "neodash.fullname" . -}} {{- $svcPort := .Values.service.port -}} {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} {{- end }} {{- end }} {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1 {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1beta1 {{- else -}} apiVersion: extensions/v1beta1 {{- end }} kind: Ingress metadata: name: {{ $fullName }} labels: {{- include "neodash.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} ingressClassName: {{ .Values.ingress.className }} {{- end }} {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ .path }} {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} pathType: {{ .pathType }} {{- end }} backend: {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} service: name: {{ $fullName }} port: number: {{ $svcPort }} {{- else }} serviceName: {{ $fullName }} servicePort: {{ $svcPort }} {{- end }} {{- end }} {{- end }} {{- end }} ================================================ FILE: k8s-deploy/neodash/templates/service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: {{ include "neodash.fullname" . }} labels: {{- include "neodash.labels" . | nindent 4 }} {{- with .Values.service.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: {{ .Values.service.targetPort }} protocol: TCP name: http selector: {{- include "neodash.selectorLabels" . | nindent 4 }} ================================================ FILE: k8s-deploy/neodash/templates/serviceaccount.yaml ================================================ {{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "neodash.serviceAccountName" . }} labels: {{- include "neodash.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} automountServiceAccountToken: {{ .Values.serviceAccount.automount }} {{- end }} ================================================ FILE: k8s-deploy/neodash/templates/tests/test-connection.yaml ================================================ apiVersion: v1 kind: Pod metadata: name: "{{ include "neodash.fullname" . }}-test-connection" labels: {{- include "neodash.labels" . | nindent 4 }} annotations: "helm.sh/hook": test spec: automountServiceAccountToken: false containers: - name: wget image: busybox command: ['wget'] args: ['{{ include "neodash.fullname" . }}:{{ .Values.service.port }}'] resources: {{- toYaml .Values.resources | nindent 8 }} restartPolicy: Never ================================================ FILE: k8s-deploy/neodash/values.yaml ================================================ # Name override or full name override nameOverride: '' fullnameOverride: neodash-test # Number of pods replicaCount: 1 # Image Details image: repository: neo4jlabs/neodash pullPolicy: IfNotPresent tag: 'latest' imagePullSecrets: [] # Image pull secret if any # Pod annotations, labels and security context podAnnotations: {} podLabels: {} podSecurityContext: {} # Mode configuration using environment variables # Set reader mode environment variables when enable_reader_mode is true enable_reader_mode: true env: - name: "ssoEnabled" value: "false" - name: "standalone" value: "true" - name: "standaloneProtocol" value: "neo4j+s" - name: "standaloneHost" value: "localhost" - name: "standalonePort" value: "7687" - name: "standaloneDatabase" value: neo4j - name: "standaloneDashboardName" value: "test" - name: "standaloneDashboardDatabase" value: neo4j - name: "standaloneAllowLoad" value: "false" - name: "standaloneLoadFromOtherDatabases" value: "false" - name: "standaloneMultiDatabase" value: "false" # Environment variable from secret envFromSecrets: [] # standaloneUsername: # secretName: "neo4j-connection-secrets" # key: "username" # standalonePassword: # secretName: "neo4j-connection-secrets" # key: "password" # Service details service: type: LoadBalancer # Can also be ClusterIP or NodePort port: 5005 # For the service to listen in for Traffic targetPort: 5005 # Target port is the container port annotations: {} # Service annotations for the LoadBalance # Ingress ingress: enabled: false # Enable Kubernetes Ingress className: 'alb' # Class Name annotations: {} # Cloud LoadBalancer annotations hosts: [] # - host: neodash.example.com # paths: # - path: '/' # pathType: Prefix tls: [] # Pod resources request, limits and health check resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "128Mi" cpu: "500m" livenessProbe: httpGet: path: /* port: 5005 readinessProbe: httpGet: path: /* port: 5005 # Pod Autoscaler autoscaling: enabled: false # minReplicas: 1 # maxReplicas: 100 # targetCPUUtilizationPercentage: 80 # Pod Volumes volumes: [] volumeMounts: [] # Service Account serviceAccount: create: true automount: true # annotations: {} # name: '' ================================================ FILE: k8s-deploy/sample-k8s-yamls/deployment.yaml ================================================ --- # Source: neodash/templates/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: neodash labels: application: neodash-deploy spec: replicas: 1 selector: matchLabels: application: neodash-deploy template: metadata: labels: application: neodash-deploy spec: serviceAccountName: neodash-test automountServiceAccountToken: false containers: - name: neodash image: "neo4jlabs/neodash:latest" imagePullPolicy: IfNotPresent ports: - name: http containerPort: 5005 protocol: TCP env: - name: ssoEnabled value: "false" - name: standalone value: "true" - name: standaloneProtocol value: neo4j+s - name: standaloneHost value: localhost - name: standalonePort value: "7687" - name: standaloneDatabase value: neo4j - name: standaloneDashboardName value: test - name: standaloneDashboardDatabase value: neo4j - name: standaloneAllowLoad value: "false" - name: standaloneLoadFromOtherDatabases value: "false" - name: standaloneMultiDatabase value: "false" livenessProbe: httpGet: path: /* port: 5005 readinessProbe: httpGet: path: /* port: 5005 resources: limits: cpu: 500m memory: 128Mi requests: cpu: 250m memory: 64Mi ================================================ FILE: k8s-deploy/sample-k8s-yamls/service.yaml ================================================ --- # Source: neodash/templates/service.yaml apiVersion: v1 kind: Service metadata: name: neodash labels: application: neodash-deploy spec: type: LoadBalancer ports: - port: 5005 targetPort: 5005 protocol: TCP name: http selector: application: neodash-deploy ================================================ FILE: package.json ================================================ { "name": "neodash", "version": "2.4.11", "description": "NeoDash - Neo4j Dashboard Builder", "neo4jDesktop": { "apiVersion": "^1.2.0" }, "repository": { "type": "git", "url": "https://github.com/neo4j-labs/neodash/" }, "license": "Apache-2.0", "icons": [ { "src": "favicon.ico", "type": "ico" }, { "src": "favicon.png", "type": "png" } ], "scripts": { "dev": "yarn webpack-dev-server --mode development", "prod": "yarn webpack-dev-server --mode production", "debug": "yarn --node-options='--inspect' webpack-dev-server --mode development", "build": "yarn webpack --mode production --env production && cp -r public/* dist/", "build-minimal": "yarn webpack --mode production --env production && cp -r public/* dist/", "format": "prettier --write \"**/*.{ts,tsx}\"", "lint": "eslint --ext .ts --ext .tsx .", "lint-staged": "lint-staged --config .lintstagedrc.json", "test": "yarn cypress open", "test-headless": "yarn cypress run" }, "keywords": [], "author": "Neo4j Labs", "dependencies": { "@azure/openai": "^1.0.0-beta.2", "@codemirror/lang-markdown": "^6.1.1", "@dnd-kit/core": "^6.0.8", "@dnd-kit/sortable": "^7.0.2", "@mui/material": "^5.12.3", "@mui/styles": "^5.12.3", "@mui/x-data-grid": "7.4.0", "@mui/x-date-pickers": "^5.0.17", "@neo4j-cypher/react-codemirror": "^1.0.3", "@neo4j-ndl/base": "1.10.3", "@neo4j-ndl/react": "1.10.8", "@nivo/bar": "^0.83.0", "@nivo/circle-packing": "^0.83.0", "@nivo/core": "^0.83.0", "@nivo/geo": "^0.83.0", "@nivo/line": "^0.83.0", "@nivo/pie": "^0.83.0", "@nivo/radar": "^0.83.0", "@nivo/sankey": "^0.83.0", "@nivo/scatterplot": "^0.83.0", "@nivo/sunburst": "^0.83.0", "@nivo/treemap": "^0.83.0", "@sentry/react": "^7.57.0", "@sentry/webpack-plugin": "^2.7.1", "babel-runtime": "^6.26.0", "chroma-js": "^2.4.2", "classnames": "^2.3.1", "d3-scale-chromatic": "^3.0.0", "dayjs": "^1.11.7", "dom-to-image": "^2.6.0", "dompurify": "^3.1.0", "leaflet": "^1.7.1", "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", "lodash.merge": "^4.6.2", "mui-color": "^2.0.0-beta.2", "mui-nested-menu": "^3.2.1", "neo4j-client-sso": "^1.2.2", "openai": "^3.3.0", "postcss": "^8.4.21", "postcss-loader": "^7.2.4", "postcss-preset-env": "^8.3.0", "prop-types": "^15.8.1", "react": "^17.0.2", "react-cool-dimensions": "^2.0.7", "react-dom": "^17.0.2", "react-force-graph-2d": "^1.23.8", "react-force-graph-3d": "^1.24.1", "react-gauge-chart": "^0.4.1", "react-grid-layout": "^1.3.4", "react-leaflet": "^3.2.5", "react-leaflet-cluster": "^1.0.4", "react-leaflet-enhanced-marker": "^1.0.21", "react-leaflet-heatmap-layer-v3": "^3.0.3-beta-1", "react-markdown": "^8.0.0", "react-redux": "^7.2.6", "react-show-more-text": "^1.6.2", "react-toggle-dark-mode": "^1.1.1", "react-use-error-boundary": "^3.0.0", "redux-persist": "^6.0.0", "redux-thunk": "^2.4.1", "remark-gfm": "^3.0.1", "reselect": "^4.1.8", "tailwindcss": "^3.3.2", "three": "^0.159.0", "three-spritetext": "^1.8.1", "urijs": "^1.19.11", "use-neo4j": "^0.3.13", "yaml": "^2.2.1" }, "devDependencies": { "@babel/cli": "^7.16.8", "@babel/core": "^7.16.12", "@babel/plugin-transform-runtime": "^7.16.10", "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.16.7", "@babel/register": "^7.16.9", "@cypress/code-coverage": "^3.10.8", "@emotion/react": "^11.7.1", "@emotion/styled": "^11.6.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@redux-devtools/extension": "^3.2.3", "@typescript-eslint/eslint-plugin": "^5.42.0", "@typescript-eslint/parser": "^5.42.0", "babel-loader": "^8.2.3", "babel-plugin-istanbul": "^6.1.1", "circular-dependency-plugin": "^5.2.2", "css-loader": "^3.6.0", "cypress": "^12.17.4", "eslint": "^8.26.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", "file-loader": "^6.2.0", "husky": "^8.0.1", "lint-staged": "^13.0.3", "prettier": "^2.7.1", "react-refresh": "^0.14.0", "serverless-finch": "^4.0.0", "source-map-loader": "^4.0.0", "style-loader": "^1.1.3", "styled-components": "^5.3.3", "typescript": "^4.8.4", "webpack": "^5.77.0", "webpack-cli": "^4.9.1", "webpack-dev-server": "^4.7.3" } } ================================================ FILE: postcss.config.js ================================================ const tailwindcss = require('tailwindcss'); module.exports = { plugins: ['postcss-preset-env', tailwindcss], }; ================================================ FILE: public/README.md ================================================ # Web Content Directory After building the application with `npm run build`, deploy this folder (now renamed from `public` to `build`) to your webserver. ================================================ FILE: public/config.json ================================================ { "ssoEnabled": false, "ssoProviders": [], "ssoDiscoveryUrl": "https://example.com", "standalone": false, "standaloneProtocol": "neo4j+s", "standaloneHost": "localhost", "standalonePort": "7687", "standaloneDatabase": "neo4j", "standaloneDashboardName": "My Dashboard", "standaloneDashboardDatabase": "dashboards", "standaloneDashboardURL": "", "standaloneAllowLoad": false, "standaloneLoadFromOtherDatabases": false, "standaloneMultiDatabase": false, "standaloneDatabaseList": "neo4j", "loggingMode": "0", "loggingDatabase": "logs", "customHeader": "" } ================================================ FILE: public/embed-test.html ================================================ Embed test

I am an iFrame of the page located at https://neodash.graphapp.io/embed-test.html

I'm embedded directly into a dashboard, and dynamically passed the user-made parameter selections.

I will not refresh when selections are updated, but, I can see variables change.

You can use me to embed external visualizations that are updated together with other charts.

Your dashboard variables:

    

================================================ FILE: public/index.html ================================================ NeoDash - Neo4j Dashboard Builder

================================================ FILE: public/manifest.json ================================================ { "short_name": "NeoDash", "name": "NeoDash", "homepage": "https://github.com/neo4j-labs/neodash/", "neo4jDesktop": { "apiVersion": "^1.2.0" }, "icons": [ { "src": "favicon.ico", "type": "ico" }, { "src": "favicon.png", "type": "png" } ], "start_url": "./index.html", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: public/style.config.json ================================================ {} ================================================ FILE: public/style.css ================================================ /* Needle */ .logo-btn.large .ndl-icon { width: 36px !important; height: 36px !important; } .ndl-modal hr { margin-top: 0.5em !important; margin-bottom: 0.5em !important; } .centered { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .MuiInputBase-root { font-family: 'Nunito Sans', sans-serif !important; } /* End Needle */ .MuiDrawer-paper { position: relative !important; } .blue-grey { background: #607d8b !important; color: white !important; } .no-underline .MuiInput-underline::before { border-bottom: none; } .MuiContainer-root { padding-left: 0px !important; padding-right: 0px !important; } .MuiFormControl-root { padding-top: 0px; } .large input { font-size: 20px; margin-top: -6px; } .MuiChip-root { border-radius: 16px !important; } .CodeMirror-lint-message-error { background-image: none !important; } .leaflet-custom-node-popup { margin-left: 1px; margin-bottom: 34px !important; } .leaflet-custom-rel-popup { margin-left: 0px; } .leaflet-marker-icon { width: 50px !important; margin-left: -25px !important; } /* Hack to make the table header smaller, TODO - clean this up */ .table-small-header { height: 36px; } .MuiDataGrid-root { border: none !important; } .MuiDataGrid-footerContainer > div { margin-top: -40px; } .MuiChip-root:before { border: none !important; } .react-resizable-handle { bottom: 4px !important; right: -2px !important; opacity: 0.5; color: rgb(222, 222, 222); } .MuiDataGrid-footerContainer { border: none !important; } .neodash-card-editable-false .react-resizable-handle { display: none; } .react-grid-item > .react-resizable-handle.react-resizable-handle-se { cursor: nwse-resize !important; } .react-grid-item > .react-resizable-handle::after { border-right: 2px solid rgba(0, 0, 0, 0.4) !important; border-bottom: 2px solid rgba(0, 0, 0, 0.4) !important; } div:has(> .table-small-header) { background: unset !important; } .MuiDataGrid-footerContainer { border-top: none !important; } .MuiTablePagination-root { margin-top: -10px; } .MuiDataGrid-panel { translate: 0px -152%; } .MuiCard-root { box-shadow: 0 0 #0000, 0 0 #0000, var(--tw-shadow) !important; box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow) !important; } .white-text { color: white !important; } .MuiCardHeader-content .MuiInputBase-root.Mui-disabled { color: inherit !important; } .MuiCardHeader-content .MuiInput-underline.Mui-disabled:before { border-bottom-style: none !important; } .textinput-linenumbers { scrollbar-width: none; resize: none; background: url(linenumbers.png); background-size: 20; background-attachment: local; background-repeat: no-repeat; font-family: monospace; font-size: 14px; line-height: 16px; border-color: #fff; width: calc(100% + 30px); display: block; background-size: 26px; white-space: pre; background-position: 0px -11px; padding-right: 0px !important; margin-top: 0px; padding-left: 30px; padding-top: 0px !important; padding-bottom: 0px !important; margin-bottom: 0px !important; overflow-x: scroll !important; overflow-y: scroll; min-height: 116px; } .textinput-linenumbers::-webkit-scrollbar { display: none; } .MuiDrawer-docked .MuiDrawer-paper { overflow-x: hidden; } #center-aligned { text-align: center; } .card-view.expanded { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: white; z-index: 1299; } .force-graph-container .graph-tooltip { color: black !important; background: none !important; } .not-animated .react-grid-item.cssTransforms { transition-property: none !important; } .react-grid-layout { overflow-x: hidden; } .cm-tooltip-autocomplete { margin-top: 4px; } .card-view .MuiTablePagination-root { margin-top: 0px; } @keyframes pulse { 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7); } 70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); } 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); } } /* Workaround for Needle not handling menu placement of dropdowns on modals */ #overlay { z-index: 99 !important; position: absolute; } /* End workaround */ /* Workaround for cleaning the Gantt chart UI */ .gantt-wrapper > div > div:first-child > div:first-child > div:first-child > div > div:not(:first-child) { display: none; } .gantt-wrapper > div > div > div > div > div > div > div:not(:first-child) { display: none; } /* End Gantt chart workaround */ .markdown-widget a { text-decoration: underline; } ================================================ FILE: release-notes.md ================================================ ## NeoDash 2.4.11 - Fixed deeplinking in standalone mode - Added deprecation notice. ================================================ FILE: scripts/config-entrypoint.sh ================================================ #!/bin/sh ########### set -e echo " \ { \ \"ssoEnabled\": ${ssoEnabled:=false}, \ \"ssoProviders\": ${ssoProviders:=[]}, \ \"ssoDiscoveryUrl\": \"${ssoDiscoveryUrl:='https://example.com'}\", \ \"standalone\": ${standalone:=false}, \ \"standaloneProtocol\": \"${standaloneProtocol:='neo4j+s'}\", \ \"standaloneHost\": \"${standaloneHost:='test.databases.neo4j.io'}\", \ \"standalonePort\": ${standalonePort:=7687}, \ \"standaloneDatabase\": \"${standaloneDatabase:='neo4j'}\", \ \"standaloneUsername\": \"${standaloneUsername:=}\", \ \"standalonePassword\": \"${standalonePassword:=}\", \ \"standaloneDashboardName\": \"${standaloneDashboardName:='My Dashboard'}\", \ \"standaloneDashboardDatabase\": \"${standaloneDashboardDatabase:='neo4j'}\", \ \"standaloneDashboardURL\": \"${standaloneDashboardURL:=}\", \ \"standaloneAllowLoad\": ${standaloneAllowLoad:=false}, \ \"standaloneLoadFromOtherDatabases\": ${standaloneLoadFromOtherDatabases:=false}, \ \"standaloneMultiDatabase\": ${standaloneMultiDatabase:=false}, \ \"standaloneDatabaseList\": \"${standaloneDatabaseList:='neo4j'}\", \ \"standalonePasswordWarningHidden\": ${standalonePasswordWarningHidden:=false}, \ \"loggingMode\": \"${loggingMode:='0'}\", \ \"loggingDatabase\": \"${loggingDatabase:='logs'}\", \ \"customHeader\": \"${customHeader:=}\" \ }" > /usr/share/nginx/html/config.json echo " \ { \ \"DASHBOARD_HEADER_BRAND_LOGO\": \"${DASHBOARD_HEADER_BRAND_LOGO:=}\", \ \"DASHBOARD_HEADER_COLOR\" : \"${DASHBOARD_HEADER_COLOR:=}\", \ \"DASHBOARD_HEADER_BUTTON_COLOR\" : \"${DASHBOARD_HEADER_BUTTON_COLOR:=}\", \ \"DASHBOARD_HEADER_TITLE_COLOR\" : \"${DASHBOARD_HEADER_TITLE_COLOR:=}\", \ \"DASHBOARD_PAGE_LIST_COLOR\" : \"${DASHBOARD_PAGE_LIST_COLOR:=}\", \ \"DASHBOARD_PAGE_LIST_ACTIVE_COLOR\": \"${DASHBOARD_PAGE_LIST_ACTIVE_COLOR:=}\", \ \"style\": { \ \"--palette-light-neutral-bg-weak\": \"${STYLE_PALETTE_LIGHT_NEUTRAL_BG_WEAK:=}\" \ } \ }" > /usr/share/nginx/html/style.config.json ================================================ FILE: scripts/docker-neo4j-initializer/docker-neo4j.sh ================================================ docker run \ --name neo4j \ -p7474:7474 -p7687:7687 \ -d \ --env NEO4J_AUTH=neo4j/test1234 \ neo4j:4.4 ================================================ FILE: scripts/docker-neo4j-initializer/movies.cypher ================================================ CREATE CONSTRAINT IF NOT EXISTS FOR (p:Person) REQUIRE (p.name) IS UNIQUE; CREATE INDEX IF NOT EXISTS FOR (p:Person) ON (p.born); CREATE CONSTRAINT IF NOT EXISTS FOR (m:Movie) REQUIRE (m.title) IS UNIQUE; CREATE INDEX IF NOT EXISTS FOR (m:Movie) ON (m.released); CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'}) CREATE (Keanu:Person {name:'Keanu Reeves', born:1964}) CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967}) CREATE (Laurence:Person {name:'Laurence Fishburne', born:1961}) CREATE (Hugo:Person {name:'Hugo Weaving', born:1960}) CREATE (LillyW:Person {name:'Lilly Wachowski', born:1967}) CREATE (LanaW:Person {name:'Lana Wachowski', born:1965}) CREATE (JoelS:Person {name:'Joel Silver', born:1952}) CREATE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix), (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix), (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix), (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix), (LillyW)-[:DIRECTED]->(TheMatrix), (LanaW)-[:DIRECTED]->(TheMatrix), (JoelS)-[:PRODUCED]->(TheMatrix) CREATE (Emil:Person {name:"Emil Eifrem", born:1978}) CREATE (Emil)-[:ACTED_IN {roles:["Emil"]}]->(TheMatrix) CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'}) CREATE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded), (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded), (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded), (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded), (LillyW)-[:DIRECTED]->(TheMatrixReloaded), (LanaW)-[:DIRECTED]->(TheMatrixReloaded), (JoelS)-[:PRODUCED]->(TheMatrixReloaded) CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'}) CREATE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions), (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions), (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions), (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions), (LillyW)-[:DIRECTED]->(TheMatrixRevolutions), (LanaW)-[:DIRECTED]->(TheMatrixRevolutions), (JoelS)-[:PRODUCED]->(TheMatrixRevolutions) CREATE (TheDevilsAdvocate:Movie {title:"The Devil's Advocate", released:1997, tagline:'Evil has its winning ways'}) CREATE (Charlize:Person {name:'Charlize Theron', born:1975}) CREATE (Al:Person {name:'Al Pacino', born:1940}) CREATE (Taylor:Person {name:'Taylor Hackford', born:1944}) CREATE (Keanu)-[:ACTED_IN {roles:['Kevin Lomax']}]->(TheDevilsAdvocate), (Charlize)-[:ACTED_IN {roles:['Mary Ann Lomax']}]->(TheDevilsAdvocate), (Al)-[:ACTED_IN {roles:['John Milton']}]->(TheDevilsAdvocate), (Taylor)-[:DIRECTED]->(TheDevilsAdvocate) CREATE (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."}) CREATE (TomC:Person {name:'Tom Cruise', born:1962}) CREATE (JackN:Person {name:'Jack Nicholson', born:1937}) CREATE (DemiM:Person {name:'Demi Moore', born:1962}) CREATE (KevinB:Person {name:'Kevin Bacon', born:1958}) CREATE (KieferS:Person {name:'Kiefer Sutherland', born:1966}) CREATE (NoahW:Person {name:'Noah Wyle', born:1971}) CREATE (CubaG:Person {name:'Cuba Gooding Jr.', born:1968}) CREATE (KevinP:Person {name:'Kevin Pollak', born:1957}) CREATE (JTW:Person {name:'J.T. Walsh', born:1943}) CREATE (JamesM:Person {name:'James Marshall', born:1967}) CREATE (ChristopherG:Person {name:'Christopher Guest', born:1948}) CREATE (RobR:Person {name:'Rob Reiner', born:1947}) CREATE (AaronS:Person {name:'Aaron Sorkin', born:1961}) CREATE (TomC)-[:ACTED_IN {roles:['Lt. Daniel Kaffee']}]->(AFewGoodMen), (JackN)-[:ACTED_IN {roles:['Col. Nathan R. Jessup']}]->(AFewGoodMen), (DemiM)-[:ACTED_IN {roles:['Lt. Cdr. JoAnne Galloway']}]->(AFewGoodMen), (KevinB)-[:ACTED_IN {roles:['Capt. Jack Ross']}]->(AFewGoodMen), (KieferS)-[:ACTED_IN {roles:['Lt. Jonathan Kendrick']}]->(AFewGoodMen), (NoahW)-[:ACTED_IN {roles:['Cpl. Jeffrey Barnes']}]->(AFewGoodMen), (CubaG)-[:ACTED_IN {roles:['Cpl. Carl Hammaker']}]->(AFewGoodMen), (KevinP)-[:ACTED_IN {roles:['Lt. Sam Weinberg']}]->(AFewGoodMen), (JTW)-[:ACTED_IN {roles:['Lt. Col. Matthew Andrew Markinson']}]->(AFewGoodMen), (JamesM)-[:ACTED_IN {roles:['Pfc. Louden Downey']}]->(AFewGoodMen), (ChristopherG)-[:ACTED_IN {roles:['Dr. Stone']}]->(AFewGoodMen), (AaronS)-[:ACTED_IN {roles:['Man in Bar']}]->(AFewGoodMen), (RobR)-[:DIRECTED]->(AFewGoodMen), (AaronS)-[:WROTE]->(AFewGoodMen) CREATE (TopGun:Movie {title:"Top Gun", released:1986, tagline:'I feel the need, the need for speed.'}) CREATE (KellyM:Person {name:'Kelly McGillis', born:1957}) CREATE (ValK:Person {name:'Val Kilmer', born:1959}) CREATE (AnthonyE:Person {name:'Anthony Edwards', born:1962}) CREATE (TomS:Person {name:'Tom Skerritt', born:1933}) CREATE (MegR:Person {name:'Meg Ryan', born:1961}) CREATE (TonyS:Person {name:'Tony Scott', born:1944}) CREATE (JimC:Person {name:'Jim Cash', born:1941}) CREATE (TomC)-[:ACTED_IN {roles:['Maverick']}]->(TopGun), (KellyM)-[:ACTED_IN {roles:['Charlie']}]->(TopGun), (ValK)-[:ACTED_IN {roles:['Iceman']}]->(TopGun), (AnthonyE)-[:ACTED_IN {roles:['Goose']}]->(TopGun), (TomS)-[:ACTED_IN {roles:['Viper']}]->(TopGun), (MegR)-[:ACTED_IN {roles:['Carole']}]->(TopGun), (TonyS)-[:DIRECTED]->(TopGun), (JimC)-[:WROTE]->(TopGun) CREATE (JerryMaguire:Movie {title:'Jerry Maguire', released:2000, tagline:'The rest of his life begins now.'}) CREATE (ReneeZ:Person {name:'Renee Zellweger', born:1969}) CREATE (KellyP:Person {name:'Kelly Preston', born:1962}) CREATE (JerryO:Person {name:"Jerry O'Connell", born:1974}) CREATE (JayM:Person {name:'Jay Mohr', born:1970}) CREATE (BonnieH:Person {name:'Bonnie Hunt', born:1961}) CREATE (ReginaK:Person {name:'Regina King', born:1971}) CREATE (JonathanL:Person {name:'Jonathan Lipnicki', born:1996}) CREATE (CameronC:Person {name:'Cameron Crowe', born:1957}) CREATE (TomC)-[:ACTED_IN {roles:['Jerry Maguire']}]->(JerryMaguire), (CubaG)-[:ACTED_IN {roles:['Rod Tidwell']}]->(JerryMaguire), (ReneeZ)-[:ACTED_IN {roles:['Dorothy Boyd']}]->(JerryMaguire), (KellyP)-[:ACTED_IN {roles:['Avery Bishop']}]->(JerryMaguire), (JerryO)-[:ACTED_IN {roles:['Frank Cushman']}]->(JerryMaguire), (JayM)-[:ACTED_IN {roles:['Bob Sugar']}]->(JerryMaguire), (BonnieH)-[:ACTED_IN {roles:['Laurel Boyd']}]->(JerryMaguire), (ReginaK)-[:ACTED_IN {roles:['Marcee Tidwell']}]->(JerryMaguire), (JonathanL)-[:ACTED_IN {roles:['Ray Boyd']}]->(JerryMaguire), (CameronC)-[:DIRECTED]->(JerryMaguire), (CameronC)-[:PRODUCED]->(JerryMaguire), (CameronC)-[:WROTE]->(JerryMaguire) CREATE (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."}) CREATE (RiverP:Person {name:'River Phoenix', born:1970}) CREATE (CoreyF:Person {name:'Corey Feldman', born:1971}) CREATE (WilW:Person {name:'Wil Wheaton', born:1972}) CREATE (JohnC:Person {name:'John Cusack', born:1966}) CREATE (MarshallB:Person {name:'Marshall Bell', born:1942}) CREATE (WilW)-[:ACTED_IN {roles:['Gordie Lachance']}]->(StandByMe), (RiverP)-[:ACTED_IN {roles:['Chris Chambers']}]->(StandByMe), (JerryO)-[:ACTED_IN {roles:['Vern Tessio']}]->(StandByMe), (CoreyF)-[:ACTED_IN {roles:['Teddy Duchamp']}]->(StandByMe), (JohnC)-[:ACTED_IN {roles:['Denny Lachance']}]->(StandByMe), (KieferS)-[:ACTED_IN {roles:['Ace Merrill']}]->(StandByMe), (MarshallB)-[:ACTED_IN {roles:['Mr. Lachance']}]->(StandByMe), (RobR)-[:DIRECTED]->(StandByMe) CREATE (AsGoodAsItGets:Movie {title:'As Good as It Gets', released:1997, tagline:'A comedy from the heart that goes for the throat.'}) CREATE (HelenH:Person {name:'Helen Hunt', born:1963}) CREATE (GregK:Person {name:'Greg Kinnear', born:1963}) CREATE (JamesB:Person {name:'James L. Brooks', born:1940}) CREATE (JackN)-[:ACTED_IN {roles:['Melvin Udall']}]->(AsGoodAsItGets), (HelenH)-[:ACTED_IN {roles:['Carol Connelly']}]->(AsGoodAsItGets), (GregK)-[:ACTED_IN {roles:['Simon Bishop']}]->(AsGoodAsItGets), (CubaG)-[:ACTED_IN {roles:['Frank Sachs']}]->(AsGoodAsItGets), (JamesB)-[:DIRECTED]->(AsGoodAsItGets) CREATE (WhatDreamsMayCome:Movie {title:'What Dreams May Come', released:1998, tagline:'After life there is more. The end is just the beginning.'}) CREATE (AnnabellaS:Person {name:'Annabella Sciorra', born:1960}) CREATE (MaxS:Person {name:'Max von Sydow', born:1929}) CREATE (WernerH:Person {name:'Werner Herzog', born:1942}) CREATE (Robin:Person {name:'Robin Williams', born:1951}) CREATE (VincentW:Person {name:'Vincent Ward', born:1956}) CREATE (Robin)-[:ACTED_IN {roles:['Chris Nielsen']}]->(WhatDreamsMayCome), (CubaG)-[:ACTED_IN {roles:['Albert Lewis']}]->(WhatDreamsMayCome), (AnnabellaS)-[:ACTED_IN {roles:['Annie Collins-Nielsen']}]->(WhatDreamsMayCome), (MaxS)-[:ACTED_IN {roles:['The Tracker']}]->(WhatDreamsMayCome), (WernerH)-[:ACTED_IN {roles:['The Face']}]->(WhatDreamsMayCome), (VincentW)-[:DIRECTED]->(WhatDreamsMayCome) CREATE (SnowFallingonCedars:Movie {title:'Snow Falling on Cedars', released:1999, tagline:'First loves last. Forever.'}) CREATE (EthanH:Person {name:'Ethan Hawke', born:1970}) CREATE (RickY:Person {name:'Rick Yune', born:1971}) CREATE (JamesC:Person {name:'James Cromwell', born:1940}) CREATE (ScottH:Person {name:'Scott Hicks', born:1953}) CREATE (EthanH)-[:ACTED_IN {roles:['Ishmael Chambers']}]->(SnowFallingonCedars), (RickY)-[:ACTED_IN {roles:['Kazuo Miyamoto']}]->(SnowFallingonCedars), (MaxS)-[:ACTED_IN {roles:['Nels Gudmundsson']}]->(SnowFallingonCedars), (JamesC)-[:ACTED_IN {roles:['Judge Fielding']}]->(SnowFallingonCedars), (ScottH)-[:DIRECTED]->(SnowFallingonCedars) CREATE (YouveGotMail:Movie {title:"You've Got Mail", released:1998, tagline:'At odds in life... in love on-line.'}) CREATE (ParkerP:Person {name:'Parker Posey', born:1968}) CREATE (DaveC:Person {name:'Dave Chappelle', born:1973}) CREATE (SteveZ:Person {name:'Steve Zahn', born:1967}) CREATE (TomH:Person {name:'Tom Hanks', born:1956}) CREATE (NoraE:Person {name:'Nora Ephron', born:1941}) CREATE (TomH)-[:ACTED_IN {roles:['Joe Fox']}]->(YouveGotMail), (MegR)-[:ACTED_IN {roles:['Kathleen Kelly']}]->(YouveGotMail), (GregK)-[:ACTED_IN {roles:['Frank Navasky']}]->(YouveGotMail), (ParkerP)-[:ACTED_IN {roles:['Patricia Eden']}]->(YouveGotMail), (DaveC)-[:ACTED_IN {roles:['Kevin Jackson']}]->(YouveGotMail), (SteveZ)-[:ACTED_IN {roles:['George Pappas']}]->(YouveGotMail), (NoraE)-[:DIRECTED]->(YouveGotMail) CREATE (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?'}) CREATE (RitaW:Person {name:'Rita Wilson', born:1956}) CREATE (BillPull:Person {name:'Bill Pullman', born:1953}) CREATE (VictorG:Person {name:'Victor Garber', born:1949}) CREATE (RosieO:Person {name:"Rosie O'Donnell", born:1962}) CREATE (TomH)-[:ACTED_IN {roles:['Sam Baldwin']}]->(SleeplessInSeattle), (MegR)-[:ACTED_IN {roles:['Annie Reed']}]->(SleeplessInSeattle), (RitaW)-[:ACTED_IN {roles:['Suzy']}]->(SleeplessInSeattle), (BillPull)-[:ACTED_IN {roles:['Walter']}]->(SleeplessInSeattle), (VictorG)-[:ACTED_IN {roles:['Greg']}]->(SleeplessInSeattle), (RosieO)-[:ACTED_IN {roles:['Becky']}]->(SleeplessInSeattle), (NoraE)-[:DIRECTED]->(SleeplessInSeattle) CREATE (JoeVersustheVolcano:Movie {title:'Joe Versus the Volcano', released:1990, tagline:'A story of love, lava and burning desire.'}) CREATE (JohnS:Person {name:'John Patrick Stanley', born:1950}) CREATE (Nathan:Person {name:'Nathan Lane', born:1956}) CREATE (TomH)-[:ACTED_IN {roles:['Joe Banks']}]->(JoeVersustheVolcano), (MegR)-[:ACTED_IN {roles:['DeDe', 'Angelica Graynamore', 'Patricia Graynamore']}]->(JoeVersustheVolcano), (Nathan)-[:ACTED_IN {roles:['Baw']}]->(JoeVersustheVolcano), (JohnS)-[:DIRECTED]->(JoeVersustheVolcano) CREATE (WhenHarryMetSally:Movie {title:'When Harry Met Sally', released:1998, tagline:'Can two friends sleep together and still love each other in the morning?'}) CREATE (BillyC:Person {name:'Billy Crystal', born:1948}) CREATE (CarrieF:Person {name:'Carrie Fisher', born:1956}) CREATE (BrunoK:Person {name:'Bruno Kirby', born:1949}) CREATE (BillyC)-[:ACTED_IN {roles:['Harry Burns']}]->(WhenHarryMetSally), (MegR)-[:ACTED_IN {roles:['Sally Albright']}]->(WhenHarryMetSally), (CarrieF)-[:ACTED_IN {roles:['Marie']}]->(WhenHarryMetSally), (BrunoK)-[:ACTED_IN {roles:['Jess']}]->(WhenHarryMetSally), (RobR)-[:DIRECTED]->(WhenHarryMetSally), (RobR)-[:PRODUCED]->(WhenHarryMetSally), (NoraE)-[:PRODUCED]->(WhenHarryMetSally), (NoraE)-[:WROTE]->(WhenHarryMetSally) CREATE (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'}) CREATE (LivT:Person {name:'Liv Tyler', born:1977}) CREATE (TomH)-[:ACTED_IN {roles:['Mr. White']}]->(ThatThingYouDo), (LivT)-[:ACTED_IN {roles:['Faye Dolan']}]->(ThatThingYouDo), (Charlize)-[:ACTED_IN {roles:['Tina']}]->(ThatThingYouDo), (TomH)-[:DIRECTED]->(ThatThingYouDo) CREATE (TheReplacements:Movie {title:'The Replacements', released:2000, tagline:'Pain heals, Chicks dig scars... Glory lasts forever'}) CREATE (Brooke:Person {name:'Brooke Langton', born:1970}) CREATE (Gene:Person {name:'Gene Hackman', born:1930}) CREATE (Orlando:Person {name:'Orlando Jones', born:1968}) CREATE (Howard:Person {name:'Howard Deutch', born:1950}) CREATE (Keanu)-[:ACTED_IN {roles:['Shane Falco']}]->(TheReplacements), (Brooke)-[:ACTED_IN {roles:['Annabelle Farrell']}]->(TheReplacements), (Gene)-[:ACTED_IN {roles:['Jimmy McGinty']}]->(TheReplacements), (Orlando)-[:ACTED_IN {roles:['Clifford Franklin']}]->(TheReplacements), (Howard)-[:DIRECTED]->(TheReplacements) CREATE (RescueDawn:Movie {title:'RescueDawn', released:2006, tagline:"Based on the extraordinary true story of one man's fight for freedom"}) CREATE (ChristianB:Person {name:'Christian Bale', born:1974}) CREATE (ZachG:Person {name:'Zach Grenier', born:1954}) CREATE (MarshallB)-[:ACTED_IN {roles:['Admiral']}]->(RescueDawn), (ChristianB)-[:ACTED_IN {roles:['Dieter Dengler']}]->(RescueDawn), (ZachG)-[:ACTED_IN {roles:['Squad Leader']}]->(RescueDawn), (SteveZ)-[:ACTED_IN {roles:['Duane']}]->(RescueDawn), (WernerH)-[:DIRECTED]->(RescueDawn) CREATE (TheBirdcage:Movie {title:'The Birdcage', released:1996, tagline:'Come as you are'}) CREATE (MikeN:Person {name:'Mike Nichols', born:1931}) CREATE (Robin)-[:ACTED_IN {roles:['Armand Goldman']}]->(TheBirdcage), (Nathan)-[:ACTED_IN {roles:['Albert Goldman']}]->(TheBirdcage), (Gene)-[:ACTED_IN {roles:['Sen. Kevin Keeley']}]->(TheBirdcage), (MikeN)-[:DIRECTED]->(TheBirdcage) CREATE (Unforgiven:Movie {title:'Unforgiven', released:1992, tagline:"It's a hell of a thing, killing a man"}) CREATE (RichardH:Person {name:'Richard Harris', born:1930}) CREATE (ClintE:Person {name:'Clint Eastwood', born:1930}) CREATE (RichardH)-[:ACTED_IN {roles:['English Bob']}]->(Unforgiven), (ClintE)-[:ACTED_IN {roles:['Bill Munny']}]->(Unforgiven), (Gene)-[:ACTED_IN {roles:['Little Bill Daggett']}]->(Unforgiven), (ClintE)-[:DIRECTED]->(Unforgiven) CREATE (JohnnyMnemonic:Movie {title:'Johnny Mnemonic', released:1995, tagline:'The hottest data on earth. In the coolest head in town'}) CREATE (Takeshi:Person {name:'Takeshi Kitano', born:1947}) CREATE (Dina:Person {name:'Dina Meyer', born:1968}) CREATE (IceT:Person {name:'Ice-T', born:1958}) CREATE (RobertL:Person {name:'Robert Longo', born:1953}) CREATE (Keanu)-[:ACTED_IN {roles:['Johnny Mnemonic']}]->(JohnnyMnemonic), (Takeshi)-[:ACTED_IN {roles:['Takahashi']}]->(JohnnyMnemonic), (Dina)-[:ACTED_IN {roles:['Jane']}]->(JohnnyMnemonic), (IceT)-[:ACTED_IN {roles:['J-Bone']}]->(JohnnyMnemonic), (RobertL)-[:DIRECTED]->(JohnnyMnemonic) CREATE (CloudAtlas:Movie {title:'Cloud Atlas', released:2012, tagline:'Everything is connected'}) CREATE (HalleB:Person {name:'Halle Berry', born:1966}) CREATE (JimB:Person {name:'Jim Broadbent', born:1949}) CREATE (TomT:Person {name:'Tom Tykwer', born:1965}) CREATE (DavidMitchell:Person {name:'David Mitchell', born:1969}) CREATE (StefanArndt:Person {name:'Stefan Arndt', born:1961}) CREATE (TomH)-[:ACTED_IN {roles:['Zachry', 'Dr. Henry Goose', 'Isaac Sachs', 'Dermot Hoggins']}]->(CloudAtlas), (Hugo)-[:ACTED_IN {roles:['Bill Smoke', 'Haskell Moore', 'Tadeusz Kesselring', 'Nurse Noakes', 'Boardman Mephi', 'Old Georgie']}]->(CloudAtlas), (HalleB)-[:ACTED_IN {roles:['Luisa Rey', 'Jocasta Ayrs', 'Ovid', 'Meronym']}]->(CloudAtlas), (JimB)-[:ACTED_IN {roles:['Vyvyan Ayrs', 'Captain Molyneux', 'Timothy Cavendish']}]->(CloudAtlas), (TomT)-[:DIRECTED]->(CloudAtlas), (LillyW)-[:DIRECTED]->(CloudAtlas), (LanaW)-[:DIRECTED]->(CloudAtlas), (DavidMitchell)-[:WROTE]->(CloudAtlas), (StefanArndt)-[:PRODUCED]->(CloudAtlas) CREATE (TheDaVinciCode:Movie {title:'The Da Vinci Code', released:2006, tagline:'Break The Codes'}) CREATE (IanM:Person {name:'Ian McKellen', born:1939}) CREATE (AudreyT:Person {name:'Audrey Tautou', born:1976}) CREATE (PaulB:Person {name:'Paul Bettany', born:1971}) CREATE (RonH:Person {name:'Ron Howard', born:1954}) CREATE (TomH)-[:ACTED_IN {roles:['Dr. Robert Langdon']}]->(TheDaVinciCode), (IanM)-[:ACTED_IN {roles:['Sir Leight Teabing']}]->(TheDaVinciCode), (AudreyT)-[:ACTED_IN {roles:['Sophie Neveu']}]->(TheDaVinciCode), (PaulB)-[:ACTED_IN {roles:['Silas']}]->(TheDaVinciCode), (RonH)-[:DIRECTED]->(TheDaVinciCode) CREATE (VforVendetta:Movie {title:'V for Vendetta', released:2006, tagline:'Freedom! Forever!'}) CREATE (NatalieP:Person {name:'Natalie Portman', born:1981}) CREATE (StephenR:Person {name:'Stephen Rea', born:1946}) CREATE (JohnH:Person {name:'John Hurt', born:1940}) CREATE (BenM:Person {name: 'Ben Miles', born:1967}) CREATE (Hugo)-[:ACTED_IN {roles:['V']}]->(VforVendetta), (NatalieP)-[:ACTED_IN {roles:['Evey Hammond']}]->(VforVendetta), (StephenR)-[:ACTED_IN {roles:['Eric Finch']}]->(VforVendetta), (JohnH)-[:ACTED_IN {roles:['High Chancellor Adam Sutler']}]->(VforVendetta), (BenM)-[:ACTED_IN {roles:['Dascomb']}]->(VforVendetta), (JamesM)-[:DIRECTED]->(VforVendetta), (LillyW)-[:PRODUCED]->(VforVendetta), (LanaW)-[:PRODUCED]->(VforVendetta), (JoelS)-[:PRODUCED]->(VforVendetta), (LillyW)-[:WROTE]->(VforVendetta), (LanaW)-[:WROTE]->(VforVendetta) CREATE (SpeedRacer:Movie {title:'Speed Racer', released:2008, tagline:'Speed has no limits'}) CREATE (EmileH:Person {name:'Emile Hirsch', born:1985}) CREATE (JohnG:Person {name:'John Goodman', born:1960}) CREATE (SusanS:Person {name:'Susan Sarandon', born:1946}) CREATE (MatthewF:Person {name:'Matthew Fox', born:1966}) CREATE (ChristinaR:Person {name:'Christina Ricci', born:1980}) CREATE (Rain:Person {name:'Rain', born:1982}) CREATE (EmileH)-[:ACTED_IN {roles:['Speed Racer']}]->(SpeedRacer), (JohnG)-[:ACTED_IN {roles:['Pops']}]->(SpeedRacer), (SusanS)-[:ACTED_IN {roles:['Mom']}]->(SpeedRacer), (MatthewF)-[:ACTED_IN {roles:['Racer X']}]->(SpeedRacer), (ChristinaR)-[:ACTED_IN {roles:['Trixie']}]->(SpeedRacer), (Rain)-[:ACTED_IN {roles:['Taejo Togokahn']}]->(SpeedRacer), (BenM)-[:ACTED_IN {roles:['Cass Jones']}]->(SpeedRacer), (LillyW)-[:DIRECTED]->(SpeedRacer), (LanaW)-[:DIRECTED]->(SpeedRacer), (LillyW)-[:WROTE]->(SpeedRacer), (LanaW)-[:WROTE]->(SpeedRacer), (JoelS)-[:PRODUCED]->(SpeedRacer) CREATE (NinjaAssassin:Movie {title:'Ninja Assassin', released:2009, tagline:'Prepare to enter a secret world of assassins'}) CREATE (NaomieH:Person {name:'Naomie Harris'}) CREATE (Rain)-[:ACTED_IN {roles:['Raizo']}]->(NinjaAssassin), (NaomieH)-[:ACTED_IN {roles:['Mika Coretti']}]->(NinjaAssassin), (RickY)-[:ACTED_IN {roles:['Takeshi']}]->(NinjaAssassin), (BenM)-[:ACTED_IN {roles:['Ryan Maslow']}]->(NinjaAssassin), (JamesM)-[:DIRECTED]->(NinjaAssassin), (LillyW)-[:PRODUCED]->(NinjaAssassin), (LanaW)-[:PRODUCED]->(NinjaAssassin), (JoelS)-[:PRODUCED]->(NinjaAssassin) CREATE (TheGreenMile:Movie {title:'The Green Mile', released:1999, tagline:"Walk a mile you'll never forget."}) CREATE (MichaelD:Person {name:'Michael Clarke Duncan', born:1957}) CREATE (DavidM:Person {name:'David Morse', born:1953}) CREATE (SamR:Person {name:'Sam Rockwell', born:1968}) CREATE (GaryS:Person {name:'Gary Sinise', born:1955}) CREATE (PatriciaC:Person {name:'Patricia Clarkson', born:1959}) CREATE (FrankD:Person {name:'Frank Darabont', born:1959}) CREATE (TomH)-[:ACTED_IN {roles:['Paul Edgecomb']}]->(TheGreenMile), (MichaelD)-[:ACTED_IN {roles:['John Coffey']}]->(TheGreenMile), (DavidM)-[:ACTED_IN {roles:['Brutus "Brutal" Howell']}]->(TheGreenMile), (BonnieH)-[:ACTED_IN {roles:['Jan Edgecomb']}]->(TheGreenMile), (JamesC)-[:ACTED_IN {roles:['Warden Hal Moores']}]->(TheGreenMile), (SamR)-[:ACTED_IN {roles:['"Wild Bill" Wharton']}]->(TheGreenMile), (GaryS)-[:ACTED_IN {roles:['Burt Hammersmith']}]->(TheGreenMile), (PatriciaC)-[:ACTED_IN {roles:['Melinda Moores']}]->(TheGreenMile), (FrankD)-[:DIRECTED]->(TheGreenMile) CREATE (FrostNixon:Movie {title:'Frost/Nixon', released:2008, tagline:'400 million people were waiting for the truth.'}) CREATE (FrankL:Person {name:'Frank Langella', born:1938}) CREATE (MichaelS:Person {name:'Michael Sheen', born:1969}) CREATE (OliverP:Person {name:'Oliver Platt', born:1960}) CREATE (FrankL)-[:ACTED_IN {roles:['Richard Nixon']}]->(FrostNixon), (MichaelS)-[:ACTED_IN {roles:['David Frost']}]->(FrostNixon), (KevinB)-[:ACTED_IN {roles:['Jack Brennan']}]->(FrostNixon), (OliverP)-[:ACTED_IN {roles:['Bob Zelnick']}]->(FrostNixon), (SamR)-[:ACTED_IN {roles:['James Reston, Jr.']}]->(FrostNixon), (RonH)-[:DIRECTED]->(FrostNixon) CREATE (Hoffa:Movie {title:'Hoffa', released:1992, tagline:"He didn't want law. He wanted justice."}) CREATE (DannyD:Person {name:'Danny DeVito', born:1944}) CREATE (JohnR:Person {name:'John C. Reilly', born:1965}) CREATE (JackN)-[:ACTED_IN {roles:['Hoffa']}]->(Hoffa), (DannyD)-[:ACTED_IN {roles:['Robert "Bobby" Ciaro']}]->(Hoffa), (JTW)-[:ACTED_IN {roles:['Frank Fitzsimmons']}]->(Hoffa), (JohnR)-[:ACTED_IN {roles:['Peter "Pete" Connelly']}]->(Hoffa), (DannyD)-[:DIRECTED]->(Hoffa) CREATE (Apollo13:Movie {title:'Apollo 13', released:1995, tagline:'Houston, we have a problem.'}) CREATE (EdH:Person {name:'Ed Harris', born:1950}) CREATE (BillPax:Person {name:'Bill Paxton', born:1955}) CREATE (TomH)-[:ACTED_IN {roles:['Jim Lovell']}]->(Apollo13), (KevinB)-[:ACTED_IN {roles:['Jack Swigert']}]->(Apollo13), (EdH)-[:ACTED_IN {roles:['Gene Kranz']}]->(Apollo13), (BillPax)-[:ACTED_IN {roles:['Fred Haise']}]->(Apollo13), (GaryS)-[:ACTED_IN {roles:['Ken Mattingly']}]->(Apollo13), (RonH)-[:DIRECTED]->(Apollo13) CREATE (Twister:Movie {title:'Twister', released:1996, tagline:"Don't Breathe. Don't Look Back."}) CREATE (PhilipH:Person {name:'Philip Seymour Hoffman', born:1967}) CREATE (JanB:Person {name:'Jan de Bont', born:1943}) CREATE (BillPax)-[:ACTED_IN {roles:['Bill Harding']}]->(Twister), (HelenH)-[:ACTED_IN {roles:['Dr. Jo Harding']}]->(Twister), (ZachG)-[:ACTED_IN {roles:['Eddie']}]->(Twister), (PhilipH)-[:ACTED_IN {roles:['Dustin "Dusty" Davis']}]->(Twister), (JanB)-[:DIRECTED]->(Twister) CREATE (CastAway:Movie {title:'Cast Away', released:2000, tagline:'At the edge of the world, his journey begins.'}) CREATE (RobertZ:Person {name:'Robert Zemeckis', born:1951}) CREATE (TomH)-[:ACTED_IN {roles:['Chuck Noland']}]->(CastAway), (HelenH)-[:ACTED_IN {roles:['Kelly Frears']}]->(CastAway), (RobertZ)-[:DIRECTED]->(CastAway) CREATE (OneFlewOvertheCuckoosNest:Movie {title:"One Flew Over the Cuckoo's Nest", released:1975, tagline:"If he's crazy, what does that make you?"}) CREATE (MilosF:Person {name:'Milos Forman', born:1932}) CREATE (JackN)-[:ACTED_IN {roles:['Randle McMurphy']}]->(OneFlewOvertheCuckoosNest), (DannyD)-[:ACTED_IN {roles:['Martini']}]->(OneFlewOvertheCuckoosNest), (MilosF)-[:DIRECTED]->(OneFlewOvertheCuckoosNest) CREATE (SomethingsGottaGive:Movie {title:"Something's Gotta Give", released:2003}) CREATE (DianeK:Person {name:'Diane Keaton', born:1946}) CREATE (NancyM:Person {name:'Nancy Meyers', born:1949}) CREATE (JackN)-[:ACTED_IN {roles:['Harry Sanborn']}]->(SomethingsGottaGive), (DianeK)-[:ACTED_IN {roles:['Erica Barry']}]->(SomethingsGottaGive), (Keanu)-[:ACTED_IN {roles:['Julian Mercer']}]->(SomethingsGottaGive), (NancyM)-[:DIRECTED]->(SomethingsGottaGive), (NancyM)-[:PRODUCED]->(SomethingsGottaGive), (NancyM)-[:WROTE]->(SomethingsGottaGive) CREATE (BicentennialMan:Movie {title:'Bicentennial Man', released:1999, tagline:"One robot's 200 year journey to become an ordinary man."}) CREATE (ChrisC:Person {name:'Chris Columbus', born:1958}) CREATE (Robin)-[:ACTED_IN {roles:['Andrew Marin']}]->(BicentennialMan), (OliverP)-[:ACTED_IN {roles:['Rupert Burns']}]->(BicentennialMan), (ChrisC)-[:DIRECTED]->(BicentennialMan) CREATE (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."}) CREATE (JuliaR:Person {name:'Julia Roberts', born:1967}) CREATE (TomH)-[:ACTED_IN {roles:['Rep. Charlie Wilson']}]->(CharlieWilsonsWar), (JuliaR)-[:ACTED_IN {roles:['Joanne Herring']}]->(CharlieWilsonsWar), (PhilipH)-[:ACTED_IN {roles:['Gust Avrakotos']}]->(CharlieWilsonsWar), (MikeN)-[:DIRECTED]->(CharlieWilsonsWar) CREATE (ThePolarExpress:Movie {title:'The Polar Express', released:2004, tagline:'This Holiday Season… Believe'}) CREATE (TomH)-[:ACTED_IN {roles:['Hero Boy', 'Father', 'Conductor', 'Hobo', 'Scrooge', 'Santa Claus']}]->(ThePolarExpress), (RobertZ)-[:DIRECTED]->(ThePolarExpress) CREATE (ALeagueofTheirOwn:Movie {title:'A League of Their Own', released:1992, tagline:'Once in a lifetime you get a chance to do something different.'}) CREATE (Madonna:Person {name:'Madonna', born:1954}) CREATE (GeenaD:Person {name:'Geena Davis', born:1956}) CREATE (LoriP:Person {name:'Lori Petty', born:1963}) CREATE (PennyM:Person {name:'Penny Marshall', born:1943}) CREATE (TomH)-[:ACTED_IN {roles:['Jimmy Dugan']}]->(ALeagueofTheirOwn), (GeenaD)-[:ACTED_IN {roles:['Dottie Hinson']}]->(ALeagueofTheirOwn), (LoriP)-[:ACTED_IN {roles:['Kit Keller']}]->(ALeagueofTheirOwn), (RosieO)-[:ACTED_IN {roles:['Doris Murphy']}]->(ALeagueofTheirOwn), (Madonna)-[:ACTED_IN {roles:['"All the Way" Mae Mordabito']}]->(ALeagueofTheirOwn), (BillPax)-[:ACTED_IN {roles:['Bob Hinson']}]->(ALeagueofTheirOwn), (PennyM)-[:DIRECTED]->(ALeagueofTheirOwn) CREATE (PaulBlythe:Person {name:'Paul Blythe'}) CREATE (AngelaScope:Person {name:'Angela Scope'}) CREATE (JessicaThompson:Person {name:'Jessica Thompson'}) CREATE (JamesThompson:Person {name:'James Thompson'}) CREATE (JamesThompson)-[:FOLLOWS]->(JessicaThompson), (AngelaScope)-[:FOLLOWS]->(JessicaThompson), (PaulBlythe)-[:FOLLOWS]->(AngelaScope) CREATE (JessicaThompson)-[:REVIEWED {summary:'An amazing journey', rating:95}]->(CloudAtlas), (JessicaThompson)-[:REVIEWED {summary:'Silly, but fun', rating:65}]->(TheReplacements), (JamesThompson)-[:REVIEWED {summary:'The coolest football movie ever', rating:100}]->(TheReplacements), (AngelaScope)-[:REVIEWED {summary:'Pretty funny at times', rating:62}]->(TheReplacements), (JessicaThompson)-[:REVIEWED {summary:'Dark, but compelling', rating:85}]->(Unforgiven), (JessicaThompson)-[:REVIEWED {summary:"Slapstick redeemed only by the Robin Williams and Gene Hackman's stellar performances", rating:45}]->(TheBirdcage), (JessicaThompson)-[:REVIEWED {summary:'A solid romp', rating:68}]->(TheDaVinciCode), (JamesThompson)-[:REVIEWED {summary:'Fun, but a little far fetched', rating:65}]->(TheDaVinciCode), (JessicaThompson)-[:REVIEWED {summary:'You had me at Jerry', rating:92}]->(JerryMaguire); ================================================ FILE: scripts/docker-neo4j-initializer/start-movies-db.sh ================================================ echo "Loading Dataset" cat ./scripts/docker-neo4j-initializer/movies.cypher | docker exec --interactive neo4j bin/cypher-shell -u neo4j -p test1234 echo "MATCH () RETURN count(*)" | docker exec --interactive neo4j bin/cypher-shell -u neo4j -p test1234 ================================================ FILE: scripts/message-entrypoint.sh ================================================ #!/bin/sh ########### echo "-----------------------------------------------------------------------------------------------------------" echo "| WARNING: You are using an unmaintained version of NeoDash. Use at your own risk! |" echo "| NeoDash is available on http://localhost:$NGINX_PORT by default. |" echo "| Make sure your ports are mapped correctly (-p ${NGINX_PORT}:${NGINX_PORT}) when starting the container. |" echo "-----------------------------------------------------------------------------------------------------------" ================================================ FILE: src/application/Application.tsx ================================================ import React, { Suspense, useEffect } from 'react'; import NeoWelcomeScreenModal from '../modal/WelcomeScreenModal'; import { connect } from 'react-redux'; import { applicationGetConnection, applicationGetShareDetails, applicationGetOldDashboard, applicationHasNeo4jDesktopConnection, applicationHasAboutModalOpen, applicationHasCachedDashboard, applicationHasConnectionModalOpen, applicationIsConnected, applicationHasWelcomeScreenOpen, applicationGetDebugState, applicationGetStandaloneSettings, applicationGetSsoSettings, applicationHasReportHelpModalOpen, applicationIsStandalone, applicationIsDeprecated, } from '../application/ApplicationSelectors'; import { createConnectionThunk, createConnectionFromDesktopIntegrationThunk, onConfirmLoadSharedDashboardThunk, loadApplicationConfigThunk, } from '../application/ApplicationThunks'; import { clearNotification, resetShareDetails, setAboutModalOpen, setCachedSSODiscoveryUrl, setConnected, setConnectionModalOpen, setConnectionProperties, setOldDashboard, setReportHelpModalOpen, setWaitForSSO, setWelcomeScreenOpen, } from '../application/ApplicationActions'; import { resetDashboardState } from '../dashboard/DashboardActions'; import { NeoDashboardPlaceholder } from '../dashboard/placeholder/DashboardPlaceholder'; import NeoConnectionModal from '../modal/ConnectionModal'; import { loadDashboardThunk } from '../dashboard/DashboardThunks'; import { downloadComponentAsImage } from '../chart/ChartUtils'; import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { resetSessionStorage } from '../sessionStorage/SessionStorageActions'; import { getDashboardTheme } from '../dashboard/DashboardSelectors'; import { Banner } from '@neo4j-ndl/react'; const NeoUpgradeOldDashboardModal = React.lazy(() => import('../modal/UpgradeOldDashboardModal')); const NeoLoadSharedDashboardModal = React.lazy(() => import('../modal/LoadSharedDashboardModal')); const NeoReportHelpModal = React.lazy(() => import('../modal/ReportHelpModal')); const NeoNotificationModal = React.lazy(() => import('../modal/NotificationModal')); const NeoAboutModal = React.lazy(() => import('../modal/AboutModal')); const Dashboard = React.lazy(() => import('../dashboard/Dashboard')); /** * This is the main application component for NeoDash. * It contains: * - The Dashboard component * - A number of modals (pop-up windows) that handle connections, loading/saving dashboards, etc. * * Parts of the application state are retrieved here and passed to the relevant compoenents. * State-changing actions are also dispatched from here. See `ApplicationThunks.tsx`, `ApplicationActions.tsx` and `ApplicationSelectors.tsx` for more info. */ const Application = ({ connection, connected, hasCachedDashboard, oldDashboard, clearOldDashboard, connectionModalOpen, reportHelpModalOpen, ssoSettings, standalone, standaloneSettings, aboutModalOpen, loadDashboard, hasNeo4jDesktopConnection, deprecated, shareDetails, createConnection, createConnectionFromDesktopIntegration, setConnectionDetails, onResetShareDetails, onConfirmLoadSharedDashboard, initializeApplication, resetDashboard, onAboutModalOpen, onAboutModalClose, resetApplication, getDebugState, onReportHelpModalClose, welcomeScreenOpen, setWelcomeScreenOpen, onConnectionModalOpen, onConnectionModalClose, onSSOAttempt, themeMode, }) => { const [initialized, setInitialized] = React.useState(false); useEffect(() => { if (!initialized) { // Tell Neo4j Desktop to disable capturing right clicking window.neo4jDesktopApi && window.neo4jDesktopApi.showMenuOnRightClick && window.neo4jDesktopApi.showMenuOnRightClick(false); setInitialized(true); initializeApplication(initialized); } }, []); const ref = React.useRef(); const [bannerOpen, setBannerOpen] = React.useState(true); useEffect(() => { if (themeMode === 'dark') { document.body.classList.add('ndl-theme-dark'); } else { document.body.classList.remove('ndl-theme-dark'); } }, [themeMode]); // Only render the dashboard component if we have an active Neo4j connection. return (
{deprecated && bannerOpen && connected ? ( setBannerOpen(false)} > This app will no longer be available in the near future.   Migrate  your dashboards to the Neo4j Console, or{' '} visit {' '} the NeoDash repository to run NeoDash yourself. ) : ( <> )} {connected ? ( downloadComponentAsImage(ref)} onAboutModalOpen={onAboutModalOpen} resetApplication={resetApplication} > ) : ( )} {/* TODO - move all models into a pop-ups (or modals) component. */}
); }; const mapStateToProps = (state) => ({ connected: applicationIsConnected(state), connection: applicationGetConnection(state), shareDetails: applicationGetShareDetails(state), oldDashboard: applicationGetOldDashboard(state), ssoSettings: applicationGetSsoSettings(state), standalone: applicationIsStandalone(state), standaloneSettings: applicationGetStandaloneSettings(state), connectionModalOpen: applicationHasConnectionModalOpen(state), aboutModalOpen: applicationHasAboutModalOpen(state), reportHelpModalOpen: applicationHasReportHelpModalOpen(state), welcomeScreenOpen: applicationHasWelcomeScreenOpen(state), hasCachedDashboard: applicationHasCachedDashboard(state), deprecated: applicationIsDeprecated(state), getDebugState: () => { return applicationGetDebugState(state); }, // TODO - change this to be variable instead of a function? hasNeo4jDesktopConnection: applicationHasNeo4jDesktopConnection(state), themeMode: getDashboardTheme(state), }); const mapDispatchToProps = (dispatch) => ({ createConnection: (protocol, url, port, database, username, password) => { dispatch(setConnected(false)); dispatch(resetSessionStorage()); dispatch(createConnectionThunk(protocol, url, port, database, username, password)); }, createConnectionFromDesktopIntegration: () => { dispatch(setConnected(false)); dispatch(createConnectionFromDesktopIntegrationThunk()); }, loadDashboard: (uuid, text) => { dispatch(clearNotification()); dispatch(loadDashboardThunk(uuid, text)); }, resetDashboard: () => dispatch(resetDashboardState()), clearOldDashboard: () => dispatch(setOldDashboard(null)), initializeApplication: (initialized) => { if (!initialized) { dispatch(loadApplicationConfigThunk()); } }, onResetShareDetails: (_) => { dispatch(setWelcomeScreenOpen(true)); dispatch(resetShareDetails()); }, onSSOAttempt: (discoveryUrlValidated) => { dispatch(setWaitForSSO(true)); dispatch(setCachedSSODiscoveryUrl(discoveryUrlValidated)); }, setConnectionDetails: (protocol, url, port, database, username, password) => { dispatch(setConnectionProperties(protocol, url, port, database, username, password)); }, onConfirmLoadSharedDashboard: (_) => dispatch(onConfirmLoadSharedDashboardThunk()), onConnectionModalOpen: (_) => dispatch(setConnectionModalOpen(true)), onConnectionModalClose: (_) => dispatch(setConnectionModalOpen(false)), onReportHelpModalClose: (_) => dispatch(setReportHelpModalOpen(false)), onAboutModalOpen: (_) => dispatch(setAboutModalOpen(true)), setWelcomeScreenOpen: (open) => dispatch(setWelcomeScreenOpen(open)), onAboutModalClose: (_) => dispatch(setAboutModalOpen(false)), resetApplication: () => { dispatch(setWelcomeScreenOpen(true)); dispatch(setConnected(false)); }, }); Application.displayName = 'Application'; export default connect(mapStateToProps, mapDispatchToProps)(Application); ================================================ FILE: src/application/ApplicationActions.ts ================================================ /** * This file contains all state-changing actions relevant for the main application. */ export const CLEAR_NOTIFICATION = 'APPLICATION/CLEAR_NOTIFICATION'; export const clearNotification = () => ({ type: CLEAR_NOTIFICATION, payload: {}, }); export const CREATE_NOTIFICATION = 'APPLICATION/CREATE_NOTIFICATION'; export const createNotification = (title: any, message: any) => ({ type: CREATE_NOTIFICATION, payload: { title, message }, }); export const SET_CONNECTED = 'APPLICATION/SET_CONNECTED'; export const setConnected = (connected: boolean) => ({ type: SET_CONNECTED, payload: { connected }, }); export const SET_DRAFT = 'APPLICATION/SET_DRAFT'; export const setDraft = (draft: boolean) => ({ type: SET_DRAFT, payload: { draft }, }); export const SET_CONNECTION_MODAL_OPEN = 'APPLICATION/SET_CONNECTION_MODAL_OPEN'; export const setConnectionModalOpen = (open: boolean) => ({ type: SET_CONNECTION_MODAL_OPEN, payload: { open }, }); export const SET_ABOUT_MODAL_OPEN = 'APPLICATION/SET_ABOUT_MODAL_OPEN'; export const setAboutModalOpen = (open: boolean) => ({ type: SET_ABOUT_MODAL_OPEN, payload: { open }, }); export const SET_REPORT_HELP_MODAL_OPEN = 'APPLICATION/SET_REPORT_HELP_MODAL_OPEN'; export const setReportHelpModalOpen = (open: boolean) => ({ type: SET_REPORT_HELP_MODAL_OPEN, payload: { open }, }); export const SET_WELCOME_SCREEN_OPEN = 'APPLICATION/SET_WELCOME_SCREEN_OPEN'; export const setWelcomeScreenOpen = (open: boolean) => ({ type: SET_WELCOME_SCREEN_OPEN, payload: { open }, }); export const SET_CONNECTION_PROPERTIES = 'APPLICATION/SET_CONNECTION_PROPERTIES'; export const setConnectionProperties = ( protocol: string, url: string, port: string, database: string, username: string, password: string ) => ({ type: SET_CONNECTION_PROPERTIES, payload: { protocol, url, port, database, username, password }, }); export const SET_BASIC_CONNECTION_PROPERTIES = 'APPLICATION/SET_BASIC_CONNECTION_PROPERTIES'; export const setBasicConnectionProperties = ( protocol: string, url: string, port: string, database: string, username: string, password: string ) => ({ type: SET_CONNECTION_PROPERTIES, payload: { protocol, url, port, database, username, password }, }); export const SET_DESKTOP_CONNECTION_PROPERTIES = 'APPLICATION/SET_DESKTOP_CONNECTION_PROPERTIES'; export const setDesktopConnectionProperties = ( protocol: string, url: string, port: string, database: string, username: string, password: string ) => ({ type: SET_DESKTOP_CONNECTION_PROPERTIES, payload: { protocol, url, port, database, username, password }, }); export const CLEAR_DESKTOP_CONNECTION_PROPERTIES = 'APPLICATION/CLEAR_DESKTOP_CONNECTION_PROPERTIES'; export const clearDesktopConnectionProperties = () => ({ type: CLEAR_DESKTOP_CONNECTION_PROPERTIES, payload: {}, }); // Legacy pre1-v2 dashboard that can be optionally upgraded. export const SET_OLD_DASHBOARD = 'APPLICATION/SET_OLD_DASHBOARD'; export const setOldDashboard = (text: string) => ({ type: SET_OLD_DASHBOARD, payload: { text }, }); // Legacy pre1-v2 dashboard that can be optionally upgraded. export const RESET_SHARE_DETAILS = 'APPLICATION/RESET_SHARE_DETAILS'; export const resetShareDetails = () => ({ type: RESET_SHARE_DETAILS, payload: {}, }); export const SET_SHARE_DETAILS_FROM_URL = 'APPLICATION/SET_SHARE_DETAILS_FROM_URL'; export const setShareDetailsFromUrl = ( type: string, id: string, standalone: boolean, protocol: string, url: string, port: string, database: string, username: string, password: string, dashboardDatabase: string, skipConfirmation: boolean ) => ({ type: SET_SHARE_DETAILS_FROM_URL, payload: { type, id, standalone, protocol, url, port, database, username, password, dashboardDatabase, skipConfirmation, }, }); export const SET_STANDALONE_ENABLED = 'APPLICATION/SET_STANDALONE_ENABLED'; export const setStandaloneEnabled = ( standalone: boolean, standaloneProtocol: string, standaloneHost: string, standalonePort: string, standaloneDatabase: string, standaloneDashboardName: string, standaloneDashboardDatabase: string, standaloneDashboardURL: string, standaloneUsername: string, standalonePassword: string, standalonePasswordWarningHidden: boolean, standaloneAllowLoad: boolean, standaloneLoadFromOtherDatabases: boolean, standaloneMultiDatabase: boolean, standaloneDatabaseList: string ) => ({ type: SET_STANDALONE_ENABLED, payload: { standalone, standaloneProtocol, standaloneHost, standalonePort, standaloneDatabase, standaloneDashboardName, standaloneDashboardDatabase, standaloneDashboardURL, standaloneUsername, standalonePassword, standalonePasswordWarningHidden, standaloneAllowLoad, standaloneLoadFromOtherDatabases, standaloneMultiDatabase, standaloneDatabaseList, }, }); export const SET_STANDALONE_MODE = 'APPLICATION/SET_STANDALONE_MODE'; export const setStandaloneMode = (standalone: boolean) => ({ type: SET_STANDALONE_MODE, payload: { standalone }, }); export const SET_STANDALONE_DASHBOARD_DATEBASE = 'APPLICATION/SET_STANDALONE_DASHBOARD_DATEBASE'; export const setStandaloneDashboardDatabase = (dashboardDatabase: string) => ({ type: SET_STANDALONE_DASHBOARD_DATEBASE, payload: { dashboardDatabase }, }); export const SET_SSO_ENABLED = 'APPLICATION/SET_SSO_ENABLED'; export const setSSOEnabled = (enabled: boolean, discoveryUrl: string) => ({ type: SET_SSO_ENABLED, payload: { enabled, discoveryUrl }, }); export const SET_SSO_PROVIDERS = 'APPLICATION/SET_SSO_PROVIDERS'; export const setSSOProviders = (providers: []) => ({ type: SET_SSO_PROVIDERS, payload: { providers }, }); export const SET_WAIT_FOR_SSO = 'APPLICATION/SET_WAIT_FOR_SSO'; export const setWaitForSSO = (wait: boolean) => ({ type: SET_WAIT_FOR_SSO, payload: { wait }, }); export const SET_CACHED_SSO_DISCOVERY_URL = 'APPLICATION/SET_CACHED_SSO_DISCOVERY_URL'; export const setCachedSSODiscoveryUrl = (url: string) => ({ type: SET_CACHED_SSO_DISCOVERY_URL, payload: { url }, }); export const SET_SESSION_PARAMETERS = 'APPLICATION/SET_SESSION_PARAMETERS'; export const setSessionParameters = (parameters: any) => ({ type: SET_SESSION_PARAMETERS, payload: { parameters }, }); export const SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING = 'APPLICATION/SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING'; export const setDashboardToLoadAfterConnecting = (id: any) => ({ type: SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING, payload: { id }, }); export const SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING = 'APPLICATION/SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING'; export const setParametersToLoadAfterConnecting = (parameters: any) => ({ type: SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING, payload: { parameters }, }); export const SET_CUSTOM_HEADER = 'APPLICATION/SET_CUSTOM_HEADER'; export const setCustomHeader = (customHeader: any) => ({ type: SET_CUSTOM_HEADER, payload: { customHeader }, }); export const SET_DEPRECATION_NOTICE = 'APPLICATION/SET_DEPRECATION_NOTICE'; export const setDeprecationNotice = (deprecated: boolean) => ({ type: SET_DEPRECATION_NOTICE, payload: { deprecated }, }); ================================================ FILE: src/application/ApplicationReducer.ts ================================================ /** * Reducers define changes to the application state when a given action is taken. */ import { HARD_RESET_CARD_SETTINGS, TOGGLE_CARD_SETTINGS, UPDATE_ALL_SELECTIONS, UPDATE_FIELDS, UPDATE_SCHEMA, UPDATE_SELECTION, } from '../card/CardActions'; import { DEFAULT_NEO4J_URL } from '../config/ApplicationConfig'; import { SET_DASHBOARD, SET_DASHBOARD_UUID } from '../dashboard/DashboardActions'; import { UPDATE_DASHBOARD_SETTING } from '../settings/SettingsActions'; import { CLEAR_DESKTOP_CONNECTION_PROPERTIES, CLEAR_NOTIFICATION, CREATE_NOTIFICATION, RESET_SHARE_DETAILS, SET_ABOUT_MODAL_OPEN, SET_CACHED_SSO_DISCOVERY_URL, SET_CONNECTED, SET_CONNECTION_MODAL_OPEN, SET_CONNECTION_PROPERTIES, SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING, SET_DESKTOP_CONNECTION_PROPERTIES, SET_DRAFT, SET_OLD_DASHBOARD, SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING, SET_REPORT_HELP_MODAL_OPEN, SET_SESSION_PARAMETERS, SET_SHARE_DETAILS_FROM_URL, SET_SSO_ENABLED, SET_SSO_PROVIDERS, SET_STANDALONE_DASHBOARD_DATEBASE, SET_STANDALONE_ENABLED, SET_STANDALONE_MODE, SET_WAIT_FOR_SSO, SET_WELCOME_SCREEN_OPEN, SET_CUSTOM_HEADER, SET_DEPRECATION_NOTICE, } from './ApplicationActions'; import { SET_LOGGING_MODE, SET_LOGGING_DATABASE, SET_LOG_ERROR_NOTIFICATION, LOGGING_PREFIX, } from './logging/LoggingActions'; import { loggingReducer, LOGGING_INITIAL_STATE } from './logging/LoggingReducer'; const update = (state, mutations) => Object.assign({}, state, mutations); const initialState = { notificationTitle: null, notificationMessage: null, connectionModalOpen: false, welcomeScreenOpen: true, draft: false, aboutModalOpen: false, connection: { protocol: 'neo4j+s', url: DEFAULT_NEO4J_URL, port: '7687', database: '', username: 'neo4j', password: '', }, shareDetails: undefined, desktopConnection: null, connected: false, dashboardToLoadAfterConnecting: null, waitForSSO: false, standalone: false, logging: LOGGING_INITIAL_STATE, }; export const applicationReducer = (state = initialState, action: { type: any; payload: any }) => { const { type, payload } = action; // This is a special application-level flag used to determine whether the dashboard needs to be saved to the database. if (action.type.startsWith('DASHBOARD/') || action.type.startsWith('PAGE/') || action.type.startsWith('CARD/')) { // if anything changes EXCEPT for the selected page, we flag that we are drafting a dashboard. const NON_TRANSFORMATIVE_ACTIONS = [ UPDATE_DASHBOARD_SETTING, UPDATE_SCHEMA, HARD_RESET_CARD_SETTINGS, SET_DASHBOARD, UPDATE_ALL_SELECTIONS, UPDATE_FIELDS, SET_DASHBOARD_UUID, TOGGLE_CARD_SETTINGS, UPDATE_SELECTION, ]; if (!state.draft && !NON_TRANSFORMATIVE_ACTIONS.includes(type)) { state = update(state, { draft: true }); return state; } } // Ignore any non-application actions. if (!action.type.startsWith('APPLICATION/')) { return state; } if (action.type.startsWith(LOGGING_PREFIX)) { const enrichedPayload = update(payload, { logging: state.logging }); const enrichedAction = { type, payload: enrichedPayload }; return { ...state, logging: loggingReducer(state.logging, enrichedAction) }; } // Application state updates are handled here. switch (type) { case CREATE_NOTIFICATION: { const { title, message } = payload; state = update(state, { notificationTitle: title, notificationMessage: message }); return state; } case CLEAR_NOTIFICATION: { state = update(state, { notificationTitle: null, notificationMessage: null, notificationIsDismissable: null }); return state; } case SET_CONNECTED: { const { connected } = payload; state = update(state, { connected: connected }); return state; } case SET_DRAFT: { const { draft } = payload; state = update(state, { draft: draft }); return state; } case SET_CONNECTION_MODAL_OPEN: { const { open } = payload; state = update(state, { connectionModalOpen: open }); return state; } case SET_ABOUT_MODAL_OPEN: { const { open } = payload; state = update(state, { aboutModalOpen: open }); return state; } case SET_REPORT_HELP_MODAL_OPEN: { const { open } = payload; state = update(state, { reportHelpModalOpen: open }); return state; } case SET_WELCOME_SCREEN_OPEN: { const { open } = payload; state = update(state, { welcomeScreenOpen: open }); return state; } case SET_STANDALONE_DASHBOARD_DATEBASE: { const { dashboardDatabase } = payload; state = update(state, { standaloneDashboardDatabase: dashboardDatabase }); return state; } case SET_STANDALONE_MODE: { const { standalone } = payload; state = update(state, { standalone: standalone }); return state; } case SET_LOGGING_MODE: { const { loggingMode } = payload; state = update(state, { loggingMode: loggingMode }); return state; } case SET_LOGGING_DATABASE: { const { loggingDatabase } = payload; state = update(state, { loggingDatabase: loggingDatabase }); return state; } case SET_LOG_ERROR_NOTIFICATION: { const { logErrorNotification } = payload; state = update(state, { logErrorNotification: logErrorNotification }); return state; } case SET_SSO_ENABLED: { const { enabled, discoveryUrl } = payload; state = update(state, { ssoEnabled: enabled, ssoDiscoveryUrl: discoveryUrl }); return state; } case SET_SSO_PROVIDERS: { const { providers } = payload; state = update(state, { ssoProviders: providers }); return state; } case SET_WAIT_FOR_SSO: { const { wait } = payload; state = update(state, { waitForSSO: wait }); return state; } case SET_SESSION_PARAMETERS: { const { parameters } = payload; state = update(state, { sessionParameters: parameters }); return state; } case SET_STANDALONE_ENABLED: { const { standalone, standaloneProtocol, standaloneHost, standalonePort, standaloneDatabase, standaloneDashboardName, standaloneDashboardDatabase, standaloneDashboardURL, standaloneUsername, standalonePassword, standalonePasswordWarningHidden, standaloneAllowLoad, standaloneLoadFromOtherDatabases, standaloneMultiDatabase, standaloneDatabaseList, } = payload; state = update(state, { standalone: standalone, standaloneProtocol: standaloneProtocol, standaloneHost: standaloneHost, standalonePort: standalonePort, standaloneDatabase: standaloneDatabase, standaloneDashboardName: standaloneDashboardName, standaloneDashboardDatabase: standaloneDashboardDatabase, standaloneDashboardURL: standaloneDashboardURL, standaloneUsername: standaloneUsername, standalonePassword: standalonePassword, standalonePasswordWarningHidden: standalonePasswordWarningHidden, standaloneAllowLoad: standaloneAllowLoad, standaloneLoadFromOtherDatabases: standaloneLoadFromOtherDatabases, standaloneMultiDatabase: standaloneMultiDatabase, standaloneDatabaseList: standaloneDatabaseList, }); return state; } case SET_OLD_DASHBOARD: { const { text } = payload; state = update(state, { oldDashboard: text }); return state; } case SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING: { const { id } = payload; state = update(state, { dashboardToLoadAfterConnecting: id }); return state; } case SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING: { const { parameters } = payload; state = update(state, { parametersToLoadAfterConnecting: parameters }); return state; } case SET_CONNECTION_PROPERTIES: { const { protocol, url, port, database, username, password } = payload; state = update(state, { connection: { protocol: protocol, url: url, port: port, database: database, username: username, password: password, }, }); return state; } case CLEAR_DESKTOP_CONNECTION_PROPERTIES: { state = update(state, { desktopConnection: null }); return state; } case SET_DESKTOP_CONNECTION_PROPERTIES: { const { protocol, url, port, database, username, password } = payload; state = update(state, { desktopConnection: { protocol: protocol, url: url, port: port, database: database, username: username, password: password, }, }); return state; } case RESET_SHARE_DETAILS: { state = update(state, { shareDetails: undefined }); return state; } case SET_CACHED_SSO_DISCOVERY_URL: { const { url } = payload; state = update(state, { cachedSSODiscoveryUrl: url }); return state; } case SET_SHARE_DETAILS_FROM_URL: { const { type, id, standalone, protocol, url, port, database, username, password, dashboardDatabase, skipConfirmation, } = payload; state = update(state, { shareDetails: { type: type, id: id, standalone: standalone, protocol: protocol, url: url, port: port, database: database, username: username, password: password, dashboardDatabase: dashboardDatabase, skipConfirmation: skipConfirmation, }, }); return state; } case SET_CUSTOM_HEADER: { const { customHeader } = payload; state = update(state, { customHeader: customHeader }); return state; } case SET_DEPRECATION_NOTICE: { const { deprecated } = payload; state = update(state, { deprecated: deprecated }); return state; } default: { return state; } } }; ================================================ FILE: src/application/ApplicationSelectors.ts ================================================ import { initialState } from '../dashboard/DashboardReducer'; import isEqual from 'lodash.isequal'; /** * Selectors define a way to retrieve parts of the global application state for a sub-component. */ export const applicationHasNotification = (state: any) => { return state.application.notificationMessage != null; }; export const getNotification = (state: any) => { return state.application.notificationMessage; }; export const getNotificationIsDismissable = (state: any) => { return state.application.notificationTitle !== 'Unable to load application configuration'; }; export const getNotificationTitle = (state: any) => { return state.application.notificationTitle; }; export const dashboardIsDraft = (state: any) => { return state.application.draft; }; export const applicationIsConnected = (state: any) => { return state.application.connected; }; export const applicationGetConnection = (state: any) => { return state.application.connection; }; export const applicationGetConnectionDatabase = (state: any) => { return state.application.connection.database; }; export const applicationGetConnectionUser = (state: any) => { return state.application.connection.username; }; export const applicationGetShareDetails = (state: any) => { return state.application.shareDetails; }; export const applicationIsStandalone = (state: any) => { return state.application.standalone; }; export const applicationGetLoggingMode = (state: any) => { return state.application.loggingMode; }; export const applicationHasNeo4jDesktopConnection = (state: any) => { return state.application.desktopConnection != null; }; export const applicationHasConnectionModalOpen = (state: any) => { return state.application.connectionModalOpen; }; export const applicationGetOldDashboard = (state: any) => { return state.application.oldDashboard; }; export const applicationHasAboutModalOpen = (state: any) => { return state.application.aboutModalOpen; }; export const applicationHasReportHelpModalOpen = (state: any) => { return state.application.reportHelpModalOpen; }; export const applicationGetSsoSettings = (state: any) => { return { ssoEnabled: state.application.ssoEnabled, ssoProviders: state.application.ssoProviders, ssoDiscoveryUrl: state.application.ssoDiscoveryUrl, cachedSSODiscoveryUrl: state.application.cachedSSODiscoveryUrl, }; }; export const applicationGetStandaloneSettings = (state: any) => { return { standalone: state.application.standalone, standaloneProtocol: state.application.standaloneProtocol, standaloneHost: state.application.standaloneHost, standalonePort: state.application.standalonePort, standaloneDatabase: state.application.standaloneDatabase, standaloneDashboardName: state.application.standaloneDashboardName, standaloneDashboardDatabase: state.application.standaloneDashboardDatabase, standaloneDashboardURL: state.application.standaloneDashboardURL, standaloneUsername: state.application.standaloneUsername, standalonePassword: state.application.standalonePassword, standalonePasswordWarningHidden: state.application.standalonePasswordWarningHidden, standaloneAllowLoad: state.application.standaloneAllowLoad, standaloneLoadFromOtherDatabases: state.application.standaloneLoadFromOtherDatabases, standaloneMultiDatabase: state.application.standaloneMultiDatabase, standaloneDatabaseList: state.application.standaloneDatabaseList, }; }; export const applicationHasWelcomeScreenOpen = (state: any) => { return state.application.welcomeScreenOpen; }; export const applicationIsDeprecated = (state: any) => { return state.application.deprecated; }; export const applicationHasCachedDashboard = (state: any) => { // Avoid this expensive check when the application is connected, as it's only for the welcome screen. if (state.application.connected) { return false; } return !isEqual(state.dashboard, initialState); }; /** * Deep-copy the current state, and remove the password. */ export const applicationGetDebugState = (state: any) => { const copy = JSON.parse(JSON.stringify(state)); copy.application.connection.password = '************'; if (copy.application.desktopConnection) { copy.application.desktopConnection.password = '************'; } return copy; }; export const applicationGetCustomHeader = (state: any) => { return state.application.customHeader; }; ================================================ FILE: src/application/ApplicationThunks.ts ================================================ import { createDriver } from 'use-neo4j'; import { initializeSSO } from '../component/sso/SSOUtils'; import { DEFAULT_SCREEN, Screens } from '../config/ApplicationConfig'; import { setDashboard } from '../dashboard/DashboardActions'; import { NEODASH_VERSION, VERSION_TO_MIGRATE } from '../dashboard/DashboardReducer'; import { assignDashboardUuidIfNotPresentThunk, loadDashboardFromNeo4jByNameThunk, loadDashboardFromNeo4jThunk, loadDashboardThunk, upgradeDashboardVersion, } from '../dashboard/DashboardThunks'; import { createNotificationThunk } from '../page/PageThunks'; import { runCypherQuery } from '../report/ReportQueryRunner'; import { setPageNumberThunk, updateGlobalParametersThunk, updateSessionParameterThunk, } from '../settings/SettingsThunks'; import { setConnected, setConnectionModalOpen, setConnectionProperties, setDesktopConnectionProperties, resetShareDetails, setShareDetailsFromUrl, setWelcomeScreenOpen, setDashboardToLoadAfterConnecting, setOldDashboard, clearDesktopConnectionProperties, clearNotification, setSSOEnabled, setSSOProviders, setStandaloneEnabled, setAboutModalOpen, setStandaloneMode, setStandaloneDashboardDatabase, setWaitForSSO, setParametersToLoadAfterConnecting, setReportHelpModalOpen, setDraft, setCustomHeader, setDeprecationNotice, } from './ApplicationActions'; import { setLoggingMode, setLoggingDatabase, setLogErrorNotification } from './logging/LoggingActions'; import { version } from '../modal/AboutModal'; import { applicationIsStandalone } from './ApplicationSelectors'; import { applicationGetLoggingSettings } from './logging/LoggingSelectors'; import { createLogThunk } from './logging/LoggingThunk'; import { createUUID } from '../utils/uuid'; /** * Application Thunks (https://redux.js.org/usage/writing-logic-thunks) handle complex state manipulations. * Several actions/other thunks may be dispatched from here. */ /** * Establish a connection to Neo4j with the specified credentials. Open/close the relevant windows when connection is made (un)successfully. * @param protocol - the neo4j protocol (e.g. bolt, bolt+s, neo4j+s, ...) * @param url - URL of the host. * @param port - port on which Neo4j is running. * @param database - the Neo4j database to connect to. * @param username - Neo4j username. * @param password - Neo4j password. */ export const createConnectionThunk = (protocol, url, port, database, username, password) => (dispatch: any, getState: any) => { const loggingState = getState(); const loggingSettings = applicationGetLoggingSettings(loggingState); const neodashMode = applicationIsStandalone(loggingState) ? 'Standalone' : 'Editor'; try { const driver = createDriver(protocol, url, port, username, password, { userAgent: `neodash/v${version}` }); // eslint-disable-next-line no-console console.log('Attempting to connect...'); const validateConnection = (records) => { // eslint-disable-next-line no-console console.log('Confirming connection was established...'); if (records && records[0] && records[0].error) { dispatch(createNotificationThunk('Unable to establish connection', records[0].error)); if (loggingSettings.loggingMode > '0') { dispatch( createLogThunk( driver, loggingSettings.loggingDatabase, neodashMode, username, 'ERR - connect to DB', database, '', `Error while trying to establish connection to Neo4j DB in ${neodashMode} mode at ${Date( Date.now() ).substring(0, 33)}` ) ); } } else if (records && records[0] && records[0].keys[0] == 'connected') { dispatch(setConnectionProperties(protocol, url, port, database, username, password)); dispatch(setConnectionModalOpen(false)); dispatch(setConnected(true)); // An old dashboard (pre-2.3.5) may not always have a UUID. We catch this case here. dispatch(assignDashboardUuidIfNotPresentThunk()); dispatch(updateSessionParameterThunk('session_uri', `${protocol}://${url}:${port}`)); dispatch(updateSessionParameterThunk('session_database', database)); dispatch(updateSessionParameterThunk('session_username', username)); if (loggingSettings.loggingMode > '0') { dispatch( createLogThunk( driver, loggingSettings.loggingDatabase, neodashMode, username, 'INF - connect to DB', database, '', `${username} established connection to Neo4j DB in ${neodashMode} mode at ${Date(Date.now()).substring( 0, 33 )}` ) ); } // If we have remembered to load a specific dashboard after connecting to the database, take care of it here. const { application } = getState(); if ( application.dashboardToLoadAfterConnecting && (application.dashboardToLoadAfterConnecting.startsWith('http') || application.dashboardToLoadAfterConnecting.startsWith('./') || application.dashboardToLoadAfterConnecting.startsWith('/')) ) { fetch(application.dashboardToLoadAfterConnecting) .then((response) => response.text()) .then((data) => dispatch(loadDashboardThunk(createUUID(), data))); dispatch(setDashboardToLoadAfterConnecting(null)); } else if (application.dashboardToLoadAfterConnecting) { const setDashboardAfterLoadingFromDatabase = (value) => { dispatch(loadDashboardThunk(createUUID(), value)); }; // If we specify a dashboard by name, load the latest version of it. // If we specify a dashboard by UUID, load it directly. if (application.dashboardToLoadAfterConnecting.startsWith('name:')) { dispatch( loadDashboardFromNeo4jByNameThunk( driver, application.standaloneDashboardDatabase, application.dashboardToLoadAfterConnecting.substring(5), setDashboardAfterLoadingFromDatabase ) ); } else { dispatch( loadDashboardFromNeo4jThunk( driver, application.standaloneDashboardDatabase, application.dashboardToLoadAfterConnecting, setDashboardAfterLoadingFromDatabase ) ); } dispatch(setDashboardToLoadAfterConnecting(null)); } } else { dispatch(createNotificationThunk('Unknown Connection Error', 'Check the browser console.')); } }; const query = 'RETURN true as connected'; const parameters = {}; runCypherQuery( driver, database, query, parameters, 1, () => {}, (records) => validateConnection(records) ); } catch (e) { dispatch(createNotificationThunk('Unable to establish connection', e)); } }; /** * Establish a connection directly from the Neo4j Desktop integration (if running inside Neo4j Desktop) */ export const createConnectionFromDesktopIntegrationThunk = () => (dispatch: any, getState: any) => { try { const desktopConnectionDetails = getState().application.desktopConnection; const { protocol, url, port, database, username, password } = desktopConnectionDetails; dispatch(createConnectionThunk(protocol, url, port, database, username, password)); } catch (e) { dispatch(createNotificationThunk('Unable to establish connection to Neo4j Desktop', e)); } }; /** * Find the active database from Neo4j Desktop. * Set global state values to remember the values retrieved from the integration so that we can connect later if possible. */ export const setDatabaseFromNeo4jDesktopIntegrationThunk = () => (dispatch: any) => { const getActiveDatabase = (context) => { for (let pi = 0; pi < context.projects.length; pi++) { let prj = context.projects[pi]; for (let gi = 0; gi < prj.graphs.length; gi++) { let grf = prj.graphs[gi]; if (grf.status == 'ACTIVE') { return grf; } } } // No active database found - ask for manual connection details. return null; }; let promise = window.neo4jDesktopApi && window.neo4jDesktopApi.getContext(); if (promise) { promise.then((context) => { let neo4j = getActiveDatabase(context); if (neo4j) { dispatch( setDesktopConnectionProperties( neo4j.connection.configuration.protocols.bolt.url.split('://')[0], neo4j.connection.configuration.protocols.bolt.url.split('://')[1].split(':')[0], neo4j.connection.configuration.protocols.bolt.port, undefined, neo4j.connection.configuration.protocols.bolt.username, neo4j.connection.configuration.protocols.bolt.password ) ); } }); } }; /** * On application startup, check the URL to see if we are loading a shared dashboard. * If yes, decode the URL parameters and set the application state accordingly, so that it can be loaded later. */ export const handleSharedDashboardsThunk = () => (dispatch: any) => { try { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); // Parse the URL parameters to see if there's any deep linking of parameters. const paramsToSetAfterConnecting = {}; Array.from(urlParams.entries()).forEach(([key, value]) => { if (key.startsWith('neodash_')) { paramsToSetAfterConnecting[key] = value; } }); if (Object.keys(paramsToSetAfterConnecting).length > 0) { dispatch(setParametersToLoadAfterConnecting(paramsToSetAfterConnecting)); } if (urlParams.get('share') !== null) { const id = decodeURIComponent(urlParams.get('id')); const type = urlParams.get('type'); const standalone = urlParams.get('standalone') == 'Yes'; const skipConfirmation = urlParams.get('skipConfirmation') == 'Yes'; const dashboardDatabase = urlParams.get('dashboardDatabase'); if (dashboardDatabase) { dispatch(setStandaloneDashboardDatabase(dashboardDatabase)); } if (urlParams.get('credentials')) { setWelcomeScreenOpen(false); const connection = decodeURIComponent(urlParams.get('credentials')); const protocol = connection.split('://')[0]; const username = connection.split('://')[1].split(':')[0]; const password = connection.split('://')[1].split(':')[1].split('@')[0]; const database = connection.split('@')[1].split(':')[0]; const url = connection.split('@')[1].split(':')[1]; const port = connection.split('@')[1].split(':')[2]; dispatch(setConnectionModalOpen(false)); dispatch( setShareDetailsFromUrl( type, id, standalone, protocol, url, port, database, username, password, dashboardDatabase, skipConfirmation ) ); if (skipConfirmation === true) { dispatch(onConfirmLoadSharedDashboardThunk()); } window.history.pushState({}, document.title, window.location.pathname); } else { dispatch(setConnectionModalOpen(false)); // dispatch(setWelcomeScreenOpen(false)); dispatch( setShareDetailsFromUrl( type, id, standalone, undefined, undefined, undefined, undefined, undefined, undefined, undefined, false ) ); window.history.pushState({}, document.title, window.location.pathname); } } else { // dispatch(resetShareDetails()); } } catch (e) { dispatch( createNotificationThunk( 'Unable to load shared dashboard', 'You have specified an invalid/incomplete share URL. Try regenerating the share URL from the sharing window.' ) ); } }; /** * Confirm that we load a shared dashboard. This requires that the state was previously set in `handleSharedDashboardsThunk()`. */ export const onConfirmLoadSharedDashboardThunk = () => (dispatch: any, getState: any) => { try { const state = getState(); const { shareDetails } = state.application; dispatch(setWelcomeScreenOpen(false)); dispatch(setDashboardToLoadAfterConnecting(shareDetails.id)); if (shareDetails.dashboardDatabase) { dispatch(setStandaloneDashboardDatabase(shareDetails.dashboardDatabase)); } else if (!state.application.standaloneDashboardDatabase) { // No standalone dashboard database configured, fall back to default dispatch(setStandaloneDashboardDatabase(shareDetails.database)); } if (shareDetails.url) { dispatch( createConnectionThunk( shareDetails.protocol, shareDetails.url, shareDetails.port, shareDetails.database, shareDetails.username, shareDetails.password ) ); } else { dispatch(setConnectionModalOpen(true)); } if (shareDetails.standalone == true) { dispatch(setStandaloneMode(true)); } dispatch(resetShareDetails()); } catch (e) { dispatch( createNotificationThunk( 'Unable to load shared dashboard', 'The provided connection or dashboard identifiers are invalid. Try regenerating the share URL from the sharing window.' ) ); } }; /** * Initializes the NeoDash application. * * This is a multi step process, starting with loading the runtime configuration. * This is present in the file located at /config.json on the URL where NeoDash is deployed. * Note: this does not work in Neo4j Desktop, so we revert to defaults. */ export const loadApplicationConfigThunk = () => async (dispatch: any, getState: any) => { let config = { ssoEnabled: false, ssoProviders: [], ssoDiscoveryUrl: 'http://example.com', standalone: false, standaloneProtocol: 'neo4j+s', standaloneHost: 'localhost', standalonePort: '7687', standaloneDatabase: 'neo4j', standaloneDashboardName: 'My Dashboard', standaloneDashboardDatabase: 'dashboards', standaloneDashboardURL: '', loggingMode: '0', loggingDatabase: 'logs', logErrorNotification: '3', standaloneAllowLoad: false, standaloneLoadFromOtherDatabases: false, standaloneMultiDatabase: false, standaloneDatabaseList: 'neo4j', customHeader: '', deprecationNotice: false, }; try { config = await (await fetch('config.json')).json(); } catch (e) { // Config may not be found, for example when we are in Neo4j Desktop. // eslint-disable-next-line no-console console.log('No config file detected. Setting to safe defaults.'); } try { // Parse the URL parameters to see if there's any deep linking of parameters. const state = getState(); const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); if (state.application.waitForSSO) { const paramsBeforeSSO = JSON.parse(sessionStorage.getItem('SSO_PARAMS_BEFORE_REDIRECT') || '{}'); Object.entries(paramsBeforeSSO).forEach(([key, value]) => { urlParams.set(key, value); }); } const paramsToSetAfterConnecting = {}; Array.from(urlParams.entries()).forEach(([key, value]) => { if (key.startsWith('neodash_')) { paramsToSetAfterConnecting[key] = value; } }); sessionStorage.getItem('SSO_PARAMS_BEFORE_REDIRECT'); const page = urlParams.get('page'); if (page !== '' && page !== null) { if (!isNaN(page)) { dispatch(setPageNumberThunk(parseInt(page))); } } dispatch(setSSOEnabled(config.ssoEnabled, state.application.cachedSSODiscoveryUrl)); dispatch(setSSOProviders(config.ssoProviders)); // Check if we are in standalone mode const standalone = config.standalone || urlParams.get('standalone') == 'Yes'; // if a dashboard database was previously set, remember to use it. const dashboardDatabase = state.application.standaloneDashboardDatabase; dispatch( setStandaloneEnabled( standalone, config.standaloneProtocol, config.standaloneHost, config.standalonePort, config.standaloneDatabase, config.standaloneDashboardName, dashboardDatabase || config.standaloneDashboardDatabase, config.standaloneDashboardURL, config.standaloneUsername, config.standalonePassword, config.standalonePasswordWarningHidden, config.standaloneAllowLoad, config.standaloneLoadFromOtherDatabases, config.standaloneMultiDatabase, config.standaloneDatabaseList ) ); dispatch(setLoggingMode(config.loggingMode)); dispatch(setLoggingDatabase(config.loggingDatabase)); dispatch(setLogErrorNotification('3')); dispatch(setConnectionModalOpen(false)); dispatch(setDeprecationNotice(config.deprecationNotice)); dispatch(setCustomHeader(config.customHeader)); // Auto-upgrade the dashboard version if an old version is cached. if (state.dashboard && state.dashboard.version !== NEODASH_VERSION) { // Attempt upgrade if dashboard version is outdated. while (VERSION_TO_MIGRATE[state.dashboard.version]) { const upgradedDashboard = upgradeDashboardVersion( state.dashboard, state.dashboard.version, VERSION_TO_MIGRATE[state.dashboard.version] ); dispatch(setDashboard(upgradedDashboard)); dispatch(setDraft(true)); dispatch( createNotificationThunk( 'Successfully upgraded dashboard', `Your old dashboard was migrated to version ${upgradedDashboard.version}. You might need to refresh this page and reactivate extensions.` ) ); } } // SSO - specific case starts here. if (state.application.waitForSSO) { // We just got redirected from the SSO provider. Hide all windows and attempt the connection. dispatch(setAboutModalOpen(false)); dispatch(setConnected(false)); dispatch(setWelcomeScreenOpen(false)); const success = await initializeSSO(state.application.cachedSSODiscoveryUrl, (credentials) => { if (standalone) { // Redirected from SSO and running in viewer mode, merge retrieved config with hardcoded credentials. dispatch( setConnectionProperties( config.standaloneProtocol, config.standaloneHost, config.standalonePort, config.standaloneDatabase, credentials.username, credentials.password ) ); dispatch( createConnectionThunk( config.standaloneProtocol, config.standaloneHost, config.standalonePort, config.standaloneDatabase, credentials.username, credentials.password ) ); } else { // Redirected from SSO and running in editor mode, merge retrieved config with existing details. dispatch( setConnectionProperties( state.application.connection.protocol, state.application.connection.url, state.application.connection.port, state.application.connection.database, credentials.username, credentials.password ) ); dispatch(setConnected(true)); } if (standalone) { if (urlParams.get('id')) { dispatch(setDashboardToLoadAfterConnecting(urlParams.get('id'))); } else if (config.standaloneDashboardURL !== undefined && config.standaloneDashboardURL.length > 0) { dispatch(setDashboardToLoadAfterConnecting(config.standaloneDashboardURL)); } else { dispatch(setDashboardToLoadAfterConnecting(`name:${config.standaloneDashboardName}`)); } dispatch(setParametersToLoadAfterConnecting(paramsToSetAfterConnecting)); } sessionStorage.removeItem('SSO_PARAMS_BEFORE_REDIRECT'); }); dispatch(setWaitForSSO(false)); if (!success) { alert('Unable to connect using SSO. See the browser console for more details.'); dispatch( createNotificationThunk( 'Unable to connect using SSO', 'Something went wrong. Most likely your credentials are incorrect...' ) ); } else { return; } } else if (state.application.ssoEnabled && !state.application.waitForSSO && urlParams) { let paramsToStore = {}; urlParams.forEach((value, key) => { paramsToStore[key] = value; }); sessionStorage.setItem('SSO_PARAMS_BEFORE_REDIRECT', JSON.stringify(paramsToStore)); } if (standalone) { dispatch(initializeApplicationAsStandaloneThunk(config, paramsToSetAfterConnecting)); } else { dispatch(initializeApplicationAsEditorThunk(config, paramsToSetAfterConnecting)); } } catch (e) { console.log(e); dispatch(setWelcomeScreenOpen(false)); dispatch( createNotificationThunk( 'Unable to load application configuration', 'Do you have a valid config.json deployed with your application?' ) ); } }; // Set up NeoDash to run in editor mode. export const initializeApplicationAsEditorThunk = (_, paramsToSetAfterConnecting) => (dispatch: any) => { const clearNotificationAfterLoad = true; dispatch(clearDesktopConnectionProperties()); dispatch(setDatabaseFromNeo4jDesktopIntegrationThunk()); const old = localStorage.getItem('neodash-dashboard'); dispatch(setOldDashboard(old)); dispatch(setConnected(false)); dispatch(setDashboardToLoadAfterConnecting(null)); dispatch(updateGlobalParametersThunk(paramsToSetAfterConnecting)); // TODO: this logic around loading/saving/upgrading/migrating dashboards needs a cleanup if (Object.keys(paramsToSetAfterConnecting).length > 0) { dispatch(setParametersToLoadAfterConnecting(null)); } // Check config to determine which screen is shown by default. if (DEFAULT_SCREEN == Screens.CONNECTION_MODAL) { dispatch(setWelcomeScreenOpen(false)); dispatch(setConnectionModalOpen(true)); } else if (DEFAULT_SCREEN == Screens.WELCOME_SCREEN) { dispatch(setWelcomeScreenOpen(true)); } if (clearNotificationAfterLoad) { dispatch(clearNotification()); } dispatch(handleSharedDashboardsThunk()); dispatch(setReportHelpModalOpen(false)); dispatch(setAboutModalOpen(false)); }; // Set up NeoDash to run in standalone mode. export const initializeApplicationAsStandaloneThunk = (config, paramsToSetAfterConnecting) => (dispatch: any, getState: any) => { const clearNotificationAfterLoad = true; const state = getState(); // If we are running in standalone mode, auto-set the connection details that are configured. dispatch( setConnectionProperties( config.standaloneProtocol, config.standaloneHost, config.standalonePort, config.standaloneDatabase, config.standaloneUsername ? config.standaloneUsername : state.application.connection.username, config.standalonePassword ? config.standalonePassword : state.application.connection.password ) ); dispatch(setAboutModalOpen(false)); dispatch(setConnected(false)); dispatch(setWelcomeScreenOpen(false)); if (config.standaloneDashboardURL !== undefined && config.standaloneDashboardURL.length > 0) { dispatch(setDashboardToLoadAfterConnecting(config.standaloneDashboardURL)); } else { dispatch(setDashboardToLoadAfterConnecting(`name:${config.standaloneDashboardName}`)); } dispatch(setParametersToLoadAfterConnecting(paramsToSetAfterConnecting)); dispatch(updateGlobalParametersThunk(paramsToSetAfterConnecting)); if (clearNotificationAfterLoad) { dispatch(clearNotification()); } // Override for when username and password are specified in the config - automatically connect to the specified URL. if (config.standaloneUsername && config.standalonePassword) { dispatch( createConnectionThunk( config.standaloneProtocol, config.standaloneHost, config.standalonePort, config.standaloneDatabase, config.standaloneUsername, config.standalonePassword ) ); } else { dispatch(setConnectionModalOpen(true)); } dispatch(handleSharedDashboardsThunk()); }; ================================================ FILE: src/application/logging/LoggingActions.ts ================================================ export const LOGGING_PREFIX = 'APPLICATION/LOGGING/'; export const SET_LOGGING_MODE = `${LOGGING_PREFIX}/SET_LOGGING_MODE`; export const setLoggingMode = (loggingMode: string) => ({ type: SET_LOGGING_MODE, payload: { loggingMode }, }); export const SET_LOGGING_DATABASE = `${LOGGING_PREFIX}/SET_LOGGING_DATABASE`; export const setLoggingDatabase = (loggingDatabase: string) => ({ type: SET_LOGGING_DATABASE, payload: { loggingDatabase }, }); export const SET_LOG_ERROR_NOTIFICATION = `${LOGGING_PREFIX}/SET_LOG_ERROR_NOTIFICATION`; export const setLogErrorNotification = (logErrorNotification: any) => ({ type: SET_LOG_ERROR_NOTIFICATION, payload: { logErrorNotification }, }); ================================================ FILE: src/application/logging/LoggingReducer.ts ================================================ import { LOGGING_PREFIX, SET_LOGGING_DATABASE, SET_LOGGING_MODE, SET_LOG_ERROR_NOTIFICATION } from './LoggingActions'; const update = (state, mutations) => Object.assign({}, state, mutations); export const LOGGING_INITIAL_STATE = { loggingMode: '0', logErrorNotification: '3', loggingDatabase: undefined, }; export const loggingReducer = (state = LOGGING_INITIAL_STATE, action: { type: any; payload: any }) => { const { type, payload } = action; if (!action.type.startsWith(LOGGING_PREFIX)) { return state; } // Logging state updates are handled here. switch (type) { case SET_LOGGING_MODE: { const { loggingMode } = payload; state = update(state, { loggingMode: loggingMode }); return state; } case SET_LOGGING_DATABASE: { const { loggingDatabase } = payload; state = update(state, { loggingDatabase: loggingDatabase }); return state; } case SET_LOG_ERROR_NOTIFICATION: { const { logErrorNotification } = payload; state = update(state, { logErrorNotification: logErrorNotification }); return state; } default: { return state; } } }; ================================================ FILE: src/application/logging/LoggingSelectors.ts ================================================ /** * Selector function for retrieving logging settings from the application state. * @param state - The application state. * @returns An object with logging settings. */ export const applicationGetLoggingSettings = (state: any) => state.application.logging; ================================================ FILE: src/application/logging/LoggingThunk.ts ================================================ import { createNotificationThunk } from '../../page/PageThunks'; import { runCypherQuery } from '../../report/ReportQueryRunner'; import { setLogErrorNotification } from './LoggingActions'; import { applicationGetLoggingSettings } from './LoggingSelectors'; import { createUUID } from '../../utils/uuid'; // Thunk to handle log events. export const createLogThunk = (loggingDriver, loggingDatabase, neodashMode, logUser, logAction, logDatabase, logDashboard = '', logMessage) => (dispatch: any, getState: any) => { try { const uuid = createUUID(); // Generate a cypher query to save the log. const query = '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'; const parameters = { uuid: uuid, user: logUser, logAction: logAction, logDatabase: logDatabase, neodashMode: neodashMode, logDashboard: logDashboard, logMessage: logMessage, }; runCypherQuery( loggingDriver, loggingDatabase, query, parameters, 1, () => {}, (records) => { if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) { console.log(`log created: ${uuid}`); } else { // we only show error notification one time const state = getState(); const loggingSettings = applicationGetLoggingSettings(state); let LogErrorNotificationNum = Number(loggingSettings.logErrorNotification); console.log(`Error creating log for ${(LogErrorNotificationNum - 4) * -1} times`); if (LogErrorNotificationNum > 0) { dispatch( createNotificationThunk( 'Error creating log', LogErrorNotificationNum > 1 ? `Please check logging configuration with your Neodash administrator` : `Please check logging configuration with your Neodash administrator - This message will not be displayed anymore in the current session` ) ); } LogErrorNotificationNum -= 1; dispatch(setLogErrorNotification(LogErrorNotificationNum.toString())); } } ); } catch (e) { // we only show error notification 3 times const state = getState(); const loggingSettings = applicationGetLoggingSettings(state); let LogErrorNotificationNum = Number(loggingSettings.logErrorNotification); console.log(`Error creating log for ${(LogErrorNotificationNum - 4) * -1} times`); if (LogErrorNotificationNum > 0) { dispatch( createNotificationThunk( 'Error creating log', LogErrorNotificationNum > 1 ? `Please check logging configuration with your Neodash administrator` : `Please check logging configuration with your Neodash administrator - This message will not be displayed anymore in the current session` ) ); } LogErrorNotificationNum -= 1; dispatch(setLogErrorNotification(LogErrorNotificationNum.toString())); } }; ================================================ FILE: src/card/Card.tsx ================================================ import { Card, Collapse, debounce } from '@mui/material'; import React, { useCallback, useContext, useEffect, useState } from 'react'; import NeoCardSettings from './settings/CardSettings'; import NeoCardView from './view/CardView'; import { connect } from 'react-redux'; import { updateFieldsThunk, updateSelectionThunk, updateReportQueryThunk, toggleCardSettingsThunk, updateReportSettingThunk, updateReportTitleThunk, updateReportTypeThunk, updateReportDatabaseThunk, } from './CardThunks'; import { toggleReportSettings } from './CardActions'; import { getReportState } from './CardSelectors'; import { getDashboardIsEditable, getDatabase, getGlobalParameters, getSessionParameters, } from '../settings/SettingsSelectors'; import { updateGlobalParameterThunk } from '../settings/SettingsThunks'; import useDimensions from 'react-cool-dimensions'; import { setReportHelpModalOpen } from '../application/ApplicationActions'; import { loadDatabaseListFromNeo4jThunk } from '../dashboard/DashboardThunks'; import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; import { getDashboardExtensions } from '../dashboard/DashboardSelectors'; import { downloadComponentAsImage } from '../chart/ChartUtils'; import { Dialog } from '@neo4j-ndl/react'; import { createNotificationThunk } from '../page/PageThunks'; const NeoCard = ({ id, // id of the card. report, // state of the card, retrieved based on card id. editable, // whether the card is editable. database, // the neo4j database that the card is running against. extensions, // A set of enabled extensions. globalParameters, // Query parameters that are globally set for the entire dashboard. dashboardSettings, // Dictionary of settings for the entire dashboard. onRemovePressed, // action to take when the card is removed. (passed from parent) onClonePressed, // action to take when user presses the clone button onReportHelpButtonPressed, // action to take when someone clicks the 'help' button in the report settings. onTitleUpdate, // action to take when the card title is updated. onTypeUpdate, // action to take when the card report type is updated. onFieldsUpdate, // action to take when the set of returned query fields is updated. onQueryUpdate, // action to take when the card query is updated. onReportSettingUpdate, // action to take when an advanced report setting is updated. onSelectionUpdate, // action to take when the selected visualization fields are updated. onGlobalParameterUpdate, // action to take when a report updates a dashboard parameter. onToggleCardSettings, // action to take when the card settings button is clicked. onToggleReportSettings, // action to take when the report settings (advanced settings) button is clicked. onDatabaseChanged, // action to take when the user changes the database related to the card loadDatabaseListFromNeo4j, // Thunk to get the list of databases createNotification, // Thunk to create a global notification pop-up. }) => { // Will be used to fetch the list of current databases const { driver } = useContext(Neo4jContext); const [databaseList, setDatabaseList] = React.useState([database]); const [databaseListLoaded, setDatabaseListLoaded] = React.useState(false); const ref = React.useRef(); // fetching the list of databases from neo4j, filtering out the 'system' db useEffect(() => { if (!databaseListLoaded) { loadDatabaseListFromNeo4j(driver, (result) => { let index = result.indexOf('system'); if (index > -1) { // only splice array when item is found result.splice(index, 1); // 2nd parameter means remove one item only } setDatabaseList(result); }); setDatabaseListLoaded(true); } }, [report.query]); const [settingsOpen, setSettingsOpen] = React.useState(false); const debouncedOnToggleCardSettings = useCallback(debounce(onToggleCardSettings, 500), []); const [collapseTimeout, setCollapseTimeout] = React.useState(report.collapseTimeout); const { observe, width, height } = useDimensions({ onResize: ({ observe, unobserve }) => { // Triggered whenever the size of the target is changed... unobserve(); // To stop observing the current target element observe(); // To re-start observing the current target element }, }); const [expanded, setExpanded] = useState(false); const onToggleCardExpand = () => { // When we re-minimize a card, close the settings to avoid position issues. if (expanded && settingsOpen) { onToggleCardSettings(id, false); } setExpanded(!expanded); }; const [active, setActive] = React.useState( report.settings && report.settings.autorun !== undefined ? report.settings.autorun : true ); useEffect(() => { if (!report.settingsOpen) { setActive(report.settings && report.settings.autorun !== undefined ? report.settings.autorun : true); } }, [report.query]); useEffect(() => { setSettingsOpen(report.settingsOpen); }, [report.settingsOpen]); useEffect(() => { setCollapseTimeout(report.collapseTimeout); }, [report.collapseTimeout]); // TODO - get rid of some of the props-drilling here... const component = (
{/* The front of the card, referred to as the 'view' */} onReportSettingUpdate(id, name, value)} createNotification={(title, message) => createNotification(title, message)} type={report.type} database={database} active={active} setActive={setActive} onDownloadImage={() => downloadComponentAsImage(ref)} query={report.query} globalParameters={globalParameters} fields={report.fields ? report.fields : []} selection={report.selection} widthPx={width} heightPx={height} title={report.title} expanded={expanded} onToggleCardExpand={onToggleCardExpand} onGlobalParameterUpdate={onGlobalParameterUpdate} onSelectionUpdate={(selectable, field) => onSelectionUpdate(id, selectable, field)} onTitleUpdate={(title) => onTitleUpdate(id, title)} onFieldsUpdate={(fields) => onFieldsUpdate(id, fields)} onToggleCardSettings={() => { setSettingsOpen(true); setCollapseTimeout('auto'); debouncedOnToggleCardSettings(id, true); }} /> {/* The back of the card, referred to as the 'settings' */} onQueryUpdate(id, query)} onDatabaseChanged={(database) => onDatabaseChanged(id, database)} onReportSettingUpdate={(setting, value) => onReportSettingUpdate(id, setting, value)} onTypeUpdate={(type) => onTypeUpdate(id, type)} onReportHelpButtonPressed={() => onReportHelpButtonPressed()} onRemovePressed={() => onRemovePressed(id)} onClonePressed={() => onClonePressed(id)} onToggleCardSettings={() => { setSettingsOpen(false); setCollapseTimeout('auto'); debouncedOnToggleCardSettings(id, false); }} onToggleReportSettings={() => onToggleReportSettings(id)} />
); // If the card is viewed in fullscreen, wrap it in a dialog. // TODO - this causes a re-render (and therefore, a re-run of the report) // Look into React Portals: https://stackoverflow.com/questions/61432878/how-to-render-child-component-outside-of-its-parent-component-dom-hierarchy if (expanded) { return ( {component} ); } return component; }; const mapStateToProps = (state, ownProps) => ({ report: getReportState(state, ownProps.id), extensions: getDashboardExtensions(state), editable: getDashboardIsEditable(state), database: getDatabase( state, ownProps && ownProps.dashboardSettings ? ownProps.dashboardSettings.pagenumber : undefined, ownProps.id ), globalParameters: { ...getGlobalParameters(state), ...getSessionParameters(state) }, }); const mapDispatchToProps = (dispatch) => ({ onTitleUpdate: (id: any, title: any) => { dispatch(updateReportTitleThunk(id, title)); }, onQueryUpdate: (id: any, query: any) => { dispatch(updateReportQueryThunk(id, query)); }, onTypeUpdate: (id: any, type: any) => { dispatch(updateReportTypeThunk(id, type)); }, onReportSettingUpdate: (id: any, setting: any, value: any) => { dispatch(updateReportSettingThunk(id, setting, value)); }, onFieldsUpdate: (id: any, fields: any) => { dispatch(updateFieldsThunk(id, fields)); }, onGlobalParameterUpdate: (key: any, value: any) => { dispatch(updateGlobalParameterThunk(key, value)); }, onSelectionUpdate: (id: any, selectable: any, field: any) => { dispatch(updateSelectionThunk(id, selectable, field)); }, onToggleCardSettings: (id: any, open: any) => { dispatch(toggleCardSettingsThunk(id, open)); }, onReportHelpButtonPressed: () => { dispatch(setReportHelpModalOpen(true)); }, onToggleReportSettings: (id: any) => { dispatch(toggleReportSettings(id)); }, onDatabaseChanged: (id: any, database: any) => { dispatch(updateReportDatabaseThunk(id, database)); }, createNotification: (title: any, message: any) => { dispatch(createNotificationThunk(title, message)); }, loadDatabaseListFromNeo4j: (driver, callback) => dispatch(loadDatabaseListFromNeo4jThunk(driver, callback)), }); export default connect(mapStateToProps, mapDispatchToProps)(NeoCard); ================================================ FILE: src/card/CardActions.ts ================================================ /** * A list of actions to perform on cards. */ export const TOGGLE_CARD_SETTINGS = 'PAGE/CARD/TOGGLE_CARD_SETTINGS'; export const toggleCardSettings = (pagenumber: any, id: any, open: any) => ({ type: TOGGLE_CARD_SETTINGS, payload: { pagenumber, id, open }, }); export const HARD_RESET_CARD_SETTINGS = 'PAGE/CARD/HARD_RESET_CARD_SETTINGS'; export const hardResetCardSettings = (pagenumber: any, id: any) => ({ type: HARD_RESET_CARD_SETTINGS, payload: { pagenumber, id }, }); export const UPDATE_REPORT_TITLE = 'PAGE/CARD/UPDATE_REPORT_TITLE'; export const updateReportTitle = (pagenumber: number, id: number, title: any) => ({ type: UPDATE_REPORT_TITLE, payload: { pagenumber, id, title }, }); export const UPDATE_REPORT_SIZE = 'PAGE/CARD/UPDATE_REPORT_SIZE'; export const updateReportSize = (pagenumber: number, id: number, width: any, height: any) => ({ type: UPDATE_REPORT_SIZE, payload: { pagenumber, id, width, height }, }); export const UPDATE_REPORT_QUERY = 'PAGE/CARD/UPDATE_REPORT_QUERY'; export const updateReportQuery = (pagenumber: number, id: number, query: any) => ({ type: UPDATE_REPORT_QUERY, payload: { pagenumber, id, query }, }); export const UPDATE_CYPHER_PARAMETERS = 'PAGE/CARD/UPDATE_CYPHER_PARAMETERS'; export const updateCypherParameters = (pagenumber: number, id: number, parameters: any) => ({ type: UPDATE_CYPHER_PARAMETERS, payload: { pagenumber, id, parameters }, }); export const UPDATE_REPORT_TYPE = 'PAGE/CARD/UPDATE_REPORT_TYPE'; export const updateReportType = (pagenumber: number, id: number, type: any) => ({ type: UPDATE_REPORT_TYPE, payload: { pagenumber, id, type }, }); export const UPDATE_FIELDS = 'PAGE/CARD/UPDATE_FIELDS'; export const updateFields = (pagenumber: number, id: number, fields: any) => ({ type: UPDATE_FIELDS, payload: { pagenumber, id, fields }, }); export const UPDATE_SCHEMA = 'PAGE/CARD/UPDATE_SCHEMA'; export const updateSchema = (pagenumber: number, id: number, schema: any) => ({ type: UPDATE_SCHEMA, payload: { pagenumber, id, schema }, }); export const UPDATE_SELECTION = 'PAGE/CARD/UPDATE_SELECTION'; export const updateSelection = (pagenumber: number, id: number, selectable: any, field: any) => ({ type: UPDATE_SELECTION, payload: { pagenumber, id, selectable, field }, }); export const UPDATE_ALL_SELECTIONS = 'PAGE/CARD/UPDATE_ALL_SELECTIONS'; export const updateAllSelections = (pagenumber: number, id: number, selections: any) => ({ type: UPDATE_ALL_SELECTIONS, payload: { pagenumber, id, selections }, }); export const CLEAR_SELECTION = 'PAGE/CARD/CLEAR_SELECTION'; export const clearSelection = (pagenumber: number, id: number) => ({ type: CLEAR_SELECTION, payload: { pagenumber, id }, }); export const UPDATE_REPORT_SETTING = 'PAGE/CARD/UPDATE_REPORT_SETTING'; export const updateReportSetting = (pagenumber: number, id: number, setting: any, value: any) => ({ type: UPDATE_REPORT_SETTING, payload: { pagenumber, id, setting, value }, }); export const TOGGLE_REPORT_SETTINGS = 'PAGE/CARD/TOGGLE_REPORT_SETTINGS'; export const toggleReportSettings = (id: any) => ({ type: TOGGLE_REPORT_SETTINGS, payload: { id }, }); export const UPDATE_REPORT_DATABASE = 'PAGE/CARD/UPDATE_REPORT_DATABASE'; export const updateReportDatabase = (pagenumber: number, id: number, database: any) => ({ type: UPDATE_REPORT_DATABASE, payload: { pagenumber, id, database }, }); ================================================ FILE: src/card/CardAddButton.tsx ================================================ import React from 'react'; import { connect } from 'react-redux'; import { Card, CardContent } from '@mui/material'; import { IconButton } from '@neo4j-ndl/react'; import { SquaresPlusIconOutline } from '@neo4j-ndl/react/icons'; /** * Button to add a new report to the current page. */ const NeoAddNewCard = ({ onCreatePressed }) => { return (
{ onCreatePressed(); }} size='large' floating >
); }; const mapStateToProps = () => ({}); const mapDispatchToProps = () => ({}); export default connect(mapStateToProps, mapDispatchToProps)(NeoAddNewCard); ================================================ FILE: src/card/CardReducer.ts ================================================ import { CLEAR_SELECTION, HARD_RESET_CARD_SETTINGS, TOGGLE_REPORT_SETTINGS, UPDATE_ALL_SELECTIONS, UPDATE_CYPHER_PARAMETERS, UPDATE_FIELDS, UPDATE_SCHEMA, UPDATE_REPORT_QUERY, UPDATE_REPORT_SETTING, UPDATE_REPORT_SIZE, UPDATE_REPORT_TITLE, UPDATE_REPORT_TYPE, UPDATE_SELECTION, UPDATE_REPORT_DATABASE, } from './CardActions'; import { TOGGLE_CARD_SETTINGS } from './CardActions'; import { createUUID } from '../utils/uuid'; const update = (state, mutations) => Object.assign({}, state, mutations); /** * State reducers for a single card instance as part of a report. */ export const CARD_INITIAL_STATE = { id: createUUID(), title: '', query: '\n\n\n', settingsOpen: false, advancedSettingsOpen: false, width: 3, height: 3, x: 0, y: 0, type: 'table', fields: [], selection: {}, settings: {}, collapseTimeout: 'auto', }; export const cardReducer = (state = CARD_INITIAL_STATE, action: { type: any; payload: any }) => { const { type, payload } = action; if (!action.type.startsWith('PAGE/CARD/')) { return state; } switch (type) { case UPDATE_REPORT_TITLE: { const { title } = payload; state = update(state, { title: title }); return state; } case UPDATE_REPORT_SIZE: { const { width, height } = payload; state = update(state, { width: width, height: height }); return state; } case UPDATE_REPORT_QUERY: { const { query } = payload; state = update(state, { query: query }); return state; } case UPDATE_CYPHER_PARAMETERS: { const { parameters } = payload; state = update(state, { parameters: parameters }); return state; } case UPDATE_FIELDS: { const { fields } = payload; state = update(state, { fields: fields }); return state; } case UPDATE_SCHEMA: { const { schema } = payload; state = update(state, { schema: schema }); return state; } case UPDATE_REPORT_TYPE: { const { type } = payload; state = update(state, { type: type }); return state; } case CLEAR_SELECTION: { state = update(state, { selection: {} }); return state; } case UPDATE_SELECTION: { const { selectable, field } = payload; const selection = state.selection ? state.selection : {}; const entry = {}; entry[selectable] = field; state = update(state, { selection: update(selection, entry) }); return state; } case UPDATE_ALL_SELECTIONS: { const { selections } = payload; state = update(state, { selection: selections }); return state; } case UPDATE_REPORT_SETTING: { const { setting, value } = payload; const settings = state.settings ? state.settings : {}; // Javascript is amazing, so "" == 0. Instead we check if the string length is zero... if (value == undefined || value.toString().length == 0) { delete settings[setting]; update(state, { settings: settings }); return state; } const entry = {}; entry[setting] = value; state = update(state, { settings: update(settings, entry) }); return state; } case TOGGLE_CARD_SETTINGS: { const { open } = payload; state = update(state, { settingsOpen: open, collapseTimeout: 'auto' }); return state; } case HARD_RESET_CARD_SETTINGS: { state = update(state, { settingsOpen: false, collapseTimeout: 0 }); return state; } case TOGGLE_REPORT_SETTINGS: { state = update(state, { advancedSettingsOpen: !state.advancedSettingsOpen }); return state; } case UPDATE_REPORT_DATABASE: { const { database } = payload; state = update(state, { database: database }); return state; } default: { return state; } } }; export default cardReducer; ================================================ FILE: src/card/CardSelectors.ts ================================================ export const getDashboardTitle = (state: any) => state.dashboard.title; export const getReportState = (state: any, id: any) => { const { pagenumber } = state.dashboard.settings; return state.dashboard.pages[pagenumber].reports.find((o) => o.id === id); }; ================================================ FILE: src/card/CardStyle.ts ================================================ // TODO We need to refactor styled components import styled from 'styled-components'; export const ReportItemContainer = styled.div``; ================================================ FILE: src/card/CardThunks.ts ================================================ import { updateReportTitle, updateReportQuery, updateSelection, updateCypherParameters, updateFields, updateReportType, updateReportSetting, toggleCardSettings, clearSelection, updateAllSelections, updateReportDatabase, updateSchema, } from './CardActions'; import { createNotificationThunk } from '../page/PageThunks'; import { getReportTypes } from '../extensions/ExtensionUtils'; import isEqual from 'lodash.isequal'; import { SELECTION_TYPES } from '../config/CardConfig'; import { getSelectionBasedOnFields } from '../chart/ChartUtils'; export const updateReportTitleThunk = (id, title) => (dispatch: any, getState: any) => { try { const state = getState(); const { pagenumber } = state.dashboard.settings; dispatch(updateReportTitle(pagenumber, id, title)); } catch (e) { dispatch(createNotificationThunk('Cannot update report title', e)); } }; /* Thunk used to update the database used from a report */ export const updateReportDatabaseThunk = (id, database) => (dispatch: any, getState: any) => { try { const state = getState(); const { pagenumber } = state.dashboard.settings; dispatch(updateReportDatabase(pagenumber, id, database)); } catch (e) { dispatch(createNotificationThunk('Cannot update report database', e)); } }; export const updateReportQueryThunk = (id, query) => (dispatch: any, getState: any) => { try { const state = getState(); const { pagenumber } = state.dashboard.settings; dispatch(updateReportQuery(pagenumber, id, query)); } catch (e) { dispatch(createNotificationThunk('Cannot update query', e)); } }; export const updateCypherParametersThunk = (id, parameters) => (dispatch: any, getState: any) => { try { const state = getState(); const { pagenumber } = state.dashboard.settings; dispatch(updateCypherParameters(pagenumber, id, parameters)); } catch (e) { dispatch(createNotificationThunk('Cannot update cypher parameters rate', e)); } }; export const updateReportTypeThunk = (id, type) => (dispatch: any, getState: any) => { try { const state = getState(); const { pagenumber } = state.dashboard.settings; dispatch(updateReportType(pagenumber, id, type)); dispatch(updateFields(pagenumber, id, [])); dispatch(updateSchema(pagenumber, id, [])); dispatch(clearSelection(pagenumber, id)); } catch (e) { dispatch(createNotificationThunk('Cannot update report type', e)); } }; export const updateFieldsThunk = (id, fields, schema = false) => (dispatch: any, getState: any) => { try { const state = getState(); const { pagenumber } = state.dashboard.settings; const extensions = Object.fromEntries(Object.entries(state.dashboard.extensions).filter(([_, v]) => v.active)); const oldReport = state.dashboard.pages[pagenumber].reports.find((o) => o.id === id); if (!oldReport) { return; } const oldFields = schema ? oldReport.schema : oldReport.fields; const reportType = oldReport.type; const oldSelection = oldReport.selection; const reportTypes = getReportTypes(extensions); const selectableFields = reportTypes[reportType].selection; // The dictionary of selectable fields as defined in the config. const { autoAssignSelectedProperties } = reportTypes[reportType]; const selectables = selectableFields ? Object.keys(selectableFields) : []; // If the new set of fields is not equal to the current set of fields, we ned to update the field selection. if (!isEqual(oldFields, fields) || Object.keys(oldSelection).length === 0) { selectables.forEach((selection, i) => { if (fields.includes(oldSelection[selection])) { // If the current selection is still present in the new set of fields, no need to reset. // Also we ignore this on a node property selector. /* continue */ } else if (selectableFields[selection].optional) { // If the fields change, always set optional selections to none. if (selectableFields[selection].multiple) { dispatch(updateSelection(pagenumber, id, selection, ['(none)'])); } else { dispatch(updateSelection(pagenumber, id, selection, '(none)')); } } else if (fields.length > 0) { // For multi selections, select the Nth item of the result fields as a single item array. if (selectableFields[selection].multiple) { // only update if the old selection no longer covers the new set of fields... if (!oldSelection[selection] || !oldSelection[selection].every((v) => fields.includes(v))) { dispatch(updateSelection(pagenumber, id, selection, [fields[Math.min(i, fields.length - 1)]])); } } else if (selectableFields[selection].type == SELECTION_TYPES.NODE_PROPERTIES) { // For node property selections, select the most obvious properties of the node to display. const selection = getSelectionBasedOnFields(fields, oldSelection, autoAssignSelectedProperties); dispatch(updateAllSelections(pagenumber, id, selection)); } else { // Else, default the selection to the Nth item of the result set fields. dispatch(updateSelection(pagenumber, id, selection, fields[Math.min(i, fields.length - 1)])); } } }); // Set the new set of fields for the report so that we may select them. if (schema) { dispatch(updateSchema(pagenumber, id, fields)); } else { dispatch(updateFields(pagenumber, id, fields)); } } } catch (e) { dispatch(createNotificationThunk('Cannot update report fields', e)); } }; export const updateSelectionThunk = (id, selectable, field) => (dispatch: any, getState: any) => { try { const state = getState(); const { pagenumber } = state.dashboard.settings; dispatch(updateSelection(pagenumber, id, selectable, field)); } catch (e) { dispatch(createNotificationThunk('Cannot update report selection', e)); } }; export const toggleCardSettingsThunk = (id, open) => (dispatch: any, getState: any) => { try { const state = getState(); const { pagenumber } = state.dashboard.settings; dispatch(toggleCardSettings(pagenumber, id, open)); } catch (e) { dispatch(createNotificationThunk('Cannot open card settings', e)); } }; export const updateReportSettingThunk = (id, setting, value) => (dispatch: any, getState: any) => { try { const state = getState(); const extensions = Object.fromEntries(Object.entries(state.dashboard.extensions).filter(([_, v]) => v.active)); const { pagenumber } = state.dashboard.settings; // If we disable optional selections (e.g. grouping), we reset these selections to their none value. if (setting == 'showOptionalSelections' && value == false) { const reportType = state.dashboard.pages[pagenumber].reports.find((o) => o.id === id).type; const reportTypes = getReportTypes(extensions); const selectableFields = reportTypes[reportType].selection; const optionalSelectables = selectableFields ? Object.keys(selectableFields).filter((key) => selectableFields[key].optional) : []; optionalSelectables.forEach((selection) => { dispatch(updateSelection(pagenumber, id, selection, '(none)')); }); } dispatch(updateReportSetting(pagenumber, id, setting, value)); } catch (e) { dispatch(createNotificationThunk('Error when updating report settings', e)); } }; ================================================ FILE: src/card/settings/CardSettings.tsx ================================================ import React from 'react'; import { ReportItemContainer } from '../CardStyle'; import NeoCardSettingsHeader from './CardSettingsHeader'; import NeoCardSettingsContent from './CardSettingsContent'; import NeoCardSettingsFooter from './CardSettingsFooter'; import { CardContent } from '@mui/material'; import { CARD_HEADER_HEIGHT } from '../../config/CardConfig'; const NeoCardSettings = ({ settingsOpen, pagenumber, reportId, query, database, // Current database related to the report databaseList, // List of databases the user can choose from ('system' is filtered out) width, height, type, reportSettings, reportSettingsOpen, fields, schema, heightPx, extensions, // A set of enabled extensions. onQueryUpdate, onDatabaseChanged, // When the database related to a report is changed it must be stored in the report state onRemovePressed, onClonePressed, onReportSettingUpdate, onToggleCardSettings, onTypeUpdate, setActive, onReportHelpButtonPressed, onToggleReportSettings, dashboardSettings, expanded, onToggleCardExpand, }) => { const reportHeight = heightPx - CARD_HEADER_HEIGHT + 19; const cardSettingsHeader = ( { setActive(reportSettings.autorun !== undefined ? reportSettings.autorun : true); onToggleCardSettings(e); }} /> ); // TODO - instead of hiding everything based on settingsopen, only hide the components that slow down render (cypher editor) const cardSettingsContent = settingsOpen ? ( ) : ( ); const cardSettingsFooter = settingsOpen ? ( ) : (
); return (
{cardSettingsHeader} {cardSettingsContent} {cardSettingsFooter}
); }; export default NeoCardSettings; ================================================ FILE: src/card/settings/CardSettingsContent.tsx ================================================ import React, { useEffect } from 'react'; import CardContent from '@mui/material/CardContent'; import debounce from 'lodash/debounce'; import { useCallback } from 'react'; import NeoCodeEditorComponent, { DEFAULT_CARD_SETTINGS_HELPER_TEXT_STYLE, } from '../../component/editor/CodeEditorComponent'; import { getReportTypes } from '../../extensions/ExtensionUtils'; import { Dropdown } from '@neo4j-ndl/react'; import { EXTENSIONS_CARD_SETTINGS_COMPONENT } from '../../extensions/ExtensionConfig'; import { objMerge } from '../../utils/ObjectManipulation'; const NeoCardSettingsContent = ({ pagenumber, reportId, query, database, // Current report database databaseList, // List of databases the user can choose from ('system' is filtered out) reportSettings, type, extensions, onQueryUpdate, onReportSettingUpdate, onTypeUpdate, forceRunQuery, // Callback to force close the card settings. onDatabaseChanged, // When the database related to a report is changed it must be stored in the report state }) => { // Ensure that we only trigger a text update event after the user has stopped typing. const [queryText, setQueryText] = React.useState(query); const debouncedQueryUpdate = useCallback(debounce(onQueryUpdate, 200), []); // State to manage the current database entry inside the form const [databaseText, setDatabaseText] = React.useState(database); const debouncedDatabaseUpdate = useCallback(debounce(onDatabaseChanged, 200), []); useEffect(() => { // Reset text to the dashboard state when the page gets reorganized. if (query !== queryText) { setQueryText(query); } }, [query]); const reportTypes = getReportTypes(extensions); const report = reportTypes[type]; const SettingsComponent = report?.settingsComponent || {}; function hasExtensionComponents() { return ( Object.keys(EXTENSIONS_CARD_SETTINGS_COMPONENT).filter( (name) => extensions[name] && EXTENSIONS_CARD_SETTINGS_COMPONENT[name] ).length > 0 ); } function updateCypherQuery(value) { debouncedQueryUpdate(value); setQueryText(value); } function renderExtensionsComponents() { const res = ( <> {Object.keys(EXTENSIONS_CARD_SETTINGS_COMPONENT).map((name) => { const Component = extensions[name] ? EXTENSIONS_CARD_SETTINGS_COMPONENT[name] : ''; return Component ? ( { onQueryUpdate(queryText); forceRunQuery(); }} cypherQuery={queryText} updateCypherQuery={updateCypherQuery} /> ) : ( <> ); })} ); return res; } const defaultQueryBoxComponent = ( <> { onQueryUpdate(queryText); forceRunQuery(); }} onChange={(value) => { updateCypherQuery(value); }} placeholder={`Enter Cypher here...`} />
{report?.helperText || ''}
); return ( newValue && onTypeUpdate(Object.keys(reportTypes).find((key) => reportTypes[key].label === newValue.value)), options: Object.keys(reportTypes).map((option) => ({ label: report && reportTypes[option].label, value: report && reportTypes[option].label, })), value: { label: report?.label || '', value: report?.label || '', }, menuPortalTarget: document.querySelector('#overlay'), }} fluid style={{ marginLeft: '0px', marginRight: '10px', width: '47%', maxWidth: '200px', display: 'inline-block' }} /> {report?.disableDatabaseSelector == undefined ? ( { newValue && setDatabaseText(newValue.value); newValue && debouncedDatabaseUpdate(newValue.value); }, options: databaseList.map((database) => ({ label: database, value: database, })), value: { label: databaseText, value: databaseText }, menuPortalTarget: document.querySelector('#overlay'), }} fluid style={{ marginLeft: '0px', marginRight: '10px', width: '47%', maxWidth: '200px', display: 'inline-block' }} /> ) : ( <> )}

{/* Allow for overriding the code box with a custom component */} {report && report.settingsComponent ? ( { onQueryUpdate(queryText); forceRunQuery(); }} /> ) : (
{hasExtensionComponents() ? renderExtensionsComponents() : defaultQueryBoxComponent}
)}
); }; export default NeoCardSettingsContent; ================================================ FILE: src/card/settings/CardSettingsFooter.tsx ================================================ import React, { useEffect } from 'react'; import debounce from 'lodash/debounce'; import { useCallback } from 'react'; import { FormGroup, Tooltip } from '@mui/material'; import NeoSetting from '../../component/field/Setting'; import { NeoCustomReportStyleModal, RULE_BASED_REPORT_CUSTOMIZATIONS, } from '../../extensions/styling/StyleRuleCreationModal'; import { getReportTypes } from '../../extensions/ExtensionUtils'; import { RULE_BASED_REPORT_ACTIONS_CUSTOMIZATIONS } from '../../extensions/actions/ActionsRuleCreationModal'; import NeoCustomReportActionsModal from '../../extensions/actions/ActionsRuleCreationModal'; import { AdjustmentsHorizontalIconOutline, SparklesIconOutline } from '@neo4j-ndl/react/icons'; import { IconButton, Switch } from '@neo4j-ndl/react'; const update = (state, mutations) => Object.assign({}, state, mutations); const NeoCardSettingsFooter = ({ type, fields = [], schema = [], reportSettings, reportSettingsOpen, extensions = {}, onToggleReportSettings, onReportSettingUpdate, }) => { const [reportSettingsText, setReportSettingsText] = React.useState(reportSettings); // Variables related to customizing report settings const [customReportStyleModalOpen, setCustomReportStyleModalOpen] = React.useState(false); const settingToCustomize = 'styleRules'; // Variables related to customizing report actions const [customReportActionsModalOpen, setCustomReportActionsModalOpen] = React.useState(false); const actionsToCustomize = 'actionsRules'; const debouncedReportSettingUpdate = useCallback(debounce(onReportSettingUpdate, 250), []); const updateSpecificReportSetting = (field: string, value: unknown) => { const entry = {}; entry[field] = value; setReportSettingsText(update(reportSettingsText, entry)); debouncedReportSettingUpdate(field, value); }; const reportTypes = getReportTypes(extensions); // Contains, for a certain type of chart, its disabling logic const disabledDependency = reportTypes[type] && reportTypes[type].disabledDependency; /** * This method manages the disabling logic for all the settings inside the footer. * The logic is based on the disabledDependency param inside the chart's configuration * @param field * @returns */ const getDisabled = (field: string) => { // By default an option is enabled let isDisabled = false; let dependencyLogic = disabledDependency[field]; if (dependencyLogic != undefined) { // Getting the current parameter defined in the settings of the report // (if undefined, the param will be treated as undefined (boolean false) let currentValue = reportSettingsText[dependencyLogic.dependsOn]; if (typeof dependencyLogic.operator === 'boolean') { if (!dependencyLogic.operator) { isDisabled = !currentValue; } } // if the value is in the list of values that enable the option, then enable the option else if (dependencyLogic.operator === 'not in') { isDisabled = !dependencyLogic.values.includes(currentValue); } } return isDisabled; }; useEffect(() => { // Reset text to the dashboard state when the page gets reorganized. setReportSettingsText(reportSettings); }, [JSON.stringify(reportSettings)]); const settings = reportTypes[type] ? reportTypes[type].settings : {}; // If there are no advanced settings, render nothing. if (Object.keys(settings).length == 0) { return
; } // Else, build the advanced settings view. const advancedReportSettings = (
{Object.keys(settings).map((setting) => { let isDisabled = false; // Adding disabling logic to specific entries but only if the logic is defined inside the configuration if (disabledDependency != undefined) { isDisabled = getDisabled(setting); } return ( updateSpecificReportSetting(setting, e)} /> ); })}
); // TODO - Make the extensions more pluggable and dynamic, instead of hardcoded here. // ^ keep modals at a higher level in the object hierarchy instead of injecting in the footer. return (
{extensions.styling && extensions.styling.active ? ( ) : ( <> )} {extensions.actions && extensions.actions.active ? ( ) : ( <> )}
{RULE_BASED_REPORT_CUSTOMIZATIONS[type] && extensions.styling && extensions.styling.active ? ( { setCustomReportStyleModalOpen(true); // Open the modal. }} clean > ) : ( <> )} {extensions.actions && extensions.actions.active && RULE_BASED_REPORT_ACTIONS_CUSTOMIZATIONS[type] ? ( { setCustomReportActionsModalOpen(true); // Open the modal. }} > ) : ( <> )}
{reportSettingsOpen ? advancedReportSettings :
}
); }; export default NeoCardSettingsFooter; ================================================ FILE: src/card/settings/CardSettingsHeader.tsx ================================================ import React from 'react'; import { Tooltip, CardHeader } from '@mui/material'; import { IconButton } from '@neo4j-ndl/react'; import { ExpandIcon, ShrinkIcon, DragIcon, QuestionMarkCircleIconOutline, TrashIconOutline, DocumentDuplicateIconOutline, PlayCircleIconSolid, } from '@neo4j-ndl/react/icons'; const NeoCardSettingsHeader = ({ onRemovePressed, onToggleCardSettings, onToggleCardExpand, expanded, fullscreenEnabled, onReportHelpButtonPressed, onClonePressed, }) => { const maximizeButton = ( ); const unMaximizeButton = ( ); return ( } action={ <> {fullscreenEnabled ? expanded ? unMaximizeButton : maximizeButton : <>} { e.preventDefault(); onToggleCardSettings(); }} clean size='medium' > } title='' subheader='' /> ); }; export default NeoCardSettingsHeader; ================================================ FILE: src/card/view/CardView.tsx ================================================ import React, { useEffect, useState } from 'react'; import { ReportItemContainer } from '../CardStyle'; import NeoCardViewHeader from './CardViewHeader'; import NeoCardViewFooter from './CardViewFooter'; import { CardContent } from '@mui/material'; import NeoCodeEditorComponent from '../../component/editor/CodeEditorComponent'; import { CARD_FOOTER_HEIGHT, CARD_HEADER_HEIGHT } from '../../config/CardConfig'; import { getReportTypes } from '../../extensions/ExtensionUtils'; import NeoCodeViewerComponent from '../../component/editor/CodeViewerComponent'; import { NeoReportWrapper } from '../../report/ReportWrapper'; import { identifyStyleRuleParameters } from '../../extensions/styling/StyleRuleEvaluator'; import { IconButton } from '@neo4j-ndl/react'; import { PlayCircleIconSolid } from '@neo4j-ndl/react/icons'; import { extensionEnabled } from '../../utils/ReportUtils'; import { objMerge } from '../../utils/ObjectManipulation'; import { REPORT_TYPES } from '../../config/ReportConfig'; const NeoCardView = ({ id, title, database, query, globalParameters, widthPx, heightPx, fields, extensions, active, setActive, onDownloadImage, type, selection, dashboardSettings, settings, updateReportSetting, createNotification, settingsOpen, editable, onGlobalParameterUpdate, onSelectionUpdate, onToggleCardSettings, onTitleUpdate, onFieldsUpdate, expanded, onToggleCardExpand, }) => { const reportHeight = heightPx - CARD_FOOTER_HEIGHT - CARD_HEADER_HEIGHT + 20; const cardHeight = heightPx - CARD_FOOTER_HEIGHT + 23; const ref = React.useRef(); const settingsSelector = Object.keys( Object.fromEntries(Object.entries(REPORT_TYPES[type]?.settings || {}).filter(([_, value]) => value.refresh)) ).reduce((obj, key) => { return Object.assign(obj, { [key]: settings[key], }); }, {}); const [lastRunTimestamp, setLastRunTimestamp] = useState(Date.now()); // TODO : selectorChange should handle every case where query execution needs to be re-executed // e.g. Change of query, type, some advanced settings... const [selectorChange, setSelectorChange] = useState(false); const getLocalParameters = (parse_string, drilldown = true): unknown => { if (!parse_string || !globalParameters) { return {}; } let re = /(?:^|\W|%20)\$(\w+)(?!\w)/g; let match; // If the report styling extension is enabled, extend the list of local (relevant) parameters with those used by the style rules. const styleRules = settings.styleRules ? settings.styleRules : []; const styleParams = extensionEnabled(extensions, 'styling') ? identifyStyleRuleParameters(styleRules) : []; // Similarly, if the forms extension is enabled, extract nested parameters used by parameter selectors inside the form. const formFields = settings.formFields ? settings.formFields : []; const formsParams = drilldown && extensionEnabled(extensions, 'forms') ? formFields .map((f) => { return Object.keys(getLocalParameters(f.query, false)); }) .flat() : []; let localQueryVariables: string[] = [...styleParams, ...formsParams]; while ((match = re.exec(parse_string))) { localQueryVariables.push(match[1]); } let params = Object.fromEntries( Object.entries(globalParameters).filter(([local]) => localQueryVariables.includes(local)) ); return settings.ignoreNonDefinedParams ? objMerge(Object.fromEntries(localQueryVariables.map((name) => [name, null])), params) : params; }; // @ts-ignore const reportHeader = ( setLastRunTimestamp(Date.now())} settings={settings} onDownloadImage={onDownloadImage} onToggleCardExpand={onToggleCardExpand} expanded={expanded} parameters={getLocalParameters(title)} > ); // @ts-ignore const reportFooter = active ? ( ) : ( <> ); const localParameters = { ...getLocalParameters(query), ...getLocalParameters(settings.drilldownLink) }; const reportTypes = getReportTypes(extensions); const reportTypeHasNoFooter = reportTypes[type] && reportTypes[type].withoutFooter; const withoutFooter = reportTypeHasNoFooter ? reportTypes[type].withoutFooter : (reportTypes[type] && !reportTypes[type].selection) || (settings && settings.hideSelections); const getGlobalParameter = (key: string): unknown => { return globalParameters ? globalParameters[key] : undefined; }; useEffect(() => { if (!settingsOpen) { setLastRunTimestamp(Date.now()); } }, [JSON.stringify(localParameters)]); useEffect(() => { if (!settingsOpen && (selectorChange || type === 'select')) { setLastRunTimestamp(Date.now()); } setSelectorChange(false); }, [settingsOpen]); useEffect(() => { setSelectorChange(true); }, [query, type, database, JSON.stringify(settingsSelector)]); // TODO - understand why CardContent is throwing a warning based on this style config. const cardContentStyle = { paddingBottom: '0px', paddingLeft: '0px', paddingRight: '0px', paddingTop: '0px', width: '100%', marginTop: '-9px', height: expanded ? withoutFooter ? '100%' : `calc(100% - ${CARD_FOOTER_HEIGHT}px)` : withoutFooter ? `${reportHeight + CARD_FOOTER_HEIGHT - (reportTypeHasNoFooter ? 0 : 20)}px` : `${reportHeight}px`, overflow: 'auto', }; const reportContent = ( {active ? ( ) : ( <> { setActive(true); }} clean > {}} placeholder={'No query specified...'} /> )} ); return (
{reportHeader} {/* if there's no selection for this report, we don't have a footer, so the report can be taller. */} {reportTypes[type] ? ( reportContent ) : ( )} {reportTypes[type] ? reportFooter : <>}
); }; export default NeoCardView; ================================================ FILE: src/card/view/CardViewFooter.tsx ================================================ import React from 'react'; import { CardActions, FormControl, InputLabel, MenuItem, Select } from '@mui/material'; import { categoricalColorSchemes } from '../../config/ColorConfig'; import { getReportTypes } from '../../extensions/ExtensionUtils'; import { SELECTION_TYPES } from '../../config/CardConfig'; import { Dropdown, Label } from '@neo4j-ndl/react'; const NeoCardViewFooter = ({ fields, settings, selection, type, extensions, showOptionalSelections, onSelectionUpdate, dashboardSettings, }) => { /** * For each selectable field in the visualization, give the user an option to select them from the query output fields. */ const reportTypes = getReportTypes(extensions); const selectableFields = reportTypes[type].selection; const selectables = selectableFields ? Object.keys(selectableFields) : []; const nodeColorScheme = settings && settings.nodeColorScheme ? settings.nodeColorScheme : 'neodash'; const hideSelections = settings && settings.hideSelections ? settings.hideSelections : false; const { ignoreLabelColors } = reportTypes[type]; if (!fields || fields.length == 0 || hideSelections) { return
; } return ( {selectables.map((selectable, index) => { const selectionIsMandatory = !selectableFields[selectable].optional; // Creates the component for node property selections. if (selectableFields[selectable].type == SELECTION_TYPES.NODE_PROPERTIES) { // Only show optional selections if we explicitly allow it. if (showOptionalSelections || selectionIsMandatory) { const totalColors = categoricalColorSchemes[nodeColorScheme] ? categoricalColorSchemes[nodeColorScheme].length : 0; const fieldSelections = fields.map((field, i) => { // TODO logically, it should be the last element in the field (node labels) array, as that is typically // the most specific node label when we have multi-labels const nodeLabel = field[0]; // TODO this convention that we have for storing node labels and properties in fields should be documented // , and probably even converted to a generic type. const discoveredProperties = field.slice(1); const properties = (discoveredProperties ? [...discoveredProperties].sort() : []).concat([ '(label)', '(id)', '(no label)', ]); const color = totalColors > 0 && !ignoreLabelColors ? categoricalColorSchemes[nodeColorScheme][i % totalColors] : 'lightgrey'; const inputColor = dashboardSettings.theme === 'dark' ? 'var(--palette-dark-neutral-border-strong)' : 'rgba(0, 0, 0, 0.6)'; return ( {nodeLabel} ); }); return fieldSelections; } } // Creates the selection for all other types of components if ( selectableFields[selectable].type == SELECTION_TYPES.LIST || selectableFields[selectable].type == SELECTION_TYPES.NUMBER || selectableFields[selectable].type == SELECTION_TYPES.NUMBER_OR_DATETIME || selectableFields[selectable].type == SELECTION_TYPES.TEXT ) { if (selectionIsMandatory || showOptionalSelections) { const sortedFields = fields ? [...fields].sort() : []; const fieldsToRender = selectionIsMandatory ? sortedFields : sortedFields.concat(['(none)']); return ( (newValue && selectableFields[selectable].multiple ? onSelectionUpdate( selectable, newValue.map((v) => v.value) ) : onSelectionUpdate(selectable, newValue.value)), options: fieldsToRender.map((option) => ({ label: option, value: option })), value: selectableFields[selectable].multiple ? selection[selectable].map((sel) => ({ label: sel, value: sel })) : { label: selection[selectable], value: selection[selectable] }, isMulti: selectableFields[selectable].multiple, isClearable: false, menuPortalTarget: document.querySelector('#overlay'), }} fluid style={{ minWidth: selectableFields[selectable].multiple ? 170 : 120, marginRight: 20, display: 'inline-block', }} placeholder={selectableFields[selectable].multiple ? 'Select (multiple)' : 'Select'} > ); } } })} ); }; export default NeoCardViewFooter; ================================================ FILE: src/card/view/CardViewHeader.tsx ================================================ import React, { useEffect } from 'react'; import { Badge, CardHeader, Dialog, DialogContent, DialogTitle, TextField, Tooltip } from '@mui/material'; import debounce from 'lodash/debounce'; import { useCallback } from 'react'; import ReactMarkdown from 'react-markdown'; import gfm from 'remark-gfm'; import { replaceDashboardParameters } from '../../chart/ChartUtils'; import { IconButton } from '@neo4j-ndl/react'; import { DragIcon, EllipsisVerticalIconOutline, ArrowPathIconOutline, ExpandIcon, ShrinkIcon, CameraIconSolid, InformationCircleIconOutline, XMarkIconOutline, } from '@neo4j-ndl/react/icons'; import { createTheme, ThemeProvider } from '@mui/material/styles'; const NeoCardViewHeader = ({ title, description, editable, onTitleUpdate, fullscreenEnabled, downloadImageEnabled, refreshButtonEnabled, onToggleCardSettings, onManualRefreshCard, onDownloadImage, onToggleCardExpand, expanded, parameters, }) => { const [text, setText] = React.useState(title); const [parsedText, setParsedText] = React.useState(title); const [editing, setEditing] = React.useState(false); const [descriptionModalOpen, setDescriptionModalOpen] = React.useState(false); function replaceParamsOnString(s, p) { let parsed: string; parsed = replaceDashboardParameters(s, p); return parsed; } // Ensure that we only trigger a text update event after the user has stopped typing. const debouncedTitleUpdate = useCallback(debounce(onTitleUpdate, 250), []); useEffect(() => { let titleParsed = replaceParamsOnString(`${title}`, parameters); if (!editing) { setParsedText(titleParsed); } }, [editing, parameters]); useEffect(() => { // Reset text to the dashboard state when the page gets reorganized. if (text !== title) { setText(title); } }, [title]); const theme = createTheme({ typography: { fontFamily: "'Nunito Sans', sans-serif !important", allVariants: { color: 'rgb(var(--palette-neutral-text-weak))' }, }, palette: { text: { primary: 'rgb(var(--palette-neutral-text))', }, action: { disabled: 'rgb(var(--palette-neutral-text-weak))', }, }, }); const cardTitle = ( {editable ? ( ) : ( <> )}
{}} > { setEditing(true); }} onBlur={() => { setEditing(false); }} className={'no-underline large'} label='' disabled={!editable} placeholder='Report name...' fullWidth maxRows={4} value={editing ? text : parsedText !== ' ' ? parsedText : ''} onChange={(event) => { setText(event.target.value); debouncedTitleUpdate(event.target.value); }} size={'small'} style={{ paddingTop: '0px important!' }} variant={'standard'} sx={{ '& .MuiInputBase-input.Mui-disabled': { WebkitTextFillColor: 'inherit', }, }} />
); const descriptionEnabled = description && description.length > 0; // TODO: all components like buttons should probably be seperate files const settingsButton = ( ); const refreshButton = ( ); const maximizeButton = ( ); const unMaximizeButton = ( ); const downloadImageButton = ( ); const descriptionButton = ( setDescriptionModalOpen(true)} aria-label='details' clean size='medium'> ); return ( <> setDescriptionModalOpen(false)} aria-labelledby='form-dialog-title' > {title} setDescriptionModalOpen(false)} style={{ padding: '3px', float: 'right' }} aria-label={'rect badge'} clean >
{downloadImageEnabled ? downloadImageButton : <>} {fullscreenEnabled ? expanded ? unMaximizeButton : maximizeButton : <>} {descriptionEnabled ? descriptionButton : <>} {refreshButtonEnabled ? refreshButton : <>} {editable ? settingsButton : <>} } title={cardTitle} /> ); }; export default NeoCardViewHeader; ================================================ FILE: src/chart/Chart.ts ================================================ import { Record as Neo4jRecord } from 'neo4j-driver'; /** * Interface for all charts that NeoDash can render. * When you extend NeoDash, make sure that your component implements this interface. */ export interface ChartProps { records: Neo4jRecord[]; // Query output, Neo4j records as returned from the driver. extensions?: Record; // A dictionary of enabled extensions. selection?: Record; // A dictionary with the selection made in the report footer. settings?: Record; // A dictionary with the 'advanced settings' specified through the NeoDash interface. dimensions?: Record; // a dictionary with the dimensions of the report (likely not needed, charts automatically fill up space). fullscreen?: boolean; // flag indicating whether the report is rendered in a fullscreen view. parameters?: Record; // A dictionary with the global dashboard parameters. query?: string; // The original query that was used to populate the `records`. queryCallback?: (query: string | undefined, parameters: Record, setRecords: any) => void; // Callback to query the database with a given set of parameters. Calls 'setReccords' upon completion. createNotification?: (title: string, message: string) => void; // Callback to create a notification that overlays the entire application. setGlobalParameter?: (name: string, value: any) => void; // Allows a chart to update a global dashboard parameter to be used in Cypher queries for other reports. getGlobalParameter?: (name) => string; // Allows a chart to get a global dashboard parameter. updateReportSetting?: (name, value) => void; // Callback to update a setting for this report. fields: (fields) => string[]; // List of fields (return values) available for the report. setFields?: (fields) => void; // Update the list of fields for this report. theme?: string; // Dashboard theme value. } /** * A simplified schema of the Neo4j database. */ export interface Neo4jSchema { nodeLabels: string[]; // list of node labels. relationshipTypes: string[]; // list of relationship types. setPageNumber?: (index: number) => void; // Callback to update the currently selected page of the dashboard. } ================================================ FILE: src/chart/ChartUtils.ts ================================================ import domtoimage from 'dom-to-image'; import { Date as Neo4jDate } from 'neo4j-driver-core/lib/temporal-types.js'; /** * Converts a neo4j record entry to a readable string representation. */ export const convertRecordObjectToString = (entry) => { if (entry == null || entry == undefined) { return entry; } const className = entry.__proto__.constructor.name; if (className == 'String') { return entry; } else if (valueIsNode(entry)) { return convertNodeToString(entry); } else if (valueIsRelationship(entry)) { return convertRelationshipToString(entry); } else if (valueIsPath(entry)) { return convertPathToString(entry); } return entry.toString(); }; /** * Converts a neo4j node record entry to a readable string representation. * if it's a fieldType =="Node" * Then, return * 1. 'name' property, if it exists, * 2. the 'title' property, if it exists, * 3. the 'id' property, if it exists... * 4. the 'uid' property, if it exists.. * 5. the ({labels}}, if they exist, * 6. Node(id). */ const convertNodeToString = (nodeEntry) => { if (nodeEntry.properties.name) { return `${nodeEntry.labels}(${nodeEntry.properties.name})`; } if (nodeEntry.properties.title) { return `${nodeEntry.labels}(${nodeEntry.properties.title})`; } if (nodeEntry.properties.id) { return `${nodeEntry.labels}(${nodeEntry.properties.id})`; } if (nodeEntry.properties.uid) { return `${nodeEntry.labels}(${nodeEntry.properties.uid})`; } return `${nodeEntry.labels}(` + `_id=${nodeEntry.identity})`; }; // if it's a fieldType == "Relationship" const convertRelationshipToString = (relEntry) => { return relEntry.toString(); }; // if it's a fieldType == "Path" const convertPathToString = (pathEntry) => { return pathEntry.toString(); }; // Anything else, return the string representation of the object. /* HELPER FUNCTIONS FOR DETERMINING TYPE OF FIELD RETURNED FROM NEO4J */ export function valueIsArray(value) { const className = value !== undefined && value.__proto__.constructor.name; return className == 'Array'; } export function valueIsNode(value) { // const className = value.__proto__.constructor.name; // return className == "Node"; return value && value.labels && value.identity && value.properties; } export function valueIsRelationship(value) { // const className = value.__proto__.constructor.name; // return className == "Relationship"; return value && value.type && value.start && value.end && value.identity && value.properties; } export function valueIsPath(value) { // const className = value.__proto__.constructor.name; // return className == "Path" return value && value.start && value.end && value.segments && value.length; } export function valueisPoint(value) { // Look at the properties and identify the type. return value && value.x && value.y && value.srid; } export function valueIsObject(value) { // TODO - this will not work in production builds. Need alternative. const className = value.__proto__.constructor.name; return className == 'Object'; } export function toNumber(ref) { if (ref === undefined || typeof ref === 'number') { return ref; } let { low, high } = ref; let res = high; for (let i = 0; i < 32; i++) { res *= 2; } return low + res; } export function getRecordType(value) { // mui data-grid native column types are: 'string' (default), // 'number', 'date', 'dateTime', 'boolean' and 'singleSelect' // https://v4.mui.com/components/data-grid/columns/#column-types // Type singleSelect is not implemented here if (value === true || value === false) { return 'boolean'; } else if (value === undefined) { return 'undefined'; } else if (value === null) { return 'null'; } else if (value.__isInteger__) { return 'integer'; } else if (typeof value == 'number') { return 'number'; } else if (value.__isDate__) { return 'date'; } else if (value.__isDateTime__) { return 'dateTime'; } else if (valueIsNode(value)) { return 'node'; } else if (valueIsRelationship(value)) { return 'relationship'; } else if (valueIsPath(value)) { return 'path'; } else if (valueIsArray(value)) { return 'array'; } else if (valueIsObject(value)) { if (!isNaN(toNumber(value))) { return 'objectNumber'; } return 'object'; } else if (typeof value === 'string' || value instanceof String) { if (value.startsWith('http') || value.startsWith('https')) { return 'link'; } return 'string'; } // Use string as default type return 'string'; } /** * Basic function to convert a table row output to a CSV file, and download it. * TODO: Make this more robust. Probably the commas should be escaped to ensure the CSV is always valid. */ export const downloadCSV = (rows) => { const element = document.createElement('a'); let csv = ''; const headers = Object.keys(rows[0]).slice(1); csv += `${headers.join(', ')}\n`; rows.forEach((row) => { headers.forEach((header) => { // Parse value let value = row[header]; if (value?.low !== undefined) { value = value.low; } csv += `${JSON.stringify(value)}`; csv += headers.indexOf(header) < headers.length - 1 ? ',' : ''; }); csv += '\n'; }); const file = new Blob([`\ufeff${csv}`], { type: 'text/plain;charset=utf8' }); element.href = URL.createObjectURL(file); element.download = 'table.csv'; document.body.appendChild(element); // Required for this to work in FireFox element.click(); }; /** * Replaces all global dashboard parameters inside a string with their values. * @param str The string to replace the parameters in. * @param parameters The parameters to replace. */ export function replaceDashboardParameters(str, parameters) { if (!str) { return ''; } let rx = /`.([^`]*)`/g; let regexSquareBrackets = /\[(.*?)\]/g; let rxSimple = /\$neodash_\w*/g; /** * Define function to access elements in an array/object type dashboard parameter. * @param _ needed for str.replace(), unused. * @param p1 - the original string. * @returns an updated markdown with injected parameters. */ const parameterElementReplacer = (_, p1) => { // Find (in the markdown) occurences of the parameter `$neodash_movie_title[index]` or `$neodash_movie_title[key]`. let matches = p1.match(regexSquareBrackets); let param = p1.split('[')[0].replace(`$`, '').trim(); let val = parameters?.[param] || null; // Inject the element at that index/key into the markdown as text. matches?.forEach((m) => { let i = m.replace(/[[\]']+/g, ''); i = isNaN(i) ? i.replace(/['"']+/g, '') : Number(i); val = val ? val[i] : null; }); return RenderSubValue(val); }; const parameterSimpleReplacer = (_) => { let param = _.replace(`$`, '').trim(); let val = parameters?.[param] || null; let type = getRecordType(val); // Arrays weren't playing nicely with RenderSubValue(). Each object would be passed separately and return [oject Object]. if (type === 'string' || type == 'link') { return val; } else if (type === 'array') { return RenderSubValue(val.join(', ')); } return RenderSubValue(val); }; let newString = str.replace(rx, parameterElementReplacer).replace(rxSimple, parameterSimpleReplacer); return newString; } export function replaceDashboardParametersInString(str, parameters) { Object.keys(parameters).forEach((key) => { str = str.replaceAll(`$${key}`, parameters[key]); }); return str; } /** * Downloads a screenshot of the element reference passed to it. * @param ref The reference to the element to download as an image. */ export const downloadComponentAsImage = (ref) => { const element = ref.current; domtoimage.toPng(element, { bgcolor: 'white' }).then((dataUrl) => { const link = document.createElement('a'); link.download = 'image.png'; link.href = dataUrl; link.click(); }); }; import { QueryResult, Record as Neo4jRecord } from 'neo4j-driver'; import { RenderSubValue } from '../report/ReportRecordProcessing'; /** * Function to cast a value received from the Neo4j Driver to its TS native type * @param input Value to cast * @returns Value casted to it's native type */ export function recordToNative(input: any): any { if (!input && input !== false) { return null; } else if (typeof input.keys === 'object' && typeof input.get === 'function') { return Object.fromEntries(input.keys.map((key) => [key, recordToNative(input.get(key))])); } else if (typeof input.toNumber === 'function') { return input.toNumber(); } else if (Array.isArray(input)) { return (input as Array).map((item) => recordToNative(item)); } else if (typeof input === 'object') { const converted = Object.entries(input).map(([key, value]) => [key, recordToNative(value)]); return Object.fromEntries(converted); } return input; } export function resultToNative(result: QueryResult): Record { if (!result) { return {}; } return result.records.map((row) => recordToNative(row)); } export function checkResultKeys(first: Neo4jRecord, keys: string[]) { const missing = keys.filter((key) => !first.keys.includes(key)); if (missing.length > 0) { return new Error( `The query is missing the following key${missing.length > 1 ? 's' : ''}: ${missing.join( ', ' )}. The expected keys are: ${keys.join(', ')}` ); } return false; } /** * For hierarchical data structures, recursively search for a key property that must have a given value. * If none can be found, return null. */ export const search = (tree, value, key = 'id', reverse = false) => { if (tree.length == 0) { return null; } const stack = Array.isArray(tree) ? [...tree] : [tree]; while (stack.length) { const node = stack[reverse ? 'pop' : 'shift'](); if (node[key] && node[key] === value) { return node; } if (node.children) { stack.push(...node.children); } } return null; }; /** * For hierarchical data, we remove all intermediate node prefixes generate by `processHierarchyFromRecords`. * This ensures that the visualization itself shows the 'real' names, and not the intermediate ones. */ export const mutateName = (currentNode) => { if (currentNode.name) { const s = currentNode.name.split('_'); currentNode.name = s.length > 0 ? s.slice(1).join('_') : s[0]; } if (currentNode.children) { currentNode.children.forEach((n) => mutateName(n)); } }; export const findObject = (data, name) => data.find((searchedName) => searchedName.name === name); export const flatten = (data) => data.reduce((acc, item) => { if (item.children) { return [...acc, item, ...flatten(item.children)]; } return [...acc, item]; }, []); /** * Converts a list of Neo4j records into a hierarchy structure for hierarchical data visualizations. */ // TODO: this needs docs export const processHierarchyFromRecords = (records: Record[], selection: any) => { return records.reduce((data: Record, row: Record) => { try { const index = recordToNative(row.get(selection.index)); // const idx = data.findIndex(item => item.index === index) // const key = selection['key'] !== "(none)" ? recordToNative(row.get(selection['key'])) : selection['value']; const value = recordToNative(row.get(selection.value)); if (!Array.isArray(index) || isNaN(value)) { throw 'Invalid data format selected for hierarchy report.'; } let holder = data; for (let [idx, val] of index.entries()) { // Add a level prefix to each item to avoid duplicates val = `lvl${idx}_${val}`; const obj = search(holder, val, 'name'); const entry = { name: val }; if (obj) { holder = obj; } else { if (Array.isArray(holder)) { holder.push(entry); // eslint-disable-next-line no-prototype-builtins } else if (holder.hasOwnProperty('children')) { holder.children.push(entry); } else { holder.children = [entry]; } holder = search(holder, val, 'name'); } } holder.loc = value; return data; } catch (e) { // eslint-disable-next-line no-console console.error(e); return []; } }, []); }; /** * Wrapper for empty check logic, to prevent calling writing the same code too many times * @param obj * @returns Returns True if the input is null, undefined or an empty object */ export const isEmptyObject = (obj: object) => { if (obj == undefined) { return true; } return Object.keys(obj).length == 0; }; /** * Checks that the value in input can be casted to Neo4j Bolt Driver Date * @param value * @returns True if it's an object castable to date */ export function isCastableToNeo4jDate(value: object) { if (value == null || value == undefined) { return false; } let keys = Object.keys(value); return keys.includes('day') && keys.includes('month') && keys.includes('year'); } /** * Casts value in input to Neo4j Date bolt driver. If can't cast, it will throw an error * @param value * @returns Casted value to Neo4j Bolt Driver Date */ export function castToNeo4jDate(value: object) { if (isCastableToNeo4jDate(value)) { return new Neo4jDate(toNumber(value.year), toNumber(value.month), toNumber(value.day)); } throw new Error(`Invalid input for castToNeo4jDate: ${value}`); } /** * Creates a default selection config for a node-property based chart footer. */ export function getSelectionBasedOnFields(fields, oldSelection = {}, autoAssignSelectedProperties = true) { const selection = {}; fields.forEach((nodeLabelAndProperties) => { const label = nodeLabelAndProperties[0]; const properties = nodeLabelAndProperties.slice(1); let selectedProp = oldSelection[label] ? oldSelection[label] : undefined; if (autoAssignSelectedProperties) { DEFAULT_NODE_LABELS.forEach((prop) => { if (properties.indexOf(prop) !== -1) { if (selectedProp == undefined) { selectedProp = prop; } } }); selection[label] = selectedProp ? selectedProp : '(label)'; } else { selection[label] = selectedProp ? selectedProp : '(no label)'; } }); return selection; } export const DEFAULT_NODE_LABELS = ['name', 'title', 'label', 'id', 'uid', '(label)']; ================================================ FILE: src/chart/SettingsUtils.ts ================================================ import { getReportTypes } from '../extensions/ExtensionUtils'; import { useStyleRules } from '../extensions/styling/StyleRuleEvaluator'; import { extensionEnabled } from '../utils/ReportUtils'; /** * Gets the user specified settings and merges it with the defaults from ReportConfig.tsx. * @param userSettings the user specified settings for the report. * @param extensions the extensions enabled for the dashboard. * @param getGlobalParameter a callback to get global parameters for the dashboard. * @returns a merged list of user settings and defaults as provided in the configuration. */ export const getSettings = ( userSettings: Record | undefined, extensions: Record | undefined, getGlobalParameter: any ) => { const settings: Record = {}; const config: Record = getReportTypes(extensions).graph.settings; if (userSettings == undefined) { return {}; } Object.keys(config).map((key) => { settings[key] = userSettings[key] !== undefined ? userSettings[key] : config[key].default; }); settings.styleRules = useStyleRules( extensionEnabled(extensions, 'styling'), userSettings && userSettings.styleRules, getGlobalParameter ); settings.actionsRules = extensionEnabled(extensions, 'actions') && settings && userSettings.actionsRules ? userSettings.actionsRules : []; return settings; }; ================================================ FILE: src/chart/Utils.ts ================================================ import { tokens } from '@neo4j-ndl/base'; import { QueryResult, Record as Neo4jRecord } from 'neo4j-driver'; export function recordToNative(input: any): any { if (!input && input !== false) { return null; } else if (typeof input.keys === 'object' && typeof input.get === 'function') { return Object.fromEntries(input.keys.map((key) => [key, recordToNative(input.get(key))])); } else if (typeof input.toNumber === 'function') { return input.toNumber(); } else if (Array.isArray(input)) { return (input as Array).map((item) => recordToNative(item)); } else if (typeof input === 'object') { const converted = Object.entries(input).map(([key, value]) => [key, recordToNative(value)]); return Object.fromEntries(converted); } return input; } export function resultToNative(result: QueryResult): Record { if (!result) { return {}; } return result.records.map((row) => recordToNative(row)); } export function checkResultKeys(first: Neo4jRecord, keys: string[]) { const missing = keys.filter((key) => !first.keys.includes(key)); if (missing.length > 0) { return new Error( `The query is missing the following key${missing.length > 1 ? 's' : ''}: ${missing.join( ', ' )}. The expected keys are: ${keys.join(', ')}` ); } return false; } /** * For hierarchical data structures, recursively search for a key property that must have a given value. * If none can be found, return null. */ export const search = (tree, value, key = 'id', reverse = false) => { if (tree.length == 0) { return null; } const stack = Array.isArray(tree) ? [...tree] : [tree]; while (stack.length) { const node = stack[reverse ? 'pop' : 'shift'](); if (node[key] && node[key] === value) { return node; } if (node.children) { stack.push(...node.children); } } return null; }; /** * For hierarchical data, we remove all intermediate node prefixes generate by `processHierarchyFromRecords`. * This ensures that the visualization itself shows the 'real' names, and not the intermediate ones. */ export const mutateName = (currentNode) => { if (currentNode.name) { const s = currentNode.name.split('_'); currentNode.name = s.length > 0 ? s.slice(1).join('_') : s[0]; } if (currentNode.children) { currentNode.children.forEach((n) => mutateName(n)); } }; export const findObject = (data, name) => data.find((searchedName) => searchedName.name === name); export const flatten = (data) => data.reduce((acc, item) => { if (item.children) { return [...acc, item, ...flatten(item.children)]; } return [...acc, item]; }, []); export const rgbaToHex = (color: string): string => { let rgba; if (/^rgb/.test(color)) { rgba = color.replace(/^rgba?\(|\s+|\)$/g, '').split(','); } else { rgba = color.split(','); } if (rgba) { // rgb to hex // eslint-disable-next-line no-bitwise let hex = `#${((1 << 24) + (parseInt(rgba[0], 10) << 16) + (parseInt(rgba[1], 10) << 8) + parseInt(rgba[2], 10)) .toString(16) .slice(1)}`; // added alpha param if exists if (rgba[4]) { const alpha = Math.round(0o1 * 255); const hexAlpha = (alpha + 0x10000).toString(16).substr(-2).toUpperCase(); hex += hexAlpha; } return hex; } return color; }; export enum EntityType { Node, Relationship, } export const themeNivo = { textColor: 'rgb(var(--palette-neutral-text-default))', text: { fill: 'rgb(var(--palette-neutral-text-default))' }, axis: { ticks: { text: { fill: 'rgb(var(--palette-neutral-text-default))' } }, legend: { text: { fill: 'rgb(var(--palette-neutral-text-default))' } }, }, legends: { text: { fill: 'rgb(var(--palette-neutral-text-default))' }, title: { text: { fill: 'rgb(var(--palette-neutral-text-default))' } }, ticks: { text: { fill: 'rgb(var(--palette-neutral-text-default))' } }, hidden: { text: { fill: 'rgb(var(--palette-neutral-text-default))' } }, }, markers: { text: { fill: 'rgb(var(--palette-neutral-text-default))' }, }, labels: { text: { fill: 'rgb(var(--palette-neutral-text-default))' }, }, annotations: { text: { fill: 'rgb(var(--palette-neutral-text-default))' }, }, tooltip: { container: { fill: 'rgb(var(--palette-neutral-text-default))', background: 'rgb(var(--palette-neutral-bg-strong))', }, }, }; export const themeNivoCanvas = (theme) => { let baseDefault = theme === 'light' ? tokens.palette.light.neutral.text.default : tokens.palette.dark.neutral.text.default; let baseWeak = theme === 'light' ? tokens.palette.light.neutral.text.weak : tokens.palette.dark.neutral.text.weak; return { // textColor: 'rgb(var(--palette-neutral-text-default))', text: { fill: baseDefault }, axis: { ticks: { text: { fill: baseDefault } }, legend: { text: { fill: baseDefault } }, }, legends: { text: { fill: baseWeak }, title: { text: { fill: baseWeak } }, ticks: { text: { fill: baseWeak } }, hidden: { text: { fill: baseWeak } }, }, markers: { text: { fill: baseDefault }, }, labels: { text: { fill: baseDefault }, }, annotations: { text: { fill: baseDefault }, }, tooltip: { container: { fill: 'rgb(var(--palette-neutral-text-default))', background: 'rgb(var(--palette-neutral-bg-strong))', }, }, }; }; ================================================ FILE: src/chart/bar/BarChart.tsx ================================================ import { ResponsiveBar, ResponsiveBarCanvas } from '@nivo/bar'; import React, { useEffect } from 'react'; import { NoDrawableDataErrorMessage } from '../../component/editor/CodeViewerComponent'; import { getD3ColorsByScheme } from '../../config/ColorConfig'; import { evaluateRulesOnDict, useStyleRules } from '../../extensions/styling/StyleRuleEvaluator'; import { ChartProps } from '../Chart'; import { convertRecordObjectToString, recordToNative } from '../ChartUtils'; import { themeNivo, themeNivoCanvas } from '../Utils'; import { extensionEnabled } from '../../utils/ReportUtils'; import { getPageNumbersAndNamesList, getRule, performActionOnElement } from '../../extensions/advancedcharts/Utils'; import { getOriginalRecordForNivoClickEvent } from './util'; const NeoBarChart = (props: ChartProps) => { const { records, selection } = props; const [keys, setKeys] = React.useState([]); const [data, setData] = React.useState[]>([]); const settings = props.settings ? props.settings : {}; const marginRight = settings.marginRight ? settings.marginRight : 24; const marginLeft = settings.marginLeft ? settings.marginLeft : 50; const customDimensions = settings.customDimensions ? settings.customDimensions : false; const legendWidth = settings.legendWidth ? settings.legendWidth : 128; const marginTop = settings.marginTop ? settings.marginTop : 24; const marginBottom = settings.marginBottom ? settings.marginBottom : 30; const legend = settings.legend ? settings.legend : false; const labelRotation = settings.labelRotation != undefined ? settings.labelRotation : 45; const barWidth = settings.barWidth ? settings.barWidth : 10; const padding = settings.padding ? settings.padding : 0.25; const innerPadding = settings.innerPadding ? settings.innerPadding : 0; const expandHeightForLegend = settings.expandHeightForLegend ? settings.expandHeightForLegend : false; const displayYAxis = settings.displayYAxis ?? true; const displayYGridLines = settings.displayYGridLines ?? true; const actionsRules = extensionEnabled(props.extensions, 'actions') && props.settings && props.settings.actionsRules ? props.settings.actionsRules : []; const pageNames = getPageNumbersAndNamesList(); const legendPosition = settings.legendPosition ? settings.legendPosition : 'Vertical'; const labelSkipWidth = settings.labelSkipWidth ? settings.labelSkipWidth : 0; const labelSkipHeight = settings.labelSkipHeight ? settings.labelSkipHeight : 0; const enableLabel = settings.barValues ? settings.barValues : false; const positionLabel = settings.positionLabel ? settings.positionLabel : 'off'; // TODO: we should make all these defaults be loaded from the config file. const layout = settings.layout ? settings.layout : 'vertical'; const colorScheme = settings.colors ? settings.colors : 'set2'; const groupMode = settings.groupMode ? settings.groupMode : 'stacked'; const valueScale = settings.valueScale ? settings.valueScale : 'linear'; const minValue = settings.minValue ? settings.minValue : 'auto'; const maxValue = settings.maxValue ? settings.maxValue : 'auto'; const styleRules = useStyleRules( extensionEnabled(props.extensions, 'styling'), settings.styleRules, props.getGlobalParameter ); // For adaptable item length in the legend // Populates data with record information useEffect(() => { let newKeys = {}; let newData: Record[] = records .reduce((data: Record[], row: Record) => { try { if (!selection || !selection.index || !selection.value) { return data; } const index = convertRecordObjectToString(row.get(selection.index)); const idx = data.findIndex((item) => item.index === index); const key = selection.key !== '(none)' ? recordToNative(row.get(selection.key)) : selection.value; const rawValue = recordToNative(row.get(selection.value)); const value = rawValue !== null ? rawValue : 0.0000001; if (isNaN(value)) { return data; } newKeys[key] = true; if (idx > -1) { data[idx][key] = value; } else { data.push({ index, [key]: value }); } return data; } catch (e) { // eslint-disable-next-line no-console console.error(e); return []; } }, []) .map((row) => { Object.keys(newKeys).forEach((key) => { // eslint-disable-next-line no-prototype-builtins if (!row.hasOwnProperty(key)) { row[key] = 0; } }); return row; }); setKeys(Object.keys(newKeys)); setData(newData); }, [selection]); if (!selection || props.records == null || props.records.length == 0 || props.records[0].keys == null) { return ; } // Function to calculate the conditional margin bottom function calculateMarginBottom(legendPosition, showLegend, legendWidth, marginBottom) { // Check if legendPosition is 'Horizontal' if (legendPosition === 'Horizontal') { // Calculate margin based on whether the legend is shown return showLegend ? legendWidth * 0.3 + marginBottom + 50 : legendWidth * 0.3 + marginBottom; } // Return the default marginBottom if legendPosition is not 'Horizontal' return marginBottom; } // Using the function in your code const conditionalMarginBottom = calculateMarginBottom(legendPosition, settings.legend, legendWidth, marginBottom); // Function to call from BarComponent. Conducts necessary logic for Report Action. const handleBarClick = (e) => { // Get the original record that was used to draw this bar (or a group in a bar). const record = getOriginalRecordForNivoClickEvent(e, records, selection); // If there's a record, check if there are any rules assigned to each of the fields (columns). if (record) { Object.keys(record).forEach((key) => { let rules = getRule({ field: key, value: record[key] }, actionsRules, 'Click'); // If there is a rule assigned, run the rule with the specified field and value retrieved from the record. rules?.forEach((rule) => { const ruleField = rule.field; const ruleValue = record[rule.value]; performActionOnElement( { field: ruleField, value: ruleValue }, actionsRules, { ...props, pageNames: pageNames }, 'Click', 'bar' ); }); }); } }; // Function to calculate the right margin function calculateRightMargin(legendPosition, legend, legendWidth, marginRight) { if (legendPosition === 'Vertical') { return legend ? legendWidth + marginRight : marginRight; } return marginRight; } // Original margin function, refactored const margin = () => { return { top: marginTop, right: calculateRightMargin(legendPosition, legend, legendWidth, marginRight), bottom: conditionalMarginBottom, left: marginLeft, }; }; const chartColorsByScheme = getD3ColorsByScheme(colorScheme); // Compute bar color based on rules - overrides default color scheme completely. const getBarColor = (bar) => { let { index: colorIndex } = bar; if (colorIndex >= chartColorsByScheme.length) { colorIndex %= chartColorsByScheme.length; } if (!props.selection) { return chartColorsByScheme[colorIndex]; } const dict = {}; dict[selection.index] = bar.indexValue; dict[selection.value] = bar.value; dict[selection.key] = bar.id; const validRuleIndex = evaluateRulesOnDict(dict, styleRules, ['bar color']); if (validRuleIndex !== -1) { return styleRules[validRuleIndex].customizationValue; } return chartColorsByScheme[colorIndex]; }; function calculateLabelPosition(bar, positionLabel, layout) { let x = bar.width ? bar.width / 2 : 0; let y = bar.height ? bar.height / 2 : 0; if (positionLabel === 'top') { if (layout === 'vertical') { y = -10; } else { x = bar.width + 10; } } else if (positionLabel === 'bottom') { if (layout === 'vertical') { y = bar.height + 10; } else { x = -10; } } return { x, y }; } // Used instead of BarChartComponent when Position Label !== 'off' const BarComponent = ({ bar, borderColor, onClick }) => { let shade = false; let darkTop = false; let includeIndex = false; let textAnchor = 'middle'; const { x, y } = calculateLabelPosition(bar, positionLabel, layout); return ( onClick(bar.data, event)} style={{ cursor: 'pointer' }} > {shade ? : <>} {darkTop ? ( ) : ( <> )} {includeIndex ? ( {bar.data.indexValue} ) : ( <> )} {enableLabel ? ( {bar.data.value} ) : ( <> )} ); }; // Fixing canvas bug, from https://github.com/plouc/nivo/issues/2162 // SVGGraphicsElement.getBBox HTMLCanvasElement.prototype.getBBox = function tooltipMapper() { return { width: this.offsetWidth, height: this.offsetHeight }; }; const extraProperties = positionLabel !== 'off' ? { barComponent: BarComponent } : {}; const canvas = data.length > 30; const BarChartComponent = canvas ? ResponsiveBarCanvas : ResponsiveBar; // Creates enough width to ensure chart doesn't get cut off const adaptableWidth = marginLeft + marginRight + data.length * barWidth * 4 + (data.length - 1) * 4 + (data.length - 1) * innerPadding * 4; // Legend F const calculateLegendConfig = () => { if (!legend) { return []; // No legend required } if (legendPosition === 'Horizontal') { return [ { dataFrom: 'keys', anchor: 'bottom', direction: 'row', justify: false, translateX: 0, translateY: legendWidth, itemsSpacing: 2, itemWidth: legendWidth, itemHeight: 20, itemDirection: 'left-to-right', itemOpacity: 0.85, symbolSize: 20, effects: [ { on: 'hover', style: { itemOpacity: 1, }, }, ], }, ]; } // Vertical legend return [ { dataFrom: 'keys', anchor: 'bottom-right', direction: 'column', justify: false, translateX: legendWidth + 10, translateY: 0, itemsSpacing: 1, itemWidth: legendWidth, itemHeight: 20, itemDirection: 'left-to-right', itemOpacity: 0.85, symbolSize: 15, effects: [ { on: 'hover', style: { itemOpacity: 1, }, }, ], }, ]; }; // Height of each legend item const itemHeight = 24.5; // Function to handle width logic, including scrollbar logic function calculateWidth(customDimensions, legendPosition, adaptableWidth, legendWidth, data, barWidth) { if (!customDimensions) { return '100%'; } if (legendPosition === 'Horizontal') { const horizontalLegendWidth = legendWidth * data.length + 200; return adaptableWidth > horizontalLegendWidth ? adaptableWidth : horizontalLegendWidth; } return barWidth * 5 * data.length + legendWidth; } // Container to make the chart scroll horizontally const scrollableWrapperStyle: React.CSSProperties = { width: calculateWidth(customDimensions, legendPosition, adaptableWidth, legendWidth, data, barWidth), height: expandHeightForLegend ? itemHeight * data.length + conditionalMarginBottom : '100%', whiteSpace: 'nowrap', }; // Container for scrolling container to scroll in const barChartStyle: React.CSSProperties = customDimensions ? { width: '100%', overflowX: 'auto', overflowY: 'auto', height: '100%', } : { width: '100%', height: '100%', overflowY: 'auto', }; const chart = (
); return chart; }; export default NeoBarChart; ================================================ FILE: src/chart/bar/util.ts ================================================ /** * Utility function to reverse engineer, from an event on a Nivo bar chart, what the original Neo4j record was the data came from. * Once we have this record, we can pass it to the action rule handler, so that users can define report actions on any variable * in their return statement. * @param e the click event on the bar chart. * @param records the Neo4j records used to build the visualization * @param selection the selection made by the user (category, index, group*) - where group is optional. * @returns */ export function getOriginalRecordForNivoClickEvent(e, records, selection) { // TODO - rewrite this to be more optimal (using list comprehensions, etc.) const usesGroups = Object.keys(e.data).length > 2; const group = e.id; const { value } = e; const category = e.indexValue; // Go through all records and find the first record `r` where the event's values match exactly. for (const i in records) { const r = records[i]; const categoryIndex = r._fieldLookup[selection.index]; const groupIndex = r._fieldLookup[selection.key]; const valueIndex = r._fieldLookup[selection.value]; const recordCategory = r._fields[categoryIndex]; const recordGroup = r._fields[groupIndex]; const recordValue = r._fields[valueIndex]; if (usesGroups) { if (recordCategory == category && recordGroup == group && recordValue == value) { const dict = {}; for (const i in Object.keys(r._fieldLookup)) { const key = Object.keys(r._fieldLookup)[i]; dict[key] = r._fields[r._fieldLookup[key]]; } return dict; } } else if (recordCategory == category && recordValue == value) { const dict = {}; for (const i in Object.keys(r._fieldLookup)) { const key = Object.keys(r._fieldLookup)[i]; dict[key] = r._fields[r._fieldLookup[key]]; } return dict; } } } ================================================ FILE: src/chart/graph/GraphChart.tsx ================================================ import React, { useEffect, useState } from 'react'; import useDimensions from 'react-cool-dimensions'; import { ChartProps } from '../Chart'; import { NeoGraphChartInspectModal } from './component/GraphChartInspectModal'; import { NeoGraphChartVisualization2D } from './GraphChartVisualization2D'; import { NeoGraphChartDeepLinkButton } from './component/button/GraphChartDeepLinkButton'; import { NeoGraphChartCanvas } from './component/GraphChartCanvas'; import { NeoGraphChartLockButton } from './component/button/GraphChartLockButton'; import { NeoGraphChartFitViewButton } from './component/button/GraphChartFitViewButton'; import { buildGraphVisualizationObjectFromRecords } from './util/RecordUtils'; import { parseNodeIconConfig } from './util/NodeUtils'; import { GraphChartVisualizationProps, Link, layouts } from './GraphChartVisualization'; import { handleExpand } from './util/ExplorationUtils'; import { categoricalColorSchemes } from '../../config/ColorConfig'; import { IconButtonArray, IconButton } from '@neo4j-ndl/react'; import { Tooltip } from '@mui/material'; import { downloadCSV } from '../ChartUtils'; import { generateSafeColumnKey } from '../table/TableChart'; import { GraphChartContextMenu } from './component/GraphChartContextMenu'; import { getSettings } from '../SettingsUtils'; import { getPageNumbersAndNamesList } from '../../extensions/advancedcharts/Utils'; import { CloudArrowDownIconOutline } from '@neo4j-ndl/react/icons'; export interface GraphChartProps extends ChartProps { lockable?: boolean; component?: any; } const DEFAULT_VISUALIZATION_COMPONENT = NeoGraphChartVisualization2D; /** * Draws graph data using a force-directed-graph visualization. * This visualization is powered by `react-force-graph`. * See https://github.com/vasturiano/react-force-graph for examples on customization. */ const NeoGraphChart = (props: GraphChartProps) => { if (props.records == null || props.records.length == 0 || props.records[0].keys == null) { return <>No data, re-run the report.; } const Visualization = props.component ? props.component : DEFAULT_VISUALIZATION_COMPONENT; // Retrieve config from advanced settings const settings = getSettings(props.settings, props.extensions, props.getGlobalParameter); const lockable = props.lockable !== undefined ? props.lockable : true; const linkDirectionalParticles = props.settings && props.settings.relationshipParticles ? 5 : undefined; const arrowLengthProp = props?.settings?.arrowLengthProp ?? 3; let nodePositions = props.settings && props.settings.nodePositions ? props.settings.nodePositions : {}; const parameters = props.parameters ? props.parameters : {}; const setNodePositions = (positions) => props.updateReportSetting && props.updateReportSetting('nodePositions', positions); const handleEntityClick = (item) => { setSelectedEntity(item); setContextMenuOpen(false); if (item !== undefined && settings.showPropertiesOnClick) { setInspectModalOpen(true); } }; const handleEntityRightClick = (item, event) => { setSelectedEntity(item); setContextMenuOpen(true); setClickPosition({ x: event.clientX, y: event.clientY, }); }; const frozen: boolean = props.settings && props.settings.frozen !== undefined ? props.settings.frozen : false; const [inspectModalOpen, setInspectModalOpen] = useState(false); const [selectedEntity, setSelectedEntity] = useState(undefined); const [contextMenuOpen, setContextMenuOpen] = useState(false); const [clickPosition, setClickPosition] = useState({ x: 0, y: 0 }); const [recenterAfterEngineStop, setRecenterAfterEngineStop] = useState(true); const [cooldownTicks, setCooldownTicks] = useState(100); let [nodeLabels, setNodeLabels] = useState({}); let [linkTypes, setLinkTypes] = useState({}); const [data, setData] = useState({ nodes: [] as Node[], links: [] as Link[] }); const setLayoutFrozen = (value) => { if (value == false) { setCooldownTicks(100); setRecenterAfterEngineStop(true); setNodePositions({}); } props.updateReportSetting && props.updateReportSetting('frozen', value); }; const setGraph = (nodes, links) => { setData({ nodes: nodes, links: links }); }; const setNodes = (nodes) => { setData({ nodes: nodes, links: data.links }); }; const setLinks = (links) => { setData({ nodes: data.nodes, links: links }); }; let icons = parseNodeIconConfig(settings.iconStyle); const colorScheme = categoricalColorSchemes[settings.nodeColorScheme]; const { theme } = props; const generateVisualizationDataGraph = (records, _) => { let nodes: Record[] = []; let links: Record[] = []; const extractedGraphFromRecords = buildGraphVisualizationObjectFromRecords( records, nodes, links, nodeLabels, linkTypes, colorScheme, props.fields, settings.nodeColorProp, settings.defaultNodeColor, settings.nodeSizeProp, settings.defaultNodeSize, settings.relWidthProp, settings.defaultRelWidth, settings.relColorProp, settings.defaultRelColor, settings.styleRules, nodePositions, frozen ); setData(extractedGraphFromRecords); }; const { observe, width, height } = useDimensions({ onResize: ({ observe, unobserve }) => { unobserve(); // To stop observing the current target element observe(); // To re-start observing the current target element }, }); const pageNames = getPageNumbersAndNamesList(); const chartProps: GraphChartVisualizationProps = { data: { nodes: data.nodes, nodeLabels: nodeLabels, links: data.links, linkTypes: linkTypes, parameters: parameters, setGraph: setGraph, setNodes: setNodes, setLinks: setLinks, setNodeLabels: setNodeLabels, setLinkTypes: setLinkTypes, }, style: { width: width, height: height, backgroundColor: theme == 'dark' && settings.backgroundColor == '#fafafa' ? '#040404' : settings.backgroundColor, // Temporary fix for default color adjustment in dark mode linkDirectionalParticles: linkDirectionalParticles, linkDirectionalArrowLength: arrowLengthProp, linkDirectionalParticleSpeed: settings.linkDirectionalParticleSpeed, nodeLabelFontSize: settings.nodeLabelFontSize, nodeLabelColor: theme == 'dark' && settings.nodeLabelColor == 'black' ? 'white' : settings.nodeLabelColor, // Temporary fix for default color adjustment in dark mode relLabelFontSize: settings.relLabelFontSize, relLabelColor: settings.relLabelColor, defaultNodeSize: settings.defaultNodeSize, nodeIcons: icons, colorScheme: colorScheme, nodeColorProp: settings.nodeColorProp, defaultNodeColor: settings.defaultNodeColor, nodeSizeProp: settings.nodeSizeProp, relWidthProp: settings.relWidthProp, defaultRelWidth: settings.defaultRelWidth, relColorProp: settings.relColorProp, defaultRelColor: settings.defaultRelColor, theme: theme, }, engine: { layout: layouts[settings.layout], graphDepthSep: settings.graphDepthSep, queryCallback: props.queryCallback, cooldownTicks: cooldownTicks, setCooldownTicks: setCooldownTicks, selection: props.selection, setSelection: () => { throw 'NotImplemented'; }, fields: props.fields, setFields: props.setFields, recenterAfterEngineStop: recenterAfterEngineStop, setRecenterAfterEngineStop: setRecenterAfterEngineStop, }, interactivity: { enableExploration: settings.enableExploration, enableEditing: settings.enableEditing, layoutFrozen: frozen, setLayoutFrozen: setLayoutFrozen, nodePositions: nodePositions, setNodePositions: setNodePositions, showPropertiesOnHover: settings.showPropertiesOnHover, showPropertiesOnClick: settings.showPropertiesOnClick, showPropertyInspector: inspectModalOpen, setPropertyInspectorOpen: setInspectModalOpen, fixNodeAfterDrag: settings.fixNodeAfterDrag, handleExpand: handleExpand, setGlobalParameter: props.setGlobalParameter, setPageNumber: props.setPageNumber, pageNames: pageNames, onNodeClick: (item) => handleEntityClick(item), onNodeRightClick: (item, event) => handleEntityRightClick(item, event), onRelationshipClick: (item) => handleEntityClick(item), onRelationshipRightClick: (item, event) => handleEntityRightClick(item, event), drilldownLink: settings.drilldownLink, selectedEntity: selectedEntity, setSelectedEntity: setSelectedEntity, contextMenuOpen: contextMenuOpen, setContextMenuOpen: setContextMenuOpen, clickPosition: clickPosition, setClickPosition: setClickPosition, createNotification: props.createNotification, }, extensions: { styleRules: settings.styleRules, actionsRules: settings.actionsRules, }, }; // When data is refreshed, rebuild the visualization data. useEffect(() => { generateVisualizationDataGraph(props.records, chartProps); }, [props.records]); return (
{lockable && settings.lockable ? : <>} {settings.drilldownLink ? : <>} {settings.allowDownload && props.records && props.records.length > 0 ? ( { const rows = props.records.map((record, rownumber) => { return Object.assign( { id: rownumber }, ...record._fields.map((field, i) => ({ [generateSafeColumnKey(record.keys[i])]: field })) ); }); downloadCSV(rows); }} /> ) : ( <> )}
); }; export default NeoGraphChart; ================================================ FILE: src/chart/graph/GraphChartVisualization.ts ================================================ /** * A mapping between human-readable layout names, and the ones used by the library. */ export const layouts = { 'force-directed': undefined, 'tree-top-down': 'td', 'tree-bottom-up': 'bu', 'tree-left-right': 'lr', 'tree-right-left': 'rl', radial: 'radialout', tree: 'td', }; type Layout = 'td' | 'bu' | 'lr' | 'rl' | 'radialout' | 'radialin'; export const defaultNodeColor = 'lightgrey'; // Color of nodes without labels /** * A node or relationship as selected in the graph. */ export interface GraphEntity { properties: any; id: string; } export interface Node extends GraphEntity { labels: string[]; mainLabel: string; x?: number; y?: number; fx?: number; fy?: number; } export interface Link extends GraphEntity { type: string; width?: number; source?: Node; target?: Node; color?: string; } /** * The set of properties a graph visualization component (and its peripheral components) expects. * objects implementing this interface are passed around the different utility functions for the graph visualization. * TODO: Split the `GraphChartVisualizationProps` into sub-interfaces that can be passed down individually. */ export interface GraphChartVisualizationProps { /** * entries in 'data' are related to anything relevant for rendering the graph. * These are the nodes/relationships, but also their labels and types. * The data dictionary can be updated by calling any of the functions in the data entry. */ data: { nodes: Node[]; nodeLabels: Record; links: Link[]; linkTypes: Record; parameters: Record; setGraph: (nodes, links) => void; setNodes: (nodes) => void; setLinks: (links) => void; setNodeLabels: (labels) => void; setLinkTypes: (types) => void; }; /** * The properties relevant for styling the graph. * Style is applied at the moment the data dictionary is generated. */ style: { width: number; height: number; backgroundColor: any; linkDirectionalParticles?: number; linkDirectionalParticleSpeed: number; linkDirectionalArrowLength: number; nodeLabelFontSize: number; nodeLabelColor: string; relLabelFontSize: number; relLabelColor: string; defaultNodeSize: number; nodeIcons: Record; colorScheme: Record; nodeColorProp: string; defaultNodeColor: string; nodeSizeProp: string; relWidthProp: string; defaultRelWidth: number; relColorProp: string; defaultRelColor: string; theme?: string; }; /** * The keys in 'engine' are related to the graph rendering engine (force-directed layout) or the NeoDash query engine. */ engine: { layout: Layout; graphDepthSep: number; queryCallback: (query: string, parameters: Record, setRecords: any) => void; cooldownTicks: number; setCooldownTicks: (ticks: number) => void; selection: Record | undefined; setSelection: (selection: Record) => void; fields: any; setFields: ((fields: any) => void) | undefined; recenterAfterEngineStop: boolean; setRecenterAfterEngineStop: (value: boolean) => void; }; /** * The entries in 'interactivity' handle the interactive elements of the visualization. * This includes handling click events, showing pop-ups, and more. * TODO: Split up interactivity user-settings and interactivity callbacks/functional variables. */ interactivity: { enableExploration: boolean; enableEditing: boolean; layoutFrozen: boolean; setLayoutFrozen: React.Dispatch>; nodePositions: Record; setNodePositions: (positions: any[]) => void; showPropertiesOnHover: boolean; showPropertiesOnClick: boolean; showPropertyInspector: boolean; setPropertyInspectorOpen: React.Dispatch>; createNotification: ((title: string, message: string) => void) | undefined; fixNodeAfterDrag: boolean; onNodeClick: (node) => void; onNodeRightClick: (node, event) => void; onRelationshipClick: (rel) => void; onRelationshipRightClick: (rel, event) => void; setGlobalParameter?: (name: string, value: string) => void; handleExpand: (id, type, dir, properties) => void; zoomToFit: () => void; drilldownLink: string; selectedEntity?: GraphEntity; setSelectedEntity: (entity) => void; contextMenuOpen: boolean; setContextMenuOpen: (boolean) => void; clickPosition: Record; setClickPosition: (pos) => void; setPageNumber: any; pageNames: []; customTablePropertiesOfModal: any[]; pageIdAndParameterName: string; }; /** * A set of configuration parameters used for the visualization engine. */ config?: { graphComponent: any; cooldownAfterengineStop: number; nodeCanvasObjectMode?: () => void; nodeCanvasObject?: (node: any, ctx: any) => void; linkCanvasObjectMode?: () => void; linkCanvasObject?: (link: any, ctx: any) => void; nodeThreeObjectExtend?: boolean; nodeThreeObject?: (node) => void; linkThreeObjectExtend?: boolean; linkThreeObject?: (link) => void; linkPositionUpdate?: (sprite: any, { start, end }: { start: any; end: any }, link: any, ref: any) => void; }; /** * entries in 'extensions' let users plug in extra functionality into the visualization based on enabled plugins. */ extensions: { styleRules: any[]; actionsRules: any[]; }; } ================================================ FILE: src/chart/graph/GraphChartVisualization2D.tsx ================================================ import React, { useRef } from 'react'; import ForceGraph2D from 'react-force-graph-2d'; import { executeActionRule, getRuleWithFieldPropertyName } from '../../extensions/advancedcharts/Utils'; import { getTooltip } from './component/GraphChartTooltip'; import { GraphChartVisualizationProps } from './GraphChartVisualization'; import { generateNodeCanvasObject } from './util/NodeUtils'; import { generateRelCanvasObject } from './util/RelUtils'; import { NeoGraphChartVisualizationBase } from './GraphChartVisualizationBase'; /* * */ export const NeoGraphChartVisualization2D = (props: GraphChartVisualizationProps) => { const config2d = { graphComponent: ForceGraph2D, cooldownAfterengineStop: 0, nodeCanvasObjectMode: () => 'after', nodeCanvasObject: (node: any, ctx: any) => generateNodeCanvasObject( node, ctx, props.style.nodeIcons, props.interactivity.layoutFrozen, props.interactivity.nodePositions, props.style.nodeLabelFontSize, props.style.defaultNodeSize, props.style.nodeLabelColor, props.extensions.styleRules, props.engine.selection ), linkCanvasObjectMode: () => 'after', linkCanvasObject: (link: any, ctx: any) => generateRelCanvasObject(link, ctx, props.style.relLabelFontSize, props.style.relLabelColor), }; const props2d = { ...props, config: config2d }; return ; }; ================================================ FILE: src/chart/graph/GraphChartVisualizationBase.tsx ================================================ import React, { useRef } from 'react'; import { executeActionRule, getRuleWithFieldPropertyName } from '../../extensions/advancedcharts/Utils'; import { getTooltip } from './component/GraphChartTooltip'; import { GraphChartVisualizationProps } from './GraphChartVisualization'; export const NeoGraphChartVisualizationBase = (props: GraphChartVisualizationProps) => { const fgRef: React.MutableRefObject = useRef(); const GraphComponent = props.config?.graphComponent; if (!props.style.width || !props.style.height) { return <>; } props.interactivity.zoomToFit = () => fgRef.current && fgRef.current.zoomToFit(400); return ( link.width} linkLabel={(link: any) => (props.interactivity.showPropertiesOnHover ? `
${getTooltip(link)}
` : '')} nodeLabel={(node: any) => (props.interactivity.showPropertiesOnHover ? `
${getTooltip(node)}
` : '')} nodeVal={(node: any) => node.size} onNodeClick={(item) => { let rules = getRuleWithFieldPropertyName(item, props.extensions.actionsRules, 'onNodeClick', 'labels'); rules != null ? rules.forEach((rule) => executeActionRule(rule, item, { ...props.interactivity })) : props.interactivity.onNodeClick(item); }} onLinkClick={(item) => { let rules = getRuleWithFieldPropertyName(item, props.extensions.actionsRules, 'onLinkClick', 'type'); rules != null ? rules.forEach((rule) => executeActionRule(rule, item, props.interactivity.setGlobalParameter)) : props.interactivity.onRelationshipClick(item); }} onNodeRightClick={(node, event) => props.interactivity.onNodeRightClick(node, event)} onLinkRightClick={(link, event) => props.interactivity.onRelationshipRightClick(link, event)} onBackgroundClick={() => props.interactivity.onNodeClick(undefined)} onBackgroundRightClick={() => props.interactivity.onNodeClick(undefined)} linkLineDash={(link) => (link.new ? [2, 1] : null)} linkDirectionalParticles={props.style.linkDirectionalParticles} linkDirectionalParticleSpeed={props.style.linkDirectionalParticleSpeed} cooldownTicks={props.engine.cooldownTicks} onEngineStop={() => { props.engine.setCooldownTicks(props.config.cooldownAfterengineStop); if (props.engine.recenterAfterEngineStop) { fgRef.current.zoomToFit(400); props.engine.setRecenterAfterEngineStop(false); } }} onZoom={() => { props.interactivity.setContextMenuOpen(false); }} onNodeDrag={() => { props.interactivity.setContextMenuOpen(false); props.engine.setCooldownTicks(1); props.engine.setRecenterAfterEngineStop(false); }} onNodeDragEnd={(node) => { props.engine.setCooldownTicks(props.config.cooldownAfterengineStop); if (props.interactivity.fixNodeAfterDrag) { node.fx = node.x; node.fy = node.y; if (node.z !== undefined) { node.fz = node.z; } } // TODO - Frozen layout only works in 2D if (props.interactivity.layoutFrozen) { const key = node.id; const val = [node.x, node.y]; const old = { ...props.interactivity.nodePositions }; old[key] = val; props.interactivity.setNodePositions(old); } }} // 2D-specific config settings nodeCanvasObjectMode={props.config?.nodeCanvasObjectMode} nodeCanvasObject={props.config?.nodeCanvasObject} linkCanvasObjectMode={props.config?.linkCanvasObjectMode} linkCanvasObject={props.config?.linkCanvasObject} // 3D-specific config settings nodeThreeObjectExtend={props.config?.nodeThreeObjectExtend} nodeThreeObject={props.config?.nodeThreeObject} linkThreeObjectExtend={props.config?.linkThreeObjectExtend} linkThreeObject={props.config?.linkThreeObject} linkPositionUpdate={(sprite, { start, end }, link) => props.config?.linkPositionUpdate(sprite, { start, end }, link, fgRef) } // Data to populate graph graphData={props.style.width ? { nodes: props.data.nodes, links: props.data.links } : { nodes: [], links: [] }} /> ); }; ================================================ FILE: src/chart/graph/component/GraphChartCanvas.tsx ================================================ import React from 'react'; const canvasStyle = { paddingLeft: '10px', marginBottom: 5, position: 'relative', overflow: 'hidden', width: '100%', height: '100%', }; /** * Renders the canvas on which the graph visualization is projected. */ export const NeoGraphChartCanvas = ({ children }) => { return (
{children}
); }; ================================================ FILE: src/chart/graph/component/GraphChartContextMenu.tsx ================================================ import * as React from 'react'; import MenuItem from '@mui/material/MenuItem'; import { GraphChartVisualizationProps } from '../GraphChartVisualization'; import { Card, CardHeader } from '@mui/material'; import { IconButton } from '@neo4j-ndl/react'; import { MagnifyingGlassCircleIconOutline, PencilIconOutline, XMarkIconOutline } from '@neo4j-ndl/react/icons'; import { NestedMenuItem, IconMenuItem } from 'mui-nested-menu'; import { RenderNode, RenderNodeChip, RenderRelationshipChip } from '../../../report/ReportRecordProcessing'; import { getNodeLabel } from '../util/NodeUtils'; import { EditAction, EditType, GraphChartEditModal } from './GraphChartEditModal'; import { handleExpand, handleGetNodeRelTypes } from '../util/ExplorationUtils'; import { useEffect } from 'react'; import { mergeDatabaseStatCountsWithCountsInView } from '../util/ExplorationUtils'; import { createPortal } from 'react-dom'; /** * Renders the context menu that is present when a user right clicks on a node or relationship in the graph. * The context menu can be used to inspect and edit nodes/relationships, or explore the graph. */ export const GraphChartContextMenu = (props: GraphChartVisualizationProps) => { const [dialogOpen, setDialogOpen] = React.useState(false); const [editableEntity, setEditableEntity] = React.useState(undefined); const [editableEntityType, setEditableEntityType] = React.useState(EditType.Node); const [action, setAction] = React.useState(EditAction.Create); const [neighbourRelCounts, setNeighbourRelCounts] = React.useState([]); const handleClose = () => { props.interactivity.setContextMenuOpen(false); }; const dialogProps = { ...props, selectedNode: editableEntity, dialogOpen: dialogOpen, setDialogOpen: setDialogOpen }; const expandable = props.interactivity.selectedEntity && props.interactivity.selectedEntity.labels !== undefined; const [cachedNeighbours, setCachedNeighbours] = React.useState(false); // Clear neighbour cache when selection changes. useEffect(() => { setCachedNeighbours(false); }, [props.interactivity.selectedEntity]); const menu = (
} titleTypographyProps={{ variant: 'h6' }} title={ props.interactivity.selectedEntity ? expandable ? props.interactivity.selectedEntity.labels.join(', ') : props.interactivity.selectedEntity.type : '' } /> } label='Inspect' onClick={() => { props.interactivity.setContextMenuOpen(false); props.interactivity.setPropertyInspectorOpen(true); }} > {props.interactivity.enableEditing ? ( } label='Edit' onClick={() => { setEditableEntityType(expandable ? EditType.Node : EditType.Relationship); setAction(EditAction.Edit); props.interactivity.setContextMenuOpen(false); setDialogOpen(true); }} > ) : ( <> )} {props.interactivity.enableExploration && expandable ? ( { if (!cachedNeighbours) { setCachedNeighbours(true); const id = props.interactivity.selectedEntity?.id; // Virtual relationships do not have any neighbours if (id < 0) { setNeighbourRelCounts([]); return; } handleGetNodeRelTypes(id, props.engine, (records) => setNeighbourRelCounts(mergeDatabaseStatCountsWithCountsInView(id, records, props.data.links)) ); } }} >
{neighbourRelCounts.length == 0 ? ( ) : ( <> )} {neighbourRelCounts.length > 0 && neighbourRelCounts.map((item, index) => { const dir = item[1] == 'any' ? undefined : item[1] == 'out'; return ( { props.interactivity.setContextMenuOpen(false); handleExpand(props.interactivity.selectedEntity.id, item[0], item[1], props); setDialogOpen(false); setCachedNeighbours(false); }} > ); })}
No relationships...
{RenderNodeChip(props.interactivity.selectedEntity.labels, '#fff', '1px solid lightgrey')}   {RenderRelationshipChip(item[0], dir, '#dedede')}   {RenderNodeChip('...', '#fff', '1px solid lightgrey')} {item[2]}
) : ( <> )} {props.interactivity.enableEditing && expandable ? (
{props.data && props.data.nodes.map((node, index) => ( { setEditableEntityType(EditType.Relationship); setAction(EditAction.Create); setEditableEntity(node); props.interactivity.setContextMenuOpen(false); setDialogOpen(true); }} > ))}
{RenderNode(node, false)} {props.engine.selection[node.mainLabel] ? getNodeLabel(props.engine.selection, node) : ''}
) : ( <> )}
); return ( <> {props.interactivity.contextMenuOpen ? createPortal(menu, document.body) : <>} ); }; ================================================ FILE: src/chart/graph/component/GraphChartEditModal.tsx ================================================ import { GraphChartVisualizationProps } from '../GraphChartVisualization'; import React, { useEffect } from 'react'; import { Dialog, DialogContent, DialogContentText, DialogTitle } from '@mui/material'; import { Button } from '@mui/material'; import { TextField, Typography } from '@mui/material'; import { PlusIconOutline, XMarkIconOutline, PlayIconOutline } from '@neo4j-ndl/react/icons'; import { IconButton } from '@neo4j-ndl/react'; import { LabelTypeAutocomplete } from './autocomplete/LabelTypeAutocomplete'; import { DeletePropertyButton } from './button/modal/DeletePropertyButton'; import { handleNodeCreate, handleNodeDelete, handleNodeEdit, handleRelationshipCreate, handleRelationshipDelete, handleRelationshipEdit, } from '../util/EditUtils'; import { PropertyNameAutocomplete } from './autocomplete/PropertyNameAutocomplete'; export enum EditType { Node = 0, Relationship = 1, } export enum EditAction { Create = 0, Edit = 1, Delete = 2, } interface GraphChartEditorVisualizationProps extends GraphChartVisualizationProps { type: EditType; action: EditAction; selectedNode: any; dialogOpen: any; setDialogOpen: any; } /** * The edit modal is a pop-up window that lets users change a node or relationship in the graph. * This is a generic component, that can be used for creating/editing/deleting either nodes or relationships, and their properties. */ export const GraphChartEditModal = (props: GraphChartEditorVisualizationProps) => { const [properties, setProperties] = React.useState([{ name: '', value: '' }]); const [labelRecords, setLabelRecords] = React.useState([]); const [labelInputText, setLabelInputText] = React.useState(''); const [propertyRecords, setPropertyRecords] = React.useState([]); const [propertyInputTexts, setPropertyInputTexts] = React.useState([]); const [label, setLabel] = React.useState(undefined); // When the dialog gets opened, and we are editing, prepopulate the fields with current node/rel data in the database. useEffect(() => { if ( props.dialogOpen && props.interactivity.selectedEntity && props.type == EditType.Node && props.action == EditAction.Edit ) { const label = props.interactivity.selectedEntity.labels ? props.interactivity.selectedEntity.labels[0] : ''; setLabelInputText(label); setLabel(label); const selectedProps = Object.keys(props.interactivity.selectedEntity.properties).map((prop) => { return { name: prop, value: props.interactivity.selectedEntity.properties[prop] }; }); setProperties(selectedProps); setPropertyInputTexts(selectedProps.map((p) => p.name)); } else if ( props.dialogOpen && props.interactivity.selectedEntity && props.type == EditType.Relationship && props.action == EditAction.Edit ) { const { type } = props.interactivity.selectedEntity; setLabelInputText(type); setLabel(type); const selectedProps = Object.keys(props.interactivity.selectedEntity.properties).map((prop) => { return { name: prop, value: props.interactivity.selectedEntity.properties[prop] }; }); setProperties(selectedProps); setPropertyInputTexts(selectedProps.map((p) => p.name)); } else if (props.dialogOpen) { setLabelInputText(''); setLabel(''); } }, [props.dialogOpen]); return ( { props.setDialogOpen(false); }} aria-labelledby='form-dialog-title' > {props.action == EditAction.Create ? 'Create' : 'Edit'} a{' '} {props.type == EditType.Node ? 'Node' : 'Relationship'} { props.interactivity.setContextMenuOpen(false); props.setDialogOpen(false); setProperties([{ name: '', value: '' }]); }} style={{ marginLeft: '40px', padding: '3px', float: 'right' }} clean >

Properties

{properties.map((property, index) => { const disabled = !( typeof property.value == 'string' || typeof property.value == 'number' || property.value.toNumber !== undefined ); return ( ); })}
{index + 1}. { const newProperties = [...properties]; newProperties[index].value = e.target.value; setProperties(newProperties); }} > { setProperties([...properties.slice(0, index), ...properties.slice(index + 1)]); setPropertyInputTexts([ ...propertyInputTexts.slice(0, index), ...propertyInputTexts.slice(index + 1), ]); }} />
{ const newProperty = { name: '', value: '' }; setProperties(properties.concat(newProperty)); }} >
); }; ================================================ FILE: src/chart/graph/component/GraphChartInspectModal.tsx ================================================ import React from 'react'; import { GraphChartVisualizationProps } from '../GraphChartVisualization'; import { getEntityHeader } from '../util/NodeUtils'; import { Dialog } from '@neo4j-ndl/react'; import GraphEntityInspectionTable from './GraphEntityInspectionTable'; /** * Renders a pop-up window to inspect a node/relationship properties in a read-only table. */ export const NeoGraphChartInspectModal = (props: GraphChartVisualizationProps) => { return (
props.interactivity.setPropertyInspectorOpen(false)} aria-labelledby='form-dialog-title' > {props.interactivity.selectedEntity ? getEntityHeader(props.interactivity.selectedEntity, props?.engine?.selection) : ''}
); }; export default NeoGraphChartInspectModal; ================================================ FILE: src/chart/graph/component/GraphChartTooltip.tsx ================================================ import React from 'react'; import { Table, TableBody, TableCell, TableContainer, TableRow, Card } from '@mui/material'; import ReactDOMServer from 'react-dom/server'; import { GraphEntity, Link } from '../GraphChartVisualization'; import { Node } from '../GraphChartVisualization'; /** * Renders a tooltip above the user's cursor showing information on the selected node/relationship. */ export function getTooltip(entity: Node | Link) { const tooltip = ( {entity.labels ? (entity.labels.length > 0 ? entity.labels.join(', ') : 'Node') : entity.type} {Object.keys(entity.properties).length == 0 ? (
(No properties)
) : ( {Object.keys(entity.properties) .sort() .map((key) => ( {key} {entity.properties[key].toString().length <= 30 ? entity.properties[key].toString() : `${entity.properties[key].toString().substring(0, 40)}...`} ))}
)}
); return ReactDOMServer.renderToString(tooltip); } ================================================ FILE: src/chart/graph/component/GraphEntityInspectionTable.tsx ================================================ import React from 'react'; import ShowMoreText from 'react-show-more-text'; import { Checkbox, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; import { TextLink } from '@neo4j-ndl/react'; // import DOMPurify from 'dompurify'; export const formatProperty = (property) => { const str = property?.toString() || ''; if (str.startsWith('http://') || str.startsWith('https://')) { return ( {str} ); } return str; }; /** * Component to render node/relationship properties in a table format */ export const GraphEntityInspectionTable = ({ entity, theme, setSelectedParameters = (_value) => { console.log('undefined function in GraphEntityInspectionTable'); }, checklistEnabled = false, }) => { const [checkedParameters, setCheckedParameters] = React.useState([]); const hasPropertyToShow = Object.keys(entity.properties).length > 0; if (!entity) { return <>; } /** * Function to manage the click * @param parameter * @param checked */ function handleCheckboxClick(parameter, checked) { let newCheckedParameters = [...checkedParameters]; if (checked) { newCheckedParameters.push(parameter); } else { const index = newCheckedParameters.indexOf(parameter); if (index > -1) { newCheckedParameters.splice(index, 1); } } if (setSelectedParameters) { setCheckedParameters(newCheckedParameters); setSelectedParameters(newCheckedParameters); } } const tableTextColor = theme === 'dark' ? 'var(--palette-dark-neutral-border-strong)' : 'rgba(0, 0, 0, 0.6)'; return ( {hasPropertyToShow ? ( Property Value {checklistEnabled ? Select Property : <>} ) : ( <> )} {!hasPropertyToShow ? ( (No properties) ) : ( Object.keys(entity.properties) .sort() .map((key) => ( {key} {formatProperty(entity?.properties[key])} {checklistEnabled ? ( { handleCheckboxClick(key, event.target.checked); }} /> ) : ( <> )} )) )}
); }; export default GraphEntityInspectionTable; ================================================ FILE: src/chart/graph/component/autocomplete/LabelTypeAutocomplete.tsx ================================================ import React from 'react'; import Autocomplete from '@mui/material/Autocomplete'; import { TextField } from '@mui/material'; import { EditType } from '../GraphChartEditModal'; /** * Renders an auto-complete text field that uses either: * - The labels from the active Neo4j database. * - The relationship types from the active Neo4j database. * TODO - check that the same database is used that the component has selected. */ export const LabelTypeAutocomplete = ({ type, disabled, input, setInput, value, setValue, records, setRecords, queryCallback, }) => { return ( (r._fields ? r._fields[0] : '(no data)'))} getOptionLabel={(option) => option || ''} style={{ width: '100%', marginLeft: '5px', marginTop: '5px' }} inputValue={input} onInputChange={(event, value) => { setInput(value); if (type == EditType.Node) { queryCallback( 'CALL db.labels() YIELD label WITH label as nodeLabel WHERE toLower(nodeLabel) CONTAINS toLower($input) RETURN DISTINCT nodeLabel LIMIT 5', { input: value }, setRecords ); } else { queryCallback( 'CALL db.relationshipTypes() YIELD relationshipType WITH relationshipType as relType WHERE toLower(relType) CONTAINS toLower($input) RETURN DISTINCT relType LIMIT 5', { input: value }, setRecords ); } }} value={value} onChange={(event, newValue) => setValue(newValue)} renderInput={(params) => ( )} /> ); }; ================================================ FILE: src/chart/graph/component/autocomplete/PropertyNameAutocomplete.tsx ================================================ import React from 'react'; import Autocomplete from '@mui/material/Autocomplete'; import { TextField } from '@mui/material'; /** * Renders an auto-complete text field for property identifiers. * TODO - check that the same database is used that the component has selected. */ export const PropertyNameAutocomplete = ({ disabled, index, inputs, setInputs, values, setValues, records, setRecords, queryCallback, }) => { return ( (r._fields ? r._fields[0] : '(no data)'))} getOptionLabel={(option) => option || ''} style={{ display: 'inline-block', width: 170, marginLeft: '5px', marginTop: '0px' }} inputValue={inputs[index]} onInputChange={(event, value) => { const newPropertyInputTexts = [...inputs]; newPropertyInputTexts[index] = value; setInputs(newPropertyInputTexts); queryCallback( 'CALL db.propertyKeys() YIELD propertyKey as propertyName WITH propertyName WHERE toLower(propertyName) CONTAINS toLower($input) RETURN DISTINCT propertyName LIMIT 5', { input: value }, setRecords ); }} value={values[index].name} onChange={(e, val) => { const newProperties = [...values]; newProperties[index].name = val; setValues(newProperties); }} renderInput={(params) => } /> ); }; ================================================ FILE: src/chart/graph/component/button/GraphChartDeepLinkButton.tsx ================================================ import React from 'react'; import { Tooltip } from '@mui/material'; import { replaceDashboardParametersInString } from '../../../ChartUtils'; import { GraphChartVisualizationProps } from '../../GraphChartVisualization'; import { IconButton } from '@neo4j-ndl/react'; import { MagnifyingGlassIconOutline } from '@neo4j-ndl/react/icons'; /** * 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. */ export const NeoGraphChartDeepLinkButton = (props: GraphChartVisualizationProps) => { return ( ); }; ================================================ FILE: src/chart/graph/component/button/GraphChartFitViewButton.tsx ================================================ import React from 'react'; import { Tooltip } from '@mui/material'; import { GraphChartVisualizationProps } from '../../GraphChartVisualization'; import { IconButton } from '@neo4j-ndl/react'; import { FitToScreenIcon } from '@neo4j-ndl/react/icons'; /** * Renders an icon on the bottom-right of the graph visualization to fit the current graph to the user's view. */ export const NeoGraphChartFitViewButton = (props: GraphChartVisualizationProps) => { return ( { props.interactivity.zoomToFit(); }} clean grouped > { props.interactivity.zoomToFit(); }} /> ); }; ================================================ FILE: src/chart/graph/component/button/GraphChartLockButton.tsx ================================================ import React from 'react'; import { GraphChartVisualizationProps } from '../../GraphChartVisualization'; import { Tooltip } from '@mui/material'; import { IconButton } from '@neo4j-ndl/react'; import { LockOpenIconSolid, LockClosedIconSolid } from '@neo4j-ndl/react/icons'; /** * Renders a button that can be used to 'lock' = freeze the current graph layout by disabling the force layout. */ export const NeoGraphChartLockButton = (props: GraphChartVisualizationProps) => { return ( {props.interactivity.layoutFrozen ? ( { props.interactivity.setLayoutFrozen(false); }} /> ) : ( { if (props.interactivity.nodePositions == undefined) { props.interactivity.nodePositions = {}; } props.interactivity.setLayoutFrozen(true); }} /> )} ); }; ================================================ FILE: src/chart/graph/component/button/modal/DeletePropertyButton.tsx ================================================ import React from 'react'; import { XMarkIconOutline } from '@neo4j-ndl/react/icons'; import { IconButton } from '@neo4j-ndl/react'; /** * Returns a button to delete a property entry from the table inside the GraphChartEditModal. */ export const DeletePropertyButton = ({ onClick, key }) => { return ( ); }; ================================================ FILE: src/chart/graph/util/EditUtils.ts ================================================ import { GraphChartVisualizationProps, Link, Node } from '../GraphChartVisualization'; import { injectNewRecordsIntoGraphVisualization } from './RecordUtils'; import { recomputeCurvatures } from './RelUtils'; export const handleNodeCreate = () => { throw 'Not Implemented'; }; export const handleNodeEdit = ( node: Node, labels: string[], properties: Record, props: GraphChartVisualizationProps ) => { // Cast properties to numbers if they are castable as such... Object.keys(properties).forEach((key) => { const value = properties[key]; if (!Number.isNaN(parseFloat(value))) { properties[key] = parseFloat(value); } }); const oldLabels = node.labels.join(':'); const newLabels = labels.join(':'); props.engine.queryCallback( `MATCH (n) WHERE id(n) = $id REMOVE n:${oldLabels} SET n:${newLabels} SET n = $properties RETURN n`, { id: node.id, properties: properties, }, (records) => { if (records && records[0] && records[0].error) { props.interactivity.createNotification('Error', records[0].error); return; } // const updatedNode = records[0]._fields[0]; const { nodes, links, nodesMap, linksMap } = injectNewRecordsIntoGraphVisualization(records, props); const newNodes = [...props.data.nodes]; // Iterate over the old nodes, and override the nodes object if it was changed. newNodes.forEach((n, i) => { nodes .filter((x) => x.id == n.id) .forEach((match) => { newNodes[i].labels = match.labels; newNodes[i].mainLabel = match.mainLabel; newNodes[i].color = match.color; newNodes[i].size = match.size; newNodes[i].properties = match.properties; }); }); props.data.setNodes(newNodes); props.interactivity.createNotification('Node Updated', 'The node details were updated successfully.'); } ); }; export const handleNodeDelete = () => { throw 'Not Implemented'; }; export const handleRelationshipCreate = ( start: Node, type: string, properties: Record, end: Node, engine, interactivity, data ) => { engine.queryCallback( `MATCH (n), (m) WHERE id(n) = $start AND id(m) = $end CREATE (n)-[r:${type}]->(m) SET r = $properties RETURN r`, { start: start.id, type: type, properties: properties, end: end.id, }, (records) => { if (records && records[0] && records[0].error) { interactivity.createNotification('Error', records[0].error); return; } const id = records[0]._fields[0].identity; // Clean up properties for displaying in the visualization. This has to do with the visualization using 'name' as an override label. Object.keys(properties).map((prop) => { if (prop == 'name') { properties[' name'] = properties[prop]; delete properties[prop]; } }); const { links } = data; links.push({ id: id, width: 2, color: 'grey', type: type, new: true, properties: properties, source: start, target: end, }); // Recompute curvature for all links, because a new link was added. data.setLinks(recomputeCurvatures(links)); interactivity.createNotification('Relationship Created', 'The new relationship was added successfully.'); interactivity.setContextMenuOpen(false); } ); }; export const handleRelationshipEdit = ( link: Link, properties: Record, props: GraphChartVisualizationProps ) => { // Cast properties to numbers if they are castable as such... Object.keys(properties).forEach((key) => { const value = properties[key]; if (!Number.isNaN(parseFloat(value))) { properties[key] = parseFloat(value); } }); props.engine.queryCallback( `MATCH ()-[r]->() WHERE id(r) = $id SET r = $properties RETURN r`, { id: link.id, properties: properties, }, (records) => { if (records && records[0] && records[0].error) { props.interactivity.createNotification('Error', records[0].error); return; } const { nodes, links, nodesMap, linksMap } = injectNewRecordsIntoGraphVisualization(records, props); const newLinks = [...props.data.links]; // Iterate over the old links, and override the links object if it was changed. newLinks.forEach((n, i) => { links .filter((x) => x.id == n.id) .forEach((match) => { newLinks[i].color = match.color; newLinks[i].width = match.width; newLinks[i].properties = match.properties; }); }); props.data.setLinks(newLinks); props.interactivity.createNotification( 'Relationship Updated', 'The relationship details were updated successfully.' ); } ); }; export const handleRelationshipDelete = () => { throw 'Not Implemented'; }; ================================================ FILE: src/chart/graph/util/ExplorationUtils.ts ================================================ import { GraphChartVisualizationProps } from '../GraphChartVisualization'; import { injectNewRecordsIntoGraphVisualization } from './RecordUtils'; import { recomputeCurvatures } from './RelUtils'; export const getNodeRelationshipCountsQuery = `MATCH (b) WHERE id(b) = $id WITH b, apoc.node.relationship.types(b) as types UNWIND types as type WITH type, apoc.node.degree.in(b,type) as in, apoc.node.degree.out(b,type) AS out UNWIND ["in", "out","any"] as direction WITH type, direction, in, out WHERE (in <> 0 AND direction = "in") OR (out <> 0 AND direction = "out") OR direction="any" RETURN type, direction, CASE WHEN direction = "in" THEN in WHEN direction = "out" THEN out ELSE in+out END as value ORDER BY type, direction `; export const getNodeRelationshipCountsQueryWithoutApoc = ` MATCH (b) WHERE id(b) = $id MATCH (b)-[r]-() WITH type(r) as type, CASE WHEN startNode(r) = b THEN "out" ELSE "in" END as dir, COUNT(*) as value UNWIND ["in", "out","any"] as direction WITH * WHERE (direction = dir) OR direction="any" RETURN type, direction, sum(value) as value ORDER BY type, direction `; export const handleGetNodeRelTypes = (id: number, engine: any, callback: any) => { engine.queryCallback(getNodeRelationshipCountsQuery, { id: id }, (records) => { if (records && records[0] && records[0].error) { handleGetNodeRelTypesWithoutApoc(id, engine, callback); } else { callback(records); } }); }; const handleGetNodeRelTypesWithoutApoc = (id: number, engine: any, callback: any) => { engine.queryCallback(getNodeRelationshipCountsQueryWithoutApoc, { id: id }, (records) => { callback(records); }); }; export const handleExpand = (id: number, type: string, dir: string, props: GraphChartVisualizationProps) => { const query = ` MATCH (n) WHERE id(n) = $id MATCH (n)${dir == 'in' ? '<' : ''}-[r${type !== '...' ? `:\`${type}\`` : ''}]-${dir == 'out' ? '>' : ''}(m) RETURN n, r, m `; props.engine.queryCallback(query, { id: id }, (records) => { if (records && records[0] && records[0].error) { props.interactivity.createNotification('Error', records[0].error); return; } const { nodes, links, nodesMap, linksMap } = injectNewRecordsIntoGraphVisualization(records, props); const newNodes = [...props.data.nodes]; nodes.forEach((n) => { if (nodesMap[n.id] === undefined) { nodesMap[n.id] = n; // do not double push newNodes.push(n); } }); const newLinks = [...props.data.links]; links.forEach((n) => { if (linksMap[n.id] === undefined) { if (n.target.id === undefined) { n.target = nodesMap[n.target]; } if (n.source.id === undefined) { n.source = nodesMap[n.source]; } linksMap[n.id] = n; // do not double push newLinks.push(n); } }); props.data.setGraph(newNodes, recomputeCurvatures(newLinks)); props.engine.setCooldownTicks(50); }); }; // Combines the database statistic on relationship frequencies with those in the current view. export const mergeDatabaseStatCountsWithCountsInView = (id, stats, links) => { const directions = ['out', 'in', 'any']; const mergedRelCounts = {}; directions.map((d) => { mergedRelCounts[`...` + `___${d}`] = 0; }); stats.forEach((item) => { const entry = `${item._fields[0]}___${item._fields[1]}`; if (mergedRelCounts[entry] === undefined) { mergedRelCounts[entry] = 0; } mergedRelCounts[entry] += parseInt(item._fields[2]); mergedRelCounts[`...` + `___${item._fields[1]}`] += parseInt(item._fields[2]); }); // Subtract if we find links in the view that are already visible... links.forEach((item) => { if (item.source.id == id) { mergedRelCounts[`${item.type}___` + `out`] -= 1; mergedRelCounts[`${item.type}___` + `any`] -= 1; mergedRelCounts['...' + '___' + 'out'] -= 1; mergedRelCounts['...' + '___' + 'any'] -= 1; } if (item.target.id == id) { mergedRelCounts[`${item.type}___` + `in`] -= 1; mergedRelCounts[`${item.type}___` + `any`] -= 1; mergedRelCounts['...' + '___' + 'in'] -= 1; mergedRelCounts['...' + '___' + 'any'] -= 1; } }); const mergedCountsList = Object.keys(mergedRelCounts).map((key) => { const [type, direction] = key.split('___'); const value = mergedRelCounts[key]; if (value !== 0) { return [type, direction, value]; } return undefined; }); return mergedCountsList.filter((v) => v !== undefined); }; ================================================ FILE: src/chart/graph/util/NodeUtils.ts ================================================ import { evaluateRulesOnNode } from '../../../extensions/styling/StyleRuleEvaluator'; import { GraphEntity } from '../GraphChartVisualization'; export const getNodeLabel = (selection, node) => { const selectedProp = selection && selection[node.mainLabel]; if (selectedProp == '(id)') { return node.id; } if (selectedProp == '(label)') { return node.labels; } if (selectedProp == '(no label)') { return ''; } return node.properties[selectedProp] ? node.properties[selectedProp] : ''; }; export const parseNodeIconConfig = (iconStyle) => { try { return iconStyle ? JSON.parse(iconStyle) : undefined; } catch (error) { // Unable to parse node icon definition as specified by the user. console.log(error); } }; const getSelectedNodeProperty = (entity: any, sourceOrTarget: string, propertySelections: any) => { const selection = propertySelections[entity[sourceOrTarget]?.mainLabel]; switch (selection) { case '(label)': return entity[sourceOrTarget]?.mainLabel; case '(id)': return entity[sourceOrTarget]?.id; default: return entity[sourceOrTarget]?.properties[selection]; } }; const getRelPatternString = (entity: any, selection: any) => { const sourceTitle = getSelectedNodeProperty(entity, 'source', selection); const targetTitle = getSelectedNodeProperty(entity, 'target', selection); return `(${sourceTitle ? sourceTitle : '[no value]'} --> ${targetTitle ? targetTitle : '[no value]'})`; }; export const getEntityHeader = (entity: any, selection: any) => { return entity.labels?.join(', ') || `${entity.type} ${getRelPatternString(entity, selection)}`; }; export const drawDataURIOnCanvas = (node, strDataURI, canvas, defaultNodeSize) => { let img = new Image(); let prop = defaultNodeSize * 6; img.src = strDataURI; canvas.drawImage(img, node.x - prop / 2, node.y - prop / 2, prop, prop); }; export const generateNodeCanvasObject = ( node: GraphEntity, ctx: any, icons: any, frozen: boolean, nodePositions: Record, nodeLabelFontSize: number, defaultNodeSize: any, nodeLabelColor: any, styleRules: any, selection: any ) => { if (icons && icons[node.mainLabel]) { drawDataURIOnCanvas(node, icons[node.mainLabel], ctx, defaultNodeSize); } else { const label = selection && selection[node.mainLabel] ? getNodeLabel(selection, node) : ''; const fontSize = nodeLabelFontSize; ctx.font = `${fontSize}px Sans-Serif`; ctx.fillStyle = evaluateRulesOnNode(node, 'node label color', nodeLabelColor, styleRules); ctx.textAlign = 'center'; ctx.fillText(label, node.x ? node.x : 0, node.y ? node.y + 1 : 0); if (frozen && !node.fx && !node.fy && nodePositions) { node.fx = node.x; node.fy = node.y; nodePositions[`${node.id}`] = [node.x, node.y]; } if (!frozen && node.fx && node.fy && nodePositions && nodePositions[node.id]) { nodePositions[node.id] = undefined; node.fx = undefined; node.fy = undefined; } } }; ================================================ FILE: src/chart/graph/util/RecordUtils.ts ================================================ import { evaluateRulesOnNode, evaluateRulesOnLink } from '../../../extensions/styling/StyleRuleEvaluator'; import { extractNodePropertiesFromRecords, mergeNodePropsFieldsLists } from '../../../report/ReportRecordProcessing'; import { valueIsArray, valueIsNode, valueIsRelationship, valueIsPath, toNumber } from '../../ChartUtils'; import { GraphChartVisualizationProps } from '../GraphChartVisualization'; import { assignCurvatureToLink } from './RelUtils'; import { isNode } from 'neo4j-driver-core/lib/graph-types.js'; const update = (state, mutations) => Object.assign({}, state, mutations); // Gets all graphy entities (nodes/relationships) from the complete set of return values. function extractGraphEntitiesFromField( value, nodes: Record[], links: Record[], nodeLabels: Record, linkTypes: Record, frozen: boolean, nodeSizeProperty: string, defaultNodeSize: number, relWidthProperty: string, defaultRelWidth: number, relColorProperty: string, defaultRelColor: string, nodePositions: Record[] ) { if (value == undefined) { return; } if (valueIsArray(value)) { value.forEach((v) => extractGraphEntitiesFromField( v, nodes, links, nodeLabels, linkTypes, frozen, nodeSizeProperty, defaultNodeSize, relWidthProperty, defaultRelWidth, relColorProperty, defaultRelColor, nodePositions ) ); } else if (valueIsNode(value)) { value.labels.forEach((l) => (nodeLabels[l] = true)); nodes[value.identity.low] = { id: value.identity.low, labels: value.labels, size: !Number.isNaN(value.properties[nodeSizeProperty]) ? toNumber(value.properties[nodeSizeProperty]) : defaultNodeSize, properties: value.properties, mainLabel: value.labels[value.labels.length - 1], }; if (frozen && nodePositions && nodePositions[value.identity.low]) { nodes[value.identity.low].fx = nodePositions[value.identity.low][0]; nodes[value.identity.low].fy = nodePositions[value.identity.low][1]; } } else if (valueIsRelationship(value)) { if (links[`${value.start.low},${value.end.low}`] == undefined) { links[`${value.start.low},${value.end.low}`] = []; } const addItem = (arr, item) => arr.find((x) => x.id === item.id) || arr.push(item); addItem(links[`${value.start.low},${value.end.low}`], { id: value.identity.low, source: value.start.low, target: value.end.low, type: value.type, width: value.properties[relWidthProperty] !== undefined && !Number.isNaN(value.properties[relWidthProperty]) ? toNumber(value.properties[relWidthProperty]) : defaultRelWidth, color: value.properties[relColorProperty] ? value.properties[relColorProperty] : defaultRelColor, properties: value.properties, }); } else if (valueIsPath(value)) { value.segments.map((segment) => { extractGraphEntitiesFromField( segment.start, nodes, links, nodeLabels, linkTypes, frozen, nodeSizeProperty, defaultNodeSize, relWidthProperty, defaultRelWidth, relColorProperty, defaultRelColor, nodePositions ); extractGraphEntitiesFromField( segment.relationship, nodes, links, nodeLabels, linkTypes, frozen, nodeSizeProperty, defaultNodeSize, relWidthProperty, defaultRelWidth, relColorProperty, defaultRelColor, nodePositions ); extractGraphEntitiesFromField( segment.end, nodes, links, nodeLabels, linkTypes, frozen, nodeSizeProperty, defaultNodeSize, relWidthProperty, defaultRelWidth, relColorProperty, defaultRelColor, nodePositions ); }); } } const isValidLink = (link, nodes) => { if (nodes[link.source] == null || nodes[link.target] == null) { return false; } return true; }; export function buildGraphVisualizationObjectFromRecords( records: any[], // Neo4jRecord[], nodes: Record[], links: Record[], nodeLabels: Record, linkTypes: Record, colorScheme: any, fields: any, nodeColorProperty: any, defaultNodeColor: any, nodeSizeProperty: any, defaultNodeSize: any, relWidthProperty: any, defaultRelWidth: any, relColorProperty: any, defaultRelColor: any, styleRules: any, nodePositions: any = {}, frozen: any = false ) { // Extract graph objects from result set. records.forEach((record) => { record._fields.forEach((field) => { extractGraphEntitiesFromField( field, nodes, links, nodeLabels, linkTypes, frozen, nodeSizeProperty, defaultNodeSize, relWidthProperty, defaultRelWidth, relColorProperty, defaultRelColor, nodePositions ); }); }); // Assign proper curvatures and colors to relationships. // Assigning curvature is needed for pairs of nodes that have multiple relationships between them, or self-loops. const linksList = Object.values(links).map((linkArray) => { return linkArray.map((link, i) => { let defaultColor = link.color; // Assign color from json based on style rule evaluation if specified let evaluatedColor = evaluateRulesOnLink(link, 'relationship color', defaultColor, styleRules); link.color = evaluatedColor; const mirroredNodePair = links[`${link.target},${link.source}`]; return assignCurvatureToLink(link, i, linkArray.length, mirroredNodePair ? mirroredNodePair.length : 0); }); }); linksList.forEach((link, idx, object) => { if (!isValidLink(link[0], nodes)) { object.splice(idx, 1); } }); // Assign proper colors to nodes. const totalColors = colorScheme ? colorScheme.length : 0; const nodeLabelsList = fields.map((e) => e[0]); const nodesList = Object.values(nodes).map((node) => { // First try to assign a node a color if it has a property specifying the color. let assignedColor = node.properties[nodeColorProperty] ? node.properties[nodeColorProperty] : totalColors > 0 ? colorScheme[nodeLabelsList.indexOf(node.mainLabel) % totalColors] : defaultNodeColor; // Next, evaluate the custom styling rules to see if there's a rule-based override assignedColor = evaluateRulesOnNode(node, 'node color', assignedColor, styleRules); return update(node, { color: assignedColor ? assignedColor : defaultNodeColor }); }); // Set the data dictionary that is read by the visualization. return { nodes: nodesList, links: linksList.flat(), }; } /** * Utility function to inject new records into an existing visualization while it already exists. * This is used to enable graph interactivity (e.g. exploration, editing). * @param records a new set of Neo4j records. * @param props properties of the existing graph visualization. */ export function injectNewRecordsIntoGraphVisualization( records: any[], // Neo4jRecord[], props: GraphChartVisualizationProps ) { // We should probably just maintain these in the state... const nodesMap = {}; props.data.nodes.forEach((node) => { nodesMap[node.id] = node; }); const linksMap = {}; props.data.links.forEach((link) => { linksMap[link.id] = link; }); const newFields = extractNodePropertiesFromRecords(records); const mergedFields = mergeNodePropsFieldsLists(props.engine.fields, newFields); props.engine.setFields(mergedFields); const { nodes, links } = buildGraphVisualizationObjectFromRecords( records, { ...nodesMap }, {}, props.data.nodeLabels, props.data.linkTypes, props.style.colorScheme, mergedFields, props.style.nodeColorProp, props.style.defaultNodeColor, props.style.nodeSizeProp, props.style.defaultNodeSize, props.style.relWidthProp, props.style.defaultRelWidth, props.style.relColorProp, props.style.defaultRelColor, props.extensions.styleRules, props.interactivity.nodePositions, props.interactivity.layoutFrozen ); return { nodes, links, nodesMap, linksMap }; } /** * TODO: generalize and fix to be consistent with other parts of the code. * TODO: maybe we shouldn't check if all records are nodes, but instead extract nodes from the records dynamically as the graph chart deos. * @param records List of records got back from the Driver * @param fieldIndex index of the field i want to check that is just nodes * @returns True if all the records are Node Objects */ export function checkIfAllRecordsAreNodes(records, fieldIndex) { try { let res = records.every((record) => { return record._fields && isNode(record._fields[fieldIndex]); }); return res; } catch (error) { // In any case of error, log and continue with false console.error(error); return false; } } /** * TODO - this functionality is duplicated in the graph chart logic. * Ideally, we want to have a Node/Relationship representation indipendent from the return * that the driver gets back. * @param records List of records got from the driver * @returns List of Object that are parsed from the Node object received from the driver */ export function parseNodeRecordsToDictionaries(records, fieldIndex = 0) { let res = records.map((record) => { let { identity, labels, properties } = record._fields[fieldIndex]; // Preventing high/low fields by casting to its primitive type identity = identity.toNumber(); return { id: identity, labels: labels, properties: properties }; }); return res; } ================================================ FILE: src/chart/graph/util/RelUtils.ts ================================================ export enum Direction { Incoming, Outgoing, } const update = (state, mutations) => Object.assign({}, state, mutations); /** * Assigns a computed curvature value to a link in the visualization. * @param link the link object (n)-[e]->(n2) * @param index the index of the link in the list between a pair of nodes. * @param nodePairListLength the amount of links between (n) and (n2) in the same direction. * @param mirroredNodePairListLength the amount of links between (n) and (n2) in the opposite direction. * @returns the link with an assigned curvature value. */ export function assignCurvatureToLink(link, index, nodePairListLength, mirroredNodePairListLength) { if (link.source == link.target) { // Self-loop return update(link, { curvature: 0.4 + index / 8 }); } // If we have edges from the target to the source, adjust curvatures accordingly. const totalRelsBetweenPair = nodePairListLength + mirroredNodePairListLength; return update(link, { curvature: link.source > link.target ? getCurvature(index, totalRelsBetweenPair) : -getCurvature(index + mirroredNodePairListLength, totalRelsBetweenPair), }); } // Function to manually compute edge curvatures for dense node pairs. export function getCurvature(index, total) { if (total <= 6) { // Precomputed edge curvatures for nodes with multiple edges in between. const curvatures = { 0: 0, 1: 0, 2: [-0.5, 0.5], // 2 = Math.floor(1/2) + 1 3: [-0.5, 0, 0.5], // 2 = Math.floor(3/2) + 1 4: [-0.66666, -0.33333, 0.33333, 0.66666], // 3 = Math.floor(4/2) + 1 5: [-0.66666, -0.33333, 0, 0.33333, 0.66666], // 3 = Math.floor(5/2) + 1 6: [-0.75, -0.5, -0.25, 0.25, 0.5, 0.75], // 4 = Math.floor(6/2) + 1 7: [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75], // 4 = Math.floor(7/2) + 1 }; return curvatures[total][index]; } if (isNaN(total)) { return 0; } // @ts-ignore const arr1 = [...Array(Math.floor(total / 2)).keys()].map((i) => { return (i + 1) / (Math.floor(total / 2) + 1); }); const arr2 = total % 2 == 1 ? [0] : []; // @ts-ignore const arr3 = [...Array(Math.floor(total / 2)).keys()].map((i) => { return (i + 1) / -(Math.floor(total / 2) + 1); }); return arr1.concat(arr2).concat(arr3)[index]; } export const selfLoopRotationDegrees = 45; export const generateRelCanvasObject = (link: any, ctx: any, relLabelFontSize: any, relLabelColor: any) => { const label = link.properties.name || link.type || link.id; const fontSize = relLabelFontSize; const { source } = link; const { target } = link; ctx.font = `${fontSize}px Sans-Serif`; ctx.fillStyle = relLabelColor; if (link.target != link.source) { const lenX = target.x - source.x; const lenY = target.y - source.y; const posX = target.x - lenX / 2; const posY = target.y - lenY / 2; const length = Math.sqrt(lenX * lenX + lenY * lenY); const angle = Math.atan(lenY / lenX); ctx.save(); ctx.translate(posX, posY); ctx.rotate(angle); // Mirrors the curvatures when the label is upside down. const mirror = link.source.x > link.target.x ? 1 : -1; ctx.textAlign = 'center'; if (link.curvature) { ctx.fillText(label, 0, mirror * length * link.curvature * 0.5); } else { ctx.fillText(label, 0, 0); } ctx.restore(); } else { ctx.save(); ctx.translate(link.source.x, link.source.y); ctx.rotate((Math.PI * selfLoopRotationDegrees) / 180); ctx.textAlign = 'center'; ctx.fillText(label, 0, -18.7 + -37.1 * (link.curvature - 0.5)); ctx.restore(); } }; /** * Recompute curvatures for all links in the visualization. * This is needed when new relationships are added by exploration or graph editing. * TODO - this could be optimized by caching a dictionary instead of transforming the list here... */ export function recomputeCurvatures(links) { const linksMap = {}; links.forEach((link) => { if (linksMap[`${link.source.id},${link.target.id}`] == undefined) { linksMap[`${link.source.id},${link.target.id}`] = []; } linksMap[`${link.source.id},${link.target.id}`].push(link); }); const linksList = Object.values(linksMap).map((linkArray) => { return linkArray.map((link, i) => { const mirroredNodePair = linksMap[`${link.target.id},${link.source.id}`]; return assignCurvatureToLink(link, i, linkArray.length, mirroredNodePair ? mirroredNodePair.length : 0); }); }); return linksList.flat(); } /** * Merges two lists of (potententially duplicate) links. */ export function mergeLinksLists(oldLinks, newLinks) { const links = {}; oldLinks.forEach((link) => { links[link.id] = link; }); newLinks.forEach((link) => { links[link.id] = link; }); return Object.values(links); } ================================================ FILE: src/chart/iframe/IFrameChart.tsx ================================================ import React from 'react'; import { ChartProps } from '../Chart'; import { replaceDashboardParameters } from '../ChartUtils'; /** * Renders an iFrame of the URL provided by the user. */ const NeoIFrameChart = (props: ChartProps) => { // Records are overridden to be a single element array with a field called 'input'. const { records } = props; const parameters = props.parameters ? props.parameters : {}; const passGlobalParameters = props.settings && props.settings.passGlobalParameters ? props.settings.passGlobalParameters : false; const replaceGlobalParameters = props.settings && props.settings.replaceGlobalParameters !== undefined ? props.settings.replaceGlobalParameters : true; const url = records[0].input.trim(); const mapParameters = records[0].parameters || {}; const queryString = Object.keys(mapParameters) .map((key) => `${key}=${mapParameters[key]}`) .join('&'); const modifiedUrl = (replaceGlobalParameters ? replaceDashboardParameters(url, parameters) : url) + (passGlobalParameters ? `#${queryString}` : ''); if (!modifiedUrl || !(modifiedUrl.startsWith('http://') || modifiedUrl.startsWith('https://'))) { return (

Invalid iFrame URL. Make sure your url starts with http:// or https://.

); } return (