From f151d32f9997cddb52941ff96dcdd409afacdda5 Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Tue, 10 Oct 2023 10:33:32 +0200 Subject: [PATCH 001/107] Made API keys hidden (password fields) in the query translator module --- src/component/field/Field.tsx | 2 ++ src/component/field/Setting.tsx | 2 ++ src/extensions/query-translator/QueryTranslatorConfig.ts | 3 +++ src/extensions/query-translator/component/ClientSettings.tsx | 1 + 4 files changed, 8 insertions(+) diff --git a/src/component/field/Field.tsx b/src/component/field/Field.tsx index 709853100..570fbed3a 100644 --- a/src/component/field/Field.tsx +++ b/src/component/field/Field.tsx @@ -15,6 +15,7 @@ const NeoField = ({ select = false, disabled = undefined, variant = undefined, + password = false, helperText = undefined, defaultValueLabel = undefined, defaultValue = undefined, @@ -77,6 +78,7 @@ const NeoField = ({ variant={variant} label={label} helpText={helperText} + type={password ? 'password' : 'text'} disabled={disabled} value={value != null ? value : defaultValue} fluid diff --git a/src/component/field/Setting.tsx b/src/component/field/Setting.tsx index 31d7ef434..f1a212341 100644 --- a/src/component/field/Setting.tsx +++ b/src/component/field/Setting.tsx @@ -72,6 +72,7 @@ const NeoSetting = ({ defaultValue, disabled = undefined, helperText = undefined, + password = false, onChange, onClick = () => {}, style = { width: '100%', marginBottom: '10px', marginRight: '10px', marginLeft: '10px' }, @@ -104,6 +105,7 @@ const NeoSetting = ({ disabled={disabled} helperText={helperText} value={value} + password={password} defaultValue={''} placeholder={`${defaultValue}`} style={style} diff --git a/src/extensions/query-translator/QueryTranslatorConfig.ts b/src/extensions/query-translator/QueryTranslatorConfig.ts index 8b7138b74..c35e8cb76 100644 --- a/src/extensions/query-translator/QueryTranslatorConfig.ts +++ b/src/extensions/query-translator/QueryTranslatorConfig.ts @@ -10,6 +10,7 @@ interface ClientSettingEntry { label: string; type: SELECTION_TYPES; default: any; + password?: boolean; authentication?: boolean; // Required for authentication, the user should insert all the required fields before trying to authenticate hasAuthButton?: boolean; // Append a button at the end of the selector to trigger an auth request. methodFromClient?: string; // String that contains the name of the client function to call to retrieve the data needed to fill the option @@ -47,6 +48,7 @@ export const QUERY_TRANSLATOR_CONFIG: QueryTranslatorConfig = { label: 'OpenAI API Key', type: SELECTION_TYPES.TEXT, default: '', + password: true, hasAuthButton: true, authentication: true, }, @@ -74,6 +76,7 @@ export const QUERY_TRANSLATOR_CONFIG: QueryTranslatorConfig = { label: 'Subscription Key', type: SELECTION_TYPES.TEXT, default: '', + password: true, hasAuthButton: true, authentication: true, }, diff --git a/src/extensions/query-translator/component/ClientSettings.tsx b/src/extensions/query-translator/component/ClientSettings.tsx index 56f855e95..19e8b5d22 100644 --- a/src/extensions/query-translator/component/ClientSettings.tsx +++ b/src/extensions/query-translator/component/ClientSettings.tsx @@ -155,6 +155,7 @@ export const ClientSettings = ({ name={setting} value={localSettings[setting]} disabled={disabled} + password={defaultSettings[setting].password} type={defaultSettings[setting].type} label={defaultSettings[setting].label} defaultValue={defaultSettings[setting].default} From d2525aed2c33bf592418f604d1f58f1ac1a66873 Mon Sep 17 00:00:00 2001 From: jacobbleakley-neo4j Date: Tue, 24 Oct 2023 11:43:42 +0100 Subject: [PATCH 002/107] Legend on bottom, working on scrolling --- package.json | 5 ++- src/chart/bar/BarChart.tsx | 36 +++++++++++---- yarn.lock | 92 ++++++++++++++++++++------------------ 3 files changed, 79 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index b430ce577..a0a7b8952 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@sentry/react": "^7.57.0", "@sentry/webpack-plugin": "^2.5.0", "babel-runtime": "^6.26.0", + "caniuse-lite": "^1.0.30001546", "chroma-js": "^2.4.2", "classnames": "^2.3.1", "d3-scale-chromatic": "^3.0.0", @@ -117,6 +118,7 @@ "@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", @@ -136,7 +138,6 @@ "typescript": "^4.8.4", "webpack": "^5.77.0", "webpack-cli": "^4.9.1", - "webpack-dev-server": "^4.7.3", - "circular-dependency-plugin": "^5.2.2" + "webpack-dev-server": "^4.7.3" } } diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index e62d17ed3..dac422159 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -210,7 +210,24 @@ const NeoBarChart = (props: ChartProps) => { const extraProperties = positionLabel == 'off' ? {} : { barComponent: BarComponent }; const canvas = data.length > 30; const BarChartComponent = canvas ? ResponsiveBarCanvas : ResponsiveBar; + + // For adaptable item length in the legend + const maxKeyLength = Math.max(...keys.map(key => key.length)); + const baseItemWidth = 40; // Some base width for color box and padding + const charWidthEstimate = 5; // An estimate of how wide each character is, you might need to adjust this based on font size and type + const itemWidthConst = baseItemWidth + (maxKeyLength * charWidthEstimate); + + // Scrollable Wrapper + + const scrollableWrapperStyle: React.CSSProperties = { + width: '100%', + overflowX: 'auto', + height: '500px', + whiteSpace: 'nowrap', + }; + const chart = ( +
{ margin={{ top: marginTop, right: legend ? legendWidth + marginRight : marginRight, - bottom: marginBottom, + bottom: legend? marginBottom + 50 : marginBottom, left: marginLeft, }} valueScale={{ type: valueScale }} @@ -252,15 +269,15 @@ const NeoBarChart = (props: ChartProps) => { ? [ { dataFrom: 'keys', - anchor: 'bottom-right', - direction: 'column', - justify: true, - translateX: 120, - translateY: 0, + anchor: 'bottom-left', + direction: 'row', + justify: false, + translateX: 40, + translateY: 80, itemsSpacing: 2, - itemWidth: legendWidth - 28, + itemWidth: itemWidthConst, itemHeight: 20, - itemDirection: 'right-to-left', + itemDirection: 'left-to-right', itemOpacity: 0.85, symbolSize: 20, effects: [ @@ -277,9 +294,10 @@ const NeoBarChart = (props: ChartProps) => { } animate={false} /> +
); return chart; }; -export default NeoBarChart; +export default NeoBarChart; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 892cd3918..658279557 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1701,7 +1701,7 @@ js-yaml "4.1.0" nyc "15.1.0" -"@cypress/request@^2.88.10": +"@cypress/request@2.88.12": version "2.88.12" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.12.tgz#ba4911431738494a85e93fb04498cb38bc55d590" integrity sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA== @@ -2506,25 +2506,25 @@ "@babel/runtime" "^7.20.13" "@neo4j-cypher/codemirror" "1.0.2" -"@neo4j-ndl/base@1.10.1", "@neo4j-ndl/base@^1.10.1": - version "1.10.1" - resolved "https://registry.yarnpkg.com/@neo4j-ndl/base/-/base-1.10.1.tgz#18b3b35b9a52d0f5f0ee978c435bc96717ff16a9" - integrity sha512-ytz82vN1qMDCZButP4Wm0bLTStz6BWXWWRXMY0iP2Wfw/OAcI3WF2fBVL902FtzCBq0MR/GHHwjgMVpy9g7XeA== +"@neo4j-ndl/base@1.10.3", "@neo4j-ndl/base@^1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@neo4j-ndl/base/-/base-1.10.3.tgz#99557e3bede274510fc465a781d2e52e0cca47ea" + integrity sha512-NTFUz8j9+yx9AN1/TR3yzs7Nt/K+p1yqo2RHqf3UCtuD4ZyMUgYW1gmPQS0Du2S43gvQJkpVenXYJFgjNcFEMA== -"@neo4j-ndl/react@1.10.2": - version "1.10.2" - resolved "https://registry.yarnpkg.com/@neo4j-ndl/react/-/react-1.10.2.tgz#aaf61a06f3c63212f275b2a67ecf588a03287c84" - integrity sha512-t4OPV+qA5EqO4qb3FQBfdNnEZAhIO4CiW1qKN4cPeOvXCAyquPBkSM2+8l/rI1+Ra8o4r5FJ1LKln1BhLQPHPw== +"@neo4j-ndl/react@1.10.8": + version "1.10.8" + resolved "https://registry.yarnpkg.com/@neo4j-ndl/react/-/react-1.10.8.tgz#ab2ad2719cbdbe8a286ec54b18afc8fae6e5da33" + integrity sha512-EVUjwyxup/uNFItJl634z4JA9EVKJ5rvdncu9FOWHs85cw3VNqIfnFuApuuslveo7AknAwkCyeqQmOtjlPVSRQ== dependencies: "@floating-ui/react" "^0.24.2" "@heroicons/react" "2.0.13" "@neo4j-cypher/react-codemirror" "^1.0.1" - "@neo4j-ndl/base" "^1.10.1" + "@neo4j-ndl/base" "^1.10.3" "@tanstack/react-table" "^8.9.3" classnames "^2.3.1" date-fns "^2.30.0" detect-browser "^5.3.0" - re-resizable "^6.9.9" + re-resizable "^6.9.11" react-aria "^3.25.0" react-datepicker "^4.14.1" react-dropzone "^14.0.0" @@ -4535,10 +4535,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== -"@types/node@^14.14.31": - version "14.18.54" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.54.tgz#fc304bd66419030141fa997dc5a9e0e374029ae8" - integrity sha512-uq7O52wvo2Lggsx1x21tKZgqkJpvwCseBBPtX/nKQfpVlEsLOb11zZ1CRsWUKvJF0+lzuA9jwvA7Pr2Wt7i3xw== +"@types/node@^16.18.39": + version "16.18.58" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.58.tgz#bf66f63983104ed57c754f4e84ccaf16f8235adb" + integrity sha512-YGncyA25/MaVtQkjWW9r0EFBukZ+JulsLcVZBlGUfIb96OBMjkoRWwQo5IEWJ8Fj06Go3GHw+bjYDitv6BaGsA== "@types/parse-json@^4.0.0": version "4.0.0" @@ -5625,15 +5625,15 @@ camelize@^1.0.0: resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== -caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: - version "1.0.30001474" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001474.tgz#13b6fe301a831fe666cce8ca4ef89352334133d5" - integrity sha512-iaIZ8gVrWfemh5DG3T9/YqarVZoYf0r188IjaGwx68j4Pf0SGY6CQkmJUIE+NZHkkecQGohzXmBGEwWDr9aM3Q== +caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001503: + version "1.0.30001546" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz" + integrity sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw== -caniuse-lite@^1.0.30001503: - version "1.0.30001512" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz#7450843fb581c39f290305a83523c7a9ef0d4cb4" - integrity sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw== +caniuse-lite@^1.0.30001546: + version "1.0.30001546" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz#10fdad03436cfe3cc632d3af7a99a0fb497407f0" + integrity sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw== canvas-color-tracker@1: version "1.2.1" @@ -5924,10 +5924,10 @@ commander@^4.0.0, commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== +commander@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== commander@^9.4.1: version "9.5.0" @@ -6194,14 +6194,14 @@ csstype@^3.1.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== -cypress@^10.11.0: - version "10.11.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.11.0.tgz#e9fbdd7638bae3d8fb7619fd75a6330d11ebb4e8" - integrity sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA== +cypress@^12.17.4: + version "12.17.4" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c" + integrity sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ== dependencies: - "@cypress/request" "^2.88.10" + "@cypress/request" "2.88.12" "@cypress/xvfb" "^1.2.4" - "@types/node" "^14.14.31" + "@types/node" "^16.18.39" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" arch "^2.2.0" @@ -6213,10 +6213,10 @@ cypress@^10.11.0: check-more-types "^2.24.0" cli-cursor "^3.1.0" cli-table3 "~0.6.1" - commander "^5.1.0" + commander "^6.2.1" common-tags "^1.8.0" dayjs "^1.10.4" - debug "^4.3.2" + debug "^4.3.4" enquirer "^2.3.6" eventemitter2 "6.4.7" execa "4.1.0" @@ -6231,12 +6231,13 @@ cypress@^10.11.0: listr2 "^3.8.3" lodash "^4.17.21" log-symbols "^4.0.0" - minimist "^1.2.6" + minimist "^1.2.8" ospath "^1.2.2" pretty-bytes "^5.6.0" + process "^0.11.10" proxy-from-env "1.0.0" request-progress "^3.0.0" - semver "^7.3.2" + semver "^7.5.3" supports-color "^8.1.1" tmp "~0.2.1" untildify "^4.0.0" @@ -10175,7 +10176,7 @@ minimatch@^7.4.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -11269,6 +11270,11 @@ process-utils@^4.0.0: memoizee "^0.4.14" type "^2.1.0" +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -11389,10 +11395,10 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -re-resizable@^6.9.9: - version "6.9.9" - resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.9.tgz#99e8b31c67a62115dc9c5394b7e55892265be216" - integrity sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA== +re-resizable@^6.9.11: + version "6.9.11" + resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.11.tgz#f356e27877f12d926d076ab9ad9ff0b95912b475" + integrity sha512-a3hiLWck/NkmyLvGWUuvkAmN1VhwAz4yOhS6FdMTaxCUVN9joIWkT11wsO68coG/iEYuwn+p/7qAmfQzRhiPLQ== react-aria@^3.25.0: version "3.25.0" @@ -12210,7 +12216,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.3.7, semver@^7.3.8: +semver@^7.3.7, semver@^7.3.8, semver@^7.5.3: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== From 66d03176e43b75d72684b64a41c3ae9220dff052 Mon Sep 17 00:00:00 2001 From: jacobbleakley-neo4j Date: Tue, 24 Oct 2023 13:29:25 +0100 Subject: [PATCH 003/107] Scrolling added, legend gets cut off. Need to set width according to content --- src/chart/bar/BarChart.tsx | 152 +++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 72 deletions(-) diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index dac422159..8d2edef5c 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -107,7 +107,7 @@ const NeoBarChart = (props: ChartProps) => { const maxValue = settings.maxValue ? settings.maxValue : 'auto'; const styleRules = useStyleRules( extensionEnabled(props.extensions, 'styling'), - props.settings.styleRules, + settings.styleRules, props.getGlobalParameter ); @@ -212,92 +212,100 @@ const NeoBarChart = (props: ChartProps) => { const BarChartComponent = canvas ? ResponsiveBarCanvas : ResponsiveBar; // For adaptable item length in the legend - const maxKeyLength = Math.max(...keys.map(key => key.length)); + const maxKeyLength = Math.max(...keys.map((key) => key.length)); const baseItemWidth = 40; // Some base width for color box and padding const charWidthEstimate = 5; // An estimate of how wide each character is, you might need to adjust this based on font size and type - const itemWidthConst = baseItemWidth + (maxKeyLength * charWidthEstimate); + const itemWidthConst = baseItemWidth + maxKeyLength * charWidthEstimate; // Scrollable Wrapper - + const scrollableWrapperStyle: React.CSSProperties = { - width: '100%', + width: '2000px', overflowX: 'auto', - height: '500px', + height: '100%', whiteSpace: 'nowrap', }; + const barChartStyle: React.CSSProperties = { + width: '100%', + overflowX: 'auto', + height: '100%' + } + const chart = ( -
- +
+ + ] + : [] + } + animate={false} + /> +
); return chart; }; -export default NeoBarChart; \ No newline at end of file +export default NeoBarChart; From 6e0aad9315d7d98d86699843ad99b9bd8640d33b Mon Sep 17 00:00:00 2001 From: jacobbleakley-neo4j Date: Tue, 24 Oct 2023 13:36:07 +0100 Subject: [PATCH 004/107] Set autowidth depending on length of array --- src/chart/bar/BarChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index 8d2edef5c..4da465f05 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -220,7 +220,7 @@ const NeoBarChart = (props: ChartProps) => { // Scrollable Wrapper const scrollableWrapperStyle: React.CSSProperties = { - width: '2000px', + width: (itemWidthConst*data.length)+200, overflowX: 'auto', height: '100%', whiteSpace: 'nowrap', From 940ed2958b5d70f7f290c77429c48c30079b9191 Mon Sep 17 00:00:00 2001 From: jacobbleakley-neo4j Date: Wed, 25 Oct 2023 12:38:23 +0100 Subject: [PATCH 005/107] wednesday --- src/chart/bar/BarChart.tsx | 29 +++++++++++++++-------------- src/config/ReportConfig.tsx | 10 ++++++++++ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index 4da465f05..d4cf43fdc 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -85,13 +85,14 @@ const NeoBarChart = (props: ChartProps) => { } const settings = props.settings ? props.settings : {}; - const legendWidth = settings.legendWidth ? settings.legendWidth : 128; const marginRight = settings.marginRight ? settings.marginRight : 24; const marginLeft = settings.marginLeft ? settings.marginLeft : 50; const marginTop = settings.marginTop ? settings.marginTop : 24; const marginBottom = settings.marginBottom ? settings.marginBottom : 40; const legend = settings.legend ? settings.legend : false; const labelRotation = settings.labelRotation != undefined ? settings.labelRotation : 45; + const barWidth = settings.barWidth? settings.barWidth : 50; + const padding = settings.padding ? settings.padding : 0.3; const labelSkipWidth = settings.labelSkipWidth ? settings.labelSkipWidth : 0; const labelSkipHeight = settings.labelSkipHeight ? settings.labelSkipHeight : 0; @@ -220,15 +221,15 @@ const NeoBarChart = (props: ChartProps) => { // Scrollable Wrapper const scrollableWrapperStyle: React.CSSProperties = { - width: (itemWidthConst*data.length)+200, - overflowX: 'auto', - height: '100%', + width: (barWidth*data.length)+itemWidthConst, + height: (18*data.length)+(itemWidthConst*1.2)+marginBottom, whiteSpace: 'nowrap', }; const barChartStyle: React.CSSProperties = { width: '100%', overflowX: 'auto', + overflowY: 'auto', height: '100%' } @@ -241,17 +242,17 @@ const NeoBarChart = (props: ChartProps) => { key={`${selection.index}___${selection.value}`} layout={layout} groupMode={groupMode == 'stacked' ? 'stacked' : 'grouped'} - enableLabel={enableLabel} + // enableLabel={enableLabel} keys={keys} indexBy='index' margin={{ top: marginTop, - right: legend ? legendWidth + marginRight : marginRight, - bottom: legend ? marginBottom + 50 : marginBottom, + right: legend ? itemWidthConst + marginRight : marginRight, + bottom: (itemWidthConst*0.3) +marginBottom, left: marginLeft, }} valueScale={{ type: valueScale }} - padding={0.3} + padding={padding} minValue={minValue} maxValue={maxValue} colors={getBarColor} @@ -276,17 +277,17 @@ const NeoBarChart = (props: ChartProps) => { ? [ { dataFrom: 'keys', - anchor: 'bottom-left', - direction: 'row', + anchor: 'bottom-right', + direction: 'column', justify: false, - translateX: 0, - translateY: 80, - itemsSpacing: 2, + translateX: itemWidthConst+10, + translateY: 0, + itemsSpacing: 1, itemWidth: itemWidthConst, itemHeight: 20, itemDirection: 'left-to-right', itemOpacity: 0.85, - symbolSize: 20, + symbolSize: 15, effects: [ { on: 'hover', diff --git a/src/config/ReportConfig.tsx b/src/config/ReportConfig.tsx index 879199d96..c0fc52412 100644 --- a/src/config/ReportConfig.tsx +++ b/src/config/ReportConfig.tsx @@ -337,6 +337,16 @@ const _REPORT_TYPES = { values: [true, false], default: false, }, + barWidth: { + label: 'Bar Width', + type: SELECTION_TYPES.NUMBER, + default: 50, + }, + padding: { + label: 'Padding', + type: SELECTION_TYPES.NUMBER, + default: 0.3, + }, showOptionalSelections: { label: 'Grouping', type: SELECTION_TYPES.LIST, From 0caa78a33a0562f1aa10826db298d6ae33cbbcdf Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Fri, 27 Oct 2023 14:00:28 +0200 Subject: [PATCH 006/107] Added new report action type for tables: multiselect checkboxes (#664) * Work in progress on table checkboxes * Fix handling external updates of parameter values in parameter selector * Improved handling of multiselector parameters for tables with checkboxes * Removed console log statement. Fix invalid behaviour * Updated comments * Added multiselect value limit * Clean up action rule create modal * Added docs on multiple selection with table rows and report actions --- .../ROOT/images/select-multiple-table.png | Bin 0 -> 92919 bytes .../ROOT/images/select-single-table.png | Bin 0 -> 100602 bytes .../ROOT/pages/user-guide/reports/table.adoc | 19 ++++ public/style.css | 4 + src/chart/Chart.ts | 2 +- .../component/NodePropertyParameterSelect.tsx | 26 +++++- src/chart/table/TableActionsHelper.ts | 46 ++++++++++ src/chart/table/TableChart.tsx | 6 ++ src/config/ReportConfig.tsx | 5 ++ .../actions/ActionsRuleCreationModal.tsx | 85 +++++++++++------- 10 files changed, 158 insertions(+), 35 deletions(-) create mode 100644 docs/modules/ROOT/images/select-multiple-table.png create mode 100644 docs/modules/ROOT/images/select-single-table.png create mode 100644 src/chart/table/TableActionsHelper.ts diff --git a/docs/modules/ROOT/images/select-multiple-table.png b/docs/modules/ROOT/images/select-multiple-table.png new file mode 100644 index 0000000000000000000000000000000000000000..b154cd20d4de137ed42eda95114dfae02b45e454 GIT binary patch literal 92919 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 literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/pages/user-guide/reports/table.adoc b/docs/modules/ROOT/pages/user-guide/reports/table.adoc index 2c8fe5559..2a472bec0 100644 --- a/docs/modules/ROOT/pages/user-guide/reports/table.adoc +++ b/docs/modules/ROOT/pages/user-guide/reports/table.adoc @@ -71,3 +71,22 @@ following style rules can be applied to 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] \ No newline at end of file diff --git a/public/style.css b/public/style.css index 16ccc2fb5..af019dc94 100644 --- a/public/style.css +++ b/public/style.css @@ -86,6 +86,10 @@ border: none !important; } +.MuiDataGrid-footerContainer > div { + margin-top: -40px; +} + .MuiChip-root:before { border: none !important; } diff --git a/src/chart/Chart.ts b/src/chart/Chart.ts index 0e92a7558..b3a5a173c 100644 --- a/src/chart/Chart.ts +++ b/src/chart/Chart.ts @@ -14,7 +14,7 @@ export interface ChartProps { parameters?: Record; // A dictionary with the global dashboard parameters. 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: string) => void; // Allows a chart to update a global dashboard parameter to be used in Cypher queries for other reports. + 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. diff --git a/src/chart/parameter/component/NodePropertyParameterSelect.tsx b/src/chart/parameter/component/NodePropertyParameterSelect.tsx index f29374cd7..50481fa14 100644 --- a/src/chart/parameter/component/NodePropertyParameterSelect.tsx +++ b/src/chart/parameter/component/NodePropertyParameterSelect.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { debounce, TextField } from '@mui/material'; import Autocomplete from '@mui/material/Autocomplete'; import { ParameterSelectProps } from './ParameterSelect'; @@ -24,6 +24,7 @@ const NodePropertyParameterSelectComponent = (props: ParameterSelectProps) => { const { multiSelector, manualParameterSave } = props; const allParameters = props.allParameters ? props.allParameters : {}; const [extraRecords, setExtraRecords] = React.useState([]); + const [inputDisplayText, setInputDisplayText] = React.useState( props.parameterDisplayValue && multiSelector ? '' : props.parameterDisplayValue ); @@ -34,6 +35,7 @@ const NodePropertyParameterSelectComponent = (props: ParameterSelectProps) => { const debouncedQueryCallback = useCallback(debounce(props.queryCallback, suggestionsUpdateTimeout), []); const label = props.settings && props.settings.entityType ? props.settings.entityType : ''; + const multiSelectLimit = props.settings && props.settings.multiSelectLimit ? props.settings.multiSelectLimit : 5; const propertyType = props.settings && props.settings.propertyType ? props.settings.propertyType : ''; const helperText = props.settings && props.settings.helperText ? props.settings.helperText : ''; const clearParameterOnFieldClear = @@ -121,11 +123,33 @@ const NodePropertyParameterSelectComponent = (props: ParameterSelectProps) => { handleParametersUpdate(newValue, newDisplay, manualParameterSave); }; + useEffect(() => { + // Handle external updates of parameter values, with varying value types and parameter selector types. + // Handles multiple scenarios if an external parameter changes type from value to lists. + const isArray = Array.isArray(props.parameterDisplayValue); + if (multiSelector) { + if (isArray) { + setInputDisplayText(props.parameterDisplayValue); + setInputValue(props.parameterDisplayValue); + } else if (props.parameterDisplayValue !== '') { + setInputDisplayText([props.parameterDisplayValue]); + setInputValue([props.parameterDisplayValue]); + } else { + setInputDisplayText(''); + setInputValue([]); + } + } else { + setInputDisplayText(props.parameterDisplayValue); + setInputValue(props.parameterDisplayValue); + } + }, [props.parameterDisplayValue]); + return (

r?._fields?.[displayValueRowIndex] || '(no data)').sort()} style={{ maxWidth: 'calc(100% - 40px)', diff --git a/src/chart/table/TableActionsHelper.ts b/src/chart/table/TableActionsHelper.ts new file mode 100644 index 000000000..7b946df2e --- /dev/null +++ b/src/chart/table/TableActionsHelper.ts @@ -0,0 +1,46 @@ +export const hasCheckboxes = (actionsRules) => { + let rules = actionsRules.filter((rule) => rule.condition && rule.condition == 'rowCheck'); + return rules.length > 0; +}; + +export const getCheckboxes = (actionsRules, rows, getGlobalParameter) => { + let rules = actionsRules.filter((rule) => rule.condition && rule.condition == 'rowCheck'); + const params = rules.map((rule) => `neodash_${rule.customizationValue}`); + // See if any of the rows should be checked. This is the case when a parameter is already in the list of checked values. + let selection: number[] = []; + params.forEach((parameter, index) => { + const fieldName = rules[index].value; + const values = getGlobalParameter(parameter); + + // If the parameter is an array (to be expected), iterate over it to find the rows to check. + if (Array.isArray(values)) { + values.forEach((value) => { + rows.forEach((row, index) => { + if (row[fieldName] == value) { + selection.push(index); + } + }); + }); + } else { + // Else (special case), still check the row if it's a single value parameter. + rows.forEach((row, index) => { + if (row[fieldName] == values) { + selection.push(index); + } + }); + } + }); + return [...new Set(selection)]; +}; + +export const updateCheckBoxes = (actionsRules, rows, selection, setGlobalParameter) => { + if (hasCheckboxes(actionsRules)) { + const selectedRows = rows.filter((_, i) => selection.includes(i)); + let rules = actionsRules.filter((rule) => rule.condition && rule.condition == 'rowCheck'); + rules.forEach((rule) => { + const parameterValues = selectedRows.map((row) => row[rule.value]).filter((v) => v !== undefined); + setGlobalParameter(`neodash_${rule.customizationValue}`, parameterValues); + setGlobalParameter(`neodash_${rule.customizationValue}_display`, parameterValues); + }); + } +}; diff --git a/src/chart/table/TableChart.tsx b/src/chart/table/TableChart.tsx index 9740158a6..a35e081a0 100644 --- a/src/chart/table/TableChart.tsx +++ b/src/chart/table/TableChart.tsx @@ -22,6 +22,7 @@ import { CloudArrowDownIconOutline, XMarkIconOutline } from '@neo4j-ndl/react/ic import { ThemeProvider, createTheme } from '@mui/material/styles'; import Button from '@mui/material/Button'; import { extensionEnabled } from '../../utils/ReportUtils'; +import { getCheckboxes, hasCheckboxes, updateCheckBoxes } from './TableActionsHelper'; const TABLE_HEADER_HEIGHT = 32; const TABLE_FOOTER_HEIGHT = 62; @@ -268,6 +269,11 @@ export const NeoTableChart = (props: ChartProps) => { navigator.clipboard.writeText(e.value); } }} + checkboxSelection={hasCheckboxes(actionsRules)} + selectionModel={getCheckboxes(actionsRules, rows, props.getGlobalParameter)} + onSelectionModelChange={(selection) => + updateCheckBoxes(actionsRules, rows, selection, props.setGlobalParameter) + } pageSize={tablePageSize > 0 ? tablePageSize : 5} rowsPerPageOptions={rows.length < 5 ? [rows.length, 5] : [5]} disableSelectionOnClick diff --git a/src/config/ReportConfig.tsx b/src/config/ReportConfig.tsx index 879199d96..d2ba89a93 100644 --- a/src/config/ReportConfig.tsx +++ b/src/config/ReportConfig.tsx @@ -1269,6 +1269,11 @@ const _REPORT_TYPES = { values: [true, false], default: false, }, + multiSelectLimit: { + label: 'Multiselect Value Limit', + type: SELECTION_TYPES.NUMBER, + default: 5, + }, helperText: { label: 'Helper Text (Override)', type: SELECTION_TYPES.TEXT, diff --git a/src/extensions/actions/ActionsRuleCreationModal.tsx b/src/extensions/actions/ActionsRuleCreationModal.tsx index b2fa8df6c..1f31e5dca 100644 --- a/src/extensions/actions/ActionsRuleCreationModal.tsx +++ b/src/extensions/actions/ActionsRuleCreationModal.tsx @@ -16,6 +16,12 @@ const RULE_CONDITIONS = { value: 'doubleClick', label: 'Cell Double Click', }, + { + value: 'rowCheck', + label: 'Row Checked', + disableFieldSelection: true, + multiple: true, + }, ], map: [ { @@ -274,6 +280,7 @@ export const NeoCustomReportActionsModal = ({ (el) => el.value === rule.customization ); const ruleTrigger = RULE_CONDITIONS[type].find((el) => el.value === rule.condition); + return ( <> @@ -286,7 +293,11 @@ export const NeoCustomReportActionsModal = ({ updateRuleField(index, 'condition', newValue.value), options: @@ -298,40 +309,46 @@ export const NeoCustomReportActionsModal = ({ value: { label: ruleTrigger ? ruleTrigger.label : '', value: rule.condition }, }} > - } - style={{ - minWidth: 125, - }} - onInputChange={(event, value) => { - updateRuleField(index, 'field', value); - }} - onChange={(event, newValue) => { - updateRuleField(index, 'field', newValue); - }} - renderInput={(params) => ( - - )} - /> + {!ruleTrigger.disableFieldSelection ? ( + } + style={{ + minWidth: 125, + }} + onInputChange={(event, value) => { + updateRuleField(index, 'field', value); + }} + onChange={(event, newValue) => { + updateRuleField(index, 'field', newValue); + }} + renderInput={(params) => ( + + )} + /> + ) : ( + <> + )}
- - SET + + + {!ruleTrigger.multiple ? 'SET' : 'APPEND'} + - +
- TO + + {!ruleTrigger.multiple ? 'TO' : 'WITH'} +
From 3a2cf4d7110d06d8d05a9e894d7897da742938b8 Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Fri, 27 Oct 2023 14:01:19 +0200 Subject: [PATCH 007/107] Fix handling external updates of parameter values in parameter selector (#663) * Fix handling external updates of parameter values in parameter selector * Removed console log statement. Fix invalid behaviour * Updated comments From 5e27a027371d6306f11b28cb4e4245870ec3c5ac Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Fri, 27 Oct 2023 14:22:19 +0200 Subject: [PATCH 008/107] Feature/new dashboard load UI (#657) * Added sidebar prototype * Return of the sidebar * Add database selector to sidebar * Iterating on the dashboard sidebar interface * Fixed usage of hardcoded color * Updated dashboard loading mechanism, iterating * Updated dashboard loading mechanism, iterating * Updated file structure for dashboard sidebar * Improved modal/menu handling for dashboard load * Import/export dashboards * New dashboard sharing interface * Removed old save/load modal * Finalized v1 of the new multi-dashboard UI * removing useless imports * Removed old isLoaded in state of dashboard load screen. Updated warning buttons --------- Co-authored-by: Alfred Rubin --- src/application/Application.tsx | 4 +- src/application/ApplicationActions.ts | 6 + src/application/ApplicationReducer.ts | 32 +- src/application/ApplicationSelectors.ts | 4 + src/application/ApplicationThunks.ts | 13 +- src/config/ApplicationConfig.ts | 2 +- src/dashboard/Dashboard.tsx | 57 +- src/dashboard/DashboardActions.ts | 6 + src/dashboard/DashboardReducer.ts | 21 +- src/dashboard/DashboardSelectors.ts | 2 + src/dashboard/DashboardThunks.ts | 183 ++++--- src/dashboard/header/DashboardHeader.tsx | 2 +- .../header/DashboardHeaderPageTitle.tsx | 6 +- src/dashboard/header/DashboardTitle.tsx | 21 +- src/dashboard/sidebar/DashboardSidebar.tsx | 493 ++++++++++++++++++ .../sidebar/DashboardSidebarListItem.tsx | 73 +++ .../menu/DashboardSidebarCreateMenu.tsx | 38 ++ .../menu/DashboardSidebarDashboardMenu.tsx | 52 ++ .../menu/DashboardSidebarDatabaseMenu.tsx | 50 ++ .../modal/DashboardSidebarCreateModal.tsx | 43 ++ .../modal/DashboardSidebarDeleteModal.tsx | 43 ++ .../modal/DashboardSidebarExportModal.tsx | 79 +++ .../modal/DashboardSidebarImportModal.tsx | 73 +++ .../modal/DashboardSidebarInfoModal.tsx | 48 ++ .../modal/DashboardSidebarLoadModal.tsx | 44 ++ .../modal/DashboardSidebarSaveModal.tsx | 39 ++ .../modal/DashboardSidebarShareModal.tsx | 92 ++++ .../sidebar/modal/legacy/LegacyShareModal.tsx | 212 ++++++++ src/modal/ExportModal.tsx | 33 ++ src/modal/LoadModal.tsx | 224 -------- src/modal/SaveModal.tsx | 227 -------- src/modal/ShareModal.tsx | 349 ------------- src/modal/UpgradeOldDashboardModal.tsx | 3 +- src/page/Page.tsx | 5 +- src/page/PageReducer.ts | 6 +- yarn.lock | 76 +-- 36 files changed, 1712 insertions(+), 949 deletions(-) create mode 100644 src/dashboard/sidebar/DashboardSidebar.tsx create mode 100644 src/dashboard/sidebar/DashboardSidebarListItem.tsx create mode 100644 src/dashboard/sidebar/menu/DashboardSidebarCreateMenu.tsx create mode 100644 src/dashboard/sidebar/menu/DashboardSidebarDashboardMenu.tsx create mode 100644 src/dashboard/sidebar/menu/DashboardSidebarDatabaseMenu.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarCreateModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarDeleteModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarExportModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarImportModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarInfoModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarLoadModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarSaveModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarShareModal.tsx create mode 100644 src/dashboard/sidebar/modal/legacy/LegacyShareModal.tsx create mode 100644 src/modal/ExportModal.tsx delete mode 100644 src/modal/LoadModal.tsx delete mode 100644 src/modal/SaveModal.tsx delete mode 100644 src/modal/ShareModal.tsx diff --git a/src/application/Application.tsx b/src/application/Application.tsx index cd8d9759a..1dbec13d1 100644 --- a/src/application/Application.tsx +++ b/src/application/Application.tsx @@ -214,9 +214,9 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(setConnected(false)); dispatch(createConnectionFromDesktopIntegrationThunk()); }, - loadDashboard: (text) => { + loadDashboard: (uuid, text) => { dispatch(clearNotification()); - dispatch(loadDashboardThunk(text)); + dispatch(loadDashboardThunk(uuid, text)); }, resetDashboard: () => dispatch(resetDashboardState()), clearOldDashboard: () => dispatch(setOldDashboard(null)), diff --git a/src/application/ApplicationActions.ts b/src/application/ApplicationActions.ts index 4f85a17c5..cf49b4715 100644 --- a/src/application/ApplicationActions.ts +++ b/src/application/ApplicationActions.ts @@ -20,6 +20,12 @@ export const setConnected = (connected: boolean) => ({ 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, diff --git a/src/application/ApplicationReducer.ts b/src/application/ApplicationReducer.ts index 5e01776c7..dc53aa7ad 100644 --- a/src/application/ApplicationReducer.ts +++ b/src/application/ApplicationReducer.ts @@ -2,7 +2,10 @@ * Reducers define changes to the application state when a given action is taken. */ +import { HARD_RESET_CARD_SETTINGS, UPDATE_ALL_SELECTIONS, UPDATE_FIELDS, UPDATE_SCHEMA } 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, @@ -15,6 +18,7 @@ import { 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, @@ -36,6 +40,7 @@ const initialState = { notificationMessage: null, connectionModalOpen: false, welcomeScreenOpen: true, + draft: false, aboutModalOpen: false, connection: { protocol: 'neo4j', @@ -55,6 +60,25 @@ const initialState = { 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, + ]; + 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; } @@ -75,6 +99,11 @@ export const applicationReducer = (state = initialState, action: { type: any; pa 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 }); @@ -82,9 +111,6 @@ export const applicationReducer = (state = initialState, action: { type: any; pa } case SET_ABOUT_MODAL_OPEN: { const { open } = payload; - if (!open) { - console.log(''); - } state = update(state, { aboutModalOpen: open }); return state; } diff --git a/src/application/ApplicationSelectors.ts b/src/application/ApplicationSelectors.ts index e99758185..e3dc20601 100644 --- a/src/application/ApplicationSelectors.ts +++ b/src/application/ApplicationSelectors.ts @@ -21,6 +21,10 @@ 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; }; diff --git a/src/application/ApplicationThunks.ts b/src/application/ApplicationThunks.ts index 38590733b..27e1b8e8c 100644 --- a/src/application/ApplicationThunks.ts +++ b/src/application/ApplicationThunks.ts @@ -4,8 +4,9 @@ import { DEFAULT_SCREEN, Screens } from '../config/ApplicationConfig'; import { setDashboard } from '../dashboard/DashboardActions'; import { NEODASH_VERSION } from '../dashboard/DashboardReducer'; import { + assignDashboardUuidIfNotPresentThunk, loadDashboardFromNeo4jByNameThunk, - loadDashboardFromNeo4jByUUIDThunk, + loadDashboardFromNeo4jThunk, loadDashboardThunk, upgradeDashboardVersion, } from '../dashboard/DashboardThunks'; @@ -40,6 +41,7 @@ import { setReportHelpModalOpen, } from './ApplicationActions'; import { version } from '../modal/AboutModal'; +import { createUUID } from '../utils/uuid'; /** * Application Thunks (https://redux.js.org/usage/writing-logic-thunks) handle complex state manipulations. @@ -67,9 +69,12 @@ export const createConnectionThunk = if (records && records[0] && records[0].error) { dispatch(createNotificationThunk('Unable to establish connection', records[0].error)); } else if (records && records[0] && records[0].keys[0] == 'connected') { + // Connected to Neo4j. Set state accordingly. 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)); @@ -83,11 +88,11 @@ export const createConnectionThunk = ) { fetch(application.dashboardToLoadAfterConnecting) .then((response) => response.text()) - .then((data) => dispatch(loadDashboardThunk(data))); + .then((data) => dispatch(loadDashboardThunk(createUUID(), data))); dispatch(setDashboardToLoadAfterConnecting(null)); } else if (application.dashboardToLoadAfterConnecting) { const setDashboardAfterLoadingFromDatabase = (value) => { - dispatch(loadDashboardThunk(value)); + dispatch(loadDashboardThunk(createUUID(), value)); }; // If we specify a dashboard by name, load the latest version of it. @@ -103,7 +108,7 @@ export const createConnectionThunk = ); } else { dispatch( - loadDashboardFromNeo4jByUUIDThunk( + loadDashboardFromNeo4jThunk( driver, application.standaloneDashboardDatabase, application.dashboardToLoadAfterConnecting, diff --git a/src/config/ApplicationConfig.ts b/src/config/ApplicationConfig.ts index 20869f3f9..7589a102c 100644 --- a/src/config/ApplicationConfig.ts +++ b/src/config/ApplicationConfig.ts @@ -9,7 +9,7 @@ const styleConfig = await StyleConfig.getInstance(); export const DEFAULT_SCREEN = Screens.WELCOME_SCREEN; // WELCOME_SCREEN export const DEFAULT_NEO4J_URL = 'localhost'; // localhost -export const DEFAULT_DASHBOARD_TITLE = 'My dashboard'; // '' +export const DEFAULT_DASHBOARD_TITLE = 'New dashboard'; export const DASHBOARD_HEADER_COLOR = styleConfig?.style?.DASHBOARD_HEADER_COLOR || '#0B297D'; // '#0B297D' diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx index 04fc8fa39..e564c8043 100644 --- a/src/dashboard/Dashboard.tsx +++ b/src/dashboard/Dashboard.tsx @@ -11,6 +11,7 @@ import { forceRefreshPage } from '../page/PageActions'; import { getPageNumber } from '../settings/SettingsSelectors'; import { createNotificationThunk } from '../page/PageThunks'; import { version } from '../modal/AboutModal'; +import NeoDashboardSidebar from './sidebar/DashboardSidebar'; const Dashboard = ({ pagenumber, @@ -43,8 +44,12 @@ const Dashboard = ({ connection={connection} onConnectionUpdate={onConnectionUpdate} /> + {/* Navigation Bar */} -
+
{/* Main Page */} -
- {/* Main Content */} -
-
-
- {/* The main content of the page */} - {applicationSettings.standalonePassword ? ( -
- Warning: NeoDash is running with a plaintext password in config.json. +
+ +
+
+ {/* Main Content */} +
+
+
+ {/* The main content of the page */} + +
+ {applicationSettings.standalonePassword ? ( +
+ Warning: NeoDash is running with a plaintext password in config.json. +
+ ) : ( + <> + )} + + + +
- ) : ( - <> - )} - - - -
+
+
- +
); diff --git a/src/dashboard/DashboardActions.ts b/src/dashboard/DashboardActions.ts index 00dffbb7a..879f18fbe 100644 --- a/src/dashboard/DashboardActions.ts +++ b/src/dashboard/DashboardActions.ts @@ -10,6 +10,12 @@ export const setDashboard = (dashboard: any) => ({ payload: { dashboard }, }); +export const SET_DASHBOARD_UUID = 'DASHBOARD/SET_DASHBOARD_UUID'; +export const setDashboardUuid = (uuid: any) => ({ + type: SET_DASHBOARD_UUID, + payload: { uuid }, +}); + export const SET_DASHBOARD_TITLE = 'DASHBOARD/SET_DASHBOARD_TITLE'; export const setDashboardTitle = (title: any) => ({ type: SET_DASHBOARD_TITLE, diff --git a/src/dashboard/DashboardReducer.ts b/src/dashboard/DashboardReducer.ts index 98ea60eda..4ede3bc0c 100644 --- a/src/dashboard/DashboardReducer.ts +++ b/src/dashboard/DashboardReducer.ts @@ -4,7 +4,7 @@ import { DEFAULT_DASHBOARD_TITLE } from '../config/ApplicationConfig'; import { extensionsReducer, INITIAL_EXTENSIONS_STATE } from '../extensions/state/ExtensionReducer'; -import { FIRST_PAGE_INITIAL_STATE, pageReducer, PAGE_INITIAL_STATE } from '../page/PageReducer'; +import { PAGE_EXAMPLE_STATE, pageReducer, PAGE_EMPTY_STATE } from '../page/PageReducer'; import { settingsReducer, SETTINGS_INITIAL_STATE } from '../settings/SettingsReducer'; import { @@ -15,6 +15,7 @@ import { SET_DASHBOARD, MOVE_PAGE, SET_EXTENSION_ENABLED, + SET_DASHBOARD_UUID, } from './DashboardActions'; export const NEODASH_VERSION = '2.3'; @@ -22,9 +23,17 @@ export const NEODASH_VERSION = '2.3'; export const initialState = { title: DEFAULT_DASHBOARD_TITLE, version: NEODASH_VERSION, + settings: SETTINGS_INITIAL_STATE, + pages: [PAGE_EXAMPLE_STATE], + parameters: {}, + extensions: INITIAL_EXTENSIONS_STATE, +}; +export const emptyDashboardState = { + title: DEFAULT_DASHBOARD_TITLE, + version: NEODASH_VERSION, settings: SETTINGS_INITIAL_STATE, - pages: [FIRST_PAGE_INITIAL_STATE], + pages: [PAGE_EMPTY_STATE], parameters: {}, extensions: INITIAL_EXTENSIONS_STATE, }; @@ -68,12 +77,16 @@ export const dashboardReducer = (state = initialState, action: { type: any; payl // Global dashboard updates are handled here. switch (type) { case RESET_DASHBOARD_STATE: { - return { ...initialState }; + return { ...emptyDashboardState }; } case SET_DASHBOARD: { const { dashboard } = payload; return { ...dashboard }; } + case SET_DASHBOARD_UUID: { + const { uuid } = payload; + return { uuid: uuid, ...state }; + } case SET_DASHBOARD_TITLE: { const { title } = payload; return { ...state, title: title }; @@ -86,7 +99,7 @@ export const dashboardReducer = (state = initialState, action: { type: any; payl return { ...state, extensions: extensions }; } case CREATE_PAGE: { - return { ...state, pages: [...state.pages, PAGE_INITIAL_STATE] }; + return { ...state, pages: [...state.pages, PAGE_EMPTY_STATE] }; } case REMOVE_PAGE: { // Removes the card at a given index on a selected page number. diff --git a/src/dashboard/DashboardSelectors.ts b/src/dashboard/DashboardSelectors.ts index 71405434a..ef2e972c6 100644 --- a/src/dashboard/DashboardSelectors.ts +++ b/src/dashboard/DashboardSelectors.ts @@ -1,3 +1,5 @@ +export const getDashboardUuid = (state: any) => state.dashboard.uuid; + export const getDashboardTitle = (state: any) => state.dashboard.title; export const getDashboardSettings = (state: any) => state.dashboard.settings; diff --git a/src/dashboard/DashboardThunks.ts b/src/dashboard/DashboardThunks.ts index da3526805..57b7cc606 100644 --- a/src/dashboard/DashboardThunks.ts +++ b/src/dashboard/DashboardThunks.ts @@ -1,8 +1,8 @@ import { createNotificationThunk } from '../page/PageThunks'; import { updateDashboardSetting } from '../settings/SettingsActions'; -import { addPage, movePage, removePage, resetDashboardState, setDashboard } from './DashboardActions'; -import { runCypherQuery } from '../report/ReportQueryRunner'; -import { setParametersToLoadAfterConnecting, setWelcomeScreenOpen } from '../application/ApplicationActions'; +import { addPage, movePage, removePage, resetDashboardState, setDashboard, setDashboardUuid } from './DashboardActions'; +import { QueryStatus, runCypherQuery } from '../report/ReportQueryRunner'; +import { setDraft, setParametersToLoadAfterConnecting, setWelcomeScreenOpen } from '../application/ApplicationActions'; import { updateGlobalParametersThunk, updateParametersToNeo4jTypeThunk } from '../settings/SettingsThunks'; import { createUUID } from '../utils/uuid'; @@ -51,7 +51,7 @@ export const movePageThunk = (oldIndex: number, newIndex: number) => (dispatch: } }; -export const loadDashboardThunk = (text) => (dispatch: any, getState: any) => { +export const loadDashboardThunk = (uuid, text) => (dispatch: any, getState: any) => { try { if (text.length == 0) { throw 'No dashboard file specified. Did you select a file?'; @@ -80,50 +80,50 @@ export const loadDashboardThunk = (text) => (dispatch: any, getState: any) => { const upgradedDashboard = upgradeDashboardVersion(dashboard, '1.1', '2.0'); dispatch(setDashboard(upgradedDashboard)); dispatch(setWelcomeScreenOpen(false)); + dispatch(setDraft(true)); dispatch( createNotificationThunk( 'Successfully upgraded dashboard', 'Your old dashboard was migrated to version 2.0. You might need to refresh this page.' ) ); - return; } if (dashboard.version == '2.0') { const upgradedDashboard = upgradeDashboardVersion(dashboard, '2.0', '2.1'); dispatch(setDashboard(upgradedDashboard)); dispatch(setWelcomeScreenOpen(false)); + dispatch(setDraft(true)); dispatch( createNotificationThunk( 'Successfully upgraded dashboard', 'Your old dashboard was migrated to version 2.1. You might need to refresh this page.' ) ); - return; } if (dashboard.version == '2.1') { const upgradedDashboard = upgradeDashboardVersion(dashboard, '2.1', '2.2'); dispatch(setDashboard(upgradedDashboard)); dispatch(setWelcomeScreenOpen(false)); + dispatch(setDraft(true)); dispatch( createNotificationThunk( 'Successfully upgraded dashboard', 'Your old dashboard was migrated to version 2.2. You might need to refresh this page.' ) ); - return; } if (dashboard.version == '2.2') { const upgradedDashboard = upgradeDashboardVersion(dashboard, '2.2', '2.3'); dispatch(setDashboard(upgradedDashboard)); dispatch(setWelcomeScreenOpen(false)); + dispatch(setDraft(true)); dispatch( createNotificationThunk( 'Successfully upgraded dashboard', 'Your old dashboard was migrated to version 2.3. You might need to refresh this page and reactivate extensions.' ) ); - return; } if (dashboard.version != '2.3') { @@ -148,57 +148,106 @@ export const loadDashboardThunk = (text) => (dispatch: any, getState: any) => { dispatch(updateGlobalParametersThunk(application.parametersToLoadAfterConnecting)); dispatch(setParametersToLoadAfterConnecting(null)); dispatch(updateParametersToNeo4jTypeThunk()); + + // Pre-2.3.4 dashboards might now always have a UUID. Set it if not present. + if (!dashboard.uuid) { + dispatch(setDashboardUuid(uuid)); + } } catch (e) { + console.log(e); dispatch(createNotificationThunk('Unable to load dashboard', e)); } }; -export const saveDashboardToNeo4jThunk = - (driver, database, dashboard, date, user, overwrite = false) => - (dispatch: any) => { - try { - const uuid = createUUID(); - const { title, version } = dashboard; - - // Generate a cypher query to save the dashboard. - const query = overwrite - ? 'OPTIONAL MATCH (n:_Neodash_Dashboard{title:$title}) DELETE n WITH 1 as X LIMIT 1 CREATE (n:_Neodash_Dashboard) SET n.uuid = $uuid, n.title = $title, n.version = $version, n.user = $user, n.content = $content, n.date = datetime($date) RETURN $uuid as uuid' - : 'CREATE (n:_Neodash_Dashboard) SET n.uuid = $uuid, n.title = $title, n.version = $version, n.user = $user, n.content = $content, n.date = datetime($date) RETURN $uuid as uuid'; - - const parameters = { - uuid: uuid, - title: title, - version: version, - user: user, - content: JSON.stringify(dashboard, null, 2), - date: date, - }; - runCypherQuery( - driver, - database, - query, - parameters, - 1, - () => {}, - (records) => { - if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) { - dispatch(createNotificationThunk('🎉 Success!', 'Your current dashboard was saved to Neo4j.')); - } else { - dispatch( - createNotificationThunk( - 'Unable to save dashboard', - `Do you have write access to the '${database}' database?` - ) - ); - } - } - ); - } catch (e) { - dispatch(createNotificationThunk('Unable to save dashboard to Neo4j', e)); +export const saveDashboardToNeo4jThunk = (driver, database, dashboard, date, user, onSuccess) => (dispatch: any) => { + try { + let { uuid } = dashboard; + + // Dashboards pre-2.3.4 may not always have a UUID. If this is the case, generate one just before we save. + if (!dashboard.uuid) { + uuid = createUUID(); + dashboard.uuid = uuid; + dispatch(setDashboardUuid(uuid)); + createUUID(); } - }; -export const loadDashboardFromNeo4jByUUIDThunk = (driver, database, uuid, callback) => (dispatch: any) => { + const { title, version } = dashboard; + + // Generate a cypher query to save the dashboard. + const query = + 'MERGE (n:_Neodash_Dashboard {uuid: $uuid }) SET n.title = $title, n.version = $version, n.user = $user, n.content = $content, n.date = datetime($date) RETURN $uuid as uuid'; + + const parameters = { + uuid: uuid, + title: title, + version: version, + user: user, + content: JSON.stringify(dashboard, null, 2), + date: date, + }; + runCypherQuery( + driver, + database, + query, + parameters, + 1, + () => {}, + (records) => { + if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) { + dispatch(createNotificationThunk('🎉 Success!', 'Your current dashboard was saved to Neo4j.')); + + onSuccess(uuid); + } else { + console.log(records); + dispatch( + createNotificationThunk( + 'Unable to save dashboard', + `Do you have write access to the '${database}' database?` + ) + ); + } + } + ); + } catch (e) { + dispatch(createNotificationThunk('Unable to save dashboard to Neo4j', e)); + } +}; + +export const deleteDashboardFromNeo4jThunk = (driver, database, uuid, onSuccess) => (dispatch: any) => { + try { + // Generate a cypher query to save the dashboard. + const query = 'MATCH (n:_Neodash_Dashboard {uuid: $uuid }) DETACH DELETE n RETURN $uuid as uuid'; + + const parameters = { + uuid: uuid, + }; + runCypherQuery( + driver, + database, + query, + parameters, + 1, + () => {}, + (records) => { + if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) { + onSuccess(uuid); + } else { + console.log(records); + dispatch( + createNotificationThunk( + 'Unable to delete dashboard', + `Do you have write access to the '${database}' database?` + ) + ); + } + } + ); + } catch (e) { + dispatch(createNotificationThunk('Unable to delete dashboard from Neo4j', e)); + } +}; + +export const loadDashboardFromNeo4jThunk = (driver, database, uuid, callback) => (dispatch: any) => { try { const query = 'MATCH (n:_Neodash_Dashboard) WHERE n.uuid = $uuid RETURN n.content as dashboard'; runCypherQuery( @@ -207,17 +256,27 @@ export const loadDashboardFromNeo4jByUUIDThunk = (driver, database, uuid, callba query, { uuid: uuid }, 1, - () => {}, + (status) => { + if (status == QueryStatus.NO_DATA) { + dispatch( + createNotificationThunk( + `Unable to load dashboard from database '${database}'.`, + `A dashboard with UUID '${uuid}' does not exist.` + ) + ); + } + }, (records) => { if (!records[0]._fields) { dispatch( createNotificationThunk( `Unable to load dashboard from database '${database}'.`, - `A dashboard with UUID '${uuid}' could not be found.` + `A dashboard with UUID '${uuid}' could not be loaded.` ) ); + } else { + callback(records[0]._fields[0]); } - callback(records[0]._fields[0]); } ); } catch (e) { @@ -265,7 +324,7 @@ export const loadDashboardListFromNeo4jThunk = (driver, database, callback) => ( runCypherQuery( driver, database, - 'MATCH (n:_Neodash_Dashboard) RETURN n.uuid as id, n.title as title, toString(n.date) as date, n.user as author, n.version as version ORDER BY date DESC', + 'MATCH (n:_Neodash_Dashboard) RETURN n.uuid as uuid, n.title as title, toString(n.date) as date, n.user as author, n.version as version ORDER BY date DESC', {}, 1000, () => {}, @@ -274,13 +333,14 @@ export const loadDashboardListFromNeo4jThunk = (driver, database, callback) => ( callback([]); return; } - const result = records.map((r) => { + const result = records.map((r, index) => { return { - id: r._fields[0], + uuid: r._fields[0], title: r._fields[1], date: r._fields[2], author: r._fields[3], version: r._fields[4], + index: index, }; }); callback(result); @@ -312,6 +372,13 @@ export const loadDatabaseListFromNeo4jThunk = (driver, callback) => (dispatch: a } }; +export const assignDashboardUuidIfNotPresentThunk = () => (dispatch: any, getState: any) => { + const { uuid } = getState().dashboard; + if (!uuid) { + dispatch(setDashboardUuid(createUUID())); + } +}; + export function upgradeDashboardVersion(dashboard: any, origin: string, target: string) { if (origin == '2.2' && target == '2.3') { dashboard.pages.forEach((p) => { diff --git a/src/dashboard/header/DashboardHeader.tsx b/src/dashboard/header/DashboardHeader.tsx index a218bc4be..c67d37c2c 100644 --- a/src/dashboard/header/DashboardHeader.tsx +++ b/src/dashboard/header/DashboardHeader.tsx @@ -47,7 +47,7 @@ export const NeoDashboardHeader = ({ }, [isDarkMode]); const content = ( -
+
diff --git a/src/dashboard/header/DashboardHeaderPageTitle.tsx b/src/dashboard/header/DashboardHeaderPageTitle.tsx index 8bdbcaef6..86eb241bb 100644 --- a/src/dashboard/header/DashboardHeaderPageTitle.tsx +++ b/src/dashboard/header/DashboardHeaderPageTitle.tsx @@ -61,7 +61,11 @@ export const DashboardHeaderPageTitle = ({ title, tabIndex, removePage, setPageT
{!editing ? ( - title + title ? ( + title + ) : ( + '(no title)' + ) ) : ( ) : (
- {dashboardTitle} + {dashboardTitle ? dashboardTitle : '(no title)'} {editable ? ( + + {/* Saving, loading, extensions, sharing is only enabled when the dashboard is editable. */} - {editable ? ( - <> - - - - {renderExtensionsButtons()} - - ) : ( - <> - )} + {editable ? <>{renderExtensionsButtons()} : <>}
diff --git a/src/dashboard/sidebar/DashboardSidebar.tsx b/src/dashboard/sidebar/DashboardSidebar.tsx new file mode 100644 index 000000000..1647ee831 --- /dev/null +++ b/src/dashboard/sidebar/DashboardSidebar.tsx @@ -0,0 +1,493 @@ +import React, { useContext, useState } from 'react'; +import { connect } from 'react-redux'; +import { getDashboardIsEditable, getPageNumber } from '../../settings/SettingsSelectors'; +import { getDashboardSettings, getDashboardTitle } from '../DashboardSelectors'; +import { Button, SideNavigation, SideNavigationGroupHeader, SideNavigationList, TextInput } from '@neo4j-ndl/react'; +import { removeReportThunk } from '../../page/PageThunks'; +import { PlusIconOutline, MagnifyingGlassIconOutline, CircleStackIconOutline } from '@neo4j-ndl/react/icons'; +import Tooltip from '@mui/material/Tooltip'; +import { DashboardSidebarListItem } from './DashboardSidebarListItem'; +import { + applicationGetConnection, + applicationGetConnectionDatabase, + applicationIsStandalone, + dashboardIsDraft, +} from '../../application/ApplicationSelectors'; +import { setDraft } from '../../application/ApplicationActions'; +import NeoDashboardSidebarLoadModal from './modal/DashboardSidebarLoadModal'; +import { resetDashboardState } from '../DashboardActions'; +import NeoDashboardSidebarCreateModal from './modal/DashboardSidebarCreateModal'; +import NeoDashboardSidebarDatabaseMenu from './menu/DashboardSidebarDatabaseMenu'; +import NeoDashboardSidebarDashboardMenu from './menu/DashboardSidebarDashboardMenu'; +import { + deleteDashboardFromNeo4jThunk, + loadDashboardFromNeo4jThunk, + loadDashboardListFromNeo4jThunk, + loadDashboardThunk, + loadDatabaseListFromNeo4jThunk, + saveDashboardToNeo4jThunk, +} from '../DashboardThunks'; +import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; +import NeoDashboardSidebarSaveModal from './modal/DashboardSidebarSaveModal'; +import { getDashboardJson } from '../../modal/ModalSelectors'; +import NeoDashboardSidebarCreateMenu from './menu/DashboardSidebarCreateMenu'; +import NeoDashboardSidebarImportModal from './modal/DashboardSidebarImportModal'; +import { createUUID } from '../../utils/uuid'; +import NeoDashboardSidebarExportModal from './modal/DashboardSidebarExportModal'; +import NeoDashboardSidebarDeleteModal from './modal/DashboardSidebarDeleteModal'; +import NeoDashboardSidebarInfoModal from './modal/DashboardSidebarInfoModal'; +import NeoDashboardSidebarShareModal from './modal/DashboardSidebarShareModal'; +import LegacyShareModal from './modal/legacy/LegacyShareModal'; + +enum Menu { + DASHBOARD, + DATABASE, + CREATE, + NONE, +} + +enum Modal { + CREATE, + IMPORT, + EXPORT, + DELETE, + SHARE, + SHARE_LEGACY, + INFO, + LOAD, + SAVE, + NONE, +} + +/** + * A component responsible for rendering the sidebar on the left of the screen. + */ +export const NeoDashboardSidebar = ({ + database, + connection, + title, + readonly, + draft, + setDraft, + dashboard, + resetLocalDashboard, + loadDashboard, + loadDatabaseListFromNeo4j, + loadDashboardListFromNeo4j, + loadDashboardFromNeo4j, + saveDashboardToNeo4j, + deleteDashboardFromNeo4j, +}) => { + const { driver } = useContext(Neo4jContext); + const [expanded, setOnExpanded] = useState(false); + const [selectedDashboardIndex, setSelectedDashboardIndex] = React.useState(-1); + const [dashboardDatabase, setDashboardDatabase] = React.useState(database ? database : 'neo4j'); + const [databases, setDatabases] = useState([]); + const [inspectedIndex, setInspectedIndex] = useState(-1); + const [searchText, setSearchText] = useState(''); + const [menuAnchor, setMenuAnchor] = useState(null); + const [menuOpen, setMenuOpen] = useState(Menu.NONE); + const [modalOpen, setModalOpen] = useState(Modal.NONE); + const [dashboards, setDashboards] = React.useState([]); + const [cachedDashboard, setCachedDashboard] = React.useState(''); + + const getDashboardListFromNeo4j = () => { + // Retrieves list of all dashboards stored in a given database. + loadDashboardListFromNeo4j(driver, dashboardDatabase, (list) => { + setDashboards(list); + + // Update the UI to reflect the currently selected dashboard. + if (dashboard && dashboard.uuid) { + const index = list.findIndex((element) => element.uuid == dashboard.uuid); + setSelectedDashboardIndex(index); + if (index == -1) { + // If we can't find the currently dashboard in the database, we are drafting a new one. + setDraft(true); + } + } + }); + }; + + function createDashboard() { + // Creates new dashboard in draft state (not yet saved to Neo4j) + resetLocalDashboard(); + setDraft(true); + } + + function deleteDashboard(uuid) { + // Creates new dashboard in draft state (not yet saved to Neo4j) + deleteDashboardFromNeo4j(driver, dashboardDatabase, uuid, () => { + if (uuid == dashboard.uuid) { + setSelectedDashboardIndex[0]; + resetLocalDashboard(); + setDraft(true); + } + setTimeout(() => { + getDashboardListFromNeo4j(); + }, 100); + }); + } + + return ( +
+ { + saveDashboardToNeo4j( + driver, + dashboardDatabase, + dashboard, + new Date().toISOString(), + connection.username, + () => { + // After saving successfully, refresh the list after a small delay. + // The new dashboard will always be on top (the latest), so we select index 0. + setDashboards([]); + setTimeout(() => { + getDashboardListFromNeo4j(); + setSelectedDashboardIndex(0); + setDraft(false); + }, 100); + } + ); + }} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + { + setModalOpen(Modal.LOAD); + const { uuid } = dashboards[inspectedIndex]; + loadDashboardFromNeo4j(driver, dashboardDatabase, uuid, (file) => { + loadDashboard(uuid, file); + setSelectedDashboardIndex(inspectedIndex); + setDraft(false); + }); + }} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + { + setModalOpen(Modal.NONE); + }} + onLegacyShareClicked={() => setModalOpen(Modal.SHARE_LEGACY)} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + setModalOpen(Modal.NONE)} /> + + { + setModalOpen(Modal.NONE); + createDashboard(); + }} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + { + setModalOpen(Modal.NONE); + if (dashboards[inspectedIndex]) { + deleteDashboard(dashboards[inspectedIndex].uuid); + } + }} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + { + setModalOpen(Modal.NONE); + setDraft(true); + loadDashboard(createUUID(), text); + }} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + { + setModalOpen(Modal.NONE); + setCachedDashboard(''); + }} + /> + + { + setModalOpen(Modal.NONE); + setCachedDashboard(''); + }} + /> + + { + setOnExpanded(open); + if (open) { + getDashboardListFromNeo4j(); + } + // Wait until the sidebar has fully opened. Then trigger a resize event to align the grid layout. + const timeout = setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, 300); + }} + > + + { + setDashboardDatabase(newDatabase); + // We changed the active dashboard database, reload the list in the sidebar. + loadDashboardListFromNeo4j(driver, newDatabase, (list) => { + setDashboards(list); + setDraft(true); + }); + }} + open={menuOpen == Menu.DATABASE} + anchorEl={menuAnchor} + handleClose={() => { + setMenuOpen(Menu.NONE); + setMenuAnchor(null); + }} + /> + { + setMenuOpen(Menu.NONE); + const d = dashboards[inspectedIndex]; + loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (text) => { + setCachedDashboard(JSON.parse(text)); + }); + setModalOpen(Modal.INFO); + }} + handleLoadClicked={() => { + setMenuOpen(Menu.NONE); + if (draft) { + setModalOpen(Modal.LOAD); + } else { + const d = dashboards[inspectedIndex]; + loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (file) => { + loadDashboard(d.uuid, file); + setSelectedDashboardIndex(inspectedIndex); + }); + } + }} + handleExportClicked={() => { + setMenuOpen(Menu.NONE); + const d = dashboards[inspectedIndex]; + loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (text) => { + setCachedDashboard(JSON.parse(text)); + }); + setModalOpen(Modal.EXPORT); + }} + handleShareClicked={() => { + setMenuOpen(Menu.NONE); + setModalOpen(Modal.SHARE); + }} + handleDeleteClicked={() => { + setMenuOpen(Menu.NONE); + setModalOpen(Modal.DELETE); + }} + handleClose={() => { + setMenuOpen(Menu.NONE); + setMenuAnchor(null); + }} + /> + + { + setMenuOpen(Menu.NONE); + if (draft) { + setModalOpen(Modal.CREATE); + } else { + createDashboard(); + } + }} + handleImportClicked={() => { + setMenuOpen(Menu.NONE); + if (draft) { + setModalOpen(Modal.IMPORT); + } else { + setModalOpen(Modal.IMPORT); + } + }} + handleClose={() => { + setMenuOpen(Menu.NONE); + setMenuAnchor(null); + }} + /> + + +
+ + Dashboards + + {/* Only let users create dashboards and change database when running in editor mode. */} + {readonly == false ? ( + <> + + + + + + + + + ) : ( + <> + )} +
+
+
+ + + } + className='n-w-full n-mr-2' + placeholder='Search...' + aria-label='Search' + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + /> + + {draft && !readonly ? ( + {}} + onSave={() => setModalOpen(Modal.SAVE)} + onSettingsOpen={() => {}} + /> + ) : ( + <> + )} + {dashboards + .filter((d) => d.title.toLowerCase().includes(searchText.toLowerCase())) + .map((d) => { + // index stored in list + return ( + { + if (draft) { + setInspectedIndex(d.index); + setModalOpen(Modal.LOAD); + } else { + loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (file) => { + loadDashboard(d.uuid, file); + setSelectedDashboardIndex(d.index); + }); + } + }} + onSave={() => {}} + onSettingsOpen={(event) => { + setInspectedIndex(d.index); + setMenuOpen(Menu.DASHBOARD); + setMenuAnchor(event.currentTarget); + }} + /> + ); + })} + +
+
+ ); +}; + +const mapStateToProps = (state) => ({ + readonly: applicationIsStandalone(state), + connection: applicationGetConnection(state), + pagenumber: getPageNumber(state), + title: getDashboardTitle(state), + editable: getDashboardIsEditable(state), + draft: dashboardIsDraft(state), + dashboard: getDashboardJson(state), + dashboardSettings: getDashboardSettings(state), + database: applicationGetConnectionDatabase(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + onRemovePressed: (id) => dispatch(removeReportThunk(id)), + resetLocalDashboard: () => dispatch(resetDashboardState()), + setDraft: (draft) => dispatch(setDraft(draft)), + loadDashboard: (uuid, text) => dispatch(loadDashboardThunk(uuid, text)), + loadDatabaseListFromNeo4j: (driver, callback) => dispatch(loadDatabaseListFromNeo4jThunk(driver, callback)), + loadDashboardFromNeo4j: (driver, database, uuid, callback) => + dispatch(loadDashboardFromNeo4jThunk(driver, database, uuid, callback)), + loadDashboardListFromNeo4j: (driver, database, callback) => + dispatch(loadDashboardListFromNeo4jThunk(driver, database, callback)), + saveDashboardToNeo4j: (driver: any, database: string, dashboard: any, date: any, user: any, onSuccess) => { + dispatch(saveDashboardToNeo4jThunk(driver, database, dashboard, date, user, onSuccess)); + }, + deleteDashboardFromNeo4j: (driver: any, database: string, uuid: string, onSuccess) => { + dispatch(deleteDashboardFromNeo4jThunk(driver, database, uuid, onSuccess)); + }, +}); + +export default connect(mapStateToProps, mapDispatchToProps)(NeoDashboardSidebar); diff --git a/src/dashboard/sidebar/DashboardSidebarListItem.tsx b/src/dashboard/sidebar/DashboardSidebarListItem.tsx new file mode 100644 index 000000000..436371ae3 --- /dev/null +++ b/src/dashboard/sidebar/DashboardSidebarListItem.tsx @@ -0,0 +1,73 @@ +import { Button, IconButton, SideNavigationGroupHeader } from '@neo4j-ndl/react'; +import React from 'react'; +import { + CloudArrowDownIconOutline, + CloudArrowUpIconOutline, + EllipsisVerticalIconOutline, +} from '@neo4j-ndl/react/icons'; +import Tooltip from '@mui/material/Tooltip'; + +export const DashboardSidebarListItem = ({ title, selected, readonly, saved, onSelect, onSave, onSettingsOpen }) => { + return ( + +
+ + {readonly !== true ? ( + { + saved == false ? onSave() : onSettingsOpen(event); + }} + > + {saved == true ? ( + + + + ) : ( + + + + )} + + ) : ( + <> + )} +
+
+ ); +}; diff --git a/src/dashboard/sidebar/menu/DashboardSidebarCreateMenu.tsx b/src/dashboard/sidebar/menu/DashboardSidebarCreateMenu.tsx new file mode 100644 index 000000000..d7e06238a --- /dev/null +++ b/src/dashboard/sidebar/menu/DashboardSidebarCreateMenu.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Menu, MenuItem, MenuItems } from '@neo4j-ndl/react'; +import { DocumentTextIconOutline, PlusCircleIconOutline } from '@neo4j-ndl/react/icons'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarCreateMenu = ({ + anchorEl, + open, + handleNewClicked, + handleImportClicked, + handleClose, +}) => { + return ( + + + + + + + ); +}; + +export default NeoDashboardSidebarCreateMenu; diff --git a/src/dashboard/sidebar/menu/DashboardSidebarDashboardMenu.tsx b/src/dashboard/sidebar/menu/DashboardSidebarDashboardMenu.tsx new file mode 100644 index 000000000..aa9956c11 --- /dev/null +++ b/src/dashboard/sidebar/menu/DashboardSidebarDashboardMenu.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Menu, MenuItem, MenuItems } from '@neo4j-ndl/react'; +import { + CloudArrowUpIconOutline, + DocumentDuplicateIconOutline, + DocumentTextIconOutline, + InformationCircleIconOutline, + ShareIconOutline, + TrashIconOutline, +} from '@neo4j-ndl/react/icons'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarDashboardMenu = ({ + anchorEl, + open, + handleInfoClicked, + handleLoadClicked, + handleExportClicked, + handleShareClicked, + handleDeleteClicked, + handleClose, +}) => { + return ( + + + } title='Info' /> + } title='Load' /> + {/* {}} icon={} title='Clone' /> */} + } title='Export' /> + } title='Share' /> + } title='Delete' /> + + + ); +}; + +export default NeoDashboardSidebarDashboardMenu; diff --git a/src/dashboard/sidebar/menu/DashboardSidebarDatabaseMenu.tsx b/src/dashboard/sidebar/menu/DashboardSidebarDatabaseMenu.tsx new file mode 100644 index 000000000..d3c783e17 --- /dev/null +++ b/src/dashboard/sidebar/menu/DashboardSidebarDatabaseMenu.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Button, Dialog, Menu, MenuItem, MenuItems } from '@neo4j-ndl/react'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarDatabaseMenu = ({ anchorEl, open, handleClose, databases, selected, setSelected }) => { + return ( + + + {databases.map((d) => { + return ( + { + setSelected(d); + }} + title={d} + style={ + d == selected + ? { + borderWidth: '1px', + borderStyle: 'solid', + color: 'rgb(var(--palette-primary-bg-strong))', + borderColor: 'rgb(var(--palette-primary-bg-strong))', + borderRadius: '8px', + } + : {} + } + /> + ); + })} + + + ); +}; + +export default NeoDashboardSidebarDatabaseMenu; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarCreateModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarCreateModal.tsx new file mode 100644 index 000000000..4f998a09c --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarCreateModal.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button, Dialog } from '@neo4j-ndl/react'; +import { BackspaceIconOutline, ExclamationTriangleIconOutline } from '@neo4j-ndl/react/icons'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarCreateModal = ({ open, onConfirm, handleClose }) => { + return ( + + Discard Draft? + + Creating a new dashboard will delete your current draft. Save the draft first to ensure your dashboard is + stored. + + + + + + + ); +}; + +export default NeoDashboardSidebarCreateModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarDeleteModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarDeleteModal.tsx new file mode 100644 index 000000000..023ac2ce6 --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarDeleteModal.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button, Dialog } from '@neo4j-ndl/react'; +import { BackspaceIconOutline, TrashIconSolid } from '@neo4j-ndl/react/icons'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarDeleteModal = ({ open, title, onConfirm, handleClose }) => { + return ( + + Delete Dashboard '{title}'? + + Are you sure you want to delete this dashboard?
This action cannot be undone. +
+ + + + +
+ ); +}; + +export default NeoDashboardSidebarDeleteModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarExportModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarExportModal.tsx new file mode 100644 index 000000000..b458bba55 --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarExportModal.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { DocumentArrowDownIconOutline } from '@neo4j-ndl/react/icons'; +import { Button, Dialog } from '@neo4j-ndl/react'; +import { valueIsArray, valueIsObject } from '../../../chart/ChartUtils'; +import { TextareaAutosize } from '@mui/material'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarExportModal = ({ open, dashboard, handleClose }) => { + /** + * Removes the specified set of keys from the nested dictionary. + */ + const filterNestedDict = (value: any, removedKeys: any[]) => { + if (value == undefined) { + return value; + } + + if (valueIsArray(value)) { + return value.map((v) => filterNestedDict(v, removedKeys)); + } + + if (valueIsObject(value)) { + const newValue = {}; + Object.keys(value).forEach((k) => { + if (removedKeys.indexOf(k) != -1) { + newValue[k] = undefined; + } else { + newValue[k] = filterNestedDict(value[k], removedKeys); + } + }); + return newValue; + } + return value; + }; + + const filteredDashboard = filterNestedDict(dashboard, [ + 'fields', + 'settingsOpen', + 'advancedSettingsOpen', + 'collapseTimeout', + 'apiKey', // Added for query-translator extension + ]); + + const dashboardString = JSON.stringify(filteredDashboard, null, 2); + const downloadDashboard = () => { + const element = document.createElement('a'); + const file = new Blob([dashboardString], { type: 'text/plain' }); + element.href = URL.createObjectURL(file); + element.download = 'dashboard.json'; + document.body.appendChild(element); // Required for this to work in FireFox + element.click(); + }; + + return ( + + Export Dashboard + + Export your dashboard as a JSON file, or copy-paste the file from here. +
+ +
+
+ +
+
+ ); +}; + +export default NeoDashboardSidebarExportModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarImportModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarImportModal.tsx new file mode 100644 index 000000000..32eaed726 --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarImportModal.tsx @@ -0,0 +1,73 @@ +import React, { useRef } from 'react'; +import { PlayIconSolid, DocumentPlusIconOutline } from '@neo4j-ndl/react/icons'; +import { Button, Checkbox, Dialog, Dropdown } from '@neo4j-ndl/react'; +import TextareaAutosize from '@mui/material/TextareaAutosize'; + +export const NeoDashboardSidebarImportModal = ({ open, onImport, handleClose }) => { + const [text, setText] = React.useState(''); + const loadFromFile = useRef(null); + + const reader = new FileReader(); + reader.onload = (e) => { + setText(e.target.result); + }; + + return ( + + Import Dashboard + + Import your dashboard from a JSON file, or copy-paste the save file here. +
+ Importing will discard your current draft, if any. +

+
+ setText(e.target.value)} + value={text} + aria-label='' + placeholder='Paste a dashboard JSON file here...' + /> + + + + +
+ ); +}; + +export default NeoDashboardSidebarImportModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarInfoModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarInfoModal.tsx new file mode 100644 index 000000000..457adc3f1 --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarInfoModal.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Dialog } from '@neo4j-ndl/react'; +import { DataGrid } from '@mui/x-data-grid'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarInfoModal = ({ open, dashboard, handleClose }) => { + const columns = [ + { field: 'field', headerName: 'Field', width: 150 }, + { field: 'value', headerName: 'Value', width: 600 }, + ]; + + const rows = dashboard + ? [ + { id: 0, field: 'ID', value: dashboard.uuid }, + { id: 1, field: 'Title', value: dashboard.title }, + { id: 2, field: 'Last Modified', value: dashboard.date }, + { id: 3, field: 'Author', value: dashboard.author }, + { id: 4, field: 'Version', value: dashboard.version }, + ] + : []; + + return ( + + About '{dashboard && dashboard.title}' + +
+ <>, + ColumnSortedAscendingIcon: () => <>, + }} + /> +
+
+
+ ); +}; + +export default NeoDashboardSidebarInfoModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarLoadModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarLoadModal.tsx new file mode 100644 index 000000000..17da9b9bb --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarLoadModal.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Button, Dialog } from '@neo4j-ndl/react'; +import { BackspaceIconOutline, ExclamationTriangleIconOutline, TrashIconOutline } from '@neo4j-ndl/react/icons'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarLoadModal = ({ open, onConfirm, handleClose }) => { + return ( + + Discard Draft? + + Switching your active dashboard will delete your current draft. +
+ Save the draft first to ensure your dashboard is stored. +
+ + + + +
+ ); +}; + +export default NeoDashboardSidebarLoadModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarSaveModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarSaveModal.tsx new file mode 100644 index 000000000..a307c5570 --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarSaveModal.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { DatabaseAddCircleIcon, BackspaceIconOutline } from '@neo4j-ndl/react/icons'; +import { Button, Dialog } from '@neo4j-ndl/react'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarSaveModal = ({ open, onConfirm, handleClose }) => { + return ( + + Save to Neo4j + + This will save your current draft as a node to your Neo4j database. +
+ Ensure you have write permissions to the database to use this feature. +
+ + + + +
+ ); +}; + +export default NeoDashboardSidebarSaveModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarShareModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarShareModal.tsx new file mode 100644 index 000000000..95dd9acde --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarShareModal.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { Checkbox, Dialog, TextLink } from '@neo4j-ndl/react'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarShareModal = ({ + uuid, + dashboardDatabase, + connection, + open, + onLegacyShareClicked, + handleClose, +}) => { + const shareBaseURL = 'http://neodash.graphapp.io'; + const shareBaseURLAlternative = 'https://neodash.graphapp.io'; + const shareLocalURL = window.location.origin.startsWith('file') ? shareBaseURL : window.location.origin; + const [selfHosted, setSelfHosted] = React.useState(false); + const [standalone, setStandalone] = React.useState(false); + const [includeCredentials, setIncludeCredentials] = React.useState(false); + + function getShareURL() { + const prefix = selfHosted ? shareLocalURL : shareBaseURL; + const id = encodeURIComponent(uuid); + const db = encodeURIComponent(dashboardDatabase); + const suffix1 = includeCredentials + ? `&credentials=${encodeURIComponent( + `${connection.protocol}://${connection.username}:${connection.password}@${connection.database}:${connection.url}:${connection.port}` + )}` + : ''; + const suffix2 = standalone ? `&standalone=Yes` : ''; + return `${prefix}/?share&type=database&id=${id}&dashboardDatabase=${db}${suffix1}${suffix2}`; + } + + return ( + + Share Dashboard + + This screen lets you create a one-off, direct link for your dashboard. Click{' '} + here to use legacy file-sharing instead. + + + {shareLocalURL !== shareBaseURL && shareLocalURL !== shareBaseURLAlternative ? ( + { + setSelfHosted(!selfHosted); + }} + /> + ) : ( + <> + )} + { + setStandalone(!standalone); + }} + /> + + { + setIncludeCredentials(!includeCredentials); + }} + /> + +
+ Your Temporary Link: +
+ + + {' '} + {getShareURL()}{' '} + +
+ {includeCredentials ? Caution: this link embeds your current database credentials. : <>} +
+
+
+ ); +}; + +export default NeoDashboardSidebarShareModal; diff --git a/src/dashboard/sidebar/modal/legacy/LegacyShareModal.tsx b/src/dashboard/sidebar/modal/legacy/LegacyShareModal.tsx new file mode 100644 index 000000000..f1fa344ee --- /dev/null +++ b/src/dashboard/sidebar/modal/legacy/LegacyShareModal.tsx @@ -0,0 +1,212 @@ +import React, { useContext } from 'react'; + +import { connect } from 'react-redux'; +import { DataGrid } from '@mui/x-data-grid'; +import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; +import NeoSetting from '../../../../component/field/Setting'; +import { applicationGetConnection } from '../../../../application/ApplicationSelectors'; +import { SELECTION_TYPES } from '../../../../config/CardConfig'; +import { MenuItem, Button, Dialog, Dropdown, TextLink } from '@neo4j-ndl/react'; +import { + ShareIconOutline, + PlayIconSolid, + DocumentCheckIconOutline, + DatabaseAddCircleIcon, +} from '@neo4j-ndl/react/icons'; + +const shareBaseURL = 'http://neodash.graphapp.io'; +const shareLocalURL = window.location.origin.startsWith('file') ? shareBaseURL : window.location.origin; + +export const NeoShareModal = ({ open, handleClose, connection }) => { + const [loadFromNeo4jModalOpen, setLoadFromNeo4jModalOpen] = React.useState(false); + const [loadFromFileModalOpen, setLoadFromFileModalOpen] = React.useState(false); + const [rows, setRows] = React.useState([]); + const { driver } = useContext(Neo4jContext); + + // One of [null, database, file] + const shareType = 'url'; + const [shareID, setShareID] = React.useState(null); + const [shareName, setShareName] = React.useState(null); + const [shareConnectionDetails, setShareConnectionDetails] = React.useState('No'); + const [shareStandalone, setShareStandalone] = React.useState('No'); + const [selfHosted, setSelfHosted] = React.useState('No'); + const [shareLink, setShareLink] = React.useState(null); + + const [dashboardDatabase, setDashboardDatabase] = React.useState('neo4j'); + + const columns = [ + { field: 'uuid', hide: true, headerName: 'ID', width: 150 }, + { field: 'date', headerName: 'Date', width: 200 }, + { field: 'title', headerName: 'Title', width: 370 }, + { field: 'author', headerName: 'Author', width: 160 }, + { + field: 'load', + headerName: ' ', + renderCell: (c) => { + return ( + + ); + }, + width: 130, + }, + ]; + + return ( + + + + Share Dashboard File + + + This window lets you create a temporary share link for your dashboard. Keep in mind that share links are not + intended as a way to publish your dashboard for users, see the  + + documentation + {' '} + for more on publishing. +
+
+
+ To share a dashboard file directly, make it accessible{' '} + + online + + .
Then, paste the direct link here: + { + setShareLink(null); + setShareID(e); + }} + /> + {shareID ? ( + <> +
+ { + if ((e == 'No') & (shareStandalone == 'Yes')) { + return; + } + setShareLink(null); + setShareConnectionDetails(e); + }} + /> + {shareLocalURL != shareBaseURL ? ( + { + setShareLink(null); + setShareStandalone(e); + if (e == 'Yes') { + setShareConnectionDetails('Yes'); + } + }} + /> + ) : ( + <> + )} + { + setShareLink(null); + setSelfHosted(e); + }} + /> + + + ) : ( + <> + )} + {shareLink ? ( + <> +
+ Use the generated link to view the dashboard: +
+ + {shareLink} + +
+ + ) : ( + <> + )} +
+
+ ); +}; + +const mapStateToProps = (state) => ({ + connection: applicationGetConnection(state), +}); + +const mapDispatchToProps = () => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(NeoShareModal); diff --git a/src/modal/ExportModal.tsx b/src/modal/ExportModal.tsx new file mode 100644 index 000000000..877f34558 --- /dev/null +++ b/src/modal/ExportModal.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { MenuItem } from '@neo4j-ndl/react'; +import NeoDashboardSidebarExportModal from '../dashboard/sidebar/modal/DashboardSidebarExportModal'; +import { getDashboardJson } from './ModalSelectors'; +import { DocumentTextIconOutline } from '@neo4j-ndl/react/icons'; +/** + * A modal to save a dashboard as a JSON text string. + * The button to open the modal is intended to use in a drawer at the side of the page. + */ +export const NeoExportModal = ({ dashboard }) => { + const [open, setOpen] = React.useState(false); + return ( +
+ setOpen(true)} icon={} title='Export' /> + { + setOpen(false); + }} + /> +
+ ); +}; + +const mapStateToProps = (state) => ({ + dashboard: getDashboardJson(state), +}); + +const mapDispatchToProps = () => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(NeoExportModal); diff --git a/src/modal/LoadModal.tsx b/src/modal/LoadModal.tsx deleted file mode 100644 index 1331612ae..000000000 --- a/src/modal/LoadModal.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import React, { useContext, useRef } from 'react'; -import { TextareaAutosize } from '@mui/material'; -import { connect } from 'react-redux'; -import { - loadDashboardFromNeo4jByUUIDThunk, - loadDashboardListFromNeo4jThunk, - loadDashboardThunk, - loadDatabaseListFromNeo4jThunk, -} from '../dashboard/DashboardThunks'; -import { DataGrid } from '@mui/x-data-grid'; -import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; -import { MenuItem, Button, Dialog, Dropdown } from '@neo4j-ndl/react'; -import { - CloudArrowUpIconOutline, - PlayIconSolid, - DatabaseAddCircleIcon, - DocumentPlusIconOutline, -} from '@neo4j-ndl/react/icons'; - -/** - * A modal to save a dashboard as a JSON text string. - * The button to open the modal is intended to use in a drawer at the side of the page. - */ - -export const NeoLoadModal = ({ - loadDashboard, - loadDatabaseListFromNeo4j, - loadDashboardFromNeo4j, - loadDashboardListFromNeo4j, -}) => { - const [loadModalOpen, setLoadModalOpen] = React.useState(false); - const [loadFromNeo4jModalOpen, setLoadFromNeo4jModalOpen] = React.useState(false); - const [text, setText] = React.useState(''); - const [rows, setRows] = React.useState([]); - const { driver } = useContext(Neo4jContext); - const [dashboardDatabase, setDashboardDatabase] = React.useState('neo4j'); - const [databases, setDatabases] = React.useState(['neo4j']); - const loadFromFile = useRef(null); - - const handleClickOpen = () => { - setLoadModalOpen(true); - }; - - const handleClose = () => { - setLoadModalOpen(false); - }; - - const handleCloseAndLoad = () => { - setLoadModalOpen(false); - loadDashboard(text); - setText(''); - }; - - function handleDashboardLoadedFromNeo4j(result) { - setText(result); - setLoadFromNeo4jModalOpen(false); - } - - const reader = new FileReader(); - reader.onload = (e) => { - setText(e.target.result); - }; - - const uploadDashboard = (e) => { - e.preventDefault(); - reader.readAsText(e.target.files[0]); - }; - - const columns = [ - { field: 'id', hide: true, headerName: 'ID', width: 150 }, - { field: 'date', headerName: 'Date', width: 200 }, - { field: 'title', headerName: 'Title', width: 300 }, - { field: 'author', headerName: 'Author', width: 160 }, - { field: 'version', headerName: 'Version', width: 85 }, - { - field: 'load', - headerName: 'Select', - renderCell: (c) => { - return ( - - ); - }, - width: 130, - }, - ]; - - return ( - <> - } /> - - - - - Load Dashboard - - -
- - - - -
- - setText(e.target.value)} - value={text} - aria-label='' - placeholder='Select a dashboard first, then preview it here...' - /> -
-
- { - setLoadFromNeo4jModalOpen(false); - }} - aria-labelledby='form-dialog-title' - > - Select from Neo4j - If dashboards are saved in your current database, choose a dashboard below. - -
- <>, - ColumnSortedAscendingIcon: () => <>, - }} - /> -
- { - setRows([]); - setDashboardDatabase(newValue.value); - loadDashboardListFromNeo4j(driver, newValue.value, (result) => { - setRows(result); - }); - }, - options: databases.map((database) => ({ label: database, value: database })), - value: { label: dashboardDatabase, value: dashboardDatabase }, - menuPlacement: 'auto', - }} - style={{ width: '150px', marginTop: '-65px' }} - > -
-
- - ); -}; - -const mapStateToProps = () => ({}); - -const mapDispatchToProps = (dispatch) => ({ - loadDashboard: (text) => dispatch(loadDashboardThunk(text)), - loadDashboardFromNeo4j: (driver, database, uuid, callback) => - dispatch(loadDashboardFromNeo4jByUUIDThunk(driver, database, uuid, callback)), - loadDashboardListFromNeo4j: (driver, database, callback) => - dispatch(loadDashboardListFromNeo4jThunk(driver, database, callback)), - loadDatabaseListFromNeo4j: (driver, callback) => dispatch(loadDatabaseListFromNeo4jThunk(driver, callback)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(NeoLoadModal); diff --git a/src/modal/SaveModal.tsx b/src/modal/SaveModal.tsx deleted file mode 100644 index 41383e830..000000000 --- a/src/modal/SaveModal.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import React, { useContext, useEffect } from 'react'; -import { FormControl, TextareaAutosize, Tooltip } from '@mui/material'; -import { connect } from 'react-redux'; -import { getDashboardJson } from './ModalSelectors'; -import { valueIsArray, valueIsObject } from '../chart/ChartUtils'; -import { applicationGetConnection } from '../application/ApplicationSelectors'; -import { loadDatabaseListFromNeo4jThunk, saveDashboardToNeo4jThunk } from '../dashboard/DashboardThunks'; -import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; -import { - CloudArrowDownIconOutline, - DatabaseAddCircleIcon, - DocumentArrowDownIconOutline, - BackspaceIconOutline, -} from '@neo4j-ndl/react/icons'; -import { Button, Checkbox, Dialog, Dropdown, MenuItem } from '@neo4j-ndl/react'; - -/** - * Removes the specified set of keys from the nested dictionary. - */ -const filterNestedDict = (value: any, removedKeys: any[]) => { - if (value == undefined) { - return value; - } - - if (valueIsArray(value)) { - return value.map((v) => filterNestedDict(v, removedKeys)); - } - - if (valueIsObject(value)) { - const newValue = {}; - Object.keys(value).forEach((k) => { - if (removedKeys.indexOf(k) != -1) { - newValue[k] = undefined; - } else { - newValue[k] = filterNestedDict(value[k], removedKeys); - } - }); - return newValue; - } - return value; -}; - -/** - * A modal to save a dashboard as a JSON text string. - * The button to open the modal is intended to use in a drawer at the side of the page. - */ - -export const NeoSaveModal = ({ dashboard, connection, saveDashboardToNeo4j, loadDatabaseListFromNeo4j }) => { - const [saveModalOpen, setSaveModalOpen] = React.useState(false); - const [saveToNeo4jModalOpen, setSaveToNeo4jModalOpen] = React.useState(false); - const [overwriteExistingDashboard, setOverwriteExistingDashboard] = React.useState(false); - const [dashboardDatabase, setDashboardDatabase] = React.useState('neo4j'); - const [databases, setDatabases] = React.useState(['neo4j']); - - const { driver } = useContext(Neo4jContext); - - useEffect(() => { - loadDatabaseListFromNeo4j(driver, (result) => { - setDatabases(result); - }); - }, []); - - const handleClickOpen = () => { - setSaveModalOpen(true); - }; - - const handleClose = () => { - setSaveModalOpen(false); - }; - - const filteredDashboard = filterNestedDict(dashboard, [ - 'fields', - 'settingsOpen', - 'advancedSettingsOpen', - 'collapseTimeout', - 'apiKey', // Added for query-translator extension - ]); - - const dashboardString = JSON.stringify(filteredDashboard, null, 2); - const downloadDashboard = () => { - const element = document.createElement('a'); - const file = new Blob([dashboardString], { type: 'text/plain' }); - element.href = URL.createObjectURL(file); - element.download = 'dashboard.json'; - document.body.appendChild(element); // Required for this to work in FireFox - element.click(); - }; - - return ( - <> - } /> - - - - - Save Dashboard - - -
- - -
- -
-
- - { - setSaveToNeo4jModalOpen(false); - }} - aria-labelledby='form-dialog-title' - > - Save to Neo4j - - This will save your current dashboard as a node to your active Neo4j database. -
- Ensure you have write permissions to the database to use this feature. - - { - newValue && setDashboardDatabase(newValue.value); - }, - options: databases.map((database) => ({ label: database, value: database })), - value: { label: dashboardDatabase, value: dashboardDatabase }, - menuPlacement: 'auto', - }} - style={{ width: '150px', display: 'inline-block' }} - > - - - setOverwriteExistingDashboard(!overwriteExistingDashboard)} - label='Overwrite' - /> - - -
- - - - -
- - ); -}; - -const mapStateToProps = (state) => ({ - dashboard: getDashboardJson(state), - connection: applicationGetConnection(state), -}); - -const mapDispatchToProps = (dispatch) => ({ - saveDashboardToNeo4j: (driver: any, database: string, dashboard: any, date: any, user: any, overwrite: boolean) => { - dispatch(saveDashboardToNeo4jThunk(driver, database, dashboard, date, user, overwrite)); - }, - loadDatabaseListFromNeo4j: (driver, callback) => dispatch(loadDatabaseListFromNeo4jThunk(driver, callback)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(NeoSaveModal); diff --git a/src/modal/ShareModal.tsx b/src/modal/ShareModal.tsx deleted file mode 100644 index ff0392411..000000000 --- a/src/modal/ShareModal.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import React, { useContext } from 'react'; - -import { connect } from 'react-redux'; -import { DataGrid } from '@mui/x-data-grid'; -import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; -import NeoSetting from '../component/field/Setting'; -import { loadDashboardListFromNeo4jThunk, loadDatabaseListFromNeo4jThunk } from '../dashboard/DashboardThunks'; -import { applicationGetConnection } from '../application/ApplicationSelectors'; -import { SELECTION_TYPES } from '../config/CardConfig'; -import { MenuItem, Button, Dialog, Dropdown, TextLink } from '@neo4j-ndl/react'; -import { - ShareIconOutline, - PlayIconSolid, - DocumentCheckIconOutline, - DatabaseAddCircleIcon, -} from '@neo4j-ndl/react/icons'; - -const shareBaseURL = 'http://neodash.graphapp.io'; -const shareLocalURL = window.location.origin.startsWith('file') ? shareBaseURL : window.location.origin; - -export const NeoShareModal = ({ connection, loadDashboardListFromNeo4j, loadDatabaseListFromNeo4j }) => { - const [shareModalOpen, setShareModalOpen] = React.useState(false); - const [loadFromNeo4jModalOpen, setLoadFromNeo4jModalOpen] = React.useState(false); - const [loadFromFileModalOpen, setLoadFromFileModalOpen] = React.useState(false); - const [rows, setRows] = React.useState([]); - const { driver } = useContext(Neo4jContext); - - // One of [null, database, file] - const [shareType, setShareType] = React.useState('database'); - const [shareID, setShareID] = React.useState(null); - const [shareName, setShareName] = React.useState(null); - const [shareFileURL, setShareFileURL] = React.useState(''); - const [shareConnectionDetails, setShareConnectionDetails] = React.useState('No'); - const [shareStandalone, setShareStandalone] = React.useState('No'); - const [selfHosted, setSelfHosted] = React.useState('No'); - - const [shareLink, setShareLink] = React.useState(null); - - const [dashboardDatabase, setDashboardDatabase] = React.useState('neo4j'); - const [databases, setDatabases] = React.useState(['neo4j']); - - const handleClickOpen = () => { - setShareID(null); - setShareLink(null); - setShareModalOpen(true); - loadDatabaseListFromNeo4j(driver, (result) => { - setDatabases(result); - }); - }; - - const handleClose = () => { - setShareModalOpen(false); - }; - - const columns = [ - { field: 'id', hide: true, headerName: 'ID', width: 150 }, - { field: 'date', headerName: 'Date', width: 200 }, - { field: 'title', headerName: 'Title', width: 370 }, - { field: 'author', headerName: 'Author', width: 160 }, - { - field: 'load', - headerName: ' ', - renderCell: (c) => { - return ( - - ); - }, - width: 130, - }, - ]; - - return ( - <> - } /> - - - - - Share Dashboard - - - This window lets you create a temporary share link for your dashboard. Keep in mind that share links are not - intended as a way to publish your dashboard for users, see the  - - documentation - {' '} - for more on publishing. -
-
-
- Step 1: Select a dashboard to share. -
-
-
- - -
- {shareID ? `Selected dashboard: ${shareName}` : ''} -
- {shareID ? ( - <> - {' '} -
- Step 2: Configure sharing settings. -
-
- { - if ((e == 'No') & (shareStandalone == 'Yes')) { - return; - } - setShareLink(null); - setShareConnectionDetails(e); - }} - /> - {shareLocalURL != shareBaseURL ? ( - { - setShareLink(null); - setShareStandalone(e); - if (e == 'Yes') { - setShareConnectionDetails('Yes'); - } - }} - /> - ) : ( - <> - )} - { - setShareLink(null); - setSelfHosted(e); - }} - /> - -
- - ) : ( - <> - )} - {shareLink ? ( - <> -
- Step 3: Use the generated link to view the dashboard: -
- - {shareLink} - -
- - ) : ( - <> - )} -
-
- { - setLoadFromNeo4jModalOpen(false); - }} - aria-labelledby='form-dialog-title' - > - Select From Neo4j - - Choose a dashboard to share below. -
- <>, - ColumnSortedAscendingIcon: () => <>, - }} - /> -
- { - setRows([]); - setDashboardDatabase(newValue.value); - loadDashboardListFromNeo4j(driver, newValue.value, (result) => { - setRows(result); - }); - }, - options: databases.map((database) => ({ label: database, value: database })), - value: { label: dashboardDatabase, value: dashboardDatabase }, - menuPlacement: 'auto', - }} - style={{ width: '150px' }} - > -
-
- { - setLoadFromFileModalOpen(false); - }} - aria-labelledby='form-dialog-title' - > - Select from URL - - To share a dashboard file directly, make it accessible{' '} - - online - - . Then, paste the direct link here: - { - setShareFileURL(e); - }} - /> -
- -
-
-
- - ); -}; - -const mapStateToProps = (state) => ({ - connection: applicationGetConnection(state), -}); - -const mapDispatchToProps = (dispatch) => ({ - loadDashboardListFromNeo4j: (driver, database, callback) => - dispatch(loadDashboardListFromNeo4jThunk(driver, database, callback)), - loadDatabaseListFromNeo4j: (driver, callback) => dispatch(loadDatabaseListFromNeo4jThunk(driver, callback)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(NeoShareModal); diff --git a/src/modal/UpgradeOldDashboardModal.tsx b/src/modal/UpgradeOldDashboardModal.tsx index c86dbb545..7ad40d1f6 100644 --- a/src/modal/UpgradeOldDashboardModal.tsx +++ b/src/modal/UpgradeOldDashboardModal.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { TextareaAutosize } from '@mui/material'; import { Button, Dialog } from '@neo4j-ndl/react'; import { TrashIconOutline, PlayIconSolid } from '@neo4j-ndl/react/icons'; +import { createUUID } from '../utils/uuid'; export const NeoUpgradeOldDashboardModal = ({ open, text, clearOldDashboard, loadDashboard }) => { return ( @@ -29,7 +30,7 @@ export const NeoUpgradeOldDashboardModal = ({ open, text, clearOldDashboard, loa
); - return !isLoaded ? loadingMessage : content; + return content; }; const mapStateToProps = (state) => ({ - isLoaded: true, pagenumber: getPageNumber(state), editable: getDashboardIsEditable(state), dashboardSettings: getDashboardSettings(state), diff --git a/src/page/PageReducer.ts b/src/page/PageReducer.ts index c3a4096d3..18996110d 100644 --- a/src/page/PageReducer.ts +++ b/src/page/PageReducer.ts @@ -11,7 +11,7 @@ import { createUUID } from '../utils/uuid'; const update = (state, mutations) => Object.assign({}, state, mutations); // TODO : Alfredo: this should source the card config defined inside the reducer and then define the first page initial state -export const FIRST_PAGE_INITIAL_STATE = { +export const PAGE_EXAMPLE_STATE = { title: 'Main Page', reports: [ { @@ -42,7 +42,7 @@ export const FIRST_PAGE_INITIAL_STATE = { ], }; -export const PAGE_INITIAL_STATE = { +export const PAGE_EMPTY_STATE = { title: 'New page', reports: [], }; @@ -52,7 +52,7 @@ export const PAGE_INITIAL_STATE = { * This reducer handles updates to a single page of the dashboard. * TODO - pagenumbers can be cut from here with new reducer architecture. */ -export const pageReducer = (state = PAGE_INITIAL_STATE, action: { type: any; payload: any }) => { +export const pageReducer = (state = PAGE_EMPTY_STATE, action: { type: any; payload: any }) => { const { type, payload } = action; if (!action.type.startsWith('PAGE/')) { diff --git a/yarn.lock b/yarn.lock index 892cd3918..67b736bdd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1701,7 +1701,7 @@ js-yaml "4.1.0" nyc "15.1.0" -"@cypress/request@^2.88.10": +"@cypress/request@2.88.12": version "2.88.12" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.12.tgz#ba4911431738494a85e93fb04498cb38bc55d590" integrity sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA== @@ -2506,25 +2506,25 @@ "@babel/runtime" "^7.20.13" "@neo4j-cypher/codemirror" "1.0.2" -"@neo4j-ndl/base@1.10.1", "@neo4j-ndl/base@^1.10.1": - version "1.10.1" - resolved "https://registry.yarnpkg.com/@neo4j-ndl/base/-/base-1.10.1.tgz#18b3b35b9a52d0f5f0ee978c435bc96717ff16a9" - integrity sha512-ytz82vN1qMDCZButP4Wm0bLTStz6BWXWWRXMY0iP2Wfw/OAcI3WF2fBVL902FtzCBq0MR/GHHwjgMVpy9g7XeA== +"@neo4j-ndl/base@1.10.3", "@neo4j-ndl/base@^1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@neo4j-ndl/base/-/base-1.10.3.tgz#99557e3bede274510fc465a781d2e52e0cca47ea" + integrity sha512-NTFUz8j9+yx9AN1/TR3yzs7Nt/K+p1yqo2RHqf3UCtuD4ZyMUgYW1gmPQS0Du2S43gvQJkpVenXYJFgjNcFEMA== -"@neo4j-ndl/react@1.10.2": - version "1.10.2" - resolved "https://registry.yarnpkg.com/@neo4j-ndl/react/-/react-1.10.2.tgz#aaf61a06f3c63212f275b2a67ecf588a03287c84" - integrity sha512-t4OPV+qA5EqO4qb3FQBfdNnEZAhIO4CiW1qKN4cPeOvXCAyquPBkSM2+8l/rI1+Ra8o4r5FJ1LKln1BhLQPHPw== +"@neo4j-ndl/react@1.10.8": + version "1.10.8" + resolved "https://registry.yarnpkg.com/@neo4j-ndl/react/-/react-1.10.8.tgz#ab2ad2719cbdbe8a286ec54b18afc8fae6e5da33" + integrity sha512-EVUjwyxup/uNFItJl634z4JA9EVKJ5rvdncu9FOWHs85cw3VNqIfnFuApuuslveo7AknAwkCyeqQmOtjlPVSRQ== dependencies: "@floating-ui/react" "^0.24.2" "@heroicons/react" "2.0.13" "@neo4j-cypher/react-codemirror" "^1.0.1" - "@neo4j-ndl/base" "^1.10.1" + "@neo4j-ndl/base" "^1.10.3" "@tanstack/react-table" "^8.9.3" classnames "^2.3.1" date-fns "^2.30.0" detect-browser "^5.3.0" - re-resizable "^6.9.9" + re-resizable "^6.9.11" react-aria "^3.25.0" react-datepicker "^4.14.1" react-dropzone "^14.0.0" @@ -4535,10 +4535,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== -"@types/node@^14.14.31": - version "14.18.54" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.54.tgz#fc304bd66419030141fa997dc5a9e0e374029ae8" - integrity sha512-uq7O52wvo2Lggsx1x21tKZgqkJpvwCseBBPtX/nKQfpVlEsLOb11zZ1CRsWUKvJF0+lzuA9jwvA7Pr2Wt7i3xw== +"@types/node@^16.18.39": + version "16.18.59" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.59.tgz#4cdbd631be6d9be266a96fb17b5d0d7ad6bbe26c" + integrity sha512-PJ1w2cNeKUEdey4LiPra0ZuxZFOGvetswE8qHRriV/sUkL5Al4tTmPV9D2+Y/TPIxTHHgxTfRjZVKWhPw/ORhQ== "@types/parse-json@^4.0.0": version "4.0.0" @@ -5924,10 +5924,10 @@ commander@^4.0.0, commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== +commander@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== commander@^9.4.1: version "9.5.0" @@ -6194,14 +6194,14 @@ csstype@^3.1.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== -cypress@^10.11.0: - version "10.11.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.11.0.tgz#e9fbdd7638bae3d8fb7619fd75a6330d11ebb4e8" - integrity sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA== +cypress@^12.17.4: + version "12.17.4" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c" + integrity sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ== dependencies: - "@cypress/request" "^2.88.10" + "@cypress/request" "2.88.12" "@cypress/xvfb" "^1.2.4" - "@types/node" "^14.14.31" + "@types/node" "^16.18.39" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" arch "^2.2.0" @@ -6213,10 +6213,10 @@ cypress@^10.11.0: check-more-types "^2.24.0" cli-cursor "^3.1.0" cli-table3 "~0.6.1" - commander "^5.1.0" + commander "^6.2.1" common-tags "^1.8.0" dayjs "^1.10.4" - debug "^4.3.2" + debug "^4.3.4" enquirer "^2.3.6" eventemitter2 "6.4.7" execa "4.1.0" @@ -6231,12 +6231,13 @@ cypress@^10.11.0: listr2 "^3.8.3" lodash "^4.17.21" log-symbols "^4.0.0" - minimist "^1.2.6" + minimist "^1.2.8" ospath "^1.2.2" pretty-bytes "^5.6.0" + process "^0.11.10" proxy-from-env "1.0.0" request-progress "^3.0.0" - semver "^7.3.2" + semver "^7.5.3" supports-color "^8.1.1" tmp "~0.2.1" untildify "^4.0.0" @@ -10175,7 +10176,7 @@ minimatch@^7.4.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -11269,6 +11270,11 @@ process-utils@^4.0.0: memoizee "^0.4.14" type "^2.1.0" +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -11389,10 +11395,10 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -re-resizable@^6.9.9: - version "6.9.9" - resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.9.tgz#99e8b31c67a62115dc9c5394b7e55892265be216" - integrity sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA== +re-resizable@^6.9.11: + version "6.9.11" + resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.11.tgz#f356e27877f12d926d076ab9ad9ff0b95912b475" + integrity sha512-a3hiLWck/NkmyLvGWUuvkAmN1VhwAz4yOhS6FdMTaxCUVN9joIWkT11wsO68coG/iEYuwn+p/7qAmfQzRhiPLQ== react-aria@^3.25.0: version "3.25.0" @@ -12210,7 +12216,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.3.7, semver@^7.3.8: +semver@^7.3.7, semver@^7.3.8, semver@^7.5.3: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== From c2b879bed06c11a2b08e4fc9718d08bf956f6266 Mon Sep 17 00:00:00 2001 From: jacobbleakley-neo4j Date: Mon, 30 Oct 2023 10:12:16 +0000 Subject: [PATCH 009/107] monday --- src/chart/bar/BarChart.tsx | 145 ++++++++++++++++++++---------------- src/config/ReportConfig.tsx | 10 +++ 2 files changed, 92 insertions(+), 63 deletions(-) diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index d4cf43fdc..9d82f3ced 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -30,69 +30,19 @@ const NeoBarChart = (props: ChartProps) => { const [keys, setKeys] = React.useState([]); const [data, setData] = React.useState[]>([]); - - 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 (loading) { - return <>; - } - - if (!selection || props.records == null || props.records.length == 0 || props.records[0].keys == null) { - return ; - } - + const [adjustedData, setAdjustedData] = React.useState[]>([]); const settings = props.settings ? props.settings : {}; + const legendWidth = settings.legendWidth ? settings.legendWidth : 128; const marginRight = settings.marginRight ? settings.marginRight : 24; const marginLeft = settings.marginLeft ? settings.marginLeft : 50; const marginTop = settings.marginTop ? settings.marginTop : 24; const marginBottom = settings.marginBottom ? settings.marginBottom : 40; const legend = settings.legend ? settings.legend : false; const labelRotation = settings.labelRotation != undefined ? settings.labelRotation : 45; - const barWidth = settings.barWidth? settings.barWidth : 50; + const barWidth = settings.barWidth ? settings.barWidth : 50; const padding = settings.padding ? settings.padding : 0.3; + const innerPadding = settings.innerPadding ? settings.innerPadding : 0; + const minBarHeight = settings.minBarHeight ? settings.minBarHeight : 0; const labelSkipWidth = settings.labelSkipWidth ? settings.labelSkipWidth : 0; const labelSkipHeight = settings.labelSkipHeight ? settings.labelSkipHeight : 0; @@ -112,6 +62,73 @@ const NeoBarChart = (props: ChartProps) => { props.getGlobalParameter ); + 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); + + if (minBarHeight > 0) { + let modifiedData = JSON.parse(JSON.stringify(newData)); // deep copy the data + for (let row of modifiedData) { + for (let key of Object.keys(newKeys)) { + if (!row[key] || row[key] < minBarHeight) { + row[key] = minBarHeight; + } + } + } + setAdjustedData(modifiedData); + } else { + setAdjustedData(newData); // Use original data if minBarHeight is 0 + } + }, [selection, minBarHeight]); + + if (loading) { + return <>; + } + + if (!selection || props.records == null || props.records.length == 0 || props.records[0].keys == null) { + return ; + } + const chartColorsByScheme = getD3ColorsByScheme(colorScheme); // Compute bar color based on rules - overrides default color scheme completely. const getBarColor = (bar) => { @@ -204,6 +221,7 @@ const NeoBarChart = (props: ChartProps) => { }; // 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 }; }; @@ -221,8 +239,8 @@ const NeoBarChart = (props: ChartProps) => { // Scrollable Wrapper const scrollableWrapperStyle: React.CSSProperties = { - width: (barWidth*data.length)+itemWidthConst, - height: (18*data.length)+(itemWidthConst*1.2)+marginBottom, + width: barWidth * data.length + itemWidthConst, + height: 18 * data.length + itemWidthConst * 1.2 + marginBottom, whiteSpace: 'nowrap', }; @@ -230,29 +248,30 @@ const NeoBarChart = (props: ChartProps) => { width: '100%', overflowX: 'auto', overflowY: 'auto', - height: '100%' - } + height: '100%', + }; const chart = (
0 ? adjustedData : data} key={`${selection.index}___${selection.value}`} layout={layout} groupMode={groupMode == 'stacked' ? 'stacked' : 'grouped'} - // enableLabel={enableLabel} + enableLabel={enableLabel} keys={keys} indexBy='index' margin={{ top: marginTop, right: legend ? itemWidthConst + marginRight : marginRight, - bottom: (itemWidthConst*0.3) +marginBottom, + bottom: itemWidthConst * 0.3 + marginBottom, left: marginLeft, }} valueScale={{ type: valueScale }} padding={padding} + innerPadding={innerPadding} minValue={minValue} maxValue={maxValue} colors={getBarColor} @@ -280,7 +299,7 @@ const NeoBarChart = (props: ChartProps) => { anchor: 'bottom-right', direction: 'column', justify: false, - translateX: itemWidthConst+10, + translateX: itemWidthConst + 10, translateY: 0, itemsSpacing: 1, itemWidth: itemWidthConst, diff --git a/src/config/ReportConfig.tsx b/src/config/ReportConfig.tsx index c0fc52412..1aea2a868 100644 --- a/src/config/ReportConfig.tsx +++ b/src/config/ReportConfig.tsx @@ -347,6 +347,16 @@ const _REPORT_TYPES = { type: SELECTION_TYPES.NUMBER, default: 0.3, }, + innerPadding: { + label: 'Padding Between Grouped Elements', + type: SELECTION_TYPES.NUMBER, + default: 0, + }, + minBarHeight: { + label: 'Minimum Bar Height', + type: SELECTION_TYPES.NUMBER, + default: 0, + }, showOptionalSelections: { label: 'Grouping', type: SELECTION_TYPES.LIST, From 5032618a5923e8c26ecd0ac791c3584d33f21e2d Mon Sep 17 00:00:00 2001 From: jacobbleakley-neo4j Date: Mon, 30 Oct 2023 11:48:01 +0000 Subject: [PATCH 010/107] Horizontal/Vertical legend switch --- src/chart/bar/BarChart.tsx | 59 +++++++++++++++++++++++++++---------- src/config/ReportConfig.tsx | 6 ++++ 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index 9d82f3ced..d3b804ca1 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -44,6 +44,8 @@ const NeoBarChart = (props: ChartProps) => { const innerPadding = settings.innerPadding ? settings.innerPadding : 0; const minBarHeight = settings.minBarHeight ? settings.minBarHeight : 0; + 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; @@ -92,16 +94,16 @@ const NeoBarChart = (props: ChartProps) => { 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; - // }); + }, []) + .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); @@ -239,9 +241,10 @@ const NeoBarChart = (props: ChartProps) => { // Scrollable Wrapper const scrollableWrapperStyle: React.CSSProperties = { - width: barWidth * data.length + itemWidthConst, - height: 18 * data.length + itemWidthConst * 1.2 + marginBottom, + width: legendPosition === 'Horizontal' ? (itemWidthConst*data.length)+200 : barWidth * data.length + itemWidthConst, + height: legendPosition === 'Horizontal' ? '100%' : 18 * data.length + itemWidthConst * 1.2 + marginBottom, whiteSpace: 'nowrap', + overflowX: 'auto', }; const barChartStyle: React.CSSProperties = { @@ -265,8 +268,8 @@ const NeoBarChart = (props: ChartProps) => { indexBy='index' margin={{ top: marginTop, - right: legend ? itemWidthConst + marginRight : marginRight, - bottom: itemWidthConst * 0.3 + marginBottom, + right: legendPosition === 'Horizontal' ? legend ? legendWidth + marginRight : marginRight : legend ? itemWidthConst + marginRight : marginRight, + bottom: legendPosition === 'Horizontal' ? legend ? marginBottom + 50 : marginBottom : itemWidthConst * 0.3 + marginBottom, left: marginLeft, }} valueScale={{ type: valueScale }} @@ -293,7 +296,31 @@ const NeoBarChart = (props: ChartProps) => { {...extraProperties} legends={ legend - ? [ + ? legendPosition === 'Horizontal' ? [ + { + dataFrom: 'keys', + anchor: 'bottom-left', + direction: 'row', + justify: false, + translateX: 0, + translateY: 80, + itemsSpacing: 2, + itemWidth: itemWidthConst, + itemHeight: 20, + itemDirection: 'left-to-right', + itemOpacity: 0.85, + symbolSize: 20, + effects: [ + { + on: 'hover', + style: { + itemOpacity: 1, + }, + }, + ], + }, + ] : + [ { dataFrom: 'keys', anchor: 'bottom-right', @@ -328,4 +355,4 @@ const NeoBarChart = (props: ChartProps) => { return chart; }; -export default NeoBarChart; +export default NeoBarChart; \ No newline at end of file diff --git a/src/config/ReportConfig.tsx b/src/config/ReportConfig.tsx index 1aea2a868..dcf0530fd 100644 --- a/src/config/ReportConfig.tsx +++ b/src/config/ReportConfig.tsx @@ -357,6 +357,12 @@ const _REPORT_TYPES = { type: SELECTION_TYPES.NUMBER, default: 0, }, + legendPosition: { + label: 'Legend Position', + type: SELECTION_TYPES.LIST, + values: ['Horizontal', 'Vertical'], + default: 'Vertical' + }, showOptionalSelections: { label: 'Grouping', type: SELECTION_TYPES.LIST, From 2e1cf0713d0f31a987aa6457897f380d655b606d Mon Sep 17 00:00:00 2001 From: jacobbleakley-neo4j Date: Mon, 30 Oct 2023 17:56:39 +0000 Subject: [PATCH 011/107] Sorted graph sizing with padding --- src/chart/bar/BarChart.tsx | 33 +++++++++++++++++---------------- src/config/ReportConfig.tsx | 2 +- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index d3b804ca1..0141879d8 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -39,8 +39,8 @@ const NeoBarChart = (props: ChartProps) => { const marginBottom = settings.marginBottom ? settings.marginBottom : 40; const legend = settings.legend ? settings.legend : false; const labelRotation = settings.labelRotation != undefined ? settings.labelRotation : 45; - const barWidth = settings.barWidth ? settings.barWidth : 50; - const padding = settings.padding ? settings.padding : 0.3; + const barWidth = settings.barWidth ? settings.barWidth : 10; + const padding = settings.padding ? settings.padding : 0.1; const innerPadding = settings.innerPadding ? settings.innerPadding : 0; const minBarHeight = settings.minBarHeight ? settings.minBarHeight : 0; @@ -95,15 +95,16 @@ const NeoBarChart = (props: ChartProps) => { return []; } }, []) - .map((row) => { - Object.keys(newKeys).forEach((key) => { - // eslint-disable-next-line no-prototype-builtins - if (!row.hasOwnProperty(key)) { - row[key] = 0; - } - }); - return row; - }); + // .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); @@ -237,14 +238,14 @@ const NeoBarChart = (props: ChartProps) => { const baseItemWidth = 40; // Some base width for color box and padding const charWidthEstimate = 5; // An estimate of how wide each character is, you might need to adjust this based on font size and type const itemWidthConst = baseItemWidth + maxKeyLength * charWidthEstimate; + const adaptableWidth = marginLeft + marginRight + (data.length * barWidth*4) + ((data.length-1)*4) + ((data.length-1)*innerPadding*4); // Scrollable Wrapper const scrollableWrapperStyle: React.CSSProperties = { - width: legendPosition === 'Horizontal' ? (itemWidthConst*data.length)+200 : barWidth * data.length + itemWidthConst, + width: legendPosition === 'Horizontal' ? (adaptableWidth > (itemWidthConst*data.length)+200 ? adaptableWidth : (itemWidthConst*data.length)+200): (adaptableWidth > adaptableWidth ? adaptableWidth : barWidth * 5 * data.length + itemWidthConst), height: legendPosition === 'Horizontal' ? '100%' : 18 * data.length + itemWidthConst * 1.2 + marginBottom, whiteSpace: 'nowrap', - overflowX: 'auto', }; const barChartStyle: React.CSSProperties = { @@ -268,8 +269,8 @@ const NeoBarChart = (props: ChartProps) => { indexBy='index' margin={{ top: marginTop, - right: legendPosition === 'Horizontal' ? legend ? legendWidth + marginRight : marginRight : legend ? itemWidthConst + marginRight : marginRight, - bottom: legendPosition === 'Horizontal' ? legend ? marginBottom + 50 : marginBottom : itemWidthConst * 0.3 + marginBottom, + right: legendPosition === 'Horizontal' ? marginRight : legend ? itemWidthConst + marginRight : marginRight, + bottom: legendPosition === 'Horizontal' ? legend ? itemWidthConst * 0.3 + marginBottom + 50 : itemWidthConst * 0.3 + marginBottom : itemWidthConst * 0.3 + marginBottom, left: marginLeft, }} valueScale={{ type: valueScale }} @@ -303,7 +304,7 @@ const NeoBarChart = (props: ChartProps) => { direction: 'row', justify: false, translateX: 0, - translateY: 80, + translateY: itemWidthConst, itemsSpacing: 2, itemWidth: itemWidthConst, itemHeight: 20, diff --git a/src/config/ReportConfig.tsx b/src/config/ReportConfig.tsx index dcf0530fd..ce736e9ad 100644 --- a/src/config/ReportConfig.tsx +++ b/src/config/ReportConfig.tsx @@ -340,7 +340,7 @@ const _REPORT_TYPES = { barWidth: { label: 'Bar Width', type: SELECTION_TYPES.NUMBER, - default: 50, + default: 10, }, padding: { label: 'Padding', From dafb5b6c86df89905139b682ddb0eee970c96572 Mon Sep 17 00:00:00 2001 From: jacobbleakley-neo4j Date: Tue, 31 Oct 2023 14:56:46 +0000 Subject: [PATCH 012/107] Tuesday Afternoon --- src/chart/bar/BarChart.tsx | 17 ++++++++++++++++- src/config/ReportConfig.tsx | 8 ++++---- .../actions/ActionsRuleCreationModal.tsx | 17 +++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index 0141879d8..5e5b07a89 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -7,6 +7,10 @@ import { ChartProps } from '../Chart'; import { convertRecordObjectToString, recordToNative } from '../ChartUtils'; import { themeNivo, themeNivoCanvas } from '../Utils'; import { extensionEnabled } from '../../utils/ReportUtils'; +import { + getPageNumbersAndNamesList, + performActionOnElement, +} from '../../extensions/advancedcharts/Utils'; /** * Embeds a BarReport (from Nivo) into NeoDash. @@ -40,10 +44,16 @@ const NeoBarChart = (props: ChartProps) => { 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.1; + const padding = settings.padding ? settings.padding : 0.25; const innerPadding = settings.innerPadding ? settings.innerPadding : 0; const minBarHeight = settings.minBarHeight ? settings.minBarHeight : 0; + 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; @@ -233,6 +243,8 @@ const NeoBarChart = (props: ChartProps) => { const canvas = data.length > 30; const BarChartComponent = canvas ? ResponsiveBarCanvas : ResponsiveBar; + + // For adaptable item length in the legend const maxKeyLength = Math.max(...keys.map((key) => key.length)); const baseItemWidth = 40; // Some base width for color box and padding @@ -265,6 +277,9 @@ const NeoBarChart = (props: ChartProps) => { layout={layout} groupMode={groupMode == 'stacked' ? 'stacked' : 'grouped'} enableLabel={enableLabel} + onClick={({value}) => + performActionOnElement(value, actionsRules, { ...props, pageNames: pageNames }, 'Click', 'bar') + } keys={keys} indexBy='index' margin={{ diff --git a/src/config/ReportConfig.tsx b/src/config/ReportConfig.tsx index ce736e9ad..d7023c61f 100644 --- a/src/config/ReportConfig.tsx +++ b/src/config/ReportConfig.tsx @@ -338,17 +338,17 @@ const _REPORT_TYPES = { default: false, }, barWidth: { - label: 'Bar Width', + label: 'Bar Width (Use to size graph width)', type: SELECTION_TYPES.NUMBER, default: 10, }, padding: { - label: 'Padding', + label: 'Padding (Proportional)', type: SELECTION_TYPES.NUMBER, - default: 0.3, + default: 0.25, }, innerPadding: { - label: 'Padding Between Grouped Elements', + label: 'Padding Between Grouped Bars', type: SELECTION_TYPES.NUMBER, default: 0, }, diff --git a/src/extensions/actions/ActionsRuleCreationModal.tsx b/src/extensions/actions/ActionsRuleCreationModal.tsx index b2fa8df6c..de4c2f3cf 100644 --- a/src/extensions/actions/ActionsRuleCreationModal.tsx +++ b/src/extensions/actions/ActionsRuleCreationModal.tsx @@ -17,6 +17,13 @@ const RULE_CONDITIONS = { label: 'Cell Double Click', }, ], + bar: [ + { + value: 'Click', + label: 'Click', + default: true, + } + ], map: [ { value: 'Click', @@ -49,6 +56,16 @@ export const RULE_BASED_REPORT_ACTIONS_CUSTOMIZATIONS = { label: 'Page', }, ], + bar: [ + { + value: 'set variable', + label: 'Parameter', + }, + { + value: 'set page', + label: 'Page', + }, + ], map: [ { value: 'set variable', From db2cbce4a596e15f54726077284c5f4883bb05fd Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Tue, 31 Oct 2023 16:14:45 +0100 Subject: [PATCH 013/107] Mapped bar chart event to standardized action event --- src/chart/bar/BarChart.tsx | 142 ++++++++++++++----------- src/extensions/advancedcharts/Utils.ts | 4 +- 2 files changed, 80 insertions(+), 66 deletions(-) diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index 5e5b07a89..e6a86f27c 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -7,10 +7,7 @@ import { ChartProps } from '../Chart'; import { convertRecordObjectToString, recordToNative } from '../ChartUtils'; import { themeNivo, themeNivoCanvas } from '../Utils'; import { extensionEnabled } from '../../utils/ReportUtils'; -import { - getPageNumbersAndNamesList, - performActionOnElement, -} from '../../extensions/advancedcharts/Utils'; +import { getPageNumbersAndNamesList, performActionOnElement } from '../../extensions/advancedcharts/Utils'; /** * Embeds a BarReport (from Nivo) into NeoDash. @@ -49,9 +46,9 @@ const NeoBarChart = (props: ChartProps) => { const minBarHeight = settings.minBarHeight ? settings.minBarHeight : 0; const actionsRules = - extensionEnabled(props.extensions, 'actions') && props.settings && props.settings.actionsRules - ? props.settings.actionsRules - : []; + extensionEnabled(props.extensions, 'actions') && props.settings && props.settings.actionsRules + ? props.settings.actionsRules + : []; const pageNames = getPageNumbersAndNamesList(); const legendPosition = settings.legendPosition ? settings.legendPosition : 'Vertical'; @@ -104,7 +101,7 @@ const NeoBarChart = (props: ChartProps) => { console.error(e); return []; } - }, []) + }, []); // .map((row) => { // Object.keys(newKeys).forEach((key) => { // // eslint-disable-next-line no-prototype-builtins @@ -114,8 +111,6 @@ const NeoBarChart = (props: ChartProps) => { // }); // return row; // }) - ; - setKeys(Object.keys(newKeys)); setData(newData); @@ -243,19 +238,27 @@ const NeoBarChart = (props: ChartProps) => { const canvas = data.length > 30; const BarChartComponent = canvas ? ResponsiveBarCanvas : ResponsiveBar; - - // For adaptable item length in the legend const maxKeyLength = Math.max(...keys.map((key) => key.length)); const baseItemWidth = 40; // Some base width for color box and padding const charWidthEstimate = 5; // An estimate of how wide each character is, you might need to adjust this based on font size and type const itemWidthConst = baseItemWidth + maxKeyLength * charWidthEstimate; - const adaptableWidth = marginLeft + marginRight + (data.length * barWidth*4) + ((data.length-1)*4) + ((data.length-1)*innerPadding*4); + const adaptableWidth = + marginLeft + + marginRight + + data.length * barWidth * 4 + + (data.length - 1) * 4 + + (data.length - 1) * innerPadding * 4; // Scrollable Wrapper const scrollableWrapperStyle: React.CSSProperties = { - width: legendPosition === 'Horizontal' ? (adaptableWidth > (itemWidthConst*data.length)+200 ? adaptableWidth : (itemWidthConst*data.length)+200): (adaptableWidth > adaptableWidth ? adaptableWidth : barWidth * 5 * data.length + itemWidthConst), + width: + legendPosition === 'Horizontal' + ? adaptableWidth > itemWidthConst * data.length + 200 + ? adaptableWidth + : itemWidthConst * data.length + 200 + : barWidth * 5 * data.length + itemWidthConst, height: legendPosition === 'Horizontal' ? '100%' : 18 * data.length + itemWidthConst * 1.2 + marginBottom, whiteSpace: 'nowrap', }; @@ -277,15 +280,25 @@ const NeoBarChart = (props: ChartProps) => { layout={layout} groupMode={groupMode == 'stacked' ? 'stacked' : 'grouped'} enableLabel={enableLabel} - onClick={({value}) => - performActionOnElement(value, actionsRules, { ...props, pageNames: pageNames }, 'Click', 'bar') - } + onClick={(e) => { + /** + * We need to transform the bar chart event `e`, into the standardized event `e2` that the action handler expects. + * The standardized event is a dictionary with two keys, `field` and `value`. + */ + const e2 = { field: e.id, value: e.value }; + performActionOnElement(e2, actionsRules, { ...props, pageNames: pageNames }, 'Click', 'bar'); + }} keys={keys} indexBy='index' margin={{ top: marginTop, right: legendPosition === 'Horizontal' ? marginRight : legend ? itemWidthConst + marginRight : marginRight, - bottom: legendPosition === 'Horizontal' ? legend ? itemWidthConst * 0.3 + marginBottom + 50 : itemWidthConst * 0.3 + marginBottom : itemWidthConst * 0.3 + marginBottom, + bottom: + legendPosition === 'Horizontal' + ? legend + ? itemWidthConst * 0.3 + marginBottom + 50 + : itemWidthConst * 0.3 + marginBottom + : itemWidthConst * 0.3 + marginBottom, left: marginLeft, }} valueScale={{ type: valueScale }} @@ -312,54 +325,55 @@ const NeoBarChart = (props: ChartProps) => { {...extraProperties} legends={ legend - ? legendPosition === 'Horizontal' ? [ - { - dataFrom: 'keys', - anchor: 'bottom-left', - direction: 'row', - justify: false, - translateX: 0, - translateY: itemWidthConst, - itemsSpacing: 2, - itemWidth: itemWidthConst, - itemHeight: 20, - itemDirection: 'left-to-right', - itemOpacity: 0.85, - symbolSize: 20, - effects: [ + ? legendPosition === 'Horizontal' + ? [ { - on: 'hover', - style: { - itemOpacity: 1, - }, + dataFrom: 'keys', + anchor: 'bottom-left', + direction: 'row', + justify: false, + translateX: 0, + translateY: itemWidthConst, + itemsSpacing: 2, + itemWidth: itemWidthConst, + itemHeight: 20, + itemDirection: 'left-to-right', + itemOpacity: 0.85, + symbolSize: 20, + effects: [ + { + on: 'hover', + style: { + itemOpacity: 1, + }, + }, + ], }, - ], - }, - ] : - [ - { - dataFrom: 'keys', - anchor: 'bottom-right', - direction: 'column', - justify: false, - translateX: itemWidthConst + 10, - translateY: 0, - itemsSpacing: 1, - itemWidth: itemWidthConst, - itemHeight: 20, - itemDirection: 'left-to-right', - itemOpacity: 0.85, - symbolSize: 15, - effects: [ - { - on: 'hover', - style: { - itemOpacity: 1, + ] + : [ + { + dataFrom: 'keys', + anchor: 'bottom-right', + direction: 'column', + justify: false, + translateX: itemWidthConst + 10, + translateY: 0, + itemsSpacing: 1, + itemWidth: itemWidthConst, + itemHeight: 20, + itemDirection: 'left-to-right', + itemOpacity: 0.85, + symbolSize: 15, + effects: [ + { + on: 'hover', + style: { + itemOpacity: 1, + }, }, - }, - ], - }, - ] + ], + }, + ] : [] } animate={false} @@ -371,4 +385,4 @@ const NeoBarChart = (props: ChartProps) => { return chart; }; -export default NeoBarChart; \ No newline at end of file +export default NeoBarChart; diff --git a/src/extensions/advancedcharts/Utils.ts b/src/extensions/advancedcharts/Utils.ts index 3b667fcb4..8d3cd8150 100644 --- a/src/extensions/advancedcharts/Utils.ts +++ b/src/extensions/advancedcharts/Utils.ts @@ -79,13 +79,13 @@ export const executeActionRule = (rule, e, props, _type = 'default') => { /** * Evaluates and runs actions based on an element based on the rule set defined in the settings. - * @param e - the element. + * @param e - the element --> should be a dictionary with two entries {field, value}, the category and the attached value. * @param actionsRules - the list of rules. * @param props - ChartProps object with callbacks to execute rule. * @param action - the type of action to perform. * @param type - the rule type. */ -export const performActionOnElement = (e, actionsRules, props, action, type = 'default') => { +export const performActionOnElement = (e: { field; value }, actionsRules, props, action, type = 'default') => { let rules = getRule(e, actionsRules, action); if (rules !== null) { rules.forEach((rule) => executeActionRule(rule, e, props, type)); From 221a6021d2d199c267788a91c3750e5bbe06188a Mon Sep 17 00:00:00 2001 From: jacobbleakley-neo4j Date: Wed, 1 Nov 2023 09:02:33 +0000 Subject: [PATCH 014/107] Wednesday Morning --- src/chart/bar/BarChart.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index e6a86f27c..0d99d8cad 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -281,11 +281,12 @@ const NeoBarChart = (props: ChartProps) => { groupMode={groupMode == 'stacked' ? 'stacked' : 'grouped'} enableLabel={enableLabel} onClick={(e) => { + console.log(e) /** * We need to transform the bar chart event `e`, into the standardized event `e2` that the action handler expects. * The standardized event is a dictionary with two keys, `field` and `value`. */ - const e2 = { field: e.id, value: e.value }; + const e2 = { field: e.id, value: props.records[e.indexValue] }; performActionOnElement(e2, actionsRules, { ...props, pageNames: pageNames }, 'Click', 'bar'); }} keys={keys} From d3fee847e9139b382098f3b1ad0e6590cbce1b5e Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Wed, 1 Nov 2023 11:04:26 +0100 Subject: [PATCH 015/107] Added event handling for actions on bar charts --- src/chart/bar/BarChart.tsx | 29 +++++++++++++++++------- src/chart/bar/util.ts | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 src/chart/bar/util.ts diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index 0d99d8cad..b6dd4d915 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -7,7 +7,8 @@ import { ChartProps } from '../Chart'; import { convertRecordObjectToString, recordToNative } from '../ChartUtils'; import { themeNivo, themeNivoCanvas } from '../Utils'; import { extensionEnabled } from '../../utils/ReportUtils'; -import { getPageNumbersAndNamesList, performActionOnElement } from '../../extensions/advancedcharts/Utils'; +import { getPageNumbersAndNamesList, getRule, performActionOnElement } from '../../extensions/advancedcharts/Utils'; +import { getOriginalRecordForNivoClickEvent } from './util'; /** * Embeds a BarReport (from Nivo) into NeoDash. @@ -281,13 +282,25 @@ const NeoBarChart = (props: ChartProps) => { groupMode={groupMode == 'stacked' ? 'stacked' : 'grouped'} enableLabel={enableLabel} onClick={(e) => { - console.log(e) - /** - * We need to transform the bar chart event `e`, into the standardized event `e2` that the action handler expects. - * The standardized event is a dictionary with two keys, `field` and `value`. - */ - const e2 = { field: e.id, value: props.records[e.indexValue] }; - performActionOnElement(e2, actionsRules, { ...props, pageNames: pageNames }, 'Click', 'bar'); + // Get the original record that was used to draw this bar (or a group in a bar). + const record = getOriginalRecordForNivoClickEvent(e, records, selection); + // From that record, check if there are any rules assigned to each of the fields (columns). + 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 && + rules.forEach((rule) => { + const ruleField = rule.field; + const ruleValue = record[rule.value]; + performActionOnElement( + { field: ruleField, value: ruleValue }, + actionsRules, + { ...props, pageNames: pageNames }, + 'Click', + 'bar' + ); + }); + }); }} keys={keys} indexBy='index' diff --git a/src/chart/bar/util.ts b/src/chart/bar/util.ts new file mode 100644 index 000000000..0608c9b93 --- /dev/null +++ b/src/chart/bar/util.ts @@ -0,0 +1,45 @@ +/** + * 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; + } + } +} From fcca63d00bf2804e6958cd6a02b3ebb22fb645fa Mon Sep 17 00:00:00 2001 From: jacobbleakley-neo4j Date: Thu, 2 Nov 2023 15:19:23 +0000 Subject: [PATCH 016/107] Fixed styling for bar chart report action --- src/chart/bar/BarChart.tsx | 10 +- .../actions/ActionsRuleCreationModal.tsx | 95 +++++++++++-------- 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index b6dd4d915..f8f70c7f8 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -165,8 +165,10 @@ const NeoBarChart = (props: ChartProps) => { let shade = false; let darkTop = false; let includeIndex = false; - let x = bar.width / 2; - let y = bar.height / 2; + let x; + bar.width ? x = bar.width / 2 : x = 0; + let y + bar.height ? y = bar.height / 2 : y=0; let textAnchor = 'middle'; if (positionLabel == 'top') { if (layout == 'vertical') { @@ -285,7 +287,7 @@ const NeoBarChart = (props: ChartProps) => { // Get the original record that was used to draw this bar (or a group in a bar). const record = getOriginalRecordForNivoClickEvent(e, records, selection); // From that record, check if there are any rules assigned to each of the fields (columns). - Object.keys(record).forEach((key) => { + 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 && @@ -300,7 +302,7 @@ const NeoBarChart = (props: ChartProps) => { 'bar' ); }); - }); + }): null; }} keys={keys} indexBy='index' diff --git a/src/extensions/actions/ActionsRuleCreationModal.tsx b/src/extensions/actions/ActionsRuleCreationModal.tsx index de4c2f3cf..d92d44c7d 100644 --- a/src/extensions/actions/ActionsRuleCreationModal.tsx +++ b/src/extensions/actions/ActionsRuleCreationModal.tsx @@ -22,7 +22,7 @@ const RULE_CONDITIONS = { value: 'Click', label: 'Click', default: true, - } + }, ], map: [ { @@ -228,7 +228,10 @@ export const NeoCustomReportActionsModal = ({
{index + 1}.  ON - +
updateRuleField(index, 'condition', newValue.value), @@ -315,44 +318,48 @@ export const NeoCustomReportActionsModal = ({ value: { label: ruleTrigger ? ruleTrigger.label : '', value: rule.condition }, }} > - } - style={{ - minWidth: 125, - }} - onInputChange={(event, value) => { - updateRuleField(index, 'field', value); - }} - onChange={(event, newValue) => { - updateRuleField(index, 'field', newValue); - }} - renderInput={(params) => ( - - )} - /> + {type !== 'bar' ? ( + } + style={{ + minWidth: 125, + }} + onInputChange={(event, value) => { + updateRuleField(index, 'field', value); + }} + onChange={(event, newValue) => { + updateRuleField(index, 'field', newValue); + }} + renderInput={(params) => ( + + )} + /> + ) : ( + <> + )}
SET - +
TO - +
} style={{ minWidth: 250 }} onInputChange={(event, value) => { - updateRuleField(index, 'value', value); + if (type == 'bar') { + updateRuleField(index, 'value', value); // Also update rule.field here if needed + updateRuleField(index, 'field', value); // Duplicate the value to rule.field + } else { + updateRuleField(index, 'value', value); + } }} onChange={(event, newValue) => { - updateRuleField(index, 'value', newValue); + if (type == 'bar') { + updateRuleField(index, 'value', newValue); // Also update rule.field here if needed + updateRuleField(index, 'customization', newValue.value); // Duplicate the value to rule.field + } else { + updateRuleField(index, 'value', newValue); + } }} renderInput={(params) => ( - + )} />
From f4c2791ec29dbe90384a12cbf679af9c34bc8309 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:08:19 +0100 Subject: [PATCH 017/107] Bump @babel/traverse from 7.21.4 to 7.23.2 in /gallery (#670) Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.21.4 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: Harold Agudelo Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gallery/yarn.lock | 113 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 13 deletions(-) diff --git a/gallery/yarn.lock b/gallery/yarn.lock index 2d61aaaee..05f3e0b28 100644 --- a/gallery/yarn.lock +++ b/gallery/yarn.lock @@ -26,6 +26,14 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.4.tgz#457ffe647c480dff59c2be092fc3acf71195c87f" @@ -71,6 +79,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" @@ -136,6 +154,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-explode-assignable-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" @@ -151,6 +174,14 @@ "@babel/template" "^7.20.7" "@babel/types" "^7.21.0" +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" @@ -158,6 +189,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5" @@ -241,16 +279,33 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-string-parser@^7.19.4": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" @@ -284,11 +339,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -1049,19 +1118,28 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4", "@babel/traverse@^7.7.2": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.4.tgz#a836aca7b116634e97a6ed99976236b3282c9d36" - integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q== +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== dependencies: - "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.21.4" - "@babel/types" "^7.21.4" + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4", "@babel/traverse@^7.7.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" debug "^4.1.0" globals "^11.1.0" @@ -1074,6 +1152,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -3068,7 +3155,7 @@ case-sensitive-paths-webpack-plugin@^2.4.0: resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== -chalk@^2.0.0, chalk@^2.4.1: +chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== From 7b7bed10f5ee7933f97c4522dc856d630ec429c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:19:55 +0100 Subject: [PATCH 018/107] Bump @babel/traverse from 7.20.13 to 7.23.2 (#669) Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.20.13 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: Harold Agudelo Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Niels de Jong --- yarn.lock | 112 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 78 insertions(+), 34 deletions(-) diff --git a/yarn.lock b/yarn.lock index 67b736bdd..07ef38fcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -126,6 +126,14 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + "@babel/code-frame@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" @@ -204,6 +212,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.16.0", "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" @@ -280,6 +298,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-environment-visitor@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" @@ -300,13 +323,13 @@ "@babel/template" "^7.18.10" "@babel/types" "^7.19.0" -"@babel/helper-function-name@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" - integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== dependencies: - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" @@ -455,6 +478,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-identifier@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" @@ -507,6 +535,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/highlight@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" @@ -521,11 +558,16 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.6.tgz#201f8b47be20c76c7c5743b9c16129760bf9a975" integrity sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw== -"@babel/parser@^7.20.13", "@babel/parser@^7.20.7": +"@babel/parser@^7.20.7": version "7.20.15" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89" integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -1255,6 +1297,15 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + "@babel/template@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" @@ -1264,35 +1315,19 @@ "@babel/parser" "^7.22.5" "@babel/types" "^7.22.5" -"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.4.5": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.13.tgz#817c1ba13d11accca89478bd5481b2d168d07473" - integrity sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.13" - "@babel/types" "^7.20.7" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.6.tgz#8f2f83a5c588251584914debeee38f35f661a300" - integrity sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w== +"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6", "@babel/traverse@^7.4.5": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== dependencies: - "@babel/code-frame" "^7.22.5" - "@babel/generator" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.22.6" - "@babel/types" "^7.22.5" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" debug "^4.1.0" globals "^11.1.0" @@ -1305,6 +1340,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.22.15", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@babel/types@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe" @@ -5660,7 +5704,7 @@ chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== From 4f29427eb753e010e67687c758c2df26380983cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:18:45 +0100 Subject: [PATCH 019/107] Bump postcss from 8.4.23 to 8.4.31 in /gallery (#656) Bumps [postcss](https://github.com/postcss/postcss) from 8.4.23 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.23...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: Harold Agudelo Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Niels de Jong --- gallery/package.json | 2 +- gallery/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gallery/package.json b/gallery/package.json index c1a93960e..e4988c384 100644 --- a/gallery/package.json +++ b/gallery/package.json @@ -33,7 +33,7 @@ }, "devDependencies": { "autoprefixer": "^10.4.12", - "postcss": "^8.4.17", + "postcss": "^8.4.31", "tailwindcss": "^3.1.8" } } diff --git a/gallery/yarn.lock b/gallery/yarn.lock index 05f3e0b28..8767f60fd 100644 --- a/gallery/yarn.lock +++ b/gallery/yarn.lock @@ -7574,10 +7574,10 @@ postcss@^7.0.35: picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.0.9, postcss@^8.3.5, postcss@^8.4.17, postcss@^8.4.19, postcss@^8.4.4: - version "8.4.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" - integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== +postcss@^8.0.9, postcss@^8.3.5, postcss@^8.4.19, postcss@^8.4.31, postcss@^8.4.4: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" From f823de2a4b6c0590cbf660b40475176da87e4c4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:19:15 +0100 Subject: [PATCH 020/107] Bump postcss from 8.4.21 to 8.4.31 (#655) Bumps [postcss](https://github.com/postcss/postcss) from 8.4.21 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.21...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: Harold Agudelo Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Niels de Jong --- yarn.lock | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index 07ef38fcf..8f433eebd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10301,7 +10301,7 @@ nano-css@^5.3.1: stacktrace-js "^2.0.2" stylis "^4.0.6" -nanoid@^3.3.4, nanoid@^3.3.6: +nanoid@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== @@ -11240,19 +11240,10 @@ postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.4.21: - version "8.4.21" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" - integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== - dependencies: - nanoid "^3.3.4" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -postcss@^8.4.23: - version "8.4.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" - integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== +postcss@^8.4.21, postcss@^8.4.23: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" From 9dfff3776970875380bf9e92c9cd5a036fe3bea9 Mon Sep 17 00:00:00 2001 From: jacobbleakley-neo4j Date: Mon, 6 Nov 2023 10:43:37 +0000 Subject: [PATCH 021/107] added minimum bar height --- src/chart/bar/BarChart.tsx | 40 ++++---- src/config/ReportConfig.tsx | 188 +++++++++++++++++++----------------- 2 files changed, 119 insertions(+), 109 deletions(-) diff --git a/src/chart/bar/BarChart.tsx b/src/chart/bar/BarChart.tsx index f8f70c7f8..58e6b6138 100644 --- a/src/chart/bar/BarChart.tsx +++ b/src/chart/bar/BarChart.tsx @@ -1,5 +1,5 @@ import { ResponsiveBar, ResponsiveBarCanvas } from '@nivo/bar'; -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { NoDrawableDataErrorMessage } from '../../component/editor/CodeViewerComponent'; import { getD3ColorsByScheme } from '../../config/ColorConfig'; import { evaluateRulesOnDict, useStyleRules } from '../../extensions/styling/StyleRuleEvaluator'; @@ -45,6 +45,7 @@ const NeoBarChart = (props: ChartProps) => { const padding = settings.padding ? settings.padding : 0.25; const innerPadding = settings.innerPadding ? settings.innerPadding : 0; const minBarHeight = settings.minBarHeight ? settings.minBarHeight : 0; + const expandForLegend = settings.expandForLegend ? settings.expandForLegend : false; const actionsRules = extensionEnabled(props.extensions, 'actions') && props.settings && props.settings.actionsRules @@ -102,16 +103,16 @@ const NeoBarChart = (props: ChartProps) => { 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; - // }) + }, []) + .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); @@ -162,6 +163,10 @@ const NeoBarChart = (props: ChartProps) => { }; const BarComponent = ({ bar, borderColor }) => { + const dataItem = data.find(item => item.value = bar.data.value); + console.log(dataItem ? dataItem.value : null); + const value = dataItem ? dataItem[bar.id] : null; + const indexValue = dataItem ? dataItem.index : null; let shade = false; let darkTop = false; let includeIndex = false; @@ -218,11 +223,11 @@ const NeoBarChart = (props: ChartProps) => { dominantBaseline='central' fill={borderColor} style={{ - fontWeight: 400, - fontSize: 13, + fontWeight: 100, + fontSize: 10, }} > - {bar.data.value} + {dataItem? dataItem.value : ''} ) : ( <> @@ -237,7 +242,8 @@ const NeoBarChart = (props: ChartProps) => { return { width: this.offsetWidth, height: this.offsetHeight }; }; - const extraProperties = positionLabel == 'off' ? {} : { barComponent: BarComponent }; + // positionLabel == 'off' ? {} : + const extraProperties = { barComponent: BarComponent }; const canvas = data.length > 30; const BarChartComponent = canvas ? ResponsiveBarCanvas : ResponsiveBar; @@ -253,8 +259,6 @@ const NeoBarChart = (props: ChartProps) => { (data.length - 1) * 4 + (data.length - 1) * innerPadding * 4; - // Scrollable Wrapper - const scrollableWrapperStyle: React.CSSProperties = { width: legendPosition === 'Horizontal' @@ -262,7 +266,7 @@ const NeoBarChart = (props: ChartProps) => { ? adaptableWidth : itemWidthConst * data.length + 200 : barWidth * 5 * data.length + itemWidthConst, - height: legendPosition === 'Horizontal' ? '100%' : 18 * data.length + itemWidthConst * 1.2 + marginBottom, + height: expandForLegend ? 18 * data.length + itemWidthConst * 1.2 + marginBottom : '100%', whiteSpace: 'nowrap', }; diff --git a/src/config/ReportConfig.tsx b/src/config/ReportConfig.tsx index d7023c61f..75c07d010 100644 --- a/src/config/ReportConfig.tsx +++ b/src/config/ReportConfig.tsx @@ -331,59 +331,46 @@ const _REPORT_TYPES = { }, maxRecords: 250, settings: { - legend: { - label: 'Show Legend', + autorun: { + label: 'Auto-run query', + type: SELECTION_TYPES.LIST, + values: [true, false], + default: true, + }, + barValues: { + label: 'Bar Values', type: SELECTION_TYPES.LIST, values: [true, false], default: false, }, barWidth: { - label: 'Bar Width (Use to size graph width)', + label: 'Bar Width', type: SELECTION_TYPES.NUMBER, default: 10, }, - padding: { - label: 'Padding (Proportional)', - type: SELECTION_TYPES.NUMBER, - default: 0.25, - }, - innerPadding: { - label: 'Padding Between Grouped Bars', - type: SELECTION_TYPES.NUMBER, - default: 0, - }, - minBarHeight: { - label: 'Minimum Bar Height', - type: SELECTION_TYPES.NUMBER, - default: 0, - }, - legendPosition: { - label: 'Legend Position', + colors: { + label: 'Colors', type: SELECTION_TYPES.LIST, - values: ['Horizontal', 'Vertical'], - default: 'Vertical' + values: ['nivo', 'category10', 'accent', 'dark2', 'paired', 'pastel1', 'pastel2', 'set1', 'set2', 'set3'], + default: 'set2', }, - showOptionalSelections: { - label: 'Grouping', + downloadImageEnabled: { + label: 'Download Image', type: SELECTION_TYPES.LIST, values: [true, false], default: false, }, - valueScale: { - label: 'Value Scale', + expandForLegend: { + label: 'Expand For Legend', type: SELECTION_TYPES.LIST, - values: ['linear', 'symlog'], - default: 'linear', - }, - minValue: { - label: 'Min Value', - type: SELECTION_TYPES.NUMBER, - default: 'auto', + values: [true, false], + default: false, }, - maxValue: { - label: 'Max Value', - type: SELECTION_TYPES.NUMBER, - default: 'auto', + fullscreenEnabled: { + label: 'Fullscreen', + type: SELECTION_TYPES.LIST, + values: [true, false], + default: false, }, groupMode: { label: 'Group Mode', @@ -391,106 +378,125 @@ const _REPORT_TYPES = { values: ['grouped', 'stacked'], default: 'stacked', }, - layout: { - label: 'Layout', - type: SELECTION_TYPES.LIST, - values: ['horizontal', 'vertical'], - default: 'vertical', - }, - colors: { - label: 'Color Scheme', - type: SELECTION_TYPES.LIST, - values: ['nivo', 'category10', 'accent', 'dark2', 'paired', 'pastel1', 'pastel2', 'set1', 'set2', 'set3'], - default: 'set2', - }, - barValues: { - label: 'Show Value on Bars', + hideSelections: { + label: 'Hide Selections', type: SELECTION_TYPES.LIST, values: [true, false], default: false, }, - labelSkipWidth: { - label: 'Skip label on width (px)', + innerPadding: { + label: 'Inner Padding', type: SELECTION_TYPES.NUMBER, default: 0, }, + labelRotation: { + label: 'Label Rotation', + type: SELECTION_TYPES.NUMBER, + default: 45, + }, labelSkipHeight: { - label: 'Skip label on height (px)', + label: 'Label Skip Height', type: SELECTION_TYPES.NUMBER, default: 0, }, - positionLabel: { - label: 'Custom label position', + labelSkipWidth: { + label: 'Label Skip Width', + type: SELECTION_TYPES.NUMBER, + default: 0, + }, + layout: { + label: 'Layout', type: SELECTION_TYPES.LIST, - values: ['off', 'top', 'bottom'], - default: 'off', + values: ['horizontal', 'vertical'], + default: 'vertical', }, - labelRotation: { - label: 'Label Rotation (degrees)', + legend: { + label: 'Legend', + type: SELECTION_TYPES.LIST, + values: [true, false], + default: false, + }, + legendPosition: { + label: 'Legend Position', + type: SELECTION_TYPES.LIST, + values: ['Horizontal', 'Vertical'], + default: 'Vertical', + }, + legendWidth: { + label: 'Legend Width', + type: SELECTION_TYPES.NUMBER, + default: 128, + }, + marginBottom: { + label: 'Margin Bottom', type: SELECTION_TYPES.NUMBER, default: 45, }, marginLeft: { - label: 'Margin Left (px)', + label: 'Margin Left', type: SELECTION_TYPES.NUMBER, default: 50, }, marginRight: { - label: 'Margin Right (px)', + label: 'Margin Right', type: SELECTION_TYPES.NUMBER, default: 24, }, marginTop: { - label: 'Margin Top (px)', + label: 'Margin Top', type: SELECTION_TYPES.NUMBER, default: 24, }, - marginBottom: { - label: 'Margin Bottom (px)', + maxValue: { + label: 'Max Value', type: SELECTION_TYPES.NUMBER, - default: 45, + default: 'auto', }, - legendWidth: { - label: 'Legend Width (px)', + minBarHeight: { + label: 'Min Bar Height', type: SELECTION_TYPES.NUMBER, - default: 128, + default: 0, }, - hideSelections: { - label: 'Hide Property Selection', + minValue: { + label: 'Min Value', + type: SELECTION_TYPES.NUMBER, + default: 'auto', + }, + padding: { + label: 'Padding', + type: SELECTION_TYPES.NUMBER, + default: 0.25, + }, + positionLabel: { + label: 'Position Label', type: SELECTION_TYPES.LIST, - values: [true, false], - default: false, + values: ['off', 'top', 'bottom'], + default: 'off', }, refreshButtonEnabled: { - label: 'Refreshable', + label: 'Refresh Button', type: SELECTION_TYPES.LIST, values: [true, false], default: false, }, - fullscreenEnabled: { - label: 'Fullscreen enabled', - type: SELECTION_TYPES.LIST, - values: [true, false], - default: false, + refreshRate: { + label: 'Refresh Rate', + type: SELECTION_TYPES.NUMBER, + default: '0 (No refresh)', }, - downloadImageEnabled: { - label: 'Download Image enabled', + showOptionalSelections: { + label: 'Show Optional Selections', type: SELECTION_TYPES.LIST, values: [true, false], default: false, }, - autorun: { - label: 'Auto-run query', + valueScale: { + label: 'Value Scale', type: SELECTION_TYPES.LIST, - values: [true, false], - default: true, - }, - refreshRate: { - label: 'Refresh rate (seconds)', - type: SELECTION_TYPES.NUMBER, - default: '0 (No refresh)', + values: ['linear', 'symlog'], + default: 'linear', }, - }, + } }, pie: { label: 'Pie Chart', From e4b0ecde073ba071f7b245a3ad9eb15365d4ea5b Mon Sep 17 00:00:00 2001 From: Jon Ander Oribe Date: Mon, 6 Nov 2023 13:22:22 +0100 Subject: [PATCH 022/107] Text hard to read on dark theme (#668) * Hiding columns in Table does not work #374 * Revert "Hiding columns in Table does not work #374" This reverts commit dc36d84e9cc3de2dc3f8bd093328b991b22aadbb. * Text hard to read on Dark Theme #667 * Fixed code style --------- Co-authored-by: Niels de Jong --- src/card/view/CardView.tsx | 1 + src/card/view/CardViewFooter.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/card/view/CardView.tsx b/src/card/view/CardView.tsx index ff4bd05b6..02b95b721 100644 --- a/src/card/view/CardView.tsx +++ b/src/card/view/CardView.tsx @@ -118,6 +118,7 @@ const NeoCardView = ({ type={type} onSelectionUpdate={onSelectionUpdate} showOptionalSelections={settings.showOptionalSelections} + dashboardSettings={dashboardSettings} > ) : ( <> diff --git a/src/card/view/CardViewFooter.tsx b/src/card/view/CardViewFooter.tsx index 790e2be19..ac0803c66 100644 --- a/src/card/view/CardViewFooter.tsx +++ b/src/card/view/CardViewFooter.tsx @@ -13,6 +13,7 @@ const NeoCardViewFooter = ({ extensions, showOptionalSelections, onSelectionUpdate, + dashboardSettings, }) => { /** * For each selectable field in the visualization, give the user an option to select them from the query output fields. @@ -62,9 +63,13 @@ const NeoCardViewFooter = ({ 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} + + {nodeLabel} +