From af78c7894155ff24643063165cf4ac85b47c9151 Mon Sep 17 00:00:00 2001 From: Dwayne Harris Date: Tue, 15 Oct 2019 02:48:01 -0400 Subject: [PATCH] WIP --- config/config.json | 3 +- package-lock.json | 160 +++++++++++++++++++++------- package.json | 1 + src/actions/apps.ts | 58 +++++++++- src/actions/authentication.ts | 14 ++- src/actions/config.ts | 14 +++ src/actions/groups.ts | 89 +++++++++++----- src/components/app-list-item.tsx | 16 +++ src/components/app.tsx | 14 ++- src/components/forms/file-field.tsx | 93 ++++++++++++++++ src/components/group-list.tsx | 17 --- src/components/navigation-menu.tsx | 2 +- src/components/pages/apps.tsx | 32 ++++++ src/components/pages/developers.tsx | 8 +- src/components/pages/edit-app.tsx | 124 +++++++++++++++++++++ src/components/pages/groups.tsx | 4 +- src/components/pages/self.tsx | 18 +++- src/components/pages/view-app.tsx | 50 ++++++--- src/components/user-info.tsx | 10 +- src/config.ts | 4 +- src/reducers/apps.ts | 50 +++++++++ src/reducers/config.ts | 20 ++++ src/reducers/groups.ts | 77 +++++++++++-- src/selectors/apps.ts | 19 +--- src/selectors/groups.ts | 26 +---- src/selectors/index.ts | 2 + src/store/index.ts | 4 + src/styles/app.scss | 3 +- src/types/index.ts | 5 + src/types/store.ts | 27 ++++- 30 files changed, 796 insertions(+), 168 deletions(-) create mode 100644 src/actions/config.ts create mode 100644 src/components/app-list-item.tsx create mode 100644 src/components/forms/file-field.tsx delete mode 100644 src/components/group-list.tsx create mode 100644 src/components/pages/apps.tsx create mode 100644 src/components/pages/edit-app.tsx create mode 100644 src/reducers/apps.ts create mode 100644 src/reducers/config.ts diff --git a/config/config.json b/config/config.json index e75c1b2..811af8c 100644 --- a/config/config.json +++ b/config/config.json @@ -1,3 +1,4 @@ { - "apiUrl": "http://localhost:5000" + "apiUrl": "http://localhost:5000", + "blobUrl": "https://flexordev.blob.core.windows.net/media/" } diff --git a/package-lock.json b/package-lock.json index 0def7a0..6c04487 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,55 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@azure/ms-rest-js": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.0.4.tgz", + "integrity": "sha512-nSOPt6st0RtxclYBQV65qXZpvMDqiDQssktvB/SMTAJ5bIytSPtBmlttTTigO5qHvwQcfzzpQE0sMceK+dJ/IQ==", + "requires": { + "@types/node-fetch": "^2.3.7", + "@types/tunnel": "0.0.1", + "abort-controller": "^3.0.0", + "form-data": "^2.5.0", + "node-fetch": "^2.6.0", + "tough-cookie": "^3.0.1", + "tslib": "^1.10.0", + "tunnel": "0.0.6", + "uuid": "^3.3.2", + "xml2js": "^0.4.19" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "@azure/storage-blob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-10.5.0.tgz", + "integrity": "sha512-67+0EP7STy9BQgzvN1RgmSvXhxRd044eDgepX7zBp7XslBxz8YGo2cSLm9w5o5Qf1FLCRlwuziRMikaPCLMpVw==", + "requires": { + "@azure/ms-rest-js": "^2.0.0", + "events": "^3.0.0", + "tslib": "^1.9.3" + } + }, "@babel/runtime": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.5.tgz", @@ -217,8 +266,15 @@ "@types/node": { "version": "12.7.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.2.tgz", - "integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==", - "dev": true + "integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==" + }, + "@types/node-fetch": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.2.tgz", + "integrity": "sha512-djYYKmdNRSBtL1x4CiE9UJb9yZhwtI1VC+UxZD0psNznrUj80ywsxKlEGAE+QL1qvLjPbfb24VosjkYM6W4RSQ==", + "requires": { + "@types/node": "*" + } }, "@types/prop-types": { "version": "15.7.3", @@ -344,6 +400,14 @@ "integrity": "sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==", "dev": true }, + "@types/tunnel": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz", + "integrity": "sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==", + "requires": { + "@types/node": "*" + } + }, "@types/uglify-js": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", @@ -617,6 +681,14 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -979,8 +1051,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.2", @@ -1629,7 +1700,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -2005,7 +2075,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -2069,8 +2138,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", @@ -2333,7 +2401,6 @@ "version": "1.13.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", - "dev": true, "requires": { "es-to-primitive": "^1.2.0", "function-bind": "^1.1.1", @@ -2347,7 +2414,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -2397,6 +2463,11 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "eventemitter3": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", @@ -2406,8 +2477,7 @@ "events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", - "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", - "dev": true + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==" }, "eventsource": { "version": "1.0.7", @@ -3488,8 +3558,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "gauge": { "version": "2.7.4", @@ -3738,7 +3807,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -3769,8 +3837,7 @@ "has-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" }, "has-unicode": { "version": "2.0.1", @@ -4336,8 +4403,7 @@ "ip-regex": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" }, "ipaddr.js": { "version": "1.9.0", @@ -4401,8 +4467,7 @@ "is-callable": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" }, "is-data-descriptor": { "version": "0.1.4", @@ -4427,8 +4492,7 @@ "is-date-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" }, "is-descriptor": { "version": "0.1.6", @@ -4534,7 +4598,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, "requires": { "has": "^1.0.1" } @@ -4549,7 +4612,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, "requires": { "has-symbols": "^1.0.0" } @@ -4951,14 +5013,12 @@ "mime-db": { "version": "1.40.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { "version": "2.1.24", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, "requires": { "mime-db": "1.40.0" } @@ -5167,6 +5227,11 @@ "lower-case": "^1.1.1" } }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, "node-forge": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", @@ -5484,8 +5549,7 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -5500,7 +5564,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, "requires": { "define-properties": "^1.1.2", "es-abstract": "^1.5.1" @@ -6031,8 +6094,7 @@ "psl": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.1.tgz", - "integrity": "sha512-2KLd5fKOdAfShtY2d/8XDWVRnmp3zp40Qt6ge2zBPFARLXOGUf2fHD5eg+TV/5oxBtQKVhjUaKFsAaE4HnwfSA==", - "dev": true + "integrity": "sha512-2KLd5fKOdAfShtY2d/8XDWVRnmp3zp40Qt6ge2zBPFARLXOGUf2fHD5eg+TV/5oxBtQKVhjUaKFsAaE4HnwfSA==" }, "public-encrypt": { "version": "4.0.3", @@ -6084,8 +6146,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "qs": { "version": "6.7.0", @@ -6964,6 +7025,11 @@ } } }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz", @@ -7938,8 +8004,7 @@ "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, "tty-browserify": { "version": "0.0.0", @@ -7947,6 +8012,11 @@ "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", "dev": true }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -8178,7 +8248,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, "requires": { "define-properties": "^1.1.2", "object.getownpropertydescriptors": "^2.0.3" @@ -8815,6 +8884,21 @@ "async-limiter": "~1.0.0" } }, + "xml2js": { + "version": "0.4.22", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.22.tgz", + "integrity": "sha512-MWTbxAQqclRSTnehWWe5nMKzI3VmJ8ltiJEco8akcC6j3miOhjjfzKum5sId+CWhfxdOs/1xauYr8/ZDBtQiRw==", + "requires": { + "sax": ">=0.6.0", + "util.promisify": "~1.0.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 71ca8bb..e2ed22a 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "webpack-dev-server": "^3.8.2" }, "dependencies": { + "@azure/storage-blob": "^10.5.0", "@fortawesome/fontawesome-common-types": "^0.2.25", "@fortawesome/fontawesome-svg-core": "^1.2.25", "@fortawesome/free-solid-svg-icons": "^5.11.2", diff --git a/src/actions/apps.ts b/src/actions/apps.ts index 0026ee3..bda301c 100644 --- a/src/actions/apps.ts +++ b/src/actions/apps.ts @@ -1,3 +1,5 @@ +import { Action } from 'redux' + import { apiFetch } from 'src/api' import { setEntities } from 'src/actions/entities' import { startRequest, finishRequest } from 'src/actions/requests' @@ -5,7 +7,57 @@ import { setFieldNotification } from 'src/actions/forms' import { objectToQuerystring } from 'src/utils' import { normalize } from 'src/utils/normalization' -import { AppThunkAction, RequestKey, EntityType, App, AvailabilityResponse, NotificationType, AppThunkDispatch }from 'src/types' +import { AppThunkAction, RequestKey, EntityType, App, AvailabilityResponse, NotificationType }from 'src/types' + +export interface AppendAppsAction extends Action { + type: 'APPS_APPEND_APPS' + payload: { + items: string[] + continuation?: string + } +} + +export interface ClearAppsAction extends Action { + type: 'APPS_CLEAR_APPS' +} + +export interface AppendCreatedAppsAction extends Action { + type: 'APPS_APPEND_CREATED_APPS' + payload: { + items: string[] + continuation?: string + } +} + +export interface ClearCreatedAppsAction extends Action { + type: 'APPS_CLEAR_CREATED_APPS' +} + +export type AppsActions = AppendAppsAction | ClearAppsAction | AppendCreatedAppsAction | ClearCreatedAppsAction + +export const appendApps = (apps: string[], continuation?: string): AppendAppsAction => ({ + type: 'APPS_APPEND_APPS', + payload: { + items: apps, + continuation, + } +}) + +export const clearApps = (): ClearAppsAction => ({ + type: 'APPS_CLEAR_APPS', +}) + +export const appendCreatedApps = (apps: string[], continuation?: string): AppendCreatedAppsAction => ({ + type: 'APPS_APPEND_CREATED_APPS', + payload: { + items: apps, + continuation, + } +}) + +export const clearCreatedApps = (): ClearCreatedAppsAction => ({ + type: 'APPS_CLEAR_CREATED_APPS', +}) interface AppsResponse { apps: App[] @@ -23,6 +75,7 @@ export const fetchApps = (sort?: string, continuation?: string): AppThunkAction const apps = normalize(response.apps, EntityType.App) dispatch(setEntities(apps.entities)) + dispatch(appendApps(apps.keys, response.continuation)) dispatch(finishRequest(RequestKey.FetchApps, true)) } catch (err) { dispatch(finishRequest(RequestKey.FetchApps, false)) @@ -30,7 +83,7 @@ export const fetchApps = (sort?: string, continuation?: string): AppThunkAction } } -export const fetchSelfApps = (sort?: string): AppThunkAction => async dispatch => { +export const fetchCreatedApps = (sort?: string): AppThunkAction => async dispatch => { dispatch(startRequest(RequestKey.FetchSelfApps)) try { @@ -41,6 +94,7 @@ export const fetchSelfApps = (sort?: string): AppThunkAction => async dispatch = const apps = normalize(response.apps, EntityType.App) dispatch(setEntities(apps.entities)) + dispatch(appendCreatedApps(apps.keys, response.continuation)) dispatch(finishRequest(RequestKey.FetchSelfApps, true)) } catch (err) { dispatch(finishRequest(RequestKey.FetchSelfApps, false)) diff --git a/src/actions/authentication.ts b/src/actions/authentication.ts index 04cec8f..a8f16af 100644 --- a/src/actions/authentication.ts +++ b/src/actions/authentication.ts @@ -107,7 +107,17 @@ export const authenticate = (name: string, password: string): AppThunkAction async dispatch => { +interface UpdateSelfOptions { + name: string + about: string + requiresApproval: boolean + privacy: string + imageUrl: string + coverImageUrl: string +} + +export const updateSelf = (options: UpdateSelfOptions): AppThunkAction => async dispatch => { + const { name, about, requiresApproval, privacy, imageUrl, coverImageUrl } = options dispatch(startRequest(RequestKey.UpdateSelf)) try { @@ -119,6 +129,8 @@ export const updateSelf = (name: string, about: string, requiresApproval: boolea about, requiresApproval, privacy, + imageUrl, + coverImageUrl, }, }) diff --git a/src/actions/config.ts b/src/actions/config.ts new file mode 100644 index 0000000..0fc5dac --- /dev/null +++ b/src/actions/config.ts @@ -0,0 +1,14 @@ +import { Action } from 'redux' +import { Config } from 'src/types' + +export interface SetConfigAction extends Action { + type: 'CONFIG_SET' + payload: Config +} + +export type ConfigActions = SetConfigAction + +export const setConfig = (config: Config): SetConfigAction => ({ + type: 'CONFIG_SET', + payload: config +}) diff --git a/src/actions/groups.ts b/src/actions/groups.ts index 545bba5..a22dc23 100644 --- a/src/actions/groups.ts +++ b/src/actions/groups.ts @@ -8,36 +8,78 @@ import { objectToQuerystring } from 'src/utils' import { normalize } from 'src/utils/normalization' import { AppThunkAction, Entity, RequestKey, EntityType, User } from 'src/types' -export interface SetGroupsAction extends Action { - type: 'GROUPS_SET_GROUPS' - payload: string[] +export interface AppendGroupsAction extends Action { + type: 'GROUPS_APPEND_GROUPS' + payload: { + items: string[] + continuation?: string + } } -export interface AppendGroupsAction extends Action { - type: 'GROUPS_APPEND_GROUPS', - payload: string[] +export interface ClearGroupsAction extends Action { + type: 'GROUPS_CLEAR_GROUPS' +} + +export interface AppendLogsAction extends Action { + type: 'GROUPS_APPEND_LOGS' + payload: { + items: string[] + continuation?: string + } +} + +export interface ClearLogsAction extends Action { + type: 'GROUPS_CLEAR_LOGS' } -export interface SetContinuationAction extends Action { - type: 'GROUPS_SET_CONTINUATION' - payload: string +export interface AppendInvitationsAction extends Action { + type: 'GROUPS_APPEND_INVITATIONS' + payload: { + items: string[] + continuation?: string + } } -export type GroupsActions = SetGroupsAction | AppendGroupsAction | SetContinuationAction +export interface ClearInvitationsAction extends Action { + type: 'GROUPS_CLEAR_INVITATIONS' +} -export const setGroups = (groups: string[]): SetGroupsAction => ({ - type: 'GROUPS_SET_GROUPS', - payload: groups, -}) +export type GroupsActions = AppendGroupsAction | ClearGroupsAction | AppendLogsAction | ClearLogsAction | AppendInvitationsAction | ClearInvitationsAction -export const appendGroups = (groups: string[]): AppendGroupsAction => ({ +export const appendGroups = (groups: string[], continuation?: string): AppendGroupsAction => ({ type: 'GROUPS_APPEND_GROUPS', - payload: groups, + payload: { + items: groups, + continuation, + }, }) -export const setContinuation = (continuation: string): SetContinuationAction => ({ - type: 'GROUPS_SET_CONTINUATION', - payload: continuation, +export const clearGroups = (): ClearGroupsAction => ({ + type: 'GROUPS_CLEAR_GROUPS', +}) + +export const appendLogs = (logs: string[], continuation?: string): AppendLogsAction => ({ + type: 'GROUPS_APPEND_LOGS', + payload: { + items: logs, + continuation, + }, +}) + +export const clearLogs = (): ClearLogsAction => ({ + type: 'GROUPS_CLEAR_LOGS', +}) + +export const appendInvitations = (invitations: string[], continuation?: string): AppendInvitationsAction => ({ + type: 'GROUPS_APPEND_INVITATIONS', + payload: { + items: invitations, + continuation, + }, +}) + +export const clearInvitations = (): ClearInvitationsAction => ({ + type: 'GROUPS_CLEAR_INVITATIONS', }) export const fetchGroup = (id: string): AppThunkAction => { @@ -74,12 +116,7 @@ export const fetchGroups = (sort?: string, continuation?: string): AppThunkActio const groups = normalize(response.groups, EntityType.Group) dispatch(setEntities(groups.entities)) - dispatch(setGroups(groups.keys)) - - if (response.continuation) { - dispatch(setContinuation(response.continuation)) - } - + dispatch(appendGroups(groups.keys, response.continuation)) dispatch(finishRequest(RequestKey.FetchGroups, true)) } catch (err) { dispatch(finishRequest(RequestKey.FetchGroups, false)) @@ -126,6 +163,7 @@ export const fetchLogs = (id: string, continuation?: string): AppThunkAction => const users = normalize(response.logs, EntityType.Log) dispatch(setEntities(users.entities)) + dispatch(appendLogs(users.keys, response.continuation)) dispatch(finishRequest(RequestKey.FetchGroupLogs, true)) } catch (err) { dispatch(finishRequest(RequestKey.FetchGroupLogs, false)) @@ -173,6 +211,7 @@ export const fetchInvitations = (id: string): AppThunkAction => async dispatch = const invitations = normalize(response.invitations, EntityType.Invitation) dispatch(setEntities(invitations.entities)) + dispatch(appendInvitations(invitations.keys, response.continuation)) dispatch(finishRequest(RequestKey.FetchInvitations, true)) } catch (err) { dispatch(finishRequest(RequestKey.FetchInvitations, false)) diff --git a/src/components/app-list-item.tsx b/src/components/app-list-item.tsx new file mode 100644 index 0000000..7a04887 --- /dev/null +++ b/src/components/app-list-item.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react' +import { Link } from 'react-router-dom' +import { App } from 'src/types' + +interface Props { + app: App +} + +const AppListItem: FC = ({ app }) => ( +
+ {app.name} + {app.about &&

{app.about}

} +
+) + +export default AppListItem diff --git a/src/components/app.tsx b/src/components/app.tsx index 2dc1e6a..b279a2d 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -1,12 +1,14 @@ import React, { FC, useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' -import { BrowserRouter as Router, Route, Switch, Link, useHistory } from 'react-router-dom' +import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom' import { handleApiError } from 'src/api/errors' import { fetchSelf, setChecked } from 'src/actions/authentication' +import { setConfig } from 'src/actions/config' import { getFetching } from 'src/selectors' import { getCollapsed } from 'src/selectors/menu' +import getConfig from 'src/config' import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from 'src/constants' import { AppState, AppThunkDispatch } from 'src/types' @@ -17,8 +19,10 @@ import Spinner from './spinner' import UserInfo from './user-info' import About from './pages/about' +import Apps from './pages/apps' import CreateApp from './pages/create-app' import Developers from './pages/developers' +import EditApp from './pages/edit-app' import Group from './pages/group' import GroupAdmin from './pages/group-admin' import Groups from './pages/groups' @@ -52,6 +56,8 @@ const App: FC = () => { } else { dispatch(setChecked()) } + + dispatch(setConfig(await getConfig())) } useEffect(() => { @@ -85,6 +91,9 @@ const App: FC = () => { + + + @@ -100,6 +109,9 @@ const App: FC = () => { + + + diff --git a/src/components/forms/file-field.tsx b/src/components/forms/file-field.tsx new file mode 100644 index 0000000..b78c9f1 --- /dev/null +++ b/src/components/forms/file-field.tsx @@ -0,0 +1,93 @@ +import React, { FC, ChangeEvent, useState } from 'react' +import { useSelector, useDispatch } from 'react-redux' +import classNames from 'classnames' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faUpload } from '@fortawesome/free-solid-svg-icons' +import { uploadBrowserDataToBlockBlob, Aborter, BlockBlobURL, AnonymousCredential } from '@azure/storage-blob' + +import { setFieldValue } from 'src/actions/forms' +import { showNotification } from 'src/actions/notifications' +import { getConfig } from 'src/selectors' +import { getFieldValue } from 'src/selectors/forms' +import { apiFetch } from 'src/api/fetch' +import { AppState, ClassDictionary, SasResponse, NotificationType, Config } from 'src/types' + +interface Props { + name: string + label: string + help?: string +} + +const CheckboxField: FC = ({ name, label, help }) => { + const value = useSelector(state => getFieldValue(state, name, false)) + const config = useSelector(getConfig) + const [progress, setProgress] = useState(0) + const [uploading, setUploading] = useState(false) + const dispatch = useDispatch() + + const classes: ClassDictionary = { + file: true, + 'is-primary': true, + 'has-name': !!value, + } + + const handleChange = async (event: ChangeEvent) => { + if (event.target.files) { + const file = event.target.files[0] + + if (file.size > 1024 * 1024 * 5) { + dispatch(showNotification(NotificationType.Error, 'Files must be less than 5 MBs')) + return + } + + const ext = file.name.substring(file.name.lastIndexOf('.')) + const { sas, id } = await apiFetch({ path: '/api/sas' }) + const filename = `${id}${ext}` + const blobURL = new BlockBlobURL(`${config.blobUrl}${filename}?${sas}`, BlockBlobURL.newPipeline(new AnonymousCredential())) + + dispatch(setFieldValue(name, filename)) + setUploading(true) + + await uploadBrowserDataToBlockBlob(Aborter.none, file, blobURL, { + blockSize: 4 * 1024 * 1024, + progress: p => { + setProgress((p.loadedBytes / file.size) * 100) + } + }) + + setUploading(false) + } + } + + if (uploading) { + return ( +
+ + {progress}% +
+ ) + } + + return ( +
+ +
+ +
+

{help}

+
+ ) +} + +export default CheckboxField diff --git a/src/components/group-list.tsx b/src/components/group-list.tsx deleted file mode 100644 index d61aca5..0000000 --- a/src/components/group-list.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { FC } from 'react' - -import { Group } from 'src/types' - -import GroupListItem from './group-list-item' - -interface Props { - groups: Group[] -} - -const GroupList: FC = ({ groups }) => ( -
- {groups.map(group => )} -
-) - -export default GroupList diff --git a/src/components/navigation-menu.tsx b/src/components/navigation-menu.tsx index f1fdb5a..96ec0e0 100644 --- a/src/components/navigation-menu.tsx +++ b/src/components/navigation-menu.tsx @@ -18,7 +18,7 @@ const NavigationMenu: FC = () => (   - Apps + Apps ) diff --git a/src/components/pages/apps.tsx b/src/components/pages/apps.tsx new file mode 100644 index 0000000..bf916b3 --- /dev/null +++ b/src/components/pages/apps.tsx @@ -0,0 +1,32 @@ +import React, { FC, useEffect } from 'react' +import { useSelector, useDispatch } from 'react-redux' + +import { fetchApps } from 'src/actions/apps' +import { getApps } from 'src/selectors/apps' +import { setTitle } from 'src/utils' +import { AppState, AppThunkDispatch, App } from 'src/types' + +import PageHeader from 'src/components/page-header' +import AppListItem from 'src/components/app-list-item' + +const Apps: FC = () => { + const apps = useSelector(getApps) + const dispatch = useDispatch() + + useEffect(() => { + dispatch(fetchApps()) + setTitle('Apps') + }, []) + + return ( +
+ + +
+ {apps.map(app => )} +
+
+ ) +} + +export default Apps diff --git a/src/components/pages/developers.tsx b/src/components/pages/developers.tsx index 58231ca..a7a0510 100644 --- a/src/components/pages/developers.tsx +++ b/src/components/pages/developers.tsx @@ -4,20 +4,20 @@ import { Link } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faPlusCircle } from '@fortawesome/free-solid-svg-icons' -import { fetchSelfApps } from 'src/actions/apps' -import { getSelfApps } from 'src/selectors/apps' +import { fetchCreatedApps } from 'src/actions/apps' +import { getCreatedApps } from 'src/selectors/apps' import { setTitle } from 'src/utils' import { AppState, App, AppThunkDispatch } from 'src/types' import PageHeader from 'src/components/page-header' const Developers: FC = () => { - const apps = useSelector(getSelfApps) + const apps = useSelector(getCreatedApps) const dispatch = useDispatch() useEffect(() => { setTitle('Developers') - dispatch(fetchSelfApps()) + dispatch(fetchCreatedApps()) }, []) return ( diff --git a/src/components/pages/edit-app.tsx b/src/components/pages/edit-app.tsx new file mode 100644 index 0000000..b2e5cc1 --- /dev/null +++ b/src/components/pages/edit-app.tsx @@ -0,0 +1,124 @@ +import React, { FC, useEffect } from 'react' +import { useSelector, useDispatch } from 'react-redux' +import { useParams, useHistory } from 'react-router-dom' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faIdCard, faCheckCircle } from '@fortawesome/free-solid-svg-icons' + +import { handleApiError } from 'src/api/errors' +import { fetchApp } from 'src/actions/apps' +import { initForm, initField } from 'src/actions/forms' +import { getAuthenticatedUserId } from 'src/selectors/authentication' +import { getEntity } from 'src/selectors/entities' + +import { useAuthenticationCheck, useDeepCompareEffect } from 'src/hooks' +import { setTitle } from 'src/utils' +import { AppState, AppThunkDispatch, EntityType, App } from 'src/types' + +import PageHeader from 'src/components/page-header' +import Loading from 'src/components/pages/loading' + +import TextField from 'src/components/forms/text-field' +import TextareaField from 'src/components/forms/textarea-field' +import FileField from 'src/components/forms/file-field' + +interface Params { + id: string +} + +const EditApp: FC = () => { + useAuthenticationCheck() + + const { id } = useParams() + const app = useSelector(state => getEntity(state, EntityType.App, id)) + const selfId = useSelector(getAuthenticatedUserId) + const dispatch = useDispatch() + const history = useHistory() + + useEffect(() => { + try { + dispatch(fetchApp(id)) + } catch (err) { + handleApiError(err, dispatch, history) + } + }, []) + + useDeepCompareEffect(() => { + if (app) { + if (app.user.id !== selfId) { + history.push(`/a/${id}`) + } + + setTitle(app.name) + + dispatch(initForm()) + dispatch(initField('name', app.name)) + dispatch(initField('about', app.about || '')) + dispatch(initField('websiteUrl', app.websiteUrl || '')) + dispatch(initField('companyName', app.companyName || '')) + dispatch(initField('version', '')) + dispatch(initField('composerUrl', app.composerUrl || '')) + dispatch(initField('rendererUrl', app.rendererUrl || '')) + dispatch(initField('image', app.imageUrl || '')) + dispatch(initField('coverImage', app.coverImageUrl || '')) + dispatch(initField('iconImage', app.iconImageUrl || '')) + } + }, [app]) + + if (!app) return + + const handleUpdate = () => { + + } + + return ( +
+ + +
+
+
+ +
+ + + + +
+
+
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +

+ + +
+ +

+ + +
+
+
+ ) +} + +export default EditApp diff --git a/src/components/pages/groups.tsx b/src/components/pages/groups.tsx index 367d5a4..bbc1abf 100644 --- a/src/components/pages/groups.tsx +++ b/src/components/pages/groups.tsx @@ -10,7 +10,7 @@ import { setTitle } from 'src/utils' import { AppState, AppThunkDispatch, Group } from 'src/types' import PageHeader from 'src/components/page-header' -import GroupList from 'src/components/group-list' +import GroupListItem from 'src/components/group-list-item' const Groups: FC = () => { const groups = useSelector(getGroups) @@ -26,7 +26,7 @@ const Groups: FC = () => {
- + {groups.map(group => )}

diff --git a/src/components/pages/self.tsx b/src/components/pages/self.tsx index 4c38875..126ba5b 100644 --- a/src/components/pages/self.tsx +++ b/src/components/pages/self.tsx @@ -22,6 +22,7 @@ import TextField from 'src/components/forms/text-field' import TextareaField from 'src/components/forms/textarea-field' import SelectField from 'src/components/forms/select-field' import CheckboxField from 'src/components/forms/checkbox-field' +import FileField from 'src/components/forms/file-field' interface Params { tab: string @@ -54,9 +55,18 @@ const Self: FC = () => { const about = valueFromForm(form, 'about', '') const requiresApproval = valueFromForm(form, 'requiresApproval', true) const privacy = valueFromForm(form, 'privacy', 'public') + const imageUrl = valueFromForm(form, 'image', '') + const coverImageUrl = valueFromForm(form, 'coverImage', '') try { - dispatch(updateSelf(name, about, requiresApproval, privacy)) + dispatch(updateSelf({ + name, + about, + requiresApproval, + privacy, + imageUrl, + coverImageUrl, + })) } catch (err) { handleApiError(err, dispatch, history) } @@ -71,6 +81,8 @@ const Self: FC = () => { dispatch(initField('about', user.about || '')) dispatch(initField('requiresApproval', user.requiresApproval)) dispatch(initField('privacy', user.privacy)) + dispatch(initField('image', user.imageUrl || '')) + dispatch(initField('coverImage', user.coverImageUrl || '')) } }, [user]) @@ -138,6 +150,10 @@ const Self: FC = () => {

+ +
+ +
You must approve each Subscription request from other users. diff --git a/src/components/pages/view-app.tsx b/src/components/pages/view-app.tsx index 178dcc0..0765de9 100644 --- a/src/components/pages/view-app.tsx +++ b/src/components/pages/view-app.tsx @@ -1,8 +1,10 @@ import React, { FC, useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' -import { useParams, useHistory } from 'react-router-dom' +import { Link, useParams, useHistory } from 'react-router-dom' import classNames from 'classnames' import moment from 'moment' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faPlusSquare, faMinusSquare } from '@fortawesome/free-solid-svg-icons' import { handleApiError } from 'src/api/errors' import { fetchApp, installApp, uninstallApp } from 'src/actions/apps' @@ -46,32 +48,43 @@ const ViewApp: FC = () => { const isCreator = app.user.id === selfId const renderButton = () => { + const classes: ClassDictionary = { + 'button': true, + 'is-danger': app.installed, + 'is-success': !app.installed, + 'is-large': true, + 'is-outlined': true, + 'is-loading': fetching, + } + if (app.installed) { const handleClick = async () => { await dispatch(uninstallApp(id)) await dispatch(fetchApp(id)) } - const classes: ClassDictionary = { - 'button': true, - 'is-danger': true, - 'is-loading': fetching, - } - - return + return ( + + ) } else { const handleClick = async () => { await dispatch(installApp(id)) await dispatch(fetchApp(id)) } - const classes: ClassDictionary = { - 'button': true, - 'is-success': true, - 'is-loading': fetching, - } - - return + return ( + + ) } } @@ -117,6 +130,13 @@ const ViewApp: FC = () => {
{renderButton()} + + {isCreator && +

+
+ Edit App +
+ }
diff --git a/src/components/user-info.tsx b/src/components/user-info.tsx index b337dc7..b94c669 100644 --- a/src/components/user-info.tsx +++ b/src/components/user-info.tsx @@ -2,13 +2,15 @@ import React, { FC } from 'react' import { useSelector } from 'react-redux' import { Link } from 'react-router-dom' +import { getConfig } from 'src/selectors' import { getAuthenticatedUser } from 'src/selectors/authentication' -import { AppState, User } from 'src/types' +import { AppState, User, Config } from 'src/types' const UserInfo: FC = () => { const user = useSelector(getAuthenticatedUser) + const config = useSelector(getConfig) const hasAvatar = user && user.imageUrl - const imageUrl = hasAvatar ? user!.imageUrl : undefined + const imageUrl = hasAvatar ? `${config.blobUrl}${user!.imageUrl}` : undefined const name = () => { if (!user) return @@ -60,10 +62,10 @@ const UserInfo: FC = () => { return (
- {hasAvatar && + {imageUrl &&

- +

} diff --git a/src/config.ts b/src/config.ts index 7ab611c..f9e05e5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,4 @@ -interface Config { - apiUrl: string -} +import { Config } from './types' declare global { interface Window { diff --git a/src/reducers/apps.ts b/src/reducers/apps.ts new file mode 100644 index 0000000..1a3e3e6 --- /dev/null +++ b/src/reducers/apps.ts @@ -0,0 +1,50 @@ +import { Reducer } from 'redux' + +import { AppsActions } from '../actions/apps' +import { AppsState } from '../types' + +const initialState: AppsState = { + items: [], + continuation: undefined, + created: { + items: [], + continuation: undefined + } +} + +const reducer: Reducer = (state = initialState, action) => { + switch (action.type) { + case 'APPS_APPEND_APPS': + return { + ...state, + items: action.payload.items, + continuation: action.payload.continuation, + } + case 'APPS_CLEAR_APPS': + return { + ...state, + items: [], + continuation: undefined, + } + case 'APPS_APPEND_CREATED_APPS': + return { + ...state, + created: { + items: action.payload.items, + continuation: action.payload.continuation, + }, + } + case 'APPS_CLEAR_CREATED_APPS': + return { + ...state, + created: { + items: [], + continuation: undefined, + }, + } + default: + return state + } +} + +export default reducer diff --git a/src/reducers/config.ts b/src/reducers/config.ts new file mode 100644 index 0000000..e2136da --- /dev/null +++ b/src/reducers/config.ts @@ -0,0 +1,20 @@ +import { Reducer } from 'redux' + +import { ConfigActions } from '../actions/config' +import { ConfigState } from '../types' + +const initialState: ConfigState = { + apiUrl: '', + blobUrl: '', +} + +const reducer: Reducer = (state = initialState, action) => { + switch (action.type) { + case 'CONFIG_SET': + return action.payload + default: + return state + } +} + +export default reducer diff --git a/src/reducers/groups.ts b/src/reducers/groups.ts index 7a44ef7..a934b81 100644 --- a/src/reducers/groups.ts +++ b/src/reducers/groups.ts @@ -4,29 +4,86 @@ import { GroupsActions } from '../actions/groups' import { GroupsState } from '../types' const initialState: GroupsState = { - groups: [], + items: [], continuation: undefined, + admin: { + invitations: { + items: [], + continuation: undefined, + }, + logs: { + items: [], + continuation: undefined, + } + } } const reducer: Reducer = (state = initialState, action) => { switch (action.type) { - case 'GROUPS_SET_GROUPS': + case 'GROUPS_APPEND_GROUPS': return { ...state, - groups: action.payload, + items: [ + ...state.items, + ...action.payload.items, + ], + continuation: action.payload.continuation, } - case 'GROUPS_APPEND_GROUPS': + case 'GROUPS_CLEAR_GROUPS': return { ...state, - groups: [ - ...state.groups, - ...action.payload, - ], + items: [], + continuation: undefined, + } + case 'GROUPS_APPEND_LOGS': + return { + ...state, + admin: { + ...state.admin, + logs: { + items: [ + ...state.admin.logs.items, + ...action.payload.items, + ], + continuation: action.payload.continuation, + }, + }, + } + case 'GROUPS_CLEAR_LOGS': + return { + ...state, + admin: { + ...state.admin, + logs: { + items: [], + continuation: undefined, + }, + }, } - case 'GROUPS_SET_CONTINUATION': + case 'GROUPS_APPEND_INVITATIONS': + return { + ...state, + admin: { + ...state.admin, + invitations: { + items: [ + ...state.admin.logs.items, + ...action.payload.items, + ], + continuation: action.payload.continuation, + }, + }, + } + case 'GROUPS_CLEAR_INVITATIONS': return { ...state, - continuation: action.payload, + admin: { + ...state.admin, + invitations: { + items: [], + continuation: undefined, + }, + }, } default: return state diff --git a/src/selectors/apps.ts b/src/selectors/apps.ts index b5fa760..758b6ad 100644 --- a/src/selectors/apps.ts +++ b/src/selectors/apps.ts @@ -1,18 +1,5 @@ - -import { createSelector } from 'reselect' - import { denormalize } from 'src/utils/normalization' -import { getEntityStore } from './entities' -import { getAuthenticatedUserId } from './authentication' - -import { EntityType, App } from 'src/types' - -export const getSelfApps = createSelector( - [getEntityStore, getAuthenticatedUserId], - (entities, userId) => { - const apps = entities[EntityType.App] - if (!apps) return [] +import { AppState, EntityType, App } from 'src/types' - return denormalize(Object.values(apps).filter(app => app.userId === userId).map(user => user.id), EntityType.User, entities) as App[] - } -) +export const getApps = (state: AppState) => denormalize(state.apps.items, EntityType.App, state.entities) as App[] +export const getCreatedApps = (state: AppState) => denormalize(state.apps.created.items, EntityType.App, state.entities) as App[] diff --git a/src/selectors/groups.ts b/src/selectors/groups.ts index 68e0ed2..ba6d917 100644 --- a/src/selectors/groups.ts +++ b/src/selectors/groups.ts @@ -1,15 +1,9 @@ -import { createSelector } from 'reselect' - -import { getEntityStore } from './entities' import { denormalize } from 'src/utils/normalization' import { AppState, Group, User, EntityType, GroupLog, Invitation } from 'src/types' -export const getGroupIds = (state: AppState) => state.groups.groups - -export const getGroups = createSelector( - [getEntityStore, getGroupIds], - (entities, groups) => denormalize(groups, EntityType.Group, entities) as Group[] -) +export const getGroups = (state: AppState) => denormalize(state.groups.items, EntityType.Group, state.entities) as Group[] +export const getLogs = (state: AppState) => denormalize(state.groups.admin.logs.items, EntityType.Log, state.entities) as GroupLog[] +export const getInvitations = (state: AppState) => denormalize(state.groups.admin.invitations.items, EntityType.Invitation, state.entities) as Invitation[] export const getGroupMembers = (state: AppState, group: string) => { const users = state.entities[EntityType.User] @@ -17,17 +11,3 @@ export const getGroupMembers = (state: AppState, group: string) => { return denormalize(Object.values(users).filter(user => user.group === group).map(user => user.id), EntityType.User, state.entities) as User[] } - -export const getLogs = (state: AppState) => { - const logs = state.entities[EntityType.Log] - if (!logs) return [] - - return denormalize(Object.keys(logs), EntityType.Log, state.entities) as GroupLog[] -} - -export const getInvitations = (state: AppState) => { - const invitations = state.entities[EntityType.Invitation] - if (!invitations) return [] - - return denormalize(Object.keys(invitations), EntityType.Invitation, state.entities) as Invitation[] -} diff --git a/src/selectors/index.ts b/src/selectors/index.ts index d20ddf3..c8d7b18 100644 --- a/src/selectors/index.ts +++ b/src/selectors/index.ts @@ -9,3 +9,5 @@ export const getRequests = (state: AppState) => state.requests export const getFetching = createSelector(getRequests, requests => { return values(requests).reduce((fetching, request) => fetching || request.fetching, false) }) + +export const getConfig = (state: AppState) => state.config diff --git a/src/store/index.ts b/src/store/index.ts index f865fda..9f8c207 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,7 +1,9 @@ import { createStore, combineReducers, applyMiddleware } from 'redux' import { AppState } from '../types' +import apps from '../reducers/apps' import authentication from '../reducers/authentication' +import config from '../reducers/config' import entities from '../reducers/entities' import forms from '../reducers/forms' import groups from '../reducers/groups' @@ -15,7 +17,9 @@ import thunk from 'redux-thunk' const store = createStore( combineReducers({ + apps, authentication, + config, entities, forms, groups, diff --git a/src/styles/app.scss b/src/styles/app.scss index d630cf9..06833d9 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -30,6 +30,7 @@ $body-size: 14px; @import "../../node_modules/bulma/sass/elements/table.sass"; @import "../../node_modules/bulma/sass/elements/tag.sass"; @import "../../node_modules/bulma/sass/elements/title.sass"; +@import "../../node_modules/bulma/sass/elements/progress.sass"; @import "../../node_modules/bulma/sass/layout/hero.sass"; @import "../../node_modules/bulma/sass/components/level.sass"; @import "../../node_modules/bulma/sass/components/media.sass"; @@ -93,7 +94,7 @@ div#notification-container { width: 40%; } -div.group-list-item { +div.group-list-item, div.app-list-item { background-color: $white; border-radius: 15px; margin: 10px 0px; diff --git a/src/types/index.ts b/src/types/index.ts index be2af60..73fea0f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -25,6 +25,11 @@ export interface AvailabilityResponse { available: boolean } +export interface SasResponse { + sas: string + id: string +} + export * from './entities' export * from './store' diff --git a/src/types/store.ts b/src/types/store.ts index 6eeef36..398dcc0 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -1,6 +1,11 @@ import { EntityStore } from './entities' +export interface Config { + apiUrl: string + blobUrl: string +} + export enum NotificationType { Info = 'info', Success = 'success', @@ -78,26 +83,42 @@ export interface Form { [name: string]: FormField } +export interface EntityList { + items: string[] + continuation?: string +} + export interface FormsState { form: Form notification?: FormNotification } -export interface GroupsState { - groups: string[] - continuation?: string +export interface GroupsAdminState { + invitations: EntityList + logs: EntityList +} + +export type GroupsState = EntityList & { + admin: GroupsAdminState } export interface RegistrationState { step: number } +export type AppsState = EntityList & { + created: EntityList +} + +export type ConfigState = Config export type RequestsState = APIRequestCollection export type NotificationsState = Notification[] export type EntitiesState = EntityStore export interface AppState { authentication: AuthenticationState + apps: AppsState + config: ConfigState entities: EntitiesState forms: FormsState groups: GroupsState