diff --git a/models/cloudbrain.go b/models/cloudbrain.go index 00c88494c..8bb3357ae 100755 --- a/models/cloudbrain.go +++ b/models/cloudbrain.go @@ -219,17 +219,20 @@ type GetImagesPayload struct { type CloudbrainsOptions struct { ListOptions - RepoID int64 // include all repos if empty - UserID int64 - JobID string - SortType string - CloudbrainIDs []int64 - // JobStatus CloudbrainStatus + RepoID int64 // include all repos if empty + UserID int64 + JobID string + SortType string + CloudbrainIDs []int64 + JobStatus []string + JobStatusNot bool + Keyword string Type int JobTypes []string VersionName string IsLatestVersion string JobTypeNot bool + NeedRepoInfo bool } type TaskPod struct { @@ -1082,16 +1085,39 @@ func Cloudbrains(opts *CloudbrainsOptions) ([]*CloudbrainInfo, int64, error) { } if (opts.IsLatestVersion) != "" { - cond = cond.And( - builder.Eq{"cloudbrain.is_latest_version": opts.IsLatestVersion}, - ) + cond = cond.And(builder.Or(builder.And(builder.Eq{"cloudbrain.is_latest_version": opts.IsLatestVersion}, builder.Eq{"cloudbrain.job_type": "TRAIN"}), builder.Neq{"cloudbrain.job_type": "TRAIN"})) } if len(opts.CloudbrainIDs) > 0 { cond = cond.And(builder.In("cloudbrain.id", opts.CloudbrainIDs)) } - count, err := sess.Where(cond).Count(new(Cloudbrain)) + if len(opts.JobStatus) > 0 { + if opts.JobStatusNot { + cond = cond.And( + builder.NotIn("cloudbrain.status", opts.JobStatus), + ) + } else { + cond = cond.And( + builder.In("cloudbrain.status", opts.JobStatus), + ) + } + } + + var count int64 + var err error + condition := "cloudbrain.user_id = `user`.id" + if len(opts.Keyword) == 0 { + count, err = sess.Where(cond).Count(new(Cloudbrain)) + } else { + lowerKeyWord := strings.ToLower(opts.Keyword) + + cond = cond.And(builder.Or(builder.Like{"LOWER(cloudbrain.job_name)", lowerKeyWord}, builder.Like{"`user`.lower_name", lowerKeyWord})) + count, err = sess.Table(&Cloudbrain{}).Where(cond). + Join("left", "`user`", condition).Count(new(CloudbrainInfo)) + + } + if err != nil { return nil, 0, fmt.Errorf("Count: %v", err) } @@ -1109,11 +1135,25 @@ func Cloudbrains(opts *CloudbrainsOptions) ([]*CloudbrainInfo, int64, error) { sess.OrderBy("cloudbrain.created_unix DESC") cloudbrains := make([]*CloudbrainInfo, 0, setting.UI.IssuePagingNum) if err := sess.Table(&Cloudbrain{}).Where(cond). - Join("left", "`user`", "cloudbrain.user_id = `user`.id"). + Join("left", "`user`", condition). Find(&cloudbrains); err != nil { return nil, 0, fmt.Errorf("Find: %v", err) } + if opts.NeedRepoInfo { + var ids []int64 + for _, task := range cloudbrains { + ids = append(ids, task.RepoID) + } + repositoryMap, err := GetRepositoriesMapByIDs(ids) + if err == nil { + for _, task := range cloudbrains { + task.Repo = repositoryMap[task.RepoID] + } + } + + } + return cloudbrains, count, nil } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index d26f603f6..5c0c600be 100755 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -864,9 +864,13 @@ confirm_choice = confirm cloudbran1_tips = Only data in zip format can create cloudbrain tasks cloudbrain_creator=Creator cloudbrain_task = Task Name +cloudbrain_task_type = Task Type +cloudbrain_task_name=Cloud Brain Task Name cloudbrain_operate = Operate cloudbrain_status_createtime = Status/Createtime cloudbrain_status_runtime = Running Time +cloudbrain_jobname_err=Name must start with a lowercase letter or number,can include lowercase letter,number,_ and -,can not end with _, and can be up to 36 characters long. +cloudbrain_query_fail=Failed to query cloudbrain information. record_begintime_get_err=Can not get the record begin time. parameter_is_wrong=The input parameter is wrong. @@ -2344,6 +2348,12 @@ datasets.owner=Owner datasets.name=name datasets.private=Private +cloudbrain.all_task_types=All Task Types +cloudbrain.all_computing_resources=All Computing Resources +cloudbrain.all_status=All Status +cloudbrain.download_report=Download Report +cloudbrain.cloudbrain_name=Cloudbrain Name + hooks.desc = Webhooks automatically make HTTP POST requests to a server when certain openi events trigger. Webhooks defined here are defaults and will be copied into all new repositories. Read more in the webhooks guide. hooks.add_webhook = Add Default Webhook hooks.update_webhook = Update Default Webhook diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 31f4ed947..42deebcb7 100755 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -868,10 +868,13 @@ confirm_choice=确定 cloudbran1_tips=只有zip格式的数据集才能发起云脑任务 cloudbrain_creator=创建者 cloudbrain_task=任务名称 +cloudbrain_task_type=任务类型 +cloudbrain_task_name=云脑侧任务名称 cloudbrain_operate=操作 cloudbrain_status_createtime=状态/创建时间 cloudbrain_status_runtime = 运行时长 cloudbrain_jobname_err=只能以小写字母或数字开头且只包含小写字母、数字、_和-,不能以_结尾,最长36个字符。 +cloudbrain_query_fail=查询云脑任务失败。 record_begintime_get_err=无法获取统计开始时间。 parameter_is_wrong=输入参数错误,请检查输入参数。 @@ -2354,6 +2357,12 @@ datasets.owner=所有者 datasets.name=名称 datasets.private=私有 +cloudbrain.all_task_types=全部任务类型 +cloudbrain.all_computing_resources=全部计算资源 +cloudbrain.all_status=全部状态 +cloudbrain.download_report=下载此报告 +cloudbrain.cloudbrain_name=云脑侧名称 + hooks.desc=当某些 openi 事件触发时, Web 钩子会自动向服务器发出 HTTP POST 请求。此处定义的 Web 钩子是默认值, 将复制到所有新建项目中。参阅 Web钩子指南 获取更多内容。 hooks.add_webhook=新增默认Web钩子 hooks.update_webhook=更新默认Web钩子 @@ -2420,7 +2429,7 @@ auths.sspi_auto_activate_users_helper=允许 SSPI 认证自动激活新用户 auths.sspi_strip_domain_names=从用户名中删除域名部分 auths.sspi_strip_domain_names_helper=如果选中此项,域名将从登录名中删除(例如,"DOMAIN\user"和"user@example.org",两者都将变成只是“用户”)。 auths.sspi_separator_replacement=要使用的分隔符代替\, / 和 @ -auths.sspi_separator_replacement_helper=用于替换下级登录名称分隔符的字符 (例如) "DOMAIN\user") 中的 \ 和用户主名字(如"user@example.org中的 @ )。 +auths.sspi_separator_replacement_helper=用于替换下级登录名称分隔符的字符 (例如) "DOMAIN\user") 中的 \ 和用户主名字(如"user@example.org"中的 @ )。 auths.sspi_default_language=默认语言 auths.sspi_default_language_helper=SSPI 认证方法为用户自动创建的默认语言。如果您想要自动检测到语言,请留空。 auths.tips=帮助提示 diff --git a/package-lock.json b/package-lock.json index a8e5e3e25..7b706207b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13869,6 +13869,130 @@ "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", "dev": true }, + "ts-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-4.0.0.tgz", + "integrity": "sha512-iissbnuJkqbB3YAmnWyEbmdNcGcoiiXopKHKyqdoCrFQVi9pnplXeveQDXJnQOCYNNcb2pjT2zzSYTX6c9QtAA==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^3.1.4", + "semver": "^5.0.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, "tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", @@ -13928,6 +14052,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "dev": true + }, "ua-parser-js": { "version": "0.7.21", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", diff --git a/package.json b/package.json index ba5459a07..e5f829bf1 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,8 @@ "script-loader": "0.7.2", "stylelint": "13.3.3", "stylelint-config-standard": "20.0.0", + "ts-loader": "4.0.0", + "typescript": "4.5.5", "updates": "10.2.11" }, "browserslist": [ diff --git a/routers/admin/cloudbrains.go b/routers/admin/cloudbrains.go new file mode 100644 index 000000000..1cf5ca256 --- /dev/null +++ b/routers/admin/cloudbrains.go @@ -0,0 +1,217 @@ +package admin + +import ( + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/360EntSecGroup-Skylar/excelize/v2" + + "code.gitea.io/gitea/modules/modelarts" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplCloudBrains base.TplName = "admin/cloudbrain/list" + EXCEL_DATE_FORMAT = "20060102150405" + CREATE_TIME_FORMAT = "2006/01/02 15:04:05.00" +) + +func CloudBrains(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("admin.cloudBrains") + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminCloudBrains"] = true + + listType := ctx.Query("listType") + jobType := ctx.Query("jobType") + jobStatus := ctx.Query("jobStatus") + + ctx.Data["ListType"] = listType + ctx.Data["JobType"] = jobType + ctx.Data["JobStatus"] = jobStatus + + page := ctx.QueryInt("page") + if page <= 0 { + page = 1 + } + debugType := modelarts.DebugType + if listType == models.GPUResource { + debugType = models.TypeCloudBrainOne + } else if listType == models.NPUResource { + debugType = models.TypeCloudBrainTwo + } + + var jobTypes []string + jobTypeNot := false + if jobType == string(models.JobTypeDebug) { + jobTypes = append(jobTypes, string(models.JobTypeSnn4imagenet), string(models.JobTypeBrainScore), string(models.JobTypeDebug)) + } else if jobType != "all" && jobType != "" { + jobTypes = append(jobTypes, jobType) + } + + var jobStatuses []string + jobStatusNot := false + if jobStatus == "other" { + jobStatusNot = true + jobStatuses = append(jobStatuses, string(models.ModelArtsTrainJobWaiting), string(models.ModelArtsTrainJobFailed), string(models.ModelArtsRunning), string(models.ModelArtsTrainJobCompleted), + string(models.ModelArtsStarting), string(models.ModelArtsRestarting), string(models.ModelArtsStartFailed), + string(models.ModelArtsStopping), string(models.ModelArtsStopped)) + } else if jobStatus != "all" && jobStatus != "" { + jobStatuses = append(jobStatuses, jobStatus) + } + + keyword := strings.Trim(ctx.Query("q"), " ") + + ciTasks, count, err := models.Cloudbrains(&models.CloudbrainsOptions{ + ListOptions: models.ListOptions{ + Page: page, + PageSize: setting.UI.IssuePagingNum, + }, + Keyword: keyword, + Type: debugType, + JobTypeNot: jobTypeNot, + JobStatusNot: jobStatusNot, + JobStatus: jobStatuses, + JobTypes: jobTypes, + NeedRepoInfo: true, + IsLatestVersion: modelarts.IsLatestVersion, + }) + if err != nil { + ctx.ServerError("Get job failed:", err) + return + } + + for i, task := range ciTasks { + ciTasks[i].CanDebug = true + ciTasks[i].CanDel = true + ciTasks[i].Cloudbrain.ComputeResource = task.ComputeResource + } + + pager := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, getTotalPage(count, setting.UI.IssuePagingNum)) + pager.SetDefaultParams(ctx) + pager.AddParam(ctx, "listType", "ListType") + ctx.Data["Page"] = pager + ctx.Data["PageIsCloudBrain"] = true + ctx.Data["Tasks"] = ciTasks + ctx.Data["CanCreate"] = true + ctx.Data["Keyword"] = keyword + + ctx.HTML(200, tplCloudBrains) + +} + +func DownloadCloudBrains(ctx *context.Context) { + + page := 1 + + pageSize := 300 + + var cloudBrain = ctx.Tr("repo.cloudbrain") + fileName := getFileName(cloudBrain) + + _, total, err := models.Cloudbrains(&models.CloudbrainsOptions{ + ListOptions: models.ListOptions{ + Page: page, + PageSize: 1, + }, + Type: modelarts.DebugType, + NeedRepoInfo: false, + IsLatestVersion: modelarts.IsLatestVersion, + }) + + if err != nil { + log.Warn("Can not get cloud brain info", err) + ctx.Error(http.StatusBadRequest, ctx.Tr("repo.cloudbrain_query_fail")) + return + } + + totalPage := getTotalPage(total, pageSize) + + f := excelize.NewFile() + + index := f.NewSheet(cloudBrain) + f.DeleteSheet("Sheet1") + + for k, v := range allHeader(ctx) { + f.SetCellValue(cloudBrain, k, v) + } + + var row = 2 + for i := 0; i < totalPage; i++ { + + pageRecords, _, err := models.Cloudbrains(&models.CloudbrainsOptions{ + ListOptions: models.ListOptions{ + Page: page, + PageSize: pageSize, + }, + Type: modelarts.DebugType, + NeedRepoInfo: true, + }) + if err != nil { + log.Warn("Can not get cloud brain info", err) + continue + } + for _, record := range pageRecords { + + for k, v := range allValues(row, record, ctx) { + f.SetCellValue(cloudBrain, k, v) + } + row++ + + } + + page++ + } + f.SetActiveSheet(index) + + ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+url.QueryEscape(fileName)) + ctx.Resp.Header().Set("Content-Type", "application/octet-stream") + + f.WriteTo(ctx.Resp) +} + +func allValues(row int, rs *models.CloudbrainInfo, ctx *context.Context) map[string]string { + return map[string]string{getCellName("A", row): rs.JobName, getCellName("B", row): rs.Status, getCellName("C", row): rs.JobType, getCellName("D", row): time.Unix(int64(rs.Cloudbrain.CreatedUnix), 0).Format(CREATE_TIME_FORMAT), getCellName("E", row): getDurationTime(rs), + getCellName("F", row): rs.ComputeResource, getCellName("G", row): rs.Name, getCellName("H", row): rs.Repo.OwnerName + "/" + rs.Repo.Alias, getCellName("I", row): rs.JobName, + } +} + +func getDurationTime(rs *models.CloudbrainInfo) string { + if rs.JobType == "TRAIN" || rs.JobType == "INFERENCE" { + return rs.TrainJobDuration + } else { + return "-" + } +} + +func getFileName(baseName string) string { + return baseName + "_" + time.Now().Format(EXCEL_DATE_FORMAT) + ".xlsx" + +} + +func getTotalPage(total int64, pageSize int) int { + + another := 0 + if int(total)%pageSize != 0 { + another = 1 + } + return int(total)/pageSize + another + +} + +func allHeader(ctx *context.Context) map[string]string { + + return map[string]string{"A1": ctx.Tr("repo.cloudbrain_task"), "B1": ctx.Tr("repo.modelarts.status"), "C1": ctx.Tr("repo.cloudbrain_task_type"), "D1": ctx.Tr("repo.modelarts.createtime"), "E1": ctx.Tr("repo.modelarts.train_job.dura_time"), "F1": ctx.Tr("repo.modelarts.computing_resources"), "G1": ctx.Tr("repo.cloudbrain_creator"), "H1": ctx.Tr("repo.repo_name"), "I1": ctx.Tr("repo.cloudbrain_task_name")} + +} + +func getCellName(col string, row int) string { + return col + strconv.Itoa(row) +} diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index fdaa60517..344115977 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -586,7 +586,13 @@ func CloudBrainDel(ctx *context.Context) { ctx.ServerError(err.Error(), err) return } - ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/debugjob?debugListType=" + listType) + + var isAdminPage = ctx.Query("isadminpage") + if ctx.IsUserSiteAdmin() && isAdminPage == "true" { + ctx.Redirect(setting.AppSubURL + "/admin" + "/cloudbrains") + } else { + ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/debugjob?debugListType=all") + } } func deleteCloudbrainJob(ctx *context.Context) error { @@ -1348,5 +1354,11 @@ func BenchmarkDel(ctx *context.Context) { ctx.ServerError(err.Error(), err) return } - ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/cloudbrain/benchmark") + + var isAdminPage = ctx.Query("isadminpage") + if ctx.IsUserSiteAdmin() && isAdminPage == "true" { + ctx.Redirect(setting.AppSubURL + "/admin" + "/cloudbrains") + } else { + ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/cloudbrain/benchmark") + } } diff --git a/routers/repo/modelarts.go b/routers/repo/modelarts.go index 611b0d9d1..4e76b8d3a 100755 --- a/routers/repo/modelarts.go +++ b/routers/repo/modelarts.go @@ -445,7 +445,12 @@ func NotebookDel(ctx *context.Context) { return } - ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/debugjob?debugListType=" + listType) + var isAdminPage = ctx.Query("isadminpage") + if ctx.IsUserSiteAdmin() && isAdminPage == "true" { + ctx.Redirect(setting.AppSubURL + "/admin" + "/cloudbrains") + } else { + ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/debugjob?debugListType=all") + } } func TrainJobIndex(ctx *context.Context) { @@ -1551,7 +1556,12 @@ func TrainJobDel(ctx *context.Context) { DeleteJobStorage(VersionListTasks[0].JobName) } - ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/modelarts/train-job") + var isAdminPage = ctx.Query("isadminpage") + if ctx.IsUserSiteAdmin() && isAdminPage == "true" { + ctx.Redirect(setting.AppSubURL + "/admin" + "/cloudbrains") + } else { + ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/modelarts/train-job") + } } func TrainJobStop(ctx *context.Context) { diff --git a/routers/routes/routes.go b/routers/routes/routes.go index aab04ac52..e9c7d3fd1 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -508,6 +508,10 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("", admin.Datasets) // m.Post("/delete", admin.DeleteDataset) }) + m.Group("/cloudbrains", func() { + m.Get("", admin.CloudBrains) + m.Get("/download", admin.DownloadCloudBrains) + }) m.Group("/^:configType(hooks|system-hooks)$", func() { m.Get("", admin.DefaultOrSystemWebhooks) diff --git a/templates/admin/cloudbrain/list.tmpl b/templates/admin/cloudbrain/list.tmpl new file mode 100644 index 000000000..0a09230eb --- /dev/null +++ b/templates/admin/cloudbrain/list.tmpl @@ -0,0 +1,219 @@ +{{template "base/head" .}} + +
+
+
+
+
+
+
+
+
+ +
+
+ {{template "admin/navbar" .}} +
+ {{template "base/alert" .}} +
+
+ {{template "admin/cloudbrain/search" .}} + +
+ +
+ +
+
+
+ {{$.i18n.Tr "repo.cloudbrain_task"}} +
+
+ {{$.i18n.Tr "repo.cloudbrain_task_type"}} +
+
+ {{$.i18n.Tr "repo.modelarts.status"}} +
+
+ {{$.i18n.Tr "repo.modelarts.createtime"}} +
+
+ {{$.i18n.Tr "repo.cloudbrain_status_runtime"}} +
+
+ {{$.i18n.Tr "repo.modelarts.computing_resources"}} +
+
+ {{$.i18n.Tr "repo.cloudbrain_creator"}} +
+
+ {{$.i18n.Tr "repository"}} +
+
+ {{.i18n.Tr "admin.cloudbrain.cloudbrain_name"}} +
+
+ {{$.i18n.Tr "repo.cloudbrain_operate"}} +
+
+
+ {{range .Tasks}} +
+
+ +
+ {{if eq .JobType "DEBUG"}} + + {{.JobName}} + + {{else if eq .JobType "INFERENCE"}} + + {{.JobName}} + + {{else if eq .JobType "TRAIN"}} + + {{.JobName}} + + {{else if eq .JobType "BENCHMARK"}} + + {{.JobName}} + + {{end}} +
+ +
+ {{.JobType}} +
+ +
+ + {{.Status}} + +
+ +
+ {{TimeSinceUnix1 .Cloudbrain.CreatedUnix}} +
+ +
+ {{if .TrainJobDuration}}{{.TrainJobDuration}}{{else}}--{{end}} +
+ +
+ {{if .ComputeResource}}{{.ComputeResource}}{{else}}--{{end}} +
+ +
+ {{if .User.Name}} + + {{else}} + + {{end}} +
+ + + +
+ {{.JobName}} +
+
+ {{if eq .JobType "DEBUG"}} +
+
+ {{$.CsrfTokenHtml}} + {{if eq .Status "RUNNING" "WAITING" "CREATING" "STARTING"}} + + {{$.i18n.Tr "repo.debug"}} + + {{else}} + + {{$.i18n.Tr "repo.debug_again"}} + + {{end}} +
+
+ {{end}} + +
+ {{if eq .JobType "DEBUG" "BENCHMARK"}} +
+ {{$.CsrfTokenHtml}} + + {{$.i18n.Tr "repo.stop"}} + +
+ {{else}} + + {{$.i18n.Tr "repo.stop"}} + + {{end}} +
+ +
+ {{$.CsrfTokenHtml}} + + {{$.i18n.Tr "repo.delete"}} + +
+
+
+
+ {{end}} +
+
+ + +
+
+
+ +
+
+
+
+ +
+ +
+
+{{template "base/footer" .}} + diff --git a/templates/admin/cloudbrain/search.tmpl b/templates/admin/cloudbrain/search.tmpl new file mode 100644 index 000000000..bbd45e550 --- /dev/null +++ b/templates/admin/cloudbrain/search.tmpl @@ -0,0 +1,46 @@ +
+
+
+ + +
+
+
+
+ + + +
\ No newline at end of file diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index 2b9f8b7c4..47a9ee811 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -14,6 +14,9 @@ {{.i18n.Tr "admin.datasets"}} + + 云脑任务 + {{.i18n.Tr "admin.hooks"}} diff --git a/templates/org/member/course_members.tmpl b/templates/org/member/course_members.tmpl index 83018739f..5ae6bec89 100644 --- a/templates/org/member/course_members.tmpl +++ b/templates/org/member/course_members.tmpl @@ -120,7 +120,6 @@ \ No newline at end of file diff --git a/templates/repo/cloudbrain/benchmark/new.tmpl b/templates/repo/cloudbrain/benchmark/new.tmpl index 7811680d4..5904ec094 100755 --- a/templates/repo/cloudbrain/benchmark/new.tmpl +++ b/templates/repo/cloudbrain/benchmark/new.tmpl @@ -189,7 +189,6 @@ let type_id = $('#benchmark_types_id').val(); let child_selected_id = $('#benchmark_child_types_id_hidden').val(); $.get(`${repolink}/cloudbrain/benchmark/get_child_types?benchmark_type_id=${type_id}`, (data) => { - console.log(JSON.stringify(data)) const n_length = data['child_types'].length let html='' for (let i=0;i{ - console.log(data) if(data.topics.length!==0){ let html='' $('#course_label_item').empty() @@ -107,5 +106,4 @@ $(document).ready(function(){ } }); }) -console.log() \ No newline at end of file diff --git a/templates/repo/debugjob/index.tmpl b/templates/repo/debugjob/index.tmpl index 955ee4efd..c5a1b9ee6 100755 --- a/templates/repo/debugjob/index.tmpl +++ b/templates/repo/debugjob/index.tmpl @@ -293,7 +293,7 @@
- + {{.Status}}
@@ -324,11 +324,11 @@ {{$.CsrfTokenHtml}} {{if .CanDebug}} {{if eq .Status "RUNNING" "WAITING" "CREATING" "STARTING"}} - + {{$.i18n.Tr "repo.debug"}} {{else}} - + {{$.i18n.Tr "repo.debug_again"}} {{end}} @@ -349,15 +349,9 @@
{{$.CsrfTokenHtml}} {{if .CanDel}} - {{if eq .ComputeResource "CPU/GPU" }} - - {{$.i18n.Tr "repo.stop"}} - - {{else}} - - {{$.i18n.Tr "repo.stop"}} - - {{end}} + + {{$.i18n.Tr "repo.stop"}} + {{else}} {{$.i18n.Tr "repo.stop"}} @@ -369,19 +363,16 @@ {{$.CsrfTokenHtml}} {{if .CanDel}} - + {{$.i18n.Tr "repo.delete"}} {{else}} - - {{$.i18n.Tr "repo.delete"}} + + {{$.i18n.Tr "repo.delete"}} - {{end}}
- - - +
{{end}} -
@@ -487,7 +477,6 @@ {{template "base/footer" .}} - diff --git a/templates/repo/modelarts/trainjob/index.tmpl b/templates/repo/modelarts/trainjob/index.tmpl index 3818df67a..c376e0e8a 100755 --- a/templates/repo/modelarts/trainjob/index.tmpl +++ b/templates/repo/modelarts/trainjob/index.tmpl @@ -113,7 +113,7 @@
- + {{.Status}}
@@ -143,11 +143,11 @@
{{$.CsrfTokenHtml}} {{if .CanDel}} - + {{$.i18n.Tr "repo.stop"}} {{else}} - + {{$.i18n.Tr "repo.stop"}} {{end}} @@ -157,7 +157,7 @@
{{$.CsrfTokenHtml}} {{if .CanDel}} - + {{$.i18n.Tr "repo.delete"}} {{else}} @@ -204,162 +204,5 @@
- -{{template "base/footer" .}} - - \ No newline at end of file +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/repo/modelarts/trainjob/show.tmpl b/templates/repo/modelarts/trainjob/show.tmpl index eff76b2dc..7280076f6 100755 --- a/templates/repo/modelarts/trainjob/show.tmpl +++ b/templates/repo/modelarts/trainjob/show.tmpl @@ -518,6 +518,7 @@ td, th { + {{template "base/footer" .}} \ No newline at end of file + \ No newline at end of file diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 85d1fb65c..eb0d76e5b 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -208,6 +208,3 @@ {{template "base/footer" .}} - \ No newline at end of file diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index 1c0ee84fc..4038ce5b0 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -117,6 +117,3 @@ {{template "base/footer" .}} - \ No newline at end of file diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index d42ed4058..fa5e0c9b7 100755 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -141,6 +141,7 @@ {{if eq .TabName "activity"}} {{if .EnableHeatmap}} +
diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..e69de29bb diff --git a/web_src/js/components/Images.vue b/web_src/js/components/Images.vue index 6fe26d532..a56d81e4c 100644 --- a/web_src/js/components/Images.vue +++ b/web_src/js/components/Images.vue @@ -310,7 +310,6 @@ export default { }, handleCurrentChange(val){ - console.log(val) this.params.page = val this.getImageList() @@ -350,7 +349,6 @@ export default { }) }, copyUrl(url){ - console.log(url) const cInput = document.createElement('input') cInput.value = url document.body.appendChild(cInput) @@ -380,16 +378,13 @@ export default { clearP(value){ - console.log("sorce value",value) if(!value) return '' const reg = /\<\/?p\>/g; value = value.replace(reg,'') - console.log("repalace:",value) return value }, transformTimestamp(timestamp){ - console.log("timestamp",timestamp) let a = new Date(timestamp).getTime(); const date = new Date(a); const Y = date.getFullYear() + '-'; diff --git a/web_src/js/components/UserAnalysis.vue b/web_src/js/components/UserAnalysis.vue index 5650e164f..682dbc78c 100755 --- a/web_src/js/components/UserAnalysis.vue +++ b/web_src/js/components/UserAnalysis.vue @@ -200,7 +200,6 @@ var saveFileName='' var Date=(this.params.startDate).split('-') var startDate=Date[0]+''+Date[1]+''+Date[2] - console.log(startDate) Date=(this.params.endDate).split('-') var endDate=Date[0]+Date[1]+Date[2] saveFileName = '用户分析_'+this.search+''+startDate+'_'+endDate @@ -258,7 +257,6 @@ getUserList(type_val,index){ this.type_val = type_val this.dynamic = index; - console.log("dj:"+type_val) var now = new Date(); // 当前日期 var nowDayOfWeek = now.getDay(); // 今天本周的第几天 var nowDay = now.getDate(); // 当前日 @@ -324,7 +322,6 @@ // console.log("res.data:"+res.data.data) this.totalNum = res.data.count - console.log("res.count:"+res.data.count) }) @@ -354,9 +351,7 @@ }, filters:{ - transformTimestamp(timestamp){ - console.log("timestamp",timestamp) let a = new Date(timestamp*1000); const date = new Date(a); const Y = date.getFullYear() + '/'; @@ -366,7 +361,6 @@ const m = (date.getMinutes() <10 ? '0'+date.getMinutes() : date.getMinutes());// + ':' ; // const s = (date.getSeconds() <10 ? '0'+date.getSeconds() : date.getSeconds()) ; // 秒 const dateString = Y + M + D + h + m ;//+ s; - console.log('dateString', dateString); // > dateString 2021-07-06 14:23 return dateString; }, }, diff --git a/web_src/js/excel/Export2Excel.js b/web_src/js/excel/Export2Excel.js index 3c6d022af..85921f7ad 100755 --- a/web_src/js/excel/Export2Excel.js +++ b/web_src/js/excel/Export2Excel.js @@ -100,7 +100,6 @@ export function export_table_to_excel(id) { /* original data */ var data = oo[0]; var ws_name = "SheetJS"; - console.log(data); var wb = new Workbook(), ws = sheet_from_array_of_arrays(data); @@ -118,7 +117,7 @@ export function export_table_to_excel(id) { } function formatJson(jsonData) { - console.log(jsonData) + } export function export_json_to_excel(th, jsonData, defaultTitle) { diff --git a/web_src/js/excel/util.js b/web_src/js/excel/util.js index b600e5e37..809b5b51b 100755 --- a/web_src/js/excel/util.js +++ b/web_src/js/excel/util.js @@ -3,7 +3,6 @@ export function export2Excel(columns,list,filename){ const { export_json_to_excel } = require('./Export2Excel'); let tHeader = [] let filterVal = [] - console.log(columns) if(!columns){ return; } diff --git a/web_src/js/features/cloudrbanin.js b/web_src/js/features/cloudrbanin.js new file mode 100644 index 000000000..5fcaf56c8 --- /dev/null +++ b/web_src/js/features/cloudrbanin.js @@ -0,0 +1,236 @@ +export default async function initCloudrain() { + let timeid = window.setInterval(loadJobStatus, 15000); + $(document).ready(loadJobStatus); + function loadJobStatus() { + $(".job-status").each((index, job) => { + const jobID = job.dataset.jobid; + const repoPath = job.dataset.repopath; + // const computeResource = job.dataset.resource + const versionname = job.dataset.version + const status_text = $(`#${jobID}-text`).text() + console.log(versionname) + const finalState = ['STOPPED','CREATE_FAILED','UNAVAILABLE','DELETED','RESIZE_FAILED','SUCCEEDED','IMAGE_FAILED','SUBMIT_FAILED','DELETE_FAILED','KILLED','COMPLETED','FAILED','CANCELED','LOST','START_FAILED','SUBMIT_MODEL_FAILED','DEPLOY_SERVICE_FAILED','CHECK_FAILED'] + if (finalState.includes(status_text)) { + return + } + // const diffResource = computeResource == "NPU" ? 'modelarts/notebook' : 'cloudbrain' + $.get(`/api/v1/repos/${repoPath}/${jobID}?version_name=${versionname}`, (data) => { + const jobID = data.JobID + const status = data.JobStatus + const duration = data.JobDuration + $('#duration-'+jobID).text(duration) + if (status != status_text) { + $('#' + jobID+'-icon').removeClass().addClass(status) + $('#' + jobID+ '-text').text(status) + finalState.includes(status) && $('#' + jobID + '-stop').removeClass('blue').addClass('disabled') + } + if(status==="RUNNING"){ + $('#ai-debug-'+jobID).removeClass('disabled').addClass('blue').text('调试').css("margin","0 1rem") + $('#model-image-'+jobID).removeClass('disabled').addClass('blue') + } + if(status!=="RUNNING"){ + // $('#model-debug-'+jobID).removeClass('blue') + // $('#model-debug-'+jobID).addClass('disabled') + $('#model-image-'+jobID).removeClass('blue').addClass('disabled') + } + if(["CREATING","STOPPING","WAITING","STARTING"].includes(status)){ + $('#ai-debug-'+jobID).removeClass('blue').addClass('disabled') + } + if(['STOPPED','FAILED','START_FAILED','CREATE_FAILED','SUCCEEDED'].includes(status)){ + $('#ai-debug-'+jobID).removeClass('disabled').addClass('blue').text('再次调试').css("margin","0") + } + if(["RUNNING","WAITING"].includes(status)){ + $('#ai-stop-'+jobID).removeClass('disabled').addClass('blue') + } + if(["CREATING","STOPPING","STARTING","STOPPED","FAILED","START_FAILED","SUCCEEDED"].includes(status)){ + $('#ai-stop-'+jobID).removeClass('blue').addClass('disabled') + } + + if(["STOPPED","FAILED","START_FAILED","KILLED","COMPLETED","SUCCEEDED"].includes(status)){ + $('#ai-delete-'+jobID).removeClass('disabled').addClass('blue') + }else{ + $('#ai-delete-'+jobID).removeClass('blue').addClass('disabled') + } + }).fail(function(err) { + console.log(err); + }); + }); + }; + function assertDelete(obj,versionName,repoPath) { + if (obj.style.color == "rgb(204, 204, 204)") { + return + } else { + const delId = obj.parentNode.id + let flag = 1; + $('.ui.basic.modal') + .modal({ + onDeny: function() { + flag = false + }, + onApprove: function() { + if(!versionName){ + document.getElementById(delId).submit() + } + else{ + deleteVersion(versionName,repoPath) + } + flag = true + }, + onHidden: function() { + if (flag == false) { + $('.alert').html('您已取消操作').removeClass('alert-success').addClass('alert-danger').show().delay(1500).fadeOut(); + } + } + }) + .modal('show') + } + } + function deleteVersion(versionName,repoPath){ + const url = `/api/v1/repos/${repoPath}` + $.post(url,{version_name:versionName},(data)=>{ + if(data.StatusOK===0){ + location.reload() + } + }).fail(function(err) { + console.log(err); + }); + } + $('.ui.basic.ai_delete').click(function() { + const repoPath = this.dataset.repopath + const versionName = this.dataset.version + if(repoPath && versionName){ + assertDelete(this,versionName,repoPath) + } + else{ + assertDelete(this) + } + }) + function stopDebug(JobID,stopUrl){ + $.ajax({ + type:"POST", + url:stopUrl, + data:$('#stopForm-'+JobID).serialize(), + success:function(res){ + if(res.result_code==="0"){ + $('#' + JobID+'-icon').removeClass().addClass(res.status) + $('#' + JobID+ '-text').text(res.status) + if(res.status==="STOPPED"){ + $('#ai-debug-'+JobID).removeClass('disabled').addClass('blue').text("再次调试").css("margin","0") + $('#ai-image-'+JobID).removeClass('blue').addClass('disabled') + $('#ai-model-debug-'+JobID).removeClass('blue').addClass('disabled') + $('#ai-delete-'+JobID).removeClass('disabled').addClass('blue') + $('#ai-stop-'+JobID).removeClass('blue').addClass('disabled') + } + else{ + $('#ai-debug-'+JobID).removeClass('blue').addClass('disabled') + $('#ai-stop-'+JobID).removeClass('blue').addClass('disabled') + } + + }else{ + $('.alert').html(res.error_msg).removeClass('alert-success').addClass('alert-danger').show().delay(2000).fadeOut(); + } + }, + error :function(res){ + console.log(res) + + } + }) + } + $('.ui.basic.ai_stop').click(function() { + const jobID = this.dataset.jobid + const repoPath = this.dataset.repopath + stopDebug(jobID,repoPath) + }) + + function stopVersion(version_name,jobID,repoPath){ + const url = `/api/v1/repos/${repoPath}/${jobID}/stop_version` + $.post(url,{version_name:version_name},(data)=>{ + if(data.StatusOK===0){ + $('#ai-stop-'+jobID).removeClass('blue') + $('#ai-stop-'+jobID).addClass('disabled') + refreshStatus(version_name,jobID,repoPath) + } + }).fail(function(err) { + console.log(err); + }); + } + function refreshStatus(version_name,jobID,repoPath){ + + const url = `/api/v1/repos/${repoPath}/${jobID}/?version_name${version_name}` + $.get(url,(data)=>{ + $(`#${jobID}-icon`).attr("class",data.JobStatus) + // detail status and duration + $(`#${jobID}-text`).text(data.JobStatus) + }).fail(function(err) { + console.log(err); + }); + } + $('.ui.basic.ai_stop_version').click(function() { + const jobID = this.dataset.jobid + const repoPath = this.dataset.repopath + const versionName = this.dataset.version + stopVersion(versionName,jobID,repoPath) + }) + function getModelInfo(repoPath,modelName,versionName,jobName){ + console.log("getModelInfo") + $.get(`${repoPath}/modelmanage/show_model_info_api?name=${modelName}`,(data)=>{ + if(data.length===0){ + $(`#${jobName}`).popup('toggle') + }else{ + let versionData = data.filter((item)=>{ + return item.Version === versionName + }) + if(versionData.length==0){ + $(`#${jobName}`).popup('toggle') + } + else{ + location.href = `${repoPath}/modelmanage/show_model_info?name=${modelName}` + } + } + }) + } + $('.goto_modelmanage').click(function() { + const repoPath = this.dataset.repopath + const modelName = this.dataset.modelname + const versionName = this.dataset.version + const jobName = this.dataset.jobname + getModelInfo(repoPath,modelName,versionName,jobName) + }) + function debugAgain(JobID,debugUrl){ + if($('#' + JobID+ '-text').text()==="RUNNING"){ + window.open(debugUrl+'debug') + }else{ + $.ajax({ + type:"POST", + url:debugUrl+'restart', + data:$('#debugAgainForm-'+JobID).serialize(), + success:function(res){ + if(res.result_code==="0"){ + if(res.job_id!==JobID){ + location.reload() + }else{ + $('#' + JobID+'-icon').removeClass().addClass(res.status) + $('#' + JobID+ '-text').text(res.status) + $('#ai-debug-'+JobID).removeClass('blue').addClass('disabled') + $('#ai-delete-'+JobID).removeClass('blue').addClass('disabled') + $('#ai-debug-'+JobID).text("调试").css("margin","0 1rem") + } + }else{ + $('.alert').html(res.error_msg).removeClass('alert-success').addClass('alert-danger').show().delay(2000).fadeOut(); + } + }, + error :function(res){ + console.log(res) + + } + }) + } + } + $('.ui.basic.ai_debug').click(function() { + const jobID = this.dataset.jobid + const repoPath = this.dataset.repopath + debugAgain(jobID,repoPath) + }) +} + + \ No newline at end of file diff --git a/web_src/js/index.js b/web_src/js/index.js index 6bc0d2b88..cec6823a6 100755 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -40,6 +40,8 @@ import EditTopics from './components/EditTopics.vue'; import DataAnalysis from './components/DataAnalysis.vue' import Contributors from './components/Contributors.vue' import Model from './components/Model.vue'; +import initCloudrain from './features/cloudrbanin.js' + Vue.use(ElementUI); Vue.prototype.$axios = axios; @@ -2936,6 +2938,7 @@ $(document).ready(async () => { initNotificationCount(); initTribute(); initDropDown(); + initCloudrain(); // Repo clone url. if ($('#repo-clone-url').length > 0) { @@ -3584,14 +3587,16 @@ function initVueApp() { } initVueComponents(); - + new Vue({ delimiters: ['${', '}'], el, data: { + page:parseInt(new URLSearchParams(window.location.search).get('page')), searchLimit: Number( (document.querySelector('meta[name=_search_limit]') || {}).content ), + page:1, suburl: AppSubUrl, uid: Number( (document.querySelector('meta[name=_context_uid]') || {}).content @@ -3600,6 +3605,15 @@ function initVueApp() { }, components: { ActivityTopAuthors + }, + mounted(){ + this.page = parseInt(new URLSearchParams(window.location.search).get('page')) + }, + methods:{ + handleCurrentChange:function (val) { + window.location.href='/admin/cloudbrains?page='+val + this.page = val + } } }); } @@ -3688,8 +3702,6 @@ function initVueModel() { } function initVueDataAnalysis() { const el = document.getElementById('data_analysis'); - console.log("el",el) - if (!el) { return; } @@ -3756,7 +3768,6 @@ function initFilterBranchTagDropdown(selector) { }); }); $data.remove(); - console.log("-this",this) new Vue({ delimiters: ['${', '}'], el: this, diff --git a/web_src/less/openi.less b/web_src/less/openi.less index 1d165dda8..c9e6a546f 100644 --- a/web_src/less/openi.less +++ b/web_src/less/openi.less @@ -720,4 +720,13 @@ display: block; .markdown:not(code).file-view{ padding: 2em 0!important; } -} \ No newline at end of file +} +//elemet-ui + +.el-pagination.is-background .el-pager li:not(.disabled).active { + background-color: #5bb973 !important; + color: #FFF !important; + } + .el-pagination.is-background .el-pager li:hover { + color: #5bb973 !important; + } \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index a08810ebc..cd3635427 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -116,6 +116,15 @@ module.exports = { ], }, { + test: /\.ts$/, + use: [ + { + loader: "ts-loader", + } + ], + exclude: /node_modules/ + }, + { test: /\.js$/, exclude: /node_modules/, use: [ @@ -252,6 +261,7 @@ module.exports = { alias: { vue$: 'vue/dist/vue.esm.js', // needed because vue's default export is the runtime only }, + extensions: ['.tsx', '.ts', '.js'] }, watchOptions: { ignored: [