DKube v1.0.0

This commit is contained in:
DingQz 2022-10-12 13:40:52 +08:00
parent 3efdc60270
commit 085d389c05
41 changed files with 36352 additions and 1 deletions

@ -1 +0,0 @@
Subproject commit 638b5bdd3c27c0b40cfd5018376a02312e41ccd3

23
dkube-web/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

66
dkube-web/README.md Normal file
View File

@ -0,0 +1,66 @@
<div style="text-align: center"></div>
<p align="center">
<img src="https://user-images.githubusercontent.com/42825450/193592031-49863a65-3f0e-4f94-bf98-82dac27b9d58.jpg" width="250px" height="220px">
<br>
<i>Make the project development and release simpler, easier and more efficient.</i>
</p>
</div>
## What is DKube
This is a K8s cluster management platformDKube Provides a wizard-style operation interface for K8s cluster management to help your team manage your cluster environment quickly and easily
## 功能
<details>
<summary><b> K8s集群管理</b></summary>
</details>
<details>
<summary><b> 平台化界面</b></summary>
</details>
<details>
<summary><b> 更加便捷的管理K8s</b></summary>
</details>
<details>
<summary><b> 支持YAML信息查看\变更</b></summary>
</details>
<details>
<summary><b> 平台化管理控制器</b></summary>
</details>
<details>
<summary><b> 实时查看容器日志</b></summary>
</details>
## 截图
<br/>
<table>
<tr>
<td width="50%" align="center"><b>登入认证管理</b></td>
<td width="50%" align="center"><b>集群信息状态</b></td>
</tr>
<tr>
<td width="50%" align="center"><img src="https://user-images.githubusercontent.com/42825450/193593148-4d258b30-b972-4583-b359-32978a8a8637.jpg?raw=true"></td>
<td width="50%" align="center"><img src="https://user-images.githubusercontent.com/42825450/193593170-3373dabd-8d5d-4a01-a59f-49851f11f433.jpg?raw=true"></td>
</tr>
<tr>
<td width="50%" align="center"><b>节点资源管理</b></td>
<td width="50%" align="center"><b>名称空间管理</b></td>
</tr>
<td width="50%" align="center"><img src="https://user-images.githubusercontent.com/42825450/193593569-daebc649-f6c4-45a2-88f6-2aa4860c3dea.jpg?raw=true"></td>
<td width="50%" align="center"><img src="https://user-images.githubusercontent.com/42825450/193593579-e0539ab0-6b22-4060-b254-c6495fb87cbd.jpg?raw=true"></td>
<tr>
</tr>
<tr>
<td width="50%" align="center"><b>YAML信息管理</b></td>
<td width="50%" align="center"><b>Pod副本管理</b></td>
</tr>
<td width="50%" align="center"><img src="https://user-images.githubusercontent.com/42825450/193593867-4a98bd0f-a910-4b90-92e3-6a3164d0c241.jpg?raw=true"></td>
<td width="50%" align="center"><img src="https://user-images.githubusercontent.com/42825450/193593871-ee004cb8-42cb-427a-a0cc-fa1e15e7d466.jpg?raw=true"></td>
<tr>
</tr>
</table>

View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

19
dkube-web/jsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

29239
dkube-web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

55
dkube-web/package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "k8s-demo-fe",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@element-plus/icons-vue": "^1.1.4",
"axios": "^0.26.1",
"codemirror-editor-vue3": "^2.1.3",
"core-js": "^3.6.5",
"echarts": "^5.3.2",
"element-plus": "^2.1.5",
"json-editor-vue3": "^1.0.5",
"json2yaml": "^1.1.0",
"jsonwebtoken": "^8.5.1",
"moment": "^2.29.2",
"nprogress": "^0.2.0",
"vue": "^3.0.0",
"vue-router": "^4.0.14",
"xterm": "^4.18.0",
"xterm-addon-fit": "^0.5.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.15",
"@vue/cli-plugin-eslint": "~4.5.15",
"@vue/cli-service": "~4.5.15",
"@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

15
dkube-web/src/App.vue Normal file
View File

@ -0,0 +1,15 @@
<template>
<router-view></router-view>
</template>
<style>
html,body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
#nprogress .bar {
background: #2186c0 !important;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -0,0 +1,58 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -0,0 +1,219 @@
<template>
<div class="common-layout">
<el-container style="height: 100vh;">
<el-aside class="aside" :width="asideWidth">
<el-affix class="aside-affix" :z-index="1200">
<div class="aside-logo" >
<el-image class="logo-image" :src="logo" />
<span :class="[isCollapse ? 'is-collapse' : '']">
<span class="logo-name" >DKube</span>
</span>
</div>
</el-affix>
<el-menu class="aside-menu"
router
:default-active="$route.path"
:collapse="isCollapse"
background-color="#131b27"
text-color="#bfcbd9"
active-text-color="#20a0ff">
<div v-for="menu in routers" :key="menu">
<el-menu-item class="aside-menu-item" v-if="menu.children && menu.children.length == 1" :index="menu.children[0].path">
<el-icon><component :is="menu.children[0].icon" /></el-icon>
<template #title>
{{menu.children[0].name}}
</template>
</el-menu-item>
<el-sub-menu class="aside-submenu" v-else-if="menu.children" :index="menu.path">
<template #title>
<el-icon><component :is="menu.icon" /></el-icon>
<span :class="[isCollapse ? 'is-collapse' : '']">{{menu.name}}</span>
</template>
<el-menu-item class="aside-menu-childitem" v-for="child in menu.children" :key="child" :index="child.path">
<el-icon><component :is="child.icon" /></el-icon>
<template #title>
{{child.name}}
</template>
</el-menu-item>
</el-sub-menu>
</div>
</el-menu>
</el-aside>
<el-container>
<el-header class="header" >
<el-row :gutter="20">
<el-col :span="1">
<div class="header-collapse" @click="onCollapse">
<el-icon><component :is="isCollapse ? 'expand':'fold'" /></el-icon>
</div>
</el-col>
<el-col :span="10" >
<div class="header-breadcrumb">
<el-breadcrumb separator="/" v-if="this.$route.matched[0].path != '/main'">
<el-breadcrumb-item :to="{ path: '/' }">工作台</el-breadcrumb-item>
<template v-for="(matched,m) in this.$route.matched" :key="m">
<el-breadcrumb-item v-if="matched.name != undefined" >
{{ matched.name }}
</el-breadcrumb-item>
</template>
</el-breadcrumb>
<el-breadcrumb separator="/" v-else>
<el-breadcrumb-item>工作台</el-breadcrumb-item>
</el-breadcrumb>
</div>
</el-col>
<el-col class="header-menu" :span="13">
<el-dropdown>
<div class="header-dropdown">
<el-image class="avator-image" :src="avator" />
<span>{{ username }}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="el-icon-switch-button" @click="logout()">退出</el-dropdown-item>
<el-dropdown-item icon="el-icon-unlock">修改密码</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
</el-row>
</el-header>
<el-main class="main">
<router-view></router-view>
</el-main>
<el-footer class="footer">
<el-icon style="width:2em;top:3px;font-size:18px"><place/></el-icon>
<a class="footer el-icon-place">2022 DevOps </a>
</el-footer>
<el-backtop target=".el-main"></el-backtop>
</el-container>
</el-container>
</div>
</template>
<script>
import {useRouter} from 'vue-router'
export default {
data() {
return {
avator: require('@/assets/avator/avator.png'),
logo: require('@/assets/k8s/k8s-metrics.png'),
isCollapse: false,
asideWidth: '220px',
routers: [],
}
},
computed: {
username() {
let username = localStorage.getItem('username');
return username ? username : '未知';
},
},
methods: {
onCollapse() {
if (this.isCollapse) {
this.asideWidth = '220px'
this.isCollapse = false
} else {
this.isCollapse = true
this.asideWidth = '64px'
}
},
logout() {
localStorage.removeItem('username');
localStorage.removeItem('token');
this.$router.push('/login');
}
},
beforeMount() {
this.routers = useRouter().options.routes
}
}
</script>
<style scoped>
.aside {
transition: all .5s;
background-color: #131b27;
}
.aside-logo {
background-color: #131b27;
height: 60px;
color: white;
cursor: pointer;
}
.logo-image {
width: 40px;
height: 40px;
top: 12px;
padding-left: 12px;
}
.logo-name {
font-size: 20px;
font-weight: bold;
padding: 10px;
}
.aside::-webkit-scrollbar {
display: none;
}
.aside-affix {
border-bottom-width: 0;
}
.aside-menu {
border-right-width: 0;
}
.aside-menu-item.is-active {
background-color: #1f2a3a;
}
.aside-menu-item {
padding-left: 20px !important;
}
.aside-menu-childitem {
padding-left: 20px !important;
}
.aside-menu-childitem.is-active {
background-color: #1f2a3a;
}
.aside-menu-childitem:hover {
background-color: #142c4e;
}
.header {
z-index:1200;
line-height: 60px;
font-size: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, .12),0 0 6px rgba(0, 0, 0, .04)
}
.header-collapse {
cursor: pointer;
}
.header-breadcrumb {
padding-top: 0.9em;
}
.header-menu {
text-align: right;
}
.is-collapse {
display: none;
}
.header-dropdown {
line-height: 60px;
cursor: pointer;
}
.avator-image {
top: 12px;
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 8px;
}
.main {
padding: 10px;
}
.footer {
z-index: 1200;
color: rgb(187, 184, 184);
font-size: 14px;
text-align: center;
line-height: 60px;
}
</style>

18
dkube-web/src/main.js Normal file
View File

@ -0,0 +1,18 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ELIcons from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
import { GlobalCmComponent } from "codemirror-editor-vue3";
import 'codemirror/theme/idea.css'
import 'codemirror/mode/yaml/yaml.js'
const app = createApp(App)
for (let iconName in ELIcons) {
app.component(iconName, ELIcons[iconName])
}
app.use(ElementPlus)
app.use(GlobalCmComponent, { componentName: "codemirror" });
app.use(router)
app.mount('#app')

View File

@ -0,0 +1,221 @@
import {createRouter,createWebHistory} from 'vue-router'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import Layout from "@/layout/Layout"
import jwt from 'jsonwebtoken'
const routes =[
{
path: '/login',
component: () => import('@/views/login/Login.vue'),
icon: "odometer",
meta: {title: "登录", requireAuth: false},
},
{
path: '/',
redirect: '/home'
},
{
path: '/home',
component: Layout,
icon: "odometer",
children: [
{
path: "/home",
name: "K8s概览",
icon: "odometer",
meta: {title: "K8s概览", requireAuth: true},
component: () => import('@/views/home/Home.vue'),
}
]
},
{
path: '/workflow',
component: Layout,
icon: "VideoPlay",
children: [
{
path: "/workflow",
name: "工作流",
icon: "VideoPlay",
meta: {title: "工作流", requireAuth: true},
component: () => import('@/views/workflow/Workflow.vue')
}
]
},
{
path: "/cluster",
name: "集群信息",
component: Layout,
icon: "home-filled",
meta: {title: "集群信息", requireAuth: true},
children: [
{
path: "/cluster/node",
name: "Node",
icon: "el-icon-s-data",
meta: {title: "Node", requireAuth: true},
component: () => import("@/views/node/Node.vue")
},
{
path: "/cluster/namespace",
name: "Namespace",
icon: "el-icon-document-add",
meta: {title: "Namespace", requireAuth: true},
component: () => import("@/views/namespace/Namespace.vue")
},
{
path: "/cluster/persistentvolume",
name: "PersistentVolume",
icon: "el-icon-document-add",
meta: {title: "PersistemtVolume", requireAuth: true},
component: () => import("@/views/persistentvolume/PersistentVolume.vue")
}
]
},
{
path: '/workload',
name: '工作负载',
component: Layout,
icon: "menu",
meta: {title: "工作负载", requireAuth: true},
children: [
{
path: '/workload/deployment',
name: 'Deployment',
icon: 'el-icon-s-data',
meta: {title: "Deployment", requireAuth: true},
component: () => import("@/views/deployment/Deployment.vue")
},
{
path: '/workload/pod',
name: 'Pod',
icon: 'el-icon-document-add',
meta: {title: "Pod", requireAuth: true},
component: () => import("@/views/pod/Pod.vue")
},
{
path: '/workload/daemonset',
name: 'DaemonSet',
icon: 'el-icon-document-add',
meta: {title: "DaemonSet", requireAuth: true},
component: () => import("@/views/daemonset/DaemonSet.vue")
},
{
path: '/workload/statefulset',
name: 'StatefulSet',
icon: 'el-icon-document-add',
meta: {title: "StatefulSet", requireAuth: true},
component: () => import("@/views/statefulset/StatefulSet.vue")
},
]
},
{
path: "/loadbalance",
name: "负载均衡",
component: Layout,
icon: "files",
meta: {title: "负载均衡", requireAuth: true},
children: [
{
path: "/loadbalance/service",
name: "Service",
icon: "el-icon-s-data",
meta: {title: "Service", requireAuth: true},
component: () => import("@/views/service/Service.vue")
},
{
path: "/loadbalance/ingress",
name: "Ingress",
icon: "el-icon-document-add",
meta: {title: "Ingress", requireAuth: true},
component: () => import("@/views/ingress/Ingress.vue")
}
]
},
{
path: "/storage",
name: "存储与配置",
component: Layout,
icon: "tickets",
meta: {title: "存储与配置", requireAuth: true},
children: [
{
path: "/storage/configmap",
name: "Configmap",
icon: "el-icon-document-add",
meta: {title: "Configmap", requireAuth: true},
component: () => import("@/views/configmap/ConfigMap.vue")
},
{
path: "/storage/secret",
name: "Secret",
icon: "el-icon-document-add",
meta: {title: "Secret", requireAuth: true},
component: () => import("@/views/secret/Secret.vue")
},
{
path: "/storage/persistentvolumeclaim",
name: "PersistentVolumeClaim",
icon: "el-icon-s-data",
meta: {title: "PersistentVolumeClaim", requireAuth: true},
component: () => import("@/views/persistentvolumeclaim/PersistentVolumeClaim.vue")
},
]
},
{
path: '/404',
component: () => import('@/views/common/404.vue'),
meta: {
title: '404'
}
},
{
path: '/403',
component: () => import('@/views/common/403.vue'),
meta: {
title: '403'
}
},
{
path: '/:pathMatch(.*)',
redirect: '/404'
},
]
const router = createRouter ({
history: createWebHistory(),
routes
})
NProgress.inc(100)
NProgress.configure({ easing: 'ease', speed: 600,showSpinner: false})
router.beforeEach((to,from,next) => {
NProgress.start()
if (to.meta.title) {
document.title = to.meta.title
} else {
document.title = "DKube"
}
next()
})
router.beforeEach((to, from, next) => {
jwt.verify(localStorage.getItem('token'), 'devops', function (err) {
if (to.path === '/login') {
next()
} else if (err) {
next('/login');
} else {
next();
}
});
});
router.afterEach (() => {
NProgress.done()
})
export default router

View File

@ -0,0 +1,44 @@
import axios from 'axios';
const httpClient = axios.create({
validateStatus(status) {
return status >= 200 && status < 504
},
timeout: 10000
});
httpClient.defaults.retry = 3
httpClient.defaults.retryDelay = 1000
httpClient.defaults.shouldRetry = true
httpClient.interceptors.request.use (
config => {
config.headers['Content-Type'] = 'application/json'
config.headers['Accept-Language'] = 'zh-CN'
config.headers['Authorization'] = localStorage.getItem('token')
if (config.method === 'post') {
if (!config.data) {
config.data = {}
}
}
return config
},
err => {
Promise.reject(err)
}
);
httpClient.interceptors.response.use (
response => {
if (response.status !== 200) {
return Promise.reject(response.data)
} else {
return response.data
}
},
err => {
return Promise.reject(err)
}
);
export default httpClient;

View File

@ -0,0 +1,44 @@
<template>
<div class="main-body-div">
<el-row>
<el-col :span="24">
<div>
<img class="main-body-img" src="../../assets/img/403.png" />
</div>
</el-col>
<el-col :span="24">
<div>
<p class="status-code">403</p>
<p class="status-describe">你暂时无权限访问该页面······</p>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.main-body-img {
margin-top: 150px
}
.main-body-div {
text-align: center;
height: 100vh;
width: 100vw;
}
.status-code {
margin-top: 20px;
margin-bottom: 10px;
font-size: 95px;
font-weight: bold;
color: rgb(54, 95, 230);
}
.status-describe {
color: rgb(145, 143, 143);
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<div class="main-body-div">
<el-row>
<el-col :span="24">
<div>
<img class="main-body-img" src="../../assets/img/404.png" />
</div>
</el-col>
<el-col :span="24">
<div>
<p class="status-code">404</p>
<p class="status-describe">你所访问的页面不存在······</p>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.main-body-img {
margin-top: 15%;
}
.main-body-div {
text-align: center;
height: 100vh;
width: 100vw;
}
.status-code {
margin: 20px 0 20px 0;
font-size: 95px;
font-weight: bold;
color: rgb(54, 95, 230);
}
.status-describe {
color: rgb(145, 143, 143);
}
</style>

View File

@ -0,0 +1,72 @@
export default {
loginAuth: 'http://localhost:9090/api/login',
k8sWorkflowCreate: 'http://localhost:9090/api/k8s/workflow/create',
k8sWorkflowDetail: 'http://localhost:9090/api/k8s/workflow/detail',
k8sWorkflowList: 'http://localhost:9090/api/k8s/workflows',
k8sWorkflowDel: 'http://localhost:9090/api/k8s/workflow/del',
k8sDeploymentList: 'http://localhost:9090/api/k8s/deployments',
k8sDeploymentDetail: 'http://localhost:9090/api/k8s/deployment/detail',
k8sDeploymentUpdate: 'http://localhost:9090/api/k8s/deployment/update',
k8sDeploymentScale: 'http://localhost:9090/api/k8s/deployment/scale',
k8sDeploymentRestart: 'http://localhost:9090/api/k8s/deployment/restart',
k8sDeploymentDel: 'http://localhost:9090/api/k8s/deployment/del',
k8sDeploymentCreate: 'http://localhost:9090/api/k8s/deployment/create',
k8sDeploymentNumNp: 'http://localhost:9090/api/k8s/deployment/numnp',
k8sPodList: 'http://localhost:9090/api/k8s/pods',
k8sPodDetail: 'http://localhost:9090/api/k8s/pod/detail',
k8sPodUpdate: 'http://localhost:9090/api/k8s/pod/update',
k8sPodDel: 'http://localhost:9090/api/k8s/pod/del',
k8sPodContainer: 'http://localhost:9090/api/k8s/pod/container',
k8sPodLog: 'http://localhost:9090/api/k8s/pod/log',
k8sPodNumNp: 'http://localhost:9090/api/k8s/pod/numnp',
k8sDaemonSetList: 'http://localhost:9090/api/k8s/daemonsets',
k8sDaemonSetDetail: 'http://localhost:9090/api/k8s/daemonset/detail',
k8sDaemonSetUpdate: 'http://localhost:9090/api/k8s/daemonset/update',
k8sDaemonSetDel: 'http://localhost:9090/api/k8s/daemonset/del',
k8sStatefulSetList: 'http://localhost:9090/api/k8s/statefulsets',
k8sStatefulSetDetail: 'http://localhost:9090/api/k8s/daemonset/detail',
k8sStatefulSetUpdate: 'http://localhost:9090/api/k8s/daemonset/update',
k8sStatefulSetDel: 'http://localhost:9090/api/k8s/daemonset/del',
k8sServiceList: 'http://localhost:9090/api/k8s/services',
k8sServiceDetail: 'http://localhost:9090/api/k8s/service/detail',
k8sServiceUpdate: 'http://localhost:9090/api/k8s/service/update',
k8sServiceDel: 'http://localhost:9090/api/k8s/service/del',
k8sServiceCreate: 'http://localhost:9090/api/k8s/service/create',
k8sIngressList: 'http://localhost:9090/api/k8s/ingresses',
k8sIngressDetail: 'http://localhost:9090/api/k8s/ingress/detail',
k8sIngressUpdate: 'http://localhost:9090/api/k8s/ingress/update',
k8sIngressDel: 'http://localhost:9090/api/k8s/ingress/del',
k8sIngressCreate: 'http://localhost:9090/api/k8s/ingress/create',
k8sConfigMapList: 'http://localhost:9090/api/k8s/configmaps',
k8sConfigMapDetail: 'http://localhost:9090/api/k8s/configmap/detail',
k8sConfigMapUpdate: 'http://localhost:9090/api/k8s/configmap/update',
k8sConfigMapDel: 'http://localhost:9090/api/k8s/configmap/del',
k8sSecretList: 'http://localhost:9090/api/k8s/secrets',
k8sSecretDetail: 'http://localhost:9090/api/k8s/secret/detail',
k8sSecretUpdate: 'http://localhost:9090/api/k8s/secret/update',
k8sSecretDel: 'http://localhost:9090/api/k8s/secret/del',
k8sPvcList: 'http://localhost:9090/api/k8s/pvcs',
k8sPvcDetail: 'http://localhost:9090/api/k8s/pvc/detail',
k8sPvcUpdate: 'http://localhost:9090/api/k8s/pvc/update',
k8sPvcDel: 'http://localhost:9090/api/k8s/pvc/del',
k8sNodeList: 'http://localhost:9090/api/k8s/nodes',
k8sNodeDetail: 'http://localhost:9090/api/k8s/node/detail',
k8sNamespaceList: 'http://localhost:9090/api/k8s/namespaces',
k8sNamespaceDetail: 'http://localhost:9090/api/k8s/namespace/detail',
k8sNamespaceDel: 'http://localhost:9090/api/k8s/namespace/del',
k8sPvList: 'http://localhost:9090/api/k8s/pvs',
k8sPvDetail: 'http://localhost:9090/api/k8s/pv/detail',
k8sTerminalWs: 'ws://localhost:8081/ws',
cmOptions: {
mode: 'text/yaml',
theme: 'idea',
lineNumbers: true,
smartIndent: true,
indentUnit: 4,
styleActiveLine: true,
matchBrackets: true,
readOnly: false,
lineWrapping: true
}
}

View File

@ -0,0 +1,365 @@
<template>
<div class="configmap">
<el-row>
<el-col :span="24">
<div>
<el-card class="configmap-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="6">
<div>
<span>命名空间: </span>
<el-select v-model="namespaceValue" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</div>
</el-col>
<el-col :span="2" :offset="16">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getConfigMaps()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="configmap-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="configmap-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getConfigMaps()">搜索</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="configmap-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="configMapList"
v-loading="appLoading">
<el-table-column width="20"></el-table-column>
<el-table-column align=left label="ConfigMap名">
<template v-slot="scope">
<a class="configmap-body-configmapname">{{ scope.row.metadata.name }}</a>
</template>
</el-table-column>
<el-table-column align=center label="标签">
<template v-slot="scope">
<div v-for="(val, key) in scope.row.metadata.labels" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="key + ':' + val">
<template #reference>
<el-tag style="margin-bottom: 5px" type="warning">{{ ellipsis(key + ":" + val) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="DATA">
<template v-slot="scope">
<el-popover
style="overflow:auto"
placement="right"
:width="400"
trigger="click">
<div style="overflow-y:auto;max-height:500px;">
<span>{{ scope.row.data }}</span>
</div>
<template #reference>
<el-icon style="font-size:18px;cursor:pointer;"><reading/></el-icon>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="操作" width="200">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getConfigMapDetail(scope)">YAML</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delConfigMap)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="configmap-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="configMapTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="yamlDialog = false"> </el-button>
<el-button type="primary" @click="updateConfigMap()"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
configMapList: [],
configMapTotal: 0,
getConfigMapsData: {
url: common.k8sConfigMapList,
params: {
filter_name: '',
namespace: '',
page: '',
limit: '',
}
},
configMapDetail: {},
getConfigMapDetailData: {
url: common.k8sConfigMapDetail,
params: {
configmap_name: '',
namespace: ''
}
},
yamlDialog: false,
updateConfigMapData: {
url: common.k8sConfigMapUpdate,
params: {
namespace: '',
content: ''
}
},
delConfigMapData: {
url: common.k8sconfigmapDel,
params: {
configmap_name: '',
namespace: '',
}
}
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getConfigMaps()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getConfigMaps()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
restartTotal(e) {
let index, sum = 0
let containerStatuses = e.row.status.containerStatuses
for ( index in containerStatuses) {
sum = sum + containerStatuses[index].restartCount
}
return sum
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getConfigMaps() {
this.appLoading = true
this.getConfigMapsData.params.filter_name = this.searchInput
this.getConfigMapsData.params.namespace = this.namespaceValue
this.getConfigMapsData.params.page = this.currentPage
this.getConfigMapsData.params.limit = this.pagesize
httpClient.get(this.getConfigMapsData.url, {params: this.getConfigMapsData.params})
.then(res => {
this.configMapList = res.data.items
this.configMapTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
getConfigMapDetail(e) {
this.getConfigMapDetailData.params.configmap_name = e.row.metadata.name
this.getConfigMapDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getConfigMapDetailData.url, {params: this.getConfigMapDetailData.params})
.then(res => {
this.configMapDetail = res.data
this.contentYaml = this.transYaml(this.configMapDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updateConfigMap() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updateConfigMapData.params.namespace = this.namespaceValue
this.updateConfigMapData.params.content = content
httpClient.put(this.updateConfigMapData.url, this.updateConfigMapData.params)
.then(res => {
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
delConfigMap(e) {
this.delConfigMapData.params.configmap_name = e.row.metadata.name
this.delConfigMapData.params.namespace = this.namespaceValue
httpClient.delete(this.delConfigMapData.url, {data: this.delConfigMapData.params})
.then(res => {
this.getConfigMaps()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
},
watch: {
namespaceValue: {
handler() {
localStorage.setItem('namespace', this.namespaceValue)
this.currentPage = 1
this.getConfigMaps()
}
},
},
beforeMount() {
if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
this.namespaceValue = localStorage.getItem('namespace')
}
this.getNamespaces()
this.getConfigMaps()
}
}
</script>
<style scoped>
.configmap-head-card,.configmap-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.configmap-head-search {
width:160px;
margin-right:10px;
}
.configmap-body-configmapname {
color: #4795EE;
}
.configmap-body-configmapname:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,369 @@
<template>
<div class="daemonset">
<el-row>
<el-col :span="24">
<div>
<el-card class="daemonset-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="6">
<div>
<span>命名空间: </span>
<el-select v-model="namespaceValue" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</div>
</el-col>
<el-col :span="2" :offset="16">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getDaemonSets()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="daemonset-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="daemonset-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getDaemonSets()">搜索</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="daemonset-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="daemonSetList"
v-loading="appLoading">
<el-table-column width="20"></el-table-column>
<el-table-column align=left label="DaemonSet名">
<template v-slot="scope">
<a class="daemonset-body-daemonsetname">{{ scope.row.metadata.name }}</a>
</template>
</el-table-column>
<el-table-column align=center label="标签">
<template v-slot="scope">
<div v-for="(val, key) in scope.row.metadata.labels" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="key + ':' + val">
<template #reference>
<el-tag style="margin-bottom: 5px" type="warning">{{ ellipsis(key + ":" + val) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="容器组">
<template v-slot="scope">
<span>{{ scope.row.status.numberAvailable>0?scope.row.status.numberAvailable:0 }} / {{ scope.row.status.desiredNumberScheduled>0?scope.row.status.desiredNumberScheduled:0 }} </span>
</template>
</el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="镜像">
<template v-slot="scope">
<div v-for="(val, key) in scope.row.spec.template.spec.containers" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="val.image">
<template #reference>
<el-tag style="margin-bottom: 5px">{{ ellipsis(val.image.split('/')[2]==undefined?val.image:val.image.split('/')[2]) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="操作" width="200">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getDaemonSetDetail(scope)">YAML</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delDaemonSet)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="daemonset-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="daemonSetTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="yamlDialog = false"> </el-button>
<el-button type="primary" @click="updateDaemonSet()"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
daemonSetList: [],
daemonSetTotal: 0,
getDaemonSetsData: {
url: common.k8sDaemonSetList,
params: {
filter_name: '',
namespace: '',
page: '',
limit: '',
}
},
daemonSetDetail: {},
getDaemonSetDetailData: {
url: common.k8sDaemonSetDetail,
params: {
daemonset_name: '',
namespace: ''
}
},
yamlDialog: false,
updateDaemonSetData: {
url: common.k8sDaemonSetUpdate,
params: {
namespace: '',
content: ''
}
},
delDaemonSetData: {
url: common.k8sdaemonsetDel,
params: {
daemonset_name: '',
namespace: '',
}
}
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getDaemonSets()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getDaemonSets()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
restartTotal(e) {
let index, sum = 0
let containerStatuses = e.row.status.containerStatuses
for ( index in containerStatuses) {
sum = sum + containerStatuses[index].restartCount
}
return sum
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getDaemonSets() {
this.appLoading = true
this.getDaemonSetsData.params.filter_name = this.searchInput
this.getDaemonSetsData.params.namespace = this.namespaceValue
this.getDaemonSetsData.params.page = this.currentPage
this.getDaemonSetsData.params.limit = this.pagesize
httpClient.get(this.getDaemonSetsData.url, {params: this.getDaemonSetsData.params})
.then(res => {
this.daemonSetList = res.data.items
this.daemonSetTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
getDaemonSetDetail(e) {
this.getDaemonSetDetailData.params.daemonset_name = e.row.metadata.name
this.getDaemonSetDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getDaemonSetDetailData.url, {params: this.getDaemonSetDetailData.params})
.then(res => {
this.daemonSetDetail = res.data
this.contentYaml = this.transYaml(this.daemonSetDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updateDaemonSet() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updateDaemonSetData.params.namespace = this.namespaceValue
this.updateDaemonSetData.params.content = content
httpClient.put(this.updateDaemonSetData.url, this.updateDaemonSetData.params)
.then(res => {
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
delDaemonSet(e) {
this.delDaemonSetData.params.daemonset_name = e.row.metadata.name
this.delDaemonSetData.params.namespace = this.namespaceValue
httpClient.delete(this.delDaemonSetData.url, {data: this.delDaemonSetData.params})
.then(res => {
this.getDaemonSets()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
},
watch: {
namespaceValue: {
handler() {
localStorage.setItem('namespace', this.namespaceValue)
this.currentPage = 1
this.getDaemonSets()
}
},
},
beforeMount() {
if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
this.namespaceValue = localStorage.getItem('namespace')
}
this.getNamespaces()
this.getDaemonSets()
}
}
</script>
<style scoped>
.daemonset-head-card,.daemonset-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.daemonset-head-search {
width:160px;
margin-right:10px;
}
.daemonset-body-daemonsetname {
color: #4795EE;
}
.daemonset-body-daemonsetname:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,611 @@
<template>
<div class="deploy">
<el-row>
<el-col :span="24">
<div>
<el-card class="deploy-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="6">
<div>
<span>命名空间: </span>
<el-select v-model="namespaceValue" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</div>
</el-col>
<el-col :span="2" :offset="16">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getDeployments()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="deploy-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button style="border-radius:2px;" icon="Edit" type="primary" @click="createDeploymentDrawer = true" v-loading.fullscreen.lock="fullscreenLoading">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="deploy-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getDeployments()">搜索</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="deploy-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="deploymentList"
v-loading="appLoading">
<el-table-column width="20"></el-table-column>
<el-table-column align=left label="Deployment名">
<template v-slot="scope">
<a class="deploy-body-deployname">{{ scope.row.metadata.name }}</a>
</template>
</el-table-column>
<el-table-column align=center label="标签">
<template v-slot="scope">
<div v-for="(val, key) in scope.row.metadata.labels" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="key + ':' + val">
<template #reference>
<el-tag style="margin-bottom: 5px" type="warning">{{ ellipsis(key + ":" + val) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="容器组">
<template v-slot="scope">
<span>{{ scope.row.status.availableReplicas>0?scope.row.status.availableReplicas:0 }} / {{ scope.row.spec.replicas>0?scope.row.spec.replicas:0 }} </span>
</template>
</el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="镜像">
<template v-slot="scope">
<div v-for="(val, key) in scope.row.spec.template.spec.containers" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="val.image">
<template #reference>
<el-tag style="margin-bottom: 5px">{{ ellipsis(val.image.split('/')[2]==undefined?val.image:val.image.split('/')[2]) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="操作" width="400">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getDeploymentDetail(scope)">YAML</el-button>
<el-button size="small" style="border-radius:2px;" icon="Plus" type="primary" @click="handleScale(scope)">扩缩</el-button>
<el-button size="small" style="border-radius:2px;" icon="RefreshLeft" type="primary" @click="handleConfirm(scope, '重启', restartDeployment)">重启</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delDeployment)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="deploy-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="deploymentTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-drawer
v-model="createDeploymentDrawer"
:direction="direction"
:before-close="handleClose">
<template #title>
<h4>创建Deployment</h4>
</template>
<template #default>
<el-row type="flex" justify="center">
<el-col :span="20">
<el-form ref="createDeployment" :rules="createDeploymentRules" :model="createDeployment" label-width="80px">
<el-form-item class="deploy-create-form" label="名称" prop="name">
<el-input v-model="createDeployment.name"></el-input>
</el-form-item>
<el-form-item class="deploy-create-form" label="命名空间" prop="namespace">
<el-select v-model="createDeployment.namespace" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</el-form-item>
<el-form-item class="deploy-create-form" label="副本数" prop="replicas">
<el-input-number v-model="createDeployment.replicas" :min="1" :max="10"></el-input-number>
<el-popover
placement="top"
:width="100"
trigger="hover"
content="申请副本数上限为10个">
<template #reference>
<el-icon style="width:2em;font-size:18px;color:#4795EE"><WarningFilled/></el-icon>
</template>
</el-popover>
</el-form-item>
<el-form-item class="deploy-create-form" label="镜像" prop="image">
<el-input v-model="createDeployment.image"></el-input>
</el-form-item>
<el-form-item class="deploy-create-form" label="标签" prop="label_str">
<el-input v-model="createDeployment.label_str" placeholder="示例: project=ms,app=gateway"></el-input>
</el-form-item>
<el-form-item class="deploy-create-form" label="资源配额" prop="resource">
<el-select v-model="createDeployment.resource" placeholder="请选择">
<el-option value="0.5/1" label="0.5C1G"></el-option>
<el-option value="1/2" label="1C2G"></el-option>
<el-option value="2/4" label="2C4G"></el-option>
<el-option value="4/8" label="4C8G"></el-option>
</el-select>
</el-form-item>
<el-form-item class="deploy-create-form" label="容器端口" prop="container_port">
<el-input v-model="createDeployment.container_port" placeholder="示例: 80"></el-input>
</el-form-item>
<el-form-item class="deploy-create-form" label="健康检查" prop="health">
<el-switch v-model="createDeployment.health_check" />
</el-form-item>
<el-form-item class="deploy-create-form" label="检查路径" prop="healthPath">
<el-input v-model="createDeployment.health_path" placeholder="示例: /health"></el-input>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<template #footer>
<el-button @click="createDeploymentDrawer = false">取消</el-button>
<el-button type="primary" @click="submitForm('createDeployment')">立即创建</el-button>
</template>
</el-drawer>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="2%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="this.yamlDialog = false"> </el-button>
<el-button type="primary" @click="updateDeployment()"> </el-button>
</span>
</template>
</el-dialog>
<el-dialog title="副本数调整" v-model="scaleDialog" width="25%">
<div style="text-align:center">
<span>实例数: </span>
<el-input-number :step="1" v-model="scaleNum" :min="0" :max="30" label="描述文字"></el-input-number>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="scaleDialog = false"> </el-button>
<el-button type="primary" @click="scaleDeployment()"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
deploymentList: [],
deploymentTotal: 0,
getDeploymentsData: {
url: common.k8sDeploymentList,
params: {
filter_name: '',
namespace: '',
page: '',
limit: '',
}
},
fullscreenLoading: false,
direction: 'rtl',
createDeploymentDrawer: false,
createDeployment: {
name: '',
namespace: '',
replicas: 1,
image: '',
resource: '',
health_check: false,
health_path: '',
label_str: '',
label: {},
container_port: ''
},
createDeploymentData: {
url: common.k8sDeploymentCreate,
params: {}
},
createDeploymentRules: {
name: [{
required: true,
message: '请填写名称',
trigger: 'change'
}],
image: [{
required: true,
message: '请填写镜像',
trigger: 'change'
}],
namespace: [{
required: true,
message: '请选择命名空间',
trigger: 'change'
}],
resource: [{
required: true,
message: '请选择配额',
trigger: 'change'
}],
label_str: [{
required: true,
message: '请填写标签',
trigger: 'change'
}],
container_port: [{
required: true,
message: '请填写容器端口',
trigger: 'change'
}],
},
deploymentDetail: {},
getDeploymentDetailData: {
url: common.k8sDeploymentDetail,
params: {
deployment_name: '',
namespace: ''
}
},
yamlDialog: false,
updateDeploymentData: {
url: common.k8sDeploymentUpdate,
params: {
namespace: '',
content: ''
}
},
scaleNum: 0,
scaleDialog: false,
scaleDeploymentData: {
url: common.k8sDeploymentScale,
params: {
deployment_name: '',
namespace: '',
scale_num: ''
}
},
restartDeploymentData: {
url: common.k8sDeploymentRestart,
params: {
deployment_name: '',
namespace: '',
}
},
delDeploymentData: {
url: common.k8sDeploymentDel,
params: {
deployment_name: '',
namespace: '',
}
},
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getDeployments()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getDeployments()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getDeployments() {
this.appLoading = true
this.getDeploymentsData.params.filter_name = this.searchInput
this.getDeploymentsData.params.namespace = this.namespaceValue
this.getDeploymentsData.params.page = this.currentPage
this.getDeploymentsData.params.limit = this.pagesize
httpClient.get(this.getDeploymentsData.url, {params: this.getDeploymentsData.params})
.then(res => {
this.deploymentList = res.data.items
this.deploymentTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
getDeploymentDetail(e) {
this.getDeploymentDetailData.params.deployment_name = e.row.metadata.name
this.getDeploymentDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getDeploymentDetailData.url, {params: this.getDeploymentDetailData.params})
.then(res => {
this.deploymentDetail = res.data
this.contentYaml = this.transYaml(this.deploymentDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updateDeployment() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updateDeploymentData.params.namespace = this.namespaceValue
this.updateDeploymentData.params.content = content
httpClient.put(this.updateDeploymentData.url, this.updateDeploymentData.params)
.then(res => {
this.$message.success({
message: res.msg
})
this.getDeployments()
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
handleScale(e) {
this.scaleDialog = true
this.deploymentDetail = e.row
this.scaleNum = e.row.spec.replicas
},
scaleDeployment() {
this.scaleDeploymentData.params.deployment_name = this.deploymentDetail.metadata.name
this.scaleDeploymentData.params.namespace = this.namespaceValue
this.scaleDeploymentData.params.scale_num = this.scaleNum
httpClient.put(this.scaleDeploymentData.url, this.scaleDeploymentData.params)
.then(res => {
this.$message.success({
message: res.msg
})
this.getDeployments()
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.scaleDialog = false
},
restartDeployment(e) {
this.restartDeploymentData.params.deployment_name = e.row.metadata.name
this.restartDeploymentData.params.namespace = this.namespaceValue
httpClient.put(this.restartDeploymentData.url, this.restartDeploymentData.params)
.then(res => {
this.$message.success({
message: res.msg
})
this.getDeployments()
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
delDeployment(e) {
this.delDeploymentData.params.deployment_name = e.row.metadata.name
this.delDeploymentData.params.namespace = this.namespaceValue
httpClient.delete(this.delDeploymentData.url, {data: this.delDeploymentData.params})
.then(res => {
this.$message.success({
message: res.msg
})
this.getDeployments()
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
createDeployFunc() {
let reg = new RegExp("(^[A-Za-z]+=[A-Za-z0-9]+).*")
if (!reg.test(this.createDeployment.label_str)) {
this.$message.warning({
message: "标签填写异常,请确认后重新填写"
})
return
}
this.fullscreenLoading = true
let label = new Map()
let cpu, memory
let a = (this.createDeployment.label_str).split(",")
a.forEach(item => {
let b = item.split("=")
label[b[0]] = b[1]
})
let resourceList = this.createDeployment.resource.split("/")
cpu = resourceList[0]
memory = resourceList[1] + "Gi"
this.createDeploymentData.params = this.createDeployment
this.createDeploymentData.params.container_port = parseInt(this.createDeployment.container_port)
this.createDeploymentData.params.label = label
this.createDeploymentData.params.cpu = cpu
this.createDeploymentData.params.memory = memory
httpClient.post(this.createDeploymentData.url, this.createDeploymentData.params)
.then(res => {
this.$message.success({
message: res.msg
})
this.getDeployments()
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.resetForm('createDeployment')
this.fullscreenLoading = false
this.createDeploymentDrawer = false
},
resetForm(formName) {
this.$refs[formName].resetFields()
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.createDeployFunc()
} else {
return false;
}
})
}
},
watch: {
namespaceValue: {
handler() {
localStorage.setItem('namespace', this.namespaceValue)
this.currentPage = 1
this.getDeployments()
}
},
},
beforeMount() {
if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
this.namespaceValue = localStorage.getItem('namespace')
}
this.getNamespaces()
this.getDeployments()
}
}
</script>
<style scoped>
.deploy-head-card,.deploy-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.deploy-head-search {
width:160px;
margin-right:10px;
}
.deploy-body-deployname {
color: #4795EE;
}
.deploy-body-deployname:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,365 @@
<template>
<div class="home">
<el-collapse v-model="activeNames">
<el-collapse-item title="集群资源" name="1">
<el-row :gutter="10" style="margin-bottom: 10px;">
<el-col :span="5">
<el-card class="home-node-card" :body-style="{padding:'10px'}">
<div style="float:left;padding-top:20%">
<el-progress :stroke-width="20" :show-text="false" type="circle" :percentage="namespaceActive/namespaceTotal * 100"></el-progress>
</div>
<div>
<p class="home-node-card-title">命名空间: Active/总量</p>
<p class="home-node-card-num">{{ namespaceActive }}/{{ namespaceTotal }}</p>
</div>
</el-card>
</el-col>
<el-col :span="5">
<el-card class="home-node-card" :body-style="{padding:'10px'}">
<div>
<p class="home-node-card-title">服务数</p>
<p class="home-node-card-num">{{ deploymentTotal }}</p>
</div>
</el-card>
</el-col>
<el-col :span="5">
<el-card class="home-node-card" :body-style="{padding:'10px'}">
<div>
<p class="home-node-card-title">实例数</p>
<p class="home-node-card-num">{{ podTotal }}</p>
</div>
</el-card>
</el-col>
</el-row>
</el-collapse-item>
<el-collapse-item title="节点资源" name="2">
<el-row :gutter="10" style="margin-bottom: 10px;">
<el-col :span="5">
<el-card class="home-node-card" :body-style="{padding:'10px'}">
<div style="float:left;padding-top:20%">
<el-progress :stroke-width="20" :show-text="false" type="circle" :percentage="nodeTotal/nodeTotal * 100"></el-progress>
</div>
<div>
<p class="home-node-card-title">节点: Ready/总数量</p>
<p class="home-node-card-num">{{ nodeTotal }}/{{ nodeTotal }}</p>
</div>
</el-card>
</el-col>
<el-col :span="5">
<el-card class="home-node-card" :body-style="{padding:'10px'}">
<div style="float:left;padding-top:20%">
<el-progress :stroke-width="20" :show-text="false" type="circle" :percentage="nodeCpuAllocatable/nodeCpuCapacity * 100"></el-progress>
</div>
<div>
<p class="home-node-card-title">CPU: 可分配/容量</p>
<p class="home-node-card-num">{{ nodeCpuAllocatable }}/{{ nodeCpuCapacity }}</p>
</div>
</el-card>
</el-col>
<el-col :span="5">
<el-card class="home-node-card" :body-style="{padding:'10px'}">
<div style="float:left;padding-top:20%">
<el-progress :stroke-width="20" :show-text="false" type="circle" :percentage="nodeMemAllocatable/nodeMemCapacity * 100"></el-progress>
</div>
<div>
<p class="home-node-card-title">内存: 可分配/容量</p>
<p class="home-node-card-num">{{ specTrans(nodeMemAllocatable) }}Gi/{{ specTrans(nodeMemCapacity) }}Gi</p>
</div>
</el-card>
</el-col>
<el-col :span="5">
<el-card class="home-node-card" :body-style="{padding:'10px'}">
<div style="float:left;padding-top:20%">
<el-progress :stroke-width="20" :show-text="false" type="circle" :percentage="nodePodAllocatable/nodePodAllocatable * 100"></el-progress>
</div>
<div>
<p class="home-node-card-title">POD: 可分配/容量</p>
<p class="home-node-card-num">{{ nodePodAllocatable }}/{{ nodePodAllocatable }}</p>
</div>
</el-card>
</el-col>
</el-row>
</el-collapse-item>
<el-collapse-item title="资源统计" name="3">
<el-row :gutter="10">
<el-col :span="24" style="margin-bottom: 10px;">
<el-card class="home-dash-card" :body-style="{padding:'10px'}">
<div id="podNumDash" style="height: 300px;">
</div>
</el-card>
</el-col>
<el-col :span="24">
<el-card class="home-dash-card" :body-style="{padding:'10px'}">
<div id="deployNumDash" style="height: 300px;">
</div>
</el-card>
</el-col>
</el-row>
</el-collapse-item>
</el-collapse>
</div>
</template>
<script>
import * as echarts from 'echarts'
import common from "../common/Config";
import httpClient from '../../utils/request';
export default {
data() {
return {
activeNames: ["1", "2", "3"],
namespaceActive: 0,
namespaceValue: 'default',
namespaceTotal: 0,
namespaceListUrl: common.k8sNamespaceList,
nodeTotal: 0,
nodeCpuAllocatable: 0,
nodeCpuCapacity: 0,
nodeMemAllocatable: 0,
nodeMemCapacity: 0,
nodePodAllocatable: 0,
nodePodCapacity: 0,
getNodesData: {
url: common.k8sNodeList,
params: {}
},
deploymentTotal: 0,
getDeploymentsData: {
url: common.k8sDeploymentList,
params: {
namespace: '',
}
},
podTotal: 0,
getPodsData: {
url: common.k8sPodList,
params: {
namespace: '',
}
},
podNumNp: [],
podNumNpUrl: common.k8sPodNumNp,
deploymentNumNp: [],
deploymentNumNpUrl: common.k8sDeploymentNumNp
}
},
methods: {
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceTotal = res.data.total
let namespaceList = res.data.items
let index
for (index in namespaceList) {
if (namespaceList[index].status.phase === "Active" ) {
this.namespaceActive = this.namespaceActive + 1
}
}
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
specTrans(num) {
let a = num / 1024 / 1024
return a.toFixed(0)
},
getNodes() {
httpClient.get(this.getNodesData.url, {params: this.getNodesData.params})
.then(res => {
this.nodeTotal = res.data.total
let nodeList = res.data.items
let index
for (index in nodeList) {
let isnum = /^\d+$/.test(nodeList[index].status.allocatable.cpu);
if (!isnum) {
continue
}
this.nodeCpuAllocatable = parseInt(nodeList[index].status.allocatable.cpu) + this.nodeCpuAllocatable
this.nodeCpuCapacity = parseInt(nodeList[index].status.capacity.cpu) + this.nodeCpuCapacity
this.nodeMemAllocatable = parseInt(nodeList[index].status.allocatable.memory) + this.nodeMemAllocatable
this.nodeMemCapacity = parseInt(nodeList[index].status.capacity.memory) + this.nodeMemCapacity
this.nodePodAllocatable = parseInt(nodeList[index].status.allocatable.pods) + this.nodePodAllocatable
this.nodePodCapacity = parseInt(nodeList[index].status.capacity.pods) + this.nodePodCapacity
}
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getDeployments() {
this.getDeploymentsData.params.namespace = this.namespaceValue
httpClient.get(this.getDeploymentsData.url, {params: this.getDeploymentsData.params})
.then(res => {
this.deploymentTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getPods() {
this.getPodsData.params.namespace = this.namespaceValue
httpClient.get(this.getPodsData.url, {params: this.getPodsData.params})
.then(res => {
this.podTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getDeploymentNumNp() {
httpClient.get(this.deploymentNumNpUrl)
.then(res => {
this.deploymentNumNp = res.data
this.getDeployNumDash()
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getPodNumNp() {
httpClient.get(this.podNumNpUrl)
.then(res => {
this.podNumNp = res.data
this.getPodNumDash()
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getPodNumDash(){
if (this.podNumDash != null && this.podNumDash != "" && this.podNumDash != undefined) {
this.podNumDash.dispose()
}
this.podNumDash = echarts.init(document.getElementById('podNumDash'));
this.podNumDash.setOption({
title: { text: 'Pods per Namespace', textStyle: {color:'rgb(134, 135, 136)'}},
color: ['#67E0E3', '#9FE6B8', '#FFDB5C','#ff9f7f', '#fb7293', '#E062AE', '#E690D1', '#e7bcf3', '#9d96f5', '#8378EA', '#96BFFF'],
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
label: {
backgroundColor: "#76baf1"
}
}
},
legend: {
data: ['Pods']
},
dataset: {
dimensions: ['namespace','pod_num'],
source: this.podNumNp
},
xAxis: {
type: 'category',
axisLabel:{
interval: 0,
formatter: function (value) {
return value.length>5?value.substring(0,5)+'...':value
}
},
},
yAxis: [
{type: 'value'}
],
series: [{
name: 'Pods',
type: 'bar',
label: {
show: true,
position: 'top'
}
}
]
});
},
getDeployNumDash(){
if (this.deployNumDash != null && this.deployNumDash != "" && this.deployNumDash != undefined) {
this.deployNumDash.dispose()
}
this.deployNumDash = echarts.init(document.getElementById('deployNumDash'));
this.deployNumDash.setOption({
title: { text: 'Deployments per Namespace', textStyle: {color:'rgb(134, 135, 136)'}},
color: ['#9FE6B8', '#FFDB5C','#ff9f7f', '#fb7293', '#E062AE', '#E690D1', '#e7bcf3', '#9d96f5', '#8378EA', '#96BFFF'],
tooltip: { trigger: "axis", axisPointer: { type: "cross", label: { backgroundColor: "#76baf1" } } },
legend: {
data: ['Deployments']
},
dataset: {
dimensions: ['namespace','deployment_num'],
source: this.deploymentNumNp
},
xAxis: {
type: 'category',
axisLabel:{
interval: 0,
formatter: function (value) {
return value.length>5?value.substring(0,5)+'...':value
}
},
},
yAxis: [
{type: 'value'}
],
series: [{
name: 'Deployments',
type: 'bar',
label: {
show: true,
position: 'top'
}
}
]
});
},
},
beforeMount() {
this.getNamespaces()
this.getNodes()
this.getDeployments()
this.getPods()
this.getDeploymentNumNp()
this.getPodNumNp()
}
}
</script>
<style scoped>
:v-deep .el-collapse-item__header {
font-size: 16px;
}
.home-node-card {
border-radius:1px;
text-align: center;
background-color: rgb(250, 253, 255);
}
.home-dash-card {
border-radius:1px;
}
.home-node-card-title {
font-size: 12px;
}
.home-node-card-num {
font-size: 22px;
font-weight: bold;
color: rgb(63, 92, 135);
}
:v-deep .el-progress-circle {
height: 50px !important;
width: 50px !important;
}
</style>

View File

@ -0,0 +1,556 @@
<template>
<div class="ingress">
<el-row>
<el-col :span="24">
<div>
<el-card class="ingress-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="6">
<div>
<span>命名空间: </span>
<el-select v-model="namespaceValue" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</div>
</el-col>
<el-col :span="2" :offset="16">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getIngresss()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="ingress-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button style="border-radius:2px;" icon="Edit" type="primary" @click="createIngressDrawer = true" v-loading.fullscreen.lock="fullscreenLoading">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="ingress-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getIngresss()">搜索</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="ingress-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="ingressList"
v-loading="appLoading">
<el-table-column width="10"></el-table-column>
<el-table-column align=left label="Ingress名">
<template v-slot="scope">
<a class="ingress-body-ingressname">{{ scope.row.metadata.name }}</a>
</template>
</el-table-column>
<el-table-column align=center label="标签" min-width='120'>
<template v-slot="scope">
<div v-for="(val, key) in scope.row.metadata.labels" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="key + ':' + val">
<template #reference>
<el-tag style="margin-bottom: 5px" type="warning">{{ ellipsis(key + ":" + val) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="Host" min-width='120'>
<template v-slot="scope">
<div v-for="(item, index) in scope.row.spec.rules" :key="index">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="item.host">
<template #reference>
<el-tag style="margin-bottom: 5px" type="danger">{{ ellipsis(item.host) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="Path">
<template v-slot="scope">
<div v-for="(item, index) in scope.row.spec.rules" :key="index">
<el-popover
placement="right"
:width="100"
trigger="hover"
:content="item.http.paths[0].path">
<template #reference>
<el-tag style="margin-bottom: 5px" type="danger">{{ item.http.paths[0].path }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="EXTERNAL-IP">
<template v-slot="scope">
<span>{{ scope.row.status.loadBalancer.ingress ? scope.row.status.loadBalancer.ingress[0].ip : '' }} </span>
</template>
</el-table-column>
<el-table-column align=center label="TLS">
<template v-slot="scope">
<span>{{ scope.row.spec.tls ? 'YES' : '' }} </span>
</template>
</el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="操作" width="200">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getIngressDetail(scope)">YAML</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delIngress)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="ingress-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="ingressTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="yamlDialog = false"> </el-button>
<el-button type="primary" @click="updateIngress()"> </el-button>
</span>
</template>
</el-dialog>
<el-drawer
v-model="createIngressDrawer"
:direction="direction"
:before-close="handleClose">
<template #title>
<h4>创建Ingress</h4>
</template>
<template #default>
<el-row type="flex" justify="center">
<el-col :span="20">
<el-form ref="createIngress" :rules="createIngressRules" :model="createIngress" label-width="80px">
<el-form-item class="ingress-create-form" label="名称" prop="name">
<el-input v-model="createIngress.name"></el-input>
</el-form-item>
<el-form-item class="ingress-create-form" label="命名空间" prop="namespace">
<el-select v-model="createIngress.namespace" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</el-form-item>
<el-form-item class="SERVICE-create-form" label="标签" prop="label_str">
<el-input v-model="createIngress.label_str" placeholder="示例: project=ms,app=gateway"></el-input>
</el-form-item>
<el-form-item class="deploy-create-form" label="域名" prop="host">
<el-input v-model="createIngress.host" placeholder="示例: www.example.com"></el-input>
</el-form-item>
<el-form-item class="ingress-create-form" label="Path" prop="path">
<el-input v-model="createIngress.path" placeholder="示例: /abc"></el-input>
</el-form-item>
<el-form-item class="deploy-create-form" label="匹配类型" prop="path_type">
<el-select v-model="createIngress.path_type" placeholder="请选择">
<el-option value="Prefix" label="Prefix"></el-option>
<el-option value="Exact" label="Exact"></el-option>
<el-option value="ImplementationSpecific" label="ImplementationSpecific"></el-option>
</el-select>
</el-form-item>
<el-form-item class="ingress-create-form" label="Service名" prop="service_name">
<el-input disabled v-model="createIngress.name"></el-input>
</el-form-item>
<el-form-item class="ingress-create-form" label="Service端口" prop="service_port">
<el-input v-model="createIngress.service_port" placeholder="示例: 80"></el-input>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<template #footer>
<el-button @click="createIngressDrawer = false">取消</el-button>
<el-button type="primary" @click="submitForm('createIngress')">立即创建</el-button>
</template>
</el-drawer>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
ingressList: [],
ingressTotal: 0,
getIngresssData: {
url: common.k8sIngressList,
params: {
filter_name: '',
namespace: '',
page: '',
limit: '',
}
},
ingressDetail: {},
getIngressDetailData: {
url: common.k8sIngressDetail,
params: {
ingress_name: '',
namespace: ''
}
},
yamlDialog: false,
updateIngressData: {
url: common.k8sIngressUpdate,
params: {
namespace: '',
content: ''
}
},
delIngressData: {
url: common.k8sIngressDel,
params: {
ingress_name: '',
namespace: '',
}
},
fullscreenLoading: false,
direction: 'rtl',
createIngressDrawer: false,
createIngress: {
name: '',
namespace: '',
label_str: '',
host: '',
path: '',
path_type: '',
service_name: '',
service_port: '',
hosts: {}
},
createIngressData: {
url: common.k8sIngressCreate,
params: {}
},
createIngressRules: {
name: [{
required: true,
message: '请填写名称',
trigger: 'change'
}],
namespace: [{
required: true,
message: '请选择命名空间',
trigger: 'change'
}],
host: [{
required: true,
message: '请填写域名',
trigger: 'change'
}],
path: [{
required: true,
message: '请填写路径',
trigger: 'change'
}],
service_port: [{
required: true,
message: '请填写Service端口',
trigger: 'change'
}],
label_str: [{
required: true,
message: '请填写标签',
trigger: 'change'
}],
path_type: [{
required: true,
message: '请选择匹配类型',
trigger: 'change'
}],
}
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getIngresss()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getIngresss()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
restartTotal(e) {
let index, sum = 0
let containerStatuses = e.row.status.containerStatuses
for ( index in containerStatuses) {
sum = sum + containerStatuses[index].restartCount
}
return sum
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getIngresss() {
this.appLoading = true
this.getIngresssData.params.filter_name = this.searchInput
this.getIngresssData.params.namespace = this.namespaceValue
this.getIngresssData.params.page = this.currentPage
this.getIngresssData.params.limit = this.pagesize
httpClient.get(this.getIngresssData.url, {params: this.getIngresssData.params})
.then(res => {
this.ingressList = res.data.items
this.ingressTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
getIngressDetail(e) {
this.getIngressDetailData.params.ingress_name = e.row.metadata.name
this.getIngressDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getIngressDetailData.url, {params: this.getIngressDetailData.params})
.then(res => {
this.ingressDetail = res.data
this.contentYaml = this.transYaml(this.ingressDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updateIngress() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updateIngressData.params.namespace = this.namespaceValue
this.updateIngressData.params.content = content
httpClient.put(this.updateIngressData.url, this.updateIngressData.params)
.then(res => {
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
delIngress(e) {
this.delIngressData.params.ingress_name = e.row.metadata.name
this.delIngressData.params.namespace = this.namespaceValue
httpClient.delete(this.delIngressData.url, {data: this.delIngressData.params})
.then(res => {
this.getIngresss()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
createIngressFunc() {
let reg = new RegExp("(^[A-Za-z]+=[A-Za-z0-9]+).*")
if (!reg.test(this.createIngress.label_str)) {
this.$message.warning({
message: "标签填写异常,请确认后重新填写"
})
return
}
this.fullscreenLoading = true
let label = new Map()
let a = (this.createIngress.label_str).split(",")
a.forEach(item => {
let b = item.split("=")
label[b[0]] = b[1]
})
let hosts = new Map()
let httpPaths = []
let httpPath = {
path: this.createIngress.path,
path_type: this.createIngress.path_type,
service_name: this.createIngress.name,
service_port: parseInt(this.createIngress.service_port)
}
httpPaths.push(httpPath)
hosts[this.createIngress.host] = httpPaths
this.createIngressData.params = this.createIngress
this.createIngressData.params.label = label
this.createIngressData.params.hosts = hosts
httpClient.post(this.createIngressData.url, this.createIngressData.params)
.then(res => {
this.$message.success({
message: res.msg
})
this.getIngresss()
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.resetForm('createIngress')
this.fullscreenLoading = false
this.createIngressDrawer = false
},
resetForm(formName) {
this.$refs[formName].resetFields()
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.createIngressFunc()
} else {
return false;
}
})
}
},
watch: {
namespaceValue: {
handler() {
localStorage.setItem('namespace', this.namespaceValue)
this.currentPage = 1
this.getIngresss()
}
},
},
beforeMount() {
if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
this.namespaceValue = localStorage.getItem('namespace')
}
this.getNamespaces()
this.getIngresss()
}
}
</script>
<style scoped>
.ingress-head-card,.ingress-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.ingress-head-search {
width:160px;
margin-right:10px;
}
.ingress-body-ingressname {
color: #4795EE;
}
.ingress-body-ingressname:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<div class="login">
<el-card class="login-card">
<template #header>
<div class="login-card-header">
<span>用户登录</span>
</div>
</template>
<el-form :model="loginData" :rules="loginDataRules" ref="loginData">
<el-form-item prop="username">
<el-input prefix-icon="UserFilled" v-model.trim="loginData.username" maxlength="32" placeholder="请输入账号" clearable></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input prefix-icon="Lock" v-model.trim="loginData.password" maxlength="16" show-password placeholder="请输入密码" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width: 100%;border-radius: 2px" :loading="loginLoading" @click="handleLogin"> </el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import moment from 'moment';
import jwt from 'jsonwebtoken';
export default{
data() {
return {
loginLoading: false,
loginUrl: common.loginAuth,
loginData: {
username: '',
password: ''
},
loginDataRules: {
username: [{
required: true,
message: '请填写用户名',
trigger: 'change'
}],
password: [{
required: true,
message: '请填写密码',
trigger: 'change'
}],
}
}
},
methods: {
handleLogin() {
httpClient.post(this.loginUrl, this.loginData)
.then(res => {
localStorage.setItem('username', this.loginData.username);
localStorage.setItem('loginDate', moment().format('YYYY-MM-DD HH:mm:ss'));
let token = jwt.sign(this.loginData, 'devops', { expiresIn: '10h' });
localStorage.setItem('token', token);
this.$router.push('/');
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
}
}
}
</script>
<style scoped>
.login {
position: absolute;
width: 100%;
height: 100%;
background: aquamarine;
background-image: url(../../assets/img/login3.webp);
background-size: 100%;
}
.login-card {
position: absolute;
left: 40%;
top: 30%;
width: 350px;
border-radius: 5px;
background: rgb(255, 255, 255);
overflow: hidden;
}
.login-card-header {
text-align: center;
}
</style>

View File

@ -0,0 +1,313 @@
<template>
<div class="namespace">
<el-row>
<el-col :span="24">
<div>
<el-card class="namespace-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="namespace-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getNamespaces()">搜索</el-button>
</div>
</el-col>
<el-col :span="2" :offset="14">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getNamespaces()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="namespace-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="namespaceList"
v-loading="appLoading">
<el-table-column width="20"></el-table-column>
<el-table-column align=left label="Namespace名">
<template v-slot="scope">
<a class="namespace-body-namespacename">{{ scope.row.metadata.name }}</a>
</template>
</el-table-column>
<el-table-column align=center label="标签" min-width='120'>
<template v-slot="scope">
<div v-for="(val, key) in scope.row.metadata.labels" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="key + ':' + val">
<template #reference>
<el-tag style="margin-bottom: 5px" type="warning">{{ ellipsis(key + ":" + val) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center prop="status.phase" label="状态">
<template v-slot="scope">
<span :class="[scope.row.status.phase === 'Active' ? 'success-status' : 'error-status']">{{ scope.row.status.phase }}</span>
</template>
</el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="操作" min-width="120">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getNamespaceDetail(scope)">YAML</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delNamespace)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="namespace-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="namespaceTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="yamlDialog = false"> </el-button>
<el-button disabled type="primary" @click="updateNamespace()"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
namespaceTotal: 0,
getNamespacesData: {
url: common.k8sNamespaceList,
params: {
filter_name: '',
namespace: '',
page: 1,
limit: 10,
}
},
namespaceDetail: {},
getNamespaceDetailData: {
url: common.k8sNamespaceDetail,
params: {
namespace_name: '',
namespace: ''
}
},
yamlDialog: false,
updateNamespaceData: {
url: common.k8sNamespaceUpdate,
params: {
namespace: '',
content: ''
}
},
delNamespaceData: {
url: common.k8snamespaceDel,
params: {
namespace_name: '',
namespace: '',
}
}
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getNamespaces()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getNamespaces()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
specTrans(str) {
if ( str.indexOf('Ki') == -1 ) {
return str
}
let num = str.slice(0,-2) / 1024 / 1024
return num.toFixed(0)
},
getNamespaces() {
this.getNamespacesData.params.filter_name = this.searchInput
this.getNamespacesData.params.limit = this.pagesize
this.getNamespacesData.params.page = this.currentPage
httpClient.get(this.getNamespacesData.url, {params: this.getNamespacesData.params})
.then(res => {
this.namespaceList = res.data.items
this.namespaceTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getNamespaceDetail(e) {
this.getNamespaceDetailData.params.namespace_name = e.row.metadata.name
this.getNamespaceDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getNamespaceDetailData.url, {params: this.getNamespaceDetailData.params})
.then(res => {
this.namespaceDetail = res.data
this.contentYaml = this.transYaml(this.namespaceDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updateNamespace() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updateNamespaceData.params.namespace = this.namespaceValue
this.updateNamespaceData.params.content = content
httpClient.put(this.updateNamespaceData.url, this.updateNamespaceData.params)
.then(res => {
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
delNamespace(e) {
this.delNamespaceData.params.namespace_name = e.row.metadata.name
this.delNamespaceData.params.namespace = this.namespaceValue
httpClient.delete(this.delNamespaceData.url, {data: this.delNamespaceData.params})
.then(res => {
this.getNamespaces()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
},
beforeMount() {
this.getNamespaces()
}
}
</script>
<style scoped>
.namespace-head-card,.namespace-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.namespace-head-search {
width:160px;
margin-right:10px;
}
.namespace-body-namespacename {
color: #4795EE;
}
.namespace-body-namespacename:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
.success-status {
color: rgb(27, 202, 21);
}
.warning-status {
color: rgb(233, 200, 16);
}
.error-status {
color: rgb(226, 23, 23);
}
</style>

View File

@ -0,0 +1,319 @@
<template>
<div class="node">
<el-row>
<el-col :span="24">
<div>
<el-card class="node-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="node-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getNodes()">搜索</el-button>
</div>
</el-col>
<el-col :span="2" :offset="14">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getNodes()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="node-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="nodeList"
v-loading="appLoading">
<el-table-column width="20"></el-table-column>
<el-table-column align=left label="Node名">
<template v-slot="scope">
<p class="node-body-nodename">{{ scope.row.metadata.name }}</p>
<p class="node-body-ip">{{ scope.row.status.addresses[0].address }}</p>
</template>
</el-table-column>
<el-table-column align=center label="规格">
<template v-slot="scope">
<el-tag type="warning">{{ scope.row.status.capacity.cpu }}{{ specTrans(scope.row.status.capacity.memory) }}G</el-tag>
</template>
</el-table-column>
<el-table-column align=center label="POD-CIDR">
<template v-slot="scope">
<span>{{ scope.row.spec.podCIDR }} </span>
</template>
</el-table-column>
<el-table-column align=center label="版本">
<template v-slot="scope">
<span>{{ scope.row.status.nodeInfo.kubeletVersion }} </span>
</template>
</el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="操作" min-width="120">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getNodeDetail(scope)">YAML</el-button>
<el-button disabled size="small" style="border-radius:2px;" icon="Document" type="warning" plain @click="handleConfirm(scope, '删除', delIngress)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="node-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="nodeTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="yamlDialog = false"> </el-button>
<el-button disabled type="primary" @click="updateNode()"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
nodeList: [],
nodeTotal: 0,
getNodesData: {
url: common.k8sNodeList,
params: {
filter_name: '',
namespace: '',
page: '',
limit: '',
}
},
nodeDetail: {},
getNodeDetailData: {
url: common.k8sNodeDetail,
params: {
node_name: '',
namespace: ''
}
},
yamlDialog: false,
updateNodeData: {
url: common.k8sNodeUpdate,
params: {
namespace: '',
content: ''
}
},
delNodeData: {
url: common.k8snodeDel,
params: {
node_name: '',
namespace: '',
}
}
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getNodes()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getNodes()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
specTrans(str) {
if ( str.indexOf('Ki') == -1 ) {
return str
}
let num = str.slice(0,-2) / 1024 / 1024
return num.toFixed(0)
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getNodes() {
this.appLoading = true
this.getNodesData.params.filter_name = this.searchInput
this.getNodesData.params.page = this.currentPage
this.getNodesData.params.limit = this.pagesize
httpClient.get(this.getNodesData.url, {params: this.getNodesData.params})
.then(res => {
this.nodeList = res.data.items
this.nodeTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
getNodeDetail(e) {
this.getNodeDetailData.params.node_name = e.row.metadata.name
this.getNodeDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getNodeDetailData.url, {params: this.getNodeDetailData.params})
.then(res => {
this.nodeDetail = res.data
this.contentYaml = this.transYaml(this.nodeDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updateNode() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updateNodeData.params.namespace = this.namespaceValue
this.updateNodeData.params.content = content
httpClient.put(this.updateNodeData.url, this.updateNodeData.params)
.then(res => {
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
delNode(e) {
this.delNodeData.params.node_name = e.row.metadata.name
this.delNodeData.params.namespace = this.namespaceValue
httpClient.delete(this.delNodeData.url, {data: this.delNodeData.params})
.then(res => {
this.getNodes()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
},
beforeMount() {
this.getNodes()
}
}
</script>
<style scoped>
.node-head-card,.node-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.node-head-search {
width:160px;
margin-right:10px;
}
.node-body-nodename {
margin: 0px;
color: #4795EE;
}
.node-body-ip {
margin: 0px;
color: rgb(145, 145, 145);
}
.node-body-nodename:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,316 @@
<template>
<div class="pv">
<el-row>
<el-col :span="24">
<div>
<el-card class="pv-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="pv-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getPvs()">搜索</el-button>
</div>
</el-col>
<el-col :span="2" :offset="14">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getPvs()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="pv-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="pvList"
v-loading="appLoading">
<el-table-column width="20"></el-table-column>
<el-table-column align=left label="PV名">
<template v-slot="scope">
<a class="pv-body-pvname">{{ scope.row.metadata.name }}</a>
</template>
</el-table-column>
<el-table-column align=center label="状态">
<template v-slot="scope">
<span :class="[scope.row.status.phase === 'Bound' ? 'success-status' : 'error-status']">{{ scope.row.status.phase }}</span>
</template>
</el-table-column>
<el-table-column align=center prop="spec.accessModes[0]" label="访问模式"></el-table-column>
<el-table-column align=center prop="spec.capacity.storage" label="容量"></el-table-column>
<el-table-column align=center prop="spec.claimRef.name" label="PVC"></el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="操作" min-width="120">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getPvDetail(scope)">YAML</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delPv)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pv-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="pvTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="yamlDialog = false"> </el-button>
<el-button disabled type="primary" @click="updatePv()"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
pvList: [],
pvTotal: 0,
getPvsData: {
url: common.k8sPvList,
params: {
filter_name: '',
namespace: '',
page: '',
limit: '',
}
},
pvDetail: {},
getPvDetailData: {
url: common.k8sPvDetail,
params: {
pv_name: '',
namespace: ''
}
},
yamlDialog: false,
updatePvData: {
url: common.k8sPvUpdate,
params: {
namespace: '',
content: ''
}
},
delPvData: {
url: common.k8spvDel,
params: {
pv_name: '',
namespace: '',
}
}
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getPvs()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getPvs()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
specTrans(str) {
if ( str.indexOf('Ki') == -1 ) {
return str
}
let num = str.slice(0,-2) / 1024 / 1024
return num.toFixed(0)
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getPvs() {
this.appLoading = true
this.getPvsData.params.filter_name = this.searchInput
this.getPvsData.params.namespace = this.namespaceValue
this.getPvsData.params.page = this.currentPage
this.getPvsData.params.limit = this.pagesize
httpClient.get(this.getPvsData.url, {params: this.getPvsData.params})
.then(res => {
this.pvList = res.data.items
this.pvTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
getPvDetail(e) {
this.getPvDetailData.params.pv_name = e.row.metadata.name
this.getPvDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getPvDetailData.url, {params: this.getPvDetailData.params})
.then(res => {
this.pvDetail = res.data
this.contentYaml = this.transYaml(this.pvDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updatePv() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updatePvData.params.namespace = this.namespaceValue
this.updatePvData.params.content = content
httpClient.put(this.updatePvData.url, this.updatePvData.params)
.then(res => {
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
delPv(e) {
this.delPvData.params.pv_name = e.row.metadata.name
this.delPvData.params.namespace = this.namespaceValue
httpClient.delete(this.delPvData.url, {data: this.delPvData.params})
.then(res => {
this.getPvs()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
},
beforeMount() {
this.getPvs()
}
}
</script>
<style scoped>
.pv-head-card,.pv-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.pv-head-search {
width:160px;
margin-right:10px;
}
.pv-body-pvname {
color: #4795EE;
}
.pv-body-pvname:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
.success-status {
color: rgb(27, 202, 21);
}
.warning-status {
color: rgb(233, 200, 16);
}
.error-status {
color: rgb(226, 23, 23);
}
</style>

View File

@ -0,0 +1,369 @@
<template>
<div class="pvc">
<el-row>
<el-col :span="24">
<div>
<el-card class="pvc-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="6">
<div>
<span>命名空间: </span>
<el-select v-model="namespaceValue" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</div>
</el-col>
<el-col :span="2" :offset="16">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getPvcs()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="pvc-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="pvc-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getPvcs()">搜索</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="pvc-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="pvcList"
v-loading="appLoading">
<el-table-column width="20"></el-table-column>
<el-table-column align=left label="PVC名">
<template v-slot="scope">
<a class="pvc-body-pvcname">{{ scope.row.metadata.name }}</a>
</template>
</el-table-column>
<el-table-column align=center label="标签">
<template v-slot="scope">
<div v-for="(val, key) in scope.row.metadata.labels" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="key + ':' + val">
<template #reference>
<el-tag style="margin-bottom: 5px" type="warning">{{ ellipsis(key + ":" + val) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="状态">
<template v-slot="scope">
<span :class="[scope.row.status.phase === 'Bound' ? 'success-status' : 'error-status']">{{ scope.row.status.phase }}</span>
</template>
</el-table-column>
<el-table-column align=center prop="status.capacity.storage" label="容量">
</el-table-column>
<el-table-column align=center prop="status.accessModes[0]" label="访问模式">
</el-table-column>
<el-table-column align=center prop="spec.storageClassName" label="StorageClass">
</el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="操作" width="200">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getPvcDetail(scope)">YAML</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delPvc)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pvc-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="pvcTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="yamlDialog = false"> </el-button>
<el-button type="primary" @click="updatePvc()"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
pvcList: [],
pvcTotal: 0,
getPvcsData: {
url: common.k8sPvcList,
params: {
filter_name: '',
namespace: '',
page: '',
limit: '',
}
},
pvcDetail: {},
getPvcDetailData: {
url: common.k8sPvcDetail,
params: {
pvc_name: '',
namespace: ''
}
},
yamlDialog: false,
updatePvcData: {
url: common.k8sPvcUpdate,
params: {
namespace: '',
content: ''
}
},
delPvcData: {
url: common.k8spvcDel,
params: {
pvc_name: '',
namespace: '',
}
}
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getPvcs()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getPvcs()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
restartTotal(e) {
let index, sum = 0
let containerStatuses = e.row.status.containerStatuses
for ( index in containerStatuses) {
sum = sum + containerStatuses[index].restartCount
}
return sum
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getPvcs() {
this.appLoading = true
this.getPvcsData.params.filter_name = this.searchInput
this.getPvcsData.params.namespace = this.namespaceValue
this.getPvcsData.params.page = this.currentPage
this.getPvcsData.params.limit = this.pagesize
httpClient.get(this.getPvcsData.url, {params: this.getPvcsData.params})
.then(res => {
this.pvcList = res.data.items
this.pvcTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
getPvcDetail(e) {
this.getPvcDetailData.params.pvc_name = e.row.metadata.name
this.getPvcDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getPvcDetailData.url, {params: this.getPvcDetailData.params})
.then(res => {
this.pvcDetail = res.data
this.contentYaml = this.transYaml(this.pvcDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updatePvc() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updatePvcData.params.namespace = this.namespaceValue
this.updatePvcData.params.content = content
httpClient.put(this.updatePvcData.url, this.updatePvcData.params)
.then(res => {
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
delPvc(e) {
this.delPvcData.params.pvc_name = e.row.metadata.name
this.delPvcData.params.namespace = this.namespaceValue
httpClient.delete(this.delPvcData.url, {data: this.delPvcData.params})
.then(res => {
this.getPvcs()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
},
watch: {
namespaceValue: {
handler() {
localStorage.setItem('namespace', this.namespaceValue)
this.currentPage = 1
this.getPvcs()
}
},
},
beforeMount() {
if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
this.namespaceValue = localStorage.getItem('namespace')
}
this.getNamespaces()
this.getPvcs()
}
}
</script>
<style scoped>
.pvc-head-card,.pvc-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.pvc-head-search {
width:160px;
margin-right:10px;
}
.pvc-body-pvcname {
color: #4795EE;
}
.pvc-body-pvcname:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
.success-status {
color: rgb(27, 202, 21);
}
.warning-status {
color: rgb(233, 200, 16);
}
.error-status {
color: rgb(226, 23, 23);
}
</style>

View File

@ -0,0 +1,644 @@
<template>
<div class="pod">
<el-row>
<el-col :span="24">
<div>
<el-card class="pod-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="6">
<div>
<span>命名空间: </span>
<el-select v-model="namespaceValue" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</div>
</el-col>
<el-col :span="2" :offset="16">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getPods()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="pod-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="pod-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getPods()">搜索</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="pod-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="podList"
v-loading="appLoading"
:row-key="getRowKeys"
:expand-row-keys="expandKeys"
@expand-change="expandChange">
<el-table-column width="10"></el-table-column>
<el-table-column type="expand">
<template #default="props">
<el-tabs v-model="activeName" type="card">
<el-tab-pane label="容器" name="container">
<el-card shadow="never" style="border-radius:1px;" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;"
:data="props.row.spec.containers">
<el-table-column align=left prop="name" label="容器名"></el-table-column>
<el-table-column align=left prop="image" label="镜像"></el-table-column>
<el-table-column align=center label="Pod IP">
<span>{{ props.row.status.podIP }}</span>
</el-table-column>
<el-table-column align=center prop="args" label="启动命令"></el-table-column>
<el-table-column align=center label="环境变量">
<template v-slot="scope">
<el-popover :width="500" placement="left" trigger="hover">
<el-table style="width:100%;font-size:12px;" size="mini" :show-header="false" :data="scope.row.env">
<el-table-column property="name" label="名称"></el-table-column>
<el-table-column property="value" label="值"></el-table-column>
</el-table>
<template #reference>
<el-button size="small">此处查看</el-button>
</template>
</el-popover>
</template>
</el-table-column>
</el-table>
</el-card>
</el-tab-pane>
<el-tab-pane label="日志" name="log">
<el-card shadow="never" style="border-radius:1px;" :body-style="{padding:'5px'}">
<el-row :gutter="10">
<el-col :span="3">
<el-select size="small" v-model="containerValue" placeholder="请选择">
<el-option v-for="item in containerList" :key="item" :value="item">
</el-option>
</el-select>
</el-col>
<el-col :span="2">
<el-button style="border-radius:2px;" size="small" type="primary" @click="getPodLog(props.row.metadata.name)">查看</el-button>
</el-col>
<el-col :span="24" style="margin-top: 5px">
<el-card shadow="never" class="pod-body-log-card" :body-style="{padding:'5px'}">
<span class="pod-body-log-span">{{ logContent }}</span>
</el-card>
</el-col>
</el-row>
</el-card>
</el-tab-pane>
<el-tab-pane label="终端" name="shell">
<el-card shadow="never" style="border-radius:1px;" :body-style="{padding:'5px'}">
<el-row :gutter="10">
<el-col :span="3">
<el-select size="small" v-model="containerValue" placeholder="请选择">
<el-option v-for="item in containerList" :key="item" :value="item">
</el-option>
</el-select>
</el-col>
<el-col :span="1">
<el-button style="border-radius:2px;" size="small" type="primary" @click="initSocket(props.row)">连接</el-button>
</el-col>
<el-col :span="1">
<el-button style="border-radius:2px;" size="small" type="danger" @click="closeSocket()">关闭</el-button>
</el-col>
<el-col :span="24" style="margin-top: 5px">
<el-card shadow="never" class="pod-body-shell-card" :body-style="{padding:'5px'}">
<div id="xterm"></div>
</el-card>
</el-col>
</el-row>
</el-card>
</el-tab-pane>
</el-tabs>
</template>
</el-table-column>
<el-table-column align=left label="Pod名">
<template v-slot="scope">
<a class="pod-body-podname" @click="expandMap[scope.row.metadata.name] ? expandChange(scope.row, []) : expandChange(scope.row, [scope.row])">{{ scope.row.metadata.name }}</a>
</template>
</el-table-column>
<el-table-column align=center min-width="150" label="节点">
<template v-slot="scope">
<el-tag v-if="scope.row.spec.nodeName !== undefined" type="warning">{{ scope.row.spec.nodeName }}</el-tag>
</template>
</el-table-column>
<el-table-column align=center label="状态">
<template v-slot="scope">
<div :class="{'success-dot':scope.row.status.phase == 'Running', 'warning-dot':scope.row.status.phase == 'Pending', 'error-dot':scope.row.status.phase != 'Running' && scope.row.status.phase != 'Pending'}"></div>
<span :class="{'success-status':scope.row.status.phase == 'Running', 'warning-status':scope.row.status.phase == 'Pending', 'error-status':scope.row.status.phase != 'Running' && scope.row.status.phase != 'Pending'}">{{ scope.row.status.phase }} </span>
</template>
</el-table-column>
<el-table-column align=center label="重启数">
<template v-slot="scope">
<span>{{ restartTotal(scope) }} </span>
</template>
</el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="操作" width="200">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getPodDetail(scope)">YAML</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delPod)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pod-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="podTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="yamlDialog = false"> </el-button>
<el-button type="primary" @click="updatePod()"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import 'xterm/lib/xterm.js';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
podList: [],
podTotal: 0,
getPodsData: {
url: common.k8sPodList,
params: {
filter_name: '',
namespace: '',
page: '',
limit: '',
}
},
podDetail: {},
getPodDetailData: {
url: common.k8sPodDetail,
params: {
pod_name: '',
namespace: ''
}
},
yamlDialog: false,
updatePodData: {
url: common.k8sPodUpdate,
params: {
namespace: '',
content: ''
}
},
delPodData: {
url: common.k8sPodDel,
params: {
pod_name: '',
namespace: ''
}
},
activeName: 'container',
expandKeys: [],
expandMap: {},
containerList: {},
containerValue: '',
getPodContainerData: {
url: common.k8sPodContainer,
params: {
pod_name: '',
namespace: ''
}
},
logContent: '',
getPodLogData: {
url: common.k8sPodLog,
params: {
container_name: '',
pod_name: '',
namespace: ''
}
},
term: null,
socket: null
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getPods()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getPods()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
restartTotal(e) {
let index, sum = 0
let containerStatuses = e.row.status.containerStatuses
for ( index in containerStatuses) {
sum = sum + containerStatuses[index].restartCount
}
return sum
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getPods() {
this.appLoading = true
this.getPodsData.params.filter_name = this.searchInput
this.getPodsData.params.namespace = this.namespaceValue
this.getPodsData.params.page = this.currentPage
this.getPodsData.params.limit = this.pagesize
httpClient.get(this.getPodsData.url, {params: this.getPodsData.params})
.then(res => {
this.podList = res.data.items
this.podTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
getPodDetail(e) {
this.getPodDetailData.params.pod_name = e.row.metadata.name
this.getPodDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getPodDetailData.url, {params: this.getPodDetailData.params})
.then(res => {
this.podDetail = res.data
this.contentYaml = this.transYaml(this.podDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updatePod() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updatePodData.params.namespace = this.namespaceValue
this.updatePodData.params.content = content
httpClient.put(this.updatePodData.url, this.updatePodData.params)
.then(res => {
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
delPod(e) {
this.delPodData.params.pod_name = e.row.metadata.name
this.delPodData.params.namespace = this.namespaceValue
httpClient.delete(this.delPodData.url, {data: this.delPodData.params})
.then(res => {
this.getPods()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
getRowKeys(row) {
return row.metadata.name
},
expandChange(row, expandedRows) {
this.expandKeys = []
this.logContent= ''
this.containerValue = ''
this.activeName = 'container'
if (expandedRows.length > 0) {
this.expandMap[row.metadata.name] = 1
this.setExpandMap(row.metadata.name)
row ? (this.expandKeys.push(row.metadata.name), this. getPodContainer(row)) : ''
} else {
this.expandMap[row.metadata.name] = 0
}
},
setExpandMap(podName) {
let key
for ( key in this.expandMap ) {
key !== podName ? this.expandMap[key] = 0 : ''
}
},
getPodContainer(row) {
this.getPodContainerData.params.pod_name = row.metadata.name
this.getPodContainerData.params.namespace = this.namespaceValue
httpClient.get(this.getPodContainerData.url, {params: this.getPodContainerData.params})
.then(res => {
this.containerList = res.data
this.containerValue = this.containerList[0]
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getPodLog(podName) {
this.getPodLogData.params.pod_name = podName
this.getPodLogData.params.container_name = this.containerValue
this.getPodLogData.params.namespace = this.namespaceValue
httpClient.get(this.getPodLogData.url, {params: this.getPodLogData.params})
.then(res => {
this.logContent = res.data
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
initTerm() {
this.term = new Terminal({
rendererType: 'canvas',
rows: 30,
cols: 110,
convertEol: false,
scrollback: 10,
disableStdin: false,
cursorStyle: 'underline',
cursorBlink: true,
theme: {
foreground: 'white',
background: '#060101',
cursor: 'help'
}
});
this.term.open(document.getElementById('xterm'))
const fitAddon = new FitAddon()
this.term.loadAddon(fitAddon)
fitAddon.fit();
this.term.focus();
let _this = this;
this.term.onData(function (key) {
let msgOrder = {
operation: 'stdin',
data: key,
};
_this.socket.send(JSON.stringify(msgOrder));
});
let msgOrder2 = {
operation: 'resize',
cols: this.term.cols,
rows: this.term.rows,
};
this.socket.send(JSON.stringify(msgOrder2))
},
initSocket(row) {
let terminalWsUrl = common.k8sTerminalWs + "?pod_name=" + row.metadata.name + "&container_name=" + this.containerValue + "&namespace=" + this.namespaceValue
this.socket = new WebSocket(terminalWsUrl);
this.socketOnClose();
this.socketOnOpen();
this.socketOnMessage();
this.socketOnError();
},
socketOnOpen() {
this.socket.onopen = () => {
this.initTerm()
}
},
socketOnMessage() {
this.socket.onmessage = (msg) => {
let content = JSON.parse(msg.data)
this.term.write(content.data)
}
},
socketOnClose() {
this.socket.onclose = () => {
this.term.write("链接已关闭")
}
},
socketOnError() {
this.socket.onerror = () => {
console.log('socket 链接失败')
}
},
closeSocket() {
if (this.socket === null) {
return
}
this.term.write("链接关闭中。。。")
this.socket.close()
}
},
watch: {
namespaceValue: {
handler() {
localStorage.setItem('namespace', this.namespaceValue)
this.currentPage = 1
this.getPods()
}
},
activeName: {
handler() {
if ( this.activeName == 'log' ) {
this.expandKeys.length == 1 ? this.getPodLog(this.expandKeys[0]) : ''
}
}
}
},
beforeMount() {
if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
this.namespaceValue = localStorage.getItem('namespace')
}
this.getNamespaces()
this.getPods()
},
beforeUnmount() {
if ( this.socket !== null ) {
this.socket.close()
}
},
}
</script>
<style scoped>
.pod-head-card,.pod-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.pod-head-search {
width:160px;
margin-right:10px;
}
.pod-body-podname {
color: #4795EE;
}
.pod-body-podname:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
.success-dot{
display:inline-block;
width: 7px;
height:7px;
background: rgb(27, 202, 21);
border-radius:50%;
border:1px solid rgb(27, 202, 21);
margin-right: 10px;
}
.warning-dot{
display:inline-block;
width: 7px;
height:7px;
background: rgb(233, 200, 16);
border-radius:50%;
border:1px solid rgb(233, 200, 16);
margin-right: 10px;
}
.error-dot{
display:inline-block;
width: 7px;
height:7px;
background: rgb(226, 23, 23);
border-radius:50%;
border:1px solid rgb(226, 23, 23);
margin-right: 10px;
}
.success-status {
color: rgb(27, 202, 21);
}
.warning-status {
color: rgb(233, 200, 16);
}
.error-status {
color: rgb(226, 23, 23);
}
:v-deep .el-tabs__item {
font-size: 12px;
}
:v-deep .el-tabs__header {
margin-bottom: 8px;
}
.pod-body-log-card, .pod-body-shell-card {
border-radius:1px;
height:600px;
overflow:auto;
background-color: #060101;
}
.pod-body-log-card {
color: aliceblue;
}
.pod-body-log-span {
white-space:pre;
}
</style>

View File

@ -0,0 +1,367 @@
<template>
<div class="secret">
<el-row>
<el-col :span="24">
<div>
<el-card class="secret-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="6">
<div>
<span>命名空间: </span>
<el-select v-model="namespaceValue" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</div>
</el-col>
<el-col :span="2" :offset="16">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getSecrets()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="secret-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="secret-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getSecrets()">搜索</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="secret-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="secretList"
v-loading="appLoading">
<el-table-column width="20"></el-table-column>
<el-table-column align=left label="Secret名">
<template v-slot="scope">
<a class="secret-body-secretname">{{ scope.row.metadata.name }}</a>
</template>
</el-table-column>
<el-table-column align=center label="标签">
<template v-slot="scope">
<div v-for="(val, key) in scope.row.metadata.labels" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="key + ':' + val">
<template #reference>
<el-tag style="margin-bottom: 5px" type="warning">{{ ellipsis(key + ":" + val) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="DATA">
<template v-slot="scope">
<el-popover
style="overflow:auto"
placement="right"
:width="400"
trigger="click">
<div style="overflow-y:auto;max-height:500px;">
<span>{{ scope.row.data }}</span>
</div>
<template #reference>
<el-icon style="font-size:18px;cursor:pointer;"><reading/></el-icon>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column align=center prop="type" min-width="100" label="类型">
</el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="操作" width="200">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getSecretDetail(scope)">YAML</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delSecret)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="secret-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
layout="total, sizes, prev, pager, next, jumper"
:prev-click="getSecrets"
:total="secretTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="yamlDialog = false"> </el-button>
<el-button type="primary" @click="updateSecret()"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
secretList: [],
secretTotal: 0,
getSecretsData: {
url: common.k8sSecretList,
params: {
filter_name: '',
namespace: '',
page: '',
limit: '',
}
},
secretDetail: {},
getSecretDetailData: {
url: common.k8sSecretDetail,
params: {
secret_name: '',
namespace: ''
}
},
yamlDialog: false,
updateSecretData: {
url: common.k8sSecretUpdate,
params: {
namespace: '',
content: ''
}
},
delSecretData: {
url: common.k8ssecretDel,
params: {
secret_name: '',
namespace: '',
}
}
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getSecrets()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getSecrets()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
restartTotal(e) {
let index, sum = 0
let containerStatuses = e.row.status.containerStatuses
for ( index in containerStatuses) {
sum = sum + containerStatuses[index].restartCount
}
return sum
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getSecrets() {
this.appLoading = true
this.getSecretsData.params.filter_name = this.searchInput
this.getSecretsData.params.namespace = this.namespaceValue
this.getSecretsData.params.page = this.currentPage
this.getSecretsData.params.limit = this.pagesize
httpClient.get(this.getSecretsData.url, {params: this.getSecretsData.params})
.then(res => {
this.secretList = res.data.items
this.secretTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
getSecretDetail(e) {
this.getSecretDetailData.params.secret_name = e.row.metadata.name
this.getSecretDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getSecretDetailData.url, {params: this.getSecretDetailData.params})
.then(res => {
this.secretDetail = res.data
this.contentYaml = this.transYaml(this.secretDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updateSecret() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updateSecretData.params.namespace = this.namespaceValue
this.updateSecretData.params.content = content
httpClient.put(this.updateSecretData.url, this.updateSecretData.params)
.then(res => {
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
delSecret(e) {
this.delSecretData.params.secret_name = e.row.metadata.name
this.delSecretData.params.namespace = this.namespaceValue
httpClient.delete(this.delSecretData.url, {data: this.delSecretData.params})
.then(res => {
this.getSecrets()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
},
watch: {
namespaceValue: {
handler() {
localStorage.setItem('namespace', this.namespaceValue)
this.currentPage = 1
this.getSecrets()
}
},
},
beforeMount() {
if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
this.namespaceValue = localStorage.getItem('namespace')
}
this.getNamespaces()
this.getSecrets()
}
}
</script>
<style scoped>
.secret-head-card,.secret-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.secret-head-search {
width:160px;
margin-right:10px;
}
.secret-body-secretname {
color: #4795EE;
}
.secret-body-secretname:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,518 @@
<template>
<div class="service">
<el-row>
<el-col :span="24">
<div>
<el-card class="service-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="6">
<div>
<span>命名空间: </span>
<el-select v-model="namespaceValue" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</div>
</el-col>
<el-col :span="2" :offset="16">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getServices()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="service-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button style="border-radius:2px;" icon="Edit" type="primary" @click="createServiceDrawer = true" v-loading.fullscreen.lock="fullscreenLoading">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="service-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getServices()">搜索</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="service-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="serviceList"
v-loading="appLoading">
<el-table-column width="20"></el-table-column>
<el-table-column align=left label="Service名">
<template v-slot="scope">
<a class="service-body-servicename">{{ scope.row.metadata.name }}</a>
</template>
</el-table-column>
<el-table-column align=center label="标签" min-width='120'>
<template v-slot="scope">
<div v-for="(val, key) in scope.row.metadata.labels" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="key + ':' + val">
<template #reference>
<el-tag style="margin-bottom: 5px" type="warning">{{ ellipsis(key + ":" + val) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="类型">
<template v-slot="scope">
<span style="font-weight:bold;">{{ scope.row.spec.type }} </span>
</template>
</el-table-column>
<el-table-column align=center label="CLUSTER-IP">
<template v-slot="scope">
<span>{{ scope.row.spec.clusterIP }} </span>
</template>
</el-table-column>
<el-table-column align=center label="EXTERNAL-IP">
<template v-slot="scope">
<span>{{ scope.row.status.loadBalancer.ingress ? scope.row.status.loadBalancer.ingress[0].ip : '' }} </span>
</template>
</el-table-column>
<el-table-column align=center label="端口">
<template v-slot="scope">
<span v-if="!scope.row.spec.ports[0].nodePort">{{ scope.row.spec.ports[0].port }}/{{ scope.row.spec.ports[0].protocol }}</span>
<span v-if="scope.row.spec.ports[0].nodePort">{{ scope.row.spec.ports[0].port }}:{{ scope.row.spec.ports[0].nodePort }}/{{ scope.row.spec.ports[0].protocol }}</span>
</template>
</el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="操作" width="200">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getServiceDetail(scope)">YAML</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delService)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="service-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="serviceTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="yamlDialog = false"> </el-button>
<el-button type="primary" @click="updateService()"> </el-button>
</span>
</template>
</el-dialog>
<el-drawer
v-model="createServiceDrawer"
:direction="direction"
:before-close="handleClose">
<template #title>
<h4>创建Service</h4>
</template>
<template #default>
<el-row type="flex" justify="center">
<el-col :span="20">
<el-form ref="createService" :rules="createServiceRules" :model="createService" label-width="80px">
<el-form-item class="service-create-form" label="名称" prop="name">
<el-input v-model="createService.name"></el-input>
</el-form-item>
<el-form-item class="service-create-form" label="命名空间" prop="namespace">
<el-select v-model="createService.namespace" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</el-form-item>
<el-form-item class="service-create-form" label="类型" prop="type">
<el-select v-model="createService.type" placeholder="请选择">
<el-option value="ClusterIP" label="ClusterIP"></el-option>
<el-option value="NodePort" label="NodePort"></el-option>
</el-select>
</el-form-item>
<el-form-item class="deploy-create-form" label="容器端口" prop="container_port">
<el-input v-model="createService.container_port" placeholder="示例: 80"></el-input>
</el-form-item>
<el-form-item class="service-create-form" label="Service端口" prop="port">
<el-input v-model="createService.port" placeholder="示例: 80"></el-input>
</el-form-item>
<el-form-item v-if="createService.type == 'NodePort'" class="service-create-form" label="NodePort" prop="node_port">
<el-input v-model="createService.node_port" placeholder="示例: 30001"></el-input>
</el-form-item>
<el-form-item class="SERVICE-create-form" label="标签" prop="label_str">
<el-input v-model="createService.label_str" placeholder="示例: project=ms,app=gateway"></el-input>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<template #footer>
<el-button @click="createServiceDrawer = false">取消</el-button>
<el-button type="primary" @click="submitForm('createService')">立即创建</el-button>
</template>
</el-drawer>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
serviceList: [],
serviceTotal: 0,
getServicesData: {
url: common.k8sServiceList,
params: {
filter_name: '',
namespace: '',
page: '',
limit: '',
}
},
serviceDetail: {},
getServiceDetailData: {
url: common.k8sServiceDetail,
params: {
service_name: '',
namespace: ''
}
},
yamlDialog: false,
updateServiceData: {
url: common.k8sServiceUpdate,
params: {
namespace: '',
content: ''
}
},
delServiceData: {
url: common.k8sServiceDel,
params: {
service_name: '',
namespace: '',
}
},
fullscreenLoading: false,
direction: 'rtl',
createServiceDrawer: false,
createService: {
name: '',
namespace: '',
type: 'ClusterIP',
container_port: '',
port: '',
node_port: '',
label: {},
label_str: ''
},
createServiceData: {
url: common.k8sServiceCreate,
params: {}
},
createServiceRules: {
name: [{
required: true,
message: '请填写名称',
trigger: 'change'
}],
namespace: [{
required: true,
message: '请选择命名空间',
trigger: 'change'
}],
port: [{
required: true,
message: '请填写Service端口',
trigger: 'change'
}],
node_port: [{
required: true,
message: '请填写NodePort',
trigger: 'change'
}],
label_str: [{
required: true,
message: '请填写标签',
trigger: 'change'
}],
container_port: [{
required: true,
message: '请填写容器端口',
trigger: 'change'
}],
},
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getServices()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getServices()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
restartTotal(e) {
let index, sum = 0
let containerStatuses = e.row.status.containerStatuses
for ( index in containerStatuses) {
sum = sum + containerStatuses[index].restartCount
}
return sum
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getServices() {
this.appLoading = true
this.getServicesData.params.filter_name = this.searchInput
this.getServicesData.params.namespace = this.namespaceValue
this.getServicesData.params.page = this.currentPage
this.getServicesData.params.limit = this.pagesize
httpClient.get(this.getServicesData.url, {params: this.getServicesData.params})
.then(res => {
this.serviceList = res.data.items
this.serviceTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
getServiceDetail(e) {
this.getServiceDetailData.params.service_name = e.row.metadata.name
this.getServiceDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getServiceDetailData.url, {params: this.getServiceDetailData.params})
.then(res => {
this.serviceDetail = res.data
this.contentYaml = this.transYaml(this.serviceDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updateService() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updateServiceData.params.namespace = this.namespaceValue
this.updateServiceData.params.content = content
httpClient.put(this.updateServiceData.url, this.updateServiceData.params)
.then(res => {
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
delService(e) {
this.delServiceData.params.service_name = e.row.metadata.name
this.delServiceData.params.namespace = this.namespaceValue
httpClient.delete(this.delServiceData.url, {data: this.delServiceData.params})
.then(res => {
this.getServices()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
createServiceFunc() {
let reg = new RegExp("(^[A-Za-z]+=[A-Za-z0-9]+).*")
if (!reg.test(this.createService.label_str)) {
this.$message.warning({
message: "标签填写异常,请确认后重新填写"
})
return
}
this.fullscreenLoading = true
let label = new Map()
let a = (this.createService.label_str).split(",")
a.forEach(item => {
let b = item.split("=")
label[b[0]] = b[1]
})
this.createServiceData.params = this.createService
this.createServiceData.params.label = label
this.createServiceData.params.container_port = parseInt(this.createService.container_port)
this.createServiceData.params.port = parseInt(this.createService.port)
this.createServiceData.params.node_port = parseInt(this.createService.node_port)
httpClient.post(this.createServiceData.url, this.createServiceData.params)
.then(res => {
this.$message.success({
message: res.msg
})
this.getServices()
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.resetForm('createService')
this.fullscreenLoading = false
this.createServiceDrawer = false
},
resetForm(formName) {
this.$refs[formName].resetFields()
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.createServiceFunc()
} else {
return false;
}
})
}
},
watch: {
namespaceValue: {
handler() {
localStorage.setItem('namespace', this.namespaceValue)
this.currentPage = 1
this.getServices()
}
},
},
beforeMount() {
if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
this.namespaceValue = localStorage.getItem('namespace')
}
this.getNamespaces()
this.getServices()
}
}
</script>
<style scoped>
.service-head-card,.service-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.service-head-search {
width:160px;
margin-right:10px;
}
.service-body-servicename {
color: #4795EE;
}
.service-body-servicename:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,370 @@
<template>
<div class="statefulset">
<el-row>
<el-col :span="24">
<div>
<el-card class="statefulset-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="6">
<div>
<span>命名空间: </span>
<el-select v-model="namespaceValue" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</div>
</el-col>
<el-col :span="2" :offset="16">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getStatefulSets()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="statefulset-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="2">
<div>
<el-button disabled style="border-radius:2px;" icon="Edit" type="primary">创建</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="statefulset-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getStatefulSets()">搜索</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="statefulset-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="statefulSetList"
v-loading="appLoading">
<el-table-column width="20"></el-table-column>
<el-table-column align=left label="StatefulSet名">
<template v-slot="scope">
<a class="statefulset-body-statefulsetname">{{ scope.row.metadata.name }}</a>
</template>
</el-table-column>
<el-table-column align=center label="标签">
<template v-slot="scope">
<div v-for="(val, key) in scope.row.metadata.labels" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="key + ':' + val">
<template #reference>
<el-tag style="margin-bottom: 5px" type="warning">{{ ellipsis(key + ":" + val) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="容器组">
<template v-slot="scope">
<span>{{ scope.row.status.currentReplicas>0?scope.row.status.currentReplicas:0 }} / {{ scope.row.spec.replicas>0?scope.row.spec.replicas:0 }} </span>
</template>
</el-table-column>
<el-table-column align=center min-width="100" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTrans(scope.row.metadata.creationTimestamp) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="镜像">
<template v-slot="scope">
<div v-for="(val, key) in scope.row.spec.template.spec.containers" :key="key">
<el-popover
placement="right"
:width="200"
trigger="hover"
:content="val.image">
<template #reference>
<el-tag style="margin-bottom: 5px">{{ ellipsis(val.image.split('/')[2]==undefined?val.image:val.image.split('/')[2]) }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column align=center label="操作" width="200">
<template v-slot="scope">
<el-button size="small" style="border-radius:2px;" icon="Edit" type="primary" plain @click="getStatefulSetDetail(scope)">YAML</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delStatefulSet)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="statefulset-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="statefulSetTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-dialog title="YAML信息" v-model="yamlDialog" width="45%" top="5%">
<codemirror
:value="contentYaml"
border
:options="cmOptions"
height="500"
style="font-size:14px;"
@change="onChange"
></codemirror>
<template #footer>
<span class="dialog-footer">
<el-button @click="yamlDialog = false"> </el-button>
<el-button type="primary" @click="updateStatefulSet()"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
import yaml2obj from 'js-yaml';
import json2yaml from 'json2yaml';
export default {
data() {
return {
cmOptions: common.cmOptions,
contentYaml: '',
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
statefulSetList: [],
statefulSetTotal: 0,
getStatefulSetsData: {
url: common.k8sStatefulSetList,
params: {
filter_name: '',
namespace: '',
page: '',
limit: '',
}
},
statefulSetDetail: {},
getStatefulSetDetailData: {
url: common.k8sStatefulSetDetail,
params: {
statefulset_name: '',
namespace: ''
}
},
yamlDialog: false,
updateStatefulSetData: {
url: common.k8sStatefulSetUpdate,
params: {
namespace: '',
content: ''
}
},
delStatefulSetData: {
url: common.k8sstatefulsetDel,
params: {
statefulset_name: '',
namespace: '',
}
}
}
},
methods: {
transYaml(content) {
return json2yaml.stringify(content)
},
transObj(content) {
return yaml2obj.load(content)
},
onChange(val) {
this.contentYaml = val
},
handleSizeChange(size) {
this.pagesize = size;
this.getStatefulSets()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getStatefulSets()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
restartTotal(e) {
let index, sum = 0
let containerStatuses = e.row.status.containerStatuses
for ( index in containerStatuses) {
sum = sum + containerStatuses[index].restartCount
}
return sum
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getStatefulSets() {
this.appLoading = true
this.getStatefulSetsData.params.filter_name = this.searchInput
this.getStatefulSetsData.params.namespace = this.namespaceValue
this.getStatefulSetsData.params.page = this.currentPage
this.getStatefulSetsData.params.limit = this.pagesize
httpClient.get(this.getStatefulSetsData.url, {params: this.getStatefulSetsData.params})
.then(res => {
this.statefulSetList = res.data.items
this.statefulSetTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
getStatefulSetDetail(e) {
this.getStatefulSetDetailData.params.statefulset_name = e.row.metadata.name
this.getStatefulSetDetailData.params.namespace = this.namespaceValue
httpClient.get(this.getStatefulSetDetailData.url, {params: this.getStatefulSetDetailData.params})
.then(res => {
this.statefulSetDetail = res.data
this.contentYaml = this.transYaml(this.statefulSetDetail)
this.yamlDialog = true
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
updateStatefulSet() {
let content = JSON.stringify(this.transObj(this.contentYaml))
this.updateStatefulSetData.params.namespace = this.namespaceValue
this.updateStatefulSetData.params.content = content
httpClient.put(this.updateStatefulSetData.url, this.updateStatefulSetData.params)
.then(res => {
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.yamlDialog = false
},
delStatefulSet(e) {
this.delStatefulSetData.params.statefulset_name = e.row.metadata.name
this.delStatefulSetData.params.namespace = this.namespaceValue
httpClient.delete(this.delStatefulSetData.url, {data: this.delStatefulSetData.params})
.then(res => {
this.getStatefulSets()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
},
watch: {
namespaceValue: {
handler() {
localStorage.setItem('namespace', this.namespaceValue)
this.currentPage = 1
this.getStatefulSets()
}
},
},
beforeMount() {
if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
this.namespaceValue = localStorage.getItem('namespace')
}
this.getNamespaces()
this.getStatefulSets()
}
}
</script>
<style scoped>
.statefulset-head-card,.statefulset-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.statefulset-head-search {
width:160px;
margin-right:10px;
}
.statefulset-body-statefulsetname {
color: #4795EE;
}
.statefulset-body-statefulsetname:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,608 @@
<template>
<div class="workflow">
<el-row>
<!-- header1 -->
<el-col :span="24">
<div>
<el-card class="workflow-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="6">
<div>
<span>命名空间: </span>
<el-select v-model="namespaceValue" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</div>
</el-col>
<el-col :span="2" :offset="16">
<div>
<el-button style="border-radius:2px;" icon="Refresh" plain @click="getWorkflows()">刷新</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<!-- header2 -->
<el-col :span="24">
<div>
<el-card class="workflow-head-card" shadow="never" :body-style="{padding:'30px 10px 20px 10px'}">
<el-steps :active="active" align-center finish-status="success">
<el-step title="步骤1" description="选择工作流类型, ClusterIP NodePort Workflow"></el-step>
<el-step title="步骤2" description="填写Deployment Workflow Workflow表单"></el-step>
<el-step title="步骤3" description="创建Deployment Workflow Workflow"></el-step>
</el-steps>
</el-card>
</div>
</el-col>
<!-- header3 -->
<el-col :span="24">
<div>
<el-card class="workflow-head-card" shadow="never" :body-style="{padding:'10px'}">
<el-row>
<el-col :span="3">
<div>
<el-button style="border-radius:2px;" icon="Edit" type="primary" @click="createWorkflowDrawerIndex1 = true" v-loading.fullscreen.lock="fullscreenLoading">创建工作流</el-button>
</div>
</el-col>
<el-col :span="6">
<div>
<el-input class="workflow-head-search" clearable placeholder="请输入" v-model="searchInput"></el-input>
<el-button style="border-radius:2px;" icon="Search" type="primary" plain @click="getWorkflows()">搜索</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="24">
<div>
<el-card class="workflow-body-card" shadow="never" :body-style="{padding:'5px'}">
<el-table
style="width:100%;font-size:12px;margin-bottom:10px;"
:data="workflowList"
v-loading="appLoading">
<el-table-column width="20"></el-table-column>
<el-table-column min-width="50" align=left label="ID" prop="id"></el-table-column>
<el-table-column min-width="100" label="Workflow名">
<template v-slot="scope">
<a class="workflow-body-workflowname">{{ scope.row.name }}</a>
</template>
</el-table-column>
<el-table-column label="类型" prop="type">
<template v-slot="scope">
<el-tag type="warning">{{ scope.row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column label="实例数" prop="replicas"></el-table-column>
<el-table-column min-width="100" label="deployment" prop="deployment"></el-table-column>
<el-table-column min-width="150" label="service" prop="service"></el-table-column>
<el-table-column min-width="150" label="ingress" prop="ingress"></el-table-column>
<el-table-column align=center min-width="150" label="创建时间">
<template v-slot="scope">
<el-tag type="info">{{ timeTransNot8(scope.row.created_at) }} </el-tag>
</template>
</el-table-column>
<el-table-column align=center label="操作" width="200">
<template v-slot="scope">
<el-button size="small" disabled style="border-radius:2px;" icon="Edit" type="primary" plain @click="getWorkflowDetail(scope)">详情</el-button>
<el-button size="small" style="border-radius:2px;" icon="Delete" type="danger" @click="handleConfirm(scope, '删除', delWorkflow)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="workflow-body-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pagesizeList"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="workflowTotal">
</el-pagination>
</el-card>
</div>
</el-col>
</el-row>
<el-drawer
v-model="createWorkflowDrawerIndex1"
:direction="direction"
:before-close="handleClose">
<template #title>
<h4>创建Workflow-步骤1</h4>
</template>
<template #default>
<el-row type="flex" justify="center">
<el-col :span="20">
<el-form label-width="80px">
<el-form-item class="workflow-create-form" label="类型" prop="name">
<el-radio v-model="createWorkflow.type" label="ClusterIP">ClusterIP</el-radio>
<el-radio v-model="createWorkflow.type" label="NodePort">NodePort</el-radio>
<el-radio v-model="createWorkflow.type" label="Ingress">Ingress</el-radio>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<template #footer>
<el-button @click="drawerCancel('createWorkflowDrawerIndex1')">取消</el-button>
<el-button type="primary" @click="workflowIndex1Next()">下一步</el-button>
</template>
</el-drawer>
<el-drawer
v-model="createWorkflowDrawerIndex2_1"
:direction="direction"
:before-close="handleClose">
<template #title>
<h4>创建Workflow-步骤2</h4>
</template>
<template #default>
<el-row type="flex" justify="center">
<el-col :span="20">
<el-form ref="createWorkflow" :rules="createWorkflowRules" :model="createWorkflow" label-width="80px">
<h4 style="margin-bottom:10px">Deployment</h4>
<el-form-item class="workflow-create-form" label="名称" prop="name">
<el-input v-model="createWorkflow.name"></el-input>
</el-form-item>
<el-form-item class="workflow-create-form" label="命名空间" prop="namespace">
<el-select v-model="createWorkflow.namespace" filterable placeholder="请选择">
<el-option
v-for="(item, index) in namespaceList"
:key="index"
:label="item.metadata.name"
:value="item.metadata.name">
</el-option>
</el-select>
</el-form-item>
<el-form-item class="workflow-create-form" label="副本数" prop="replicas">
<el-input-number v-model="createWorkflow.replicas" :min="1" :max="10"></el-input-number>
<el-popover
placement="top"
:width="100"
trigger="hover"
content="申请副本数上限为10个">
<template #reference>
<el-icon style="width:2em;font-size:18px;color:#4795EE"><WarningFilled/></el-icon>
</template>
</el-popover>
</el-form-item>
<el-form-item class="workflow-create-form" label="镜像" prop="image">
<el-input v-model="createWorkflow.image"></el-input>
</el-form-item>
<el-form-item class="workflow-create-form" label="标签" prop="label_str">
<el-input v-model="createWorkflow.label_str" placeholder="示例: project=ms,app=gateway"></el-input>
</el-form-item>
<el-form-item class="workflow-create-form" label="资源配额" prop="resource">
<el-select v-model="createWorkflow.resource" placeholder="请选择">
<el-option value="0.5/1" label="0.5C1G"></el-option>
<el-option value="1/2" label="1C2G"></el-option>
<el-option value="2/4" label="2C4G"></el-option>
<el-option value="4/8" label="4C8G"></el-option>
</el-select>
</el-form-item>
<el-form-item class="workflow-create-form" label="容器端口" prop="container_port">
<el-input v-model="createWorkflow.container_port" placeholder="示例: 80"></el-input>
</el-form-item>
<el-form-item class="workflow-create-form" label="健康检查" prop="health">
<el-switch v-model="createWorkflow.health_check" />
</el-form-item>
<el-form-item class="workflow-create-form" label="检查路径" prop="healthPath">
<el-input v-model="createWorkflow.health_path" placeholder="示例: /health"></el-input>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<template #footer>
<el-button @click="drawerCancel('createWorkflowDrawerIndex2_1')">取消</el-button>
<el-button type="primary" @click="submitForm('createWorkflow', workflowIndex2_1Next)">下一步</el-button>
</template>
</el-drawer>
<el-drawer
v-model="createWorkflowDrawerIndex2_2"
:direction="direction"
:before-close="handleClose">
<template #title>
<h4>创建Workflow-步骤2</h4>
</template>
<template #default>
<el-row type="flex" justify="center">
<el-col :span="20">
<el-form ref="createWorkflow" :rules="createWorkflowRules" :model="createWorkflow" label-width="80px">
<h4 style="margin-bottom:10px">Service</h4>
<el-form-item class="service-create-form" label="Service端口" prop="port">
<el-input v-model="createWorkflow.port" placeholder="示例: 80"></el-input>
</el-form-item>
<el-form-item v-if="createWorkflow.type == 'NodePort'" class="service-create-form" label="NodePort" prop="node_port">
<el-input v-model="createWorkflow.node_port" placeholder="示例: 30001"></el-input>
</el-form-item>
<el-divider v-if="createWorkflow.type == 'Ingress'"></el-divider>
<h4 v-if="createWorkflow.type == 'Ingress'" style="margin-bottom:10px">Ingress</h4>
<el-form-item v-if="createWorkflow.type == 'Ingress'" class="deploy-create-form" label="域名" prop="host">
<el-input v-model="createWorkflow.host" placeholder="示例: www.example.com"></el-input>
</el-form-item>
<el-form-item v-if="createWorkflow.type == 'Ingress'" class="ingress-create-form" label="Path" prop="path">
<el-input v-model="createWorkflow.path" placeholder="示例: /abc"></el-input>
</el-form-item>
<el-form-item v-if="createWorkflow.type == 'Ingress'" class="deploy-create-form" label="匹配类型" prop="path_type">
<el-select v-model="createWorkflow.path_type" placeholder="请选择">
<el-option value="Prefix" label="Prefix"></el-option>
<el-option value="Exact" label="Exact"></el-option>
<el-option value="ImplementationSpecific" label="ImplementationSpecific"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<template #footer>
<el-button @click="drawerCancel('createWorkflowDrawerIndex2_2')">取消</el-button>
<el-button type="primary" @click="submitForm('createWorkflow', createWorkflowFunc)">立即创建</el-button>
</template>
</el-drawer>
</div>
</template>
<script>
import common from "../common/Config";
import httpClient from '../../utils/request';
export default {
data() {
return {
active: 0,
createWorkflowDrawerIndex1: false,
createWorkflowDrawerIndex2_1: false,
createWorkflowDrawerIndex2_2: false,
currentPage: 1,
pagesize: 10,
pagesizeList: [10, 20, 30],
searchInput: '',
namespaceValue: 'default',
namespaceList: [],
namespaceListUrl: common.k8sNamespaceList,
appLoading: false,
workflowList: [],
workflowTotal: 0,
getWorkflowsData: {
url: common.k8sWorkflowList,
params: {
name: '',
namespace: '',
page: '',
limit: '',
}
},
fullscreenLoading: false,
direction: 'rtl',
createWorkflowDrawer: false,
createWorkflow: {
name: '',
namespace: '',
replicas: 1,
image: '',
resource: '',
health_check: false,
health_path: '',
label_str: '',
label: {},
container_port: '',
type: '',
port: '',
node_port: '',
host: '',
path: '',
path_type: ''
},
createWorkflowData: {
url: common.k8sWorkflowCreate,
params: {}
},
createWorkflowRules: {
name: [{
required: true,
message: '请填写名称',
trigger: 'change'
}],
image: [{
required: true,
message: '请填写镜像',
trigger: 'change'
}],
namespace: [{
required: true,
message: '请选择命名空间',
trigger: 'change'
}],
resource: [{
required: true,
message: '请选择配额',
trigger: 'change'
}],
label_str: [{
required: true,
message: '请填写标签',
trigger: 'change'
}],
container_port: [{
required: true,
message: '请填写容器端口',
trigger: 'change'
}],
type: [{
required: true,
message: '请填写工作流类型',
trigger: 'change'
}],
port: [{
required: true,
message: '请填写Workflow端口',
trigger: 'change'
}],
node_port: [{
required: true,
message: '请填写NodePort',
trigger: 'change'
}],
host: [{
required: true,
message: '请填写域名',
trigger: 'change'
}],
path: [{
required: true,
message: '请填写路径',
trigger: 'change'
}],
path_type: [{
required: true,
message: '你选择匹配类型',
trigger: 'change'
}],
},
delWorkflowData: {
url: common.k8sWorkflowDel,
params: {
id: ''
}
},
}
},
methods: {
handleSizeChange(size) {
this.pagesize = size;
this.getWorkflows()
},
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getWorkflows()
},
handleClose(done) {
this.$confirm('确认关闭?')
.then(() => {
done();
})
.catch(() => {});
this.active = 0
},
drawerCancel(drawerName) {
switch (drawerName) {
case 'createWorkflowDrawerIndex1':
this.createWorkflowDrawerIndex1 = false
break
case 'createWorkflowDrawerIndex2_1':
this.createWorkflowDrawerIndex2_1 = false
break
case 'createWorkflowDrawerIndex2_2':
this.createWorkflowDrawerIndex2_2 = false
}
this.active = 0
},
ellipsis(value) {
return value.length>15?value.substring(0,15)+'...':value
},
timeTrans(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
timeTransNot8(timestamp) {
let date = new Date(new Date(timestamp).getTime() + 8 * 3600 * 1000)
date = date.toJSON();
date = date.substring(0, 19).replace('T', ' ')
return date
},
getNamespaces() {
httpClient.get(this.namespaceListUrl)
.then(res => {
this.namespaceList = res.data.items
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
},
getWorkflows() {
this.appLoading = true
this.getWorkflowsData.params.name = this.searchInput
this.getWorkflowsData.params.namespace = this.namespaceValue
this.getWorkflowsData.params.page = this.currentPage
this.getWorkflowsData.params.limit = this.pagesize
httpClient.get(this.getWorkflowsData.url, {params: this.getWorkflowsData.params})
.then(res => {
this.workflowList = res.data.items
this.workflowTotal = res.data.total
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.appLoading = false
},
delWorkflow(e) {
this.delWorkflowData.params.id = e.row.id
httpClient.delete(this.delWorkflowData.url, {data: this.delWorkflowData.params})
.then(res => {
this.getWorkflows()
this.$message.success({
message: res.msg
})
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
console.log(123)
},
handleConfirm(obj, operateName, fn) {
this.confirmContent = '确认继续 ' + operateName + ' 操作吗?'
this.$confirm(this.confirmContent,'提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
fn(obj)
})
.catch(() => {
this.$message.info({
message: '已取消操作'
})
})
},
createWorkflowFunc() {
let reg = new RegExp("(^[A-Za-z]+=[A-Za-z0-9]+).*")
if (!reg.test(this.createWorkflow.label_str)) {
this.$message.warning({
message: "标签填写异常,请确认后重新填写"
})
return
}
this.fullscreenLoading = true
let label = new Map()
let cpu, memory
let a = (this.createWorkflow.label_str).split(",")
a.forEach(item => {
let b = item.split("=")
label[b[0]] = b[1]
})
let resourceList = this.createWorkflow.resource.split("/")
cpu = resourceList[0]
memory = resourceList[1] + "Gi"
this.createWorkflowData.params = this.createWorkflow
this.createWorkflowData.params.label = label
this.createWorkflowData.params.cpu = cpu
this.createWorkflowData.params.memory = memory
this.createWorkflowData.params.container_port = parseInt(this.createWorkflow.container_port)
this.createWorkflowData.params.port = parseInt(this.createWorkflow.port)
this.createWorkflowData.params.node_port = parseInt(this.createWorkflow.node_port)
if (this.createWorkflow.type == 'Ingress') {
let hosts = new Map()
let httpPaths = []
let httpPath = {
path: this.createWorkflow.path,
path_type: this.createWorkflow.path_type,
service_name: this.createWorkflow.name,
service_port: parseInt(this.createWorkflow.port)
}
httpPaths.push(httpPath)
hosts[this.createWorkflow.host] = httpPaths
this.createWorkflowData.params.hosts = hosts
}
httpClient.post(this.createWorkflowData.url, this.createWorkflowData.params)
.then(res => {
this.$message.success({
message: res.msg
})
this.getWorkflows()
})
.catch(res => {
this.$message.error({
message: res.msg
})
})
this.resetForm('createWorkflow')
this.createWorkflowDrawerIndex2_2 = false
this.active = 3
this.fullscreenLoading = false
},
resetForm(formName) {
this.$refs[formName].resetFields()
},
submitForm(formName, fn) {
this.$refs[formName].validate((valid) => {
if (valid) {
fn()
} else {
return false;
}
})
},
workflowIndex1Next() {
if (!this.createWorkflow.type) {
this.$message.warning({
message: "请选择工作流类型"
})
return
}
this.createWorkflowDrawerIndex1 = false
this.createWorkflowDrawerIndex2_1 = true
this.active = 1
},
workflowIndex2_1Next() {
this.createWorkflowDrawerIndex2_1 = false
this.createWorkflowDrawerIndex2_2 = true
}
},
watch: {
namespaceValue: {
handler() {
localStorage.setItem('namespace', this.namespaceValue)
this.currentPage = 1
this.getWorkflows()
}
},
},
beforeMount() {
if (localStorage.getItem('namespace') !== undefined && localStorage.getItem('namespace') !== null) {
this.namespaceValue = localStorage.getItem('namespace')
}
this.getNamespaces()
this.getWorkflows()
}
}
</script>
<style scoped>
.workflow-head-card,.workflow-body-card {
border-radius: 1px;
margin-bottom: 5px;
}
.workflow-head-search {
width:160px;
margin-right:10px;
}
.workflow-body-workflowname {
color: #4795EE;
}
.workflow-body-workflowname:hover {
color: rgb(84, 138, 238);
cursor: pointer;
font-weight: bold;
}
:v-deep .el-drawer__header {
margin-bottom: 0px !important;
}
v-deep .el-drawer__body {
padding: 0px 0px 0px 0px;
}
</style>

8
dkube-web/vue.config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
devServer:{
host: '0.0.0.0',
port: 8080,
open: true
},
lintOnSave: false
}