| @@ -2,7 +2,6 @@ package models | |||||
| import ( | import ( | ||||
| "encoding/json" | "encoding/json" | ||||
| "errors" | |||||
| "fmt" | "fmt" | ||||
| "time" | "time" | ||||
| "xorm.io/xorm" | "xorm.io/xorm" | ||||
| @@ -300,7 +299,7 @@ type CommitImageResult struct { | |||||
| Payload map[string]interface{} `json:"payload"` | Payload map[string]interface{} `json:"payload"` | ||||
| } | } | ||||
| type StopJobResult struct { | |||||
| type CloudBrainResult struct { | |||||
| Code string `json:"code"` | Code string `json:"code"` | ||||
| Msg string `json:"msg"` | Msg string `json:"msg"` | ||||
| } | } | ||||
| @@ -569,7 +568,7 @@ func getRepoCloudBrain(cb *Cloudbrain) (*Cloudbrain, error) { | |||||
| if err != nil { | if err != nil { | ||||
| return nil, err | return nil, err | ||||
| } else if !has { | } else if !has { | ||||
| return nil, errors.New("cloudbrain task is not found") | |||||
| return nil, ErrJobNotExist{} | |||||
| } | } | ||||
| return cb, nil | return cb, nil | ||||
| } | } | ||||
| @@ -609,3 +608,8 @@ func deleteJob(e Engine, job *Cloudbrain) error { | |||||
| _, err := e.ID(job.ID).Delete(job) | _, err := e.ID(job.ID).Delete(job) | ||||
| return err | return err | ||||
| } | } | ||||
| func GetCloudbrainByName(jobName string) (*Cloudbrain, error) { | |||||
| cb := &Cloudbrain{JobName: jobName} | |||||
| return getRepoCloudBrain(cb) | |||||
| } | |||||
| @@ -1987,3 +1987,15 @@ func IsErrFileChunkNotExist(err error) bool { | |||||
| _, ok := err.(ErrFileChunkNotExist) | _, ok := err.(ErrFileChunkNotExist) | ||||
| return ok | return ok | ||||
| } | } | ||||
| type ErrJobNotExist struct { | |||||
| } | |||||
| func IsErrJobNotExist(err error) bool { | |||||
| _, ok := err.(ErrJobNotExist) | |||||
| return ok | |||||
| } | |||||
| func (err ErrJobNotExist) Error() string { | |||||
| return fmt.Sprintf("the job does not exist") | |||||
| } | |||||
| @@ -2,6 +2,7 @@ package cloudbrain | |||||
| import ( | import ( | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "encoding/json" | |||||
| "fmt" | "fmt" | ||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| @@ -143,14 +144,26 @@ sendjob: | |||||
| return nil, fmt.Errorf("resty GetImages: %v", err) | return nil, fmt.Errorf("resty GetImages: %v", err) | ||||
| } | } | ||||
| if getImagesResult.Code == "S401" && retry < 1 { | |||||
| var response models.CloudBrainResult | |||||
| err = json.Unmarshal(res.Body(), &response) | |||||
| if err != nil { | |||||
| log.Error("json.Unmarshal failed: %s", err.Error()) | |||||
| return &getImagesResult, fmt.Errorf("json.Unmarshal failed: %s", err.Error()) | |||||
| } | |||||
| if response.Code == "S401" && retry < 1 { | |||||
| retry++ | retry++ | ||||
| _ = loginCloudbrain() | _ = loginCloudbrain() | ||||
| goto sendjob | goto sendjob | ||||
| } | } | ||||
| if len(response.Code) != 0 { | |||||
| log.Error("getImagesResult failed(%s): %s", response.Code, response.Msg) | |||||
| return &getImagesResult, fmt.Errorf("getImagesResult failed(%s): %s", response.Code, response.Msg) | |||||
| } | |||||
| if getImagesResult.Code != Success { | if getImagesResult.Code != Success { | ||||
| return &getImagesResult, fmt.Errorf("getImgesResult err: %s", res.String()) | |||||
| return &getImagesResult, fmt.Errorf("getImagesResult err: %s", res.String()) | |||||
| } | } | ||||
| return &getImagesResult, nil | return &getImagesResult, nil | ||||
| @@ -223,7 +236,7 @@ sendjob: | |||||
| func StopJob(jobID string) error { | func StopJob(jobID string) error { | ||||
| checkSetting() | checkSetting() | ||||
| client := getRestyClient() | client := getRestyClient() | ||||
| var result models.StopJobResult | |||||
| var result models.CloudBrainResult | |||||
| retry := 0 | retry := 0 | ||||
| @@ -67,19 +67,43 @@ func (l *Logger) Log(skip int, level Level, format string, v ...interface{}) err | |||||
| caller = fn.Name() + "()" | caller = fn.Name() + "()" | ||||
| } | } | ||||
| } | } | ||||
| msg := format | |||||
| if len(v) > 0 { | |||||
| msg = ColorSprintf(format, v...) | |||||
| } | |||||
| stack := "" | stack := "" | ||||
| if l.GetStacktraceLevel() <= level { | if l.GetStacktraceLevel() <= level { | ||||
| stack = Stack(skip + 1) | stack = Stack(skip + 1) | ||||
| } | } | ||||
| return l.SendLog(level, caller, strings.TrimPrefix(filename, prefix), line, msg, stack) | |||||
| msg := format | |||||
| if len(v) > 0 { | |||||
| switch v[len(v)-1].(type) { | |||||
| case string: | |||||
| if !strings.Contains(v[len(v)-1].(string), "-") { | |||||
| //has no msgID | |||||
| msg = ColorSprintf(format, v...) | |||||
| return l.SendLog(level, caller, strings.TrimPrefix(filename, prefix), line, msg, "", stack) | |||||
| } else { | |||||
| if len(v) > 1 { | |||||
| args := make([]interface{}, len(v)-1) | |||||
| for i := 0; i < len(v)-1; i++ { | |||||
| args[i] = v[i] | |||||
| } | |||||
| msg = ColorSprintf(format, args...) | |||||
| } | |||||
| return l.SendLog(level, caller, strings.TrimPrefix(filename, prefix), line, msg, v[len(v)-1].(string), stack) | |||||
| } | |||||
| default: | |||||
| //has no msgID | |||||
| msg = ColorSprintf(format, v...) | |||||
| return l.SendLog(level, caller, strings.TrimPrefix(filename, prefix), line, msg, "", stack) | |||||
| } | |||||
| } else { | |||||
| //has no msgID | |||||
| return l.SendLog(level, caller, strings.TrimPrefix(filename, prefix), line, msg, "", stack) | |||||
| } | |||||
| } | } | ||||
| // SendLog sends a log event at the provided level with the information given | // SendLog sends a log event at the provided level with the information given | ||||
| func (l *Logger) SendLog(level Level, caller, filename string, line int, msg string, stack string) error { | |||||
| func (l *Logger) SendLog(level Level, caller, filename string, line int, msg, msgID, stack string) error { | |||||
| if l.GetLevel() > level { | if l.GetLevel() > level { | ||||
| return nil | return nil | ||||
| } | } | ||||
| @@ -88,7 +112,7 @@ func (l *Logger) SendLog(level Level, caller, filename string, line int, msg str | |||||
| caller: caller, | caller: caller, | ||||
| filename: filename, | filename: filename, | ||||
| line: line, | line: line, | ||||
| msg: msg, | |||||
| msg: msg + "[" + msgID + "]", | |||||
| time: time.Now(), | time: time.Now(), | ||||
| stacktrace: stack, | stacktrace: stack, | ||||
| } | } | ||||
| @@ -83,7 +83,7 @@ func (m *MinioStorage) PresignedGetURL(path string, fileName string) (string, er | |||||
| reqParams.Set("response-content-disposition", "attachment; filename=\""+fileName+"\"") | reqParams.Set("response-content-disposition", "attachment; filename=\""+fileName+"\"") | ||||
| var preURL *url.URL | var preURL *url.URL | ||||
| preURL, err := m.client.PresignedGetObject(m.bucket, m.buildMinioPath(path), PresignedGetUrlExpireTime, reqParams) | |||||
| preURL, err := m.client.PresignedGetObject(m.bucket, path, PresignedGetUrlExpireTime, reqParams) | |||||
| if err != nil { | if err != nil { | ||||
| return "", err | return "", err | ||||
| } | } | ||||
| @@ -1819,6 +1819,7 @@ org_full_name_holder=组织全名 | |||||
| org_name_helper=组织名字应该简单明了。 | org_name_helper=组织名字应该简单明了。 | ||||
| create_org=创建组织 | create_org=创建组织 | ||||
| repo_updated=最后更新于 | repo_updated=最后更新于 | ||||
| home=组织主页 | |||||
| people=组织成员 | people=组织成员 | ||||
| teams=组织团队 | teams=组织团队 | ||||
| lower_members=名成员 | lower_members=名成员 | ||||
| @@ -20,6 +20,7 @@ const ( | |||||
| // Home show organization home page | // Home show organization home page | ||||
| func Home(ctx *context.Context) { | func Home(ctx *context.Context) { | ||||
| ctx.SetParams(":org", ctx.Params(":username")) | ctx.SetParams(":org", ctx.Params(":username")) | ||||
| ctx.Data["PageIsOrgHome"] = true | |||||
| context.HandleOrgAssignment(ctx) | context.HandleOrgAssignment(ctx) | ||||
| if ctx.Written() { | if ctx.Written() { | ||||
| return | return | ||||
| @@ -205,7 +205,7 @@ func GetAttachment(ctx *context.Context) { | |||||
| if setting.Attachment.StoreType == storage.MinioStorageType { | if setting.Attachment.StoreType == storage.MinioStorageType { | ||||
| url := "" | url := "" | ||||
| if typeCloudBrain == models.TypeCloudBrainOne { | if typeCloudBrain == models.TypeCloudBrainOne { | ||||
| url, err = storage.Attachments.PresignedGetURL(attach.RelativePath(), attach.Name) | |||||
| url, err = storage.Attachments.PresignedGetURL(setting.Attachment.Minio.BasePath + attach.RelativePath(), attach.Name) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("PresignedGetURL", err) | ctx.ServerError("PresignedGetURL", err) | ||||
| return | return | ||||
| @@ -28,14 +28,14 @@ const ( | |||||
| func BlockChainIndex(ctx *context.Context) { | func BlockChainIndex(ctx *context.Context) { | ||||
| repo := ctx.Repo.Repository | repo := ctx.Repo.Repository | ||||
| if repo.ContractAddress == "" || ctx.User.PublicKey == "" { | if repo.ContractAddress == "" || ctx.User.PublicKey == "" { | ||||
| log.Error("the repo(%d) or the user(%d) has not been initialized in block_chain", repo.RepoID, ctx.User.ID) | |||||
| log.Error("the repo(%d) or the user(%d) has not been initialized in block_chain", repo.RepoID, ctx.User.ID, ctx.Data["msgID"]) | |||||
| ctx.HTML(http.StatusInternalServerError, tplBlockChainIndex) | ctx.HTML(http.StatusInternalServerError, tplBlockChainIndex) | ||||
| return | return | ||||
| } | } | ||||
| res, err := blockchain.GetBalance(repo.ContractAddress, ctx.User.PublicKey) | res, err := blockchain.GetBalance(repo.ContractAddress, ctx.User.PublicKey) | ||||
| if err != nil { | if err != nil { | ||||
| log.Error("GetBalance(%s) failed:%v", ctx.User.PublicKey, err) | |||||
| log.Error("GetBalance(%s) failed:%s", ctx.User.PublicKey, err, ctx.Data["msgID"]) | |||||
| ctx.HTML(http.StatusInternalServerError, tplBlockChainIndex) | ctx.HTML(http.StatusInternalServerError, tplBlockChainIndex) | ||||
| return | return | ||||
| } | } | ||||
| @@ -52,7 +52,7 @@ func HandleBlockChainInitNotify(ctx *context.Context) { | |||||
| repo, err := models.GetRepositoryByID(req.RepoId) | repo, err := models.GetRepositoryByID(req.RepoId) | ||||
| if err != nil { | if err != nil { | ||||
| log.Error("GetRepositoryByID failed:", err.Error()) | |||||
| log.Error("GetRepositoryByID failed:%v", err.Error(), ctx.Data["msgID"]) | |||||
| ctx.JSON(200, map[string]string{ | ctx.JSON(200, map[string]string{ | ||||
| "code": "-1", | "code": "-1", | ||||
| "message": "internal error", | "message": "internal error", | ||||
| @@ -61,7 +61,7 @@ func HandleBlockChainInitNotify(ctx *context.Context) { | |||||
| } | } | ||||
| if repo.BlockChainStatus == models.RepoBlockChainSuccess && len(repo.ContractAddress) != 0 { | if repo.BlockChainStatus == models.RepoBlockChainSuccess && len(repo.ContractAddress) != 0 { | ||||
| log.Error("the repo has been RepoBlockChainSuccess:", req.RepoId) | |||||
| log.Error("the repo has been RepoBlockChainSuccess:%d", req.RepoId, ctx.Data["msgID"]) | |||||
| ctx.JSON(200, map[string]string{ | ctx.JSON(200, map[string]string{ | ||||
| "code": "-1", | "code": "-1", | ||||
| "message": "the repo has been RepoBlockChainSuccess", | "message": "the repo has been RepoBlockChainSuccess", | ||||
| @@ -73,7 +73,7 @@ func HandleBlockChainInitNotify(ctx *context.Context) { | |||||
| repo.ContractAddress = req.ContractAddress | repo.ContractAddress = req.ContractAddress | ||||
| if err = models.UpdateRepositoryCols(repo, "block_chain_status", "contract_address"); err != nil { | if err = models.UpdateRepositoryCols(repo, "block_chain_status", "contract_address"); err != nil { | ||||
| log.Error("UpdateRepositoryCols failed:", err.Error()) | |||||
| log.Error("UpdateRepositoryCols failed:%v", err.Error(), ctx.Data["msgID"]) | |||||
| ctx.JSON(200, map[string]string{ | ctx.JSON(200, map[string]string{ | ||||
| "code": "-1", | "code": "-1", | ||||
| "message": "internal error", | "message": "internal error", | ||||
| @@ -91,7 +91,7 @@ func HandleBlockChainCommitNotify(ctx *context.Context) { | |||||
| var req BlockChainCommitNotify | var req BlockChainCommitNotify | ||||
| data, _ := ctx.Req.Body().Bytes() | data, _ := ctx.Req.Body().Bytes() | ||||
| if err := json.Unmarshal(data, &req); err != nil { | if err := json.Unmarshal(data, &req); err != nil { | ||||
| log.Error("json.Unmarshal failed:", err.Error()) | |||||
| log.Error("json.Unmarshal failed:%v", err.Error(), ctx.Data["msgID"]) | |||||
| ctx.JSON(200, map[string]string{ | ctx.JSON(200, map[string]string{ | ||||
| "code": "-1", | "code": "-1", | ||||
| "message": "response data error", | "message": "response data error", | ||||
| @@ -101,7 +101,7 @@ func HandleBlockChainCommitNotify(ctx *context.Context) { | |||||
| blockChain, err := models.GetBlockChainByCommitID(req.CommitID) | blockChain, err := models.GetBlockChainByCommitID(req.CommitID) | ||||
| if err != nil { | if err != nil { | ||||
| log.Error("GetRepositoryByID failed:", err.Error()) | |||||
| log.Error("GetRepositoryByID failed:%v", err.Error(), ctx.Data["msgID"]) | |||||
| ctx.JSON(200, map[string]string{ | ctx.JSON(200, map[string]string{ | ||||
| "code": "-1", | "code": "-1", | ||||
| "message": "internal error", | "message": "internal error", | ||||
| @@ -110,7 +110,7 @@ func HandleBlockChainCommitNotify(ctx *context.Context) { | |||||
| } | } | ||||
| if blockChain.Status == models.BlockChainCommitSuccess { | if blockChain.Status == models.BlockChainCommitSuccess { | ||||
| log.Error("the commit has been BlockChainCommitReady:", blockChain.RepoID) | |||||
| log.Error("the commit has been BlockChainCommitReady:%s", blockChain.RepoID, ctx.Data["msgID"]) | |||||
| ctx.JSON(200, map[string]string{ | ctx.JSON(200, map[string]string{ | ||||
| "code": "-1", | "code": "-1", | ||||
| "message": "the commit has been BlockChainCommitReady", | "message": "the commit has been BlockChainCommitReady", | ||||
| @@ -122,7 +122,7 @@ func HandleBlockChainCommitNotify(ctx *context.Context) { | |||||
| blockChain.TransactionHash = req.TransactionHash | blockChain.TransactionHash = req.TransactionHash | ||||
| if err = models.UpdateBlockChainCols(blockChain, "status", "transaction_hash"); err != nil { | if err = models.UpdateBlockChainCols(blockChain, "status", "transaction_hash"); err != nil { | ||||
| log.Error("UpdateBlockChainCols failed:", err.Error()) | |||||
| log.Error("UpdateBlockChainCols failed:%v", err.Error(), ctx.Data["msgID"]) | |||||
| ctx.JSON(200, map[string]string{ | ctx.JSON(200, map[string]string{ | ||||
| "code": "-1", | "code": "-1", | ||||
| "message": "internal error", | "message": "internal error", | ||||
| @@ -2,8 +2,10 @@ package repo | |||||
| import ( | import ( | ||||
| "code.gitea.io/gitea/modules/git" | "code.gitea.io/gitea/modules/git" | ||||
| "code.gitea.io/gitea/modules/storage" | |||||
| "encoding/json" | "encoding/json" | ||||
| "errors" | "errors" | ||||
| "net/http" | |||||
| "os" | "os" | ||||
| "os/exec" | "os/exec" | ||||
| "strconv" | "strconv" | ||||
| @@ -23,6 +25,7 @@ const ( | |||||
| tplCloudBrainIndex base.TplName = "repo/cloudbrain/index" | tplCloudBrainIndex base.TplName = "repo/cloudbrain/index" | ||||
| tplCloudBrainNew base.TplName = "repo/cloudbrain/new" | tplCloudBrainNew base.TplName = "repo/cloudbrain/new" | ||||
| tplCloudBrainShow base.TplName = "repo/cloudbrain/show" | tplCloudBrainShow base.TplName = "repo/cloudbrain/show" | ||||
| tplCloudBrainShowModels base.TplName = "repo/cloudbrain/models/index" | |||||
| ) | ) | ||||
| var ( | var ( | ||||
| @@ -93,7 +96,7 @@ func CloudBrainNew(ctx *context.Context) { | |||||
| result, err := cloudbrain.GetImages() | result, err := cloudbrain.GetImages() | ||||
| if err != nil { | if err != nil { | ||||
| ctx.Data["error"] = err.Error() | ctx.Data["error"] = err.Error() | ||||
| log.Error("cloudbrain.GetImages failed:", err.Error()) | |||||
| log.Error("cloudbrain.GetImages failed:", err.Error(), ctx.Data["msgID"]) | |||||
| } | } | ||||
| for i, payload := range result.Payload.ImageInfo { | for i, payload := range result.Payload.ImageInfo { | ||||
| @@ -109,7 +112,7 @@ func CloudBrainNew(ctx *context.Context) { | |||||
| resultPublic, err := cloudbrain.GetPublicImages() | resultPublic, err := cloudbrain.GetPublicImages() | ||||
| if err != nil { | if err != nil { | ||||
| ctx.Data["error"] = err.Error() | ctx.Data["error"] = err.Error() | ||||
| log.Error("cloudbrain.GetPublicImages failed:", err.Error()) | |||||
| log.Error("cloudbrain.GetPublicImages failed:", err.Error(), ctx.Data["msgID"]) | |||||
| } | } | ||||
| for i, payload := range resultPublic.Payload.ImageInfo { | for i, payload := range resultPublic.Payload.ImageInfo { | ||||
| @@ -161,15 +164,28 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { | |||||
| codePath := setting.JobPath + jobName + cloudbrain.CodeMountPath | codePath := setting.JobPath + jobName + cloudbrain.CodeMountPath | ||||
| if jobType != string(models.JobTypeBenchmark) && jobType != string(models.JobTypeDebug) && jobType != string(models.JobTypeSnn4imagenet) { | if jobType != string(models.JobTypeBenchmark) && jobType != string(models.JobTypeDebug) && jobType != string(models.JobTypeSnn4imagenet) { | ||||
| log.Error("jobtype error:", jobType) | |||||
| log.Error("jobtype error:", jobType, ctx.Data["msgID"]) | |||||
| ctx.RenderWithErr("jobtype error", tplCloudBrainNew, &form) | ctx.RenderWithErr("jobtype error", tplCloudBrainNew, &form) | ||||
| return | return | ||||
| } | } | ||||
| _, err := models.GetCloudbrainByName(jobName) | |||||
| if err == nil { | |||||
| log.Error("the job name did already exist", ctx.Data["MsgID"]) | |||||
| ctx.RenderWithErr("the job name did already exist", tplCloudBrainNew, &form) | |||||
| return | |||||
| } else { | |||||
| if !models.IsErrJobNotExist(err) { | |||||
| log.Error("system error, %v", err, ctx.Data["MsgID"]) | |||||
| ctx.RenderWithErr("system error", tplCloudBrainNew, &form) | |||||
| return | |||||
| } | |||||
| } | |||||
| repo := ctx.Repo.Repository | repo := ctx.Repo.Repository | ||||
| downloadCode(repo, codePath) | downloadCode(repo, codePath) | ||||
| modelPath := setting.JobPath + jobName + cloudbrain.ModelMountPath | modelPath := setting.JobPath + jobName + cloudbrain.ModelMountPath | ||||
| err := os.MkdirAll(modelPath, os.ModePerm) | |||||
| err = os.MkdirAll(modelPath, os.ModePerm) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.RenderWithErr(err.Error(), tplCloudBrainNew, &form) | ctx.RenderWithErr(err.Error(), tplCloudBrainNew, &form) | ||||
| return | return | ||||
| @@ -264,7 +280,7 @@ func CloudBrainCommitImage(ctx *context.Context, form auth.CommitImageCloudBrain | |||||
| ImageTag: form.Tag, | ImageTag: form.Tag, | ||||
| }) | }) | ||||
| if err != nil { | if err != nil { | ||||
| log.Error("CommitImage(%s) failed:", task.JobName, err.Error()) | |||||
| log.Error("CommitImage(%s) failed:%v", task.JobName, err.Error(), ctx.Data["msgID"]) | |||||
| ctx.JSON(200, map[string]string{ | ctx.JSON(200, map[string]string{ | ||||
| "result_code": "-1", | "result_code": "-1", | ||||
| "error_msg": "CommitImage failed", | "error_msg": "CommitImage failed", | ||||
| @@ -287,14 +303,14 @@ func CloudBrainStop(ctx *context.Context) { | |||||
| } | } | ||||
| if task.Status == string(models.JobStopped) { | if task.Status == string(models.JobStopped) { | ||||
| log.Error("the job(%s) has been stopped", task.JobName) | |||||
| log.Error("the job(%s) has been stopped", task.JobName, ctx.Data["msgID"]) | |||||
| ctx.ServerError("the job has been stopped", errors.New("the job has been stopped")) | ctx.ServerError("the job has been stopped", errors.New("the job has been stopped")) | ||||
| return | return | ||||
| } | } | ||||
| err = cloudbrain.StopJob(jobID) | err = cloudbrain.StopJob(jobID) | ||||
| if err != nil { | if err != nil { | ||||
| log.Error("StopJob(%s) failed:%v", task.JobName, err.Error()) | |||||
| log.Error("StopJob(%s) failed:%v", task.JobName, err.Error(), ctx.Data["msgID"]) | |||||
| ctx.ServerError("StopJob failed", err) | ctx.ServerError("StopJob failed", err) | ||||
| return | return | ||||
| } | } | ||||
| @@ -318,7 +334,7 @@ func CloudBrainDel(ctx *context.Context) { | |||||
| } | } | ||||
| if task.Status != string(models.JobStopped) { | if task.Status != string(models.JobStopped) { | ||||
| log.Error("the job(%s) has not been stopped", task.JobName) | |||||
| log.Error("the job(%s) has not been stopped", task.JobName, ctx.Data["msgID"]) | |||||
| ctx.ServerError("the job has not been stopped", errors.New("the job has not been stopped")) | ctx.ServerError("the job has not been stopped", errors.New("the job has not been stopped")) | ||||
| return | return | ||||
| } | } | ||||
| @@ -332,6 +348,69 @@ func CloudBrainDel(ctx *context.Context) { | |||||
| ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/cloudbrain") | ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/cloudbrain") | ||||
| } | } | ||||
| func CloudBrainShowModels(ctx *context.Context) { | |||||
| ctx.Data["PageIsCloudBrain"] = true | |||||
| jobID := ctx.Params(":jobid") | |||||
| parentDir := ctx.Query("parentDir") | |||||
| dirArray := strings.Split(parentDir, "/") | |||||
| task, err := models.GetCloudbrainByJobID(jobID) | |||||
| if err != nil { | |||||
| log.Error("no such job!", ctx.Data["msgID"]) | |||||
| ctx.ServerError("no such job:", err) | |||||
| return | |||||
| } | |||||
| //get dirs | |||||
| dirs, err := getModelDirs(task.JobName, parentDir) | |||||
| if err != nil { | |||||
| log.Error("getModelDirs failed:%v", err.Error(), ctx.Data["msgID"]) | |||||
| ctx.ServerError("getModelDirs failed:", err) | |||||
| return | |||||
| } | |||||
| var fileInfos []FileInfo | |||||
| err = json.Unmarshal([]byte(dirs), &fileInfos) | |||||
| if err != nil { | |||||
| log.Error("json.Unmarshal failed:%v", err.Error(), ctx.Data["msgID"]) | |||||
| ctx.ServerError("json.Unmarshal failed:", err) | |||||
| return | |||||
| } | |||||
| ctx.Data["Path"] = dirArray | |||||
| ctx.Data["Dirs"] = fileInfos | |||||
| ctx.Data["task"] = task | |||||
| ctx.Data["JobID"] = jobID | |||||
| ctx.HTML(200, tplCloudBrainShowModels) | |||||
| } | |||||
| func getModelDirs(jobName string, parentDir string) (string, error) { | |||||
| var req string | |||||
| modelActualPath := setting.JobPath + jobName + "/model/" | |||||
| if parentDir == "" { | |||||
| req = "baseDir=" + modelActualPath | |||||
| } else { | |||||
| req = "baseDir=" + modelActualPath + "&parentDir=" + parentDir | |||||
| } | |||||
| return getDirs(req) | |||||
| } | |||||
| func CloudBrainDownloadModel(ctx *context.Context) { | |||||
| parentDir := ctx.Query("parentDir") | |||||
| fileName := ctx.Query("fileName") | |||||
| jobName := ctx.Query("jobName") | |||||
| filePath := "jobs/" +jobName + "/model/" + parentDir | |||||
| url, err := storage.Attachments.PresignedGetURL(filePath, fileName) | |||||
| if err != nil { | |||||
| log.Error("PresignedGetURL failed: %v", err.Error(), ctx.Data["msgID"]) | |||||
| ctx.ServerError("PresignedGetURL", err) | |||||
| return | |||||
| } | |||||
| http.Redirect(ctx.Resp, ctx.Req.Request, url, http.StatusMovedPermanently) | |||||
| } | |||||
| func GetRate(ctx *context.Context) { | func GetRate(ctx *context.Context) { | ||||
| var jobID = ctx.Params(":jobid") | var jobID = ctx.Params(":jobid") | ||||
| job, err := models.GetCloudbrainByJobID(jobID) | job, err := models.GetCloudbrainByJobID(jobID) | ||||
| @@ -341,11 +420,11 @@ func GetRate(ctx *context.Context) { | |||||
| } | } | ||||
| if job.JobType == string(models.JobTypeBenchmark) { | if job.JobType == string(models.JobTypeBenchmark) { | ||||
| ctx.Redirect(setting.BenchmarkServerHost) | |||||
| ctx.Redirect(setting.BenchmarkServerHost + "?username=" + ctx.User.Name) | |||||
| } else if job.JobType == string(models.JobTypeSnn4imagenet) { | } else if job.JobType == string(models.JobTypeSnn4imagenet) { | ||||
| ctx.Redirect(setting.Snn4imagenetServerHost) | ctx.Redirect(setting.Snn4imagenetServerHost) | ||||
| } else { | } else { | ||||
| log.Error("JobType error:", job.JobType) | |||||
| log.Error("JobType error:%s", job.JobType, ctx.Data["msgID"]) | |||||
| } | } | ||||
| } | } | ||||
| @@ -58,10 +58,10 @@ func DirIndex(ctx *context.Context) { | |||||
| dirArray = []string{attachment.Name} | dirArray = []string{attachment.Name} | ||||
| } | } | ||||
| dirs, err := getDirs(uuid, parentDir) | |||||
| dirs, err := getDatasetDirs(uuid, parentDir) | |||||
| if err != nil { | if err != nil { | ||||
| log.Error("getDirs failed:", err.Error()) | |||||
| ctx.ServerError("getDirs failed:", err) | |||||
| log.Error("getDatasetDirs failed:", err.Error()) | |||||
| ctx.ServerError("getDatasetDirs failed:", err) | |||||
| return | return | ||||
| } | } | ||||
| @@ -75,20 +75,31 @@ func DirIndex(ctx *context.Context) { | |||||
| ctx.Data["Path"] = dirArray | ctx.Data["Path"] = dirArray | ||||
| ctx.Data["Dirs"] = fileInfos | ctx.Data["Dirs"] = fileInfos | ||||
| ctx.Data["Uuid"] = uuid | |||||
| ctx.Data["PageIsDataset"] = true | ctx.Data["PageIsDataset"] = true | ||||
| ctx.HTML(200, tplDirIndex) | ctx.HTML(200, tplDirIndex) | ||||
| } | } | ||||
| func getDirs(uuid string, parentDir string) (string, error) { | |||||
| var dirs string | |||||
| func getDatasetDirs(uuid string, parentDir string) (string, error) { | |||||
| var req string | var req string | ||||
| dataActualPath := setting.Attachment.Minio.RealPath + | |||||
| setting.Attachment.Minio.Bucket + "/" + | |||||
| setting.Attachment.Minio.BasePath + | |||||
| models.AttachmentRelativePath(uuid) + | |||||
| uuid + "/" | |||||
| if parentDir == "" { | if parentDir == "" { | ||||
| req = "uuid=" + uuid | |||||
| req = "baseDir=" + dataActualPath | |||||
| } else { | } else { | ||||
| req = "uuid=" + uuid + "&parentDir=" + parentDir | |||||
| req = "baseDir=" + dataActualPath + "&parentDir=" + parentDir | |||||
| } | } | ||||
| return getDirs(req) | |||||
| } | |||||
| func getDirs(req string) (string, error) { | |||||
| var dirs string | |||||
| url := setting.DecompressAddress + "/dirs?" + req | url := setting.DecompressAddress + "/dirs?" + req | ||||
| reqHttp, err := http.NewRequest(http.MethodGet, url, nil) | reqHttp, err := http.NewRequest(http.MethodGet, url, nil) | ||||
| if err != nil { | if err != nil { | ||||
| @@ -127,6 +138,5 @@ func getDirs(uuid string, parentDir string) (string, error) { | |||||
| } | } | ||||
| dirs = resp.FileInfos | dirs = resp.FileInfos | ||||
| return dirs, nil | return dirs, nil | ||||
| } | } | ||||
| @@ -8,6 +8,7 @@ package repo | |||||
| import ( | import ( | ||||
| "fmt" | "fmt" | ||||
| "io" | "io" | ||||
| "net/url" | |||||
| "path" | "path" | ||||
| "strings" | "strings" | ||||
| @@ -32,6 +33,7 @@ func ServeData(ctx *context.Context, name string, reader io.Reader) error { | |||||
| // Google Chrome dislike commas in filenames, so let's change it to a space | // Google Chrome dislike commas in filenames, so let's change it to a space | ||||
| name = strings.Replace(name, ",", " ", -1) | name = strings.Replace(name, ",", " ", -1) | ||||
| name = url.QueryEscape(name) | |||||
| if base.IsTextFile(buf) || ctx.QueryBool("render") { | if base.IsTextFile(buf) || ctx.QueryBool("render") { | ||||
| cs, err := charset.DetectEncoding(buf) | cs, err := charset.DetectEncoding(buf) | ||||
| @@ -49,6 +49,7 @@ import ( | |||||
| "gitea.com/macaron/session" | "gitea.com/macaron/session" | ||||
| "gitea.com/macaron/toolbox" | "gitea.com/macaron/toolbox" | ||||
| "github.com/prometheus/client_golang/prometheus" | "github.com/prometheus/client_golang/prometheus" | ||||
| gouuid "github.com/satori/go.uuid" | |||||
| "github.com/tstranex/u2f" | "github.com/tstranex/u2f" | ||||
| ) | ) | ||||
| @@ -85,7 +86,7 @@ func setupAccessLogger(m *macaron.Macaron) { | |||||
| log.Error("Could not set up macaron access logger: %v", err.Error()) | log.Error("Could not set up macaron access logger: %v", err.Error()) | ||||
| } | } | ||||
| err = logger.SendLog(log.INFO, "", "", 0, buf.String(), "") | |||||
| err = logger.SendLog(log.INFO, "", "", 0, buf.String(), ctx.Data["msgID"].(string), "") | |||||
| if err != nil { | if err != nil { | ||||
| log.Error("Could not set up macaron access logger: %v", err.Error()) | log.Error("Could not set up macaron access logger: %v", err.Error()) | ||||
| } | } | ||||
| @@ -107,6 +108,24 @@ func RouterHandler(level log.Level) func(ctx *macaron.Context) { | |||||
| } | } | ||||
| } | } | ||||
| // SetLogMsgID set msgID in Context | |||||
| func SetLogMsgID() func(ctx *macaron.Context) { | |||||
| return func(ctx *macaron.Context) { | |||||
| start := time.Now() | |||||
| uuid := gouuid.NewV4().String() | |||||
| ctx.Data["MsgID"] = uuid | |||||
| log.Info("Started %s %s for %s", log.ColoredMethod(ctx.Req.Method), ctx.Req.URL.RequestURI(), ctx.RemoteAddr(), ctx.Data["MsgID"]) | |||||
| rw := ctx.Resp.(macaron.ResponseWriter) | |||||
| ctx.Next() | |||||
| status := rw.Status() | |||||
| log.Info("Completed %s %s %v %s in %v", log.ColoredMethod(ctx.Req.Method), ctx.Req.URL.RequestURI(), log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(rw.Status())), log.ColoredTime(time.Since(start)), ctx.Data["MsgID"]) | |||||
| } | |||||
| } | |||||
| // NewMacaron initializes Macaron instance. | // NewMacaron initializes Macaron instance. | ||||
| func NewMacaron() *macaron.Macaron { | func NewMacaron() *macaron.Macaron { | ||||
| gob.Register(&u2f.Challenge{}) | gob.Register(&u2f.Challenge{}) | ||||
| @@ -125,6 +144,7 @@ func NewMacaron() *macaron.Macaron { | |||||
| m.Use(macaron.Logger()) | m.Use(macaron.Logger()) | ||||
| } | } | ||||
| } | } | ||||
| m.Use(SetLogMsgID()) | |||||
| // Access Logger is similar to Router Log but more configurable and by default is more like the NCSA Common Log format | // Access Logger is similar to Router Log but more configurable and by default is more like the NCSA Common Log format | ||||
| if setting.EnableAccessLog { | if setting.EnableAccessLog { | ||||
| setupAccessLogger(m) | setupAccessLogger(m) | ||||
| @@ -910,9 +930,11 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Post("/commit_image", reqRepoCloudBrainWriter, bindIgnErr(auth.CommitImageCloudBrainForm{}), repo.CloudBrainCommitImage) | m.Post("/commit_image", reqRepoCloudBrainWriter, bindIgnErr(auth.CommitImageCloudBrainForm{}), repo.CloudBrainCommitImage) | ||||
| m.Post("/stop", reqRepoCloudBrainWriter, repo.CloudBrainStop) | m.Post("/stop", reqRepoCloudBrainWriter, repo.CloudBrainStop) | ||||
| m.Post("/del", reqRepoCloudBrainWriter, repo.CloudBrainDel) | m.Post("/del", reqRepoCloudBrainWriter, repo.CloudBrainDel) | ||||
| m.Get("/rate", reqRepoCloudBrainWriter, repo.GetRate) | |||||
| m.Get("/rate", reqRepoCloudBrainReader, repo.GetRate) | |||||
| m.Get("/models", reqRepoCloudBrainReader, repo.CloudBrainShowModels) | |||||
| m.Get("/download_model", reqRepoCloudBrainReader, repo.CloudBrainDownloadModel) | |||||
| }) | }) | ||||
| m.Get("/create", reqRepoCloudBrainWriter, repo.CloudBrainNew) | |||||
| m.Get("/create", reqRepoCloudBrainReader, repo.CloudBrainNew) | |||||
| m.Post("/create", reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate) | m.Post("/create", reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate) | ||||
| }, context.RepoRef()) | }, context.RepoRef()) | ||||
| @@ -535,7 +535,10 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR | |||||
| log.Error("Unable to store session: %v", err) | log.Error("Unable to store session: %v", err) | ||||
| } | } | ||||
| // Language setting of the user overwrites the one previously set | |||||
| // Language setting of the user use the one previously set | |||||
| if len(ctx.GetCookie("lang")) != 0 { | |||||
| u.Language = ctx.GetCookie("lang") | |||||
| } | |||||
| // If the user does not have a locale set, we save the current one. | // If the user does not have a locale set, we save the current one. | ||||
| if len(u.Language) == 0 { | if len(u.Language) == 0 { | ||||
| u.Language = ctx.Locale.Language() | u.Language = ctx.Locale.Language() | ||||
| @@ -25,7 +25,7 @@ | |||||
| {{range .Datasets}} | {{range .Datasets}} | ||||
| <div class="item"> | <div class="item"> | ||||
| <div class="ui header"> | <div class="ui header"> | ||||
| <a class="name" href="{{.Repo.Link}}/datasets"> | |||||
| <a class="name" href="{{.Repo.Link}}/datasets?type=0"> | |||||
| {{.Repo.OwnerName}} / {{.Title}} | {{.Repo.OwnerName}} / {{.Title}} | ||||
| </a> | </a> | ||||
| <div class="ui right metas"> | <div class="ui right metas"> | ||||
| @@ -1,14 +1,16 @@ | |||||
| <div class="tablet only mobile only sixteen wide mobile sixteen wide tablet column row"> | <div class="tablet only mobile only sixteen wide mobile sixteen wide tablet column row"> | ||||
| <div class="ui secondary pointing tabular top attached borderless menu navbar"> | <div class="ui secondary pointing tabular top attached borderless menu navbar"> | ||||
| <a class="{{if $.PageIsOrgHome}}active{{end}} item" href="{{.HomeLink}}"> | <a class="{{if $.PageIsOrgHome}}active{{end}} item" href="{{.HomeLink}}"> | ||||
| {{svg "octicon-home" 16}} {{$.i18n.Tr "org.组织主页"}} | |||||
| </a> | |||||
| <a class="{{if $.PageIsOrgMembers}}active{{end}} item" href="{{$.OrgLink}}/members"> | |||||
| {{svg "octicon-organization" 16}} {{$.i18n.Tr "org.people"}} | |||||
| </a> | |||||
| <a class="{{if $.PageIsOrgTeams}}active{{end}} item" href="{{$.OrgLink}}/teams"> | |||||
| {{svg "octicon-jersey" 16}} {{$.i18n.Tr "org.teams"}} | |||||
| {{svg "octicon-home" 16}} {{$.i18n.Tr "org.home"}} | |||||
| </a> | </a> | ||||
| {{if or ($.IsOrganizationMember) ($.IsOrganizationOwner)}} | |||||
| <a class="{{if $.PageIsOrgMembers}}active{{end}} item" href="{{$.OrgLink}}/members"> | |||||
| {{svg "octicon-organization" 16}} {{$.i18n.Tr "org.people"}} | |||||
| </a> | |||||
| <a class="{{if $.PageIsOrgTeams}}active{{end}} item" href="{{$.OrgLink}}/teams"> | |||||
| {{svg "octicon-jersey" 16}} {{$.i18n.Tr "org.teams"}} | |||||
| </a> | |||||
| {{end}} | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <!--平板、移动端--> | <!--平板、移动端--> | ||||
| @@ -18,15 +20,17 @@ | |||||
| <div class="sixteen wide column ui secondary sticky pointing tabular vertical menu"> | <div class="sixteen wide column ui secondary sticky pointing tabular vertical menu"> | ||||
| {{with .Org}} | {{with .Org}} | ||||
| <a class="{{if $.PageIsOrgHome}}active{{end}} item" href="{{.HomeLink}}"> | <a class="{{if $.PageIsOrgHome}}active{{end}} item" href="{{.HomeLink}}"> | ||||
| {{svg "octicon-home" 16}} {{$.i18n.Tr "org.主页"}} | |||||
| {{svg "octicon-home" 16}} {{$.i18n.Tr "org.home"}} | |||||
| </a> | </a> | ||||
| {{end}} | {{end}} | ||||
| <a class="{{if $.PageIsOrgMembers}}active{{end}} item" href="{{$.OrgLink}}/members"> | |||||
| {{svg "octicon-organization" 16}} {{$.i18n.Tr "org.people"}} | |||||
| </a> | |||||
| <a class="{{if $.PageIsOrgTeams}}active{{end}} item" href="{{$.OrgLink}}/teams"> | |||||
| {{svg "octicon-jersey" 16}} {{$.i18n.Tr "org.teams"}} | |||||
| </a> | |||||
| {{if or ($.IsOrganizationMember) ($.IsOrganizationOwner)}} | |||||
| <a class="{{if $.PageIsOrgMembers}}active{{end}} item" href="{{$.OrgLink}}/members"> | |||||
| {{svg "octicon-organization" 16}} {{$.i18n.Tr "org.people"}} | |||||
| </a> | |||||
| <a class="{{if $.PageIsOrgTeams}}active{{end}} item" href="{{$.OrgLink}}/teams"> | |||||
| {{svg "octicon-jersey" 16}} {{$.i18n.Tr "org.teams"}} | |||||
| </a> | |||||
| {{end}} | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -248,7 +248,7 @@ | |||||
| </div> | </div> | ||||
| <!--任务状态 --> | <!--任务状态 --> | ||||
| <div class="three wide column job-status" id="{{.JobID}}" data-repopath="{{$.RepoRelPath}}" data-jobid="{{.JobID}}"> | |||||
| <div class="two wide column job-status" id="{{.JobID}}" data-repopath="{{$.RepoRelPath}}" data-jobid="{{.JobID}}"> | |||||
| {{.Status}} | {{.Status}} | ||||
| </div> | </div> | ||||
| @@ -257,15 +257,6 @@ | |||||
| <span class="ui text center">{{svg "octicon-flame" 16}} {{TimeSinceUnix .CreatedUnix $.Lang}}</span> | <span class="ui text center">{{svg "octicon-flame" 16}} {{TimeSinceUnix .CreatedUnix $.Lang}}</span> | ||||
| </div> | </div> | ||||
| <!-- 查看 --> | |||||
| <div class="one wide column"> | |||||
| <span class="ui text clipboard"> | |||||
| <a class="title" href="{{$.Link}}/{{.JobID}}"> | |||||
| <span class="fitted">查看</span> | |||||
| </a> | |||||
| </span> | |||||
| </div> | |||||
| <!-- 评分 --> | <!-- 评分 --> | ||||
| <div class="one wide column"> | <div class="one wide column"> | ||||
| <div class="ui text center clipboard"> | <div class="ui text center clipboard"> | ||||
| @@ -304,6 +295,15 @@ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <!-- 模型下载 --> | |||||
| <div class="two wide column"> | |||||
| <span class="ui text clipboard"> | |||||
| <a class="title" href="{{$.Link}}/{{.JobID}}/models"> | |||||
| <span class="fitted">模型下载</span> | |||||
| </a> | |||||
| </span> | |||||
| </div> | |||||
| <!-- 接收结果 --> | <!-- 接收结果 --> | ||||
| <iframe src="" frameborder="0" name="iframeContent" style="display: none;"></iframe> | <iframe src="" frameborder="0" name="iframeContent" style="display: none;"></iframe> | ||||
| <a class="imageBtn" style="{{if not .CanDebug}}color:#CCCCCC;cursor:pointer;pointer-events:none;{{end}}; font-size:16px; font-weight:bold" value="{{.CanDebug}}">提交镜像</a> | <a class="imageBtn" style="{{if not .CanDebug}}color:#CCCCCC;cursor:pointer;pointer-events:none;{{end}}; font-size:16px; font-weight:bold" value="{{.CanDebug}}">提交镜像</a> | ||||
| @@ -0,0 +1,27 @@ | |||||
| {{if .Dirs}} | |||||
| <table id="repo-files-table" class="ui single line table"> | |||||
| <tbody> | |||||
| {{range .Dirs}} | |||||
| <tr> | |||||
| <td class="name four wide"> | |||||
| <span class="truncate"> | |||||
| <span class="octicon octicon-file-directory"></span> | |||||
| <a class="title" href="{{if .IsDir}}{{$.RepoLink}}/cloudbrain/{{$.JobID}}/models?parentDir={{.ParenDir}}{{else}}{{$.RepoLink}}/cloudbrain/{{$.JobID}}/download_model?parentDir={{.ParenDir}}&fileName={{.FileName}}&jobName={{$.task.JobName}}{{end}}"> | |||||
| <span class="fitted">{{if .IsDir}} {{svg "octicon-file-directory" 16}}{{else}}{{svg "octicon-file" 16}}{{end}}</span> {{.FileName}} | |||||
| </a> | |||||
| </span> | |||||
| </td> | |||||
| <td class="message nine wide"> | |||||
| <span class="truncate has-emoji"> | |||||
| {{.Size | FileSize}} | |||||
| </span> | |||||
| </td> | |||||
| <td class="text right age three wide"> | |||||
| <span class="time-since poping up">{{.ModTime}}</span> | |||||
| </td> | |||||
| </tr> | |||||
| {{end}} | |||||
| </tbody> | |||||
| </table> | |||||
| {{end}} | |||||
| @@ -0,0 +1,29 @@ | |||||
| {{template "base/head" .}} | |||||
| <div class="repository dataset dir-list view"> | |||||
| {{template "repo/header" .}} | |||||
| <form class="ui container"> | |||||
| <div class="ui stackable grid {{if .Error}}hide{{end}}" id="dir-content"> | |||||
| <div class="row"> | |||||
| <div class="column sixteen wide"> | |||||
| <p> | |||||
| {{ range $index, $item := .Path }}<a href='{{$.Link}}/?parentDir={{if gt $index 0}}{{DatasetPathJoin $.Path $index "/"}}{{else}}{{end}}'>{{ $item }}</a><span class="directory-seperator">/</span>{{ end }} | |||||
| </p> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="ui grid"> | |||||
| <div class="row"> | |||||
| <div class="ui sixteen wide column"> | |||||
| <div class="dir list"> | |||||
| {{template "repo/cloudbrain/models/dir_list" .}} | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </form> | |||||
| </div> | |||||
| {{template "base/footer" .}} | |||||
| @@ -6,7 +6,7 @@ | |||||
| <td class="name four wide"> | <td class="name four wide"> | ||||
| <span class="truncate"> | <span class="truncate"> | ||||
| <span class="octicon octicon-file-directory"></span> | <span class="octicon octicon-file-directory"></span> | ||||
| <a class="title" href="{{if .IsDir}}{{$.RepoLink}}/datasets/dirs/{{.UUID}}?parentDir={{.ParenDir}}{{end}}"> | |||||
| <a class="title" href="{{if .IsDir}}{{$.RepoLink}}/datasets/dirs/{{$.Uuid}}?parentDir={{.ParenDir}}{{end}}"> | |||||
| <span class="fitted">{{if .IsDir}} {{svg "octicon-file-directory" 16}}{{else}}{{svg "octicon-file" 16}}{{end}}</span> {{.FileName}} | <span class="fitted">{{if .IsDir}} {{svg "octicon-file-directory" 16}}{{else}}{{svg "octicon-file" 16}}{{end}}</span> {{.FileName}} | ||||
| </a> | </a> | ||||
| </span> | </span> | ||||
| @@ -76,8 +76,8 @@ | |||||
| {{.i18n.Tr "repo.issues.filter_sort"}}<i class="dropdown icon"></i> | {{.i18n.Tr "repo.issues.filter_sort"}}<i class="dropdown icon"></i> | ||||
| </span> | </span> | ||||
| <div class="menu"> | <div class="menu"> | ||||
| <a class="item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}&tab={{$.TabName}}">{{.i18n.Tr "repo.issues.filter_sort.latest"}}</a> | |||||
| <a class="item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}&tab={{$.TabName}}">{{.i18n.Tr "repo.issues.filter_sort.oldest"}}</a> | |||||
| <a class="item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}&tab={{$.TabName}}&type={{.Type}}">{{.i18n.Tr "repo.issues.filter_sort.latest"}}</a> | |||||
| <a class="item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}&tab={{$.TabName}}&type={{.Type}}">{{.i18n.Tr "repo.issues.filter_sort.oldest"}}</a> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||