Compare commits
	
		
			7 Commits 
		
	
	
		
			master
			...
			feat/muzan
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 621c720767 | |
|  | 733dada3fe | |
|  | 556f673b3e | |
|  | f039033077 | |
|  | 2ecac19807 | |
|  | 56841cc8be | |
|  | 31c141b6f9 | 
							
								
								
									
										27
									
								
								README.md
								
								
								
								
							
							
						
						
									
										27
									
								
								README.md
								
								
								
								
							|  | @ -65,33 +65,6 @@ sudo sh -c "curl -fsSL RELEASE_URL | tar -C /usr/local/bin/ -xz" | |||
| 
 | ||||
| `configui -f PATH/TO/CONFIG` | ||||
| 
 | ||||
| ## Add custom links in nav | ||||
| 
 | ||||
| **config** | ||||
| ```json | ||||
| { | ||||
|   "cust":"path/to/dir" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| **No link** | ||||
| > path/to/dir/none.tmpl | ||||
| ```go | ||||
| {{ define "links" }} | ||||
| <!-- none --> | ||||
| {{end}} | ||||
| ``` | ||||
| 
 | ||||
| **Custom links** | ||||
| > path/to/dir/links.tmpl | ||||
| ```go | ||||
| {{ define "links" }} | ||||
| <a class="button is-white level-item"  | ||||
|   href="https://kumoly.io/tools/configui/issues" | ||||
| >Report Issue</a> | ||||
| {{end}} | ||||
| ``` | ||||
| 
 | ||||
| ## Add integrations to ConfigUI | ||||
| 
 | ||||
| ```go | ||||
|  |  | |||
							
								
								
									
										43
									
								
								app.go
								
								
								
								
							
							
						
						
									
										43
									
								
								app.go
								
								
								
								
							|  | @ -27,6 +27,7 @@ type Page struct { | |||
| 	BaseUrl      string | ||||
| 	Actions      []Action | ||||
| 	Integrations []*Integration | ||||
| 	Onitachi     map[string]*Oni | ||||
| 	Version      string | ||||
| 	Build        string | ||||
| 	Files        []ActiveFile | ||||
|  | @ -79,6 +80,7 @@ func (cui *ConfigUI) App(w http.ResponseWriter, r *http.Request) { | |||
| 		}, | ||||
| 		Actions:      cui.Actions, | ||||
| 		Integrations: cui.Integrations, | ||||
| 		Onitachi:     cui.Onitachi, | ||||
| 		Static:       cui.NoReconfig, | ||||
| 		HideConfig:   cui.HideConfig, | ||||
| 		ResultBellow: cui.ResultBellow, | ||||
|  | @ -127,3 +129,44 @@ func (cui *ConfigUI) App(w http.ResponseWriter, r *http.Request) { | |||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) Program(w http.ResponseWriter, r *http.Request) { | ||||
| 	Files := []ActiveFile{} | ||||
| 	for _, i := range cui.fileIndex { | ||||
| 		Files = append(Files, ActiveFile{ | ||||
| 			Name:  cui.Files[i].Name, | ||||
| 			Path:  cui.Files[i].Path, | ||||
| 			Order: cui.Files[i].Order, | ||||
| 		}) | ||||
| 	} | ||||
| 	sort.Slice(Files, func(i, j int) bool { | ||||
| 		if Files[i].Order == Files[j].Order { | ||||
| 			return Files[i].Name < Files[j].Name | ||||
| 		} | ||||
| 		return Files[i].Order < Files[j].Order | ||||
| 	}) | ||||
| 	plat := "unix" | ||||
| 	if runtime.GOOS == "windows" { | ||||
| 		plat = "windows" | ||||
| 	} | ||||
| 	data := Page{ | ||||
| 		AppName: cui.AppName, | ||||
| 		BaseUrl: cui.BaseUrl, | ||||
| 		File:    ActiveFile{}, | ||||
| 		Files:   Files, | ||||
| 		Editor: Editor{ | ||||
| 			Platform: plat, | ||||
| 		}, | ||||
| 		Actions:      cui.Actions, | ||||
| 		Integrations: cui.Integrations, | ||||
| 		Onitachi:     cui.Onitachi, | ||||
| 		Static:       cui.NoReconfig, | ||||
| 		HideConfig:   cui.HideConfig, | ||||
| 		Version:      version, | ||||
| 	} | ||||
| 
 | ||||
| 	err := cui.tmpl.ExecuteTemplate(w, "muzan", data) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -18,7 +18,6 @@ var ( | |||
| 	flagConfigPath string | ||||
| 
 | ||||
| 	flagBind       string | ||||
| 	flagAddr       string | ||||
| 	flagNoReconfig bool | ||||
| 	flagLogFile    string | ||||
| 	flagAllow      string | ||||
|  | @ -37,8 +36,7 @@ func init() { | |||
| 	flag.StringVar(&flagAction, "c", "", "cmd to apply") | ||||
| 	flag.StringVar(&flagLogFile, "log", "", "log to file") | ||||
| 	flag.StringVar(&flagAllow, "allow", "", "IPs to allow, blank to allow all") | ||||
| 	flag.StringVar(&flagBind, "bind", "0.0.0.0:8000", "address to bind, (deprecated, use -addr instead)") | ||||
| 	flag.StringVar(&flagAddr, "addr", "0.0.0.0:8000", "address to bind") | ||||
| 	flag.StringVar(&flagBind, "bind", "127.0.0.1:8000", "address to bind") | ||||
| 	flag.BoolVar(&flagNoReconfig, "static", false, "disable config api") | ||||
| 	flag.BoolVar(&flagVer, "v", false, "show version") | ||||
| 	flag.Usage = func() { | ||||
|  | @ -55,11 +53,6 @@ func main() { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// deprecate fix
 | ||||
| 	if flagBind != "0.0.0.0:8000" && flagAddr == "0.0.0.0:8000" { | ||||
| 		flagAddr = flagBind | ||||
| 	} | ||||
| 
 | ||||
| 	cui := configui.New() | ||||
| 
 | ||||
| 	// setup values
 | ||||
|  | @ -105,7 +98,7 @@ func main() { | |||
| 
 | ||||
| 	// setup routes
 | ||||
| 	server := &http.Server{ | ||||
| 		Addr: flagAddr, | ||||
| 		Addr: flagBind, | ||||
| 		// disable timeout due to cui controlling cmd timeouts
 | ||||
| 		// WriteTimeout: time.Second * 30,
 | ||||
| 		// ReadTimeout:  time.Second * 30,
 | ||||
|  | @ -113,7 +106,7 @@ func main() { | |||
| 	} | ||||
| 
 | ||||
| 	// start server
 | ||||
| 	log.Info("Listening on ", flagAddr) | ||||
| 	log.Info("Listening on ", flagBind) | ||||
| 	err := server.ListenAndServe() | ||||
| 	if err != nil { | ||||
| 		log.Error(err) | ||||
|  |  | |||
							
								
								
									
										104
									
								
								configui.go
								
								
								
								
							
							
						
						
									
										104
									
								
								configui.go
								
								
								
								
							|  | @ -22,14 +22,11 @@ const UNIX_SHELL = "/usr/bin/sh" | |||
| const WIN_SHELL = "C:\\Windows\\System32\\cmd" | ||||
| const DARWIN_SHELL = "/bin/bash" | ||||
| 
 | ||||
| const version = "v0.1.14" | ||||
| const version = "v0.1.12" | ||||
| 
 | ||||
| //go:embed templates
 | ||||
| var tmplFS embed.FS | ||||
| 
 | ||||
| //go:embed schema.json
 | ||||
| var SCHEMA []byte | ||||
| 
 | ||||
| var Ext2Mode map[string]string = map[string]string{ | ||||
| 	"go":   "golang", | ||||
| 	"log":  "sh", | ||||
|  | @ -40,10 +37,12 @@ var Ext2Mode map[string]string = map[string]string{ | |||
| } | ||||
| 
 | ||||
| type Action struct { | ||||
| 	Name string        `json:"name"` | ||||
| 	Cmd  string        `json:"cmd"` | ||||
| 	run  chan struct{} `json:"-"` | ||||
| 	pid  int           `json:"-"` | ||||
| 	Name string `json:"name"` | ||||
| 	Cmd  string `json:"cmd"` | ||||
| 	Dir  string `json:"dir"` | ||||
| 
 | ||||
| 	run chan struct{} `json:"-"` | ||||
| 	pid int | ||||
| } | ||||
| 
 | ||||
| type Integration struct { | ||||
|  | @ -77,8 +76,10 @@ type ConfigUI struct { | |||
| 	LogPath  string      `json:"log_path"` | ||||
| 	LogLevel klog.Llevel `json:"log_level"` | ||||
| 
 | ||||
| 	// Running commands
 | ||||
| 	Onitachi map[string]*Oni `json:"onitachi"` | ||||
| 
 | ||||
| 	TmplFS     embed.FS `json:"-"` | ||||
| 	CustTmpl   string   `json:"cust,omitempty"` | ||||
| 	tmpl       *engine.Engine | ||||
| 	PublicFS   embed.FS `json:"-"` | ||||
| 	log        *klog.Logger | ||||
|  | @ -99,6 +100,30 @@ func New() *ConfigUI { | |||
| 		"normal": func(name string) string { | ||||
| 			return strings.ReplaceAll(name, " ", "-") | ||||
| 		}, | ||||
| 		"state_class": func(state State) string { | ||||
| 			switch state { | ||||
| 			case STARTED: | ||||
| 				return "is-success" | ||||
| 			case ERROR: | ||||
| 				return "is-danger" | ||||
| 			case ENDED: | ||||
| 				return "has-background-grey-light" | ||||
| 			default: | ||||
| 				return "" | ||||
| 			} | ||||
| 		}, | ||||
| 		"state_icon": func(state State) string { | ||||
| 			switch state { | ||||
| 			case STARTED: | ||||
| 				return "play_arrow" | ||||
| 			case ERROR: | ||||
| 				return "error" | ||||
| 			case ENDED: | ||||
| 				return "stop" | ||||
| 			default: | ||||
| 				return "pending" | ||||
| 			} | ||||
| 		}, | ||||
| 	}).ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl")) | ||||
| 	sh := "" | ||||
| 	switch runtime.GOOS { | ||||
|  | @ -123,6 +148,7 @@ func New() *ConfigUI { | |||
| 		cmdTimeout: time.Second * 10, | ||||
| 		LogLevel:   klog.Lerror | klog.Linfo, | ||||
| 		log:        klog.Sub("ConfigUI"), | ||||
| 		Onitachi:   make(map[string]*Oni), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -143,6 +169,8 @@ func (cui *ConfigUI) File(file_name string) (*File, error) { | |||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) LoadConfig(confstr string) error { | ||||
| 	cui.configLock.Lock() | ||||
| 	defer cui.configLock.Unlock() | ||||
| 	tmpConf := &ConfigUI{} | ||||
| 	err := json.Unmarshal([]byte(confstr), tmpConf) | ||||
| 	if err != nil { | ||||
|  | @ -152,9 +180,6 @@ func (cui *ConfigUI) LoadConfig(confstr string) error { | |||
| 	// construct fileIndex
 | ||||
| 	tmpIndex := map[string]int{} | ||||
| 	for i, f := range tmpConf.Files { | ||||
| 		if f.Name == "" && f.Path == "" { | ||||
| 			continue | ||||
| 		} | ||||
| 		if f.Name == "" { | ||||
| 			f.Name = f.Path | ||||
| 		} | ||||
|  | @ -168,9 +193,15 @@ func (cui *ConfigUI) LoadConfig(confstr string) error { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// del oni dry run
 | ||||
| 	for k, v := range cui.Onitachi { | ||||
| 		_, ok := tmpConf.Onitachi[k] | ||||
| 		if !ok && v.State == STARTED { | ||||
| 			return ErrorNoDeleteRunning.New(k) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// copy
 | ||||
| 	cui.configLock.Lock() | ||||
| 	defer cui.configLock.Unlock() | ||||
| 	cui.fileIndex = tmpIndex | ||||
| 	cui.Files = tmpConf.Files | ||||
| 	cui.AllowIP = tmpConf.AllowIP | ||||
|  | @ -182,13 +213,36 @@ func (cui *ConfigUI) LoadConfig(confstr string) error { | |||
| 	cui.ResultBellow = tmpConf.ResultBellow | ||||
| 	cui.SHELL = tmpConf.SHELL | ||||
| 
 | ||||
| 	cui.CustTmpl = tmpConf.CustTmpl | ||||
| 	if cui.CustTmpl != "" { | ||||
| 		ntmpl, err := cui.tmpl.OverrideGlob(filepath.Join(cui.CustTmpl, "*.tmpl")) | ||||
| 		if err != nil { | ||||
| 			cui.log.Error(err) | ||||
| 	cui.log = klog.Sub(cui.AppName) | ||||
| 	cui.LogLevel = tmpConf.LogLevel | ||||
| 	if cui.LogLevel == 0 { | ||||
| 		cui.LogLevel = klog.Lerror | klog.Linfo | ||||
| 	} | ||||
| 	klog.LEVEL = cui.LogLevel | ||||
| 	cui.LogPath = tmpConf.LogPath | ||||
| 	cui.setLog() | ||||
| 
 | ||||
| 	// construct oni - update existing, del
 | ||||
| 	for k, v := range cui.Onitachi { | ||||
| 		n, ok := tmpConf.Onitachi[k] | ||||
| 		if !ok { | ||||
| 			delete(cui.Onitachi, k) | ||||
| 		} else { | ||||
| 			cui.tmpl = ntmpl | ||||
| 			v.Name = k | ||||
| 			v.Cmd = n.Cmd | ||||
| 			v.Args = n.Args | ||||
| 			v.Envs = n.Envs | ||||
| 			v.Dir = n.Dir | ||||
| 			v.Policy = n.Policy | ||||
| 		} | ||||
| 	} | ||||
| 	// construct oni - append new
 | ||||
| 	for k, v := range tmpConf.Onitachi { | ||||
| 		_, ok := cui.Onitachi[k] | ||||
| 		if !ok { | ||||
| 			v.Name = k | ||||
| 			v.Init(cui) | ||||
| 			cui.Onitachi[k] = v | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -218,15 +272,6 @@ func (cui *ConfigUI) LoadConfig(confstr string) error { | |||
| 		cui.cmdTimeout = ct | ||||
| 	} | ||||
| 
 | ||||
| 	cui.log = klog.Sub(cui.AppName) | ||||
| 
 | ||||
| 	cui.LogLevel = tmpConf.LogLevel | ||||
| 	if cui.LogLevel == 0 { | ||||
| 		cui.LogLevel = klog.Lerror | klog.Linfo | ||||
| 	} | ||||
| 	klog.LEVEL = cui.LogLevel | ||||
| 	cui.LogPath = tmpConf.LogPath | ||||
| 	cui.setLog() | ||||
| 	// fmt.Printf("%+v", cui)
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -237,6 +282,7 @@ func (cui *ConfigUI) setLog() { | |||
| 		cui.f.Close() | ||||
| 	} | ||||
| 	if cui.LogPath != "" { | ||||
| 		mkdir(filepath.Dir(cui.LogPath)) | ||||
| 		cui.f, err = os.OpenFile(cui.LogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) | ||||
| 		if err != nil { | ||||
| 			cui.log.Error(err) | ||||
|  |  | |||
							
								
								
									
										42
									
								
								errors.go
								
								
								
								
							
							
						
						
									
										42
									
								
								errors.go
								
								
								
								
							|  | @ -11,3 +11,45 @@ var ErrorServerReloading = ksrv.Error{ | |||
| 	ID:      "ErrorServerReloading", | ||||
| 	Message: "server is reloading", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniHasNoPID = ksrv.Error{ | ||||
| 	Code:    http.StatusBadRequest, | ||||
| 	ID:      "ErrorOniHasNoPID", | ||||
| 	Message: "oni has no pid", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniNotStarted = ksrv.Error{ | ||||
| 	Code: http.StatusConflict, | ||||
| 	ID:   "ErrorOniNotStarted", | ||||
| 	Tmpl: "%s hasn't start", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniHasStarted = ksrv.Error{ | ||||
| 	Code: http.StatusConflict, | ||||
| 	ID:   "ErrorOniHasStarted", | ||||
| 	Tmpl: "%s has started", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniNotFound = ksrv.Error{ | ||||
| 	Code: http.StatusNotFound, | ||||
| 	ID:   "ErrorOniNotFound", | ||||
| 	Tmpl: "%s not found", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniNotValid = ksrv.Error{ | ||||
| 	Code:    http.StatusBadRequest, | ||||
| 	ID:      "ErrorOniNotValid", | ||||
| 	Message: "oni no name or cmd", | ||||
| } | ||||
| 
 | ||||
| var ErrorNoDeleteRunning = ksrv.Error{ | ||||
| 	Code: http.StatusBadRequest, | ||||
| 	ID:   "ErrorNoDeleteRunning", | ||||
| 	Tmpl: "cannot delete running: %s", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniNoLog = ksrv.Error{ | ||||
| 	Code: http.StatusNotFound, | ||||
| 	ID:   "ErrorOniNoLog", | ||||
| 	Tmpl: "%s has no logs", | ||||
| } | ||||
|  |  | |||
							
								
								
									
										2
									
								
								file.go
								
								
								
								
							
							
						
						
									
										2
									
								
								file.go
								
								
								
								
							|  | @ -17,7 +17,7 @@ type File struct { | |||
| 	Cmd  string `json:"cmd"` | ||||
| 	// RO is readonly
 | ||||
| 	RO   bool   `json:"ro"` | ||||
| 	Lang string `json:"lang,omitempty"` | ||||
| 	Lang string `json:"lang"` | ||||
| 
 | ||||
| 	// Order order of the display on ui
 | ||||
| 	Order int `json:"order"` | ||||
|  |  | |||
							
								
								
									
										5
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										5
									
								
								go.mod
								
								
								
								
							|  | @ -5,13 +5,10 @@ go 1.17 | |||
| require ( | ||||
| 	kumoly.io/lib/klog v0.0.8 | ||||
| 	kumoly.io/lib/ksrv v0.0.2-0.20211112060911-0d61b343a298 | ||||
| 	kumoly.io/lib/stat v0.0.1 | ||||
| 	kumoly.io/tools/kconfig v0.1.3 | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/mattn/go-isatty v0.0.14 // indirect | ||||
| 	github.com/rs/zerolog v1.26.0 // indirect | ||||
| 	golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect | ||||
| 	kumoly.io/lib/guard v0.1.2-0.20211124052638-9dfd98f9a848 // indirect | ||||
| 	kumoly.io/lib/stat v0.0.1 // indirect | ||||
| ) | ||||
|  |  | |||
							
								
								
									
										36
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										36
									
								
								go.sum
								
								
								
								
							|  | @ -1,44 +1,14 @@ | |||
| github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | ||||
| github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||||
| github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= | ||||
| github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | ||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||||
| github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE= | ||||
| github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo= | ||||
| github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4= | ||||
| golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| kumoly.io/lib/guard v0.1.2-0.20211124052638-9dfd98f9a848 h1:ALCeJga3775AJDkk7YbMQEcYxpwHtinUN+YjkpacNhA= | ||||
| kumoly.io/lib/guard v0.1.2-0.20211124052638-9dfd98f9a848/go.mod h1:yWg9RDSI6YXkOPmP6Ad93aMqzlxhgW8LOe/ZRjjYX3U= | ||||
| kumoly.io/lib/klog v0.0.4/go.mod h1:Snm+c1xRrh/RbXsxQf7UGYbAJGPcIa6bEEN+CmzJh7M= | ||||
| kumoly.io/lib/klog v0.0.8 h1:6hTfDlZh7KGnPrd2tUrauCKRImSnyyN9DHXpey3Czn8= | ||||
| kumoly.io/lib/klog v0.0.8/go.mod h1:Snm+c1xRrh/RbXsxQf7UGYbAJGPcIa6bEEN+CmzJh7M= | ||||
| kumoly.io/lib/ksrv v0.0.1 h1:JfWwJ9GeiTtDfGoeG7YxJwsckralbhsLKEPLQb20Uzo= | ||||
| kumoly.io/lib/ksrv v0.0.1/go.mod h1:ykHXeAPjNvA5jEZo5rp32edzkugLf0e+2pspct3FOFQ= | ||||
| kumoly.io/lib/ksrv v0.0.2-0.20211112060911-0d61b343a298 h1:0raqoIXmNpD6s1SrJbieAyIIkDyhe+aqfaXvx8wenrI= | ||||
| kumoly.io/lib/ksrv v0.0.2-0.20211112060911-0d61b343a298/go.mod h1:pwd+NspxnoxPJAETRY2V4i2qZc+orKLxvWzGUBiqBW8= | ||||
| kumoly.io/lib/stat v0.0.1 h1:Ck596El7Ixk7GZyzQq/86F1YCl7iYffHmzEdFx1sSRM= | ||||
| kumoly.io/lib/stat v0.0.1/go.mod h1:zqMV9q4TC94VGbpDn/mGBTwRNWBVWlVg1taLlCBAWc8= | ||||
| kumoly.io/tools/kconfig v0.1.3 h1:okLWqlvASZzfAj3kXzpB/Q8V7WZNd24Y6MLPhWPTwP8= | ||||
| kumoly.io/tools/kconfig v0.1.3/go.mod h1:j6GYUb50c0pFMxfQgoUl5n4P++AapoG+L5YHxHyyjN4= | ||||
|  |  | |||
							
								
								
									
										99
									
								
								handler.go
								
								
								
								
							
							
						
						
									
										99
									
								
								handler.go
								
								
								
								
							|  | @ -121,7 +121,7 @@ func (cui *ConfigUI) Apply(w http.ResponseWriter, r *http.Request) { | |||
| 		ksrv.Response(w, 404, []byte("file not found")) | ||||
| 		return | ||||
| 	} | ||||
| 	result, err := file.Do(cui.cmdTimeout, make(chan int, 1)) | ||||
| 	result, err := file.Do(cui.cmdTimeout, make(chan int)) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | @ -154,6 +154,7 @@ func (cui *ConfigUI) DoAction(w http.ResponseWriter, r *http.Request) { | |||
| 				panic(err) | ||||
| 			} | ||||
| 			w.Write([]byte(result)) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | @ -258,3 +259,99 @@ func (cui *ConfigUI) GetConfig(w http.ResponseWriter, r *http.Request) { | |||
| 	} | ||||
| 	w.Write(data) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) GetOni(w http.ResponseWriter, r *http.Request) { | ||||
| 	name := r.URL.Query().Get("name") | ||||
| 	if name == "" { | ||||
| 		ksrv.JSON(w, cui.Onitachi) | ||||
| 		return | ||||
| 	} | ||||
| 	oni, ok := cui.Onitachi[name] | ||||
| 	if !ok { | ||||
| 		panic(ErrorOniNotFound.New(name)) | ||||
| 	} | ||||
| 	ksrv.JSON(w, oni) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) OniStart(w http.ResponseWriter, r *http.Request) { | ||||
| 	name := r.URL.Query().Get("name") | ||||
| 	oni, ok := cui.Onitachi[name] | ||||
| 	if !ok { | ||||
| 		panic(ErrorOniNotFound.New(name)) | ||||
| 	} | ||||
| 	switch oni.State { | ||||
| 	case "": | ||||
| 		oni.Init(cui) | ||||
| 	case STARTED: | ||||
| 		panic(ErrorOniHasStarted.New(name)) | ||||
| 	} | ||||
| 	err := oni.Start() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	w.Write([]byte("ok")) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) OniStop(w http.ResponseWriter, r *http.Request) { | ||||
| 	name := r.URL.Query().Get("name") | ||||
| 	oni, ok := cui.Onitachi[name] | ||||
| 	if !ok { | ||||
| 		panic(ErrorOniNotFound.New(name)) | ||||
| 	} | ||||
| 	switch oni.State { | ||||
| 	case STARTED: | ||||
| 		err := oni.Stop() | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
| 	default: | ||||
| 		panic(ErrorOniNotStarted.New(name)) | ||||
| 	} | ||||
| 	w.Write([]byte("ok")) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) OniLog(w http.ResponseWriter, r *http.Request) { | ||||
| 	name := r.URL.Query().Get("name") | ||||
| 	oni, ok := cui.Onitachi[name] | ||||
| 	if !ok { | ||||
| 		panic(ErrorOniNotFound.New(name)) | ||||
| 	} | ||||
| 	if oni.LogFile == "" { | ||||
| 		panic(ErrorOniNoLog.New(name)) | ||||
| 	} | ||||
| 	stat, err := os.Stat(oni.LogFile) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	data, err := os.ReadFile(oni.LogFile) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	ksrv.JSON(w, map[string]string{ | ||||
| 		"name":  name, | ||||
| 		"data":  string(data), | ||||
| 		"delta": strconv.Itoa(int(stat.Size())), | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) OniKill(w http.ResponseWriter, r *http.Request) { | ||||
| 	name := r.URL.Query().Get("name") | ||||
| 	oni, ok := cui.Onitachi[name] | ||||
| 	if !ok { | ||||
| 		panic(ErrorOniNotFound.New(name)) | ||||
| 	} | ||||
| 	switch oni.State { | ||||
| 	case STARTED: | ||||
| 		err := oni.Kill() | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
| 	default: | ||||
| 		panic(ErrorOniNotStarted.New(name)) | ||||
| 	} | ||||
| 	w.Write([]byte("ok")) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) OniAttach(w http.ResponseWriter, r *http.Request) { | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,172 @@ | |||
| package configui | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path/filepath" | ||||
| 	"syscall" | ||||
| ) | ||||
| 
 | ||||
| type State string | ||||
| 
 | ||||
| const ( | ||||
| 	READY   State = "ready" | ||||
| 	ERROR   State = "error" | ||||
| 	STARTED State = "started" | ||||
| 	ENDED   State = "ended" | ||||
| ) | ||||
| 
 | ||||
| type Policy string | ||||
| 
 | ||||
| const ( | ||||
| 	NO         Policy = "no" | ||||
| 	ONFAIL     Policy = "on-failure" | ||||
| 	ALWAYS     Policy = "always" | ||||
| 	UNLESSSTOP Policy = "unless-stopped" | ||||
| ) | ||||
| 
 | ||||
| type Oni struct { | ||||
| 	Name   string   `json:"-"` | ||||
| 	Cmd    string   `json:"cmd"` | ||||
| 	Dir    string   `json:"dir"` | ||||
| 	Args   []string `json:"args"` | ||||
| 	Envs   []string `json:"envs"` | ||||
| 	Policy Policy   `json:"policy"` | ||||
| 
 | ||||
| 	PID     int    `json:"pid"` | ||||
| 	LogFile string `json:"log_file"` | ||||
| 	State   State  `json:"state"` | ||||
| 	Error   string `json:"error"` | ||||
| 	ManStop bool   `json:"manual_stop"` | ||||
| 
 | ||||
| 	parent  *ConfigUI | ||||
| 	cmd     *exec.Cmd | ||||
| 	running chan struct{} | ||||
| 	buff    bytes.Buffer | ||||
| 	log     io.Writer | ||||
| 
 | ||||
| 	listeners []io.Writer | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) Init(cui *ConfigUI) { | ||||
| 	oni.parent = cui | ||||
| 	oni.running = make(chan struct{}, 1) | ||||
| 	oni.listeners = make([]io.Writer, 0) | ||||
| 	oni.StateChange(READY) | ||||
| 	oni.SetLog() | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) Start() error { | ||||
| 	if oni.Cmd == "" { | ||||
| 		return ErrorOniNotValid | ||||
| 	} | ||||
| 	select { | ||||
| 	case oni.running <- struct{}{}: | ||||
| 		defer func() { | ||||
| 			err := recover() | ||||
| 			if err != nil { | ||||
| 				oni.end(err) | ||||
| 			} | ||||
| 		}() | ||||
| 	default: | ||||
| 		return ErrorOniHasStarted | ||||
| 	} | ||||
| 	var err error | ||||
| 	cmd := exec.Command(oni.Cmd, oni.Args...) | ||||
| 	setgpid(cmd) | ||||
| 	cmd.Env = oni.Envs | ||||
| 	cmd.Dir = oni.Dir | ||||
| 	var out io.Writer | ||||
| 	if oni.LogFile != "" { | ||||
| 		out = io.MultiWriter(oni.log, &oni.buff) | ||||
| 	} else { | ||||
| 		out = &oni.buff | ||||
| 	} | ||||
| 	cmd.Stderr = out | ||||
| 	cmd.Stdout = out | ||||
| 	oni.cmd = cmd | ||||
| 	err = oni.cmd.Start() | ||||
| 	if err != nil { | ||||
| 		oni.end(err) | ||||
| 		return err | ||||
| 	} | ||||
| 	go oni.read() | ||||
| 	oni.PID = cmd.Process.Pid | ||||
| 	oni.StateChange(STARTED) | ||||
| 	go func() { | ||||
| 		oni.end(cmd.Wait()) | ||||
| 	}() | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) Stop() error { | ||||
| 	if oni.cmd == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return oni.cmd.Process.Signal(syscall.SIGTERM) | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) end(v interface{}) { | ||||
| 	if v == nil { | ||||
| 		oni.StateChange(ENDED) | ||||
| 	} else { | ||||
| 		oni.Error = fmt.Sprint(v) | ||||
| 		oni.StateChange(ERROR) | ||||
| 	} | ||||
| 	if oni.log != nil { | ||||
| 		file, ok := oni.log.(*os.File) | ||||
| 		if ok { | ||||
| 			file.Close() | ||||
| 		} | ||||
| 	} | ||||
| 	if len(oni.running) == 1 { | ||||
| 		<-oni.running | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) read() { | ||||
| 	go func() { | ||||
| 		scanner := bufio.NewScanner(&oni.buff) | ||||
| 		for scanner.Scan() { | ||||
| 			if len(oni.listeners) == 0 { | ||||
| 				continue | ||||
| 			} | ||||
| 			w := io.MultiWriter(oni.listeners...) | ||||
| 			w.Write([]byte(scanner.Text())) | ||||
| 		} | ||||
| 	}() | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) StateChange(state State) { | ||||
| 	oni.State = state | ||||
| 	// for i := range oni.parent.Actions {
 | ||||
| 	// 	if oni.parent.Actions[i].Name == oni.Name {
 | ||||
| 	// 		oni.parent.Actions[i].State = state
 | ||||
| 	// 	}
 | ||||
| 	// }
 | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) SetLog() error { | ||||
| 	var err error | ||||
| 	if oni.log != nil { | ||||
| 		file, ok := oni.log.(*os.File) | ||||
| 		if ok { | ||||
| 			file.Close() | ||||
| 		} | ||||
| 	} | ||||
| 	if oni.parent.LogPath != "" { | ||||
| 		logpath := filepath.Join(filepath.Dir(oni.parent.LogPath), oni.Name+".log") | ||||
| 		oni.LogFile = logpath | ||||
| 		oni.log, err = os.OpenFile(logpath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) | ||||
| 		if err != nil { | ||||
| 			oni.end(err) | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
|  | @ -0,0 +1,24 @@ | |||
| //go:build !windows
 | ||||
| // +build !windows
 | ||||
| 
 | ||||
| package configui | ||||
| 
 | ||||
| import ( | ||||
| 	"os/exec" | ||||
| 	"syscall" | ||||
| ) | ||||
| 
 | ||||
| func setgpid(cmd *exec.Cmd) { | ||||
| 	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) Kill() error { | ||||
| 	if oni.cmd == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	pgid, err := syscall.Getpgid(oni.cmd.Process.Pid) | ||||
| 	if err == nil { | ||||
| 		return syscall.Kill(-pgid, 15) // note the minus sign
 | ||||
| 	} | ||||
| 	return oni.cmd.Process.Kill() | ||||
| } | ||||
|  | @ -0,0 +1,13 @@ | |||
| package configui | ||||
| 
 | ||||
| import "os/exec" | ||||
| 
 | ||||
| func setgpid(cmd *exec.Cmd) { | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) Kill() error { | ||||
| 	if oni.cmd == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return oni.cmd.Process.Kill() | ||||
| } | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										148
									
								
								schema.json
								
								
								
								
							
							
						
						
									
										148
									
								
								schema.json
								
								
								
								
							|  | @ -1,148 +0,0 @@ | |||
| { | ||||
|   "title": "ConfigUI", | ||||
|   "type": "object", | ||||
|   "id": "configui", | ||||
|   "properties": { | ||||
|     "app_name": { | ||||
|       "type": "string", | ||||
|       "title": "AppName", | ||||
|       "default": "ConfigUI", | ||||
|       "required":true | ||||
|     }, | ||||
|     "files":{ | ||||
|       "title":"Files", | ||||
|       "type": "array", | ||||
|       "format":"table", | ||||
|       "items":{ | ||||
|         "$ref": "#/definitions/file" | ||||
|       } | ||||
|     }, | ||||
|     "actions":{ | ||||
|       "title":"Actions", | ||||
|       "type": "array", | ||||
|       "format":"table", | ||||
|       "items":{ | ||||
|         "$ref": "#/definitions/action" | ||||
|       } | ||||
|     }, | ||||
|     "base_url":{ | ||||
|       "type": "string", | ||||
|       "title": "BaseUrl", | ||||
|       "default": "/" | ||||
|     }, | ||||
|     "config_path":{ | ||||
|       "title":"Config Path", | ||||
|       "type": "string", | ||||
|       "discription":"path to config file" | ||||
|     }, | ||||
|     "shell":{ | ||||
|       "title":"Shell", | ||||
|       "type": "string", | ||||
|       "description":"shell to use when running cmds", | ||||
|       "required":true, | ||||
|       "options":{ | ||||
|         "infoText":"run commands are structured as SHELL -c \"CMD\", \nthe default shell is \nunix:`/usr/bin/sh`\nwindows:`C:\\Windows\\System32\\cmd`\ndarwin:`/bin/bash`" | ||||
|       } | ||||
|     }, | ||||
|     "allow_ip":{ | ||||
|       "type":"string", | ||||
|       "title":"AllowedIP", | ||||
|       "description":"IPs to allow, blank to allow all" | ||||
|     }, | ||||
|     "timeout":{ | ||||
|       "type": "string", | ||||
|       "title": "Command Timeout", | ||||
|       "default": "10s", | ||||
|       "description": "timeout to wait for command to finish" | ||||
|     }, | ||||
|     "log_path": { | ||||
|       "type":"string", | ||||
|       "title":"Log Path", | ||||
|       "description":"empty for stdout" | ||||
|     }, | ||||
|     "log_level": { | ||||
|       "type": "integer", | ||||
|       "title": "log level", | ||||
|       "default": 9 | ||||
|     }, | ||||
|     "cust":{ | ||||
|       "title": "Custom Template", | ||||
|       "type": "string", | ||||
|       "description": "path to custom templates" | ||||
|     }, | ||||
|      | ||||
|     "result_bellow": { | ||||
|       "title":"Result Bellow", | ||||
|       "type": "boolean", | ||||
|       "format": "checkbox", | ||||
|       "description":"show results bellow editor", | ||||
|       "default": false | ||||
|     }, | ||||
|     "production":{ | ||||
|       "type": "boolean", | ||||
|       "format": "checkbox", | ||||
|       "title": "Production Mode", | ||||
|       "default": true | ||||
|     }, | ||||
|     "no_reconfig":{ | ||||
|       "title":"NoReconfig", | ||||
|       "type": "boolean", | ||||
|       "format": "checkbox", | ||||
|       "description":"disable config at runtime", | ||||
|       "default": false | ||||
|     }, | ||||
|     "hide_config": { | ||||
|       "title":"Hide Config", | ||||
|       "type": "boolean", | ||||
|       "format": "checkbox", | ||||
|       "description":"hide config panel", | ||||
|       "default": false | ||||
|     } | ||||
|   }, | ||||
|   "definitions": { | ||||
|     "file": { | ||||
|       "type": "object", | ||||
|       "properties":{ | ||||
|         "name": { | ||||
|           "title": "Name", | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "path": { | ||||
|           "title": "Path", | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "cmd": { | ||||
|           "title": "Command", | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "ro": { | ||||
|           "title": "Read Only", | ||||
|           "format":"checkbox", | ||||
|           "type": "boolean" | ||||
|         }, | ||||
|         "lang": { | ||||
|           "title": "Lang", | ||||
|           "type": "string", | ||||
|           "description":"set editor language mode" | ||||
|         }, | ||||
|         "order": { | ||||
|           "type": "integer", | ||||
|           "default":0 | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "action":{ | ||||
|       "type": "object", | ||||
|       "properties":{ | ||||
|         "name": { | ||||
|           "title": "Name", | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "cmd": { | ||||
|           "title": "Command", | ||||
|           "type": "string" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										92
									
								
								server.go
								
								
								
								
							
							
						
						
									
										92
									
								
								server.go
								
								
								
								
							|  | @ -2,11 +2,10 @@ package configui | |||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"kumoly.io/lib/ksrv" | ||||
| 	"kumoly.io/lib/stat" | ||||
| 	"kumoly.io/tools/kconfig" | ||||
| ) | ||||
| 
 | ||||
| func (cui *ConfigUI) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
|  | @ -18,19 +17,10 @@ func (cui *ConfigUI) middleware(next http.Handler) http.Handler { | |||
| 	cui.ksrv_log = k.GetLogger() | ||||
| 	cui.setLog() | ||||
| 	k.SetNoLogCondition(func(r *http.Request) bool { | ||||
| 		ext := filepath.Ext(r.URL.Path) | ||||
| 		switch ext { | ||||
| 		case ".js": | ||||
| 		if strings.HasPrefix(r.URL.Path, "/public") || r.URL.Query().Get("f") == "true" { | ||||
| 			return true | ||||
| 		case ".css": | ||||
| 			return true | ||||
| 		case ".ttf": | ||||
| 			return true | ||||
| 		case ".ico": | ||||
| 			return true | ||||
| 
 | ||||
| 		} | ||||
| 		return r.URL.Query().Get("f") == "true" | ||||
| 		return false | ||||
| 	}) | ||||
| 	return k.Middleware( | ||||
| 		http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||
|  | @ -41,6 +31,9 @@ func (cui *ConfigUI) middleware(next http.Handler) http.Handler { | |||
| 					panic("permission denied") | ||||
| 				} | ||||
| 			} | ||||
| 			if MutexLocked(&cui.configLock) { | ||||
| 				panic(ErrorServerReloading) | ||||
| 			} | ||||
| 			next.ServeHTTP(rw, r) | ||||
| 		}), | ||||
| 	) | ||||
|  | @ -49,13 +42,6 @@ func (cui *ConfigUI) middleware(next http.Handler) http.Handler { | |||
| 
 | ||||
| func (cui *ConfigUI) mux() *http.ServeMux { | ||||
| 	cuiR := http.NewServeMux() | ||||
| 	cuiR.HandleFunc("/api/profile", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "GET" { | ||||
| 			ksrv.JSON(w, stat.GetProfile()) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/conf", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "GET" { | ||||
| 			cui.GetConfig(w, r) | ||||
|  | @ -116,27 +102,57 @@ func (cui *ConfigUI) mux() *http.ServeMux { | |||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 
 | ||||
| 	k := kconfig.New() | ||||
| 	k.Schema = SCHEMA | ||||
| 	k.Load = func() []byte { | ||||
| 		b, err := cui.Config() | ||||
| 		if err != nil { | ||||
| 			cui.log.Error(err) | ||||
| 			return []byte{} | ||||
| 	cuiR.HandleFunc("/api/profile", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "GET" { | ||||
| 			ksrv.JSON(w, stat.GetProfile()) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 		return b | ||||
| 	} | ||||
| 	k.Apply = func(b []byte) error { | ||||
| 		err := cui.LoadConfig(string(b)) | ||||
| 		if err == nil { | ||||
| 			k.AppName = cui.AppName | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "GET" { | ||||
| 			cui.GetOni(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 		return err | ||||
| 	} | ||||
| 	cuiR.Handle("/kconfig/", http.StripPrefix("/kconfig", k)) | ||||
| 
 | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni/start", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "POST" { | ||||
| 			cui.OniStart(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni/stop", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "POST" { | ||||
| 			cui.OniStop(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni/kill", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "POST" { | ||||
| 			cui.OniKill(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni/log", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == http.MethodGet { | ||||
| 			cui.OniLog(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni/attach", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == http.MethodGet { | ||||
| 			cui.OniAttach(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.FS(cui.PublicFS)))) | ||||
| 	cuiR.HandleFunc("/program", cui.Program) | ||||
| 	cuiR.HandleFunc("/", cui.App) | ||||
| 	return cuiR | ||||
| } | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ | |||
| 
 | ||||
| $modal-content-width: 90vw; | ||||
| $footer-padding: 0.5rem 1.5rem 0.5rem; | ||||
| // $navbar-z: 0; | ||||
| 
 | ||||
| @import "../node_modules/bulma/bulma.sass"; | ||||
| @import "../node_modules/@creativebulma/bulma-tooltip/src/sass/index.sass"; | ||||
|  | @ -22,13 +23,6 @@ $footer-padding: 0.5rem 1.5rem 0.5rem; | |||
|   padding: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| #kconfigFrame{ | ||||
|   border: none;  | ||||
|   width: 100%;  | ||||
|   height: 100%; | ||||
|   display: block; | ||||
| } | ||||
| 
 | ||||
| .icn-spinner { | ||||
|   animation: spin-animation 1s infinite; | ||||
|   display: inline-block; | ||||
|  | @ -47,9 +41,3 @@ $footer-padding: 0.5rem 1.5rem 0.5rem; | |||
|     transform: rotate(359deg); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| .ace_selection { | ||||
|   // background: #999900 !important; | ||||
|   background: #7f7f00 !important; | ||||
| } | ||||
|  |  | |||
|  | @ -6,11 +6,6 @@ | |||
|       </p> | ||||
|     </div> | ||||
|   </footer> | ||||
|   <script src="{{.BaseUrl}}public/ace/js/ace.js" type="text/javascript" charset="utf-8"></script> | ||||
|   <script src="{{.BaseUrl}}public/ace/js/ext-beautify.js" type="text/javascript" charset="utf-8"></script> | ||||
|   <script src="{{.BaseUrl}}public/ace/js/ext-searchbox.js" type="text/javascript" charset="utf-8"></script> | ||||
|   {{template "base/script" .}} | ||||
|   <!-- <script src="/public/js/main.js"></script> --> | ||||
|   </body> | ||||
| </html> | ||||
| {{end}} | ||||
|  | @ -8,4 +8,6 @@ | |||
|     <link rel="stylesheet" href="{{.BaseUrl}}public/css/main.css"> | ||||
|   </head> | ||||
|   <body> | ||||
|    | ||||
| {{template "base/nav" .}} | ||||
| {{end}} | ||||
|  | @ -0,0 +1,76 @@ | |||
| 
 | ||||
| {{define "base/nav"}} | ||||
| <nav class="navbar" role="navigation" aria-label="main navigation"> | ||||
|   <div class="navbar-brand"> | ||||
|     <a class="navbar-item" href="{{.BaseUrl}}"> | ||||
|       <p><strong>{{.AppName}}</strong></p> | ||||
|     </a> | ||||
| 
 | ||||
|     <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample"> | ||||
|       <span aria-hidden="true"></span> | ||||
|       <span aria-hidden="true"></span> | ||||
|       <span aria-hidden="true"></span> | ||||
|     </a> | ||||
|   </div> | ||||
| 
 | ||||
|   <div id="navbarBasicExample" class="navbar-menu"> | ||||
|     {{/* | ||||
|     <div class="navbar-start"> | ||||
|       {{if .Actions}} | ||||
|       <a class="navbar-item"> | ||||
|         Actions | ||||
|       </a> | ||||
|       {{end}} | ||||
|       {{if .Integrations}} | ||||
|       <a class="navbar-item"> | ||||
|         Integrations | ||||
|       </a> | ||||
|       {{end}} | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="navbar-end"> | ||||
|       <div class="navbar-item"> | ||||
|         <div class="buttons"> | ||||
|           <a class="button is-primary"> | ||||
|             <strong>Sign up</strong> | ||||
|           </a> | ||||
|           <a class="button is-light"> | ||||
|             Log in | ||||
|           </a> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     */}} | ||||
|   </div> | ||||
| </nav> | ||||
| 
 | ||||
| {{/* not having pages | ||||
| <script> | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
| 
 | ||||
|   // Get all "navbar-burger" elements | ||||
|   const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); | ||||
| 
 | ||||
|   // Check if there are any navbar burgers | ||||
|   if ($navbarBurgers.length > 0) { | ||||
| 
 | ||||
|     // Add a click event on each of them | ||||
|     $navbarBurgers.forEach( el => { | ||||
|       el.addEventListener('click', () => { | ||||
| 
 | ||||
|         // Get the target from the "data-target" attribute | ||||
|         const target = el.dataset.target; | ||||
|         const $target = document.getElementById(target); | ||||
| 
 | ||||
|         // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" | ||||
|         el.classList.toggle('is-active'); | ||||
|         $target.classList.toggle('is-active'); | ||||
| 
 | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| }); | ||||
| */}} | ||||
| </script> | ||||
| {{end}} | ||||
|  | @ -1,4 +1,8 @@ | |||
| {{define "base/script"}} | ||||
| 
 | ||||
| <script src="{{.BaseUrl}}public/ace/js/ace.js" type="text/javascript" charset="utf-8"></script> | ||||
| <script src="{{.BaseUrl}}public/ace/js/ext-beautify.js" type="text/javascript" charset="utf-8"></script> | ||||
| <script src="{{.BaseUrl}}public/ace/js/ext-searchbox.js" type="text/javascript" charset="utf-8"></script> | ||||
| <script> | ||||
| window.ToolIsFollow = false; | ||||
| window.LastDelta = 0; | ||||
|  | @ -123,7 +127,7 @@ function setResult(){ | |||
|   result_editor.session.setMode("ace/mode/sh"); | ||||
|   result_editor.setReadOnly(true); | ||||
|   result_editor.setOptions({ | ||||
|     maxLines: Infinity | ||||
|     maxLines: 1000 | ||||
|   }); | ||||
|   {{if not .ResultBellow}} | ||||
|     document.addEventListener('keydown', function (e) { | ||||
|  |  | |||
|  | @ -1,42 +0,0 @@ | |||
| {{define "components/kconfig"}} | ||||
| <div class="modal" id="kconfig"> | ||||
|   <div class="modal-background"></div> | ||||
|   <div class="modal-card" style="max-width:960px"> | ||||
|     <header class="modal-card-head"> | ||||
|       <p class="modal-card-title">{{.AppName}} Config</p> | ||||
|       <button class="delete" aria-label="close" onclick="KconfigViewTog()"></button> | ||||
|     </header> | ||||
|     <section class="modal-card-body" style="height:80vh;"> | ||||
|       <iframe src="{{.BaseUrl}}kconfig/" id="kconfigFrame"></iframe> | ||||
|     </section> | ||||
|     <footer class="modal-card-foot"> | ||||
|       <div class="level"> | ||||
|         <div class="level-left"></div> | ||||
|         <div class="level-left"> | ||||
|           <button class="button" onclick="KconfigViewTog()">OK</button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </footer> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <script> | ||||
| document.addEventListener('keydown', function (e) { | ||||
|   if (e.key === "Escape"){ | ||||
|     let el = document.getElementById('kconfig') | ||||
|     el.classList.remove('is-active') | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| function KSubmit(){ | ||||
|   window.location.reload(); | ||||
|   window.ContentChanged = false | ||||
| } | ||||
| 
 | ||||
| function KconfigViewTog(){ | ||||
|   let el = document.getElementById('kconfig') | ||||
|   el.classList.toggle('is-active') | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| {{end}} | ||||
|  | @ -14,6 +14,26 @@ | |||
|       </li> | ||||
|       {{ end }} | ||||
|     </ul> | ||||
|     {{if .Onitachi}} | ||||
|     <p class="menu-label"> | ||||
|       Programs | ||||
|     </p> | ||||
|     <ul class="menu-list"> | ||||
|       {{- range $key, $value := .Onitachi -}} | ||||
|       <li> | ||||
|         <a class="has-tooltip-arrow" data-tooltip="{{$value.Cmd}}" href="{{$.BaseUrl}}program?name={{.Name}}"> | ||||
|           <div class="level"> | ||||
|           <span>{{$key}}</span> | ||||
|             <div class="tags has-addons"> | ||||
|               <span class="tag">State</span> | ||||
|               <span class="tag {{$value.State|state_class}}">{{$value.State}}</span> | ||||
|             </div> | ||||
|           </div> | ||||
|         </a> | ||||
|       </li> | ||||
|       {{- end -}} | ||||
|     </ul> | ||||
|     {{end}} | ||||
|     {{if not .HideConfig}} | ||||
|     <p class="menu-label"> | ||||
|       System | ||||
|  |  | |||
|  | @ -1,29 +1,10 @@ | |||
| {{define "home"}} | ||||
| {{template "base/header" .}} | ||||
| {{template "components/error" .}} | ||||
| {{template "components/kconfig" .}} | ||||
| <script> | ||||
| var Active = "{{.File.Name}}"; | ||||
| </script> | ||||
| 
 | ||||
| <nav class="level py-1"> | ||||
|   <div class="level-left"> | ||||
|     <div class="level-item"> | ||||
|       <a class="title is-size-4 ml-2" href="{{.BaseUrl}}"> | ||||
|         {{.AppName}} | ||||
|       </a> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="level-right"> | ||||
|     {{ block "links" .}} | ||||
|     <a class="button is-white level-item"  | ||||
|       href="https://kumoly.io/tools/configui/issues" | ||||
|     >Report Bug</a> | ||||
|     {{end}} | ||||
|     <p class="level-item mr-2"></p> | ||||
|   </div> | ||||
| </nav> | ||||
| 
 | ||||
| <div class="columns"> | ||||
|   <div class="column is-one-quarter"> | ||||
|     <div class="box"> | ||||
|  | @ -57,7 +38,6 @@ var Active = "{{.File.Name}}"; | |||
|     {{end}} | ||||
|     <div class="box has-text-centered"> | ||||
|       <a href="{{.BaseUrl}}api/export" class="button is-small">Export Files</a> | ||||
|       {{if not .HideConfig}}<a onclick="KconfigViewTog()" class="button is-small">Config</a>{{end}} | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="column"> | ||||
|  | @ -100,5 +80,6 @@ async function toolDoIntegration(name){ | |||
| {{if not .ResultBellow}} | ||||
| {{template "components/result" .}} | ||||
| {{end}} | ||||
| {{template "base/script" .}} | ||||
| {{template "base/footer" .}} | ||||
| {{end}} | ||||
|  | @ -0,0 +1,73 @@ | |||
| {{define "muzan"}} | ||||
| {{template "base/header" .}} | ||||
| {{template "components/error" .}} | ||||
| <script> | ||||
| var Active = "{{.File.Name}}"; | ||||
| </script> | ||||
| 
 | ||||
| <div class="columns"> | ||||
|   <div class="column is-one-quarter"> | ||||
|     <div class="box"> | ||||
|       {{template "components/menu" .}} | ||||
|     </div> | ||||
|     {{if .Actions}} | ||||
|     <div class="box"> | ||||
|       <p class="menu-label has-text-left">Actions</p> | ||||
|       <div class="buttons are-small"> | ||||
|       {{- range .Actions -}} | ||||
|         <button class="button has-tooltip-arrow" id="actbtn-{{.Name|normal}}" | ||||
|           data-tooltip="{{.Cmd}}" onclick="toolDoAction('{{.Name}}')"> | ||||
|           <span>{{- .Name -}}</span> | ||||
|         </button> | ||||
|       {{- end -}} | ||||
|       </div> | ||||
|     </div> | ||||
|     {{end}} | ||||
|     {{if .Integrations}} | ||||
|     <div class="box"> | ||||
|       <p class="menu-label has-text-left">Integrations</p> | ||||
|       <div class="buttons are-small"> | ||||
|       {{- range .Integrations -}} | ||||
|         <button class="button has-tooltip-arrow" id="itgbtn-{{.Name|normal}}" | ||||
|           data-tooltip="{{.Description}}" onclick="toolDoIntegration('{{.Name}}')"> | ||||
|           <span>{{- .Name -}}</span> | ||||
|         </button> | ||||
|       {{- end -}} | ||||
|       </div> | ||||
|     </div> | ||||
|     {{end}} | ||||
|     <div class="box has-text-centered"> | ||||
|       <a href="{{.BaseUrl}}api/export" class="button is-small">Export Files</a> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="column"> | ||||
|     <div class="box"> | ||||
|       Home | ||||
|       {{/* {{template "components/editor" .}} */}} | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <script> | ||||
| async function toolDoAction(name){ | ||||
|   let el = document.getElementById('actbtn-'+name.replaceAll(" ","-")); | ||||
|   el.classList.add("is-loading") | ||||
|   el.classList.remove("has-tooltip-arrow") | ||||
|   await DoAction('action',name) | ||||
|   el.classList.remove("is-loading") | ||||
|   el.classList.add("has-tooltip-arrow") | ||||
|   {{if not .ResultBellow}}ResultViewTog(){{end}} | ||||
| } | ||||
| 
 | ||||
| async function toolDoIntegration(name){ | ||||
|   let el = document.getElementById('itgbtn-'+name.replaceAll(" ","-")); | ||||
|   el.classList.add("is-loading") | ||||
|   el.classList.remove("has-tooltip-arrow") | ||||
|   await DoAction('integration',name) | ||||
|   el.classList.remove("is-loading") | ||||
|   el.classList.add("has-tooltip-arrow") | ||||
|   {{if not .ResultBellow}}ResultViewTog(){{end}} | ||||
| } | ||||
| </script> | ||||
| {{template "base/footer" .}} | ||||
| {{end}} | ||||
		Loading…
	
		Reference in New Issue