#2780 资源管理一期

Merged
lewis merged 53 commits from res-manage into V20220830 1 year ago
  1. +49
    -0
      models/admin_operate_log.go
  2. +44
    -0
      models/cloudbrain.go
  3. +5
    -0
      models/models.go
  4. +349
    -0
      models/resource_queue.go
  5. +329
    -0
      models/resource_scene.go
  6. +285
    -0
      models/resource_specification.go
  7. +13
    -0
      modules/cron/tasks_basic.go
  8. +33
    -0
      modules/grampus/resty.go
  9. +7
    -0
      options/locale/locale_en-US.ini
  10. +7
    -0
      options/locale/locale_zh-CN.ini
  11. +19351
    -17
      package-lock.json
  12. +1
    -0
      package.json
  13. +248
    -0
      routers/admin/resources.go
  14. +14
    -0
      routers/response/error.go
  15. +5
    -1
      routers/response/response.go
  16. +4
    -0
      routers/response/response_list.go
  17. +26
    -0
      routers/routes/routes.go
  18. +14
    -0
      services/admin/operate_log/operate_log.go
  19. +122
    -0
      services/cloudbrain/resource/resource_queue.go
  20. +35
    -0
      services/cloudbrain/resource/resource_scene.go
  21. +186
    -0
      services/cloudbrain/resource/resource_specification.go
  22. +3
    -4
      templates/admin/cloudbrain/images.tmpl
  23. +14
    -15
      templates/admin/cloudbrain/list.tmpl
  24. +63
    -43
      templates/admin/navbar.tmpl
  25. +10
    -0
      templates/admin/resources/queue.tmpl
  26. +10
    -0
      templates/admin/resources/scene.tmpl
  27. +10
    -0
      templates/admin/resources/specification.tmpl
  28. +5
    -6
      web_src/js/components/images/adminImages.vue
  29. +55
    -1
      web_src/less/_admin.less
  30. +41
    -0
      web_src/vuepages/apis/modules/point.js
  31. +174
    -0
      web_src/vuepages/apis/modules/resources.js
  32. +26
    -0
      web_src/vuepages/apis/service.js
  33. +99
    -0
      web_src/vuepages/components/BaseDialog.vue
  34. +16
    -0
      web_src/vuepages/const/index.js
  35. +156
    -0
      web_src/vuepages/langs/config/en-US.js
  36. +156
    -0
      web_src/vuepages/langs/config/zh-CN.js
  37. +16
    -0
      web_src/vuepages/langs/index.js
  38. +259
    -0
      web_src/vuepages/pages/resources/components/QueueDialog.vue
  39. +366
    -0
      web_src/vuepages/pages/resources/components/SceneDialog.vue
  40. +336
    -0
      web_src/vuepages/pages/resources/components/SpecificationDialog.vue
  41. +284
    -0
      web_src/vuepages/pages/resources/queue/index.vue
  42. +17
    -0
      web_src/vuepages/pages/resources/queue/vp-resources-queue.js
  43. +361
    -0
      web_src/vuepages/pages/resources/scene/index.vue
  44. +17
    -0
      web_src/vuepages/pages/resources/scene/vp-resources-scene.js
  45. +451
    -0
      web_src/vuepages/pages/resources/specification/index.vue
  46. +17
    -0
      web_src/vuepages/pages/resources/specification/vp-resources-specification.js
  47. +148
    -0
      web_src/vuepages/pages/reward/point/utils.js
  48. +16
    -0
      web_src/vuepages/pages/reward/point/vp-point.js
  49. +308
    -0
      web_src/vuepages/pages/reward/point/vp-point.vue
  50. +7
    -0
      web_src/vuepages/utils/index.js
  51. +7
    -0
      webpack.config.js
  52. +7
    -0
      webpack_pro.config.js

+ 49
- 0
models/admin_operate_log.go View File

@@ -0,0 +1,49 @@
package models

import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"encoding/json"
)

type AdminOperateLog struct {
ID int64 `xorm:"pk autoincr"`
BizType string
OperateType string
OldValue string `xorm:"TEXT"`
NewValue string `xorm:"TEXT"`
RelatedId string `xorm:"INDEX"`
Comment string
CreatedTime timeutil.TimeStamp `xorm:"created"`
CreatedBy int64
}

type LogValues struct {
Params []LogValue
}

type LogValue struct {
Key string
Val interface{}
}

func (l *LogValues) Add(key string, val interface{}) *LogValues {
l.Params = append(l.Params, LogValue{Key: key, Val: val})
return l
}

func (l *LogValues) JsonString() string {
if len(l.Params) == 0 {
return ""
}
b, err := json.Marshal(l)
if err != nil {
log.Error("LogValues JsonString error . %v", err)
return ""
}
return string(b)
}

func InsertAdminOperateLog(log AdminOperateLog) (int64, error) {
return x.Insert(&log)
}

+ 44
- 0
models/cloudbrain.go View File

@@ -111,6 +111,16 @@ const (
GrampusStatusWaiting = "WAITING"
)

const (
//cluster
OpenICluster = "OpenI"
C2NetCluster = "C2Net"

//AI center
AICenterOfCloudBrainOne = "OpenIOne"
AICenterOfCloudBrainTwo = "OpenITwo"
)

type Cloudbrain struct {
ID int64 `xorm:"pk autoincr"`
JobID string `xorm:"INDEX NOT NULL"`
@@ -1338,6 +1348,34 @@ type GrampusSpec struct {
Name string `json:"name"`
ProcessorType string `json:"processorType"`
Centers []Center `json:"centers"`
SpecInfo SpecInfo `json:"specInfo"`
}

type GrampusAiCenter struct {
AccDevices []GrampusAccDevice `json:"accDevices"`
Id string `json:"id"`
Name string `json:"name"`
Resource []GrampusCenterResource `json:"resource"`
}

type GrampusAccDevice struct {
Kind string `json:"kind"` //加速卡类别, npu.huawei.com/NPU,nvidia.com/gpu,cambricon.com/mlu
Model string `json:"model"` //加速卡型号
}

type GrampusCenterResource struct {
Allocated string `json:"allocated"`
Capacity string `json:"capacity"`
Name string `json:"name"`
}

type SpecInfo struct {
AccDeviceKind string `json:"accDeviceKind"`
AccDeviceMemory string `json:"accDeviceMemory"`
AccDeviceModel string `json:"accDeviceModel"`
AccDeviceNum int `json:"accDeviceNum"`
CpuCoreNum int `json:"cpuCoreNum"`
MemorySize string `json:"memorySize"`
}

type GetGrampusResourceSpecsResult struct {
@@ -1345,6 +1383,12 @@ type GetGrampusResourceSpecsResult struct {
Infos []GrampusSpec `json:"resourceSpecs"`
}

type GetGrampusAiCentersResult struct {
GrampusResult
Infos []GrampusAiCenter `json:"aiCenterInfos"`
TotalSize int `json:"totalSize"`
}

type GrampusImage struct {
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`


+ 5
- 0
models/models.go View File

@@ -145,6 +145,11 @@ func init() {
new(OrgStatistic),
new(SearchRecord),
new(AiModelConvert),
new(ResourceQueue),
new(ResourceSpecification),
new(ResourceScene),
new(ResourceSceneSpec),
new(AdminOperateLog),
new(CloudbrainTemp),
new(DatasetReference),
)


+ 349
- 0
models/resource_queue.go View File

@@ -0,0 +1,349 @@
package models

import (
"code.gitea.io/gitea/modules/timeutil"
"errors"
"strconv"
"strings"
"xorm.io/builder"
)

type ResourceQueue struct {
ID int64 `xorm:"pk autoincr"`
QueueCode string
Cluster string `xorm:"notnull"`
AiCenterCode string
AiCenterName string
ComputeResource string
AccCardType string
CardsTotalNum int
IsAutomaticSync bool
Remark string
DeletedTime timeutil.TimeStamp `xorm:"deleted"`
CreatedTime timeutil.TimeStamp `xorm:"created"`
CreatedBy int64
UpdatedTime timeutil.TimeStamp `xorm:"updated"`
UpdatedBy int64
}

func (r ResourceQueue) ConvertToRes() *ResourceQueueRes {
return &ResourceQueueRes{
ID: r.ID,
QueueCode: r.QueueCode,
Cluster: r.Cluster,
AiCenterCode: r.AiCenterCode,
AiCenterName: r.AiCenterName,
ComputeResource: r.ComputeResource,
AccCardType: r.AccCardType,
CardsTotalNum: r.CardsTotalNum,
UpdatedTime: r.UpdatedTime,
Remark: r.Remark,
}
}

type ResourceQueueReq struct {
QueueCode string
Cluster string `binding:"Required"`
AiCenterCode string
ComputeResource string `binding:"Required"`
AccCardType string `binding:"Required"`
CardsTotalNum int
CreatorId int64
IsAutomaticSync bool
Remark string
}

func (r ResourceQueueReq) ToDTO() ResourceQueue {
q := ResourceQueue{
QueueCode: r.QueueCode,
Cluster: r.Cluster,
AiCenterCode: r.AiCenterCode,
ComputeResource: strings.ToUpper(r.ComputeResource),
AccCardType: strings.ToUpper(r.AccCardType),
CardsTotalNum: r.CardsTotalNum,
IsAutomaticSync: r.IsAutomaticSync,
Remark: r.Remark,
CreatedBy: r.CreatorId,
UpdatedBy: r.CreatorId,
}
if r.Cluster == OpenICluster {
if r.AiCenterCode == AICenterOfCloudBrainOne {
q.AiCenterName = "云脑一"
} else if r.AiCenterCode == AICenterOfCloudBrainTwo {
q.AiCenterName = "云脑二"
}
}
return q
}

type SearchResourceQueueOptions struct {
ListOptions
Cluster string
AiCenterCode string
ComputeResource string
AccCardType string
}

type ResourceQueueListRes struct {
TotalSize int64
List []*ResourceQueueRes
}

type ResourceQueueCodesRes struct {
ID int64
QueueCode string
Cluster string
AiCenterCode string
AiCenterName string
}

func (ResourceQueueCodesRes) TableName() string {
return "resource_queue"
}

type ResourceAiCenterRes struct {
AiCenterCode string
AiCenterName string
}

type GetQueueCodesOptions struct {
Cluster string
}

func NewResourceQueueListRes(totalSize int64, list []ResourceQueue) *ResourceQueueListRes {
resList := make([]*ResourceQueueRes, len(list))
for i, v := range list {
resList[i] = v.ConvertToRes()
}
return &ResourceQueueListRes{
TotalSize: totalSize,
List: resList,
}
}

type ResourceQueueRes struct {
ID int64
QueueCode string
Cluster string
AiCenterCode string
AiCenterName string
ComputeResource string
AccCardType string
CardsTotalNum int
UpdatedTime timeutil.TimeStamp
Remark string
}

func InsertResourceQueue(queue ResourceQueue) (int64, error) {
return x.Insert(&queue)
}

func UpdateResourceQueueById(queueId int64, queue ResourceQueue) (int64, error) {
return x.ID(queueId).Update(&queue)
}

func SearchResourceQueue(opts SearchResourceQueueOptions) (int64, []ResourceQueue, error) {
var cond = builder.NewCond()
if opts.Page <= 0 {
opts.Page = 1
}
if opts.Cluster != "" {
cond = cond.And(builder.Eq{"cluster": opts.Cluster})
}
if opts.AiCenterCode != "" {
cond = cond.And(builder.Eq{"ai_center_code": opts.AiCenterCode})
}
if opts.ComputeResource != "" {
cond = cond.And(builder.Eq{"compute_resource": opts.ComputeResource})
}
if opts.AccCardType != "" {
cond = cond.And(builder.Eq{"acc_card_type": opts.AccCardType})
}
n, err := x.Where(cond).Unscoped().Count(&ResourceQueue{})
if err != nil {
return 0, nil, err
}

r := make([]ResourceQueue, 0)
err = x.Where(cond).Desc("id").Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).Unscoped().Find(&r)
if err != nil {
return 0, nil, err
}
return n, r, nil
}

func GetResourceQueueCodes(opts GetQueueCodesOptions) ([]*ResourceQueueCodesRes, error) {
cond := builder.NewCond()
if opts.Cluster != "" {
cond = cond.And(builder.Eq{"cluster": opts.Cluster})
}
cond = cond.And(builder.Or(builder.IsNull{"deleted_time"}, builder.Eq{"deleted_time": 0}))
r := make([]*ResourceQueueCodesRes, 0)
err := x.Where(cond).OrderBy("cluster desc,ai_center_code asc").Find(&r)
if err != nil {
return nil, err
}
return r, nil
}

func GetResourceQueue(r *ResourceQueue) (*ResourceQueue, error) {
has, err := x.Get(r)
if err != nil {
return nil, err
} else if !has {
return nil, nil
}
return r, nil
}

func ParseComputeResourceFormGrampus(grampusDeviceKind string) string {
t := strings.Split(grampusDeviceKind, "/")
if len(t) < 2 {
return ""
}
return strings.ToUpper(t[1])
}

type MemSize struct {
Sizes []string
Hex int
}

var memSize = MemSize{Sizes: []string{"K", "M", "G", "T", "P", "E"}, Hex: 1000}
var iMemSize = MemSize{Sizes: []string{"Ki", "Mi", "Gi", "Ti", "Pi", "Ei"}, Hex: 1024}

func MatchMemSize(memSize MemSize, val string) (int, float32, error) {
for i, v := range memSize.Sizes {
if strings.HasSuffix(val, v) {
s := strings.TrimSuffix(val, v)
f, err := strconv.ParseFloat(s, 32)
if err != nil {
return 0, 0, err
}
return i, float32(f), nil
}
}
return -1, 0, nil
}

//TransferMemSize transfer oldValue format from old index to new index
//eg: memSize.Sizes = []string{"M", "G", "T", "P", "E"}, oldValue = 10 , oldIndex = 1 , newIndex = 0. it means transfer 10G to 10000M
//so it returns 10000
func TransferMemSize(memSize MemSize, oldValue float32, oldIndex int, newIndex int) float32 {
diff := oldIndex - newIndex
r := oldValue
if diff > 0 {
r = oldValue * float32(diff) * float32(memSize.Hex)
} else if diff < 0 {
r = oldValue / float32(-1*diff) / float32(memSize.Hex)
}
return r
}

//ParseMemSize find the memSize which matches value's format,and parse the number from value
func ParseMemSize(value string, memSize MemSize, newIndex int) (bool, float32, error) {
index, r, err := MatchMemSize(memSize, value)
if err != nil {
return false, 0, err
}
if index < 0 {
return false, 0, nil
}
return true, TransferMemSize(memSize, r, index, newIndex), nil
}

func ParseMemSizeFromGrampus(grampusMemSize string) (float32, error) {
if grampusMemSize == "" {
return 0, nil
}
memflag, memResult, err := ParseMemSize(grampusMemSize, memSize, 2)
if err != nil {
return 0, err
}
if memflag {
return memResult, nil
}

iMemFlag, imemResult, err := ParseMemSize(grampusMemSize, iMemSize, 2)
if err != nil {
return 0, err
}
if iMemFlag {
return imemResult, nil
}
return 0, errors.New("grampus memSize format error")
}

func SyncGrampusQueues(updateList []ResourceQueue, insertList []ResourceQueue, existIds []int64) error {
sess := x.NewSession()
var err error
defer func() {
if err != nil {
sess.Rollback()
}
sess.Close()
}()

//delete queues that no longer exists
deleteQueueIds := make([]int64, 0)
queueCond := builder.NewCond()
queueCond = queueCond.And(builder.NotIn("resource_queue.id", existIds)).And(builder.Eq{"resource_queue.cluster": C2NetCluster})
if err := sess.Cols("resource_queue.id").Table("resource_queue").
Where(queueCond).Find(&deleteQueueIds); err != nil {
return err
}

if len(deleteQueueIds) > 0 {
if _, err = sess.In("id", deleteQueueIds).Update(&ResourceQueue{Remark: "自动同步时被下架"}); err != nil {
return err
}
if _, err = sess.In("id", deleteQueueIds).Delete(&ResourceQueue{}); err != nil {
return err
}

//delete specs and scene that no longer exists
deleteSpcIds := make([]int64, 0)
if err := sess.Cols("resource_specification.id").Table("resource_specification").
In("queue_id", deleteQueueIds).Find(&deleteSpcIds); err != nil {
return err
}
if len(deleteSpcIds) > 0 {
if _, err = sess.In("id", deleteSpcIds).Update(&ResourceSpecification{Status: SpecOffShelf}); err != nil {
return err
}
if _, err = sess.In("spec_id", deleteSpcIds).Delete(&ResourceSceneSpec{}); err != nil {
return err
}
}

}

//update exists specs
if len(updateList) > 0 {
for _, v := range updateList {
if _, err = sess.ID(v.ID).Update(&v); err != nil {
return err
}
}

}

//insert new specs
if len(insertList) > 0 {
if _, err = sess.Insert(insertList); err != nil {
return err
}
}

return sess.Commit()
}

func GetResourceAiCenters() ([]ResourceAiCenterRes, error) {
r := make([]ResourceAiCenterRes, 0)

err := x.SQL("SELECT t.ai_center_code, t.ai_center_name FROM (SELECT DISTINCT ai_center_code, ai_center_name,cluster FROM resource_queue WHERE (deleted_time IS NULL OR deleted_time=0)) t ORDER BY cluster desc,ai_center_code asc").Find(&r)
if err != nil {
return nil, err
}
return r, nil
}

+ 329
- 0
models/resource_scene.go View File

@@ -0,0 +1,329 @@
package models

import (
"code.gitea.io/gitea/modules/timeutil"
"errors"
"xorm.io/builder"
)

const (
Exclusive = iota + 1
NotExclusive
)

type ResourceScene struct {
ID int64 `xorm:"pk autoincr"`
SceneName string
JobType string
IsExclusive bool
ExclusiveOrg string
CreatedTime timeutil.TimeStamp `xorm:"created"`
CreatedBy int64
UpdatedTime timeutil.TimeStamp `xorm:"updated"`
UpdatedBy int64
DeleteTime timeutil.TimeStamp `xorm:"deleted"`
DeletedBy int64
}

type ResourceSceneSpec struct {
ID int64 `xorm:"pk autoincr"`
SceneId int64 `xorm:"unique(idx_scene_spec)"`
SpecId int64 `xorm:"unique(idx_scene_spec)"`
CreatedTime timeutil.TimeStamp `xorm:"created"`
}

type ResourceSceneReq struct {
ID int64
SceneName string
JobType string
IsExclusive bool
ExclusiveOrg string
CreatorId int64
SpecIds []int64
}

type SearchResourceSceneOptions struct {
ListOptions
JobType string
IsExclusive int
AiCenterCode string
QueueId int64
}

type ResourceSceneListRes struct {
TotalSize int64
List []ResourceSceneRes
}

func NewResourceSceneListRes(totalSize int64, list []ResourceSceneRes) *ResourceSceneListRes {
return &ResourceSceneListRes{
TotalSize: totalSize,
List: list,
}
}

type ResourceSceneRes struct {
ID int64
SceneName string
JobType JobType
IsExclusive bool
ExclusiveOrg string
Specs []ResourceSpecWithSceneId
}

func (ResourceSceneRes) TableName() string {
return "resource_scene"
}

type ResourceSceneBriefRes struct {
ID int64
SceneName string
}

func (ResourceSceneBriefRes) TableName() string {
return "resource_scene"
}

type ResourceSpecWithSceneId struct {
ID int64
SourceSpecId string
AccCardsNum int
CpuCores int
MemGiB float32
GPUMemGiB float32
ShareMemGiB float32
UnitPrice int
Status int
UpdatedTime timeutil.TimeStamp
SceneId int64
//queue
Cluster string
AiCenterCode string
AiCenterName string
QueueCode string
QueueId int64
ComputeResource string
AccCardType string
}

func (ResourceSpecWithSceneId) TableName() string {
return "resource_specification"
}

func InsertResourceScene(r ResourceSceneReq) error {
sess := x.NewSession()
defer sess.Close()

//check
specs := make([]ResourceSpecification, 0)
cond := builder.In("id", r.SpecIds).And(builder.Eq{"status": SpecOnShelf})
if err := sess.Where(cond).Find(&specs); err != nil {
return err
}
if len(specs) < len(r.SpecIds) {
return errors.New("specIds not correct")
}

rs := ResourceScene{
SceneName: r.SceneName,
JobType: r.JobType,
IsExclusive: r.IsExclusive,
ExclusiveOrg: r.ExclusiveOrg,
CreatedBy: r.CreatorId,
UpdatedBy: r.CreatorId,
}
_, err := sess.InsertOne(&rs)
if err != nil {
sess.Rollback()
return err
}

if len(r.SpecIds) == 0 {
return sess.Commit()
}
rss := make([]ResourceSceneSpec, len(r.SpecIds))
for i, v := range r.SpecIds {
rss[i] = ResourceSceneSpec{
SceneId: rs.ID,
SpecId: v,
}
}

_, err = sess.Insert(&rss)
if err != nil {
sess.Rollback()
return err
}

return sess.Commit()
}

func UpdateResourceScene(r ResourceSceneReq) error {
sess := x.NewSession()
var err error
defer func() {
if err != nil {
sess.Rollback()
}
sess.Close()
}()

// find old scene
old := ResourceScene{}
if has, _ := sess.ID(r.ID).Get(&old); !has {
return errors.New("ResourceScene not exist")
}
//check specification
specs := make([]ResourceSpecification, 0)
cond := builder.In("id", r.SpecIds).And(builder.Eq{"status": SpecOnShelf})
if err := sess.Where(cond).Find(&specs); err != nil {
return err
}
if len(specs) < len(r.SpecIds) {
return errors.New("specIds not correct")
}

//update scene
rs := ResourceScene{
SceneName: r.SceneName,
IsExclusive: r.IsExclusive,
ExclusiveOrg: r.ExclusiveOrg,
}
if _, err = sess.ID(r.ID).UseBool("is_exclusive").Update(&rs); err != nil {
return err
}

//delete scene spec relation
if _, err = sess.Where("scene_id = ? ", r.ID).Delete(&ResourceSceneSpec{}); err != nil {
sess.Rollback()
return err
}

if len(r.SpecIds) == 0 {
return sess.Commit()
}
//build new scene spec relation
rss := make([]ResourceSceneSpec, len(r.SpecIds))
for i, v := range r.SpecIds {
rss[i] = ResourceSceneSpec{
SceneId: r.ID,
SpecId: v,
}
}
if _, err = sess.Insert(&rss); err != nil {
sess.Rollback()
return err
}

return sess.Commit()
}

func DeleteResourceScene(sceneId int64) error {
sess := x.NewSession()
var err error
defer func() {
if err != nil {
sess.Rollback()
}
sess.Close()
}()

if _, err = sess.ID(sceneId).Delete(&ResourceScene{}); err != nil {
return err
}
if _, err = sess.Where("scene_id = ? ", sceneId).Delete(&ResourceSceneSpec{}); err != nil {
return err
}
return sess.Commit()
}

func SearchResourceScene(opts SearchResourceSceneOptions) (int64, []ResourceSceneRes, error) {
var cond = builder.NewCond()
if opts.Page <= 0 {
opts.Page = 1
}
if opts.JobType != "" {
cond = cond.And(builder.Eq{"resource_scene.job_type": opts.JobType})
}
if opts.IsExclusive == Exclusive {
cond = cond.And(builder.Eq{"resource_scene.is_exclusive": 1})
} else if opts.IsExclusive == NotExclusive {
cond = cond.And(builder.Eq{"resource_scene.is_exclusive": 0})
}
if opts.AiCenterCode != "" {
cond = cond.And(builder.Eq{"resource_queue.ai_center_code": opts.AiCenterCode})
}
if opts.QueueId > 0 {
cond = cond.And(builder.Eq{"resource_queue.id": opts.QueueId})
}
cond = cond.And(builder.NewCond().Or(builder.Eq{"resource_scene.delete_time": 0}).Or(builder.IsNull{"resource_scene.delete_time"}))
cols := []string{"resource_scene.id", "resource_scene.scene_name", "resource_scene.job_type", "resource_scene.is_exclusive",
"resource_scene.exclusive_org"}
count, err := x.Where(cond).
Distinct("resource_scene.id").
Join("INNER", "resource_scene_spec", "resource_scene_spec.scene_id = resource_scene.id").
Join("INNER", "resource_specification", "resource_specification.id = resource_scene_spec.spec_id").
Join("INNER", "resource_queue", "resource_queue.id = resource_specification.queue_id").
Count(&ResourceSceneRes{})
if err != nil {
return 0, nil, err
}

r := make([]ResourceSceneRes, 0)
if err = x.Where(cond).Distinct(cols...).
Join("INNER", "resource_scene_spec", "resource_scene_spec.scene_id = resource_scene.id").
Join("INNER", "resource_specification", "resource_specification.id = resource_scene_spec.spec_id").
Join("INNER", "resource_queue", "resource_queue.id = resource_specification.queue_id").
Desc("resource_scene.id").
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
Find(&r); err != nil {
return 0, nil, err
}

if len(r) == 0 {
return 0, r, err
}
//find related specs
sceneIds := make([]int64, 0, len(r))
for _, v := range r {
sceneIds = append(sceneIds, v.ID)
}

specs := make([]ResourceSpecWithSceneId, 0)

if err := x.Cols("resource_specification.id", "resource_specification.source_spec_id",
"resource_specification.acc_cards_num", "resource_specification.cpu_cores",
"resource_specification.mem_gi_b", "resource_specification.gpu_mem_gi_b",
"resource_specification.share_mem_gi_b", "resource_specification.unit_price",
"resource_specification.status", "resource_specification.updated_time",
"resource_scene_spec.scene_id", "resource_queue.cluster",
"resource_queue.ai_center_code", "resource_queue.acc_card_type",
"resource_queue.id as queue_id", "resource_queue.compute_resource",
"resource_queue.queue_code", "resource_queue.ai_center_name",
).In("resource_scene_spec.scene_id", sceneIds).
Join("INNER", "resource_scene_spec", "resource_scene_spec.spec_id = resource_specification.id").
Join("INNER", "resource_queue", "resource_queue.ID = resource_specification.queue_id").
OrderBy("resource_specification.acc_cards_num").
Find(&specs); err != nil {
return 0, nil, err
}

specsMap := make(map[int64][]ResourceSpecWithSceneId, 0)
for _, v := range specs {
if _, ok := specsMap[v.SceneId]; !ok {
specsMap[v.SceneId] = []ResourceSpecWithSceneId{v}
} else {
specsMap[v.SceneId] = append(specsMap[v.SceneId], v)
}
}

for i, v := range r {
s := specsMap[v.ID]
if s == nil {
s = make([]ResourceSpecWithSceneId, 0)
}
r[i].Specs = s
}

return count, r, nil
}

+ 285
- 0
models/resource_specification.go View File

@@ -0,0 +1,285 @@
package models

import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
)

const (
SpecNotVerified int = iota + 1
SpecOnShelf
SpecOffShelf
)

type ResourceSpecification struct {
ID int64 `xorm:"pk autoincr"`
QueueId int64 `xorm:"INDEX"`
SourceSpecId string `xorm:"INDEX"`
AccCardsNum int
CpuCores int
MemGiB float32
GPUMemGiB float32
ShareMemGiB float32
UnitPrice int
Status int
IsAutomaticSync bool
CreatedTime timeutil.TimeStamp `xorm:"created"`
CreatedBy int64
UpdatedTime timeutil.TimeStamp `xorm:"updated"`
UpdatedBy int64
}

func (r ResourceSpecification) ConvertToRes() *ResourceSpecificationRes {
return &ResourceSpecificationRes{
ID: r.ID,
SourceSpecId: r.SourceSpecId,
AccCardsNum: r.AccCardsNum,
CpuCores: r.CpuCores,
MemGiB: r.MemGiB,
ShareMemGiB: r.ShareMemGiB,
GPUMemGiB: r.GPUMemGiB,
UnitPrice: r.UnitPrice,
Status: r.Status,
UpdatedTime: r.UpdatedTime,
}
}

type ResourceSpecificationReq struct {
QueueId int64 `binding:"Required"`
SourceSpecId string
AccCardsNum int
CpuCores int
MemGiB float32
GPUMemGiB float32
ShareMemGiB float32
UnitPrice int
Status int
IsAutomaticSync bool
CreatorId int64
}

func (r ResourceSpecificationReq) ToDTO() ResourceSpecification {
return ResourceSpecification{
QueueId: r.QueueId,
SourceSpecId: r.SourceSpecId,
AccCardsNum: r.AccCardsNum,
CpuCores: r.CpuCores,
MemGiB: r.MemGiB,
GPUMemGiB: r.GPUMemGiB,
ShareMemGiB: r.ShareMemGiB,
UnitPrice: r.UnitPrice,
Status: r.Status,
IsAutomaticSync: r.IsAutomaticSync,
CreatedBy: r.CreatorId,
UpdatedBy: r.CreatorId,
}
}

type SearchResourceSpecificationOptions struct {
ListOptions
QueueId int64
Status int
Cluster string
}

type SearchResourceBriefSpecificationOptions struct {
QueueId int64
Cluster string
}

type ResourceSpecAndQueueListRes struct {
TotalSize int64
List []*ResourceSpecAndQueueRes
}

func NewResourceSpecAndQueueListRes(totalSize int64, list []ResourceSpecAndQueue) *ResourceSpecAndQueueListRes {
resList := make([]*ResourceSpecAndQueueRes, len(list))
for i, v := range list {
resList[i] = v.ConvertToRes()
}
return &ResourceSpecAndQueueListRes{
TotalSize: totalSize,
List: resList,
}
}

type ResourceSpecificationRes struct {
ID int64
SourceSpecId string
AccCardsNum int
CpuCores int
MemGiB float32
GPUMemGiB float32
ShareMemGiB float32
UnitPrice int
Status int
UpdatedTime timeutil.TimeStamp
}

func (ResourceSpecificationRes) TableName() string {
return "resource_specification"
}

type ResourceSpecAndQueueRes struct {
Spec *ResourceSpecificationRes
Queue *ResourceQueueRes
}

type ResourceSpecAndQueue struct {
ResourceSpecification `xorm:"extends"`
ResourceQueue `xorm:"extends"`
}

func (*ResourceSpecAndQueue) TableName() string {
return "resource_specification"
}

func (r ResourceSpecAndQueue) ConvertToRes() *ResourceSpecAndQueueRes {
return &ResourceSpecAndQueueRes{
Spec: r.ResourceSpecification.ConvertToRes(),
Queue: r.ResourceQueue.ConvertToRes(),
}
}

func InsertResourceSpecification(r ResourceSpecification) (int64, error) {
return x.Insert(&r)
}

func UpdateResourceSpecificationById(queueId int64, spec ResourceSpecification) (int64, error) {
return x.ID(queueId).Update(&spec)
}
func UpdateSpecUnitPriceById(id int64, unitPrice int) error {
_, err := x.Exec("update resource_specification set unit_price = ? ,updated_time = ? where id = ?", unitPrice, timeutil.TimeStampNow(), id)
return err
}

func SearchResourceSpecification(opts SearchResourceSpecificationOptions) (int64, []ResourceSpecAndQueue, error) {
var cond = builder.NewCond()
if opts.Page <= 0 {
opts.Page = 1
}
if opts.QueueId > 0 {
cond = cond.And(builder.Eq{"resource_specification.queue_id": opts.QueueId})
}
if opts.Status > 0 {
cond = cond.And(builder.Eq{"resource_specification.status": opts.Status})
}
if opts.Cluster != "" {
cond = cond.And(builder.Eq{"resource_queue.cluster": opts.Cluster})
}
//cond = cond.And(builder.Or(builder.Eq{"resource_queue.deleted_time": 0}).Or(builder.IsNull{"resource_queue.deleted_time"}))
n, err := x.Where(cond).Join("INNER", "resource_queue", "resource_queue.ID = resource_specification.queue_id").
Unscoped().Count(&ResourceSpecAndQueue{})
if err != nil {
return 0, nil, err
}

r := make([]ResourceSpecAndQueue, 0)
err = x.Where(cond).
Join("INNER", "resource_queue", "resource_queue.ID = resource_specification.queue_id").
Desc("resource_specification.id").
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
Unscoped().Find(&r)
if err != nil {
return 0, nil, err
}
return n, r, nil
}

func GetSpecScenes(specId int64) ([]ResourceSceneBriefRes, error) {
r := make([]ResourceSceneBriefRes, 0)
err := x.Where("resource_scene_spec.spec_id = ?", specId).
Join("INNER", "resource_scene_spec", "resource_scene_spec.scene_id = resource_scene.id").
Find(&r)
if err != nil {
return nil, err
}
return r, nil
}

func ResourceSpecOnShelf(id int64, unitPrice int) error {
_, err := x.Exec("update resource_specification set unit_price = ?,updated_time = ?,status = ? where id = ?", unitPrice, timeutil.TimeStampNow(), SpecOnShelf, id)
return err
}

func ResourceSpecOffShelf(id int64) (int64, error) {
sess := x.NewSession()
var err error
defer func() {
if err != nil {
sess.Rollback()
}
sess.Close()
}()
//delete scene spec relation
if _, err = sess.Where("spec_id = ?", id).Delete(&ResourceSceneSpec{}); err != nil {
return 0, err
}

param := ResourceSpecification{
Status: SpecOffShelf,
}
n, err := sess.Where("id = ? and status = ?", id, SpecOnShelf).Update(&param)
if err != nil {
return 0, err
}
sess.Commit()
return n, err
}

func GetResourceSpecification(r *ResourceSpecification) (*ResourceSpecification, error) {
has, err := x.Get(r)
if err != nil {
return nil, err
} else if !has {
return nil, nil
}
return r, nil
}

func SyncGrampusSpecs(updateList []ResourceSpecification, insertList []ResourceSpecification, existIds []int64) error {
sess := x.NewSession()
var err error
defer func() {
if err != nil {
sess.Rollback()
}
sess.Close()
}()
//delete specs and scene that no longer exists
deleteIds := make([]int64, 0)
cond := builder.NewCond()
cond = cond.And(builder.NotIn("resource_specification.id", existIds)).And(builder.Eq{"resource_queue.cluster": C2NetCluster})
if err := sess.Cols("resource_specification.id").Table("resource_specification").
Where(cond).Join("INNER", "resource_queue", "resource_queue.id = resource_specification.queue_id").
Find(&deleteIds); err != nil {
return err
}
if len(deleteIds) > 0 {
if _, err = sess.In("id", deleteIds).Update(&ResourceSpecification{Status: SpecOffShelf}); err != nil {
return err
}
if _, err = sess.In("spec_id", deleteIds).Delete(&ResourceSceneSpec{}); err != nil {
return err
}
}

//update exists specs
if len(updateList) > 0 {
for _, v := range updateList {
if _, err = sess.ID(v.ID).Update(&v); err != nil {
return err
}
}

}

//insert new specs
if len(insertList) > 0 {
if _, err = sess.Insert(insertList); err != nil {
return err
}
}

return sess.Commit()
}

+ 13
- 0
modules/cron/tasks_basic.go View File

@@ -5,6 +5,7 @@
package cron

import (
"code.gitea.io/gitea/services/cloudbrain/resource"
"code.gitea.io/gitea/modules/modelarts"
"context"
"time"
@@ -208,6 +209,17 @@ func registerSyncCloudbrainStatus() {
})
}

func registerSyncResourceSpecs() {
RegisterTaskFatal("sync_grampus_specs", &BaseConfig{
Enabled: true,
RunAtStart: true,
Schedule: "0 0 1 * * ?",
}, func(ctx context.Context, _ *models.User, _ Config) error {
resource.SyncGrampusQueueAndSpecs()
return nil
})
}

func registerSyncModelArtsTempJobs() {
RegisterTaskFatal("sync_model_arts_temp_jobs", &BaseConfig{
Enabled: true,
@@ -239,5 +251,6 @@ func initBasicTasks() {

registerSyncCloudbrainStatus()
registerHandleOrgStatistic()
registerSyncResourceSpecs()
registerSyncModelArtsTempJobs()
}

+ 33
- 0
modules/grampus/resty.go View File

@@ -23,6 +23,7 @@ const (
urlGetToken = urlOpenApiV1 + "token"
urlTrainJob = urlOpenApiV1 + "trainjob"
urlGetResourceSpecs = urlOpenApiV1 + "resourcespec"
urlGetAiCenter = urlOpenApiV1 + "sharescreen/aicenter"
urlGetImages = urlOpenApiV1 + "image"

errorIllegalToken = 1005
@@ -275,3 +276,35 @@ sendjob:

return &result, nil
}

func GetAiCenters(pageIndex, pageSize int) (*models.GetGrampusAiCentersResult, error) {
checkSetting()
client := getRestyClient()
var result models.GetGrampusAiCentersResult

retry := 0

sendjob:
_, err := client.R().
SetAuthToken(TOKEN).
SetResult(&result).
Get(HOST + urlGetAiCenter + "?pageIndex=" + fmt.Sprint(pageIndex) + "&pageSize=" + fmt.Sprint(pageSize))

if err != nil {
return nil, fmt.Errorf("resty GetAiCenters: %v", err)
}

if result.ErrorCode == errorIllegalToken && retry < 1 {
retry++
log.Info("retry get token")
_ = getToken()
goto sendjob
}

if result.ErrorCode != 0 {
log.Error("GetAiCenters failed(%d): %s", result.ErrorCode, result.ErrorMsg)
return &result, fmt.Errorf("GetAiCenters failed(%d): %s", result.ErrorCode, result.ErrorMsg)
}

return &result, nil
}

+ 7
- 0
options/locale/locale_en-US.ini View File

@@ -3024,6 +3024,13 @@ notices.desc = Description
notices.op = Op.
notices.delete_success = The system notices have been deleted.

user_management = User Management
resource_management = Resource Management
resource_pool = Resource Pool(queue)
resource_price = Resource Price
application_scenario = Application Scenario
system_configuration = System Configuration

[action]
create_repo = created repository <a href="%s">%s</a>
rename_repo = renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>


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

@@ -3040,6 +3040,13 @@ notices.desc=提示描述
notices.op=操作
notices.delete_success=系统通知已被删除。

user_management = 用户管理
resource_management = 资源管理
resource_pool = 资源池(队列)
resource_price = 资源规格单价
application_scenario = 应用场景
system_configuration = 系统配置

[action]
create_repo=创建了项目 <a href="%s">%s</a>
rename_repo=重命名项目 <code>%[1]s</code> 为 <a href="%[2]s">%[3]s</a>


+ 19351
- 17
package-lock.json
File diff suppressed because it is too large
View File


+ 1
- 0
package.json View File

@@ -54,6 +54,7 @@
"vue": "2.6.11",
"vue-bar-graph": "1.2.0",
"vue-calendar-heatmap": "0.8.4",
"vue-i18n": "6.1.3",
"vue-loader": "15.9.2",
"vue-router": "3.3.4",
"vue-template-compiler": "2.6.11",


+ 248
- 0
routers/admin/resources.go View File

@@ -0,0 +1,248 @@
package admin

import (
"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/routers/response"
"code.gitea.io/gitea/services/cloudbrain/resource"
"net/http"
)

const (
tplResourceQueue base.TplName = "admin/resources/queue"
tplResourceSpecification base.TplName = "admin/resources/specification"
tplResourceScene base.TplName = "admin/resources/scene"
)

func GetQueuePage(ctx *context.Context) {
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminResources"] = true
ctx.Data["PageIsAdminResourcesQueue"] = true
ctx.HTML(200, tplResourceQueue)
}

func GetSpecificationPage(ctx *context.Context) {
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminResources"] = true
ctx.Data["PageIsAdminResourcesSpecification"] = true
ctx.HTML(200, tplResourceSpecification)
}

func GetScenePage(ctx *context.Context) {
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminResources"] = true
ctx.Data["PageIsAdminResourcesScene"] = true
ctx.HTML(200, tplResourceScene)
}

func GetResourceQueueList(ctx *context.Context) {
page := ctx.QueryInt("page")
cluster := ctx.Query("cluster")
aiCenterCode := ctx.Query("center")
computeResource := ctx.Query("resource")
accCardType := ctx.Query("card")
list, err := resource.GetResourceQueueList(models.SearchResourceQueueOptions{
ListOptions: models.ListOptions{Page: page, PageSize: 10},
Cluster: cluster,
AiCenterCode: aiCenterCode,
ComputeResource: computeResource,
AccCardType: accCardType,
})
if err != nil {
log.Error("GetResourceQueueList error.%v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.SuccessWithData(list))
}

func GetResourceQueueCodes(ctx *context.Context) {
cluster := ctx.Query("cluster")
list, err := resource.GetResourceQueueCodes(models.GetQueueCodesOptions{Cluster: cluster})
if err != nil {
log.Error("GetResourceQueueCodes error.%v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.SuccessWithData(list))
}

func GetResourceAiCenters(ctx *context.Context) {
list, err := resource.GetResourceAiCenters()
if err != nil {
log.Error("GetResourceAiCenters error.%v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.SuccessWithData(list))
}

func AddResourceQueue(ctx *context.Context, req models.ResourceQueueReq) {
req.IsAutomaticSync = false
req.CreatorId = ctx.User.ID
err := resource.AddResourceQueue(req)
if err != nil {
log.Error("AddResourceQueue error. %v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.Success())
}

func UpdateResourceQueue(ctx *context.Context, req models.ResourceQueueReq) {
queueId := ctx.ParamsInt64(":id")
//only CardsTotalNum permitted to change
err := resource.UpdateResourceQueue(queueId, req)
if err != nil {
log.Error("UpdateResourceQueue error. %v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.Success())
}

func SyncGrampusQueue(ctx *context.Context) {
err := resource.SyncGrampusQueue(ctx.User.ID)
if err != nil {
log.Error("AddResourceQueue error. %v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.Success())
}

func GetResourceSpecificationList(ctx *context.Context) {
page := ctx.QueryInt("page")
queue := ctx.QueryInt64("queue")
status := ctx.QueryInt("status")
cluster := ctx.Query("cluster")
list, err := resource.GetResourceSpecificationList(models.SearchResourceSpecificationOptions{
ListOptions: models.ListOptions{Page: page, PageSize: 10},
QueueId: queue,
Status: status,
Cluster: cluster,
})
if err != nil {
log.Error("GetResourceSpecificationList error.%v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.SuccessWithData(list))
}

func GetResourceSpecificationScenes(ctx *context.Context) {
specId := ctx.ParamsInt64(":id")
list, err := resource.GetResourceSpecificationScenes(specId)
if err != nil {
log.Error("GetResourceSpecificationScenes error.%v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
r := make(map[string]interface{})
r["List"] = list
ctx.JSON(http.StatusOK, response.SuccessWithData(r))
}

func AddResourceSpecification(ctx *context.Context, req models.ResourceSpecificationReq) {
req.IsAutomaticSync = false
req.CreatorId = ctx.User.ID
err := resource.AddResourceSpecification(ctx.User.ID, req)
if err != nil {
log.Error("AddResourceQueue error. %v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.Success())
}

func UpdateResourceSpecification(ctx *context.Context, req models.ResourceSpecificationReq) {
id := ctx.ParamsInt64(":id")
action := ctx.Query("action")

var err *response.BizError
switch action {
case "edit":
if req.UnitPrice < 0 {
ctx.JSON(http.StatusOK, response.ServerError("param error"))
return
}
//only UnitPrice and permitted to change
err = resource.UpdateSpecUnitPrice(ctx.User.ID, id, req.UnitPrice)
case "on-shelf":
err = resource.ResourceSpecOnShelf(ctx.User.ID, id, req.UnitPrice)
case "off-shelf":
err = resource.ResourceSpecOffShelf(ctx.User.ID, id)
}

if err != nil {
log.Error("UpdateResourceSpecification error. %v", err)
ctx.JSON(http.StatusOK, response.ResponseError(err))
return
}
ctx.JSON(http.StatusOK, response.Success())
}

func SyncGrampusSpecs(ctx *context.Context) {
err := resource.SyncGrampusSpecs(ctx.User.ID)
if err != nil {
log.Error("AddResourceQueue error. %v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.Success())
}

func GetResourceSceneList(ctx *context.Context) {
page := ctx.QueryInt("page")
jobType := ctx.Query("jobType")
aiCenterCode := ctx.Query("center")
queueId := ctx.QueryInt64("queue")
isExclusive := ctx.QueryInt("IsExclusive")
list, err := resource.GetResourceSceneList(models.SearchResourceSceneOptions{
ListOptions: models.ListOptions{Page: page, PageSize: 10},
JobType: jobType,
IsExclusive: isExclusive,
AiCenterCode: aiCenterCode,
QueueId: queueId,
})
if err != nil {
log.Error("GetResourceSceneList error.%v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.SuccessWithData(list))
}

func AddResourceScene(ctx *context.Context, req models.ResourceSceneReq) {
req.CreatorId = ctx.User.ID
err := resource.AddResourceScene(req)
if err != nil {
log.Error("AddResourceScene error. %v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.Success())
}

func UpdateResourceScene(ctx *context.Context, req models.ResourceSceneReq) {
id := ctx.ParamsInt64(":id")
action := ctx.Query("action")

req.ID = id
var err error
switch action {
case "edit":
err = resource.UpdateResourceScene(req)
case "delete":
err = resource.DeleteResourceScene(id)
}

if err != nil {
log.Error("UpdateResourceScene error. %v", err)
ctx.JSON(http.StatusOK, response.ServerError(err.Error()))
return
}
ctx.JSON(http.StatusOK, response.Success())
}

+ 14
- 0
routers/response/error.go View File

@@ -0,0 +1,14 @@
package response

type BizError struct {
Code int
Err string
}

func (b BizError) Error() string {
return b.Err
}

func NewBizError(err error) *BizError {
return &BizError{Code: RESPONSE_CODE_ERROR_DEFAULT, Err: err.Error()}
}

+ 5
- 1
routers/response/response.go View File

@@ -24,8 +24,12 @@ func ServerError(msg string) *AiforgeResponse {
return &AiforgeResponse{Code: RESPONSE_CODE_ERROR_DEFAULT, Msg: msg}
}

func ResponseError(err *BizError) *AiforgeResponse {
return &AiforgeResponse{Code: err.Code, Msg: err.Err}
}

func SuccessWithData(data interface{}) *AiforgeResponse {
return &AiforgeResponse{Code: RESPONSE_CODE_ERROR_DEFAULT, Msg: RESPONSE_MSG_SUCCESS, Data: data}
return &AiforgeResponse{Code: RESPONSE_CODE_SUCCESS, Msg: RESPONSE_MSG_SUCCESS, Data: data}
}
func ErrorWithData(code int, msg string, data interface{}) *AiforgeResponse {
return &AiforgeResponse{Code: code, Msg: msg, Data: data}


+ 4
- 0
routers/response/response_list.go View File

@@ -0,0 +1,4 @@
package response

var RESOURCE_QUEUE_NOT_AVAILABLE = &BizError{Code: 1001, Err: "resource queue not available"}
var SPECIFICATION_NOT_EXIST = &BizError{Code: 1002, Err: "specification not exist"}

+ 26
- 0
routers/routes/routes.go View File

@@ -605,6 +605,32 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/delete", admin.DeleteNotices)
m.Post("/empty", admin.EmptyNotices)
})

m.Group("/resources", func() {
m.Group("/queue", func() {
m.Get("", admin.GetQueuePage)
m.Get("/list", admin.GetResourceQueueList)
m.Post("/grampus/sync", admin.SyncGrampusQueue)
m.Get("/codes", admin.GetResourceQueueCodes)
m.Get("/centers", admin.GetResourceAiCenters)
m.Post("/add", binding.Bind(models.ResourceQueueReq{}), admin.AddResourceQueue)
m.Post("/update/:id", binding.BindIgnErr(models.ResourceQueueReq{}), admin.UpdateResourceQueue)
})
m.Group("/specification", func() {
m.Get("", admin.GetSpecificationPage)
m.Get("/list", admin.GetResourceSpecificationList)
m.Get("/scenes/:id", admin.GetResourceSpecificationScenes)
m.Post("/grampus/sync", admin.SyncGrampusSpecs)
m.Post("/add", binding.Bind(models.ResourceSpecificationReq{}), admin.AddResourceSpecification)
m.Post("/update/:id", binding.BindIgnErr(models.ResourceSpecificationReq{}), admin.UpdateResourceSpecification)
})
m.Group("/scene", func() {
m.Get("", admin.GetScenePage)
m.Get("/list", admin.GetResourceSceneList)
m.Post("/add", binding.Bind(models.ResourceSceneReq{}), admin.AddResourceScene)
m.Post("/update/:id", binding.BindIgnErr(models.ResourceSceneReq{}), admin.UpdateResourceScene)
})
})
}, adminReq)
// ***** END: Admin *****



+ 14
- 0
services/admin/operate_log/operate_log.go View File

@@ -0,0 +1,14 @@
package operate_log

import (
"code.gitea.io/gitea/models"
)

func Log(log models.AdminOperateLog) error {
_, err := models.InsertAdminOperateLog(log)
return err
}

func NewLogValues() *models.LogValues {
return &models.LogValues{Params: make([]models.LogValue, 0)}
}

+ 122
- 0
services/cloudbrain/resource/resource_queue.go View File

@@ -0,0 +1,122 @@
package resource

import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/grampus"
"code.gitea.io/gitea/modules/log"
"fmt"
"strings"
)

func AddResourceQueue(req models.ResourceQueueReq) error {
if _, err := models.InsertResourceQueue(req.ToDTO()); err != nil {
return err
}
return nil
}

func UpdateResourceQueue(queueId int64, req models.ResourceQueueReq) error {
if _, err := models.UpdateResourceQueueById(queueId, models.ResourceQueue{
CardsTotalNum: req.CardsTotalNum,
Remark: req.Remark,
}); err != nil {
return err
}
return nil
}

func GetResourceQueueList(opts models.SearchResourceQueueOptions) (*models.ResourceQueueListRes, error) {
n, r, err := models.SearchResourceQueue(opts)
if err != nil {
return nil, err
}

return models.NewResourceQueueListRes(n, r), nil
}

func GetResourceQueueCodes(opts models.GetQueueCodesOptions) ([]*models.ResourceQueueCodesRes, error) {
r, err := models.GetResourceQueueCodes(opts)
if err != nil {
return nil, err
}

return r, nil
}

func GetResourceAiCenters() ([]models.ResourceAiCenterRes, error) {
r, err := models.GetResourceAiCenters()
if err != nil {
return nil, err
}

return r, nil
}

func SyncGrampusQueue(doerId int64) error {
r, err := grampus.GetAiCenters(1, 100)
if err != nil {
return err
}
log.Info("SyncGrampusQueue result = %+v", r)
queueUpdateList := make([]models.ResourceQueue, 0)
queueInsertList := make([]models.ResourceQueue, 0)
existIds := make([]int64, 0)

for _, center := range r.Infos {
for _, device := range center.AccDevices {
computeResource := models.ParseComputeResourceFormGrampus(device.Kind)
accCardType := strings.ToUpper(device.Model)
if computeResource == "" {
continue
}
//Determine if this quque already exists.if exist,update params
//if not exist,insert a new record
oldQueue, err := models.GetResourceQueue(&models.ResourceQueue{
Cluster: models.C2NetCluster,
AiCenterCode: center.Id,
ComputeResource: computeResource,
AccCardType: accCardType,
})
if err != nil {
return err
}

if oldQueue == nil {
queueInsertList = append(queueInsertList, models.ResourceQueue{
Cluster: models.C2NetCluster,
AiCenterCode: center.Id,
AiCenterName: center.Name,
ComputeResource: computeResource,
AccCardType: accCardType,
IsAutomaticSync: true,
CreatedBy: doerId,
UpdatedBy: doerId,
})
} else {
existIds = append(existIds, oldQueue.ID)
queueUpdateList = append(queueUpdateList, models.ResourceQueue{
ID: oldQueue.ID,
ComputeResource: computeResource,
AiCenterName: center.Name,
AccCardType: accCardType,
UpdatedBy: doerId,
})
}

}
}
return models.SyncGrampusQueues(queueUpdateList, queueInsertList, existIds)
}

func SyncGrampusQueueAndSpecs() {
defer func() {
if err := recover(); err != nil {
combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2))
log.Error("PANIC:", combinedErr)
}
}()
log.Info("start to sync grampus queue and specs")
SyncGrampusQueue(0)
SyncGrampusSpecs(0)
log.Info("sync grampus queue and specs finished")
}

+ 35
- 0
services/cloudbrain/resource/resource_scene.go View File

@@ -0,0 +1,35 @@
package resource

import (
"code.gitea.io/gitea/models"
)

func AddResourceScene(req models.ResourceSceneReq) error {
if err := models.InsertResourceScene(req); err != nil {
return err
}
return nil
}

func UpdateResourceScene(req models.ResourceSceneReq) error {
if err := models.UpdateResourceScene(req); err != nil {
return err
}
return nil
}

func DeleteResourceScene(id int64) error {
if err := models.DeleteResourceScene(id); err != nil {
return err
}
return nil
}

func GetResourceSceneList(opts models.SearchResourceSceneOptions) (*models.ResourceSceneListRes, error) {
n, r, err := models.SearchResourceScene(opts)
if err != nil {
return nil, err
}

return models.NewResourceSceneListRes(n, r), nil
}

+ 186
- 0
services/cloudbrain/resource/resource_specification.go View File

@@ -0,0 +1,186 @@
package resource

import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/grampus"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/routers/response"
"code.gitea.io/gitea/services/admin/operate_log"
"fmt"
"strings"
)

func AddResourceSpecification(doerId int64, req models.ResourceSpecificationReq) error {
if req.Status == 0 {
req.Status = models.SpecNotVerified
}
spec := req.ToDTO()
if _, err := models.InsertResourceSpecification(spec); err != nil {
return err
}
return nil
}

func UpdateSpecUnitPrice(doerId int64, specId int64, unitPrice int) *response.BizError {
oldSpec, err := models.GetResourceSpecification(&models.ResourceSpecification{ID: specId})
if err != nil {
return response.NewBizError(err)
}
if oldSpec == nil {
return response.SPECIFICATION_NOT_EXIST
}
err = models.UpdateSpecUnitPriceById(specId, unitPrice)
if err != nil {
return response.NewBizError(err)
}

if oldSpec.UnitPrice != unitPrice {
AddSpecOperateLog(doerId, "edit", operate_log.NewLogValues().Add("unitPrice", unitPrice), operate_log.NewLogValues().Add("unitPrice", oldSpec.UnitPrice), specId, fmt.Sprintf("修改资源规格单价从%d积分到%d积分", oldSpec.UnitPrice, unitPrice))
}
return nil
}

func SyncGrampusSpecs(doerId int64) error {
r, err := grampus.GetResourceSpecs("")
if err != nil {
return err
}
log.Info("SyncGrampusSpecs result = %+v", r)
specUpdateList := make([]models.ResourceSpecification, 0)
specInsertList := make([]models.ResourceSpecification, 0)
existIds := make([]int64, 0)
for _, spec := range r.Infos {
for _, c := range spec.Centers {
computeResource := models.ParseComputeResourceFormGrampus(spec.SpecInfo.AccDeviceKind)
if computeResource == "" {
continue
}
accCardType := strings.ToUpper(spec.SpecInfo.AccDeviceModel)
memGiB, err := models.ParseMemSizeFromGrampus(spec.SpecInfo.MemorySize)
gpuMemGiB, err := models.ParseMemSizeFromGrampus(spec.SpecInfo.AccDeviceMemory)
if err != nil {
log.Error("ParseMemSizeFromGrampus error. MemorySize=%s AccDeviceMemory=%s", spec.SpecInfo.MemorySize, spec.SpecInfo.AccDeviceMemory)
}
// get resource queue.if queue not exist,skip it
r, err := models.GetResourceQueue(&models.ResourceQueue{
Cluster: models.C2NetCluster,
AiCenterCode: c.ID,
ComputeResource: computeResource,
AccCardType: accCardType,
})
if err != nil || r == nil {
continue
}

//Determine if this specification already exists.if exist,update params
//if not exist,insert a new record and status is SpecNotVerified
oldSpec, err := models.GetResourceSpecification(&models.ResourceSpecification{
QueueId: r.ID,
SourceSpecId: spec.ID,
})
if err != nil {
return err
}

if oldSpec == nil {
specInsertList = append(specInsertList, models.ResourceSpecification{
QueueId: r.ID,
SourceSpecId: spec.ID,
AccCardsNum: spec.SpecInfo.AccDeviceNum,
CpuCores: spec.SpecInfo.CpuCoreNum,
MemGiB: memGiB,
GPUMemGiB: gpuMemGiB,
Status: models.SpecNotVerified,
IsAutomaticSync: true,
CreatedBy: doerId,
UpdatedBy: doerId,
})
} else {
existIds = append(existIds, oldSpec.ID)
specUpdateList = append(specUpdateList, models.ResourceSpecification{
ID: oldSpec.ID,
AccCardsNum: spec.SpecInfo.AccDeviceNum,
CpuCores: spec.SpecInfo.CpuCoreNum,
MemGiB: memGiB,
GPUMemGiB: gpuMemGiB,
UpdatedBy: doerId,
})
}

}
}
return models.SyncGrampusSpecs(specUpdateList, specInsertList, existIds)
}

//GetResourceSpecificationList returns specification and queue
func GetResourceSpecificationList(opts models.SearchResourceSpecificationOptions) (*models.ResourceSpecAndQueueListRes, error) {
n, r, err := models.SearchResourceSpecification(opts)
if err != nil {
return nil, err
}

return models.NewResourceSpecAndQueueListRes(n, r), nil
}

func GetResourceSpecificationScenes(specId int64) ([]models.ResourceSceneBriefRes, error) {
r, err := models.GetSpecScenes(specId)
if err != nil {
return nil, err
}

return r, nil
}

func ResourceSpecOnShelf(doerId int64, id int64, unitPrice int) *response.BizError {
spec, err := models.GetResourceSpecification(&models.ResourceSpecification{ID: id})
if err != nil {
return response.NewBizError(err)
}
if spec == nil {
return response.SPECIFICATION_NOT_EXIST
}
if q, err := models.GetResourceQueue(&models.ResourceQueue{ID: spec.QueueId}); err != nil || q == nil {
return response.RESOURCE_QUEUE_NOT_AVAILABLE
}

err = models.ResourceSpecOnShelf(id, unitPrice)
if err != nil {
return response.NewBizError(err)
}
if spec.UnitPrice != unitPrice {
AddSpecOperateLog(doerId, "on-shelf", operate_log.NewLogValues().Add("UnitPrice", unitPrice), operate_log.NewLogValues().Add("UnitPrice", spec.UnitPrice), id, fmt.Sprintf("定价上架资源规格,单价为%d", unitPrice))
} else {
AddSpecOperateLog(doerId, "on-shelf", nil, nil, id, "上架资源规格")
}
return nil

}
func ResourceSpecOffShelf(doerId int64, id int64) *response.BizError {
_, err := models.ResourceSpecOffShelf(id)
if err != nil {
return response.NewBizError(err)
}
AddSpecOperateLog(doerId, "off-shelf", nil, nil, id, "下架资源规格")
return nil

}

func AddSpecOperateLog(doerId int64, operateType string, newValue, oldValue *models.LogValues, specId int64, comment string) {
var newString = ""
var oldString = ""
if newValue != nil {
newString = newValue.JsonString()
}
if oldValue != nil {
oldString = oldValue.JsonString()
}
operate_log.Log(models.AdminOperateLog{
BizType: "SpecOperate",
OperateType: operateType,
OldValue: oldString,
NewValue: newString,
RelatedId: fmt.Sprint(specId),
CreatedBy: doerId,
Comment: comment,
})
}

+ 3
- 4
templates/admin/cloudbrain/images.tmpl View File

@@ -13,10 +13,9 @@
<div class="alert"></div>
<div class="admin user">
{{template "admin/navbar" .}}
<div id="images-admin">

</div>
<div class="ui container">
<div id="images-admin"></div>
</div>
</div>
<!-- 确认模态框 -->
<div>


+ 14
- 15
templates/admin/cloudbrain/list.tmpl View File

@@ -22,19 +22,19 @@
data-all-compute="{{.i18n.Tr "admin.cloudbrain.all_computing_resources"}}"
data-all-status="{{.i18n.Tr "admin.cloudbrain.all_status"}}"></div>
{{template "admin/navbar" .}}
<div class="ui container" style="width: 95%;">
<div class="ui container">
{{template "base/alert" .}}
<div class="ui grid">
<div class="row" style="border: 1px solid #d4d4d5;margin-top: 15px;padding-top: 0;">
<div class="ui grid" style="margin:0">
<div class="row" style="border: 1px solid #d4d4d5;margin-top: 0px;padding-top: 0;">
{{template "admin/cloudbrain/search" .}}
<div class="ui six wide column right aligned" style="margin: 1rem 0;">
<a class="ui compact blue basic icon button" style="box-shadow: none !important; padding: 0.8em;"
href="/admin/cloudbrains/download"><i
class="ri-download-line middle aligned icon"></i>{{.i18n.Tr "admin.cloudbrain.download_report"}}</a>
</div>
<div class="ui sixteen wide column">
<div class="ui sixteen wide column" style="overflow-x:auto;">
<!-- 任务展示 -->
<div class="dataset list">
<div class="dataset list" style="min-width:2100px;margin-top:15px;margin-bottom:15px;">
<!-- 表头 -->
<div class="ui grid stackable" style="background: #f0f0f0;;">
<div class="row">
@@ -412,17 +412,16 @@
</div>

{{end}}
{{end}}
<div id="app" style="margin-top: 2rem;">
<div class="center">
<el-pagination background @current-change="handleCurrentChange" :current-page="page"
:page-sizes="[10]" :page-size="10" layout="total, sizes, prev, pager, next, jumper"
:total="{{.Page.Paginater.Total}}">
</el-pagination>
</div>
</div>
{{end}}
</div>
</div>
<div id="app" style="margin-top: 2rem;width:100%;">
<div class="center">
<el-pagination background @current-change="handleCurrentChange" :current-page="page"
:page-sizes="[10]" :page-size="10" layout="total, sizes, prev, pager, next, jumper"
:total="{{.Page.Paginater.Total}}">
</el-pagination>
</div>

</div>
</div>
</div>


+ 63
- 43
templates/admin/navbar.tmpl View File

@@ -1,44 +1,64 @@
<div class="ui secondary pointing tabular top attached borderless menu stackable new-menu navbar">
<a class="{{if .PageIsAdminDashboard}}active{{end}} item" href="{{AppSubUrl}}/admin">
{{.i18n.Tr "admin.dashboard"}}
</a>
<a class="{{if .PageIsAdminUsers}}active{{end}} item" href="{{AppSubUrl}}/admin/users">
{{.i18n.Tr "admin.users"}}
</a>
<a class="{{if .PageIsAdminOrganizations}}active{{end}} item" href="{{AppSubUrl}}/admin/orgs">
{{.i18n.Tr "admin.organizations"}}
</a>
<a class="{{if .PageIsAdminRepositories}}active{{end}} item" href="{{AppSubUrl}}/admin/repos">
{{.i18n.Tr "admin.repositories"}}
</a>
<a class="{{if .PageIsAdminDatasets}}active{{end}} item" href="{{AppSubUrl}}/admin/datasets">
{{.i18n.Tr "admin.datasets"}}
</a>
<a class="{{if .PageIsAdminCloudBrains}}active{{end}} item" href="{{AppSubUrl}}/admin/cloudbrains">
{{.i18n.Tr "repo.cloudbrain.task"}}
</a>
<a class="{{if .PageIsAdminImages}}active{{end}} item" href="{{AppSubUrl}}/admin/images">
{{.i18n.Tr "explore.images"}}
</a>
<a class="{{if .PageIsAdminHooks}}active{{end}} item" href="{{AppSubUrl}}/admin/hooks">
{{.i18n.Tr "admin.hooks"}}
</a>
<a class="{{if .PageIsAdminSystemHooks}}active{{end}} item" href="{{AppSubUrl}}/admin/system-hooks">
{{.i18n.Tr "admin.systemhooks"}}
</a>
<a class="{{if .PageIsAdminAuthentications}}active{{end}} item" href="{{AppSubUrl}}/admin/auths">
{{.i18n.Tr "admin.authentication"}}
</a>
<a class="{{if .PageIsAdminEmails}}active{{end}} item" href="{{AppSubUrl}}/admin/emails">
{{.i18n.Tr "admin.emails"}}
</a>
<a class="{{if .PageIsAdminConfig}}active{{end}} item" href="{{AppSubUrl}}/admin/config">
{{.i18n.Tr "admin.config"}}
</a>
<a class="{{if .PageIsAdminNotices}}active{{end}} item" href="{{AppSubUrl}}/admin/notices">
{{.i18n.Tr "admin.notices"}}
</a>
<a class="{{if .PageIsAdminMonitor}}active{{end}} item" href="{{AppSubUrl}}/admin/monitor">
{{.i18n.Tr "admin.monitor"}}
</a>
<div class="ui secondary pointing tabular top attached borderless menu stackable new-menu navbar" style="margin-top:0">
<div class="item-container">
<a class="{{if .PageIsAdminDashboard}}active{{end}} item" href="{{AppSubUrl}}/admin">
{{.i18n.Tr "admin.dashboard"}}
</a>
<a class="item item-first" href="javascript:void(0);">
{{.i18n.Tr "admin.user_management"}}
</a>
<a class="{{if .PageIsAdminUsers}}active{{end}} item item-next" href="{{AppSubUrl}}/admin/users">
{{.i18n.Tr "admin.users"}}
</a>
<a class="{{if .PageIsAdminEmails}}active{{end}} item item-next" href="{{AppSubUrl}}/admin/emails">
{{.i18n.Tr "admin.emails"}}
</a>
<a class="{{if .PageIsAdminOrganizations}}active{{end}} item" href="{{AppSubUrl}}/admin/orgs">
{{.i18n.Tr "admin.organizations"}}
</a>
<a class="{{if .PageIsAdminRepositories}}active{{end}} item" href="{{AppSubUrl}}/admin/repos">
{{.i18n.Tr "admin.repositories"}}
</a>
<a class="{{if .PageIsAdminDatasets}}active{{end}} item" href="{{AppSubUrl}}/admin/datasets">
{{.i18n.Tr "admin.datasets"}}
</a>
<a class="{{if .PageIsAdminCloudBrains}}active{{end}} item" href="{{AppSubUrl}}/admin/cloudbrains">
{{.i18n.Tr "repo.cloudbrain.task"}}
</a>
<a class="{{if .PageIsAdminImages}}active{{end}} item" href="{{AppSubUrl}}/admin/images">
{{.i18n.Tr "explore.images"}}
</a>
<a class="item item-first" href="javascript:void(0);">
{{.i18n.Tr "admin.resource_management"}}
</a>
<a class="{{if .PageIsAdminResourcesQueue}}active{{end}} item item-next" href="{{AppSubUrl}}/admin/resources/queue">
{{.i18n.Tr "admin.resource_pool"}}
</a>
<a class="{{if .PageIsAdminResourcesSpecification}}active{{end}} item item-next" href="{{AppSubUrl}}/admin/resources/specification">
{{.i18n.Tr "admin.resource_price"}}
</a>
<a class="{{if .PageIsAdminResourcesScene}}active{{end}} item item-next" href="{{AppSubUrl}}/admin/resources/scene">
{{.i18n.Tr "admin.application_scenario"}}
</a>
<a class="item item-first" href="javascript:void(0);">
{{.i18n.Tr "admin.system_configuration"}}
</a>
<a class="{{if .PageIsAdminMonitor}}active{{end}} item item-next" href="{{AppSubUrl}}/admin/monitor">
{{.i18n.Tr "admin.monitor"}}
</a>
<a class="{{if .PageIsAdminHooks}}active{{end}} item item-next" href="{{AppSubUrl}}/admin/hooks">
{{.i18n.Tr "admin.hooks"}}
</a>
<a class="{{if .PageIsAdminSystemHooks}}active{{end}} item item-next" href="{{AppSubUrl}}/admin/system-hooks">
{{.i18n.Tr "admin.systemhooks"}}
</a>
<a class="{{if .PageIsAdminAuthentications}}active{{end}} item item-next" href="{{AppSubUrl}}/admin/auths">
{{.i18n.Tr "admin.authentication"}}
</a>
<a class="{{if .PageIsAdminConfig}}active{{end}} item item-next" href="{{AppSubUrl}}/admin/config">
{{.i18n.Tr "admin.config"}}
</a>
<a class="{{if .PageIsAdminNotices}}active{{end}} item item-next" href="{{AppSubUrl}}/admin/notices">
{{.i18n.Tr "admin.notices"}}
</a>
</div>
</div>

+ 10
- 0
templates/admin/resources/queue.tmpl View File

@@ -0,0 +1,10 @@
{{template "base/head" .}}
<link rel="stylesheet" href="{{StaticUrlPrefix}}/css/vp-resources-queue.css?v={{MD5 AppVer}}" />
<div class="admin resource">
{{template "admin/navbar" .}}
<div class="ui container">
<div id="__vue-root"></div>
</duv>
</div>
<script src="{{StaticUrlPrefix}}/js/vp-resources-queue.js?v={{MD5 AppVer}}"></script>
{{template "base/footer" .}}

+ 10
- 0
templates/admin/resources/scene.tmpl View File

@@ -0,0 +1,10 @@
{{template "base/head" .}}
<link rel="stylesheet" href="{{StaticUrlPrefix}}/css/vp-resources-scene.css?v={{MD5 AppVer}}" />
<div class="admin resource">
{{template "admin/navbar" .}}
<div class="ui container">
<div id="__vue-root"></div>
</duv>
</div>
<script src="{{StaticUrlPrefix}}/js/vp-resources-scene.js?v={{MD5 AppVer}}"></script>
{{template "base/footer" .}}

+ 10
- 0
templates/admin/resources/specification.tmpl View File

@@ -0,0 +1,10 @@
{{template "base/head" .}}
<link rel="stylesheet" href="{{StaticUrlPrefix}}/css/vp-resources-specification.css?v={{MD5 AppVer}}" />
<div class="admin resource">
{{template "admin/navbar" .}}
<div class="ui container">
<div id="__vue-root"></div>
</duv>
</div>
<script src="{{StaticUrlPrefix}}/js/vp-resources-specification.js?v={{MD5 AppVer}}"></script>
{{template "base/footer" .}}

+ 5
- 6
web_src/js/components/images/adminImages.vue View File

@@ -1,9 +1,8 @@
<template>
<div>
<div class="ui container" style="width: 80%;">
<div class="ui grid">
<div class="row" style="border: 1px solid #d4d4d5;margin-top: 15px;padding-top: 0;">

<div class="ui container" style="width: 100% !important;padding-right: 0;">
<div class="ui grid" style="margin: 0 !important">
<div class="row" style="border: 1px solid #d4d4d5;margin-top:0px;padding-top: 0;">
<div class="ui attached segment">
<div class="ui form ignore-dirty">
<div class="ui fluid action input">
@@ -31,8 +30,8 @@
<div class="ui six wide column right aligned" style="margin: 1rem 0;">
<a class="ui blue small button" href="/admin/images/commit_image">创建云脑镜像</a>
</div>
<div class="ui sixteen wide column" style="padding: 0;">
<el-table :data="tableDataCustom" style="width: 100%" :header-cell-style="tableHeaderStyle">
<div class="ui sixteen wide column" style="padding: 0;overflow-x: auto;">
<el-table :data="tableDataCustom" style="width: 100%;min-width:1700px;" :header-cell-style="tableHeaderStyle">
<el-table-column label="镜像Tag" min-width="19%" align="left" prop="tag">
<template slot-scope="scope">
<div style="display: flex;align-items: center;">


+ 55
- 1
web_src/less/_admin.less View File

@@ -1,5 +1,5 @@
.admin {
padding-top: 15px;
padding-top: 15px !important;

.table.segment {
padding: 0;
@@ -75,4 +75,58 @@
white-space: pre-wrap;
word-wrap: break-word;
}
display: flex;
.new-menu.navbar {
width: 230px !important;
display: flex;
flex-direction: column;
justify-content: flex-start !important;
border-bottom: none !important;
background-color: transparent !important;
.item-container {
display: flex;
flex-direction: column;
padding-top: 8px;
padding-bottom: 8px;
margin-left: 10px !important;
margin-right: 10px !important;
border: 1px solid #d4d4d5;
border-radius: 4px;
box-shadow: 0 1px 2px 0 rgb(34 36 38 / 15%);
background-color: #fafafa !important;
.item {
align-self: flex-start !important;
width: 100%;
padding-left: 20px;
&.active {
color: #40a9ff !important;
border-right: 4px solid #40a9ff;
border-radius: 0 !important;
border-bottom: none !important;
background-color: rgb(255, 255, 255);
}
&:hover {
background-color: #ffffff !important;
}
&:active {
border-color: transparent !important;
}
&.item-next {
padding-left: 45px;
}
&.item-first {
color: rgba(0,0,0,.87);
&:hover {
background-color: transparent !important;
}
}
}
}

}
>.ui.container {
flex: 1 !important;
padding-right: 10px;
}
}

+ 41
- 0
web_src/vuepages/apis/modules/point.js View File

@@ -0,0 +1,41 @@
import service from '../service';

// 算力积分概要
export const getPointAccount = () => {
return service({
url: '/reward/point/account',
method: 'get',
params: {},
});
}

// 算力积分获取、消耗明细
// operate-INCREASE 表示获取明细 DECREASE表示消耗明细, page-当前页, pageSize-每页条数
export const getPointList = (params) => {
return service({
url: '/reward/point/record/list',
method: 'get',
params,
});
}

// 管理员充值、扣减用户积分
// TargetUserId, OperateType-INCREASE,DECREASE, Amount, Remark, RewardType-POINT
export const setPointOperate = (data) => {
return service({
url: '/operation/reward/point/account/operate',
method: 'post',
data,
params: {}
});
}

// 算力积分页面
export const getPoint = () => {
return service({
url: '/reward/point',
method: 'get',
params: {},
data: {},
});
}

+ 174
- 0
web_src/vuepages/apis/modules/resources.js View File

@@ -0,0 +1,174 @@
import service from '../service';

// 查询智算列表
export const getAiCenterList = () => {
return service({
url: '/admin/resources/queue/centers',
method: 'get',
params: {},
data: {},
});
}

// 查询资源队列列表
// page 当前页数,从1开始
// cluster 所属集群 :OpenI 启智集群,C2Net 智算集群
// center 智算中心:OpenIOne 云脑一,OpenITwo 云脑二, chendu 成都人工智能计算中心, pclcci 鹏城云计算所 ,hefei 合肥类脑类脑智能开放平台, xuchang 中原人工智能计算中心
// resource 计算资源: GPU NPU
// card XPU类型: T4、A100、V100、Ascend 910
export const getResQueueList = (params) => {
return service({
url: '/admin/resources/queue/list',
method: 'get',
params,
});
}

// 新增资源队列
export const addResQueue = (data) => { // Cluster,QueueCode,AiCenterCode,ComputeResource,AccCardType,CardsTotalNum,Remark
return service({
url: '/admin/resources/queue/add',
method: 'post',
params: {},
data,
});
}

// 更新资源队列
export const updateResQueue = (data) => { // CardsTotalNum,Remark
return service({
url: `/admin/resources/queue/update/${data.ID}`,
method: 'post',
params: {},
data,
});
}

// 查询所有资源队列名称列表
export const getResQueueCode = (params) => { // cluster
return service({
url: '/admin/resources/queue/codes',
method: 'get',
params,
data: {},
});
}

// 同步智算网络资源池(队列)
export const syncResQueue = () => {
return service({
url: '/admin/resources/queue/grampus/sync',
method: 'post',
params: {},
data: {},
});
}

// 新增资源规格
export const addResSpecification = (data) => {
return service({
url: '/admin/resources/specification/add',
method: 'post',
params: {},
data,
});
}

// 查询资源规格所属场景 - 下架时提醒
export const getResSpecificationScenes = (data) => { // data => { ID: 1 }
return service({
url: `/admin/resources/specification/scenes/${data.ID}`,
method: 'get',
params: {},
data: {}
});
}

// 更新资源规格
// params: action edit-编辑 on-shelf 上架 off-shelf 下架
// data: UnitPrice
export const updateResSpecification = (data) => { // data => { ID: 1, action: 'edit|on-shelf|off-shelf', UnitPrice: 1 | undefined }
return service({
url: `/admin/resources/specification/update/${data.ID}`,
method: 'post',
params: { action: data.action },
data: { UnitPrice: data.action === 'edit' || data.action === 'on-shelf' ? data.UnitPrice : undefined }
});
}

// 查询资源规格列表
// page
// cluster 所属集群 :OpenI 启智集群,C2Net 智算集群
// queue 所属队列id
// status 状态 : 1 待审核 2已上架 3已下架
export const getResSpecificationList = (params) => {
return service({
url: '/admin/resources/specification/list',
method: 'get',
params,
data: {},
});
}

// 同步智算网络资源池(队列)
export const syncResSpecification = () => {
return service({
url: '/admin/resources/specification/grampus/sync',
method: 'post',
params: {},
data: {},
});
}

// 新增资源应用场景
/*
{
"SceneName":"启智集群调试任务", //应用场景名
"JobType":"TRAIN", //任务类型 DEBUG调试任务 BENCHMARK 评测任务 TRAIN 训练 INFERENCE 推理
"IsExclusive":true, //是否专属
"ExclusiveOrg":"123,456", //专属组织
"SpecIds":[2,3] // 资源规格id
}
*/
export const addResScene = (data) => {
return service({
url: '/admin/resources/scene/add',
method: 'post',
params: {},
data,
});
}

// 更新资源应用场景
// params: action:edit-编辑 delete-删除,
// data: {
// "SceneName":"启智集群调试任务", //应用场景名
// "IsExclusive":true, //是否专属
// "ExclusiveOrg":"123,456", //专属组织
// "SpecIds":[2,3] // 资源规格id
//}
export const updateResScene = (data) => {
return service({
url: `/admin/resources/scene/update/${data.ID}`,
method: 'post',
params: { action: data.action },
data: {
...data
},
});
}

// 查询资源应用场景
// page
// jobType
// center
// queue 所属队列
// IsExclusive 是否专属 1 专属 2 非专属
export const getResSceneList = (params) => {
return service({
url: '/admin/resources/scene/list',
method: 'get',
params,
data: {},
});
}

+ 26
- 0
web_src/vuepages/apis/service.js View File

@@ -0,0 +1,26 @@
import axios from 'axios';

const service = axios.create({
baseURL: '/',
timeout: 20000,
});

service.interceptors.request.use((config) => {
config.data && Object.assign(config.data, {
_csrf: window.config ? window.config.csrf : '',
});
config.params && Object.assign(config.params, {
_csrf: window.config ? window.config.csrf : '',
});
return config;
}, (error) => {
return Promise.reject(error);
});

service.interceptors.response.use((response) => {
return response;
}, (error) => {
return Promise.reject(error);
});

export default service;

+ 99
- 0
web_src/vuepages/components/BaseDialog.vue View File

@@ -0,0 +1,99 @@
<template>
<div class="base-dlg">
<el-dialog :visible.sync="dialogShow" :title="title" :width="width" :fullscreen="fullscreen" :top="top"
:modal="modal" :modal-append-to-body="modalAppendToBody" :append-to-body="appendToBody" :lock-scroll="lockScroll"
:custom-class="customClass" :close-on-click-modal="closeOnClickModal" :close-on-press-escape="closeOnPressEscape"
:show-close="showClose" :center="center" :destroy-on-close="destroyOnClose" :before-close="beforeClose"
@open="open" @opened="opened" @close="close" @closed="closed">
<template v-slot:title>
<slot name="title"></slot>
</template>
<template v-slot:default>
<slot name="default"></slot>
</template>
<template v-slot:footer>
<slot name="footer"></slot>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
name: "BaseDialog",
props: {
visible: { type: Boolean, default: false },
title: { type: String, default: "" },
width: { type: String, default: "" },
fullscreen: { type: Boolean, default: false },
top: { type: String },
modal: { type: Boolean, default: true },
modalAppendToBody: { type: Boolean, default: true },
appendToBody: { type: Boolean, default: false },
lockScroll: { type: Boolean, default: false },
customClass: { type: String, default: "" },
closeOnClickModal: { type: Boolean, default: false },
closeOnPressEscape: { type: Boolean, default: true },
showClose: { type: Boolean, default: true },
beforeClose: { type: Function },
center: { type: Boolean, default: false },
destroyOnClose: { type: Boolean, default: false },
},
data() {
return {
dialogShow: false,
};
},
watch: {
visible: function (val) {
this.dialogShow = val;
},
},
methods: {
open() {
this.$emit("open");
},
opened() {
this.$emit("opened");
},
close() {
this.$emit("close");
},
closed() {
this.$emit("closed");
this.$emit("update:visible", false);
},
},
};
</script>
<style scoped lang="less">
.base-dlg {
/deep/ .el-dialog__header {
text-align: left;
height: 45px;
background: rgb(240, 240, 240);
border-radius: 5px 5px 0px 0px;
border-bottom: 1px solid rgb(212, 212, 213);
padding: 0 15px;
display: flex;
align-items: center;
font-weight: 500;
font-size: 16px;
color: rgb(16, 16, 16);

.el-dialog__title {
font-weight: 500;
font-size: 16px;
color: rgb(16, 16, 16);
}

.el-dialog__headerbtn {
top: 15px;
right: 15px;
}
}

/deep/ .el-dialog__body {
padding: 15px 15px;
}
}
</style>

+ 16
- 0
web_src/vuepages/const/index.js View File

@@ -0,0 +1,16 @@
import { i18n } from '~/langs';

export const SOURCE_TYPE = [{ k: 'ACCOMPLISH_TASK', v: i18n.t('accomplishTask') }, { k: 'ADMIN_OPERATE', v: i18n.t('adminOperate') }, { k: 'RUN_CLOUDBRAIN_TASK', v: i18n.t('runCloudBrainTask') }];
export const CONSUME_STATUS = [{ k: 'OPERATING', v: i18n.t('operating') }, { k: 'SUCCEEDED', v: i18n.t('succeeded') }];
export const POINT_ACTIONS = [
{ k: 1, v: i18n.t('createPublicProject') }, { k: 6, v: i18n.t('dailyPutforwardTasks') }, { k: 7, v: i18n.t('dailyPR') }, { k: 10, v: i18n.t('comment') }, { k: 24, v: i18n.t('uploadDatasetFile') }, { k: 30, v: i18n.t('importNewModel') }, { k: 34, v: i18n.t('completeWechatCodeScanningVerification') },
{ k: 35, v: i18n.t('dailyRunCloudbrainTasks') }, { k: 36, v: i18n.t('datasetRecommendedByThePlatform') }, { k: 37, v: i18n.t('submitNewPublicImage') }, { k: 38, v: i18n.t('imageRecommendedByThePlatform') }, { k: 39, v: i18n.t('firstChangeofAvatar') }, { k: 40, v: i18n.t('dailyCommit') },
];
export const JOB_TYPE = [{ k: 'DEBUG', v: i18n.t('debugTask') }, { k: 'TRAIN', v: i18n.t('trainTask') }, { k: 'INFERENCE', v: i18n.t('inferenceTask') }, { k: 'BENCHMARK', v: i18n.t('benchmarkTask') }];

// 资源管理
export const CLUSTERS = [{ k: 'OpenI', v: i18n.t('resourcesManagement.OpenI') }, { k: 'C2Net', v: i18n.t('resourcesManagement.C2Net') }];
export const AI_CENTER = [{ k: 'OpenIOne', v: i18n.t('resourcesManagement.OpenIOne') }, { k: 'OpenITwo', v: i18n.t('resourcesManagement.OpenITwo') }, { k: 'chendu', v: i18n.t('resourcesManagement.chenduCenter') }, { k: 'pclcci', v: i18n.t('resourcesManagement.pclcci') }, { k: 'hefei', v: i18n.t('resourcesManagement.hefeiCenter') }, { k: 'xuchang', v: i18n.t('resourcesManagement.xuchangCenter') }];
export const COMPUTER_RESOURCES = [{ k: 'GPU', v: 'GPU' }, { k: 'NPU', v: 'NPU' }, { k: 'MLU', v: 'MLU' }];
export const ACC_CARD_TYPE = [{ k: 'T4', v: 'T4' }, { k: 'A100', v: 'A100' }, { k: 'V100', v: 'V100' }, { k: 'ASCEND910', v: 'Ascend 910' }, { k: 'MLU270', v: 'MLU270' }, { k: 'RTX3080', v: 'RTX3080' }];
export const SPECIFICATION_STATUS = [{ k: '1', v: i18n.t('resourcesManagement.willOnShelf') }, { k: '2', v: i18n.t('resourcesManagement.onShelf') }, { k: '3', v: i18n.t('resourcesManagement.offShelf') }];

+ 156
- 0
web_src/vuepages/langs/config/en-US.js View File

@@ -0,0 +1,156 @@
const en = {
loading: 'Loading...',
noData: 'No Data',
date: 'Date',

confirm: 'Confirm',
cancel: 'Cancel',
confirm1: 'Confirm',
pleaseCompleteTheInformationFirst: 'Please Complete the Information first!',
submittedSuccessfully: 'Submitted Successfully!',
submittedFailed: 'Submitted Failed!',
operation: 'Operation',
edit: 'Edit',
delete: 'Delete',
tips: 'Tips',

accomplishTask: 'Accomplish Task',
adminOperate: 'Administrator Operation',
runCloudBrainTask: 'Run CloudBrain Task',
operating: 'Operating',
succeeded: 'Succeeded',
debugTask: 'Debug Task',
trainTask: 'Train Task',
inferenceTask: 'Inference Task',
benchmarkTask: 'Benchmark Task',
createPublicProject: 'Create Public Projects',
dailyPutforwardTasks: 'Daily Put Forward Tasks',
dailyPR: 'Daily PR',
comment: 'Comment',
uploadDatasetFile: 'Upload Dataset Files',
importNewModel: 'Import New Models',
completeWechatCodeScanningVerification: 'Complete Wechat Code Scanning Verification',
dailyRunCloudbrainTasks: 'Daily Run Cloudbrain Tasks',
datasetRecommendedByThePlatform: 'Dataset Recommended by the Platform',
submitNewPublicImage: 'Submit New Public Images',
imageRecommendedByThePlatform: 'Image Recommended by the Platform',
firstChangeofAvatar: 'First Change of Avatar',
dailyCommit: 'Daily Commit',
calcPointDetails: 'Calculation Points Details',
calcPointAcquisitionInstructions: 'Calculation Points Acquisition Instructions',
CurrAvailableCalcPoints: 'Currently Available Calculation Points',
totalGainCalcPoints: 'Total Gain of Calculation Points',
totalConsumeCalcPoints: 'Total Consume of Calculation Points',
gainDetail: 'Gain Detail',
consumeDetail: 'Consume Detail',
serialNumber: 'Serial Number',
time: 'Time',
scene: 'Scene',
behaviorOfPoint: 'Behavior Of Point',
explanation: 'Explanation',
points: 'Points',
status: 'Status',
runTime: 'Run Time',
taskName: 'Task Name',

createdRepository: 'created repository ',
openedIssue: 'opened issue ',
createdPullRequest: 'created pull request ',
commentedOnIssue: 'commented on issue ',
uploadDataset: 'upload dataset ',
createdNewModel: 'created new model ',
firstBindingWechatRewards: 'first binding wechat rewards',
created: 'created ',
type: ' type ',
dataset: 'dataset ',
setAsRecommendedDataset: ' was set as recommended dataset',
committedImage: 'committed image ',
image: 'image ',
setAsRecommendedImage: ' was set as recommended image',
updatedAvatar: 'updated avatar',
pushedBranch: 'pushed to {branch} at ',
dailyMaxTips: `can't get full points when reach the daily upper limit`,
memory: 'Memory',
sharedMemory: 'Shared Memory',
';': ', ',

noPointGainRecord: 'No Point Earn Record Yet',
noPointConsumeRecord: 'No Point Consume Record Yet',

resourcesManagement: {
OpenI: 'OpenI',
C2Net: 'C2Net',
OpenIOne: 'OpenI One',
OpenITwo: 'OpenI Two',
chenduCenter: 'ChenDu AI Center',
pclcci: 'PCL Cloud Computer Institute',
hefeiCenter: 'HeFei AI Center',
xuchangCenter: 'XuChang AI Center',
willOnShelf: 'To Be On Shelf',
onShelf: 'On Shelf',
offShelf: 'Off Shelf',
toOnShelf: 'To On Shelf',
toOffShelf: 'To Off Shelf',
toSetPriceAndOnShelf: 'To Set Price and On Shelf',
status: 'Status',
allStatus: 'All Status',
syncAiNetwork: 'Sync AI Network',
resQueue: 'Resources Queue',
allResQueue: 'All Resources Queues',
addResQueue: 'Add Resources Queue',
addResQueueBtn: 'Add Resources Queue',
editResQueue: 'Edit Resources Queue',
resQueueName: 'Resources Queue Name',
whichCluster: 'Cluster',
allCluster: 'All Clusters',
aiCenter: 'AI Center',
aiCenterID: 'AI Center ID',
allAiCenter: 'All AI Centers',
computeResource: 'Compute Resource',
allComputeResource: 'All Compute Resources',
accCardType: 'Acc Card Type',
allAccCardType: 'All Acc Card Type',
cardsTotalNum: 'Cards Total Number',
accCardsNum: 'Acc Cards Number',
remark: 'Remark',
pleaseEnterRemark: 'Please Enter Remark(The maximum length shall not exceed 255)',
pleaseEnterPositiveIntegerCardsTotalNum: 'Please Enter Positive Integer Cards Total Number!',
addResSpecificationAndPriceInfo: 'Add Resources Specification and Price Info',
addResSpecificationBtn: 'Add Resources Specification',
editResSpecificationAndPriceInfo: 'Edit Resources Specification and Price Info',
resSpecificationAndPriceManagement: 'Resources Specification and Price Management',
sourceSpecCode: 'Source Specification Code',
sourceSpecCodeTips: 'OpenI Two Should Enter the Source Specification Code',
sourceSpecId: 'Source Specification ID',
cpuNum: 'CPU Number',
gpuMem: 'GPU Memory',
mem: 'Memory',
shareMem: 'Share Memory',
unitPrice: 'Unit Price',
point_hr: 'Point/hr',
onShelfConfirm: 'Are you sure to on shelf the resources specification?',
offShelfConfirm: 'Are you sure to off shelf the resources specification?',
onShelfCode1001: 'On shelf failed, the resources queues not available.',
offShelfDlgTip1: 'The resources specification has already used in scene:',
offShelfDlgTip2: 'Please confirm to off shelf?',
resSceneManagement: 'Resources Scene Management',
addResScene: 'Add Resources Scene',
addResSceneBtn: 'Add Resources Scene',
editResScene: 'Edit Resources Scene',
resSceneName: 'Resources Scene Name',
jobType: 'Job Type',
allJobType: 'All Job Type',
isExclusive: 'Is Exclusive?',
allExclusiveAndCommonUse: 'All Exclusive and Common Use',
exclusive: 'Exclusive',
commonUse: 'Common Use',
exclusiveOrg: 'Exclusive Organization',
exclusiveOrgTips: 'Multiple organization names are separated by semicolons',
computeCluster: 'Compute Cluster',
resourceSpecification: 'Resource Specification',
lastUpdateTime: 'Last Update Time',
resSceneDeleteConfirm: 'Are you sure to delete the current Resource Scene?',
},
}

export default en;

+ 156
- 0
web_src/vuepages/langs/config/zh-CN.js View File

@@ -0,0 +1,156 @@
const zh = {
loading: '加载中...',
noData: '暂无数据',
date: '日期',

confirm: '确定',
cancel: '取消',
confirm1: '确认',
pleaseCompleteTheInformationFirst: '请先完善信息!',
submittedSuccessfully: '提交成功!',
submittedFailed: '提交失败!',
operation: '操作',
edit: '修改',
delete: '删除',
tips: '提示',

accomplishTask: '积分任务',
adminOperate: '管理员操作',
runCloudBrainTask: '运行云脑任务',
operating: '消耗中',
succeeded: '已完成',
debugTask: '调试任务',
trainTask: '训练任务',
inferenceTask: '推理任务',
benchmarkTask: '评测任务',
createPublicProject: '创建公开项目',
dailyPutforwardTasks: '每日提出任务',
dailyPR: '每日提出PR',
comment: '发表评论',
uploadDatasetFile: '上传数据集文件',
importNewModel: '导入新模型',
completeWechatCodeScanningVerification: '完成微信扫码验证',
dailyRunCloudbrainTasks: '每日运行云脑任务',
datasetRecommendedByThePlatform: '数据集被平台推荐',
submitNewPublicImage: '提交新公开镜像',
imageRecommendedByThePlatform: '镜像被平台推荐',
firstChangeofAvatar: '首次更换头像',
dailyCommit: '每日commit',
calcPointDetails: '算力积分明细',
calcPointAcquisitionInstructions: '积分获取说明',
CurrAvailableCalcPoints: '当前可用算力积分(分)',
totalGainCalcPoints: '总获取算力积分(分)',
totalConsumeCalcPoints: '总消耗算力积分(分)',
gainDetail: '获取明细',
consumeDetail: '消耗明细',
serialNumber: '流水号',
time: '时间',
scene: '场景',
behaviorOfPoint: '积分行为',
explanation: '说明',
points: '积分',
status: '状态',
runTime: '运行时长',
taskName: '任务名称',

createdRepository: '创建了项目',
openedIssue: '创建了任务',
createdPullRequest: '创建了合并请求',
commentedOnIssue: '评论了任务',
uploadDataset: '上传了数据集文件',
createdNewModel: '导入了新模型',
firstBindingWechatRewards: '首次绑定微信奖励',
created: '创建了',
type: '类型',
dataset: '数据集',
setAsRecommendedDataset: '被设置为推荐数据集',
committedImage: '提交了镜像',
image: '镜像',
setAsRecommendedImage: '被设置为推荐镜像',
updatedAvatar: '更新了头像',
pushedBranch: '推送了{branch}分支代码到',
dailyMaxTips: '达到每日上限积分,不能拿满分',
memory: '内存',
sharedMemory: '共享内存',
';': ';',

noPointGainRecord: '还没有积分获取记录',
noPointConsumeRecord: '还没有积分消耗记录',

resourcesManagement: {
OpenI: '启智集群',
C2Net: '智算集群',
OpenIOne: '云脑一',
OpenITwo: '云脑二',
chenduCenter: '成都人工智能计算中心',
pclcci: '鹏城云计算所',
hefeiCenter: '合肥类脑类脑智能开放平台',
xuchangCenter: '中原人工智能计算中心',
willOnShelf: '待上架',
onShelf: '已上架',
offShelf: '已下架',
toOnShelf: '上架',
toOffShelf: '下架',
toSetPriceAndOnShelf: '定价上架',
status: '状态',
allStatus: '全部状态',
syncAiNetwork: '同步智算网络',
resQueue: '资源池(队列)',
allResQueue: '全部资源池(队列)',
addResQueue: '新建资源池(队列)',
addResQueueBtn: '新增资源池',
editResQueue: '修改资源池(队列)',
resQueueName: '资源池(队列)名称',
whichCluster: '所属集群',
allCluster: '全部集群',
aiCenter: '智算中心',
aiCenterID: '智算中心ID',
allAiCenter: '全部智算中心',
computeResource: '计算资源',
allComputeResource: '全部计算资源',
accCardType: '卡类型',
allAccCardType: '全部卡类型',
cardsTotalNum: '卡数',
accCardsNum: '卡数',
remark: '备注',
pleaseEnterRemark: '请输入备注(最大长度不超过255)',
pleaseEnterPositiveIntegerCardsTotalNum: '请输入正整数的卡数!',
addResSpecificationAndPriceInfo: '新增资源规格和单价信息',
addResSpecificationBtn: '新增资源规格',
editResSpecificationAndPriceInfo: '修改资源规格和单价信息',
resSpecificationAndPriceManagement: '资源规格单价管理',
sourceSpecCode: '对应资源编码',
sourceSpecCodeTips: '云脑II需要填写对应的资源编码',
sourceSpecId: '智算网络资源规格ID',
cpuNum: 'CPU数',
gpuMem: '显存',
mem: '内存',
shareMem: '共享内存',
unitPrice: '单价',
point_hr: '积分/时',
onShelfConfirm: '请确认上架该规格?',
offShelfConfirm: '请确认下架该规格?',
onShelfCode1001: '上架失败,资源池(队列)不可用。',
offShelfDlgTip1: '当前资源规格已在以下场景中使用:',
offShelfDlgTip2: '请确认进行下架操作?',
resSceneManagement: '算力资源应用场景管理',
addResScene: '新建算力资源应用场景',
addResSceneBtn: '新增应用场景',
editResScene: '修改算力资源应用场景',
resSceneName: '应用场景名称',
jobType: '任务类型',
allJobType: '全部任务类型',
isExclusive: '是否专属',
allExclusiveAndCommonUse: '全部专属和通用',
exclusive: '专属',
commonUse: '通用',
exclusiveOrg: '专属组织',
exclusiveOrgTips: '多个组织名之间用英文分号隔开',
computeCluster: '算力集群',
resourceSpecification: '资源规格',
lastUpdateTime: '最后更新时间',
resSceneDeleteConfirm: '是否确认删除当前应用场景?',
},
}

export default zh;

+ 16
- 0
web_src/vuepages/langs/index.js View File

@@ -0,0 +1,16 @@
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import jsCookie from 'js-cookie';
import zh from './config/zh-CN';
import en from './config/en-US';

Vue.use(VueI18n);

export const lang = jsCookie.get('lang') || 'zh-CN';
export const i18n = new VueI18n({
locale: lang,
messages: {
'zh-CN': zh,
'en-US': en
},
});

+ 259
- 0
web_src/vuepages/pages/resources/components/QueueDialog.vue View File

@@ -0,0 +1,259 @@
<template>
<div class="base-dlg">
<BaseDialog :visible.sync="dialogShow" :width="`750px`"
:title="type === 'add' ? $t('resourcesManagement.addResQueue') : $t('resourcesManagement.editResQueue')"
@open="open" @opened="opened" @close="close" @closed="closed">
<div class="dlg-content">
<div class="form">
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.resQueueName') }}</span>
</div>
<div class="content">
<el-input v-model="dataInfo.QueueCode" placeholder="" :disabled="type === 'edit'" maxlength="255"></el-input>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.whichCluster') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.Cluster" :disabled="type === 'edit'">
<el-option v-for="item in clusterList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.aiCenter') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.AiCenterCode" :disabled="type === 'edit'">
<el-option v-for="item in computingCenterList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.computeResource') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.ComputeResource" :disabled="type === 'edit'">
<el-option v-for="item in computingTypeList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.accCardType') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.AccCardType" :disabled="type === 'edit'">
<el-option v-for="item in cardTypeList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.cardsTotalNum') }}</span>
</div>
<div class="content">
<el-input v-model="dataInfo.CardsTotalNum" type="number" placeholder=""></el-input>
</div>
</div>
<div class="form-row" style="margin-top: 10px">
<div class="title"><span>{{ $t('resourcesManagement.remark') }}</span></div>
<div class="content" style="width: 400px">
<el-input type="textarea" :autosize="{ minRows: 3, maxRows: 4 }" maxlength="255"
:placeholder="$t('resourcesManagement.pleaseEnterRemark')" v-model="dataInfo.Remark">
</el-input>
</div>
</div>
<div class="form-row" style="margin-top: 20px">
<div class="title"></div>
<div class="content">
<el-button type="primary" class="btn confirm-btn" @click="confirm">{{ $t('confirm') }}</el-button>
<el-button class="btn" @click="cancel">{{ $t('cancel') }}</el-button>
</div>
</div>
</div>
</div>
</BaseDialog>
</div>
</template>
<script>
import BaseDialog from '~/components/BaseDialog.vue';
import { addResQueue, updateResQueue } from '~/apis/modules/resources';
import { CLUSTERS, AI_CENTER, COMPUTER_RESOURCES, ACC_CARD_TYPE } from '~/const';

export default {
name: "QueueDialog",
props: {
visible: { type: Boolean, default: false },
title: { type: String, default: '' },
type: { type: String, defalut: 'add' },
data: { type: Object, default: () => ({}) },
},
components: {
BaseDialog
},
data() {
return {
dialogShow: false,
clusterList: [CLUSTERS[0]],
computingCenterList: [AI_CENTER[0], AI_CENTER[1]],
computingTypeList: [...COMPUTER_RESOURCES],
cardTypeList: [...ACC_CARD_TYPE],

dataInfo: {},
};
},
watch: {
visible: function (val) {
this.dialogShow = val;
},
},
methods: {
resetDataInfo() {
this.dataInfo = {
ID: '',
QueueCode: '',
Cluster: '',
AiCenterCode: '',
ComputeResource: '',
AccCardType: '',
CardsTotalNum: '',
Remark: '',
}
},
open() {
this.resetDataInfo();
if (this.type === 'add') {
//
} else if (this.type === 'edit') {
this.dataInfo = Object.assign(this.dataInfo, { ...this.data });
}
this.$emit("open");
},
opened() {
this.$emit("opened");
},
close() {
this.$emit("close");
},
closed() {
this.$emit("closed");
this.$emit("update:visible", false);
},
confirm() {
if (!this.dataInfo.QueueCode || !this.dataInfo.Cluster || !this.dataInfo.AiCenterCode || !this.dataInfo.ComputeResource || !this.dataInfo.AccCardType || !this.dataInfo.CardsTotalNum) {
this.$message({
type: 'info',
message: this.$t('pleaseCompleteTheInformationFirst'),
});
return;
}
if (parseInt(this.dataInfo.CardsTotalNum) != Number(this.dataInfo.CardsTotalNum)) {
this.$message({
type: 'info',
message: this.$t('pleaseEnterPositiveIntegerCardsTotalNum')
});
return;
}
const setApi = this.type === 'add' ? addResQueue : updateResQueue;
setApi({ ...this.dataInfo, CardsTotalNum: Number(this.dataInfo.CardsTotalNum) }).then(res => {
res = res.data;
if (res.Code === 0) {
this.$message({
type: 'success',
message: this.$t('submittedSuccessfully')
});
this.$emit("confirm");
} else {
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
}
}).catch(err => {
console.log(err);
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
})
},
cancel() {
this.dialogShow = false;
this.$emit("update:visible", false);
}
},
mounted() {
this.resetDataInfo();
},
};
</script>
<style scoped lang="less">
.dlg-content {
margin: 20px 0 25px 0;
display: flex;
justify-content: center;

.form {
width: 600px;

.form-row {
display: flex;
min-height: 42px;
margin-bottom: 4px;

.title {
width: 160px;
display: flex;
justify-content: flex-end;
align-items: center;
margin-right: 20px;
color: rgb(136, 136, 136);
font-size: 14px;

&.required {
span {
position: relative;
}

span::after {
position: absolute;
right: -10px;
top: -2px;
vertical-align: top;
content: '*';
color: #db2828;
}
}
}

.content {
width: 300px;
display: flex;
align-items: center;

/deep/ .el-select {
width: 100%;
}
}
}
}

.btn {
color: rgb(2, 0, 4);
background-color: rgb(194, 199, 204);
border-color: rgb(194, 199, 204);

&.confirm-btn {
color: #fff;
background-color: rgb(56, 158, 13);
border-color: rgb(56, 158, 13);
}
}
}
</style>

+ 366
- 0
web_src/vuepages/pages/resources/components/SceneDialog.vue View File

@@ -0,0 +1,366 @@
<template>
<div class="base-dlg">
<BaseDialog :visible.sync="dialogShow" :width="`750px`"
:title="type === 'add' ? $t('resourcesManagement.addResScene') : $t('resourcesManagement.editResScene')"
@open="open" @opened="opened" @close="close" @closed="closed">
<div class="dlg-content">
<div class="form">
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.resSceneName') }}</span>
</div>
<div class="content">
<el-input v-model="dataInfo.SceneName" placeholder="" maxlength="255"></el-input>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.jobType') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.JobType" :disabled="type === 'edit'">
<el-option v-for="item in taskTypeList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.isExclusive') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.IsExclusive" @change="changeIsExclusive">
<el-option v-for="item in isExclusiveList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row" v-if="dataInfo.IsExclusive === '1'">
<div class="title required">
<span>{{ $t('resourcesManagement.exclusiveOrg') }}</span>
</div>
<div class="content">
<el-input v-model="dataInfo.ExclusiveOrg" :placeholder="$t('resourcesManagement.exclusiveOrgTips')"
maxlength="255">
</el-input>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.computeCluster') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.Cluster" @change="changeCluster" :disabled="type === 'edit'">
<el-option v-for="item in clusterList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row">
<div class="title">
<span>{{ $t('resourcesManagement.resQueue') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.QueueId" @change="changeQueue" :disabled="type === 'edit'">
<el-option v-for="item in queueList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.resourceSpecification') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.SpecIds" multiple collapse-tags class="specSel">
<el-option v-for="item in specsList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row" style="margin-top: 20px">
<div class="title"></div>
<div class="content">
<el-button type="primary" class="btn confirm-btn" @click="confirm">{{ $t('confirm') }}</el-button>
<el-button class="btn" @click="cancel">{{ $t('cancel') }}</el-button>
</div>
</div>
</div>
</div>
</BaseDialog>
</div>
</template>
<script>
import BaseDialog from '~/components/BaseDialog.vue';
import { getResQueueCode, getResSpecificationList, addResScene, updateResScene } from '~/apis/modules/resources';
import { JOB_TYPE, CLUSTERS, AI_CENTER, ACC_CARD_TYPE, SPECIFICATION_STATUS } from '~/const';
import { getListValueWithKey } from '~/utils';

export default {
name: "SceneDialog",
props: {
visible: { type: Boolean, default: false },
title: { type: String, default: '' },
type: { type: String, defalut: 'add' },
data: { type: Object, default: () => ({}) },
},
components: {
BaseDialog
},
data() {
return {
dialogShow: false,
dataInfo: {},
taskTypeList: [...JOB_TYPE],
clusterList: [...CLUSTERS],
accCardTypeList: [...ACC_CARD_TYPE],
statusList: [...SPECIFICATION_STATUS],
isExclusiveList: [{ k: '2', v: this.$t('resourcesManagement.commonUse') }, { k: '1', v: this.$t('resourcesManagement.exclusive') }],
queueList: [],
specsList: [],
};
},
watch: {
visible: function (val) {
this.dialogShow = val;
},
},
methods: {
resetDataInfo() {
this.dataInfo = {
SceneName: '',
JobType: '',
IsExclusive: '2',
ExclusiveOrg: '',
Cluster: '',
QueueId: '',
SpecIds: [],
}
this.queueList.splice(0, Infinity);
this.specsList.splice(0, Infinity);
},
getQueueList(next) {
return getResQueueCode({ cluster: this.dataInfo.Cluster }).then(res => {
res = res.data;
if (res.Code === 0) {
const data = res.Data;
const list = [];
for (let i = 0, iLen = data.length; i < iLen; i++) {
const item = data[i];
list.push({
k: item.ID,
v: `${item.QueueCode}(${getListValueWithKey(this.clusterList, item.Cluster)} - ${item.AiCenterName})`,
});
}
list.unshift({
k: '-1',
v: this.$t('resourcesManagement.allResQueue'),
});
this.queueList.splice(0, Infinity, ...list);
if (next) {
if (this.type === 'add') {
this.dataInfo.QueueId = '-1';
}
this.getResSpecificationList();
}
}
}).catch(err => {
console.log(err);
});
},
getResSpecificationList() {
const params = {
cluster: this.dataInfo.Cluster,
queue: this.dataInfo.QueueId === '-1' ? '' : this.dataInfo.QueueId,
status: 2,
page: 1,
};
return getResSpecificationList(params).then(res => {
res = res.data;
if (res.Code === 0) {
const list = res.Data.List;
const data = list.map((item) => {
const Queue = item.Queue;
const Spec = item.Spec;
// const NGPU = `${Queue.ComputeResource}:${Spec.AccCardsNum === 0 ? '0' : Spec.AccCardsNum + '*' + getListValueWithKey(this.accCardTypeList, Queue.AccCardType)}`;
const NGPU = `${Queue.ComputeResource}:${Spec.AccCardsNum + '*' + getListValueWithKey(this.accCardTypeList, Queue.AccCardType)}`;
return {
k: Spec.ID,
v: `${NGPU}, CPU:${Spec.CpuCores}, ${this.$t('resourcesManagement.gpuMem')}:${Spec.GPUMemGiB}GB, ${this.$t('resourcesManagement.mem')}:${Spec.MemGiB}GB, ${this.$t('resourcesManagement.shareMem')}:${Spec.ShareMemGiB}GB, ${this.$t('resourcesManagement.unitPrice')}:${Spec.UnitPrice}${this.$t('resourcesManagement.point_hr')}`,
}
});
this.specsList.splice(0, Infinity, ...data);
}
}).catch(err => {
console.log(err);
});
},
changeIsExclusive() {
this.dataInfo.ExclusiveOrg = '';
},
changeCluster() {
this.dataInfo.QueueId = '';
this.dataInfo.SpecIds = [];
this.queueList.splice(0, Infinity);
this.specsList.splice(0, Infinity);
this.getQueueList(true);
},
changeQueue() {
this.dataInfo.SpecIds = [];
this.specsList.splice(0, Infinity);
this.getResSpecificationList();
},
open() {
this.resetDataInfo();
if (this.type === 'add') {
//
} else if (this.type === 'edit') {
Object.assign(this.dataInfo, { ...this.data, QueueId: this.data.QueueIds.length === 1 ? this.data.QueueIds[0] : '-1' });
this.queueList.splice(0, Infinity);
this.specsList.splice(0, Infinity);
this.getQueueList(true);
}
this.$emit("open");
},
opened() {
this.$emit("opened");
},
close() {
this.$emit("close");
},
closed() {
this.$emit("closed");
this.$emit("update:visible", false);
},
confirm() {
if (!this.dataInfo.SceneName || !this.dataInfo.JobType || !this.dataInfo.SpecIds.length || (this.dataInfo.IsExclusive === '1' && !this.dataInfo.ExclusiveOrg)) {
this.$message({
type: 'info',
message: this.$t('pleaseCompleteTheInformationFirst')
});
return;
}
const setApi = this.type === 'add' ? addResScene : updateResScene;
setApi({
...this.dataInfo,
action: this.type === 'edit' ? 'edit' : undefined,
IsExclusive: this.dataInfo.IsExclusive === '1',
}).then(res => {
res = res.data;
if (res.Code === 0) {
this.$message({
type: 'success',
message: this.$t('submittedSuccessfully')
});
this.$emit("confirm");
} else {
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
}
}).catch(err => {
console.log(err);
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
})

},
cancel() {
this.dialogShow = false;
this.$emit("update:visible", false);
}
},
mounted() {
this.resetDataInfo();
},
};
</script>
<style scoped lang="less">
.dlg-content {
margin: 20px 0 25px 0;
display: flex;
justify-content: center;

.form {
width: 600px;

.form-row {
display: flex;
min-height: 42px;
margin-bottom: 4px;

.title {
width: 160px;
display: flex;
justify-content: flex-end;
align-items: center;
margin-right: 20px;
color: rgb(136, 136, 136);
font-size: 14px;

&.required {
span {
position: relative;
}

span::after {
position: absolute;
right: -10px;
top: -2px;
vertical-align: top;
content: '*';
color: #db2828;
}
}
}

.content {
width: 300px;
display: flex;
align-items: center;

/deep/ .el-select {
width: 100%;
}
}

.specSel {
/deep/ .el-tag.el-tag--info {
max-width: 81%;
display: flex;
align-items: center;

.el-select__tags-text {
overflow: hidden;
text-overflow: ellipsis;
}

.el-tag__close {
flex-shrink: 0;
right: -5px;
}
}
}
}
}

.btn {
color: rgb(2, 0, 4);
background-color: rgb(194, 199, 204);
border-color: rgb(194, 199, 204);

&.confirm-btn {
color: #fff;
background-color: rgb(56, 158, 13);
border-color: rgb(56, 158, 13);
}
}
}

.el-select-dropdown__item {
padding-left: 26px !important;
}

.el-select-dropdown__item.selected::after {
right: 0;
left: 6px;
}
</style>

+ 336
- 0
web_src/vuepages/pages/resources/components/SpecificationDialog.vue View File

@@ -0,0 +1,336 @@
<template>
<div class="base-dlg">
<BaseDialog :visible.sync="dialogShow" :width="`700px`"
:title="type === 'add' ? $t('resourcesManagement.addResSpecificationAndPriceInfo') : $t('resourcesManagement.editResSpecificationAndPriceInfo')"
@open="open" @opened="opened" @close="close" @closed="closed">
<div class="dlg-content">
<div class="form">
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.resQueue') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.QueueId" :disabled="type === 'edit'">
<el-option v-for="item in this.queueList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row">
<div class="title">
<span>{{ $t('resourcesManagement.sourceSpecCode') }}</span>
</div>
<div class="content">
<el-input v-model="dataInfo.SourceSpecId" :placeholder="$t('resourcesManagement.sourceSpecCodeTips')" maxlength="255"
:disabled="type === 'edit'">
</el-input>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.accCardsNum') }}</span>
</div>
<div class="content">
<el-input v-model="dataInfo.AccCardsNum" type="number" placeholder="" :disabled="type === 'edit'">
</el-input>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.cpuNum') }}</span>
</div>
<div class="content">
<el-input v-model="dataInfo.CpuCores" type="number" placeholder="" :disabled="type === 'edit'"></el-input>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.gpuMem') }}(GB)</span>
</div>
<div class="content">
<el-input v-model="dataInfo.GPUMemGiB" type="number" placeholder="" :disabled="type === 'edit'">
</el-input>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.mem') }}(GB)</span>
</div>
<div class="content">
<el-input v-model="dataInfo.MemGiB" type="number" placeholder="" :disabled="type === 'edit'"></el-input>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.shareMem') }}(GB)</span>
</div>
<div class="content">
<el-input v-model="dataInfo.ShareMemGiB" type="number" placeholder="" :disabled="type === 'edit'">
</el-input>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('resourcesManagement.unitPrice') }}({{ $t('resourcesManagement.point_hr') }})</span>
</div>
<div class="content">
<el-input v-model="dataInfo.UnitPrice" type="number" placeholder=""></el-input>
</div>
</div>
<div class="form-row">
<div class="title required">
<span>{{ $t('status') }}</span>
</div>
<div class="content">
<el-select v-model="dataInfo.Status" :disabled="type === 'edit'">
<el-option v-for="item in this.statusList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
</div>
<div class="form-row" style="margin-top: 20px">
<div class="title"></div>
<div class="content">
<el-button type="primary" class="btn confirm-btn" @click="confirm">{{ $t('confirm') }}</el-button>
<el-button class="btn" @click="cancel">{{ $t('cancel') }}</el-button>
</div>
</div>
</div>
</div>
</BaseDialog>
</div>
</template>
<script>
import BaseDialog from '~/components/BaseDialog.vue';
import { getResQueueCode, addResSpecification, updateResSpecification, getAiCenterList } from '~/apis/modules/resources';
import { SPECIFICATION_STATUS, CLUSTERS } from '~/const';
import { getListValueWithKey } from '~/utils';

export default {
name: "SpecificationDialog",
props: {
visible: { type: Boolean, default: false },
title: { type: String, default: '' },
type: { type: String, defalut: 'add' },
editOr: { type: Boolean, defalut: false },
data: { type: Object, default: () => ({}) },
},
components: {
BaseDialog
},
data() {
return {
dialogShow: false,
dataInfo: {},
queueList: [],
statusList: [...SPECIFICATION_STATUS],
clusterList: [...CLUSTERS],
aiCenterList: [],
};
},
watch: {
visible: function (val) {
this.dialogShow = val;
},
},
methods: {
resetDataInfo() {
this.dataInfo = {
QueueId: '',
AccCardsNum: '',
CpuCores: '',
MemGiB: '',
ShareMemGiB: '',
GPUMemGiB: '',
UnitPrice: '',
Status: '1',
}
},
getAiCenterList() {
getAiCenterList().then(res => {
res = res.data;
if (res.Code === 0) {
const list = res.Data;
const data = list.map(item => {
return {
k: item.AiCenterCode,
v: item.AiCenterName
};
});
this.aiCenterList.splice(0, Infinity, ...data);
}
}).catch(err => {
console.log(err);
});
},
getQueueList() {
getResQueueCode({ cluster: this.type === 'add' ? 'OpenI' : undefined }).then(res => {
res = res.data;
if (res.Code === 0) {
const data = res.Data;
const list = [];
for (let i = 0, iLen = data.length; i < iLen; i++) {
const item = data[i];
list.push({
k: item.ID,
v: `${item.QueueCode}(${getListValueWithKey(this.clusterList, item.Cluster)} - ${item.AiCenterName})`,
});
}
this.queueList.splice(0, Infinity, ...list);
}
}).catch(err => {
console.log(err);
});
},

open() {
this.resetDataInfo();
this.getQueueList();
this.getAiCenterList();
if (this.type === 'add') {
//
} else if (this.type === 'edit') {
this.dataInfo = Object.assign(this.dataInfo, { ...this.data, Status: '2' });
}
this.$emit("open");
},
opened() {
this.$emit("opened");
},
close() {
this.$emit("close");
},
closed() {
this.$emit("closed");
this.$emit("update:visible", false);
},
confirm() {
if (this.dataInfo.AccCardsNum === '' || this.dataInfo.CpuCores === '' || this.dataInfo.MemGiB === '' || this.dataInfo.ShareMemGiB === '' || this.dataInfo.GPUMemGiB === ''
|| this.dataInfo.UnitPrice === '' || !this.dataInfo.Status
) {
this.$message({
type: 'info',
message: this.$t('pleaseCompleteTheInformationFirst')
});
return;
}
if (parseInt(this.dataInfo.AccCardsNum) != Number(this.dataInfo.AccCardsNum)) {
this.$message({
type: 'info',
message: this.$t('pleaseEnterPositiveIntegerCardsTotalNum')
});
return;
}
const setApi = this.type === 'add' ? addResSpecification : updateResSpecification;
const action = this.editOr ? 'edit' : this.type === 'edit' ? 'on-shelf' : undefined;
setApi({
...this.dataInfo,
action: action,
AccCardsNum: Number(this.dataInfo.AccCardsNum),
CpuCores: Number(this.dataInfo.CpuCores),
MemGiB: Number(this.dataInfo.MemGiB),
ShareMemGiB: Number(this.dataInfo.ShareMemGiB),
GPUMemGiB: Number(this.dataInfo.GPUMemGiB),
UnitPrice: Number(this.dataInfo.UnitPrice),
Status: Number(this.dataInfo.Status),
}).then(res => {
res = res.data;
if (res.Code === 0) {
this.$message({
type: 'success',
message: this.$t('submittedSuccessfully')
});
this.$emit("confirm");
} else {
if (action === 'on-shelf' && res.Code === 1001) {
this.$message({
type: 'info',
message: this.$t('resourcesManagement.onShelfCode1001')
});
} else {
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
}
}
}).catch(err => {
console.log(err);
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
})
},
cancel() {
this.dialogShow = false;
this.$emit("update:visible", false);
}
},
mounted() {
this.resetDataInfo();
},
};
</script>
<style scoped lang="less">
.dlg-content {
margin: 20px 0 25px 0;
display: flex;
justify-content: center;

.form {
width: 600px;

.form-row {
display: flex;
min-height: 42px;
margin-bottom: 4px;

.title {
width: 160px;
display: flex;
justify-content: flex-end;
align-items: center;
margin-right: 20px;
color: rgb(136, 136, 136);
font-size: 14px;

&.required {
span {
position: relative;
}

span::after {
position: absolute;
right: -10px;
top: -2px;
vertical-align: top;
content: '*';
color: #db2828;
}
}
}

.content {
width: 300px;
display: flex;
align-items: center;

/deep/ .el-select {
width: 100%;
}
}
}
}

.btn {
color: rgb(2, 0, 4);
background-color: rgb(194, 199, 204);
border-color: rgb(194, 199, 204);

&.confirm-btn {
color: #fff;
background-color: rgb(56, 158, 13);
border-color: rgb(56, 158, 13);
}
}
}
</style>

+ 284
- 0
web_src/vuepages/pages/resources/queue/index.vue View File

@@ -0,0 +1,284 @@
<template>
<div>
<div class="title"><span>{{ $t('resourcesManagement.resQueue') }}</span></div>
<div class="tools-bar">
<div>
<el-select class="select" size="medium" v-model="selCluster" @change="selectChange">
<el-option v-for="item in clusterList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
<el-select class="select" size="medium" v-model="selComputingCenter" @change="selectChange">
<el-option v-for="item in computingCenterList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
<el-select class="select" size="medium" v-model="selComputingType" @change="selectChange">
<el-option v-for="item in computingTypeList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
<el-select class="select" size="medium" v-model="selCardType" @change="selectChange">
<el-option v-for="item in cardTypeList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
<div>
<el-button size="medium" icon="el-icon-refresh" @click="syncComputerNetwork" v-loading="syncLoading">
{{ $t('resourcesManagement.syncAiNetwork') }}</el-button>
<el-button type="primary" icon="el-icon-plus" size="medium" @click="showDialog('add')">
{{ $t('resourcesManagement.addResQueueBtn') }}</el-button>
</div>
</div>
<div class="table-container">
<div style="min-height:600px;">
<el-table border :data="tableData" style="width: 100%" v-loading="loading" stripe>
<el-table-column prop="ID" label="ID" align="center" header-align="center" width="80"></el-table-column>
<el-table-column prop="QueueCode" :label="$t('resourcesManagement.resQueueName')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="ClusterName" :label="$t('resourcesManagement.whichCluster')" align="center"
header-align="center">
<template slot-scope="scope">
<span :title="scope.row.Cluster">{{ scope.row.ClusterName }}</span>
</template>
</el-table-column>
<el-table-column prop="AiCenterCode" :label="$t('resourcesManagement.aiCenterID')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="AiCenterName" :label="$t('resourcesManagement.aiCenter')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="ComputeResourceName" :label="$t('resourcesManagement.computeResource')" align="center"
header-align="center">
</el-table-column>
<el-table-column prop="AccCardTypeName" :label="$t('resourcesManagement.accCardType')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="CardsTotalNum" :label="$t('resourcesManagement.cardsTotalNum')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="UpdatedTimeStr" :label="$t('resourcesManagement.lastUpdateTime')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="Remark" :label="$t('resourcesManagement.remark')" align="left" header-align="center"
min-width="160">
</el-table-column>
<el-table-column :label="$t('operation')" align="center" header-align="center" width="80">
<template slot-scope="scope">
<span v-if="scope.row.Cluster !== 'C2Net'" class="op-btn" @click="showDialog('edit', scope.row)">{{
$t('edit')
}}</span>
<span v-else class="op-btn" style="color:rgb(187, 187, 187);cursor:not-allowed">{{
$t('edit')
}}</span>
</template>
</el-table-column>
<template slot="empty">
<span style="font-size: 12px">{{
loading ? $t('loading') : $t('noData')
}}</span>
</template>
</el-table>
</div>
<div class="__r_p_pagination">
<div style="margin-top: 2rem">
<div class="center">
<el-pagination background @current-change="currentChange" :current-page="pageInfo.curpage"
:page-sizes="pageInfo.pageSizes" :page-size="pageInfo.pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="pageInfo.total">
</el-pagination>
</div>
</div>
</div>
</div>
<QueueDialog :visible.sync="queueDialogShow" :type="queueDialogType" :data="queueDialogData"
@confirm="queueDialogConfirm"></QueueDialog>
</div>
</template>

<script>
import QueueDialog from '../components/QueueDialog.vue';
import { getAiCenterList, getResQueueList, addResQueue, updateResQueue, syncResQueue } from '~/apis/modules/resources';
import { CLUSTERS, COMPUTER_RESOURCES, ACC_CARD_TYPE } from '~/const';
import { getListValueWithKey } from '~/utils';
import { formatDate } from 'element-ui/lib/utils/date-util';

export default {
data() {
return {
selCluster: '',
clusterList: [{ k: '', v: this.$t('resourcesManagement.allCluster') }, ...CLUSTERS],
selComputingCenter: '',
computingCenterList: [{ k: '', v: this.$t('resourcesManagement.allAiCenter') }],
selComputingType: '',
computingTypeList: [{ k: '', v: this.$t('resourcesManagement.allComputeResource') }, ...COMPUTER_RESOURCES],
selCardType: '',
cardTypeList: [{ k: '', v: this.$t('resourcesManagement.allAccCardType') }, ...ACC_CARD_TYPE],
syncLoading: false,
loading: false,
tableData: [],
pageInfo: {
curpage: 1,
pageSize: 10,
pageSizes: [10],
total: 0,
},
queueDialogShow: false,
queueDialogType: 'add',
queueDialogData: {},
};
},
components: { QueueDialog },
methods: {
getAiCenterList() {
getAiCenterList().then(res => {
res = res.data;
if (res.Code === 0) {
const list = res.Data;
const data = list.map(item => {
return {
k: item.AiCenterCode,
v: item.AiCenterName
};
});
this.computingCenterList.splice(1, Infinity, ...data);
}
}).catch(err => {
console.log(err);
});
},
getTableData() {
const params = {
cluster: this.selCluster,
center: this.selComputingCenter,
resource: this.selComputingType,
card: this.selCardType,
page: this.pageInfo.curpage,
pagesize: this.pageInfo.pageSize,
};
this.loading = true;
getResQueueList(params).then(res => {
this.loading = false;
res = res.data;
if (res.Code === 0) {
const list = res.Data.List;
const data = list.map((item) => {
return {
...item,
QueueCode: item.QueueCode || '--',
ClusterName: getListValueWithKey(this.clusterList, item.Cluster),
ComputeResourceName: getListValueWithKey(this.computingTypeList, item.ComputeResource),
AccCardTypeName: getListValueWithKey(this.cardTypeList, item.AccCardType),
UpdatedTimeStr: formatDate(new Date(item.UpdatedTime * 1000), 'yyyy-MM-dd HH:mm:ss'),
}
});
this.tableData = data;
this.pageInfo.total = res.Data.TotalSize;
}
}).catch(err => {
console.log(err);
this.loading = false;
});
},
syncComputerNetwork() {
this.syncLoading = true;
syncResQueue().then(res => {
this.syncLoading = false;
res = res.data;
if (res.Code === 0) {
this.$message({
type: 'success',
message: this.$t('submittedSuccessfully')
});
this.getAiCenterList();
this.getTableData();
} else {
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
}
}).catch(err => {
console.log(err);
this.syncLoading = false;
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
});
},
selectChange() {
this.pageInfo.curpage = 1;
this.getTableData();
},
currentChange(val) {
this.pageInfo.curpage = val;
this.getTableData();
},
showDialog(type, data) {
this.queueDialogType = type;
this.queueDialogData = data ? { ...data } : {};
this.queueDialogShow = true;
},
queueDialogConfirm() {
this.queueDialogShow = false;
this.getAiCenterList();
this.getTableData();
}
},
mounted() {
this.getAiCenterList();
this.getTableData();
},
beforeDestroy() {
},
};
</script>

<style scoped lang="less">
.title {
height: 30px;
display: flex;
align-items: center;
margin-bottom: 5px;

span {
font-weight: 700;
font-size: 16px;
color: rgb(16, 16, 16);
}
}

.tools-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;

.select {
margin-right: 10px;

/deep/ .el-input__inner {
border-radius: 0;
}
}
}

.table-container {
margin-bottom: 16px;

/deep/ .el-table__header {
th {
background: rgb(245, 245, 246);
font-size: 12px;
color: rgb(36, 36, 36);
}
}

/deep/ .el-table__body {
td {
font-size: 12px;
}
}

.op-btn {
cursor: pointer;
font-size: 12px;
color: rgb(25, 103, 252);
margin: 0 5px;
}
}

.center {
display: flex;
justify-content: center;
}
</style>

+ 17
- 0
web_src/vuepages/pages/resources/queue/vp-resources-queue.js View File

@@ -0,0 +1,17 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import localeEn from 'element-ui/lib/locale/lang/en';
import localeZh from 'element-ui/lib/locale/lang/zh-CN';
import { i18n, lang } from '~/langs';
import App from './index.vue';

Vue.use(ElementUI, {
locale: lang === 'zh-CN' ? localeZh : localeEn,
size: 'small',
});

new Vue({
i18n,
render: (h) => h(App),
}).$mount('#__vue-root');

+ 361
- 0
web_src/vuepages/pages/resources/scene/index.vue View File

@@ -0,0 +1,361 @@
<template>
<div>
<div class="title"><span>{{ $t('resourcesManagement.resSceneManagement') }}</span></div>
<div class="tools-bar">
<div>
<el-select class="select" size="medium" v-model="selTaskType" @change="selectChange">
<el-option v-for="item in taskTypeList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
<el-select class="select" size="medium" v-model="selIsExclusive" @change="selectChange">
<el-option v-for="item in isExclusiveList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
<el-select class="select" size="medium" v-model="selAiCenter" @change="selectChange">
<el-option v-for="item in aiCenterList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
<el-select class="select" size="medium" v-model="selQueue" @change="selectChange">
<el-option v-for="item in queueList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
<div>
<el-button type="primary" icon="el-icon-plus" size="medium" @click="showDialog('add')">
{{ $t('resourcesManagement.addResSceneBtn') }}</el-button>
</div>
</div>
<div class="table-container">
<div style="min-height:600px;">
<el-table border :data="tableData" style="width: 100%" v-loading="loading" stripe>
<el-table-column prop="ID" label="ID" align="center" header-align="center" width="60"></el-table-column>
<el-table-column prop="SceneName" :label="$t('resourcesManagement.resSceneName')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="JobTypeStr" :label="$t('resourcesManagement.jobType')" align="center"
header-align="center" width="120">
</el-table-column>
<el-table-column prop="IsExclusiveStr" :label="$t('resourcesManagement.isExclusive')" align="center"
header-align="center" width="120">
<template slot-scope="scope">
<span :style="{ color: scope.row.IsExclusive ? 'red' : '' }">{{ scope.row.IsExclusiveStr }}</span>
</template>
</el-table-column>
<el-table-column prop="ExclusiveOrg" :label="$t('resourcesManagement.exclusiveOrg')" align="center"
header-align="center">
<template slot-scope="scope">
<span>{{ scope.row.IsExclusive ? scope.row.ExclusiveOrg : '--' }}</span>
</template>
</el-table-column>
<el-table-column prop="AiCenterStr" :label="$t('resourcesManagement.aiCenter')" align="center"
header-align="center">
<template slot-scope="scope">
<div v-if="!scope.row.Queues.length">--</div>
<div v-for="item in scope.row.Queues" :key="item.QueueId">
<span>{{ item.AiCenterName }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="QueueStr" :label="$t('resourcesManagement.resQueue')" align="center"
header-align="center">
<template slot-scope="scope">
<div v-if="!scope.row.Queues.length">--</div>
<div v-for="item in scope.row.Queues" :key="item.QueueId">
<span>{{ item.QueueStr }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="SpecsList" :label="$t('resourcesManagement.resourceSpecification')" align="left"
header-align="center" min-width="180">
<template slot-scope="scope">
<div v-for="item in scope.row.SpecsList" :key="item.k">
<span>{{ item.v }}</span>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('operation')" align="center" header-align="center" width="100">
<template slot-scope="scope">
<span class="op-btn" @click="showDialog('edit', scope.row)">{{ $t('edit') }}</span>
<span class="op-btn" style="color:red" @click="deleteRow(scope.row)">{{ $t('delete') }}</span>
</template>
</el-table-column>
<template slot="empty">
<span style="font-size: 12px">{{
loading ? $t('loading') : $t('noData')
}}</span>
</template>
</el-table>
</div>
<div class="__r_p_pagination">
<div style="margin-top: 2rem">
<div class="center">
<el-pagination background @current-change="currentChange" :current-page="pageInfo.curpage"
:page-sizes="pageInfo.pageSizes" :page-size="pageInfo.pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="pageInfo.total">
</el-pagination>
</div>
</div>
</div>
</div>
<SceneDialog :visible.sync="sceneDialogShow" :type="sceneDialogType" :data="sceneDialogData"
@confirm="sceneDialogConfirm"></SceneDialog>
</div>
</template>

<script>
import SceneDialog from '../components/SceneDialog.vue';
import { getQueueList, getResQueueCode, getResSceneList, updateResScene, getAiCenterList } from '~/apis/modules/resources';
import { JOB_TYPE, CLUSTERS, ACC_CARD_TYPE } from '~/const';
import { getListValueWithKey } from '~/utils';
import { formatDate } from 'element-ui/lib/utils/date-util';

export default {
data() {
return {
selTaskType: '',
taskTypeList: [{ k: '', v: this.$t('resourcesManagement.allJobType') }, ...JOB_TYPE],
selIsExclusive: '',
isExclusiveList: [{ k: '', v: this.$t('resourcesManagement.allExclusiveAndCommonUse') }, { k: '1', v: this.$t('resourcesManagement.exclusive') }, { k: '2', v: this.$t('resourcesManagement.commonUse') }],
selQueue: '',
queueList: [{ k: '', v: this.$t('resourcesManagement.allResQueue') }],
clusterList: [...CLUSTERS],
selAiCenter: '',
aiCenterList: [{ k: '', v: this.$t('resourcesManagement.allAiCenter') }],
accCardTypeList: [...ACC_CARD_TYPE],
loading: false,
tableData: [],
pageInfo: {
curpage: 1,
pageSize: 10,
pageSizes: [10],
total: 0,
},
sceneDialogShow: false,
sceneDialogType: 'add',
sceneDialogData: {},
};
},
components: { SceneDialog },
methods: {
getAiCenterList() {
getAiCenterList().then(res => {
res = res.data;
if (res.Code === 0) {
const list = res.Data;
const data = list.map(item => {
return {
k: item.AiCenterCode,
v: item.AiCenterName
};
});
this.aiCenterList.splice(1, Infinity, ...data);
}
}).catch(err => {
console.log(err);
});
},
getQueueList() {
getResQueueCode().then(res => {
res = res.data;
if (res.Code === 0) {
const data = res.Data;
const list = [];
for (let i = 0, iLen = data.length; i < iLen; i++) {
const item = data[i];
list.push({
k: item.ID,
v: `${item.QueueCode}(${getListValueWithKey(this.clusterList, item.Cluster)} - ${item.AiCenterName})`,
});
}
this.queueList.push(...list);
}
}).catch(err => {
console.log(err);
});
},
getTableData() {
const params = {
jobType: this.selTaskType,
IsExclusive: this.selIsExclusive,
queue: this.selQueue,
center: this.selAiCenter,
page: this.pageInfo.curpage,
pagesize: this.pageInfo.pageSize,
};
this.loading = true;
getResSceneList(params).then(res => {
this.loading = false;
res = res.data;
if (res.Code === 0) {
const list = res.Data.List;
const data = list.map((item) => {
const Specs = item.Specs;
const specsList = [];
const queues = [];
const queueIds = [];
let cluster = '';
for (let i = 0, iLen = Specs.length; i < iLen; i++) {
const Spec = Specs[i];
// const NGPU = `${Spec.ComputeResource}:${Spec.AccCardsNum === 0 ? '0' : Spec.AccCardsNum + '*' + getListValueWithKey(this.accCardTypeList, Spec.AccCardType)}`;
const NGPU = `${Spec.ComputeResource}:${Spec.AccCardsNum + '*' + getListValueWithKey(this.accCardTypeList, Spec.AccCardType)}`;
specsList.push({
k: Spec.ID,
v: `${NGPU}, CPU:${Spec.CpuCores}, ${this.$t('resourcesManagement.gpuMem')}:${Spec.GPUMemGiB}GB, ${this.$t('resourcesManagement.mem')}:${Spec.MemGiB}GB, ${this.$t('resourcesManagement.shareMem')}:${Spec.ShareMemGiB}GB, ${this.$t('resourcesManagement.unitPrice')}:${Spec.UnitPrice}${this.$t('resourcesManagement.point_hr')}`,
});
cluster = Spec.Cluster;
if (queueIds.indexOf(Spec.QueueId) < 0) {
queues.push({
QueueId: Spec.QueueId,
QueueCode: Spec.QueueCode,
AiCenterCode: Spec.AiCenterCode,
AiCenterName: Spec.AiCenterName,
QueueStr: `${Spec.QueueCode}(${getListValueWithKey(this.clusterList, Spec.Cluster)} - ${Spec.AiCenterName})`,
});
queueIds.push(Spec.QueueId);
}
}
return {
ID: item.ID,
SceneName: item.SceneName,
JobType: item.JobType,
JobTypeStr: getListValueWithKey(this.taskTypeList, item.JobType),
IsExclusive: item.IsExclusive,
IsExclusiveStr: getListValueWithKey(this.isExclusiveList, item.IsExclusive ? '1' : '2'),
ExclusiveOrg: item.ExclusiveOrg,
Cluster: cluster,
QueueIds: queueIds,
Queues: queues,
SpecsList: specsList,
}
});
this.tableData = data;
this.pageInfo.total = res.Data.TotalSize;
}
}).catch(err => {
console.log(err);
this.loading = false;
});
},
selectChange() {
this.pageInfo.curpage = 1;
this.getTableData();
},
currentChange(val) {
this.pageInfo.curpage = val;
this.getTableData();
},
deleteRow(row) {
this.$confirm(this.$t('resourcesManagement.resSceneDeleteConfirm'), this.$t('tips'), {
confirmButtonText: this.$t('confirm1'),
cancelButtonText: this.$t('cancel'),
type: 'warning',
lockScroll: false,
}).then(() => {
updateResScene({
action: 'delete',
ID: row.ID,
}).then(res => {
res = res.data;
if (res.Code === 0) {
this.$message({
type: 'success',
message: this.$t('submittedSuccessfully')
});
this.getTableData();
} else {
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
}
}).catch(err => {
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
});
}).catch(() => { });
},
showDialog(type, data) {
this.sceneDialogType = type;
this.sceneDialogData = data ? {
ID: data.ID,
SceneName: data.SceneName,
JobType: data.JobType,
IsExclusive: data.IsExclusive ? '1' : '2',
ExclusiveOrg: data.ExclusiveOrg,
Cluster: data.Cluster,
QueueIds: data.QueueIds,
SpecIds: data.SpecsList.map((item) => item.k),
} : {};
this.sceneDialogShow = true;
},
sceneDialogConfirm() {
this.sceneDialogShow = false;
this.getTableData();
}
},
mounted() {
this.getAiCenterList();
this.getQueueList();
this.getTableData();
},
beforeDestroy() {
},
};
</script>

<style scoped lang="less">
.title {
height: 30px;
display: flex;
align-items: center;
margin-bottom: 5px;

span {
font-weight: 700;
font-size: 16px;
color: rgb(16, 16, 16);
}
}

.tools-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;

.select {
margin-right: 10px;

/deep/ .el-input__inner {
border-radius: 0;
}
}
}

.table-container {
margin-bottom: 16px;

/deep/ .el-table__header {
th {
background: rgb(245, 245, 246);
font-size: 12px;
color: rgb(36, 36, 36);
}
}

/deep/ .el-table__body {
td {
font-size: 12px;
}
}

.op-btn {
cursor: pointer;
font-size: 12px;
color: rgb(25, 103, 252);
margin: 0 5px;
}
}

.center {
display: flex;
justify-content: center;
}
</style>

+ 17
- 0
web_src/vuepages/pages/resources/scene/vp-resources-scene.js View File

@@ -0,0 +1,17 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import localeEn from 'element-ui/lib/locale/lang/en';
import localeZh from 'element-ui/lib/locale/lang/zh-CN';
import { i18n, lang } from '~/langs';
import App from './index.vue';

Vue.use(ElementUI, {
locale: lang === 'zh-CN' ? localeZh : localeEn,
size: 'small',
});

new Vue({
i18n,
render: (h) => h(App),
}).$mount('#__vue-root');

+ 451
- 0
web_src/vuepages/pages/resources/specification/index.vue View File

@@ -0,0 +1,451 @@
<template>
<div>
<div class="title"><span>{{ $t('resourcesManagement.resSpecificationAndPriceManagement') }}</span></div>
<div class="tools-bar">
<div>
<el-select class="select" size="medium" v-model="selQueue" @change="selectChange">
<el-option v-for="item in queueList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
<el-select class="select" size="medium" v-model="selStatus" @change="selectChange">
<el-option v-for="item in statusList" :key="item.k" :label="item.v" :value="item.k" />
</el-select>
</div>
<div>
<el-button size="medium" icon="el-icon-refresh" @click="syncComputerNetwork" v-loading="syncLoading">
{{ $t('resourcesManagement.syncAiNetwork') }}</el-button>
<el-button type="primary" icon="el-icon-plus" size="medium" @click="showDialog('add')">
{{ $t('resourcesManagement.addResSpecificationBtn') }}</el-button>
</div>
</div>
<div class="table-container">
<div style="min-height:600px;">
<el-table border :data="tableData" style="width: 100%" v-loading="loading" stripe>
<el-table-column prop="ID" label="ID" align="center" header-align="center" width="60"></el-table-column>
<el-table-column prop="SpecStr" :label="$t('resourcesManagement.resourceSpecification')" align="left"
header-align="center" min-width="160">
</el-table-column>
<el-table-column prop="QueueInfo" :label="$t('resourcesManagement.resQueue')" align="center"
header-align="center" min-width="100">
</el-table-column>
<el-table-column prop="SourceSpecId" :label="$t('resourcesManagement.sourceSpecCode')" align="center"
header-align="center">
</el-table-column>
<el-table-column prop="AccCardsNum" :label="$t('resourcesManagement.accCardsNum')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="CpuCores" :label="$t('resourcesManagement.cpuNum')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="GPUMemGiB" :label="`${$t('resourcesManagement.gpuMem')}(GB)`" align="center"
header-align="center"></el-table-column>
<el-table-column prop="MemGiB" :label="`${$t('resourcesManagement.mem')}(GB)`" align="center"
header-align="center"></el-table-column>
<el-table-column prop="ShareMemGiB" :label="`${$t('resourcesManagement.shareMem')}(GB)`" align="center"
header-align="center"></el-table-column>
<el-table-column prop="UpdatedTimeStr" :label="$t('resourcesManagement.lastUpdateTime')" align="center"
header-align="center"></el-table-column>
<el-table-column prop="UnitPrice"
:label="`${$t('resourcesManagement.unitPrice')}(${$t('resourcesManagement.point_hr')})`" align="center"
header-align="center">
<template slot-scope="scope">
<span style="font-weight:600;font-size:14px;">{{ scope.row.UnitPrice }}</span>
</template>
</el-table-column>
<el-table-column prop="StatusStr" :label="$t('resourcesManagement.status')" align="center"
header-align="center" width="100">
<template slot-scope="scope">
<span :style="{ color: scope.row.Status == '2' ? 'rgb(82, 196, 26)' : 'rgb(245, 34, 45)' }">{{
scope.row.StatusStr
}}</span>
</template>
</el-table-column>
<el-table-column :label="$t('operation')" align="center" header-align="center" width="100">
<template slot-scope="scope">
<span v-if="scope.row.Status == '1' && !scope.row.UnitPrice">
<span class="op-btn" @click="showDialog('edit', scope.row)">{{
$t('resourcesManagement.toSetPriceAndOnShelf')
}}</span>
</span>
<span v-if="scope.row.Status == '2'">
<span class="op-btn" @click="showDialog('edit', scope.row, true)">{{ $t('edit') }}</span>
<span class="op-btn" @click="offShelfPrev(scope.row)">{{
$t('resourcesManagement.toOffShelf')
}}</span>
</span>
<span v-if="scope.row.Status == '3' || scope.row.Status == '1' && scope.row.UnitPrice">
<span class="op-btn" @click="onShelf(scope.row)">{{
$t('resourcesManagement.toOnShelf')
}}</span>
</span>
</template>
</el-table-column>
<template slot="empty">
<span style="font-size: 12px">{{
loading ? $t('loading') : $t('noData')
}}</span>
</template>
</el-table>
</div>
<div class="__r_p_pagination">
<div style="margin-top: 2rem">
<div class="center">
<el-pagination background @current-change="currentChange" :current-page="pageInfo.curpage"
:page-sizes="pageInfo.pageSizes" :page-size="pageInfo.pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="pageInfo.total">
</el-pagination>
</div>
</div>
</div>
</div>
<SpecificationDialog :visible.sync="specificationDialogShow" :type="specificationDialogType"
:editOr="specificationDialogEditOr" :data="specificationDialogData" @confirm="specificationDialogConfirm">
</SpecificationDialog>

<BaseDialog :visible.sync="offShelfDialogShow" :width="`600px`" :title="$t('tips')">
<div class="form">
<div class="form-row" style="flex-direction:column;">
<div class="content" style="margin:8px 0">{{ $t('resourcesManagement.offShelfDlgTip1') }}</div>
<div class="content" style="margin:8px 0;font-weight: bold;">{{ offSelfDialogContent }}</div>
<div class="content" style="margin:8px 0">{{ $t('resourcesManagement.offShelfDlgTip2') }}</div>
</div>
<div class="form-row" style="margin-top: 20px">
<div class="content">
<el-button type="primary" class="btn confirm-btn" @click="offShelf">{{ $t('confirm') }}</el-button>
<el-button class="btn" @click="offShelfDialogShow = false">{{ $t('cancel') }}</el-button>
</div>
</div>
</div>
</BaseDialog>
</div>
</template>

<script>
import SpecificationDialog from '../components/SpecificationDialog.vue';
import BaseDialog from '~/components/BaseDialog.vue';
import { getResQueueCode, getResSpecificationList, updateResSpecification, syncResSpecification, getResSpecificationScenes } from '~/apis/modules/resources';
import { SPECIFICATION_STATUS, CLUSTERS, ACC_CARD_TYPE } from '~/const';
import { getListValueWithKey } from '~/utils';
import { formatDate } from 'element-ui/lib/utils/date-util';

export default {
data() {
return {
selQueue: '',
queueList: [{ k: '', v: this.$t('resourcesManagement.allResQueue') }],
selStatus: '',
statusList: [{ k: '', v: this.$t('resourcesManagement.allStatus') }, ...SPECIFICATION_STATUS],
clusterList: [...CLUSTERS],
accCardTypeList: [...ACC_CARD_TYPE],
syncLoading: false,
loading: false,
tableData: [],
pageInfo: {
curpage: 1,
pageSize: 10,
pageSizes: [10],
total: 0,
},
specificationDialogShow: false,
specificationDialogType: 'add',
specificationDialogEditOr: false,
specificationDialogData: {},

offShelfDialogShow: false,
offShelfDialogData: {},
offSelfDialogContent: '',
};
},
components: { BaseDialog, SpecificationDialog },
methods: {
getQueueList() {
getResQueueCode().then(res => {
res = res.data;
if (res.Code === 0) {
const data = res.Data;
const list = [];
for (let i = 0, iLen = data.length; i < iLen; i++) {
const item = data[i];
list.push({
k: item.ID,
v: `${item.QueueCode}(${getListValueWithKey(this.clusterList, item.Cluster)} - ${item.AiCenterName})`,
});
}
this.queueList.push(...list);
}
}).catch(err => {
console.log(err);
});
},
getTableData() {
const params = {
queue: this.selQueue,
status: this.selStatus,
page: this.pageInfo.curpage,
pagesize: this.pageInfo.pageSize,
};
this.loading = true;
getResSpecificationList(params).then(res => {
this.loading = false;
res = res.data;
if (res.Code === 0) {
const list = res.Data.List;
const data = list.map((item) => {
const Queue = item.Queue;
const Spec = item.Spec;
// const NGPU = `${Queue.ComputeResource}:${Spec.AccCardsNum === 0 ? '0' : Spec.AccCardsNum + '*' + getListValueWithKey(this.accCardTypeList, Queue.AccCardType)}`;
const NGPU = `${Queue.ComputeResource}:${Spec.AccCardsNum + '*' + getListValueWithKey(this.accCardTypeList, Queue.AccCardType)}`;
return {
...Spec,
SourceSpecId: Spec.SourceSpecId || '--',
SpecStr: `${NGPU}, CPU:${Spec.CpuCores}, ${this.$t('resourcesManagement.gpuMem')}:${Spec.GPUMemGiB}GB, ${this.$t('resourcesManagement.mem')}:${Spec.MemGiB}GB, ${this.$t('resourcesManagement.shareMem')}:${Spec.ShareMemGiB}GB`,
QueueId: Queue.ID,
QueueInfo: `${Queue.QueueCode}(${getListValueWithKey(this.clusterList, Queue.Cluster)} - ${Queue.AiCenterName})`,
UpdatedTimeStr: formatDate(new Date(Spec.UpdatedTime * 1000), 'yyyy-MM-dd HH:mm:ss'),
Status: Spec.Status.toString(),
StatusStr: getListValueWithKey(this.statusList, Spec.Status.toString()),
}
});
this.tableData = data;
this.pageInfo.total = res.Data.TotalSize;
}
}).catch(err => {
console.log(err);
this.loading = false;
});
},
syncComputerNetwork() {
this.syncLoading = true;
syncResSpecification().then(res => {
this.syncLoading = false;
res = res.data;
if (res.Code === 0) {
this.$message({
type: 'success',
message: this.$t('submittedSuccessfully')
});
this.getTableData();
} else {
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
}
}).catch(err => {
console.log(err);
this.syncLoading = false;
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
});
},
selectChange() {
this.pageInfo.curpage = 1;
this.getTableData();
},
currentChange(val) {
this.pageInfo.curpage = val;
this.getTableData();
},
showDialog(type, data, editOr) {
this.specificationDialogType = type;
this.specificationDialogEditOr = !!editOr;
this.specificationDialogData = data ? { ...data } : {};
this.specificationDialogShow = true;
},
specificationDialogConfirm() {
this.specificationDialogShow = false;
this.getTableData();
},
onShelf(data) {
const type = 'on-shelf';
this.$confirm(type === 'on-shelf' ? this.$t('resourcesManagement.onShelfConfirm') : this.$t('resourcesManagement.offShelfConfirm'), this.$t('tips'), {
confirmButtonText: this.$t('confirm1'),
cancelButtonText: this.$t('cancel'),
type: 'warning',
lockScroll: false,
}).then(() => {
updateResSpecification({
ID: data.ID,
action: type
}).then(res => {
res = res.data;
if (res.Code === 0) {
this.$message({
type: 'success',
message: this.$t('submittedSuccessfully')
});
this.getTableData();
} else {
if (type === 'on-shelf' && res.Code === 1001) {
this.$message({
type: 'info',
message: this.$t('resourcesManagement.onShelfCode1001')
});
} else {
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
}
}
}).catch(err => {
console.log(err);
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
});
}).catch(() => { });
},
offShelfPrev(data) {
this.$confirm(this.$t('resourcesManagement.offShelfConfirm'), this.$t('tips'), {
confirmButtonText: this.$t('confirm1'),
cancelButtonText: this.$t('cancel'),
type: 'warning',
lockScroll: false,
}).then(() => {
this.offShelfDialogData = data;
getResSpecificationScenes({ ID: data.ID }).then(res => {
res = res.data;
if (res.Code === 0) {
if (res.Data.List.length) {
this.offShelfDialogShow = true;
this.offSelfDialogContent = res.Data.List.map((item) => `[${item.ID}]${item.SceneName}`).join(', ');
} else {
this.offShelf();
}
} else {
console.log(res);
}
}).catch(err => {
console.log(err);
});
}).catch(() => { });
},
offShelf() {
updateResSpecification({
ID: this.offShelfDialogData.ID,
action: 'off-shelf'
}).then(res => {
res = res.data;
if (res.Code === 0) {
this.$message({
type: 'success',
message: this.$t('submittedSuccessfully')
});
this.offShelfDialogShow = false;
this.getTableData();
} else {
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
}
}).catch(err => {
console.log(err);
this.$message({
type: 'error',
message: this.$t('submittedFailed')
});
});
},
},
mounted: function () {
this.getQueueList();
this.getTableData();
},
beforeDestroy: function () {
},
};
</script>

<style scoped lang="less">
.title {
height: 30px;
display: flex;
align-items: center;
margin-bottom: 5px;

span {
font-weight: 700;
font-size: 16px;
color: rgb(16, 16, 16);
}
}

.tools-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;

.select {
margin-right: 10px;

/deep/ .el-input__inner {
border-radius: 0;
}
}
}

.table-container {
margin-bottom: 16px;

/deep/ .el-table__header {
th {
background: rgb(245, 245, 246);
font-size: 12px;
color: rgb(36, 36, 36);
}
}

/deep/ .el-table__body {
td {
font-size: 12px;
}
}

.op-btn {
cursor: pointer;
font-size: 12px;
color: rgb(25, 103, 252);
margin-right: 4px;
}
}

.center {
display: flex;
justify-content: center;
}

.form {
margin: 5px 0 5px 0;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}

.form-row {
display: flex;
min-height: 42px;
margin-bottom: 4px;

.content {
width: 500px;
display: flex;
align-items: center;
}
}

.btn {
color: rgb(2, 0, 4);
background-color: rgb(194, 199, 204);
border-color: rgb(194, 199, 204);

&.confirm-btn {
color: #fff;
background-color: rgb(56, 158, 13);
border-color: rgb(56, 158, 13);
}
}
</style>

+ 17
- 0
web_src/vuepages/pages/resources/specification/vp-resources-specification.js View File

@@ -0,0 +1,17 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import localeEn from 'element-ui/lib/locale/lang/en';
import localeZh from 'element-ui/lib/locale/lang/zh-CN';
import { i18n, lang } from '~/langs';
import App from './index.vue';

Vue.use(ElementUI, {
locale: lang === 'zh-CN' ? localeZh : localeEn,
size: 'small',
});

new Vue({
i18n,
render: (h) => h(App),
}).$mount('#__vue-root');

+ 148
- 0
web_src/vuepages/pages/reward/point/utils.js View File

@@ -0,0 +1,148 @@

import { formatDate } from 'element-ui/lib/utils/date-util';
import { SOURCE_TYPE, CONSUME_STATUS, POINT_ACTIONS, JOB_TYPE } from '~/const';
import { i18n } from '~/langs';

const getSourceType = (key) => {
const find = SOURCE_TYPE.filter(item => item.k === key);
return find.length ? find[0].v : key;
};

const getConsumeStatus = (key) => {
const find = CONSUME_STATUS.filter(item => item.k === key);
return find.length ? find[0].v : key;
};

const getPointAction = (key) => {
const find = POINT_ACTIONS.filter(item => item.k === key);
return find.length ? find[0].v : key;
};

const getJobType = (key) => {
const find = JOB_TYPE.filter(item => item.k === key);
return find.length ? find[0].v : key;
};

const getJobTypeLink = (record, type) => {
let link = type === 'INCREASE' ? record.Action.RepoLink : '/' + record.Cloudbrain.RepoFullName;
const cloudbrain = type === 'INCREASE' ? record.Action?.Cloudbrain : record.Cloudbrain;
switch (cloudbrain?.JobType) {
case 'DEBUG':
if (cloudbrain.ComputeResource === 'CPU/GPU') {
link += `/cloudbrain/${cloudbrain.ID}`;
} else {
link += `/modelarts/notebook/${cloudbrain.ID}`;
}
break;
case 'TRAIN':
if (cloudbrain.Type === 1) {
link += `/modelarts/train-job/${cloudbrain.JobID}`;
} else if (cloudbrain.Type === 0) {
link += `/cloudbrain/train-job/${cloudbrain.JobID}`;
} else if (cloudbrain.Type === 2) {
link += `/grampus/train-job/${cloudbrain.JobID}`;
}
break;
case 'INFERENCE':
link += `/modelarts/inference-job/${cloudbrain.JobID}`;
break;
case 'BENCHMARK':
link += `/cloudbrain/benchmark/${cloudbrain.ID}`;
break;
default:
break;
};
return link;
};

export const getRewardPointRecordInfo = (record) => {
const out = {
sn: record.SerialNo,
date: formatDate(new Date(record.LastOperateDate * 1000), 'yyyy-MM-dd HH:mm:ss'),
_status: record.Status,
status: getConsumeStatus(record.Status) || '--',
statusColor: record.Status === 'OPERATING' ? 'rgb(33, 186, 69)' : '',
_sourceType: record.SourceType,
sourceType: getSourceType(record.SourceType),
duration: record?.Cloudbrain?.Duration || '--',
taskName: record?.Cloudbrain?.DisplayJobName || '--',
taskId: record?.Cloudbrain?.ID,
action: record?.Action?.OpType ? getPointAction(record.Action.OpType) : '--',
remark: record.Remark,
amount: record.Amount,
};
if (record.OperateType === 'INCREASE') {
if (record.SourceType === 'ADMIN_OPERATE') {
out.remark = record.Remark;
} else if (record.SourceType === 'ACCOMPLISH_TASK') {
switch (record?.Action?.OpType) {
case 1: // 创建公开项目 - 创建了项目OpenI/aiforge
out.remark = `${i18n.t('createdRepository')}<a href="${record.Action.RepoLink}" rel="nofollow">${record.Action.ShortRepoFullDisplayName}</a>`;
break;
case 6: // 每日提出任务 - 创建了任务PCL-Platform.Intelligence/AISynergy#19
out.remark = `${i18n.t('openedIssue')}<a href="${record.Action.RepoLink}/issues/${record.Action.IssueInfos[0]}" rel="nofollow">${record.Action.ShortRepoFullDisplayName}#${record.Action.IssueInfos[0]}</a>`;
break;
case 7: // 每日提出PR - 创建了合并请求OpenI/aiforge#1
out.remark = `${i18n.t('createdPullRequest')}<a href="${record.Action.RepoLink}/pulls/${record.Action.IssueInfos[0]}" rel="nofollow">${record.Action.ShortRepoFullDisplayName}#${record.Action.IssueInfos[0]}</a>`;
break;
case 10: // 发表评论 - 评论了任务PCL-Platform.Intelligence/AISynergy#19
out.remark = `${i18n.t('commentedOnIssue')}<a href="${record.Action.CommentLink}" rel="nofollow">${record.Action.ShortRepoFullDisplayName}#${record.Action.IssueInfos[0]}</a>`;
break;
case 24: // 上传数据集文件 - 上传了数据集文件MMISTData.zip
out.remark = `${i18n.t('uploadDataset')}<a href="${record.Action.RepoLink}/datasets" rel="nofollow">${record.Action.RefName}</a>`;
break;
case 30: // 导入新模型 - 导入了新模型resnet50_qx7l
out.remark = `${i18n.t('createdNewModel')}<a href="${record.Action.RepoLink}/modelmanage/show_model_info?name=${record.Action.RefName}" rel="nofollow">${record.Action.RefName}</a>`;
break;
case 34: // 完成微信扫码验证 - 首次绑定微信奖励
out.remark = `${i18n.t('firstBindingWechatRewards')}`;
break;
case 35: // 每日运行云脑任务 - 创建了(CPU/GPU/NPU)类型(调试/训练/推理/评测)任务tangl202204131431995
out.remark = `${i18n.t('created')}${record.Action?.Cloudbrain?.ComputeResource}${i18n.t('type')}${getJobType(record.Action?.Cloudbrain?.JobType)} <a href="${getJobTypeLink(record, 'INCREASE')}" rel="nofollow">${record.Action.RefName}</a>`;
break;
case 36: // 数据集被平台推荐 - 数据集XXX被设置为推荐数据集
out.remark = `${i18n.t('dataset')}<a href="${record.Action.RepoLink}/datasets" rel="nofollow">${record.Action.Content && record.Action.Content.split('|')[1]}</a>${i18n.t('setAsRecommendedDataset')}`;
break;
case 37: // 提交新公开镜像 - 提交了镜像jiangxiang_ceshi_tang03
out.remark = `${i18n.t('committedImage')}<span style="font-weight:bold;">${record.Action.Content && record.Action.Content.split('|')[1]}</span>`;
break;
case 38: // 镜像被平台推荐 - 镜像XXX被设置为推荐镜像
out.remark = `${i18n.t('image')}<span style="font-weight:bold;">${record.Action.Content && record.Action.Content.split('|')[1]}</span>${i18n.t('setAsRecommendedImage')}`;
break;
case 39: // 首次更换头像 - 更新了头像
out.remark = `${i18n.t('updatedAvatar')}`;
break;
case 40: // 每日commit - 推送了xxxx分支的代码到OpenI/aiforge
const words = record.Action.RefName.split('/');
const branch = words[words.length - 1];
out.remark = `${i18n.t('pushedBranch', {
branch: `<a href="${record.Action.RepoLink}/src/branch/${branch}" rel="nofollow">${branch}</a>`
})}<a href="${record.Action.RepoLink}" rel="nofollow">${record.Action.ShortRepoFullDisplayName}</a>`;
break;
default:
break;
}
} else if (record.SourceType === 'RUN_CLOUDBRAIN_TASK') {
//
}
if (record.LossAmount !== 0) {
out.amount = record.Amount;
out.remark += `${out.remark ? i18n.t(';') : ''}${i18n.t('dailyMaxTips')}`;
}
} else if (record.OperateType === 'DECREASE') {
if (record.SourceType === 'ADMIN_OPERATE') {
out.remark = record.Remark;
} else if (record.SourceType === 'ACCOMPLISH_TASK') {
//
} else if (record.SourceType === 'RUN_CLOUDBRAIN_TASK') {
out.taskName = `<a href="${getJobTypeLink(record, 'DECREASE')}" rel="nofollow">${record?.Cloudbrain?.DisplayJobName}</a>`;
if (record?.Cloudbrain?.ComputeResource === 'CPU/GPU') {
const resourceSpec = record?.Cloudbrain?.ResourceSpec?.ResourceSpec;
out.remark = `【${getJobType(record?.Cloudbrain?.JobType)}】【${record?.Cloudbrain?.ComputeResource}】【GPU: ${resourceSpec?.gpu}, CPU: ${resourceSpec?.cpu}, ${i18n.t('memory')}: ${(resourceSpec?.memMiB / 1024).toFixed(2)}GB, ${i18n.t('sharedMemory')}: ${(resourceSpec?.shareMemMiB / 1024).toFixed(2)}GB】`;
} else {
out.remark = `【${getJobType(record?.Cloudbrain?.JobType)}】【${record?.Cloudbrain?.ComputeResource}】【${record?.Cloudbrain?.ResourceSpec.FlavorInfo.desc}】`;
}
}
}
return out;
};

+ 16
- 0
web_src/vuepages/pages/reward/point/vp-point.js View File

@@ -0,0 +1,16 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import localeEn from 'element-ui/lib/locale/lang/en';
import localeZh from 'element-ui/lib/locale/lang/zh-CN';
import { i18n, lang } from '~/langs';
import App from './vp-point.vue';

Vue.use(ElementUI, {
locale: lang === 'zh-CN' ? localeZh : localeEn
});

new Vue({
i18n,
render: (h) => h(App),
}).$mount('#__vue-root');

+ 308
- 0
web_src/vuepages/pages/reward/point/vp-point.vue View File

@@ -0,0 +1,308 @@
<template>
<div class="__reward-pointer-c">
<div class="ui container" style="width:80%;min-width:1200px;">
<div class="__r_p_header">
<div>
<p class="__title">{{ $t('calcPointDetails') }}</p>
</div>
<div style="padding: 0 5px; font-size: 14px">
<span>
<i class="question circle icon link" style="color: rgba(3, 102, 214, 1)" data-position="right center"
data-variation="mini"></i>
<a href="/reward/point/rule" target="_blank" style="color: rgba(3, 102, 214, 1)">{{
$t('calcPointAcquisitionInstructions')
}}</a>
</span>
</div>
</div>
<div class="__r_p_summary">
<div class="__r_p_summary_item-c __flex-1">
<div class="__val">{{ summaryInfo.available }}</div>
<div class="__exp">{{ $t('CurrAvailableCalcPoints') }}</div>
</div>
<div class="__r_p_summary_line"></div>
<div class="__r_p_summary_item-c __flex-1">
<div class="__val">{{ summaryInfo.gain }}</div>
<div class="__exp">{{ $t('totalGainCalcPoints') }}</div>
</div>
<div class="__r_p_summary_item-c __flex-1">
<div class="__val">{{ summaryInfo.used }}</div>
<div class="__exp">{{ $t('totalConsumeCalcPoints') }}</div>
</div>
</div>
<div class="__r_p_tab">
<div class="__r_p_tab-item" :class="tabIndex === 0 ? '__focus' : ''" style="border-radius: 5px 0px 0px 5px"
@click="tabChange(0)">
{{ $t('gainDetail') }}
</div>
<div class="__r_p_tab-item" :class="tabIndex === 1 ? '__focus' : ''" style="border-radius: 0px 5px 5px 0px"
@click="tabChange(1)">
{{ $t('consumeDetail') }}
</div>
</div>
<div class="__r_p_table">
<div v-show="tabIndex === 0">
<el-table :data="tableData" row-key="sn" style="width: 100%" v-loading="loading" stripe
v-if="tableData.length">
<el-table-column column-key="sn" prop="sn" :label="$t('serialNumber')" align="center" header-align="center"
width="180">
</el-table-column>
<el-table-column column-key="date" prop="date" :label="$t('time')" align="center" header-align="center"
width="180">
</el-table-column>
<el-table-column column-key="sourceType" prop="sourceType" :label="$t('scene')" align="center"
header-align="center" width="180"></el-table-column>
<el-table-column column-key="action" prop="action" :label="$t('behaviorOfPoint')" align="center"
header-align="center" width="200"></el-table-column>
<el-table-column column-key="remark" prop="remark" :label="$t('explanation')" align="left" min-width="200"
header-align="center">
<template slot-scope="scope">
<span v-html="scope.row.remark"></span>
</template>
</el-table-column>
<el-table-column column-key="amount" prop="amount" :label="$t('points')" align="center"
header-align="center" width="120"></el-table-column>
<template slot="empty">
<span>{{ loading ? $t('loading') : $t('noData') }}</span>
</template>
</el-table>
<el-empty v-else :image-size="140" :description="$t('noPointGainRecord')"></el-empty>
</div>
<div v-show="tabIndex === 1">
<el-table :data="tableData" row-key="sn" style="width: 100%" v-loading="loading" stripe
v-if="tableData.length">
<el-table-column column-key="sn" prop="sn" :label="$t('serialNumber')" align="center" header-align="center"
width="180">
</el-table-column>
<el-table-column column-key="date" prop="date" :label="$t('time')" align="center" header-align="center"
width="180">
</el-table-column>
<el-table-column column-key="status" prop="status" :label="$t('status')" align="center"
header-align="center" width="120">
<template slot-scope="scope">
<span :style="{ color: scope.row.statusColor }">{{ scope.row.status }}</span>
</template>
</el-table-column>
<el-table-column column-key="sourceType" prop="sourceType" :label="$t('scene')" align="center"
header-align="center" width="180"></el-table-column>
<el-table-column column-key="duration" prop="duration" :label="$t('runTime')" align="center"
header-align="center" width="120"></el-table-column>
<el-table-column column-key="remark" prop="remark" :label="$t('explanation')" align="left" min-width="200"
header-align="center">
<template slot-scope="scope">
<span v-html="scope.row.remark"></span>
</template>
</el-table-column>
<el-table-column column-key="taskName" prop="taskName" :label="$t('taskName')" align="center"
header-align="center" width="180">
<template slot-scope="scope">
<span v-html="scope.row.taskName"></span>
</template>
</el-table-column>
<el-table-column column-key="amount" prop="amount" :label="$t('points')" align="center"
header-align="center" width="120"></el-table-column>
<template slot="empty">
<span>{{ loading ? $t('loading') : $t('noData') }}</span>
</template>
</el-table>
<el-empty v-else :image-size="140" :description="$t('noPointConsumeRecord')"></el-empty>
</div>
<div class="__r_p_pagination" v-if="tableData.length">
<div style="margin-top: 2rem">
<div class="center">
<el-pagination background @current-change="currentChange" :current-page="pageInfo.curpage"
:page-sizes="pageInfo.pageSizes" :page-size="pageInfo.pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="pageInfo.total">
</el-pagination>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
import { getPoint, getPointAccount, getPointList } from "~/apis/modules/point";
import { getRewardPointRecordInfo } from './utils';
export default {
data() {
return {
loading: false,
summaryInfo: {
available: 0,
gain: 0,
used: 0,
},
tabIndex: 0,
tableData: [],
pageInfo: {
curpage: 1,
pageSize: 10,
pageSizes: [10],
total: 0,
},
eventSource: null,
};
},
components: {},
methods: {
currentChange: function (val) {
this.pageInfo.curpage = val;
this.getTableData();
},
tabChange: function (index) {
if (this.tabIndex === index) return;
this.tabIndex = index;
this.pageInfo.curpage = 1;
this.pageInfo.total = 0;
this.getTableData();
},
getSummaryInfo: function () {
getPointAccount().then(res => {
if (res.data && res.data.Code === 0) {
const data = res.data.Data;
this.summaryInfo.available = data.Balance;
this.summaryInfo.gain = data.TotalEarned;
this.summaryInfo.used = data.TotalConsumed;
}
}).catch(err => {
console.log(err);
})
},
getTableData: function () {
this.loading = true;
getPointList({
Operate: this.tabIndex === 0 ? 'INCREASE' : 'DECREASE',
Page: this.pageInfo.curpage,
// pageSize: this.pageInfo.pageSize,
}).then((res) => {
this.loading = false;
const tableData = [];
if (res.data && res.data.Code === 0) {
const data = res.data.Data;
const records = data.Records;
for (let i = 0, iLen = records.length; i < iLen; i++) {
const record = records[i];
tableData.push(getRewardPointRecordInfo(record));
}
this.tableData.splice(0, Infinity, ...tableData);
this.pageInfo.total = data.Total;
}
})
.catch((err) => {
console.log(err);
this.loading = false;
this.tableData.splice(0, Infinity);
});
},
},
mounted: function () {
this.getSummaryInfo();
this.getTableData();
const { AppSubUrl, csrf, NotificationSettings } = window.config;
if (NotificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource) {
const source = new EventSource(`${AppSubUrl}/user/events`);
source.addEventListener('reward-operation', (e) => {
try {
this.getSummaryInfo();
this.getTableData();
} catch (err) {
console.error(err);
}
});
this.eventSource = source;
}
},
beforeDestroy: function () {
this.eventSource && this.eventSource.close();
},
};
</script>

<style scoped lang="less">
.__flex-1 {
flex: 1;
}

.__reward-pointer-c {
.__r_p_header {
height: 30px;
margin: 10px 0;
display: flex;
align-items: center;
justify-content: space-between;

.__title {
font-weight: 400;
font-size: 18px;
color: rgb(16, 16, 16);
line-height: 26px;
}
}

.__r_p_summary {
display: flex;
align-items: center;
height: 100px;
background-color: rgb(245, 245, 246);

.__r_p_summary_item-c {
.__val {
text-align: center;
margin: 12px 0;
font-weight: 400;
font-size: 28px;
color: rgb(16, 16, 16);
}

.__exp {
text-align: center;
font-weight: 400;
font-size: 14px;
color: rgba(54, 56, 64, 1);
}
}
}
}

.__r_p_summary_line {
width: 1px;
height: 80%;
background-color: rgb(212, 212, 213);
}

.__r_p_tab {
display: flex;
margin: 18px 0;

.__r_p_tab-item {
width: 115px;
height: 38px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid rgb(225, 227, 230);
color: #101010;
box-sizing: border-box;
cursor: pointer;

&.__focus {
border-color: rgb(50, 145, 248);
color: rgb(50, 145, 248);
cursor: default;
}
}
}

.__r_p_table {
/deep/ .el-table__header {
th {
background: rgb(245, 245, 246);
color: rgb(96, 98, 102);
font-weight: 400;
font-size: 14px;
}
}
}
</style>

+ 7
- 0
web_src/vuepages/utils/index.js View File

@@ -0,0 +1,7 @@
export const getListValueWithKey = (list, key, k = 'k', v = 'v') => {
for (let i = 0, iLen = list.length; i < iLen; i++) {
const listI = list[i];
if (listI[k] === key) return listI[v];
}
return '';
};

+ 7
- 0
webpack.config.js View File

@@ -29,6 +29,11 @@ for (const path of stadalonePaths) {
standalone[parse(path).name] = [path];
}

const vuePages = {};
for (const path of glob('web_src/vuepages/**/vp-*.js')) {
vuePages[parse(path).name] = [path];
}

const isProduction = process.env.NODE_ENV !== 'development';

module.exports = {
@@ -44,6 +49,7 @@ module.exports = {
icons: glob('node_modules/@primer/octicons/build/svg/**/*.svg'),
...standalone,
...themes,
...vuePages,
},
devtool: false,
output: {
@@ -267,6 +273,7 @@ module.exports = {
symlinks: false,
alias: {
vue$: 'vue/dist/vue.esm.js', // needed because vue's default export is the runtime only
'~': resolve(__dirname, 'web_src/vuepages'),
},
extensions: ['.tsx', '.ts', '.js']
},


+ 7
- 0
webpack_pro.config.js View File

@@ -29,6 +29,11 @@ for (const path of stadalonePaths) {
standalone[parse(path).name] = [path];
}

const vuePages = {};
for (const path of glob('web_src/vuepages/**/vp-*.js')) {
vuePages[parse(path).name] = [path];
}

const isProduction = process.env.NODE_ENV !== 'development';

module.exports = {
@@ -44,6 +49,7 @@ module.exports = {
icons: glob('node_modules/@primer/octicons/build/svg/**/*.svg'),
...standalone,
...themes,
...vuePages
},
devtool: false,
output: {
@@ -267,6 +273,7 @@ module.exports = {
symlinks: false,
alias: {
vue$: 'vue/dist/vue.esm.js', // needed because vue's default export is the runtime only
'~': resolve(__dirname, 'web_src/vuepages'),
},
extensions: ['.tsx', '.ts', '.js']
},


Loading…
Cancel
Save