├── .dockerignore ├── .github └── workflows │ ├── ci.yml │ └── docker.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── b0x.yaml ├── config ├── config.go └── load.go ├── deployment.yaml ├── docker-compose.yml ├── docs ├── docs.go ├── screenshots │ ├── 1.login.png │ ├── 2.dashboard.png │ ├── 3.services.png │ ├── 4.service detail.png │ ├── 5.nodes.png │ └── 6.call.png ├── swagger.json └── swagger.yaml ├── frontend ├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── README.md ├── angular.json ├── karma.conf.js ├── ng-alain.json ├── nswag.json ├── package-lock.json ├── package.json ├── proxy.conf.json ├── src │ ├── app │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── core │ │ │ ├── README.md │ │ │ ├── core.module.ts │ │ │ ├── index.ts │ │ │ ├── module-import-guard.ts │ │ │ ├── net │ │ │ │ └── default.interceptor.ts │ │ │ └── startup │ │ │ │ └── startup.service.ts │ │ ├── global-config.module.ts │ │ ├── layout │ │ │ ├── basic │ │ │ │ ├── README.md │ │ │ │ ├── basic.component.ts │ │ │ │ └── widgets │ │ │ │ │ ├── clear-storage.component.ts │ │ │ │ │ ├── fullscreen.component.ts │ │ │ │ │ ├── search.component.ts │ │ │ │ │ └── user.component.ts │ │ │ ├── blank │ │ │ │ ├── README.md │ │ │ │ └── blank.component.ts │ │ │ ├── layout.module.ts │ │ │ └── passport │ │ │ │ ├── passport.component.html │ │ │ │ ├── passport.component.less │ │ │ │ └── passport.component.ts │ │ ├── routes │ │ │ ├── client │ │ │ │ ├── call.component.html │ │ │ │ ├── call.component.ts │ │ │ │ ├── publish.component.html │ │ │ │ └── publish.component.ts │ │ │ ├── dashboard │ │ │ │ ├── dashboard.component.html │ │ │ │ └── dashboard.component.ts │ │ │ ├── exception │ │ │ │ ├── 403.component.ts │ │ │ │ ├── 404.component.ts │ │ │ │ ├── 500.component.ts │ │ │ │ ├── exception-routing.module.ts │ │ │ │ ├── exception.module.ts │ │ │ │ └── trigger.component.ts │ │ │ ├── passport │ │ │ │ └── login │ │ │ │ │ ├── login.component.html │ │ │ │ │ ├── login.component.less │ │ │ │ │ └── login.component.ts │ │ │ ├── routes-routing.module.ts │ │ │ ├── routes.module.ts │ │ │ └── services │ │ │ │ ├── detail.component.html │ │ │ │ ├── detail.component.ts │ │ │ │ ├── list.component.html │ │ │ │ ├── list.component.ts │ │ │ │ ├── nodes.component.html │ │ │ │ └── nodes.component.ts │ │ └── shared │ │ │ ├── index.ts │ │ │ ├── json-schema │ │ │ ├── README.md │ │ │ ├── json-schema.module.ts │ │ │ └── test │ │ │ │ └── test.widget.ts │ │ │ ├── pipes │ │ │ ├── endpoint.pipe.ts │ │ │ └── pipes.module.ts │ │ │ ├── service-proxies │ │ │ ├── service-proxies.ts │ │ │ └── service-proxy.module.ts │ │ │ ├── shared-delon.module.ts │ │ │ ├── shared-zorro.module.ts │ │ │ ├── shared.module.ts │ │ │ └── st-widget │ │ │ └── st-widget.module.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── color.less │ │ ├── logo-color.png │ │ ├── logo-full.png │ │ ├── logo.png │ │ ├── style.compact.css │ │ └── style.dark.css │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── style-icons-auto.ts │ ├── style-icons.ts │ ├── styles.less │ ├── styles │ │ ├── index.less │ │ └── theme.less │ ├── test.ts │ └── typings.d.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── go.mod ├── go.sum ├── handler ├── account │ └── service.go ├── client │ ├── models.go │ ├── service.go │ └── service_test.go ├── handler.go ├── registry │ ├── models.go │ └── service.go ├── route │ ├── middleware.go │ └── route.go └── statistics │ ├── models.go │ └── service.go ├── main.go ├── plugins.go ├── util └── goroutine.go └── web ├── ab0x.go ├── b0xfile__449.fe5f02b3a65993ed3ea1.js.go ├── b0xfile__assets_color.less.go ├── b0xfile__assets_logo-color.png.go ├── b0xfile__assets_logo-full.png.go ├── b0xfile__assets_logo.png.go ├── b0xfile__assets_style.compact.css.go ├── b0xfile__assets_style.dark.css.go ├── b0xfile__favicon.ico.go ├── b0xfile__index.html.go ├── b0xfile__main.d0a89e1924b0ce950fa9.js.go ├── b0xfile__polyfills.96f1279b4eda5600c60b.js.go ├── b0xfile__runtime.337b117ff6241f38ea69.js.go ├── b0xfile__styles.d4c5181bd12329c53117.css.go └── route.go /.dockerignore: -------------------------------------------------------------------------------- 1 | docs 2 | frontend/node_modules 3 | frontend/dist 4 | web/b0xfile*.go -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "main" 7 | push: 8 | branches: 9 | - "main" 10 | 11 | jobs: 12 | backend-test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - uses: dorny/paths-filter@v2 18 | id: changes 19 | with: 20 | filters: | 21 | frontend: 22 | - 'frontend/**' 23 | backend: 24 | - '**/*.go' 25 | 26 | - name: Set up Go 27 | if: steps.changes.outputs.backend == 'true' 28 | uses: actions/setup-go@v4 29 | with: 30 | go-version: '1.20' 31 | 32 | - name: Test Backend 33 | if: steps.changes.outputs.backend == 'true' 34 | run: | 35 | go vet ./... 36 | go test -v ./... 37 | 38 | - name: Set up Node.js 39 | if: steps.changes.outputs.frontend == 'true' 40 | uses: actions/setup-node@v3 41 | with: 42 | node-version: 17 43 | cache: "npm" 44 | cache-dependency-path: frontend/package-lock.json 45 | 46 | - name: Test Frontend 47 | if: steps.changes.outputs.frontend == 'true' 48 | run: | 49 | cd frontend 50 | npm install 51 | npm run lint 52 | 53 | - name: Notify of test failure 54 | if: failure() 55 | id: slack 56 | uses: slackapi/slack-github-action@v1.18.0 57 | with: 58 | channel-id: 'github-actions' 59 | slack-message: "Dashboard tests: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" 60 | env: 61 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | # Publish semver tags as releases. 6 | tags: ["*"] 7 | 8 | env: 9 | IMAGE_NAME: xpunch/go-micro-dashboard 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | # Extract metadata (tags, labels) for Docker 23 | # https://github.com/docker/metadata-action 24 | - name: Extract Docker metadata 25 | id: meta 26 | uses: docker/metadata-action@v3 27 | with: 28 | images: ${{ env.IMAGE_NAME }} 29 | tags: | 30 | # set latest tag for master branch 31 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} 32 | # minimal 33 | type=pep440,pattern={{version}} 34 | 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v2 37 | 38 | - name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@v2 40 | 41 | # Login against a Docker registry except on PR 42 | # https://github.com/docker/login-action 43 | - name: Login to DockerHub 44 | uses: docker/login-action@v2 45 | with: 46 | username: ${{ secrets.DOCKERHUB_USERNAME }} 47 | password: ${{ secrets.DOCKERHUB_TOKEN }} 48 | 49 | # Build and push Docker image with Buildx (don't push on PR) 50 | # https://github.com/docker/build-push-action 51 | - name: Build and push Docker Image 52 | id: build-and-push 53 | uses: docker/build-push-action@v3 54 | with: 55 | context: . 56 | platforms: linux/amd64,linux/arm64 57 | push: true 58 | tags: ${{ steps.meta.outputs.tags }} 59 | labels: ${{ steps.meta.outputs.labels }} 60 | 61 | - name: Notify of docker publish failure 62 | if: failure() 63 | id: slack 64 | uses: slackapi/slack-github-action@v1.18.0 65 | with: 66 | channel-id: 'github-actions' 67 | slack-message: "Dashboard docker publish: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" 68 | env: 69 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # IDE 15 | .vscode 16 | 17 | # config files 18 | config.yaml 19 | config.toml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.4.0 (2022/04/08) 2 | > 1. Migrate to github.com/go-micro/dashboard. 3 | 4 | ### 1.3.0 (2021/12/16) 5 | > 1. Support service nodes health check, but http server type is unsupported currently. 6 | 7 | ### 1.2.0 (2021/12/07) 8 | > 1. Update login page, using new logo and icon. 9 | > 2. Using delon auth config. 10 | > 3. Fixing client publish default topic issue. 11 | 12 | ### 1.1.0 (2021/12/01) 13 | > 1. Support publish message. 14 | > 2. Import client call user experience. 15 | > 3. Using new logo. 16 | 17 | ### 1.0.0 (2021/11/22) 18 | > 1. Authenticate. 19 | > 2. Dashboard. 20 | > 3. View service list/detail. 21 | > 4. Client call. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 as frontend-builder 2 | 3 | WORKDIR /micro 4 | 5 | COPY frontend . 6 | 7 | RUN npm install -g @angular/cli && \ 8 | npm install && \ 9 | npm run build 10 | 11 | FROM golang:1.20 as backend-builder 12 | 13 | WORKDIR /micro 14 | 15 | COPY . . 16 | COPY --from=frontend-builder /micro/dist frontend/dist 17 | 18 | RUN go install github.com/swaggo/swag/cmd/swag@latest && \ 19 | swag init && \ 20 | go install github.com/UnnoTed/fileb0x@latest && \ 21 | fileb0x b0x.yaml && \ 22 | CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o dashboard . 23 | 24 | FROM alpine:latest 25 | 26 | WORKDIR /usr/local/bin 27 | 28 | COPY --from=backend-builder /micro/dashboard . 29 | 30 | EXPOSE 80 31 | 32 | ENTRYPOINT [ "/usr/local/bin/dashboard" ] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | fmt: 2 | @go fmt ./... 3 | 4 | vet: 5 | @go vet ./... 6 | 7 | test: 8 | @go test ./... 9 | 10 | build: 11 | @cd frontend && npm install && npm run build 12 | @go install github.com/UnnoTed/fileb0x@latest && fileb0x b0x.yaml 13 | @swag init 14 | 15 | docker: 16 | docker build -t xpunch/go-micro-dashboard:latest . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dashboard [](https://opensource.org/licenses/Apache-2.0) [](https://godoc.org/github.com/go-micro/dashboard) [](https://github.com/go-micro/dashboard/actions/workflows/ci.yml) [](https://github.com/go-micro/dashboard/actions/workflows/docker.yml) [](https://goreportcard.com/report/github.com/go-micro/dashboard) [](https://hits.seeyoufarm.com) 2 | 3 | ## Features 4 | 5 | - [x] Logo 6 | - [x] Web UI 7 | - [x] Service discovery 8 | - [ ] Register service 9 | - [ ] Deregister service 10 | - [x] Health check 11 | - [ ] Configuration service 12 | - [x] Synchronous communication 13 | - [x] RPC 14 | - [ ] Stream 15 | - [x] Asynchronous communication 16 | - [x] Publish 17 | - [ ] Subscribe 18 | 19 | ## Installation 20 | 21 | ``` 22 | go install github.com/go-micro/dashboard@latest 23 | ``` 24 | 25 | ## Development 26 | 27 | ### Server 28 | 29 | #### Swagger 30 | 31 | ``` 32 | swagger generate spec -o docs/swagger.json -b ./docs 33 | swag init 34 | ``` 35 | 36 | #### Config 37 | 38 | ``` 39 | default username: admin 40 | default password: micro 41 | ``` 42 | 43 | ##### ENV 44 | ``` 45 | export SERVER_ADDRESS=:8082 46 | export SERVER_AUTH_USERNAME=user 47 | export SERVER_AUTH_PASSWORD=pass 48 | ``` 49 | 50 | ##### YAML 51 | ``` 52 | export CONFIG_TYPE=yaml 53 | ``` 54 | ```yaml 55 | server: 56 | env: "dev" 57 | address: ":8082" 58 | swagger: 59 | host: "localhost:8082" 60 | ``` 61 | 62 | ##### TOML 63 | ``` 64 | export CONFIG_TYPE=toml 65 | ``` 66 | ```toml 67 | [server] 68 | env = "dev" 69 | address = ":8082" 70 | [server.swagger] 71 | host = "localhost:8082" 72 | ``` 73 | 74 | ### Web UI 75 | 76 | [Document](https://github.com/go-micro/dashboard/tree/main/frontend) 77 | 78 | #### Generate Web Files 79 | 80 | ``` 81 | go install github.com/UnnoTed/fileb0x@latest 82 | fileb0x b0x.yaml 83 | ``` 84 | 85 | ## Docker 86 | 87 | ``` 88 | docker run -d --name micro-dashboard -p 8082:8082 xpunch/go-micro-dashboard:latest 89 | ``` 90 | 91 | ## Docker Compose 92 | 93 | ``` 94 | docker-compose -f docker-compose.yml up -d 95 | ``` 96 | 97 | ## Kubernetes 98 | 99 | ``` 100 | kubectl apply -f deployment.yaml 101 | ``` 102 | 103 | ## Community 104 | 105 | - [Discord](https://discord.gg/qV3HvnEJfB) 106 | - [Slack](https://join.slack.com/t/go-micro/shared_invite/zt-175aaev1d-iHExPTlfxvfkOeeKLIYEYw) 107 | - [QQ Group](https://jq.qq.com/?_wv=1027&k=5Gmrfv9i) 108 | 109 | ## Screen Shots 110 |  111 |  112 |  113 |  114 |  115 |  116 | 117 | ## License 118 | 119 | [Apache License 2.0](./LICENSE) 120 | -------------------------------------------------------------------------------- /b0x.yaml: -------------------------------------------------------------------------------- 1 | # all folders and files are relative to the path 2 | # where fileb0x was run at! 3 | 4 | # default: main 5 | pkg: web 6 | 7 | # destination 8 | dest: "./web/" 9 | 10 | # gofmt 11 | # type: bool 12 | # default: false 13 | fmt: true 14 | 15 | # compress files 16 | # at the moment, only supports gzip 17 | # 18 | # type: object 19 | compression: 20 | # activates the compression 21 | # 22 | # type: bool 23 | # default: false 24 | compress: false 25 | 26 | # valid values are: 27 | # -> "NoCompression" 28 | # -> "BestSpeed" 29 | # -> "BestCompression" 30 | # -> "DefaultCompression" or "" 31 | # 32 | # type: string 33 | # default: "DefaultCompression" # when: Compress == true && Method == "" 34 | method: "" 35 | 36 | # true = do it yourself (the file is written as gzip compressed file into the memory file system) 37 | # false = decompress files at run time (while writing file into memory file system) 38 | # 39 | # type: bool 40 | # default: false 41 | keep: false 42 | 43 | # --------------- 44 | # -- DANGEROUS -- 45 | # --------------- 46 | # 47 | # cleans the destination folder (only b0xfiles) 48 | # you should use this when using the spread function 49 | # type: bool 50 | # default: false 51 | clean: true 52 | 53 | # default: ab0x.go 54 | output: "ab0x.go" 55 | 56 | # [unexporTed] builds non-exporTed functions, variables and types... 57 | # type: bool 58 | # default: false 59 | unexporTed: false 60 | 61 | # [spread] means it will make a file to hold all fileb0x data 62 | # and each file into a separaTed .go file 63 | # 64 | # example: 65 | # theres 2 files in the folder assets, they're: hello.json and world.txt 66 | # when spread is activaTed, fileb0x will make a file: 67 | # b0x.go or [output]'s data, assets_hello.json.go and assets_world.txt.go 68 | # 69 | # 70 | # type: bool 71 | # default: false 72 | spread: true 73 | 74 | # [lcf] log changed files when spread is active 75 | lcf: true 76 | 77 | # type: array of objects 78 | custom: 79 | - files: 80 | - "frontend/dist" 81 | base: "frontend/dist" 82 | exclude: 83 | - "/3rdpartylicenses.txt" 84 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "time" 4 | 5 | const ( 6 | Name = "go.micro.dashboard" 7 | Version = "1.4.5" 8 | ) 9 | 10 | const ( 11 | EnvDev = "dev" 12 | EnvProd = "prod" 13 | ) 14 | 15 | type Config struct { 16 | Server ServerConfig 17 | } 18 | 19 | type ServerConfig struct { 20 | Env string 21 | Address string 22 | Auth AuthConfig 23 | CORS CORSConfig 24 | Swagger SwaggerConfig 25 | } 26 | 27 | type AuthConfig struct { 28 | Username string 29 | Password string 30 | TokenSecret string 31 | TokenExpiration time.Duration 32 | } 33 | 34 | type CORSConfig struct { 35 | Enable bool `toml:"enable"` 36 | Origin string `toml:"origin"` 37 | } 38 | 39 | type SwaggerConfig struct { 40 | Host string 41 | Base string 42 | } 43 | 44 | func GetConfig() Config { 45 | return *_cfg 46 | } 47 | 48 | func GetServerConfig() ServerConfig { 49 | return _cfg.Server 50 | } 51 | 52 | func GetAuthConfig() AuthConfig { 53 | return _cfg.Server.Auth 54 | } 55 | 56 | func GetSwaggerConfig() SwaggerConfig { 57 | return _cfg.Server.Swagger 58 | } 59 | -------------------------------------------------------------------------------- /config/load.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "time" 7 | 8 | "github.com/go-micro/dashboard/util" 9 | "github.com/go-micro/plugins/v4/config/encoder/toml" 10 | "github.com/go-micro/plugins/v4/config/encoder/yaml" 11 | "github.com/pkg/errors" 12 | "go-micro.dev/v4/config" 13 | "go-micro.dev/v4/config/reader" 14 | "go-micro.dev/v4/config/reader/json" 15 | "go-micro.dev/v4/config/source/env" 16 | "go-micro.dev/v4/config/source/file" 17 | "go-micro.dev/v4/logger" 18 | ) 19 | 20 | // internal instance of Config 21 | var _cfg *Config = &Config{ 22 | Server: ServerConfig{ 23 | Env: EnvProd, 24 | Address: ":8082", 25 | Auth: AuthConfig{ 26 | Username: "admin", 27 | Password: "micro", 28 | TokenSecret: "modifyme", 29 | TokenExpiration: 24 * time.Hour, 30 | }, 31 | Swagger: SwaggerConfig{ 32 | Host: "localhost:8082", 33 | Base: "/", 34 | }, 35 | }, 36 | } 37 | 38 | // Load will load configurations and update it when changed 39 | func Load() error { 40 | var configor config.Config 41 | var err error 42 | switch strings.ToLower(os.Getenv("CONFIG_TYPE")) { 43 | case "toml": 44 | filename := "config.toml" 45 | if name := os.Getenv("CONFIG_FILE"); len(name) > 0 { 46 | filename = name 47 | } 48 | configor, err = config.NewConfig( 49 | config.WithSource(file.NewSource(file.WithPath(filename))), 50 | config.WithReader(json.NewReader(reader.WithEncoder(toml.NewEncoder()))), 51 | ) 52 | case "yaml": 53 | filename := "config.yaml" 54 | if name := os.Getenv("CONFIG_FILE"); len(name) > 0 { 55 | filename = name 56 | } 57 | configor, err = config.NewConfig( 58 | config.WithSource(file.NewSource(file.WithPath(filename))), 59 | config.WithReader(json.NewReader(reader.WithEncoder(yaml.NewEncoder()))), 60 | ) 61 | default: 62 | configor, err = config.NewConfig( 63 | config.WithSource(env.NewSource()), 64 | ) 65 | } 66 | if err != nil { 67 | return errors.Wrap(err, "configor.New") 68 | } 69 | if err := configor.Load(); err != nil { 70 | return errors.Wrap(err, "configor.Load") 71 | } 72 | if err := configor.Scan(_cfg); err != nil { 73 | return errors.Wrap(err, "configor.Scan") 74 | } 75 | w, err := configor.Watch() 76 | if err != nil { 77 | return errors.Wrap(err, "configor.Watch") 78 | } 79 | util.GoSafe(func() { 80 | for { 81 | v, err := w.Next() 82 | if err != nil { 83 | logger.Error(err) 84 | return 85 | } 86 | if err := v.Scan(_cfg); err != nil { 87 | logger.Error(err) 88 | return 89 | } 90 | } 91 | }) 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: micro 5 | namespace: micro 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: micro-registry 11 | rules: 12 | - apiGroups: 13 | - "" 14 | resources: 15 | - pods 16 | verbs: 17 | - list 18 | - patch 19 | - watch 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1 22 | kind: RoleBinding 23 | metadata: 24 | name: micro-registry 25 | namespace: micro 26 | roleRef: 27 | apiGroup: rbac.authorization.k8s.io 28 | kind: ClusterRole 29 | name: micro-registry 30 | subjects: 31 | - kind: ServiceAccount 32 | name: micro 33 | --- 34 | kind: Deployment 35 | apiVersion: apps/v1 36 | metadata: 37 | name: micro-dashboard 38 | namespace: micro 39 | labels: 40 | app: micro-dashboard 41 | spec: 42 | replicas: 1 43 | selector: 44 | matchLabels: 45 | app: micro-dashboard 46 | strategy: 47 | rollingUpdate: 48 | maxSurge: 25% 49 | maxUnavailable: 25% 50 | type: RollingUpdate 51 | template: 52 | metadata: 53 | labels: 54 | app: micro-dashboard 55 | spec: 56 | containers: 57 | - image: xpunch/go-micro-dashboard:latest 58 | imagePullPolicy: IfNotPresent 59 | name: dashboard 60 | ports: 61 | - containerPort: 80 62 | protocol: TCP 63 | env: 64 | - name: MICRO_REGISTRY 65 | value: "kubernetes" 66 | - name: MICRO_CLIENT_RETRIES 67 | value: "0" 68 | # default config type env 69 | # - SERVER_AUTH_USERNAME=user 70 | # - SERVER_AUTH_PASSWORD=pass 71 | resources: 72 | limits: 73 | memory: 512Mi 74 | cpu: "0.25" 75 | requests: 76 | memory: 512Mi 77 | cpu: "0.25" 78 | serviceAccountName: micro 79 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | dashboard: 5 | image: xpunch/go-micro-dashboard:latest 6 | container_name: micro-dashboard 7 | ports: 8 | - "8082:8082" 9 | environment: 10 | - MICRO_REGISTRY=etcd 11 | - MICRO_REGISTRY_ADDRESS=etcd 12 | - CONFIG_TYPE=yaml 13 | - CONFIG_FILE=/etc/micro/dashboard.yaml 14 | # default config type env 15 | # - SERVER_AUTH_USERNAME=user 16 | # - SERVER_AUTH_PASSWORD=pass 17 | volumes: 18 | - "./config.yaml:/etc/micro/dashboard.yaml" -------------------------------------------------------------------------------- /docs/screenshots/1.login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-micro/dashboard/041f2a7c1811ec554dafef211506f2668471bba5/docs/screenshots/1.login.png -------------------------------------------------------------------------------- /docs/screenshots/2.dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-micro/dashboard/041f2a7c1811ec554dafef211506f2668471bba5/docs/screenshots/2.dashboard.png -------------------------------------------------------------------------------- /docs/screenshots/3.services.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-micro/dashboard/041f2a7c1811ec554dafef211506f2668471bba5/docs/screenshots/3.services.png -------------------------------------------------------------------------------- /docs/screenshots/4.service detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-micro/dashboard/041f2a7c1811ec554dafef211506f2668471bba5/docs/screenshots/4.service detail.png -------------------------------------------------------------------------------- /docs/screenshots/5.nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-micro/dashboard/041f2a7c1811ec554dafef211506f2668471bba5/docs/screenshots/5.nodes.png -------------------------------------------------------------------------------- /docs/screenshots/6.call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-micro/dashboard/041f2a7c1811ec554dafef211506f2668471bba5/docs/screenshots/6.call.png -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | _cli-tpl/ 2 | dist/ 3 | coverage/ 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Dependency directories 13 | node_modules/ 14 | 15 | # TypeScript cache 16 | *.tsbuildinfo 17 | 18 | # Optional npm cache directory 19 | .npm 20 | 21 | # Optional eslint cache 22 | .eslintcache 23 | 24 | # Yarn Integrity file 25 | .yarn-integrity 26 | 27 | # dotenv environment variables file 28 | .env 29 | .env.test 30 | 31 | .cache/ 32 | 33 | # yarn v2 34 | .yarn 35 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const prettierConfig = require('./.prettierrc.js'); 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { ecmaVersion: 2021 }, 6 | overrides: [ 7 | { 8 | files: ['*.ts'], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | tsconfigRootDir: __dirname, 12 | project: ['tsconfig.json'], 13 | createDefaultProgram: true 14 | }, 15 | plugins: ['@typescript-eslint', 'jsdoc', 'import'], 16 | extends: [ 17 | 'plugin:@angular-eslint/recommended', 18 | 'plugin:@angular-eslint/template/process-inline-templates', 19 | 'plugin:prettier/recommended' 20 | ], 21 | rules: { 22 | 'prettier/prettier': ['error', prettierConfig], 23 | 'jsdoc/newline-after-description': 1, 24 | '@angular-eslint/component-class-suffix': [ 25 | 'error', 26 | { 27 | suffixes: ['Directive', 'Component', 'Base', 'Widget'] 28 | } 29 | ], 30 | '@angular-eslint/directive-class-suffix': [ 31 | 'error', 32 | { 33 | suffixes: ['Directive', 'Component', 'Base', 'Widget'] 34 | } 35 | ], 36 | '@angular-eslint/component-selector': [ 37 | 'off', 38 | { 39 | type: ['element', 'attribute'], 40 | prefix: ['app', 'test'], 41 | style: 'kebab-case' 42 | } 43 | ], 44 | '@angular-eslint/directive-selector': [ 45 | 'off', 46 | { 47 | type: 'attribute', 48 | prefix: ['app'] 49 | } 50 | ], 51 | '@angular-eslint/no-attribute-decorator': 'error', 52 | '@angular-eslint/no-conflicting-lifecycle': 'off', 53 | '@angular-eslint/no-forward-ref': 'off', 54 | '@angular-eslint/no-host-metadata-property': 'off', 55 | '@angular-eslint/no-lifecycle-call': 'off', 56 | '@angular-eslint/no-pipe-impure': 'off', 57 | '@angular-eslint/prefer-output-readonly': 'error', 58 | '@angular-eslint/use-component-selector': 'off', 59 | '@angular-eslint/use-component-view-encapsulation': 'off', 60 | '@angular-eslint/no-input-rename': 'off', 61 | '@angular-eslint/no-output-native': 'off', 62 | '@typescript-eslint/array-type': [ 63 | 'error', 64 | { 65 | default: 'array-simple' 66 | } 67 | ], 68 | '@typescript-eslint/ban-types': [ 69 | 'off', 70 | { 71 | types: { 72 | String: { 73 | message: 'Use string instead.' 74 | }, 75 | Number: { 76 | message: 'Use number instead.' 77 | }, 78 | Boolean: { 79 | message: 'Use boolean instead.' 80 | }, 81 | Function: { 82 | message: 'Use specific callable interface instead.' 83 | } 84 | } 85 | } 86 | ], 87 | 'import/no-duplicates': 'error', 88 | 'import/no-unused-modules': 'error', 89 | 'import/no-unassigned-import': 'error', 90 | 'import/order': [ 91 | 'error', 92 | { 93 | alphabetize: { order: 'asc', caseInsensitive: false }, 94 | 'newlines-between': 'always', 95 | groups: ['external', 'internal', ['parent', 'sibling', 'index']], 96 | pathGroups: [], 97 | pathGroupsExcludedImportTypes: [] 98 | } 99 | ], 100 | '@typescript-eslint/no-this-alias': 'error', 101 | '@typescript-eslint/member-ordering': 'off', 102 | 'no-irregular-whitespace': 'error', 103 | 'no-multiple-empty-lines': 'error', 104 | 'no-sparse-arrays': 'error', 105 | 'prefer-object-spread': 'error', 106 | 'prefer-template': 'error', 107 | 'prefer-const': 'off', 108 | 'max-len': 'off' 109 | } 110 | }, 111 | { 112 | files: ['*.html'], 113 | extends: ['plugin:@angular-eslint/template/recommended'], 114 | rules: {} 115 | }, 116 | { 117 | files: ['*.html'], 118 | excludedFiles: ['*inline-template-*.component.html'], 119 | extends: ['plugin:prettier/recommended'], 120 | rules: { 121 | 'prettier/prettier': ['error', { parser: 'angular' }], 122 | '@angular-eslint/template/eqeqeq': 'off' 123 | } 124 | } 125 | ] 126 | }; 127 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.sass-cache 35 | /connect.lock 36 | /coverage 37 | /libpeerconnection.log 38 | npm-debug.log 39 | yarn-error.log 40 | testem.log 41 | /typings 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | -------------------------------------------------------------------------------- /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | 12.14.1 2 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | # add files you wish to ignore here 2 | **/*.md 3 | **/*.svg 4 | **/test.ts 5 | 6 | .stylelintrc 7 | .prettierrc 8 | 9 | src/assets/* 10 | src/index.html 11 | node_modules/ 12 | .vscode/ 13 | coverage/ 14 | dist/ 15 | package.json 16 | tslint.json 17 | 18 | _cli-tpl/**/* 19 | -------------------------------------------------------------------------------- /frontend/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | useTabs: false, 4 | printWidth: 140, 5 | tabWidth: 2, 6 | semi: true, 7 | htmlWhitespaceSensitivity: 'strict', 8 | arrowParens: 'avoid', 9 | bracketSpacing: true, 10 | proseWrap: 'preserve', 11 | trailingComma: 'none', 12 | endOfLine: 'lf' 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-rational-order", 5 | "stylelint-config-prettier" 6 | ], 7 | "plugins": [ 8 | "stylelint-order", 9 | "stylelint-declaration-block-no-ignored-properties" 10 | ], 11 | "rules": { 12 | "no-descending-specificity": null, 13 | "plugin/declaration-block-no-ignored-properties": true, 14 | "selector-type-no-unknown": [ 15 | true, 16 | { 17 | "ignoreTypes": [ 18 | "/^g2-/", 19 | "/^nz-/", 20 | "/^app-/" 21 | ] 22 | } 23 | ], 24 | "selector-pseudo-element-no-unknown": [ 25 | true, 26 | { 27 | "ignorePseudoElements": [ 28 | "ng-deep" 29 | ] 30 | } 31 | ] 32 | }, 33 | "ignoreFiles": [ 34 | "src/assets/**/*" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "cipchk.ng-alain-extension-pack" 4 | ] 5 | } -------------------------------------------------------------------------------- /frontend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:4200", 12 | "webRoot": "${workspaceRoot}", 13 | "sourceMaps": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /frontend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | // For ESLint 6 | "source.fixAll.eslint": true, 7 | // For Stylelint 8 | "source.fixAll.stylelint": true 9 | }, 10 | "[markdown]": { 11 | "editor.formatOnSave": false 12 | }, 13 | "[javascript]": { 14 | "editor.formatOnSave": false 15 | }, 16 | "[json]": { 17 | "editor.formatOnSave": false 18 | }, 19 | "[jsonc]": { 20 | "editor.formatOnSave": false 21 | }, 22 | "files.watcherExclude": { 23 | "**/.git/*/**": true, 24 | "**/node_modules/*/**": true, 25 | "**/dist/*/**": true, 26 | "**/coverage/*/**": true 27 | }, 28 | "files.associations": { 29 | "*.json": "jsonc", 30 | ".prettierrc": "jsonc", 31 | ".stylelintrc": "jsonc" 32 | }, 33 | // Angular schematics 插件: https://marketplace.visualstudio.com/items?itemName=cyrilletuzi.angular-schematics 34 | "ngschematics.schematics": [ 35 | "ng-alain" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Go Micro Dashboard 2 | 3 | ## Development 4 | 5 | ### NG-Alain 6 | 7 | ``` 8 | ng new go-micro-dashboard 9 | ng add ng-alain 10 | ``` 11 | 12 | ### Swagger 13 | 14 | ``` 15 | npm install 16 | node_modules\.bin\nswag run 17 | ``` 18 | -------------------------------------------------------------------------------- /frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": false 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "go-micro-dashboard": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "skipTests": false, 14 | "flat": false, 15 | "inlineStyle": true, 16 | "inlineTemplate": false, 17 | "style": "less" 18 | }, 19 | "@schematics/angular:application": { 20 | "strict": true 21 | }, 22 | "ng-alain:module": { 23 | "routing": true, 24 | "skipTests": false 25 | }, 26 | "ng-alain:list": { 27 | "skipTests": false 28 | }, 29 | "ng-alain:edit": { 30 | "skipTests": false, 31 | "modal": true 32 | }, 33 | "ng-alain:view": { 34 | "skipTests": false, 35 | "modal": true 36 | }, 37 | "ng-alain:curd": { 38 | "skipTests": false 39 | }, 40 | "@schematics/angular:module": { 41 | "routing": true, 42 | "skipTests": false 43 | }, 44 | "@schematics/angular:directive": { 45 | "skipTests": false 46 | }, 47 | "@schematics/angular:service": { 48 | "skipTests": false 49 | } 50 | }, 51 | "root": "", 52 | "sourceRoot": "src", 53 | "prefix": "app", 54 | "architect": { 55 | "build": { 56 | "builder": "@angular-devkit/build-angular:browser", 57 | "options": { 58 | "outputPath": "dist", 59 | "index": "src/index.html", 60 | "main": "src/main.ts", 61 | "polyfills": "src/polyfills.ts", 62 | "tsConfig": "tsconfig.app.json", 63 | "inlineStyleLanguage": "less", 64 | "assets": [ 65 | "src/favicon.ico", 66 | "src/assets" 67 | ], 68 | "styles": [ 69 | "src/styles.less" 70 | ], 71 | "scripts": [], 72 | "allowedCommonJsDependencies": [ 73 | "@antv/g2", 74 | "ajv", 75 | "ajv-formats", 76 | "date-fns", 77 | "file-saver" 78 | ] 79 | }, 80 | "configurations": { 81 | "production": { 82 | "budgets": [ 83 | { 84 | "type": "initial", 85 | "maximumWarning": "2mb", 86 | "maximumError": "3mb" 87 | }, 88 | { 89 | "type": "anyComponentStyle", 90 | "maximumWarning": "2kb", 91 | "maximumError": "4kb" 92 | } 93 | ], 94 | "fileReplacements": [ 95 | { 96 | "replace": "src/environments/environment.ts", 97 | "with": "src/environments/environment.prod.ts" 98 | } 99 | ], 100 | "outputHashing": "all" 101 | }, 102 | "development": { 103 | "buildOptimizer": false, 104 | "optimization": false, 105 | "vendorChunk": true, 106 | "extractLicenses": false, 107 | "sourceMap": true, 108 | "namedChunks": true 109 | } 110 | }, 111 | "defaultConfiguration": "production" 112 | }, 113 | "serve": { 114 | "builder": "@angular-devkit/build-angular:dev-server", 115 | "configurations": { 116 | "production": { 117 | "browserTarget": "go-micro-dashboard:build:production" 118 | }, 119 | "development": { 120 | "browserTarget": "go-micro-dashboard:build:development" 121 | } 122 | }, 123 | "defaultConfiguration": "development", 124 | "options": { 125 | "proxyConfig": "proxy.conf.json" 126 | } 127 | }, 128 | "extract-i18n": { 129 | "builder": "@angular-devkit/build-angular:extract-i18n", 130 | "options": { 131 | "browserTarget": "go-micro-dashboard:build" 132 | } 133 | }, 134 | "test": { 135 | "builder": "@angular-devkit/build-angular:karma", 136 | "options": { 137 | "main": "src/test.ts", 138 | "polyfills": "src/polyfills.ts", 139 | "tsConfig": "tsconfig.spec.json", 140 | "karmaConfig": "karma.conf.js", 141 | "inlineStyleLanguage": "less", 142 | "assets": [ 143 | "src/favicon.ico", 144 | "src/assets" 145 | ], 146 | "styles": [ 147 | "src/styles.less" 148 | ], 149 | "scripts": [] 150 | } 151 | }, 152 | "lint": { 153 | "builder": "@angular-eslint/builder:lint", 154 | "options": { 155 | "lintFilePatterns": [ 156 | "src/**/*.ts", 157 | "src/**/*.html" 158 | ] 159 | } 160 | } 161 | } 162 | } 163 | }, 164 | "defaultProject": "go-micro-dashboard" 165 | } 166 | -------------------------------------------------------------------------------- /frontend/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/go-micro-dashboard'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /frontend/ng-alain.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-alain/schema.json", 3 | "theme": { 4 | "list": [ 5 | { 6 | "theme": "dark" 7 | }, 8 | { 9 | "theme": "compact" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/nswag.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtime": "Default", 3 | "defaultVariables": null, 4 | "documentGenerator": { 5 | "fromDocument": { 6 | "url": "http://localhost:8082/swagger/doc.json", 7 | "output": null, 8 | "newLineBehavior": "Auto" 9 | } 10 | }, 11 | "codeGenerators": { 12 | "openApiToTypeScriptClient": { 13 | "className": "{controller}ServiceProxy", 14 | "moduleName": "", 15 | "namespace": "", 16 | "typeScriptVersion": 2.7, 17 | "template": "Angular", 18 | "promiseType": "Promise", 19 | "httpClass": "HttpClient", 20 | "withCredentials": false, 21 | "useSingletonProvider": false, 22 | "injectionTokenType": "InjectionToken", 23 | "rxJsVersion": 6.0, 24 | "dateTimeType": "Date", 25 | "nullValue": "Undefined", 26 | "generateClientClasses": true, 27 | "generateClientInterfaces": false, 28 | "generateOptionalParameters": false, 29 | "exportTypes": true, 30 | "wrapDtoExceptions": false, 31 | "exceptionClass": "ApiException", 32 | "clientBaseClass": null, 33 | "wrapResponses": false, 34 | "wrapResponseMethods": [], 35 | "generateResponseClasses": true, 36 | "responseClass": "SwaggerResponse", 37 | "protectedMethods": [], 38 | "configurationClass": null, 39 | "useTransformOptionsMethod": false, 40 | "useTransformResultMethod": false, 41 | "generateDtoTypes": true, 42 | "operationGenerationMode": "MultipleClientsFromOperationId", 43 | "markOptionalProperties": true, 44 | "generateCloneMethod": false, 45 | "typeStyle": "Class", 46 | "enumStyle": "Enum", 47 | "useLeafType": false, 48 | "classTypes": [], 49 | "extendedClasses": [], 50 | "extensionCode": null, 51 | "generateDefaultValues": true, 52 | "excludedTypeNames": [], 53 | "excludedParameterNames": [], 54 | "handleReferences": false, 55 | "generateConstructorInterface": true, 56 | "convertConstructorInterfaceData": false, 57 | "importRequiredTypes": true, 58 | "useGetBaseUrlMethod": false, 59 | "baseUrlTokenName": "API_BASE_URL", 60 | "queryNullValue": "", 61 | "useAbortSignal": false, 62 | "inlineNamedDictionaries": false, 63 | "inlineNamedAny": false, 64 | "includeHttpContext": false, 65 | "templateDirectory": null, 66 | "typeNameGeneratorType": null, 67 | "propertyNameGeneratorType": null, 68 | "enumNameGeneratorType": null, 69 | "checksumCacheEnabled": false, 70 | "serviceHost": null, 71 | "serviceSchemes": null, 72 | "output": "src/app/shared/service-proxies/service-proxies.ts", 73 | "newLineBehavior": "Auto" 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "go-micro-dashboard", 3 | "version": "1.4.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng s -o", 7 | "build": "npm run ng-high-memory build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "ng-high-memory": "node --max_old_space_size=8000 ./node_modules/@angular/cli/bin/ng", 11 | "hmr": "ng s -o --hmr", 12 | "analyze": "npm run ng-high-memory build -- --source-map", 13 | "analyze:view": "source-map-explorer dist/**/*.js", 14 | "test-coverage": "ng test --code-coverage --watch=false", 15 | "color-less": "ng-alain-plugin-theme -t=colorLess", 16 | "theme": "ng-alain-plugin-theme -t=themeCss", 17 | "icon": "ng g ng-alain:plugin icon", 18 | "lint": "npm run lint:ts && npm run lint:style", 19 | "lint:ts": "ng lint --fix", 20 | "lint:style": "stylelint \"src/**/*.less\" --syntax less --fix" 21 | }, 22 | "private": true, 23 | "dependencies": { 24 | "@angular/animations": "~12.2.0", 25 | "@angular/common": "~12.2.0", 26 | "@angular/compiler": "~12.2.0", 27 | "@angular/core": "~12.2.0", 28 | "@angular/forms": "~12.2.0", 29 | "@angular/platform-browser": "~12.2.0", 30 | "@angular/platform-browser-dynamic": "~12.2.0", 31 | "@angular/router": "~12.2.0", 32 | "@delon/abc": "^12.3.0", 33 | "@delon/acl": "^12.3.0", 34 | "@delon/auth": "^12.3.0", 35 | "@delon/cache": "^12.3.0", 36 | "@delon/chart": "^12.3.0", 37 | "@delon/form": "^12.3.0", 38 | "@delon/mock": "^12.3.0", 39 | "@delon/theme": "^12.3.0", 40 | "@delon/util": "^12.3.0", 41 | "ajv": "^8.6.2", 42 | "ajv-formats": "^2.1.1", 43 | "ng-alain": "^12.3.0", 44 | "ngx-clipboard": "^14.0.2", 45 | "rxjs": "~6.6.0", 46 | "screenfull": "^5.1.0", 47 | "tslib": "^2.3.0", 48 | "zone.js": "~0.11.4" 49 | }, 50 | "devDependencies": { 51 | "@angular-devkit/build-angular": "~12.2.12", 52 | "@angular-eslint/builder": "~12.3.1", 53 | "@angular-eslint/eslint-plugin": "~12.3.1", 54 | "@angular-eslint/eslint-plugin-template": "~12.3.1", 55 | "@angular-eslint/schematics": "~12.3.1", 56 | "@angular-eslint/template-parser": "~12.3.1", 57 | "@angular/cli": "~12.2.12", 58 | "@angular/compiler-cli": "~12.2.0", 59 | "@angular/language-service": "~12.2.0", 60 | "@delon/testing": "^12.3.0", 61 | "@types/jasmine": "~3.8.0", 62 | "@types/node": "^12.11.1", 63 | "@typescript-eslint/eslint-plugin": "~4.29.2", 64 | "@typescript-eslint/parser": "~4.29.2", 65 | "eslint": "^7.32.0", 66 | "eslint-config-prettier": "^2.2.1", 67 | "eslint-plugin-import": "~2.24.1", 68 | "eslint-plugin-jsdoc": "~36.0.7", 69 | "eslint-plugin-prefer-arrow": "~1.2.3", 70 | "eslint-plugin-prettier": "^2.2.1", 71 | "jasmine-core": "~3.8.0", 72 | "karma": "~6.3.0", 73 | "karma-chrome-launcher": "~3.1.0", 74 | "karma-coverage": "~2.0.3", 75 | "karma-jasmine": "~4.0.0", 76 | "karma-jasmine-html-reporter": "~1.7.0", 77 | "lint-staged": "^11.1.2", 78 | "ng-alain": "^12.3.0", 79 | "ng-alain-plugin-theme": "^12.0.0", 80 | "nswag": "^13.14.0", 81 | "prettier": "^2.2.1", 82 | "source-map-explorer": "^2.5.2", 83 | "stylelint": "^13.13.1", 84 | "stylelint-config-prettier": "^8.0.2", 85 | "stylelint-config-rational-order": "^0.1.2", 86 | "stylelint-config-standard": "^22.0.0", 87 | "stylelint-declaration-block-no-ignored-properties": "^2.4.0", 88 | "stylelint-order": "^4.1.0", 89 | "typescript": "~4.3.5" 90 | }, 91 | "lint-staged": { 92 | "(src)/**/*.{html,ts}": [ 93 | "eslint --fix" 94 | ], 95 | "(src)/**/*.less": [ 96 | "stylelint --syntax less --fix" 97 | ] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /frontend/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnInit, Renderer2 } from '@angular/core'; 2 | import { NavigationEnd, Router } from '@angular/router'; 3 | import { TitleService, VERSION as VERSION_ALAIN } from '@delon/theme'; 4 | import { NzModalService } from 'ng-zorro-antd/modal'; 5 | import { VERSION as VERSION_ZORRO } from 'ng-zorro-antd/version'; 6 | import { filter } from 'rxjs/operators'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | template: ` ` 11 | }) 12 | export class AppComponent implements OnInit { 13 | constructor( 14 | el: ElementRef, 15 | renderer: Renderer2, 16 | private router: Router, 17 | private titleSrv: TitleService, 18 | private modalSrv: NzModalService 19 | ) { 20 | renderer.setAttribute(el.nativeElement, 'ng-alain-version', VERSION_ALAIN.full); 21 | renderer.setAttribute(el.nativeElement, 'ng-zorro-version', VERSION_ZORRO.full); 22 | } 23 | 24 | ngOnInit(): void { 25 | this.router.events.pipe(filter(evt => evt instanceof NavigationEnd)).subscribe(() => { 26 | this.titleSrv.setTitle(); 27 | this.modalSrv.closeAll(); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/order */ 2 | /* eslint-disable import/no-duplicates */ 3 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 4 | import { APP_INITIALIZER, Injector, LOCALE_ID, NgModule, Type } from '@angular/core'; 5 | import { BrowserModule } from '@angular/platform-browser'; 6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7 | import { NzMessageModule } from 'ng-zorro-antd/message'; 8 | import { NzNotificationModule } from 'ng-zorro-antd/notification'; 9 | import { Observable } from 'rxjs'; 10 | 11 | // #region default language 12 | // Reference: https://ng-alain.com/docs/i18n 13 | import { default as ngLang } from '@angular/common/locales/en'; 14 | import { DELON_LOCALE, en_US as delonLang } from '@delon/theme'; 15 | import { zhCN as dateLang } from 'date-fns/locale'; 16 | import { NZ_DATE_LOCALE, NZ_I18N, en_US as zorroLang } from 'ng-zorro-antd/i18n'; 17 | const LANG = { 18 | abbr: 'en', 19 | ng: ngLang, 20 | zorro: zorroLang, 21 | date: dateLang, 22 | delon: delonLang 23 | }; 24 | // register angular 25 | import { registerLocaleData } from '@angular/common'; 26 | registerLocaleData(LANG.ng, LANG.abbr); 27 | const LANG_PROVIDES = [ 28 | { provide: LOCALE_ID, useValue: LANG.abbr }, 29 | { provide: NZ_I18N, useValue: LANG.zorro }, 30 | { provide: NZ_DATE_LOCALE, useValue: LANG.date }, 31 | { provide: DELON_LOCALE, useValue: LANG.delon } 32 | ]; 33 | // #endregion 34 | 35 | // #region JSON Schema form (using @delon/form) 36 | import { JsonSchemaModule } from '@shared'; 37 | const FORM_MODULES = [JsonSchemaModule]; 38 | // #endregion 39 | 40 | // #region Http Interceptors 41 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 42 | import { DefaultInterceptor } from '@core'; 43 | import { SimpleInterceptor } from '@delon/auth'; 44 | const INTERCEPTOR_PROVIDES = [ 45 | { provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true }, 46 | { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true } 47 | ]; 48 | // #endregion 49 | 50 | // #region global third module 51 | const GLOBAL_THIRD_MODULES: Array> = []; 52 | // #endregion 53 | 54 | // #region Startup Service 55 | import { StartupService } from '@core'; 56 | export function StartupServiceFactory(startupService: StartupService): () => Observable { 57 | return () => startupService.load(); 58 | } 59 | const APPINIT_PROVIDES = [ 60 | StartupService, 61 | { 62 | provide: APP_INITIALIZER, 63 | useFactory: StartupServiceFactory, 64 | deps: [StartupService], 65 | multi: true 66 | } 67 | ]; 68 | // #endregion 69 | 70 | import { environment } from '@env/environment'; 71 | import { API_BASE_URL } from './shared/service-proxies/service-proxies'; 72 | export function getRemoteServiceBaseUrl(): string { 73 | return environment.api.baseUrl; 74 | } 75 | 76 | import { AppComponent } from './app.component'; 77 | import { CoreModule } from './core/core.module'; 78 | import { GlobalConfigModule } from './global-config.module'; 79 | import { LayoutModule } from './layout/layout.module'; 80 | import { RoutesModule } from './routes/routes.module'; 81 | import { SharedModule } from './shared/shared.module'; 82 | import { STWidgetModule } from './shared/st-widget/st-widget.module'; 83 | 84 | @NgModule({ 85 | declarations: [AppComponent], 86 | imports: [ 87 | BrowserModule, 88 | BrowserAnimationsModule, 89 | HttpClientModule, 90 | GlobalConfigModule.forRoot(), 91 | CoreModule, 92 | SharedModule, 93 | LayoutModule, 94 | RoutesModule, 95 | STWidgetModule, 96 | NzMessageModule, 97 | NzNotificationModule, 98 | ...FORM_MODULES, 99 | ...GLOBAL_THIRD_MODULES 100 | ], 101 | providers: [ 102 | ...LANG_PROVIDES, 103 | ...INTERCEPTOR_PROVIDES, 104 | ...APPINIT_PROVIDES, 105 | { provide: API_BASE_URL, useFactory: getRemoteServiceBaseUrl } 106 | ], 107 | bootstrap: [AppComponent] 108 | }) 109 | export class AppModule {} 110 | -------------------------------------------------------------------------------- /frontend/src/app/core/README.md: -------------------------------------------------------------------------------- 1 | ### CoreModule 2 | 3 | **应** 仅只留 `providers` 属性。 4 | 5 | **作用:** 一些通用服务,例如:用户消息、HTTP数据访问。 6 | -------------------------------------------------------------------------------- /frontend/src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Optional, SkipSelf } from '@angular/core'; 2 | 3 | import { throwIfAlreadyLoaded } from './module-import-guard'; 4 | 5 | @NgModule({ 6 | providers: [] 7 | }) 8 | export class CoreModule { 9 | constructor(@Optional() @SkipSelf() parentModule: CoreModule) { 10 | throwIfAlreadyLoaded(parentModule, 'CoreModule'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/app/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module-import-guard'; 2 | export * from './net/default.interceptor'; 3 | export * from './startup/startup.service'; 4 | -------------------------------------------------------------------------------- /frontend/src/app/core/module-import-guard.ts: -------------------------------------------------------------------------------- 1 | // https://angular.io/guide/styleguide#style-04-12 2 | export function throwIfAlreadyLoaded(parentModule: any, moduleName: string): void { 3 | if (parentModule) { 4 | throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/app/core/net/default.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpErrorResponse, 3 | HttpEvent, 4 | HttpHandler, 5 | HttpHeaders, 6 | HttpInterceptor, 7 | HttpRequest, 8 | HttpResponseBase 9 | } from '@angular/common/http'; 10 | import { Injectable, Injector } from '@angular/core'; 11 | import { Router } from '@angular/router'; 12 | import { ALAIN_I18N_TOKEN, _HttpClient } from '@delon/theme'; 13 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 14 | import { Observable, of, throwError } from 'rxjs'; 15 | import { catchError, mergeMap } from 'rxjs/operators'; 16 | 17 | @Injectable() 18 | export class DefaultInterceptor implements HttpInterceptor { 19 | constructor(private injector: Injector) {} 20 | 21 | private get notification(): NzNotificationService { 22 | return this.injector.get(NzNotificationService); 23 | } 24 | 25 | private goTo(url: string): void { 26 | setTimeout(() => this.injector.get(Router).navigateByUrl(url)); 27 | } 28 | 29 | private handleData(ev: HttpResponseBase, req: HttpRequest, next: HttpHandler): Observable { 30 | switch (ev.status) { 31 | case 200: 32 | break; 33 | case 401: 34 | this.notification.error(`Login`, `Not logged in or login expired, please log in again`); 35 | this.goTo('/passport/login'); 36 | break; 37 | case 403: 38 | case 404: 39 | this.goTo(`/exception/${ev.status}`); 40 | break; 41 | default: 42 | console.log(ev); 43 | if (ev instanceof HttpErrorResponse) { 44 | this.blobToText(ev.error).subscribe(resp => { 45 | this.notification.error(ev.statusText, `${resp}\n${ev.url}`, { nzDuration: 15000 }); 46 | }); 47 | return throwError(ev); 48 | } 49 | break; 50 | } 51 | if (ev instanceof HttpErrorResponse) { 52 | return throwError(ev); 53 | } else { 54 | return of(ev); 55 | } 56 | } 57 | 58 | private getAdditionalHeaders(headers?: HttpHeaders): { [name: string]: string } { 59 | const res: { [name: string]: string } = {}; 60 | const lang = this.injector.get(ALAIN_I18N_TOKEN).currentLang; 61 | if (!headers?.has('Accept-Language') && lang) { 62 | res['Accept-Language'] = lang; 63 | } 64 | return res; 65 | } 66 | 67 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 68 | const newReq = req.clone({ url: req.url, setHeaders: this.getAdditionalHeaders(req.headers) }); 69 | return next.handle(newReq).pipe( 70 | mergeMap(ev => { 71 | if (ev instanceof HttpResponseBase) { 72 | return this.handleData(ev, newReq, next); 73 | } 74 | return of(ev); 75 | }), 76 | catchError((err: HttpErrorResponse) => this.handleData(err, newReq, next)) 77 | ); 78 | } 79 | 80 | blobToText(blob: any): Observable { 81 | return new Observable((observer: any) => { 82 | if (blob instanceof Blob) { 83 | let reader = new FileReader(); 84 | reader.onload = event => { 85 | observer.next((event.target).result); 86 | observer.complete(); 87 | }; 88 | reader.readAsText(blob); 89 | } else { 90 | observer.next(blob && blob.error ? blob.error : ''); 91 | observer.complete(); 92 | } 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /frontend/src/app/core/startup/startup.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ACLService } from '@delon/acl'; 3 | import { MenuService, SettingsService, TitleService } from '@delon/theme'; 4 | import type { NzSafeAny } from 'ng-zorro-antd/core/types'; 5 | import { NzIconService } from 'ng-zorro-antd/icon'; 6 | import { Observable, zip, of } from 'rxjs'; 7 | import { catchError, map } from 'rxjs/operators'; 8 | import { AccountServiceProxy, ServiceProxy } from 'src/app/shared/service-proxies/service-proxies'; 9 | 10 | import { ICONS } from '../../../style-icons'; 11 | import { ICONS_AUTO } from '../../../style-icons-auto'; 12 | 13 | /** 14 | * Used for application startup 15 | * Generally used to get the basic data of the application, like: Menu Data, User Data, etc. 16 | */ 17 | @Injectable() 18 | export class StartupService { 19 | constructor( 20 | iconSrv: NzIconService, 21 | private menuService: MenuService, 22 | private settingService: SettingsService, 23 | private aclService: ACLService, 24 | private titleService: TitleService, 25 | private accountService: AccountServiceProxy, 26 | private readonly serviceProxy: ServiceProxy 27 | ) { 28 | iconSrv.addIcon(...ICONS_AUTO, ...ICONS); 29 | } 30 | 31 | load(): Observable { 32 | const app: any = { 33 | name: `Go Micro`, 34 | description: `go-micro dashboard` 35 | }; 36 | // Application information: including site name, description, year 37 | this.settingService.setApp(app); 38 | // ACL: Set the permissions to full, https://ng-alain.com/acl/getting-started 39 | this.aclService.setFull(true); 40 | // Menu data, https://ng-alain.com/theme/menu 41 | this.menuService.add([ 42 | { 43 | text: 'Main', 44 | group: true, 45 | children: [ 46 | { 47 | text: 'Dashboard', 48 | link: '/dashboard', 49 | icon: { type: 'icon', value: 'dashboard' } 50 | } 51 | ] 52 | }, 53 | { 54 | text: 'Services', 55 | group: true, 56 | children: [ 57 | { 58 | text: 'Services', 59 | link: '/services', 60 | icon: { type: 'icon', value: 'cloud' } 61 | }, 62 | { 63 | text: 'Nodes', 64 | link: '/service/nodes', 65 | icon: { type: 'icon', value: 'apartment' } 66 | } 67 | ] 68 | }, 69 | { 70 | text: 'Client', 71 | group: true, 72 | children: [ 73 | { 74 | text: 'Call', 75 | link: '/client/call', 76 | icon: { type: 'icon', value: 'api' } 77 | }, 78 | { 79 | text: 'Publish', 80 | link: '/client/publish', 81 | icon: { type: 'icon', value: 'sound' } 82 | } 83 | ] 84 | } 85 | ]); 86 | this.serviceProxy.getVersion().subscribe(resp => { 87 | this.settingService.setData('version', resp.version); 88 | }); 89 | // Can be set page suffix title, https://ng-alain.com/theme/title 90 | this.titleService.suffix = app.name; 91 | return this.accountService.profile().pipe( 92 | catchError((res: NzSafeAny) => { 93 | console.warn(`StartupService.load: Network request failed`, res); 94 | return of({ name: 'admin' }); 95 | }), 96 | map(resp => { 97 | // User information: including name, avatar, email address 98 | this.settingService.setUser({ name: resp.name }); 99 | }) 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /frontend/src/app/global-config.module.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/order */ 2 | import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; 3 | import { DelonACLModule } from '@delon/acl'; 4 | import { AlainThemeModule } from '@delon/theme'; 5 | import { AlainConfig, ALAIN_CONFIG } from '@delon/util/config'; 6 | 7 | import { throwIfAlreadyLoaded } from '@core'; 8 | 9 | import { environment } from '@env/environment'; 10 | 11 | // Please refer to: https://ng-alain.com/docs/global-config 12 | // #region NG-ALAIN Config 13 | 14 | const alainConfig: AlainConfig = { 15 | st: { modal: { size: 'lg' } }, 16 | pageHeader: { homeI18n: 'home' }, 17 | lodop: { 18 | license: `A59B099A586B3851E0F0D7FDBF37B603`, 19 | licenseA: `C94CEE276DB2187AE6B65D56B3FC2848` 20 | }, 21 | auth: { 22 | login_url: '/passport/login', 23 | token_send_key: 'Authorization', 24 | token_send_template: 'Bearer ${token}', 25 | ignores: [/\/login/, /assets\//, /passport\//, /\/version/] 26 | } 27 | }; 28 | 29 | const alainModules: any[] = [AlainThemeModule.forRoot(), DelonACLModule.forRoot()]; 30 | const alainProvides = [{ provide: ALAIN_CONFIG, useValue: alainConfig }]; 31 | 32 | // #region reuse-tab 33 | /** 34 | * 若需要[路由复用](https://ng-alain.com/components/reuse-tab)需要: 35 | * 1、在 `shared-delon.module.ts` 导入 `ReuseTabModule` 模块 36 | * 2、注册 `RouteReuseStrategy` 37 | * 3、在 `src/app/layout/default/default.component.html` 修改: 38 | * ```html 39 | * 40 | * 41 | * 42 | * 43 | * ``` 44 | */ 45 | // import { RouteReuseStrategy } from '@angular/router'; 46 | // import { ReuseTabService, ReuseTabStrategy } from '@delon/abc/reuse-tab'; 47 | // alainProvides.push({ 48 | // provide: RouteReuseStrategy, 49 | // useClass: ReuseTabStrategy, 50 | // deps: [ReuseTabService], 51 | // } as any); 52 | 53 | // #endregion 54 | 55 | // #endregion 56 | 57 | // Please refer to: https://ng.ant.design/docs/global-config/en#how-to-use 58 | // #region NG-ZORRO Config 59 | 60 | import { NzConfig, NZ_CONFIG } from 'ng-zorro-antd/core/config'; 61 | 62 | const ngZorroConfig: NzConfig = {}; 63 | 64 | const zorroProvides = [{ provide: NZ_CONFIG, useValue: ngZorroConfig }]; 65 | 66 | // #endregion 67 | 68 | @NgModule({ 69 | imports: [...alainModules, ...(environment.modules || [])] 70 | }) 71 | export class GlobalConfigModule { 72 | constructor(@Optional() @SkipSelf() parentModule: GlobalConfigModule) { 73 | throwIfAlreadyLoaded(parentModule, 'GlobalConfigModule'); 74 | } 75 | 76 | static forRoot(): ModuleWithProviders { 77 | return { 78 | ngModule: GlobalConfigModule, 79 | providers: [...alainProvides, ...zorroProvides] 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /frontend/src/app/layout/basic/README.md: -------------------------------------------------------------------------------- 1 | [Document](https://ng-alain.com/theme/default) 2 | -------------------------------------------------------------------------------- /frontend/src/app/layout/basic/basic.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { SettingsService, User } from '@delon/theme'; 3 | import { LayoutDefaultOptions } from '@delon/theme/layout-default'; 4 | import { environment } from '@env/environment'; 5 | 6 | @Component({ 7 | selector: 'layout-basic', 8 | template: ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ` 26 | }) 27 | export class LayoutBasicComponent { 28 | options: LayoutDefaultOptions = { 29 | logoExpanded: `./assets/logo-full.png`, 30 | logoCollapsed: `./assets/logo.png` 31 | }; 32 | searchToggleStatus = false; 33 | showSettingDrawer = !environment.production; 34 | get user(): User { 35 | return this.settings.user; 36 | } 37 | 38 | constructor(private settings: SettingsService) {} 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/app/layout/basic/widgets/clear-storage.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core'; 2 | import { NzMessageService } from 'ng-zorro-antd/message'; 3 | import { NzModalService } from 'ng-zorro-antd/modal'; 4 | 5 | @Component({ 6 | selector: 'header-clear-storage', 7 | template: ` 8 | 9 | Clear Local Storage 10 | `, 11 | host: { 12 | '[class.d-block]': 'true' 13 | }, 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class HeaderClearStorageComponent { 17 | constructor(private modalSrv: NzModalService, private messageSrv: NzMessageService) {} 18 | 19 | @HostListener('click') 20 | _click(): void { 21 | this.modalSrv.confirm({ 22 | nzTitle: 'Make sure clear all local storage?', 23 | nzOnOk: () => { 24 | localStorage.clear(); 25 | this.messageSrv.success('Clear Finished!'); 26 | } 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/app/layout/basic/widgets/fullscreen.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core'; 2 | import * as screenfull from 'screenfull'; 3 | 4 | @Component({ 5 | selector: 'header-fullscreen', 6 | template: ` 7 | 8 | {{ status ? 'Exit Fullscreen' : 'Fullscreen' }} 9 | `, 10 | host: { 11 | '[class.d-block]': 'true' 12 | }, 13 | changeDetection: ChangeDetectionStrategy.OnPush 14 | }) 15 | export class HeaderFullScreenComponent { 16 | status = false; 17 | private get sf(): screenfull.Screenfull { 18 | return screenfull as screenfull.Screenfull; 19 | } 20 | 21 | @HostListener('window:resize') 22 | _resize(): void { 23 | this.status = this.sf.isFullscreen; 24 | } 25 | 26 | @HostListener('click') 27 | _click(): void { 28 | if (this.sf.isEnabled) { 29 | this.sf.toggle(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/layout/basic/widgets/search.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | ChangeDetectionStrategy, 4 | ChangeDetectorRef, 5 | Component, 6 | ElementRef, 7 | EventEmitter, 8 | HostBinding, 9 | Input, 10 | OnDestroy, 11 | Output 12 | } from '@angular/core'; 13 | import { BehaviorSubject } from 'rxjs'; 14 | import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; 15 | 16 | @Component({ 17 | selector: 'header-search', 18 | template: ` 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 36 | 37 | 38 | {{ i }} 39 | 40 | `, 41 | changeDetection: ChangeDetectionStrategy.OnPush 42 | }) 43 | export class HeaderSearchComponent implements AfterViewInit, OnDestroy { 44 | q = ''; 45 | qIpt: HTMLInputElement | null = null; 46 | options: string[] = []; 47 | search$ = new BehaviorSubject(''); 48 | loading = false; 49 | 50 | @HostBinding('class.alain-default__search-focus') 51 | focus = false; 52 | @HostBinding('class.alain-default__search-toggled') 53 | searchToggled = false; 54 | 55 | @Input() 56 | set toggleChange(value: boolean) { 57 | if (typeof value === 'undefined') { 58 | return; 59 | } 60 | this.searchToggled = value; 61 | this.focus = value; 62 | if (value) { 63 | setTimeout(() => this.qIpt!.focus()); 64 | } 65 | } 66 | @Output() readonly toggleChangeChange = new EventEmitter(); 67 | 68 | constructor(private el: ElementRef, private cdr: ChangeDetectorRef) {} 69 | 70 | ngAfterViewInit(): void { 71 | this.qIpt = this.el.nativeElement.querySelector('.ant-input') as HTMLInputElement; 72 | this.search$ 73 | .pipe( 74 | debounceTime(500), 75 | distinctUntilChanged(), 76 | tap({ 77 | complete: () => { 78 | this.loading = true; 79 | } 80 | }) 81 | ) 82 | .subscribe(value => { 83 | this.options = value ? [value, value + value, value + value + value] : []; 84 | this.loading = false; 85 | this.cdr.detectChanges(); 86 | }); 87 | } 88 | 89 | qFocus(): void { 90 | this.focus = true; 91 | } 92 | 93 | qBlur(): void { 94 | this.focus = false; 95 | this.searchToggled = false; 96 | this.options.length = 0; 97 | this.toggleChangeChange.emit(false); 98 | } 99 | 100 | search(ev: Event): void { 101 | this.search$.next((ev.target as HTMLInputElement).value); 102 | } 103 | 104 | ngOnDestroy(): void { 105 | this.search$.complete(); 106 | this.search$.unsubscribe(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /frontend/src/app/layout/basic/widgets/user.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth'; 4 | import { SettingsService, User } from '@delon/theme'; 5 | 6 | @Component({ 7 | selector: 'header-user', 8 | template: ` 9 | 10 | {{ user.name }} 11 | 12 | 13 | 14 | 15 | 16 | About 17 | 18 | 19 | 20 | 21 | Logout 22 | 23 | 24 | 25 | 32 | 33 | Version: {{ version }} 34 | 35 | 36 | `, 37 | changeDetection: ChangeDetectionStrategy.OnPush 38 | }) 39 | export class HeaderUserComponent { 40 | get user(): User { 41 | return this.settings.user; 42 | } 43 | aboutVisible = false; 44 | version: string = ''; 45 | 46 | constructor(private settings: SettingsService, private router: Router, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {} 47 | 48 | about(): void { 49 | this.aboutVisible = true; 50 | this.version = this.settings.getData('version'); 51 | } 52 | 53 | logout(): void { 54 | this.tokenService.clear(); 55 | this.router.navigateByUrl(this.tokenService.login_url!); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/app/layout/blank/README.md: -------------------------------------------------------------------------------- 1 | [Document](https://ng-alain.com/theme/blank) 2 | -------------------------------------------------------------------------------- /frontend/src/app/layout/blank/blank.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'layout-blank', 5 | template: ` `, 6 | host: { 7 | '[class.alain-blank]': 'true' 8 | } 9 | }) 10 | export class LayoutBlankComponent {} 11 | -------------------------------------------------------------------------------- /frontend/src/app/layout/layout.module.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/order */ 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { RouterModule } from '@angular/router'; 6 | import { GlobalFooterModule } from '@delon/abc/global-footer'; 7 | import { NoticeIconModule } from '@delon/abc/notice-icon'; 8 | import { LayoutDefaultModule } from '@delon/theme/layout-default'; 9 | import { SettingDrawerModule } from '@delon/theme/setting-drawer'; 10 | import { ThemeBtnModule } from '@delon/theme/theme-btn'; 11 | import { NzAutocompleteModule } from 'ng-zorro-antd/auto-complete'; 12 | import { NzAvatarModule } from 'ng-zorro-antd/avatar'; 13 | import { NzBadgeModule } from 'ng-zorro-antd/badge'; 14 | import { NzDropDownModule } from 'ng-zorro-antd/dropdown'; 15 | import { NzFormModule } from 'ng-zorro-antd/form'; 16 | import { NzGridModule } from 'ng-zorro-antd/grid'; 17 | import { NzIconModule } from 'ng-zorro-antd/icon'; 18 | import { NzInputModule } from 'ng-zorro-antd/input'; 19 | import { NzSpinModule } from 'ng-zorro-antd/spin'; 20 | import { NzModalModule } from 'ng-zorro-antd/modal'; 21 | 22 | import { LayoutBasicComponent } from './basic/basic.component'; 23 | import { HeaderClearStorageComponent } from './basic/widgets/clear-storage.component'; 24 | import { HeaderFullScreenComponent } from './basic/widgets/fullscreen.component'; 25 | import { HeaderSearchComponent } from './basic/widgets/search.component'; 26 | import { HeaderUserComponent } from './basic/widgets/user.component'; 27 | import { LayoutBlankComponent } from './blank/blank.component'; 28 | 29 | const COMPONENTS = [LayoutBasicComponent, LayoutBlankComponent]; 30 | 31 | const HEADERCOMPONENTS = [HeaderSearchComponent, HeaderFullScreenComponent, HeaderClearStorageComponent, HeaderUserComponent]; 32 | 33 | // passport 34 | import { LayoutPassportComponent } from './passport/passport.component'; 35 | const PASSPORT = [LayoutPassportComponent]; 36 | 37 | @NgModule({ 38 | imports: [ 39 | CommonModule, 40 | FormsModule, 41 | RouterModule, 42 | ThemeBtnModule, 43 | SettingDrawerModule, 44 | LayoutDefaultModule, 45 | NoticeIconModule, 46 | GlobalFooterModule, 47 | NzDropDownModule, 48 | NzInputModule, 49 | NzAutocompleteModule, 50 | NzGridModule, 51 | NzFormModule, 52 | NzSpinModule, 53 | NzBadgeModule, 54 | NzAvatarModule, 55 | NzIconModule, 56 | NzModalModule 57 | ], 58 | declarations: [...COMPONENTS, ...HEADERCOMPONENTS, ...PASSPORT], 59 | exports: [...COMPONENTS, ...PASSPORT] 60 | }) 61 | export class LayoutModule {} 62 | -------------------------------------------------------------------------------- /frontend/src/app/layout/passport/passport.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Github 11 | {{ version }} 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/src/app/layout/passport/passport.component.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/index'; 2 | :host ::ng-deep { 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | min-height: 100%; 7 | background: #f0f2f5; 8 | } 9 | .langs { 10 | width: 100%; 11 | height: 40px; 12 | line-height: 44px; 13 | text-align: right; 14 | .anticon { 15 | margin-top: 24px; 16 | margin-right: 24px; 17 | font-size: 14px; 18 | vertical-align: top; 19 | cursor: pointer; 20 | } 21 | } 22 | .wrap { 23 | flex: 1; 24 | padding: 32px 0; 25 | } 26 | .ant-form-item { 27 | display: flex; 28 | justify-content: space-between; 29 | margin-bottom: 24px; 30 | } 31 | 32 | @media (min-width: @screen-md-min) { 33 | .container { 34 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); 35 | background-repeat: no-repeat; 36 | background-position: center 110px; 37 | background-size: 100%; 38 | } 39 | .wrap { 40 | padding: 32px 0 24px; 41 | } 42 | } 43 | .top { 44 | text-align: center; 45 | } 46 | .header { 47 | height: 44px; 48 | line-height: 44px; 49 | a { 50 | text-decoration: none; 51 | } 52 | } 53 | .logo { 54 | height: 96px; 55 | margin-top: 64px; 56 | margin-right: 16px; 57 | margin-bottom: 64px; 58 | } 59 | .title { 60 | position: relative; 61 | color: @heading-color; 62 | font-weight: 600; 63 | font-size: 33px; 64 | font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif; 65 | vertical-align: middle; 66 | } 67 | .desc { 68 | margin-top: 12px; 69 | margin-bottom: 40px; 70 | color: @text-color-secondary; 71 | font-size: @font-size-base; 72 | } 73 | } 74 | 75 | [data-theme='dark'] { 76 | :host ::ng-deep { 77 | .container { 78 | background: #141414; 79 | } 80 | .title { 81 | color: fade(@white, 85%); 82 | } 83 | .desc { 84 | color: fade(@white, 45%); 85 | } 86 | @media (min-width: @screen-md-min) { 87 | .container { 88 | background-image: none; 89 | } 90 | } 91 | } 92 | } 93 | 94 | [data-theme='compact'] { 95 | :host ::ng-deep { 96 | .ant-form-item { 97 | margin-bottom: 16px; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /frontend/src/app/layout/passport/passport.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth'; 3 | import { ServiceProxy } from 'src/app/shared/service-proxies/service-proxies'; 4 | 5 | @Component({ 6 | selector: 'layout-passport', 7 | templateUrl: './passport.component.html', 8 | styleUrls: ['./passport.component.less'] 9 | }) 10 | export class LayoutPassportComponent implements OnInit { 11 | constructor(@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService, private readonly service: ServiceProxy) {} 12 | 13 | version: string = ''; 14 | 15 | ngOnInit(): void { 16 | this.tokenService.clear(); 17 | this.service.getVersion().subscribe(resp => { 18 | this.version = resp.version; 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/app/routes/client/call.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard 6 | 7 | 8 | Services 9 | 10 | Call 11 | 12 | 13 | 14 | Refresh 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 70 | 71 | 72 | 73 | 74 | 82 | 83 | 84 | 85 | 86 | Timeout(Seconds) 87 | 95 | 96 | 97 | 98 | Call 99 | 100 | 101 | 102 | 103 | 104 | {{ response | json }} 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /frontend/src/app/routes/client/call.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { NzMessageService } from 'ng-zorro-antd/message'; 4 | import { ClipboardService } from 'ngx-clipboard'; 5 | import { finalize } from 'rxjs/operators'; 6 | import { 7 | CallRequest, 8 | ClientServiceProxy, 9 | RegistryEndpoint, 10 | RegistryServiceProxy, 11 | RegistryServiceSummary, 12 | RegistryValue 13 | } from 'src/app/shared/service-proxies/service-proxies'; 14 | 15 | interface RequestPayload { 16 | [key: string]: any; 17 | } 18 | @Component({ 19 | selector: 'micro-client-call', 20 | templateUrl: './call.component.html', 21 | changeDetection: ChangeDetectionStrategy.Default 22 | }) 23 | export class ClientCallComponent implements OnInit { 24 | loading = false; 25 | service: string = ''; 26 | version: string = ''; 27 | endpoint: string = ''; 28 | timeout = 10; 29 | metadata: any = undefined; 30 | request: any = undefined; 31 | response: any = undefined; 32 | 33 | services: RegistryServiceSummary[] = []; 34 | selectedService: RegistryServiceSummary | undefined = undefined; 35 | endpoints: RegistryEndpoint[] = []; 36 | selectedEndpoint: RegistryEndpoint | undefined = undefined; 37 | 38 | constructor( 39 | private readonly route: ActivatedRoute, 40 | private readonly clientService: ClientServiceProxy, 41 | private readonly clipboardService: ClipboardService, 42 | private readonly messageService: NzMessageService, 43 | private readonly registryService: RegistryServiceProxy 44 | ) {} 45 | 46 | ngOnInit(): void { 47 | var service = this.route.snapshot.queryParams['service']; 48 | if (service) { 49 | this.service = service; 50 | } 51 | var version = this.route.snapshot.queryParams['version']; 52 | if (version) { 53 | this.version = version; 54 | } 55 | var endpoint = this.route.snapshot.queryParams['endpoint']; 56 | if (endpoint) { 57 | this.endpoint = endpoint; 58 | } 59 | this.load(); 60 | } 61 | 62 | load() { 63 | this.loading = true; 64 | this.selectedEndpoint = undefined; 65 | this.services = []; 66 | this.response = undefined; 67 | this.registryService 68 | .getServices() 69 | .pipe( 70 | finalize(() => { 71 | this.loading = false; 72 | }) 73 | ) 74 | .subscribe(resp => { 75 | this.services = resp.services; 76 | if (!this.service || !resp.services.length) { 77 | return; 78 | } 79 | resp.services.forEach(s => { 80 | if (s.name == this.service) { 81 | this.selectedService = s; 82 | if (!this.version) { 83 | this.version = s.versions ? s.versions[0] : ''; 84 | } 85 | } 86 | }); 87 | this.loadEndpoints(); 88 | }); 89 | } 90 | 91 | call() { 92 | this.loading = true; 93 | this.response = undefined; 94 | var input = new CallRequest({ 95 | service: this.service, 96 | version: this.version, 97 | endpoint: this.endpoint, 98 | metadata: JSON.stringify(this.metadata), 99 | request: JSON.stringify(this.request), 100 | timeout: this.timeout 101 | }); 102 | this.clientService 103 | .call(input) 104 | .pipe( 105 | finalize(() => { 106 | this.loading = false; 107 | }) 108 | ) 109 | .subscribe(resp => { 110 | this.response = resp; 111 | }); 112 | } 113 | 114 | serviceChanged(service: RegistryServiceSummary) { 115 | this.service = service.name; 116 | if (service.versions && service.versions.length) { 117 | this.version = service.versions[0]; 118 | } 119 | this.endpoint = ''; 120 | this.selectedEndpoint = undefined; 121 | this.loadEndpoints(); 122 | } 123 | 124 | versionChanged(version: string) { 125 | this.version = version; 126 | this.loadEndpoints(); 127 | } 128 | 129 | endpointChanged(endpoint: RegistryEndpoint) { 130 | this.endpoint = endpoint.name; 131 | this.loadEndpointReuqest(endpoint); 132 | } 133 | 134 | loadEndpointReuqest(endpoint: RegistryEndpoint) { 135 | var previousRequest = localStorage.getItem(`${endpoint.name}.call`); 136 | if (previousRequest) { 137 | try { 138 | this.request = eval(`(${previousRequest})`); 139 | } catch (e) { 140 | // SyntaxError 141 | } 142 | } else { 143 | this.updateRequestPayload(endpoint.request); 144 | } 145 | } 146 | 147 | metadataChanged(metadata: string) { 148 | try { 149 | this.metadata = eval(`(${metadata})`); 150 | } catch (e) { 151 | // SyntaxError 152 | } 153 | } 154 | 155 | requestChanged(request: string) { 156 | try { 157 | this.request = eval(`(${request})`); 158 | localStorage.setItem(`${this.endpoint}.call`, JSON.stringify(this.request)); 159 | } catch (e) { 160 | // SyntaxError 161 | } 162 | } 163 | 164 | copyToClipboard(text: string) { 165 | this.clipboardService.copy(JSON.stringify(text)); 166 | this.messageService.create('success', `Copied to Clipboard`); 167 | } 168 | 169 | private loadEndpoints() { 170 | if (!this.service) { 171 | return; 172 | } 173 | this.endpoints = []; 174 | this.request = undefined; 175 | this.registryService.getServiceHandlers(this.service, this.version).subscribe(resp => { 176 | this.endpoints = resp.handlers ? resp.handlers : []; 177 | if (resp.handlers && resp.handlers.length) { 178 | if (this.endpoint) { 179 | resp.handlers?.forEach(e => { 180 | if (e.name == this.endpoint) { 181 | this.selectedEndpoint = e; 182 | this.loadEndpointReuqest(this.selectedEndpoint); 183 | } 184 | }); 185 | } else { 186 | this.selectedEndpoint = this.endpoints[0]; 187 | this.endpoint = this.endpoints[0].name; 188 | this.loadEndpointReuqest(this.selectedEndpoint); 189 | } 190 | } 191 | }); 192 | } 193 | 194 | private updateRequestPayload(request: RegistryValue) { 195 | let payload: RequestPayload = {}; 196 | if (request && request.values) { 197 | request.values.forEach(v => { 198 | if (!v.name || v.name === 'MessageState' || v.name === 'int32' || v.name === 'unknownFields') { 199 | return; 200 | } 201 | let value: any; 202 | switch (v.type) { 203 | case 'string': 204 | value = ''; 205 | break; 206 | case 'int': 207 | case 'int32': 208 | case 'int64': 209 | case 'uint': 210 | case 'uint32': 211 | case 'uint64': 212 | value = 0; 213 | break; 214 | case 'float64': 215 | case 'float32': 216 | value = 0.0; 217 | break; 218 | case 'bool': 219 | value = false; 220 | break; 221 | default: 222 | if (v.type.startsWith('[]')) { 223 | if (v.type.endsWith('byte') || v.type.endsWith('int8')) { 224 | value = 'base64'; 225 | } else { 226 | value = []; 227 | } 228 | } else { 229 | console.log(v.type); 230 | value = v.type; 231 | } 232 | } 233 | payload[v.name] = value; 234 | }); 235 | } 236 | this.request = payload; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /frontend/src/app/routes/client/publish.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard 6 | 7 | 8 | Services 9 | 10 | Publish 11 | 12 | 13 | 14 | Refresh 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 70 | 71 | 72 | 73 | 74 | 82 | 83 | 84 | 85 | Publish 86 | 87 | 88 | 89 | 90 | 91 | {{ response | json }} 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /frontend/src/app/routes/client/publish.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { NzMessageService } from 'ng-zorro-antd/message'; 4 | import { ClipboardService } from 'ngx-clipboard'; 5 | import { finalize } from 'rxjs/operators'; 6 | import { 7 | ClientServiceProxy, 8 | PublishRequest, 9 | RegistryEndpoint, 10 | RegistryServiceProxy, 11 | RegistryServiceSummary, 12 | RegistryValue 13 | } from 'src/app/shared/service-proxies/service-proxies'; 14 | 15 | interface RequestPayload { 16 | [key: string]: any; 17 | } 18 | @Component({ 19 | selector: 'micro-client-publish', 20 | templateUrl: './publish.component.html', 21 | changeDetection: ChangeDetectionStrategy.Default 22 | }) 23 | export class ClientPublishComponent implements OnInit { 24 | loading = false; 25 | service: string = ''; 26 | version: string = ''; 27 | topic: string = ''; 28 | timeout = 10; 29 | metadata: any = undefined; 30 | request: any = undefined; 31 | response: any = undefined; 32 | 33 | services: RegistryServiceSummary[] = []; 34 | selectedService: RegistryServiceSummary | undefined = undefined; 35 | endpoints: RegistryEndpoint[] = []; 36 | selectedEndpoint: RegistryEndpoint | undefined = undefined; 37 | 38 | constructor( 39 | private readonly route: ActivatedRoute, 40 | private readonly clientService: ClientServiceProxy, 41 | private readonly clipboardService: ClipboardService, 42 | private readonly messageService: NzMessageService, 43 | private readonly registryService: RegistryServiceProxy 44 | ) {} 45 | 46 | ngOnInit(): void { 47 | var service = this.route.snapshot.queryParams['service']; 48 | if (service) { 49 | this.service = service; 50 | } 51 | var version = this.route.snapshot.queryParams['version']; 52 | if (version) { 53 | this.version = version; 54 | } 55 | var topic = this.route.snapshot.queryParams['topic']; 56 | if (topic) { 57 | this.topic = topic; 58 | } 59 | this.load(); 60 | } 61 | 62 | load() { 63 | this.loading = true; 64 | this.selectedEndpoint = undefined; 65 | this.services = []; 66 | this.response = undefined; 67 | this.registryService 68 | .getServices() 69 | .pipe( 70 | finalize(() => { 71 | this.loading = false; 72 | }) 73 | ) 74 | .subscribe(resp => { 75 | this.services = resp.services; 76 | if (!this.service || !resp.services.length) { 77 | return; 78 | } 79 | resp.services.forEach(s => { 80 | if (s.name == this.service) { 81 | this.selectedService = s; 82 | if (!this.version) { 83 | this.version = s.versions ? s.versions[0] : ''; 84 | } 85 | } 86 | }); 87 | this.loadEndpoints(); 88 | }); 89 | } 90 | 91 | publish() { 92 | this.loading = true; 93 | this.response = undefined; 94 | var input = new PublishRequest({ 95 | topic: this.topic, 96 | metadata: JSON.stringify(this.metadata), 97 | message: JSON.stringify(this.request) 98 | }); 99 | this.clientService 100 | .publish(input) 101 | .pipe( 102 | finalize(() => { 103 | this.loading = false; 104 | }) 105 | ) 106 | .subscribe(resp => { 107 | this.response = resp; 108 | }); 109 | } 110 | 111 | serviceChanged(service: RegistryServiceSummary) { 112 | this.service = service.name; 113 | if (service.versions && service.versions.length) { 114 | this.version = service.versions[0]; 115 | } 116 | this.topic = ''; 117 | this.selectedEndpoint = undefined; 118 | this.loadEndpoints(); 119 | } 120 | 121 | versionChanged(version: string) { 122 | this.version = version; 123 | this.loadEndpoints(); 124 | } 125 | 126 | endpointChanged(endpoint: RegistryEndpoint) { 127 | this.topic = endpoint.name; 128 | this.loadEndpointReuqest(endpoint); 129 | } 130 | 131 | loadEndpointReuqest(endpoint: RegistryEndpoint) { 132 | var previousRequest = localStorage.getItem(`${endpoint.name}.publish`); 133 | if (previousRequest) { 134 | try { 135 | this.request = eval(`(${previousRequest})`); 136 | } catch (e) { 137 | // SyntaxError 138 | } 139 | } else { 140 | this.updateRequestPayload(endpoint.request); 141 | } 142 | } 143 | 144 | metadataChanged(metadata: string) { 145 | try { 146 | this.metadata = eval(`(${metadata})`); 147 | } catch (e) { 148 | // SyntaxError 149 | } 150 | } 151 | 152 | requestChanged(request: string) { 153 | try { 154 | this.request = eval(`(${request})`); 155 | localStorage.setItem(`${this.topic}.publish`, JSON.stringify(this.request)); 156 | } catch (e) { 157 | // SyntaxError 158 | } 159 | } 160 | 161 | copyToClipboard(text: string) { 162 | this.clipboardService.copy(JSON.stringify(text)); 163 | this.messageService.create('success', `Copied to Clipboard`); 164 | } 165 | 166 | private loadEndpoints() { 167 | if (!this.service) { 168 | return; 169 | } 170 | this.endpoints = []; 171 | this.request = undefined; 172 | this.registryService.getServiceSubscribers(this.service, this.version).subscribe(resp => { 173 | this.endpoints = resp.subscribers ? resp.subscribers : []; 174 | if (resp.subscribers && resp.subscribers.length) { 175 | if (this.topic) { 176 | resp.subscribers?.forEach(e => { 177 | if (e.name == this.topic) { 178 | this.selectedEndpoint = e; 179 | this.loadEndpointReuqest(this.selectedEndpoint); 180 | } 181 | }); 182 | } else { 183 | this.selectedEndpoint = this.endpoints[0]; 184 | this.topic = this.endpoints[0].name; 185 | this.loadEndpointReuqest(this.selectedEndpoint); 186 | } 187 | } 188 | }); 189 | } 190 | 191 | private updateRequestPayload(request: RegistryValue) { 192 | let payload: RequestPayload = {}; 193 | if (request && request.values) { 194 | request.values.forEach(v => { 195 | if (!v.name || v.name === 'MessageState' || v.name === 'int32' || v.name === 'unknownFields') { 196 | return; 197 | } 198 | let value: any; 199 | switch (v.type) { 200 | case 'string': 201 | value = ''; 202 | break; 203 | case 'int': 204 | case 'int32': 205 | case 'int64': 206 | case 'uint': 207 | case 'uint32': 208 | case 'uint64': 209 | value = 0; 210 | break; 211 | case 'float64': 212 | case 'float32': 213 | value = 0.0; 214 | break; 215 | case 'bool': 216 | value = false; 217 | break; 218 | default: 219 | if (v.type.startsWith('[]')) { 220 | value = []; 221 | } else { 222 | console.log(v.type); 223 | value = v.type; 224 | } 225 | } 226 | payload[v.name] = value; 227 | }); 228 | } 229 | this.request = payload; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /frontend/src/app/routes/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dashboard 5 | 6 | Services 7 | 8 | 9 | 10 | 11 | Refresh 12 | 13 | 14 | 15 | 16 | 17 | {{ registryType }} 18 | {{ registryAddrs }} 19 | 20 | 21 | 22 | 23 | {{ servicesCount }} 26 | {{ nodesCount }} 29 | 30 | 31 | -------------------------------------------------------------------------------- /frontend/src/app/routes/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { finalize } from 'rxjs/operators'; 4 | import { StatisticsServiceProxy } from 'src/app/shared/service-proxies/service-proxies'; 5 | 6 | @Component({ 7 | selector: 'app-dashboard', 8 | templateUrl: './dashboard.component.html', 9 | changeDetection: ChangeDetectionStrategy.Default 10 | }) 11 | export class DashboardComponent implements OnInit { 12 | loading: boolean; 13 | registryType: string; 14 | registryAddrs: string[]; 15 | servicesCount: number; 16 | nodesCount: number; 17 | 18 | constructor(private readonly router: Router, private statisticsService: StatisticsServiceProxy) { 19 | this.loading = false; 20 | this.registryType = ''; 21 | this.registryAddrs = []; 22 | this.servicesCount = 0; 23 | this.nodesCount = 0; 24 | } 25 | 26 | ngOnInit() { 27 | this.load(); 28 | } 29 | 30 | load() { 31 | this.loading = true; 32 | this.statisticsService 33 | .getSummary() 34 | .pipe( 35 | finalize(() => { 36 | this.loading = false; 37 | }) 38 | ) 39 | .subscribe(resp => { 40 | if (resp.registry) { 41 | if (resp.registry.type) { 42 | this.registryType = resp.registry.type; 43 | } 44 | if (resp.registry.addrs) { 45 | this.registryAddrs = resp.registry.addrs; 46 | } 47 | } 48 | if (resp.services) { 49 | if (resp.services.count) { 50 | this.servicesCount = resp.services.count; 51 | } 52 | if (resp.services.nodes_count) { 53 | this.nodesCount = resp.services.nodes_count; 54 | } 55 | } 56 | }); 57 | } 58 | 59 | goto(url: string) { 60 | this.router.navigateByUrl(url); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /frontend/src/app/routes/exception/403.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'exception-403', 5 | template: ` ` 6 | }) 7 | export class Exception403Component {} 8 | -------------------------------------------------------------------------------- /frontend/src/app/routes/exception/404.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'exception-404', 5 | template: ` ` 6 | }) 7 | export class Exception404Component {} 8 | -------------------------------------------------------------------------------- /frontend/src/app/routes/exception/500.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'exception-500', 5 | template: ` ` 6 | }) 7 | export class Exception500Component {} 8 | -------------------------------------------------------------------------------- /frontend/src/app/routes/exception/exception-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { Exception403Component } from './403.component'; 5 | import { Exception404Component } from './404.component'; 6 | import { Exception500Component } from './500.component'; 7 | import { ExceptionTriggerComponent } from './trigger.component'; 8 | 9 | const routes: Routes = [ 10 | { path: '403', component: Exception403Component }, 11 | { path: '404', component: Exception404Component }, 12 | { path: '500', component: Exception500Component }, 13 | { path: 'trigger', component: ExceptionTriggerComponent } 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [RouterModule.forChild(routes)], 18 | exports: [RouterModule] 19 | }) 20 | export class ExceptionRoutingModule {} 21 | -------------------------------------------------------------------------------- /frontend/src/app/routes/exception/exception.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { ExceptionModule as DelonExceptionModule } from '@delon/abc/exception'; 4 | import { NzButtonModule } from 'ng-zorro-antd/button'; 5 | import { NzCardModule } from 'ng-zorro-antd/card'; 6 | 7 | import { Exception403Component } from './403.component'; 8 | import { Exception404Component } from './404.component'; 9 | import { Exception500Component } from './500.component'; 10 | import { ExceptionRoutingModule } from './exception-routing.module'; 11 | import { ExceptionTriggerComponent } from './trigger.component'; 12 | 13 | const COMPONENTS = [Exception403Component, Exception404Component, Exception500Component, ExceptionTriggerComponent]; 14 | 15 | @NgModule({ 16 | imports: [CommonModule, DelonExceptionModule, NzButtonModule, NzCardModule, ExceptionRoutingModule], 17 | declarations: [...COMPONENTS] 18 | }) 19 | export class ExceptionModule {} 20 | -------------------------------------------------------------------------------- /frontend/src/app/routes/exception/trigger.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth'; 3 | import { _HttpClient } from '@delon/theme'; 4 | 5 | @Component({ 6 | selector: 'exception-trigger', 7 | template: ` 8 | 9 | 10 | 触发{{ t }} 11 | 触发刷新Token 12 | 13 | 14 | ` 15 | }) 16 | export class ExceptionTriggerComponent { 17 | types = [401, 403, 404, 500]; 18 | 19 | constructor(private http: _HttpClient, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {} 20 | 21 | go(type: number): void { 22 | this.http.get(`/api/${type}`).subscribe(); 23 | } 24 | 25 | refresh(): void { 26 | this.tokenService.set({ token: 'invalid-token' }); 27 | // 必须提供一个后端地址,无法通过 Mock 来模拟 28 | this.http.post(`https://localhost:5001/auth`).subscribe( 29 | res => console.warn('成功', res), 30 | err => { 31 | console.log('最后结果失败', err); 32 | } 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/app/routes/passport/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Remember me 20 | 21 | 22 | 23 | Login 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/app/routes/passport/login/login.component.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/index'; 2 | :host { 3 | display: block; 4 | width: 368px; 5 | margin: 0 auto; 6 | ::ng-deep { 7 | .ant-tabs .ant-tabs-bar { 8 | margin-bottom: 24px; 9 | text-align: center; 10 | border-bottom: 0; 11 | } 12 | .ant-tabs-tab { 13 | font-size: 16px; 14 | line-height: 24px; 15 | } 16 | .ant-tabs-nav-list { 17 | margin: auto; 18 | } 19 | .ant-input-affix-wrapper .ant-input:not(:first-child) { 20 | padding-left: 4px; 21 | } 22 | .icon { 23 | margin-left: 16px; 24 | color: rgba(0, 0, 0, 0.2); 25 | font-size: 24px; 26 | vertical-align: middle; 27 | cursor: pointer; 28 | transition: color 0.3s; 29 | &:hover { 30 | color: @primary-color; 31 | } 32 | } 33 | .other { 34 | margin-top: 24px; 35 | line-height: 22px; 36 | text-align: left; 37 | nz-tooltip { 38 | vertical-align: middle; 39 | } 40 | .register { 41 | float: right; 42 | } 43 | } 44 | } 45 | } 46 | 47 | [data-theme='dark'] { 48 | :host ::ng-deep { 49 | .icon { 50 | color: rgba(255, 255, 255, 0.2); 51 | &:hover { 52 | color: #fff; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/app/routes/passport/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Optional } from '@angular/core'; 2 | import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { StartupService } from '@core'; 5 | import { ReuseTabService } from '@delon/abc/reuse-tab'; 6 | import { DA_SERVICE_TOKEN, ITokenService, SocialService } from '@delon/auth'; 7 | import { NzTabChangeEvent } from 'ng-zorro-antd/tabs'; 8 | import { finalize } from 'rxjs/operators'; 9 | import { AccountServiceProxy, LoginRequest } from 'src/app/shared/service-proxies/service-proxies'; 10 | 11 | @Component({ 12 | selector: 'passport-login', 13 | templateUrl: './login.component.html', 14 | styleUrls: ['./login.component.less'], 15 | providers: [SocialService], 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class UserLoginComponent { 19 | constructor( 20 | fb: FormBuilder, 21 | private router: Router, 22 | @Optional() 23 | @Inject(ReuseTabService) 24 | private reuseTabService: ReuseTabService, 25 | @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService, 26 | private startupSrv: StartupService, 27 | private cdr: ChangeDetectorRef, 28 | private accountService: AccountServiceProxy 29 | ) { 30 | this.form = fb.group({ 31 | username: [null, [Validators.required]], 32 | password: [null, [Validators.required]], 33 | remember: [true] 34 | }); 35 | } 36 | 37 | // #region fields 38 | 39 | get username(): AbstractControl { 40 | return this.form.controls.username; 41 | } 42 | get password(): AbstractControl { 43 | return this.form.controls.password; 44 | } 45 | form: FormGroup; 46 | error = ''; 47 | type = 0; 48 | loading = false; 49 | 50 | // #region get captcha 51 | 52 | count = 0; 53 | 54 | // #endregion 55 | 56 | switch({ index }: NzTabChangeEvent): void { 57 | this.type = index!; 58 | } 59 | 60 | // #endregion 61 | 62 | submit(): void { 63 | this.error = ''; 64 | if (this.type === 0) { 65 | this.username.markAsDirty(); 66 | this.username.updateValueAndValidity(); 67 | this.password.markAsDirty(); 68 | this.password.updateValueAndValidity(); 69 | if (this.username.invalid || this.password.invalid) { 70 | return; 71 | } 72 | } 73 | 74 | this.loading = true; 75 | this.cdr.detectChanges(); 76 | this.accountService 77 | .login(new LoginRequest({ username: this.username.value, password: this.password.value })) 78 | .pipe( 79 | finalize(() => { 80 | this.loading = false; 81 | this.cdr.detectChanges(); 82 | }) 83 | ) 84 | .subscribe(resp => { 85 | if (!resp.token) { 86 | this.error = 'login failed'; 87 | this.cdr.detectChanges(); 88 | return; 89 | } 90 | this.reuseTabService.clear(); 91 | this.tokenService.set({ token: resp.token }); 92 | this.startupSrv.load().subscribe(() => { 93 | let url = this.tokenService.referrer!.url || '/'; 94 | if (url.includes('/passport')) { 95 | url = '/'; 96 | } 97 | this.router.navigateByUrl(url); 98 | }); 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /frontend/src/app/routes/routes-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { SimpleGuard } from '@delon/auth'; 4 | import { environment } from '@env/environment'; 5 | 6 | // layout 7 | import { LayoutBasicComponent } from '../layout/basic/basic.component'; 8 | import { LayoutPassportComponent } from '../layout/passport/passport.component'; 9 | import { ClientCallComponent } from './client/call.component'; 10 | import { ClientPublishComponent } from './client/publish.component'; 11 | // dashboard pages 12 | import { DashboardComponent } from './dashboard/dashboard.component'; 13 | // passport pages 14 | import { UserLoginComponent } from './passport/login/login.component'; 15 | import { ServiceDetailComponent } from './services/detail.component'; 16 | import { ServicesListComponent } from './services/list.component'; 17 | import { ServiceNodesComponent } from './services/nodes.component'; 18 | 19 | const routes: Routes = [ 20 | { 21 | path: '', 22 | component: LayoutBasicComponent, 23 | canActivate: [SimpleGuard], 24 | children: [ 25 | { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, 26 | { path: 'dashboard', component: DashboardComponent, data: { title: 'Dashboard', titleI18n: 'dashboard' } }, 27 | { path: 'services', component: ServicesListComponent, data: { title: 'Services', titleI18n: 'services' } }, 28 | { path: 'service/detail', component: ServiceDetailComponent }, 29 | { path: 'service/nodes', component: ServiceNodesComponent }, 30 | { path: 'client/call', component: ClientCallComponent, data: { title: 'Call', titleI18n: 'call' } }, 31 | { path: 'client/publish', component: ClientPublishComponent, data: { title: 'Call', titleI18n: 'call' } }, 32 | { path: 'exception', loadChildren: () => import('./exception/exception.module').then(m => m.ExceptionModule) } 33 | ] 34 | }, 35 | { 36 | path: 'passport', 37 | component: LayoutPassportComponent, 38 | children: [{ path: 'login', component: UserLoginComponent, data: { title: 'Login', titleI18n: 'login' } }] 39 | }, 40 | { path: '**', redirectTo: 'exception/404' } 41 | ]; 42 | 43 | @NgModule({ 44 | imports: [ 45 | RouterModule.forRoot(routes, { 46 | useHash: environment.useHash, 47 | // NOTICE: If you use `reuse-tab` component and turn on keepingScroll you can set to `disabled` 48 | // Pls refer to https://ng-alain.com/components/reuse-tab 49 | scrollPositionRestoration: 'top' 50 | }) 51 | ], 52 | exports: [RouterModule] 53 | }) 54 | export class RouteRoutingModule {} 55 | -------------------------------------------------------------------------------- /frontend/src/app/routes/routes.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Type } from '@angular/core'; 2 | import { SharedModule } from '@shared'; 3 | 4 | import { ClientCallComponent } from './client/call.component'; 5 | import { ClientPublishComponent } from './client/publish.component'; 6 | import { DashboardComponent } from './dashboard/dashboard.component'; 7 | import { UserLoginComponent } from './passport/login/login.component'; 8 | import { RouteRoutingModule } from './routes-routing.module'; 9 | import { ServiceDetailComponent } from './services/detail.component'; 10 | import { ServicesListComponent } from './services/list.component'; 11 | import { ServiceNodesComponent } from './services/nodes.component'; 12 | 13 | const COMPONENTS: Array> = [ 14 | DashboardComponent, 15 | UserLoginComponent, 16 | ServicesListComponent, 17 | ServiceDetailComponent, 18 | ServiceNodesComponent, 19 | ClientCallComponent, 20 | ClientPublishComponent 21 | ]; 22 | 23 | @NgModule({ 24 | imports: [SharedModule, RouteRoutingModule], 25 | declarations: COMPONENTS 26 | }) 27 | export class RoutesModule {} 28 | -------------------------------------------------------------------------------- /frontend/src/app/routes/services/detail.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard 6 | 7 | 8 | Services 9 | 10 | Service Detail 11 | 12 | 13 | 14 | Refresh 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Id 24 | Address 25 | Metadata 26 | 27 | 28 | 29 | 30 | {{ data.id }} 31 | {{ data.address }} 32 | {{ data.metadata | json }} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Name 42 | Request 43 | Response 44 | Metadata 45 | 46 | 47 | 48 | 49 | 50 | {{ data.name }} 51 | Call 52 | 53 | 54 | {{ data.request | endpoint }} 55 | 56 | 57 | {{ data.response | endpoint }} 58 | 59 | {{ data.metadata | json }} 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Topic 71 | Payload 72 | Metadata 73 | 74 | 75 | 76 | 77 | 78 | {{ data.name }} 79 | Publish 80 | 81 | 82 | {{ data.request | endpoint }} 83 | 84 | {{ data.metadata | json }} 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /frontend/src/app/routes/services/detail.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; 3 | import { finalize } from 'rxjs/operators'; 4 | import { RegistryService, RegistryServiceProxy } from 'src/app/shared/service-proxies/service-proxies'; 5 | 6 | @Component({ 7 | selector: 'micro-service', 8 | templateUrl: './detail.component.html', 9 | changeDetection: ChangeDetectionStrategy.Default 10 | }) 11 | export class ServiceDetailComponent implements OnInit { 12 | loading = false; 13 | name = ''; 14 | version: string | null = null; 15 | services: RegistryService[] = []; 16 | 17 | constructor( 18 | private readonly route: ActivatedRoute, 19 | private readonly router: Router, 20 | private readonly registryService: RegistryServiceProxy 21 | ) {} 22 | 23 | ngOnInit(): void { 24 | var name = this.route.snapshot.queryParams['name']; 25 | if (name) { 26 | this.name = name; 27 | } 28 | var version = this.route.snapshot.queryParams['version']; 29 | if (version) { 30 | this.version = version; 31 | } 32 | this.load(); 33 | } 34 | 35 | load() { 36 | this.loading = true; 37 | this.registryService 38 | .getServiceDetail(this.name, this.version) 39 | .pipe( 40 | finalize(() => { 41 | this.loading = false; 42 | }) 43 | ) 44 | .subscribe(resp => { 45 | if (resp.services) { 46 | this.services = resp.services; 47 | } 48 | }); 49 | } 50 | 51 | gotoCall(service: string, version: string, endpoint: string) { 52 | let navigationExtras: NavigationExtras = { 53 | queryParams: { service: service, version: version, endpoint: endpoint } 54 | }; 55 | this.router.navigate(['/client/call'], navigationExtras); 56 | } 57 | 58 | gotoPublish(service: string, version: string, topic: string) { 59 | let navigationExtras: NavigationExtras = { 60 | queryParams: { service: service, version: version, topic: topic } 61 | }; 62 | this.router.navigate(['/client/publish'], navigationExtras); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/app/routes/services/list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard 6 | 7 | Services 8 | 9 | 10 | 11 | Refresh 12 | 13 | 14 | 15 | 16 | 17 | {{ service.name }} 18 | 19 | {{ version }} 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/src/app/routes/services/list.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { NavigationExtras, Router } from '@angular/router'; 3 | import { finalize } from 'rxjs/operators'; 4 | import { RegistryServiceSummary, RegistryServiceProxy } from 'src/app/shared/service-proxies/service-proxies'; 5 | 6 | @Component({ 7 | selector: 'micro-services', 8 | templateUrl: './list.component.html', 9 | changeDetection: ChangeDetectionStrategy.Default 10 | }) 11 | export class ServicesListComponent implements OnInit { 12 | loading: boolean = false; 13 | services: RegistryServiceSummary[] = []; 14 | 15 | constructor(private readonly router: Router, private registryService: RegistryServiceProxy) {} 16 | 17 | ngOnInit() { 18 | this.load(); 19 | } 20 | 21 | load() { 22 | this.loading = true; 23 | this.registryService 24 | .getServices() 25 | .pipe( 26 | finalize(() => { 27 | this.loading = false; 28 | }) 29 | ) 30 | .subscribe(resp => { 31 | if (resp.services) { 32 | this.services = resp.services; 33 | } 34 | }); 35 | } 36 | 37 | gotoServiceDetail(name: any, version: any) { 38 | let navigationExtras: NavigationExtras = { 39 | queryParams: { name: name, version: version } 40 | }; 41 | this.router.navigate(['/service/detail'], navigationExtras); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/app/routes/services/nodes.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard 6 | 7 | 8 | Services 9 | 10 | Nodes 11 | 12 | 13 | 14 | Refresh 15 | 16 | 17 | 18 | 19 | 20 | 28 | 29 | 30 | Id 31 | Version 32 | Address 33 | Metadata 34 | Actions 35 | 36 | 37 | 38 | 39 | 40 | 47 | 54 | {{ data.id }} 55 | 56 | {{ data.version }} 57 | {{ data.address }} 58 | {{ data.metadata | json }} 59 | 60 | Health 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /frontend/src/app/routes/services/nodes.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { finalize } from 'rxjs/operators'; 4 | import { ClientServiceProxy, HealthCheckRequest, RegistryServiceProxy } from 'src/app/shared/service-proxies/service-proxies'; 5 | 6 | @Component({ 7 | selector: 'micro-nodes', 8 | templateUrl: './nodes.component.html', 9 | changeDetection: ChangeDetectionStrategy.Default 10 | }) 11 | export class ServiceNodesComponent implements OnInit { 12 | loading = false; 13 | name = ''; 14 | version: string | null = null; 15 | services: any; 16 | 17 | constructor(private readonly registryService: RegistryServiceProxy, private readonly clientService: ClientServiceProxy) {} 18 | 19 | ngOnInit(): void { 20 | this.load(); 21 | } 22 | 23 | load() { 24 | this.loading = true; 25 | this.registryService 26 | .getNodes() 27 | .pipe( 28 | finalize(() => { 29 | this.loading = false; 30 | }) 31 | ) 32 | .subscribe(resp => { 33 | if (resp.services) { 34 | this.services = resp.services; 35 | } 36 | }); 37 | } 38 | 39 | healthCheck(service: any, data: any) { 40 | data.loading = true; 41 | var input = new HealthCheckRequest({ 42 | service: service, 43 | address: data.address, 44 | version: data.version 45 | }); 46 | this.clientService 47 | .healthCheck(input) 48 | .pipe( 49 | finalize(() => { 50 | data.loading = false; 51 | }) 52 | ) 53 | .subscribe(resp => { 54 | if (resp && resp.success) { 55 | data.valid = true; 56 | data.tip = `Status: ${resp.status}`; 57 | } else { 58 | console.log(resp); 59 | data.valid = false; 60 | if (resp.error) { 61 | data.tip = resp.error.detail ? resp.error.detail : JSON.stringify(resp.error); 62 | } 63 | } 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /frontend/src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | // Components 2 | 3 | // Module 4 | export * from './shared.module'; 5 | export * from './json-schema/json-schema.module'; 6 | -------------------------------------------------------------------------------- /frontend/src/app/shared/json-schema/README.md: -------------------------------------------------------------------------------- 1 | # 建议统一在 `widgets` 目录下自定义小部件 2 | 3 | > 注:@delon/form 本身提供 nz-zorro-antd 数据录入组件的全部实现,以及若干第三方组件的代码,可从[widgets-third](https://github.com/ng-alain/delon/tree/master/packages/form/widgets-third)中获取并放置 `widgets` 目录下注册即可。 4 | -------------------------------------------------------------------------------- /frontend/src/app/shared/json-schema/json-schema.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { DelonFormModule, WidgetRegistry } from '@delon/form'; 3 | 4 | import { SharedModule } from '../shared.module'; 5 | import { TestWidget } from './test/test.widget'; 6 | 7 | export const SCHEMA_THIRDS_COMPONENTS = [TestWidget]; 8 | 9 | @NgModule({ 10 | declarations: SCHEMA_THIRDS_COMPONENTS, 11 | imports: [SharedModule, DelonFormModule.forRoot()], 12 | exports: SCHEMA_THIRDS_COMPONENTS 13 | }) 14 | export class JsonSchemaModule { 15 | constructor(widgetRegistry: WidgetRegistry) { 16 | widgetRegistry.register(TestWidget.KEY, TestWidget); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/app/shared/json-schema/test/test.widget.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { ControlWidget } from '@delon/form'; 3 | 4 | @Component({ 5 | selector: 'test', 6 | template: ` 7 | 8 | test widget 9 | 10 | `, 11 | preserveWhitespaces: false, 12 | changeDetection: ChangeDetectionStrategy.OnPush 13 | }) 14 | export class TestWidget extends ControlWidget implements OnInit { 15 | static readonly KEY = 'test'; 16 | 17 | ngOnInit(): void { 18 | console.warn('init test widget'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/app/shared/pipes/endpoint.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'endpoint', 5 | pure: false 6 | }) 7 | export class EndpointPipe implements PipeTransform { 8 | transform(d: any, args?: any[]): string { 9 | return this.format(d); 10 | } 11 | 12 | format(v: any): string { 13 | if (!v || !v.values || !v.values.length) { 14 | return '{}'; 15 | } 16 | 17 | const result = new Array(); 18 | v.values.forEach((value: any) => { 19 | result.push(this.formatEndpoint(value, 0)); 20 | }); 21 | return `{\n${result.join('')}}`; 22 | } 23 | 24 | formatEndpoint(v: any, r: number): string { 25 | const fparts = new Array(); 26 | for (let i = 0; i < r + 1; i++) { 27 | fparts.push(' '); 28 | } 29 | if (!v.values || !v.values.length) { 30 | return `${fparts.join('')}${v.name} ${v.type}\n`; 31 | } 32 | fparts.push(`${v.name} ${v.type} {\n`); 33 | 34 | v.values.forEach((value: any) => { 35 | fparts.push(this.formatEndpoint(value, r + 1)); 36 | }); 37 | for (let i = 0; i < r + 1; i++) { 38 | fparts.push(' '); 39 | } 40 | fparts.push('}\n'); 41 | return fparts.join(''); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/app/shared/pipes/pipes.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { EndpointPipe } from './endpoint.pipe'; 5 | 6 | @NgModule({ 7 | declarations: [EndpointPipe], 8 | imports: [CommonModule], 9 | exports: [EndpointPipe] 10 | }) 11 | export class PipesModule {} 12 | -------------------------------------------------------------------------------- /frontend/src/app/shared/service-proxies/service-proxy.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import * as ApiServiceProxies from './service-proxies'; 4 | 5 | @NgModule({ 6 | providers: [ 7 | ApiServiceProxies.AccountServiceProxy, 8 | ApiServiceProxies.ClientServiceProxy, 9 | ApiServiceProxies.RegistryServiceProxy, 10 | ApiServiceProxies.StatisticsServiceProxy, 11 | ApiServiceProxies.ServiceProxy 12 | ] 13 | }) 14 | export class ServiceProxyModule {} 15 | -------------------------------------------------------------------------------- /frontend/src/app/shared/shared-delon.module.ts: -------------------------------------------------------------------------------- 1 | import { PageHeaderModule } from '@delon/abc/page-header'; 2 | import { ResultModule } from '@delon/abc/result'; 3 | import { SEModule } from '@delon/abc/se'; 4 | import { STModule } from '@delon/abc/st'; 5 | import { SVModule } from '@delon/abc/sv'; 6 | 7 | export const SHARED_DELON_MODULES = [PageHeaderModule, STModule, SEModule, SVModule, ResultModule]; 8 | -------------------------------------------------------------------------------- /frontend/src/app/shared/shared-zorro.module.ts: -------------------------------------------------------------------------------- 1 | import { NzAlertModule } from 'ng-zorro-antd/alert'; 2 | import { NzAvatarModule } from 'ng-zorro-antd/avatar'; 3 | import { NzBackTopModule } from 'ng-zorro-antd/back-top'; 4 | import { NzBreadCrumbModule } from 'ng-zorro-antd/breadcrumb'; 5 | import { NzButtonModule } from 'ng-zorro-antd/button'; 6 | import { NzCardModule } from 'ng-zorro-antd/card'; 7 | import { NzCheckboxModule } from 'ng-zorro-antd/checkbox'; 8 | import { NzCollapseModule } from 'ng-zorro-antd/collapse'; 9 | import { NzDrawerModule } from 'ng-zorro-antd/drawer'; 10 | import { NzDropDownModule } from 'ng-zorro-antd/dropdown'; 11 | import { NzFormModule } from 'ng-zorro-antd/form'; 12 | import { NzGridModule } from 'ng-zorro-antd/grid'; 13 | import { NzIconModule } from 'ng-zorro-antd/icon'; 14 | import { NzInputModule } from 'ng-zorro-antd/input'; 15 | import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; 16 | import { NzListModule } from 'ng-zorro-antd/list'; 17 | import { NzModalModule } from 'ng-zorro-antd/modal'; 18 | import { NzPageHeaderModule } from 'ng-zorro-antd/page-header'; 19 | import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm'; 20 | import { NzPopoverModule } from 'ng-zorro-antd/popover'; 21 | import { NzProgressModule } from 'ng-zorro-antd/progress'; 22 | import { NzSelectModule } from 'ng-zorro-antd/select'; 23 | import { NzSpaceModule } from 'ng-zorro-antd/space'; 24 | import { NzSpinModule } from 'ng-zorro-antd/spin'; 25 | import { NzTableModule } from 'ng-zorro-antd/table'; 26 | import { NzTabsModule } from 'ng-zorro-antd/tabs'; 27 | import { NzTagModule } from 'ng-zorro-antd/tag'; 28 | import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; 29 | 30 | export const SHARED_ZORRO_MODULES = [ 31 | NzFormModule, 32 | NzGridModule, 33 | NzButtonModule, 34 | NzInputModule, 35 | NzInputNumberModule, 36 | NzAlertModule, 37 | NzProgressModule, 38 | NzSelectModule, 39 | NzAvatarModule, 40 | NzCardModule, 41 | NzDropDownModule, 42 | NzPopconfirmModule, 43 | NzTableModule, 44 | NzPopoverModule, 45 | NzDrawerModule, 46 | NzModalModule, 47 | NzTabsModule, 48 | NzToolTipModule, 49 | NzIconModule, 50 | NzCheckboxModule, 51 | NzSpinModule, 52 | NzListModule, 53 | NzTagModule, 54 | NzBreadCrumbModule, 55 | NzBackTopModule, 56 | NzPageHeaderModule, 57 | NzCollapseModule, 58 | NzSpaceModule 59 | ]; 60 | -------------------------------------------------------------------------------- /frontend/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule, Type } from '@angular/core'; 3 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | import { DelonACLModule } from '@delon/acl'; 6 | import { DelonFormModule } from '@delon/form'; 7 | import { AlainThemeModule } from '@delon/theme'; 8 | import { ClipboardModule } from 'ngx-clipboard'; 9 | 10 | import { EndpointPipe } from './pipes/endpoint.pipe'; 11 | import { ServiceProxyModule } from './service-proxies/service-proxy.module'; 12 | import { SHARED_DELON_MODULES } from './shared-delon.module'; 13 | import { SHARED_ZORRO_MODULES } from './shared-zorro.module'; 14 | 15 | // #region third libs 16 | 17 | const THIRDMODULES: Array> = [ClipboardModule]; 18 | 19 | // #endregion 20 | 21 | // #region your componets & directives 22 | 23 | const COMPONENTS: Array> = [EndpointPipe]; 24 | const DIRECTIVES: Array> = []; 25 | 26 | // #endregion 27 | 28 | @NgModule({ 29 | imports: [ 30 | CommonModule, 31 | FormsModule, 32 | RouterModule, 33 | ReactiveFormsModule, 34 | AlainThemeModule.forChild(), 35 | DelonACLModule, 36 | DelonFormModule, 37 | ServiceProxyModule, 38 | ...SHARED_DELON_MODULES, 39 | ...SHARED_ZORRO_MODULES, 40 | // third libs 41 | ...THIRDMODULES 42 | ], 43 | declarations: [ 44 | // your components 45 | ...COMPONENTS, 46 | ...DIRECTIVES 47 | ], 48 | exports: [ 49 | CommonModule, 50 | FormsModule, 51 | ReactiveFormsModule, 52 | RouterModule, 53 | AlainThemeModule, 54 | DelonACLModule, 55 | DelonFormModule, 56 | ServiceProxyModule, 57 | ...SHARED_DELON_MODULES, 58 | ...SHARED_ZORRO_MODULES, 59 | // third libs 60 | ...THIRDMODULES, 61 | // your components 62 | ...COMPONENTS, 63 | ...DIRECTIVES 64 | ] 65 | }) 66 | export class SharedModule {} 67 | -------------------------------------------------------------------------------- /frontend/src/app/shared/st-widget/st-widget.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | // import { STWidgetRegistry } from '@delon/abc/st'; 4 | import { SharedModule } from '../shared.module'; 5 | 6 | export const STWIDGET_COMPONENTS = []; 7 | 8 | @NgModule({ 9 | declarations: STWIDGET_COMPONENTS, 10 | imports: [SharedModule], 11 | exports: [...STWIDGET_COMPONENTS] 12 | }) 13 | export class STWidgetModule { 14 | // constructor(widgetRegistry: STWidgetRegistry) { 15 | // widgetRegistry.register(STImgWidget.KEY, STImgWidget); 16 | // } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-micro/dashboard/041f2a7c1811ec554dafef211506f2668471bba5/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend/src/assets/logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-micro/dashboard/041f2a7c1811ec554dafef211506f2668471bba5/frontend/src/assets/logo-color.png -------------------------------------------------------------------------------- /frontend/src/assets/logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-micro/dashboard/041f2a7c1811ec554dafef211506f2668471bba5/frontend/src/assets/logo-full.png -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-micro/dashboard/041f2a7c1811ec554dafef211506f2668471bba5/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import { Environment } from '@delon/theme'; 2 | 3 | export const environment = { 4 | production: true, 5 | useHash: true, 6 | api: { 7 | baseUrl: '' 8 | } 9 | } as Environment; 10 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | import { Environment } from '@delon/theme'; 6 | 7 | export const environment = { 8 | production: false, 9 | useHash: true, 10 | api: { 11 | baseUrl: 'http://localhost:8082' 12 | } 13 | } as Environment; 14 | 15 | /* 16 | * In development mode, to ignore zone related error stack frames such as 17 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 18 | * import the following file, but please comment it out in production mode 19 | * because it will have performance impact when throw error 20 | */ 21 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 22 | -------------------------------------------------------------------------------- /frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-micro/dashboard/041f2a7c1811ec554dafef211506f2668471bba5/frontend/src/favicon.ico -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Go Micro Dashboard 7 | 8 | 9 | 10 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | ● ● ● ● 112 | ● ● 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode, ViewEncapsulation } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { preloaderFinished } from '@delon/theme'; 4 | import { NzSafeAny } from 'ng-zorro-antd/core/types'; 5 | 6 | import { AppModule } from './app/app.module'; 7 | import { environment } from './environments/environment'; 8 | 9 | preloaderFinished(); 10 | 11 | if (environment.production) { 12 | enableProdMode(); 13 | } 14 | 15 | platformBrowserDynamic() 16 | .bootstrapModule(AppModule, { 17 | defaultEncapsulation: ViewEncapsulation.Emulated, 18 | preserveWhitespaces: false 19 | }) 20 | .then(res => { 21 | const win = window as NzSafeAny; 22 | if (win && win.appBootstrap) { 23 | win.appBootstrap(); 24 | } 25 | return res; 26 | }) 27 | .catch(err => console.error(err)); 28 | -------------------------------------------------------------------------------- /frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unassigned-import */ 2 | /** 3 | * This file includes polyfills needed by Angular and is loaded before the app. 4 | * You can add your own extra polyfills to this file. 5 | * 6 | * This file is divided into 2 sections: 7 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 8 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 9 | * file. 10 | * 11 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 12 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 13 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 14 | * 15 | * Learn more in https://angular.io/guide/browser-support 16 | */ 17 | 18 | /*************************************************************************************************** 19 | * BROWSER POLYFILLS 20 | */ 21 | 22 | /** 23 | * IE11 requires the following for NgClass support on SVG elements 24 | */ 25 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 26 | 27 | /** 28 | * Web Animations `@angular/platform-browser/animations` 29 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 30 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 31 | */ 32 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 33 | 34 | /** 35 | * By default, zone.js will patch all possible macroTask and DomEvents 36 | * user can disable parts of macroTask/DomEvents patch by setting following flags 37 | * because those flags need to be set before `zone.js` being loaded, and webpack 38 | * will put import in the top of bundle, so user need to create a separate file 39 | * in this directory (for example: zone-flags.ts), and put the following flags 40 | * into that file, and then add the following code before importing zone.js. 41 | * import './zone-flags'; 42 | * 43 | * The flags allowed in zone-flags.ts are listed here. 44 | * 45 | * The following flags will work for all browsers. 46 | * 47 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 48 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 49 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 50 | * 51 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 52 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 53 | * 54 | * (window as any).__Zone_enable_cross_context_check = true; 55 | * 56 | */ 57 | 58 | /*************************************************************************************************** 59 | * Zone JS is required by default for Angular itself. 60 | */ 61 | import 'zone.js'; // Included with Angular CLI. 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /frontend/src/style-icons-auto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Automatically generated by 'ng g ng-alain:plugin icon' 3 | * @see https://ng-alain.com/cli/plugin#icon 4 | */ 5 | 6 | import { 7 | AlipayCircleOutline, 8 | ApiOutline, 9 | AppstoreOutline, 10 | ArrowDownOutline, 11 | BookOutline, 12 | BorderLeftOutline, 13 | BorderRightOutline, 14 | CloudOutline, 15 | CopyrightOutline, 16 | CustomerServiceOutline, 17 | DashboardOutline, 18 | DatabaseOutline, 19 | DingdingOutline, 20 | DislikeOutline, 21 | DownloadOutline, 22 | ForkOutline, 23 | FrownOutline, 24 | FullscreenExitOutline, 25 | FullscreenOutline, 26 | GithubOutline, 27 | GlobalOutline, 28 | HddOutline, 29 | LaptopOutline, 30 | LikeOutline, 31 | LockOutline, 32 | LogoutOutline, 33 | MailOutline, 34 | MenuFoldOutline, 35 | MenuUnfoldOutline, 36 | MessageOutline, 37 | PayCircleOutline, 38 | PieChartOutline, 39 | PrinterOutline, 40 | RocketOutline, 41 | ScanOutline, 42 | SettingOutline, 43 | ShareAltOutline, 44 | ShoppingCartOutline, 45 | SoundOutline, 46 | StarOutline, 47 | TaobaoCircleOutline, 48 | TaobaoOutline, 49 | TeamOutline, 50 | ToolOutline, 51 | TrophyOutline, 52 | UsbOutline, 53 | UserOutline, 54 | WeiboCircleOutline, 55 | ClearOutline, 56 | InfoCircleOutline, 57 | ApartmentOutline, 58 | CheckCircleTwoTone, 59 | CloseCircleTwoTone 60 | } from '@ant-design/icons-angular/icons'; 61 | 62 | export const ICONS_AUTO = [ 63 | AlipayCircleOutline, 64 | ApiOutline, 65 | AppstoreOutline, 66 | ArrowDownOutline, 67 | BookOutline, 68 | BorderLeftOutline, 69 | BorderRightOutline, 70 | CloudOutline, 71 | CopyrightOutline, 72 | CustomerServiceOutline, 73 | DashboardOutline, 74 | DatabaseOutline, 75 | DingdingOutline, 76 | DislikeOutline, 77 | DownloadOutline, 78 | ForkOutline, 79 | FrownOutline, 80 | FullscreenExitOutline, 81 | FullscreenOutline, 82 | GithubOutline, 83 | GlobalOutline, 84 | HddOutline, 85 | LaptopOutline, 86 | LikeOutline, 87 | LockOutline, 88 | LogoutOutline, 89 | MailOutline, 90 | MenuFoldOutline, 91 | MenuUnfoldOutline, 92 | MessageOutline, 93 | PayCircleOutline, 94 | PieChartOutline, 95 | PrinterOutline, 96 | RocketOutline, 97 | ScanOutline, 98 | SettingOutline, 99 | ShareAltOutline, 100 | ShoppingCartOutline, 101 | SoundOutline, 102 | StarOutline, 103 | TaobaoCircleOutline, 104 | TaobaoOutline, 105 | TeamOutline, 106 | ToolOutline, 107 | TrophyOutline, 108 | UsbOutline, 109 | UserOutline, 110 | WeiboCircleOutline, 111 | ClearOutline, 112 | InfoCircleOutline, 113 | ApartmentOutline, 114 | CheckCircleTwoTone, 115 | CloseCircleTwoTone 116 | ]; 117 | -------------------------------------------------------------------------------- /frontend/src/style-icons.ts: -------------------------------------------------------------------------------- 1 | // Custom icon static resources 2 | 3 | import { BulbOutline, ExceptionOutline, InfoOutline, LinkOutline, ProfileOutline } from '@ant-design/icons-angular/icons'; 4 | 5 | export const ICONS = [InfoOutline, BulbOutline, ProfileOutline, ExceptionOutline, LinkOutline]; 6 | -------------------------------------------------------------------------------- /frontend/src/styles.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/system/index'; 2 | @import '~@delon/abc/index'; 3 | @import '~@delon/chart/index'; 4 | @import '~@delon/theme/layout-default/style/index'; 5 | @import '~@delon/theme/layout-blank/style/index'; 6 | 7 | @import './styles/index'; 8 | @import './styles/theme'; 9 | -------------------------------------------------------------------------------- /frontend/src/styles/index.less: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | .alain-default__header-logo-expanded{ 3 | max-height: 80px; 4 | } 5 | 6 | .alain-default__header-logo-collapsed{ 7 | max-height: 48px; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/styles/theme.less: -------------------------------------------------------------------------------- 1 | // You can directly set the default theme 2 | // - `default` Default theme 3 | // - `dark` Import the official dark less style file 4 | // - `compact` Import the official compact less style file 5 | @import '~@delon/theme/theme-default.less'; 6 | 7 | // ==========The following is the custom theme variable area========== 8 | // The theme paraments can be generated at https://ng-alain.github.io/ng-alain/ 9 | // @primary-color: #f50; 10 | -------------------------------------------------------------------------------- /frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unassigned-import */ 2 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 3 | 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: any; 12 | 13 | // First, initialize the Angular testing environment. 14 | getTestBed().initTestEnvironment( 15 | BrowserDynamicTestingModule, 16 | platformBrowserDynamicTesting() 17 | ); 18 | 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /frontend/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | // # 3rd Party Library 2 | // If the library doesn't have typings available at `@types/`, 3 | // you can still use it by manually adding typings for it 4 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "sourceMap": true, 11 | "declaration": false, 12 | "downlevelIteration": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "node", 15 | "importHelpers": true, 16 | "target": "es2017", 17 | "module": "es2020", 18 | "lib": [ 19 | "es2018", 20 | "dom" 21 | ], 22 | "paths": { 23 | "@shared": [ 24 | "src/app/shared/index" 25 | ], 26 | "@core": [ 27 | "src/app/core/index" 28 | ], 29 | "@env/*": [ 30 | "src/environments/*" 31 | ] 32 | } 33 | }, 34 | "angularCompilerOptions": { 35 | "enableI18nLegacyMessageIdFormat": false, 36 | "strictInjectionParameters": true, 37 | "strictInputAccessModifiers": true, 38 | "strictTemplates": true 39 | } 40 | } -------------------------------------------------------------------------------- /frontend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-micro/dashboard 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gin-gonic/gin v1.8.1 8 | github.com/go-micro/plugins/v4/broker/kafka v1.2.0 9 | github.com/go-micro/plugins/v4/broker/mqtt v1.2.0 10 | github.com/go-micro/plugins/v4/broker/nats v1.2.0 11 | github.com/go-micro/plugins/v4/broker/nsq v1.2.0 12 | github.com/go-micro/plugins/v4/broker/rabbitmq v1.2.0 13 | github.com/go-micro/plugins/v4/broker/redis v1.2.0 14 | github.com/go-micro/plugins/v4/client/grpc v1.2.0 15 | github.com/go-micro/plugins/v4/client/http v1.2.0 16 | github.com/go-micro/plugins/v4/client/mucp v1.2.0 17 | github.com/go-micro/plugins/v4/config/encoder/toml v1.2.0 18 | github.com/go-micro/plugins/v4/config/encoder/yaml v1.2.0 19 | github.com/go-micro/plugins/v4/registry/consul v1.2.0 20 | github.com/go-micro/plugins/v4/registry/etcd v1.2.0 21 | github.com/go-micro/plugins/v4/registry/eureka v1.2.0 22 | github.com/go-micro/plugins/v4/registry/gossip v1.2.0 23 | github.com/go-micro/plugins/v4/registry/kubernetes v1.1.1 24 | github.com/go-micro/plugins/v4/registry/nacos v1.2.0 25 | github.com/go-micro/plugins/v4/registry/nats v1.2.1 26 | github.com/go-micro/plugins/v4/registry/zookeeper v1.2.0 27 | github.com/go-micro/plugins/v4/server/http v1.2.0 28 | github.com/go-micro/plugins/v4/transport/grpc v1.2.0 29 | github.com/pkg/errors v0.9.1 30 | github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a 31 | github.com/swaggo/gin-swagger v1.5.3 32 | github.com/swaggo/swag v1.8.8 33 | go-micro.dev/v4 v4.10.0 34 | golang.org/x/net v0.8.0 35 | ) 36 | 37 | require ( 38 | github.com/BurntSushi/toml v1.2.1 // indirect 39 | github.com/KyleBanks/depth v1.2.1 // indirect 40 | github.com/Microsoft/go-winio v0.6.0 // indirect 41 | github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect 42 | github.com/Shopify/sarama v1.30.1 // indirect 43 | github.com/acomagu/bufpipe v1.0.3 // indirect 44 | github.com/aliyun/alibaba-cloud-sdk-go v1.62.68 // indirect 45 | github.com/armon/go-metrics v0.4.1 // indirect 46 | github.com/beorn7/perks v1.0.1 // indirect 47 | github.com/bitly/go-simplejson v0.5.0 // indirect 48 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect 49 | github.com/buger/jsonparser v1.1.1 // indirect 50 | github.com/cenkalti/backoff/v4 v4.1.3 // indirect 51 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 52 | github.com/clbanning/mxj v1.8.4 // indirect 53 | github.com/cloudflare/circl v1.3.0 // indirect 54 | github.com/coreos/go-semver v0.3.0 // indirect 55 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 56 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 57 | github.com/davecgh/go-spew v1.1.1 // indirect 58 | github.com/eapache/go-resiliency v1.3.0 // indirect 59 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect 60 | github.com/eapache/queue v1.1.0 // indirect 61 | github.com/eclipse/paho.mqtt.golang v1.4.2 // indirect 62 | github.com/emirpasic/gods v1.18.1 // indirect 63 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 64 | github.com/fatih/color v1.13.0 // indirect 65 | github.com/felixge/httpsnoop v1.0.3 // indirect 66 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect 67 | github.com/fsnotify/fsnotify v1.6.0 // indirect 68 | github.com/ghodss/yaml v1.0.0 // indirect 69 | github.com/gin-contrib/sse v0.1.0 // indirect 70 | github.com/go-acme/lego/v4 v4.9.1 // indirect 71 | github.com/go-git/gcfg v1.5.0 // indirect 72 | github.com/go-git/go-billy/v5 v5.3.1 // indirect 73 | github.com/go-git/go-git/v5 v5.5.0 // indirect 74 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 75 | github.com/go-openapi/jsonreference v0.20.0 // indirect 76 | github.com/go-openapi/spec v0.20.7 // indirect 77 | github.com/go-openapi/swag v0.22.3 // indirect 78 | github.com/go-playground/locales v0.14.0 // indirect 79 | github.com/go-playground/universal-translator v0.18.0 // indirect 80 | github.com/go-playground/validator/v10 v10.11.1 // indirect 81 | github.com/go-zookeeper/zk v1.0.3 // indirect 82 | github.com/gobwas/httphead v0.1.0 // indirect 83 | github.com/gobwas/pool v0.2.1 // indirect 84 | github.com/gobwas/ws v1.1.0 // indirect 85 | github.com/goccy/go-json v0.10.0 // indirect 86 | github.com/gogo/protobuf v1.3.2 // indirect 87 | github.com/golang/mock v1.6.0 // indirect 88 | github.com/golang/protobuf v1.5.2 // indirect 89 | github.com/golang/snappy v0.0.4 // indirect 90 | github.com/gomodule/redigo v1.8.9 // indirect 91 | github.com/google/btree v1.1.2 // indirect 92 | github.com/google/uuid v1.3.0 // indirect 93 | github.com/gorilla/handlers v1.5.1 // indirect 94 | github.com/gorilla/websocket v1.5.0 // indirect 95 | github.com/hashicorp/consul/api v1.18.0 // indirect 96 | github.com/hashicorp/errwrap v1.1.0 // indirect 97 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 98 | github.com/hashicorp/go-hclog v1.3.1 // indirect 99 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 100 | github.com/hashicorp/go-msgpack v1.1.5 // indirect 101 | github.com/hashicorp/go-multierror v1.1.1 // indirect 102 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 103 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 104 | github.com/hashicorp/go-uuid v1.0.3 // indirect 105 | github.com/hashicorp/golang-lru v0.5.4 // indirect 106 | github.com/hashicorp/memberlist v0.5.0 // indirect 107 | github.com/hashicorp/serf v0.10.1 // indirect 108 | github.com/hudl/fargo v1.4.0 // indirect 109 | github.com/imdario/mergo v0.3.13 // indirect 110 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 111 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 112 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 113 | github.com/jcmturner/gofork v1.7.6 // indirect 114 | github.com/jcmturner/gokrb5/v8 v8.4.3 // indirect 115 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 116 | github.com/jmespath/go-jmespath v0.4.0 // indirect 117 | github.com/josharian/intern v1.0.0 // indirect 118 | github.com/json-iterator/go v1.1.12 // indirect 119 | github.com/kevinburke/ssh_config v1.2.0 // indirect 120 | github.com/klauspost/compress v1.15.12 // indirect 121 | github.com/leodido/go-urn v1.2.1 // indirect 122 | github.com/mailru/easyjson v0.7.7 // indirect 123 | github.com/mattn/go-colorable v0.1.13 // indirect 124 | github.com/mattn/go-isatty v0.0.16 // indirect 125 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 126 | github.com/miekg/dns v1.1.50 // indirect 127 | github.com/minio/highwayhash v1.0.2 // indirect 128 | github.com/mitchellh/go-homedir v1.1.0 // indirect 129 | github.com/mitchellh/hashstructure v1.1.0 // indirect 130 | github.com/mitchellh/mapstructure v1.5.0 // indirect 131 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 132 | github.com/modern-go/reflect2 v1.0.2 // indirect 133 | github.com/nacos-group/nacos-sdk-go/v2 v2.1.2 // indirect 134 | github.com/nats-io/jwt/v2 v2.2.0 // indirect 135 | github.com/nats-io/nats.go v1.20.0 // indirect 136 | github.com/nats-io/nkeys v0.3.0 // indirect 137 | github.com/nats-io/nuid v1.0.1 // indirect 138 | github.com/nsqio/go-nsq v1.0.8 // indirect 139 | github.com/nxadm/tail v1.4.8 // indirect 140 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect 141 | github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect 142 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect 143 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 144 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 145 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 146 | github.com/pjbgf/sha1cd v0.2.3 // indirect 147 | github.com/prometheus/client_golang v1.14.0 // indirect 148 | github.com/prometheus/client_model v0.3.0 // indirect 149 | github.com/prometheus/common v0.37.0 // indirect 150 | github.com/prometheus/procfs v0.8.0 // indirect 151 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 152 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 153 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect 154 | github.com/sergi/go-diff v1.2.0 // indirect 155 | github.com/skeema/knownhosts v1.1.0 // indirect 156 | github.com/streadway/amqp v1.0.0 // indirect 157 | github.com/ugorji/go/codec v1.2.7 // indirect 158 | github.com/urfave/cli/v2 v2.23.5 // indirect 159 | github.com/xanzy/ssh-agent v0.3.3 // indirect 160 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 161 | go.etcd.io/etcd/api/v3 v3.5.6 // indirect 162 | go.etcd.io/etcd/client/pkg/v3 v3.5.6 // indirect 163 | go.etcd.io/etcd/client/v3 v3.5.6 // indirect 164 | go.uber.org/atomic v1.10.0 // indirect 165 | go.uber.org/multierr v1.6.0 // indirect 166 | go.uber.org/zap v1.21.0 // indirect 167 | golang.org/x/crypto v0.3.0 // indirect 168 | golang.org/x/mod v0.9.0 // indirect 169 | golang.org/x/oauth2 v0.4.0 // indirect 170 | golang.org/x/sync v0.1.0 // indirect 171 | golang.org/x/sys v0.6.0 // indirect 172 | golang.org/x/text v0.8.0 // indirect 173 | golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect 174 | golang.org/x/tools v0.7.0 // indirect 175 | google.golang.org/appengine v1.6.7 // indirect 176 | google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect 177 | google.golang.org/grpc v1.53.0 // indirect 178 | google.golang.org/protobuf v1.28.1 // indirect 179 | gopkg.in/gcfg.v1 v1.2.3 // indirect 180 | gopkg.in/ini.v1 v1.67.0 // indirect 181 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 182 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 183 | gopkg.in/warnings.v0 v0.1.2 // indirect 184 | gopkg.in/yaml.v2 v2.4.0 // indirect 185 | gopkg.in/yaml.v3 v3.0.1 // indirect 186 | ) 187 | -------------------------------------------------------------------------------- /handler/account/service.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dgrijalva/jwt-go" 7 | "github.com/gin-gonic/gin" 8 | "github.com/gin-gonic/gin/render" 9 | "github.com/go-micro/dashboard/config" 10 | "github.com/go-micro/dashboard/handler/route" 11 | ) 12 | 13 | type service struct{} 14 | 15 | func NewRouteRegistrar() route.Registrar { 16 | return service{} 17 | } 18 | 19 | func (s service) RegisterRoute(router gin.IRoutes) { 20 | router.POST("/api/account/login", s.Login) 21 | router.Use(route.AuthRequired()).GET("/api/account/profile", s.Profile) 22 | } 23 | 24 | type loginRequest struct { 25 | Username string `json:"username" binding:"required"` 26 | Password string `json:"password" binding:"required"` 27 | } 28 | 29 | type loginResponse struct { 30 | Token string `json:"token" binding:"required"` 31 | } 32 | 33 | // @Tags Account 34 | // @ID account_login 35 | // @Param input body loginRequest true "request" 36 | // @Success 200 {object} loginResponse "success" 37 | // @Failure 400 {object} string 38 | // @Failure 401 {object} string 39 | // @Failure 500 {object} string 40 | // @Router /api/account/login [post] 41 | func (s *service) Login(ctx *gin.Context) { 42 | var req loginRequest 43 | if err := ctx.ShouldBindJSON(&req); nil != err { 44 | ctx.Render(400, render.String{Format: err.Error()}) 45 | return 46 | } 47 | if req.Username != config.GetServerConfig().Auth.Username || 48 | req.Password != config.GetServerConfig().Auth.Password { 49 | ctx.Render(400, render.String{Format: "incorrect username or password"}) 50 | return 51 | } 52 | claims := jwt.StandardClaims{ 53 | Subject: req.Username, 54 | IssuedAt: time.Now().Unix(), 55 | ExpiresAt: time.Now().Add(config.GetAuthConfig().TokenExpiration).Unix(), 56 | } 57 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 58 | signedToken, err := token.SignedString([]byte(config.GetAuthConfig().TokenSecret)) 59 | if err != nil { 60 | ctx.Render(400, render.String{Format: err.Error()}) 61 | return 62 | } 63 | ctx.JSON(200, loginResponse{Token: signedToken}) 64 | } 65 | 66 | type profileResponse struct { 67 | Name string `json:"name"` 68 | } 69 | 70 | // @Security ApiKeyAuth 71 | // @Tags Account 72 | // @ID account_profile 73 | // @Success 200 {object} profileResponse "success" 74 | // @Failure 400 {object} string 75 | // @Failure 401 {object} string 76 | // @Failure 500 {object} string 77 | // @Router /api/account/profile [get] 78 | func (s *service) Profile(ctx *gin.Context) { 79 | ctx.JSON(200, profileResponse{Name: config.GetAuthConfig().Username}) 80 | } 81 | -------------------------------------------------------------------------------- /handler/client/models.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | type callRequest struct { 4 | Service string `json:"service" binding:"required"` 5 | Version string `json:"version"` 6 | Endpoint string `json:"endpoint" binding:"required"` 7 | Metadata string `json:"metadata"` 8 | Request string `json:"request"` 9 | Timeout int64 `json:"timeout"` 10 | } 11 | 12 | type publishRequest struct { 13 | Topic string `json:"topic" binding:"required"` 14 | Metadata string `json:"metadata"` 15 | Message string `json:"message" binding:"required"` 16 | } 17 | 18 | type healthCheckRequest struct { 19 | Service string `json:"service" binding:"required"` 20 | Version string `json:"version" binding:"required"` 21 | Address string `json:"address" binding:"required"` 22 | Timeout int64 `json:"timeout"` 23 | } 24 | -------------------------------------------------------------------------------- /handler/client/service.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "go-micro.dev/v4/metadata" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/gin-gonic/gin/render" 12 | "github.com/go-micro/dashboard/handler/route" 13 | cgrpc "github.com/go-micro/plugins/v4/client/grpc" 14 | chttp "github.com/go-micro/plugins/v4/client/http" 15 | cmucp "github.com/go-micro/plugins/v4/client/mucp" 16 | "go-micro.dev/v4/client" 17 | debug "go-micro.dev/v4/debug/proto" 18 | "go-micro.dev/v4/errors" 19 | "go-micro.dev/v4/registry" 20 | "go-micro.dev/v4/selector" 21 | ) 22 | 23 | type service struct { 24 | client client.Client 25 | registry registry.Registry 26 | 27 | clients map[string]client.Client 28 | clientsMu sync.Mutex 29 | } 30 | 31 | func NewRouteRegistrar(client client.Client, registry registry.Registry) route.Registrar { 32 | return &service{client: client, registry: registry} 33 | } 34 | 35 | func (s *service) RegisterRoute(router gin.IRoutes) { 36 | router.Use(route.AuthRequired()). 37 | POST("/api/client/call", s.Call). 38 | POST("/api/client/publish", s.Publish). 39 | POST("/api/client/healthcheck", s.HealthCheck) 40 | } 41 | 42 | // @Security ApiKeyAuth 43 | // @Tags Client 44 | // @ID client_call 45 | // @Param input body callRequest true "request" 46 | // @Success 200 {object} object "success" 47 | // @Failure 400 {object} string 48 | // @Failure 401 {object} string 49 | // @Failure 500 {object} string 50 | // @Router /api/client/call [post] 51 | func (s *service) Call(ctx *gin.Context) { 52 | var ( 53 | req callRequest 54 | mCtx = context.Context(ctx) 55 | ) 56 | if err := ctx.ShouldBindJSON(&req); nil != err { 57 | ctx.Render(400, render.String{Format: err.Error()}) 58 | return 59 | } 60 | var callReq json.RawMessage 61 | if len(req.Request) > 0 { 62 | if err := json.Unmarshal([]byte(req.Request), &callReq); err != nil { 63 | ctx.Render(400, render.String{Format: "parse request failed: %s", Data: []interface{}{err.Error()}}) 64 | return 65 | } 66 | } 67 | if len(req.Metadata) > 0 { 68 | md := metadata.Metadata{} 69 | if err := json.Unmarshal([]byte(req.Metadata), &md); err != nil { 70 | ctx.Render(400, render.String{Format: "parse metadata failed: %s", Data: []interface{}{err.Error()}}) 71 | return 72 | } 73 | mCtx = metadata.NewContext(mCtx, md) 74 | } 75 | services, err := s.registry.GetService(req.Service) 76 | if err != nil { 77 | ctx.Render(400, render.String{Format: err.Error()}) 78 | return 79 | } 80 | var c client.Client 81 | for _, srv := range services { 82 | if len(req.Version) > 0 && req.Version != srv.Version { 83 | continue 84 | } 85 | if len(srv.Nodes) == 0 { 86 | ctx.Render(400, render.String{Format: "service node not found"}) 87 | return 88 | } 89 | c = s.getClient(srv.Nodes[0].Metadata["server"]) 90 | break 91 | } 92 | if c == nil { 93 | ctx.Render(400, render.String{Format: "service not found"}) 94 | return 95 | } 96 | var resp json.RawMessage 97 | callOpts := []client.CallOption{} 98 | if len(req.Version) > 0 { 99 | callOpts = append(callOpts, client.WithSelectOption(selector.WithFilter(selector.FilterVersion(req.Version)))) 100 | } 101 | requestOpts := []client.RequestOption{client.WithContentType("application/json")} 102 | if req.Timeout > 0 { 103 | callOpts = append(callOpts, client.WithRequestTimeout(time.Duration(req.Timeout)*time.Second)) 104 | } 105 | if err := c.Call(mCtx, client.NewRequest(req.Service, req.Endpoint, callReq, requestOpts...), &resp, callOpts...); err != nil { 106 | if merr := errors.Parse(err.Error()); merr != nil { 107 | ctx.JSON(200, gin.H{"success": false, "error": merr}) 108 | } else { 109 | ctx.JSON(200, gin.H{"success": false, "error": err.Error}) 110 | } 111 | return 112 | } 113 | ctx.JSON(200, resp) 114 | } 115 | 116 | // @Security ApiKeyAuth 117 | // @Tags Client 118 | // @ID client_healthCheck 119 | // @Param input body healthCheckRequest true "request" 120 | // @Success 200 {object} object "success" 121 | // @Failure 400 {object} string 122 | // @Failure 401 {object} string 123 | // @Failure 500 {object} string 124 | // @Router /api/client/healthcheck [post] 125 | func (s *service) HealthCheck(ctx *gin.Context) { 126 | var req healthCheckRequest 127 | if err := ctx.ShouldBindJSON(&req); nil != err { 128 | ctx.Render(400, render.String{Format: err.Error()}) 129 | return 130 | } 131 | services, err := s.registry.GetService(req.Service) 132 | if err != nil { 133 | ctx.JSON(200, gin.H{"success": false, "error": err.Error()}) 134 | return 135 | } 136 | var c client.Client 137 | for _, srv := range services { 138 | if len(req.Version) > 0 && req.Version != srv.Version { 139 | continue 140 | } 141 | for _, n := range srv.Nodes { 142 | if req.Address == n.Address { 143 | c = s.getClient(n.Metadata["server"]) 144 | break 145 | } 146 | } 147 | } 148 | if c == nil { 149 | ctx.JSON(200, gin.H{"success": false, "error": "service node not found"}) 150 | return 151 | } 152 | callOpts := []client.CallOption{ 153 | client.WithAddress(req.Address), 154 | client.WithSelectOption(selector.WithFilter(selector.FilterVersion(req.Version))), 155 | } 156 | if req.Timeout > 0 { 157 | callOpts = append(callOpts, client.WithRequestTimeout(time.Duration(req.Timeout)*time.Second)) 158 | } 159 | debugService := debug.NewDebugService(req.Service, c) 160 | reply, err := debugService.Health(ctx, &debug.HealthRequest{}, callOpts...) 161 | if err != nil { 162 | if merr := errors.Parse(err.Error()); merr != nil { 163 | ctx.JSON(200, gin.H{"success": false, "error": merr}) 164 | } else { 165 | ctx.JSON(200, gin.H{"success": false, "error": err.Error}) 166 | } 167 | return 168 | } 169 | ctx.JSON(200, gin.H{"success": true, "status": reply.Status}) 170 | } 171 | 172 | // @Security ApiKeyAuth 173 | // @Tags Client 174 | // @ID client_publish 175 | // @Param input body publishRequest true "request" 176 | // @Success 200 {object} object "success" 177 | // @Failure 400 {object} string 178 | // @Failure 401 {object} string 179 | // @Failure 500 {object} string 180 | // @Router /api/client/publish [post] 181 | func (s *service) Publish(ctx *gin.Context) { 182 | var ( 183 | req publishRequest 184 | mCtx = context.Context(ctx) 185 | ) 186 | if err := ctx.ShouldBindJSON(&req); nil != err { 187 | ctx.Render(400, render.String{Format: err.Error()}) 188 | return 189 | } 190 | var msg json.RawMessage 191 | if len(req.Message) > 0 { 192 | if err := json.Unmarshal([]byte(req.Message), &msg); err != nil { 193 | ctx.Render(400, render.String{Format: "parse request failed: %s", Data: []interface{}{err.Error()}}) 194 | return 195 | } 196 | } 197 | if len(req.Metadata) > 0 { 198 | md := metadata.Metadata{} 199 | if err := json.Unmarshal([]byte(req.Metadata), &md); err != nil { 200 | ctx.Render(400, render.String{Format: "parse metadata failed: %s", Data: []interface{}{err.Error()}}) 201 | return 202 | } 203 | mCtx = metadata.NewContext(mCtx, md) 204 | } 205 | err := s.client.Publish(mCtx, client.NewMessage(req.Topic, msg, client.WithMessageContentType("application/json"))) 206 | if err != nil { 207 | if merr := errors.Parse(err.Error()); merr != nil { 208 | ctx.JSON(200, gin.H{"success": false, "error": merr}) 209 | } else { 210 | ctx.JSON(200, gin.H{"success": false, "error": err.Error}) 211 | } 212 | return 213 | } 214 | ctx.JSON(200, gin.H{"success": true}) 215 | } 216 | 217 | func (s *service) getClient(serverType string) client.Client { 218 | if serverType == s.client.String() { 219 | return s.client 220 | } 221 | s.clientsMu.Lock() 222 | defer s.clientsMu.Unlock() 223 | if s.clients == nil { 224 | s.clients = make(map[string]client.Client) 225 | } else { 226 | if c, ok := s.clients[serverType]; ok { 227 | return c 228 | } 229 | } 230 | var c client.Client 231 | switch serverType { 232 | case "grpc": 233 | c = cgrpc.NewClient() 234 | s.clients[serverType] = c 235 | case "http": 236 | c = chttp.NewClient() 237 | s.clients[serverType] = c 238 | case "mucp": 239 | c = cmucp.NewClient() 240 | s.clients[serverType] = c 241 | default: 242 | c = s.client 243 | } 244 | return c 245 | } 246 | -------------------------------------------------------------------------------- /handler/client/service_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "go-micro.dev/v4/client" 7 | ) 8 | 9 | func TestGetClient(t *testing.T) { 10 | s := &service{client: client.DefaultClient} 11 | if s.getClient("grpc").String() != "grpc" { 12 | t.Fail() 13 | } 14 | if s.getClient("http").String() != "http" { 15 | t.Fail() 16 | } 17 | if s.getClient("mucp").String() != "mucp" { 18 | t.Fail() 19 | } 20 | if s.getClient("other").String() != client.DefaultClient.String() { 21 | t.Fail() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-micro/dashboard/config" 6 | "github.com/go-micro/dashboard/docs" 7 | "github.com/go-micro/dashboard/handler/account" 8 | handlerclient "github.com/go-micro/dashboard/handler/client" 9 | "github.com/go-micro/dashboard/handler/registry" 10 | "github.com/go-micro/dashboard/handler/route" 11 | "github.com/go-micro/dashboard/handler/statistics" 12 | "github.com/go-micro/dashboard/web" 13 | swaggerFiles "github.com/swaggo/files" 14 | ginSwagger "github.com/swaggo/gin-swagger" 15 | "go-micro.dev/v4/client" 16 | ) 17 | 18 | type Options struct { 19 | Client client.Client 20 | Router *gin.Engine 21 | } 22 | 23 | func Register(opts Options) error { 24 | router := opts.Router 25 | if cfg := config.GetServerConfig(); cfg.Env == config.EnvDev { 26 | docs.SwaggerInfo.Host = cfg.Swagger.Host 27 | docs.SwaggerInfo.BasePath = cfg.Swagger.Base 28 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 29 | } 30 | if err := web.RegisterRoute(router); err != nil { 31 | return err 32 | } 33 | if cfg := config.GetServerConfig().CORS; cfg.Enable { 34 | router.Use(route.CorsHandler(cfg.Origin)) 35 | } 36 | for _, r := range []route.Registrar{ 37 | account.NewRouteRegistrar(), 38 | handlerclient.NewRouteRegistrar(opts.Client, opts.Client.Options().Registry), 39 | registry.NewRouteRegistrar(opts.Client.Options().Registry), 40 | statistics.NewRouteRegistrar(opts.Client.Options().Registry), 41 | } { 42 | r.RegisterRoute(router.Group("")) 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /handler/registry/models.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import "go-micro.dev/v4/registry" 4 | 5 | type registryServiceSummary struct { 6 | Name string `json:"name" binding:"required"` 7 | Versions []string `json:"versions,omitempty"` 8 | } 9 | 10 | type getServiceListResponse struct { 11 | Services []registryServiceSummary `json:"services" binding:"required"` 12 | } 13 | 14 | type registryService struct { 15 | Name string `json:"name" binding:"required"` 16 | Version string `json:"version" binding:"required"` 17 | Metadata map[string]string `json:"metadata,omitempty"` 18 | Handlers []registryEndpoint `json:"handlers,omitempty"` 19 | Subscribers []registryEndpoint `json:"subscribers,omitempty"` 20 | Nodes []registryNode `json:"nodes,omitempty"` 21 | } 22 | 23 | type registryEndpoint struct { 24 | Name string `json:"name" binding:"required"` 25 | Request registryValue `json:"request" binding:"required"` 26 | Response registryValue `json:"response"` 27 | Stream bool `json:"stream,omitempty"` 28 | Metadata map[string]string `json:"metadata,omitempty"` 29 | } 30 | 31 | type registryNode struct { 32 | Id string `json:"id" binding:"required"` 33 | Address string `json:"address" binding:"required"` 34 | Metadata map[string]string `json:"metadata,omitempty"` 35 | } 36 | 37 | type registryValue struct { 38 | Name string `json:"name" binding:"required"` 39 | Type string `json:"type" binding:"required"` 40 | Values []registryValue `json:"values,omitempty"` 41 | } 42 | 43 | type getServiceDetailResponse struct { 44 | Services []registryService `json:"services"` 45 | } 46 | 47 | type getServiceHandlersResponse struct { 48 | Handlers []registryEndpoint `json:"handlers"` 49 | } 50 | 51 | type getServiceSubscribersResponse struct { 52 | Subscribers []registryEndpoint `json:"subscribers"` 53 | } 54 | 55 | type registryNodeDetail struct { 56 | Id string `json:"id" binding:"required"` 57 | Version string `json:"version" binding:"required"` 58 | Address string `json:"address" binding:"required"` 59 | Metadata map[string]string `json:"metadata,omitempty"` 60 | } 61 | 62 | type registryServiceNodes struct { 63 | Name string `json:"name"` 64 | Nodes []registryNodeDetail `json:"nodes"` 65 | } 66 | 67 | type getNodeListResponse struct { 68 | Services []registryServiceNodes `json:"services"` 69 | } 70 | 71 | func convertRegistryValue(v *registry.Value) registryValue { 72 | if v == nil { 73 | return registryValue{} 74 | } 75 | res := registryValue{ 76 | Name: v.Name, 77 | Type: v.Type, 78 | Values: make([]registryValue, 0, len(v.Values)), 79 | } 80 | for _, vv := range v.Values { 81 | res.Values = append(res.Values, convertRegistryValue(vv)) 82 | } 83 | return res 84 | } 85 | -------------------------------------------------------------------------------- /handler/registry/service.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/gin-gonic/gin/render" 8 | "github.com/go-micro/dashboard/handler/route" 9 | "go-micro.dev/v4/registry" 10 | ) 11 | 12 | type service struct { 13 | registry registry.Registry 14 | } 15 | 16 | func NewRouteRegistrar(registry registry.Registry) route.Registrar { 17 | return service{registry: registry} 18 | } 19 | 20 | func (s service) RegisterRoute(router gin.IRoutes) { 21 | router.Use(route.AuthRequired()). 22 | GET("/api/registry/services", s.GetServices). 23 | GET("/api/registry/service", s.GetServiceDetail). 24 | GET("/api/registry/service/handlers", s.GetServiceHandlers). 25 | GET("/api/registry/service/subscribers", s.GetServiceSubscribers). 26 | GET("/api/registry/service/nodes", s.GetServiceNodes) 27 | } 28 | 29 | // @Security ApiKeyAuth 30 | // @Tags Registry 31 | // @ID registry_getServices 32 | // @Success 200 {object} getServiceListResponse 33 | // @Failure 400 {object} string 34 | // @Failure 401 {object} string 35 | // @Failure 500 {object} string 36 | // @Router /api/registry/services [get] 37 | func (s *service) GetServices(ctx *gin.Context) { 38 | services, err := s.registry.ListServices() 39 | if err != nil { 40 | ctx.Render(500, render.String{Format: err.Error()}) 41 | return 42 | } 43 | tmp := make(map[string][]string) 44 | resp := getServiceListResponse{Services: make([]registryServiceSummary, 0, len(services))} 45 | for _, s := range services { 46 | if sr, ok := tmp[s.Name]; ok { 47 | sr = append(sr, s.Version) 48 | tmp[s.Name] = sr 49 | } else { 50 | tmp[s.Name] = []string{s.Version} 51 | } 52 | } 53 | for k, v := range tmp { 54 | sort.Strings(v) 55 | resp.Services = append(resp.Services, registryServiceSummary{Name: k, Versions: v}) 56 | } 57 | sort.Slice(resp.Services, func(i, j int) bool { 58 | return resp.Services[i].Name < resp.Services[j].Name 59 | }) 60 | ctx.JSON(200, resp) 61 | } 62 | 63 | // @Security ApiKeyAuth 64 | // @Tags Registry 65 | // @ID registry_getServiceDetail 66 | // @Param name query string true "service name" 67 | // @Param version query string false "service version" 68 | // @Success 200 {object} getServiceDetailResponse 69 | // @Failure 400 {object} string 70 | // @Failure 401 {object} string 71 | // @Failure 500 {object} string 72 | // @Router /api/registry/service [get] 73 | func (s *service) GetServiceDetail(ctx *gin.Context) { 74 | name := ctx.Query("name") 75 | if len(name) == 0 { 76 | ctx.Render(400, render.String{Format: "service name required"}) 77 | return 78 | } 79 | services, err := s.registry.GetService(name) 80 | if err != nil { 81 | ctx.Render(500, render.String{Format: err.Error()}) 82 | return 83 | } 84 | version := ctx.Query("version") 85 | resp := getServiceDetailResponse{Services: make([]registryService, 0, len(services))} 86 | for _, s := range services { 87 | if len(version) > 0 && s.Version != version { 88 | continue 89 | } 90 | handlers := make([]registryEndpoint, 0) 91 | subscribers := make([]registryEndpoint, 0) 92 | for _, e := range s.Endpoints { 93 | if isSubscriber(e) { 94 | subscribers = append(subscribers, registryEndpoint{ 95 | Name: e.Metadata["topic"], 96 | Request: convertRegistryValue(e.Request), 97 | Metadata: e.Metadata, 98 | }) 99 | } else { 100 | handlers = append(handlers, registryEndpoint{ 101 | Name: e.Name, 102 | Request: convertRegistryValue(e.Request), 103 | Response: convertRegistryValue(e.Response), 104 | Metadata: e.Metadata, 105 | }) 106 | } 107 | } 108 | nodes := make([]registryNode, 0, len(s.Nodes)) 109 | for _, n := range s.Nodes { 110 | nodes = append(nodes, registryNode{ 111 | Id: n.Id, 112 | Address: n.Address, 113 | Metadata: n.Metadata, 114 | }) 115 | } 116 | resp.Services = append(resp.Services, registryService{ 117 | Name: s.Name, 118 | Version: s.Version, 119 | Metadata: s.Metadata, 120 | Handlers: handlers, 121 | Subscribers: subscribers, 122 | Nodes: nodes, 123 | }) 124 | } 125 | ctx.JSON(200, resp) 126 | } 127 | 128 | // @Security ApiKeyAuth 129 | // @Tags Registry 130 | // @ID registry_getServiceHandlers 131 | // @Param name query string true "service name" 132 | // @Param version query string false "service version" 133 | // @Success 200 {object} getServiceHandlersResponse 134 | // @Failure 400 {object} string 135 | // @Failure 401 {object} string 136 | // @Failure 500 {object} string 137 | // @Router /api/registry/service/handlers [get] 138 | func (s *service) GetServiceHandlers(ctx *gin.Context) { 139 | name := ctx.Query("name") 140 | if len(name) == 0 { 141 | ctx.Render(400, render.String{Format: "service name required"}) 142 | return 143 | } 144 | services, err := s.registry.GetService(name) 145 | if err != nil { 146 | ctx.Render(500, render.String{Format: err.Error()}) 147 | return 148 | } 149 | version := ctx.Query("version") 150 | resp := getServiceHandlersResponse{} 151 | for _, s := range services { 152 | if s.Version != version { 153 | continue 154 | } 155 | handlers := make([]registryEndpoint, 0, len(s.Endpoints)) 156 | for _, e := range s.Endpoints { 157 | if isSubscriber(e) { 158 | continue 159 | } 160 | handlers = append(handlers, registryEndpoint{ 161 | Name: e.Name, 162 | Request: convertRegistryValue(e.Request), 163 | Stream: isStream(e), 164 | }) 165 | } 166 | resp.Handlers = handlers 167 | break 168 | } 169 | ctx.JSON(200, resp) 170 | } 171 | 172 | // @Security ApiKeyAuth 173 | // @Tags Registry 174 | // @ID registry_getServiceSubscribers 175 | // @Param name query string true "service name" 176 | // @Param version query string false "service version" 177 | // @Success 200 {object} getServiceSubscribersResponse 178 | // @Failure 400 {object} string 179 | // @Failure 401 {object} string 180 | // @Failure 500 {object} string 181 | // @Router /api/registry/service/subscribers [get] 182 | func (s *service) GetServiceSubscribers(ctx *gin.Context) { 183 | name := ctx.Query("name") 184 | if len(name) == 0 { 185 | ctx.Render(400, render.String{Format: "service name required"}) 186 | return 187 | } 188 | services, err := s.registry.GetService(name) 189 | if err != nil { 190 | ctx.Render(500, render.String{Format: err.Error()}) 191 | return 192 | } 193 | version := ctx.Query("version") 194 | resp := getServiceSubscribersResponse{} 195 | for _, s := range services { 196 | if s.Version != version { 197 | continue 198 | } 199 | subscribers := make([]registryEndpoint, 0, len(s.Endpoints)) 200 | for _, e := range s.Endpoints { 201 | if !isSubscriber(e) { 202 | continue 203 | } 204 | subscribers = append(subscribers, registryEndpoint{ 205 | Name: e.Metadata["topic"], 206 | Request: convertRegistryValue(e.Request), 207 | }) 208 | } 209 | resp.Subscribers = subscribers 210 | break 211 | } 212 | ctx.JSON(200, resp) 213 | } 214 | 215 | // @Security ApiKeyAuth 216 | // @Tags Registry 217 | // @ID registry_getNodes 218 | // @Success 200 {object} getNodeListResponse 219 | // @Failure 400 {object} string 220 | // @Failure 401 {object} string 221 | // @Failure 500 {object} string 222 | // @Router /api/registry/service/nodes [get] 223 | func (s *service) GetServiceNodes(ctx *gin.Context) { 224 | serviceNames, err := s.registry.ListServices() 225 | if err != nil { 226 | ctx.Render(500, render.String{Format: err.Error()}) 227 | return 228 | } 229 | sCache := make(map[string]map[string]registryNodeDetail) 230 | for _, sn := range serviceNames { 231 | if _, ok := sCache[sn.Name]; ok { 232 | continue 233 | } 234 | sv, err := s.registry.GetService(sn.Name) 235 | if err != nil { 236 | ctx.Render(500, render.String{Format: err.Error()}) 237 | return 238 | } 239 | nCache := make(map[string]registryNodeDetail) 240 | for _, v := range sv { 241 | for _, n := range v.Nodes { 242 | if _, ok := nCache[n.Id]; ok { 243 | continue 244 | } 245 | nCache[n.Id] = registryNodeDetail{ 246 | Id: n.Id, 247 | Version: v.Version, 248 | Address: n.Address, 249 | Metadata: n.Metadata, 250 | } 251 | } 252 | } 253 | sCache[sn.Name] = nCache 254 | } 255 | resp := getNodeListResponse{Services: make([]registryServiceNodes, 0)} 256 | for k, v := range sCache { 257 | nodes := make([]registryNodeDetail, 0, len(v)) 258 | for _, n := range v { 259 | nodes = append(nodes, n) 260 | } 261 | sort.Slice(nodes, func(i, j int) bool { 262 | return nodes[i].Id < nodes[j].Id 263 | }) 264 | resp.Services = append(resp.Services, registryServiceNodes{Name: k, Nodes: nodes}) 265 | } 266 | sort.Slice(resp.Services, func(i, j int) bool { 267 | return resp.Services[i].Name < resp.Services[j].Name 268 | }) 269 | ctx.JSON(200, resp) 270 | } 271 | 272 | func isSubscriber(ep *registry.Endpoint) bool { 273 | if ep == nil || len(ep.Metadata) == 0 { 274 | return false 275 | } 276 | if s, ok := ep.Metadata["subscriber"]; ok && s == "true" { 277 | return true 278 | } 279 | return false 280 | } 281 | 282 | func isStream(ep *registry.Endpoint) bool { 283 | if ep == nil || len(ep.Metadata) == 0 { 284 | return false 285 | } 286 | if s, ok := ep.Metadata["stream"]; ok && s == "true" { 287 | return true 288 | } 289 | return false 290 | } 291 | -------------------------------------------------------------------------------- /handler/route/middleware.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/dgrijalva/jwt-go" 8 | "github.com/gin-gonic/gin" 9 | "github.com/go-micro/dashboard/config" 10 | ) 11 | 12 | func AuthRequired() gin.HandlerFunc { 13 | return func(ctx *gin.Context) { 14 | if ctx.Request.Method == "OPTIONS" { 15 | ctx.Next() 16 | return 17 | } 18 | tokenString := ctx.GetHeader("Authorization") 19 | if len(tokenString) == 0 || !strings.HasPrefix(tokenString, "Bearer ") { 20 | ctx.AbortWithStatusJSON(http.StatusUnauthorized, "") 21 | return 22 | } 23 | tokenString = tokenString[7:] 24 | claims := jwt.StandardClaims{} 25 | token, err := jwt.ParseWithClaims(tokenString, &claims, func(t *jwt.Token) (interface{}, error) { 26 | return []byte(config.GetAuthConfig().TokenSecret), nil 27 | }) 28 | if err != nil { 29 | ctx.AbortWithError(http.StatusUnauthorized, err) 30 | } 31 | if !token.Valid { 32 | ctx.AbortWithStatus(http.StatusUnauthorized) 33 | } 34 | ctx.Set("username", claims.Subject) 35 | ctx.Next() 36 | } 37 | } 38 | 39 | func CorsHandler(allowOrigin string) gin.HandlerFunc { 40 | return func(ctx *gin.Context) { 41 | ctx.Header("Access-Control-Allow-Origin", allowOrigin) 42 | ctx.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, token") 43 | ctx.Header("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT, OPTIONS") 44 | ctx.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type") 45 | ctx.Header("Access-Control-Allow-Credentials", "true") 46 | if ctx.Request.Method == "OPTIONS" { 47 | ctx.AbortWithStatus(http.StatusNoContent) 48 | } 49 | ctx.Next() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /handler/route/route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type Registrar interface { 6 | RegisterRoute(gin.IRoutes) 7 | } 8 | -------------------------------------------------------------------------------- /handler/statistics/models.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | type getSummaryResponse struct { 4 | Registry registrySummary `json:"registry"` 5 | Services servicesSummary `json:"services"` 6 | } 7 | 8 | type registrySummary struct { 9 | Type string `json:"type"` 10 | Addrs []string `json:"addrs"` 11 | } 12 | 13 | type servicesSummary struct { 14 | Count int `json:"count"` 15 | NodesCount int `json:"nodes_count"` 16 | } 17 | -------------------------------------------------------------------------------- /handler/statistics/service.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-micro/dashboard/config" 6 | "github.com/go-micro/dashboard/handler/route" 7 | "go-micro.dev/v4/registry" 8 | ) 9 | 10 | type service struct { 11 | registry registry.Registry 12 | } 13 | 14 | func NewRouteRegistrar(registry registry.Registry) route.Registrar { 15 | return service{registry: registry} 16 | } 17 | 18 | func (s service) RegisterRoute(router gin.IRoutes) { 19 | router.GET("/version", s.GetVersion) 20 | router.Use(route.AuthRequired()).GET("/api/summary", s.GetSummary) 21 | } 22 | 23 | // @Security ApiKeyAuth 24 | // @Tags Statistics 25 | // @ID statistics_getSummary 26 | // @Success 200 {object} getSummaryResponse 27 | // @Failure 400 {object} string 28 | // @Failure 401 {object} string 29 | // @Failure 500 {object} string 30 | // @Router /api/summary [get] 31 | func (s *service) GetSummary(ctx *gin.Context) { 32 | services, err := s.registry.ListServices() 33 | if err != nil { 34 | ctx.AbortWithStatusJSON(500, err) 35 | } 36 | servicesByName := make(map[string]struct{}) 37 | var servicesNodesCount int 38 | for _, s := range services { 39 | if _, ok := servicesByName[s.Name]; !ok { 40 | servicesByName[s.Name] = struct{}{} 41 | } 42 | servicesNodesCount += len(s.Nodes) 43 | } 44 | var resp = getSummaryResponse{ 45 | Registry: registrySummary{ 46 | Type: s.registry.String(), 47 | Addrs: s.registry.Options().Addrs, 48 | }, 49 | Services: servicesSummary{ 50 | Count: len(servicesByName), 51 | NodesCount: servicesNodesCount, 52 | }, 53 | } 54 | ctx.JSON(200, resp) 55 | } 56 | 57 | // @ID getVersion 58 | // @Success 200 {object} object 59 | // @Router /version [get] 60 | func (s *service) GetVersion(ctx *gin.Context) { 61 | ctx.JSON(200, gin.H{"version": config.Version}) 62 | } 63 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-micro/dashboard/config" 6 | "github.com/go-micro/dashboard/handler" 7 | "github.com/go-micro/plugins/v4/server/http" 8 | "go-micro.dev/v4" 9 | "go-micro.dev/v4/logger" 10 | ) 11 | 12 | // @title Go Micro Dashboard 13 | // @version 1.4.0 14 | // @description go micro dashboard restful-api 15 | // @termsOfService http://swagger.io/terms/ 16 | // @BasePath / 17 | // @securityDefinitions.apikey ApiKeyAuth 18 | // @in header 19 | // @name Authorization 20 | 21 | func main() { 22 | if err := config.Load(); err != nil { 23 | logger.Fatal(err) 24 | } 25 | srv := micro.NewService(micro.Server(http.NewServer())) 26 | opts := []micro.Option{ 27 | micro.Name(config.Name), 28 | micro.Address(config.GetServerConfig().Address), 29 | micro.Version(config.Version), 30 | } 31 | srv.Init(opts...) 32 | if config.GetServerConfig().Env == config.EnvProd { 33 | gin.SetMode(gin.ReleaseMode) 34 | } 35 | router := gin.New() 36 | router.Use(gin.Recovery(), gin.Logger()) 37 | if err := handler.Register(handler.Options{Client: srv.Client(), Router: router}); err != nil { 38 | logger.Fatal(err) 39 | } 40 | if err := micro.RegisterHandler(srv.Server(), router); err != nil { 41 | logger.Fatal(err) 42 | } 43 | if err := srv.Run(); err != nil { 44 | logger.Fatal(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /plugins.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/go-micro/plugins/v4/broker/kafka" 5 | _ "github.com/go-micro/plugins/v4/broker/mqtt" 6 | _ "github.com/go-micro/plugins/v4/broker/nats" 7 | _ "github.com/go-micro/plugins/v4/broker/nsq" 8 | _ "github.com/go-micro/plugins/v4/broker/rabbitmq" 9 | _ "github.com/go-micro/plugins/v4/broker/redis" 10 | 11 | _ "github.com/go-micro/plugins/v4/registry/consul" 12 | _ "github.com/go-micro/plugins/v4/registry/etcd" 13 | _ "github.com/go-micro/plugins/v4/registry/eureka" 14 | _ "github.com/go-micro/plugins/v4/registry/gossip" 15 | _ "github.com/go-micro/plugins/v4/registry/kubernetes" 16 | _ "github.com/go-micro/plugins/v4/registry/nacos" 17 | _ "github.com/go-micro/plugins/v4/registry/nats" 18 | _ "github.com/go-micro/plugins/v4/registry/zookeeper" 19 | 20 | _ "github.com/go-micro/plugins/v4/transport/grpc" 21 | ) 22 | -------------------------------------------------------------------------------- /util/goroutine.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "runtime/debug" 5 | 6 | "go-micro.dev/v4/logger" 7 | ) 8 | 9 | // GoSafe will run func in goroutine safely, avoid crash from unexpected panic 10 | func GoSafe(fn func()) { 11 | if fn == nil { 12 | return 13 | } 14 | go func() { 15 | defer func() { 16 | if e := recover(); e != nil { 17 | logger.Errorf("[panic]%v\n%s", e, debug.Stack()) 18 | } 19 | }() 20 | fn() 21 | }() 22 | } 23 | -------------------------------------------------------------------------------- /web/ab0x.go: -------------------------------------------------------------------------------- 1 | // Code generated by fileb0x at "2023-03-26 22:22:38.783613 +0800 CST m=+0.017636709" from config file "b0x.yaml" DO NOT EDIT. 2 | // modification hash(751c52e6460a02036aec388d812ea7ce.f4db6ec17c31519d1f2efc2377822182) 3 | 4 | package web 5 | 6 | import ( 7 | "bytes" 8 | 9 | "context" 10 | "io" 11 | "net/http" 12 | "os" 13 | "path" 14 | 15 | "golang.org/x/net/webdav" 16 | ) 17 | 18 | var ( 19 | // CTX is a context for webdav vfs 20 | CTX = context.Background() 21 | 22 | // FS is a virtual memory file system 23 | FS = webdav.NewMemFS() 24 | 25 | // Handler is used to server files through a http handler 26 | Handler *webdav.Handler 27 | 28 | // HTTP is the http file system 29 | HTTP http.FileSystem = new(HTTPFS) 30 | ) 31 | 32 | // HTTPFS implements http.FileSystem 33 | type HTTPFS struct { 34 | // Prefix allows to limit the path of all requests. F.e. a prefix "css" would allow only calls to /css/* 35 | Prefix string 36 | } 37 | 38 | func init() { 39 | err := CTX.Err() 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | err = FS.Mkdir(CTX, "/assets/", 0777) 45 | if err != nil && err != os.ErrExist { 46 | panic(err) 47 | } 48 | 49 | Handler = &webdav.Handler{ 50 | FileSystem: FS, 51 | LockSystem: webdav.NewMemLS(), 52 | } 53 | 54 | } 55 | 56 | // Open a file 57 | func (hfs *HTTPFS) Open(path string) (http.File, error) { 58 | path = hfs.Prefix + path 59 | 60 | f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return f, nil 66 | } 67 | 68 | // ReadFile is adapTed from ioutil 69 | func ReadFile(path string) ([]byte, error) { 70 | f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | buf := bytes.NewBuffer(make([]byte, 0, bytes.MinRead)) 76 | 77 | // If the buffer overflows, we will get bytes.ErrTooLarge. 78 | // Return that as an error. Any other panic remains. 79 | defer func() { 80 | e := recover() 81 | if e == nil { 82 | return 83 | } 84 | if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { 85 | err = panicErr 86 | } else { 87 | panic(e) 88 | } 89 | }() 90 | _, err = buf.ReadFrom(f) 91 | return buf.Bytes(), err 92 | } 93 | 94 | // WriteFile is adapTed from ioutil 95 | func WriteFile(filename string, data []byte, perm os.FileMode) error { 96 | f, err := FS.OpenFile(CTX, filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) 97 | if err != nil { 98 | return err 99 | } 100 | n, err := f.Write(data) 101 | if err == nil && n < len(data) { 102 | err = io.ErrShortWrite 103 | } 104 | if err1 := f.Close(); err == nil { 105 | err = err1 106 | } 107 | return err 108 | } 109 | 110 | // WalkDirs looks for files in the given dir and returns a list of files in it 111 | // usage for all files in the b0x: WalkDirs("", false) 112 | func WalkDirs(name string, includeDirsInList bool, files ...string) ([]string, error) { 113 | f, err := FS.OpenFile(CTX, name, os.O_RDONLY, 0) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | fileInfos, err := f.Readdir(0) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | err = f.Close() 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | for _, info := range fileInfos { 129 | filename := path.Join(name, info.Name()) 130 | 131 | if includeDirsInList || !info.IsDir() { 132 | files = append(files, filename) 133 | } 134 | 135 | if info.IsDir() { 136 | files, err = WalkDirs(filename, includeDirsInList, files...) 137 | if err != nil { 138 | return nil, err 139 | } 140 | } 141 | } 142 | 143 | return files, nil 144 | } 145 | -------------------------------------------------------------------------------- /web/b0xfile__runtime.337b117ff6241f38ea69.js.go: -------------------------------------------------------------------------------- 1 | // Code generaTed by fileb0x at "2023-03-26 22:22:38.923125 +0800 CST m=+0.157149834" from config file "b0x.yaml" DO NOT EDIT. 2 | // modified(2023-03-26 22:22:37.816710114 +0800 CST) 3 | // original path: frontend/dist/runtime.337b117ff6241f38ea69.js 4 | 5 | package web 6 | 7 | import ( 8 | "os" 9 | ) 10 | 11 | // FileRuntime337b117ff6241f38ea69Js is "/runtime.337b117ff6241f38ea69.js" 12 | var FileRuntime337b117ff6241f38ea69Js = []byte("\x28\x28\x29\x3d\x3e\x7b\x22\x75\x73\x65\x20\x73\x74\x72\x69\x63\x74\x22\x3b\x76\x61\x72\x20\x65\x2c\x76\x3d\x7b\x7d\x2c\x6d\x3d\x7b\x7d\x3b\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x72\x28\x65\x29\x7b\x76\x61\x72\x20\x6e\x3d\x6d\x5b\x65\x5d\x3b\x69\x66\x28\x76\x6f\x69\x64\x20\x30\x21\x3d\x3d\x6e\x29\x72\x65\x74\x75\x72\x6e\x20\x6e\x2e\x65\x78\x70\x6f\x72\x74\x73\x3b\x76\x61\x72\x20\x74\x3d\x6d\x5b\x65\x5d\x3d\x7b\x65\x78\x70\x6f\x72\x74\x73\x3a\x7b\x7d\x7d\x3b\x72\x65\x74\x75\x72\x6e\x20\x76\x5b\x65\x5d\x2e\x63\x61\x6c\x6c\x28\x74\x2e\x65\x78\x70\x6f\x72\x74\x73\x2c\x74\x2c\x74\x2e\x65\x78\x70\x6f\x72\x74\x73\x2c\x72\x29\x2c\x74\x2e\x65\x78\x70\x6f\x72\x74\x73\x7d\x72\x2e\x6d\x3d\x76\x2c\x65\x3d\x5b\x5d\x2c\x72\x2e\x4f\x3d\x28\x6e\x2c\x74\x2c\x69\x2c\x75\x29\x3d\x3e\x7b\x69\x66\x28\x21\x74\x29\x7b\x76\x61\x72\x20\x61\x3d\x31\x2f\x30\x3b\x66\x6f\x72\x28\x6f\x3d\x30\x3b\x6f\x3c\x65\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x6f\x2b\x2b\x29\x7b\x66\x6f\x72\x28\x76\x61\x72\x5b\x74\x2c\x69\x2c\x75\x5d\x3d\x65\x5b\x6f\x5d\x2c\x73\x3d\x21\x30\x2c\x66\x3d\x30\x3b\x66\x3c\x74\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x66\x2b\x2b\x29\x28\x21\x31\x26\x75\x7c\x7c\x61\x3e\x3d\x75\x29\x26\x26\x4f\x62\x6a\x65\x63\x74\x2e\x6b\x65\x79\x73\x28\x72\x2e\x4f\x29\x2e\x65\x76\x65\x72\x79\x28\x70\x3d\x3e\x72\x2e\x4f\x5b\x70\x5d\x28\x74\x5b\x66\x5d\x29\x29\x3f\x74\x2e\x73\x70\x6c\x69\x63\x65\x28\x66\x2d\x2d\x2c\x31\x29\x3a\x28\x73\x3d\x21\x31\x2c\x75\x3c\x61\x26\x26\x28\x61\x3d\x75\x29\x29\x3b\x69\x66\x28\x73\x29\x7b\x65\x2e\x73\x70\x6c\x69\x63\x65\x28\x6f\x2d\x2d\x2c\x31\x29\x3b\x76\x61\x72\x20\x6c\x3d\x69\x28\x29\x3b\x76\x6f\x69\x64\x20\x30\x21\x3d\x3d\x6c\x26\x26\x28\x6e\x3d\x6c\x29\x7d\x7d\x72\x65\x74\x75\x72\x6e\x20\x6e\x7d\x75\x3d\x75\x7c\x7c\x30\x3b\x66\x6f\x72\x28\x76\x61\x72\x20\x6f\x3d\x65\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x6f\x3e\x30\x26\x26\x65\x5b\x6f\x2d\x31\x5d\x5b\x32\x5d\x3e\x75\x3b\x6f\x2d\x2d\x29\x65\x5b\x6f\x5d\x3d\x65\x5b\x6f\x2d\x31\x5d\x3b\x65\x5b\x6f\x5d\x3d\x5b\x74\x2c\x69\x2c\x75\x5d\x7d\x2c\x72\x2e\x6e\x3d\x65\x3d\x3e\x7b\x76\x61\x72\x20\x6e\x3d\x65\x26\x26\x65\x2e\x5f\x5f\x65\x73\x4d\x6f\x64\x75\x6c\x65\x3f\x28\x29\x3d\x3e\x65\x2e\x64\x65\x66\x61\x75\x6c\x74\x3a\x28\x29\x3d\x3e\x65\x3b\x72\x65\x74\x75\x72\x6e\x20\x72\x2e\x64\x28\x6e\x2c\x7b\x61\x3a\x6e\x7d\x29\x2c\x6e\x7d\x2c\x72\x2e\x64\x3d\x28\x65\x2c\x6e\x29\x3d\x3e\x7b\x66\x6f\x72\x28\x76\x61\x72\x20\x74\x20\x69\x6e\x20\x6e\x29\x72\x2e\x6f\x28\x6e\x2c\x74\x29\x26\x26\x21\x72\x2e\x6f\x28\x65\x2c\x74\x29\x26\x26\x4f\x62\x6a\x65\x63\x74\x2e\x64\x65\x66\x69\x6e\x65\x50\x72\x6f\x70\x65\x72\x74\x79\x28\x65\x2c\x74\x2c\x7b\x65\x6e\x75\x6d\x65\x72\x61\x62\x6c\x65\x3a\x21\x30\x2c\x67\x65\x74\x3a\x6e\x5b\x74\x5d\x7d\x29\x7d\x2c\x72\x2e\x66\x3d\x7b\x7d\x2c\x72\x2e\x65\x3d\x65\x3d\x3e\x50\x72\x6f\x6d\x69\x73\x65\x2e\x61\x6c\x6c\x28\x4f\x62\x6a\x65\x63\x74\x2e\x6b\x65\x79\x73\x28\x72\x2e\x66\x29\x2e\x72\x65\x64\x75\x63\x65\x28\x28\x6e\x2c\x74\x29\x3d\x3e\x28\x72\x2e\x66\x5b\x74\x5d\x28\x65\x2c\x6e\x29\x2c\x6e\x29\x2c\x5b\x5d\x29\x29\x2c\x72\x2e\x75\x3d\x65\x3d\x3e\x65\x2b\x22\x2e\x66\x65\x35\x66\x30\x32\x62\x33\x61\x36\x35\x39\x39\x33\x65\x64\x33\x65\x61\x31\x2e\x6a\x73\x22\x2c\x72\x2e\x6d\x69\x6e\x69\x43\x73\x73\x46\x3d\x65\x3d\x3e\x22\x73\x74\x79\x6c\x65\x73\x2e\x64\x34\x63\x35\x31\x38\x31\x62\x64\x31\x32\x33\x32\x39\x63\x35\x33\x31\x31\x37\x2e\x63\x73\x73\x22\x2c\x72\x2e\x6f\x3d\x28\x65\x2c\x6e\x29\x3d\x3e\x4f\x62\x6a\x65\x63\x74\x2e\x70\x72\x6f\x74\x6f\x74\x79\x70\x65\x2e\x68\x61\x73\x4f\x77\x6e\x50\x72\x6f\x70\x65\x72\x74\x79\x2e\x63\x61\x6c\x6c\x28\x65\x2c\x6e\x29\x2c\x28\x28\x29\x3d\x3e\x7b\x76\x61\x72\x20\x65\x3d\x7b\x7d\x2c\x6e\x3d\x22\x67\x6f\x2d\x6d\x69\x63\x72\x6f\x2d\x64\x61\x73\x68\x62\x6f\x61\x72\x64\x3a\x22\x3b\x72\x2e\x6c\x3d\x28\x74\x2c\x69\x2c\x75\x2c\x6f\x29\x3d\x3e\x7b\x69\x66\x28\x65\x5b\x74\x5d\x29\x65\x5b\x74\x5d\x2e\x70\x75\x73\x68\x28\x69\x29\x3b\x65\x6c\x73\x65\x7b\x76\x61\x72\x20\x61\x2c\x73\x3b\x69\x66\x28\x76\x6f\x69\x64\x20\x30\x21\x3d\x3d\x75\x29\x66\x6f\x72\x28\x76\x61\x72\x20\x66\x3d\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\x73\x63\x72\x69\x70\x74\x22\x29\x2c\x6c\x3d\x30\x3b\x6c\x3c\x66\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x6c\x2b\x2b\x29\x7b\x76\x61\x72\x20\x64\x3d\x66\x5b\x6c\x5d\x3b\x69\x66\x28\x64\x2e\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x73\x72\x63\x22\x29\x3d\x3d\x74\x7c\x7c\x64\x2e\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x64\x61\x74\x61\x2d\x77\x65\x62\x70\x61\x63\x6b\x22\x29\x3d\x3d\x6e\x2b\x75\x29\x7b\x61\x3d\x64\x3b\x62\x72\x65\x61\x6b\x7d\x7d\x61\x7c\x7c\x28\x73\x3d\x21\x30\x2c\x28\x61\x3d\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x73\x63\x72\x69\x70\x74\x22\x29\x29\x2e\x63\x68\x61\x72\x73\x65\x74\x3d\x22\x75\x74\x66\x2d\x38\x22\x2c\x61\x2e\x74\x69\x6d\x65\x6f\x75\x74\x3d\x31\x32\x30\x2c\x72\x2e\x6e\x63\x26\x26\x61\x2e\x73\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x6e\x6f\x6e\x63\x65\x22\x2c\x72\x2e\x6e\x63\x29\x2c\x61\x2e\x73\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x64\x61\x74\x61\x2d\x77\x65\x62\x70\x61\x63\x6b\x22\x2c\x6e\x2b\x75\x29\x2c\x61\x2e\x73\x72\x63\x3d\x72\x2e\x74\x75\x28\x74\x29\x29\x2c\x65\x5b\x74\x5d\x3d\x5b\x69\x5d\x3b\x76\x61\x72\x20\x63\x3d\x28\x67\x2c\x70\x29\x3d\x3e\x7b\x61\x2e\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x61\x2e\x6f\x6e\x6c\x6f\x61\x64\x3d\x6e\x75\x6c\x6c\x2c\x63\x6c\x65\x61\x72\x54\x69\x6d\x65\x6f\x75\x74\x28\x62\x29\x3b\x76\x61\x72\x20\x5f\x3d\x65\x5b\x74\x5d\x3b\x69\x66\x28\x64\x65\x6c\x65\x74\x65\x20\x65\x5b\x74\x5d\x2c\x61\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x26\x26\x61\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2e\x72\x65\x6d\x6f\x76\x65\x43\x68\x69\x6c\x64\x28\x61\x29\x2c\x5f\x26\x26\x5f\x2e\x66\x6f\x72\x45\x61\x63\x68\x28\x68\x3d\x3e\x68\x28\x70\x29\x29\x2c\x67\x29\x72\x65\x74\x75\x72\x6e\x20\x67\x28\x70\x29\x7d\x2c\x62\x3d\x73\x65\x74\x54\x69\x6d\x65\x6f\x75\x74\x28\x63\x2e\x62\x69\x6e\x64\x28\x6e\x75\x6c\x6c\x2c\x76\x6f\x69\x64\x20\x30\x2c\x7b\x74\x79\x70\x65\x3a\x22\x74\x69\x6d\x65\x6f\x75\x74\x22\x2c\x74\x61\x72\x67\x65\x74\x3a\x61\x7d\x29\x2c\x31\x32\x65\x34\x29\x3b\x61\x2e\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x63\x2e\x62\x69\x6e\x64\x28\x6e\x75\x6c\x6c\x2c\x61\x2e\x6f\x6e\x65\x72\x72\x6f\x72\x29\x2c\x61\x2e\x6f\x6e\x6c\x6f\x61\x64\x3d\x63\x2e\x62\x69\x6e\x64\x28\x6e\x75\x6c\x6c\x2c\x61\x2e\x6f\x6e\x6c\x6f\x61\x64\x29\x2c\x73\x26\x26\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x68\x65\x61\x64\x2e\x61\x70\x70\x65\x6e\x64\x43\x68\x69\x6c\x64\x28\x61\x29\x7d\x7d\x7d\x29\x28\x29\x2c\x72\x2e\x72\x3d\x65\x3d\x3e\x7b\x22\x75\x6e\x64\x65\x66\x69\x6e\x65\x64\x22\x21\x3d\x74\x79\x70\x65\x6f\x66\x20\x53\x79\x6d\x62\x6f\x6c\x26\x26\x53\x79\x6d\x62\x6f\x6c\x2e\x74\x6f\x53\x74\x72\x69\x6e\x67\x54\x61\x67\x26\x26\x4f\x62\x6a\x65\x63\x74\x2e\x64\x65\x66\x69\x6e\x65\x50\x72\x6f\x70\x65\x72\x74\x79\x28\x65\x2c\x53\x79\x6d\x62\x6f\x6c\x2e\x74\x6f\x53\x74\x72\x69\x6e\x67\x54\x61\x67\x2c\x7b\x76\x61\x6c\x75\x65\x3a\x22\x4d\x6f\x64\x75\x6c\x65\x22\x7d\x29\x2c\x4f\x62\x6a\x65\x63\x74\x2e\x64\x65\x66\x69\x6e\x65\x50\x72\x6f\x70\x65\x72\x74\x79\x28\x65\x2c\x22\x5f\x5f\x65\x73\x4d\x6f\x64\x75\x6c\x65\x22\x2c\x7b\x76\x61\x6c\x75\x65\x3a\x21\x30\x7d\x29\x7d\x2c\x28\x28\x29\x3d\x3e\x7b\x76\x61\x72\x20\x65\x3b\x72\x2e\x74\x75\x3d\x6e\x3d\x3e\x28\x76\x6f\x69\x64\x20\x30\x3d\x3d\x3d\x65\x26\x26\x28\x65\x3d\x7b\x63\x72\x65\x61\x74\x65\x53\x63\x72\x69\x70\x74\x55\x52\x4c\x3a\x74\x3d\x3e\x74\x7d\x2c\x22\x75\x6e\x64\x65\x66\x69\x6e\x65\x64\x22\x21\x3d\x74\x79\x70\x65\x6f\x66\x20\x74\x72\x75\x73\x74\x65\x64\x54\x79\x70\x65\x73\x26\x26\x74\x72\x75\x73\x74\x65\x64\x54\x79\x70\x65\x73\x2e\x63\x72\x65\x61\x74\x65\x50\x6f\x6c\x69\x63\x79\x26\x26\x28\x65\x3d\x74\x72\x75\x73\x74\x65\x64\x54\x79\x70\x65\x73\x2e\x63\x72\x65\x61\x74\x65\x50\x6f\x6c\x69\x63\x79\x28\x22\x61\x6e\x67\x75\x6c\x61\x72\x23\x62\x75\x6e\x64\x6c\x65\x72\x22\x2c\x65\x29\x29\x29\x2c\x65\x2e\x63\x72\x65\x61\x74\x65\x53\x63\x72\x69\x70\x74\x55\x52\x4c\x28\x6e\x29\x29\x7d\x29\x28\x29\x2c\x72\x2e\x70\x3d\x22\x22\x2c\x28\x28\x29\x3d\x3e\x7b\x76\x61\x72\x20\x65\x3d\x7b\x36\x36\x36\x3a\x30\x7d\x3b\x72\x2e\x66\x2e\x6a\x3d\x28\x69\x2c\x75\x29\x3d\x3e\x7b\x76\x61\x72\x20\x6f\x3d\x72\x2e\x6f\x28\x65\x2c\x69\x29\x3f\x65\x5b\x69\x5d\x3a\x76\x6f\x69\x64\x20\x30\x3b\x69\x66\x28\x30\x21\x3d\x3d\x6f\x29\x69\x66\x28\x6f\x29\x75\x2e\x70\x75\x73\x68\x28\x6f\x5b\x32\x5d\x29\x3b\x65\x6c\x73\x65\x20\x69\x66\x28\x36\x36\x36\x21\x3d\x69\x29\x7b\x76\x61\x72\x20\x61\x3d\x6e\x65\x77\x20\x50\x72\x6f\x6d\x69\x73\x65\x28\x28\x64\x2c\x63\x29\x3d\x3e\x6f\x3d\x65\x5b\x69\x5d\x3d\x5b\x64\x2c\x63\x5d\x29\x3b\x75\x2e\x70\x75\x73\x68\x28\x6f\x5b\x32\x5d\x3d\x61\x29\x3b\x76\x61\x72\x20\x73\x3d\x72\x2e\x70\x2b\x72\x2e\x75\x28\x69\x29\x2c\x66\x3d\x6e\x65\x77\x20\x45\x72\x72\x6f\x72\x3b\x72\x2e\x6c\x28\x73\x2c\x64\x3d\x3e\x7b\x69\x66\x28\x72\x2e\x6f\x28\x65\x2c\x69\x29\x26\x26\x28\x30\x21\x3d\x3d\x28\x6f\x3d\x65\x5b\x69\x5d\x29\x26\x26\x28\x65\x5b\x69\x5d\x3d\x76\x6f\x69\x64\x20\x30\x29\x2c\x6f\x29\x29\x7b\x76\x61\x72\x20\x63\x3d\x64\x26\x26\x28\x22\x6c\x6f\x61\x64\x22\x3d\x3d\x3d\x64\x2e\x74\x79\x70\x65\x3f\x22\x6d\x69\x73\x73\x69\x6e\x67\x22\x3a\x64\x2e\x74\x79\x70\x65\x29\x2c\x62\x3d\x64\x26\x26\x64\x2e\x74\x61\x72\x67\x65\x74\x26\x26\x64\x2e\x74\x61\x72\x67\x65\x74\x2e\x73\x72\x63\x3b\x66\x2e\x6d\x65\x73\x73\x61\x67\x65\x3d\x22\x4c\x6f\x61\x64\x69\x6e\x67\x20\x63\x68\x75\x6e\x6b\x20\x22\x2b\x69\x2b\x22\x20\x66\x61\x69\x6c\x65\x64\x2e\x5c\x6e\x28\x22\x2b\x63\x2b\x22\x3a\x20\x22\x2b\x62\x2b\x22\x29\x22\x2c\x66\x2e\x6e\x61\x6d\x65\x3d\x22\x43\x68\x75\x6e\x6b\x4c\x6f\x61\x64\x45\x72\x72\x6f\x72\x22\x2c\x66\x2e\x74\x79\x70\x65\x3d\x63\x2c\x66\x2e\x72\x65\x71\x75\x65\x73\x74\x3d\x62\x2c\x6f\x5b\x31\x5d\x28\x66\x29\x7d\x7d\x2c\x22\x63\x68\x75\x6e\x6b\x2d\x22\x2b\x69\x2c\x69\x29\x7d\x65\x6c\x73\x65\x20\x65\x5b\x69\x5d\x3d\x30\x7d\x2c\x72\x2e\x4f\x2e\x6a\x3d\x69\x3d\x3e\x30\x3d\x3d\x3d\x65\x5b\x69\x5d\x3b\x76\x61\x72\x20\x6e\x3d\x28\x69\x2c\x75\x29\x3d\x3e\x7b\x76\x61\x72\x20\x66\x2c\x6c\x2c\x5b\x6f\x2c\x61\x2c\x73\x5d\x3d\x75\x2c\x64\x3d\x30\x3b\x66\x6f\x72\x28\x66\x20\x69\x6e\x20\x61\x29\x72\x2e\x6f\x28\x61\x2c\x66\x29\x26\x26\x28\x72\x2e\x6d\x5b\x66\x5d\x3d\x61\x5b\x66\x5d\x29\x3b\x69\x66\x28\x73\x29\x76\x61\x72\x20\x63\x3d\x73\x28\x72\x29\x3b\x66\x6f\x72\x28\x69\x26\x26\x69\x28\x75\x29\x3b\x64\x3c\x6f\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x64\x2b\x2b\x29\x72\x2e\x6f\x28\x65\x2c\x6c\x3d\x6f\x5b\x64\x5d\x29\x26\x26\x65\x5b\x6c\x5d\x26\x26\x65\x5b\x6c\x5d\x5b\x30\x5d\x28\x29\x2c\x65\x5b\x6f\x5b\x64\x5d\x5d\x3d\x30\x3b\x72\x65\x74\x75\x72\x6e\x20\x72\x2e\x4f\x28\x63\x29\x7d\x2c\x74\x3d\x73\x65\x6c\x66\x2e\x77\x65\x62\x70\x61\x63\x6b\x43\x68\x75\x6e\x6b\x67\x6f\x5f\x6d\x69\x63\x72\x6f\x5f\x64\x61\x73\x68\x62\x6f\x61\x72\x64\x3d\x73\x65\x6c\x66\x2e\x77\x65\x62\x70\x61\x63\x6b\x43\x68\x75\x6e\x6b\x67\x6f\x5f\x6d\x69\x63\x72\x6f\x5f\x64\x61\x73\x68\x62\x6f\x61\x72\x64\x7c\x7c\x5b\x5d\x3b\x74\x2e\x66\x6f\x72\x45\x61\x63\x68\x28\x6e\x2e\x62\x69\x6e\x64\x28\x6e\x75\x6c\x6c\x2c\x30\x29\x29\x2c\x74\x2e\x70\x75\x73\x68\x3d\x6e\x2e\x62\x69\x6e\x64\x28\x6e\x75\x6c\x6c\x2c\x74\x2e\x70\x75\x73\x68\x2e\x62\x69\x6e\x64\x28\x74\x29\x29\x7d\x29\x28\x29\x7d\x29\x28\x29\x3b") 13 | 14 | func init() { 15 | 16 | f, err := FS.OpenFile(CTX, "/runtime.337b117ff6241f38ea69.js", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | _, err = f.Write(FileRuntime337b117ff6241f38ea69Js) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | err = f.Close() 27 | if err != nil { 28 | panic(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /web/route.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path/filepath" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func RegisterRoute(router *gin.Engine) error { 12 | files, err := WalkDirs("", false) 13 | if err != nil { 14 | return err 15 | } 16 | for _, f := range files { 17 | router.GET(f, func(name string) gin.HandlerFunc { 18 | return func(c *gin.Context) { 19 | data, err := ReadFile(name) 20 | if err != nil { 21 | c.AbortWithError(500, err) 22 | return 23 | } 24 | switch filepath.Ext(name) { 25 | case ".html": 26 | c.Header("Content-Type", "text/html; charset=utf-8") 27 | case ".css": 28 | c.Header("Content-Type", "text/css; charset=utf-8") 29 | case ".js": 30 | c.Header("Content-Type", "text/javascript") 31 | case ".svg": 32 | c.Header("Content-Type", "image/svg+xml") 33 | } 34 | if path, err := exec.LookPath(os.Args[0]); err == nil { 35 | if file, err := os.Stat(path); err == nil { 36 | c.Header("Last-Modified", file.ModTime().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")) 37 | } 38 | } 39 | if _, err := c.Writer.Write(data); err != nil { 40 | c.AbortWithError(500, err) 41 | return 42 | } 43 | } 44 | }(f)) 45 | } 46 | router.GET("/", func(c *gin.Context) { 47 | data, err := ReadFile("index.html") 48 | if err != nil { 49 | c.AbortWithError(500, err) 50 | return 51 | } 52 | c.Header("Content-Type", "text/html; charset=utf-8") 53 | if _, err := c.Writer.Write(data); err != nil { 54 | c.AbortWithError(500, err) 55 | return 56 | } 57 | }) 58 | return nil 59 | } 60 | --------------------------------------------------------------------------------
Version: {{ version }}
{{ version }}
{{ response | json }}
{{ data.request | endpoint }}
{{ data.response | endpoint }}