mirror of
https://github.com/go-micro/go-micro.git
synced 2025-07-12 22:41:07 +02:00
[feature] dashboard (#2361)
This commit is contained in:
15
cmd/dashboard/README.md
Normal file
15
cmd/dashboard/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Dashboard
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go install github.com/asim/go-micro/cmd/dashboard/v4@latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
dashboard --registry etcd --server_address :4000
|
||||
```
|
||||
|
||||
Visit: [http://localhost:4000](http://localhost:4000)(deafult admin@123456)
|
37
cmd/dashboard/config/config.go
Normal file
37
cmd/dashboard/config/config.go
Normal file
@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Address string
|
||||
Auth AuthConfig
|
||||
CORS CORSConfig
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
Username string
|
||||
Password string
|
||||
TokenSecret string
|
||||
TokenExpiration time.Duration
|
||||
}
|
||||
|
||||
type CORSConfig struct {
|
||||
Enable bool `toml:"enable"`
|
||||
Origin string `toml:"origin"`
|
||||
}
|
||||
|
||||
func GetConfig() Config {
|
||||
return *_cfg
|
||||
}
|
||||
|
||||
func GetServerConfig() ServerConfig {
|
||||
return _cfg.Server
|
||||
}
|
||||
|
||||
func GetAuthConfig() AuthConfig {
|
||||
return _cfg.Server.Auth
|
||||
}
|
87
cmd/dashboard/config/load.go
Normal file
87
cmd/dashboard/config/load.go
Normal file
@ -0,0 +1,87 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/util"
|
||||
"github.com/asim/go-micro/plugins/config/encoder/toml/v4"
|
||||
"github.com/asim/go-micro/plugins/config/encoder/yaml/v4"
|
||||
"github.com/pkg/errors"
|
||||
"go-micro.dev/v4/config"
|
||||
"go-micro.dev/v4/config/reader"
|
||||
"go-micro.dev/v4/config/reader/json"
|
||||
"go-micro.dev/v4/config/source/env"
|
||||
"go-micro.dev/v4/config/source/file"
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
// internal instance of Config
|
||||
var _cfg *Config = &Config{
|
||||
Server: ServerConfig{
|
||||
Auth: AuthConfig{
|
||||
Username: "admin",
|
||||
Password: "123456",
|
||||
TokenSecret: "modifyme",
|
||||
TokenExpiration: 24 * time.Hour,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Load will load configurations and update it when changed
|
||||
func Load() error {
|
||||
var configor config.Config
|
||||
var err error
|
||||
switch strings.ToLower(os.Getenv("CONFIG_TYPE")) {
|
||||
case "toml":
|
||||
filename := "config.toml"
|
||||
if name := os.Getenv("CONFIG_FILE"); len(name) > 0 {
|
||||
filename = name
|
||||
}
|
||||
configor, err = config.NewConfig(
|
||||
config.WithSource(file.NewSource(file.WithPath(filename))),
|
||||
config.WithReader(json.NewReader(reader.WithEncoder(toml.NewEncoder()))),
|
||||
)
|
||||
case "yaml":
|
||||
filename := "config.yaml"
|
||||
if name := os.Getenv("CONFIG_FILE"); len(name) > 0 {
|
||||
filename = name
|
||||
}
|
||||
configor, err = config.NewConfig(
|
||||
config.WithSource(file.NewSource(file.WithPath(filename))),
|
||||
config.WithReader(json.NewReader(reader.WithEncoder(yaml.NewEncoder()))),
|
||||
)
|
||||
default:
|
||||
configor, err = config.NewConfig(
|
||||
config.WithSource(env.NewSource()),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "configor.New")
|
||||
}
|
||||
if err := configor.Load(); err != nil {
|
||||
return errors.Wrap(err, "configor.Load")
|
||||
}
|
||||
if err := configor.Scan(_cfg); err != nil {
|
||||
return errors.Wrap(err, "configor.Scan")
|
||||
}
|
||||
w, err := configor.Watch()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "configor.Watch")
|
||||
}
|
||||
util.GoSafe(func() {
|
||||
for {
|
||||
v, err := w.Next()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
if err := v.Scan(_cfg); err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
125
cmd/dashboard/go.mod
Normal file
125
cmd/dashboard/go.mod
Normal file
@ -0,0 +1,125 @@
|
||||
module github.com/asim/go-micro/cmd/dashboard/v4
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/asim/go-micro/plugins/client/grpc/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/client/http/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/client/mucp/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/config/encoder/toml/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/config/encoder/yaml/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/registry/consul/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/registry/etcd/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/registry/eureka/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/registry/gossip/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/registry/kubernetes/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/registry/nacos/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/registry/nats/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/registry/zookeeper/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/asim/go-micro/plugins/server/http/v4 v4.0.0-20211118090700-90b3e4af0b58
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/gin-gonic/gin v1.7.6
|
||||
github.com/pkg/errors v0.9.1
|
||||
go-micro.dev/v4 v4.4.0
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.976 // indirect
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect
|
||||
github.com/bitly/go-simplejson v0.5.0 // indirect
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.3.1 // indirect
|
||||
github.com/go-git/go-git/v5 v5.4.2 // indirect
|
||||
github.com/go-playground/locales v0.13.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||
github.com/go-zookeeper/zk v1.0.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/btree v1.0.0 // indirect
|
||||
github.com/google/uuid v1.2.0 // indirect
|
||||
github.com/hashicorp/consul/api v1.9.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
|
||||
github.com/hashicorp/go-hclog v0.12.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0 // indirect
|
||||
github.com/hashicorp/go-msgpack v0.5.3 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.0 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.1 // indirect
|
||||
github.com/hashicorp/memberlist v0.2.2 // indirect
|
||||
github.com/hashicorp/serf v0.9.5 // indirect
|
||||
github.com/hudl/fargo v1.3.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.11 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
github.com/leodido/go-urn v1.2.0 // indirect
|
||||
github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f // indirect
|
||||
github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/miekg/dns v1.1.43 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/hashstructure v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.3.3 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.0.0-Beta.1 // indirect
|
||||
github.com/nats-io/jwt v1.1.0 // indirect
|
||||
github.com/nats-io/nats.go v1.10.0 // indirect
|
||||
github.com/nats-io/nkeys v0.1.4 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect
|
||||
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.17.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
|
||||
google.golang.org/grpc v1.42.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/gcfg.v1 v1.2.3 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
1122
cmd/dashboard/go.sum
Normal file
1122
cmd/dashboard/go.sum
Normal file
File diff suppressed because it is too large
Load Diff
83
cmd/dashboard/handler/account/service.go
Normal file
83
cmd/dashboard/handler/account/service.go
Normal file
@ -0,0 +1,83 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/config"
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/handler/route"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
)
|
||||
|
||||
type service struct{}
|
||||
|
||||
func NewRouteRegistrar() route.Registrar {
|
||||
return service{}
|
||||
}
|
||||
|
||||
func (s service) RegisterAuthRoute(router gin.IRoutes) {
|
||||
router.GET("/api/account/profile", s.Profile)
|
||||
}
|
||||
|
||||
func (s service) RegisterNonAuthRoute(router gin.IRoutes) {
|
||||
router.POST("/api/account/login", s.Login)
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type loginResponse struct {
|
||||
Token string `json:"token" binding:"required"`
|
||||
}
|
||||
|
||||
// @Tags Account
|
||||
// @ID account_login
|
||||
// @Param input body loginRequest true "request"
|
||||
// @Success 200 {object} loginResponse "success"
|
||||
// @Failure 400 {object} string
|
||||
// @Failure 401 {object} string
|
||||
// @Failure 500 {object} string
|
||||
// @Router /api/account/login [post]
|
||||
func (s *service) Login(ctx *gin.Context) {
|
||||
var req loginRequest
|
||||
if err := ctx.ShouldBindJSON(&req); nil != err {
|
||||
ctx.Render(400, render.String{Format: err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Username != config.GetServerConfig().Auth.Username ||
|
||||
req.Password != config.GetServerConfig().Auth.Password {
|
||||
ctx.Render(400, render.String{Format: "incorrect username or password"})
|
||||
return
|
||||
}
|
||||
claims := jwt.StandardClaims{
|
||||
Subject: req.Username,
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: time.Now().Add(config.GetAuthConfig().TokenExpiration).Unix(),
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signedToken, err := token.SignedString([]byte(config.GetAuthConfig().TokenSecret))
|
||||
if err != nil {
|
||||
ctx.Render(400, render.String{Format: err.Error()})
|
||||
return
|
||||
}
|
||||
ctx.JSON(200, loginResponse{Token: signedToken})
|
||||
}
|
||||
|
||||
type profileResponse struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Account
|
||||
// @ID account_profile
|
||||
// @Success 200 {object} profileResponse "success"
|
||||
// @Failure 400 {object} string
|
||||
// @Failure 401 {object} string
|
||||
// @Failure 500 {object} string
|
||||
// @Router /api/account/profile [get]
|
||||
func (s *service) Profile(ctx *gin.Context) {
|
||||
ctx.JSON(200, profileResponse{Name: config.GetAuthConfig().Username})
|
||||
}
|
9
cmd/dashboard/handler/client/models.go
Normal file
9
cmd/dashboard/handler/client/models.go
Normal file
@ -0,0 +1,9 @@
|
||||
package client
|
||||
|
||||
type callRequest struct {
|
||||
Service string `json:"service" binding:"required"`
|
||||
Version string `json:"version"`
|
||||
Endpoint string `json:"endpoint" binding:"required"`
|
||||
Request string `json:"request"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
}
|
106
cmd/dashboard/handler/client/service.go
Normal file
106
cmd/dashboard/handler/client/service.go
Normal file
@ -0,0 +1,106 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/handler/route"
|
||||
cgrpc "github.com/asim/go-micro/plugins/client/grpc/v4"
|
||||
chttp "github.com/asim/go-micro/plugins/client/http/v4"
|
||||
cmucp "github.com/asim/go-micro/plugins/client/mucp/v4"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/errors"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/selector"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
client client.Client
|
||||
registry registry.Registry
|
||||
}
|
||||
|
||||
func NewRouteRegistrar(client client.Client, registry registry.Registry) route.Registrar {
|
||||
return service{client: client, registry: registry}
|
||||
}
|
||||
|
||||
func (s service) RegisterAuthRoute(router gin.IRoutes) {
|
||||
router.POST("/api/client/endpoint/call", s.CallEndpoint)
|
||||
}
|
||||
|
||||
func (s service) RegisterNonAuthRoute(router gin.IRoutes) {
|
||||
}
|
||||
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Client
|
||||
// @ID client_callEndpoint
|
||||
// @Param input body callRequest true "request"
|
||||
// @Success 200 {object} object "success"
|
||||
// @Failure 400 {object} string
|
||||
// @Failure 401 {object} string
|
||||
// @Failure 500 {object} string
|
||||
// @Router /api/client/endpoint/call [post]
|
||||
func (s *service) CallEndpoint(ctx *gin.Context) {
|
||||
var req callRequest
|
||||
if err := ctx.ShouldBindJSON(&req); nil != err {
|
||||
ctx.Render(400, render.String{Format: err.Error()})
|
||||
return
|
||||
}
|
||||
var callReq json.RawMessage
|
||||
if len(req.Request) > 0 {
|
||||
if err := json.Unmarshal([]byte(req.Request), &callReq); err != nil {
|
||||
ctx.Render(400, render.String{Format: "parse request failed: %s", Data: []interface{}{err.Error()}})
|
||||
return
|
||||
}
|
||||
}
|
||||
services, err := s.registry.GetService(req.Service)
|
||||
if err != nil {
|
||||
ctx.Render(400, render.String{Format: err.Error()})
|
||||
return
|
||||
}
|
||||
var c client.Client
|
||||
for _, srv := range services {
|
||||
if len(req.Version) > 0 && req.Version != srv.Version {
|
||||
continue
|
||||
}
|
||||
if len(srv.Nodes) == 0 {
|
||||
ctx.Render(400, render.String{Format: "service node not found"})
|
||||
return
|
||||
}
|
||||
switch srv.Nodes[0].Metadata["server"] {
|
||||
case "grpc":
|
||||
c = cgrpc.NewClient()
|
||||
case "http":
|
||||
c = chttp.NewClient()
|
||||
case "mucp":
|
||||
c = cmucp.NewClient()
|
||||
default:
|
||||
c = s.client
|
||||
}
|
||||
break
|
||||
}
|
||||
if c == nil {
|
||||
ctx.Render(400, render.String{Format: "service not found"})
|
||||
return
|
||||
}
|
||||
var resp json.RawMessage
|
||||
callOpts := []client.CallOption{}
|
||||
if len(req.Version) > 0 {
|
||||
callOpts = append(callOpts, client.WithSelectOption(selector.WithFilter(selector.FilterVersion(req.Version))))
|
||||
}
|
||||
requestOpts := []client.RequestOption{client.WithContentType("application/json")}
|
||||
if req.Timeout > 0 {
|
||||
callOpts = append(callOpts, client.WithRequestTimeout(time.Duration(req.Timeout)*time.Second))
|
||||
}
|
||||
if err := c.Call(context.TODO(), client.NewRequest(req.Service, req.Endpoint, callReq, requestOpts...), &resp, callOpts...); err != nil {
|
||||
if merr := errors.Parse(err.Error()); merr != nil {
|
||||
ctx.JSON(200, gin.H{"success": false, "error": merr})
|
||||
} else {
|
||||
ctx.JSON(200, gin.H{"success": false, "error": err.Error})
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.JSON(200, resp)
|
||||
}
|
39
cmd/dashboard/handler/handler.go
Normal file
39
cmd/dashboard/handler/handler.go
Normal file
@ -0,0 +1,39 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/config"
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/handler/account"
|
||||
handlerclient "github.com/asim/go-micro/cmd/dashboard/v4/handler/client"
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/handler/registry"
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/handler/route"
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/handler/statistics"
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/web"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go-micro.dev/v4/client"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Client client.Client
|
||||
Router *gin.Engine
|
||||
}
|
||||
|
||||
func Register(opts Options) error {
|
||||
router := opts.Router
|
||||
if err := web.RegisterRoute(router); err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg := config.GetServerConfig().CORS; cfg.Enable {
|
||||
router.Use(CorsHandler(cfg.Origin))
|
||||
}
|
||||
authRouter := router.Group("").Use(AuthRequired())
|
||||
for _, r := range []route.Registrar{
|
||||
account.NewRouteRegistrar(),
|
||||
handlerclient.NewRouteRegistrar(opts.Client, opts.Client.Options().Registry),
|
||||
registry.NewRouteRegistrar(opts.Client.Options().Registry),
|
||||
statistics.NewRouteRegistrar(opts.Client.Options().Registry),
|
||||
} {
|
||||
r.RegisterNonAuthRoute(router)
|
||||
r.RegisterAuthRoute(authRouter)
|
||||
}
|
||||
return nil
|
||||
}
|
51
cmd/dashboard/handler/middleware.go
Normal file
51
cmd/dashboard/handler/middleware.go
Normal file
@ -0,0 +1,51 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/config"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AuthRequired() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
if ctx.Request.Method == "OPTIONS" {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
tokenString := ctx.GetHeader("Authorization")
|
||||
if len(tokenString) == 0 || !strings.HasPrefix(tokenString, "Bearer ") {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, "")
|
||||
return
|
||||
}
|
||||
tokenString = tokenString[7:]
|
||||
claims := jwt.StandardClaims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, &claims, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte(config.GetAuthConfig().TokenSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusUnauthorized, err)
|
||||
}
|
||||
if !token.Valid {
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
}
|
||||
ctx.Set("username", claims.Subject)
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func CorsHandler(allowOrigin string) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
ctx.Header("Access-Control-Allow-Origin", allowOrigin)
|
||||
ctx.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, token")
|
||||
ctx.Header("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT, OPTIONS")
|
||||
ctx.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
|
||||
ctx.Header("Access-Control-Allow-Credentials", "true")
|
||||
if ctx.Request.Method == "OPTIONS" {
|
||||
ctx.AbortWithStatus(http.StatusNoContent)
|
||||
}
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
62
cmd/dashboard/handler/registry/models.go
Normal file
62
cmd/dashboard/handler/registry/models.go
Normal file
@ -0,0 +1,62 @@
|
||||
package registry
|
||||
|
||||
import "go-micro.dev/v4/registry"
|
||||
|
||||
type registryServiceSummary struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Versions []string `json:"versions,omitempty"`
|
||||
}
|
||||
|
||||
type getServiceListResponse struct {
|
||||
Services []registryServiceSummary `json:"services" binding:"required"`
|
||||
}
|
||||
|
||||
type registryService struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Version string `json:"version" binding:"required"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Endpoints []registryEndpoint `json:"endpoints,omitempty"`
|
||||
Nodes []registryNode `json:"nodes,omitempty"`
|
||||
}
|
||||
|
||||
type registryEndpoint struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Request registryValue `json:"request" binding:"required"`
|
||||
Response registryValue `json:"response"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type registryNode struct {
|
||||
Id string `json:"id" binding:"required"`
|
||||
Address string `json:"address" binding:"required"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type registryValue struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Values []registryValue `json:"values,omitempty"`
|
||||
}
|
||||
|
||||
type getServiceDetailResponse struct {
|
||||
Services []registryService `json:"services"`
|
||||
}
|
||||
|
||||
type getServiceEndpointsResponse struct {
|
||||
Endpoints []registryEndpoint `json:"endpoints"`
|
||||
}
|
||||
|
||||
func convertRegistryValue(v *registry.Value) registryValue {
|
||||
if v == nil {
|
||||
return registryValue{}
|
||||
}
|
||||
res := registryValue{
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
Values: make([]registryValue, 0, len(v.Values)),
|
||||
}
|
||||
for _, vv := range v.Values {
|
||||
res.Values = append(res.Values, convertRegistryValue(vv))
|
||||
}
|
||||
return res
|
||||
}
|
161
cmd/dashboard/handler/registry/service.go
Normal file
161
cmd/dashboard/handler/registry/service.go
Normal file
@ -0,0 +1,161 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/handler/route"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
registry registry.Registry
|
||||
}
|
||||
|
||||
func NewRouteRegistrar(registry registry.Registry) route.Registrar {
|
||||
return service{registry: registry}
|
||||
}
|
||||
|
||||
func (s service) RegisterAuthRoute(router gin.IRoutes) {
|
||||
router.GET("/api/registry/services", s.GetServices)
|
||||
router.GET("/api/registry/service", s.GetServiceDetail)
|
||||
router.GET("/api/registry/service/endpoints", s.GetServiceEndpoints)
|
||||
}
|
||||
|
||||
func (s service) RegisterNonAuthRoute(router gin.IRoutes) {
|
||||
}
|
||||
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Registry
|
||||
// @ID registry_getServices
|
||||
// @Success 200 {object} getServiceListResponse
|
||||
// @Failure 400 {object} string
|
||||
// @Failure 401 {object} string
|
||||
// @Failure 500 {object} string
|
||||
// @Router /api/registry/services [get]
|
||||
func (s *service) GetServices(ctx *gin.Context) {
|
||||
services, err := s.registry.ListServices()
|
||||
if err != nil {
|
||||
ctx.Render(500, render.String{Format: err.Error()})
|
||||
return
|
||||
}
|
||||
tmp := make(map[string][]string)
|
||||
resp := getServiceListResponse{Services: make([]registryServiceSummary, 0, len(services))}
|
||||
for _, s := range services {
|
||||
if sr, ok := tmp[s.Name]; ok {
|
||||
sr = append(sr, s.Version)
|
||||
tmp[s.Name] = sr
|
||||
} else {
|
||||
tmp[s.Name] = []string{s.Version}
|
||||
}
|
||||
}
|
||||
for k, v := range tmp {
|
||||
sort.Strings(v)
|
||||
resp.Services = append(resp.Services, registryServiceSummary{Name: k, Versions: v})
|
||||
}
|
||||
sort.Slice(resp.Services, func(i, j int) bool {
|
||||
return resp.Services[i].Name < resp.Services[j].Name
|
||||
})
|
||||
ctx.JSON(200, resp)
|
||||
}
|
||||
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Registry
|
||||
// @ID registry_getServiceDetail
|
||||
// @Param name query string true "service name"
|
||||
// @Param version query string false "service version"
|
||||
// @Success 200 {object} getServiceDetailResponse
|
||||
// @Failure 400 {object} string
|
||||
// @Failure 401 {object} string
|
||||
// @Failure 500 {object} string
|
||||
// @Router /api/registry/service [get]
|
||||
func (s *service) GetServiceDetail(ctx *gin.Context) {
|
||||
name := ctx.Query("name")
|
||||
if len(name) == 0 {
|
||||
ctx.Render(400, render.String{Format: "service name required"})
|
||||
return
|
||||
}
|
||||
services, err := s.registry.GetService(name)
|
||||
if err != nil {
|
||||
ctx.Render(500, render.String{Format: err.Error()})
|
||||
return
|
||||
}
|
||||
version := ctx.Query("version")
|
||||
resp := getServiceDetailResponse{Services: make([]registryService, 0, len(services))}
|
||||
for _, s := range services {
|
||||
if len(version) > 0 && s.Version != version {
|
||||
continue
|
||||
}
|
||||
endpoints := make([]registryEndpoint, 0, len(s.Endpoints))
|
||||
for _, e := range s.Endpoints {
|
||||
endpoints = append(endpoints, registryEndpoint{
|
||||
Name: e.Name,
|
||||
Request: convertRegistryValue(e.Request),
|
||||
Response: convertRegistryValue(e.Response),
|
||||
Metadata: e.Metadata,
|
||||
})
|
||||
}
|
||||
nodes := make([]registryNode, 0, len(s.Nodes))
|
||||
for _, n := range s.Nodes {
|
||||
nodes = append(nodes, registryNode{
|
||||
Id: n.Id,
|
||||
Address: n.Address,
|
||||
Metadata: n.Metadata,
|
||||
})
|
||||
}
|
||||
resp.Services = append(resp.Services, registryService{
|
||||
Name: s.Name,
|
||||
Version: s.Version,
|
||||
Metadata: s.Metadata,
|
||||
Endpoints: endpoints,
|
||||
Nodes: nodes,
|
||||
})
|
||||
}
|
||||
ctx.JSON(200, resp)
|
||||
}
|
||||
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Registry
|
||||
// @ID registry_getServiceEndpoints
|
||||
// @Param name query string true "service name"
|
||||
// @Param version query string false "service version"
|
||||
// @Success 200 {object} getServiceEndpointsResponse
|
||||
// @Failure 400 {object} string
|
||||
// @Failure 401 {object} string
|
||||
// @Failure 500 {object} string
|
||||
// @Router /api/registry/service/endpoints [get]
|
||||
func (s *service) GetServiceEndpoints(ctx *gin.Context) {
|
||||
name := ctx.Query("name")
|
||||
if len(name) == 0 {
|
||||
ctx.Render(400, render.String{Format: "service name required"})
|
||||
return
|
||||
}
|
||||
services, err := s.registry.GetService(name)
|
||||
if err != nil {
|
||||
ctx.Render(500, render.String{Format: err.Error()})
|
||||
return
|
||||
}
|
||||
version := ctx.Query("version")
|
||||
resp := getServiceEndpointsResponse{}
|
||||
for _, s := range services {
|
||||
if s.Version != version {
|
||||
continue
|
||||
}
|
||||
endpoints := make([]registryEndpoint, 0, len(s.Endpoints))
|
||||
for _, e := range s.Endpoints {
|
||||
if e.Name == "Func" {
|
||||
continue
|
||||
}
|
||||
endpoints = append(endpoints, registryEndpoint{
|
||||
Name: e.Name,
|
||||
Request: convertRegistryValue(e.Request),
|
||||
Response: convertRegistryValue(e.Response),
|
||||
Metadata: e.Metadata,
|
||||
})
|
||||
}
|
||||
resp.Endpoints = endpoints
|
||||
break
|
||||
}
|
||||
ctx.JSON(200, resp)
|
||||
}
|
8
cmd/dashboard/handler/route/route.go
Normal file
8
cmd/dashboard/handler/route/route.go
Normal file
@ -0,0 +1,8 @@
|
||||
package route
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type Registrar interface {
|
||||
RegisterAuthRoute(gin.IRoutes)
|
||||
RegisterNonAuthRoute(gin.IRoutes)
|
||||
}
|
16
cmd/dashboard/handler/statistics/models.go
Normal file
16
cmd/dashboard/handler/statistics/models.go
Normal file
@ -0,0 +1,16 @@
|
||||
package statistics
|
||||
|
||||
type getSummaryResponse struct {
|
||||
Registry registrySummary `json:"registry"`
|
||||
Services servicesSummary `json:"services"`
|
||||
}
|
||||
|
||||
type registrySummary struct {
|
||||
Type string `json:"type"`
|
||||
Addrs []string `json:"addrs"`
|
||||
}
|
||||
|
||||
type servicesSummary struct {
|
||||
Count int `json:"count"`
|
||||
NodesCount int `json:"nodes_count"`
|
||||
}
|
56
cmd/dashboard/handler/statistics/service.go
Normal file
56
cmd/dashboard/handler/statistics/service.go
Normal file
@ -0,0 +1,56 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/handler/route"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
registry registry.Registry
|
||||
}
|
||||
|
||||
func NewRouteRegistrar(registry registry.Registry) route.Registrar {
|
||||
return service{registry: registry}
|
||||
}
|
||||
|
||||
func (s service) RegisterAuthRoute(router gin.IRoutes) {
|
||||
router.GET("/api/summary", s.GetSummary)
|
||||
}
|
||||
|
||||
func (s service) RegisterNonAuthRoute(router gin.IRoutes) {
|
||||
}
|
||||
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Statistics
|
||||
// @ID statistics_getSummary
|
||||
// @Success 200 {object} getSummaryResponse
|
||||
// @Failure 400 {object} string
|
||||
// @Failure 401 {object} string
|
||||
// @Failure 500 {object} string
|
||||
// @Router /api/summary [get]
|
||||
func (s *service) GetSummary(ctx *gin.Context) {
|
||||
services, err := s.registry.ListServices()
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(500, err)
|
||||
}
|
||||
servicesByName := make(map[string]struct{})
|
||||
var servicesNodesCount int
|
||||
for _, s := range services {
|
||||
if _, ok := servicesByName[s.Name]; !ok {
|
||||
servicesByName[s.Name] = struct{}{}
|
||||
}
|
||||
servicesNodesCount += len(s.Nodes)
|
||||
}
|
||||
var resp = getSummaryResponse{
|
||||
Registry: registrySummary{
|
||||
Type: s.registry.String(),
|
||||
Addrs: s.registry.Options().Addrs,
|
||||
},
|
||||
Services: servicesSummary{
|
||||
Count: len(servicesByName),
|
||||
NodesCount: servicesNodesCount,
|
||||
},
|
||||
}
|
||||
ctx.JSON(200, resp)
|
||||
}
|
39
cmd/dashboard/main.go
Normal file
39
cmd/dashboard/main.go
Normal file
@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/config"
|
||||
"github.com/asim/go-micro/cmd/dashboard/v4/handler"
|
||||
mhttp "github.com/asim/go-micro/plugins/server/http/v4"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go-micro.dev/v4"
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
Name = "go.micro.dashboard"
|
||||
Version = "1.0.0"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := config.Load(); err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
srv := micro.NewService(micro.Server(mhttp.NewServer()))
|
||||
opts := []micro.Option{
|
||||
micro.Name(Name),
|
||||
micro.Version(Version),
|
||||
}
|
||||
srv.Init(opts...)
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery(), gin.Logger())
|
||||
if err := handler.Register(handler.Options{Client: srv.Client(), Router: router}); err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
if err := micro.RegisterHandler(srv.Server(), router); err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
if err := srv.Run(); err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}
|
12
cmd/dashboard/plugins.go
Normal file
12
cmd/dashboard/plugins.go
Normal file
@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/asim/go-micro/plugins/registry/consul/v4"
|
||||
_ "github.com/asim/go-micro/plugins/registry/etcd/v4"
|
||||
_ "github.com/asim/go-micro/plugins/registry/eureka/v4"
|
||||
_ "github.com/asim/go-micro/plugins/registry/gossip/v4"
|
||||
_ "github.com/asim/go-micro/plugins/registry/kubernetes/v4"
|
||||
_ "github.com/asim/go-micro/plugins/registry/nacos/v4"
|
||||
_ "github.com/asim/go-micro/plugins/registry/nats/v4"
|
||||
_ "github.com/asim/go-micro/plugins/registry/zookeeper/v4"
|
||||
)
|
22
cmd/dashboard/util/goroutine.go
Normal file
22
cmd/dashboard/util/goroutine.go
Normal file
@ -0,0 +1,22 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
// GoSafe will run func in goroutine safely, avoid crash from unexpected panic
|
||||
func GoSafe(fn func()) {
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
logger.Errorf("[panic]%v\n%s", e, debug.Stack())
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}()
|
||||
}
|
144
cmd/dashboard/web/ab0x.go
Normal file
144
cmd/dashboard/web/ab0x.go
Normal file
@ -0,0 +1,144 @@
|
||||
// Code generated by fileb0x at "2021-11-22 18:03:08.6921519 +0800 CST m=+0.057344801" from config file "b0x.yaml" DO NOT EDIT.
|
||||
// modification hash(781cbd1c3197446ff0f2b64f4796afe6.8be3f833d63e3c844663716446e13a42)
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
var (
|
||||
// CTX is a context for webdav vfs
|
||||
CTX = context.Background()
|
||||
|
||||
// FS is a virtual memory file system
|
||||
FS = webdav.NewMemFS()
|
||||
|
||||
// Handler is used to server files through a http handler
|
||||
Handler *webdav.Handler
|
||||
|
||||
// HTTP is the http file system
|
||||
HTTP http.FileSystem = new(HTTPFS)
|
||||
)
|
||||
|
||||
// HTTPFS implements http.FileSystem
|
||||
type HTTPFS struct {
|
||||
// Prefix allows to limit the path of all requests. F.e. a prefix "css" would allow only calls to /css/*
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := CTX.Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = FS.Mkdir(CTX, "/assets/", 0777)
|
||||
if err != nil && err != os.ErrExist {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Handler = &webdav.Handler{
|
||||
FileSystem: FS,
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Open a file
|
||||
func (hfs *HTTPFS) Open(path string) (http.File, error) {
|
||||
path = hfs.Prefix + path
|
||||
|
||||
f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ReadFile is adapTed from ioutil
|
||||
func ReadFile(path string) ([]byte, error) {
|
||||
f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, 0, bytes.MinRead))
|
||||
|
||||
// If the buffer overflows, we will get bytes.ErrTooLarge.
|
||||
// Return that as an error. Any other panic remains.
|
||||
defer func() {
|
||||
e := recover()
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
|
||||
err = panicErr
|
||||
} else {
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
_, err = buf.ReadFrom(f)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
// WriteFile is adapTed from ioutil
|
||||
func WriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||
f, err := FS.OpenFile(CTX, filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := f.Write(data)
|
||||
if err == nil && n < len(data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
if err1 := f.Close(); err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// WalkDirs looks for files in the given dir and returns a list of files in it
|
||||
// usage for all files in the b0x: WalkDirs("", false)
|
||||
func WalkDirs(name string, includeDirsInList bool, files ...string) ([]string, error) {
|
||||
f, err := FS.OpenFile(CTX, name, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileInfos, err := f.Readdir(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, info := range fileInfos {
|
||||
filename := path.Join(name, info.Name())
|
||||
|
||||
if includeDirsInList || !info.IsDir() {
|
||||
files = append(files, filename)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
files, err = WalkDirs(filename, includeDirsInList, files...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
30
cmd/dashboard/web/b0xfile__449.ea3505f8c3a78bc5ec51.js.go
Normal file
30
cmd/dashboard/web/b0xfile__449.ea3505f8c3a78bc5ec51.js.go
Normal file
File diff suppressed because one or more lines are too long
30
cmd/dashboard/web/b0xfile__assets_color.less.go
Normal file
30
cmd/dashboard/web/b0xfile__assets_color.less.go
Normal file
File diff suppressed because one or more lines are too long
30
cmd/dashboard/web/b0xfile__assets_logo-full.svg.go
Normal file
30
cmd/dashboard/web/b0xfile__assets_logo-full.svg.go
Normal file
File diff suppressed because one or more lines are too long
30
cmd/dashboard/web/b0xfile__assets_logo.svg.go
Normal file
30
cmd/dashboard/web/b0xfile__assets_logo.svg.go
Normal file
File diff suppressed because one or more lines are too long
30
cmd/dashboard/web/b0xfile__assets_style.compact.css.go
Normal file
30
cmd/dashboard/web/b0xfile__assets_style.compact.css.go
Normal file
File diff suppressed because one or more lines are too long
30
cmd/dashboard/web/b0xfile__assets_style.dark.css.go
Normal file
30
cmd/dashboard/web/b0xfile__assets_style.dark.css.go
Normal file
File diff suppressed because one or more lines are too long
30
cmd/dashboard/web/b0xfile__favicon.ico.go
Normal file
30
cmd/dashboard/web/b0xfile__favicon.ico.go
Normal file
File diff suppressed because one or more lines are too long
30
cmd/dashboard/web/b0xfile__index.html.go
Normal file
30
cmd/dashboard/web/b0xfile__index.html.go
Normal file
File diff suppressed because one or more lines are too long
30
cmd/dashboard/web/b0xfile__main.ba6b921c87c70912326c.js.go
Normal file
30
cmd/dashboard/web/b0xfile__main.ba6b921c87c70912326c.js.go
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
52
cmd/dashboard/web/route.go
Normal file
52
cmd/dashboard/web/route.go
Normal file
@ -0,0 +1,52 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterRoute(router *gin.Engine) error {
|
||||
files, err := WalkDirs("", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range files {
|
||||
router.GET(f, func(name string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
data, err := ReadFile(name)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
switch filepath.Ext(name) {
|
||||
case ".html":
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
case ".css":
|
||||
c.Header("Content-Type", "text/css; charset=utf-8")
|
||||
case ".js":
|
||||
c.Header("Content-Type", "application/javascript")
|
||||
case ".svg":
|
||||
c.Header("Content-Type", "image/svg+xml")
|
||||
}
|
||||
if _, err := c.Writer.Write(data); err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(f))
|
||||
}
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
data, err := ReadFile("index.html")
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
if _, err := c.Writer.Write(data); err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user