Browse Source

Merge branch 'web' of https://git.openi.org.cn/OpenI/aiforge into web

tags/v1.21.12.1
OpenIhu 4 years ago
parent
commit
778366aeba
28 changed files with 537 additions and 68 deletions
  1. +17
    -5
      models/cloudbrain.go
  2. +4
    -4
      models/user.go
  3. +12
    -0
      models/user_mail.go
  4. +1
    -0
      modules/auth/cloudbrain.go
  5. +22
    -5
      modules/cloudbrain/cloudbrain.go
  6. +1
    -0
      modules/convert/convert.go
  7. +3
    -1
      modules/setting/setting.go
  8. +2
    -0
      modules/structs/user.go
  9. +1
    -1
      options/locale/locale_en-US.ini
  10. +1
    -0
      options/locale/locale_zh-CN.ini
  11. +48
    -0
      package-lock.json
  12. +1
    -0
      package.json
  13. +26
    -20
      routers/repo/attachment.go
  14. +7
    -1
      routers/repo/cloudbrain.go
  15. +2
    -2
      routers/repo/dataset.go
  16. +1
    -1
      routers/routes/routes.go
  17. +18
    -1
      routers/secure/user.go
  18. +9
    -0
      routers/user/auth.go
  19. +12
    -0
      routers/user/setting/profile.go
  20. +4
    -4
      templates/home.tmpl
  21. +15
    -5
      templates/repo/cloudbrain/new.tmpl
  22. +38
    -11
      templates/repo/home.tmpl
  23. +1
    -1
      templates/status/500.tmpl
  24. +1
    -1
      templates/user/settings/profile.tmpl
  25. +144
    -0
      web_src/js/components/EditAboutInfo.vue
  26. +18
    -4
      web_src/js/components/MinioUploader.vue
  27. +105
    -0
      web_src/js/components/basic/editDialog.vue
  28. +23
    -1
      web_src/js/index.js

+ 17
- 5
models/cloudbrain.go View File

@@ -72,11 +72,11 @@ type CloudBrainLoginResult struct {

type TaskRole struct {
Name string `json:"name"`
TaskNumber int8 `json:"taskNumber"`
MinSucceededTaskCount int8 `json:"minSucceededTaskCount"`
MinFailedTaskCount int8 `json:"minFailedTaskCount"`
CPUNumber int8 `json:"cpuNumber"`
GPUNumber int8 `json:"gpuNumber"`
TaskNumber int `json:"taskNumber"`
MinSucceededTaskCount int `json:"minSucceededTaskCount"`
MinFailedTaskCount int `json:"minFailedTaskCount"`
CPUNumber int `json:"cpuNumber"`
GPUNumber int `json:"gpuNumber"`
MemoryMB int `json:"memoryMB"`
ShmMB int `json:"shmMB"`
Command string `json:"command"`
@@ -286,6 +286,18 @@ type GpuInfo struct {
Queue string `json:"queue"`
}

type ResourceSpecs struct {
ResourceSpec []*ResourceSpec `json:"resorce_specs"`
}

type ResourceSpec struct {
Id int `json:"id"`
CpuNum int `json:"cpu"`
GpuNum int `json:"gpu"`
MemMiB int `json:"memMiB"`
ShareMemMiB int `json:"shareMemMiB"`
}

type CommitImageParams struct {
Ip string `json:"ip"`
TaskContainerId string `json:"taskContainerId"`


+ 4
- 4
models/user.go View File

@@ -976,12 +976,12 @@ func CreateUser(u *User) (err error) {
result, err := blockchain.CreateBlockchainAccount()
if err != nil {
log.Error("createBlockchainAccount failed:", err.Error())
return err
//return err
} else {
u.PublicKey = result.Payload["publickey"].(string)
u.PrivateKey = result.Payload["privatekey"].(string)
}

u.PublicKey = result.Payload["publickey"].(string)
u.PrivateKey = result.Payload["privatekey"].(string)

sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {


+ 12
- 0
models/user_mail.go View File

@@ -80,6 +80,18 @@ func GetEmailAddressByID(uid, id int64) (*EmailAddress, error) {
return email, nil
}

// GetEmailAddressByIDAndEmail gets a user's email address by ID and email
func GetEmailAddressByIDAndEmail(uid int64, emailAddr string) (*EmailAddress, error) {
// User ID is required for security reasons
email := &EmailAddress{UID: uid, Email: emailAddr}
if has, err := x.Get(email); err != nil {
return nil, err
} else if !has {
return nil, nil
}
return email, nil
}

func isEmailActive(e Engine, email string, userID, emailID int64) (bool, error) {
if len(email) == 0 {
return true, nil


+ 1
- 0
modules/auth/cloudbrain.go View File

@@ -13,6 +13,7 @@ type CreateCloudBrainForm struct {
JobType string `form:"job_type" binding:"Required"`
BenchmarkCategory string `form:"get_benchmark_category"`
GpuType string `form:"gpu_type"`
ResourceSpecId int `form:"resource_spec_id" binding:"Required"`
}

type CommitImageCloudBrainForm struct {


+ 22
- 5
modules/cloudbrain/cloudbrain.go View File

@@ -23,12 +23,29 @@ const (
Success = "S000"
)

func GenerateTask(ctx *context.Context, jobName, image, command, uuid, codePath, modelPath, benchmarkPath, snn4imagenetPath, jobType, gpuQueue string) error {
var (
ResourceSpecs *models.ResourceSpecs
)

func GenerateTask(ctx *context.Context, jobName, image, command, uuid, codePath, modelPath, benchmarkPath, snn4imagenetPath, jobType, gpuQueue string, resourceSpecId int) error {
dataActualPath := setting.Attachment.Minio.RealPath +
setting.Attachment.Minio.Bucket + "/" +
setting.Attachment.Minio.BasePath +
models.AttachmentRelativePath(uuid) +
uuid

var resourceSpec *models.ResourceSpec
for _, spec := range ResourceSpecs.ResourceSpec {
if resourceSpecId == spec.Id {
resourceSpec = spec
}
}

if resourceSpec == nil {
log.Error("no such resourceSpecId(%d)", resourceSpecId, ctx.Data["MsgID"])
return errors.New("no such resourceSpec")
}
jobResult, err := CreateJob(jobName, models.CreateJobParams{
JobName: jobName,
RetryCount: 1,
@@ -40,10 +57,10 @@ func GenerateTask(ctx *context.Context, jobName, image, command, uuid, codePath,
TaskNumber: 1,
MinSucceededTaskCount: 1,
MinFailedTaskCount: 1,
CPUNumber: 2,
GPUNumber: 1,
MemoryMB: 16384,
ShmMB: 8192,
CPUNumber: resourceSpec.CpuNum,
GPUNumber: resourceSpec.GpuNum,
MemoryMB: resourceSpec.MemMiB,
ShmMB: resourceSpec.ShareMemMiB,
Command: command,
NeedIBDevice: false,
IsMainRole: false,


+ 1
- 0
modules/convert/convert.go View File

@@ -335,6 +335,7 @@ func ToUser(user *models.User, signed, authed bool) *api.User {
AvatarURL: user.AvatarLink(),
FullName: markup.Sanitize(user.FullName),
Created: user.CreatedUnix.AsTime(),
IsActive: user.IsActive,
}
// hide primary email if API caller is anonymous or user keep email private
if signed && (!user.KeepEmailPrivate || authed) {


+ 3
- 1
modules/setting/setting.go View File

@@ -439,6 +439,7 @@ var (
JobType string
GpuTypes string
DebugServerHost string
ResourceSpecs string

//benchmark config
IsBenchmarkEnabled bool
@@ -1147,7 +1148,8 @@ func NewContext() {
JobPath = sec.Key("JOB_PATH").MustString("/datasets/minio/data/opendata/jobs/")
DebugServerHost = sec.Key("DEBUG_SERVER_HOST").MustString("http://192.168.202.73")
JobType = sec.Key("GPU_TYPE_DEFAULT").MustString("openidebug")
GpuTypes = sec.Key("GPU_TYPES").MustString("openidebug,openidgx")
GpuTypes = sec.Key("GPU_TYPES").MustString("")
ResourceSpecs = sec.Key("RESOURCE_SPECS").MustString("")

sec = Cfg.Section("benchmark")
IsBenchmarkEnabled = sec.Key("ENABLED").MustBool(false)


+ 2
- 0
modules/structs/user.go View File

@@ -26,6 +26,8 @@ type User struct {
Language string `json:"language"`
// Is the user an administrator
IsAdmin bool `json:"is_admin"`
// Is the user active
IsActive bool `json:"is_active"`
// swagger:strfmt date-time
LastLogin time.Time `json:"last_login,omitempty"`
// swagger:strfmt date-time


+ 1
- 1
options/locale/locale_en-US.ini View File

@@ -90,7 +90,7 @@ loading = Loading…
error404_index = Request forbidden by administrative rules
error500_index = Internal Server Error
error404 = The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it.
error500= Sorry, the site has encountered some problems, we are trying to <strong>fix the page</strong>, please try again later.
[error]
occurred = An error has occurred
report_message = An error has occurred


+ 1
- 0
options/locale/locale_zh-CN.ini View File

@@ -90,6 +90,7 @@ loading=正在加载...
error404_index = 您的访问受限!
error500_index = 抱歉!您指定的网页无法访问。
error404=您正尝试访问的页面 <strong>不存在</strong> 或 <strong>您尚未被授权</strong> 查看该页面。
error500=抱歉,站点遇到一些问题,我们正尝试<strong>修复网页</strong>,请您稍后再试。

[error]
occurred=发生错误


+ 48
- 0
package-lock.json View File

@@ -1841,6 +1841,14 @@
"async-done": "^1.2.2"
}
},
"async-validator": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-1.8.5.tgz",
"integrity": "sha512-tXBM+1m056MAX0E8TL2iCjg8WvSyXu0Zc8LNtYqrVeyoL3+esHRZ4SieE9fKQyyU09uONjnMEjrNBMqT0mbvmA==",
"requires": {
"babel-runtime": "6.x"
}
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1934,6 +1942,11 @@
}
}
},
"babel-helper-vue-jsx-merge-props": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz",
"integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg=="
},
"babel-loader": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz",
@@ -3882,6 +3895,26 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.437.tgz",
"integrity": "sha512-PBQn2q68ErqMyBUABh9Gh8R6DunGky8aB5y3N5lPM7OVpldwyUbAK5AX9WcwE/5F6ceqvQ+iQLYkJYRysAs6Bg=="
},
"element-ui": {
"version": "2.15.5",
"resolved": "https://registry.npmjs.org/element-ui/-/element-ui-2.15.5.tgz",
"integrity": "sha512-B/YCdz2aRY2WnFXzbTRTHPKZHBD/2KV6u88EBnkaARC/Lyxnap+7vpvrcW5UNTyVwjItS5Fj1eQyRy6236lbXg==",
"requires": {
"async-validator": "~1.8.1",
"babel-helper-vue-jsx-merge-props": "^2.0.0",
"deepmerge": "^1.2.0",
"normalize-wheel": "^1.0.1",
"resize-observer-polyfill": "^1.5.0",
"throttle-debounce": "^1.0.1"
},
"dependencies": {
"deepmerge": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz",
"integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ=="
}
}
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
@@ -9186,6 +9219,11 @@
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz",
"integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg=="
},
"normalize-wheel": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
"integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
},
"now-and-later": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz",
@@ -11687,6 +11725,11 @@
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
"integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA=="
},
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"resolve": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
@@ -13526,6 +13569,11 @@
"resolved": "https://registry.npmjs.org/textextensions/-/textextensions-2.6.0.tgz",
"integrity": "sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ=="
},
"throttle-debounce": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-1.1.0.tgz",
"integrity": "sha512-XH8UiPCQcWNuk2LYePibW/4qL97+ZQ1AN3FNXwZRBNPPowo/NRU5fAlDCSNBJIYCKbioZfuYtMhG4quqoJhVzg=="
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",


+ 1
- 0
package.json View File

@@ -19,6 +19,7 @@
"cssnano": "4.1.10",
"domino": "2.1.5",
"dropzone": "5.7.2",
"element-ui": "2.15.5",
"esdk-obs-browserjs": "3.20.7",
"esdk-obs-nodejs": "3.20.11",
"fast-glob": "3.2.2",


+ 26
- 20
routers/repo/attachment.go View File

@@ -151,18 +151,20 @@ func DownloadUserIsOrgOrCollaboration(ctx *context.Context, attach *models.Attac
log.Info("query repo error.")
} else {
repo.GetOwner()
if repo.Owner.IsOrganization() {
//log.Info("ower is org.")
if repo.Owner.IsUserPartOfOrg(ctx.User.ID) {
log.Info("org user may visit the attach.")
if ctx.User != nil {

if repo.Owner.IsOrganization() {
if repo.Owner.IsUserPartOfOrg(ctx.User.ID) {
log.Info("org user may visit the attach.")
return true
}
}
isCollaborator, _ := repo.IsCollaborator(ctx.User.ID)
if isCollaborator {
log.Info("Collaborator user may visit the attach.")
return true
}
}
isCollaborator, _ := repo.IsCollaborator(ctx.User.ID)
if isCollaborator {
log.Info("Collaborator user may visit the attach.")
return true
}
}
}
return false
@@ -192,19 +194,29 @@ func GetAttachment(ctx *context.Context) {
ctx.ServerError("LinkedRepository", err)
return
}
dataSet, err := attach.LinkedDataSet()
if err != nil {
ctx.ServerError("LinkedDataSet", err)
return
}

if repository == nil && dataSet != nil {
repository, _ = models.GetRepositoryByID(dataSet.RepoID)
unitType = models.UnitTypeDatasets
}

if repository == nil { //If not linked
//if !(ctx.IsSigned && attach.UploaderID == ctx.User.ID) && attach.IsPrivate { //We block if not the uploader
if !(ctx.IsSigned && attach.UploaderID == ctx.User.ID) && !DownloadUserIsOrgOrCollaboration(ctx, attach) { //We block if not the uploader
//log.Info("ctx.IsSigned =" + fmt.Sprintf("%v", ctx.IsSigned))
if !(ctx.IsSigned && attach.UploaderID == ctx.User.ID) && attach.IsPrivate && !DownloadUserIsOrgOrCollaboration(ctx, attach) { //We block if not the uploader
ctx.Error(http.StatusNotFound)
return
}

} else { //If we have the repository we check access

perm, err := models.GetUserRepoPermission(repository, ctx.User)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
perm, errPermission := models.GetUserRepoPermission(repository, ctx.User)
if errPermission != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", errPermission.Error())
return
}
if !perm.CanRead(unitType) {
@@ -213,12 +225,6 @@ func GetAttachment(ctx *context.Context) {
}
}

dataSet, err := attach.LinkedDataSet()
if err != nil {
ctx.ServerError("LinkedDataSet", err)
return
}

if dataSet != nil {
isPermit, err := models.GetUserDataSetPermission(dataSet, ctx.User)
if err != nil {


+ 7
- 1
routers/repo/cloudbrain.go View File

@@ -148,6 +148,11 @@ func CloudBrainNew(ctx *context.Context) {
json.Unmarshal([]byte(setting.GpuTypes), &gpuInfos)
}
ctx.Data["gpu_types"] = gpuInfos.GpuInfo

if cloudbrain.ResourceSpecs == nil {
json.Unmarshal([]byte(setting.ResourceSpecs), &cloudbrain.ResourceSpecs)
}
ctx.Data["resource_specs"] = cloudbrain.ResourceSpecs.ResourceSpec
ctx.Data["snn4imagenet_path"] = cloudbrain.Snn4imagenetMountPath
ctx.Data["is_snn4imagenet_enabled"] = setting.IsSnn4imagenetEnabled
ctx.HTML(200, tplCloudBrainNew)
@@ -162,6 +167,7 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) {
jobType := form.JobType
gpuQueue := setting.JobType
codePath := setting.JobPath + jobName + cloudbrain.CodeMountPath
resourceSpecId := form.ResourceSpecId

if jobType != string(models.JobTypeBenchmark) && jobType != string(models.JobTypeDebug) && jobType != string(models.JobTypeSnn4imagenet) {
log.Error("jobtype error:", jobType, ctx.Data["msgID"])
@@ -208,7 +214,7 @@ func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) {
downloadRateCode(repo, jobName, setting.Snn4imagenetCode, snn4imagenetPath, "", "")
}

err = cloudbrain.GenerateTask(ctx, jobName, image, command, uuid, codePath, modelPath, benchmarkPath, snn4imagenetPath, jobType, gpuQueue)
err = cloudbrain.GenerateTask(ctx, jobName, image, command, uuid, codePath, modelPath, benchmarkPath, snn4imagenetPath, jobType, gpuQueue, resourceSpecId)
if err != nil {
ctx.RenderWithErr(err.Error(), tplCloudBrainNew, &form)
return


+ 2
- 2
routers/repo/dataset.go View File

@@ -32,13 +32,13 @@ func newFilterPrivateAttachments(ctx *context.Context, list []*models.Attachment
repo.GetOwner()
}
permission := false
if repo.Owner.IsOrganization() {
if repo.Owner.IsOrganization() && ctx.User != nil {
if repo.Owner.IsUserPartOfOrg(ctx.User.ID) {
log.Info("user is member of org.")
permission = true
}
}
if !permission {
if !permission && ctx.User != nil {
isCollaborator, _ := repo.IsCollaborator(ctx.User.ID)
if isCollaborator {
log.Info("Collaborator user may visit the attach.")


+ 1
- 1
routers/routes/routes.go View File

@@ -1129,7 +1129,7 @@ func RegisterRoutes(m *macaron.Macaron) {

//secure api,
m.Group("/secure", func() {
m.Post("/user", binding.Bind(structs.CreateUserOption{}), secure.CreateUser)
m.Post("/user", binding.BindIgnErr(structs.CreateUserOption{}), secure.CreateUser)
}, reqBasicAuth)

m.Group("/api/internal", func() {


+ 18
- 1
routers/secure/user.go View File

@@ -7,6 +7,7 @@ package secure

import (
"net/http"
"strings"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
@@ -75,11 +76,19 @@ func CreateUser(ctx *context.Context, form api.CreateUserOption) {
u.MustChangePassword = *form.MustChangePassword
}

if strings.Contains(form.Email, " ") {
log.Error("CreateUser failed: email(%s) contains blank space", form.Email, ctx.Data["MsgID"])
ctx.JSON(http.StatusBadRequest, map[string]string{
"error_msg": "Email contains blank space",
})
return
}

parseLoginSource(ctx, u, form.SourceID, form.LoginName)
if ctx.Written() {
return
}
if !password.IsComplexEnough(form.Password) {
if !password.IsComplexEnough(form.Password) || len(form.Password) < setting.MinPasswordLength {
log.Error("CreateUser failed: PasswordComplexity", ctx.Data["MsgID"])
ctx.JSON(http.StatusBadRequest, map[string]string{
"error_msg": "PasswordComplexity",
@@ -119,6 +128,14 @@ func CreateUser(ctx *context.Context, form api.CreateUserOption) {
return
}

// Send confirmation email
if setting.Service.RegisterEmailConfirm{
mailer.SendActivateAccountMail(ctx.Locale, u)
if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
}

log.Trace("Account created (%s): %s", ctx.User.Name, u.Name, ctx.Data["MsgID"])

// Send email notification.


+ 9
- 0
routers/user/auth.go View File

@@ -1266,6 +1266,15 @@ func Activate(ctx *context.Context) {
log.Error("Error storing session: %v", err)
}

email, err := models.GetEmailAddressByIDAndEmail(user.ID, user.Email)
if err != nil || email == nil{
log.Error("GetEmailAddressByIDAndEmail failed", ctx.Data["MsgID"])
} else {
if err := email.Activate(); err != nil {
log.Error("Activate failed: %v", err, ctx.Data["MsgID"])
}
}

ctx.Flash.Success(ctx.Tr("auth.account_activated"))
ctx.Redirect(setting.AppSubURL + "/")
return


+ 12
- 0
routers/user/setting/profile.go View File

@@ -96,6 +96,18 @@ func ProfilePost(ctx *context.Context, form auth.UpdateProfileForm) {
ctx.User.Location = form.Location
ctx.User.Language = form.Language
ctx.User.Description = form.Description
isUsed, err := models.IsEmailUsed(form.Email)
if err != nil {
ctx.ServerError("IsEmailUsed", err)
return
}

if isUsed {
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}

if err := models.UpdateUserSetting(ctx.User); err != nil {
if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
ctx.Flash.Error(ctx.Tr("form.email_been_used"))


+ 4
- 4
templates/home.tmpl View File

@@ -127,7 +127,7 @@
<img src="/img/i-pic-01.svg">
</div>
<div class="content">
<a class="ui centered header">开发要素统一管理</a>
<h3 class="ui centered header">开发要素统一管理</h3>
<div class="description">
平台提供了AI开发四大要素:模型代码、数据集、模型和执行环境的统一管理
</div>
@@ -138,7 +138,7 @@
<img src="/img/i-pic-02.svg">
</div>
<div class="content">
<a class="ui centered header">数据协同与共享</a>
<h3 class="ui centered header">数据协同与共享</h3>
<div class="description">
通过在项目中上传数据集,项目成员多人协作完成数据预处理;也可以通过将数据设置为公有数据集,与社区开发者共同建立更好的模型
</div>
@@ -149,7 +149,7 @@
<img src="/img/i-pic-03.svg">
</div>
<div class="content">
<a class="ui centered header">模型管理与共享</a>
<h3 class="ui centered header">模型管理与共享</h3>
<div class="description">
将模型与代码版本建立关联,可以基于代码历史版本,使用不同的方式调整模型,并将结果保存下来;训练好的模型可以开放共享,让更多人的使用模型测试并提出反馈
</div>
@@ -160,7 +160,7 @@
<img src="/img/i-pic-04.svg">
</div>
<div class="content">
<a class="ui centered header">一次配置,多次使用</a>
<h3 class="ui centered header">一次配置,多次使用</h3>
<div class="description">
提供执行环境共享,一次配置,多次使用,降低模型开发门槛,避免花费重复的时间配置复杂的环境
</div>


+ 15
- 5
templates/repo/cloudbrain/new.tmpl View File

@@ -161,14 +161,15 @@

<div class="inline required field">
<label>镜像</label>
<select class="ui search" id="cloudbrain_image" placeholder="选择镜像" style='width:385px;' name="image">
<input type="text" list="cloudbrain_image" placeholder="选择镜像" name="image">
<datalist class="ui search" id="cloudbrain_image" style='width:385px;' name="image">
{{range .images}}
<option name="image" value="{{.Place}}">{{.PlaceView}}</option>
{{end}}
{{range .public_images}}
<option name="image" value="{{.Place}}">{{.PlaceView}}</option>
{{end}}
</select>
</datalist>
</div>

<div class="inline required field">
@@ -180,6 +181,15 @@
</select>
</div>

<div class="inline required field">
<label>资源规格</label>
<select id="cloudbrain_resource_spec" class="ui search dropdown" placeholder="选择资源规格" style='width:385px' name="resource_spec_id">
{{range .resource_specs}}
<option name="resource_spec_id" value="{{.Id}}">GPU数:{{.GpuNum}},CPU数:{{.CpuNum}},内存(MB):{{.MemMiB}},共享内存(MB):{{.ShareMemMiB}}</option>
{{end}}
</select>
</div>

<div class="inline required field">
<label>数据集存放路径</label>
<input name="dataset_path" id="cloudbrain_dataset_path" value="{{.dataset_path}}" tabindex="3" autofocus required maxlength="255" readonly="readonly">
@@ -242,9 +252,9 @@
.dropdown();

$('#cloudbrain_image').select2({
placeholder: "选择镜像"
});
// $('#cloudbrain_image').select2({
// placeholder: "选择镜像"
// });

$(".ui.button.reset").click(function(e){


+ 38
- 11
templates/repo/home.tmpl View File

@@ -12,6 +12,15 @@
float: left;
margin: .25em;
}
.edit-link{
vertical-align: top;
display: inline-block;
overflow: hidden;
word-break: keep-all;
white-space: nowrap;
text-overflow: ellipsis;
width: 16.5em;
}
#contributorInfo > a.circular{
height: 2.0em;
padding: 0;
@@ -197,37 +206,55 @@
</div>
<div class="ui six wide tablet four wide computer column">
<div id="repo-desc">
<h4 class="ui header">简介</h4>
<h4 id="about-desc" class="ui header">简介
<!-- <a class="edit-icon" href="javascript:void(0)">
<i class="gray edit outline icon"></i>
</a> -->
</h4>
<p>
{{if .Repository.DescriptionHTML}}
<span class="description">{{.Repository.DescriptionHTML}}</span>
{{else}}
<span class="no-description text-italic">{{.i18n.Tr "repo.no_desc"}}</span>
{{end}}
<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
{{if .Repository.DescriptionHTML}}
<span class="description" style="word-break:break-all">{{.Repository.DescriptionHTML}}</span>
{{else}}
<span class="no-description text-italic">{{.i18n.Tr "repo.no_desc"}}</span>
{{end}}
</p>

</div>


{{if .Repository.Website}}
<p class="ui">
<i class="gray linkify icon"></i>
<a class="link edit-link" target="_blank" title="{{.Repository.Website}}" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
</p>
{{end}}

<p class="ui" id="repo-topics">
<i class="grey bookmark icon"></i>
{{range .Topics}}<a class="ui repo-topic small label topic" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<a id="manage_topic">{{.i18n.Tr "repo.topic.manage_topics"}}</a>{{end}}
</p>

<p class="ui">
<i class="grey code icon"></i>
{{range .LanguageStats}}
{{.Language}}
{{end}}
</p>

{{if .LICENSE}}
<p class="ui">
<i class="grey clone icon"></i>
{{if .LICENSE}}
{{.LICENSE}}
{{end}}
{{.LICENSE}}
</p>
{{end}}

<div class="ui divider"></div>



+ 1
- 1
templates/status/500.tmpl View File

@@ -6,7 +6,7 @@
<img class="ui centered medium image" src="{{StaticUrlPrefix}}/img/icon-500@2x.png">
<h2>{{.i18n.Tr "error500_index"}}</h2>
<p>{{.i18n.Tr "error404" | Safe}}</p>
<p>{{.i18n.Tr "error500" | Safe}}</p>
</div>
</div>


+ 1
- 1
templates/user/settings/profile.tmpl View File

@@ -23,7 +23,7 @@
</div>
<div class="required field {{if .Err_Email}}error{{end}}">
<label for="email">{{.i18n.Tr "email"}}</label>
<input id="email" name="email" value="{{.SignedUser.Email}}">
<input id="email" name="email" disabled value="{{.SignedUser.Email}}">
</div>
<div class="inline field">
<div class="ui checkbox" id="keep-email-private">


+ 144
- 0
web_src/js/components/EditAboutInfo.vue View File

@@ -0,0 +1,144 @@
<template>
<div>
<h4 id="about-desc" class="ui header">简介
<!-- <a class="edit-icon" href="javascript:void(0)" @click="editClick">
<i class="gray edit outline icon"></i>
</a> -->
</h4>
<edit-dialog-cmpt
:vmContext="vmContext"
dialogTitle="编辑仓库信息"
v-model="editDataDialog"
:deleteCallback="editDataFunc"
:deleteLoading ="editDataListLoading"
deleteParam = "ruleForm"
@input="initForm"
>
<div slot="title">
</div>
<div slot="content">
<el-form label-position="top" :model="info" :rules="rule" ref="ruleForm">
<el-form-item label="简介" prop="desc">
<el-input v-model="info.desc" type="textarea" :autosize="{minRows:2,maxRows:6}"></el-input>
</el-form-item>
<el-form-item label="主页" prop="index_web" >
<el-input v-model="info.index_web" placeholder="主页(eg: https://git.openi.org.cn)"></el-input>
</el-form-item>
</el-form>
</div>
</edit-dialog-cmpt>
</div>
</template>

<script>

const {_AppSubUrl, _StaticUrlPrefix, csrf} = window.config;

import editDialogCmpt from './basic/editDialog.vue';


export default {
components: {
editDialogCmpt,
},
data() {
return {
vmContext: this,
editDataDialog: false,
editDataListLoading: false,
url: '',
info: {
desc: '',
index_web: '',
repo_name_name: '',
},
// rule1:[{min:3,max:5,message:'1',trigger:"blur"}],
rule: {
index_web: [
{required: false, pattern: /(^$)|(^(http|https):\/\/(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).*)|(^(http|https):\/\/[a-zA-Z0-9]+([_\-\.][a-zA-Z0-9]+)*\.[a-zA-Z]{2,10}(:[0-9]{1,10})?(\?.*)?(\/.*)?$)/,message:'请输入有效的URL',tigger:['change','blur']}
]
}
};
},
methods: {
editClick() {
this.editDataDialog = true;
},
getDesc() {
const el = $('span.description').text();
this.info.desc = el;
},
getWeb() {
const el = $('a.link').text();
this.info.index_web = el;
},
getRepoName() {
const el = this.url.split('/')[2];
this.info.repo_name = el;
},
initForm(diaolog) {
if (diaolog === false) {
console.log("--watch----------")
this.getRepoName();
this.getDesc();
this.getWeb();
}

},
editDataFunc(formName) {
this.$refs[formName].validate((valid)=>{
if (valid) {
this.$axios({
method: 'post',
url: this.url,
header: {'content-type': 'application/x-www-form-urlencoded'},
data: this.qs.stringify({
_csrf: csrf,
action: 'update',
repo_name: this.info.repo_name,
description: this.info.desc,
website: this.info.index_web
})
}).then((res) => {
location.reload();
this.editDataDialog = false;
}).catch((error) => {
this.editDataDialog = false;
})
}
else {
return false;
}
})

},
getUrl() {
const url = `${window.location.pathname}/settings`;
this.url = url;
}
},
mounted() {
this.getUrl();
this.getRepoName();
this.getDesc();
this.getWeb();
},
created() {
}

};
</script>

<style scoped>
.edit-icon{
float: right;
font-size: 16px;
display: block;
top: -2px;
color: #8c92a4;
background-color: transparent;
}

</style>

+ 18
- 4
web_src/js/components/MinioUploader.vue View File

@@ -335,8 +335,19 @@ export default {

async function uploadMinio(url, e) {
const res = await axios.put(url, e.target.result);
delete e.target.result
delete e.target.result
etags[currentChunk] = res.headers.etag;
}
async function uploadMinioNewMethod(url,e){
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, false);
xhr.setRequestHeader('Content-Type', 'text/plain')
xhr.send(e.target.result);
var etagValue = xhr.getResponseHeader('etag');
//console.log(etagValue);
etags[currentChunk] = etagValue;
}

async function updateChunk(currentChunk) {
@@ -359,8 +370,10 @@ export default {
// 获取分片上传url
await getUploadChunkUrl(currentChunk, partSize);
if (urls[currentChunk] != '') {
// 上传到minio
await uploadMinio(urls[currentChunk], e);
// 上传到minio
//await uploadMinio(urls[currentChunk], e);
await uploadMinioNewMethod(urls[currentChunk], e);
if (etags[currentChunk] != '') {
// 更新数据库:分片上传结果
//await updateChunk(currentChunk);
@@ -372,8 +385,9 @@ export default {
}
}
} catch (error) {
this.emitDropzoneFailed(file);
console.log(error);
//this.emitDropzoneFailed(file);
//console.log(error);
}
}



+ 105
- 0
web_src/js/components/basic/editDialog.vue View File

@@ -0,0 +1,105 @@
<template>

<el-dialog :close-on-click-modal="!deleteLoading" v-dlg-drag :title="dialogTitle" :visible.sync="deleteDialog">
<div class="message-box__content">
<div class="message-box-title" >
<slot name="title"></slot>
</div>
<div class="message-box-info">
<slot name="info"></slot>
</div>
</div>
<slot name="content"></slot>
<div slot="footer" class="dialog-footer">
<el-button @click="deleteDialog = false">{{"取消"}}</el-button>
<el-button type="primary" @click="deleteCallback.call(vmContext,deleteParam)">{{"确定"}}</el-button>
</div>
</el-dialog>
</template>
<script type="text/javascript">
export default {
data() {
return {
deleteDialog: false,
};
},
props: {

vmContext: {
type: Object,
default() {
return {};
}
},
dialogTitle: {
type: String,
default: '',
},
deleteLoading: {
type: Boolean,
default: false,
},
deleteCallback: {
type: Function,
default() {
return () => {};
}
},
deleteParam: {
type: String,
default: ''
},
value: {
type: Boolean,
default: false,
}
},
computed: {
},
watch: {
deleteDialog() {
this.$emit('input', this.deleteDialog);
},
value() {
this.deleteDialog = this.value;
},
},
created() {
this.deleteDialog = this.value;
}
};
</script>

<style scoped>
.el-message-box__content .icon{float:left;}
.message-box__content{}
.message-box__content .icon{float:left;margin-right:20px;}
.message-box__content .message-box-title{font-size:16px;padding:2px 0;color:#333;}
.message-box__content .message-box-p{font-size:16px;padding:20px 0;color:#333;}
.message-box__content .message-box-info{color:#999;padding:2px 0;}
.dialog-footer,.el-message-box__btns{text-align:center;}


/deep/ .el-dialog__header {
background: #0067b3;
padding: 12px 30px;
line-height: 25px;}
/deep/ .el-dialog__title {
font-family: PingFangSC-Regular;
font-size: 18px;
color: #fff;
font-weight: 200;
line-height: 25px;
height: 25px;
}
/deep/ .el-dialog__footer {
background: #eff3f9;
padding: 20px 30px;
}
/deep/ .el-dialog{
width: 40%;
}
</style>

+ 23
- 1
web_src/js/index.js View File

@@ -6,6 +6,10 @@ import './publicpath.js';
import './polyfills.js';

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import axios from 'axios';
import qs from 'qs';
import 'jquery.are-you-sure';
import './vendor/semanticdropdown.js';
import {svg} from './utils.js';
@@ -29,8 +33,12 @@ import {
} from './features/notification.js';
import {createCodeEditor} from './features/codeeditor.js';
import MinioUploader from './components/MinioUploader.vue';
import ObsUploader from './components/ObsUploader.vue'
import ObsUploader from './components/ObsUploader.vue';
import EditAboutInfo from './components/EditAboutInfo.vue';

Vue.use(ElementUI);
Vue.prototype.$axios = axios;
Vue.prototype.qs = qs;
const {AppSubUrl, StaticUrlPrefix, csrf} = window.config;

function htmlEncode(text) {
@@ -2957,6 +2965,7 @@ $(document).ready(async () => {
initVueApp();
initVueUploader();
initObsUploader();
initVueEditAbout();
initTeamSettings();
initCtrlEnterSubmit();
initNavbarContentToggle();
@@ -3642,6 +3651,19 @@ function initVueUploader() {
});
}

function initVueEditAbout() {
const el = document.getElementById('about-desc');
console.log(el)
if (!el) {
return;
}

new Vue({
el: '#about-desc',
render: h => h(EditAboutInfo)
});
}

// 新增
function initObsUploader() {
const el = document.getElementById('obsUploader');


Loading…
Cancel
Save