diff --git a/Makefile b/Makefile index f14354ad..28df614e 100644 --- a/Makefile +++ b/Makefile @@ -271,11 +271,15 @@ release: clean echo "BASE=__yao_admin_root" > .tmp/xgen/v1.0/packages/xgen/.env cd .tmp/xgen/v1.0 && pnpm install && pnpm run build +# Setup UI + cd .tmp/xgen/v1.0/packages/setup && pnpm install && pnpm run build + # Packing mkdir -p .tmp/data/xgen cp -r ./ui .tmp/data/ui cp -r ./yao .tmp/data/yao cp -r .tmp/xgen/v0.9/dist .tmp/data/xgen/v0.9 + cp -r .tmp/xgen/v1.0/packages/xgen/build .tmp/data/xgen/setup cp -r .tmp/xgen/v1.0/packages/xgen/dist .tmp/data/xgen/v1.0 go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/... rm -rf .tmp/data diff --git a/cmd/start.go b/cmd/start.go index 8c5449af..b6e7dc60 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -22,6 +22,7 @@ import ( "github.com/yaoapp/yao/engine" "github.com/yaoapp/yao/fs" "github.com/yaoapp/yao/service" + "github.com/yaoapp/yao/setup" "github.com/yaoapp/yao/share" "github.com/yaoapp/yao/studio" ) @@ -35,6 +36,19 @@ var startCmd = &cobra.Command{ Long: L("Start Engine"), Run: func(cmd *cobra.Command, args []string) { + // Setup + if setup.Check() { + go setup.Start() + select { + case <-setup.Done: + setup.Stop() + break + case <-setup.Canceled: + os.Exit(1) + break + } + } + // recive interrupt signal interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) @@ -58,10 +72,6 @@ var startCmd = &cobra.Command{ } host := config.Conf.Host - if host == "0.0.0.0" { - host = "127.0.0.1" - } - dataRoot, _ := fs.Root(config.Conf) fmt.Println(color.WhiteString("\n---------------------------------")) @@ -73,15 +83,29 @@ var startCmd = &cobra.Command{ } if share.App.XGen == "1.0" { + root, _ := adminRoot() - fmt.Println(color.WhiteString(L(" XGen")), color.GreenString(" 1.0")) + urls := []string{fmt.Sprintf("http://%s:%s", host, port)} + if host == "0.0.0.0" { + urls, _ = setup.URLs(config.Conf) + } + + fmt.Println(color.WhiteString(L("XGen")), color.GreenString(" 1.0")) fmt.Println(color.WhiteString(L("Data")), color.GreenString(" %s", dataRoot)) - fmt.Println(color.WhiteString(L("Frontend")), color.GreenString(" http://%s%s/", host, port)) - fmt.Println(color.WhiteString(L("Dashboard")), color.GreenString(" http://%s%s%slogin/admin", host, port, root)) - fmt.Println(color.WhiteString(L("API")), color.GreenString(" http://%s%s/api", host, port)) + for _, url := range urls { + fmt.Println(color.WhiteString(L("Frontend ")), color.GreenString(" %s", url)) + fmt.Println(color.WhiteString(L("Dashboard")), color.GreenString(" %s/%s/login/admin", url, strings.Trim(root, "/"))) + fmt.Println(color.WhiteString(L("API ")), color.GreenString(" %s/api", url)) + } + fmt.Println(color.WhiteString(L("Listening")), color.GreenString(" %s:%d", config.Conf.Host, config.Conf.Port)) } else { + + if host == "0.0.0.0" { + host = "127.0.0.1" + } + fmt.Println(color.WhiteString(L("Data")), color.GreenString(" %s", dataRoot)) fmt.Println(color.WhiteString(L("Frontend")), color.GreenString(" http://%s%s/", host, port)) fmt.Println(color.WhiteString(L("Dashboard")), color.GreenString(" http://%s%s/xiang/login/admin", host, port)) diff --git a/config/config.go b/config/config.go index 04381c39..66436838 100644 --- a/config/config.go +++ b/config/config.go @@ -40,13 +40,15 @@ func LoadFrom(envfile string) Config { file, err := filepath.Abs(envfile) if err != nil { - log.Warn("Can't load env file. %s", err.Error()) - } - err = godotenv.Overload(file) - if err != nil { - log.Warn("Can't load env file. %s", err.Error()) + // log.Warn("Can't load env file. %s", err.Error()) + return Load() } + godotenv.Overload(file) + // if err != nil { + // // log.Warn("Can't load env file. %s", err.Error()) + // } + return Load() } diff --git a/data/bindata.go b/data/bindata.go index 8d558d21..be045306 100644 --- a/data/bindata.go +++ b/data/bindata.go @@ -1,6 +1,7 @@ // Code generated for package data by go-bindata DO NOT EDIT. (@generated) // sources: // .tmp/data/ui/index.html +// .tmp/data/xgen/setup/index.html // .tmp/data/xgen/v0.9/index.html // .tmp/data/xgen/v1.0/index.html // .tmp/data/xgen/v1.0/layouts__index.async.js @@ -216,7 +217,27 @@ func uiIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _xgenSetupIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8a\x70\x77\xf5\x53\x08\x76\x0d\x09\x0d\xe0\x02\x04\x00\x00\xff\xff\xdb\x53\x4c\xdc\x0b\x00\x00\x00") + +func xgenSetupIndexHtmlBytes() ([]byte, error) { + return bindataRead( + _xgenSetupIndexHtml, + "xgen/setup/index.html", + ) +} + +func xgenSetupIndexHtml() (*asset, error) { + bytes, err := xgenSetupIndexHtmlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "xgen/setup/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -236,7 +257,7 @@ func xgenV09IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "xgen/v0.9/index.html", size: 14, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "xgen/v0.9/index.html", size: 14, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -256,7 +277,7 @@ func xgenV10IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "xgen/v1.0/index.html", size: 50, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "xgen/v1.0/index.html", size: 50, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -276,7 +297,7 @@ func xgenV10Layouts__indexAsyncJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "xgen/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "xgen/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -296,7 +317,7 @@ func xgenV10UmiJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "xgen/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "xgen/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -316,7 +337,7 @@ func yaoApisReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/apis/README.md", size: 13, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/apis/README.md", size: 13, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -336,7 +357,7 @@ func yaoApisChartHttpJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/apis/chart.http.json", size: 624, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/apis/chart.http.json", size: 624, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -356,7 +377,7 @@ func yaoApisImportHttpJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/apis/import.http.json", size: 1676, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/apis/import.http.json", size: 1676, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -376,7 +397,7 @@ func yaoApisPageHttpJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/apis/page.http.json", size: 609, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/apis/page.http.json", size: 609, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -396,7 +417,7 @@ func yaoApisStorageHttpJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/apis/storage.http.json", size: 1026, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/apis/storage.http.json", size: 1026, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -416,7 +437,7 @@ func yaoApisTableHttpJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/apis/table.http.json", size: 3213, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/apis/table.http.json", size: 3213, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -436,7 +457,7 @@ func yaoApisUserHttpJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/apis/user.http.json", size: 1453, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/apis/user.http.json", size: 1453, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -456,7 +477,7 @@ func yaoApisXiangHttpJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/apis/xiang.http.json", size: 1239, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/apis/xiang.http.json", size: 1239, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -476,7 +497,7 @@ func yaoDataAppJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/app.json", size: 277, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/data/app.json", size: 277, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -496,7 +517,7 @@ func yaoDataIcons404Png() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -516,7 +537,7 @@ func yaoDataIconsIconIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -536,7 +557,7 @@ func yaoDataIconsIconIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -556,7 +577,7 @@ func yaoDataIconsIconPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -576,7 +597,7 @@ func yaoDataIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -596,7 +617,7 @@ func yaoFieldsModelTransJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14777, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14777, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -616,7 +637,7 @@ func yaoFlowsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/flows/README.md", size: 12, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/flows/README.md", size: 12, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -636,7 +657,7 @@ func yaoFlowsAdminGuardFlowJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/flows/admin/guard.flow.json", size: 559, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/flows/admin/guard.flow.json", size: 559, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -656,7 +677,7 @@ func yaoFlowsMenuFlowJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/flows/menu.flow.json", size: 1045, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/flows/menu.flow.json", size: 1045, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -676,7 +697,7 @@ func yaoFlowsUserFindFlowJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/flows/user/find.flow.json", size: 328, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/flows/user/find.flow.json", size: 328, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -696,7 +717,7 @@ func yaoLangsEnUsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -716,7 +737,7 @@ func yaoLangsZhCnGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1629, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1629, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -736,7 +757,7 @@ func yaoLangsZhCnLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -756,7 +777,7 @@ func yaoLangsZhCnLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -776,7 +797,7 @@ func yaoLangsZhHkGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1629, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1629, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -796,7 +817,7 @@ func yaoLangsZhHkLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -816,7 +837,7 @@ func yaoLangsZhHkLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -836,7 +857,7 @@ func yaoModelsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/README.md", size: 15, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/models/README.md", size: 15, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -856,7 +877,7 @@ func yaoModelsMenuModJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/menu.mod.json", size: 2378, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/models/menu.mod.json", size: 2378, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -876,7 +897,7 @@ func yaoModelsUserModJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user.mod.json", size: 8826, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/models/user.mod.json", size: 8826, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -896,7 +917,7 @@ func yaoModelsWorkflowModJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/workflow.mod.json", size: 3333, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/models/workflow.mod.json", size: 3333, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -916,7 +937,7 @@ func yaoTablesReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/tables/README.md", size: 15, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/tables/README.md", size: 15, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -936,7 +957,7 @@ func yaoTablesMenuTabJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/tables/menu.tab.json", size: 10471, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/tables/menu.tab.json", size: 10471, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -956,7 +977,7 @@ func yaoTablesUserTabJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/tables/user.tab.json", size: 2357, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)} + info := bindataFileInfo{name: "yao/tables/user.tab.json", size: 2357, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1014,6 +1035,7 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "ui/index.html": uiIndexHtml, + "xgen/setup/index.html": xgenSetupIndexHtml, "xgen/v0.9/index.html": xgenV09IndexHtml, "xgen/v1.0/index.html": xgenV10IndexHtml, "xgen/v1.0/layouts__index.async.js": xgenV10Layouts__indexAsyncJs, @@ -1100,6 +1122,9 @@ var _bintree = &bintree{nil, map[string]*bintree{ "index.html": {uiIndexHtml, map[string]*bintree{}}, }}, "xgen": {nil, map[string]*bintree{ + "setup": {nil, map[string]*bintree{ + "index.html": {xgenSetupIndexHtml, map[string]*bintree{}}, + }}, "v0.9": {nil, map[string]*bintree{ "index.html": {xgenV09IndexHtml, map[string]*bintree{}}, }}, diff --git a/data/data.go b/data/data.go index d49c82ce..886fe8da 100644 --- a/data/data.go +++ b/data/data.go @@ -33,6 +33,18 @@ func XgenV1() *assetfs.AssetFS { panic("unreachable") } +// Setup Setup ui +func Setup() *assetfs.AssetFS { + assetInfo := func(path string) (os.FileInfo, error) { + return os.Stat(path) + } + for k := range _bintree.Children { + k = "xgen/setup" + return &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: assetInfo, Prefix: k, Fallback: "index.html"} + } + panic("unreachable") +} + // ReplaceXGen bindata file func ReplaceXGen(search, replace string) error { err := replaceXGenIndex(search, replace) diff --git a/main_test.go b/main_test.go index 5d30f841..6cb2572c 100644 --- a/main_test.go +++ b/main_test.go @@ -53,7 +53,6 @@ func TestCommandStart(t *testing.T) { request := func() (maps.MapStr, error) { time.Sleep(time.Microsecond * 2000) url := fmt.Sprintf("http://%s:%d/api/user/find/1?select=id,name", "127.0.0.1", config.Conf.Port) - // utils.Dump(url) resp, err := http.Get(url) if err != nil { return nil, err diff --git a/service/middleware.go b/service/middleware.go index 89b378e5..6e3338f3 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -1,38 +1,17 @@ package service import ( - "fmt" - "net/http" - "path/filepath" "strings" "github.com/gin-gonic/gin" - "github.com/yaoapp/yao/config" - "github.com/yaoapp/yao/data" "github.com/yaoapp/yao/share" ) -// XGenFileServerV0 XGen v0.9 -var XGenFileServerV0 http.Handler = http.FileServer(data.XgenV0()) - -// XGenFileServerV1 XGen v1.0 -var XGenFileServerV1 http.Handler = http.FileServer(data.XgenV1()) - -// AppFileServer 应用静态文件 -var AppFileServer http.Handler = http.FileServer(http.Dir(filepath.Join(config.Conf.Root, "ui"))) - // Middlewares 服务中间件 var Middlewares = []gin.HandlerFunc{ - // BindDomain, BinStatic, } -// AdminRoot cache -var AdminRoot = "" - -// AdminRootLen cache -var AdminRootLen = 0 - // BinStatic 静态文件服务 func BinStatic(c *gin.Context) { @@ -44,10 +23,10 @@ func BinStatic(c *gin.Context) { return } else if share.App.XGen == "1.0" { + // Xgen 1.0 - adminRoot, adminRootLen := adminRoot() - if length >= adminRootLen && c.Request.URL.Path[0:adminRootLen] == adminRoot { - c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, c.Request.URL.Path[0:adminRootLen-1]) + if length >= AdminRootLen && c.Request.URL.Path[0:AdminRootLen] == AdminRoot { + c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, c.Request.URL.Path[0:AdminRootLen-1]) XGenFileServerV1.ServeHTTP(c.Writer, c.Request) c.Abort() return @@ -69,24 +48,7 @@ func BinStatic(c *gin.Context) { } - // 应用内静态文件目录(/ui) + // 应用内静态文件目录(/ui or public) AppFileServer.ServeHTTP(c.Writer, c.Request) c.Abort() } - -func adminRoot() (string, int) { - if AdminRoot != "" { - return AdminRoot, AdminRootLen - } - - adminRoot := "/yao/" - if share.App.AdminRoot != "" { - root := strings.TrimPrefix(share.App.AdminRoot, "/") - root = strings.TrimSuffix(root, "/") - adminRoot = fmt.Sprintf("/%s/", root) - } - adminRootLen := len(adminRoot) - AdminRoot = adminRoot - AdminRootLen = adminRootLen - return AdminRoot, AdminRootLen -} diff --git a/service/service.go b/service/service.go index cec86fbd..bc54eb4c 100644 --- a/service/service.go +++ b/service/service.go @@ -10,12 +10,13 @@ import ( ) var shutdown = make(chan bool, 1) + var shutdownComplete = make(chan bool, 1) // Start 启动服务 func Start() error { - err := share.SessionStart() + err := prepare() if err != nil { return err } @@ -84,3 +85,19 @@ func StopWithContext(ctx context.Context, onComplete func()) { onComplete() } } + +func prepare() error { + + // Session server + err := share.SessionStart() + if err != nil { + return err + } + + err = SetupStatic() + if err != nil { + return err + } + + return nil +} diff --git a/service/static.go b/service/static.go new file mode 100644 index 00000000..a13be588 --- /dev/null +++ b/service/static.go @@ -0,0 +1,126 @@ +package service + +import ( + "errors" + "fmt" + "io/fs" + "net/http" + "os" + "path" + "path/filepath" + "strings" + + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/data" + "github.com/yaoapp/yao/share" +) + +// AppFileServer static file server +var AppFileServer http.Handler + +// XGenFileServerV0 XGen v0.9 +var XGenFileServerV0 http.Handler = http.FileServer(data.XgenV0()) + +// XGenFileServerV1 XGen v1.0 +var XGenFileServerV1 http.Handler = http.FileServer(data.XgenV1()) + +// AdminRoot cache +var AdminRoot = "" + +// AdminRootLen cache +var AdminRootLen = 0 + +// Dir files +type Dir string + +// SetupStatic setup static file server +func SetupStatic() error { + + // SetAdmin Root + adminRoot() + + // Static file server + AppFileServer = http.FileServer(Dir(filepath.Join(config.Conf.Root, "public"))) + if share.App.XGen == "" || share.App.XGen == "0.9" { + AppFileServer = http.FileServer(Dir(filepath.Join(config.Conf.Root, "ui"))) + } + + return nil +} + +// Open implements FileSystem using os.Open, opening files for reading rooted +// and relative to the directory d. +func (d Dir) Open(name string) (http.File, error) { + if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) { + return nil, errors.New("http: invalid character in file path") + } + + dir := string(d) + if dir == "" { + dir = "." + } + + fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))) + + // Close dir views Disable directory listing + stat, err := os.Stat(fullName) + if err != nil { + return nil, mapOpenError(err, fullName, filepath.Separator, os.Stat) + } + + if stat.IsDir() { + indexFile := filepath.Join(fullName, "index.html") + if _, err := os.Stat(indexFile); os.IsNotExist(err) { + return nil, mapOpenError(fs.ErrNotExist, fullName, filepath.Separator, os.Stat) + } + } + + f, err := os.Open(fullName) + if err != nil { + return nil, mapOpenError(err, fullName, filepath.Separator, os.Stat) + } + + return f, nil +} + +// mapOpenError maps the provided non-nil error from opening name +// to a possibly better non-nil error. In particular, it turns OS-specific errors +// about opening files in non-directories into fs.ErrNotExist. See Issues 18984 and 49552. +func mapOpenError(originalErr error, name string, sep rune, stat func(string) (fs.FileInfo, error)) error { + if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) { + return originalErr + } + + parts := strings.Split(name, string(sep)) + for i := range parts { + if parts[i] == "" { + continue + } + fi, err := stat(strings.Join(parts[:i+1], string(sep))) + if err != nil { + return originalErr + } + if !fi.IsDir() { + return fs.ErrNotExist + } + } + return originalErr +} + +// SetupAdmin setup admin static root +func adminRoot() (string, int) { + if AdminRoot != "" { + return AdminRoot, AdminRootLen + } + + adminRoot := "/yao/" + if share.App.AdminRoot != "" { + root := strings.TrimPrefix(share.App.AdminRoot, "/") + root = strings.TrimSuffix(root, "/") + adminRoot = fmt.Sprintf("/%s/", root) + } + adminRootLen := len(adminRoot) + AdminRoot = adminRoot + AdminRootLen = adminRootLen + return AdminRoot, AdminRootLen +} diff --git a/service/watch.go b/service/watch.go index 6d3ecafe..fd573e1a 100644 --- a/service/watch.go +++ b/service/watch.go @@ -215,14 +215,14 @@ func watchModel(root string, file string, event string, cfg config.Config) { fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error())) return } - mod, err := gou.LoadModelReturn(string(content), name) + _, err = gou.LoadModelReturn(string(content), name) if err != nil { fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error())) return } - mod.Migrate(true) - fmt.Println(color.GreenString("[Watch] Model: %s Created", name)) + // mod.Migrate(true) + fmt.Println(color.GreenString("[Watch] Model: %s Created (Please run yao migrate manually)", name)) break case "WRITE": @@ -231,13 +231,13 @@ func watchModel(root string, file string, event string, cfg config.Config) { fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error())) return } - mod, err := gou.LoadModelReturn(string(content), name) + _, err = gou.LoadModelReturn(string(content), name) if err != nil { fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error())) return } - mod.Migrate(false) - fmt.Println(color.GreenString("[Watch] Model: %s Reloaded", name)) + // mod.Migrate(false) + fmt.Println(color.GreenString("[Watch] Model: %s Reloaded (Please run yao migrate manually)", name)) break case "REMOVE", "RENAME": diff --git a/setup/check.go b/setup/check.go new file mode 100644 index 00000000..9ff17202 --- /dev/null +++ b/setup/check.go @@ -0,0 +1,105 @@ +package setup + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/yaoapp/gou/connector" + "github.com/yaoapp/yao/config" +) + +// Check setup programe +func Check() bool { + + root := appRoot() + + appfile := filepath.Join(root, "app.json") + if _, err := os.Stat(appfile); err != nil && os.IsNotExist(err) { + return true + } + + envfile := filepath.Join(root, ".env") + if _, err := os.Stat(envfile); err != nil && os.IsNotExist(err) { + cfg, err := getConfig() + if err != nil || !hasInstalled(cfg) { + return true + } + } + + return false +} + +// Validate db link +func Validate() (err error) { + + root := appRoot() + path := filepath.Join(root, "db", "yao.db") + + data := []byte(fmt.Sprintf(`{ + "type": "sqlite3", + "options": { + "file": "%s" + } + }`, path)) + + data = []byte(`{ + "type": "mysql", + "options": { + "db": "test", + "hosts": [{ "host": "127.0.0.1", "user":"root", "pass":"123456" }] + } + }`) + + _, err = connector.Load(string(data), "test") + return err + +} + +func appRoot() string { + + root := os.Getenv("YAO_ROOT") + if root == "" { + path, err := os.Getwd() + if err != nil { + printError("无法获取应用目录: %s", err) + Stop() + } + root = path + } + + root, err := filepath.Abs(root) + if err != nil { + printError("无法获取应用目录: %s", err) + Stop() + } + + return root +} + +func getConfig() (config.Config, error) { + root := appRoot() + envfile := filepath.Join(root, ".env") + cfg := config.LoadFrom(envfile) + return cfg, nil +} + +func hasInstalled(cfg config.Config) bool { + + switch cfg.DB.Driver { + + case "sqlite3": + if cfg.DB.Primary != nil { + return true + } + break + + case "mysql": + if cfg.DB.Primary != nil { + return true + } + break + } + + return false +} diff --git a/setup/check_test.go b/setup/check_test.go new file mode 100644 index 00000000..df67d09a --- /dev/null +++ b/setup/check_test.go @@ -0,0 +1,14 @@ +package setup + +import ( + "testing" +) + +func TestValidate(t *testing.T) { + // err := Validate() + // fmt.Println(err) + + // if err != nil { + // t.Fatal(err) + // } +} diff --git a/setup/env.go b/setup/env.go new file mode 100644 index 00000000..aeb17558 --- /dev/null +++ b/setup/env.go @@ -0,0 +1,65 @@ +package setup + +import ( + "os" + "path/filepath" + + "github.com/joho/godotenv" +) + +func envSet(file, name, value string) error { + + file, err := filepath.Abs(file) + if err != nil { + dir := filepath.Dir(file) + err := os.MkdirAll(dir, os.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + + } + + if _, err := os.Stat(file); err != nil && os.IsNotExist(err) { + err = os.WriteFile(file, []byte{}, 0644) + if err != nil { + return err + } + + err = godotenv.Write(map[string]string{"YAO_ENV": "development"}, file) + if err != nil { + return err + } + } + + vars, err := godotenv.Read(file) + if err != nil { + return err + } + + vars[name] = value + return godotenv.Write(vars, file) +} + +func envGet(file, name string) (string, error) { + + v := os.Getenv(name) + if v != "" { + return v, nil + } + + vars, err := godotenv.Read(file) + if err == nil { + return "", err + } + return vars[name], nil +} + +func envHas(file, name string) (bool, error) { + + v, err := envGet(file, name) + if err != nil { + return false, nil + } + + return v != "", nil +} diff --git a/setup/handler.go b/setup/handler.go new file mode 100644 index 00000000..257975ad --- /dev/null +++ b/setup/handler.go @@ -0,0 +1,115 @@ +package setup + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/xun" +) + +// recovered custom recovered +func recovered(c *gin.Context, recovered interface{}) { + + var code = http.StatusInternalServerError + + if err, ok := recovered.(string); ok { + c.JSON(code, xun.R{ + "code": code, + "message": fmt.Sprintf("%s", err), + }) + } else if err, ok := recovered.(exception.Exception); ok { + code = err.Code + c.JSON(code, xun.R{ + "code": code, + "message": err.Message, + }) + } else if err, ok := recovered.(*exception.Exception); ok { + code = err.Code + c.JSON(code, xun.R{ + "code": code, + "message": err.Message, + }) + } else { + c.JSON(code, xun.R{ + "code": code, + "message": fmt.Sprintf("%v", recovered), + }) + } + + c.AbortWithStatus(code) +} + +// { +// "env": { +// "YAO_LANG": "中文", +// "YAO_ENV": "开发模式(推荐)", +// "YAO_PORT": "5099", +// "YAO_STUDIO_PORT": "5077" +// }, +// "db": { +// "type": "sqlite", +// "option.file": "db/yao.db" +// } +// } +func runSetup(c *gin.Context) { + + payload := getPayload(c) + + cfg, err := getConfig() + if err != nil { + c.JSON(500, gin.H{"code": 500, "message": err.Error()}) + return + } + + if hasInstalled(cfg) { + c.JSON(403, gin.H{"code": 400, "message": "应用已安装, 删除 .env 文件后重试"}) + return + } + + err = Install(payload) + if err != nil { + c.JSON(500, gin.H{"code": 500, "message": err.Error()}) + return + } + + // Reload Config + cfg, err = getConfig() + if err != nil { + c.JSON(500, gin.H{"code": 500, "message": err.Error()}) + return + } + + // Return + urls, err := AdminURL(cfg) + if err != nil { + c.JSON(500, gin.H{"code": 500, "message": err.Error()}) + return + } + + c.JSON(200, gin.H{ + "code": 200, + "message": "安装成功", + "urls": urls, + }) + + Complete() +} + +func runCheck(c *gin.Context) { + time.Sleep(2 * time.Second) + c.JSON(200, gin.H{"code": 200}) +} + +func getPayload(c *gin.Context) map[string]map[string]string { + var payload map[string]map[string]string + err := c.ShouldBindJSON(&payload) + if err != nil { + c.JSON(500, gin.H{"code": 400, "message": err.Error()}) + return nil + } + + return payload +} diff --git a/setup/install.go b/setup/install.go new file mode 100644 index 00000000..1aa62d0e --- /dev/null +++ b/setup/install.go @@ -0,0 +1,1254 @@ +package setup + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/yaoapp/gou" + "github.com/yaoapp/gou/fs" + "github.com/yaoapp/gou/fs/dsl" + "github.com/yaoapp/gou/fs/system" + "github.com/yaoapp/kun/log" + "github.com/yaoapp/yao/engine" + "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/widgets/app" +) + +// Install app install +// +// { +// "env": { +// "YAO_LANG": "中文", +// "YAO_ENV": "开发模式(推荐)", +// "YAO_PORT": "5099", +// "YAO_STUDIO_PORT": "5077" +// }, +// "db": { +// "type": "sqlite", +// "option.file": "db/yao.db" +// } +// } +// +// { +// "env": { +// "YAO_LANG": "中文", +// "YAO_ENV": "开发模式(推荐)", +// "YAO_PORT": "5099", +// "YAO_STUDIO_PORT": "5077" +// }, +// "db": { +// "type": "mysql", +// "option.db": "yao", +// "option.host.host": "127.0.0.1", +// "option.host.port": "3306", +// "option.host.user": "root", +// "option.host.pass": "123456" +// } +// } +func Install(option map[string]map[string]string) error { + + env, has := option["env"] + if !has { + return fmt.Errorf("缺少服务配置信息") + } + + db, has := option["db"] + if !has { + return fmt.Errorf("缺少数据库配置信息") + } + + if env["YAO_ENV"] == "开发模式(推荐)" { + env["YAO_ENV"] = "development" + } else { + env["YAO_ENV"] = "production" + } + + if env["YAO_LANG"] == "中文" { + env["YAO_LANG"] = "zh-cn" + } else { + env["YAO_LANG"] = "en-us" + } + + dbName := db["type"] + dbOption := map[string]string{} + switch dbName { + case "", "sqlite", "sqlite3": + dbName = "sqlite3" + dbOption["file"] = db["option.file"] + break + + case "mysql": + dbName = "mysql" + dbOption["db"] = db["option.db"] + dbOption["host"] = db["option.host.host"] + dbOption["port"] = db["option.host.port"] + dbOption["user"] = db["option.host.user"] + dbOption["pass"] = db["option.host.pass"] + break + } + + root := appRoot() + err := makeService(root, "0.0.0.0", env["YAO_PORT"], env["YAO_STUDIO_PORT"], env["YAO_LANG"]) + if err != nil { + return err + } + + err = makeDB(root, dbName, dbOption) + if err != nil { + return err + } + + err = makeSession(root) + if err != nil { + return err + } + + err = makeLog(root) + if err != nil { + return err + } + + err = makeDirs(root) + if err != nil { + return err + } + + err = makeDemoWidgets(root) + if err != nil { + return err + } + + err = makeMigrate(root) + if err != nil { + return err + } + + err = makeSetup(root) + if err != nil { + return err + } + + return nil +} + +func makeService(root string, host string, port string, studioPort string, lang string) error { + file := filepath.Join(root, ".env") + err := envSet(file, "YAO_HOST", host) + if err != nil { + return err + } + + err = envSet(file, "YAO_PORT", port) + if err != nil { + return err + } + + return envSet(file, "YAO_LANG", lang) +} + +func makeDB(root string, driver string, option map[string]string) error { + + if driver != "mysql" && driver != "sqlite3" { + return fmt.Errorf("数据库驱动应该为: mysql/sqlite3") + } + + file := filepath.Join(root, ".env") + + dsn := "" + switch driver { + case "mysql": + + db := "yao" + if v, has := option["db"]; has { + db = v + } + + host := "127.0.0.1" + if v, has := option["host"]; has { + host = v + } + + port := "3306" + if v, has := option["port"]; has { + port = v + } + + user := "root" + if v, has := option["user"]; has { + user = v + } + + pass := "" + if v, has := option["pass"]; has { + pass = v + } + + err := envSet(file, "YAO_DB_DRIVER", driver) + if err != nil { + return err + } + + dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, pass, host, port, db) + return envSet(file, "YAO_DB_PRIMARY", dsn) + + case "sqlite", "sqlite3": + + var err error + db := filepath.Join("db", "yao.db") + if v, has := option["db"]; has { + file = v + } + + if !strings.HasPrefix(db, "/") { + db = filepath.Join(root, db) + db, err = filepath.Abs(db) + if err != nil && !os.IsNotExist(err) { + return err + } + } + + dir := filepath.Dir(db) + err = os.MkdirAll(dir, os.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + + err = envSet(file, "YAO_DB_DRIVER", "sqlite3") + if err != nil { + return err + } + + return envSet(file, "YAO_DB_PRIMARY", db) + } + + return fmt.Errorf("数据库驱动应该为: mysql/sqlite3") +} + +func makeSession(root string) error { + file := filepath.Join(root, ".env") + if has, _ := envHas(file, "YAO_SESSION_STORE"); has { + return nil + } + + if has, _ := envHas(file, "YAO_SESSION_FILE"); has { + return nil + } + + err := os.MkdirAll(filepath.Join(root, "data"), os.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + + err = envSet(file, "YAO_SESSION_STORE", "file") + if err != nil { + return err + } + + ssfile := filepath.Join(root, "data", ".session") + return envSet(file, "YAO_SESSION_FILE", ssfile) +} + +func makeLog(root string) error { + file := filepath.Join(root, ".env") + if has, _ := envHas(file, "YAO_LOG_MODE"); has { + return nil + } + + if has, _ := envHas(file, "YAO_LOG"); has { + return nil + } + + err := os.MkdirAll(filepath.Join(root, "data"), os.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + + err = envSet(file, "YAO_LOG_MODE", "TEXT") + if err != nil { + return err + } + + logfile := filepath.Join(root, "logs", "application.log") + return envSet(file, "YAO_LOG", logfile) +} + +func makeDirs(root string) error { + + err := os.MkdirAll(filepath.Join(root, "public"), os.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + + err = os.MkdirAll(filepath.Join(root, "services"), os.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + + err = os.MkdirAll(filepath.Join(root, "studio"), os.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + + err = os.MkdirAll(filepath.Join(root, "logs"), os.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + + return nil +} + +func makeDemoWidgets(root string) error { + + dsl := dsl.New(root) + + // make app + ok, err := makeAppWidget(dsl, root) + if err != nil || !ok { + return err + } + + // make flows + err = makeMenuFlowWidget(dsl, root) + if err != nil { + return err + } + + // make logins + err = makeLoginWidget(dsl, root) + if err != nil { + return err + } + + // make models + err = makeModelWidget(dsl, root) + if err != nil { + return err + } + + // make tables & forms + err = makeTableFormWidget(dsl, root) + if err != nil { + return err + } + + // make chart + err = makeChartWidget(dsl, root) + if err != nil { + return err + } + + // make scripts + err = makeScript(root) + if err != nil { + return err + } + + // make scripts + err = makeLang(root) + if err != nil { + return err + } + + // make index page + err = makeIndexPage(root) + if err != nil { + return err + } + + return nil +} + +func makeAppWidget(dsl fs.FileSystem, root string) (bool, error) { + + file := filepath.Join("app.json") + if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists + return false, nil + } + + _, err := dsl.WriteFile(file, []byte(fmt.Sprintf(` + { + "xgen": "1.0", + "name": "::Demo Application", + "short": "::Demo", + "description": "::Another yao application", + "version": "%s", + "adminRoot": "admin", + "setup": "scripts.demo.Data", + "menu": { + "process": "flows.app.menu", + "args": ["demo"] + }, + "optional": { + "hideNotification": true, + "hideSetting": false + } + } + `, share.VERSION)), 0644) + + if err != nil { + return false, err + } + + return true, nil +} + +func makeMenuFlowWidget(dsl fs.FileSystem, root string) error { + + file := filepath.Join("flows", "app", "menu.flow.json") + if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists + return nil + } + + _, err := dsl.WriteFile(file, []byte(` + { + "name": "APP Menu", + "nodes": [], + "output": [ + { + "blocks": 0, + "icon": "icon-activity", + "id": 1, + "name": "图表", + "parent": null, + "path": "/x/Chart/dashboard", + "visible_menu": 0 + }, + { + "blocks": 0, + "icon": "icon-book", + "id": 2, + "name": "表格", + "parent": null, + "path": "/x/Table/pet", + "visible_menu": 1, + "children": [ + { + "blocks": 0, + "icon": "icon-book", + "name": "宠物列表", + "id": 2010, + "parent": 2, + "path": "/x/Table/pet", + "visible_menu": 1 + } + ] + }, + { + "blocks": 0, + "icon": "icon-clipboard", + "id": 2, + "name": "表单", + "parent": null, + "path": "/x/Form/pet/1/edit", + "visible_menu": 1, + "children": [ + { + "blocks": 0, + "icon": "icon-clipboard", + "name": "编辑模式", + "id": 2010, + "parent": 2, + "path": "/x/Form/pet/1/edit", + "visible_menu": 1 + }, + { + "blocks": 0, + "icon": "icon-clipboard", + "name": "查看模式", + "id": 2010, + "parent": 2, + "path": "/x/Form/pet/1/view", + "visible_menu": 1 + } + ] + } + ] + } + `), 0644) + + if err != nil { + return err + } + + return nil +} + +func makeLoginWidget(dsl fs.FileSystem, root string) error { + file := filepath.Join("logins", "admin.login.json") + if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists + return nil + } + _, err := dsl.WriteFile(file, []byte(` + { + "name": "::Admin Login", + "action": { + "process": "yao.login.Admin", + "args": [":payload"] + }, + "layout": { + "entry": "/x/Chart/dashboard", + "slogan": "::Make Your Dream With Yao App Engine", + "site": "https://yaoapps.com?from=instance-admin-login" + } + } + `), 0644) + + if err != nil { + return err + } + + file = filepath.Join("logins", "user.login.json") + if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists + return nil + } + _, err = dsl.WriteFile(file, []byte(` + { + "name": "::User Login", + "action": { + "process": "scripts.user.Login", + "args": [":payload"] + }, + "layout": { + "entry": "/x/Table/pet", + "slogan": "::Make Your Dream With Yao App Engine", + "site": "https://yaoapps.com/from=instance-user-login" + } + } + `), 0644) + if err != nil { + return err + } + + return nil +} + +func makeModelWidget(dsl fs.FileSystem, root string) error { + file := filepath.Join("models", "pet.mod.json") + if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists + return nil + } + _, err := dsl.WriteFile(file, []byte(` + { + "name": "Pet", + "table": { "name": "pet", "comment": "宠物表" }, + "columns": [ + { "name": "id", "comment": "ID", "type": "ID" }, + { + "name": "name", + "comment": "昵称", + "type": "string", + "length": 80, + "index": true, + "nullable": true + }, + { + "name": "type", + "comment": "类型", + "type": "enum", + "option": ["cat", "dog", "others"], + "index": true + }, + { + "name": "status", + "comment": "入院状态", + "type": "enum", + "option": ["checked", "curing", "cured"], + "index": true + }, + { + "name": "mode", + "comment": "状态", + "type": "enum", + "option": ["enabled", "disabled"], + "index": true + }, + { + "name": "online", + "comment": "是否在线", + "type": "boolean", + "default": false, + "index": true + }, + { + "name": "curing_status", + "comment": "治疗状态", + "type": "enum", + "default": "0", + "option": ["0", "1", "2"], + "index": true + }, + { + "name": "stay", + "comment": "入院时间", + "type": "integer" + }, + { + "name": "cost", + "comment": "花费", + "type": "integer" + }, + { + "name": "doctor_id", + "type": "integer", + "comment": "相关职工", + "nullable": true + }, + { + "name": "images", + "type": "json", + "comment": "相关图片", + "nullable": true + }, + { + "name": "test_string", + "comment": "测试字段(字符串)", + "type": "string", + "nullable": true + }, + { + "name": "test_number", + "comment": "测试字段(数字)", + "type": "integer", + "nullable": true + }, + { + "name": "test_array", + "comment": "测试字段(数组)", + "type": "json", + "nullable": true + } + ], + "relations": {}, + "values": [], + "indexes": [], + "option": { "timestamps": true,"soft_deletes": true } + } + `), 0644) + + if err != nil { + return err + } + + return nil +} + +func makeTableFormWidget(dsl fs.FileSystem, root string) error { + file := filepath.Join("tables", "pet.tab.json") + if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists + return nil + } + _, err := dsl.WriteFile(file, []byte(` + { + "name": "::Pet Admin Bind Model", + "action": { + "bind": { "model": "pet", "option": {} } + } + } + `), 0644) + + if err != nil { + return err + } + + file = filepath.Join("forms", "pet.form.json") + if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists + return nil + } + _, err = dsl.WriteFile(file, []byte(` + { + "name": "::Pet Admin Bind Table", + "action": { + "bind": { "table": "pet", "option": {} } + } + } + `), 0644) + if err != nil { + return err + } + + return nil +} + +func makeChartWidget(dsl fs.FileSystem, root string) error { + + file := filepath.Join("charts", "dashboard.chart.json") + if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists + return nil + } + + _, err := dsl.WriteFile(file, []byte(` + { + "name": "宠物医院数据图表", + + "config":{"full":false}, + + "action": { + "before:data": "scripts.stat.BeforeData", + "data": { "process": "scripts.stat.Data", "default": ["2022-09-20"] }, + "after:data": "scripts.stat.AfterData" + }, + + "layout": { + "operation": { + "actions": [ + { + "title": "跳转至大屏", + "icon": "icon-airplay", + "action": { "Common.historyPush": { "pathname": "/x/Cool/demo" } } + } + ] + }, + + "filter": { + "columns": [{ "name": "时间区间", "width": 6 }] + }, + + "chart": { + "columns": [ + { "name": "宠物数量", "width": 6 }, + { "name": "宠物类型", "width": 6 }, + { "name": "当月收入", "width": 6 }, + { "name": "医师数量", "width": 6 }, + { "name": "宠物数量_上月", "width": 6 }, + { "name": "宠物类型_上月", "width": 6 }, + { "name": "当月收入_上月", "width": 6 }, + { "name": "医师数量_上月", "width": 6 }, + { "name": "收入", "width": 8 }, + { "name": "支出", "width": 8 }, + { "name": "综合评分", "width": 8 }, + { "name": "收入_折线图", "width": 8 }, + { "name": "支出_折线图", "width": 8 }, + { "name": "综合评分_折线图", "width": 8 }, + { "name": "类型排布", "width": 12 }, + { "name": "状态分布", "width": 12 }, + { "name": "综合消费", "width": 24 } + ] + } + }, + + "fields": { + "filter": { + "时间区间": { + "bind": "range", + "edit": { "type": "RangePicker", "props": {} } + }, + "状态": { + "bind": "status", + "edit": { + "type": "Select", + "props": { + "xProps": { + "$remote": { + "process": "models.pet.Get", + "query": { "select": "name,status", "limit": 2 } + } + } + } + } + } + }, + "chart": { + "收入": { + "bind": "income", + "link": "/x/Table/pet", + "out": "scripts.stat.Income", + "view": { + "type": "NumberChart", + "props": { + "chartHeight": 150, + "prefix": "¥", + "decimals": 2, + "nameKey": "date", + "valueKey": "value" + } + } + }, + "支出": { + "bind": "cost", + "link": "/x/Table/pet", + "view": { + "type": "NumberChart", + "props": { + "chartHeight": 150, + "color": "red", + "prefix": "¥", + "decimals": 2, + "nameKey": "date", + "valueKey": "value" + } + } + }, + "综合评分": { + "bind": "rate", + "link": "/x/Table/pet", + "view": { + "type": "NumberChart", + "props": { + "chartHeight": 150, + "color": "orange", + "unit": "分", + "decimals": 1, + "nameKey": "date", + "valueKey": "value" + } + } + }, + "收入_折线图": { + "bind": "income", + "link": "/x/Table/pet", + "view": { + "type": "NumberChart", + "props": { + "cardStyle": { "padding": 0 }, + "type": "line", + "chartHeight": 120, + "prefix": "¥", + "decimals": 2, + "nameKey": "date", + "valueKey": "value" + } + } + }, + "支出_折线图": { + "bind": "cost", + "link": "/x/Table/pet", + "view": { + "type": "NumberChart", + "props": { + "cardStyle": { "padding": 0 }, + "type": "line", + "chartHeight": 120, + "color": "red", + "prefix": "¥", + "decimals": 2, + "nameKey": "date", + "valueKey": "value" + } + } + }, + "综合评分_折线图": { + "bind": "rate", + "link": "/x/Table/pet", + "view": { + "type": "NumberChart", + "props": { + "cardStyle": { "padding": 0 }, + "type": "line", + "chartHeight": 120, + "color": "orange", + "unit": "分", + "decimals": 1, + "nameKey": "date", + "valueKey": "value" + } + } + }, + "宠物数量": { + "bind": "pet_count", + "link": "/x/Table/pet", + "view": { "type": "Number", "props": { "unit": "个" } } + }, + "宠物类型": { + "bind": "pet_type", + "view": { "type": "Number", "props": { "unit": "种" } } + }, + "当月收入": { + "bind": "income_monthly", + "view": { "type": "Number", "props": { "unit": "元" } } + }, + "医师数量": { + "bind": "doctor_count", + "view": { "type": "Number", "props": { "unit": "个" } } + }, + "宠物数量_上月": { + "bind": "prev_pet_count", + "link": "/x/Table/pet", + "view": { + "type": "Number", + "props": { + "cardStyle": { "padding": 0 }, + "unit": "个", + "prev_title": "上月数据" + } + } + }, + "宠物类型_上月": { + "bind": "prev_pet_type", + "link": "/x/Table/pet", + "view": { + "type": "Number", + "props": { + "cardStyle": { "padding": 0 }, + "unit": "种", + "prev_title": "上月数据" + } + } + }, + "当月收入_上月": { + "bind": "prev_income_monthly", + "link": "/x/Table/pet", + "view": { + "type": "Number", + "props": { + "cardStyle": { "padding": 0 }, + "unit": "元", + "prev_title": "上月数据" + } + } + }, + "医师数量_上月": { + "bind": "prev_doctor_count", + "link": "/x/Table/pet", + "view": { + "type": "Number", + "props": { + "cardStyle": { "padding": 0 }, + "unit": "个", + "prev_title": "上月数据" + } + } + }, + "类型排布": { + "bind": "datasource_type", + "link": "/x/Table/pet", + "view": { + "type": "Pie", + "props": { + "height": 240, + "nameKey": "type", + "series": [ + { + "valueKey": "count", + "roseType": "area", + "radius": [10, 100], + "center": ["60%", "50%"], + "itemStyle": { "borderRadius": 6 } + } + ] + } + }, + "refer": { + "type": "Table", + "props": { + "columns": [ + { "title": "类型", "dataIndex": "type" }, + { "title": "数量", "dataIndex": "count" } + ] + } + } + }, + "状态分布": { + "bind": "datasource_status", + "view": { + "type": "Bar", + "props": { + "height": 240, + "nameKey": "status", + "axisLabel": { "interval": 0, "fontSize": 12 }, + "series": [ + { + "valueKey": "count", + "type": "bar", + "colorBy": "data", + "itemStyle": { "borderRadius": 6 }, + "splitLine": { "show": false }, + "axisLabel": { "show": false } + } + ] + } + }, + "refer": { + "type": "Table", + "props": { + "columns": [ + { "title": "状态", "dataIndex": "status" }, + { "title": "数量", "dataIndex": "count" } + ] + } + } + }, + "综合消费": { + "bind": "datasource_cost", + "view": { + "type": "LineBar", + "props": { + "height": 240, + "nameKey": "name", + "axisLabel": { "interval": 0, "fontSize": 12 }, + "series": [ + { + "valueKey": "stay", + "type": "line", + "smooth": true, + "symbolSize": 8, + "itemStyle": { "borderRadius": 6 }, + "splitLine": { "show": false }, + "axisLabel": { "show": false } + }, + { + "valueKey": "cost", + "type": "bar", + "itemStyle": { "borderRadius": 6 }, + "splitLine": { "show": false }, + "axisLabel": { "show": false } + } + ] + } + } + } + } + } + } + `), 0644) + + if err != nil { + return err + } + + return nil +} + +func makeScript(root string) error { + + fs := system.New(filepath.Join(root, "scripts")) + file := "demo.js" + if _, err := os.Stat(filepath.Join(root, "scripts", file)); err == nil { // exists + return nil + } + _, err := fs.WriteFile(file, []byte(` +/** + * Demo Data + */ +function Data() { + return Process( + "yao.table.Insert", + "pet", + ["name", "type", "status", "mode", "stay", "cost", "doctor_id"], + [ + ["Cookie", "cat", "checked", "enabled", 200, 105, 1], + ["Baby", "dog", "checked", "enabled", 186, 24, 1], + ["Poo", "others", "checked", "enabled", 199, 66, 1], + ] + ); +} + `), 0644) + + if err != nil { + return err + } + + file = filepath.Join("stat.js") + if _, err := os.Stat(filepath.Join(root, "scripts", file)); err == nil { // exists + return nil + } + _, err = fs.WriteFile(file, []byte(` +/** + * before:data hook + * @param {*} params + * @returns + */ +function BeforeData(params) { + log.Info("[chart] before data hook: %s", JSON.stringify(params)); + return [params]; +} + +/** + * after:data hook + * @param {*} data + * @returns + */ +function AfterData(data) { + log.Info("[chart] after data hook: %s", JSON.stringify(data)); + return data; +} + +/** + * Get Data + * @param {*} params + */ +function Data(params) { + log.Info("[chart] process data query: %s", JSON.stringify(params)); + return { + income: [ + { value: 40300, date: "2022-1-1" }, + { value: 50800, date: "2022-2-1" }, + { value: 31300, date: "2022-3-1" }, + { value: 48800, date: "2022-4-1" }, + { value: 69900, date: "2022-5-1" }, + { value: 37800, date: "2022-6-1" }, + ], + cost: [ + { value: 28100, date: "2022-1-1" }, + { value: 23000, date: "2022-2-1" }, + { value: 29300, date: "2022-3-1" }, + { value: 26700, date: "2022-4-1" }, + { value: 26400, date: "2022-5-1" }, + { value: 31200, date: "2022-6-1" }, + ], + rate: [ + { value: 8.0, date: "2022-1-1" }, + { value: 7.6, date: "2022-2-1" }, + { value: 9.1, date: "2022-3-1" }, + { value: 8.4, date: "2022-4-1" }, + { value: 6.9, date: "2022-5-1" }, + { value: 9.0, date: "2022-6-1" }, + ], + pet_count: 54, + pet_type: 8, + income_monthly: 68900, + doctor_count: 23, + prev_pet_count: { current: 54, prev: 45 }, + prev_pet_type: { current: 8, prev: 13 }, + prev_income_monthly: { current: 68900, prev: 92000 }, + prev_doctor_count: { current: 23, prev: 27 }, + datasource_type: [ + { type: "猫猫", count: 18 }, + { type: "狗狗", count: 6 }, + { type: "其他", count: 3 }, + ], + datasource_status: [ + { status: "已查看", count: 3 }, + { status: "治疗中", count: 12 }, + { status: "已治愈", count: 9 }, + ], + datasource_cost: [ + { name: "毛毛", stay: 3, cost: 2000 }, + { name: "阿布", stay: 6, cost: 4200 }, + { name: "咪咪", stay: 7, cost: 6000 }, + { name: "狗蛋", stay: 1, cost: 1000 }, + ], + }; +} + +/** + * Compute out + * @param {*} field + * @param {*} value + * @param {*} data + * @returns + */ +function Income(field, value, data) { + log.Info( + "[chart] Income Compute: %s", + JSON.stringify({ field: field, value: value, data: data }) + ); + return value; +} + `), 0644) + if err != nil { + return err + } + + return nil +} + +func makeLang(root string) error { + fs := system.New(filepath.Join(root, "langs")) + file := filepath.Join("zh-cn", "global.yml") + if _, err := os.Stat(filepath.Join(root, "langs", file)); err == nil { // exists + return nil + } + + _, err := fs.WriteFile(file, []byte(` +Demo: 演示 +Demo Application: 示例应用 +Another yao application: 又一个 YAO 应用`), 0644) + + if err != nil { + return err + } + + file = filepath.Join("zh-hk", "langs", "global.yml") + if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists + return nil + } + _, err = fs.WriteFile(file, []byte(` +Demo: 演示 +Demo Application: 示例應用 +Another yao application: 又一個YAO應用`), 0644) + + if err != nil { + return err + } + + return nil +} + +func makeIndexPage(root string) error { + fs := system.New(filepath.Join(root, "public")) + file := "index.html" + if _, err := os.Stat(filepath.Join(root, "public", file)); err == nil { // exists + return nil + } + + _, err := fs.WriteFile(file, []byte(` +It works!`), 0644) + + if err != nil { + return err + } + + return nil +} + +func makeMigrate(root string) error { + + cfg, err := getConfig() + if err != nil { + return err + } + + err = engine.Load(cfg) + if err != nil { + return err + } + + // Do Stuff Here + for _, mod := range gou.Models { + has, err := mod.HasTable() + if err != nil { + return err + } + + if has { + log.Warn("%s (%s) table already exists", mod.ID, mod.MetaData.Table.Name) + continue + } + + err = mod.Migrate(false) + if err != nil { + return err + } + } + + return nil +} + +func makeSetup(root string) error { + + cfg, err := getConfig() + if err != nil { + return err + } + + err = engine.Load(cfg) + if err != nil { + return err + } + + if app.Setting != nil && app.Setting.Setup != "" { + + p, err := gou.ProcessOf(app.Setting.Setup) + if err != nil { + return err + } + _, err = p.Exec() + if err != nil { + return err + } + } + + return nil +} diff --git a/setup/install_test.go b/setup/install_test.go new file mode 100644 index 00000000..2d96ecb6 --- /dev/null +++ b/setup/install_test.go @@ -0,0 +1,12 @@ +package setup + +import ( + "testing" +) + +func TestInstall(t *testing.T) { + // err := Install() + // if err != nil { + // t.Fatal(err) + // } +} diff --git a/setup/setup.go b/setup/setup.go new file mode 100644 index 00000000..5af2f869 --- /dev/null +++ b/setup/setup.go @@ -0,0 +1,230 @@ +package setup + +import ( + "fmt" + "net" + "net/http" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + + "github.com/fatih/color" + "github.com/gin-gonic/gin" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/data" + "github.com/yaoapp/yao/engine" + "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/widgets/app" +) + +// XGenSetupServer XGen Setup +var XGenSetupServer http.Handler = http.FileServer(data.Setup()) + +// Done check done +var Done = make(chan bool, 1) + +// shutdown check done +var shutdown = make(chan bool, 1) + +// Canceled check if setup is canceld +var Canceled = make(chan bool, 1) + +// Start start the studio api server +func Start() (err error) { + + // recive interrupt signal + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) + + errCh := make(chan error, 1) + + // Set router + gin.SetMode(gin.ReleaseMode) + router := gin.New() + + if os.Getenv("YAO_SETUP_DEV") != "" { + fmt.Println(color.WhiteString("\nSETUP DEV: %s\n", os.Getenv("YAO_SETUP_DEV"))) + } + + router.Use(func(c *gin.Context) { + length := len(c.Request.URL.Path) + if length >= 5 && c.Request.URL.Path[0:5] == "/api/" { + c.Next() + return + } + + if os.Getenv("YAO_SETUP_DEV") != "" { + root, err := filepath.Abs(os.Getenv("YAO_SETUP_DEV")) + if err != nil { + printError("%s", err) + } + + static := http.FileServer(http.Dir(root)) + static.ServeHTTP(c.Writer, c.Request) + return + } + + // Setup Pages + XGenSetupServer.ServeHTTP(c.Writer, c.Request) + return + }, gin.CustomRecovery(recovered)) + + router.POST("/api/__yao/app/check", runCheck) + router.POST("/api/__yao/app/setup", runSetup) + + // Server setting + addr := ":5099" + + // Listen + l, err := net.Listen("tcp4", addr) + if err != nil { + addr = ":0" + l, err = net.Listen("tcp4", addr) + if err != nil { + return err + } + } + + srv := &http.Server{ + Addr: addr, + Handler: router, + } + defer func() { + // printInfo("[Setup] %s Close Serve", addr) + err = srv.Close() + if err != nil { + printError("[Setup] Error (%v)", err) + } + }() + + // start serve + go func() { + // printInfo("[Setup] Starting: %s", addr) + if err := srv.Serve(l); err != nil && err != http.ErrServerClosed { + errCh <- err + } + }() + + welcome(l) + + select { + + case <-shutdown: + printInfo("Setup Shutdown") + return err + + case <-interrupt: + printInfo("Setup Interrupt") + Canceled <- true + return err + + case err := <-errCh: + printError("Setup Error: ", err) + Canceled <- true + return err + } +} + +// Complete stop the studio api server +func Complete() { + Done <- true +} + +// Stop stop the studio api server +func Stop() { + shutdown <- true +} + +// AdminURL get admin url +func AdminURL(cfg config.Config) ([]string, error) { + err := engine.Load(cfg) + if err != nil { + return nil, err + } + + urls, err := URLs(cfg) + + adminRoot := "yao" + if app.Setting.AdminRoot != "" { + adminRoot = app.Setting.AdminRoot + } + adminRoot = strings.Trim(adminRoot, "/") + + for i := range urls { + urls[i] = fmt.Sprintf("%s/%s/", urls[i], adminRoot) + } + return urls, nil +} + +// URLs get admin url +func URLs(cfg config.Config) ([]string, error) { + err := engine.Load(cfg) + if err != nil { + return nil, err + } + + ips, err := Ips() + if err != nil { + return nil, err + } + + for i := range ips { + ips[i] = fmt.Sprintf("http://%s:%d", ips[i], cfg.Port) + } + + return ips, nil +} + +func welcome(l net.Listener) { + fmt.Println(color.WhiteString("---------------------------------")) + fmt.Println(color.WhiteString("Yao Application Setup v%s", share.VERSION)) + fmt.Println(color.WhiteString("---------------------------------")) + + ips, err := Ips() + if err != nil { + printError("Error: ", err.Error()) + } + + addr := strings.Split(l.Addr().String(), ":") + if len(addr) != 2 { + printError("Error: can't get port") + } + + port := addr[1] + for _, ip := range ips { + fmt.Println(color.WhiteString("\nOpen URL in the browser to continue:\n")) + printInfo("http://%s:%s", ip, port) + } + + fmt.Println() +} + +func printError(message string, args ...interface{}) { + fmt.Println(color.RedString(message, args...)) + os.Exit(1) +} + +func printInfo(message string, args ...interface{}) { + fmt.Println(color.GreenString(message, args...)) +} + +// Ips get the local ip list +func Ips() ([]string, error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + return nil, err + } + + iplist := []string{} + for _, address := range addrs { + // check the address type and if it is not a loopback the display it + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + iplist = append(iplist, ipnet.IP.String()) + } + } + } + return iplist, nil +} diff --git a/widgets/compute/compute.go b/widgets/compute/compute.go index a4b2dcc2..551a5a8b 100644 --- a/widgets/compute/compute.go +++ b/widgets/compute/compute.go @@ -51,6 +51,26 @@ func (c *Computable) ComputeEdit(name string, process *gou.Process, args []inter return nil } + if columns, ok := args[0].([]interface{}); ok { + new := []string{} + for _, col := range columns { + new = append(new, fmt.Sprintf("%v", col)) + } + args[0] = new + } + + if values, ok := args[1].([]interface{}); ok { + new := [][]interface{}{} + for _, value := range values { + arr, ok := value.([]interface{}) + if !ok { + return fmt.Errorf("args[1] is not a [][] %s", reflect.ValueOf(args[1]).Type().Name()) + } + new = append(new, arr) + } + args[1] = new + } + if _, ok := args[0].([]string); !ok { return fmt.Errorf("args[0] is not a []string %s", reflect.ValueOf(args[0]).Type().Name()) } diff --git a/xgen/setup/index.html b/xgen/setup/index.html new file mode 100644 index 00000000..1d68d1b6 --- /dev/null +++ b/xgen/setup/index.html @@ -0,0 +1 @@ +XGEN SETUP