From 511ac51c2bf9a3f4a582673d1b7e92ef83e6c299 Mon Sep 17 00:00:00 2001 From: Stian Aune Date: Wed, 10 Apr 2019 22:16:51 +0200 Subject: [PATCH] Squashed commit of the following: commit 042130f11c6f807d128e2ffbdb76f4be2170a43f Author: Stian Aune Date: Wed Apr 10 22:16:14 2019 +0200 Done? commit 3089c6f8be3135ab1aa2b95b9314a753596714a7 Author: Stian Aune Date: Wed Mar 27 22:20:07 2019 +0100 Bugfix + some unit tests bc why not? commit 38f874b5538a7d9563b44d60b2fa881d941defc8 Author: Stian Aune Date: Tue Mar 26 22:14:22 2019 +0100 Front page. commit 5bdf2a0c48e0e388bffb53714b0410ebd3f8b8e7 Author: Stian Aune Date: Thu Mar 7 21:10:57 2019 +0100 Rename groups and stuff. commit 3130710b198c434fe312ea3123cbe5ba3fb526b7 Merge: 118a21a fa98a0b Author: Stian Aune Date: Thu Mar 7 21:06:16 2019 +0100 Merge remote-tracking branch 'origin/master' into webui commit 118a21ad42adaa4968318b1e798a58f0565db849 Author: Stian Aune Date: Thu Mar 7 21:06:02 2019 +0100 I can a lot of things. commit 089ee6816986e06e77eff404eaa9f04cda0e4275 Merge: 0a6cc16 f27c0a3 Author: Stian Aune Date: Tue Mar 5 22:13:41 2019 +0100 Merge remote-tracking branch 'origin/master' into webui commit 0a6cc16e0fda482c04dd88c658273c2774b50d7a Author: Stian Aune Date: Tue Mar 5 22:13:31 2019 +0100 More stuff. commit 1ef2623637f2f7c852edffa7914c2095a990539b Author: Stian Aune Date: Sun Mar 3 21:59:23 2019 +0100 Add/remove group. commit 59240ce22d305b2b335dd46bb5f528f720b5eedc Merge: 537e86c e11ba33 Author: Stian Aune Date: Sun Mar 3 21:15:06 2019 +0100 Merge remote-tracking branch 'origin/master' into webui commit 537e86cced5ab9b8919aaa0f89b277ed9ecdde27 Author: Stian Aune Date: Sun Mar 3 21:14:55 2019 +0100 For reals. commit de5fb0d16ddde159a54ce7bb2af09eb4a87960c2 Author: Stian Aune Date: Sun Mar 3 21:14:30 2019 +0100 Modal cleanup + light page. commit 01daa2800085df9799de4890a3574ef405b1ec89 Author: Stian Aune Date: Sun Feb 24 00:27:36 2019 +0100 Hmm... commit be2fe56e62a212e2691288f5db0a2d826719689d Merge: 99867a5 22dddc7 Author: Stian Aune Date: Sat Feb 23 15:16:07 2019 +0100 Merge remote-tracking branch 'origin/master' into webui commit 99867a5fd680e7bd2489df4a01a5609d1e7867d5 Author: Stian Aune Date: Sat Feb 23 15:16:02 2019 +0100 WIP. commit d82bbf53f84ab3c47814e5d586400c34787be4d6 Author: Stian Aune Date: Tue Feb 19 21:37:59 2019 +0100 WIP. commit 7ed6d2148dcd991050bbc2ecfaef6fc42a0dc4ce Author: Stian Aune Date: Sun Feb 10 13:30:53 2019 +0100 Stuffs. commit ee9a3d485efcf3cf7308a30e4bea27b97002324e Author: Stian Aune Date: Sun Feb 10 12:29:47 2019 +0100 Bleurgh. commit 59ef68ad8f8ce1e05e81c4322bad52cb319cd716 Author: Stian Aune Date: Thu Feb 7 21:15:37 2019 +0100 Thursday night commit. commit ee3ff503427fd86306e88bdd4b857690c38f07f4 Merge: daf071a e644f48 Author: Stian Aune Date: Wed Feb 6 22:02:37 2019 +0100 Merge remote-tracking branch 'origin/master' into webui commit daf071a4429f5e0158c88e6b142659b6ad6c851a Author: Stian Aune Date: Mon Feb 4 20:56:45 2019 +0100 Rewrote light tests. commit edffa49d18ad07b652e5f9d72bd350bd499144a9 Merge: 83052a2 8c302ec Author: Stian Aune Date: Mon Feb 4 20:31:26 2019 +0100 Merge remote-tracking branch 'origin/master' into webui commit 83052a2ab8d2b51c3003417142ba15ce1ed06921 Author: Stian Aune Date: Mon Feb 4 20:31:22 2019 +0100 Blah. commit aec6cd0b7d5aa812b907b16e1c65687e94e16866 Merge: e6fefb5 0503f79 Author: Stian Aune Date: Tue Jan 29 21:06:24 2019 +0100 Merge remote-tracking branch 'origin/master' into webui commit e6fefb5bd1fc487b32515ee5041cd42b37058d52 Merge: 97f7a78 eca8a30 Author: Stian Aune Date: Mon Jan 28 22:44:19 2019 +0100 Merge remote-tracking branch 'origin/master' into webui commit 97f7a78f3f72331996eee63c7623d04272bc1d71 Merge: 195738d 174b818 Author: Stian Aune Date: Sun Jan 13 13:20:10 2019 +0100 Merge remote-tracking branch 'origin/master' into webui commit 195738d6eddde94f7ca60a2d9b5ea086a204aa7f Merge: a2f2c2b 2e3e120 Author: Stian Aune Date: Sat Jan 12 17:24:00 2019 +0100 Merge remote-tracking branch 'origin/master' into webui commit a2f2c2b0bce7d8f54f851dd14d527763390b0123 Merge: edd629e 9004f2f Author: Stian Aune Date: Mon Jan 7 20:58:37 2019 +0100 Merge remote-tracking branch 'origin/master' into webui commit edd629eb395af92b250cef9bb452952eded115aa Author: Stian Aune Date: Mon Jan 7 20:58:32 2019 +0100 More stuff. commit 206d5005db09f3a149b9dccf9fff886b44252394 Author: Stian Aune Date: Mon Jan 7 19:11:03 2019 +0100 Cleanup. --- go.mod | 1 + go.sum | 7 + models/group_test.go | 53 ++ models/light_test.go | 127 +++ models/session_test.go | 73 ++ models/user_test.go | 55 ++ readme.md | 2 +- webui/README.md | 68 -- webui/package-lock.json | 836 ++++++++++++++++-- webui/package.json | 18 +- webui/proxy.json | 3 + webui/src/App.css | 39 +- webui/src/App.js | 53 +- webui/src/Components/Bridge.jsx | 53 ++ webui/src/Components/BridgeLight.jsx | 23 + webui/src/Components/Bridges.jsx | 25 + webui/src/Components/Forms/LoginForm.jsx | 141 +++ webui/src/Components/Group.jsx | 101 +++ webui/src/Components/Groups.jsx | 27 + webui/src/Components/Header.jsx | 21 - webui/src/Components/LGroup.jsx | 70 ++ webui/src/Components/LGroups.jsx | 21 + webui/src/Components/Light.jsx | 60 ++ webui/src/Components/Loading.jsx | 12 + .../src/Components/Misc/BrightnessSlider.jsx | 42 + webui/src/Components/Misc/ColorPicker.jsx | 40 + webui/src/Components/Modals/BridgeModal.jsx | 94 ++ webui/src/Components/Modals/ColorModal.jsx | 39 + webui/src/Components/Modals/GroupAddModal.jsx | 43 + .../Modals/GroupPropertiesModal.jsx | 103 +++ .../Modals/LightPropertiesModal.jsx | 64 ++ .../Components/Modals/PermissionsModal.jsx | 84 ++ webui/src/Components/Pages/AdminPage.jsx | 18 + webui/src/Components/Pages/GroupPage.jsx | 10 + webui/src/Components/Pages/IndexPage.jsx | 10 + webui/src/Components/Pages/LightPage.jsx | 25 + webui/src/Components/Structure/Header.jsx | 37 + webui/src/Helpers/fetcher.js | 92 ++ webui/src/Helpers/groups.js | 107 +++ webui/src/Helpers/keys.js | 7 + webui/src/Helpers/lights.js | 141 +++ webui/src/Helpers/null.js | 7 + webui/src/Helpers/percentage.js | 3 + webui/src/Helpers/random.js | 12 + webui/src/Hooks/auth.js | 91 ++ webui/src/Hooks/bridge.js | 99 +++ webui/src/Hooks/group.js | 20 + webui/src/Hooks/light.js | 24 + webui/src/Hooks/lights.js | 42 + webui/src/Reducers/authReducer.js | 33 - webui/src/Reducers/index.js | 10 - webui/src/index.js | 10 +- webui/src/setupProxy.js | 15 + 53 files changed, 2943 insertions(+), 268 deletions(-) create mode 100644 models/group_test.go create mode 100644 models/light_test.go create mode 100644 models/session_test.go create mode 100644 models/user_test.go delete mode 100755 webui/README.md create mode 100644 webui/proxy.json create mode 100644 webui/src/Components/Bridge.jsx create mode 100644 webui/src/Components/BridgeLight.jsx create mode 100644 webui/src/Components/Bridges.jsx create mode 100644 webui/src/Components/Forms/LoginForm.jsx create mode 100644 webui/src/Components/Group.jsx create mode 100644 webui/src/Components/Groups.jsx delete mode 100644 webui/src/Components/Header.jsx create mode 100644 webui/src/Components/LGroup.jsx create mode 100644 webui/src/Components/LGroups.jsx create mode 100644 webui/src/Components/Light.jsx create mode 100644 webui/src/Components/Loading.jsx create mode 100644 webui/src/Components/Misc/BrightnessSlider.jsx create mode 100644 webui/src/Components/Misc/ColorPicker.jsx create mode 100644 webui/src/Components/Modals/BridgeModal.jsx create mode 100644 webui/src/Components/Modals/ColorModal.jsx create mode 100644 webui/src/Components/Modals/GroupAddModal.jsx create mode 100644 webui/src/Components/Modals/GroupPropertiesModal.jsx create mode 100644 webui/src/Components/Modals/LightPropertiesModal.jsx create mode 100644 webui/src/Components/Modals/PermissionsModal.jsx create mode 100644 webui/src/Components/Pages/AdminPage.jsx create mode 100644 webui/src/Components/Pages/GroupPage.jsx create mode 100644 webui/src/Components/Pages/IndexPage.jsx create mode 100644 webui/src/Components/Pages/LightPage.jsx create mode 100644 webui/src/Components/Structure/Header.jsx create mode 100644 webui/src/Helpers/fetcher.js create mode 100644 webui/src/Helpers/groups.js create mode 100644 webui/src/Helpers/keys.js create mode 100644 webui/src/Helpers/lights.js create mode 100644 webui/src/Helpers/null.js create mode 100644 webui/src/Helpers/percentage.js create mode 100644 webui/src/Helpers/random.js create mode 100644 webui/src/Hooks/auth.js create mode 100644 webui/src/Hooks/bridge.js create mode 100644 webui/src/Hooks/group.js create mode 100644 webui/src/Hooks/light.js create mode 100644 webui/src/Hooks/lights.js delete mode 100644 webui/src/Reducers/authReducer.js delete mode 100644 webui/src/Reducers/index.js create mode 100644 webui/src/setupProxy.js diff --git a/go.mod b/go.mod index 6502e0a..e5cd925 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gorilla/mux v1.6.2 github.com/jmoiron/sqlx v1.2.0 github.com/mattn/go-sqlite3 v1.10.0 + github.com/stretchr/testify v1.3.0 golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 google.golang.org/appengine v1.4.0 // indirect diff --git a/go.sum b/go.sum index ef90c0d..2ea5dad 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/collinux/GoHue v0.0.0-20181229002551-d259041d5eb8 h1:NOPuu1sMqBVC3iyl github.com/collinux/GoHue v0.0.0-20181229002551-d259041d5eb8/go.mod h1:ex2AEcUvgoeoE/uknn0ZUExwGhBcH9jwA5heqv2xq6w= github.com/collinux/gohue v0.0.0-20181229002551-d259041d5eb8 h1:Qhd31xZ6GUL0nEaXYP4nXOn8J6l9jqa6xEyp70qfjZE= github.com/collinux/gohue v0.0.0-20181229002551-d259041d5eb8/go.mod h1:HFm7vkh/1EJQ9ymYsKUQtK7JlG3om1r61wMAHtl+bxw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -18,6 +20,11 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b h1:Elez2XeF2p9uyVj0yEUDqQ56NFcDtcBNkYP7yv8YbUE= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/models/group_test.go b/models/group_test.go new file mode 100644 index 0000000..3275c61 --- /dev/null +++ b/models/group_test.go @@ -0,0 +1,53 @@ +package models_test + +import ( + "git.aiterp.net/lucifer/lucifer/models" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGroup_Permission_NotExisting(t *testing.T) { + group := models.Group{ + ID: 1001, + Name: "Illuminati", + Permissions: []models.GroupPermission{}, + } + + perm := group.Permission(2001) + + assert.Equal(t, 1001, perm.GroupID) + assert.Equal(t, 2001, perm.UserID) + assert.False(t, perm.Read) + assert.False(t, perm.Write) + assert.False(t, perm.Create) + assert.False(t, perm.Delete) + assert.False(t, perm.Manage) +} + +func TestGroup_Permission_Existing(t *testing.T) { + group := models.Group{ + ID: 1002, + Name: "The Not-So-Dark Side", + Permissions: []models.GroupPermission{ + { + GroupID: 1002, + UserID: 2002, + Read: true, + Write: true, + Create: false, + Delete: false, + Manage: false, + }, + }, + } + + perm := group.Permission(2002) + + assert.Equal(t, 1002, perm.GroupID) + assert.Equal(t, 2002, perm.UserID) + assert.True(t, perm.Read) + assert.True(t, perm.Write) + assert.False(t, perm.Create) + assert.False(t, perm.Delete) + assert.False(t, perm.Manage) +} \ No newline at end of file diff --git a/models/light_test.go b/models/light_test.go new file mode 100644 index 0000000..a901678 --- /dev/null +++ b/models/light_test.go @@ -0,0 +1,127 @@ +package models_test + +import ( + "git.aiterp.net/lucifer/lucifer/models" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestLight_SetColor_LoppingOffHash(t *testing.T) { + light := models.Light{} + + err := light.SetColor("#FFB34F") + + assert.Nil(t, err) + assert.Equal(t, "FFB34F", light.Color) +} + +func TestLight_SetColor_InvalidLength(t *testing.T) { + light := models.Light{} + + err := light.SetColor("FFB34F32") + + assert.NotNil(t, err) +} + +func TestLight_SetColor_InvalidHex(t *testing.T) { + light := models.Light{} + + err := light.SetColor("FFB34G") + + assert.NotNil(t, err) +} + +func TestLight_SetColorRGB(t *testing.T) { + light := models.Light{} + + light.SetColorRGB(32, 55, 100) + + assert.Equal(t, "203764", light.Color) +} + +func TestLight_SetColorRGBf(t *testing.T) { + light := models.Light{} + + light.SetColorRGBf(0.1, 0.2, 0.3) + + assert.Equal(t, "19334c", light.Color) +} + +func TestLight_ColorRGB_Empty(t *testing.T) { + light := models.Light{} + light.Color = "" + + r, g, b, err := light.ColorRGB() + + assert.Equal(t, uint8(0), r) + assert.Equal(t, uint8(0), g) + assert.Equal(t, uint8(0), b) + assert.Nil(t, err) +} + +func TestLight_ColorRGB_Invalid(t *testing.T) { + light := models.Light{} + light.Color = "Utterly bonkers" + + _, _, _, err := light.ColorRGB() + + assert.NotNil(t, err) + assert.Equal(t, models.ErrMalformedColor, err) +} + +func TestLight_ColorRGB_TooShort(t *testing.T) { + light := models.Light{} + light.Color = "3002" + + _, _, _, err := light.ColorRGB() + + assert.NotNil(t, err) + assert.Equal(t, models.ErrMalformedColor, err) +} + +func TestLight_ColorRGB_TooLong(t *testing.T) { + light := models.Light{} + light.Color = "3002DB05" + + _, _, _, err := light.ColorRGB() + + assert.NotNil(t, err) + assert.Equal(t, models.ErrMalformedColor, err) +} + +func TestLight_ColorRGB_Valid(t *testing.T) { + light := models.Light{} + light.Color = "203764" + + r, g, b, err := light.ColorRGB() + + assert.Equal(t, uint8(32), r) + assert.Equal(t, uint8(55), g) + assert.Equal(t, uint8(100), b) + assert.Nil(t, err) +} + +func TestLight_ColorRGBf_Invalid(t *testing.T) { + light := models.Light{} + light.Color = "G0I1J2" + + r, g, b, err := light.ColorRGBf() + + assert.Equal(t, float64(0), r) + assert.Equal(t, float64(0), g) + assert.Equal(t, float64(0), b) + assert.NotNil(t, err) +} + +func TestLight_ColorRGBf_Valid(t *testing.T) { + light := models.Light{} + light.Color = "19334C" + + r, g, b, err := light.ColorRGBf() + + // Within half a percent + assert.InDelta(t, float64(0.1), r, 0.005) + assert.InDelta(t, float64(0.2), g, 0.005) + assert.InDelta(t, float64(0.3), b, 0.005) + assert.Nil(t, err) +} diff --git a/models/session_test.go b/models/session_test.go new file mode 100644 index 0000000..b312eca --- /dev/null +++ b/models/session_test.go @@ -0,0 +1,73 @@ +package models_test + +import ( + "context" + "git.aiterp.net/lucifer/lucifer/models" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestSession_GenerateID_Works(t *testing.T) { + session := &models.Session{} + + err := session.GenerateID() + + assert.Nil(t, err) +} + +func TestSession_Cookie_IsCorrect(t *testing.T) { + sessID := "testId" + expiry := time.Now().AddDate(0, 0, 2) + + session := &models.Session{ + ID: sessID, + UserID: 0, + Expires: expiry, + } + + cookie := session.Cookie() + + assert.Equal(t, sessID, cookie.Value) + assert.Equal(t, expiry, cookie.Expires) +} + +func TestSession_Expired_BeforeExpiry(t *testing.T) { + session := &models.Session{ + ID: "12345", + UserID: 0, + Expires: time.Now().AddDate(0, 0, 1), + } + + assert.False(t, session.Expired()) +} + +func TestSession_Expired_AfterExpiry(t *testing.T) { + session := &models.Session{ + ID: "12345", + UserID: 0, + Expires: time.Now().AddDate(0, 0, -1), + } + + assert.True(t, session.Expired()) +} + +func TestSessionFromContext_InContext(t *testing.T) { + session := &models.Session{ + ID: "12345", + UserID: 0, + Expires: time.Now().AddDate(0, 0, 1), + } + + ctx := session.InContext(context.Background()) + + session2 := models.SessionFromContext(ctx) + + assert.Equal(t, session2, session) +} + +func TestSessionFromContext_Empty(t *testing.T) { + session := models.SessionFromContext(context.Background()) + + assert.Nil(t, session) +} \ No newline at end of file diff --git a/models/user_test.go b/models/user_test.go new file mode 100644 index 0000000..5d44942 --- /dev/null +++ b/models/user_test.go @@ -0,0 +1,55 @@ +package models_test + +import ( + "context" + "git.aiterp.net/lucifer/lucifer/models" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestUser_SetPassword_TooShort(t *testing.T) { + user := &models.User{} + + err := user.SetPassword("short") + + assert.NotNil(t, err) +} + +func TestUser_SetPassword_Valid(t *testing.T) { + user := &models.User{} + + err := user.SetPassword("longer") + + assert.Nil(t, err) + assert.NotEqual(t, "", user.PassHash) +} + +func TestUser_CheckPassword_Wrong(t *testing.T) { + user := &models.User{} + _ = user.SetPassword("longer") + + err := user.CheckPassword("wrong attempt") + + assert.NotNil(t, err) +} + +func TestUser_CheckPassword_Correct(t *testing.T) { + user := &models.User{} + _ = user.SetPassword("correct") + + err := user.CheckPassword("correct") + + assert.Nil(t, err) +} + +func TestUserFromContext(t *testing.T) { + nilUser := models.UserFromContext(context.Background()) + + assert.Nil(t, nilUser) + + user := &models.User{ID: 1001, Name: "Freddie"} + + ctx := user.InContext(context.Background()) + + assert.Equal(t, user, models.UserFromContext(ctx)) +} diff --git a/readme.md b/readme.md index b9263b1..6ec6d71 100644 --- a/readme.md +++ b/readme.md @@ -6,4 +6,4 @@ Lucifer is a web interface for controlling Phillips Hue bulbs and providing acce * `cmd/`: The CLI entry points. * `models`: The models and their repository interfaces. * `database/`: Repository implementations for the various models. -* `react/`: React client. \ No newline at end of file +* `webui/`: React client. diff --git a/webui/README.md b/webui/README.md deleted file mode 100755 index 9d9614c..0000000 --- a/webui/README.md +++ /dev/null @@ -1,68 +0,0 @@ -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.
-You will also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.
-It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting - -### Analyzing the Bundle Size - -This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size - -### Making a Progressive Web App - -This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app - -### Advanced Configuration - -This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration - -### Deployment - -This section has moved here: https://facebook.github.io/create-react-app/docs/deployment - -### `npm run build` fails to minify - -This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify diff --git a/webui/package-lock.json b/webui/package-lock.json index dfd30a3..ed19833 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -844,6 +844,16 @@ "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" }, + "@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==" + }, + "@jaames/iro": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@jaames/iro/-/iro-4.0.1.tgz", + "integrity": "sha512-Q4DcB7poRpH3Gok74isb5PD/FTBlxPbZ5RMovM7bUIJ7q8kDyJow1qUJ4qIywmWnUNZyXCa6dpvNXnQ24WP5TQ==" + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -885,11 +895,43 @@ "loader-utils": "^1.1.0" } }, + "@types/node": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.9.0.tgz", + "integrity": "sha512-ry4DOrC+xenhQbzk1iIPzCZGhhPGEFv7ia7Iu6XXSLVluiJIe9FfG7Iu3mObH9mpxEXCWLCMU4JWbCCR9Oy1Zg==", + "dev": true + }, + "@types/prop-types": { + "version": "15.5.8", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.8.tgz", + "integrity": "sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw==", + "dev": true + }, "@types/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.1.tgz", "integrity": "sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA==" }, + "@types/react": { + "version": "16.8.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.2.tgz", + "integrity": "sha512-6mcKsqlqkN9xADrwiUz2gm9Wg4iGnlVGciwBRYFQSMWG6MQjhOZ/AVnxn+6v8nslFgfYTV8fNdE6XwKu6va5PA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "@types/reactstrap": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/reactstrap/-/reactstrap-7.1.3.tgz", + "integrity": "sha512-WUYfvKLIOfJG/bZI5WmWLbrVk5Q1usloszE/+tmW/B0SQD8ZaiUkT9X1HIY54ysGvt6CTRoqxj5GMlFi5pPWYA==", + "dev": true, + "requires": { + "@types/react": "*", + "popper.js": "^1.14.1" + } + }, "@types/tapable": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.2.tgz", @@ -1117,6 +1159,14 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==" }, + "add-dom-event-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz", + "integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==", + "requires": { + "object-assign": "4.x" + } + }, "address": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/address/-/address-1.0.3.tgz", @@ -2949,11 +2999,24 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, + "component-classes": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/component-classes/-/component-classes-1.2.6.tgz", + "integrity": "sha1-xkI5TDYYpNiwuJGe/Mu9kw5c1pE=", + "requires": { + "component-indexof": "0.0.3" + } + }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" }, + "component-indexof": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-indexof/-/component-indexof-0.0.3.tgz", + "integrity": "sha1-EdCRMSI5648yyPJa6csAL/6NPCQ=" + }, "compressible": { "version": "2.0.15", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.15.tgz", @@ -3136,6 +3199,16 @@ "sha.js": "^2.4.8" } }, + "create-react-class": { + "version": "15.6.3", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", + "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==", + "requires": { + "fbjs": "^0.8.9", + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -3166,6 +3239,15 @@ "randomfill": "^1.0.3" } }, + "css-animation": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/css-animation/-/css-animation-1.5.0.tgz", + "integrity": "sha512-hWYoWiOZ7Vr20etzLh3kpWgtC454tW5vn4I6rLANDgpzNSkO7UfOqyCEeaoBSG9CYWQpRkFWTWbWW8o3uZrNLw==", + "requires": { + "babel-runtime": "6.x", + "component-classes": "^1.2.5" + } + }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -3460,6 +3542,12 @@ "cssom": "0.3.x" } }, + "csstype": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.2.tgz", + "integrity": "sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow==", + "dev": true + }, "cyclist": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", @@ -3778,6 +3866,11 @@ "esutils": "^2.0.2" } }, + "dom-align": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.8.0.tgz", + "integrity": "sha512-B85D4ef2Gj5lw0rK0KM2+D5/pH7yqNxg2mB+E8uzFaolpm7RQmsxEfjyEuNiF8UBBkffumYDeKRzTzc3LePP+w==" + }, "dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -3927,6 +4020,14 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -4932,6 +5033,35 @@ "bser": "^2.0.0" } }, + "fbjs": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", + "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "requires": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + } + } + }, "figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", @@ -5470,7 +5600,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -5488,11 +5619,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5505,15 +5638,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -5616,7 +5752,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -5626,6 +5763,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5638,17 +5776,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -5665,6 +5806,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5737,7 +5879,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5747,6 +5890,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5822,7 +5966,8 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5852,6 +5997,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5869,6 +6015,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5907,11 +6054,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.2", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -6258,6 +6407,28 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "history": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", + "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", + "requires": { + "invariant": "^2.2.1", + "loose-envify": "^1.2.0", + "resolve-pathname": "^2.2.0", + "value-equal": "^0.4.0", + "warning": "^3.0.0" + }, + "dependencies": { + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -6274,12 +6445,9 @@ "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" }, "hoist-non-react-statics": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz", - "integrity": "sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==", - "requires": { - "react-is": "^16.3.2" - } + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" }, "home-or-tmp": { "version": "2.0.0", @@ -6456,30 +6624,34 @@ } }, "http-proxy-middleware": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", - "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, "requires": { - "http-proxy": "^1.16.2", + "http-proxy": "^1.17.0", "is-glob": "^4.0.0", - "lodash": "^4.17.5", - "micromatch": "^3.1.9" + "lodash": "^4.17.11", + "micromatch": "^3.1.10" }, "dependencies": { "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true }, "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -6497,6 +6669,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -6507,6 +6680,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -6515,6 +6689,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, "requires": { "debug": "^2.3.3", "define-property": "^0.2.5", @@ -6529,6 +6704,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -6537,6 +6713,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -6545,6 +6722,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -6553,6 +6731,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -6563,6 +6742,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -6571,6 +6751,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -6581,6 +6762,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, "requires": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -6590,7 +6772,8 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true } } }, @@ -6598,6 +6781,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, "requires": { "array-unique": "^0.3.2", "define-property": "^1.0.0", @@ -6613,6 +6797,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -6621,6 +6806,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -6631,6 +6817,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -6642,6 +6829,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -6652,6 +6840,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -6660,6 +6849,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -6668,6 +6858,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -6677,12 +6868,14 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true }, "is-glob": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -6691,6 +6884,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -6699,6 +6893,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -6708,12 +6903,14 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -6733,7 +6930,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, @@ -7234,6 +7432,15 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -8199,6 +8406,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -8214,6 +8426,16 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" + }, "lodash.isfunction": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", @@ -8224,6 +8446,16 @@ "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=" }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -8342,6 +8574,11 @@ "object-visit": "^1.0.0" } }, + "material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "math-random": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", @@ -8680,6 +8917,15 @@ "lower-case": "^1.1.1" } }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, "node-forge": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", @@ -11300,15 +11546,86 @@ } } }, + "rc-align": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-2.4.5.tgz", + "integrity": "sha512-nv9wYUYdfyfK+qskThf4BQUSIadeI/dCsfaMZfNEoxm9HwOIioQ+LyqmMK6jWHAZQgOzMLaqawhuBXlF63vgjw==", + "requires": { + "babel-runtime": "^6.26.0", + "dom-align": "^1.7.0", + "prop-types": "^15.5.8", + "rc-util": "^4.0.4" + } + }, + "rc-animate": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-2.6.0.tgz", + "integrity": "sha512-JXDycchgbOI+7T/VKmFWnAIn042LLScK1fNkmNunb0jz5q5aPGCAybx2bTo7X5t31Jkj9OsxKNb/vZPDPWufCg==", + "requires": { + "babel-runtime": "6.x", + "classnames": "^2.2.6", + "css-animation": "^1.3.2", + "prop-types": "15.x", + "raf": "^3.4.0", + "react-lifecycles-compat": "^3.0.4" + } + }, + "rc-color-picker": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rc-color-picker/-/rc-color-picker-1.2.6.tgz", + "integrity": "sha512-AaC9Pg7qCHSy5M4eVbqDIaNb2FC4SEw82GOHB2C4R/+vF2FVa/r5XA+Igg5+zLPmAvBLhz9tL4MAfkRA8yWNJw==", + "requires": { + "classnames": "^2.2.5", + "prop-types": "^15.5.8", + "rc-trigger": "1.x", + "rc-util": "^4.0.2", + "tinycolor2": "^1.4.1" + } + }, + "rc-trigger": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-1.11.5.tgz", + "integrity": "sha512-MBuUPw1nFzA4K7jQOwb7uvFaZFjXGd00EofUYiZ+l/fgKVq8wnLC0lkv36kwqM7vfKyftRo2sh7cWVpdPuNnnw==", + "requires": { + "babel-runtime": "6.x", + "create-react-class": "15.x", + "prop-types": "15.x", + "rc-align": "2.x", + "rc-animate": "2.x", + "rc-util": "4.x" + } + }, + "rc-util": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.6.0.tgz", + "integrity": "sha512-rbgrzm1/i8mgfwOI4t1CwWK7wGe+OwX+dNa7PVMgxZYPBADGh86eD4OcJO1UKGeajIMDUUKMluaZxvgraQIOmw==", + "requires": { + "add-dom-event-listener": "^1.1.0", + "babel-runtime": "6.x", + "prop-types": "^15.5.10", + "shallowequal": "^0.2.2" + } + }, "react": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.7.0.tgz", - "integrity": "sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A==", + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/react/-/react-16.8.1.tgz", + "integrity": "sha512-wLw5CFGPdo7p/AgteFz7GblI2JPOos0+biSoxf1FPsGxWQZdN/pj6oToJs1crn61DL3Ln7mN86uZ4j74p31ELQ==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.12.0" + "scheduler": "^0.13.1" + }, + "dependencies": { + "scheduler": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.1.tgz", + "integrity": "sha512-VJKOkiKIN2/6NOoexuypwSrybx13MY7NSy9RNt8wPvZDMRT1CW6qlpF5jXRToXNHz3uWzbm2elNpZfXfGPqP9A==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } } }, "react-app-polyfill": { @@ -11330,6 +11647,19 @@ } } }, + "react-color": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.17.0.tgz", + "integrity": "sha512-kJfE5tSaFe6GzalXOHksVjqwCPAsTl+nzS9/BWfP7j3EXbQ4IiLAF9sZGNzk3uq7HfofGYgjmcUgh0JP7xAQ0w==", + "requires": { + "@icons/material": "^0.2.4", + "lodash": ">4.17.4", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + } + }, "react-dev-utils": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-7.0.1.tgz", @@ -11465,14 +11795,14 @@ } }, "react-dom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.7.0.tgz", - "integrity": "sha512-D0Ufv1ExCAmF38P2Uh1lwpminZFRXEINJe53zRAbm4KPwSyd6DY/uDoS0Blj9jvPpn1+wivKpZYc8aAAN/nAkg==", + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.1.tgz", + "integrity": "sha512-N74IZUrPt6UiDjXaO7UbDDFXeUXnVhZzeRLy/6iqqN1ipfjrhR60Bp5NuBK+rv3GMdqdIuwIl22u1SYwf330bg==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.12.0" + "scheduler": "^0.13.1" } }, "react-error-overlay": { @@ -11480,11 +11810,6 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.2.tgz", "integrity": "sha512-7kEBKwU9R8fKnZJBRa5RSIfay4KJwnYvKB6gODGicUmDSAhQJ7Tdnll5S0RLtYrzRfMVXlqYw61rzrSpP4ThLQ==" }, - "react-is": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.7.0.tgz", - "integrity": "sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g==" - }, "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", @@ -11499,34 +11824,48 @@ "prop-types": "^15.6.1" } }, - "react-redux": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.0.tgz", - "integrity": "sha512-EmbC3uLl60pw2VqSSkj6HpZ6jTk12RMrwXMBdYtM6niq0MdEaRq9KYCwpJflkOZj349BLGQm1MI/JO1W96kLWQ==", + "react-router": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz", + "integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==", "requires": { - "@babel/runtime": "^7.2.0", - "hoist-non-react-statics": "^3.2.1", + "history": "^4.7.2", + "hoist-non-react-statics": "^2.5.0", "invariant": "^2.2.4", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.3" + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.1", + "warning": "^4.0.1" }, "dependencies": { - "@babel/runtime": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", - "integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==", + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", "requires": { - "regenerator-runtime": "^0.12.0" + "isarray": "0.0.1" } - }, - "regenerator-runtime": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" } } }, + "react-router-dom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz", + "integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==", + "requires": { + "history": "^4.7.2", + "invariant": "^2.2.4", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.1", + "react-router": "^4.3.1", + "warning": "^4.0.1" + } + }, "react-scripts": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-2.1.3.tgz", @@ -11583,9 +11922,9 @@ } }, "react-transition-group": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.2.tgz", - "integrity": "sha512-vwHP++S+f6KL7rg8V1mfs62+MBKtbMeZDR8KiNmD7v98Gs3UPGsDZDahPJH2PVprFW5YHJfh6cbNim3zPndaSQ==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.3.tgz", + "integrity": "sha512-2DGFck6h99kLNr8pOFk+z4Soq3iISydwOFeeEVPjTN6+Y01CmvbWmnN02VuTWyFdnRtIDPe+wy2q6Ui8snBPZg==", "requires": { "dom-helpers": "^3.3.1", "loose-envify": "^1.4.0", @@ -11593,10 +11932,26 @@ "react-lifecycles-compat": "^3.0.4" } }, + "reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "requires": { + "lodash": "^4.0.1" + } + }, + "reactn": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reactn/-/reactn-0.2.2.tgz", + "integrity": "sha512-8yjBMXg3V1RsIqM9aAHbjRQyL46pwSpd3JgFD5XVr/zsbUommhIMT9HHiDEWhpUTmKriKegy+ZFaKWJRhDjJoQ==", + "requires": { + "use-force-update": "^1.0.0" + } + }, "reactstrap": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-7.0.2.tgz", - "integrity": "sha512-VVa/1tqCiKa63YPwAIWrn4za806TVva/GEx7yP/2U+xmq1iKIzGo0UHmFL/6XpZKqx/ZzP/olkTorP/2o/2pTg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-7.1.0.tgz", + "integrity": "sha512-wtc4RkgnGn1TsZ0AxOZ2OqT+b8YmCWZj/tErPujWLepxzlEEhveZGC+uDerdaHVSAzJUP2DTk605iper7hutQQ==", "requires": { "@babel/runtime": "^7.2.0", "classnames": "^2.2.3", @@ -11610,9 +11965,9 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", - "integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz", + "integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==", "requires": { "regenerator-runtime": "^0.12.0" } @@ -11960,15 +12315,6 @@ "minimatch": "3.0.4" } }, - "redux": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", - "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", - "requires": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" - } - }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -12252,6 +12598,11 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" }, + "resolve-pathname": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", + "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -12694,9 +13045,9 @@ } }, "scheduler": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.12.0.tgz", - "integrity": "sha512-t7MBR28Akcp4Jm+QoR63XgAi9YgCUmgvDHqf5otgAj4QvdoBE4ImCX0ffehefePPG+aitiYHp0g/mW6s4Tp+dw==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.1.tgz", + "integrity": "sha512-VJKOkiKIN2/6NOoexuypwSrybx13MY7NSy9RNt8wPvZDMRT1CW6qlpF5jXRToXNHz3uWzbm2elNpZfXfGPqP9A==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -12886,6 +13237,14 @@ } } }, + "shallowequal": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-0.2.2.tgz", + "integrity": "sha1-HjL9W8q2rWiKSBLLDMBO/HXHAU4=", + "requires": { + "lodash.keys": "^3.1.2" + } + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -13503,11 +13862,6 @@ "util.promisify": "~1.0.0" } }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" - }, "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", @@ -13694,6 +14048,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tinycolor2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", + "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -13835,6 +14194,11 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "ua-parser-js": { + "version": "0.7.19", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz", + "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==" + }, "uglify-js": { "version": "3.4.9", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", @@ -14156,6 +14520,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-force-update": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/use-force-update/-/use-force-update-1.0.2.tgz", + "integrity": "sha512-/DOz91GCBRg/xMWx9+23JJvo4jLUiksnL4d+OFbbeJEm7BV5Ckn+y+1Z527niwhK8xcHh4zFgkPBtzWyrMkEyg==" + }, "util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -14202,6 +14571,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-equal": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", + "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -14256,6 +14630,14 @@ "makeerror": "1.0.x" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watch": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", @@ -14648,6 +15030,43 @@ "yargs": "12.0.2" }, "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", @@ -14691,6 +15110,156 @@ "strip-eof": "^1.0.0" } }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -14707,6 +15276,17 @@ "pump": "^3.0.0" } }, + "http-proxy-middleware": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", + "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", + "requires": { + "http-proxy": "^1.16.2", + "is-glob": "^4.0.0", + "lodash": "^4.17.5", + "micromatch": "^3.1.9" + } + }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -14721,6 +15301,68 @@ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", @@ -14748,6 +15390,26 @@ "p-is-promise": "^1.1.0" } }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, "os-locale": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", diff --git a/webui/package.json b/webui/package.json index 6e9e88c..89f3e6d 100644 --- a/webui/package.json +++ b/webui/package.json @@ -3,13 +3,21 @@ "version": "0.1.0", "private": true, "dependencies": { + "@jaames/iro": "^4.0.1", "bootstrap": "^4.2.1", - "react": "^16.7.0", - "react-dom": "^16.7.0", - "react-redux": "^6.0.0", + "react": "^16.8.1", + "react-dom": "^16.8.1", + "react-router": "^4.3.1", + "react-router-dom": "^4.3.1", "react-scripts": "2.1.3", - "reactstrap": "^7.0.2", - "redux": "^4.0.1" + "reactn": "^0.2.2", + "reactstrap": "^7.1.0" + }, + "devDependencies": { + "@types/react": "^16.8.1", + "@types/reactstrap": "^7.1.0", + "http-proxy-middleware": "latest", + "@types/node": "^11.9.0" }, "scripts": { "start": "react-scripts start", diff --git a/webui/proxy.json b/webui/proxy.json new file mode 100644 index 0000000..fae496a --- /dev/null +++ b/webui/proxy.json @@ -0,0 +1,3 @@ +{ + "url": "http://10.32.7.1:8000/" +} \ No newline at end of file diff --git a/webui/src/App.css b/webui/src/App.css index 92f956e..473beaa 100755 --- a/webui/src/App.css +++ b/webui/src/App.css @@ -1,32 +1,25 @@ -.App { - text-align: center; +.nav-tabs .nav-item { + cursor: pointer; } -.App-logo { - animation: App-logo-spin infinite 20s linear; - height: 40vmin; +a, button { + cursor: pointer; } -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; +.badge { + cursor: pointer; } -.App-link { - color: #61dafb; +.loading { + margin: 2em 0; + text-align: center; } -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +.on-switch { + text-align: center; } + +.free-buttons { + margin-top: 2em; + text-align: center; +} \ No newline at end of file diff --git a/webui/src/App.js b/webui/src/App.js index f8a9d9e..0bc4916 100755 --- a/webui/src/App.js +++ b/webui/src/App.js @@ -1,15 +1,46 @@ -import React, {Component} from 'react'; +import React, {useEffect, useState} from 'react'; import './App.css'; -import Header from "./Components/Header"; +import Header from "./Components/Structure/Header"; +import LoginForm from "./Components/Forms/LoginForm"; +import {Container} from "reactstrap"; +import {BrowserRouter} from "react-router-dom"; +import {Route} from "react-router"; +import useAuth from "./Hooks/auth"; +import IndexPage from "./Components/Pages/IndexPage"; +import GroupPage from "./Components/Pages/GroupPage"; +import Loading from "./Components/Loading"; +import LightPage from "./Components/Pages/LightPage"; +import AdminPage from "./Components/Pages/AdminPage"; -class App extends Component { - render() { - return ( -
+export default function App() { + + const [hasStarted, setHasStarted] = useState(false); + const {isLoggedIn, isChecked, verify} = useAuth(); + + useEffect(() => { + if (!hasStarted) { + verify(); + setHasStarted(true); + } + }); + + return ( + + <>
-
- ); - } + + {!isChecked && } + {isChecked && !isLoggedIn && } + {isChecked && isLoggedIn && ( + <> + + + + + + )} + + + + ); } - -export default App; diff --git a/webui/src/Components/Bridge.jsx b/webui/src/Components/Bridge.jsx new file mode 100644 index 0000000..cfff8fc --- /dev/null +++ b/webui/src/Components/Bridge.jsx @@ -0,0 +1,53 @@ +import React, {useState} from "react"; +import {Button, Card, CardBody, CardFooter, CardHeader} from "reactstrap"; +import useBridges from "../Hooks/bridge"; +import BridgeModal from "./Modals/BridgeModal"; +import useLights from "../Hooks/lights"; +import Loading from "./Loading"; +import BridgeLight from "./BridgeLight"; + +function Bridge({addr, driver, id, name}) { + const {forgetBridge, discoverLights} = useBridges(); + const {lightsByBridge} = useLights(); + const [modal, setModal] = useState(false); + + function edit() { + setModal(true); + } + + function unEdit() { + setModal(false); + } + + function forget() { + if (window.confirm(`Vil du virkelig glemme bruen "${name}"?`)) { + forgetBridge(id); + } + } + + function discover() { + discoverLights(id); + } + + const lights = lightsByBridge(id); + const hasLights = lights !== null; + + return ( + + {name} ({driver}, {addr}) + + {hasLights ? lights.map(l => ) : } + + + + {" "} + + {" "} + + + {modal && } + + ); +} + +export default Bridge; diff --git a/webui/src/Components/BridgeLight.jsx b/webui/src/Components/BridgeLight.jsx new file mode 100644 index 0000000..b001b7e --- /dev/null +++ b/webui/src/Components/BridgeLight.jsx @@ -0,0 +1,23 @@ +import React, {useState} from "react"; +import {Button, FormGroup} from "reactstrap"; +import useBridges from "../Hooks/bridge"; + +function BridgeLight({ id, bridgeId, name }) { + const [waiting, setWaiting] = useState(false); + const {forgetLight} = useBridges(); + + function forget() { + setWaiting(true); + forgetLight(bridgeId, id); + } + + return ( + + + {" "} + {name} + + ); +} + +export default BridgeLight; diff --git a/webui/src/Components/Bridges.jsx b/webui/src/Components/Bridges.jsx new file mode 100644 index 0000000..39888e0 --- /dev/null +++ b/webui/src/Components/Bridges.jsx @@ -0,0 +1,25 @@ +import React, {useState} from "react"; +import useBridges from "../Hooks/bridge"; +import Bridge from "./Bridge"; +import {Button, FormGroup} from "reactstrap"; +import BridgeModal from "./Modals/BridgeModal"; + +function Bridges() { + const [modal, setModal] = useState(false); + const {bridges} = useBridges(); + if (bridges === null) { + return <>; + } + + return ( +
+ {bridges.map(bridge => )} + + + + {modal && setModal(false)}/> } +
+ ); +} + +export default Bridges; diff --git a/webui/src/Components/Forms/LoginForm.jsx b/webui/src/Components/Forms/LoginForm.jsx new file mode 100644 index 0000000..75ad6ec --- /dev/null +++ b/webui/src/Components/Forms/LoginForm.jsx @@ -0,0 +1,141 @@ +import React, {useState} from "react"; +import { + Button, + Card, + CardBody, + CardFooter, + CardHeader, + Col, + Form, + FormGroup, + Input, + Label, + Nav, + NavItem, + NavLink, + TabPane +} from "reactstrap"; +import TabContent from "reactstrap/es/TabContent"; +import useAuth from "../../Hooks/auth"; +import {onEnter} from "../../Helpers/keys"; + +export default function LoginForm() { + const {login, register} = useAuth(); + + const [tab, setTab] = useState(1); + const [loginUser, setLoginUser] = useState(""); + const [loginPass, setLoginPass] = useState(""); + const [regUser, setRegUser] = useState(""); + const [regPass1, setRegPass1] = useState(""); + const [regPass2, setRegPass2] = useState(""); + + return ( + + Tilgangskontroll + + + + +
+ + + + setLoginUser(e.target.value)} + /> + + + + + + setLoginPass(e.target.value)} + /> + + +
+
+ +
+ + + + setRegUser(e.target.value)} + /> + + + + + + setRegPass1(e.target.value)} + /> + + + + + + setRegPass2(e.target.value)} + /> + + +
+
+
+
+ + {tab === 1 && } + {tab === 2 && ( + <> + + {" "} + + + )} + +
+ ); +} diff --git a/webui/src/Components/Group.jsx b/webui/src/Components/Group.jsx new file mode 100644 index 0000000..68c7c53 --- /dev/null +++ b/webui/src/Components/Group.jsx @@ -0,0 +1,101 @@ +import React, {useState} from "react"; +import {Button, Card, CardBody, CardFooter, CardHeader, ListGroup} from "reactstrap"; +import useLights from "../Hooks/light"; +import Light from "./Light"; +import Loading from "./Loading"; +import {deleteGroup} from "../Helpers/groups"; +import ColorModal from "./Modals/ColorModal"; +import {changeColor} from "../Helpers/lights"; +import GroupPropertiesModal from "./Modals/GroupPropertiesModal"; +import useAuth from "../Hooks/auth"; + +function Group({id, name, permissions}) { + const {user} = useAuth(); + const lights = useLights({groupId: id}); + + const [colorModal, setColorModal] = useState(false); + const [propModal, setPropModal] = useState(false); + + const ready = lights !== null; + const noLights = ready && lights.length === 0; + const hasLights = ready && lights.length > 0; + + if (id === 0 && noLights) { + return <>; + } + + function onDelete() { + if (window.confirm(`Vil du virkelig fjerne "${name}"?`)) { + deleteGroup(id); + } + } + + let cValue = null; + let bValue = null; + let pValue = null; + if (hasLights) { + cValue = lights[0].color; + bValue = lights[0].brightness; + pValue = lights[0].on; + } + + function iCan(name) { + if (permissions === null) { + return false; + } + + const perm = permissions.find(p => p.userId === user.id); + + return typeof perm !== "undefined" + ? perm[name] + : false; + } + + return ( + + {name} + + {ready + ? {lights.map(light => )} + : } + + {(iCan("manage") || iCan("write") || iCan("delete")) && ( + + {iCan("manage") && ( + + )} + {" "} + {hasLights && iCan("write") && ( + + )} + {" "} + {iCan("delete") && (id > 0) && ( + + )} + + )} + {colorModal && ( + { + lights.forEach(light => { + changeColor(light.id, color, brightness, power); + setColorModal(false); + }); + }} + onCancel={() => setColorModal(false)} + /> + )} + {propModal && ( + setPropModal(false)} + /> + )} + + ); +} + +export default Group; diff --git a/webui/src/Components/Groups.jsx b/webui/src/Components/Groups.jsx new file mode 100644 index 0000000..e81de4f --- /dev/null +++ b/webui/src/Components/Groups.jsx @@ -0,0 +1,27 @@ +import React, {useState} from "react"; +import useGroups from "../Hooks/group"; +import Group from "./Group"; +import Loading from "./Loading"; +import GroupAddModal from "./Modals/GroupAddModal"; +import {Button, FormGroup} from "reactstrap"; + +function Groups() { + const groups = useGroups(); + const [addModal, setAddModal] = useState(false); + + if (groups === null) { + return ; + } + + return ( +
+ {groups.map(group => )} + + + + {addModal && setAddModal(false)}/>} +
+ ); +} + +export default Groups; diff --git a/webui/src/Components/Header.jsx b/webui/src/Components/Header.jsx deleted file mode 100644 index 8c0f490..0000000 --- a/webui/src/Components/Header.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, {Component} from "react" -import {connect} from "react-redux"; -import {Navbar, NavbarBrand} from "reactstrap"; - -class Header extends Component { - constructor(props) { - super(props); - } - - render() { - return ( - - Lucifer - - ); - } -} - -const mapStateToProps = (state) => ({}); - -export default connect(mapStateToProps)(Header); \ No newline at end of file diff --git a/webui/src/Components/LGroup.jsx b/webui/src/Components/LGroup.jsx new file mode 100644 index 0000000..8e38e92 --- /dev/null +++ b/webui/src/Components/LGroup.jsx @@ -0,0 +1,70 @@ +import React, {useEffect, useState} from "react"; +import useLights from "../Hooks/light"; +import {Card, CardBody, CardHeader, Col, CustomInput, FormGroup} from "reactstrap"; +import ColorPicker from "./Misc/ColorPicker"; +import {changeColor} from "../Helpers/lights"; +import BrightnessSlider from "./Misc/BrightnessSlider"; + +function LGroup({id, name}) { + const lights = useLights({groupId: id}); + + const light = lights !== null && lights.length > 0 ? lights[0] : null; + const [color, setColor] = useState(null); + const [brightness, setBrightness] = useState(null); + const [power, setPower] = useState(null); + + useEffect(() => { + if (light !== null) { + setColor(light.color); + setBrightness(light.brightness); + setPower(light.on); + } + }, [light]); + + if (light === null || color === null || brightness === null || power === null) { + return <>; + } + + function recolor(newColor) { + setColor(newColor); + lights.forEach(l => changeColor(l.id, newColor, null, null)); + } + + function rebrightness(newBrightness) { + setBrightness(newBrightness); + lights.forEach(l => changeColor(l.id, null, newBrightness, null)); + } + + function repower(newPower) { + setPower(newPower); + lights.forEach(l => changeColor(l.id, null, null, newPower)); + } + + return ( + + + + {name} + + + + {power && } + + + {power && } + + + repower(e.target.checked)}/> + + + + + ); +} + +export default LGroup; diff --git a/webui/src/Components/LGroups.jsx b/webui/src/Components/LGroups.jsx new file mode 100644 index 0000000..f0bdb15 --- /dev/null +++ b/webui/src/Components/LGroups.jsx @@ -0,0 +1,21 @@ +import React from "react"; +import useGroups from "../Hooks/group"; +import Loading from "./Groups"; +import LGroup from "./LGroup"; +import {Row} from "reactstrap"; + +function LGroups() { + const groups = useGroups(); + + if (groups === null) { + return ; + } + + return ( + + {groups.map(group => )} + + ); +} + +export default LGroups; diff --git a/webui/src/Components/Light.jsx b/webui/src/Components/Light.jsx new file mode 100644 index 0000000..52f6713 --- /dev/null +++ b/webui/src/Components/Light.jsx @@ -0,0 +1,60 @@ +import React, {useState} from "react"; +import {Badge, ButtonDropdown, Col, DropdownItem, DropdownMenu, DropdownToggle, ListGroupItem, Row} from "reactstrap"; +import ColorModal from "./Modals/ColorModal"; +import {changeColor} from "../Helpers/lights"; +import LightPropertiesModal from "./Modals/LightPropertiesModal"; + +function Light({id, groupId, name, on, color, brightness}) { + const [modal, setModal] = useState(false); + const [dropDown, setDropDown] = useState(false); + const [propModal, setPropModal] = useState(false); + + return ( + + + + setDropDown(!dropDown)}> + + {name} + + + setPropModal(true)}>Egenskaper + Glem + + + + + {on && ( + setModal(true)}> + {color.toUpperCase()} + + )} + + + setModal(true)}> + {on ? brightness : "Av"} + + + + {modal && ( + { + changeColor(id, newColor, newBrightness, newPower); + setModal(false); + }} + onCancel={() => setModal(false)} + /> + )} + {propModal && ( + setPropModal(false)}/> + )} + + ); +} + +export default Light; diff --git a/webui/src/Components/Loading.jsx b/webui/src/Components/Loading.jsx new file mode 100644 index 0000000..199713b --- /dev/null +++ b/webui/src/Components/Loading.jsx @@ -0,0 +1,12 @@ +import React from "react"; +import {Spinner} from "reactstrap"; + +function Loading() { + return ( +
+ +
+ ); +} + +export default Loading; diff --git a/webui/src/Components/Misc/BrightnessSlider.jsx b/webui/src/Components/Misc/BrightnessSlider.jsx new file mode 100644 index 0000000..a426e1a --- /dev/null +++ b/webui/src/Components/Misc/BrightnessSlider.jsx @@ -0,0 +1,42 @@ +import React, {useLayoutEffect, useState} from "react"; +import {randId} from "../../Helpers/random"; +import iro from "@jaames/iro"; + +function BrightnessSlider({brightness, onChange}) { + const [random] = useState(randId()); + + useLayoutEffect(() => { + const colorPicker = new iro.ColorPicker("#brightness-picker-" + random, { + color: `rgb(${brightness}, ${brightness}, ${brightness})`, + layout: [ + { + component: iro.ui.Slider, + options: { + borderColor: '#888' + } + } + ], + }); + + colorPicker.on("input:end", color => { + onChange(color.rgb.r); + }); + + return () => { + const elem = document.getElementById(`brightness-picker-${random}`); + if (elem === null) { + return; + } + + elem.innerHTML = ""; + }; + }, [brightness]); + + return ( +
+ +
+ ); +} + +export default BrightnessSlider; diff --git a/webui/src/Components/Misc/ColorPicker.jsx b/webui/src/Components/Misc/ColorPicker.jsx new file mode 100644 index 0000000..7b70957 --- /dev/null +++ b/webui/src/Components/Misc/ColorPicker.jsx @@ -0,0 +1,40 @@ +import React, {useLayoutEffect, useState} from "react"; +import iro from '@jaames/iro'; +import {randId} from "../../Helpers/random"; + +function ColorPicker({color, onChange}) { + const [random] = useState(randId()); + + useLayoutEffect(() => { + const colorPicker = new iro.ColorPicker("#color-picker-" + random, { + color: `#${color}`, + layout: [ + { + component: iro.ui.Wheel, + options: {} + } + ], + }); + + colorPicker.on("input:end", color => { + onChange(color.hexString.substr(1)); + }); + + return () => { + const elem = document.getElementById(`color-picker-${random}`); + if (elem === null) { + return; + } + + elem.innerHTML = ""; + }; + }, [color]); + + return ( +
+ +
+ ); +} + +export default ColorPicker; diff --git a/webui/src/Components/Modals/BridgeModal.jsx b/webui/src/Components/Modals/BridgeModal.jsx new file mode 100644 index 0000000..d1a10d0 --- /dev/null +++ b/webui/src/Components/Modals/BridgeModal.jsx @@ -0,0 +1,94 @@ +import React, {useState} from "react"; +import {Button, Col, Form, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader} from "reactstrap"; +import useBridges from "../../Hooks/bridge"; +import Loading from "../Loading"; + +function BridgeModal({onCancel, bridge = null}) { + const {addBridge, editBridge} = useBridges(); + + const edit = bridge !== null; + + const [name, setName] = useState(edit ? bridge.name : ""); + const [driver, setDriver] = useState(edit ? bridge.driver : ""); + const [addr, setAddr] = useState(edit ? bridge.addr : ""); + + const [waiting, setWaiting] = useState(false); + + function onConfirm() { + if (edit) { + editBridge(bridge.id, name); + } else { + setWaiting(true); + addBridge(name, driver, addr, (result) => { + if (!result) { + alert("Noe gikk galt"); + } + + setWaiting(false); + onCancel(); + }); + } + } + + return ( + + {edit ? "Bruegenskaper" : "Ny bru"} + + {waiting ? ( + + ) : ( +
+ + + + setName(e.target.value)} + /> + + + + + + setDriver(e.target.value)} + disabled={edit} + /> + + + + + + setAddr(e.target.value)} + disabled={edit} + /> + + +
+ )} +
+ + + {" "} + + +
+ ); +} + +export default BridgeModal; diff --git a/webui/src/Components/Modals/ColorModal.jsx b/webui/src/Components/Modals/ColorModal.jsx new file mode 100644 index 0000000..7945716 --- /dev/null +++ b/webui/src/Components/Modals/ColorModal.jsx @@ -0,0 +1,39 @@ +import React, {useState} from "react"; +import {Button, CustomInput, FormGroup, Modal, ModalBody, ModalFooter, ModalHeader} from "reactstrap"; +import ColorPicker from "../Misc/ColorPicker"; +import BrightnessSlider from "../Misc/BrightnessSlider"; + +function ColorModal({cValue, bValue, pValue, onConfirm, onCancel}) { + const [color, setColor] = useState(cValue); + const [brightness, setBrightness] = useState(bValue); + const [power, setPower] = useState(pValue); + + return ( + + Fargevalg + + + + + + + + + setPower(e.target.checked)}/> + + + + + {" "} + + + + ); +} + +export default ColorModal; diff --git a/webui/src/Components/Modals/GroupAddModal.jsx b/webui/src/Components/Modals/GroupAddModal.jsx new file mode 100644 index 0000000..d2c5d26 --- /dev/null +++ b/webui/src/Components/Modals/GroupAddModal.jsx @@ -0,0 +1,43 @@ +import React, {useState} from "react"; +import {Button, Col, Form, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader} from "reactstrap"; +import {addGroup} from "../../Helpers/groups"; + +function GroupAddModal({onClose}) { + const [name, setName] = useState(""); + + return ( + + Ny gruppe + +
+ + + + setName(e.target.value)}/> + + +
+
+ + + {" "} + + +
+ ); +} + +export default GroupAddModal; diff --git a/webui/src/Components/Modals/GroupPropertiesModal.jsx b/webui/src/Components/Modals/GroupPropertiesModal.jsx new file mode 100644 index 0000000..6d0fb14 --- /dev/null +++ b/webui/src/Components/Modals/GroupPropertiesModal.jsx @@ -0,0 +1,103 @@ +import React, {useState} from "react"; +import useAuth from "../../Hooks/auth"; +import { + Button, + Col, + Form, + FormGroup, + Input, + InputGroup, + InputGroupAddon, + Label, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + Row +} from "reactstrap"; +import PermissionsModal from "./PermissionsModal"; +import {saveGroupMetadata} from "../../Helpers/groups"; + +function GroupPropertiesModal({id, nValue, permissions, onClose}) { + const {users} = useAuth(); + const [name, setName] = useState(nValue); + const [permUserId, setPermUserId] = useState(null); + const [permModal, setPermModal] = useState(false); + + function permissionFor(userId) { + const perm = permissions.find(p => p.userId === userId); + + return typeof perm !== "undefined" + ? perm + : null; + } + + return ( + + Endre gruppe + +
+ + + + setName(e.target.value)}/> + + + + + + {users.map(user => { + const permission = permissionFor(user.id); + const btnColor = permission !== null ? "secondary" : "success"; + const btnLabel = permission !== null ? "Endre" : "Opprett"; + + return ( + + + + + + + + + ) + })} + + +
+
+ + + {" "} + + + {permModal && ( + setPermModal(false)} + /> + )} +
+ ); +} + +export default GroupPropertiesModal; diff --git a/webui/src/Components/Modals/LightPropertiesModal.jsx b/webui/src/Components/Modals/LightPropertiesModal.jsx new file mode 100644 index 0000000..27594a3 --- /dev/null +++ b/webui/src/Components/Modals/LightPropertiesModal.jsx @@ -0,0 +1,64 @@ +import React, {useState} from "react"; +import {Button, Col, Form, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader} from "reactstrap"; +import useGroups from "../../Hooks/group"; +import Loading from "../Loading"; +import {changeLight} from "../../Helpers/lights"; + +function LightPropertiesModal({id, gValue, nValue, onClose}) { + const [groupId, setGroupId] = useState(gValue); + const [name, setName] = useState(nValue); + const groups = useGroups(); + if (groups === null) { + return ; + } + + return ( + + Lysegenskaper + +
+ + + + setName(e.target.value)} + /> + + +
+
+ + + + setGroupId(parseInt(e.target.value, 10))}> + {groups.map(g => )} + + + +
+
+ + + {" "} + + +
+ ); +} + +export default LightPropertiesModal; diff --git a/webui/src/Components/Modals/PermissionsModal.jsx b/webui/src/Components/Modals/PermissionsModal.jsx new file mode 100644 index 0000000..ffc73ee --- /dev/null +++ b/webui/src/Components/Modals/PermissionsModal.jsx @@ -0,0 +1,84 @@ +import React, {useState} from "react"; +import {Button, CustomInput, FormGroup, Modal, ModalBody, ModalFooter, ModalHeader} from "reactstrap"; +import {savePermissions} from "../../Helpers/groups"; + +function PermissionsModal({groupId, userId, permissions, onClose}) { + function getPermission(type) { + if (permissions === null || typeof permissions[type] === "undefined") { + return false; + } + + return permissions[type]; + } + + const [read, setRead] = useState(getPermission("read")); + const [write, setWrite] = useState(getPermission("write")); + const [create, setCreate] = useState(getPermission("create")); + const [remove, setRemove] = useState(getPermission("delete")); + const manage = getPermission("manage"); + + return ( + + Endre gruppe + + + setRead(e.target.checked)}/> + + + setWrite(e.target.checked)}/> + + + setCreate(e.target.checked)}/> + + + setRemove(e.target.checked)}/> + + + + + + + + {" "} + + + + ); +} + +export default PermissionsModal; diff --git a/webui/src/Components/Pages/AdminPage.jsx b/webui/src/Components/Pages/AdminPage.jsx new file mode 100644 index 0000000..8ebdf68 --- /dev/null +++ b/webui/src/Components/Pages/AdminPage.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import Bridges from "../Bridges"; +import useAuth from "../../Hooks/auth"; + +function AdminPage() { + const {user} = useAuth(); + if (user.name === "Admin") { + return <>; + } + + return ( +
+ +
+ ); +} + +export default AdminPage; diff --git a/webui/src/Components/Pages/GroupPage.jsx b/webui/src/Components/Pages/GroupPage.jsx new file mode 100644 index 0000000..57841d0 --- /dev/null +++ b/webui/src/Components/Pages/GroupPage.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import Groups from "../Groups"; + +function GroupPage() { + return ( + + ); +} + +export default GroupPage; diff --git a/webui/src/Components/Pages/IndexPage.jsx b/webui/src/Components/Pages/IndexPage.jsx new file mode 100644 index 0000000..b794c8f --- /dev/null +++ b/webui/src/Components/Pages/IndexPage.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import LGroups from "../LGroups"; + +function IndexPage() { + return ( + + ); +} + +export default IndexPage; diff --git a/webui/src/Components/Pages/LightPage.jsx b/webui/src/Components/Pages/LightPage.jsx new file mode 100644 index 0000000..863ae8d --- /dev/null +++ b/webui/src/Components/Pages/LightPage.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import useLights from "../../Hooks/light"; +import Light from "../Light"; +import Loading from "../Loading"; +import {Card, CardBody, CardHeader} from "reactstrap"; + +function LightPage() { + const lights = useLights(); + if (lights === null) { + return ; + } + + return ( + + + Tilgjengelige lys + + + {lights.map(light => )} + + + ); +} + +export default LightPage; diff --git a/webui/src/Components/Structure/Header.jsx b/webui/src/Components/Structure/Header.jsx new file mode 100644 index 0000000..1675a20 --- /dev/null +++ b/webui/src/Components/Structure/Header.jsx @@ -0,0 +1,37 @@ +import React, {useState} from "react" +import {Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink} from "reactstrap"; +import useAuth from "../../Hooks/auth"; +import {Link} from "react-router-dom"; +import {onEnter} from "../../Helpers/keys"; + +export default function Header() { + const [showMenu, setShowMenu] = useState(false); + const {isLoggedIn, logout} = useAuth(); + + return ( + + Lucifer + setShowMenu(!showMenu)}/> + + {isLoggedIn && ( + + )} + + + ); +} \ No newline at end of file diff --git a/webui/src/Helpers/fetcher.js b/webui/src/Helpers/fetcher.js new file mode 100644 index 0000000..b2284a2 --- /dev/null +++ b/webui/src/Helpers/fetcher.js @@ -0,0 +1,92 @@ +const PREFIX = "/api"; + +function formatUrl(url, params = {}) { + const urlWithPrefix = PREFIX + url; + + const queryString = + Object + .keys(params) + .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])); + + if (queryString.length > 0) { + return urlWithPrefix + "?" + queryString; + } + + return urlWithPrefix; +} + +/** + * @param {Response} res + * @return {Promise>} + */ +function authCheck(res) { + return res.json().then(json => { + if (typeof json.data === "undefined") { + json.data = null; + } + + if (typeof json.error === "undefined") { + json.error = null; + } + + return Promise.resolve(json); + }) +} + +/** + * @param {string} url + * @param {object} params + * @returns {Promise} + */ +export function fetchGet(url, params = {}) { + return fetch(formatUrl(url, params), { + method: "GET", + credentials: "include", + }).then(authCheck); +} + +/** + * @param {string} url + * @param {object} params + * @returns {Promise} + */ +export function fetchDelete(url, params = {}) { + return fetch(formatUrl(url, params), { + method: "DELETE", + credentials: "include", + }).then(authCheck); +} + + +/** + * @param {string} url + * @param {object} data + * @returns {Promise} + */ +export function fetchPost(url, data = {}) { + return fetch(formatUrl(url), { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + body: JSON.stringify(data), + }).then(authCheck); +} + + +/** + * @param {string} url + * @param {object} data + * @returns {Promise} + */ +export function fetchPatch(url, data = {}) { + return fetch(formatUrl(url), { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + body: JSON.stringify(data), + }).then(authCheck); +} diff --git a/webui/src/Helpers/groups.js b/webui/src/Helpers/groups.js new file mode 100644 index 0000000..dce64fa --- /dev/null +++ b/webui/src/Helpers/groups.js @@ -0,0 +1,107 @@ +import {randId} from "./random"; +import {fetchDelete, fetchGet, fetchPatch, fetchPost} from "./fetcher"; +import {nullish} from "./null"; + +const localData = {}; +const callbacks = []; + +export function subscribeToGroup(groupId, callback) { + const callbackId = randId(); + + callbacks.push({callbackId, groupId, callback}); + + if (groupId >= 0) { + fetchOne(groupId); + } else { + fetchAll(); + } + + return callbackId; +} + +export function unsubscribeFromGroup(callbackId) { + const callback = callbacks.find(c => c !== null && c.callbackId === callbackId); + const index = callbacks.indexOf(callback); + callbacks[index] = null; +} + +export function addGroup(name) { + fetchPost("/group/", {name}).then(({data, error}) => { + if (error === null) { + handleGroup(data); + fetchAll(); + } + }) +} + +export function deleteGroup(groupId) { + fetchDelete(`/group/${groupId}`).then(({data, error}) => { + if (error === null) { + fetchAll(); + } + }); +} + +export function saveGroupMetadata(groupId, name) { + fetchPatch(`/group/${groupId}`, {name}).then(({data, error}) => { + if (error === null) { + fetchAll(); + } + }); +} + +export function savePermissions(groupId, userId, permissions) { + fetchPatch(`/group/${groupId}/permission/${userId}`, permissions).then(({data, error}) => { + if (error === null) { + fetchAll(); + } + }); +} + +function fetchAll() { + fetchGet("/group/").then(({data, error}) => { + if (error === null) { + handleGroups(data); + } + }); +} + +function fetchOne(id) { + fetchGet(`/group/${id}`).then(({data, error}) => { + if (error === null) { + handleGroup(data); + } + }); +} + +function handleGroups(groups) { + groups.forEach(g => handleGroup(g)); + + for (let key in localData) { + if (localData.hasOwnProperty(key) && nullish(groups.find(g => g.id === key))) { + delete localData[key]; + } + } + + dispatch(groups); +} + +function handleGroup(group) { + localData[group.id] = group; + + dispatch(group); +} + +function dispatch(data) { + if (Array.isArray(data)) { + callbacks + .filter(c => c !== null) + .filter(c => c.groupId === -1) + .forEach(c => c.callback(data)); + } else { + callbacks + .filter(c => c !== null) + .filter(c => c.groupId === data.id) + .forEach(c => c.callback(data)); + } +} diff --git a/webui/src/Helpers/keys.js b/webui/src/Helpers/keys.js new file mode 100644 index 0000000..ce8e662 --- /dev/null +++ b/webui/src/Helpers/keys.js @@ -0,0 +1,7 @@ +export function onEnter(callback) { + return event => { + if (event.keyCode === 13) { + callback(); + } + } +} \ No newline at end of file diff --git a/webui/src/Helpers/lights.js b/webui/src/Helpers/lights.js new file mode 100644 index 0000000..0638820 --- /dev/null +++ b/webui/src/Helpers/lights.js @@ -0,0 +1,141 @@ +import {randId} from "./random"; +import {nullish} from "./null"; +import {fetchGet, fetchPatch} from "./fetcher"; + +const localData = {}; +const callbacks = []; + +export function subscribeToLight(lightId, callback) { + const callbackId = randId(); + + callbacks.push({callbackId, lightId, callback}); + + if (lightId >= 0) { + dispatch(); + + fetchOne(lightId); + } else { + dispatch(); + + fetchAll(); + } +} + +export function unsubscribeFromLight(callbackId) { + const callback = callbacks.find(c => c !== null && c.callbackId === callbackId); + const index = callbacks.indexOf(callback); + callbacks[index] = null; +} + +export function changeColor(lightId, newColor, newBrightness, newPower) { + const light = localData[lightId]; + if (nullish(light)) { + return; + } + + const oldBrightness = light.brightness; + const oldPower = light.on; + const oldColor = light.color; + light.color = newColor; + light.brightness = newBrightness; + light.on = newPower; + dispatch(); + + fetchPatch(`/light/${lightId}`, { + color: newColor, + brightness: newBrightness, + on: newPower, + }).then(({data, error}) => { + if (error !== null) { + light.color = oldColor; + light.brightness = oldBrightness; + light.on = oldPower; + dispatch(); + return; + } + + localData[lightId] = data; + dispatch(); + }); +} + +export function changeLight(lightId, name, groupId) { + const light = localData[lightId]; + if (nullish(light)) { + return; + } + + const oldName = light.name; + const oldGroupId = light.groupId; + light.name = name; + light.groupId = groupId; + dispatch(); + console.log({name,groupId}); + + fetchPatch(`/light/${lightId}`, { + name: name, + groupId: groupId, + }).then(({data, error}) => { + if (error !== null) { + light.name = oldName; + light.groupId = oldGroupId; + dispatch(); + return; + } + + localData[lightId] = data; + dispatch(); + }); +} + +function fetchAll() { + fetchGet(`/light/`).then(({data, error}) => { + if (error === null) { + handleLights(data); + } + }); +} + +function fetchOne(id) { + fetchGet(`/light/${id}`).then(({data, error}) => { + if (error === null) { + handleLight(data); + } + }); +} + +function handleLights(lights) { + lights.forEach(l => handleLight(l)); + + for (let key in localData) { + if (localData.hasOwnProperty(key) && nullish(lights.find(l => l.id === parseInt(key, 10)))) { + delete localData[key]; + } + } + + dispatch(lights); +} + +function handleLight(light) { + localData[light.id] = light; + + dispatch(light); +} + +function dispatch(data = null) { + if (data === null) { + data = Object.values(localData); + } + + if (Array.isArray(data)) { + callbacks + .filter(c => c !== null) + .filter(c => c.lightId === -1) + .forEach(c => c.callback(data)); + } else { + callbacks + .filter(c => c !== null) + .filter(c => c.lightId === data.id) + .forEach(c => c.callback(data)); + } +} diff --git a/webui/src/Helpers/null.js b/webui/src/Helpers/null.js new file mode 100644 index 0000000..edf07dc --- /dev/null +++ b/webui/src/Helpers/null.js @@ -0,0 +1,7 @@ +export function nullish(value) { + return typeof value === "undefined" || value === null; +} + +export function notNullish(value) { + return !nullish(value); +} \ No newline at end of file diff --git a/webui/src/Helpers/percentage.js b/webui/src/Helpers/percentage.js new file mode 100644 index 0000000..b9a3d56 --- /dev/null +++ b/webui/src/Helpers/percentage.js @@ -0,0 +1,3 @@ +export function percentage(value, max) { + return (100 * value / max).toFixed(1) + " %"; +} \ No newline at end of file diff --git a/webui/src/Helpers/random.js b/webui/src/Helpers/random.js new file mode 100644 index 0000000..d586b85 --- /dev/null +++ b/webui/src/Helpers/random.js @@ -0,0 +1,12 @@ +const POSSIBLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +const POSSIBLE_LENGTH = POSSIBLE.length; + +export function randId() { + let text = ""; + + for (let i = 0; i < 16; i++) { + text += POSSIBLE.charAt(Math.floor(Math.random() * POSSIBLE_LENGTH)); + } + + return text; +} \ No newline at end of file diff --git a/webui/src/Hooks/auth.js b/webui/src/Hooks/auth.js new file mode 100644 index 0000000..f9b18d8 --- /dev/null +++ b/webui/src/Hooks/auth.js @@ -0,0 +1,91 @@ +import {setGlobal, useGlobal} from "reactn"; +import {fetchGet, fetchPost} from "../Helpers/fetcher"; + +setGlobal({ + "auth/login": false, + "auth/checked": false, + "auth/users/me": null, + "auth/users/all": [], +}); + +export default function useAuth() { + const [isLoggedIn, setIsLoggedIn] = useGlobal("auth/login"); + const [isChecked, setIsChecked] = useGlobal("auth/checked"); + const [user, setUser] = useGlobal("auth/users/me"); + const [users, setUsers] = useGlobal("auth/users/all"); + + function fetchUsers() { + fetchGet("/user/").then(({data, error}) => { + if (error === null) { + setUsers(data); + } + }); + } + + function verify() { + setIsChecked(false); + + fetchGet("/user/session").then(({data, error}) => { + const validSession = error === null && data.loggedIn; + + setIsLoggedIn(validSession); + + if (validSession) { + setUser(data.user); + fetchUsers(); + } + + setIsChecked(true); + }) + } + + function login(username, password) { + setIsChecked(false); + + fetchPost("/user/login", {username, password}).then(({data, error}) => { + setIsChecked(true); + + if (error !== null) { + setIsLoggedIn(false); + return; + } + + setIsLoggedIn(true); + setIsLoggedIn(data); + fetchUsers(); + }); + } + + function logout() { + setIsChecked(false); + + fetchPost("/user/logout").then(({data, error}) => { + setIsChecked(true); + + if (error !== null) { + setIsLoggedIn(false); + return; + // TODO: Show errors ¿? + } + + setIsLoggedIn(false); + }); + } + + function register(username, password, repeated) { + setIsChecked(false); + + if (password !== repeated) { + alert("Passordene er ikke like"); + return; + } + + fetchPost("/user/register", {username, password}).then(({data, error}) => { + if (error !== null) { + login(username, password); + } + }) + } + + return {isLoggedIn, isChecked, user, users, verify, login, logout, register}; +} diff --git a/webui/src/Hooks/bridge.js b/webui/src/Hooks/bridge.js new file mode 100644 index 0000000..bbbc997 --- /dev/null +++ b/webui/src/Hooks/bridge.js @@ -0,0 +1,99 @@ +import {setGlobal, useGlobal} from "reactn"; +import {useEffect} from "react"; +import {fetchDelete, fetchGet, fetchPatch, fetchPost} from "../Helpers/fetcher"; +import useLights from "./lights"; + +setGlobal({ + bridges: null, +}); + +function useBridges() { + const [bridges, setBridges] = useGlobal("bridges"); + const {reloadLights} = useLights(); + + function reloadBridges() { + setBridges(null); + + fetchGet("/bridge/").then(({error, data}) => { + if (error !== null) { + console.error(error); + return; + } + + setBridges(data); + }); + } + + useEffect(() => { + if (bridges === null) { + reloadBridges(); + } + }, []); + + function bridge(id) { + if (bridges === null) { + return null; + } + + return bridges.find(b => b.id === id); + } + + function forgetBridge(id) { + fetchDelete(`/bridge/${id}`).then(({data, error}) => { + if (error !== null) { + console.error(error); + } + + reloadBridges(); + }); + } + + function forgetLight(bridgeId, lightId) { + fetchDelete(`/bridge/${bridgeId}/light/${lightId}`).then(({data, error}) => { + if (error !== null) { + console.error(error); + } + + reloadBridges(); + }); + } + + function addBridge(name, driver, addr, callback = null) { + fetchPost("/bridge/", {name, driver, addr}).then(({error}) => { + if (error !== null) { + console.error(error); + } + + if (callback !== null) { + callback(error === null); + } + + reloadBridges(); + }); + } + + function editBridge(id, name) { + fetchPatch(`/bridge/${id}`, {name}).then(({error}) => { + if (error !== null) { + console.error(error); + } + + reloadBridges(); + }); + } + + function discoverLights(id) { + fetchPost(`/bridge/${id}/discover`).then(({error}) => { + if (error !== null) { + console.error(error); + } + + reloadLights(); + reloadBridges(); + }); + } + + return {bridges, bridge, forgetBridge, addBridge, editBridge, forgetLight, discoverLights}; +} + +export default useBridges; diff --git a/webui/src/Hooks/group.js b/webui/src/Hooks/group.js new file mode 100644 index 0000000..e4c609a --- /dev/null +++ b/webui/src/Hooks/group.js @@ -0,0 +1,20 @@ +import {useEffect, useState} from "react"; +import {subscribeToGroup, unsubscribeFromGroup} from "../Helpers/groups"; + +export default function useGroups(id = -1) { + const [group, setGroup] = useState(null); + + function onChange(group) { + setGroup(group); + } + + useEffect(() => { + const cbId = subscribeToGroup(id, onChange); + + return () => { + unsubscribeFromGroup(cbId); + }; + }, []); + + return group; +} diff --git a/webui/src/Hooks/light.js b/webui/src/Hooks/light.js new file mode 100644 index 0000000..3956ef1 --- /dev/null +++ b/webui/src/Hooks/light.js @@ -0,0 +1,24 @@ +import {useEffect, useState} from "react"; +import {subscribeToLight, unsubscribeFromLight} from "../Helpers/lights"; + +export default function useLights({groupId = -1, id = -1} = {}) { + const [light, setLight] = useState(null); + + function onChange(light) { + setLight(light); + } + + useEffect(() => { + const cbId = subscribeToLight(id, onChange); + + return () => { + unsubscribeFromLight(cbId); + }; + }, []); + + if (groupId >= 0 && light !== null) { + return light.filter(l => l.groupId === groupId); + } + + return light; +} \ No newline at end of file diff --git a/webui/src/Hooks/lights.js b/webui/src/Hooks/lights.js new file mode 100644 index 0000000..cc9c343 --- /dev/null +++ b/webui/src/Hooks/lights.js @@ -0,0 +1,42 @@ +import {setGlobal, useGlobal} from "reactn"; +import {fetchGet} from "../Helpers/fetcher"; +import {useEffect} from "react"; + +setGlobal({ + lights: null, +}); + +function useLights() { + const [lights, setLights] = useGlobal("lights"); + + function reloadLights() { + setLights(null); + + fetchGet("/light/").then(({error, data}) => { + if (error !== null) { + console.error(error); + return; + } + + setLights(data); + }); + } + + useEffect(() => { + if (lights === null) { + reloadLights(); + } + }, []); + + function lightsByBridge(bridgeId) { + if (lights === null) { + return null; + } + + return lights.filter(light => light.bridgeId === bridgeId); + } + + return {lights, lightsByBridge, reloadLights}; +} + +export default useLights; \ No newline at end of file diff --git a/webui/src/Reducers/authReducer.js b/webui/src/Reducers/authReducer.js deleted file mode 100644 index 38bc94b..0000000 --- a/webui/src/Reducers/authReducer.js +++ /dev/null @@ -1,33 +0,0 @@ -const initialState = { - isChecked: false, - isLoggedIn: false, -}; - -const VERIFICATION_STARTED = "auth/verification/started"; -const VERIFICATION_SUCCESS = "auth/verification/changed"; -const VERIFICATION_FAILED = "auth/verification/failed"; - -const authReducer = (state = initialState, {type, payload} = {}) => { - switch (type) { - case VERIFICATION_STARTED: - return initialState; - case VERIFICATION_SUCCESS: - return { - isChecked: true, - isLoggedIn: true, - }; - case VERIFICATION_FAILED: - return { - isChecked: true, - isLoggedIn: true, - }; - default: - return state; - } -}; - -export const verificationStartedEvent = () => ({ type: VERIFICATION_STARTED }); -export const verificationSucceededEvent = () => ({ type: VERIFICATION_SUCCESS }); -export const verificationFailedEvent = () => ({ type: VERIFICATION_FAILED }); - -export default authReducer; \ No newline at end of file diff --git a/webui/src/Reducers/index.js b/webui/src/Reducers/index.js deleted file mode 100644 index 6b711a8..0000000 --- a/webui/src/Reducers/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import {combineReducers, createStore} from "redux"; -import authReducer from "./authReducer"; - -const rootReducer = combineReducers({ - auth: authReducer, -}); - -const store = createStore(rootReducer); - -export default store; \ No newline at end of file diff --git a/webui/src/index.js b/webui/src/index.js index 0a61806..9e92ffb 100755 --- a/webui/src/index.js +++ b/webui/src/index.js @@ -3,13 +3,5 @@ import ReactDOM from "react-dom"; import App from "./App"; import "bootstrap/dist/css/bootstrap.min.css"; -import {Provider} from "react-redux"; -import store from "./Reducers"; -ReactDOM.render( - ( - - - - ), - document.getElementById("root")); +ReactDOM.render(, document.getElementById("root")); diff --git a/webui/src/setupProxy.js b/webui/src/setupProxy.js new file mode 100644 index 0000000..f27be7a --- /dev/null +++ b/webui/src/setupProxy.js @@ -0,0 +1,15 @@ +const fs = require("fs"); +const proxy = require("http-proxy-middleware"); + +let config = {url: "http://10.12.121.228:8100/"}; +try { + const data = fs.readFileSync("./proxy.json", "utf-8"); + config = JSON.parse(data) +} catch(err) { + console.error("Failed to load proxy.json in project root"); +} + +module.exports = function(app) { + app.use(proxy('/api/ws', { target: config.url, ws: true })); + app.use(proxy('/api', { target: config.url, ws: false })); +};