From 0d62d15a9a0a04e3c88bb7ab52b8f91fbfc5039c Mon Sep 17 00:00:00 2001 From: Michael Stauffer Date: Fri, 12 Jun 2020 10:56:56 -0400 Subject: [PATCH 01/30] add file upload via drag-drop --- lab/webapp/dist/App.css | 10 + lab/webapp/package-lock.json | 257 ++++++++++-------- lab/webapp/package.json | 1 + lab/webapp/src/components/FileUpload/index.js | 116 +++++--- 4 files changed, 238 insertions(+), 146 deletions(-) diff --git a/lab/webapp/dist/App.css b/lab/webapp/dist/App.css index 0d0fa2305..67bdf61ac 100644 --- a/lab/webapp/dist/App.css +++ b/lab/webapp/dist/App.css @@ -436,6 +436,16 @@ pre.schema { font-style: italic; } +/* For file drag-n-drop functionality */ +.pennai .dropzone { + text-align: center; + padding: 20px; + border: 3px dashed #474747; + background-color:#2185D0; + color:#ffffff; + font-size: 1.5rem; +} + @media (max-width: 321px) { } diff --git a/lab/webapp/package-lock.json b/lab/webapp/package-lock.json index ae9793c18..660b8f0b7 100644 --- a/lab/webapp/package-lock.json +++ b/lab/webapp/package-lock.json @@ -3857,9 +3857,9 @@ } }, "@react-native-community/cli-platform-android": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-4.9.0.tgz", - "integrity": "sha512-Tw2rQ84zXl5BGZT3rWmDXXUtkmHr73qa/qaP2WMemHTM28boXBdzkkVCnJMdCLZnCphOcpikYsBQALgqFKWOeQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-4.10.0.tgz", + "integrity": "sha512-/nfCQDbrS0F2u6nwo+4qgx+Fjcv/Rqrn4JbQWdGWEXULfCN+g2Zx9O7sSDNjV7AxOwd+sBOnU945wHkSQdASFA==", "dev": true, "requires": { "@react-native-community/cli-tools": "^4.9.0", @@ -3869,7 +3869,7 @@ "glob": "^7.1.3", "jetifier": "^1.6.2", "lodash": "^4.17.15", - "logkitty": "^0.6.0", + "logkitty": "^0.7.1", "slash": "^3.0.0", "xmldoc": "^1.1.2" }, @@ -5454,6 +5454,11 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "attr-accept": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.1.0.tgz", + "integrity": "sha512-sLzVM3zCCmmDtDNhI0i96k6PUztkotSOXqE4kDGQt/6iDi5M+H0srjeF+QC6jN581l4X/Zq3Zu/tgcErEssavg==" + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -5785,8 +5790,7 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "resolved": "", "dev": true } } @@ -6525,12 +6529,6 @@ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -7019,9 +7017,9 @@ "dev": true }, "dayjs": { - "version": "1.8.27", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.27.tgz", - "integrity": "sha512-Jpa2acjWIeOkg8KURUHICk0EqnEFSSF5eMEscsOgyJ92ZukXwmpmRkPSUka7KHSfbj5eKH30ieosYip+ky9emQ==", + "version": "1.8.28", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.28.tgz", + "integrity": "sha512-ccnYgKC0/hPSGXxj7Ju6AV/BP4HUkXC2u15mikXT5mX9YorEaoi1bEKOmAqdkJHN4EEkmAf97SpH66Try5Mbeg==", "dev": true }, "debug": { @@ -8182,6 +8180,14 @@ "escape-string-regexp": "^1.0.5" } }, + "file-selector": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.12.tgz", + "integrity": "sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==", + "requires": { + "tslib": "^1.9.0" + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -11481,9 +11487,9 @@ } }, "jetifier": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/jetifier/-/jetifier-1.6.5.tgz", - "integrity": "sha512-T7yzBSu9PR+DqjYt+I0KVO1XTb1QhAfHnXV5Nd3xpbXM6Xg4e3vP60Q4qkNU8Fh6PHC2PivPUNN3rY7G2MxcDQ==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/jetifier/-/jetifier-1.6.6.tgz", + "integrity": "sha512-JNAkmPeB/GS2tCRqUzRPsTOHpGDah7xP18vGJfIjZC+W2sxEHbxgJxetIjIqhjQ3yYbYNEELkM/spKLtwoOSUQ==", "dev": true }, "js-tokens": { @@ -11816,135 +11822,158 @@ } }, "logkitty": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.6.1.tgz", - "integrity": "sha512-cHuXN8qUZuzX/7kB6VyS7kB4xyD24e8gyHXIFNhIv+fjW3P+jEXNUhj0o/7qWJtv7UZpbnPgUqzu/AZQ8RAqxQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", + "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==", "dev": true, "requires": { "ansi-fragments": "^0.2.1", "dayjs": "^1.8.15", - "yargs": "^12.0.5" + "yargs": "^15.1.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^5.0.0" } }, "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", "dev": true, "requires": { - "cliui": "^4.0.0", + "cliui": "^6.0.0", "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^2.0.0", + "string-width": "^4.2.0", "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" } }, "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -13055,12 +13084,6 @@ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "dev": true }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -14356,6 +14379,16 @@ } } }, + "react-dropzone": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.0.1.tgz", + "integrity": "sha512-x/6wqRHaR8jsrNiu/boVMIPYuoxb83Vyfv77hO7/3ZRn8Pr+KH5onsCsB8MLBa3zdJl410C5FXPUINbu16XIzw==", + "requires": { + "attr-accept": "^2.0.0", + "file-selector": "^0.1.12", + "prop-types": "^15.7.2" + } + }, "react-is": { "version": "16.8.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", @@ -16001,8 +16034,7 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "resolved": "", "dev": true } } @@ -17143,8 +17175,7 @@ "tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", - "dev": true + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" }, "tty-browserify": { "version": "0.0.0", diff --git a/lab/webapp/package.json b/lab/webapp/package.json index dd778c2fb..38b1225a6 100644 --- a/lab/webapp/package.json +++ b/lab/webapp/package.json @@ -29,6 +29,7 @@ "papaparse": "^4.6.3", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-dropzone": "^11.0.1", "react-redux": "^5.1.2", "react-responsive": "^8.0.3", "react-router": "^3.2.6", diff --git a/lab/webapp/src/components/FileUpload/index.js b/lab/webapp/src/components/FileUpload/index.js index 5decb84ad..b1596fa1e 100644 --- a/lab/webapp/src/components/FileUpload/index.js +++ b/lab/webapp/src/components/FileUpload/index.js @@ -23,8 +23,10 @@ import { Accordion, Icon, Label, - Divider + Divider, + Modal } from 'semantic-ui-react'; +import Dropzone from 'react-dropzone' class FileUpload extends Component { /** @@ -40,7 +42,8 @@ class FileUpload extends Component { catFeatures: '', ordinalFeatures: {}, ordinalIndex: 0, - activeAccordionIndexes: [] + activeAccordionIndexes: [], + openFileTypePop: false }; // enter info in text fields @@ -52,6 +55,7 @@ class FileUpload extends Component { this.getAccordionInputs = this.getAccordionInputs.bind(this); this.generateFileData = this.generateFileData.bind(this); this.errorPopupTimeout = this.errorPopupTimeout.bind(this); + this.handleClose = this.handleClose.bind(this); //this.cleanedInput = this.cleanedInput.bind(this) this.defaultPredictionType = "classification" @@ -233,15 +237,30 @@ class FileUpload extends Component { return data; } + /** + * Event handler for showing message when unsupported filetype is selected for upload by user. + * @param {Array} fileObj - array of rejected files (we only expect one, and use just the first) + * @returns {void} - no return value + */ + handleRejectedFile = files => { + console.log('Filetype not csv or tsv:', files[0]); + this.setState({ + selectedFile: null, + datasetPreview: null, + errorResp: undefined, + openFileTypePopup: true + }); + } + /** * Event handler for selecting files, takes user file from html file input, stores * selected file in component react state, generates file preview and stores that * in the state as well. If file is valid does the abovementioned, else error * is generated - * @param {Event} event - DOM Event from user interacting with UI text field + * @param {Array} fileObj - array of selected files (we only expect one, and use just the first) * @returns {void} - no return value */ - handleSelectedFile = event => { + handleSelectedFile = files => { const fileExtList = ['csv', 'tsv']; let papaConfig = { @@ -254,15 +273,14 @@ class FileUpload extends Component { }; // check for selected file - if(event.target.files && event.target.files[0]) { + if(files && files[0]) { // immediately try to get dataset preview on file input html element change // need to be mindful of garbage data/files //console.log(typeof event.target.files[0]); //console.log(event.target.files[0]); - let uploadFile = event.target.files[0] + let uploadFile = files[0] let fileExt = uploadFile.name.split('.').pop(); - //Papa.parse(event.target.files[0], papaConfig); // check file extensions if (fileExtList.includes(fileExt)) { // use try/catch block to deal with potential bad file input when trying to @@ -281,14 +299,14 @@ class FileUpload extends Component { } this.setState({ - selectedFile: event.target.files[0], + selectedFile: files[0], errorResp: undefined, datasetPreview: null, openFileTypePopup: false }); } else { - console.warn('Filetype not csv or tsv:', uploadFile); + console.log('Filetype not csv or tsv:', uploadFile); this.setState({ selectedFile: null, datasetPreview: null, @@ -581,11 +599,17 @@ class FileUpload extends Component { /** * Simple timeout function, resets error message */ - errorPopupTimeout() { - this.setState({ - errorResp: undefined - }); - } + errorPopupTimeout() { + this.setState({ + errorResp: undefined + }); + } + + handleClose(){ + this.setState({ + openFileTypePopup: false + }); + } render() { @@ -609,34 +633,60 @@ class FileUpload extends Component { // display file extension Popup let openFileTypePop; this.state.openFileTypePopup ? openFileTypePop = this.state.openFileTypePopup : openFileTypePop = false; + + /* ORIG + let fileInputElem = ( + +

Please select new dataset +
+ Supported file types: (csv, tsv)

+ + } + id="upload_dataset_file_browser_button" + onChange={this.handleSelectedFile} + /> + ); + */ + // file input + // https://react-dropzone.js.org/ + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept* let fileInputElem = ( - -

Please select new dataset -
- Supported file types: (csv, tsv)

- - } - id="upload_dataset_file_browser_button" - onChange={this.handleSelectedFile} - /> + + {({getRootProps, getInputProps}) => ( +
+
+ +

Upload a csv or tsv file

+

Drag 'n' drop it here, or click to select

+
+
+ )} +
); + return (
- + {fileInputElem} + + Invalid file type chosen + + {"Please choose .cvs or .tsv files"} + +
Date: Thu, 2 Jul 2020 17:53:40 -0400 Subject: [PATCH 02/30] File Upload UI: categorical and ordinal types Add dropdowns for each feature to select its type. Code working for categorical types. Code working mostly for ordinal types. Need UI for setting ordinal order and UI visual changes overall. --- lab/webapp/src/components/FileUpload/index.js | 243 ++++++++++++++---- 1 file changed, 198 insertions(+), 45 deletions(-) diff --git a/lab/webapp/src/components/FileUpload/index.js b/lab/webapp/src/components/FileUpload/index.js index b1596fa1e..d1fc9d41c 100644 --- a/lab/webapp/src/components/FileUpload/index.js +++ b/lab/webapp/src/components/FileUpload/index.js @@ -24,31 +24,43 @@ import { Icon, Label, Divider, - Modal + Modal, + Message } from 'semantic-ui-react'; import Dropzone from 'react-dropzone' class FileUpload extends Component { + + //Some pseudo-constants to avoid typos + get featureTypeNumeric() { return 'numeric'; } + get featureTypeCategorical() { return 'categorical'; } + get featureTypeOrdinal() { return 'ordinal'; } + /** - * FileUpload reac component - UI form for uploading datasets - * @constructor - */ + * FileUpload reac component - UI form for uploading datasets + * @constructor + */ constructor(props) { super(props); this.state = { selectedFile: null, dependentCol: '', - catFeatures: '', - ordinalFeatures: {}, + catFeaturesText: '', + /** {boolean} True when user has specified the categorical features via the text box */ + catFeaturesStringInUse: false, + ordinalFeaturesText: '', + ordinalFeaturesValues: {}, ordinalIndex: 0, activeAccordionIndexes: [], - openFileTypePop: false + openFileTypePop: false, + /** {array} String-array holding the type for each feature, in same index order */ + featureTypeFromDropdown: [] }; // enter info in text fields this.handleDepColField = this.handleDepColField.bind(this); - this.handleCatFeatures = this.handleCatFeatures.bind(this); + this.handlecatFeaturesText = this.handlecatFeaturesText.bind(this); this.handleOrdinalFeatures = this.handleOrdinalFeatures.bind(this); this.handlePredictionType = this.handlePredictionType.bind(this); this.getDataTablePreview = this.getDataTablePreview.bind(this); @@ -56,6 +68,10 @@ class FileUpload extends Component { this.generateFileData = this.generateFileData.bind(this); this.errorPopupTimeout = this.errorPopupTimeout.bind(this); this.handleClose = this.handleClose.bind(this); + this.handleFeatureTypeDropdown = this.handleFeatureTypeDropdown.bind(this); + this.initDatasetPreview = this.initDatasetPreview.bind(this); + this.getCatFeaturesFromDropdowns = this.getCatFeaturesFromDropdowns.bind(this); + this.handleOrderButton = this.handleOrderButton.bind(this); //this.cleanedInput = this.cleanedInput.bind(this) this.defaultPredictionType = "classification" @@ -65,7 +81,7 @@ class FileUpload extends Component { For example, if analyzing a dataset of patients with different types of diabetes, this column may have the values "type1", "type2", or "none".`; - this.predictionTypeHelp = (

Classification algorithms to are used to model discrete categorical outputs. + this.predictionTypeHelp = (

Classification algorithms are used to model discrete categorical outputs. Examples include modeling the color car someone might buy ("red", "green", "blue"...) or a disease state ("type1Diabetes", "type2Diabetes", "none"...)

Regression algorithms are used to model a continuous valued output. Examples include modeling the amount of money a house is predicted to sell for.

); @@ -82,6 +98,9 @@ class FileUpload extends Component { Describe these features using a json map. The map key is the name of the field, and the map value is an ordered list of the values the field can take. Example:
{"{\"rank\":[\"first\", \"second\", \"third\"], \"size\":[\"small\", \"medium\", \"large\"]}"}

); + + //A counter to use in loops + this.indexCounter = 0; } /** @@ -91,12 +110,25 @@ class FileUpload extends Component { this.setState({ selectedFile: null, dependentCol: '', - catFeatures: '', - ordinalFeatures: '', + catFeaturesText: '', + catFeaturesStringInUse: false, + ordinalFeaturesText: '', + /** {object} Object used as dictionary to track features designated as ordinal by user via dataset preview UI. + * + * key: feature name from dataPreview + * + * value: string-array holding possibly-ordered values for the feature. + * + * Will be empty object if none defined. + * Gets updated with new order as user orders them using the UI in dataset preview. + * Using objects as dictionary: https://pietschsoft.com/post/2015/09/05/javascript-basics-how-to-create-a-dictionary-with-keyvalue-pairs + */ + ordinalFeaturesValues: {}, ordinalIndex: 0, predictionType: this.defaultPredictionType, activeAccordionIndexes: [], - errorResp: undefined + errorResp: undefined, + featureTypeFromDropdown: [] }); } @@ -135,11 +167,12 @@ class FileUpload extends Component { * @param {Event} e - DOM Event from user interacting with UI text field * @returns {void} - no return value */ - handleCatFeatures(e) { + handlecatFeaturesText(e) { //let safeInput = this.purgeUserInput(e.target.value); //window.console.log('safe input cat: ', safeInput); this.setState({ - catFeatures: e.target.value, + catFeaturesText: e.target.value, + catFeaturesStringInUse: e.target.value != "", errorResp: undefined }); } @@ -156,7 +189,7 @@ class FileUpload extends Component { //let safeInput = this.purgeUserInput(props.value); //window.console.log('safe input ord: ', safeInput); this.setState({ - ordinalFeatures: e.target.value, + ordinalFeaturesText: e.target.value, errorResp: undefined }); } @@ -178,8 +211,8 @@ class FileUpload extends Component { const data = new FormData(); this.setState({errorResp: undefined}); let depCol = this.state.dependentCol; - let ordFeatures = this.state.ordinalFeatures; - let catFeatures = this.state.catFeatures; + let ordFeatures = this.state.ordinalFeaturesText; + let catFeaturesText = this.state.catFeaturesText; let predictionType = this.state.predictionType; if(this.state.selectedFile && this.state.selectedFile.name) { @@ -192,25 +225,31 @@ class FileUpload extends Component { // try to parse ord features input as JSON if not empty if(ordFeatures !== '') { try { - ordFeatures = JSON.parse(this.state.ordinalFeatures); + ordFeatures = JSON.parse(this.state.ordinalFeaturesText); } catch(e) { // if expecting oridinal stuff, return error to stop upload process return { errorResp: e.toString() }; } } - if(catFeatures !== "") { + // Categorical feature assignments + // Start with getting assignments from the dropdowns available in the dataset preview. + // If user has specified categorical features in the text box, we use that instead. + // + let catFeaturesAssigned = this.getCatFeaturesFromDropdowns(); + if(catFeaturesText !== "") { // remove all whitespace - catFeatures = catFeatures.replace(/ /g, ''); + catFeaturesText = catFeaturesText.replace(/ /g, ''); // parse on comma - catFeatures = catFeatures.split(','); + catFeaturesText = catFeaturesText.split(','); // if input contains empty items - ex: 'one,,two,three' // filter out resulting empty item - catFeatures = catFeatures.filter(item => { + catFeaturesAssigned = catFeaturesText.filter(item => { return item !== "" }) } + console.log('catFeaturesAssigned: ' + catFeaturesAssigned); // keys specified for server to upload repsective fields, // filter let metadata = JSON.stringify({ @@ -219,7 +258,7 @@ class FileUpload extends Component { 'timestamp': Date.now(), 'dependent_col' : depCol, 'prediction_type' : predictionType, - 'categorical_features': catFeatures, + 'categorical_features': catFeaturesAssigned, 'ordinal_features': ordFeatures }); @@ -252,6 +291,20 @@ class FileUpload extends Component { }); } + /** + * Called when a new dataset has been loaded for preview. + * Do whatever needs to be done. + * @returns {void} - no return value + */ + initDatasetPreview = () => { + let dataPrev = this.state.datasetPreview; + //Init the state-tracking of feature-type dropdowns + let featureTypes = Array(dataPrev.meta.fields.length).fill(this.featureTypeNumeric); + this.setState({featureTypeFromDropdown: featureTypes}) + //Init oridinal values + this.setState({ordinalFeaturesValues: {}}) + } + /** * Event handler for selecting files, takes user file from html file input, stores * selected file in component react state, generates file preview and stores that @@ -263,12 +316,13 @@ class FileUpload extends Component { handleSelectedFile = files => { const fileExtList = ['csv', 'tsv']; + //Config for csv reader. We load the whole file so we can let user sort the ordinal features let papaConfig = { header: true, - preview: 5, complete: (result) => { //window.console.log('preview of uploaded data: ', result); this.setState({datasetPreview: result}); + this.initDatasetPreview(); } }; @@ -296,8 +350,12 @@ class FileUpload extends Component { datasetPreview: null, openFileTypePopup: false }); + //Added this return, otherwise it will fall through to state below + return; } + //NOTE - this code is reached before the papaConfig.complete callback is called, + // so if file is parsed successfully, the datasetPreview property will be set this.setState({ selectedFile: files[0], errorResp: undefined, @@ -408,6 +466,42 @@ class FileUpload extends Component { } + handleFeatureTypeDropdown = (e, data) => { + //console.log(data); + // Store the dropdown value + let featureTypes = this.state.featureTypeFromDropdown; + featureTypes[data.customindexid] = data.value; + this.setState({featureTypeFromDropdown: featureTypes}) + //console.log(this.state.featureTypeFromDropdown); + + //Type ordinal + // + let dataPrev = this.state.datasetPreview; + let field = dataPrev.meta.fields[data.customindexid]; + let ords = this.state.ordinalFeaturesValues; + if(data.value == this.featureTypeOrdinal) { + //it's been set as type ordinal, setup its list of values + let column = []; + dataPrev.data.map( (row) => { + column.push( row[field] ); + }) + column = [...new Set(column)]; + ords[field] = column; + } + else { + //Clear the ordinal list in case we had one from before + delete ords[field]; + } + this.setState({ordinalFeaturesValues: ords}); + + console.log('ords: '); for(var f in ords) { console.log(f + ': ' + ords[f]) } + } + + /** Handle clicks from button for user to define order of values in an ordinal feature */ + handleOrderButton = (e, data) => { + console.log('ordnung must sein!'); + } + /** * Small helper method to create table for dataset preview upon selecting csv file. * Copied from Dataset component - relies upon javascript library papaparse to @@ -421,7 +515,8 @@ class FileUpload extends Component { let innerContent; if(dataPrev && dataPrev.data) { - innerContent = dataPrev.data.slice(0, 100).map((row, i) => + //Show at most 50 rows + innerContent = dataPrev.data.slice(0, 50).map((row, i) => {dataPrev.meta.fields.map(field => { let tempKey = i + field; @@ -435,6 +530,13 @@ class FileUpload extends Component { ); + //Options for the per-feature dropdown in dataset preview + const featureTypeOptions = [ + { key: 1, text: 'Numeric', value: this.featureTypeNumeric}, + { key: 2, text: 'Categorical', value: this.featureTypeCategorical }, + { key: 3, text: 'Ordinal', value: this.featureTypeOrdinal }, + ] + dataPrevTable = (

@@ -445,12 +547,54 @@ class FileUpload extends Component { + {/*'key' is a React property to id elements in a list*/} {dataPrev.meta.fields.map(field => {field} )} + {/* row of dropdowns for specifying feature type*/} + + { dataPrev.meta.fields.map((field, i) => { + return ( + + + + ) + } + )} + + {/* row of buttons for ordering ordinal features */} + + { dataPrev.meta.fields.map((field, i) => { + if(Object.keys(this.state.ordinalFeaturesValues).length == 0) + return undefined; + return ( + +
@@ -493,6 +637,21 @@ class FileUpload extends Component { return predictionSelector; } + /** + * Small helper to get an array of features that have been assigned + * to type 'categorical' using the dropdowns in the Dataset Preview + * @returns {array} - array of strings + */ + getCatFeaturesFromDropdowns(){ + let dataPrev = this.state.datasetPreview; + let featureTypes = this.state.featureTypeFromDropdown; + if(!dataPrev) + return []; + return dataPrev.meta.fields.filter( (field,i) => { + return featureTypes[i] == this.featureTypeCategorical; + }) + } + /** * Small helper method to create semantic ui accordion for categorical & * ordinal text inputs @@ -548,8 +707,13 @@ class FileUpload extends Component { className="file-upload-categorical-text-area" id="categorical_features_text_area_input" label="Categorical Features" - placeholder={"cat_feat_1, cat_feat_2"} - onChange={this.handleCatFeatures} + placeholder= { + ("Enter a comma-separated list here to override selections in the Dataset Preview.\n"+ + "For example:\n\n \theight,age,toe_length\n\n" + + "Current selections from Dataset Preview: \n" + + (this.getCatFeaturesFromDropdowns().length > 0 ? this.getCatFeaturesFromDropdowns().join() : "(none)")) + } + onChange={this.handlecatFeaturesText} /> -

Please select new dataset -
- Supported file types: (csv, tsv)

-
- } - id="upload_dataset_file_browser_button" - onChange={this.handleSelectedFile} - /> - ); - */ - // file input // https://react-dropzone.js.org/ // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept* @@ -755,6 +901,13 @@ class FileUpload extends Component { } /> + +

+ You can specify what type of data each feature/column holds. + Each feature is assumed to be Numerical unless it is designated as Ordinal or Categorical. + Designate a feature type using either of the text input boxes below or by using the dropdown choices available for each column in the Dataset Preview. +

+
Date: Thu, 9 Jul 2020 17:20:28 -0400 Subject: [PATCH 03/30] File Upload UI: add basics of popup for ordinal value ordering Commit before adding react-sortable-hoc package --- lab/webapp/src/components/FileUpload/index.js | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/lab/webapp/src/components/FileUpload/index.js b/lab/webapp/src/components/FileUpload/index.js index d1fc9d41c..bab5c9837 100644 --- a/lab/webapp/src/components/FileUpload/index.js +++ b/lab/webapp/src/components/FileUpload/index.js @@ -576,22 +576,41 @@ class FileUpload extends Component { {/* row of buttons for ordering ordinal features */} { dataPrev.meta.fields.map((field, i) => { - if(Object.keys(this.state.ordinalFeaturesValues).length == 0) - return undefined; + //If the feature is not type ordinal, just add an empty cell + if(this.state.ordinalFeaturesValues[field] === undefined) + return ; return ( -
+ } + trigger={ +
- } - trigger={ + } + //If we're currently ranking this ordinal feature, show the sortable list + if(this.state.ordinalFeatureToRank === field) + return ( + //This puts the sortable list right in the cell. Awkward but it works. + + } + open={this.state.catFeaturesUserTextModalOpen} + onOpen={() => this.setState({catFeaturesUserTextModalOpen: true})} + closeOnDimmerClick={false} + closeOnEscape={false} + > + Categorical Feature Input + +

Enter a comma-separated list to specify which features are Categorical.
+ This will override selections in the Dataset Preview.

+

For example:
+  sex,eye_color,disease_state +

+
+
+
+ +
+ + } + trigger= + {} open={this.state.ordinalFeaturesUserTextModalOpen} onOpen={() => this.setState({ordinalFeaturesUserTextModalOpen: true})} + onClose={() => this.handleOrdinalFeaturesUserTextCancel()} closeOnDimmerClick={false} - closeOnEscape={false} + closeOnEscape={true} + disabled={this.state.ordinalFeatureToRank !== undefined} > Ordinal Feature Input @@ -1104,6 +1209,7 @@ handleCatFeaturesUserTextCancel() {

For example:
 month,jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec
 day,mon,tue,wed,thu,fri,sat,sun

+

To populate this text box with all features and their unique values, close this window and use the button to set all feature types as ordinal.


@@ -1159,18 +1265,32 @@ handleCatFeaturesUserTextCancel() { {/*---- Categorical Feature Text Input ----*/} Specify Categorical Features} + trigger={} open={this.state.catFeaturesUserTextModalOpen} onOpen={() => this.setState({catFeaturesUserTextModalOpen: true})} + onClose={() => this.handleCatFeaturesUserTextCancel()} closeOnDimmerClick={false} - closeOnEscape={false} + closeOnEscape={true} > Categorical Feature Input

Enter a comma-separated list to specify which features are Categorical.
This will override selections in the Dataset Preview.

For example:
-  sex,eye_color,disease_state +  sex,eye_color,hair_color,disease_state +

+

Ranges - you can specify features using ranges. Each feature name in a range is converted to a column number within the data, + and the range is expanded using the column numbers. For example, working from the example above, entering
+  sex-disease_state
+ would expand to
+  sex,eye_color,hair_color,disease_state


@@ -1180,8 +1300,9 @@ handleCatFeaturesUserTextCancel() { id="categorical_features_text_area_input" label="Categorical Features" onChange={this.handleCatFeaturesUserTextOnChange} + onBlur={this.handleCatFeaturesUserTextBlur} placeholder={this.getCatFeatures().length === 0 ? "(No Categorical features have been specified)" : "" } - value={this.state.catFeaturesUserText} + value={catFeaturesUserTextToDisplay} > @@ -1388,17 +1509,44 @@ handleCatFeaturesUserTextCancel() { } /> -

You can specify what type of data each feature/column holds.

Each feature is assumed to be Numerical unless it is designated as either Ordinal or Categorical (aka Nominal).

Designate feature types with either the text input boxes below, or by using the dropdown choices in the Dataset Preview.

-
+ {userTextFeatureInputs} + + } + open={this.state.ordinalFeaturesUserTextModalOpen} + onOpen={() => this.setState({ordinalFeaturesUserTextModalOpen: true})} + onClose={() => this.handleOrdinalFeaturesUserTextCancel()} + closeOnDimmerClick={false} + closeOnEscape={true} + disabled={this.state.ordinalFeatureToRank !== undefined} + > + Ordinal Feature Input + +

For each ordinal feature, enter one comma-separated line with the following format (this overrides selections in the Dataset Preview):
+  [feature name],[1st unique value],[2nd unique value],...

+

For example:
+  month,jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec
+  day,mon,tue,wed,thu,fri,sat,sun

+

To populate this text box with all features and their unique values, close this window and use the button to set all feature types as ordinal.

+
+
+ + + + + } - open={this.state.ordinalFeaturesUserTextModalOpen} - onOpen={() => this.setState({ordinalFeaturesUserTextModalOpen: true})} - onClose={() => this.handleOrdinalFeaturesUserTextCancel()} - closeOnDimmerClick={false} - closeOnEscape={true} - disabled={this.state.ordinalFeatureToRank !== undefined} - > - Ordinal Feature Input - -

For each ordinal feature, enter one comma-separated line with the following format (this overrides selections in the Dataset Preview):
-  [feature name],[1st unique value],[2nd unique value],...

-

For example:
-  month,jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec
-  day,mon,tue,wed,thu,fri,sat,sun

-

To populate this text box with all features and their unique values, close this window and use the button to set all feature types as ordinal.

-
-
-
- -
- - } - open={this.state.catFeaturesUserTextModalOpen} - onOpen={() => this.setState({catFeaturesUserTextModalOpen: true})} - onClose={() => this.handleCatFeaturesUserTextCancel()} - closeOnDimmerClick={false} - closeOnEscape={true} - > - Categorical Feature Input - -

Enter a comma-separated list to specify which features are Categorical.
- This will override selections in the Dataset Preview.

-

For example:
+ Categorical Feature Input + +

Enter a comma-separated list to specify which features are Categorical.
+ This will override selections in the Dataset Preview.

+

For example:
+  sex,eye_color,hair_color,disease_state +

+

Ranges - you can specify features using ranges. Each feature name in a range is converted to a column number within the data, + and the range is expanded using the column numbers. For example, working from the example above in which + we assume the features are present in the data in the same order as listed, entering
+  sex-disease_state

+ would expand to
sex,eye_color,hair_color,disease_state -

-

Ranges - you can specify features using ranges. Each feature name in a range is converted to a column number within the data, - and the range is expanded using the column numbers. For example, working from the example above in which - we assume the features are present in the data in the same order as listed, entering
-  sex-disease_state

- would expand to
-  sex,eye_color,hair_color,disease_state -

-
-
-
- -
- -