From e4078de8454dde40f3fa3b89110c67dc4df01b01 Mon Sep 17 00:00:00 2001 From: Stian Fredrik Aune Date: Sun, 23 May 2021 15:08:37 +0200 Subject: [PATCH 1/3] driver in bridgetest --- app/config/driver.go | 15 +++++++++++++-- cmd/bridgetest/main.go | 11 +++++++---- internal/drivers/provider.go | 16 ++++++++++++++++ models/bridge.go | 2 +- models/errors.go | 1 + webui/src/res/colors.sass | 2 +- 6 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 internal/drivers/provider.go diff --git a/app/config/driver.go b/app/config/driver.go index 59c8525..2ec8fa7 100644 --- a/app/config/driver.go +++ b/app/config/driver.go @@ -1,12 +1,23 @@ package config -import "git.aiterp.net/lucifer/new-server/models" +import ( + "git.aiterp.net/lucifer/new-server/internal/drivers" + "git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf" + "git.aiterp.net/lucifer/new-server/models" + "sync" +) var dp models.DriverProvider +var dpMutex sync.Mutex func DriverProvider() models.DriverProvider { + dpMutex.Lock() + defer dpMutex.Unlock() + if dp == nil { - panic("not implemented yet") + dp = &drivers.MapBasedDriverProvider{Data: map[models.DriverKind]models.Driver{ + models.DTNanoLeaf: &nanoleaf.Driver{}, + }} } return dp diff --git a/cmd/bridgetest/main.go b/cmd/bridgetest/main.go index c1b2ec8..055bc02 100644 --- a/cmd/bridgetest/main.go +++ b/cmd/bridgetest/main.go @@ -5,7 +5,7 @@ import ( "context" "flag" "fmt" - "git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf" + "git.aiterp.net/lucifer/new-server/app/config" "git.aiterp.net/lucifer/new-server/models" "log" "os" @@ -15,7 +15,7 @@ import ( "time" ) -var flagDriver = flag.String("driver", "Nanoleaf", "The bridge driver to use") +var flagDriver = flag.String("driver", string(models.DTNanoLeaf), "The bridge driver to use") var flagAddress = flag.String("address", "127.0.0.1", "The bridge's address") var flagToken = flag.String("token", "", "The bridge's access token / api key / login") var flagPair = flag.Bool("pair", false, "Try to pair with the bridge.") @@ -25,8 +25,11 @@ var flagSearchTimeout = flag.Duration("search-timeout", time.Second*3, "Timeout func main() { flag.Parse() - // TODO: Select driver - driver := nanoleaf.Driver{} + // Find drivers + driver, err := config.DriverProvider().Provide(models.DriverKind(*flagDriver)) + if err != nil { + log.Fatalln("Failed to find driver:", err) + } // Find bridge bridges, err := driver.SearchBridge(context.Background(), *flagAddress, !*flagPair) diff --git a/internal/drivers/provider.go b/internal/drivers/provider.go new file mode 100644 index 0000000..4490640 --- /dev/null +++ b/internal/drivers/provider.go @@ -0,0 +1,16 @@ +package drivers + +import "git.aiterp.net/lucifer/new-server/models" + +type MapBasedDriverProvider struct { + Data map[models.DriverKind]models.Driver +} + +func (m *MapBasedDriverProvider) Provide(kind models.DriverKind) (models.Driver, error) { + if m.Data[kind] == nil { + return nil, models.ErrUnknownDriver + } + + return m.Data[kind], nil +} + diff --git a/models/bridge.go b/models/bridge.go index f8f31ea..2bf1773 100644 --- a/models/bridge.go +++ b/models/bridge.go @@ -21,7 +21,7 @@ type DriverKind string var ( DTHue DriverKind = "Hue" - DTNanoLeaf DriverKind = "NanoLeaf" + DTNanoLeaf DriverKind = "Nanoleaf" ) var ValidDriverKinds = []DriverKind{ diff --git a/models/errors.go b/models/errors.go index 69ab456..be2062f 100644 --- a/models/errors.go +++ b/models/errors.go @@ -8,6 +8,7 @@ var ErrBadInput = errors.New("bad input") var ErrBadColor = errors.New("bad color") var ErrInternal = errors.New("internal") var ErrUnknownColorFormat = errors.New("unknown color format") +var ErrUnknownDriver = errors.New("unknown driver") var ErrMissingToken = errors.New("driver is missing authentication information") var ErrIncorrectToken = errors.New("driver is not accepting authentication information") diff --git a/webui/src/res/colors.sass b/webui/src/res/colors.sass index 2df2e61..4ae4006 100644 --- a/webui/src/res/colors.sass +++ b/webui/src/res/colors.sass @@ -1,5 +1,5 @@ $color-background: #111 -$color-foreground: rgb(238, 238, 238) +$color-foreground: rgb(204, 204, 204) $color-foreground-dark: rgba(238, 238, 238, 0.05) body, html From f045bc0f4dc9d713fcf8bd372f31990de8a9423e Mon Sep 17 00:00:00 2001 From: Stian Fredrik Aune Date: Sun, 23 May 2021 22:22:15 +0200 Subject: [PATCH 2/3] color api --- app/api/bridges.go | 4 + app/api/presets.go | 83 ++++++++++++++++ app/config/driver.go | 4 +- app/config/repo.go | 9 ++ app/server.go | 1 + cmd/bridgetest/main.go | 2 +- cmd/goose/main.go | 2 + internal/drivers/provider.go | 10 +- internal/mysql/bridgerepo.go | 2 +- internal/mysql/presetrepo.go | 99 ++++++++++++++++++++ models/colorpreset.go | 26 +++++ scripts/20210523175111_color_preset.sql | 17 ++++ scripts/20210523175425_color_preset_name.sql | 11 +++ 13 files changed, 260 insertions(+), 10 deletions(-) create mode 100644 app/api/presets.go create mode 100644 internal/mysql/presetrepo.go create mode 100644 models/colorpreset.go create mode 100644 scripts/20210523175111_color_preset.sql create mode 100644 scripts/20210523175425_color_preset_name.sql diff --git a/app/api/bridges.go b/app/api/bridges.go index e9fe792..c82a0ca 100644 --- a/app/api/bridges.go +++ b/app/api/bridges.go @@ -12,6 +12,10 @@ func Bridges(r gin.IRoutes) { return config.BridgeRepository().FetchAll(ctxOf(c)) })) + r.GET("/:id", handler(func(c *gin.Context) (interface{}, error) { + return config.BridgeRepository().Find(ctxOf(c), intParam(c, "id")) + })) + r.POST("", handler(func(c *gin.Context) (interface{}, error) { var body struct { Driver models.DriverKind `json:"driver"` diff --git a/app/api/presets.go b/app/api/presets.go new file mode 100644 index 0000000..6682d21 --- /dev/null +++ b/app/api/presets.go @@ -0,0 +1,83 @@ +package api + +import ( + "git.aiterp.net/lucifer/new-server/app/config" + "git.aiterp.net/lucifer/new-server/models" + "github.com/gin-gonic/gin" +) + +func ColorPresets(r gin.IRoutes) { + r.GET("", handler(func(c *gin.Context) (interface{}, error) { + return config.ColorPresetRepository().FetchAll(ctxOf(c)) + })) + + r.GET("/:id", handler(func(c *gin.Context) (interface{}, error) { + return config.ColorPresetRepository().Find(ctxOf(c), intParam(c, "id")) + })) + + r.POST("", handler(func(c *gin.Context) (interface{}, error) { + var body struct { + Name string `json:"name"` + ColorString string `json:"colorString"` + } + err := parseBody(c, &body) + if err != nil { + return nil, err + } + + newColor, err := models.ParseColorValue(body.ColorString) + if err != nil { + return nil, err + } + + preset := models.ColorPreset{ + Name: body.Name, + Value: newColor, + } + + preset.Validate() + err = config.ColorPresetRepository().Save(ctxOf(c), &preset) + if err != nil { + return nil, err + } + + return preset, nil + })) + + r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) { + var body struct { + Name *string `json:"name"` + ColorString *string `json:"colorString"` + } + err := parseBody(c, &body) + if err != nil { + return nil, err + } + + preset, err := config.ColorPresetRepository().Find(ctxOf(c), intParam(c, "id")) + if err != nil { + return nil, err + } + + if body.Name != nil { + preset.Name = *body.Name + } + + if body.ColorString != nil { + newColor, err := models.ParseColorValue(*body.ColorString) + if err != nil { + return nil, err + } + + preset.Value = newColor + } + + preset.Validate() + err = config.ColorPresetRepository().Save(ctxOf(c), &preset) + if err != nil { + return nil, err + } + + return preset, nil + })) +} diff --git a/app/config/driver.go b/app/config/driver.go index 2ec8fa7..51fa613 100644 --- a/app/config/driver.go +++ b/app/config/driver.go @@ -15,9 +15,9 @@ func DriverProvider() models.DriverProvider { defer dpMutex.Unlock() if dp == nil { - dp = &drivers.MapBasedDriverProvider{Data: map[models.DriverKind]models.Driver{ + dp = drivers.DriverMap{ models.DTNanoLeaf: &nanoleaf.Driver{}, - }} + } } return dp diff --git a/app/config/repo.go b/app/config/repo.go index 53b65a4..b251e22 100644 --- a/app/config/repo.go +++ b/app/config/repo.go @@ -7,6 +7,7 @@ import ( var ( bRepo models.BridgeRepository + cpRepo models.ColorPresetRepository dRepo models.DeviceRepository ehRepo models.EventHandlerRepository ) @@ -19,6 +20,14 @@ func BridgeRepository() models.BridgeRepository { return bRepo } +func ColorPresetRepository() models.ColorPresetRepository { + if cpRepo == nil { + cpRepo = &mysql.ColorPresetRepo{DBX: DBX()} + } + + return cpRepo +} + func DeviceRepository() models.DeviceRepository { if dRepo == nil { panic("panik") diff --git a/app/server.go b/app/server.go index f13657d..ba1f6ea 100644 --- a/app/server.go +++ b/app/server.go @@ -17,6 +17,7 @@ func StartServer() { apiGin := ginny.Group("/api") api.Bridges(apiGin.Group("/bridges")) + api.ColorPresets(apiGin.Group("/color-presets")) api.DriverKinds(apiGin.Group("/driver-kinds")) api.Events(apiGin.Group("/events")) diff --git a/cmd/bridgetest/main.go b/cmd/bridgetest/main.go index 055bc02..06dc50b 100644 --- a/cmd/bridgetest/main.go +++ b/cmd/bridgetest/main.go @@ -65,7 +65,7 @@ func main() { _ = driver.Publish(context.Background(), bridge, devices) - ch := make(chan models.Event) + ch := config.EventChannel go func() { err := driver.Run(context.Background(), bridge, ch) if err != nil { diff --git a/cmd/goose/main.go b/cmd/goose/main.go index b02c383..a0eadbe 100644 --- a/cmd/goose/main.go +++ b/cmd/goose/main.go @@ -4,11 +4,13 @@ import ( "git.aiterp.net/lucifer/new-server/app/config" "github.com/pressly/goose" "log" + "time" ) func main() { db := config.DBX().DB + log.Printf("Target version: %s",time.Now().Format("20060102150405")) log.Printf("Database: %s:%d/%s", config.MySqlHost(), config.MySqlPort(), config.MySqlSchema()) log.Printf("Authenticating as: %s", config.MySqlUsername()) diff --git a/internal/drivers/provider.go b/internal/drivers/provider.go index 4490640..c3763b4 100644 --- a/internal/drivers/provider.go +++ b/internal/drivers/provider.go @@ -2,15 +2,13 @@ package drivers import "git.aiterp.net/lucifer/new-server/models" -type MapBasedDriverProvider struct { - Data map[models.DriverKind]models.Driver -} +type DriverMap map[models.DriverKind]models.Driver -func (m *MapBasedDriverProvider) Provide(kind models.DriverKind) (models.Driver, error) { - if m.Data[kind] == nil { +func (m DriverMap) Provide(kind models.DriverKind) (models.Driver, error) { + if m[kind] == nil { return nil, models.ErrUnknownDriver } - return m.Data[kind], nil + return m[kind], nil } diff --git a/internal/mysql/bridgerepo.go b/internal/mysql/bridgerepo.go index 172fe58..926b9d6 100644 --- a/internal/mysql/bridgerepo.go +++ b/internal/mysql/bridgerepo.go @@ -22,7 +22,7 @@ func (b *BridgeRepo) Find(ctx context.Context, id int) (models.Bridge, error) { func (b *BridgeRepo) FetchAll(ctx context.Context) ([]models.Bridge, error) { bridges := make([]models.Bridge, 0, 8) - err := b.DBX.GetContext(ctx, bridges, "SELECT * FROM bridge") + err := b.DBX.GetContext(ctx, &bridges, "SELECT * FROM bridge") if err != nil { return nil, dbErr(err) } diff --git a/internal/mysql/presetrepo.go b/internal/mysql/presetrepo.go new file mode 100644 index 0000000..296fcc1 --- /dev/null +++ b/internal/mysql/presetrepo.go @@ -0,0 +1,99 @@ +package mysql + +import ( + "context" + "git.aiterp.net/lucifer/new-server/models" + "github.com/jmoiron/sqlx" +) + +type presetRecord struct { + ID int `db:"id"` + Name string `db:"name"` + Hue float64 `db:"hue"` + Saturation float64 `db:"saturation"` + Kelvin int `db:"kelvin"` +} + +type ColorPresetRepo struct { + DBX *sqlx.DB +} + +func (c *ColorPresetRepo) Find(ctx context.Context, id int) (models.ColorPreset, error) { + var record presetRecord + err := c.DBX.GetContext(ctx, &record, "SELECT * FROM color_preset WHERE id = ?", id) + if err != nil { + return models.ColorPreset{}, dbErr(err) + } + + return c.fromRecords(record)[0], nil +} + +func (c *ColorPresetRepo) FetchAll(ctx context.Context) ([]models.ColorPreset, error) { + records := make([]presetRecord, 0, 16) + err := c.DBX.SelectContext(ctx, &records, "SELECT * FROM color_preset") + if err != nil { + return nil, dbErr(err) + } + + return c.fromRecords(records...), nil +} + +func (c *ColorPresetRepo) Save(ctx context.Context, preset *models.ColorPreset) error { + if preset.ID > 0 { + _, err := c.DBX.ExecContext( + ctx, + "UPDATE color_preset SET name = ?, hue = ?, saturation = ?, kelvin = ? WHERE id = ?", + preset.Name, preset.Value.Hue, preset.Value.Saturation, preset.Value.Kelvin, preset.ID, + ) + + if err != nil { + return dbErr(err) + } + } else { + rs, err := c.DBX.ExecContext( + ctx, + "INSERT INTO color_preset (name, hue, saturation, kelvin) VALUES (?, ?, ?, ?)", + preset.Name, preset.Value.Hue, preset.Value.Saturation, preset.Value.Kelvin, + ) + + if err != nil { + return dbErr(err) + } + + id, err := rs.LastInsertId() + if err != nil { + return dbErr(err) + } + + preset.ID = int(id) + } + + return nil +} + +func (c *ColorPresetRepo) Delete(ctx context.Context, preset *models.ColorPreset) error { + _, err := c.DBX.ExecContext(ctx, "DELETE FROM color_preset WHERE id = ?", preset.ID) + if err != nil { + return dbErr(err) + } + + preset.ID = 0 + return nil +} + +func (c *ColorPresetRepo) fromRecords(records ...presetRecord) []models.ColorPreset { + newList := make([]models.ColorPreset, len(records), len(records)) + for i, record := range records { + newList[i] = models.ColorPreset{ + ID: record.ID, + Name: record.Name, + Value: models.ColorValue{ + Hue: record.Hue, + Saturation: record.Saturation, + Kelvin: record.Kelvin, + }, + } + } + + return newList +} diff --git a/models/colorpreset.go b/models/colorpreset.go new file mode 100644 index 0000000..c2b3f71 --- /dev/null +++ b/models/colorpreset.go @@ -0,0 +1,26 @@ +package models + +import ( + "context" + "strings" +) + +type ColorPreset struct { + ID int `json:"id"` + Name string `json:"name"` + Value ColorValue `json:"value"` +} + +type ColorPresetRepository interface { + Find(ctx context.Context, id int) (ColorPreset, error) + FetchAll(ctx context.Context) ([]ColorPreset, error) + Save(ctx context.Context, preset *ColorPreset) error + Delete(ctx context.Context, preset *ColorPreset) error +} + +func (c *ColorPreset) Validate() { + c.Name = strings.Trim(c.Name, " \t\n") + if len(c.Name) == 0 { + c.Name = c.Value.String() + } +} diff --git a/scripts/20210523175111_color_preset.sql b/scripts/20210523175111_color_preset.sql new file mode 100644 index 0000000..108d9e6 --- /dev/null +++ b/scripts/20210523175111_color_preset.sql @@ -0,0 +1,17 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE color_preset +( + id INT NOT NULL AUTO_INCREMENT, + hue DOUBLE NOT NULL, + saturation DOUBLE NOT NULL, + kelvin INT NOT NULL, + + PRIMARY KEY (id) +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE color_preset; +-- +goose StatementEnd diff --git a/scripts/20210523175425_color_preset_name.sql b/scripts/20210523175425_color_preset_name.sql new file mode 100644 index 0000000..8b7ead9 --- /dev/null +++ b/scripts/20210523175425_color_preset_name.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE color_preset + ADD COLUMN name VARCHAR(255) NOT NULL DEFAULT 'Unnamed'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE color_preset + DROP COLUMN name; +-- +goose StatementEnd From e1f38e3dcdcd3bea561903781f3f851fe953a25b Mon Sep 17 00:00:00 2001 From: Stian Fredrik Aune Date: Sun, 23 May 2021 23:51:31 +0200 Subject: [PATCH 3/3] color picker component --- webui/package.json | 2 ++ webui/src/App.tsx | 7 +++++- webui/src/primitives/Forms.tsx | 43 +++++++++++++++++++++++++++++++++ webui/src/primitives/Layout.tsx | 2 +- webui/yarn.lock | 23 ++++++++++++++++++ 5 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 webui/src/primitives/Forms.tsx diff --git a/webui/package.json b/webui/package.json index 3ecf9a0..8147c84 100644 --- a/webui/package.json +++ b/webui/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@jaames/iro": "^5.5.1", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", @@ -15,6 +16,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-scripts": "4.0.3", + "react-semantic-ui-range": "^0.7.1", "typescript": "^4.1.2", "web-vitals": "^1.0.1" }, diff --git a/webui/src/App.tsx b/webui/src/App.tsx index e137f40..c6a00d1 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -1,9 +1,14 @@ import React from 'react'; import {HookRouter, navigate, usePath, useRoutes} from "hookrouter"; import {Tabs} from "./primitives/Layout"; +import {HSColorPicker} from "./primitives/Forms"; const routeObj: HookRouter.RouteObject = { - "/": () =>
1
, + "/": () => ( +
+ void(0)}/> +
+ ), "/devices": () =>
2
, "/settings": () =>
3
, } diff --git a/webui/src/primitives/Forms.tsx b/webui/src/primitives/Forms.tsx new file mode 100644 index 0000000..1a512d6 --- /dev/null +++ b/webui/src/primitives/Forms.tsx @@ -0,0 +1,43 @@ +import React, {useLayoutEffect, useState} from 'react'; +// @ts-ignore +import iro from "@jaames/iro"; + +interface ColorPickerProps { + h: number + s: number + onChange: (h: number, v: number) => void +} + +const randomId = () => Math.floor(Math.random() * 100000); + +export const HSColorPicker: React.FC = ({h, s, onChange}) => { + const [random] = useState(() => `color-picker-${randomId()}`); + + useLayoutEffect(() => { + // @ts-ignore + const colorPicker = new iro.ColorPicker(`#${random}`, { + color: {h, s: s * 100, v: 255}, + layout: [ + { + component: iro.ui.Wheel, + options: {} + } + ], + }); + + colorPicker.on("input:end", (color: { hsv: { h: number, s: number } }) => { + onChange(color.hsv.h || 0, (color.hsv.s || 0) / 100); + }); + + return () => { + const elem = document.getElementById(`color-picker-${random}`); + if (elem === null) { + return; + } + + elem.innerHTML = ""; + }; + }, [h, s, onChange, random]); + + return
; +}; diff --git a/webui/src/primitives/Layout.tsx b/webui/src/primitives/Layout.tsx index c6a700e..a243f65 100644 --- a/webui/src/primitives/Layout.tsx +++ b/webui/src/primitives/Layout.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo} from "react"; +import React, {useCallback, useEffect} from "react"; import "./Layout.sass"; interface TabsProps { diff --git a/webui/yarn.lock b/webui/yarn.lock index 2cf11df..0007f81 100644 --- a/webui/yarn.lock +++ b/webui/yarn.lock @@ -1209,6 +1209,11 @@ dependencies: "@hapi/hoek" "^8.3.0" +"@irojs/iro-core@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@irojs/iro-core/-/iro-core-1.2.0.tgz#3587c2db7a6de09f76dbf75b94605ac251039ca8" + integrity sha512-RVxd4lEx3KPXRBYo7urLSr9C3PJsm3t9AgPQppP5LDqQcMrXCXcZoeneMtQndMk60k72NziNiPpbZlZIvcT4VQ== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -1225,6 +1230,14 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== +"@jaames/iro@^5.5.1": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@jaames/iro/-/iro-5.5.1.tgz#1935713fe3cca8c4eadcfc6013b6f846ac8be097" + integrity sha512-HR2phfXjEINFKXxEdbGqp9/MfHfQ/xP+nLi2gjNV4RHB3tBOgHMpS1WFMU7fp1vl//vWXxKxVKa91D+5JoFgMA== + dependencies: + "@irojs/iro-core" "^1.2.0" + preact "^10.0.0" + "@jest/console@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" @@ -9070,6 +9083,11 @@ postcss@^8.1.0: nanoid "^3.1.20" source-map "^0.6.1" +preact@^10.0.0: + version "10.5.13" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.13.tgz#85f6c9197ecd736ce8e3bec044d08fd1330fa019" + integrity sha512-q/vlKIGNwzTLu+jCcvywgGrt+H/1P/oIRSD6mV4ln3hmlC+Aa34C7yfPI4+5bzW8pONyVXYS7SvXosy2dKKtWQ== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -9449,6 +9467,11 @@ react-scripts@4.0.3: optionalDependencies: fsevents "^2.1.3" +react-semantic-ui-range@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/react-semantic-ui-range/-/react-semantic-ui-range-0.7.1.tgz#bc552f889e4243d500d4673dd7966752664935fe" + integrity sha512-6AQLuFeARHcIO7yl4Pd0BBIB9sJyeNK1mku7luK6aTXrj9EGaqr1fWS7w4b0+GUP7CvkixmcyYqykxzpKS4Lcw== + react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"