├── .github ├── weekly-digest.yml └── workflows │ ├── release.yml │ └── reviewdog.yml ├── .gitignore ├── .goreleaser.yml ├── Makefile ├── README.md ├── cmd ├── command.go ├── migrate │ └── migrate.go ├── run │ ├── docs.go │ ├── reload.go │ ├── run.go │ └── watch.go ├── version │ ├── banner.go │ └── version.go └── web │ └── web.go ├── docs └── images │ ├── lowcode-dsl.png │ ├── lowcode-home.png │ ├── lowcode-projects.png │ └── lowcode-template.png ├── getlatest.sh ├── go.mod ├── go.sum ├── internal ├── app │ └── module │ │ └── web │ │ ├── api.go │ │ ├── constx │ │ └── constx.go │ │ ├── core │ │ ├── core.go │ │ └── validator.go │ │ ├── invoker.go │ │ ├── parser │ │ ├── container.go │ │ ├── model.go │ │ ├── parser.go │ │ ├── parser_struct.go │ │ ├── parser_test.go │ │ ├── pongo2.go │ │ ├── pongo2 │ │ │ ├── LICENSE │ │ │ ├── context.go │ │ │ ├── doc.go │ │ │ ├── error.go │ │ │ ├── filters.go │ │ │ ├── filters_builtin.go │ │ │ ├── helpers.go │ │ │ ├── lexer.go │ │ │ ├── nodes.go │ │ │ ├── nodes_html.go │ │ │ ├── nodes_wrapper.go │ │ │ ├── options.go │ │ │ ├── parser.go │ │ │ ├── parser_document.go │ │ │ ├── parser_expression.go │ │ │ ├── pongo2.go │ │ │ ├── pongo2_issues_test.go │ │ │ ├── pongo2_template_test.go │ │ │ ├── pongo2_test.go │ │ │ ├── tags.go │ │ │ ├── tags_autoescape.go │ │ │ ├── tags_block.go │ │ │ ├── tags_comment.go │ │ │ ├── tags_cycle.go │ │ │ ├── tags_extends.go │ │ │ ├── tags_filter.go │ │ │ ├── tags_firstof.go │ │ │ ├── tags_for.go │ │ │ ├── tags_if.go │ │ │ ├── tags_ifchanged.go │ │ │ ├── tags_ifequal.go │ │ │ ├── tags_ifnotequal.go │ │ │ ├── tags_import.go │ │ │ ├── tags_include.go │ │ │ ├── tags_lorem.go │ │ │ ├── tags_macro.go │ │ │ ├── tags_now.go │ │ │ ├── tags_set.go │ │ │ ├── tags_spaceless.go │ │ │ ├── tags_ssi.go │ │ │ ├── tags_templatetag.go │ │ │ ├── tags_widthratio.go │ │ │ ├── tags_with.go │ │ │ ├── template.go │ │ │ ├── template_loader.go │ │ │ ├── template_sets.go │ │ │ ├── value.go │ │ │ └── variable.go │ │ ├── pongo2render │ │ │ └── render.go │ │ ├── render.go │ │ ├── schema.go │ │ ├── schema_model.go │ │ ├── schema_render.go │ │ ├── typeparser.go │ │ ├── typeparser_test.go │ │ └── util.go │ │ ├── project │ │ └── service.go │ │ ├── template │ │ └── template.go │ │ └── web.go ├── colors │ ├── color.go │ ├── colorwriter.go │ └── colorwriter_windows.go ├── command │ └── cmd.go ├── config │ └── conf.go ├── git │ └── repository.go ├── logger │ └── logger.go ├── system │ └── system.go └── utils │ ├── dsn.go │ ├── file.go │ ├── notification.go │ └── utils.go ├── main.go ├── scripts └── build │ ├── gobuild.sh │ └── report_build_info.sh └── webui ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── README.md ├── config ├── config.dev.ts ├── config.ts ├── defaultSettings.ts ├── proxy.ts └── routes.ts ├── embed_webui.go ├── jest.config.js ├── jsconfig.json ├── mock ├── listTableList.ts ├── notices.ts ├── route.ts └── user.ts ├── package-lock.json ├── package.json ├── public ├── CNAME ├── favicon.ico ├── home_bg.png ├── icons │ ├── icon-128x128.png │ ├── icon-192x192.png │ └── icon-512x512.png └── pro_icon.svg ├── src ├── assets │ └── logo.svg ├── components │ ├── Authorized │ │ ├── Authorized.tsx │ │ ├── AuthorizedRoute.tsx │ │ ├── CheckPermissions.tsx │ │ ├── PromiseRender.tsx │ │ ├── Secured.tsx │ │ ├── index.tsx │ │ └── renderAuthorize.ts │ ├── GlobalHeader │ │ ├── AvatarDropdown.tsx │ │ ├── NoticeIconView.tsx │ │ ├── RightContent.tsx │ │ └── index.less │ ├── HeaderDropdown │ │ ├── index.less │ │ └── index.tsx │ ├── HeaderSearch │ │ ├── index.less │ │ └── index.tsx │ ├── NoticeIcon │ │ ├── NoticeList.less │ │ ├── NoticeList.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── PageLoading │ │ └── index.tsx │ └── SearchTable │ │ ├── index.less │ │ └── index.tsx ├── e2e │ ├── __mocks__ │ │ └── antd-pro-merge-less.js │ └── baseLayout.e2e.js ├── global.less ├── global.tsx ├── layouts │ ├── BasicLayout.tsx │ ├── BlankLayout.tsx │ ├── SecurityLayout.tsx │ ├── UserLayout.less │ └── UserLayout.tsx ├── locales │ ├── en-US.ts │ ├── en-US │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pages.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── id-ID.ts │ ├── id-ID │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pages.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── ja-JP.ts │ ├── ja-JP │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pages.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── pt-BR.ts │ ├── pt-BR │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── zh-CN.ts │ ├── zh-CN │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pages.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── zh-TW.ts │ └── zh-TW │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts ├── manifest.json ├── models │ ├── connect.d.ts │ ├── global.ts │ ├── login.ts │ ├── setting.ts │ └── user.ts ├── pages │ ├── 404.tsx │ ├── Admin.tsx │ ├── Projects │ │ ├── components │ │ │ ├── Editor.tsx │ │ │ ├── ListForm.tsx │ │ │ └── Render.tsx │ │ └── index.tsx │ ├── Templates │ │ ├── components │ │ │ └── ListForm.tsx │ │ └── index.tsx │ ├── User │ │ └── login │ │ │ ├── index.less │ │ │ └── index.tsx │ ├── Welcome.less │ ├── Welcome.tsx │ └── document.ejs ├── service-worker.js ├── services │ ├── api.ts │ ├── login.ts │ └── user.ts ├── typings.d.ts └── utils │ ├── Authorized.ts │ ├── authority.ts │ ├── request.ts │ ├── utils.less │ ├── utils.test.ts │ └── utils.ts ├── tests ├── PuppeteerEnvironment.js ├── beforeTest.js ├── getBrowser.js └── run-tests.js └── tsconfig.json /.github/weekly-digest.yml: -------------------------------------------------------------------------------- 1 | # Configuration for weekly-digest - https://github.com/apps/weekly-digest 2 | publishDay: sun 3 | canPublishIssues: true 4 | canPublishPullRequests: true 5 | canPublishContributors: true 6 | canPublishStargazers: true 7 | canPublishCommits: true -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - name: Set up Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: 1.18 19 | - name: Build Mac and build 20 | run: | 21 | make build 22 | - name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v2 24 | with: 25 | version: latest 26 | args: release --rm-dist 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/reviewdog.yml: -------------------------------------------------------------------------------- 1 | name: actions 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | golangci-lint: 7 | name: runner / golangci-lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out code into the Go module directory 11 | uses: actions/checkout@v1 12 | - name: golangci-lint 13 | uses: reviewdog/action-golangci-lint@v1 14 | with: 15 | golangci_lint_flags: "--enable-all --timeout=10m --exclude-use-default=false --tests=false --disable=gochecknoinits,gochecknoglobals,exhaustive,nakedret,exhaustivestruct" 16 | workdir: cmd -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | bak 4 | crm 5 | bin.tar.gz 6 | dist.tar.gz 7 | default.json.* 8 | *.log 9 | *.log.* 10 | /bin/* 11 | *.sys.* 12 | 13 | egoctl 14 | egoctl-linux 15 | egoctl-darwin 16 | 17 | 18 | *.swp 19 | *.swo 20 | egoctl.go 21 | egoctl.toml 22 | .egoctl.timestamp 23 | logs 24 | 25 | release/ 26 | dist/ 27 | node_modules -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod download 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - linux 14 | - windows 15 | - darwin 16 | ldflags: 17 | - -s -w -X {{.ModulePath}}/cmd/version.buildVersion={{.Version}} -X {{.ModulePath}}/cmd/version.buildGitVersion={{.Version}} -X {{.ModulePath}}/cmd/version.buildTag={{.Tag}} -X {{.ModulePath}}/cmd/version.buildStatus=Clean -X {{.ModulePath}}/cmd/version.buildUser=goreleaser -X {{.ModulePath}}/cmd/version.buildHost=goreleaser -X {{.ModulePath}}/cmd/version.buildTime={{.Date}} 18 | archives: 19 | - name_template: "egoctl-{{ .Version }}-{{ .Os }}-{{ .Arch }}" 20 | replacements: 21 | darwin: Darwin 22 | linux: Linux 23 | windows: Windows 24 | 386: i386 25 | amd64: x86_64 26 | checksum: 27 | name_template: 'checksums.txt' 28 | snapshot: 29 | name_template: "{{ .Tag }}-next" 30 | changelog: 31 | sort: asc 32 | filters: 33 | exclude: 34 | - '^docs:' 35 | - '^test:' 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 2 | APP_NAME := $(shell basename $(APP_PATH)) 3 | APP_PKG := $(shell go list -m) 4 | SCRIPT_PATH := $(APP_PATH)/scripts 5 | COMPILE_OUT:=$(APP_PATH)/bin/$(APP_NAME) 6 | 7 | .PHONY: all test clean build install 8 | 9 | GOFLAGS ?= $(GOFLAGS:) 10 | 11 | all: install test 12 | 13 | install: install.go 14 | build: build.ui build.linux build.darwin 15 | 16 | install.go: 17 | @echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>making $@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 18 | @go get $(GOFLAGS) ./... 19 | @echo -e "\n" 20 | 21 | build.ui: 22 | @echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>making $@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 23 | @cd $(APP_PATH)/webui && yarn install --frozen-lockfile && yarn run build 24 | @echo -e "\n" 25 | 26 | build.linux: 27 | @echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>make $@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 28 | @export GOOS=linux && $(SCRIPT_PATH)/build/gobuild.sh $(APP_NAME) $(COMPILE_OUT)-$${GOOS} $(APP_PKG) 29 | @echo -e "\n" 30 | 31 | build.darwin: 32 | @echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>make $@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 33 | @export GOOS=darwin && $(SCRIPT_PATH)/build/gobuild.sh $(APP_NAME) $(COMPILE_OUT)-$${GOOS} $(APP_PKG) 34 | @echo -e "\n" 35 | 36 | test: install 37 | @echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>making $@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 38 | @go test $(GOFLAGS) ./... 39 | @echo -e "\n" 40 | 41 | bench: install 42 | @echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>making $@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 43 | @go test -run=NONE -bench=. $(GOFLAGS) ./... 44 | @echo -e "\n" 45 | 46 | clean: 47 | @echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>making $@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 48 | @go clean $(GOFLAGS) -i ./... 49 | @echo -e "\n" 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egoctl 2 | ## 1 Requirements 3 | - Go version >= 1.18 4 | 5 | ## 2 Installation 6 | ``` 7 | bash <(curl -L https://raw.githubusercontent.com/gotomicro/egoctl/main/getlatest.sh) 8 | ``` 9 | 通过以上脚本,可以下载protoc工具全家桶,以及ego的protoc插件和egoctl 10 | * /usr/local/bin/protoc 11 | * /usr/local/bin/protoc-gen-go 12 | * /usr/local/bin/protoc-gen-go-grpc 13 | * /usr/local/bin/protoc-gen-openapiv2 14 | * /usr/local/bin/protoc-gen-go-errors 15 | * /usr/local/bin/protoc-gen-go-http 16 | * /usr/local/bin/egoctl 17 | 18 | ## 3 快速生成代码 19 | * 启动web: egoctl web start 20 | * 访问http://127.0.0.1:9999 21 | * 如下所示 22 | 23 | ![](./docs/images/lowcode-home.png) 24 | 25 | * 我们第一次进入页面,先配置生成代码的模板,你也可以自己自定义自己的模板 26 | 27 | ![](./docs/images/lowcode-template.png) 28 | 29 | * 然后在进入项目页面,创建项目 30 | 31 | ![](./docs/images/lowcode-projects.png) 32 | 33 | * 编写DSL 34 | 35 | ![](./docs/images/lowcode-dsl.png) 36 | 37 | * 创建项目的go mod 38 | ```bash 39 | mkdir /Users/askuy/tmp/egotest1 40 | cd /Users/askuy/tmp/egotest1 && go mod init egotest1 41 | ``` 42 | * 点击生成代码 43 | * 运行你的代码 44 | ```bash 45 | cd /Users/askuy/tmp/egotest1 46 | go mod tidy 47 | vim config/local.toml 更改db配置 48 | export EGO_DEBUG=true && go run main.go 49 | ``` 50 | 51 | ## 3 模板 52 | 因为前端会使用关键字`{{`, `}}`,而`pongo2`的模板也会使用该关键字,所以`egoctl`将`pongo2/v6`版本`fork`到项目里, 53 | 将模板关键字`{{`,`}}`改为`{$`,`$}` 54 | 55 | ## 5 DSL配置 56 | ``` 57 | package egoctl 58 | type User struct { 59 | Uid int `gorm:"AUTO_INCREMENT" json:"id" dto:"" ego:"primary_key"` // id 60 | UserName string `gorm:"not null" json:"userName" dto:""` // 昵称 61 | } 62 | ``` 63 | 64 | 65 | ## 6 模板配置 66 | ### 6.1 根据模型设置模板 67 | DSL配置 68 | ``` 69 | package egoctl 70 | type User struct { 71 | Uid int `gorm:"AUTO_INCREMENT" json:"id" dto:"" ego:"primary_key"` // id 72 | UserName string `gorm:"not null" json:"userName" dto:""` // 昵称 73 | } 74 | ``` 75 | ### 6.2 获取主键 76 | 模版配置 77 | ``` 78 | {$modelSchemas|fieldsGetPrimaryKey|snakeString$} 79 | ``` 80 | 81 | ### 6.3 生成结构体 82 | 模板配置 83 | ``` 84 | type {{modelName|upperFirst}} struct { 85 | {% for value in modelSchemas %} 86 | {$ value.FieldName $} {$ value.FieldType $} `gorm:"{$ value|fieldGetTag:"gorm" $}"` {$ value.Comment $} 87 | {% endfor %} 88 | } 89 | ``` 90 | 91 | ### 6.4 判断某字段是否存在 92 | 模板配置 93 | ``` 94 | {% if modelSchemas|fieldsExist:Uid %} 95 | {% endif %} 96 | ``` 97 | 98 | ### 6.5 判断某个字段中某个tag值是否存在 99 | ``` 100 | {% if modelSchemas|fieldsTagExist:json,userName %} 101 | {% endif %} 102 | ``` 103 | 104 | ## 7 根据单个字段设置模板 105 | DSL配置 106 | ``` 107 | type User struct { 108 | Uid int `gorm:"AUTO_INCREMENT" json:"id" dto:"" ego:"primary_key"` // id 109 | UserName string `gorm:"not null" json:"userName" dto:""` // 昵称 110 | } 111 | ``` 112 | 113 | ### 7.1 获取某个字段的驼峰(常用于JSON,前后端对接) 114 | ``` 115 | {$ value.FieldName|camelString|lowerFirst $} 116 | UserName 变成 userName 117 | ``` 118 | 119 | ### 7.2 获取某个字段的蛇形(常用于数据库) 120 | ``` 121 | {$ value.FieldName|snakeString|lowerFirst $} 122 | UserName 变成 user_name 123 | ``` 124 | -------------------------------------------------------------------------------- /cmd/command.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var RootCommand = &cobra.Command{ 8 | Use: "egoctl", 9 | } 10 | -------------------------------------------------------------------------------- /cmd/run/docs.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "archive/zip" 5 | "io" 6 | "net/http" 7 | "os" 8 | "strings" 9 | 10 | "github.com/gotomicro/egoctl/internal/logger" 11 | ) 12 | 13 | var ( 14 | swaggerVersion = "3" 15 | swaggerlink = "https://github.com/beego/swagger/archive/v" + swaggerVersion + ".zip" 16 | ) 17 | 18 | func downloadFromURL(url, fileName string) { 19 | var down bool 20 | if fd, err := os.Stat(fileName); err != nil && os.IsNotExist(err) { 21 | down = true 22 | } else if fd.Size() == int64(0) { 23 | down = true 24 | } else { 25 | logger.Log.Infof("'%s' already exists", fileName) 26 | return 27 | } 28 | if down { 29 | logger.Log.Infof("Downloading '%s' to '%s'...", url, fileName) 30 | output, err := os.Create(fileName) 31 | if err != nil { 32 | logger.Log.Errorf("Error while creating '%s': %s", fileName, err) 33 | return 34 | } 35 | defer output.Close() 36 | 37 | response, err := http.Get(url) 38 | if err != nil { 39 | logger.Log.Errorf("Error while downloading '%s': %s", url, err) 40 | return 41 | } 42 | defer response.Body.Close() 43 | 44 | n, err := io.Copy(output, response.Body) 45 | if err != nil { 46 | logger.Log.Errorf("Error while downloading '%s': %s", url, err) 47 | return 48 | } 49 | logger.Log.Successf("%d bytes downloaded!", n) 50 | } 51 | } 52 | 53 | func unzipAndDelete(src string) error { 54 | logger.Log.Infof("Unzipping '%s'...", src) 55 | r, err := zip.OpenReader(src) 56 | if err != nil { 57 | return err 58 | } 59 | defer r.Close() 60 | 61 | rp := strings.NewReplacer("swagger-"+swaggerVersion, "swagger") 62 | for _, f := range r.File { 63 | rc, err := f.Open() 64 | if err != nil { 65 | return err 66 | } 67 | defer rc.Close() 68 | 69 | fname := rp.Replace(f.Name) 70 | if f.FileInfo().IsDir() { 71 | os.MkdirAll(fname, f.Mode()) 72 | } else { 73 | f, err := os.OpenFile( 74 | fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 75 | if err != nil { 76 | return err 77 | } 78 | defer f.Close() 79 | 80 | _, err = io.Copy(f, rc) 81 | if err != nil { 82 | return err 83 | } 84 | } 85 | } 86 | logger.Log.Successf("Done! Deleting '%s'...", src) 87 | return os.RemoveAll(src) 88 | } 89 | -------------------------------------------------------------------------------- /cmd/version/banner.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "text/template" 7 | "time" 8 | 9 | "github.com/gotomicro/egoctl/internal/logger" 10 | ) 11 | 12 | var ( 13 | buildVersion string 14 | buildGitVersion string 15 | buildTag string 16 | buildStatus string 17 | buildUser string 18 | buildHost string 19 | buildTime string 20 | ) 21 | 22 | // InitBanner loads the banner and prints it to output 23 | // All errors are ignored, the application will not 24 | // print the banner in case of error. 25 | func InitBanner(out io.Writer, in io.Reader) { 26 | if in == nil { 27 | logger.Log.Fatal("The input is nil") 28 | } 29 | 30 | banner, err := ioutil.ReadAll(in) 31 | if err != nil { 32 | logger.Log.Fatalf("Error while trying to read the banner: %s", err) 33 | } 34 | 35 | show(out, string(banner)) 36 | } 37 | 38 | func show(out io.Writer, content string) { 39 | t, err := template.New("banner"). 40 | Funcs(template.FuncMap{"Now": Now}). 41 | Parse(content) 42 | 43 | if err != nil { 44 | logger.Log.Fatalf("Cannot parse the banner template: %s", err) 45 | } 46 | 47 | data := map[string]interface{}{ 48 | "Version": buildVersion, 49 | "BuildGitVersion": buildGitVersion, 50 | "BuildTag": buildTag, 51 | "BuildStatus": buildStatus, 52 | "BuildUser": buildUser, 53 | "BuildHost": buildHost, 54 | "BuildTime": buildTime, 55 | } 56 | err = t.Execute(out, data) 57 | if err != nil { 58 | logger.Log.Error(err.Error()) 59 | } 60 | } 61 | 62 | // Now returns the current local time in the specified layout 63 | func Now(layout string) string { 64 | return time.Now().Format(layout) 65 | } 66 | -------------------------------------------------------------------------------- /cmd/web/web.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "github.com/gotomicro/egoctl/cmd" 5 | "github.com/gotomicro/egoctl/internal/app/module/web" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var CmdGenerate = &cobra.Command{ 10 | Use: "web [command]", 11 | Short: "Web Generator", 12 | Long: ``, 13 | } 14 | 15 | var ( 16 | flagConfig string 17 | flagSql string 18 | ) 19 | 20 | func init() { 21 | codeCmd := &cobra.Command{ 22 | Use: "start", 23 | Short: "front-end code or backend-code generator", 24 | Run: func(cmd *cobra.Command, args []string) { 25 | web.DefaultWebContainer.Run() 26 | }, 27 | } 28 | CmdGenerate.PersistentFlags().StringVarP(&flagConfig, "start", "s", "./egoctl.toml", "") 29 | CmdGenerate.AddCommand(codeCmd) 30 | cmd.RootCommand.AddCommand(CmdGenerate) 31 | } 32 | -------------------------------------------------------------------------------- /docs/images/lowcode-dsl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/egoctl/65e30a38dc7650f135fcc263408edcf74d54187f/docs/images/lowcode-dsl.png -------------------------------------------------------------------------------- /docs/images/lowcode-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/egoctl/65e30a38dc7650f135fcc263408edcf74d54187f/docs/images/lowcode-home.png -------------------------------------------------------------------------------- /docs/images/lowcode-projects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/egoctl/65e30a38dc7650f135fcc263408edcf74d54187f/docs/images/lowcode-projects.png -------------------------------------------------------------------------------- /docs/images/lowcode-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/egoctl/65e30a38dc7650f135fcc263408edcf74d54187f/docs/images/lowcode-template.png -------------------------------------------------------------------------------- /internal/app/module/web/constx/constx.go: -------------------------------------------------------------------------------- 1 | package constx 2 | 3 | const ( 4 | LevelDBProjects = "projects" 5 | LevelDBProjectIdMax = "projectIdMax" // 最大项目id 6 | LevelDBProjectConfig = "projects_config_%d" // 项目配置存储 7 | LevelDBTemplates = "templates" 8 | ) 9 | 10 | const ( 11 | LanguageGo = "Go" 12 | LanguageReact = "React" 13 | LanguageVue = "Vue" 14 | LanguageElse = "其他" 15 | ) 16 | -------------------------------------------------------------------------------- /internal/app/module/web/core/validator.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/gin-gonic/gin/binding" 10 | "github.com/go-playground/locales/zh" 11 | ut "github.com/go-playground/universal-translator" 12 | "github.com/go-playground/validator/v10" 13 | tzh "github.com/go-playground/validator/v10/translations/zh" 14 | "github.com/gotomicro/ego/core/elog" 15 | ) 16 | 17 | func init() { 18 | binding.Validator = &defaultValidator{} 19 | } 20 | 21 | type defaultValidator struct { 22 | once sync.Once 23 | validate *validator.Validate 24 | } 25 | 26 | var _ binding.StructValidator = &defaultValidator{} 27 | 28 | func (v *defaultValidator) ValidateStruct(obj interface{}) error { 29 | value := reflect.ValueOf(obj) 30 | valueType := value.Kind() 31 | if valueType == reflect.Ptr { 32 | valueType = value.Elem().Kind() 33 | } 34 | if valueType == reflect.Struct { 35 | v.lazyinit() 36 | if err := v.validate.Struct(obj); err != nil { 37 | return err 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | func (v *defaultValidator) Engine() interface{} { 44 | v.lazyinit() 45 | return v.validate 46 | } 47 | 48 | func newValidator() *validator.Validate { 49 | // 注册translator 50 | zhTranslator := zh.New() 51 | uni := ut.New(zhTranslator, zhTranslator) 52 | trans, _ = uni.GetTranslator("zh") 53 | validate := validator.New() 54 | validate.RegisterTagNameFunc(func(field reflect.StructField) string { 55 | label := field.Tag.Get("label") 56 | if label == "" { 57 | return field.Name 58 | } 59 | return label 60 | }) 61 | if err := tzh.RegisterDefaultTranslations(validate, trans); err != nil { 62 | elog.DefaultLogger.Fatal("Gin fail to registered Translation") 63 | } 64 | return validate 65 | } 66 | 67 | func (v *defaultValidator) lazyinit() { 68 | v.once.Do(func() { 69 | v.validate = newValidator() 70 | v.validate.SetTagName("binding") 71 | }) 72 | } 73 | 74 | var trans ut.Translator 75 | 76 | func validate(errs error) error { 77 | if validationErrors, ok := errs.(validator.ValidationErrors); ok { 78 | var errList []string 79 | for _, e := range validationErrors { 80 | errList = append(errList, e.Translate(trans)) 81 | } 82 | return errors.New(strings.Join(errList, "|")) 83 | } 84 | return errs 85 | } 86 | -------------------------------------------------------------------------------- /internal/app/module/web/invoker.go: -------------------------------------------------------------------------------- 1 | package web 2 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | "sort" 10 | "strings" 11 | ) 12 | 13 | type astParser struct { 14 | objectM map[string]*SpecType // parser struct文件 15 | modelArr []SpecType // 模型生成的描述文件 16 | readContent string // 读取原文件数据 17 | userOption UserOption 18 | tmplOption TmplOption 19 | } 20 | 21 | func AstParserBuild(userOption UserOption, tmplOption TmplOption) (*astParser, error) { 22 | a := &astParser{ 23 | userOption: userOption, 24 | tmplOption: tmplOption, 25 | objectM: make(map[string]*SpecType), 26 | } 27 | err := a.initReadContent() 28 | if err != nil { 29 | return nil, err 30 | } 31 | a.parserStruct() 32 | return a, nil 33 | } 34 | 35 | func (a *astParser) initReadContent() error { 36 | if a.userOption.ScaffoldDSLContent == "" { 37 | return fmt.Errorf("内容不能为空") 38 | } 39 | a.readContent = a.userOption.ScaffoldDSLContent 40 | return nil 41 | } 42 | 43 | func (a *astParser) parserStruct() error { 44 | fSet := token.NewFileSet() 45 | 46 | // strings.NewReader 47 | f, err := parser.ParseFile(fSet, "", strings.NewReader(a.readContent), parser.ParseComments) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | commentMap := ast.NewCommentMap(fSet, f, f.Comments) 53 | f.Comments = commentMap.Filter(f).Comments() 54 | 55 | scope := f.Scope 56 | if scope == nil { 57 | return errors.New("struct nil") 58 | } 59 | objects := scope.Objects 60 | structs := make([]*SpecType, 0) 61 | for structName, obj := range objects { 62 | st, err := a.parseObject(structName, obj) 63 | if err != nil { 64 | return err 65 | } 66 | structs = append(structs, st) 67 | } 68 | sort.Slice(structs, func(i, j int) bool { 69 | return structs[i].Name < structs[j].Name 70 | }) 71 | 72 | resp := make([]SpecType, 0) 73 | for _, item := range structs { 74 | resp = append(resp, *item) 75 | } 76 | a.modelArr = resp 77 | return nil 78 | } 79 | 80 | func (t *astParser) GetRenderInfos(descriptor Descriptor) (output []RenderInfo) { 81 | output = make([]RenderInfo, 0) 82 | modelNames := make([]string, 0) 83 | for _, content := range t.modelArr { 84 | modelNames = append(modelNames, content.Name) 85 | } 86 | 87 | // model table name, model table schema 88 | for _, content := range t.modelArr { 89 | output = append(output, RenderInfo{ 90 | Module: descriptor.Module, 91 | ModelNames: modelNames, 92 | ModelName: content.Name, 93 | Content: content.ToModelInfos(), 94 | Option: t.userOption, 95 | Descriptor: descriptor, 96 | TmplPath: t.tmplOption.RenderPath, 97 | }) 98 | } 99 | return 100 | } 101 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/parser_struct.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "go/ast" 5 | ) 6 | 7 | func (c *astParser) parseObject(structName string, obj *ast.Object) (*SpecType, error) { 8 | if data, ok := c.objectM[structName]; ok { 9 | return data, nil 10 | } 11 | var st SpecType 12 | st.Name = structName 13 | if obj.Decl == nil { 14 | c.objectM[structName] = &st 15 | return &st, nil 16 | } 17 | decl, ok := obj.Decl.(*ast.TypeSpec) 18 | if !ok { 19 | c.objectM[structName] = &st 20 | return &st, nil 21 | } 22 | if decl.Type == nil { 23 | c.objectM[structName] = &st 24 | return &st, nil 25 | } 26 | tp, ok := decl.Type.(*ast.StructType) 27 | if !ok { 28 | c.objectM[structName] = &st 29 | return &st, nil 30 | } 31 | fields := tp.Fields 32 | if fields == nil { 33 | c.objectM[structName] = &st 34 | return &st, nil 35 | } 36 | fieldList := fields.List 37 | members, err := c.parseFields(fieldList) 38 | if err != nil { 39 | return nil, err 40 | } 41 | st.Members = members 42 | c.objectM[structName] = &st 43 | return &st, nil 44 | } 45 | 46 | func (c *astParser) parseFields(fields []*ast.Field) ([]SpecMember, error) { 47 | members := make([]SpecMember, 0) 48 | for _, field := range fields { 49 | docs := parseCommentOrDoc(field.Doc) 50 | comments := parseCommentOrDoc(field.Comment) 51 | name := parseName(field.Names) 52 | tp, stringExpr, err := c.parseType(field.Type) 53 | if err != nil { 54 | return nil, err 55 | } 56 | tag := parseTag(field.Tag) 57 | isInline := name == "" 58 | if isInline { 59 | var err error 60 | name, err = c.getInlineName(tp) 61 | if err != nil { 62 | return nil, err 63 | } 64 | } 65 | members = append(members, SpecMember{ 66 | Name: name, 67 | Type: stringExpr, 68 | Expr: tp, 69 | Tag: tag, 70 | Comments: comments, 71 | Docs: docs, 72 | IsInline: isInline, 73 | }) 74 | 75 | } 76 | return members, nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/davecgh/go-spew/spew" 7 | ) 8 | 9 | func Test_astParser_parserStruct(t *testing.T) { 10 | ast, _ := AstParserBuild(UserOption{ 11 | ScaffoldDSLContent: "testdata/user/ego.go", 12 | }, TmplOption{}) 13 | if len(ast.modelArr) != 1 { 14 | t.Fatalf("got %d model arr, want 1", len(ast.modelArr)) 15 | } 16 | t.Log(ast.modelArr) 17 | } 18 | 19 | func Test_astParser_parserStructTag(t *testing.T) { 20 | ast, _ := AstParserBuild(UserOption{ 21 | ScaffoldDSLContent: "testdata/user/ego.go", 22 | }, TmplOption{}) 23 | if len(ast.modelArr) != 1 { 24 | t.Fatalf("got %d model arr, want 1", len(ast.modelArr)) 25 | } 26 | spew.Dump(ast.modelArr) 27 | } 28 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2022 Florian Schlachter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/doc.go: -------------------------------------------------------------------------------- 1 | // Package pongo2 is a Django-syntax like template-engine 2 | // 3 | // More info about pongo2: https://www.schlachter.tech/pongo2 4 | // 5 | // Complete documentation on the template language: 6 | // https://docs.djangoproject.com/en/dev/topics/templates/ 7 | // 8 | // Make sure to read README.md in the repository as well. 9 | // 10 | // A tiny example with template strings: 11 | // 12 | // 13 | // // Compile the template first (i. e. creating the AST) 14 | // tpl, err := pongo2.FromString("Hello {$ name|capfirst $}!") 15 | // if err != nil { 16 | // panic(err) 17 | // } 18 | // // Now you can render the template with the given 19 | // // pongo2.Context how often you want to. 20 | // out, err := tpl.Execute(pongo2.Context{"name": "fred"}) 21 | // if err != nil { 22 | // panic(err) 23 | // } 24 | // fmt.Println(out) // Output: Hello Fred! 25 | // 26 | package pongo2 27 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/error.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | // The Error type is being used to address an error during lexing, parsing or 10 | // execution. If you want to return an error object (for example in your own 11 | // tag or filter) fill this object with as much information as you have. 12 | // Make sure "Sender" is always given (if you're returning an error within 13 | // a filter, make Sender equals 'filter:yourfilter'; same goes for tags: 'tag:mytag'). 14 | // It's okay if you only fill in ErrorMsg if you don't have any other details at hand. 15 | type Error struct { 16 | Template *Template 17 | Filename string 18 | Line int 19 | Column int 20 | Token *Token 21 | Sender string 22 | OrigError error 23 | } 24 | 25 | func (e *Error) updateFromTokenIfNeeded(template *Template, t *Token) *Error { 26 | if e.Template == nil { 27 | e.Template = template 28 | } 29 | 30 | if e.Token == nil { 31 | e.Token = t 32 | if e.Line <= 0 { 33 | e.Line = t.Line 34 | e.Column = t.Col 35 | } 36 | } 37 | 38 | return e 39 | } 40 | 41 | // Returns a nice formatted error string. 42 | func (e *Error) Error() string { 43 | s := "[Error" 44 | if e.Sender != "" { 45 | s += " (where: " + e.Sender + ")" 46 | } 47 | if e.Filename != "" { 48 | s += " in " + e.Filename 49 | } 50 | if e.Line > 0 { 51 | s += fmt.Sprintf(" | Line %d Col %d", e.Line, e.Column) 52 | if e.Token != nil { 53 | s += fmt.Sprintf(" near '%s'", e.Token.Val) 54 | } 55 | } 56 | s += "] " 57 | s += e.OrigError.Error() 58 | return s 59 | } 60 | 61 | // RawLine returns the affected line from the original template, if available. 62 | func (e *Error) RawLine() (line string, available bool, outErr error) { 63 | if e.Line <= 0 || e.Filename == "" { 64 | return "", false, nil 65 | } 66 | 67 | filename := e.Filename 68 | if e.Template != nil { 69 | filename = e.Template.set.resolveFilename(e.Template, e.Filename) 70 | } 71 | file, err := os.Open(filename) 72 | if err != nil { 73 | return "", false, err 74 | } 75 | defer func() { 76 | err := file.Close() 77 | if err != nil && outErr == nil { 78 | outErr = err 79 | } 80 | }() 81 | 82 | scanner := bufio.NewScanner(file) 83 | l := 0 84 | for scanner.Scan() { 85 | l++ 86 | if l == e.Line { 87 | return scanner.Text(), true, nil 88 | } 89 | } 90 | return "", false, nil 91 | } 92 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/helpers.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | func max(a, b int) int { 4 | if a > b { 5 | return a 6 | } 7 | return b 8 | } 9 | 10 | func min(a, b int) int { 11 | if a < b { 12 | return a 13 | } 14 | return b 15 | } 16 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/nodes.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | // The root document 4 | type nodeDocument struct { 5 | Nodes []INode 6 | } 7 | 8 | func (doc *nodeDocument) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 9 | for _, n := range doc.Nodes { 10 | err := n.Execute(ctx, writer) 11 | if err != nil { 12 | return err 13 | } 14 | } 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/nodes_html.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type nodeHTML struct { 8 | token *Token 9 | trimLeft bool 10 | trimRight bool 11 | } 12 | 13 | func (n *nodeHTML) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 14 | res := n.token.Val 15 | if n.trimLeft { 16 | res = strings.TrimLeft(res, tokenSpaceChars) 17 | } 18 | if n.trimRight { 19 | res = strings.TrimRight(res, tokenSpaceChars) 20 | } 21 | writer.WriteString(res) 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/nodes_wrapper.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type NodeWrapper struct { 4 | Endtag string 5 | nodes []INode 6 | } 7 | 8 | func (wrapper *NodeWrapper) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 9 | for _, n := range wrapper.nodes { 10 | err := n.Execute(ctx, writer) 11 | if err != nil { 12 | return err 13 | } 14 | } 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/options.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | // Options allow you to change the behavior of template-engine. 4 | // You can change the options before calling the Execute method. 5 | type Options struct { 6 | // If this is set to true the first newline after a block is removed (block, not variable tag!). Defaults to false. 7 | TrimBlocks bool 8 | 9 | // If this is set to true leading spaces and tabs are stripped from the start of a line to a block. Defaults to false 10 | LStripBlocks bool 11 | } 12 | 13 | func newOptions() *Options { 14 | return &Options{ 15 | TrimBlocks: false, 16 | LStripBlocks: false, 17 | } 18 | } 19 | 20 | // Update updates this options from another options. 21 | func (opt *Options) Update(other *Options) *Options { 22 | opt.TrimBlocks = other.TrimBlocks 23 | opt.LStripBlocks = other.LStripBlocks 24 | 25 | return opt 26 | } 27 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/parser_document.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | // Doc = { ( Filter | Tag | HTML ) } 4 | func (p *Parser) parseDocElement() (INode, *Error) { 5 | t := p.Current() 6 | 7 | switch t.Typ { 8 | case TokenHTML: 9 | n := &nodeHTML{token: t} 10 | left := p.PeekTypeN(-1, TokenSymbol) 11 | right := p.PeekTypeN(1, TokenSymbol) 12 | n.trimLeft = left != nil && left.TrimWhitespaces 13 | n.trimRight = right != nil && right.TrimWhitespaces 14 | p.Consume() // consume HTML element 15 | return n, nil 16 | case TokenSymbol: 17 | switch t.Val { 18 | case "{$": 19 | // parse variable 20 | variable, err := p.parseVariableElement() 21 | if err != nil { 22 | return nil, err 23 | } 24 | return variable, nil 25 | case "{%": 26 | // parse tag 27 | tag, err := p.parseTagElement() 28 | if err != nil { 29 | return nil, err 30 | } 31 | return tag, nil 32 | } 33 | } 34 | return nil, p.Error("Unexpected token (only HTML/tags/filters in templates allowed)", t) 35 | } 36 | 37 | func (tpl *Template) parse() *Error { 38 | tpl.parser = newParser(tpl.name, tpl.tokens, tpl) 39 | doc, err := tpl.parser.parseDocument() 40 | if err != nil { 41 | return err 42 | } 43 | tpl.root = doc 44 | return nil 45 | } 46 | 47 | func (p *Parser) parseDocument() (*nodeDocument, *Error) { 48 | doc := &nodeDocument{} 49 | 50 | for p.Remaining() > 0 { 51 | node, err := p.parseDocElement() 52 | if err != nil { 53 | return nil, err 54 | } 55 | doc.Nodes = append(doc.Nodes, node) 56 | } 57 | 58 | return doc, nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/pongo2.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | // Version string 4 | const Version = "6.0.0" 5 | 6 | // Must panics, if a Template couldn't successfully parsed. This is how you 7 | // would use it: 8 | // var baseTemplate = pongo2.Must(pongo2.FromFile("templates/base.html")) 9 | func Must(tpl *Template, err error) *Template { 10 | if err != nil { 11 | panic(err) 12 | } 13 | return tpl 14 | } 15 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/pongo2_issues_test.go: -------------------------------------------------------------------------------- 1 | package pongo2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gotomicro/egoctl/internal/app/module/web/parser/pongo2" 7 | ) 8 | 9 | func TestIssue151(t *testing.T) { 10 | tpl, err := pongo2.FromString("{$ mydict.51232_3 $}{$ 12345_123$}{$ 995189baz $}") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | str, err := tpl.Execute(pongo2.Context{ 16 | "mydict": map[string]string{ 17 | "51232_3": "foo", 18 | }, 19 | "12345_123": "bar", 20 | "995189baz": "baz", 21 | }) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if str != "foobarbaz" { 27 | t.Fatalf("Expected output 'foobarbaz', but got '%s'.", str) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/pongo2_test.go: -------------------------------------------------------------------------------- 1 | package pongo2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gotomicro/egoctl/internal/app/module/web/parser/pongo2" 7 | . "gopkg.in/check.v1" 8 | ) 9 | 10 | // Hook up gocheck into the "go test" runner. 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type TestSuite struct{} 14 | 15 | var ( 16 | _ = Suite(&TestSuite{}) 17 | testSuite2 = pongo2.NewSet("test suite 2", pongo2.MustNewLocalFileSystemLoader("")) 18 | ) 19 | 20 | func parseTemplate(s string, c pongo2.Context) string { 21 | t, err := testSuite2.FromString(s) 22 | if err != nil { 23 | panic(err) 24 | } 25 | out, err := t.Execute(c) 26 | if err != nil { 27 | panic(err) 28 | } 29 | return out 30 | } 31 | 32 | func parseTemplateFn(s string, c pongo2.Context) func() { 33 | return func() { 34 | parseTemplate(s, c) 35 | } 36 | } 37 | 38 | func (s *TestSuite) TestMisc(c *C) { 39 | // Must 40 | // TODO: Add better error message (see issue #18) 41 | c.Check( 42 | func() { pongo2.Must(testSuite2.FromFile("template_tests/inheritance/base2.tpl")) }, 43 | PanicMatches, 44 | `\[Error \(where: fromfile\) in .*template_tests[/\\]inheritance[/\\]doesnotexist.tpl | Line 1 Col 12 near 'doesnotexist.tpl'\] open .*template_tests[/\\]inheritance[/\\]doesnotexist.tpl: no such file or directory`, 45 | ) 46 | 47 | // Context 48 | c.Check(parseTemplateFn("", pongo2.Context{"'illegal": nil}), PanicMatches, ".*not a valid identifier.*") 49 | 50 | // Registers 51 | c.Check(pongo2.RegisterFilter("escape", nil).Error(), Matches, ".*is already registered") 52 | c.Check(pongo2.RegisterTag("for", nil).Error(), Matches, ".*is already registered") 53 | 54 | // ApplyFilter 55 | v, err := pongo2.ApplyFilter("title", pongo2.AsValue("this is a title"), nil) 56 | if err != nil { 57 | c.Fatal(err) 58 | } 59 | c.Check(v.String(), Equals, "This Is A Title") 60 | c.Check(func() { 61 | _, err := pongo2.ApplyFilter("doesnotexist", nil, nil) 62 | if err != nil { 63 | panic(err) 64 | } 65 | }, PanicMatches, `\[Error \(where: applyfilter\)\] filter with name 'doesnotexist' not found`) 66 | } 67 | 68 | func (s *TestSuite) TestImplicitExecCtx(c *C) { 69 | tpl, err := pongo2.FromString("{$ ImplicitExec $}") 70 | if err != nil { 71 | c.Fatalf("Error in FromString: %v", err) 72 | } 73 | 74 | val := "a stringy thing" 75 | 76 | res, err := tpl.Execute(pongo2.Context{ 77 | "Value": val, 78 | "ImplicitExec": func(ctx *pongo2.ExecutionContext) string { 79 | return ctx.Public["Value"].(string) 80 | }, 81 | }) 82 | if err != nil { 83 | c.Fatalf("Error executing template: %v", err) 84 | } 85 | 86 | c.Check(res, Equals, val) 87 | 88 | // The implicit ctx should not be persisted from call-to-call 89 | res, err = tpl.Execute(pongo2.Context{ 90 | "ImplicitExec": func() string { 91 | return val 92 | }, 93 | }) 94 | 95 | if err != nil { 96 | c.Fatalf("Error executing template: %v", err) 97 | } 98 | 99 | c.Check(res, Equals, val) 100 | } 101 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_autoescape.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type tagAutoescapeNode struct { 4 | wrapper *NodeWrapper 5 | autoescape bool 6 | } 7 | 8 | func (node *tagAutoescapeNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 9 | old := ctx.Autoescape 10 | ctx.Autoescape = node.autoescape 11 | 12 | err := node.wrapper.Execute(ctx, writer) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | ctx.Autoescape = old 18 | 19 | return nil 20 | } 21 | 22 | func tagAutoescapeParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 23 | autoescapeNode := &tagAutoescapeNode{} 24 | 25 | wrapper, _, err := doc.WrapUntilTag("endautoescape") 26 | if err != nil { 27 | return nil, err 28 | } 29 | autoescapeNode.wrapper = wrapper 30 | 31 | modeToken := arguments.MatchType(TokenIdentifier) 32 | if modeToken == nil { 33 | return nil, arguments.Error("A mode is required for autoescape-tag.", nil) 34 | } 35 | if modeToken.Val == "on" { 36 | autoescapeNode.autoescape = true 37 | } else if modeToken.Val == "off" { 38 | autoescapeNode.autoescape = false 39 | } else { 40 | return nil, arguments.Error("Only 'on' or 'off' is valid as an autoescape-mode.", nil) 41 | } 42 | 43 | if arguments.Remaining() > 0 { 44 | return nil, arguments.Error("Malformed autoescape-tag arguments.", nil) 45 | } 46 | 47 | return autoescapeNode, nil 48 | } 49 | 50 | func init() { 51 | RegisterTag("autoescape", tagAutoescapeParser) 52 | } 53 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_comment.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type tagCommentNode struct{} 4 | 5 | func (node *tagCommentNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 6 | return nil 7 | } 8 | 9 | func tagCommentParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 10 | commentNode := &tagCommentNode{} 11 | 12 | // TODO: Process the endtag's arguments (see django 'comment'-tag documentation) 13 | err := doc.SkipUntilTag("endcomment") 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | if arguments.Count() != 0 { 19 | return nil, arguments.Error("Tag 'comment' does not take any argument.", nil) 20 | } 21 | 22 | return commentNode, nil 23 | } 24 | 25 | func init() { 26 | RegisterTag("comment", tagCommentParser) 27 | } 28 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_cycle.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type tagCycleValue struct { 4 | node *tagCycleNode 5 | value *Value 6 | } 7 | 8 | type tagCycleNode struct { 9 | position *Token 10 | args []IEvaluator 11 | idx int 12 | asName string 13 | silent bool 14 | } 15 | 16 | func (cv *tagCycleValue) String() string { 17 | return cv.value.String() 18 | } 19 | 20 | func (node *tagCycleNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 21 | item := node.args[node.idx%len(node.args)] 22 | node.idx++ 23 | 24 | val, err := item.Evaluate(ctx) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if t, ok := val.Interface().(*tagCycleValue); ok { 30 | // {% cycle "test1" "test2" 31 | // {% cycle cycleitem %} 32 | 33 | // Update the cycle value with next value 34 | item := t.node.args[t.node.idx%len(t.node.args)] 35 | t.node.idx++ 36 | 37 | val, err := item.Evaluate(ctx) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | t.value = val 43 | 44 | if !t.node.silent { 45 | writer.WriteString(val.String()) 46 | } 47 | } else { 48 | // Regular call 49 | 50 | cycleValue := &tagCycleValue{ 51 | node: node, 52 | value: val, 53 | } 54 | 55 | if node.asName != "" { 56 | ctx.Private[node.asName] = cycleValue 57 | } 58 | if !node.silent { 59 | writer.WriteString(val.String()) 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | // HINT: We're not supporting the old comma-separated list of expressions argument-style 67 | func tagCycleParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 68 | cycleNode := &tagCycleNode{ 69 | position: start, 70 | } 71 | 72 | for arguments.Remaining() > 0 { 73 | node, err := arguments.ParseExpression() 74 | if err != nil { 75 | return nil, err 76 | } 77 | cycleNode.args = append(cycleNode.args, node) 78 | 79 | if arguments.MatchOne(TokenKeyword, "as") != nil { 80 | // as 81 | 82 | nameToken := arguments.MatchType(TokenIdentifier) 83 | if nameToken == nil { 84 | return nil, arguments.Error("Name (identifier) expected after 'as'.", nil) 85 | } 86 | cycleNode.asName = nameToken.Val 87 | 88 | if arguments.MatchOne(TokenIdentifier, "silent") != nil { 89 | cycleNode.silent = true 90 | } 91 | 92 | // Now we're finished 93 | break 94 | } 95 | } 96 | 97 | if arguments.Remaining() > 0 { 98 | return nil, arguments.Error("Malformed cycle-tag.", nil) 99 | } 100 | 101 | return cycleNode, nil 102 | } 103 | 104 | func init() { 105 | RegisterTag("cycle", tagCycleParser) 106 | } 107 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_extends.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type tagExtendsNode struct { 4 | filename string 5 | } 6 | 7 | func (node *tagExtendsNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 8 | return nil 9 | } 10 | 11 | func tagExtendsParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 12 | extendsNode := &tagExtendsNode{} 13 | 14 | if doc.template.level > 1 { 15 | return nil, arguments.Error("The 'extends' tag can only defined on root level.", start) 16 | } 17 | 18 | if doc.template.parent != nil { 19 | // Already one parent 20 | return nil, arguments.Error("This template has already one parent.", start) 21 | } 22 | 23 | if filenameToken := arguments.MatchType(TokenString); filenameToken != nil { 24 | // prepared, static template 25 | 26 | // Get parent's filename 27 | parentFilename := doc.template.set.resolveFilename(doc.template, filenameToken.Val) 28 | 29 | // Parse the parent 30 | parentTemplate, err := doc.template.set.FromFile(parentFilename) 31 | if err != nil { 32 | return nil, err.(*Error) 33 | } 34 | 35 | // Keep track of things 36 | parentTemplate.child = doc.template 37 | doc.template.parent = parentTemplate 38 | extendsNode.filename = parentFilename 39 | } else { 40 | return nil, arguments.Error("Tag 'extends' requires a template filename as string.", nil) 41 | } 42 | 43 | if arguments.Remaining() > 0 { 44 | return nil, arguments.Error("Tag 'extends' does only take 1 argument.", nil) 45 | } 46 | 47 | return extendsNode, nil 48 | } 49 | 50 | func init() { 51 | RegisterTag("extends", tagExtendsParser) 52 | } 53 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_filter.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type nodeFilterCall struct { 8 | name string 9 | paramExpr IEvaluator 10 | } 11 | 12 | type tagFilterNode struct { 13 | position *Token 14 | bodyWrapper *NodeWrapper 15 | filterChain []*nodeFilterCall 16 | } 17 | 18 | func (node *tagFilterNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 19 | temp := bytes.NewBuffer(make([]byte, 0, 1024)) // 1 KiB size 20 | 21 | err := node.bodyWrapper.Execute(ctx, temp) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | value := AsValue(temp.String()) 27 | 28 | for _, call := range node.filterChain { 29 | var param *Value 30 | if call.paramExpr != nil { 31 | param, err = call.paramExpr.Evaluate(ctx) 32 | if err != nil { 33 | return err 34 | } 35 | } else { 36 | param = AsValue(nil) 37 | } 38 | value, err = ApplyFilter(call.name, value, param) 39 | if err != nil { 40 | return ctx.Error(err.Error(), node.position) 41 | } 42 | } 43 | 44 | writer.WriteString(value.String()) 45 | 46 | return nil 47 | } 48 | 49 | func tagFilterParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 50 | filterNode := &tagFilterNode{ 51 | position: start, 52 | } 53 | 54 | wrapper, _, err := doc.WrapUntilTag("endfilter") 55 | if err != nil { 56 | return nil, err 57 | } 58 | filterNode.bodyWrapper = wrapper 59 | 60 | for arguments.Remaining() > 0 { 61 | filterCall := &nodeFilterCall{} 62 | 63 | nameToken := arguments.MatchType(TokenIdentifier) 64 | if nameToken == nil { 65 | return nil, arguments.Error("Expected a filter name (identifier).", nil) 66 | } 67 | filterCall.name = nameToken.Val 68 | 69 | if arguments.MatchOne(TokenSymbol, ":") != nil { 70 | // Filter parameter 71 | // NOTICE: we can't use ParseExpression() here, because it would parse the next filter "|..." as well in the argument list 72 | expr, err := arguments.parseVariableOrLiteral() 73 | if err != nil { 74 | return nil, err 75 | } 76 | filterCall.paramExpr = expr 77 | } 78 | 79 | filterNode.filterChain = append(filterNode.filterChain, filterCall) 80 | 81 | if arguments.MatchOne(TokenSymbol, "|") == nil { 82 | break 83 | } 84 | } 85 | 86 | if arguments.Remaining() > 0 { 87 | return nil, arguments.Error("Malformed filter-tag arguments.", nil) 88 | } 89 | 90 | return filterNode, nil 91 | } 92 | 93 | func init() { 94 | RegisterTag("filter", tagFilterParser) 95 | } 96 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_firstof.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type tagFirstofNode struct { 4 | position *Token 5 | args []IEvaluator 6 | } 7 | 8 | func (node *tagFirstofNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 9 | for _, arg := range node.args { 10 | val, err := arg.Evaluate(ctx) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | if val.IsTrue() { 16 | if ctx.Autoescape && !arg.FilterApplied("safe") { 17 | val, err = ApplyFilter("escape", val, nil) 18 | if err != nil { 19 | return err 20 | } 21 | } 22 | 23 | writer.WriteString(val.String()) 24 | return nil 25 | } 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func tagFirstofParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 32 | firstofNode := &tagFirstofNode{ 33 | position: start, 34 | } 35 | 36 | for arguments.Remaining() > 0 { 37 | node, err := arguments.ParseExpression() 38 | if err != nil { 39 | return nil, err 40 | } 41 | firstofNode.args = append(firstofNode.args, node) 42 | } 43 | 44 | return firstofNode, nil 45 | } 46 | 47 | func init() { 48 | RegisterTag("firstof", tagFirstofParser) 49 | } 50 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_if.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type tagIfNode struct { 4 | conditions []IEvaluator 5 | wrappers []*NodeWrapper 6 | } 7 | 8 | func (node *tagIfNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 9 | for i, condition := range node.conditions { 10 | result, err := condition.Evaluate(ctx) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | if result.IsTrue() { 16 | return node.wrappers[i].Execute(ctx, writer) 17 | } 18 | // Last condition? 19 | if len(node.conditions) == i+1 && len(node.wrappers) > i+1 { 20 | return node.wrappers[i+1].Execute(ctx, writer) 21 | } 22 | } 23 | return nil 24 | } 25 | 26 | func tagIfParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 27 | ifNode := &tagIfNode{} 28 | 29 | // Parse first and main IF condition 30 | condition, err := arguments.ParseExpression() 31 | if err != nil { 32 | return nil, err 33 | } 34 | ifNode.conditions = append(ifNode.conditions, condition) 35 | 36 | if arguments.Remaining() > 0 { 37 | return nil, arguments.Error("If-condition is malformed.", nil) 38 | } 39 | 40 | // Check the rest 41 | for { 42 | wrapper, tagArgs, err := doc.WrapUntilTag("elif", "else", "endif") 43 | if err != nil { 44 | return nil, err 45 | } 46 | ifNode.wrappers = append(ifNode.wrappers, wrapper) 47 | 48 | if wrapper.Endtag == "elif" { 49 | // elif can take a condition 50 | condition, err = tagArgs.ParseExpression() 51 | if err != nil { 52 | return nil, err 53 | } 54 | ifNode.conditions = append(ifNode.conditions, condition) 55 | 56 | if tagArgs.Remaining() > 0 { 57 | return nil, tagArgs.Error("Elif-condition is malformed.", nil) 58 | } 59 | } else { 60 | if tagArgs.Count() > 0 { 61 | // else/endif can't take any conditions 62 | return nil, tagArgs.Error("Arguments not allowed here.", nil) 63 | } 64 | } 65 | 66 | if wrapper.Endtag == "endif" { 67 | break 68 | } 69 | } 70 | 71 | return ifNode, nil 72 | } 73 | 74 | func init() { 75 | RegisterTag("if", tagIfParser) 76 | } 77 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_ifequal.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type tagIfEqualNode struct { 4 | var1, var2 IEvaluator 5 | thenWrapper *NodeWrapper 6 | elseWrapper *NodeWrapper 7 | } 8 | 9 | func (node *tagIfEqualNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 10 | r1, err := node.var1.Evaluate(ctx) 11 | if err != nil { 12 | return err 13 | } 14 | r2, err := node.var2.Evaluate(ctx) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | result := r1.EqualValueTo(r2) 20 | 21 | if result { 22 | return node.thenWrapper.Execute(ctx, writer) 23 | } 24 | if node.elseWrapper != nil { 25 | return node.elseWrapper.Execute(ctx, writer) 26 | } 27 | return nil 28 | } 29 | 30 | func tagIfEqualParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 31 | ifequalNode := &tagIfEqualNode{} 32 | 33 | // Parse two expressions 34 | var1, err := arguments.ParseExpression() 35 | if err != nil { 36 | return nil, err 37 | } 38 | var2, err := arguments.ParseExpression() 39 | if err != nil { 40 | return nil, err 41 | } 42 | ifequalNode.var1 = var1 43 | ifequalNode.var2 = var2 44 | 45 | if arguments.Remaining() > 0 { 46 | return nil, arguments.Error("ifequal only takes 2 arguments.", nil) 47 | } 48 | 49 | // Wrap then/else-blocks 50 | wrapper, endargs, err := doc.WrapUntilTag("else", "endifequal") 51 | if err != nil { 52 | return nil, err 53 | } 54 | ifequalNode.thenWrapper = wrapper 55 | 56 | if endargs.Count() > 0 { 57 | return nil, endargs.Error("Arguments not allowed here.", nil) 58 | } 59 | 60 | if wrapper.Endtag == "else" { 61 | // if there's an else in the if-statement, we need the else-Block as well 62 | wrapper, endargs, err = doc.WrapUntilTag("endifequal") 63 | if err != nil { 64 | return nil, err 65 | } 66 | ifequalNode.elseWrapper = wrapper 67 | 68 | if endargs.Count() > 0 { 69 | return nil, endargs.Error("Arguments not allowed here.", nil) 70 | } 71 | } 72 | 73 | return ifequalNode, nil 74 | } 75 | 76 | func init() { 77 | RegisterTag("ifequal", tagIfEqualParser) 78 | } 79 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_ifnotequal.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type tagIfNotEqualNode struct { 4 | var1, var2 IEvaluator 5 | thenWrapper *NodeWrapper 6 | elseWrapper *NodeWrapper 7 | } 8 | 9 | func (node *tagIfNotEqualNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 10 | r1, err := node.var1.Evaluate(ctx) 11 | if err != nil { 12 | return err 13 | } 14 | r2, err := node.var2.Evaluate(ctx) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | result := !r1.EqualValueTo(r2) 20 | 21 | if result { 22 | return node.thenWrapper.Execute(ctx, writer) 23 | } 24 | if node.elseWrapper != nil { 25 | return node.elseWrapper.Execute(ctx, writer) 26 | } 27 | return nil 28 | } 29 | 30 | func tagIfNotEqualParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 31 | ifnotequalNode := &tagIfNotEqualNode{} 32 | 33 | // Parse two expressions 34 | var1, err := arguments.ParseExpression() 35 | if err != nil { 36 | return nil, err 37 | } 38 | var2, err := arguments.ParseExpression() 39 | if err != nil { 40 | return nil, err 41 | } 42 | ifnotequalNode.var1 = var1 43 | ifnotequalNode.var2 = var2 44 | 45 | if arguments.Remaining() > 0 { 46 | return nil, arguments.Error("ifequal only takes 2 arguments.", nil) 47 | } 48 | 49 | // Wrap then/else-blocks 50 | wrapper, endargs, err := doc.WrapUntilTag("else", "endifnotequal") 51 | if err != nil { 52 | return nil, err 53 | } 54 | ifnotequalNode.thenWrapper = wrapper 55 | 56 | if endargs.Count() > 0 { 57 | return nil, endargs.Error("Arguments not allowed here.", nil) 58 | } 59 | 60 | if wrapper.Endtag == "else" { 61 | // if there's an else in the if-statement, we need the else-Block as well 62 | wrapper, endargs, err = doc.WrapUntilTag("endifnotequal") 63 | if err != nil { 64 | return nil, err 65 | } 66 | ifnotequalNode.elseWrapper = wrapper 67 | 68 | if endargs.Count() > 0 { 69 | return nil, endargs.Error("Arguments not allowed here.", nil) 70 | } 71 | } 72 | 73 | return ifnotequalNode, nil 74 | } 75 | 76 | func init() { 77 | RegisterTag("ifnotequal", tagIfNotEqualParser) 78 | } 79 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_import.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type tagImportNode struct { 8 | position *Token 9 | filename string 10 | macros map[string]*tagMacroNode // alias/name -> macro instance 11 | } 12 | 13 | func (node *tagImportNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 14 | for name, macro := range node.macros { 15 | func(name string, macro *tagMacroNode) { 16 | ctx.Private[name] = func(args ...*Value) (*Value, error) { 17 | return macro.call(ctx, args...) 18 | } 19 | }(name, macro) 20 | } 21 | return nil 22 | } 23 | 24 | func tagImportParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 25 | importNode := &tagImportNode{ 26 | position: start, 27 | macros: make(map[string]*tagMacroNode), 28 | } 29 | 30 | filenameToken := arguments.MatchType(TokenString) 31 | if filenameToken == nil { 32 | return nil, arguments.Error("Import-tag needs a filename as string.", nil) 33 | } 34 | 35 | importNode.filename = doc.template.set.resolveFilename(doc.template, filenameToken.Val) 36 | 37 | if arguments.Remaining() == 0 { 38 | return nil, arguments.Error("You must at least specify one macro to import.", nil) 39 | } 40 | 41 | // Compile the given template 42 | tpl, err := doc.template.set.FromFile(importNode.filename) 43 | if err != nil { 44 | return nil, err.(*Error).updateFromTokenIfNeeded(doc.template, start) 45 | } 46 | 47 | for arguments.Remaining() > 0 { 48 | macroNameToken := arguments.MatchType(TokenIdentifier) 49 | if macroNameToken == nil { 50 | return nil, arguments.Error("Expected macro name (identifier).", nil) 51 | } 52 | 53 | asName := macroNameToken.Val 54 | if arguments.Match(TokenKeyword, "as") != nil { 55 | aliasToken := arguments.MatchType(TokenIdentifier) 56 | if aliasToken == nil { 57 | return nil, arguments.Error("Expected macro alias name (identifier).", nil) 58 | } 59 | asName = aliasToken.Val 60 | } 61 | 62 | macroInstance, has := tpl.exportedMacros[macroNameToken.Val] 63 | if !has { 64 | return nil, arguments.Error(fmt.Sprintf("Macro '%s' not found (or not exported) in '%s'.", macroNameToken.Val, 65 | importNode.filename), macroNameToken) 66 | } 67 | 68 | importNode.macros[asName] = macroInstance 69 | 70 | if arguments.Remaining() == 0 { 71 | break 72 | } 73 | 74 | if arguments.Match(TokenSymbol, ",") == nil { 75 | return nil, arguments.Error("Expected ','.", nil) 76 | } 77 | } 78 | 79 | return importNode, nil 80 | } 81 | 82 | func init() { 83 | RegisterTag("import", tagImportParser) 84 | } 85 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_now.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type tagNowNode struct { 8 | position *Token 9 | format string 10 | fake bool 11 | } 12 | 13 | func (node *tagNowNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 14 | var t time.Time 15 | if node.fake { 16 | t = time.Date(2014, time.February, 05, 18, 31, 45, 00, time.UTC) 17 | } else { 18 | t = time.Now() 19 | } 20 | 21 | writer.WriteString(t.Format(node.format)) 22 | 23 | return nil 24 | } 25 | 26 | func tagNowParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 27 | nowNode := &tagNowNode{ 28 | position: start, 29 | } 30 | 31 | formatToken := arguments.MatchType(TokenString) 32 | if formatToken == nil { 33 | return nil, arguments.Error("Expected a format string.", nil) 34 | } 35 | nowNode.format = formatToken.Val 36 | 37 | if arguments.MatchOne(TokenIdentifier, "fake") != nil { 38 | nowNode.fake = true 39 | } 40 | 41 | if arguments.Remaining() > 0 { 42 | return nil, arguments.Error("Malformed now-tag arguments.", nil) 43 | } 44 | 45 | return nowNode, nil 46 | } 47 | 48 | func init() { 49 | RegisterTag("now", tagNowParser) 50 | } 51 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_set.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type tagSetNode struct { 4 | name string 5 | expression IEvaluator 6 | } 7 | 8 | func (node *tagSetNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 9 | // Evaluate expression 10 | value, err := node.expression.Evaluate(ctx) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | ctx.Private[node.name] = value 16 | return nil 17 | } 18 | 19 | func tagSetParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 20 | node := &tagSetNode{} 21 | 22 | // Parse variable name 23 | typeToken := arguments.MatchType(TokenIdentifier) 24 | if typeToken == nil { 25 | return nil, arguments.Error("Expected an identifier.", nil) 26 | } 27 | node.name = typeToken.Val 28 | 29 | if arguments.Match(TokenSymbol, "=") == nil { 30 | return nil, arguments.Error("Expected '='.", nil) 31 | } 32 | 33 | // Variable expression 34 | keyExpression, err := arguments.ParseExpression() 35 | if err != nil { 36 | return nil, err 37 | } 38 | node.expression = keyExpression 39 | 40 | // Remaining arguments 41 | if arguments.Remaining() > 0 { 42 | return nil, arguments.Error("Malformed 'set'-tag arguments.", nil) 43 | } 44 | 45 | return node, nil 46 | } 47 | 48 | func init() { 49 | RegisterTag("set", tagSetParser) 50 | } 51 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_spaceless.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | ) 7 | 8 | type tagSpacelessNode struct { 9 | wrapper *NodeWrapper 10 | } 11 | 12 | var tagSpacelessRegexp = regexp.MustCompile(`(?U:(<.*>))([\t\n\v\f\r ]+)(?U:(<.*>))`) 13 | 14 | func (node *tagSpacelessNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 15 | b := bytes.NewBuffer(make([]byte, 0, 1024)) // 1 KiB 16 | 17 | err := node.wrapper.Execute(ctx, b) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | s := b.String() 23 | // Repeat this recursively 24 | changed := true 25 | for changed { 26 | s2 := tagSpacelessRegexp.ReplaceAllString(s, "$1$3") 27 | changed = s != s2 28 | s = s2 29 | } 30 | 31 | writer.WriteString(s) 32 | 33 | return nil 34 | } 35 | 36 | func tagSpacelessParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 37 | spacelessNode := &tagSpacelessNode{} 38 | 39 | wrapper, _, err := doc.WrapUntilTag("endspaceless") 40 | if err != nil { 41 | return nil, err 42 | } 43 | spacelessNode.wrapper = wrapper 44 | 45 | if arguments.Remaining() > 0 { 46 | return nil, arguments.Error("Malformed spaceless-tag arguments.", nil) 47 | } 48 | 49 | return spacelessNode, nil 50 | } 51 | 52 | func init() { 53 | RegisterTag("spaceless", tagSpacelessParser) 54 | } 55 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_ssi.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | import ( 4 | "io/ioutil" 5 | ) 6 | 7 | type tagSSINode struct { 8 | filename string 9 | content string 10 | template *Template 11 | } 12 | 13 | func (node *tagSSINode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 14 | if node.template != nil { 15 | // Execute the template within the current context 16 | includeCtx := make(Context) 17 | includeCtx.Update(ctx.Public) 18 | includeCtx.Update(ctx.Private) 19 | 20 | err := node.template.execute(includeCtx, writer) 21 | if err != nil { 22 | return err.(*Error) 23 | } 24 | } else { 25 | // Just print out the content 26 | writer.WriteString(node.content) 27 | } 28 | return nil 29 | } 30 | 31 | func tagSSIParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 32 | SSINode := &tagSSINode{} 33 | 34 | if fileToken := arguments.MatchType(TokenString); fileToken != nil { 35 | SSINode.filename = fileToken.Val 36 | 37 | if arguments.Match(TokenIdentifier, "parsed") != nil { 38 | // parsed 39 | temporaryTpl, err := doc.template.set.FromFile(doc.template.set.resolveFilename(doc.template, fileToken.Val)) 40 | if err != nil { 41 | return nil, err.(*Error).updateFromTokenIfNeeded(doc.template, fileToken) 42 | } 43 | SSINode.template = temporaryTpl 44 | } else { 45 | // plaintext 46 | buf, err := ioutil.ReadFile(doc.template.set.resolveFilename(doc.template, fileToken.Val)) 47 | if err != nil { 48 | return nil, (&Error{ 49 | Sender: "tag:ssi", 50 | OrigError: err, 51 | }).updateFromTokenIfNeeded(doc.template, fileToken) 52 | } 53 | SSINode.content = string(buf) 54 | } 55 | } else { 56 | return nil, arguments.Error("First argument must be a string.", nil) 57 | } 58 | 59 | if arguments.Remaining() > 0 { 60 | return nil, arguments.Error("Malformed SSI-tag argument.", nil) 61 | } 62 | 63 | return SSINode, nil 64 | } 65 | 66 | func init() { 67 | RegisterTag("ssi", tagSSIParser) 68 | } 69 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_templatetag.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type tagTemplateTagNode struct { 4 | content string 5 | } 6 | 7 | var templateTagMapping = map[string]string{ 8 | "openblock": "{%", 9 | "closeblock": "%}", 10 | "openvariable": "{$", 11 | "closevariable": "$}", 12 | "openbrace": "{", 13 | "closebrace": "}", 14 | "opencomment": "{#", 15 | "closecomment": "#}", 16 | } 17 | 18 | func (node *tagTemplateTagNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 19 | writer.WriteString(node.content) 20 | return nil 21 | } 22 | 23 | func tagTemplateTagParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 24 | ttNode := &tagTemplateTagNode{} 25 | 26 | if argToken := arguments.MatchType(TokenIdentifier); argToken != nil { 27 | output, found := templateTagMapping[argToken.Val] 28 | if !found { 29 | return nil, arguments.Error("Argument not found", argToken) 30 | } 31 | ttNode.content = output 32 | } else { 33 | return nil, arguments.Error("Identifier expected.", nil) 34 | } 35 | 36 | if arguments.Remaining() > 0 { 37 | return nil, arguments.Error("Malformed templatetag-tag argument.", nil) 38 | } 39 | 40 | return ttNode, nil 41 | } 42 | 43 | func init() { 44 | RegisterTag("templatetag", tagTemplateTagParser) 45 | } 46 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_widthratio.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | type tagWidthratioNode struct { 9 | position *Token 10 | current, max IEvaluator 11 | width IEvaluator 12 | ctxName string 13 | } 14 | 15 | func (node *tagWidthratioNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 16 | current, err := node.current.Evaluate(ctx) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | max, err := node.max.Evaluate(ctx) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | width, err := node.width.Evaluate(ctx) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | value := int(math.Ceil(current.Float()/max.Float()*width.Float() + 0.5)) 32 | 33 | if node.ctxName == "" { 34 | writer.WriteString(fmt.Sprintf("%d", value)) 35 | } else { 36 | ctx.Private[node.ctxName] = value 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func tagWidthratioParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 43 | widthratioNode := &tagWidthratioNode{ 44 | position: start, 45 | } 46 | 47 | current, err := arguments.ParseExpression() 48 | if err != nil { 49 | return nil, err 50 | } 51 | widthratioNode.current = current 52 | 53 | max, err := arguments.ParseExpression() 54 | if err != nil { 55 | return nil, err 56 | } 57 | widthratioNode.max = max 58 | 59 | width, err := arguments.ParseExpression() 60 | if err != nil { 61 | return nil, err 62 | } 63 | widthratioNode.width = width 64 | 65 | if arguments.MatchOne(TokenKeyword, "as") != nil { 66 | // Name follows 67 | nameToken := arguments.MatchType(TokenIdentifier) 68 | if nameToken == nil { 69 | return nil, arguments.Error("Expected name (identifier).", nil) 70 | } 71 | widthratioNode.ctxName = nameToken.Val 72 | } 73 | 74 | if arguments.Remaining() > 0 { 75 | return nil, arguments.Error("Malformed widthratio-tag arguments.", nil) 76 | } 77 | 78 | return widthratioNode, nil 79 | } 80 | 81 | func init() { 82 | RegisterTag("widthratio", tagWidthratioParser) 83 | } 84 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2/tags_with.go: -------------------------------------------------------------------------------- 1 | package pongo2 2 | 3 | type tagWithNode struct { 4 | withPairs map[string]IEvaluator 5 | wrapper *NodeWrapper 6 | } 7 | 8 | func (node *tagWithNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error { 9 | //new context for block 10 | withctx := NewChildExecutionContext(ctx) 11 | 12 | // Put all custom with-pairs into the context 13 | for key, value := range node.withPairs { 14 | val, err := value.Evaluate(ctx) 15 | if err != nil { 16 | return err 17 | } 18 | withctx.Private[key] = val 19 | } 20 | 21 | return node.wrapper.Execute(withctx, writer) 22 | } 23 | 24 | func tagWithParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) { 25 | withNode := &tagWithNode{ 26 | withPairs: make(map[string]IEvaluator), 27 | } 28 | 29 | if arguments.Count() == 0 { 30 | return nil, arguments.Error("Tag 'with' requires at least one argument.", nil) 31 | } 32 | 33 | wrapper, endargs, err := doc.WrapUntilTag("endwith") 34 | if err != nil { 35 | return nil, err 36 | } 37 | withNode.wrapper = wrapper 38 | 39 | if endargs.Count() > 0 { 40 | return nil, endargs.Error("Arguments not allowed here.", nil) 41 | } 42 | 43 | // Scan through all arguments to see which style the user uses (old or new style). 44 | // If we find any "as" keyword we will enforce old style; otherwise we will use new style. 45 | oldStyle := false // by default we're using the new_style 46 | for i := 0; i < arguments.Count(); i++ { 47 | if arguments.PeekN(i, TokenKeyword, "as") != nil { 48 | oldStyle = true 49 | break 50 | } 51 | } 52 | 53 | for arguments.Remaining() > 0 { 54 | if oldStyle { 55 | valueExpr, err := arguments.ParseExpression() 56 | if err != nil { 57 | return nil, err 58 | } 59 | if arguments.Match(TokenKeyword, "as") == nil { 60 | return nil, arguments.Error("Expected 'as' keyword.", nil) 61 | } 62 | keyToken := arguments.MatchType(TokenIdentifier) 63 | if keyToken == nil { 64 | return nil, arguments.Error("Expected an identifier", nil) 65 | } 66 | withNode.withPairs[keyToken.Val] = valueExpr 67 | } else { 68 | keyToken := arguments.MatchType(TokenIdentifier) 69 | if keyToken == nil { 70 | return nil, arguments.Error("Expected an identifier", nil) 71 | } 72 | if arguments.Match(TokenSymbol, "=") == nil { 73 | return nil, arguments.Error("Expected '='.", nil) 74 | } 75 | valueExpr, err := arguments.ParseExpression() 76 | if err != nil { 77 | return nil, err 78 | } 79 | withNode.withPairs[keyToken.Val] = valueExpr 80 | } 81 | } 82 | 83 | return withNode, nil 84 | } 85 | 86 | func init() { 87 | RegisterTag("with", tagWithParser) 88 | } 89 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/pongo2render/render.go: -------------------------------------------------------------------------------- 1 | package pongo2render 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | 7 | "github.com/gotomicro/egoctl/internal/app/module/web/parser/pongo2" 8 | ) 9 | 10 | // var render = pongo2render.NewRender("./templates") 11 | // 12 | // http.HandleFunc("/m", func(w http.ResponseWriter, req *http.Request) { 13 | // render.HTML(w, 200, "index.html", pongo2.Context{"aa": "eeeeeee"}) 14 | // }) 15 | // http.ListenAndServe(":9005", nil) 16 | 17 | // -------------------------------------------------------------------------------- 18 | var htmlContentType = []string{"text/html; charset=utf-8"} 19 | 20 | type Render struct { 21 | TemplateDir string 22 | Cache bool 23 | } 24 | 25 | func NewRender(templateDir string) *Render { 26 | var r = &Render{} 27 | r.TemplateDir = templateDir 28 | return r 29 | } 30 | 31 | func (this *Render) Template(name string) *Template { 32 | var template *pongo2.Template 33 | var filename string 34 | if len(this.TemplateDir) > 0 { 35 | filename = path.Join(this.TemplateDir, name) 36 | } else { 37 | filename = name 38 | } 39 | 40 | if this.Cache { 41 | template = pongo2.Must(pongo2.DefaultSet.FromCache(filename)) 42 | } else { 43 | template = pongo2.Must(pongo2.DefaultSet.FromFile(filename)) 44 | } 45 | 46 | if template == nil { 47 | panic("template " + name + " not exists") 48 | } 49 | 50 | var r = &Template{} 51 | r.template = template 52 | return r 53 | } 54 | 55 | func (this *Render) TemplateFromString(tpl string) *Template { 56 | var template = pongo2.Must(pongo2.DefaultSet.FromString(tpl)) 57 | var r = &Template{} 58 | r.template = template 59 | return r 60 | } 61 | 62 | func (this *Render) HTML(w http.ResponseWriter, status int, name string, data any) { 63 | w.WriteHeader(status) 64 | this.Template(name).ExecuteWriter(w, data) 65 | } 66 | 67 | // -------------------------------------------------------------------------------- 68 | type Template struct { 69 | template *pongo2.Template 70 | context pongo2.Context 71 | } 72 | 73 | func (this *Template) ExecuteWriter(w http.ResponseWriter, data any) (err error) { 74 | WriteContentType(w, htmlContentType) 75 | this.context = DataToContext(data) 76 | err = this.template.ExecuteWriter(this.context, w) 77 | return err 78 | } 79 | 80 | func (this *Template) Execute(data any) (string, error) { 81 | this.context = DataToContext(data) 82 | return this.template.Execute(this.context) 83 | } 84 | 85 | // -------------------------------------------------------------------------------- 86 | func WriteContentType(w http.ResponseWriter, value []string) { 87 | header := w.Header() 88 | if val := header["Content-Type"]; len(val) == 0 { 89 | header["Content-Type"] = value 90 | } 91 | } 92 | 93 | func DataToContext(data interface{}) pongo2.Context { 94 | var ctx pongo2.Context 95 | if data != nil { 96 | switch data.(type) { 97 | case pongo2.Context: 98 | ctx = data.(pongo2.Context) 99 | case map[string]any: 100 | ctx = data.(map[string]any) 101 | } 102 | } 103 | return ctx 104 | } 105 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/schema_model.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // parse get the model info 4 | type ModelSchema struct { 5 | FieldName string `json:"name"` // 字段名称 6 | FieldType string `json:"goType"` // go type 7 | FieldTags map[string]SpecTag `json:"fieldTags"` // map[gorm]{name:"gorm",origin:"not null;comment:"名称""} 8 | FieldComment string `json:"comment"` // mysql comment 9 | } 10 | 11 | type ModelSchemas []ModelSchema 12 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/schema_render.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | type RenderInfo struct { 4 | ModelNames []string `json:"modelNames"` // 所有model names 5 | ModelName string `json:"modelName"` // 当前model names 6 | Module string `json:"-"` 7 | TmplPath string `json:"tmplPath"` 8 | GenerateTime string `json:"generateTime"` 9 | Option UserOption `json:"-"` 10 | Content ModelSchemas `json:"content"` 11 | Descriptor Descriptor `json:"-"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/app/module/web/parser/typeparser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_parseLineTag(t *testing.T) { 8 | value := `gorm:"not null;PRIMARY_KEY;comment:'用户uid'" json:"uid"` 9 | tags := parseLineTag(value) 10 | t.Log(tags) 11 | } 12 | -------------------------------------------------------------------------------- /internal/colors/colorwriter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 bee authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | //go:build !windows 16 | // +build !windows 17 | 18 | package colors 19 | 20 | import "io" 21 | 22 | type colorWriter struct { 23 | w io.Writer 24 | mode outputMode 25 | } 26 | 27 | func (cw *colorWriter) Write(p []byte) (int, error) { 28 | return cw.w.Write(p) 29 | } 30 | -------------------------------------------------------------------------------- /internal/command/cmd.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // ExecCmdDirBytes executes system command in given directory 12 | // and return stdout, stderr in bytes type, along with possible error. 13 | func ExecCmdDirBytes(dir, cmdName string, args ...string) ([]byte, []byte, error) { 14 | bufOut := new(bytes.Buffer) 15 | bufErr := new(bytes.Buffer) 16 | 17 | cmd := exec.Command(cmdName, args...) 18 | cmd.Dir = dir 19 | cmd.Stdout = bufOut 20 | cmd.Stderr = bufErr 21 | 22 | err := cmd.Run() 23 | return bufOut.Bytes(), bufErr.Bytes(), err 24 | } 25 | 26 | // ExecCmdBytes executes system command 27 | // and return stdout, stderr in bytes type, along with possible error. 28 | func ExecCmdBytes(cmdName string, args ...string) ([]byte, []byte, error) { 29 | return ExecCmdDirBytes("", cmdName, args...) 30 | } 31 | 32 | // ExecCmdDir executes system command in given directory 33 | // and return stdout, stderr in string type, along with possible error. 34 | func ExecCmdDir(dir, cmdName string, args ...string) (string, string, error) { 35 | bufOut, bufErr, err := ExecCmdDirBytes(dir, cmdName, args...) 36 | return string(bufOut), string(bufErr), err 37 | } 38 | 39 | // ExecCmd executes system command 40 | // and return stdout, stderr in string type, along with possible error. 41 | func ExecCmd(cmdName string, args ...string) (string, string, error) { 42 | return ExecCmdDir("", cmdName, args...) 43 | } 44 | 45 | // VerCompare 版本对比 v1比v2大返回1,小于返回-1,等于返回0 46 | func VerCompare(ver1, ver2 string) int { 47 | ver1 = strings.TrimLeft(ver1, "ver") // 清除v,e,r 48 | ver2 = strings.TrimLeft(ver2, "ver") // 清除v,e,r 49 | p1 := strings.Split(ver1, ".") 50 | p2 := strings.Split(ver2, ".") 51 | 52 | ver1 = "" 53 | for _, v := range p1 { 54 | iv, _ := strconv.Atoi(v) 55 | ver1 = fmt.Sprintf("%s%04d", ver1, iv) 56 | } 57 | 58 | ver2 = "" 59 | for _, v := range p2 { 60 | iv, _ := strconv.Atoi(v) 61 | ver2 = fmt.Sprintf("%s%04d", ver2, iv) 62 | } 63 | 64 | return strings.Compare(ver1, ver2) 65 | } 66 | -------------------------------------------------------------------------------- /internal/system/system.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "path/filepath" 7 | ) 8 | 9 | // Egoctl System Params ... 10 | var ( 11 | Usr, _ = user.Current() 12 | EgoctlHome = filepath.Join(Usr.HomeDir, "/.egoctl") 13 | CurrentDir = getCurrentDirectory() 14 | GoPath = os.Getenv("GOPATH") 15 | ) 16 | 17 | func getCurrentDirectory() string { 18 | if dir, err := os.Getwd(); err == nil { 19 | return dir 20 | } 21 | return "" 22 | } 23 | -------------------------------------------------------------------------------- /internal/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // Mkdir ... 8 | func Mkdir(dir string) bool { 9 | if dir == "" { 10 | return false 11 | } 12 | err := os.MkdirAll(dir, 0755) 13 | if err != nil { 14 | return false 15 | } 16 | return true 17 | } 18 | 19 | // IsDir ... 20 | func IsDir(dir string) bool { 21 | f, e := os.Stat(dir) 22 | if e != nil { 23 | return false 24 | } 25 | return f.IsDir() 26 | } 27 | 28 | // IsExist returns whether a file or directory exists. 29 | func IsExist(path string) bool { 30 | _, err := os.Stat(path) 31 | return err == nil || os.IsExist(err) 32 | } 33 | -------------------------------------------------------------------------------- /internal/utils/notification.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 bee authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | package utils 15 | 16 | import ( 17 | "fmt" 18 | "os/exec" 19 | "runtime" 20 | "strconv" 21 | "strings" 22 | 23 | "github.com/gotomicro/egoctl/internal/config" 24 | ) 25 | 26 | const appName = "Beego" 27 | 28 | func Notify(text, title string) { 29 | if !config.Conf.EnableNotification { 30 | return 31 | } 32 | switch runtime.GOOS { 33 | case "darwin": 34 | osxNotify(text, title) 35 | case "linux": 36 | linuxNotify(text, title) 37 | case "windows": 38 | windowsNotify(text, title) 39 | } 40 | } 41 | 42 | func osxNotify(text, title string) { 43 | var cmd *exec.Cmd 44 | if existTerminalNotifier() { 45 | cmd = exec.Command("terminal-notifier", "-title", appName, "-message", text, "-subtitle", title) 46 | } else if MacOSVersionSupport() { 47 | notification := fmt.Sprintf("display notification \"%s\" with title \"%s\" subtitle \"%s\"", text, appName, title) 48 | cmd = exec.Command("osascript", "-e", notification) 49 | } else { 50 | cmd = exec.Command("growlnotify", "-n", appName, "-m", title) 51 | } 52 | cmd.Run() 53 | } 54 | 55 | func windowsNotify(text, title string) { 56 | exec.Command("growlnotify", "/i:", "", "/t:", title, text).Run() 57 | } 58 | 59 | func linuxNotify(text, title string) { 60 | exec.Command("notify-send", "-i", "", title, text).Run() 61 | } 62 | 63 | func existTerminalNotifier() bool { 64 | cmd := exec.Command("which", "terminal-notifier") 65 | err := cmd.Start() 66 | if err != nil { 67 | return false 68 | } 69 | err = cmd.Wait() 70 | return err != nil 71 | } 72 | 73 | func MacOSVersionSupport() bool { 74 | cmd := exec.Command("sw_vers", "-productVersion") 75 | check, _ := cmd.Output() 76 | version := strings.Split(string(check), ".") 77 | major, _ := strconv.Atoi(version[0]) 78 | minor, _ := strconv.Atoi(version[1]) 79 | if major < 10 || (major == 10 && minor < 9) { 80 | return false 81 | } 82 | return true 83 | } 84 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 bee authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | package main 15 | 16 | import ( 17 | _ "embed" 18 | "fmt" 19 | "os" 20 | 21 | "github.com/gotomicro/egoctl/cmd" 22 | _ "github.com/gotomicro/egoctl/cmd/migrate" 23 | _ "github.com/gotomicro/egoctl/cmd/run" 24 | _ "github.com/gotomicro/egoctl/cmd/version" 25 | _ "github.com/gotomicro/egoctl/cmd/web" 26 | "github.com/gotomicro/egoctl/internal/config" 27 | ) 28 | 29 | func main() { 30 | config.LoadConfig() 31 | err := cmd.RootCommand.Execute() 32 | if err != nil { 33 | fmt.Fprintln(os.Stderr, err) 34 | os.Exit(1) 35 | } 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /scripts/build/report_build_info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # WARNING: DO NOT EDIT, THIS FILE IS PROBABLY A COPY 4 | # 5 | # The original version of this file is located in the https://github.com/istio/common-files repo. 6 | # If you're looking at this file in a different repo and want to make a change, please go to the 7 | # common-files repo, make the change there and check it in. Then come back to this repo and run 8 | # "make update-common". 9 | 10 | # Copyright Istio Authors 11 | # 12 | # Licensed under the Apache License, Version 2.0 (the "License"); 13 | # you may not use this file except in compliance with the License. 14 | # You may obtain a copy of the License at 15 | # 16 | # http://www.apache.org/licenses/LICENSE-2.0 17 | # 18 | # Unless required by applicable law or agreed to in writing, software 19 | # distributed under the License is distributed on an "AS IS" BASIS, 20 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | # See the License for the specific language governing permissions and 22 | # limitations under the License. 23 | APP_NAME=${1:?"app name"} 24 | 25 | if BUILD_GIT_REVISION=$(git rev-parse HEAD 2> /dev/null); then 26 | if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then 27 | BUILD_GIT_REVISION=${BUILD_GIT_REVISION}"-dirty" 28 | fi 29 | else 30 | BUILD_GIT_REVISION=unknown 31 | fi 32 | 33 | # Check for local changes 34 | if git diff-index --quiet HEAD --; then 35 | tree_status="Clean" 36 | else 37 | tree_status="Modified" 38 | fi 39 | 40 | # XXX This needs to be updated to accomodate tags added after building, rather than prior to builds 41 | GIT_DESCRIBE_TAG=$(git describe --tags) 42 | 43 | # security wanted VERSION='unknown' 44 | VERSION="${BUILD_GIT_REVISION}" 45 | if [[ -n "${GIT_DESCRIBE_TAG}" ]]; then 46 | VERSION="${GIT_DESCRIBE_TAG}" 47 | fi 48 | 49 | APP_PKG=${2:?"app pkg"} 50 | # used by scripts/build/gobuild.sh 51 | echo "${APP_PKG}/cmd/version.buildVersion=${VERSION}" 52 | echo "${APP_PKG}/cmd/version.buildGitVersion=${BUILD_GIT_REVISION}" 53 | echo "${APP_PKG}/cmd/version.buildTag=${GIT_DESCRIBE_TAG}" 54 | echo "${APP_PKG}/cmd/version.buildStatus=${tree_status}" 55 | echo "${APP_PKG}/cmd/version.buildUser=$(whoami)" 56 | echo "${APP_PKG}/cmd/version.buildHost=$(hostname -f)" 57 | echo "${APP_PKG}/cmd/version.buildTime=$(date '+%Y-%m-%d--%T')" 58 | 59 | 60 | -------------------------------------------------------------------------------- /webui/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /webui/.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | .history 5 | public 6 | dist 7 | .umi 8 | mock -------------------------------------------------------------------------------- /webui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | globals: { 4 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, 5 | page: true, 6 | REACT_APP_ENV: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /webui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /.vscode 11 | 12 | # misc 13 | .DS_Store 14 | npm-debug.log* 15 | yarn-error.log 16 | 17 | /coverage 18 | .idea 19 | #yarn.lock 20 | #package-lock.json 21 | *bak 22 | .vscode 23 | 24 | # visual studio code 25 | .history 26 | *.log 27 | functions/* 28 | .temp/** 29 | 30 | # umi 31 | .umi 32 | .umi-production 33 | 34 | # screenshot 35 | screenshot 36 | .firebase 37 | .eslintcache 38 | 39 | build 40 | -------------------------------------------------------------------------------- /webui/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | LICENSE 17 | .eslintcache 18 | *.lock 19 | yarn-error.log 20 | .history 21 | CNAME 22 | /build 23 | /public -------------------------------------------------------------------------------- /webui/.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.prettier, 5 | }; 6 | -------------------------------------------------------------------------------- /webui/.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.stylelint, 5 | }; 6 | -------------------------------------------------------------------------------- /webui/README.md: -------------------------------------------------------------------------------- 1 | # Ant Design Pro 2 | 3 | This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use. 4 | 5 | ## Environment Prepare 6 | 7 | Install `node_modules`: 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | or 14 | 15 | ```bash 16 | yarn 17 | ``` 18 | 19 | ## Provided Scripts 20 | 21 | Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test. 22 | 23 | Scripts provided in `package.json`. It's safe to modify or add additional script: 24 | 25 | ### Start project 26 | 27 | ```bash 28 | npm start 29 | ``` 30 | 31 | ### Build project 32 | 33 | ```bash 34 | npm run build 35 | ``` 36 | 37 | ### Check code style 38 | 39 | ```bash 40 | npm run lint 41 | ``` 42 | 43 | You can also use script to auto fix some lint error: 44 | 45 | ```bash 46 | npm run lint:fix 47 | ``` 48 | 49 | ### Test code 50 | 51 | ```bash 52 | npm test 53 | ``` 54 | 55 | ## More 56 | 57 | You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro). 58 | -------------------------------------------------------------------------------- /webui/config/config.dev.ts: -------------------------------------------------------------------------------- 1 | // https://umijs.org/config/ 2 | import { defineConfig } from 'umi'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | // https://github.com/zthxxx/react-dev-inspector 7 | 'react-dev-inspector/plugins/umi/react-inspector', 8 | ], 9 | // https://github.com/zthxxx/react-dev-inspector#inspector-loader-props 10 | inspectorConfig: { 11 | exclude: [], 12 | babelPlugins: [], 13 | babelOptions: {}, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /webui/config/config.ts: -------------------------------------------------------------------------------- 1 | // https://umijs.org/config/ 2 | import MonacoEditorWebpackPlugin from 'monaco-editor-webpack-plugin'; 3 | import { defineConfig } from 'umi'; 4 | import defaultSettings from './defaultSettings'; 5 | import proxy from './proxy'; 6 | import routes from './routes'; 7 | 8 | const { REACT_APP_ENV } = process.env; 9 | 10 | export default defineConfig({ 11 | hash: true, 12 | antd: {}, 13 | dva: { 14 | hmr: true, 15 | }, 16 | history: { 17 | type: 'browser', 18 | }, 19 | locale: { 20 | // default zh-CN 21 | default: 'zh-CN', 22 | antd: true, 23 | // default true, when it is true, will use `navigator.language` overwrite default 24 | baseNavigator: true, 25 | }, 26 | dynamicImport: { 27 | loading: '@/components/PageLoading/index', 28 | }, 29 | targets: { 30 | ie: 11, 31 | }, 32 | // umi routes: https://umijs.org/docs/routing 33 | routes, 34 | // Theme for antd: https://ant.design/docs/react/customize-theme-cn 35 | theme: { 36 | 'primary-color': defaultSettings.primaryColor, 37 | }, 38 | title: false, 39 | ignoreMomentLocale: true, 40 | proxy: proxy[REACT_APP_ENV || 'dev'], 41 | manifest: { 42 | basePath: '/webui/', 43 | }, 44 | publicPath: '/webui/', 45 | esbuild: {}, 46 | chainWebpack(config, { env, webpack, createCSSRule }) { 47 | config.plugin('monaco-editor').use(MonacoEditorWebpackPlugin, [ 48 | { 49 | languages: ['go', 'json'], 50 | features: [ 51 | 'find', 52 | 'comment', 53 | 'format', 54 | 'bracketMatching', 55 | 'wordOperations', 56 | 'suggest', 57 | 'multicursor', 58 | 'links', 59 | ], 60 | }, 61 | ]); 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /webui/config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { Settings as ProSettings } from '@ant-design/pro-layout'; 2 | 3 | type DefaultSettings = Partial & { 4 | pwa: boolean; 5 | }; 6 | 7 | const proSettings: DefaultSettings = { 8 | "navTheme": "dark", 9 | "primaryColor": "#1890ff", 10 | "layout": "side", 11 | "contentWidth": "Fluid", 12 | "fixedHeader": false, 13 | "fixSiderbar": true, 14 | "title": "EGO Low Code", 15 | "pwa": false, 16 | "iconfontUrl": "", 17 | "menu": { 18 | "locale": false, 19 | }, 20 | "headerHeight": 48, 21 | "headerRender": false, 22 | "footerRender": false 23 | }; 24 | 25 | export type { DefaultSettings }; 26 | 27 | export default proSettings; 28 | -------------------------------------------------------------------------------- /webui/config/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 3 | * The agent cannot take effect in the production environment 4 | * so there is no configuration of the production environment 5 | * For details, please see 6 | * https://pro.ant.design/docs/deploy 7 | */ 8 | export default { 9 | dev: { 10 | '/api/': { 11 | target: 'http://localhost:9999', 12 | changeOrigin: true, 13 | pathRewrite: { '^': '' }, 14 | }, 15 | }, 16 | test: { 17 | '/api/': { 18 | target: 'https://preview.pro.ant.design', 19 | changeOrigin: true, 20 | pathRewrite: { '^': '' }, 21 | }, 22 | }, 23 | pre: { 24 | '/api/': { 25 | target: 'your pre url', 26 | changeOrigin: true, 27 | pathRewrite: { '^': '' }, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /webui/config/routes.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/', 4 | component: '../layouts/BlankLayout', 5 | routes: [ 6 | { 7 | path: '/user', 8 | component: '../layouts/UserLayout', 9 | routes: [ 10 | { 11 | name: 'login', 12 | path: '/user/login', 13 | component: './User/login', 14 | }, 15 | ], 16 | }, 17 | { 18 | path: '/', 19 | component: '../layouts/SecurityLayout', 20 | routes: [ 21 | { 22 | path: '/', 23 | component: '../layouts/BasicLayout', 24 | routes: [ 25 | { 26 | path: '/webui/', 27 | redirect: '/projects', 28 | }, 29 | { 30 | path: '/', 31 | redirect: '/projects', 32 | }, 33 | { 34 | path: '/projects', 35 | name: '项目列表', 36 | icon: 'smile', 37 | component: './Projects', 38 | }, 39 | { 40 | path: '/templates', 41 | name: '模板列表', 42 | icon: 'smile', 43 | component: './Templates', 44 | }, 45 | { 46 | path: '/admin', 47 | name: 'admin', 48 | icon: 'crown', 49 | component: './Admin', 50 | authority: ['admin'], 51 | routes: [ 52 | { 53 | path: '/admin/sub-page', 54 | name: 'sub-page', 55 | icon: 'smile', 56 | component: './Welcome', 57 | authority: ['admin'], 58 | }, 59 | ], 60 | }, 61 | { 62 | component: './404', 63 | }, 64 | ], 65 | }, 66 | { 67 | component: './404', 68 | }, 69 | ], 70 | }, 71 | ], 72 | }, 73 | { 74 | component: './404', 75 | }, 76 | ]; 77 | -------------------------------------------------------------------------------- /webui/embed_webui.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import "embed" 4 | 5 | //go:embed dist 6 | var WebUI embed.FS 7 | -------------------------------------------------------------------------------- /webui/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | testEnvironment: './tests/PuppeteerEnvironment', 4 | verbose: false, 5 | globals: { 6 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 7 | localStorage: null, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /webui/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /webui/mock/route.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | '/api/auth_routes': { 3 | '/form/advanced-form': { authority: ['admin', 'user'] }, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /webui/public/CNAME: -------------------------------------------------------------------------------- 1 | preview.pro.ant.design -------------------------------------------------------------------------------- /webui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/egoctl/65e30a38dc7650f135fcc263408edcf74d54187f/webui/public/favicon.ico -------------------------------------------------------------------------------- /webui/public/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/egoctl/65e30a38dc7650f135fcc263408edcf74d54187f/webui/public/home_bg.png -------------------------------------------------------------------------------- /webui/public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/egoctl/65e30a38dc7650f135fcc263408edcf74d54187f/webui/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /webui/public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/egoctl/65e30a38dc7650f135fcc263408edcf74d54187f/webui/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /webui/public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/egoctl/65e30a38dc7650f135fcc263408edcf74d54187f/webui/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /webui/public/pro_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /webui/src/components/Authorized/Authorized.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Result } from 'antd'; 3 | import check from './CheckPermissions'; 4 | import type { IAuthorityType } from './CheckPermissions'; 5 | import type AuthorizedRoute from './AuthorizedRoute'; 6 | import type Secured from './Secured'; 7 | 8 | type AuthorizedProps = { 9 | authority: IAuthorityType; 10 | noMatch?: React.ReactNode; 11 | }; 12 | 13 | type IAuthorizedType = React.FunctionComponent & { 14 | Secured: typeof Secured; 15 | check: typeof check; 16 | AuthorizedRoute: typeof AuthorizedRoute; 17 | }; 18 | 19 | const Authorized: React.FunctionComponent = ({ 20 | children, 21 | authority, 22 | noMatch = ( 23 | 28 | ), 29 | }) => { 30 | const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children; 31 | const dom = check(authority, childrenRender, noMatch); 32 | return <>{dom}; 33 | }; 34 | 35 | export default Authorized as IAuthorizedType; 36 | -------------------------------------------------------------------------------- /webui/src/components/Authorized/AuthorizedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Redirect, Route } from 'umi'; 2 | 3 | import React from 'react'; 4 | import Authorized from './Authorized'; 5 | import type { IAuthorityType } from './CheckPermissions'; 6 | 7 | type AuthorizedRouteProps = { 8 | currentAuthority: string; 9 | component: React.ComponentClass; 10 | render: (props: any) => React.ReactNode; 11 | redirectPath: string; 12 | authority: IAuthorityType; 13 | }; 14 | 15 | const AuthorizedRoute: React.SFC = ({ 16 | component: Component, 17 | render, 18 | authority, 19 | redirectPath, 20 | ...rest 21 | }) => ( 22 | } />} 25 | > 26 | (Component ? : render(props))} 29 | /> 30 | 31 | ); 32 | 33 | export default AuthorizedRoute; 34 | -------------------------------------------------------------------------------- /webui/src/components/Authorized/CheckPermissions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CURRENT } from './renderAuthorize'; 3 | // eslint-disable-next-line import/no-cycle 4 | import PromiseRender from './PromiseRender'; 5 | 6 | export type IAuthorityType = 7 | | undefined 8 | | string 9 | | string[] 10 | | Promise 11 | | ((currentAuthority: string | string[]) => IAuthorityType); 12 | 13 | /** 14 | * 通用权限检查方法 Common check permissions method 15 | * 16 | * @param { 权限判定 | Permission judgment } authority 17 | * @param { 你的权限 | Your permission description } currentAuthority 18 | * @param { 通过的组件 | Passing components } target 19 | * @param { 未通过的组件 | no pass components } Exception 20 | */ 21 | const checkPermissions = ( 22 | authority: IAuthorityType, 23 | currentAuthority: string | string[], 24 | target: T, 25 | Exception: K, 26 | ): T | K | React.ReactNode => { 27 | // 没有判定权限.默认查看所有 28 | // Retirement authority, return target; 29 | if (!authority) { 30 | return target; 31 | } 32 | // 数组处理 33 | if (Array.isArray(authority)) { 34 | if (Array.isArray(currentAuthority)) { 35 | if (currentAuthority.some((item) => authority.includes(item))) { 36 | return target; 37 | } 38 | } else if (authority.includes(currentAuthority)) { 39 | return target; 40 | } 41 | return Exception; 42 | } 43 | // string 处理 44 | if (typeof authority === 'string') { 45 | if (Array.isArray(currentAuthority)) { 46 | if (currentAuthority.some((item) => authority === item)) { 47 | return target; 48 | } 49 | } else if (authority === currentAuthority) { 50 | return target; 51 | } 52 | return Exception; 53 | } 54 | // Promise 处理 55 | if (authority instanceof Promise) { 56 | return ok={target} error={Exception} promise={authority} />; 57 | } 58 | // Function 处理 59 | if (typeof authority === 'function') { 60 | const bool = authority(currentAuthority); 61 | // 函数执行后返回值是 Promise 62 | if (bool instanceof Promise) { 63 | return ok={target} error={Exception} promise={bool} />; 64 | } 65 | if (bool) { 66 | return target; 67 | } 68 | return Exception; 69 | } 70 | throw new Error('unsupported parameters'); 71 | }; 72 | 73 | export { checkPermissions }; 74 | 75 | function check(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode { 76 | return checkPermissions(authority, CURRENT, target, Exception); 77 | } 78 | 79 | export default check; 80 | -------------------------------------------------------------------------------- /webui/src/components/Authorized/PromiseRender.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spin } from 'antd'; 3 | import isEqual from 'lodash/isEqual'; 4 | import { isComponentClass } from './Secured'; 5 | // eslint-disable-next-line import/no-cycle 6 | 7 | type PromiseRenderProps = { 8 | ok: T; 9 | error: K; 10 | promise: Promise; 11 | }; 12 | 13 | type PromiseRenderState = { 14 | component: React.ComponentClass | React.FunctionComponent; 15 | }; 16 | 17 | export default class PromiseRender extends React.Component< 18 | PromiseRenderProps, 19 | PromiseRenderState 20 | > { 21 | state: PromiseRenderState = { 22 | component: () => null, 23 | }; 24 | 25 | componentDidMount(): void { 26 | this.setRenderComponent(this.props); 27 | } 28 | 29 | shouldComponentUpdate = ( 30 | nextProps: PromiseRenderProps, 31 | nextState: PromiseRenderState, 32 | ): boolean => { 33 | const { component } = this.state; 34 | if (!isEqual(nextProps, this.props)) { 35 | this.setRenderComponent(nextProps); 36 | } 37 | if (nextState.component !== component) return true; 38 | return false; 39 | }; 40 | 41 | // set render Component : ok or error 42 | setRenderComponent(props: PromiseRenderProps): void { 43 | const ok = this.checkIsInstantiation(props.ok); 44 | const error = this.checkIsInstantiation(props.error); 45 | props.promise 46 | .then(() => { 47 | this.setState({ 48 | component: ok, 49 | }); 50 | return true; 51 | }) 52 | .catch(() => { 53 | this.setState({ 54 | component: error, 55 | }); 56 | }); 57 | } 58 | 59 | // Determine whether the incoming component has been instantiated 60 | // AuthorizedRoute is already instantiated 61 | // Authorized render is already instantiated, children is no instantiated 62 | // Secured is not instantiated 63 | checkIsInstantiation = ( 64 | target: React.ReactNode | React.ComponentClass, 65 | ): React.FunctionComponent => { 66 | if (isComponentClass(target)) { 67 | const Target = target as React.ComponentClass; 68 | return (props: any) => ; 69 | } 70 | if (React.isValidElement(target)) { 71 | return (props: any) => React.cloneElement(target, props); 72 | } 73 | return () => target as React.ReactNode & null; 74 | }; 75 | 76 | render() { 77 | const { component: Component } = this.state; 78 | const { ok, error, promise, ...rest } = this.props; 79 | 80 | return Component ? ( 81 | 82 | ) : ( 83 |
92 | 93 |
94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /webui/src/components/Authorized/Secured.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CheckPermissions from './CheckPermissions'; 3 | 4 | /** 默认不能访问任何页面 default is "NULL" */ 5 | const Exception403 = () => 403; 6 | 7 | export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => { 8 | if (!component) return false; 9 | const proto = Object.getPrototypeOf(component); 10 | if (proto === React.Component || proto === Function.prototype) return true; 11 | return isComponentClass(proto); 12 | }; 13 | 14 | // Determine whether the incoming component has been instantiated 15 | // AuthorizedRoute is already instantiated 16 | // Authorized render is already instantiated, children is no instantiated 17 | // Secured is not instantiated 18 | const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => { 19 | if (isComponentClass(target)) { 20 | const Target = target as React.ComponentClass; 21 | return (props: any) => ; 22 | } 23 | if (React.isValidElement(target)) { 24 | return (props: any) => React.cloneElement(target, props); 25 | } 26 | return () => target; 27 | }; 28 | 29 | /** 30 | * 用于判断是否拥有权限访问此 view 权限 authority 支持传入 string, () => boolean | Promise e.g. 'user' 只有 user 用户能访问 31 | * e.g. 'user,admin' user 和 admin 都能访问 e.g. ()=>boolean 返回true能访问,返回false不能访问 e.g. Promise then 能访问 32 | * catch不能访问 e.g. authority support incoming string, () => boolean | Promise e.g. 'user' only user 33 | * user can access e.g. 'user, admin' user and admin can access e.g. () => boolean true to be able 34 | * to visit, return false can not be accessed e.g. Promise then can not access the visit to catch 35 | * 36 | * @param {string | function | Promise} authority 37 | * @param {ReactNode} error 非必需参数 38 | */ 39 | const authorize = (authority: string, error?: React.ReactNode) => { 40 | /** 41 | * Conversion into a class 防止传入字符串时找不到staticContext造成报错 String parameters can cause staticContext 42 | * not found error 43 | */ 44 | let classError: boolean | React.FunctionComponent = false; 45 | if (error) { 46 | classError = (() => error) as React.FunctionComponent; 47 | } 48 | if (!authority) { 49 | throw new Error('authority is required'); 50 | } 51 | return function decideAuthority(target: React.ComponentClass | React.ReactNode) { 52 | const component = CheckPermissions(authority, target, classError || Exception403); 53 | return checkIsInstantiation(component); 54 | }; 55 | }; 56 | 57 | export default authorize; 58 | -------------------------------------------------------------------------------- /webui/src/components/Authorized/index.tsx: -------------------------------------------------------------------------------- 1 | import Authorized from './Authorized'; 2 | import Secured from './Secured'; 3 | import check from './CheckPermissions'; 4 | import renderAuthorize from './renderAuthorize'; 5 | 6 | Authorized.Secured = Secured; 7 | Authorized.check = check; 8 | 9 | const RenderAuthorize = renderAuthorize(Authorized); 10 | 11 | export default RenderAuthorize; 12 | -------------------------------------------------------------------------------- /webui/src/components/Authorized/renderAuthorize.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable import/no-mutable-exports */ 3 | let CURRENT: string | string[] = 'NULL'; 4 | 5 | type CurrentAuthorityType = string | string[] | (() => typeof CURRENT); 6 | /** 7 | * Use authority or getAuthority 8 | * 9 | * @param {string|()=>String} currentAuthority 10 | */ 11 | const renderAuthorize = (Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => ( 12 | currentAuthority: CurrentAuthorityType, 13 | ): T => { 14 | if (currentAuthority) { 15 | if (typeof currentAuthority === 'function') { 16 | CURRENT = currentAuthority(); 17 | } 18 | if ( 19 | Object.prototype.toString.call(currentAuthority) === '[object String]' || 20 | Array.isArray(currentAuthority) 21 | ) { 22 | CURRENT = currentAuthority as string[]; 23 | } 24 | } else { 25 | CURRENT = 'NULL'; 26 | } 27 | return Authorized; 28 | }; 29 | 30 | export { CURRENT }; 31 | export default (Authorized: T) => renderAuthorize(Authorized); 32 | -------------------------------------------------------------------------------- /webui/src/components/GlobalHeader/AvatarDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; 2 | import { Avatar, Menu, Spin } from 'antd'; 3 | import React from 'react'; 4 | import type { ConnectProps } from 'umi'; 5 | import { history, connect } from 'umi'; 6 | import type { ConnectState } from '@/models/connect'; 7 | import type { CurrentUser } from '@/models/user'; 8 | import HeaderDropdown from '../HeaderDropdown'; 9 | import styles from './index.less'; 10 | 11 | export type GlobalHeaderRightProps = { 12 | currentUser?: CurrentUser; 13 | menu?: boolean; 14 | } & Partial; 15 | 16 | class AvatarDropdown extends React.Component { 17 | onMenuClick = (event: { 18 | key: React.Key; 19 | keyPath: React.Key[]; 20 | item: React.ReactInstance; 21 | domEvent: React.MouseEvent; 22 | }) => { 23 | const { key } = event; 24 | 25 | if (key === 'logout') { 26 | const { dispatch } = this.props; 27 | 28 | if (dispatch) { 29 | dispatch({ 30 | type: 'login/logout', 31 | }); 32 | } 33 | 34 | return; 35 | } 36 | 37 | history.push(`/account/${key}`); 38 | }; 39 | 40 | render(): React.ReactNode { 41 | const { 42 | currentUser = { 43 | avatar: '', 44 | name: '', 45 | }, 46 | menu, 47 | } = this.props; 48 | const menuHeaderDropdown = ( 49 | 50 | {menu && ( 51 | 52 | 53 | 个人中心 54 | 55 | )} 56 | {menu && ( 57 | 58 | 59 | 个人设置 60 | 61 | )} 62 | {menu && } 63 | 64 | 65 | 66 | 退出登录 67 | 68 | 69 | ); 70 | return currentUser && currentUser.name ? ( 71 | 72 | 73 | 74 | {currentUser.name} 75 | 76 | 77 | ) : ( 78 | 79 | 86 | 87 | ); 88 | } 89 | } 90 | 91 | export default connect(({ user }: ConnectState) => ({ 92 | currentUser: user.currentUser, 93 | }))(AvatarDropdown); 94 | -------------------------------------------------------------------------------- /webui/src/components/GlobalHeader/RightContent.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, Tag } from 'antd'; 2 | import type { Settings as ProSettings } from '@ant-design/pro-layout'; 3 | import { QuestionCircleOutlined } from '@ant-design/icons'; 4 | import React from 'react'; 5 | import type { ConnectProps } from 'umi'; 6 | import { connect, SelectLang } from 'umi'; 7 | import type { ConnectState } from '@/models/connect'; 8 | import Avatar from './AvatarDropdown'; 9 | import HeaderSearch from '../HeaderSearch'; 10 | import styles from './index.less'; 11 | 12 | export type GlobalHeaderRightProps = { 13 | theme?: ProSettings['navTheme'] | 'realDark'; 14 | } & Partial & 15 | Partial; 16 | 17 | const ENVTagColor = { 18 | dev: 'orange', 19 | test: 'green', 20 | pre: '#87d068', 21 | }; 22 | 23 | const GlobalHeaderRight: React.SFC = (props) => { 24 | const { theme, layout } = props; 25 | let className = styles.right; 26 | 27 | if (theme === 'dark' && layout === 'top') { 28 | className = `${styles.right} ${styles.dark}`; 29 | } 30 | 31 | return ( 32 |
33 | umi ui, value: 'umi ui' }, 39 | { 40 | label: Ant Design, 41 | value: 'Ant Design', 42 | }, 43 | { 44 | label: Pro Table, 45 | value: 'Pro Table', 46 | }, 47 | { 48 | label: Pro Layout, 49 | value: 'Pro Layout', 50 | }, 51 | ]} 52 | // onSearch={value => { 53 | // //console.log('input', value); 54 | // }} 55 | /> 56 | 57 | 66 | 67 | 68 | 69 | 70 | {REACT_APP_ENV && ( 71 | 72 | {REACT_APP_ENV} 73 | 74 | )} 75 | 76 |
77 | ); 78 | }; 79 | 80 | export default connect(({ settings }: ConnectState) => ({ 81 | theme: settings.navTheme, 82 | layout: settings.layout, 83 | }))(GlobalHeaderRight); 84 | -------------------------------------------------------------------------------- /webui/src/components/GlobalHeader/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025); 4 | 5 | .menu { 6 | :global(.anticon) { 7 | margin-right: 8px; 8 | } 9 | :global(.ant-dropdown-menu-item) { 10 | min-width: 160px; 11 | } 12 | } 13 | 14 | .right { 15 | display: flex; 16 | float: right; 17 | height: 48px; 18 | margin-left: auto; 19 | overflow: hidden; 20 | .action { 21 | display: flex; 22 | align-items: center; 23 | height: 100%; 24 | padding: 0 12px; 25 | cursor: pointer; 26 | transition: all 0.3s; 27 | > span { 28 | vertical-align: middle; 29 | } 30 | &:hover { 31 | background: @pro-header-hover-bg; 32 | } 33 | &:global(.opened) { 34 | background: @pro-header-hover-bg; 35 | } 36 | } 37 | .search { 38 | padding: 0 12px; 39 | &:hover { 40 | background: transparent; 41 | } 42 | } 43 | .account { 44 | .avatar { 45 | margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0; 46 | margin-right: 8px; 47 | color: @primary-color; 48 | vertical-align: top; 49 | background: rgba(255, 255, 255, 0.85); 50 | } 51 | } 52 | } 53 | 54 | .dark { 55 | .action { 56 | color: rgba(255, 255, 255, 0.85); 57 | > span { 58 | color: rgba(255, 255, 255, 0.85); 59 | } 60 | &:hover, 61 | &:global(.opened) { 62 | background: @primary-color; 63 | } 64 | } 65 | } 66 | 67 | :global(.ant-pro-global-header) { 68 | .dark { 69 | .action { 70 | color: @text-color; 71 | > span { 72 | color: @text-color; 73 | } 74 | &:hover { 75 | color: rgba(255, 255, 255, 0.85); 76 | > span { 77 | color: rgba(255, 255, 255, 0.85); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /webui/src/components/HeaderDropdown/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container > * { 4 | background-color: @popover-bg; 5 | border-radius: 4px; 6 | box-shadow: @shadow-1-down; 7 | } 8 | 9 | @media screen and (max-width: @screen-xs) { 10 | .container { 11 | width: 100% !important; 12 | } 13 | .container > * { 14 | border-radius: 0 !important; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /webui/src/components/HeaderDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import type { DropDownProps } from 'antd/es/dropdown'; 2 | import { Dropdown } from 'antd'; 3 | import React from 'react'; 4 | import classNames from 'classnames'; 5 | import styles from './index.less'; 6 | 7 | export type HeaderDropdownProps = { 8 | overlayClassName?: string; 9 | overlay: React.ReactNode | (() => React.ReactNode) | any; 10 | placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; 11 | } & Omit; 12 | 13 | const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( 14 | 15 | ); 16 | 17 | export default HeaderDropdown; 18 | -------------------------------------------------------------------------------- /webui/src/components/HeaderSearch/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .headerSearch { 4 | .input { 5 | width: 0; 6 | min-width: 0; 7 | overflow: hidden; 8 | background: transparent; 9 | border-radius: 0; 10 | transition: width 0.3s, margin-left 0.3s; 11 | :global(.ant-select-selection) { 12 | background: transparent; 13 | } 14 | input { 15 | padding-right: 0; 16 | padding-left: 0; 17 | border: 0; 18 | box-shadow: none !important; 19 | } 20 | &, 21 | &:hover, 22 | &:focus { 23 | border-bottom: 1px solid @border-color-base; 24 | } 25 | &.show { 26 | width: 210px; 27 | margin-left: 8px; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webui/src/components/NoticeIcon/NoticeList.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .list { 4 | max-height: 400px; 5 | overflow: auto; 6 | &::-webkit-scrollbar { 7 | display: none; 8 | } 9 | .item { 10 | padding-right: 24px; 11 | padding-left: 24px; 12 | overflow: hidden; 13 | cursor: pointer; 14 | transition: all 0.3s; 15 | 16 | .meta { 17 | width: 100%; 18 | } 19 | 20 | .avatar { 21 | margin-top: 4px; 22 | background: @component-background; 23 | } 24 | .iconElement { 25 | font-size: 32px; 26 | } 27 | 28 | &.read { 29 | opacity: 0.4; 30 | } 31 | &:last-child { 32 | border-bottom: 0; 33 | } 34 | &:hover { 35 | background: @primary-1; 36 | } 37 | .title { 38 | margin-bottom: 8px; 39 | font-weight: normal; 40 | } 41 | .description { 42 | font-size: 12px; 43 | line-height: @line-height-base; 44 | } 45 | .datetime { 46 | margin-top: 4px; 47 | font-size: 12px; 48 | line-height: @line-height-base; 49 | } 50 | .extra { 51 | float: right; 52 | margin-top: -1.5px; 53 | margin-right: 0; 54 | color: @text-color-secondary; 55 | font-weight: normal; 56 | } 57 | } 58 | .loadMore { 59 | padding: 8px 0; 60 | color: @primary-6; 61 | text-align: center; 62 | cursor: pointer; 63 | &.loadedAll { 64 | color: rgba(0, 0, 0, 0.25); 65 | cursor: unset; 66 | } 67 | } 68 | } 69 | 70 | .notFound { 71 | padding: 73px 0 88px; 72 | color: @text-color-secondary; 73 | text-align: center; 74 | img { 75 | display: inline-block; 76 | height: 76px; 77 | margin-bottom: 16px; 78 | } 79 | } 80 | 81 | .bottomBar { 82 | height: 46px; 83 | color: @text-color; 84 | line-height: 46px; 85 | text-align: center; 86 | border-top: 1px solid @border-color-split; 87 | border-radius: 0 0 @border-radius-base @border-radius-base; 88 | transition: all 0.3s; 89 | div { 90 | display: inline-block; 91 | width: 50%; 92 | cursor: pointer; 93 | transition: all 0.3s; 94 | user-select: none; 95 | 96 | &:only-child { 97 | width: 100%; 98 | } 99 | &:not(:only-child):last-child { 100 | border-left: 1px solid @border-color-split; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /webui/src/components/NoticeIcon/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .popover { 4 | position: relative; 5 | width: 336px; 6 | } 7 | 8 | .noticeButton { 9 | display: inline-block; 10 | cursor: pointer; 11 | transition: all 0.3s; 12 | } 13 | .icon { 14 | padding: 4px; 15 | vertical-align: middle; 16 | } 17 | 18 | .badge { 19 | font-size: 16px; 20 | } 21 | 22 | .tabs { 23 | :global { 24 | .ant-tabs-nav-list { 25 | margin: auto; 26 | } 27 | 28 | .ant-tabs-nav-scroll { 29 | text-align: center; 30 | } 31 | .ant-tabs-bar { 32 | margin-bottom: 0; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webui/src/components/PageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageLoading } from '@ant-design/pro-layout'; 2 | 3 | // loading components from code split 4 | // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport 5 | export default PageLoading; 6 | -------------------------------------------------------------------------------- /webui/src/components/SearchTable/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | -------------------------------------------------------------------------------- /webui/src/e2e/__mocks__/antd-pro-merge-less.js: -------------------------------------------------------------------------------- 1 | export default undefined; 2 | -------------------------------------------------------------------------------- /webui/src/e2e/baseLayout.e2e.js: -------------------------------------------------------------------------------- 1 | const { uniq } = require('lodash'); 2 | const RouterConfig = require('../../config/config').default.routes; 3 | 4 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; 5 | 6 | function formatter(routes, parentPath = '') { 7 | const fixedParentPath = parentPath.replace(/\/{1,}/g, '/'); 8 | let result = []; 9 | routes.forEach((item) => { 10 | if (item.path) { 11 | result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/')); 12 | } 13 | if (item.routes) { 14 | result = result.concat( 15 | formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath), 16 | ); 17 | } 18 | }); 19 | return uniq(result.filter((item) => !!item)); 20 | } 21 | 22 | beforeEach(async () => { 23 | await page.goto(`${BASE_URL}`); 24 | await page.evaluate(() => { 25 | localStorage.setItem('antd-pro-authority', '["admin"]'); 26 | }); 27 | }); 28 | 29 | describe('Ant Design Pro E2E test', () => { 30 | const testPage = (path) => async () => { 31 | await page.goto(`${BASE_URL}${path}`); 32 | await page.waitForSelector('footer', { 33 | timeout: 2000, 34 | }); 35 | const haveFooter = await page.evaluate( 36 | () => document.getElementsByTagName('footer').length > 0, 37 | ); 38 | expect(haveFooter).toBeTruthy(); 39 | }; 40 | 41 | const routers = formatter(RouterConfig); 42 | routers.forEach((route) => { 43 | it(`test pages ${route}`, testPage(route)); 44 | }); 45 | 46 | it('topmenu should have footer', async () => { 47 | const params = '?navTheme=light&layout=topmenu'; 48 | await page.goto(`${BASE_URL}${params}`); 49 | await page.waitForSelector('footer', { 50 | timeout: 2000, 51 | }); 52 | const haveFooter = await page.evaluate( 53 | () => document.getElementsByTagName('footer').length > 0, 54 | ); 55 | expect(haveFooter).toBeTruthy(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /webui/src/global.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | html, 4 | body, 5 | #root { 6 | height: 100%; 7 | } 8 | 9 | .colorWeak { 10 | filter: invert(80%); 11 | } 12 | 13 | .ant-layout { 14 | min-height: 100vh; 15 | } 16 | 17 | canvas { 18 | display: block; 19 | } 20 | 21 | body { 22 | text-rendering: optimizeLegibility; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | ul, 28 | ol { 29 | list-style: none; 30 | } 31 | 32 | @media (max-width: @screen-xs) { 33 | .ant-table { 34 | width: 100%; 35 | overflow-x: auto; 36 | &-thead > tr, 37 | &-tbody > tr { 38 | > th, 39 | > td { 40 | white-space: pre; 41 | > span { 42 | display: block; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | // 兼容IE11 50 | @media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) { 51 | body .ant-design-pro > .ant-layout { 52 | min-height: 100vh; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /webui/src/global.tsx: -------------------------------------------------------------------------------- 1 | import { Button, message, notification } from 'antd'; 2 | 3 | import React from 'react'; 4 | import { useIntl } from 'umi'; 5 | import defaultSettings from '../config/defaultSettings'; 6 | 7 | const { pwa } = defaultSettings; 8 | const isHttps = document.location.protocol === 'https:'; 9 | 10 | // if pwa is true 11 | if (pwa) { 12 | // Notify user if offline now 13 | window.addEventListener('sw.offline', () => { 14 | message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' })); 15 | }); 16 | 17 | // Pop up a prompt on the page asking the user if they want to use the latest version 18 | window.addEventListener('sw.updated', (event: Event) => { 19 | const e = event as CustomEvent; 20 | const reloadSW = async () => { 21 | // Check if there is sw whose state is waiting in ServiceWorkerRegistration 22 | // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration 23 | const worker = e.detail && e.detail.waiting; 24 | if (!worker) { 25 | return true; 26 | } 27 | // Send skip-waiting event to waiting SW with MessageChannel 28 | await new Promise((resolve, reject) => { 29 | const channel = new MessageChannel(); 30 | channel.port1.onmessage = (msgEvent) => { 31 | if (msgEvent.data.error) { 32 | reject(msgEvent.data.error); 33 | } else { 34 | resolve(msgEvent.data); 35 | } 36 | }; 37 | worker.postMessage({ type: 'skip-waiting' }, [channel.port2]); 38 | }); 39 | // Refresh current page to use the updated HTML and other assets after SW has skiped waiting 40 | window.location.reload(true); 41 | return true; 42 | }; 43 | const key = `open${Date.now()}`; 44 | const btn = ( 45 | 54 | ); 55 | notification.open({ 56 | message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }), 57 | description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }), 58 | btn, 59 | key, 60 | onClose: async () => null, 61 | }); 62 | }); 63 | } else if ('serviceWorker' in navigator && isHttps) { 64 | // unregister service worker 65 | const { serviceWorker } = navigator; 66 | if (serviceWorker.getRegistrations) { 67 | serviceWorker.getRegistrations().then((sws) => { 68 | sws.forEach((sw) => { 69 | sw.unregister(); 70 | }); 71 | }); 72 | } 73 | serviceWorker.getRegistration().then((sw) => { 74 | if (sw) sw.unregister(); 75 | }); 76 | 77 | // remove all caches 78 | if (window.caches && window.caches.keys) { 79 | caches.keys().then((keys) => { 80 | keys.forEach((key) => { 81 | caches.delete(key); 82 | }); 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /webui/src/layouts/BlankLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Inspector } from 'react-dev-inspector'; 3 | 4 | const InspectorWrapper = process.env.NODE_ENV === 'development' ? Inspector : React.Fragment; 5 | 6 | const Layout: React.FC = ({ children }) => { 7 | return {children}; 8 | }; 9 | 10 | export default Layout; 11 | -------------------------------------------------------------------------------- /webui/src/layouts/SecurityLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PageLoading } from '@ant-design/pro-layout'; 3 | import type { ConnectProps } from 'umi'; 4 | import { Redirect, connect } from 'umi'; 5 | import { stringify } from 'querystring'; 6 | import type { ConnectState } from '@/models/connect'; 7 | import type { CurrentUser } from '@/models/user'; 8 | 9 | type SecurityLayoutProps = { 10 | loading?: boolean; 11 | currentUser?: CurrentUser; 12 | } & ConnectProps; 13 | 14 | type SecurityLayoutState = { 15 | isReady: boolean; 16 | }; 17 | 18 | class SecurityLayout extends React.Component { 19 | state: SecurityLayoutState = { 20 | isReady: false, 21 | }; 22 | 23 | componentDidMount() { 24 | this.setState({ 25 | isReady: true, 26 | }); 27 | } 28 | 29 | render() { 30 | const { isReady } = this.state; 31 | const { children, loading, currentUser } = this.props; 32 | // You can replace it to your authentication rule (such as check token exists) 33 | // 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在) 34 | // const isLogin = currentUser && currentUser.userid; 35 | // const queryString = stringify({ 36 | // redirect: window.location.href, 37 | // }); 38 | 39 | // if ((!isLogin && loading) || !isReady) { 40 | // return ; 41 | // } 42 | // if (!isLogin && window.location.pathname !== '/user/login') { 43 | // return ; 44 | // } 45 | return children; 46 | } 47 | } 48 | 49 | export default connect(({ user, loading }: ConnectState) => ({ 50 | currentUser: user.currentUser, 51 | loading: loading.models.user, 52 | }))(SecurityLayout); 53 | -------------------------------------------------------------------------------- /webui/src/layouts/UserLayout.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100vh; 7 | overflow: auto; 8 | background: @layout-body-background; 9 | } 10 | 11 | .lang { 12 | width: 100%; 13 | height: 40px; 14 | line-height: 44px; 15 | text-align: right; 16 | :global(.ant-dropdown-trigger) { 17 | margin-right: 24px; 18 | } 19 | } 20 | 21 | .content { 22 | flex: 1; 23 | padding: 32px 0; 24 | } 25 | 26 | @media (min-width: @screen-md-min) { 27 | .container { 28 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); 29 | background-repeat: no-repeat; 30 | background-position: center 110px; 31 | background-size: 100%; 32 | } 33 | 34 | .content { 35 | padding: 32px 0 24px; 36 | } 37 | } 38 | 39 | .top { 40 | text-align: center; 41 | } 42 | 43 | .header { 44 | height: 44px; 45 | line-height: 44px; 46 | a { 47 | text-decoration: none; 48 | } 49 | } 50 | 51 | .logo { 52 | height: 44px; 53 | margin-right: 16px; 54 | vertical-align: top; 55 | } 56 | 57 | .title { 58 | position: relative; 59 | top: 2px; 60 | color: @heading-color; 61 | font-weight: 600; 62 | font-size: 33px; 63 | font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif; 64 | } 65 | 66 | .desc { 67 | margin-top: 12px; 68 | margin-bottom: 40px; 69 | color: @text-color-secondary; 70 | font-size: @font-size-base; 71 | } 72 | -------------------------------------------------------------------------------- /webui/src/layouts/UserLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { MenuDataItem } from '@ant-design/pro-layout'; 2 | import { DefaultFooter, getMenuData, getPageTitle } from '@ant-design/pro-layout'; 3 | import { Helmet, HelmetProvider } from 'react-helmet-async'; 4 | import type { ConnectProps } from 'umi'; 5 | import { Link, SelectLang, useIntl, connect, FormattedMessage } from 'umi'; 6 | import React from 'react'; 7 | import type { ConnectState } from '@/models/connect'; 8 | import logo from '../assets/logo.svg'; 9 | import styles from './UserLayout.less'; 10 | 11 | export type UserLayoutProps = { 12 | breadcrumbNameMap: Record; 13 | } & Partial; 14 | 15 | const UserLayout: React.FC = (props) => { 16 | const { 17 | route = { 18 | routes: [], 19 | }, 20 | } = props; 21 | const { routes = [] } = route; 22 | const { 23 | children, 24 | location = { 25 | pathname: '', 26 | }, 27 | } = props; 28 | const { formatMessage } = useIntl(); 29 | const { breadcrumb } = getMenuData(routes); 30 | const title = getPageTitle({ 31 | pathname: location.pathname, 32 | formatMessage, 33 | breadcrumb, 34 | ...props, 35 | }); 36 | return ( 37 | 38 | 39 | {title} 40 | 41 | 42 | 43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 | 51 | logo 52 | Ant Design 53 | 54 |
55 |
56 | 60 |
61 |
62 | {children} 63 |
64 | 65 |
66 |
67 | ); 68 | }; 69 | 70 | export default connect(({ settings }: ConnectState) => ({ ...settings }))(UserLayout); 71 | -------------------------------------------------------------------------------- /webui/src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | import component from './en-US/component'; 2 | import globalHeader from './en-US/globalHeader'; 3 | import menu from './en-US/menu'; 4 | import pwa from './en-US/pwa'; 5 | import settingDrawer from './en-US/settingDrawer'; 6 | import settings from './en-US/settings'; 7 | import pages from './en-US/pages'; 8 | 9 | export default { 10 | 'navBar.lang': 'Languages', 11 | 'layout.user.link.help': 'Help', 12 | 'layout.user.link.privacy': 'Privacy', 13 | 'layout.user.link.terms': 'Terms', 14 | 'app.preview.down.block': 'Download this page to your local project', 15 | 'app.welcome.link.fetch-blocks': 'Get all block', 16 | 'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development', 17 | ...globalHeader, 18 | ...menu, 19 | ...settingDrawer, 20 | ...settings, 21 | ...pwa, 22 | ...component, 23 | ...pages, 24 | }; 25 | -------------------------------------------------------------------------------- /webui/src/locales/en-US/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expand', 3 | 'component.tagSelect.collapse': 'Collapse', 4 | 'component.tagSelect.all': 'All', 5 | }; 6 | -------------------------------------------------------------------------------- /webui/src/locales/en-US/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Search', 3 | 'component.globalHeader.search.example1': 'Search example 1', 4 | 'component.globalHeader.search.example2': 'Search example 2', 5 | 'component.globalHeader.search.example3': 'Search example 3', 6 | 'component.globalHeader.help': 'Help', 7 | 'component.globalHeader.notification': 'Notification', 8 | 'component.globalHeader.notification.empty': 'You have viewed all notifications.', 9 | 'component.globalHeader.message': 'Message', 10 | 'component.globalHeader.message.empty': 'You have viewed all messsages.', 11 | 'component.globalHeader.event': 'Event', 12 | 'component.globalHeader.event.empty': 'You have viewed all events.', 13 | 'component.noticeIcon.clear': 'Clear', 14 | 'component.noticeIcon.cleared': 'Cleared', 15 | 'component.noticeIcon.empty': 'No notifications', 16 | 'component.noticeIcon.view-more': 'View more', 17 | }; 18 | -------------------------------------------------------------------------------- /webui/src/locales/en-US/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.more-blocks': 'More Blocks', 4 | 'menu.home': 'Home', 5 | 'menu.admin': 'Admin', 6 | 'menu.admin.sub-page': 'Sub-Page', 7 | 'menu.login': 'Login', 8 | 'menu.register': 'Register', 9 | 'menu.register.result': 'Register Result', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': 'Analysis', 12 | 'menu.dashboard.monitor': 'Monitor', 13 | 'menu.dashboard.workplace': 'Workplace', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': 'Form', 18 | 'menu.form.basic-form': 'Basic Form', 19 | 'menu.form.step-form': 'Step Form', 20 | 'menu.form.step-form.info': 'Step Form(write transfer information)', 21 | 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)', 22 | 'menu.form.step-form.result': 'Step Form(finished)', 23 | 'menu.form.advanced-form': 'Advanced Form', 24 | 'menu.list': 'List', 25 | 'menu.list.table-list': 'Search Table', 26 | 'menu.list.basic-list': 'Basic List', 27 | 'menu.list.card-list': 'Card List', 28 | 'menu.list.search-list': 'Search List', 29 | 'menu.list.search-list.articles': 'Search List(articles)', 30 | 'menu.list.search-list.projects': 'Search List(projects)', 31 | 'menu.list.search-list.applications': 'Search List(applications)', 32 | 'menu.profile': 'Profile', 33 | 'menu.profile.basic': 'Basic Profile', 34 | 'menu.profile.advanced': 'Advanced Profile', 35 | 'menu.result': 'Result', 36 | 'menu.result.success': 'Success', 37 | 'menu.result.fail': 'Fail', 38 | 'menu.exception': 'Exception', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': 'Trigger', 43 | 'menu.account': 'Account', 44 | 'menu.account.center': 'Account Center', 45 | 'menu.account.settings': 'Account Settings', 46 | 'menu.account.trigger': 'Trigger Error', 47 | 'menu.account.logout': 'Logout', 48 | 'menu.editor': 'Graphic Editor', 49 | 'menu.editor.flow': 'Flow Editor', 50 | 'menu.editor.mind': 'Mind Editor', 51 | 'menu.editor.koni': 'Koni Editor', 52 | }; 53 | -------------------------------------------------------------------------------- /webui/src/locales/en-US/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'You are offline now', 3 | 'app.pwa.serviceworker.updated': 'New content is available', 4 | 'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page', 5 | 'app.pwa.serviceworker.updated.ok': 'Refresh', 6 | }; 7 | -------------------------------------------------------------------------------- /webui/src/locales/en-US/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Page style setting', 3 | 'app.setting.pagestyle.dark': 'Dark style', 4 | 'app.setting.pagestyle.light': 'Light style', 5 | 'app.setting.content-width': 'Content Width', 6 | 'app.setting.content-width.fixed': 'Fixed', 7 | 'app.setting.content-width.fluid': 'Fluid', 8 | 'app.setting.themecolor': 'Theme Color', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Navigation Mode', 18 | 'app.setting.sidemenu': 'Side Menu Layout', 19 | 'app.setting.topmenu': 'Top Menu Layout', 20 | 'app.setting.fixedheader': 'Fixed Header', 21 | 'app.setting.fixedsidebar': 'Fixed Sidebar', 22 | 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout', 23 | 'app.setting.hideheader': 'Hidden Header when scrolling', 24 | 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled', 25 | 'app.setting.othersettings': 'Other Settings', 26 | 'app.setting.weakmode': 'Weak Mode', 27 | 'app.setting.copy': 'Copy Setting', 28 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js', 29 | 'app.setting.production.hint': 30 | 'Setting panel shows in development environment only, please manually modify', 31 | }; 32 | -------------------------------------------------------------------------------- /webui/src/locales/id-ID.ts: -------------------------------------------------------------------------------- 1 | import component from './id-ID/component'; 2 | import globalHeader from './id-ID/globalHeader'; 3 | import menu from './id-ID/menu'; 4 | import pwa from './id-ID/pwa'; 5 | import settingDrawer from './id-ID/settingDrawer'; 6 | import settings from './id-ID/settings'; 7 | import pages from './id-ID/pages'; 8 | 9 | export default { 10 | 'navbar.lang': 'Bahasa', 11 | 'layout.user.link.help': 'Bantuan', 12 | 'layout.user.link.privacy': 'Privasi', 13 | 'layout.user.link.terms': 'Ketentuan', 14 | 'app.preview.down.block': 'Unduh halaman ini dalam projek lokal anda', 15 | 'app.welcome.link.fetch-blocks': 'Dapatkan semua blok', 16 | 'app.welcome.link.block-list': 17 | 'Buat standar dengan cepat, halaman-halaman berdasarkan pengembangan `block`', 18 | ...globalHeader, 19 | ...menu, 20 | ...settingDrawer, 21 | ...settings, 22 | ...pwa, 23 | ...component, 24 | ...pages, 25 | }; 26 | -------------------------------------------------------------------------------- /webui/src/locales/id-ID/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Perluas', 3 | 'component.tagSelect.collapse': 'Lipat', 4 | 'component.tagSelect.all': 'Semua', 5 | }; 6 | -------------------------------------------------------------------------------- /webui/src/locales/id-ID/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Pencarian', 3 | 'component.globalHeader.search.example1': 'Contoh 1 Pencarian', 4 | 'component.globalHeader.search.example2': 'Contoh 2 Pencarian', 5 | 'component.globalHeader.search.example3': 'Contoh 3 Pencarian', 6 | 'component.globalHeader.help': 'Bantuan', 7 | 'component.globalHeader.notification': 'Notifikasi', 8 | 'component.globalHeader.notification.empty': 'Anda telah membaca semua notifikasi', 9 | 'component.globalHeader.message': 'Pesan', 10 | 'component.globalHeader.message.empty': 'Anda telah membaca semua pesan.', 11 | 'component.globalHeader.event': 'Acara', 12 | 'component.globalHeader.event.empty': 'Anda telah melihat semua acara.', 13 | 'component.noticeIcon.clear': 'Kosongkan', 14 | 'component.noticeIcon.cleared': 'Berhasil dikosongkan', 15 | 'component.noticeIcon.empty': 'Tidak ada pemberitahuan', 16 | 'component.noticeIcon.view-more': 'Melihat lebih', 17 | }; 18 | -------------------------------------------------------------------------------- /webui/src/locales/id-ID/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Selamat Datang', 3 | 'menu.more-blocks': 'Blocks Lainnya', 4 | 'menu.home': 'Halaman Awal', 5 | 'menu.admin': 'Admin', 6 | 'menu.admin.sub-page': 'Sub-Halaman', 7 | 'menu.login': 'Masuk', 8 | 'menu.register': 'Pendaftaran', 9 | 'menu.register.result': 'Hasil Pendaftaran', 10 | 'menu.dashboard': 'Dasbor', 11 | 'menu.dashboard.analysis': 'Analisis', 12 | 'menu.dashboard.monitor': 'Monitor', 13 | 'menu.dashboard.workplace': 'Workplace', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': 'Form', 18 | 'menu.form.basic-form': 'Form Dasar', 19 | 'menu.form.step-form': 'Form Bertahap', 20 | 'menu.form.step-form.info': 'Form Bertahap(menulis informasi yang dibagikan)', 21 | 'menu.form.step-form.confirm': 'Form Bertahap(konfirmasi informasi yang dibagikan)', 22 | 'menu.form.step-form.result': 'Form Bertahap(selesai)', 23 | 'menu.form.advanced-form': 'Form Lanjutan', 24 | 'menu.list': 'Daftar', 25 | 'menu.list.table-list': 'Tabel Pencarian', 26 | 'menu.list.basic-list': 'Daftar Dasar', 27 | 'menu.list.card-list': 'Daftar Kartu', 28 | 'menu.list.search-list': 'Daftar Pencarian', 29 | 'menu.list.search-list.articles': 'Daftar Pencarian(artikel)', 30 | 'menu.list.search-list.projects': 'Daftar Pencarian(projek)', 31 | 'menu.list.search-list.applications': 'Daftar Pencarian(aplikasi)', 32 | 'menu.profile': 'Profil', 33 | 'menu.profile.basic': 'Profil Dasar', 34 | 'menu.profile.advanced': 'Profile Lanjutan', 35 | 'menu.result': 'Hasil', 36 | 'menu.result.success': 'Sukses', 37 | 'menu.result.fail': 'Gagal', 38 | 'menu.exception': 'Pengecualian', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': 'Jalankan', 43 | 'menu.account': 'Akun', 44 | 'menu.account.center': 'Detail Akun', 45 | 'menu.account.settings': 'Pengaturan Akun', 46 | 'menu.account.trigger': 'Mengaktivasi Error', 47 | 'menu.account.logout': 'Keluar', 48 | 'menu.editor': 'Penyusun Grafis', 49 | 'menu.editor.flow': 'Penyusun Alur', 50 | 'menu.editor.mind': 'Penyusun Mind', 51 | 'menu.editor.koni': 'Penyusun Koni', 52 | }; 53 | -------------------------------------------------------------------------------- /webui/src/locales/id-ID/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'Koneksi anda terputus', 3 | 'app.pwa.serviceworker.updated': 'Konten baru sudah tersedia', 4 | 'app.pwa.serviceworker.updated.hint': 5 | 'Silahkan klik tombol "Refresh" untuk memuat ulang halaman ini', 6 | 'app.pwa.serviceworker.updated.ok': 'Memuat ulang', 7 | }; 8 | -------------------------------------------------------------------------------- /webui/src/locales/id-ID/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Pengaturan style Halaman', 3 | 'app.setting.pagestyle.dark': 'Style Gelap', 4 | 'app.setting.pagestyle.light': 'Style Cerah', 5 | 'app.setting.content-width': 'Lebar Konten', 6 | 'app.setting.content-width.fixed': 'Tetap', 7 | 'app.setting.content-width.fluid': 'Fluid', 8 | 'app.setting.themecolor': 'Theme Color', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (bawaan)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Mode Navigasi', 18 | 'app.setting.sidemenu': 'Susunan Menu Samping', 19 | 'app.setting.topmenu': 'Susunan Menu Atas', 20 | 'app.setting.fixedheader': 'Header Tetap', 21 | 'app.setting.fixedsidebar': 'Sidebar Tetap', 22 | 'app.setting.fixedsidebar.hint': 'Berjalan pada Susunan Menu Samping', 23 | 'app.setting.hideheader': 'Sembunyikan Header ketika gulir ke bawah', 24 | 'app.setting.hideheader.hint': 'Bekerja ketika Header tersembunyi dimunculkan', 25 | 'app.setting.othersettings': 'Pengaturan Lainnya', 26 | 'app.setting.weakmode': 'Mode Lemah', 27 | 'app.setting.copy': 'Salin Pengaturan', 28 | 'app.setting.copyinfo': 29 | 'Berhasil disalin,tolong ubah defaultSettings pada src/models/setting.js', 30 | 'app.setting.production.hint': 31 | 'Panel pengaturan hanya muncul pada lingkungan pengembangan, silahkan modifikasi secara menual', 32 | }; 33 | -------------------------------------------------------------------------------- /webui/src/locales/ja-JP.ts: -------------------------------------------------------------------------------- 1 | import globalHeader from './ja-JP/globalHeader'; 2 | import menu from './ja-JP/menu'; 3 | import settingDrawer from './ja-JP/settingDrawer'; 4 | import settings from './ja-JP/settings'; 5 | import pwa from './ja-JP/pwa'; 6 | import component from './ja-JP/component'; 7 | import pages from './ja-JP/pages'; 8 | 9 | export default { 10 | 'navBar.lang': '言語', 11 | 'layout.user.link.help': 'ヘルプ', 12 | 'layout.user.link.privacy': 'プライバシー', 13 | 'layout.user.link.terms': '利用規約', 14 | 'app.preview.down.block': 'このページをローカルプロジェクトにダウンロードしてください', 15 | 'app.welcome.link.fetch-blocks': '', 16 | 'app.welcome.link.block-list': '', 17 | ...globalHeader, 18 | ...menu, 19 | ...settingDrawer, 20 | ...settings, 21 | ...pwa, 22 | ...component, 23 | ...pages, 24 | }; 25 | -------------------------------------------------------------------------------- /webui/src/locales/ja-JP/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展開', 3 | 'component.tagSelect.collapse': '折りたたむ', 4 | 'component.tagSelect.all': 'すべて', 5 | }; 6 | -------------------------------------------------------------------------------- /webui/src/locales/ja-JP/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '検索', 3 | 'component.globalHeader.search.example1': '検索例1', 4 | 'component.globalHeader.search.example2': '検索例2', 5 | 'component.globalHeader.search.example3': '検索例3', 6 | 'component.globalHeader.help': 'ヘルプ', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': 'すべての通知を表示しました。', 9 | 'component.globalHeader.message': 'メッセージ', 10 | 'component.globalHeader.message.empty': 'すべてのメッセージを表示しました。', 11 | 'component.globalHeader.event': 'イベント', 12 | 'component.globalHeader.event.empty': 'すべてのイベントを表示しました。', 13 | 'component.noticeIcon.clear': 'クリア', 14 | 'component.noticeIcon.cleared': 'クリア済み', 15 | 'component.noticeIcon.empty': '通知なし', 16 | 'component.noticeIcon.view-more': 'もっと見る', 17 | }; 18 | -------------------------------------------------------------------------------- /webui/src/locales/ja-JP/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'ようこそ', 3 | 'menu.more-blocks': 'その他のブロック', 4 | 'menu.home': 'ホーム', 5 | 'menu.admin': '管理者', 6 | 'menu.admin.sub-page': 'サブページ', 7 | 'menu.login': 'ログイン', 8 | 'menu.register': '登録', 9 | 'menu.register.result': '登録結果', 10 | 'menu.dashboard': 'ダッシュボード', 11 | 'menu.dashboard.analysis': '分析', 12 | 'menu.dashboard.monitor': 'モニター', 13 | 'menu.dashboard.workplace': '職場', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': 'フォーム', 18 | 'menu.form.basic-form': '基本フォーム', 19 | 'menu.form.step-form': 'ステップフォーム', 20 | 'menu.form.step-form.info': 'ステップフォーム(転送情報の書き込み)', 21 | 'menu.form.step-form.confirm': 'ステップフォーム(転送情報の確認)', 22 | 'menu.form.step-form.result': 'ステップフォーム(完成)', 23 | 'menu.form.advanced-form': '高度なフォーム', 24 | 'menu.list': 'リスト', 25 | 'menu.list.table-list': '検索テーブル', 26 | 'menu.list.basic-list': '基本リスト', 27 | 'menu.list.card-list': 'カードリスト', 28 | 'menu.list.search-list': '検索リスト', 29 | 'menu.list.search-list.articles': '検索リスト(記事)', 30 | 'menu.list.search-list.projects': '検索リスト(プロジェクト)', 31 | 'menu.list.search-list.applications': '検索リスト(アプリ)', 32 | 'menu.profile': 'プロフィール', 33 | 'menu.profile.basic': '基本プロフィール', 34 | 'menu.profile.advanced': '高度なプロフィール', 35 | 'menu.result': '結果', 36 | 'menu.result.success': '成功', 37 | 'menu.result.fail': '失敗', 38 | 'menu.exception': '例外', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': 'トリガー', 43 | 'menu.account': 'アカウント', 44 | 'menu.account.center': 'アカウントセンター', 45 | 'menu.account.settings': 'アカウント設定', 46 | 'menu.account.trigger': 'トリガーエラー', 47 | 'menu.account.logout': 'ログアウト', 48 | 'menu.editor': 'グラフィックエディタ', 49 | 'menu.editor.flow': 'フローエディタ', 50 | 'menu.editor.mind': 'マインドエディター', 51 | 'menu.editor.koni': 'コニエディター', 52 | }; 53 | -------------------------------------------------------------------------------- /webui/src/locales/ja-JP/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'あなたは今オフラインです', 3 | 'app.pwa.serviceworker.updated': '新しいコンテンツが利用可能です', 4 | 'app.pwa.serviceworker.updated.hint': 5 | '現在のページをリロードするには、「更新」ボタンを押してください', 6 | 'app.pwa.serviceworker.updated.ok': 'リフレッシュ', 7 | }; 8 | -------------------------------------------------------------------------------- /webui/src/locales/ja-JP/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'ページスタイル設定', 3 | 'app.setting.pagestyle.dark': 'ダークスタイル', 4 | 'app.setting.pagestyle.light': 'ライトスタイル', 5 | 'app.setting.content-width': 'コンテンツの幅', 6 | 'app.setting.content-width.fixed': '固定', 7 | 'app.setting.content-width.fluid': '流体', 8 | 'app.setting.themecolor': 'テーマカラー', 9 | 'app.setting.themecolor.dust': 'ダストレッド', 10 | 'app.setting.themecolor.volcano': 'ボルケ-ノ', 11 | 'app.setting.themecolor.sunset': 'サンセットオレンジ', 12 | 'app.setting.themecolor.cyan': 'シアン', 13 | 'app.setting.themecolor.green': 'ポーラーグリーン', 14 | 'app.setting.themecolor.daybreak': '夜明けの青(デフォルト)', 15 | 'app.setting.themecolor.geekblue': 'ギーク ブルー', 16 | 'app.setting.themecolor.purple': 'ゴールデンパープル', 17 | 'app.setting.navigationmode': 'ナビゲーションモード', 18 | 'app.setting.sidemenu': 'サイドメニューのレイアウト', 19 | 'app.setting.topmenu': 'トップメニューのレイアウト', 20 | 'app.setting.fixedheader': '固定ヘッダー', 21 | 'app.setting.fixedsidebar': '固定サイドバー', 22 | 'app.setting.fixedsidebar.hint': 'サイドメニューのレイアウトで動作します', 23 | 'app.setting.hideheader': 'スクロール時の非表示ヘッダー', 24 | 'app.setting.hideheader.hint': '非表示ヘッダーが有効になっている場合に機能します', 25 | 'app.setting.othersettings': 'その他の設定', 26 | 'app.setting.weakmode': 'ウィークモード', 27 | 'app.setting.copy': 'コピー設定', 28 | 'app.setting.copyinfo': 29 | 'コピーが成功しました。src/models/setting.jsのdefaultSettingsを置き換えてください', 30 | 'app.setting.production.hint': '設定パネルは開発環境でのみ表示されます。手動で変更してください', 31 | }; 32 | -------------------------------------------------------------------------------- /webui/src/locales/ja-JP/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本設定', 3 | 'app.settings.menuMap.security': 'セキュリティ設定', 4 | 'app.settings.menuMap.binding': 'アカウントのバインド', 5 | 'app.settings.menuMap.notification': '新しいメッセージの通知', 6 | 'app.settings.basic.avatar': 'アバター', 7 | 'app.settings.basic.change-avatar': 'アバターを変更する', 8 | 'app.settings.basic.email': 'メール', 9 | 'app.settings.basic.email-message': 'メールアドレスを入力してください!', 10 | 'app.settings.basic.nickname': 'ニックネーム', 11 | 'app.settings.basic.nickname-message': 'ニックネームを入力してください!', 12 | 'app.settings.basic.profile': '個人プロフィール', 13 | 'app.settings.basic.profile-message': '個人プロフィールを入力してください!', 14 | 'app.settings.basic.profile-placeholder': '自己紹介', 15 | 'app.settings.basic.country': '国/地域', 16 | 'app.settings.basic.country-message': 'あなたの国を入力してください!', 17 | 'app.settings.basic.geographic': '州または市', 18 | 'app.settings.basic.geographic-message': '地理情報を入力してください!', 19 | 'app.settings.basic.address': '住所', 20 | 'app.settings.basic.address-message': '住所を入力してください!', 21 | 'app.settings.basic.phone': '電話番号', 22 | 'app.settings.basic.phone-message': '電話番号を入力してください!', 23 | 'app.settings.basic.update': '更新情報', 24 | 'app.settings.security.strong': '強い', 25 | 'app.settings.security.medium': 'ミディアム', 26 | 'app.settings.security.weak': '弱い', 27 | 'app.settings.security.password': 'アカウントパスワード', 28 | 'app.settings.security.password-description': '現在のパスワードの強度', 29 | 'app.settings.security.phone': 'セキュリティ電話番号', 30 | 'app.settings.security.phone-description': 'バインドされた電話番号', 31 | 'app.settings.security.question': '秘密の質問', 32 | 'app.settings.security.question-description': 33 | 'セキュリティの質問が設定されてません。セキュリティポリシーはアカウントのセキュリティを効果的に保護できます', 34 | 'app.settings.security.email': 'バックアップメール', 35 | 'app.settings.security.email-description': 'バインドされたメール', 36 | 'app.settings.security.mfa': '多要素認証デバイス', 37 | 'app.settings.security.mfa-description': 38 | 'バインドされていない多要素認証デバイスは、バインド後、2回確認できます', 39 | 'app.settings.security.modify': '変更する', 40 | 'app.settings.security.set': 'セットする', 41 | 'app.settings.security.bind': 'バインド', 42 | 'app.settings.binding.taobao': 'タオバオをバインドする', 43 | 'app.settings.binding.taobao-description': '現在バインドされていないタオバオアカウント', 44 | 'app.settings.binding.alipay': 'アリペイをバインドする', 45 | 'app.settings.binding.alipay-description': '現在バインドされていないアリペイアカウント', 46 | 'app.settings.binding.dingding': 'ディントークをバインドする', 47 | 'app.settings.binding.dingding-description': '現在バインドされていないディントークアカウント', 48 | 'app.settings.binding.bind': 'バインド', 49 | 'app.settings.notification.password': 'アカウントパスワード', 50 | 'app.settings.notification.password-description': 51 | '他のユーザーからのメッセージは、ステーションレターの形式で通知されます', 52 | 'app.settings.notification.messages': 'システムメッセージ', 53 | 'app.settings.notification.messages-description': 54 | 'システムメッセージは、ステーションレターの形式で通知されます', 55 | 'app.settings.notification.todo': 'To Do(用事) 通知', 56 | 'app.settings.notification.todo-description': 'To Doタスクは、内部レターの形式で通知されます', 57 | 'app.settings.open': '開く', 58 | 'app.settings.close': '閉じる', 59 | }; 60 | -------------------------------------------------------------------------------- /webui/src/locales/pt-BR.ts: -------------------------------------------------------------------------------- 1 | import component from './pt-BR/component'; 2 | import globalHeader from './pt-BR/globalHeader'; 3 | import menu from './pt-BR/menu'; 4 | import pwa from './pt-BR/pwa'; 5 | import settingDrawer from './pt-BR/settingDrawer'; 6 | import settings from './pt-BR/settings'; 7 | 8 | export default { 9 | 'navBar.lang': 'Idiomas', 10 | 'layout.user.link.help': 'ajuda', 11 | 'layout.user.link.privacy': 'política de privacidade', 12 | 'layout.user.link.terms': 'termos de serviços', 13 | 'app.preview.down.block': 'Download this page to your local project', 14 | ...globalHeader, 15 | ...menu, 16 | ...settingDrawer, 17 | ...settings, 18 | ...pwa, 19 | ...component, 20 | }; 21 | -------------------------------------------------------------------------------- /webui/src/locales/pt-BR/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expandir', 3 | 'component.tagSelect.collapse': 'Diminuir', 4 | 'component.tagSelect.all': 'Todas', 5 | }; 6 | -------------------------------------------------------------------------------- /webui/src/locales/pt-BR/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Busca', 3 | 'component.globalHeader.search.example1': 'Exemplo de busca 1', 4 | 'component.globalHeader.search.example2': 'Exemplo de busca 2', 5 | 'component.globalHeader.search.example3': 'Exemplo de busca 3', 6 | 'component.globalHeader.help': 'Ajuda', 7 | 'component.globalHeader.notification': 'Notificação', 8 | 'component.globalHeader.notification.empty': 'Você visualizou todas as notificações.', 9 | 'component.globalHeader.message': 'Mensagem', 10 | 'component.globalHeader.message.empty': 'Você visualizou todas as mensagens.', 11 | 'component.globalHeader.event': 'Evento', 12 | 'component.globalHeader.event.empty': 'Você visualizou todos os eventos.', 13 | 'component.noticeIcon.clear': 'Limpar', 14 | 'component.noticeIcon.cleared': 'Limpo', 15 | 'component.noticeIcon.empty': 'Sem notificações', 16 | 'component.noticeIcon.loaded': 'Carregado', 17 | 'component.noticeIcon.view-more': 'Veja mais', 18 | }; 19 | -------------------------------------------------------------------------------- /webui/src/locales/pt-BR/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.more-blocks': 'More Blocks', 4 | 'menu.home': 'Início', 5 | 'menu.login': 'Login', 6 | 'menu.admin': 'Admin', 7 | 'menu.admin.sub-page': 'Sub-Page', 8 | 'menu.register': 'Registro', 9 | 'menu.register.result': 'Resultado de registro', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': 'Análise', 12 | 'menu.dashboard.monitor': 'Monitor', 13 | 'menu.dashboard.workplace': 'Ambiente de Trabalho', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': 'Formulário', 18 | 'menu.form.basic-form': 'Formulário Básico', 19 | 'menu.form.step-form': 'Formulário Assistido', 20 | 'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)', 21 | 'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)', 22 | 'menu.form.step-form.result': 'Formulário Assistido(finalizado)', 23 | 'menu.form.advanced-form': 'Formulário Avançado', 24 | 'menu.list': 'Lista', 25 | 'menu.list.table-list': 'Tabela de Busca', 26 | 'menu.list.basic-list': 'Lista Básica', 27 | 'menu.list.card-list': 'Lista de Card', 28 | 'menu.list.search-list': 'Lista de Busca', 29 | 'menu.list.search-list.articles': 'Lista de Busca(artigos)', 30 | 'menu.list.search-list.projects': 'Lista de Busca(projetos)', 31 | 'menu.list.search-list.applications': 'Lista de Busca(aplicações)', 32 | 'menu.profile': 'Perfil', 33 | 'menu.profile.basic': 'Perfil Básico', 34 | 'menu.profile.advanced': 'Perfil Avançado', 35 | 'menu.result': 'Resultado', 36 | 'menu.result.success': 'Sucesso', 37 | 'menu.result.fail': 'Falha', 38 | 'menu.exception': 'Exceção', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': 'Disparar', 43 | 'menu.account': 'Conta', 44 | 'menu.account.center': 'Central da Conta', 45 | 'menu.account.settings': 'Configurar Conta', 46 | 'menu.account.trigger': 'Disparar Erro', 47 | 'menu.account.logout': 'Sair', 48 | 'menu.editor': 'Graphic Editor', 49 | 'menu.editor.flow': 'Flow Editor', 50 | 'menu.editor.mind': 'Mind Editor', 51 | 'menu.editor.koni': 'Koni Editor', 52 | }; 53 | -------------------------------------------------------------------------------- /webui/src/locales/pt-BR/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'Você está offline agora', 3 | 'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível', 4 | 'app.pwa.serviceworker.updated.hint': 5 | 'Por favor, pressione o botão "Atualizar" para recarregar a página atual', 6 | 'app.pwa.serviceworker.updated.ok': 'Atualizar', 7 | }; 8 | -------------------------------------------------------------------------------- /webui/src/locales/pt-BR/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Configuração de estilo da página', 3 | 'app.setting.pagestyle.dark': 'Dark style', 4 | 'app.setting.pagestyle.light': 'Light style', 5 | 'app.setting.content-width': 'Largura do conteúdo', 6 | 'app.setting.content-width.fixed': 'Fixo', 7 | 'app.setting.content-width.fluid': 'Fluido', 8 | 'app.setting.themecolor': 'Cor do Tema', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Modo de Navegação', 18 | 'app.setting.sidemenu': 'Layout do Menu Lateral', 19 | 'app.setting.topmenu': 'Layout do Menu Superior', 20 | 'app.setting.fixedheader': 'Cabeçalho fixo', 21 | 'app.setting.fixedsidebar': 'Barra lateral fixa', 22 | 'app.setting.fixedsidebar.hint': 'Funciona no layout do menu lateral', 23 | 'app.setting.hideheader': 'Esconder o cabeçalho quando rolar', 24 | 'app.setting.hideheader.hint': 'Funciona quando o esconder cabeçalho está abilitado', 25 | 'app.setting.othersettings': 'Outras configurações', 26 | 'app.setting.weakmode': 'Weak Mode', 27 | 'app.setting.copy': 'Copiar Configuração', 28 | 'app.setting.copyinfo': 29 | 'copiado com sucesso,por favor trocar o defaultSettings em src/models/setting.js', 30 | 'app.setting.production.hint': 31 | 'O painel de configuração apenas é exibido no ambiente de desenvolvimento, por favor modifique manualmente o', 32 | }; 33 | -------------------------------------------------------------------------------- /webui/src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import component from './zh-CN/component'; 2 | import globalHeader from './zh-CN/globalHeader'; 3 | import menu from './zh-CN/menu'; 4 | import pwa from './zh-CN/pwa'; 5 | import settingDrawer from './zh-CN/settingDrawer'; 6 | import settings from './zh-CN/settings'; 7 | import pages from './zh-CN/pages'; 8 | 9 | export default { 10 | 'navBar.lang': '语言', 11 | 'layout.user.link.help': '帮助', 12 | 'layout.user.link.privacy': '隐私', 13 | 'layout.user.link.terms': '条款', 14 | 'app.preview.down.block': '下载此页面到本地项目', 15 | 'app.welcome.link.fetch-blocks': '获取全部区块', 16 | 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面', 17 | ...pages, 18 | ...globalHeader, 19 | ...menu, 20 | ...settingDrawer, 21 | ...settings, 22 | ...pwa, 23 | ...component, 24 | }; 25 | -------------------------------------------------------------------------------- /webui/src/locales/zh-CN/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展开', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /webui/src/locales/zh-CN/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站内搜索', 3 | 'component.globalHeader.search.example1': '搜索提示一', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用文档', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '你已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已读完所有消息', 11 | 'component.globalHeader.event': '待办', 12 | 'component.globalHeader.event.empty': '你已完成所有待办', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暂无数据', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /webui/src/locales/zh-CN/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '欢迎', 3 | 'menu.more-blocks': '更多区块', 4 | 'menu.home': '首页', 5 | 'menu.admin': '管理页', 6 | 'menu.admin.sub-page': '二级管理页', 7 | 'menu.login': '登录', 8 | 'menu.register': '注册', 9 | 'menu.register.result': '注册结果', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': '分析页', 12 | 'menu.dashboard.monitor': '监控页', 13 | 'menu.dashboard.workplace': '工作台', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': '表单页', 18 | 'menu.form.basic-form': '基础表单', 19 | 'menu.form.step-form': '分步表单', 20 | 'menu.form.step-form.info': '分步表单(填写转账信息)', 21 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)', 22 | 'menu.form.step-form.result': '分步表单(完成)', 23 | 'menu.form.advanced-form': '高级表单', 24 | 'menu.list': '列表页', 25 | 'menu.list.table-list': '查询表格', 26 | 'menu.list.basic-list': '标准列表', 27 | 'menu.list.card-list': '卡片列表', 28 | 'menu.list.search-list': '搜索列表', 29 | 'menu.list.search-list.articles': '搜索列表(文章)', 30 | 'menu.list.search-list.projects': '搜索列表(项目)', 31 | 'menu.list.search-list.applications': '搜索列表(应用)', 32 | 'menu.profile': '详情页', 33 | 'menu.profile.basic': '基础详情页', 34 | 'menu.profile.advanced': '高级详情页', 35 | 'menu.result': '结果页', 36 | 'menu.result.success': '成功页', 37 | 'menu.result.fail': '失败页', 38 | 'menu.exception': '异常页', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': '触发错误', 43 | 'menu.account': '个人页', 44 | 'menu.account.center': '个人中心', 45 | 'menu.account.settings': '个人设置', 46 | 'menu.account.trigger': '触发报错', 47 | 'menu.account.logout': '退出登录', 48 | 'menu.editor': '图形编辑器', 49 | 'menu.editor.flow': '流程编辑器', 50 | 'menu.editor.mind': '脑图编辑器', 51 | 'menu.editor.koni': '拓扑编辑器', 52 | }; 53 | -------------------------------------------------------------------------------- /webui/src/locales/zh-CN/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '当前处于离线状态', 3 | 'app.pwa.serviceworker.updated': '有新内容', 4 | 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /webui/src/locales/zh-CN/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整体风格设置', 3 | 'app.setting.pagestyle.dark': '暗色菜单风格', 4 | 'app.setting.pagestyle.light': '亮色菜单风格', 5 | 'app.setting.content-width': '内容区域宽度', 6 | 'app.setting.content-width.fixed': '定宽', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主题色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '极光绿', 14 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)', 15 | 'app.setting.themecolor.geekblue': '极客蓝', 16 | 'app.setting.themecolor.purple': '酱紫', 17 | 'app.setting.navigationmode': '导航模式', 18 | 'app.setting.sidemenu': '侧边菜单布局', 19 | 'app.setting.topmenu': '顶部菜单布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定侧边菜单', 22 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置', 23 | 'app.setting.hideheader': '下滑时隐藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 时可配置', 25 | 'app.setting.othersettings': '其他设置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷贝设置', 28 | 'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置', 29 | 'app.setting.production.hint': 30 | '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /webui/src/locales/zh-CN/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本设置', 3 | 'app.settings.menuMap.security': '安全设置', 4 | 'app.settings.menuMap.binding': '账号绑定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '头像', 7 | 'app.settings.basic.change-avatar': '更换头像', 8 | 'app.settings.basic.email': '邮箱', 9 | 'app.settings.basic.email-message': '请输入您的邮箱!', 10 | 'app.settings.basic.nickname': '昵称', 11 | 'app.settings.basic.nickname-message': '请输入您的昵称!', 12 | 'app.settings.basic.profile': '个人简介', 13 | 'app.settings.basic.profile-message': '请输入个人简介!', 14 | 'app.settings.basic.profile-placeholder': '个人简介', 15 | 'app.settings.basic.country': '国家/地区', 16 | 'app.settings.basic.country-message': '请输入您的国家或地区!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '请输入您的街道地址!', 21 | 'app.settings.basic.phone': '联系电话', 22 | 'app.settings.basic.phone-message': '请输入您的联系电话!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '强', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '账户密码', 28 | 'app.settings.security.password-description': '当前密码强度', 29 | 'app.settings.security.phone': '密保手机', 30 | 'app.settings.security.phone-description': '已绑定手机', 31 | 'app.settings.security.question': '密保问题', 32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全', 33 | 'app.settings.security.email': '备用邮箱', 34 | 'app.settings.security.email-description': '已绑定邮箱', 35 | 'app.settings.security.mfa': 'MFA 设备', 36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '设置', 39 | 'app.settings.security.bind': '绑定', 40 | 'app.settings.binding.taobao': '绑定淘宝', 41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号', 42 | 'app.settings.binding.alipay': '绑定支付宝', 43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号', 44 | 'app.settings.binding.dingding': '绑定钉钉', 45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号', 46 | 'app.settings.binding.bind': '绑定', 47 | 'app.settings.notification.password': '账户密码', 48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知', 49 | 'app.settings.notification.messages': '系统消息', 50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知', 51 | 'app.settings.notification.todo': '待办任务', 52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知', 53 | 'app.settings.open': '开', 54 | 'app.settings.close': '关', 55 | }; 56 | -------------------------------------------------------------------------------- /webui/src/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | import component from './zh-TW/component'; 2 | import globalHeader from './zh-TW/globalHeader'; 3 | import menu from './zh-TW/menu'; 4 | import pwa from './zh-TW/pwa'; 5 | import settingDrawer from './zh-TW/settingDrawer'; 6 | import settings from './zh-TW/settings'; 7 | 8 | export default { 9 | 'navBar.lang': '語言', 10 | 'layout.user.link.help': '幫助', 11 | 'layout.user.link.privacy': '隱私', 12 | 'layout.user.link.terms': '條款', 13 | 'app.preview.down.block': '下載此頁面到本地項目', 14 | ...globalHeader, 15 | ...menu, 16 | ...settingDrawer, 17 | ...settings, 18 | ...pwa, 19 | ...component, 20 | }; 21 | -------------------------------------------------------------------------------- /webui/src/locales/zh-TW/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展開', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /webui/src/locales/zh-TW/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站內搜索', 3 | 'component.globalHeader.search.example1': '搜索提示壹', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用手冊', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '妳已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已讀完所有消息', 11 | 'component.globalHeader.event': '待辦', 12 | 'component.globalHeader.event.empty': '妳已完成所有待辦', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暫無資料', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /webui/src/locales/zh-TW/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '歡迎', 3 | 'menu.more-blocks': '更多區塊', 4 | 'menu.home': '首頁', 5 | 'menu.login': '登錄', 6 | 'menu.admin': '权限', 7 | 'menu.admin.sub-page': '二级管理页', 8 | 'menu.exception.403': '403', 9 | 'menu.exception.404': '404', 10 | 'menu.exception.500': '500', 11 | 'menu.register': '註冊', 12 | 'menu.register.result': '註冊結果', 13 | 'menu.dashboard': 'Dashboard', 14 | 'menu.dashboard.analysis': '分析頁', 15 | 'menu.dashboard.monitor': '監控頁', 16 | 'menu.dashboard.workplace': '工作臺', 17 | 'menu.form': '表單頁', 18 | 'menu.form.basic-form': '基礎表單', 19 | 'menu.form.step-form': '分步表單', 20 | 'menu.form.step-form.info': '分步表單(填寫轉賬信息)', 21 | 'menu.form.step-form.confirm': '分步表單(確認轉賬信息)', 22 | 'menu.form.step-form.result': '分步表單(完成)', 23 | 'menu.form.advanced-form': '高級表單', 24 | 'menu.list': '列表頁', 25 | 'menu.list.table-list': '查詢表格', 26 | 'menu.list.basic-list': '標淮列表', 27 | 'menu.list.card-list': '卡片列表', 28 | 'menu.list.search-list': '搜索列表', 29 | 'menu.list.search-list.articles': '搜索列表(文章)', 30 | 'menu.list.search-list.projects': '搜索列表(項目)', 31 | 'menu.list.search-list.applications': '搜索列表(應用)', 32 | 'menu.profile': '詳情頁', 33 | 'menu.profile.basic': '基礎詳情頁', 34 | 'menu.profile.advanced': '高級詳情頁', 35 | 'menu.result': '結果頁', 36 | 'menu.result.success': '成功頁', 37 | 'menu.result.fail': '失敗頁', 38 | 'menu.account': '個人頁', 39 | 'menu.account.center': '個人中心', 40 | 'menu.account.settings': '個人設置', 41 | 'menu.account.trigger': '觸發報錯', 42 | 'menu.account.logout': '退出登錄', 43 | 'menu.exception': '异常页', 44 | 'menu.exception.not-permission': '403', 45 | 'menu.exception.not-find': '404', 46 | 'menu.exception.server-error': '500', 47 | 'menu.exception.trigger': '触发错误', 48 | 'menu.editor': '圖形編輯器', 49 | 'menu.editor.flow': '流程編輯器', 50 | 'menu.editor.mind': '腦圖編輯器', 51 | 'menu.editor.koni': '拓撲編輯器', 52 | }; 53 | -------------------------------------------------------------------------------- /webui/src/locales/zh-TW/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '當前處於離線狀態', 3 | 'app.pwa.serviceworker.updated': '有新內容', 4 | 'app.pwa.serviceworker.updated.hint': '請點擊“刷新”按鈕或者手動刷新頁面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /webui/src/locales/zh-TW/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整體風格設置', 3 | 'app.setting.pagestyle.dark': '暗色菜單風格', 4 | 'app.setting.pagestyle.light': '亮色菜單風格', 5 | 'app.setting.content-width': '內容區域寬度', 6 | 'app.setting.content-width.fixed': '定寬', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主題色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '極光綠', 14 | 'app.setting.themecolor.daybreak': '拂曉藍(默認)', 15 | 'app.setting.themecolor.geekblue': '極客藍', 16 | 'app.setting.themecolor.purple': '醬紫', 17 | 'app.setting.navigationmode': '導航模式', 18 | 'app.setting.sidemenu': '側邊菜單布局', 19 | 'app.setting.topmenu': '頂部菜單布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定側邊菜單', 22 | 'app.setting.fixedsidebar.hint': '側邊菜單布局時可配置', 23 | 'app.setting.hideheader': '下滑時隱藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 時可配置', 25 | 'app.setting.othersettings': '其他設置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷貝設置', 28 | 'app.setting.copyinfo': '拷貝成功,請到 src/defaultSettings.js 中替換默認配置', 29 | 'app.setting.production.hint': 30 | '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /webui/src/locales/zh-TW/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本設置', 3 | 'app.settings.menuMap.security': '安全設置', 4 | 'app.settings.menuMap.binding': '賬號綁定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '頭像', 7 | 'app.settings.basic.change-avatar': '更換頭像', 8 | 'app.settings.basic.email': '郵箱', 9 | 'app.settings.basic.email-message': '請輸入您的郵箱!', 10 | 'app.settings.basic.nickname': '昵稱', 11 | 'app.settings.basic.nickname-message': '請輸入您的昵稱!', 12 | 'app.settings.basic.profile': '個人簡介', 13 | 'app.settings.basic.profile-message': '請輸入個人簡介!', 14 | 'app.settings.basic.profile-placeholder': '個人簡介', 15 | 'app.settings.basic.country': '國家/地區', 16 | 'app.settings.basic.country-message': '請輸入您的國家或地區!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '請輸入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '請輸入您的街道地址!', 21 | 'app.settings.basic.phone': '聯系電話', 22 | 'app.settings.basic.phone-message': '請輸入您的聯系電話!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '強', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '賬戶密碼', 28 | 'app.settings.security.password-description': '當前密碼強度', 29 | 'app.settings.security.phone': '密保手機', 30 | 'app.settings.security.phone-description': '已綁定手機', 31 | 'app.settings.security.question': '密保問題', 32 | 'app.settings.security.question-description': '未設置密保問題,密保問題可有效保護賬戶安全', 33 | 'app.settings.security.email': '備用郵箱', 34 | 'app.settings.security.email-description': '已綁定郵箱', 35 | 'app.settings.security.mfa': 'MFA 設備', 36 | 'app.settings.security.mfa-description': '未綁定 MFA 設備,綁定後,可以進行二次確認', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '設置', 39 | 'app.settings.security.bind': '綁定', 40 | 'app.settings.binding.taobao': '綁定淘寶', 41 | 'app.settings.binding.taobao-description': '當前未綁定淘寶賬號', 42 | 'app.settings.binding.alipay': '綁定支付寶', 43 | 'app.settings.binding.alipay-description': '當前未綁定支付寶賬號', 44 | 'app.settings.binding.dingding': '綁定釘釘', 45 | 'app.settings.binding.dingding-description': '當前未綁定釘釘賬號', 46 | 'app.settings.binding.bind': '綁定', 47 | 'app.settings.notification.password': '賬戶密碼', 48 | 'app.settings.notification.password-description': '其他用戶的消息將以站內信的形式通知', 49 | 'app.settings.notification.messages': '系統消息', 50 | 'app.settings.notification.messages-description': '系統消息將以站內信的形式通知', 51 | 'app.settings.notification.todo': '待辦任務', 52 | 'app.settings.notification.todo-description': '待辦任務將以站內信的形式通知', 53 | 'app.settings.open': '開', 54 | 'app.settings.close': '關', 55 | }; 56 | -------------------------------------------------------------------------------- /webui/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ant Design Pro", 3 | "short_name": "Ant Design Pro", 4 | "display": "standalone", 5 | "start_url": "./?utm_source=homescreen", 6 | "theme_color": "#002140", 7 | "background_color": "#001529", 8 | "icons": [ 9 | { 10 | "src": "icons/icon-192x192.png", 11 | "sizes": "192x192" 12 | }, 13 | { 14 | "src": "icons/icon-128x128.png", 15 | "sizes": "128x128" 16 | }, 17 | { 18 | "src": "icons/icon-512x512.png", 19 | "sizes": "512x512" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /webui/src/models/connect.d.ts: -------------------------------------------------------------------------------- 1 | import type { MenuDataItem, Settings as ProSettings } from '@ant-design/pro-layout'; 2 | import { GlobalModelState } from './global'; 3 | import { UserModelState } from './user'; 4 | import type { StateType } from './login'; 5 | 6 | export { GlobalModelState, UserModelState }; 7 | 8 | export type Loading = { 9 | global: boolean; 10 | effects: Record; 11 | models: { 12 | global?: boolean; 13 | menu?: boolean; 14 | setting?: boolean; 15 | user?: boolean; 16 | login?: boolean; 17 | }; 18 | }; 19 | 20 | export type ConnectState = { 21 | global: GlobalModelState; 22 | loading: Loading; 23 | settings: ProSettings; 24 | user: UserModelState; 25 | login: StateType; 26 | }; 27 | 28 | export type Route = { 29 | routes?: Route[]; 30 | } & MenuDataItem; 31 | -------------------------------------------------------------------------------- /webui/src/models/login.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from 'querystring'; 2 | import type { Reducer, Effect } from 'umi'; 3 | import { history } from 'umi'; 4 | 5 | import { fakeAccountLogin } from '@/services/login'; 6 | import { setAuthority } from '@/utils/authority'; 7 | import { getPageQuery } from '@/utils/utils'; 8 | import { message } from 'antd'; 9 | 10 | export type StateType = { 11 | status?: 'ok' | 'error'; 12 | type?: string; 13 | currentAuthority?: 'user' | 'guest' | 'admin'; 14 | }; 15 | 16 | export type LoginModelType = { 17 | namespace: string; 18 | state: StateType; 19 | effects: { 20 | login: Effect; 21 | logout: Effect; 22 | }; 23 | reducers: { 24 | changeLoginStatus: Reducer; 25 | }; 26 | }; 27 | 28 | const Model: LoginModelType = { 29 | namespace: 'login', 30 | 31 | state: { 32 | status: undefined, 33 | }, 34 | 35 | effects: { 36 | *login({ payload }, { call, put }) { 37 | const response = yield call(fakeAccountLogin, payload); 38 | yield put({ 39 | type: 'changeLoginStatus', 40 | payload: response, 41 | }); 42 | // Login successfully 43 | if (response.status === 'ok') { 44 | const urlParams = new URL(window.location.href); 45 | const params = getPageQuery(); 46 | message.success('🎉 🎉 🎉 登录成功!'); 47 | let { redirect } = params as { redirect: string }; 48 | if (redirect) { 49 | const redirectUrlParams = new URL(redirect); 50 | if (redirectUrlParams.origin === urlParams.origin) { 51 | redirect = redirect.substr(urlParams.origin.length); 52 | if (redirect.match(/^\/.*#/)) { 53 | redirect = redirect.substr(redirect.indexOf('#') + 1); 54 | } 55 | } else { 56 | window.location.href = '/'; 57 | return; 58 | } 59 | } 60 | history.replace(redirect || '/'); 61 | } 62 | }, 63 | 64 | logout() { 65 | const { redirect } = getPageQuery(); 66 | // Note: There may be security issues, please note 67 | if (window.location.pathname !== '/user/login' && !redirect) { 68 | history.replace({ 69 | pathname: '/user/login', 70 | search: stringify({ 71 | redirect: window.location.href, 72 | }), 73 | }); 74 | } 75 | }, 76 | }, 77 | 78 | reducers: { 79 | changeLoginStatus(state, { payload }) { 80 | setAuthority(payload.currentAuthority); 81 | return { 82 | ...state, 83 | status: payload.status, 84 | type: payload.type, 85 | }; 86 | }, 87 | }, 88 | }; 89 | 90 | export default Model; 91 | -------------------------------------------------------------------------------- /webui/src/models/setting.ts: -------------------------------------------------------------------------------- 1 | import type { Reducer } from 'umi'; 2 | import type { DefaultSettings } from '../../config/defaultSettings'; 3 | import defaultSettings from '../../config/defaultSettings'; 4 | 5 | export type SettingModelType = { 6 | namespace: 'settings'; 7 | state: DefaultSettings; 8 | reducers: { 9 | changeSetting: Reducer; 10 | }; 11 | }; 12 | 13 | const updateColorWeak: (colorWeak: boolean) => void = (colorWeak) => { 14 | const root = document.getElementById('root'); 15 | if (root) { 16 | root.className = colorWeak ? 'colorWeak' : ''; 17 | } 18 | }; 19 | 20 | const SettingModel: SettingModelType = { 21 | namespace: 'settings', 22 | state: defaultSettings, 23 | reducers: { 24 | changeSetting(state = defaultSettings, { payload }) { 25 | const { colorWeak, contentWidth } = payload; 26 | 27 | if (state.contentWidth !== contentWidth && window.dispatchEvent) { 28 | window.dispatchEvent(new Event('resize')); 29 | } 30 | updateColorWeak(!!colorWeak); 31 | return { 32 | ...state, 33 | ...payload, 34 | }; 35 | }, 36 | }, 37 | }; 38 | export default SettingModel; 39 | -------------------------------------------------------------------------------- /webui/src/models/user.ts: -------------------------------------------------------------------------------- 1 | import type { Effect, Reducer } from 'umi'; 2 | 3 | import { queryCurrent, query as queryUsers } from '@/services/user'; 4 | 5 | export type CurrentUser = { 6 | avatar?: string; 7 | name?: string; 8 | title?: string; 9 | group?: string; 10 | signature?: string; 11 | tags?: { 12 | key: string; 13 | label: string; 14 | }[]; 15 | userid?: string; 16 | unreadCount?: number; 17 | }; 18 | 19 | export type UserModelState = { 20 | currentUser?: CurrentUser; 21 | }; 22 | 23 | export type UserModelType = { 24 | namespace: 'user'; 25 | state: UserModelState; 26 | effects: { 27 | fetch: Effect; 28 | fetchCurrent: Effect; 29 | }; 30 | reducers: { 31 | saveCurrentUser: Reducer; 32 | changeNotifyCount: Reducer; 33 | }; 34 | }; 35 | 36 | const UserModel: UserModelType = { 37 | namespace: 'user', 38 | 39 | state: { 40 | currentUser: {}, 41 | }, 42 | 43 | effects: { 44 | *fetch(_, { call, put }) { 45 | const response = yield call(queryUsers); 46 | yield put({ 47 | type: 'save', 48 | payload: response, 49 | }); 50 | }, 51 | *fetchCurrent(_, { call, put }) { 52 | const response = yield call(queryCurrent); 53 | yield put({ 54 | type: 'saveCurrentUser', 55 | payload: response, 56 | }); 57 | }, 58 | }, 59 | 60 | reducers: { 61 | saveCurrentUser(state, action) { 62 | return { 63 | ...state, 64 | currentUser: action.payload || {}, 65 | }; 66 | }, 67 | changeNotifyCount( 68 | state = { 69 | currentUser: {}, 70 | }, 71 | action, 72 | ) { 73 | return { 74 | ...state, 75 | currentUser: { 76 | ...state.currentUser, 77 | notifyCount: action.payload.totalCount, 78 | unreadCount: action.payload.unreadCount, 79 | }, 80 | }; 81 | }, 82 | }, 83 | }; 84 | 85 | export default UserModel; 86 | -------------------------------------------------------------------------------- /webui/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import React from 'react'; 3 | import { history } from 'umi'; 4 | 5 | const NoFoundPage: React.FC = () => ( 6 | history.push('/')}> 12 | Back Home 13 | 14 | } 15 | /> 16 | ); 17 | 18 | export default NoFoundPage; 19 | -------------------------------------------------------------------------------- /webui/src/pages/Admin.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons'; 3 | import { Card, Typography, Alert } from 'antd'; 4 | import { PageHeaderWrapper } from '@ant-design/pro-layout'; 5 | import { useIntl } from 'umi'; 6 | 7 | export default (): React.ReactNode => { 8 | const intl = useIntl(); 9 | return ( 10 | 16 | 17 | 30 | 31 | Ant Design Pro You 32 | 33 | 34 |

35 | Want to add more pages? Please refer to{' '} 36 | 37 | use block 38 | 39 | 。 40 |

41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /webui/src/pages/Projects/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Input, Modal, InputNumber } from "antd"; 2 | import React, { useEffect, useState } from "react"; 3 | import MonacoEditor from "react-monaco-editor"; 4 | 5 | interface ListFormProps { 6 | modalVisible: boolean; 7 | formTitle: string; 8 | initialValues: {}; 9 | onSubmit: () => void; 10 | onCancel: () => void; 11 | } 12 | 13 | const formLayout = { 14 | labelCol: { span: 4 }, 15 | wrapperCol: { span: 20 }, 16 | }; 17 | 18 | const ListForm: React.FC = (props) => { 19 | const { modalVisible, onCancel, onSubmit, initialValues, formTitle } = props; 20 | const [form] = Form.useForm(); 21 | 22 | useEffect(() => { 23 | if (initialValues) { 24 | form.resetFields(); 25 | form.setFieldsValue({ 26 | ...initialValues, 27 | }); 28 | } 29 | }, [initialValues]); 30 | 31 | const handleSubmit = () => { 32 | if (!form) return; 33 | form.submit(); 34 | }; 35 | 36 | const modalFooter = { 37 | okText: "保存", 38 | onOk: handleSubmit, 39 | onCancel, 40 | }; 41 | 42 | return ( 43 | 50 |
51 | 54 | 57 | 68 | 69 |
70 |
71 | ); 72 | }; 73 | export default ListForm; 74 | -------------------------------------------------------------------------------- /webui/src/pages/Projects/components/Render.tsx: -------------------------------------------------------------------------------- 1 | import {Form, Input, Modal, InputNumber, message} from "antd"; 2 | import React, { useEffect, useState } from "react"; 3 | import MonacoEditor from "react-monaco-editor"; 4 | import api from "@/services/api"; 5 | 6 | interface ListFormProps { 7 | modalVisible: boolean; 8 | formTitle: string; 9 | initialValues: {}; 10 | onSubmit: () => void; 11 | onCancel: () => void; 12 | } 13 | 14 | const formLayout = { 15 | labelCol: { span: 4 }, 16 | wrapperCol: { span: 20 }, 17 | }; 18 | 19 | const ListForm: React.FC = (props) => { 20 | const { modalVisible, onCancel, onSubmit, initialValues, formTitle } = props; 21 | const [form] = Form.useForm(); 22 | 23 | useEffect(() => { 24 | if (initialValues && initialValues.path != undefined) { 25 | form.resetFields(); 26 | api.ProjectRender({path:initialValues.path}).then((res) => { 27 | if (res.code !== 0) { 28 | message.error(res.msg); 29 | return false; 30 | } 31 | form.setFieldsValue({ 32 | ...initialValues, 33 | render: JSON.stringify(res.data, null, 2), 34 | }); 35 | return true; 36 | }); 37 | 38 | form.setFieldsValue({ 39 | ...initialValues, 40 | }); 41 | } 42 | }, [initialValues]); 43 | 44 | const modalFooter = { 45 | okText: "OK", 46 | onOk: onCancel, 47 | onCancel, 48 | }; 49 | 50 | return ( 51 | 58 |
59 | 62 | 65 | 76 | 77 |
78 |
79 | ); 80 | }; 81 | export default ListForm; 82 | -------------------------------------------------------------------------------- /webui/src/pages/Templates/components/ListForm.tsx: -------------------------------------------------------------------------------- 1 | import {Form, Input, Modal} from 'antd'; 2 | import React, {useEffect} from "react"; 3 | 4 | interface ListFormProps { 5 | modalVisible: boolean; 6 | formTitle: string; 7 | initialValues: {}; 8 | onSubmit: () => void; 9 | onCancel: () => void; 10 | } 11 | 12 | const formLayout = { 13 | labelCol: {span: 6}, 14 | wrapperCol: {span: 18}, 15 | }; 16 | 17 | const ListForm: React.FC = (props) => { 18 | const {modalVisible, onCancel, onSubmit, initialValues, formTitle} = props; 19 | const [form] = Form.useForm(); 20 | 21 | useEffect(() => { 22 | if (initialValues) { 23 | form.resetFields(); 24 | form.setFieldsValue({ 25 | ...initialValues, 26 | }); 27 | } 28 | }, [initialValues]); 29 | 30 | const handleSubmit = () => { 31 | if (!form) return; 32 | form.submit(); 33 | }; 34 | 35 | const modalFooter = {okText: '保存', onOk: handleSubmit, onCancel} 36 | 37 | return ( 38 | 45 |
51 | 55 | 56 | 57 | 61 | 62 | 63 | {initialValues.mode !== "create" && 67 | 68 | } 69 |
70 |
71 | ); 72 | }; 73 | export default ListForm; 74 | 75 | -------------------------------------------------------------------------------- /webui/src/pages/User/login/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .main { 4 | width: 328px; 5 | margin: 0 auto; 6 | @media screen and (max-width: @screen-sm) { 7 | width: 95%; 8 | max-width: 328px; 9 | } 10 | 11 | :global { 12 | .@{ant-prefix}-tabs-nav-list { 13 | margin: auto; 14 | font-size: 16px; 15 | } 16 | } 17 | 18 | .icon { 19 | margin-left: 16px; 20 | color: rgba(0, 0, 0, 0.2); 21 | font-size: 24px; 22 | vertical-align: middle; 23 | cursor: pointer; 24 | transition: color 0.3s; 25 | 26 | &:hover { 27 | color: @primary-color; 28 | } 29 | } 30 | 31 | .other { 32 | margin-top: 24px; 33 | line-height: 22px; 34 | text-align: left; 35 | .register { 36 | float: right; 37 | } 38 | } 39 | 40 | .prefixIcon { 41 | color: @primary-color; 42 | font-size: @font-size-base; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /webui/src/pages/Welcome.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | 3 | .pre { 4 | margin: 12px 0; 5 | padding: 12px 20px; 6 | background: @input-bg; 7 | box-shadow: @card-shadow; 8 | } 9 | -------------------------------------------------------------------------------- /webui/src/pages/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PageContainer } from '@ant-design/pro-layout'; 3 | import { Card, Alert, Typography } from 'antd'; 4 | import { useIntl, FormattedMessage } from 'umi'; 5 | import styles from './Welcome.less'; 6 | 7 | const CodePreview: React.FC = ({ children }) => ( 8 |
 9 |     
10 |       {children}
11 |     
12 |   
13 | ); 14 | 15 | export default (): React.ReactNode => { 16 | const intl = useIntl(); 17 | return ( 18 | 19 | 20 | 33 | 34 | {' '} 35 | 40 | 41 | 42 | 43 | yarn add @ant-design/pro-table 44 | 50 | {' '} 51 | 56 | 57 | 58 | 59 | yarn add @ant-design/pro-layout 60 | 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /webui/src/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable no-restricted-globals */ 3 | /* eslint-disable no-underscore-dangle */ 4 | /* globals workbox */ 5 | workbox.core.setCacheNameDetails({ 6 | prefix: 'antd-pro', 7 | suffix: 'v1', 8 | }); 9 | // Control all opened tabs ASAP 10 | workbox.clientsClaim(); 11 | 12 | /** 13 | * Use precaching list generated by workbox in build process. 14 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching 15 | */ 16 | workbox.precaching.precacheAndRoute(self.__precacheManifest || []); 17 | 18 | /** 19 | * Register a navigation route. 20 | * https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route 21 | */ 22 | workbox.routing.registerNavigationRoute('/index.html'); 23 | 24 | /** 25 | * Use runtime cache: 26 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute 27 | * 28 | * Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc. 29 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies 30 | */ 31 | 32 | /** Handle API requests */ 33 | workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst()); 34 | 35 | /** Handle third party requests */ 36 | workbox.routing.registerRoute( 37 | /^https:\/\/gw\.alipayobjects\.com\//, 38 | workbox.strategies.networkFirst(), 39 | ); 40 | workbox.routing.registerRoute( 41 | /^https:\/\/cdnjs\.cloudflare\.com\//, 42 | workbox.strategies.networkFirst(), 43 | ); 44 | workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst()); 45 | 46 | /** Response to client after skipping waiting with MessageChannel */ 47 | addEventListener('message', (event) => { 48 | const replyPort = event.ports[0]; 49 | const message = event.data; 50 | if (replyPort && message && message.type === 'skip-waiting') { 51 | event.waitUntil( 52 | self.skipWaiting().then( 53 | () => { 54 | replyPort.postMessage({ 55 | error: null, 56 | }); 57 | }, 58 | (error) => { 59 | replyPort.postMessage({ 60 | error, 61 | }); 62 | }, 63 | ), 64 | ); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /webui/src/services/api.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export default { 4 | ProjectList: async (params: any) => { 5 | return request("/api/projects", { 6 | method: "GET", 7 | params, 8 | }); 9 | }, 10 | ProjectGen: async (params: any) => { 11 | return request(`/api/projects/gen`, { 12 | method: "GET", 13 | params: { 14 | path: params.path, 15 | }, 16 | }); 17 | }, 18 | ProjectRender: async (params: any) => { 19 | return request(`/api/projects/render`, { 20 | method: "GET", 21 | params: { 22 | path: params.path, 23 | }, 24 | }); 25 | }, 26 | ProjectCreate: async (params: any) => { 27 | return request("/api/projects", { 28 | method: "POST", 29 | data: params, 30 | }); 31 | }, 32 | ProjectUpdate: async (params: any) => { 33 | return request(`/api/projects`, { 34 | method: "PUT", 35 | data: params, 36 | }); 37 | }, 38 | ProjectUpdateDSL: async (params: any) => { 39 | return request(`/api/projects/dsl`, { 40 | method: "PUT", 41 | data: params, 42 | }); 43 | }, 44 | ProjectDelete: async (params: any) => { 45 | return request(`/api/projects`, { 46 | method: "DELETE", 47 | data: params, 48 | }); 49 | }, 50 | TemplateList: async (params: any) => { 51 | return request("/api/templates", { 52 | method: "GET", 53 | params, 54 | }); 55 | }, 56 | TemplateSelect: async (params: any) => { 57 | return request("/api/templates/select", { 58 | method: "GET", 59 | params, 60 | }); 61 | }, 62 | TemplateCreate: async (params: any) => { 63 | return request("/api/templates", { 64 | method: "POST", 65 | data: params, 66 | }); 67 | }, 68 | TemplateUpdate: async (params: any) => { 69 | return request(`/api/templates`, { 70 | method: "PUT", 71 | data: params, 72 | }); 73 | }, 74 | TemplateSync: async (params: any) => { 75 | return request(`/api/templates/sync`, { 76 | method: "PUT", 77 | data: params, 78 | }); 79 | }, 80 | TemplateDelete: async (params: any) => { 81 | return request(`/api/templates`, { 82 | method: "DELETE", 83 | data: params, 84 | }); 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /webui/src/services/login.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export type LoginParamsType = { 4 | userName: string; 5 | password: string; 6 | mobile: string; 7 | captcha: string; 8 | }; 9 | 10 | export async function fakeAccountLogin(params: LoginParamsType) { 11 | return request('/api/login/account', { 12 | method: 'POST', 13 | data: params, 14 | }); 15 | } 16 | 17 | export async function getFakeCaptcha(mobile: string) { 18 | return request(`/api/login/captcha?mobile=${mobile}`); 19 | } 20 | -------------------------------------------------------------------------------- /webui/src/services/user.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export async function query(): Promise { 4 | return request('/api/users'); 5 | } 6 | 7 | export async function queryCurrent(): Promise { 8 | return request('/api/currentUser'); 9 | } 10 | 11 | export async function queryNotices(): Promise { 12 | return request('/api/notices'); 13 | } 14 | -------------------------------------------------------------------------------- /webui/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'slash2'; 2 | declare module '*.css'; 3 | declare module '*.less'; 4 | declare module '*.scss'; 5 | declare module '*.sass'; 6 | declare module '*.svg'; 7 | declare module '*.png'; 8 | declare module '*.jpg'; 9 | declare module '*.jpeg'; 10 | declare module '*.gif'; 11 | declare module '*.bmp'; 12 | declare module '*.tiff'; 13 | declare module 'omit.js'; 14 | 15 | // google analytics interface 16 | type GAFieldsObject = { 17 | eventCategory: string; 18 | eventAction: string; 19 | eventLabel?: string; 20 | eventValue?: number; 21 | nonInteraction?: boolean; 22 | }; 23 | 24 | interface Window { 25 | ga: ( 26 | command: 'send', 27 | hitType: 'event' | 'pageview', 28 | fieldsObject: GAFieldsObject | string, 29 | ) => void; 30 | reloadAuthorized: () => void; 31 | } 32 | 33 | declare let ga: () => void; 34 | 35 | // preview.pro.ant.design only do not use in your production ; 36 | // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。 37 | declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined; 38 | 39 | declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false; 40 | -------------------------------------------------------------------------------- /webui/src/utils/Authorized.ts: -------------------------------------------------------------------------------- 1 | import RenderAuthorize from '@/components/Authorized'; 2 | import { getAuthority } from './authority'; 3 | /* eslint-disable eslint-comments/disable-enable-pair */ 4 | /* eslint-disable import/no-mutable-exports */ 5 | let Authorized = RenderAuthorize(getAuthority()); 6 | 7 | // Reload the rights component 8 | const reloadAuthorized = (): void => { 9 | Authorized = RenderAuthorize(getAuthority()); 10 | }; 11 | 12 | /** Hard code block need it。 */ 13 | window.reloadAuthorized = reloadAuthorized; 14 | 15 | export { reloadAuthorized }; 16 | export default Authorized; 17 | -------------------------------------------------------------------------------- /webui/src/utils/authority.ts: -------------------------------------------------------------------------------- 1 | import { reloadAuthorized } from './Authorized'; 2 | 3 | // use localStorage to store the authority info, which might be sent from server in actual project. 4 | export function getAuthority(str?: string): string | string[] { 5 | const authorityString = 6 | typeof str === 'undefined' && localStorage ? localStorage.getItem('antd-pro-authority') : str; 7 | // authorityString could be admin, "admin", ["admin"] 8 | let authority; 9 | try { 10 | if (authorityString) { 11 | authority = JSON.parse(authorityString); 12 | } 13 | } catch (e) { 14 | authority = authorityString; 15 | } 16 | if (typeof authority === 'string') { 17 | return [authority]; 18 | } 19 | // preview.pro.ant.design only do not use in your production. 20 | // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。 21 | if (!authority && ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') { 22 | return ['admin']; 23 | } 24 | return authority; 25 | } 26 | 27 | export function setAuthority(authority: string | string[]): void { 28 | const proAuthority = typeof authority === 'string' ? [authority] : authority; 29 | localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority)); 30 | // auto reload 31 | reloadAuthorized(); 32 | } 33 | -------------------------------------------------------------------------------- /webui/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | /** Request 网络请求工具 更详细的 api 文档: https://github.com/umijs/umi-request */ 2 | import { extend } from 'umi-request'; 3 | import { notification } from 'antd'; 4 | 5 | const codeMessage = { 6 | 200: '服务器成功返回请求的数据。', 7 | 201: '新建或修改数据成功。', 8 | 202: '一个请求已经进入后台排队(异步任务)。', 9 | 204: '删除数据成功。', 10 | 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', 11 | 401: '用户没有权限(令牌、用户名、密码错误)。', 12 | 403: '用户得到授权,但是访问是被禁止的。', 13 | 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', 14 | 406: '请求的格式不可得。', 15 | 410: '请求的资源被永久删除,且不会再得到的。', 16 | 422: '当创建一个对象时,发生一个验证错误。', 17 | 500: '服务器发生错误,请检查服务器。', 18 | 502: '网关错误。', 19 | 503: '服务不可用,服务器暂时过载或维护。', 20 | 504: '网关超时。', 21 | }; 22 | 23 | /** 异常处理程序 */ 24 | const errorHandler = (error: { response: Response }): Response => { 25 | const { response } = error; 26 | if (response && response.status) { 27 | const errorText = codeMessage[response.status] || response.statusText; 28 | const { status, url } = response; 29 | 30 | notification.error({ 31 | message: `请求错误 ${status}: ${url}`, 32 | description: errorText, 33 | }); 34 | } else if (!response) { 35 | notification.error({ 36 | description: '您的网络发生异常,无法连接服务器', 37 | message: '网络异常', 38 | }); 39 | } 40 | return response; 41 | }; 42 | 43 | /** 配置request请求时的默认参数 */ 44 | const request = extend({ 45 | errorHandler, // 默认错误处理 46 | credentials: 'include', // 默认请求是否带上cookie 47 | }); 48 | 49 | export default request; 50 | -------------------------------------------------------------------------------- /webui/src/utils/utils.less: -------------------------------------------------------------------------------- 1 | // mixins for clearfix 2 | // ------------------------ 3 | .clearfix() { 4 | zoom: 1; 5 | &::before, 6 | &::after { 7 | display: table; 8 | content: ' '; 9 | } 10 | &::after { 11 | clear: both; 12 | height: 0; 13 | font-size: 0; 14 | visibility: hidden; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /webui/src/utils/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { isUrl } from './utils'; 2 | 3 | describe('isUrl tests', (): void => { 4 | it('should return false for invalid and corner case inputs', (): void => { 5 | expect(isUrl([] as any)).toBeFalsy(); 6 | expect(isUrl({} as any)).toBeFalsy(); 7 | expect(isUrl(false as any)).toBeFalsy(); 8 | expect(isUrl(true as any)).toBeFalsy(); 9 | expect(isUrl(NaN as any)).toBeFalsy(); 10 | expect(isUrl(null as any)).toBeFalsy(); 11 | expect(isUrl(undefined as any)).toBeFalsy(); 12 | expect(isUrl('')).toBeFalsy(); 13 | }); 14 | 15 | it('should return false for invalid URLs', (): void => { 16 | expect(isUrl('foo')).toBeFalsy(); 17 | expect(isUrl('bar')).toBeFalsy(); 18 | expect(isUrl('bar/test')).toBeFalsy(); 19 | expect(isUrl('http:/example.com/')).toBeFalsy(); 20 | expect(isUrl('ttp://example.com/')).toBeFalsy(); 21 | }); 22 | 23 | it('should return true for valid URLs', (): void => { 24 | expect(isUrl('http://example.com/')).toBeTruthy(); 25 | expect(isUrl('https://example.com/')).toBeTruthy(); 26 | expect(isUrl('http://example.com/test/123')).toBeTruthy(); 27 | expect(isUrl('https://example.com/test/123')).toBeTruthy(); 28 | expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy(); 29 | expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy(); 30 | expect(isUrl('http://www.example.com/')).toBeTruthy(); 31 | expect(isUrl('https://www.example.com/')).toBeTruthy(); 32 | expect(isUrl('http://www.example.com/test/123')).toBeTruthy(); 33 | expect(isUrl('https://www.example.com/test/123')).toBeTruthy(); 34 | expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy(); 35 | expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /webui/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'querystring'; 2 | 3 | /* eslint no-useless-escape:0 import/prefer-default-export:0 */ 4 | const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/; 5 | 6 | export const isUrl = (path: string): boolean => reg.test(path); 7 | 8 | export const isAntDesignPro = (): boolean => { 9 | if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') { 10 | return true; 11 | } 12 | return window.location.hostname === 'preview.pro.ant.design'; 13 | }; 14 | 15 | // 给官方演示站点用,用于关闭真实开发环境不需要使用的特性 16 | export const isAntDesignProOrDev = (): boolean => { 17 | const { NODE_ENV } = process.env; 18 | if (NODE_ENV === 'development') { 19 | return true; 20 | } 21 | return isAntDesignPro(); 22 | }; 23 | 24 | export const getPageQuery = () => parse(window.location.href.split('?')[1]); 25 | -------------------------------------------------------------------------------- /webui/tests/PuppeteerEnvironment.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | const NodeEnvironment = require('jest-environment-node'); 3 | const getBrowser = require('./getBrowser'); 4 | 5 | class PuppeteerEnvironment extends NodeEnvironment { 6 | // Jest is not available here, so we have to reverse engineer 7 | // the setTimeout function, see https://github.com/facebook/jest/blob/v23.1.0/packages/jest-runtime/src/index.js#L823 8 | setTimeout(timeout) { 9 | if (this.global.jasmine) { 10 | // eslint-disable-next-line no-underscore-dangle 11 | this.global.jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout; 12 | } else { 13 | this.global[Symbol.for('TEST_TIMEOUT_SYMBOL')] = timeout; 14 | } 15 | } 16 | 17 | async setup() { 18 | const browser = await getBrowser(); 19 | const page = await browser.newPage(); 20 | this.global.browser = browser; 21 | this.global.page = page; 22 | } 23 | 24 | async teardown() { 25 | const { page, browser } = this.global; 26 | 27 | if (page) { 28 | await page.close(); 29 | } 30 | 31 | if (browser) { 32 | await browser.disconnect(); 33 | } 34 | 35 | if (browser) { 36 | await browser.close(); 37 | } 38 | } 39 | } 40 | 41 | module.exports = PuppeteerEnvironment; 42 | -------------------------------------------------------------------------------- /webui/tests/beforeTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | const { execSync } = require('child_process'); 4 | const { join } = require('path'); 5 | const findChrome = require('carlo/lib/find_chrome'); 6 | const detectInstaller = require('detect-installer'); 7 | 8 | const installPuppeteer = () => { 9 | // find can use package manger 10 | const packages = detectInstaller(join(__dirname, '../../')); 11 | // get installed package manger 12 | const packageName = packages.find(detectInstaller.hasPackageCommand) || 'npm'; 13 | console.log(`🤖 will use ${packageName} install puppeteer`); 14 | const command = `${packageName} ${packageName.includes('yarn') ? 'add' : 'i'} puppeteer`; 15 | execSync(command, { 16 | stdio: 'inherit', 17 | }); 18 | }; 19 | 20 | const initPuppeteer = async () => { 21 | try { 22 | // eslint-disable-next-line import/no-unresolved 23 | const findChromePath = await findChrome({}); 24 | const { executablePath } = findChromePath; 25 | console.log(`🧲 find you browser in ${executablePath}`); 26 | return; 27 | } catch (error) { 28 | console.log('🧲 no find chrome'); 29 | } 30 | 31 | try { 32 | require.resolve('puppeteer'); 33 | } catch (error) { 34 | // need install puppeteer 35 | await installPuppeteer(); 36 | } 37 | }; 38 | 39 | initPuppeteer(); 40 | -------------------------------------------------------------------------------- /webui/tests/getBrowser.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | const findChrome = require('carlo/lib/find_chrome'); 4 | 5 | const getBrowser = async () => { 6 | try { 7 | // eslint-disable-next-line import/no-unresolved 8 | const puppeteer = require('puppeteer'); 9 | const browser = await puppeteer.launch({ 10 | args: [ 11 | '--disable-gpu', 12 | '--disable-dev-shm-usage', 13 | '--no-first-run', 14 | '--no-zygote', 15 | '--no-sandbox', 16 | ], 17 | }); 18 | return browser; 19 | } catch (error) { 20 | // console.log(error) 21 | } 22 | 23 | try { 24 | // eslint-disable-next-line import/no-unresolved 25 | const puppeteer = require('puppeteer-core'); 26 | const findChromePath = await findChrome({}); 27 | const { executablePath } = findChromePath; 28 | const browser = await puppeteer.launch({ 29 | executablePath, 30 | args: [ 31 | '--disable-gpu', 32 | '--disable-dev-shm-usage', 33 | '--no-first-run', 34 | '--no-zygote', 35 | '--no-sandbox', 36 | ], 37 | }); 38 | return browser; 39 | } catch (error) { 40 | console.log('🧲 no find chrome'); 41 | } 42 | throw new Error('no find puppeteer'); 43 | }; 44 | 45 | module.exports = getBrowser; 46 | -------------------------------------------------------------------------------- /webui/tests/run-tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | /* eslint-disable eslint-comments/no-unlimited-disable */ 4 | const { spawn } = require('child_process'); 5 | // eslint-disable-next-line import/no-extraneous-dependencies 6 | const { kill } = require('cross-port-killer'); 7 | 8 | const env = Object.create(process.env); 9 | env.BROWSER = 'none'; 10 | env.TEST = true; 11 | env.UMI_UI = 'none'; 12 | env.PROGRESS = 'none'; 13 | // flag to prevent multiple test 14 | let once = false; 15 | 16 | const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['start'], { 17 | env, 18 | }); 19 | 20 | startServer.stderr.on('data', (data) => { 21 | // eslint-disable-next-line 22 | console.log(data.toString()); 23 | }); 24 | 25 | startServer.on('exit', () => { 26 | kill(process.env.PORT || 8000); 27 | }); 28 | 29 | console.log('Starting development server for e2e tests...'); 30 | startServer.stdout.on('data', (data) => { 31 | console.log(data.toString()); 32 | // hack code , wait umi 33 | if ( 34 | (!once && data.toString().indexOf('Compiled successfully') >= 0) || 35 | data.toString().indexOf('Theme generated successfully') >= 0 36 | ) { 37 | // eslint-disable-next-line 38 | once = true; 39 | console.log('Development server is started, ready to run tests.'); 40 | const testCmd = spawn( 41 | /^win/.test(process.platform) ? 'npm.cmd' : 'npm', 42 | ['test', '--', '--maxWorkers=1', '--runInBand'], 43 | { 44 | stdio: 'inherit', 45 | }, 46 | ); 47 | testCmd.on('exit', (code) => { 48 | startServer.kill(); 49 | process.exit(code); 50 | }); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "sourceMap": true, 8 | "baseUrl": ".", 9 | "jsx": "preserve", 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noUnusedLocals": true, 16 | "allowJs": true, 17 | "skipLibCheck": true, 18 | "experimentalDecorators": true, 19 | "strict": true, 20 | "paths": { 21 | "@/*": ["./src/*"], 22 | "@@/*": ["./src/.umi/*"] 23 | } 24 | }, 25 | "include": [ 26 | "mock/**/*", 27 | "src/**/*", 28 | "tests/**/*", 29 | "test/**/*", 30 | "__test__/**/*", 31 | "typings/**/*", 32 | "config/**/*", 33 | ".eslintrc.js", 34 | ".stylelintrc.js", 35 | ".prettierrc.js", 36 | "jest.config.js", 37 | "mock/*" 38 | ], 39 | "exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"] 40 | } 41 | --------------------------------------------------------------------------------