修改开发容器保存功能,优化前端界面,增加转圈定时状态检测
Some checks are pending
DevStar Studio Auto Test Pipeline / unit-frontend-test (pull_request) Waiting to run
DevStar Studio Auto Test Pipeline / unit-backend-test (pull_request) Waiting to run
DevStar Studio CI/CD Pipeline / build-and-push-x86-64-docker-image (pull_request) Waiting to run
DevStar E2E Test / e2e-test (pull_request) Waiting to run
Some checks are pending
DevStar Studio Auto Test Pipeline / unit-frontend-test (pull_request) Waiting to run
DevStar Studio Auto Test Pipeline / unit-backend-test (pull_request) Waiting to run
DevStar Studio CI/CD Pipeline / build-and-push-x86-64-docker-image (pull_request) Waiting to run
DevStar E2E Test / e2e-test (pull_request) Waiting to run
This commit is contained in:
@@ -3906,6 +3906,8 @@ management = Secrets Management
|
||||
|
||||
[devcontainer]
|
||||
manage = Container Management
|
||||
registry_username = Registry Username
|
||||
registry_password = Registry Password
|
||||
scripts = ShellScripts
|
||||
scripts.management = ShellScripts Management
|
||||
scripts.creation = Add ShellScript
|
||||
|
||||
@@ -3897,6 +3897,8 @@ management=密钥管理
|
||||
|
||||
[devcontainer]
|
||||
manage=容器管理
|
||||
registry_username = 镜像仓库用户名
|
||||
registry_password = 镜像仓库密码
|
||||
scripts=脚本管理
|
||||
scripts.management=脚本管理
|
||||
scripts.creation=添加脚本
|
||||
|
||||
@@ -73,9 +73,15 @@ func GetDevContainerDetails(ctx *context.Context) {
|
||||
imageName := configurationModel.Image
|
||||
registry, namespace, repo, tag := devcontainer_service.ParseImageName(imageName)
|
||||
log.Info("%v %v", repo, tag)
|
||||
// 保留这些数据用于向后兼容,但不再在表单中使用
|
||||
ctx.Data["RepositoryAddress"] = registry
|
||||
ctx.Data["RepositoryUsername"] = namespace
|
||||
ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest"
|
||||
// 默认 ImageName 使用完整路径格式
|
||||
if namespace != "" {
|
||||
ctx.Data["ImageName"] = registry + "/" + namespace + "/dev-" + ctx.Repo.Repository.Name + ":latest"
|
||||
} else {
|
||||
ctx.Data["ImageName"] = registry + "/dev-" + ctx.Repo.Repository.Name + ":latest"
|
||||
}
|
||||
}
|
||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||
// 获取WebSSH服务端口
|
||||
|
||||
@@ -710,6 +710,8 @@ func UpdateDevContainer(ctx context.Context, doer *user.User, repo *gitea_contex
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 保存原始状态,以便出错时回滚
|
||||
originalStatus := devContainerInfo.DevcontainerStatus
|
||||
_, err = dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.ID).
|
||||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 5})
|
||||
@@ -721,15 +723,22 @@ func UpdateDevContainer(ctx context.Context, doer *user.User, repo *gitea_contex
|
||||
//k8s的逻辑
|
||||
} else {
|
||||
updateErr := UpdateDevContainerByDocker(otherCtx, &devContainerInfo, updateInfo, repo, doer)
|
||||
if updateErr != nil {
|
||||
// 出错时回滚状态
|
||||
_, rollbackErr := dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.ID).
|
||||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: originalStatus})
|
||||
if rollbackErr != nil {
|
||||
log.Error("Failed to rollback devcontainer status: %v", rollbackErr)
|
||||
}
|
||||
return updateErr
|
||||
}
|
||||
_, err = dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.ID).
|
||||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/*
|
||||
* Copyright (c) Mengning Software. 2025. All rights reserved.
|
||||
* Authors: DevStar Team
|
||||
* Create: 2025-12-19
|
||||
* Description: DevContainer Management - Type Definitions.
|
||||
*/
|
||||
|
||||
package devcontainer
|
||||
|
||||
import (
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func IsAdmin(ctx context.Context, doer *user.User, repoID int64) (bool, error) {
|
||||
func IsAdmin(ctx context.Context, doer *user_model.User, repoID int64) (bool, error) {
|
||||
if doer.IsAdmin {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -404,7 +404,27 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai
|
||||
}
|
||||
defer cli.Close()
|
||||
// update容器
|
||||
imageRef := updateInfo.RepositoryAddress + "/" + updateInfo.RepositoryUsername + "/" + updateInfo.ImageName
|
||||
// ImageName 应该是完整路径,例如:devstar.cn/devstar/image-name:tag 或 docker.io/username/image:tag
|
||||
// 直接使用 ImageName 作为 imageRef
|
||||
imageRef := updateInfo.ImageName
|
||||
|
||||
// 从 ImageName 解析 registry 用于登录
|
||||
// 格式:registry/namespace/image:tag 或 registry/image:tag
|
||||
var registryUrl string
|
||||
if strings.Contains(updateInfo.ImageName, "/") {
|
||||
parts := strings.Split(updateInfo.ImageName, "/")
|
||||
firstPart := parts[0]
|
||||
// 如果第一部分包含点或端口,认为是registry地址
|
||||
if strings.Contains(firstPart, ".") || strings.Contains(firstPart, ":") || strings.EqualFold(firstPart, "localhost") {
|
||||
registryUrl = firstPart
|
||||
} else {
|
||||
// 否则默认使用 docker.io
|
||||
registryUrl = "docker.io"
|
||||
}
|
||||
} else {
|
||||
// 没有斜杠,默认使用 docker.io
|
||||
registryUrl = "docker.io"
|
||||
}
|
||||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -497,7 +517,7 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = docker_module.PushImage(dockerHost, updateInfo.RepositoryUsername, updateInfo.PassWord, updateInfo.RepositoryAddress, imageRef)
|
||||
err = docker_module.PushImage(dockerHost, updateInfo.RepositoryUsername, updateInfo.PassWord, registryUrl, imageRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -264,10 +264,8 @@ function getStatus() {
|
||||
}
|
||||
// 容器运行中时继续轮询,以检测状态变化(如被停止、重启等)
|
||||
}else if (data.status == '5') {
|
||||
concealElement();
|
||||
if (loadingElement) {
|
||||
loadingElement.style.display = 'block';
|
||||
}
|
||||
// 状态5:正在保存镜像,不影响容器运行,什么都不改变
|
||||
// 保存镜像只是构建/提交镜像、推送到Registry、更新配置,不影响正在运行的容器
|
||||
}else if (data.status == '6') {
|
||||
concealElement();
|
||||
if (deleteContainer){
|
||||
@@ -399,13 +397,12 @@ document.getElementById('customAlert').addEventListener('click', function(e) {
|
||||
// 使用共享的保存模态框功能
|
||||
document.getElementById('updateSubmitButton').addEventListener('click', function() {
|
||||
const targetUrl = '{{.Repository.Link}}/devcontainer/update';
|
||||
submitSaveForm(targetUrl, function(data) {
|
||||
const statusUrl = '{{.Repository.Link}}/devcontainer/status';
|
||||
submitSaveForm(targetUrl, statusUrl, function(data) {
|
||||
// 成功回调
|
||||
showCustomAlert(data.message);
|
||||
if(data.redirect){
|
||||
closeCustomAlert();
|
||||
}
|
||||
intervalID = setInterval(getStatus, 3000);
|
||||
// 不需要重新启动状态检测,因为已经在运行
|
||||
// 只需要立即刷新一次状态
|
||||
getStatus();
|
||||
}, function(error) {
|
||||
// 错误回调
|
||||
console.error('Save failed:', error);
|
||||
|
||||
@@ -346,14 +346,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 打开模态框
|
||||
openSaveModal();
|
||||
// 打开模态框,会自动检查Dockerfile是否存在
|
||||
openSaveModal(repoLink);
|
||||
|
||||
// 绑定提交按钮事件(每次打开时重新绑定)
|
||||
const submitButton = document.getElementById('updateSubmitButton');
|
||||
submitButton.onclick = function() {
|
||||
const targetUrl = '{{AppSubUrl}}' + repoLink + '/devcontainer/update';
|
||||
submitSaveForm(targetUrl, function(data) {
|
||||
const statusUrl = '{{AppSubUrl}}' + repoLink + '/devcontainer/status';
|
||||
submitSaveForm(targetUrl, statusUrl, function(data) {
|
||||
// 成功后刷新列表
|
||||
refreshContainerList();
|
||||
}, function(error) {
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
<div class="header">
|
||||
保存为容器镜像
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="content" style="position: relative;">
|
||||
<form class="ui form tw-max-w-2xl tw-m-auto" id="updateForm">
|
||||
|
||||
<div class="required field ">
|
||||
<label for="RepositoryAddress">Registry:</label>
|
||||
<input style="border: 1px solid black;" type="text" id="RepositoryAddress" name="RepositoryAddress" placeholder="例如: docker.io" value="{{.RepositoryAddress}}">
|
||||
<label for="ImageName">Image Name:</label>
|
||||
<input style="border: 1px solid black;" type="text" id="ImageName" name="ImageName" placeholder="devstar.cn/devstar/image-name:tag" value="{{.ImageName}}">
|
||||
</div>
|
||||
<div class="required field ">
|
||||
<label for="RepositoryUsername">Registry Username:</label>
|
||||
<input style="border: 1px solid black;" type="text" id="RepositoryUsername" name="RepositoryUsername" placeholder="镜像仓库用户名" value="{{.RepositoryUsername}}">
|
||||
<label for="RepositoryUsername">{{ctx.Locale.Tr "devcontainer.registry_username"}}:</label>
|
||||
<input style="border: 1px solid black;" type="text" id="RepositoryUsername" name="RepositoryUsername" placeholder="{{ctx.Locale.Tr "devcontainer.registry_username"}}" value="{{.RepositoryUsername}}">
|
||||
</div>
|
||||
<div class="required field ">
|
||||
<label for="RepositoryPassword">Registry Password:</label>
|
||||
<label for="RepositoryPassword">{{ctx.Locale.Tr "devcontainer.registry_password"}}:</label>
|
||||
<div style="position: relative; display: inline-block; width: 100%;">
|
||||
<input style="border: 1px solid black; width: 100%; padding-right: 80px;"
|
||||
type="password"
|
||||
@@ -23,7 +23,7 @@
|
||||
name="RepositoryPassword"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
placeholder="镜像仓库密码">
|
||||
placeholder="{{ctx.Locale.Tr "devcontainer.registry_password"}}">
|
||||
<button type="button"
|
||||
style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
|
||||
background: none; border: none; cursor: pointer; color: #666;
|
||||
@@ -33,27 +33,31 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="required field ">
|
||||
<label for="ImageName">Image(name:tag):</label>
|
||||
<input style="border: 1px solid black;" type="text" id="ImageName" name="ImageName" placeholder="例如: myimage:latest" value="{{.ImageName}}">
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
{{if .HasDevContainerDockerfile}}
|
||||
<input type="checkbox" id="SaveMethod" name="SaveMethod" value="on" checked>
|
||||
<label for="SaveMethod">Build From Dockerfile{{if .DockerfilePath}}: {{.DockerfilePath}}{{end}}</label>
|
||||
<label for="SaveMethod">Build From Dockerfile{{if .DockerfilePath}}: <a id="dockerfileLinkStatic" href="{{.Repository.Link}}/_edit/{{.Repository.DefaultBranch | PathEscapeSegments}}/{{.DockerfilePath | PathEscapeSegments}}" target="_blank" style="color: #2185d0; text-decoration: underline;">{{.DockerfilePath}}</a>{{end}}<span id="dockerfilePathSpan" style="display: none;">: <a id="dockerfileLink" href="#" target="_blank" style="color: #2185d0; text-decoration: underline;"></a></span></label>
|
||||
{{else}}
|
||||
<input type="checkbox" id="SaveMethod" name="SaveMethod" value="on" checked>
|
||||
<label for="SaveMethod">Build From Dockerfile</label>
|
||||
<label for="SaveMethod">Build From Dockerfile<span id="dockerfilePathSpan" style="display: none;">: <a id="dockerfileLink" href="#" target="_blank" style="color: #2185d0; text-decoration: underline;"></a></span></label>
|
||||
{{end}}
|
||||
</div>
|
||||
<div style="color: #2185d0; font-size: 13px; margin-top: 12px; padding: 10px; background-color: #f0f8ff; border-left: 3px solid #2185d0; border-radius: 3px;">
|
||||
<strong>💡 提示:</strong>建议优先使用 <strong>Build From Dockerfile</strong> 方式保存镜像。从容器直接保存镜像不透明且存储臃肿,而通过Dockerfile构建可以确保镜像的可重现性和最小化。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="ui primary button" type="button" id="updateSubmitButton">提交</button>
|
||||
<button class="ui cancel button" type="button" id="updateCloseButton">取消</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<!-- Loading 遮罩层(初始隐藏) -->
|
||||
<div id="saveLoadingOverlay" class="save-loading-overlay" style="display: none;">
|
||||
<div class="save-loading"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -77,27 +81,34 @@
|
||||
};
|
||||
|
||||
// 提交保存表单
|
||||
window.submitSaveForm = function(targetUrl, onSuccess, onError) {
|
||||
// targetUrl: 保存请求的URL
|
||||
// statusUrl: 状态检查URL(用于轮询)
|
||||
// onSuccess: 成功回调
|
||||
// onError: 错误回调
|
||||
window.submitSaveForm = function(targetUrl, statusUrl, onSuccess, onError) {
|
||||
const form = document.getElementById('updateForm');
|
||||
const formData = new FormData(form);
|
||||
const submitButton = document.getElementById('updateSubmitButton');
|
||||
const modal = document.getElementById('updatemodal');
|
||||
const cancelButton = document.getElementById('updateCloseButton');
|
||||
const loadingOverlay = document.getElementById('saveLoadingOverlay');
|
||||
|
||||
const RepositoryAddress = formData.get('RepositoryAddress');
|
||||
const RepositoryUsername = formData.get('RepositoryUsername');
|
||||
const RepositoryPassword = formData.get('RepositoryPassword');
|
||||
const SaveMethod = formData.get('SaveMethod');
|
||||
const ImageName = formData.get('ImageName');
|
||||
|
||||
// 验证必填字段
|
||||
if (!ImageName || !RepositoryAddress || !RepositoryUsername || !RepositoryPassword) {
|
||||
if (!ImageName || !RepositoryUsername || !RepositoryPassword) {
|
||||
alert('请填写所有必填字段');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
// 显示加载状态(遮罩层覆盖整个表单)
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.style.display = 'flex';
|
||||
}
|
||||
submitButton.disabled = true;
|
||||
$('#updatemodal').addClass('loading');
|
||||
cancelButton.disabled = true;
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.content ||
|
||||
(window.config && window.config.csrfToken);
|
||||
@@ -110,7 +121,6 @@
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
RepositoryAddress: RepositoryAddress,
|
||||
RepositoryUsername: RepositoryUsername,
|
||||
RepositoryPassword: RepositoryPassword,
|
||||
SaveMethod: SaveMethod || '',
|
||||
@@ -119,43 +129,171 @@
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(data) {
|
||||
submitButton.disabled = false;
|
||||
$('#updatemodal').removeClass('loading');
|
||||
|
||||
if (data.message) {
|
||||
alert(data.message);
|
||||
// 检查是否有错误
|
||||
if (data.message && data.message !== '成功' && !data.redirect) {
|
||||
// 立即返回错误
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.style.display = 'none';
|
||||
}
|
||||
submitButton.disabled = false;
|
||||
cancelButton.disabled = false;
|
||||
alert('保存失败:' + data.message);
|
||||
if (onError) {
|
||||
onError(data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.message === '成功' || data.redirect) {
|
||||
// 关闭模态框
|
||||
closeSaveModal();
|
||||
// 调用成功回调
|
||||
// 提交成功,开始轮询状态
|
||||
if (statusUrl) {
|
||||
startStatusPolling(statusUrl, onSuccess, onError, submitButton, cancelButton, loadingOverlay);
|
||||
} else {
|
||||
// 没有状态URL,直接显示成功
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.style.display = 'none';
|
||||
}
|
||||
submitButton.disabled = false;
|
||||
cancelButton.disabled = false;
|
||||
alert('保存成功!');
|
||||
if (onSuccess) {
|
||||
closeSaveModal();
|
||||
onSuccess(data);
|
||||
}
|
||||
} else if (onError) {
|
||||
onError(data);
|
||||
}
|
||||
}).catch(function(error) {
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.style.display = 'none';
|
||||
}
|
||||
submitButton.disabled = false;
|
||||
$('#updatemodal').removeClass('loading');
|
||||
cancelButton.disabled = false;
|
||||
console.error('Save error:', error);
|
||||
alert('保存容器失败');
|
||||
alert('保存失败:' + (error.message || '未知错误'));
|
||||
if (onError) {
|
||||
onError(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 轮询状态
|
||||
function startStatusPolling(statusUrl, onSuccess, onError, submitButton, cancelButton, loadingOverlay) {
|
||||
let pollCount = 0;
|
||||
const maxPolls = 300; // 最多轮询5分钟
|
||||
|
||||
const pollInterval = setInterval(function() {
|
||||
pollCount++;
|
||||
|
||||
// 超时检查
|
||||
if (pollCount > maxPolls) {
|
||||
clearInterval(pollInterval);
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.style.display = 'none';
|
||||
}
|
||||
submitButton.disabled = false;
|
||||
cancelButton.disabled = false;
|
||||
alert('操作超时,请检查容器状态');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(statusUrl)
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
const status = String(data.status || '');
|
||||
|
||||
if (status === '5') {
|
||||
// 正在保存,继续转圈
|
||||
return;
|
||||
} else if (status === '4') {
|
||||
// 完成
|
||||
clearInterval(pollInterval);
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.style.display = 'none';
|
||||
}
|
||||
submitButton.disabled = false;
|
||||
cancelButton.disabled = false;
|
||||
alert('保存成功!镜像已推送到Registry');
|
||||
if (onSuccess) {
|
||||
closeSaveModal();
|
||||
onSuccess({message: '成功'});
|
||||
}
|
||||
} else {
|
||||
// 其他状态,可能是错误
|
||||
clearInterval(pollInterval);
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.style.display = 'none';
|
||||
}
|
||||
submitButton.disabled = false;
|
||||
cancelButton.disabled = false;
|
||||
alert('保存失败:状态异常(' + status + ')');
|
||||
if (onError) {
|
||||
onError({message: '状态异常:' + status});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('Status poll error:', error);
|
||||
// 轮询错误不中断,继续尝试
|
||||
});
|
||||
}, 3000); // 每3秒轮询一次
|
||||
}
|
||||
|
||||
// 打开模态框
|
||||
window.openSaveModal = function() {
|
||||
window.openSaveModal = function(repoLink, dockerfilePath) {
|
||||
// 如果提供了repoLink,尝试检查Dockerfile是否存在(管理页面场景)
|
||||
if (repoLink) {
|
||||
const dockerfileLink = document.getElementById('dockerfileLink');
|
||||
const dockerfileLinkStatic = document.getElementById('dockerfileLinkStatic');
|
||||
const dockerfilePathSpan = document.getElementById('dockerfilePathSpan');
|
||||
|
||||
// 如果已经有静态链接(详情页场景),不需要动态添加
|
||||
if (dockerfileLinkStatic) {
|
||||
// 详情页已经有链接,隐藏动态span
|
||||
if (dockerfilePathSpan) {
|
||||
dockerfilePathSpan.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// 管理页面场景,需要动态检查并添加链接
|
||||
// 先隐藏Dockerfile链接
|
||||
if (dockerfilePathSpan) {
|
||||
dockerfilePathSpan.style.display = 'none';
|
||||
}
|
||||
|
||||
// 如果提供了dockerfilePath,直接使用;否则尝试检查默认路径
|
||||
const pathToCheck = dockerfilePath || '.devcontainer/Dockerfile';
|
||||
const defaultBranch = 'main'; // 可以后续优化为从API获取
|
||||
|
||||
// 使用HEAD请求检查文件是否存在(更轻量)
|
||||
fetch(repoLink + '/raw/branch/' + defaultBranch + '/' + encodeURIComponent(pathToCheck), {
|
||||
method: 'HEAD'
|
||||
}).then(function(response) {
|
||||
// 只有文件存在(200 OK)且找到了元素,才显示链接
|
||||
if (response.ok && response.status === 200 && dockerfileLink && dockerfilePathSpan) {
|
||||
// 文件存在,显示链接
|
||||
const editUrl = repoLink + '/_edit/' + encodeURIComponent(defaultBranch) + '/' + encodeURIComponent(pathToCheck);
|
||||
dockerfileLink.href = editUrl;
|
||||
dockerfileLink.textContent = pathToCheck;
|
||||
dockerfilePathSpan.style.display = 'inline';
|
||||
}
|
||||
// 如果文件不存在(404),保持隐藏状态
|
||||
}).catch(function(error) {
|
||||
// 检查失败,不显示链接
|
||||
console.log('Dockerfile check failed:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
$('#updatemodal').modal('show');
|
||||
};
|
||||
|
||||
// 关闭模态框
|
||||
window.closeSaveModal = function() {
|
||||
$('#updatemodal').modal('hide');
|
||||
document.getElementById('updateForm').reset();
|
||||
const form = document.getElementById('updateForm');
|
||||
const loadingOverlay = document.getElementById('saveLoadingOverlay');
|
||||
form.reset();
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭按钮事件
|
||||
@@ -166,6 +304,38 @@
|
||||
closeSaveModal();
|
||||
});
|
||||
}
|
||||
})();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.save-loading-overlay{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.save-loading{
|
||||
width:60px;
|
||||
height:60px;
|
||||
border-radius:150px;
|
||||
border:8px solid #f3f3f3;
|
||||
border-top-color:rgba(0,0,0,0.3);
|
||||
box-sizing:border-box;
|
||||
animation:save-loading 1.2s linear infinite;
|
||||
-webkit-animation:save-loading 1.2s linear infinite;
|
||||
}
|
||||
@keyframes save-loading{
|
||||
0%{transform:rotate(0deg)}
|
||||
100%{transform:rotate(360deg)}
|
||||
}
|
||||
@-webkit-keyframes save-loading{
|
||||
0%{-webkit-transform:rotate(0deg)}
|
||||
100%{-webkit-transform:rotate(360deg)}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user