diff --git a/models/attachment.go b/models/attachment.go index a3fc6fa01..7c95a73dd 100755 --- a/models/attachment.go +++ b/models/attachment.go @@ -51,6 +51,7 @@ type Attachment struct { FileChunk *FileChunk `xorm:"-"` CanDel bool `xorm:"-"` Uploader *User `xorm:"-"` + Md5 string `xorm:"-"` } type AttachmentUsername struct { diff --git a/models/dataset.go b/models/dataset.go index 7cac6c468..95800100c 100755 --- a/models/dataset.go +++ b/models/dataset.go @@ -1,6 +1,7 @@ package models import ( + "code.gitea.io/gitea/modules/log" "errors" "fmt" "sort" @@ -62,19 +63,20 @@ func (datasets DatasetList) loadAttributes(e Engine) error { } set := make(map[int64]struct{}) + userIdSet := make(map[int64]struct{}) datasetIDs := make([]int64, len(datasets)) for i := range datasets { - set[datasets[i].UserID] = struct{}{} + userIdSet[datasets[i].UserID] = struct{}{} set[datasets[i].RepoID] = struct{}{} datasetIDs[i] = datasets[i].ID } // Load owners. - users := make(map[int64]*User, len(set)) + users := make(map[int64]*User, len(userIdSet)) repos := make(map[int64]*Repository, len(set)) if err := e. Where("id > 0"). - In("id", keysInt64(set)). + In("id", keysInt64(userIdSet)). Find(&users); err != nil { return fmt.Errorf("find users: %v", err) } @@ -296,7 +298,13 @@ func getDatasetAttachments(e Engine, typeCloudBrain int, isSigned bool, user *Us if err != nil { return err } - attachment.FileChunk = fileChunks[0] + if len(fileChunks) > 0 { + attachment.Md5 = fileChunks[0].Md5 + } else { + log.Error("has attachment record, but has no file_chunk record") + attachment.Md5 = "no_record" + } + attachment.CanDel = CanDelAttachment(isSigned, user, attachment) sortedRels.Rel[currentIndex].Attachments = append(sortedRels.Rel[currentIndex].Attachments, attachment) } diff --git a/models/dbsql/issue_foreigntable_for_es.sql b/models/dbsql/issue_foreigntable_for_es.sql index 30fa01550..d6a16cd27 100644 --- a/models/dbsql/issue_foreigntable_for_es.sql +++ b/models/dbsql/issue_foreigntable_for_es.sql @@ -193,6 +193,7 @@ $def$ name=NEW.name, is_closed=NEW.is_closed, num_comments=NEW.num_comments, + updated_unix=NEW.updated_unix, comment=(select array_to_string(array_agg(content order by created_unix desc),',') from public.comment where issue_id=NEW.id) where id=NEW.id; return new; diff --git a/modules/modelarts/modelarts.go b/modules/modelarts/modelarts.go index 538fcfbd9..b1e7b269e 100755 --- a/modules/modelarts/modelarts.go +++ b/modules/modelarts/modelarts.go @@ -280,6 +280,7 @@ func GenerateNotebook2(ctx *context.Context, displayJobName, jobName, uuid, desc RepoID: ctx.Repo.Repository.ID, JobID: jobResult.ID, JobName: jobName, + FlavorCode: flavor, DisplayJobName: displayJobName, JobType: string(models.JobTypeDebug), Type: models.TypeCloudBrainTwo, diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 8af3eaaf4..dceb48f16 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -40,6 +40,7 @@ var ( DisabledRepoUnits []string DefaultRepoUnits []string PrefixArchiveFiles bool + RepoMaxSize int64 // Repository editor settings Editor struct { @@ -54,6 +55,7 @@ var ( AllowedTypes []string `delim:"|"` FileMaxSize int64 MaxFiles int + TotalMaxSize int64 } `ini:"-"` // Repository local settings @@ -104,6 +106,7 @@ var ( DisabledRepoUnits: []string{}, DefaultRepoUnits: []string{}, PrefixArchiveFiles: true, + RepoMaxSize: 1024, // Repository editor settings Editor: struct { @@ -121,12 +124,14 @@ var ( AllowedTypes []string `delim:"|"` FileMaxSize int64 MaxFiles int + TotalMaxSize int64 }{ Enabled: true, TempPath: "data/tmp/uploads", AllowedTypes: []string{}, - FileMaxSize: 3, - MaxFiles: 5, + FileMaxSize: 30, + MaxFiles: 10, + TotalMaxSize: 1024, }, // Repository local settings diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 58168d1e2..394b9b7df 100755 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -919,6 +919,10 @@ cloudbrain_helper=Use GPU/NPU resources to open notebooks, model training tasks, cloudbrain.exitinfo=Exit Information cloudbrain.platform=Platform cloudbrain.endtime=End Time +cloudbrain.runinfo=Task Runtime Information +cloudbrain.time.starttime=Start run time +cloudbrain.time.endtime=End run time +cloudbrain.datasetdownload=Dataset download url model_manager = Model model_noright=No right model_rename=Duplicate model name, please modify model name. @@ -1256,6 +1260,10 @@ editor.cannot_commit_to_protected_branch = Cannot commit to protected branch '%s editor.no_commit_to_branch = Unable to commit directly to branch because: editor.user_no_push_to_branch = User cannot push to branch editor.require_signed_commit = Branch requires a signed commit +editor.repo_too_large = Repository can not exceed %d MB +editor.repo_file_invalid = Upload files are invalid +editor.upload_file_too_much = Can not upload more than %d files at a time + commits.desc = Browse source code change history. commits.commits = Commits @@ -2863,6 +2871,8 @@ uploading = Uploading upload_complete = Uploading complete failed = Upload Failed enable_minio_support = Enable minio support to use the dataset service +max_file_tooltips= Upload a maximum of ? files at a time, each file does not exceed ? MB. +max_size_tooltips= You can only upload a maximum of ? files at a time. The upload limit has been reached, please do not add more files. [notification] notifications = Notifications diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 28041428f..8982d59d7 100755 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -966,6 +966,10 @@ cloudbrain.mirror_description = 镜像描述 cloudbrain.exitinfo=退出信息 cloudbrain.platform=平台 cloudbrain.endtime=结束时间 +cloudbrain.runinfo=任务运行简况 +cloudbrain.time.starttime=开始运行时间 +cloudbrain.time.endtime=结束运行时间 +cloudbrain.datasetdownload=数据集下载地址 record_begintime_get_err=无法获取统计开始时间。 parameter_is_wrong=输入参数错误,请检查输入参数。 total_count_get_error=查询总页数失败。 @@ -1263,6 +1267,9 @@ editor.cannot_commit_to_protected_branch=不可以提交到受保护的分支 '% editor.no_commit_to_branch=无法直接提交分支,因为: editor.user_no_push_to_branch=用户不能推送到分支 editor.require_signed_commit=分支需要签名提交 +editor.repo_too_large = 代码仓总大小不能超过%dMB +editor.repo_file_invalid = 提交的文件非法 +editor.upload_file_too_much = 不能同时提交超过%d个文件 commits.desc=浏览代码修改历史 commits.commits=次代码提交 @@ -2869,6 +2876,8 @@ uploading=正在上传 upload_complete=上传完成 failed=上传失败 enable_minio_support=启用minio支持以使用数据集服务 +max_file_tooltips=单次最多上传?个文件,每个文件不超过? MB。 +max_size_tooltips=一次最多只能上传?个文件, 上传已达到上限,请勿再添加文件。 [notification] notifications=通知 diff --git a/routers/repo/cloudbrain.go b/routers/repo/cloudbrain.go index 73b1914e5..0d007a27d 100755 --- a/routers/repo/cloudbrain.go +++ b/routers/repo/cloudbrain.go @@ -346,6 +346,24 @@ func CloudBrainRestart(ctx *context.Context) { break } + var hasSameResource bool + if gpuInfos == nil { + json.Unmarshal([]byte(setting.GpuTypes), &gpuInfos) + } + for _, resourceType := range gpuInfos.GpuInfo { + if resourceType.Queue == task.GpuQueue { + hasSameResource = true + continue + } + } + + if !hasSameResource { + log.Error("has no same resource, can not restart", ctx.Data["MsgID"]) + resultCode = "-1" + errorMsg = "the job's version is too old and can not be restarted" + break + } + count, err := models.GetCloudbrainCountByUserID(ctx.User.ID, string(models.JobTypeDebug)) if err != nil { log.Error("GetCloudbrainCountByUserID failed:%v", err, ctx.Data["MsgID"]) @@ -417,6 +435,18 @@ func cloudBrainShow(ctx *context.Context, tpName base.TplName, jobType models.Jo return } + if cloudbrain.ResourceSpecs == nil { + json.Unmarshal([]byte(setting.ResourceSpecs), &cloudbrain.ResourceSpecs) + } + for _, tmp := range cloudbrain.ResourceSpecs.ResourceSpec { + if tmp.Id == task.ResourceSpecId { + ctx.Data["GpuNum"] = tmp.GpuNum + ctx.Data["CpuNum"] = tmp.CpuNum + ctx.Data["MemMiB"] = tmp.MemMiB + ctx.Data["ShareMemMiB"] = tmp.ShareMemMiB + } + } + if result != nil { jobRes, _ := models.ConvertToJobResultPayload(result.Payload) jobRes.Resource.Memory = strings.ReplaceAll(jobRes.Resource.Memory, "Mi", "MB") @@ -549,7 +579,7 @@ func cloudBrainShow(ctx *context.Context, tpName base.TplName, jobType models.Jo ctx.Data["code_path"] = cloudbrain.CodeMountPath ctx.Data["dataset_path"] = cloudbrain.DataSetMountPath ctx.Data["model_path"] = cloudbrain.ModelMountPath - ctx.Data["canDownload"] = cloudbrain.CanDeleteJob(ctx, task) + ctx.Data["canDownload"] = cloudbrain.CanModifyJob(ctx, task) ctx.HTML(200, tpName) } diff --git a/routers/repo/editor.go b/routers/repo/editor.go index 2fa7976e0..8e13735df 100644 --- a/routers/repo/editor.go +++ b/routers/repo/editor.go @@ -5,6 +5,7 @@ package repo import ( + repo_service "code.gitea.io/gitea/services/repository" "encoding/json" "fmt" "io/ioutil" @@ -614,6 +615,19 @@ func UploadFilePost(ctx *context.Context, form auth.UploadRepoFileForm) { message += "\n\n" + form.CommitMessage } + if err := repo_service.CheckPushSizeLimit4Web(ctx.Repo.Repository, form.Files); err != nil { + if repo_service.IsRepoTooLargeErr(err) { + ctx.RenderWithErr(ctx.Tr("repo.editor.repo_too_large", setting.Repository.RepoMaxSize), tplUploadFile, &form) + } else if repo_service.IsUploadFileInvalidErr(err) { + ctx.RenderWithErr(ctx.Tr("repo.editor.repo_file_invalid"), tplUploadFile, &form) + } else if repo_service.IsUploadFileTooMuchErr(err) { + ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_too_much", setting.Repository.Upload.MaxFiles), tplUploadFile, &form) + } else { + ctx.RenderWithErr(err.Error(), tplUploadFile, &form) + } + return + } + if err := repofiles.UploadRepoFiles(ctx.Repo.Repository, ctx.User, &repofiles.UploadRepoFileOptions{ LastCommitID: ctx.Repo.CommitID, OldBranch: oldBranchName, diff --git a/routers/repo/modelarts.go b/routers/repo/modelarts.go index 32d9db9ce..e2b75e704 100755 --- a/routers/repo/modelarts.go +++ b/routers/repo/modelarts.go @@ -2,10 +2,9 @@ package repo import ( "archive/zip" - "code.gitea.io/gitea/modules/notification" - "code.gitea.io/gitea/modules/timeutil" "encoding/json" "errors" + "fmt" "io" "io/ioutil" "net/http" @@ -16,6 +15,9 @@ import ( "time" "unicode/utf8" + "code.gitea.io/gitea/modules/notification" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" @@ -247,7 +249,7 @@ func NotebookShow(ctx *context.Context) { debugListType := ctx.Query("debugListType") var ID = ctx.Params(":id") - task, err := models.GetCloudbrainByID(ID) + task, err := models.GetCloudbrainByIDWithDeleted(ID) if err != nil { ctx.Data["error"] = err.Error() ctx.RenderWithErr(err.Error(), tplModelArtsNotebookShow, nil) @@ -262,33 +264,64 @@ func NotebookShow(ctx *context.Context) { } if result != nil { - task.Status = result.Status - err = models.UpdateJob(task) - if err != nil { - ctx.Data["error"] = err.Error() - ctx.RenderWithErr(err.Error(), tplModelArtsNotebookShow, nil) - return - } + if task.DeletedAt.IsZero() { //normal record + if task.Status != result.Status { + task.Status = result.Status + err = models.UpdateJob(task) + if err != nil { + ctx.Data["error"] = err.Error() + ctx.RenderWithErr(err.Error(), tplModelArtsNotebookShow, nil) + return + } + } + } else { //deleted record - result.CreateTime = time.Unix(int64(result.CreateAt/1000), 0).Format("2006-01-02 15:04:05") - result.LatestUpdateTime = time.Unix(int64(result.UpdateAt/1000), 0).Format("2006-01-02 15:04:05") + } } - datasetDownloadLink := "-" + datasetDownloadLink := "" if ctx.IsSigned { if task.Uuid != "" && task.UserID == ctx.User.ID { attachment, err := models.GetAttachmentByUUID(task.Uuid) if err == nil { + task.DatasetName = attachment.Name datasetDownloadLink = attachment.S3DownloadURL() } } } - + user, err := models.GetUserByID(task.UserID) + if err == nil { + task.User = user + } + if modelarts.FlavorInfos == nil { + json.Unmarshal([]byte(setting.FlavorInfos), &modelarts.FlavorInfos) + } + if modelarts.FlavorInfos != nil { + ctx.Data["resource_spec"] = modelarts.FlavorInfos.FlavorInfo[0].Desc + for _, f := range modelarts.FlavorInfos.FlavorInfo { + if fmt.Sprint(f.Value) == task.FlavorCode { + ctx.Data["resource_spec"] = f.Desc + break + } + } + } + if task.TrainJobDuration == "" { + if task.Duration == 0 { + var duration int64 + if task.Status == string(models.JobRunning) { + duration = time.Now().Unix() - int64(task.CreatedUnix) + } else { + duration = int64(task.UpdatedUnix) - int64(task.CreatedUnix) + } + task.Duration = duration + } + task.TrainJobDuration = models.ConvertDurationToStr(task.Duration) + } + ctx.Data["duration"] = task.TrainJobDuration ctx.Data["datasetDownloadLink"] = datasetDownloadLink ctx.Data["task"] = task ctx.Data["ID"] = ID ctx.Data["jobName"] = task.JobName - ctx.Data["result"] = result ctx.Data["debugListType"] = debugListType ctx.HTML(200, tplModelArtsNotebookShow) } @@ -1553,7 +1586,7 @@ func TrainJobShow(ctx *context.Context) { ctx.Data["displayJobName"] = VersionListTasks[0].DisplayJobName ctx.Data["version_list_task"] = VersionListTasks ctx.Data["version_list_count"] = VersionListCount - ctx.Data["canDownload"] = cloudbrain.CanDeleteJob(ctx, &VersionListTasks[0].Cloudbrain) + ctx.Data["canDownload"] = cloudbrain.CanModifyJob(ctx, &VersionListTasks[0].Cloudbrain) ctx.HTML(http.StatusOK, tplModelArtsTrainJobShow) } @@ -2184,7 +2217,7 @@ func InferenceJobShow(ctx *context.Context) { ctx.Data["jobName"] = task.JobName ctx.Data["displayJobName"] = task.DisplayJobName ctx.Data["task"] = task - ctx.Data["canDownload"] = cloudbrain.CanDeleteJob(ctx, task) + ctx.Data["canDownload"] = cloudbrain.CanModifyJob(ctx, task) tempUids := []int64{} tempUids = append(tempUids, task.UserID) diff --git a/routers/search.go b/routers/search.go index bc1bc5fac..c5655b9e1 100644 --- a/routers/search.go +++ b/routers/search.go @@ -573,7 +573,8 @@ func trimFontHtml(text []rune) string { startRune := rune('<') endRune := rune('>') count := 0 - for i := 0; i < len(text); i++ { + i := 0 + for ; i < len(text); i++ { if text[i] == startRune { //start < re := false j := i + 1 @@ -592,11 +593,14 @@ func trimFontHtml(text []rune) string { } else { return string(text[0:i]) } - } } } - return string(text) + if count%2 == 1 { + return string(text[0:i]) + "" + } else { + return string(text[0:i]) + } } func trimHrefHtml(result string) string { @@ -1125,7 +1129,7 @@ func makePrivateIssueOrPr(issues []*models.Issue, res *SearchRes, Key string, la record["num_comments"] = issue.NumComments record["is_closed"] = issue.IsClosed record["updated_unix"] = issue.UpdatedUnix - record["updated_html"] = timeutil.TimeSinceUnix(repo.UpdatedUnix, language) + record["updated_html"] = timeutil.TimeSinceUnix(issue.UpdatedUnix, language) res.Result = append(res.Result, record) } } diff --git a/services/repository/repository.go b/services/repository/repository.go index cea16516a..d0cd52653 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "net/http" + "os" "strings" "code.gitea.io/gitea/models" @@ -172,3 +173,137 @@ func RecommendFromPromote(url string) ([]string, error) { } return result, nil } + +func CheckPushSizeLimit4Web(repo *models.Repository, fileIds []string) error { + if err := CheckRepoNumOnceLimit(len(fileIds)); err != nil { + return err + } + totalSize, err := CountUploadFileSizeByIds(fileIds) + if err != nil { + return UploadFileInvalidErr{} + } + if err := CheckRepoTotalSizeLimit(repo, totalSize); err != nil { + return err + } + return nil +} + +func CheckPushSizeLimit4Http(repo *models.Repository, uploadFileSize int64) error { + if err := CheckRepoOnceTotalSizeLimit(uploadFileSize); err != nil { + return err + } + if err := CheckRepoTotalSizeLimit(repo, uploadFileSize); err != nil { + return err + } + return nil +} + +func CheckRepoTotalSizeLimit(repo *models.Repository, uploadFileSize int64) error { + if repo.Size+uploadFileSize > setting.Repository.RepoMaxSize*1024*1024 { + return RepoTooLargeErr{} + } + return nil +} + +func CheckRepoOnceTotalSizeLimit(uploadFileSize int64) error { + if uploadFileSize > setting.Repository.Upload.TotalMaxSize*1024*1024 { + return UploadFileTooLargeErr{} + } + return nil +} + +func CheckRepoNumOnceLimit(uploadFileNum int) error { + if uploadFileNum > setting.Repository.Upload.MaxFiles { + return UploadFileTooMuchErr{} + } + return nil +} + +func CountUploadFileSizeByIds(fileIds []string) (int64, error) { + if len(fileIds) == 0 { + return 0, nil + } + uploads, err := models.GetUploadsByUUIDs(fileIds) + if err != nil { + return 0, fmt.Errorf("CountUploadFileSizeByIds error [uuids: %v]: %v", fileIds, err) + } + var totalSize int64 + for _, upload := range uploads { + size, err := GetUploadFileSize(upload) + if err != nil { + return 0, err + } + totalSize += size + } + return totalSize, nil +} + +func GetUploadFileSize(upload *models.Upload) (int64, error) { + info, err := os.Lstat(upload.LocalPath()) + + if err != nil { + return 0, err + } + return info.Size(), nil + +} + +type RepoTooLargeErr struct { +} + +func (RepoTooLargeErr) Error() string { + return fmt.Sprintf("Repository can not exceed %d MB. Please remove some unnecessary files and try again", setting.Repository.RepoMaxSize) +} + +func IsRepoTooLargeErr(err error) bool { + _, ok := err.(RepoTooLargeErr) + return ok +} + +type UploadFileTooLargeErr struct { +} + +func (UploadFileTooLargeErr) Error() string { + return fmt.Sprintf("Upload files can not exceed %d MB at a time", setting.Repository.Upload.TotalMaxSize) +} + +func IsUploadFileTooLargeErr(err error) bool { + _, ok := err.(UploadFileTooLargeErr) + return ok +} + +type RepoFileTooLargeErr struct { +} + +func (RepoFileTooLargeErr) Error() string { + return "repository file is too large" +} + +func IsRepoFileTooLargeErr(err error) bool { + _, ok := err.(RepoFileTooLargeErr) + return ok +} + +type UploadFileTooMuchErr struct { +} + +func (UploadFileTooMuchErr) Error() string { + return "upload files are too lmuch" +} + +func IsUploadFileTooMuchErr(err error) bool { + _, ok := err.(UploadFileTooMuchErr) + return ok +} + +type UploadFileInvalidErr struct { +} + +func (UploadFileInvalidErr) Error() string { + return "upload files are invalid" +} + +func IsUploadFileInvalidErr(err error) bool { + _, ok := err.(UploadFileInvalidErr) + return ok +} diff --git a/templates/admin/cloudbrain/list.tmpl b/templates/admin/cloudbrain/list.tmpl index 02f100e46..6fea2eef7 100755 --- a/templates/admin/cloudbrain/list.tmpl +++ b/templates/admin/cloudbrain/list.tmpl @@ -155,7 +155,7 @@ {{else}} - + {{$.i18n.Tr "repo.stop"}} {{end}} diff --git a/templates/custom/select_dataset.tmpl b/templates/custom/select_dataset.tmpl index dc5ca6c9e..273477dd2 100644 --- a/templates/custom/select_dataset.tmpl +++ b/templates/custom/select_dataset.tmpl @@ -135,4 +135,4 @@ - \ No newline at end of file + diff --git a/templates/custom/select_dataset_train.tmpl b/templates/custom/select_dataset_train.tmpl new file mode 100644 index 000000000..2771200b6 --- /dev/null +++ b/templates/custom/select_dataset_train.tmpl @@ -0,0 +1,134 @@ + + +
+     + + + {{.i18n.Tr "dataset.select_dataset"}} + +
+ + +
+ + + +
+
+
${dataset.Repo.OwnerName}/${dataset.Repo.Alias} ${dataset.Name}
+
+ + + + ${dataset.Description} +
+
+
+ + + + 解压中 + + + + 解压失败 + +
+
+ + +
+ +
+
+
${dataset.Repo.OwnerName}/${dataset.Repo.Alias}${dataset.Name}
+
+ + + + ${dataset.Description} +
+
+
+ + + + 解压中 + + + + 解压失败 + +
+
+ +
+ +
+
+
${dataset.Repo.OwnerName}/${dataset.Repo.Alias}${dataset.Name}
+
+ + + + ${dataset.Description} +
+
+
+ + + + 解压中 + + + + 解压失败 + +
+
+ +
+ +
+
+
${dataset.Repo.OwnerName}/${dataset.Repo.Alias}${dataset.Name}
+
+ + + + ${dataset.Description} +
+
+
+ + + + 解压中 + + + + 解压失败 + +
+
+ +
+
+
+ + +
+
+ + +
diff --git a/templates/explore/datasets.tmpl b/templates/explore/datasets.tmpl index ff18a3da3..a1692cf35 100644 --- a/templates/explore/datasets.tmpl +++ b/templates/explore/datasets.tmpl @@ -121,10 +121,10 @@ @@ -184,9 +184,15 @@
+ {{if eq .UserID 0}} + {{else}} + + + + {{end}} 创建于:{{TimeSinceUnix1 .CreatedUnix}}
@@ -215,4 +221,4 @@ -{{template "base/footer" .}} +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/org/navber.tmpl b/templates/org/navber.tmpl index f7e88c1e9..772d9f67e 100755 --- a/templates/org/navber.tmpl +++ b/templates/org/navber.tmpl @@ -42,18 +42,15 @@ .active{ color:#0366D6 !important; } - .mleft{ - margin-left: 30% !important; - } .mbom{ margin-bottom: 10px !important; }
-