fix docker 控制台页面布局优化,支持单独查看 docker-compose

This commit is contained in:
bwcx_jzy 2023-04-03 15:17:53 +08:00
parent 16b25431f7
commit 6d1853cdbf
No known key found for this signature in database
GPG Key ID: 5E48E9372088B9E5
29 changed files with 1191 additions and 693 deletions

View File

@ -6,6 +6,8 @@
1. 【server】修复 资产管理机器管理当个分配工作空间无法正常使用(感谢@咻咻咻秀啊)
2. 【server】修复 资产管理相关权限、操作日志无法记录问题(感谢@咻咻咻秀啊)
3. 【server】修复 docker 控制台 、日志无法正常使用
4. 【server】优化 docker 控制台页面布局优化,支持单独查看 docker-compose
------

View File

@ -11,45 +11,47 @@
5. ~~docker 集群~~
6. ~~ssh 监控~~
7. 仓库管理(待定,可以和凭证管理一起考虑)
3. scp 发布实践案例
4. netty-agent
5. 凭证管理
6. 升级 JDK 11 或者 17
7. 端口监控、监控报警、机器监控、ssh 监控报警
8. nginx 流量切换
9. acme.sh ui
10. 执行审计
11. 执行部分命令耗时和直接执行相差太大
12. ~~SSH 上传文件进度(前端分片+进度)~~
13. ~~**用户体系支持接入第三方系统**~~
14. 监控通知模块优化支持更多(飞书)
15. ~~传输信息加密(混淆,避免 http 明文传输)~~
16. 非 root 用户提升权限写入 root 用户文件
17. 部分数据迁移工作空间(项目,构建,仓库、节点分发)
18. ~~插件端证书验证迁移到服务端~~
19. 前端表格用户自定义列显示
20. 导入云效仓库
21. 清理触发器表
22. ~~稳定版/体验版~~
23. ~~插件端自定义发布文件~~
24. 节点取消,白名单配置和下载白名单(统一到服务端工作空间配置)
25. 仓库、构建、分发、项目导入导出
26. 使用服务端的 git 插件
3. netty-agent
4. 凭证管理
5. 升级 JDK 11 或者 17
6. 端口监控、监控报警、机器监控、ssh 监控报警
7. nginx 流量切换
8. acme.sh ui
9. 执行审计
10. 执行部分命令耗时和直接执行相差太大
11. 监控通知模块优化支持更多(飞书)
12. 非 root 用户提升权限写入 root 用户文件
13. 部分数据迁移工作空间(项目,构建,仓库、节点分发)
14. 前端表格用户自定义列显示
15. 导入云效仓库
16. 清理触发器表
17. 节点取消,白名单配置和下载白名单(统一到服务端工作空间配置)
18. 仓库、构建、分发、项目导入导出
19. 使用服务端的 git 插件
20. SSH 连接 docker、docker-compose
21. 隧道节点
### DONE
1. ~~容器构建基础镜像的管理~~
2. ~~tomcat 实践案例~~
3. ~~**分片上传文件**~~
4. ~~**支持 mysql 数据库**~~
5. ~~配置文件优化~~
6. ~~项目触发器~~
7. ~~节点转发模块优化~~
8. ~~构建事件触发新增更多(前后)~~
9. ~~复制项目~~
10. ~~测命令行参数~~
11. ~~标签页缓存问题(定时器未清空)~~
12. ~~发布到指定目录~~
1. ~~scp 发布实践案例~~
2. ~~SSH 上传文件进度(前端分片+进度)~~
3. ~~**用户体系支持接入第三方系统**~~
4. ~~传输信息加密(混淆,避免 http 明文传输)~~
5. ~~插件端证书验证迁移到服务端~~
6. ~~稳定版/体验版~~
7. ~~插件端自定义发布文件~~
8. ~~容器构建基础镜像的管理~~
9. ~~tomcat 实践案例~~
10. ~~**分片上传文件**~~
11. ~~**支持 mysql 数据库**~~
12. ~~配置文件优化~~
13. ~~项目触发器~~
14. ~~节点转发模块优化~~
15. ~~构建事件触发新增更多(前后)~~
16. ~~复制项目~~
17. ~~测命令行参数~~
18. ~~标签页缓存问题(定时器未清空)~~
19. ~~发布到指定目录~~
## 2.8.x

View File

@ -90,6 +90,22 @@ public abstract class BaseDockerContainerController extends BaseDockerController
return JsonMessage.success("", listContainer);
}
/**
* @return json
*/
@PostMapping(value = "list-compose", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public JsonMessage<List<JSONObject>> listCompose(@ValidatorItem String id) throws Exception {
IPlugin plugin = PluginFactory.getPlugin(DockerInfoService.DOCKER_PLUGIN_NAME);
Map<String, Object> parameter = this.toDockerParameter(id);
parameter.put("name", getParameter("name"));
parameter.put("containerId", getParameter("containerId"));
parameter.put("imageId", getParameter("imageId"));
parameter.put("showAll", getParameter("showAll"));
List<JSONObject> listContainer = (List<JSONObject>) plugin.execute("listComposeContainer", parameter);
return JsonMessage.success("", listContainer);
}
/**
* @return json
*/
@ -165,7 +181,7 @@ public abstract class BaseDockerContainerController extends BaseDockerController
*/
@GetMapping(value = "inspect-container", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.EXECUTE)
public JsonMessage<JSONObject> inspectContainer(@ValidatorItem String id, String containerId) throws Exception {
public JsonMessage<JSONObject> inspectContainer(@ValidatorItem String id, @ValidatorItem String containerId) throws Exception {
IPlugin plugin = PluginFactory.getPlugin(DockerInfoService.DOCKER_PLUGIN_NAME);
Map<String, Object> parameter = this.toDockerParameter(id);
parameter.put("containerId", containerId);

View File

@ -37,7 +37,6 @@ import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.*;
import cn.hutool.cron.task.Task;
import cn.hutool.db.Entity;
import cn.hutool.extra.ssh.JschRuntimeException;
import cn.hutool.extra.ssh.JschUtil;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
@ -372,10 +371,10 @@ public class MachineSshServer extends BaseDbService<MachineSshModel> implements
} else if (StrUtil.startWith(privateKey, JschUtils.HEADER)) {
// 直接采用 private key content 登录无需写入文件
session = JschUtils.createSession(sshModel.getHost(),
sshModel.getPort(),
user,
StrUtil.trim(privateKey),
passwordByte);
sshModel.getPort(),
user,
StrUtil.trim(privateKey),
passwordByte);
} else if (StrUtil.isEmpty(privateKey)) {
File home = FileUtil.getUserHomeDir();
Assert.notNull(home, "用户目录没有找到");
@ -398,7 +397,7 @@ public class MachineSshServer extends BaseDbService<MachineSshModel> implements
// 简要私钥文件是否存在
Assert.state(FileUtil.isFile(rsaFile), "私钥文件不存在:" + FileUtil.getAbsolutePath(rsaFile));
session = JschUtil.createSession(sshModel.getHost(),
sshModel.getPort(), user, FileUtil.getAbsolutePath(rsaFile), passwordByte);
sshModel.getPort(), user, FileUtil.getAbsolutePath(rsaFile), passwordByte);
}
try {
session.setServerAliveInterval(timeout);
@ -409,7 +408,7 @@ public class MachineSshServer extends BaseDbService<MachineSshModel> implements
try {
session.connect(timeout);
} catch (JSchException e) {
throw new JschRuntimeException(e);
throw Lombok.sneakyThrow(e);
}
} else {
throw new IllegalArgumentException("不支持的模式");

View File

@ -30,7 +30,6 @@ import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.ssh.ChannelType;
import cn.hutool.extra.ssh.JschRuntimeException;
import cn.hutool.extra.ssh.JschUtil;
import cn.hutool.extra.ssh.Sftp;
import com.jcraft.jsch.*;
@ -209,7 +208,7 @@ public class JschUtils {
Identity identity = ContentIdentity.newInstance(privateKeyByte, null, sshUser, jsch);
jsch.addIdentity(identity, passphrase);
} catch (Exception e) {
throw new JschRuntimeException(e);
throw Lombok.sneakyThrow(e);
}
return JschUtil.createSession(jsch, sshHost, sshPort, sshUser);
}

View File

@ -22,6 +22,7 @@
*/
package org.dromara.jpom;
import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.exceptions.InvocationTargetRuntimeException;
@ -131,60 +132,60 @@ public class DefaultDockerPluginImpl implements IDockerConfigPlugin {
UpdateContainerCmd updateContainerCmd = dockerClient.updateContainerCmd(containerId);
//
Optional.ofNullable(parameter.get("cpusetCpus"))
.map(StrUtil::toStringOrNull)
.ifPresent(updateContainerCmd::withCpusetCpus);
.map(StrUtil::toStringOrNull)
.ifPresent(updateContainerCmd::withCpusetCpus);
Optional.ofNullable(parameter.get("cpusetMems"))
.map(StrUtil::toStringOrNull)
.ifPresent(updateContainerCmd::withCpusetMems);
.map(StrUtil::toStringOrNull)
.ifPresent(updateContainerCmd::withCpusetMems);
Optional.ofNullable(parameter.get("cpuPeriod"))
.map(Convert::toInt)
.ifPresent(updateContainerCmd::withCpuPeriod);
.map(Convert::toInt)
.ifPresent(updateContainerCmd::withCpuPeriod);
Optional.ofNullable(parameter.get("cpuQuota"))
.map(Convert::toInt)
.ifPresent(updateContainerCmd::withCpuQuota);
.map(Convert::toInt)
.ifPresent(updateContainerCmd::withCpuQuota);
Optional.ofNullable(parameter.get("cpuShares"))
.map(Convert::toInt)
.ifPresent(updateContainerCmd::withCpuShares);
.map(Convert::toInt)
.ifPresent(updateContainerCmd::withCpuShares);
Optional.ofNullable(parameter.get("blkioWeight"))
.map(Convert::toInt)
.ifPresent(updateContainerCmd::withBlkioWeight);
.map(Convert::toInt)
.ifPresent(updateContainerCmd::withBlkioWeight);
Optional.ofNullable(parameter.get("memoryReservation"))
.map(StrUtil::toStringOrNull)
.map(s -> {
if (StrUtil.isEmpty(s)) {
return null;
}
return DataSizeUtil.parse(s);
})
.ifPresent(updateContainerCmd::withMemoryReservation);
.map(StrUtil::toStringOrNull)
.map(s -> {
if (StrUtil.isEmpty(s)) {
return null;
}
return DataSizeUtil.parse(s);
})
.ifPresent(updateContainerCmd::withMemoryReservation);
Optional.ofNullable(parameter.get("memory"))
.map(StrUtil::toStringOrNull)
.map(s -> {
if (StrUtil.isEmpty(s)) {
return null;
}
return DataSizeUtil.parse(s);
})
.ifPresent(updateContainerCmd::withMemory);
.map(StrUtil::toStringOrNull)
.map(s -> {
if (StrUtil.isEmpty(s)) {
return null;
}
return DataSizeUtil.parse(s);
})
.ifPresent(updateContainerCmd::withMemory);
// updateContainerCmd.withKernelMemory(DataSizeUtil.parse("10M"));
Optional.ofNullable(parameter.get("memorySwap"))
.map(StrUtil::toStringOrNull)
.map(s -> {
if (StrUtil.isEmpty(s)) {
return null;
}
return DataSizeUtil.parse(s);
})
.ifPresent(updateContainerCmd::withMemorySwap);
.map(StrUtil::toStringOrNull)
.map(s -> {
if (StrUtil.isEmpty(s)) {
return null;
}
return DataSizeUtil.parse(s);
})
.ifPresent(updateContainerCmd::withMemorySwap);
UpdateContainerResponse updateContainerResponse = updateContainerCmd.exec();
return DockerUtil.toJSON(updateContainerResponse);
@ -230,8 +231,8 @@ public class DefaultDockerPluginImpl implements IDockerConfigPlugin {
tag = StrUtil.emptyToDefault(tag, "latest");
logConsumer.accept(StrUtil.format("start pull {}:{}", repository, tag));
PullImageCmd pullImageCmd = dockerClient.pullImageCmd(repository)
.withTag(tag)
.withAuthConfig(dockerClient.authConfig());
.withTag(tag)
.withAuthConfig(dockerClient.authConfig());
pullImageCmd.exec(new InvocationBuilder.AsyncResultCallback<PullResponseItem>() {
@Override
public void onNext(PullResponseItem object) {
@ -263,33 +264,33 @@ public class DefaultDockerPluginImpl implements IDockerConfigPlugin {
CreateContainerCmd containerCmd = dockerClient.createContainerCmd(imageId);
containerCmd.withName(name);
Opt.ofBlankAble(labels)
.map(s -> UrlQuery.of(s, CharsetUtil.CHARSET_UTF_8))
.map(UrlQuery::getQueryMap)
.map((Function<Map<CharSequence, CharSequence>, Map<String, String>>) map -> {
HashMap<String, String> labelMap = MapUtil.newHashMap();
for (Map.Entry<CharSequence, CharSequence> entry : map.entrySet()) {
labelMap.put(StrUtil.toString(entry.getKey()), StrUtil.toString(entry.getValue()));
}
return labelMap;
})
.ifPresent(containerCmd::withLabels);
.map(s -> UrlQuery.of(s, CharsetUtil.CHARSET_UTF_8))
.map(UrlQuery::getQueryMap)
.map((Function<Map<CharSequence, CharSequence>, Map<String, String>>) map -> {
HashMap<String, String> labelMap = MapUtil.newHashMap();
for (Map.Entry<CharSequence, CharSequence> entry : map.entrySet()) {
labelMap.put(StrUtil.toString(entry.getKey()), StrUtil.toString(entry.getValue()));
}
return labelMap;
})
.ifPresent(containerCmd::withLabels);
HostConfig hostConfig = HostConfig.newHostConfig();
Opt.ofBlankAble(runtime).ifPresent(hostConfig::withRuntime);
List<ExposedPort> exposedPortList = new ArrayList<>();
if (StrUtil.isNotEmpty(exposedPorts)) {
List<PortBinding> portBindings = StrUtil.splitTrim(exposedPorts, StrUtil.COMMA)
.stream()
.map(PortBinding::parse)
.peek(portBinding -> exposedPortList.add(portBinding.getExposedPort()))
.collect(Collectors.toList());
.stream()
.map(PortBinding::parse)
.peek(portBinding -> exposedPortList.add(portBinding.getExposedPort()))
.collect(Collectors.toList());
hostConfig.withPortBindings(portBindings);
}
if (StrUtil.isNotEmpty(volumes)) {
List<Bind> binds = StrUtil.splitTrim(volumes, StrUtil.COMMA)
.stream()
.map(Bind::parse)
.collect(Collectors.toList());
.stream()
.map(Bind::parse)
.collect(Collectors.toList());
hostConfig.withBinds(binds);
}
Opt.ofBlankAble(networkMode).ifPresent(hostConfig::withNetworkMode);
@ -298,9 +299,9 @@ public class DefaultDockerPluginImpl implements IDockerConfigPlugin {
// 环境变量
if (env != null) {
List<String> envList = env.entrySet()
.stream()
.map(entry -> StrUtil.format("{}={}", entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
.stream()
.map(entry -> StrUtil.format("{}={}", entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
containerCmd.withEnv(envList);
}
Optional.ofNullable(storageOpt).map(map -> {
@ -315,8 +316,8 @@ public class DefaultDockerPluginImpl implements IDockerConfigPlugin {
List<String> commands = (List<String>) parameter.get("commands");
Optional.ofNullable(commands).ifPresent(strings -> {
List<String> list = strings.stream()
.filter(StrUtil::isNotEmpty)
.collect(Collectors.toList());
.filter(StrUtil::isNotEmpty)
.collect(Collectors.toList());
if (CollUtil.isNotEmpty(list)) {
containerCmd.withCmd(list);
}
@ -385,18 +386,18 @@ public class DefaultDockerPluginImpl implements IDockerConfigPlugin {
BuildImageCmd buildImageCmd = dockerClient.buildImageCmd();
buildImageCmd
.withBaseDirectory(baseDirectory)
.withDockerfile(dockerfile)
.withBuildAuthConfigs(authConfigurations)
.withTags(CollUtil.newHashSet(StrUtil.splitTrim(tags, StrUtil.COMMA)));
.withBaseDirectory(baseDirectory)
.withDockerfile(dockerfile)
.withBuildAuthConfigs(authConfigurations)
.withTags(CollUtil.newHashSet(StrUtil.splitTrim(tags, StrUtil.COMMA)));
// 添加构建参数
UrlQuery query = UrlQuery.of(buildArgs, CharsetUtil.CHARSET_UTF_8);
query.getQueryMap()
.forEach((key, value) -> {
String valueStr = StrUtil.toString(value);
valueStr = StringUtil.formatStrByMap(valueStr, env);
buildImageCmd.withBuildArg(StrUtil.toString(key), valueStr);
});
.forEach((key, value) -> {
String valueStr = StrUtil.toString(value);
valueStr = StringUtil.formatStrByMap(valueStr, env);
buildImageCmd.withBuildArg(StrUtil.toString(key), valueStr);
});
// 标签
UrlQuery labelsQuery = UrlQuery.of(labels, CharsetUtil.CHARSET_UTF_8);
HashMap<String, String> labelMap = MapUtil.newHashMap();
@ -523,8 +524,25 @@ public class DefaultDockerPluginImpl implements IDockerConfigPlugin {
dockerClient.removeImageCmd(imageId).withForce(true).exec();
}
/**
* 不包含 docker compose
*
* @param parameter 参数
* @return list
*/
private List<JSONObject> listContainerCmd(Map<String, Object> parameter) {
List<JSONObject> list = this.listContainerByLabelCmd(parameter, null);
String composeLabel = "com.docker.compose.project";
return list.stream()
.filter(jsonObject -> {
JSONObject labels = jsonObject.getJSONObject("labels");
String project = MapUtil.get(labels, composeLabel, String.class);
return project == null;
})
.collect(Collectors.toList());
}
private List<JSONObject> listContainerByLabelCmd(Map<String, Object> parameter, String label) {
DockerClient dockerClient = DockerUtil.get(parameter);
ListContainersCmd listContainersCmd = dockerClient.listContainersCmd();
@ -537,14 +555,51 @@ public class DefaultDockerPluginImpl implements IDockerConfigPlugin {
if (StrUtil.isNotEmpty(containerId)) {
listContainersCmd.withIdFilter(CollUtil.newArrayList(containerId));
}
// String imageId = (String) parameter.get("imageId");
// if (StrUtil.isNotEmpty(imageId)) {
// listContainersCmd.withFilter("image", CollUtil.newArrayList(imageId));
// }
Opt.ofBlankAble(label).ifPresent(s -> {
// 只筛选 docker compose
listContainersCmd.withLabelFilter(CollUtil.newArrayList(s));
});
String imageId = (String) parameter.get("imageId");
List<Container> exec = listContainersCmd.exec();
exec = ObjectUtil.defaultIfNull(exec, new ArrayList<>());
return exec.stream().map(DockerUtil::toJSON).collect(Collectors.toList());
return exec.stream()
.map(DockerUtil::toJSON)
.filter(jsonObject -> {
if (StrUtil.isEmpty(imageId)) {
return true;
}
String imageId1 = jsonObject.getString("imageId");
return StrUtil.contains(imageId1, imageId);
})
.collect(Collectors.toList());
}
/**
* 不包含 docker compose
*
* @param parameter 参数
* @return list
*/
private List<JSONObject> listComposeContainerCmd(Map<String, Object> parameter) {
String composeLabel = "com.docker.compose.project";
List<JSONObject> list = this.listContainerByLabelCmd(parameter, composeLabel);
//
Map<String, List<JSONObject>> map = CollStreamUtil.groupKeyValue(list, jsonObject -> {
JSONObject labels = jsonObject.getJSONObject("labels");
return Optional.ofNullable(labels)
.map(jsonObject1 -> jsonObject1.getString(composeLabel))
.orElse("null");
}, jsonObject -> jsonObject);
//
return map.entrySet().stream()
.map(stringListEntry -> {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", stringListEntry.getKey());
jsonObject.put("child", stringListEntry.getValue());
return jsonObject;
})
.collect(Collectors.toList());
}
private void restartContainerCmd(Map<String, Object> parameter) {

View File

@ -44,8 +44,8 @@
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.15.1",
"prettier": "^2.3.2",
"stylus": "^0.54.8",
"stylus-loader": "^3.0.2",
"less": "^3.0.4",
"less-loader": "^5.0.0",
"vue-template-compiler": "^2.6.14"
},
"eslintConfig": {

View File

@ -78,7 +78,7 @@ export default {
};
</script>
<style lang="stylus">
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
@ -89,12 +89,12 @@ export default {
}
.full-content {
min-height calc(100vh - 120px);
min-height: calc(100vh - 120px);
padding-bottom: 20px;
}
.node-full-content {
min-height calc(100vh - 130px) !important;
min-height: calc(100vh - 130px) !important;
}
.globalLoading {
@ -104,37 +104,15 @@ export default {
background-color: rgba(0, 0, 0, 0.7);
position: fixed !important;
z-index: 99999;
top:0;
bottom:0;
left:0;
right:0;
display:flex;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
}
.ant-spin-text{
.ant-spin-text {
text-shadow: 0 0 black !important;
}
</style>
<style>
.hide-scrollbar *::-webkit-scrollbar {
width: 0 !important;
display: none;
}
.hide-scrollbar * {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar pre::-webkit-scrollbar {
width: 0 !important;
display: none;
}
.hide-scrollbar pre {
-ms-overflow-style: none;
scrollbar-width: none;
}
.ant-space {
flex-wrap: wrap;
gap: 10px 0;

View File

@ -61,6 +61,21 @@ export function dockerContainerList(urlPrefix, params) {
});
}
/**
* 容器中的列表
* @param {JSON} params
*/
export function dockerContainerListCompose(urlPrefix, params) {
return axios({
url: urlPrefix + "/container/list-compose",
method: "post",
data: params,
headers: {
loading: "no",
},
});
}
/**
* 查看 docker info
* @param {JSON} params

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1 @@
<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" fill="#c71d23" r="16"/><path d="m24.0987698 14.2225144h-9.0863697c-.4362899.000207-.7900048.3538292-.790326.7901191l-.0005173 1.9752185c-.0003277.4363707.353328.7902117.7896987.790326.0000712 0 .0001424 0 .0002135-.0002135l5.5317648-.0000461c.4363708-.0000102.7901221.3537352.7901257.790106 0 .0000022 0 .0000044-.0000066.0000066v.1975077.1975318c0 1.3091122-1.0612451 2.3703573-2.3703573 2.3703573h-7.5067195c-.4363081-.0000218-.790009-.353713-.7900429-.7900211l-.0002069-7.5059917c-.0001014-1.3091122 1.0611145-2.3703865 2.3702267-2.3704226.0000217 0 .0000435 0 .0000653.0000653h11.0602463c.4361793-.0004902.7898484-.35394.7906091-.79011894l.0012251-1.97521881c.0007606-.43637034-.3527683-.79033806-.7891389-.79060871-.0001634-.0000001-.0003268-.00000015-.0004901.00048976h-11.0617654c-3.27278051 0-5.92589329 2.65311278-5.92589329 5.9258933v11.0612755c0 .4363707.35374837.7901191.7901191.7901191h11.65447149c2.9454379 0 5.3331872-2.3877493 5.3331872-5.3331872v-4.5430682c0-.4363707-.3537484-.7901191-.7901191-.7901191z" fill="#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,51 +0,0 @@
/* 全局样式,重置样式 */
html,
body {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
}
.ant-drawer-body {
padding: 10px;
}
.ant-table-wrapper {
overflow-y: auto;
overflow-x: auto;
}
.ant-table-body {
overflow-x: auto !important;
}
.search-input-item {
width: 140px;
/* margin-right: 10px; */
}
.filter {
margin-bottom: 10px;
}
/*
.ant-btn {
margin-right: 10px;
} */
.ant-btn-sm {
font-size: 12px;
}
pre {
margin: 0;
}
.ant-modal-confirm-body .ant-modal-confirm-content {
font-size: 16px;
}
.ant-modal-confirm-body .ant-modal-confirm-title {
font-size: 18px;
font-weight: bold;
}

View File

@ -0,0 +1,103 @@
/* 全局样式,重置样式 */
html,
body {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
}
.ant-drawer-body {
padding: 10px;
}
.ant-table-wrapper {
overflow-y: auto;
overflow-x: auto;
}
.ant-table-body {
overflow-x: auto !important;
}
.search-input-item {
width: 140px;
/* margin-right: 10px; */
}
.filter {
margin-bottom: 10px;
}
/*
.ant-btn {
margin-right: 10px;
} */
.ant-btn-sm {
font-size: 12px;
}
pre {
margin: 0;
}
.ant-modal-confirm-body .ant-modal-confirm-content {
font-size: 16px;
}
.ant-modal-confirm-body .ant-modal-confirm-title {
font-size: 18px;
font-weight: bold;
}
.hide-scrollbar *::-webkit-scrollbar {
width: 0 !important;
display: none;
}
.hide-scrollbar * {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar pre::-webkit-scrollbar {
width: 0 !important;
display: none;
}
.hide-scrollbar pre {
-ms-overflow-style: none;
scrollbar-width: none;
}
@color-border-last: #e5e5e5;
@color-neutral-last: #f7f8fa;
@scrollbar-size: 4px;
// 兼容火狐
* {
scrollbar-width: thin;
scrollbar-color: @color-border-last @color-neutral-last;
}
// 滚动条样式
.ant-table-body::-webkit-scrollbar {
width: @scrollbar-size;
// height: @scrollbar-size;
// width: 10px;
height: 10px;
// width: 2px;
border-radius: 2px;
background-color: transparent;
}
// 滚动条-活动按钮
.ant-table-body::-webkit-scrollbar-thumb {
background: @color-border-last;
border-radius: 2px;
box-shadow: inset 0 0 6px @color-border-last;
}
// 滚动条背景
.ant-table-body::-webkit-scrollbar-track {
background-color: @color-neutral-last;
border-radius: 2px;
box-shadow: inset 0 0 6px @color-neutral-last;
}

View File

@ -3,7 +3,7 @@ import App from "./App.vue";
import "./components/lazy_antd";
import "./assets/reset.css";
import "./assets/reset.less";
const introJs = require("intro.js");
import "intro.js/introjs.css";
// import 'intro.js/themes/introjs-flattener.css';

View File

@ -56,7 +56,7 @@
</a-col>
<a-col :span="7" style="text-align: right" class="text-overflow-hidden">
<a-tooltip :title="`当前状态:${statusMap[item.status]} ${item.statusMsg ? '状态消息:' + item.statusMsg : ''} `">
<a-tag :color="statusColor[item.status]" style="margin-right: 0px"> {{ statusMap[item.status] }}</a-tag>
<a-tag :color="statusColor[item.status]" style="margin-right: 0px"> {{ statusMap[item.status] || "未知" }}</a-tag>
</a-tooltip>
</a-col>
</a-row>

View File

@ -813,7 +813,7 @@ export default {
totalProjectNum: 0,
columns: [
{ title: "分发 ID", dataIndex: "id", ellipsis: true, width: 110, scopedSlots: { customRender: "tooltip" } },
{ title: "分发名称", dataIndex: "name", ellipsis: true, width: 150, scopedSlots: { customRender: "name" } },
{ title: "分发名称", dataIndex: "name", ellipsis: true, width: 200, scopedSlots: { customRender: "name" } },
{ title: "项目分组", dataIndex: "group", sorter: true, width: "100px", ellipsis: true, scopedSlots: { customRender: "group" } },
{ title: "分发类型", dataIndex: "outGivingProject", width: "90px", ellipsis: true, scopedSlots: { customRender: "outGivingProject" } },
{ title: "分发后", dataIndex: "afterOpt", ellipsis: true, width: "150px", scopedSlots: { customRender: "afterOpt" } },

View File

@ -1,41 +1,66 @@
<template>
<a-layout class="docker-layout">
<a-layout-sider width="100px" style="min-height: calc(100vh - 85px)">
<a-menu theme="light" mode="inline" class="docker-menu" v-model="menuKeyArray" @click="menuClick">
<a-menu-item key="containers">
<span class="nav-text">容器</span>
</a-menu-item>
<a-menu-item key="images">
<span class="nav-text">镜像</span>
</a-menu-item>
<a-menu-item key="volumes">
<span class="nav-text"></span>
</a-menu-item>
<a-menu-item key="networks">
<span class="nav-text">网络</span>
</a-menu-item>
<a-menu-item key="info">
<span class="nav-text">信息</span>
</a-menu-item>
<a-menu-item key="prune">
<span class="nav-text">裁剪</span>
</a-menu-item>
</a-menu>
</a-layout-sider>
<!-- style="{ background: '#fff', padding: '10px' }" -->
<a-layout class="layout-content drawer-layout-content">
<!-- <a-layout-header :style="{ background: '#fff', padding: 0 }" /> -->
<!-- :style="{ margin: '24px 16px 0' }" -->
<a-layout-content>
<container v-if="menuKey === 'containers'" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
<images v-if="menuKey === 'images'" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
<volumes v-if="menuKey === 'volumes'" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
<info v-if="menuKey === 'info'" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
<networks v-if="menuKey === 'networks'" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
<prune v-if="menuKey === 'prune'" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
</a-layout-content>
</a-layout>
</a-layout>
<div :class="`${this.scrollbarFlag ? '' : 'hide-scrollbar'}`">
<!-- 控制台 -->
<a-drawer
destroyOnClose
placement="right"
:width="`${this.getCollapsed ? 'calc(100vw - 80px)' : 'calc(100vw - 200px)'}`"
:visible="true"
@close="onClose"
:bodyStyle="{
padding: '0',
}"
>
<template #title>
<a-space>
控制台
<a-menu theme="light" mode="horizontal" class="docker-menu" v-model="menuKeyArray" @click="menuClick">
<a-menu-item key="containers">
<span class="nav-text">独立容器</span>
</a-menu-item>
<a-menu-item key="docker-compose">
<span class="nav-text">docker-compose</span>
</a-menu-item>
<a-menu-item key="images">
<span class="nav-text">镜像</span>
</a-menu-item>
<a-menu-item key="volumes">
<span class="nav-text"></span>
</a-menu-item>
<a-menu-item key="networks">
<span class="nav-text">网络</span>
</a-menu-item>
<a-menu-item key="info">
<span class="nav-text">信息</span>
</a-menu-item>
<a-menu-item key="prune">
<span class="nav-text">裁剪</span>
</a-menu-item>
</a-menu>
</a-space>
</template>
<a-layout :class="`layout-content drawer-layout-content ${this.scrollbarFlag ? '' : 'hide-scrollbar'}`" style="padding-bottom: 10px">
<a-layout-content>
<container :key="menuKey" v-if="menuKey === 'containers'" type="container" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
<container
:key="`docker-compose`"
v-else-if="menuKey === 'docker-compose'"
type="compose"
:id="this.id"
:machineDockerId="this.machineDockerId"
:visible="this.visible"
:urlPrefix="this.urlPrefix"
/>
<images v-if="menuKey === 'images'" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
<volumes v-if="menuKey === 'volumes'" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
<info v-if="menuKey === 'info'" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
<networks v-if="menuKey === 'networks'" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
<prune v-if="menuKey === 'prune'" :id="this.id" :machineDockerId="this.machineDockerId" :visible="this.visible" :urlPrefix="this.urlPrefix" />
</a-layout-content>
</a-layout>
</a-drawer>
</div>
</template>
<script>
import Container from "./container";
@ -44,6 +69,7 @@ import Volumes from "./volumes";
import Info from "./info";
import Networks from "./networks";
import Prune from "./prune";
import { mapGetters } from "vuex";
export default {
props: {
id: {
@ -74,16 +100,21 @@ export default {
menuKey: "containers",
};
},
computed: {
...mapGetters(["getCollapsed", "getGuideCache"]),
scrollbarFlag() {
return this.getGuideCache.scrollbarFlag === undefined ? true : this.getGuideCache.scrollbarFlag;
},
},
mounted() {},
methods: {
menuClick(item) {
this.menuKey = item.key;
},
onClose() {
this.$emit("close");
},
},
};
</script>
<style scoped>
.docker-menu {
height: 100%;
}
</style>
<style scoped></style>

View File

@ -1,215 +1,405 @@
<template>
<div>
<a-table :data-source="list" size="middle" :columns="columns" :pagination="false" bordered rowKey="id" @expand="expand">
<template slot="title">
<a-space>
<a-input v-model="listQuery['name']" @pressEnter="loadData" placeholder="名称" class="search-input-item" />
<a-input v-model="listQuery['containerId']" @pressEnter="loadData" placeholder="容器id" class="search-input-item" />
<!-- <a-input v-model="listQuery['imageId']" @keyup.enter="loadData" placeholder="镜像id" class="search-input-item" /> -->
<div>
显示所有
<a-switch checked-children="" un-checked-children="" v-model="listQuery['showAll']" />
</div>
<a-button type="primary" @click="loadData" :loading="loading">搜索</a-button>
<a-statistic-countdown format=" s 秒" title="刷新倒计时" :value="countdownTime" @finish="autoUpdate" />
</a-space>
</template>
<a-popover :title="`容器名称:${(text || []).join(',')}`" slot="names" slot-scope="text, record">
<template slot="content">
<p>容器Id: {{ record.id }}</p>
<p>镜像{{ record.image }}</p>
<p>镜像Id: {{ record.imageId }}</p>
<template v-if="record.labels">
<p v-for="(value, key) in record.labels" :key="key">{{ key }}<a-icon type="arrow-right" />{{ value }}</p>
</template>
<template v-if="this.type === 'container'">
<a-table :data-source="list" size="middle" :columns="columns" :pagination="false" bordered rowKey="id">
<template slot="title">
<a-space>
<a-input v-model="listQuery['name']" @pressEnter="loadData" placeholder="名称" class="search-input-item" />
<a-input v-model="listQuery['containerId']" @pressEnter="loadData" placeholder="容器id" class="search-input-item" />
<a-input v-model="listQuery['imageId']" @keyup.enter="loadData" placeholder="镜像id" class="search-input-item" />
<div>
查看
<a-switch checked-children="所有" un-checked-children="运行中" v-model="listQuery['showAll']" />
</div>
<a-button type="primary" @click="loadData" :loading="loading">搜索</a-button>
<a-statistic-countdown format=" s 秒" title="刷新倒计时" :value="countdownTime" @finish="autoUpdate" />
</a-space>
</template>
<a-icon v-if="record.labels && Object.keys(record.labels).length" type="pushpin" />
<span>{{ (text || []).join(",") }}</span>
</a-popover>
<a-tooltip slot="tooltip" slot-scope="text" placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
<a-popover :title="`容器名称:${(text || []).join(',')}`" slot="names" slot-scope="text, record">
<template slot="content">
<p>容器Id: {{ record.id }}</p>
<p>镜像{{ record.image }}</p>
<p>镜像Id: {{ record.imageId }}</p>
</template>
<a-tooltip slot="id" slot-scope="text" placement="topLeft" :title="text">
<span style="display: none"> {{ (array = text.split(":")) }}</span>
<span>{{ array[array.length - 1].slice(0, 12) }}</span>
</a-tooltip>
<span>{{ (text || []).join(",") }}</span>
</a-popover>
<a-popover
slot="ports"
slot-scope="text, record"
placement="topLeft"
:title="`
网络信息 ${(text || [])
.map((item) => {
return item.type + ' ' + (item.ip || '') + ':' + (item.publicPort || '') + ':' + item.privatePort;
})
.join('/')}
`"
>
<template slot="content">
<template v-if="record.networkSettings">
<template v-if="record.networkSettings.networks">
<template v-if="record.networkSettings.networks.bridge">
桥接模式
<p v-if="record.networkSettings.networks.bridge.ipAddress">
IP: <a-tag>{{ record.networkSettings.networks.bridge.ipAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.macAddress">
MAC: <a-tag>{{ record.networkSettings.networks.bridge.macAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.gateway">
网关: <a-tag>{{ record.networkSettings.networks.bridge.gateway }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.networkID">
networkID: <a-tag>{{ record.networkSettings.networks.bridge.networkID }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.endpointId">
endpointId: <a-tag>{{ record.networkSettings.networks.bridge.endpointId }}</a-tag>
</p>
</template>
<template v-if="record.networkSettings.networks.ingress">
<p v-if="record.networkSettings.networks.ingress.ipAddress">
IP: <a-tag>{{ record.networkSettings.networks.ingress.ipAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.macAddress">
MAC: <a-tag>{{ record.networkSettings.networks.ingress.macAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.gateway">
网关: <a-tag>{{ record.networkSettings.networks.ingress.gateway }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.networkID">
networkID: <a-tag>{{ record.networkSettings.networks.ingress.networkID }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.endpointId">
endpointId: <a-tag>{{ record.networkSettings.networks.ingress.endpointId }}</a-tag>
<a-popover :title="`容器名标签`" slot="labels" slot-scope="text, record">
<template slot="content">
<template v-if="record.labels">
<p v-for="(value, key) in record.labels" :key="key">{{ key }}<a-icon type="arrow-right" />{{ value }}</p>
</template>
</template>
<template v-if="record.labels && Object.keys(record.labels).length">
<span>{{ (record.labels && Object.keys(record.labels).length) || 0 }} <a-icon type="tags" /></span>
</template>
<template v-else>-</template>
</a-popover>
<a-popover :title="`挂载`" slot="mounts" slot-scope="text, record">
<template slot="content">
<template v-if="record.mounts">
<div v-for="(item, index) in record.mounts" :key="index">
<p>
名称{{ item.name }} <a-tag>{{ item.rw ? "读写" : "读" }}</a-tag>
</p>
<p>路径{{ item.source }}(宿主机) => {{ item.destination }}(容器)</p>
<a-divider></a-divider>
</div>
</template>
</template>
<template v-if="record.mounts && Object.keys(record.mounts).length">
<span>{{ (record.mounts && Object.keys(record.mounts).length) || 0 }} <a-icon type="api" /></span>
</template>
<template v-else>-</template>
</a-popover>
<a-tooltip slot="tooltip" slot-scope="text" placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
<a-tooltip slot="showid" slot-scope="text" placement="topLeft" :title="text">
<span style="display: none"> {{ (array = text.split(":")) }}</span>
<span>{{ array[array.length - 1].slice(0, 12) }}</span>
</a-tooltip>
<a-popover slot="ports" slot-scope="text, record" placement="topLeft">
<template #title>
网络端口
<ul>
<li v-for="(item, index) in text || []" :key="index">
{{ item.type + " " + (item.ip || "") + ":" + (item.publicPort || "") + ":" + item.privatePort }}
</li>
</ul>
</template>
<template slot="content">
<template v-if="record.networkSettings">
<template v-if="record.networkSettings.networks">
<template v-if="record.networkSettings.networks.bridge">
桥接模式
<p v-if="record.networkSettings.networks.bridge.ipAddress">
IP: <a-tag>{{ record.networkSettings.networks.bridge.ipAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.macAddress">
MAC: <a-tag>{{ record.networkSettings.networks.bridge.macAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.gateway">
网关: <a-tag>{{ record.networkSettings.networks.bridge.gateway }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.networkID">
networkID: <a-tag>{{ record.networkSettings.networks.bridge.networkID }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.endpointId">
endpointId: <a-tag>{{ record.networkSettings.networks.bridge.endpointId }}</a-tag>
</p>
</template>
<template v-if="record.networkSettings.networks.ingress">
<p v-if="record.networkSettings.networks.ingress.ipAddress">
IP: <a-tag>{{ record.networkSettings.networks.ingress.ipAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.macAddress">
MAC: <a-tag>{{ record.networkSettings.networks.ingress.macAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.gateway">
网关: <a-tag>{{ record.networkSettings.networks.ingress.gateway }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.networkID">
networkID: <a-tag>{{ record.networkSettings.networks.ingress.networkID }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.endpointId">
endpointId: <a-tag>{{ record.networkSettings.networks.ingress.endpointId }}</a-tag>
</p>
</template>
</template>
</template>
</template>
</template>
<span>{{
(text || [])
.map((item) => {
return item.type + " " + (item.publicPort || "") + ":" + item.privatePort;
})
.join("/")
}}</span>
</a-popover>
<span>{{
(text || [])
.map((item) => {
return item.type + " " + (item.publicPort || "") + ":" + item.privatePort;
})
.join("/")
}}</span>
</a-popover>
<template slot="state" slot-scope="text, record">
<a-tooltip :title="(record.status || '') + ' 点击查看日志'" @click="viewLog(record)">
<a-switch :checked="text === 'running'" :disabled="true">
<a-icon slot="checkedChildren" type="check-circle" />
<a-icon slot="unCheckedChildren" type="warning" />
</a-switch>
</a-tooltip>
</template>
<template slot="operation" slot-scope="text, record">
<a-space>
<a-tooltip title="容器是运行中可以进入终端">
<a-button size="small" type="link" :disabled="record.state !== 'running'" @click="handleTerminal(record)"><a-icon type="code" /></a-button>
<template slot="state" slot-scope="text, record">
<a-tooltip :title="(record.status || '') + ' 点击查看日志'" @click="viewLog(record)">
<a-switch :checked="text === 'running'" :disabled="true">
<a-icon slot="checkedChildren" type="check-circle" />
<a-icon slot="unCheckedChildren" type="warning" />
</a-switch>
</a-tooltip>
<a-tooltip title="停止" v-if="record.state === 'running'">
<a-button size="small" type="link" @click="doAction(record, 'stop')"><a-icon type="stop" /></a-button>
</a-tooltip>
<a-tooltip title="启动" v-else>
<a-button size="small" type="link" @click="doAction(record, 'start')"> <a-icon type="play-circle" /></a-button>
</a-tooltip>
<a-tooltip title="重启">
<a-button size="small" type="link" :disabled="record.state !== 'running'" @click="doAction(record, 'restart')"><a-icon type="reload" /></a-button>
</a-tooltip>
<a-tooltip title="删除">
<a-button size="small" type="link" @click="doAction(record, 'remove')"><a-icon type="delete" /></a-button>
</a-tooltip>
<a-tooltip title="点击查看日志">
<a-button size="small" type="link" @click="viewLog(record)"><a-icon type="message" /></a-button>
</a-tooltip>
</a-space>
</template>
</template>
<template slot="operation" slot-scope="text, record">
<a-space>
<template v-if="record.state === 'running'">
<a-tooltip title="容器是运行中可以进入终端">
<a-button size="small" type="link" :disabled="record.state !== 'running'" @click="handleTerminal(record)"><a-icon type="code" /></a-button>
</a-tooltip>
<a-tooltip title="停止">
<a-button size="small" type="link" @click="doAction(record, 'stop')"><a-icon type="stop" /></a-button>
</a-tooltip>
<a-tooltip title="重启">
<a-button size="small" type="link" @click="doAction(record, 'restart')"><a-icon type="reload" /></a-button>
</a-tooltip>
</template>
<template v-else>
<a-tooltip title="启动">
<a-button size="small" type="link" @click="doAction(record, 'start')"> <a-icon type="play-circle" /></a-button>
</a-tooltip>
<a-tooltip title="停止">
<a-button size="small" type="link" :disabled="true"><a-icon type="stop" /></a-button>
</a-tooltip>
<a-tooltip title="重启">
<a-button size="small" type="link" :disabled="true"><a-icon type="reload" /></a-button>
</a-tooltip>
</template>
<!-- stats -->
<a-table slot="expandedRowRender" :rowKey="(record, index) => index" slot-scope="parentRecord" :columns="statsColumns" :data-source="statsMap[parentRecord.id]" :pagination="false">
<template slot="cpus" slot-scope="text, record">
{{ (record.cpuStats && record.cpuStats.percpuUsage) || (record.cpuStats && record.cpuStats.onlineCpus) }}
<a-dropdown>
<a class="ant-dropdown-link" @click="(e) => e.preventDefault()">
<a-icon type="more" />
</a>
<a-menu slot="overlay">
<a-menu-item>
<atooltip title="编辑容器的一些基础参数">
<a-button size="small" type="link" icon="edit" :disabled="record.state !== 'running'" @click="editContainer(record)">编辑</a-button>
</atooltip>
</a-menu-item>
<a-menu-item>
<a-tooltip title="点击查看日志">
<a-button size="small" type="link" icon="message" @click="viewLog(record)">日志</a-button>
</a-tooltip>
</a-menu-item>
<a-menu-item>
<a-tooltip title="强制删除">
<a-button size="small" type="link" icon="delete" @click="doAction(record, 'remove')">删除</a-button>
</a-tooltip>
</a-menu-item>
</a-menu>
</a-dropdown>
</a-space>
</template>
<template slot="cpuRatio" slot-scope="text, record">
{{
(
((((record.cpuStats && record.cpuStats.cpuUsage && record.cpuStats.cpuUsage.totalUsage) || 0) -
((record.precpuStats && record.precpuStats.cpuUsage && record.precpuStats.cpuUsage.totalUsage) || 0)) /
((record.cpuStats && record.cpuStats.systemCpuUsage) || 0) -
((record.precpuStats && record.precpuStats.systemCpuUsage) || 0)) *
100.0
).toFixed(4)
}}
%
</template>
<template slot="memory" slot-scope="text, record">
<!-- record.state !== 'running' -->
<!-- text === 'running' -->
<template v-if="parentRecord.state === 'running'">
<a-button type="link" icon="edit" @click="editContainer(parentRecord)">
{{ renderSize(((record.memoryStats && record.memoryStats.usage) || 0) - ((record.memoryStats && record.memoryStats.stats && record.memoryStats.stats.cache) || 0)) }}
/
{{ renderSize((record.memoryStats && record.memoryStats.limit) || 0) }}
</a-button>
</template>
<template v-else>
{{ renderSize(((record.memoryStats && record.memoryStats.usage) || 0) - ((record.memoryStats && record.memoryStats.stats && record.memoryStats.stats.cache) || 0)) }}
/
{{ renderSize((record.memoryStats && record.memoryStats.limit) || 0) }}
</template>
</template>
<template slot="memoryRatio" slot-scope="text, record">
<!-- memoryRatio -->
{{
(
((((record.memoryStats && record.memoryStats.usage) || 0) - ((record.memoryStats && record.memoryStats.stats && record.memoryStats.stats.cache) || 0)) /
(record.memoryStats && record.memoryStats.limit)) *
100.0
).toFixed(4)
}}
%
</template>
<template slot="blockIo" slot-scope="text, record">
<a-tooltip
:title="`${
(record.blkioStats && record.blkioStats.ioServiceBytesRecursive && record.blkioStats.ioServiceBytesRecursive[0] && record.blkioStats.ioServiceBytesRecursive[0].op) || 'blkioStats'
}`"
>
{{ renderSize(record.blkioStats && record.blkioStats.ioServiceBytesRecursive && record.blkioStats.ioServiceBytesRecursive[0] && record.blkioStats.ioServiceBytesRecursive[0].value) || 0 }}
</a-tooltip>
/
<a-tooltip
:title="`${
(record.blkioStats && record.blkioStats.ioServiceBytesRecursive && record.blkioStats.ioServiceBytesRecursive[1] && record.blkioStats.ioServiceBytesRecursive[1].op) || 'blkioStats'
}`"
>
{{ renderSize(record.blkioStats && record.blkioStats.ioServiceBytesRecursive && record.blkioStats.ioServiceBytesRecursive[1] && record.blkioStats.ioServiceBytesRecursive[1].value) || 0 }}
</a-tooltip>
</template>
<template slot="netIo" slot-scope="text, record">
<!-- // rx_bytes -->
<!-- // tx_bytes -->
<div :key="index" v-for="(item, index) in Object.keys(record.networks || {})">
<a-tooltip :title="`${item} 接收流量`">
{{ renderSize(record.networks[item] && record.networks[item].rxBytes) || 0 }}
</a-tooltip>
/
<a-tooltip :title="`${item} 输出流量`">
{{ renderSize(record.networks[item] && record.networks[item].txBytes) || 0 }}
</a-tooltip>
</div>
</template>
<!-- // 线 -->
<template slot="pids" slot-scope="text, record"> {{ record.pidsStats && record.pidsStats.current }}</template>
</a-table>
</a-table>
</template>
<template v-else-if="this.type === 'compose'">
<a-table
:data-source="list"
size="middle"
:columns="parentColumns"
:pagination="false"
bordered
:rowKey="
(item, index) => {
return index;
}
"
>
<template slot="title">
<a-space>
<a-input v-model="listQuery['name']" @pressEnter="loadData" placeholder="名称" class="search-input-item" />
<a-input v-model="listQuery['containerId']" @pressEnter="loadData" placeholder="容器id" class="search-input-item" />
<a-input v-model="listQuery['imageId']" @keyup.enter="loadData" placeholder="镜像id" class="search-input-item" />
<div>
查看
<a-switch checked-children="所有" un-checked-children="运行中" v-model="listQuery['showAll']" />
</div>
<a-button type="primary" @click="loadData" :loading="loading">搜索</a-button>
<a-statistic-countdown format=" s 秒" title="刷新倒计时" :value="countdownTime" @finish="autoUpdate" />
</a-space>
</template>
<a-tooltip slot="tooltip" slot-scope="text" placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
<template slot="state" slot-scope="text, record">
<span style="display: none">
{{
(array = (record.child || []).map((item) => {
return item.state;
}))
}}
{{
(runningCount = array
.map((item) => {
return item === "running" ? 1 : 0;
})
.reduce((prev, curr) => {
return prev + curr;
}, 0))
}}</span
>
<span v-if="runningCount">Running({{ runningCount }}/{{ array.length }})</span>
<span v-else>Exited</span>
</template>
<a-table slot="expandedRowRender" slot-scope="record" :data-source="record.child" size="middle" :columns="columns" :pagination="false" bordered rowKey="id">
<a-popover :title="`容器名称:${(text || []).join(',')}`" slot="names" slot-scope="text, record">
<template slot="content">
<p>容器Id: {{ record.id }}</p>
<p>镜像{{ record.image }}</p>
<p>镜像Id: {{ record.imageId }}</p>
</template>
<span>{{ (text || []).join(",") }}</span>
</a-popover>
<a-popover :title="`容器名标签`" slot="labels" slot-scope="text, record">
<template slot="content">
<template v-if="record.labels">
<p v-for="(value, key) in record.labels" :key="key">{{ key }}<a-icon type="arrow-right" />{{ value }}</p>
</template>
</template>
<template v-if="record.labels && Object.keys(record.labels).length">
<span>{{ (record.labels && Object.keys(record.labels).length) || 0 }} <a-icon type="tags" /></span>
</template>
<template v-else>-</template>
</a-popover>
<a-popover :title="`挂载`" slot="mounts" slot-scope="text, record">
<template slot="content">
<template v-if="record.mounts">
<div v-for="(item, index) in record.mounts" :key="index">
<p>
名称{{ item.name }} <a-tag>{{ item.rw ? "读写" : "读" }}</a-tag>
</p>
<p>路径{{ item.source }}(宿主机) => {{ item.destination }}(容器)</p>
<a-divider></a-divider>
</div>
</template>
</template>
<template v-if="record.mounts && Object.keys(record.mounts).length">
<span>{{ (record.mounts && Object.keys(record.mounts).length) || 0 }} <a-icon type="api" /></span>
</template>
<template v-else>-</template>
</a-popover>
<a-tooltip slot="tooltip" slot-scope="text" placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
<a-tooltip slot="showid" slot-scope="text" placement="topLeft" :title="text">
<span style="display: none"> {{ (array = text.split(":")) }}</span>
<span>{{ array[array.length - 1].slice(0, 12) }}</span>
</a-tooltip>
<a-popover slot="ports" slot-scope="text, record" placement="topLeft">
<template #title>
网络端口
<ul>
<li v-for="(item, index) in text || []" :key="index">
{{ item.type + " " + (item.ip || "") + ":" + (item.publicPort || "") + ":" + item.privatePort }}
</li>
</ul>
</template>
<template slot="content">
<template v-if="record.networkSettings">
<template v-if="record.networkSettings.networks">
<template v-if="record.networkSettings.networks.bridge">
桥接模式
<p v-if="record.networkSettings.networks.bridge.ipAddress">
IP: <a-tag>{{ record.networkSettings.networks.bridge.ipAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.macAddress">
MAC: <a-tag>{{ record.networkSettings.networks.bridge.macAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.gateway">
网关: <a-tag>{{ record.networkSettings.networks.bridge.gateway }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.networkID">
networkID: <a-tag>{{ record.networkSettings.networks.bridge.networkID }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.bridge.endpointId">
endpointId: <a-tag>{{ record.networkSettings.networks.bridge.endpointId }}</a-tag>
</p>
</template>
<template v-if="record.networkSettings.networks.ingress">
<p v-if="record.networkSettings.networks.ingress.ipAddress">
IP: <a-tag>{{ record.networkSettings.networks.ingress.ipAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.macAddress">
MAC: <a-tag>{{ record.networkSettings.networks.ingress.macAddress }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.gateway">
网关: <a-tag>{{ record.networkSettings.networks.ingress.gateway }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.networkID">
networkID: <a-tag>{{ record.networkSettings.networks.ingress.networkID }}</a-tag>
</p>
<p v-if="record.networkSettings.networks.ingress.endpointId">
endpointId: <a-tag>{{ record.networkSettings.networks.ingress.endpointId }}</a-tag>
</p>
</template>
</template>
</template>
</template>
<span>{{
(text || [])
.map((item) => {
return item.type + " " + (item.publicPort || "") + ":" + item.privatePort;
})
.join("/")
}}</span>
</a-popover>
<template slot="state" slot-scope="text, record">
<a-tooltip :title="(record.status || '') + ' 点击查看日志'" @click="viewLog(record)">
<a-switch :checked="text === 'running'" :disabled="true">
<a-icon slot="checkedChildren" type="check-circle" />
<a-icon slot="unCheckedChildren" type="warning" />
</a-switch>
</a-tooltip>
</template>
<template slot="operation" slot-scope="text, record">
<a-space>
<template v-if="record.state === 'running'">
<a-tooltip title="容器是运行中可以进入终端">
<a-button size="small" type="link" @click="handleTerminal(record)"><a-icon type="code" /></a-button>
</a-tooltip>
<a-tooltip title="停止">
<a-button size="small" type="link" @click="doAction(record, 'stop')"><a-icon type="stop" /></a-button>
</a-tooltip>
<a-tooltip title="重启">
<a-button size="small" type="link" @click="doAction(record, 'restart')"><a-icon type="reload" /></a-button>
</a-tooltip>
</template>
<template v-else>
<a-tooltip title="启动">
<a-button size="small" type="link" @click="doAction(record, 'start')"> <a-icon type="play-circle" /></a-button>
</a-tooltip>
<a-tooltip title="停止">
<a-button size="small" type="link" :disabled="true"><a-icon type="stop" /></a-button>
</a-tooltip>
<a-tooltip title="重启">
<a-button size="small" type="link" :disabled="true"><a-icon type="reload" /></a-button>
</a-tooltip>
</template>
<a-dropdown>
<a class="ant-dropdown-link" @click="(e) => e.preventDefault()">
<a-icon type="more" />
</a>
<a-menu slot="overlay">
<a-menu-item>
<atooltip title="编辑容器的一些基础参数">
<a-button size="small" type="link" icon="edit" :disabled="record.state !== 'running'" @click="editContainer(record)">编辑</a-button>
</atooltip>
</a-menu-item>
<a-menu-item>
<a-tooltip title="点击查看日志">
<a-button size="small" type="link" icon="message" @click="viewLog(record)">日志</a-button>
</a-tooltip>
</a-menu-item>
<a-menu-item>
<a-tooltip title="强制删除">
<a-button size="small" type="link" icon="delete" @click="doAction(record, 'remove')">删除</a-button>
</a-tooltip>
</a-menu-item>
</a-menu>
</a-dropdown>
</a-space>
</template>
</a-table>
</a-table>
</template>
<!-- 日志 -->
<a-modal destroyOnClose :width="'80vw'" v-model="logVisible" title="执行日志" :footer="null" :maskClosable="false">
<log-view v-if="logVisible" :id="this.id" :machineDockerId="this.machineDockerId" :containerId="temp.id" />
@ -231,122 +421,37 @@
<terminal v-if="terminalVisible" :id="this.id" :machineDockerId="this.machineDockerId" :containerId="temp.id" />
</a-modal>
<!-- 编辑容器配置 -->
<a-modal destroyOnClose v-model="editVisible" title="配置容器" @ok="handleEditOk" :maskClosable="false">
<a-form-model ref="editForm" :model="temp" :label-col="{ span: 7 }" :wrapper-col="{ span: 17 }">
<a-form-model-item prop="blkioWeight">
<template slot="label">
Block IO 权重
<a-tooltip>
<template slot="title"> Block IO 权重相对权重 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input-number style="width: 100%" v-model="temp.blkioWeight" :min="0" :max="1000" />
</a-form-model-item>
<a-form-model-item prop="cpuShares">
<template slot="label">
CPU 权重
<a-tooltip>
<template slot="title"> 一个整数值表示此容器相对于其他容器的相对 CPU 权重 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input-number style="width: 100%" v-model="temp.cpuShares" />
</a-form-model-item>
<a-form-model-item prop="cpusetCpus">
<template slot="label">
执行的 CPU
<a-tooltip>
<template slot="title"> 允许执行的 CPU例如0-30,1 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input style="width: 100%" v-model="temp.cpusetCpus" />
</a-form-model-item>
<a-form-model-item prop="cpusetMems">
<template slot="label">
CpusetMems
<a-tooltip>
<template slot="title"> 允许执行的内存节点 (MEM) (0-3, 0,1) 仅在 NUMA 系统上有效 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input style="width: 100%" v-model="temp.cpusetMems" />
</a-form-model-item>
<a-form-model-item prop="cpuPeriod">
<template slot="label">
CPU 周期
<a-tooltip>
<template slot="title"> CPU 周期的长度以微秒为单位 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input-number style="width: 100%" v-model="temp.cpuPeriod" />
</a-form-model-item>
<a-form-model-item prop="cpuQuota">
<template slot="label">
CPU 时间
<a-tooltip>
<template slot="title"> 容器在一个 CPU 周期内可以获得的 CPU 时间的微秒 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input-number style="width: 100%" v-model="temp.cpuQuota" />
</a-form-model-item>
<a-form-model-item prop="memory">
<template slot="label">
内存
<a-tooltip>
<template slot="title"> 设置内存限制 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input style="width: 100%" v-model="temp.memory" />
</a-form-model-item>
<a-form-model-item prop="memorySwap">
<template slot="label">
总内存
<a-tooltip>
<template slot="title"> 总内存内存 + 交换 设置为 -1 以禁用交换 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input style="width: 100%" v-model="temp.memorySwap" />
</a-form-model-item>
<a-form-model-item prop="memoryReservation">
<template slot="label">
软内存
<a-tooltip>
<template slot="title"> 软内存限制 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input style="width: 100%" v-model="temp.memoryReservation" />
</a-form-model-item>
</a-form-model>
<a-modal
destroyOnClose
v-model="editVisible"
width="60vw"
title="配置容器"
@ok="
() => {
this.$refs.editContainer.handleEditOk();
this.editVisible = false;
this.loadData();
}
"
:maskClosable="false"
>
<editContainer ref="editContainer" :id="this.id" :machineDockerId="this.machineDockerId" :urlPrefix="this.urlPrefix" :containerId="temp.id"></editContainer>
</a-modal>
</div>
</template>
<script>
import { parseTime, renderSize } from "@/utils/const";
import {
dockerContainerList,
dockerContainerRemove,
dockerContainerRestart,
dockerContainerStart,
dockerContainerStats,
dockerContainerStop,
dockerInspectContainer,
dockerUpdateContainer,
} from "@/api/docker-api";
import { parseTime } from "@/utils/const";
import { dockerContainerList, dockerContainerRemove, dockerContainerRestart, dockerContainerStart, dockerContainerStop, dockerContainerListCompose } from "@/api/docker-api";
import LogView from "@/pages/docker/log-view";
import Terminal from "@/pages/docker/terminal";
import editContainer from "./editContainer.vue";
export default {
name: "container",
components: {
LogView,
Terminal,
editContainer,
},
props: {
id: {
@ -364,6 +469,11 @@ export default {
type: String,
default: "",
},
type: {
type: String,
// container or compose
default: "container",
},
},
data() {
return {
@ -377,41 +487,36 @@ export default {
temp: {},
columns: [
{ title: "序号", width: 80, ellipsis: true, align: "center", customRender: (text, record, index) => `${index + 1}` },
{ title: "名称", dataIndex: "names", ellipsis: true, scopedSlots: { customRender: "names" } },
{ title: "容器ID", dataIndex: "id", ellipsis: true, width: 150, scopedSlots: { customRender: "id" } },
// { title: "", dataIndex: "image", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
// { title: "ID", dataIndex: "imageId", ellipsis: true, width: 150, scopedSlots: { customRender: "id" } },
{ title: "序号", width: "80px", ellipsis: true, align: "center", customRender: (text, record, index) => `${index + 1}` },
{ title: "名称", dataIndex: "names", ellipsis: true, width: 150, scopedSlots: { customRender: "names" } },
{ title: "容器ID", dataIndex: "id", ellipsis: true, width: "130px", scopedSlots: { customRender: "showid" } },
{ title: "镜像ID", dataIndex: "imageId", ellipsis: true, width: "130px", scopedSlots: { customRender: "showid" } },
{ title: "状态", dataIndex: "state", ellipsis: true, align: "center", width: "80px", scopedSlots: { customRender: "state" } },
{ title: "状态描述", dataIndex: "status", ellipsis: true, align: "center", width: 100, scopedSlots: { customRender: "tooltip" } },
{ title: "端口", dataIndex: "ports", ellipsis: true, width: 150, scopedSlots: { customRender: "ports" } },
{ title: "状态", dataIndex: "state", ellipsis: true, align: "center", width: 90, scopedSlots: { customRender: "state" } },
{ title: "命令", dataIndex: "command", ellipsis: true, width: 150, scopedSlots: { customRender: "tooltip" } },
{ title: "标签", dataIndex: "labels", ellipsis: true, width: "50px", scopedSlots: { customRender: "labels" } },
{ title: "挂载", dataIndex: "mounts", ellipsis: true, width: "50px", scopedSlots: { customRender: "mounts" } },
{
title: "创建时间",
dataIndex: "created",
ellipsis: true,
sorter: (a, b) => new Number(a.created) - new Number(b.created),
sorter: (a, b) => Number(a.created) - new Number(b.created),
sortDirections: ["descend", "ascend"],
defaultSortOrder: "descend",
customRender: (text) => {
return parseTime(text);
},
width: 170,
customRender: (text) => parseTime(text),
width: "170px",
},
{ title: "操作", dataIndex: "operation", scopedSlots: { customRender: "operation" }, width: 200 },
{ title: "操作", dataIndex: "operation", fixed: "right", scopedSlots: { customRender: "operation" }, width: "160px" },
],
statsColumns: [
{ title: "CPUS", width: "80px", dataIndex: "cpus", ellipsis: true, scopedSlots: { customRender: "cpus" } },
{ title: "CPU %", width: 100, dataIndex: "cpuRatio", ellipsis: true, scopedSlots: { customRender: "cpuRatio" } },
{ title: "MEM USAGE / LIMIT", dataIndex: "memory", ellipsis: true, scopedSlots: { customRender: "memory" } },
{ title: "MEM %", width: 100, dataIndex: "memoryRatio", ellipsis: true, scopedSlots: { customRender: "memoryRatio" } },
{ title: "NET I/O", dataIndex: "netIo", ellipsis: true, scopedSlots: { customRender: "netIo" } },
{ title: "BLOCK I/O", dataIndex: "blockIo", ellipsis: true, scopedSlots: { customRender: "blockIo" } },
{ title: "PIDS", width: "80px", dataIndex: "pids", ellipsis: true, scopedSlots: { customRender: "pids" } },
parentColumns: [
{ title: "序号", width: "80px", ellipsis: true, align: "center", customRender: (text, record, index) => `${index + 1}` },
{ title: "名称", width: 200, dataIndex: "name", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "状态", dataIndex: "state", width: "150px", ellipsis: true, scopedSlots: { customRender: "state" } },
{ title: "操作", width: "80px", ellipsis: true, scopedSlots: { customRender: "pids" } },
],
statsMap: {},
expandedRowKeys: [],
action: {
remove: {
msg: "您确定要删除当前容器吗?",
@ -431,6 +536,7 @@ export default {
},
},
editVisible: false,
countdownTime: Date.now(),
};
},
@ -444,10 +550,8 @@ export default {
this.autoUpdate();
},
methods: {
renderSize,
autoUpdate() {
this.loadData();
this.pullStats();
},
//
loadData() {
@ -457,7 +561,7 @@ export default {
this.loading = true;
//this.listQuery.page = pointerEvent?.altKey || pointerEvent?.ctrlKey ? 1 : this.listQuery.page;
this.listQuery.id = this.reqDataId;
dockerContainerList(this.urlPrefix, this.listQuery).then((res) => {
(this.type === "container" ? dockerContainerList(this.urlPrefix, this.listQuery) : dockerContainerListCompose(this.urlPrefix, this.listQuery)).then((res) => {
if (res.code === 200) {
this.list = res.data;
}
@ -487,9 +591,6 @@ export default {
message: res.msg,
});
this.loadData();
if (actionKey === "remove") {
this.expandedRowKeys = this.expandedRowKeys.filter((item2) => item2 !== record.id);
}
}
});
},
@ -504,76 +605,10 @@ export default {
this.temp = Object.assign({}, record);
this.terminalVisible = true;
},
//
expand(status, item) {
if (status) {
this.expandedRowKeys.push(item.id);
this.pullStats();
} else {
this.expandedRowKeys = this.expandedRowKeys.filter((item2) => item2 !== item.id);
}
},
//
pullStats() {
if (!this.expandedRowKeys.length) {
return;
}
dockerContainerStats(this.urlPrefix, {
id: this.reqDataId,
containerId: this.expandedRowKeys.join(","),
}).then((res) => {
if (res.code === 200) {
Object.keys(res.data).forEach((item) => {
this.statsMap = { ...this.statsMap, [item]: [res.data[item]] };
});
}
// console.log(res);
});
},
//
editContainer(record) {
dockerInspectContainer(this.urlPrefix, {
id: this.reqDataId,
containerId: record.id,
}).then((res) => {
if (res.code === 200) {
this.editVisible = true;
const hostConfig = res.data.hostConfig || {};
const data = {
containerId: record.id,
cpusetCpus: hostConfig.cpusetCpus,
cpusetMems: hostConfig.cpusetMems,
cpuPeriod: hostConfig.cpuPeriod,
cpuShares: hostConfig.cpuShares,
cpuQuota: hostConfig.cpuQuota,
blkioWeight: hostConfig.blkioWeight,
memoryReservation: renderSize(hostConfig.memoryReservation, hostConfig.memoryReservation),
// Deprecated: This field is deprecated as the kernel 5.4 deprecated kmem.limit_in_bytes.
// kernelMemory: hostConfig.kernelMemory,
memory: renderSize(hostConfig.memory, hostConfig.memory),
memorySwap: renderSize(hostConfig.memorySwap, hostConfig.memorySwap),
};
this.temp = Object.assign({}, data);
}
});
},
handleEditOk() {
this.$refs["editForm"].validate((valid) => {
if (!valid) {
return false;
}
const temp = Object.assign({}, this.temp, { id: this.reqDataId });
dockerUpdateContainer(this.urlPrefix, temp).then((res) => {
if (res.code === 200) {
this.$notification.success({
message: res.msg,
});
this.editVisible = false;
}
});
});
this.temp = Object.assign({}, record);
this.editVisible = true;
// console.log(this.temp);
},
},
};

View File

@ -0,0 +1,277 @@
<template>
<div>
<!-- stats -->
<a-row>
<a-col :span="12">
<a-descriptions bordered :column="1" size="small">
<a-descriptions-item label="CPUS"> {{ (statsData.cpuStats && statsData.cpuStats.percpuUsage) || (statsData.cpuStats && statsData.cpuStats.onlineCpus) }} </a-descriptions-item>
<a-descriptions-item label="CPU %">
{{
(
((((statsData.cpuStats && statsData.cpuStats.cpuUsage && statsData.cpuStats.cpuUsage.totalUsage) || 0) -
((statsData.precpuStats && statsData.precpuStats.cpuUsage && statsData.precpuStats.cpuUsage.totalUsage) || 0)) /
((statsData.cpuStats && statsData.cpuStats.systemCpuUsage) || 0) -
((statsData.precpuStats && statsData.precpuStats.systemCpuUsage) || 0)) *
100.0
).toFixed(4)
}}
%
</a-descriptions-item>
<a-descriptions-item label="MEM USAGE">
{{ renderSize(((statsData.memoryStats && statsData.memoryStats.usage) || 0) - ((statsData.memoryStats && statsData.memoryStats.stats && statsData.memoryStats.stats.cache) || 0)) }}
</a-descriptions-item>
<a-descriptions-item label="MEM LIMIT">
{{ renderSize((statsData.memoryStats && statsData.memoryStats.limit) || 0) }}
</a-descriptions-item>
<!-- memoryRatio -->
<a-descriptions-item label="MEM %">
{{
(
((((statsData.memoryStats && statsData.memoryStats.usage) || 0) - ((statsData.memoryStats && statsData.memoryStats.stats && statsData.memoryStats.stats.cache) || 0)) /
(statsData.memoryStats && statsData.memoryStats.limit)) *
100.0
).toFixed(4)
}}
%
</a-descriptions-item>
<!-- // rx_bytes -->
<!-- // tx_bytes -->
<a-descriptions-item label="NET I/O rx">
<div :key="index" v-for="(item, index) in Object.keys(statsData.networks || {})">
<a-tooltip :title="`${item} 接收流量`">
{{ renderSize(statsData.networks[item] && statsData.networks[item].rxBytes) || 0 }}
</a-tooltip>
</div>
</a-descriptions-item>
<a-descriptions-item label="NET I/O tx">
<div :key="index" v-for="(item, index) in Object.keys(statsData.networks || {})">
<a-tooltip :title="`${item} 输出流量`">
{{ renderSize(statsData.networks[item] && statsData.networks[item].txBytes) || 0 }}
</a-tooltip>
</div>
</a-descriptions-item>
<a-descriptions-item label="BLOCK I/O">
<a-tooltip
:title="`${
(statsData.blkioStats && statsData.blkioStats.ioServiceBytesRecursive && statsData.blkioStats.ioServiceBytesRecursive[0] && statsData.blkioStats.ioServiceBytesRecursive[0].op) ||
'blkioStats'
}`"
>
{{
renderSize(
statsData.blkioStats && statsData.blkioStats.ioServiceBytesRecursive && statsData.blkioStats.ioServiceBytesRecursive[0] && statsData.blkioStats.ioServiceBytesRecursive[0].value
) || 0
}}
</a-tooltip>
/
<a-tooltip
:title="`${
(statsData.blkioStats && statsData.blkioStats.ioServiceBytesRecursive && statsData.blkioStats.ioServiceBytesRecursive[1] && statsData.blkioStats.ioServiceBytesRecursive[1].op) ||
'blkioStats'
}`"
>
{{
renderSize(
statsData.blkioStats && statsData.blkioStats.ioServiceBytesRecursive && statsData.blkioStats.ioServiceBytesRecursive[1] && statsData.blkioStats.ioServiceBytesRecursive[1].value
) || 0
}}
</a-tooltip>
</a-descriptions-item>
<!-- // 线 -->
<a-descriptions-item label="PIDS"> {{ statsData.pidsStats && statsData.pidsStats.current }} </a-descriptions-item>
</a-descriptions></a-col
>
<a-col :span="12">
<a-form-model ref="editForm" :model="temp" :label-col="{ span: 7 }" :wrapper-col="{ span: 17 }">
<a-form-model-item prop="blkioWeight">
<template slot="label">
Block IO 权重
<a-tooltip>
<template slot="title"> Block IO 权重相对权重 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input-number style="width: 100%" v-model="temp.blkioWeight" placeholder="Block IO 权重" :min="0" :max="1000" />
</a-form-model-item>
<a-form-model-item prop="cpuShares">
<template slot="label">
CPU 权重
<a-tooltip>
<template slot="title"> 一个整数值表示此容器相对于其他容器的相对 CPU 权重 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input-number style="width: 100%" v-model="temp.cpuShares" placeholder="一个整数值,表示此容器相对于其他容器的相对 CPU 权重。" />
</a-form-model-item>
<a-form-model-item prop="cpusetCpus">
<template slot="label">
执行的 CPU
<a-tooltip>
<template slot="title"> 允许执行的 CPU例如0-30,1 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input style="width: 100%" v-model="temp.cpusetCpus" placeholder="允许执行的 CPU例如0-3、0,1。" />
</a-form-model-item>
<a-form-model-item prop="cpusetMems">
<template slot="label">
CpusetMems
<a-tooltip>
<template slot="title"> 允许执行的内存节点 (MEM) (0-3, 0,1) 仅在 NUMA 系统上有效 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input style="width: 100%" v-model="temp.cpusetMems" placeholder="允许执行的内存节点 (MEM) (0-3, 0,1)。 仅在 NUMA 系统上有效。" />
</a-form-model-item>
<a-form-model-item prop="cpuPeriod">
<template slot="label">
CPU 周期
<a-tooltip>
<template slot="title"> CPU 周期的长度以微秒为单位 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input-number style="width: 100%" v-model="temp.cpuPeriod" placeholder=" CPU 周期的长度,以微秒为单位。" />
</a-form-model-item>
<a-form-model-item prop="cpuQuota">
<template slot="label">
CPU 时间
<a-tooltip>
<template slot="title"> 容器在一个 CPU 周期内可以获得的 CPU 时间的微秒 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input-number style="width: 100%" v-model="temp.cpuQuota" placeholder="容器在一个 CPU 周期内可以获得的 CPU 时间的微秒。" />
</a-form-model-item>
<a-form-model-item prop="memory">
<template slot="label">
内存
<a-tooltip>
<template slot="title"> 设置内存限制 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input style="width: 100%" v-model="temp.memory" placeholder="设置内存限制。" />
</a-form-model-item>
<a-form-model-item prop="memorySwap">
<template slot="label">
总内存
<a-tooltip>
<template slot="title"> 总内存内存 + 交换 设置为 -1 以禁用交换 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input style="width: 100%" v-model="temp.memorySwap" placeholder="总内存(内存 + 交换)。 设置为 -1 以禁用交换。" />
</a-form-model-item>
<a-form-model-item prop="memoryReservation">
<template slot="label">
软内存
<a-tooltip>
<template slot="title"> 软内存限制 </template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</template>
<a-input style="width: 100%" v-model="temp.memoryReservation" placeholder="软内存限制。" />
</a-form-model-item>
</a-form-model>
</a-col>
</a-row>
</div>
</template>
<script>
import { dockerContainerStats, dockerInspectContainer, dockerUpdateContainer } from "@/api/docker-api";
import { renderSize } from "@/utils/const";
export default {
props: {
id: {
type: String,
default: "",
},
urlPrefix: {
type: String,
},
machineDockerId: {
type: String,
default: "",
},
containerId: {
type: String,
},
},
data() {
return {
statsData: {},
temp: {},
};
},
computed: {
reqDataId() {
return this.id || this.machineDockerId;
},
},
mounted() {
this.editContainer();
},
methods: {
renderSize,
//
editContainer() {
dockerContainerStats(this.urlPrefix, {
id: this.reqDataId,
containerId: this.containerId,
}).then((res2) => {
if (res2.code === 200) {
this.statsData = (res2.data && res2.data[this.containerId]) || {};
dockerInspectContainer(this.urlPrefix, {
id: this.reqDataId,
containerId: this.containerId,
}).then((res) => {
if (res.code === 200) {
this.editVisible = true;
const hostConfig = res.data.hostConfig || {};
const data = {
containerId: this.containerId,
cpusetCpus: hostConfig.cpusetCpus,
cpusetMems: hostConfig.cpusetMems,
cpuPeriod: hostConfig.cpuPeriod,
cpuShares: hostConfig.cpuShares,
cpuQuota: hostConfig.cpuQuota,
blkioWeight: hostConfig.blkioWeight,
memoryReservation: renderSize(hostConfig.memoryReservation, hostConfig.memoryReservation),
// Deprecated: This field is deprecated as the kernel 5.4 deprecated kmem.limit_in_bytes.
// kernelMemory: hostConfig.kernelMemory,
memory: renderSize(hostConfig.memory, hostConfig.memory),
memorySwap: renderSize(hostConfig.memorySwap, hostConfig.memorySwap),
};
this.temp = Object.assign({}, data);
}
});
}
// console.log(res);
});
},
handleEditOk() {
this.$refs["editForm"].validate((valid) => {
if (!valid) {
return false;
}
const temp = Object.assign({}, this.temp, { id: this.reqDataId });
dockerUpdateContainer(this.urlPrefix, temp).then((res) => {
if (res.code === 200) {
this.$notification.success({
message: res.msg,
});
this.editVisible = false;
}
});
});
},
},
};
</script>

View File

@ -35,9 +35,18 @@
<a-tag color="red">信息丢失</a-tag>
</a-tooltip>
</template>
<template slot="tags" slot-scope="tags">
<a-tooltip
slot="tags"
slot-scope="tags"
:title="
(tags || '')
.split(':')
.filter((item) => item)
.join(',')
"
>
<a-tag v-for="item in (tags || '').split(':').filter((item) => item)" :key="item"> {{ item }}</a-tag>
</template>
</a-tooltip>
<template slot="operation" slot-scope="text, record">
<a-space>
<a-button size="small" type="primary" :disabled="!record.machineDocker || record.machineDocker.status !== 1" @click="handleConsole(record)">控制台</a-button>
@ -89,9 +98,9 @@
</a-modal>
<!-- 控制台 -->
<a-drawer destroyOnClose :title="`${temp.name} 控制台`" placement="right" :width="`${this.getCollapsed ? 'calc(100vw - 80px)' : 'calc(100vw - 200px)'}`" :visible="consoleVisible" @close="onClose">
<console v-if="consoleVisible" :visible="consoleVisible" :id="temp.id" urlPrefix="/docker"></console>
</a-drawer>
<!-- <a-drawer destroyOnClose :title="`${temp.name} 控制台`" placement="right" :width="`${this.getCollapsed ? 'calc(100vw - 80px)' : 'calc(100vw - 200px)'}`" :visible="consoleVisible" @close="onClose"> -->
<console v-if="consoleVisible" :visible="consoleVisible" :id="temp.id" urlPrefix="/docker" @close="onClose"></console>
<!-- </a-drawer> -->
<!-- 同步到其他工作空间 -->
<a-modal destroyOnClose v-model="syncToWorkspaceVisible" title="同步到其他工作空间" @ok="handleSyncToWorkspace" :maskClosable="false">
<a-alert message="温馨提示" type="warning">
@ -141,7 +150,7 @@ export default {
columns: [
{ title: "名称", dataIndex: "name", ellipsis: true, width: 100, scopedSlots: { customRender: "tooltip" } },
{ title: "host", dataIndex: "machineDocker.host", width: 120, ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "host", dataIndex: "machineDocker.host", width: 150, ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "状态", dataIndex: "machineDocker.status", ellipsis: true, align: "center", width: "100px", scopedSlots: { customRender: "status" } },
{ title: "docker版本", dataIndex: "machineDocker.dockerVersion", ellipsis: true, width: "120px", scopedSlots: { customRender: "tooltip" } },
{ title: "标签", dataIndex: "tags", width: 100, ellipsis: true, scopedSlots: { customRender: "tags" } },

View File

@ -42,9 +42,12 @@ export default {
};
},
computed: {
...mapGetters(["getLongTermToken"]),
...mapGetters(["getLongTermToken", "getWorkspaceId"]),
socketUrl() {
return getWebSocketUrl("/socket/docker_log", `userId=${this.getLongTermToken}&id=${this.id}&machineDockerId=${this.machineDockerId}&type=dockerLog&nodeId=system`);
return getWebSocketUrl(
"/socket/docker_log",
`userId=${this.getLongTermToken}&id=${this.id}&machineDockerId=${this.machineDockerId}&type=dockerLog&nodeId=system&workspaceId=${this.getWorkspaceId}`
);
},
},
mounted() {

View File

@ -29,9 +29,12 @@ export default {
return {};
},
computed: {
...mapGetters(["getLongTermToken"]),
...mapGetters(["getLongTermToken", "getWorkspaceId"]),
socketUrl() {
return getWebSocketUrl("/socket/docker_cli", `userId=${this.getLongTermToken}&id=${this.id}&machineDockerId=${this.machineDockerId}&nodeId=system&type=docker&containerId=${this.containerId}`);
return getWebSocketUrl(
"/socket/docker_cli",
`userId=${this.getLongTermToken}&id=${this.id}&machineDockerId=${this.machineDockerId}&nodeId=system&type=docker&containerId=${this.containerId}&workspaceId=${this.getWorkspaceId}`
);
},
},
mounted() {},

View File

@ -172,7 +172,7 @@ export default {
},
};
</script>
<style scoped lang="stylus">
<style scoped>
#app-layout {
min-height: 100vh;
}
@ -224,7 +224,7 @@ export default {
overflow-y: auto;
}
.layout-content-full-screen {
height calc(100vh - 120px);
height: calc(100vh - 120px);
overflow-y: scroll;
}
</style>

View File

@ -55,15 +55,15 @@
<a-divider>第三方登录</a-divider>
<!-- <a-form-model-item :wrapper-col="{ span: 24 }"> </a-form-model-item> -->
<a-form-model-item :wrapper-col="{ span: 24 }">
<a-space>
<div style="width: 32px; height: 32px" v-if="this.enabledOauth2Provides.indexOf('gitee') > -1">
<a-tooltip @click="toOauth2Url('gitee')" title="gitee"><img :src="giteeImg" style="width: 100%; height: 100%; object-fit: cover" /></a-tooltip>
<a-space :size="20">
<div class="oauth2-item" v-if="this.enabledOauth2Provides.indexOf('gitee') > -1">
<a-tooltip @click="toOauth2Url('gitee')" title="gitee"><img :src="giteeImg" /></a-tooltip>
</div>
<div style="width: 32px; height: 32px" v-if="this.enabledOauth2Provides.indexOf('maxkey') > -1">
<a-tooltip @click="toOauth2Url('maxkey')" title="maxkey"><img :src="maxkeyImg" style="width: 100%; height: 100%; object-fit: cover" /></a-tooltip>
<div class="oauth2-item" v-if="this.enabledOauth2Provides.indexOf('maxkey') > -1">
<a-tooltip @click="toOauth2Url('maxkey')" title="maxkey"><img :src="maxkeyImg" /></a-tooltip>
</div>
<div style="width: 32px; height: 32px" v-if="this.enabledOauth2Provides.indexOf('github') > -1">
<a-tooltip @click="toOauth2Url('github')" title="github"><img :src="githubImg" style="width: 100%; height: 100%; object-fit: cover" /></a-tooltip>
<div class="oauth2-item" v-if="this.enabledOauth2Provides.indexOf('github') > -1">
<a-tooltip @click="toOauth2Url('github')" title="github"><img :src="githubImg" /></a-tooltip>
</div>
</a-space>
</a-form-model-item>
@ -72,7 +72,7 @@
</template>
<template v-if="this.action === 'mfa'">
<a-form-model ref="mfaDataForm" :label-col="{ span: 0 }" :model="mfaData" :rules="rules" @submit="handleMfa">
<a-form-model-item label="验证码" :label-col="{ span: 5 }" :wrapper-col="{ span: 19 }" prop="mfaCode">
<a-form-model-item label="验证码" :label-col="{ span: 5 }" :wrapper-col="{ span: 19 }" prop="mfaCode" help="需要验证 MFA">
<a-input v-model="mfaData.mfaCode" placeholder="mfa 验证码" />
</a-form-model-item>
@ -113,7 +113,7 @@ export default {
disabledCaptcha: false,
enabledOauth2Provides: [],
maxkeyImg: require(`@/assets/images/maxkey.png`),
giteeImg: require(`@/assets/images/gitee.png`),
giteeImg: require(`@/assets/images/gitee.svg`),
githubImg: require(`@/assets/images/github.png`),
};
},
@ -350,18 +350,29 @@ export default {
.rand-code img {
width: 100%;
height: 100%;
border: 1px solid #d9d9d9;
border-radius: 4px;
display: inherit;
}
.btn-login {
width: 100%;
margin: 10px 0;
}
</style>
<style>
.ant-card-meta-title {
/deep/ .ant-card-meta-title {
font-size: 30px;
}
.ant-card-body {
/deep/ .ant-card-body {
padding: 30px;
}
.oauth2-item {
width: 40px;
height: 40px;
}
.oauth2-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>

View File

@ -514,8 +514,8 @@ export default {
syncToWorkspaceVisible: false,
columns: [
{ title: "节点名称", dataIndex: "name", width: 100, sorter: true, key: "name", ellipsis: true, scopedSlots: { customRender: "name" } },
{ title: "状态", dataIndex: "status", width: 100, ellipsis: true, scopedSlots: { customRender: "status" } },
{ title: "节点名称", dataIndex: "name", width: 200, sorter: true, key: "name", ellipsis: true, scopedSlots: { customRender: "name" } },
{ title: "状态", dataIndex: "status", width: "100px", ellipsis: true, scopedSlots: { customRender: "status" } },
{ title: "节点地址", dataIndex: "url", key: "url", width: "190px", ellipsis: true, scopedSlots: { customRender: "url" } },
{ title: "系统名", dataIndex: "osName", key: "osName", width: "100px", ellipsis: true, scopedSlots: { customRender: "osName" } },
{ title: "JDK 版本", dataIndex: "javaVersion", width: 100, key: "javaVersion", ellipsis: true, scopedSlots: { customRender: "javaVersion" } },

View File

@ -1,8 +1,15 @@
<template>
<a-layout class="node-layout" ref="nodeLayout" id="nodeLayout">
<a-layout class="node-layout" ref="nodeLayout" id="nodeLayout" :class="` ${this.scrollbarFlag ? '' : 'hide-scrollbar'}`">
<!-- 侧边栏 节点管理菜单 -->
<a-layout-sider theme="light" :class="`node-sider jpom-node-sider ${this.fullScreenFlag ? 'sider-scroll' : 'sider-full-screen'}`">
<a-menu theme="light" mode="inline" @openChange="openChange" :default-selected-keys="selectedKeys" :openKeys="openKey">
<a-layout-sider theme="light" :class="`node-sider jpom-node-sider ${this.fullScreenFlag ? 'sider-scroll' : 'sider-full-screen'} ${this.scrollbarFlag ? '' : 'hide-scrollbar'}`">
<a-menu
theme="light"
mode="inline"
@openChange="openChange"
:default-selected-keys="selectedKeys"
:openKeys="openKey"
:class="`${this.fullScreenFlag ? 'sider-scroll' : 'sider-full-screen'} ${this.scrollbarFlag ? '' : 'hide-scrollbar'}`"
>
<template v-for="(menu, index) in nodeMenuList">
<a-sub-menu v-if="menu.childs" :key="menu.id" :class="menu.id">
<span slot="title">
@ -22,7 +29,8 @@
</a-layout-sider>
<!-- 节点管理的各个组件 -->
<!-- class="layout-content jpom-node-content drawer-layout-content" -->
<a-layout-content :class="`layout-content jpom-node-content ${this.fullScreenFlag ? 'layout-content-scroll' : 'layout-content-full-screen'}`">
<a-layout-content :class="`layout-content jpom-node-content ${this.fullScreenFlag ? 'layout-content-scroll' : 'layout-content-full-screen'} ${this.scrollbarFlag ? '' : 'hide-scrollbar'}`">
<welcome v-if="currentId === 'welcome'" :node="node" />
<project-list v-if="currentId === 'manageList'" :node="node" />
@ -97,6 +105,9 @@ export default {
menuMultipleFlag() {
return this.getGuideCache.menuMultipleFlag === undefined ? true : this.getGuideCache.menuMultipleFlag;
},
scrollbarFlag() {
return this.getGuideCache.scrollbarFlag === undefined ? true : this.getGuideCache.scrollbarFlag;
},
},
watch: {},
created() {
@ -180,15 +191,14 @@ export default {
},
};
</script>
<style scoped lang="stylus">
<style scoped>
.sider-scroll {
min-height: calc(100vh -85px);
overflow-y: auto;
}
.layout-content-scroll {
min-height: calc(100vh - 85px)
min-height: calc(100vh - 85px);
overflow-y: auto;
}
@ -198,7 +208,7 @@ export default {
}
.layout-content-full-screen {
height: calc(100vh - 85px);
height: calc(100vh - 85px);
overflow-y: scroll;
}
</style>

View File

@ -249,7 +249,7 @@ export default {
batchVisible: false,
batchTitle: "",
columns: [
{ title: "项目名称", dataIndex: "name", width: 120, ellipsis: true, scopedSlots: { customRender: "name" } },
{ title: "项目名称", dataIndex: "name", width: 200, ellipsis: true, scopedSlots: { customRender: "name" } },
{ title: "项目分组", dataIndex: "group", sorter: true, width: "100px", ellipsis: true, scopedSlots: { customRender: "group" } },
{ title: "节点名称", dataIndex: "nodeId", width: 90, ellipsis: true, scopedSlots: { customRender: "nodeId" } },
{

View File

@ -557,10 +557,10 @@ export default {
},
};
</script>
<style scoped lang="stylus">
<style scoped>
.ssh-file-layout {
padding: 0;
min-height calc(100vh - 75px);
min-height: calc(100vh - 75px);
}
.dir-container {
@ -572,7 +572,7 @@ export default {
border: 1px solid #e2e2e2;
/* height: calc(100vh - 80px); */
/* overflow-y: auto; */
overflow-x: auto;
overflow-x: auto;
}
.file-content {

View File

@ -206,9 +206,9 @@
</a-form-model>
</a-modal>
<!-- 控制台 -->
<a-drawer destroyOnClose :title="`${temp.name} 控制台`" placement="right" :width="`${this.getCollapsed ? 'calc(100vw - 80px)' : 'calc(100vw - 200px)'}`" :visible="consoleVisible" @close="onClose">
<console v-if="consoleVisible" :visible="consoleVisible" :machineDockerId="temp.id" urlPrefix="/system/assets/docker"></console>
</a-drawer>
<!-- <a-drawer destroyOnClose :title="`${temp.name} 控制台`" placement="right" :width="`${this.getCollapsed ? 'calc(100vw - 80px)' : 'calc(100vw - 200px)'}`" :visible="consoleVisible" @close="onClose"> -->
<console v-if="consoleVisible" :visible="consoleVisible" :machineDockerId="temp.id" urlPrefix="/system/assets/docker" @close="onClose"></console>
<!-- </a-drawer> -->
<!-- 集群控制台 -->
<a-drawer
destroyOnClose