basic user auth

This commit is contained in:
yinuo
2022-05-06 10:36:00 +08:00
parent b1feaad9f7
commit 8942760a5a
58 changed files with 2265 additions and 13977 deletions

13125
ui/package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -17,11 +17,6 @@
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",

View File

@@ -1,127 +1,5 @@
<template>
<div id="app">
<div id="nav">
<h2 class="logo">Kafka 控制台</h2>
<router-link to="/" class="pad-l-r">主页</router-link>
<span>|</span
><router-link to="/cluster-page" class="pad-l-r">集群</router-link>
<span>|</span
><router-link to="/topic-page" class="pad-l-r">Topic</router-link>
<span>|</span
><router-link to="/group-page" class="pad-l-r">消费组</router-link>
<span>|</span
><router-link to="/message-page" class="pad-l-r">消息</router-link>
<span v-show="enableSasl">|</span
><router-link to="/acl-page" class="pad-l-r" v-show="enableSasl"
>Acl</router-link
>
<span>|</span
><router-link to="/op-page" class="pad-l-r">运维</router-link>
<span class="right">集群{{ clusterName }}</span>
</div>
<router-view class="content" />
<router-view />
</div>
</template>
<script>
import { KafkaClusterApi } from "@/utils/api";
import request from "@/utils/request";
import { mapMutations, mapState } from "vuex";
import { getClusterInfo } from "@/utils/local-cache";
import notification from "ant-design-vue/lib/notification";
import { CLUSTER } from "@/store/mutation-types";
export default {
data() {
return {
config: {},
};
},
created() {
const clusterInfo = getClusterInfo();
if (!clusterInfo) {
request({
url: KafkaClusterApi.peekClusterInfo.url,
method: KafkaClusterApi.peekClusterInfo.method,
}).then((res) => {
if (res.code == 0) {
this.switchCluster(res.data);
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
} else {
this.switchCluster(clusterInfo);
}
},
computed: {
...mapState({
clusterName: (state) => state.clusterInfo.clusterName,
enableSasl: (state) => state.clusterInfo.enableSasl,
}),
},
methods: {
...mapMutations({
switchCluster: CLUSTER.SWITCH,
}),
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
#app {
width: 100%;
height: 100%;
}
#nav {
background-color: #9fe0e0;
font-size: large;
padding-top: 1%;
padding-bottom: 1%;
margin-bottom: 1%;
text-align: center;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #61c126;
}
.pad-l-r {
padding-left: 10px;
padding-right: 10px;
}
.content {
padding-left: 2%;
padding-right: 2%;
height: 90%;
width: 100%;
}
.logo {
float: left;
left: 1%;
top: 1%;
position: absolute;
}
.right {
float: right;
right: 1%;
top: 2%;
position: absolute;
}
</style>
</template>

BIN
ui/src/assets/bg.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -0,0 +1,162 @@
<template>
<div id="main">
<div id="nav">
<h2 class="logo">Kafka 控制台</h2>
<span v-show="manager">
<router-link to="/home" class="pad-l-r">主页</router-link>
<span>|</span>
</span>
<span>
<router-link to="/cluster-page" class="pad-l-r">集群</router-link>
</span>
<span>
<span>|</span>
<router-link to="/topic-page" class="pad-l-r">Topic</router-link>
</span>
<span>
<span>|</span>
<router-link to="/group-page" class="pad-l-r">消费组</router-link>
</span>
<span>
<span>|</span>
<router-link to="/message-page" class="pad-l-r">消息</router-link>
</span>
<span v-show="manager && enableSasl">
<span>|</span>
<router-link to="/acl-page" class="pad-l-r">Acl</router-link>
</span>
<span>
<span>|</span>
<router-link to="/op-page" class="pad-l-r">运维</router-link>
</span>
<span v-show="manager">
<span>|</span>
<router-link to="/devops/user" class="pad-l-r">用户</router-link>
</span>
<span class="right">
<span>集群{{ clusterName }}</span>
<span> | </span>
<span @click="logout" style="cursor: pointer">登出</span>
</span>
</div>
<router-view class="content" />
</div>
</template>
<script>
import { KafkaClusterApi } from "@/utils/api";
import request from "@/utils/request";
import { mapMutations, mapState } from "vuex";
import { getClusterInfo } from "@/utils/local-cache";
import notification from "ant-design-vue/lib/notification";
import { CLUSTER } from "@/store/mutation-types";
import {isManager} from "../utils/role";
import router from "../router";
export default {
name: "Header",
data() {
return {
manager: isManager(),
config: {},
};
},
created() {
const clusterInfo = getClusterInfo();
if (!clusterInfo) {
request({
url: KafkaClusterApi.peekClusterInfo.url,
method: KafkaClusterApi.peekClusterInfo.method,
}).then((res) => {
if (res.code == 0) {
this.switchCluster(res.data);
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
} else {
this.switchCluster(clusterInfo);
}
},
computed: {
...mapState({
clusterName: (state) => state.clusterInfo.clusterName,
enableSasl: (state) => state.clusterInfo.enableSasl,
}),
},
methods: {
...mapMutations({
switchCluster: CLUSTER.SWITCH,
}),
logout: function (){
localStorage.clear();
router.push("/")
}
},
};
</script>
<style>
#main {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
#main {
width: 100%;
height: 100%;
}
#nav {
background-color: #9fe0e0;
font-size: large;
padding-top: 1%;
padding-bottom: 1%;
margin-bottom: 1%;
text-align: center;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #61c126;
}
.pad-l-r {
padding-left: 10px;
padding-right: 10px;
}
.content {
padding-left: 2%;
padding-right: 2%;
height: 90%;
width: 100%;
}
.logo {
float: left;
left: 1%;
top: 1%;
position: absolute;
}
.cluster {
float: right;
right: 8%;
top: 2%;
position: absolute;
}
.right {
float: right;
right: 2%;
top: 2%;
position: absolute;
}
</style>

View File

@@ -1,32 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</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

@@ -1,12 +1,30 @@
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
import Home from "../views/home/Home.vue";
import Login from "@/views/login/index";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "Login",
component: Login,
},
{
path: "/main",
name: "Main",
component: () =>
import( "../components/Header"),
},
{
path: "/devops/user",
name: "DevOpsUser",
component: () =>
import( "../views/user/index"),
},
{
path: "/home",
name: "Home",
component: Home,
},

View File

@@ -92,6 +92,29 @@ export const KafkaConfigApi = {
},
};
export const DevOpsUserAPi = {
createUser: {
url: "/devops/user/add",
method: "post",
},
userList: {
url: "/devops/user/list",
method: "get",
},
deleteUser: {
url: "/devops/user/",
method: "delete",
},
updateUser: {
url: "/devops/user/update",
method: "post",
},
login: {
url: "/devops/user/login",
method: "post",
},
}
export const KafkaTopicApi = {
getTopicNameList: {
url: "/topic",

View File

@@ -1,4 +1,5 @@
import axios from "axios";
import router from "../router";
import notification from "ant-design-vue/es/notification";
import { VueAxios } from "./axios";
import { getClusterInfo } from "@/utils/local-cache";
@@ -25,6 +26,7 @@ const errorHandler = (error) => {
// request interceptor
request.interceptors.request.use((config) => {
const clusterInfo = getClusterInfo();
config.headers["token"] = localStorage.getItem('token');
if (clusterInfo) {
config.headers["X-Cluster-Info-Id"] = clusterInfo.id;
// config.headers["X-Cluster-Info-Name"] = encodeURIComponent(clusterInfo.clusterName);
@@ -34,6 +36,10 @@ request.interceptors.request.use((config) => {
// response interceptor
request.interceptors.response.use((response) => {
if (response.data.code === -5000){
router.push({ path:'/'})
return
}
return response.data;
}, errorHandler);

3
ui/src/utils/role.js Normal file
View File

@@ -0,0 +1,3 @@
export function isManager() {
return 'manager' === localStorage.getItem("role");
}

87
ui/src/utils/validate.js Normal file
View File

@@ -0,0 +1,87 @@
/**
* Created by PanJiaChen on 16/11/18.
*/
/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ['admin', 'editor']
return valid_map.indexOf(str.trim()) >= 0
}
/**
* @param {string} url
* @returns {Boolean}
*/
export function validURL(url) {
const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
return reg.test(url)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validLowerCase(str) {
const reg = /^[a-z]+$/
return reg.test(str)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUpperCase(str) {
const reg = /^[A-Z]+$/
return reg.test(str)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validAlphabets(str) {
const reg = /^[A-Za-z]+$/
return reg.test(str)
}
/**
* @param {string} email
* @returns {Boolean}
*/
export function validEmail(email) {
const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return reg.test(email)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function isString(str) {
if (typeof str === 'string' || str instanceof String) {
return true
}
return false
}
/**
* @param {Array} arg
* @returns {Boolean}
*/
export function isArray(arg) {
if (typeof Array.isArray === 'undefined') {
return Object.prototype.toString.call(arg) === '[object Array]'
}
return Array.isArray(arg)
}

View File

@@ -1,157 +1,160 @@
<template>
<div class="content">
<a-spin :spinning="loading">
<div class="acl">
<div id="components-form-acl-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`用户名`">
<a-input
placeholder="username"
class="input-w"
v-decorator="['username']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`topic`">
<a-input
placeholder="topic"
class="input-w"
v-decorator="['topic']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`消费组`">
<a-input
placeholder="groupId"
class="input-w"
v-decorator="['groupId']"
/>
</a-form-item>
</a-col>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<div class="acl">
<div id="components-form-acl-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`用户名`">
<a-input
placeholder="username"
class="input-w"
v-decorator="['username']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`topic`">
<a-input
placeholder="topic"
class="input-w"
v-decorator="['topic']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`消费组`">
<a-input
placeholder="groupId"
class="input-w"
v-decorator="['groupId']"
/>
</a-form-item>
</a-col>
<a-col :span="24" :style="{ textAlign: 'right' }">
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="updateUser">新增/更新用户</a-button>
<UpdateUser
:visible="showUpdateUser"
@updateUserDialogData="closeUpdateUserDialog"
></UpdateUser>
</div>
<a-table :columns="columns" :data-source="data" bordered>
<div slot="username" slot-scope="username">
<a-col :span="24" :style="{ textAlign: 'right' }">
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="updateUser">新增/更新用户</a-button>
<UpdateUser
:visible="showUpdateUser"
@updateUserDialogData="closeUpdateUserDialog"
></UpdateUser>
</div>
<a-table :columns="columns" :data-source="data" bordered>
<div slot="username" slot-scope="username">
<span>{{ username }}</span
><a-button
size="small"
shape="round"
type="dashed"
style="float: right"
@click="onUserDetail(username)"
>详情</a-button
size="small"
shape="round"
type="dashed"
style="float: right"
@click="onUserDetail(username)"
>详情</a-button
>
</div>
</div>
<div slot="topicList" slot-scope="topicList, record">
<a
href="#"
v-for="t in topicList"
:key="t"
@click="onTopicDetail(t, record.username)"
<div slot="topicList" slot-scope="topicList, record">
<a
href="#"
v-for="t in topicList"
:key="t"
@click="onTopicDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
</a>
</div>
<div slot="groupList" slot-scope="groupList, record">
<a
href="#"
v-for="t in groupList"
:key="t"
@click="onGroupDetail(t, record.username)"
<div slot="groupList" slot-scope="groupList, record">
<a
href="#"
v-for="t in groupList"
:key="t"
@click="onGroupDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
</a>
</div>
<div
slot="operation"
slot-scope="record"
v-show="!record.user || record.user.role != 'admin'"
>
<a-popconfirm
:title="'删除用户: ' + record.username + '及相关权限?'"
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUser(record)"
<div
slot="operation"
slot-scope="record"
v-show="!record.user || record.user.role != 'admin'"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除</a-button
<a-popconfirm
:title="'删除用户: ' + record.username + '及相关权限?'"
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUser(record)"
>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageProducerAuth(record)"
<a-button size="small" href="javascript:;" class="operation-btn"
>删除</a-button
>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageProducerAuth(record)"
>管理生产权限
</a-button>
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
>管理消费权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onAddAuth(record)"
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onAddAuth(record)"
>增加权限
</a-button>
</div>
</a-table>
<UserDetail
:visible="openUserDetailDialog"
:username="selectDetail.username"
@userDetailDialog="closeUserDetailDialog"
></UserDetail>
<AclDetail
:visible="openAclDetailDialog"
:selectDetail="selectDetail"
@aclDetailDialog="closeAclDetailDialog"
></AclDetail>
<ManageProducerAuth
:visible="openManageProducerAuthDialog"
:record="selectRow"
@manageProducerAuthDialog="closeManageProducerAuthDialog"
></ManageProducerAuth>
<ManageConsumerAuth
:visible="openManageConsumerAuthDialog"
:record="selectRow"
@manageConsumerAuthDialog="closeManageConsumerAuthDialog"
></ManageConsumerAuth>
<AddAuth
:visible="openAddAuthDialog"
:record="selectRow"
@addAuthDialog="closeAddAuthDialog"
></AddAuth>
</div>
</a-spin>
</a-button>
</div>
</a-table>
<UserDetail
:visible="openUserDetailDialog"
:username="selectDetail.username"
@userDetailDialog="closeUserDetailDialog"
></UserDetail>
<AclDetail
:visible="openAclDetailDialog"
:selectDetail="selectDetail"
@aclDetailDialog="closeAclDetailDialog"
></AclDetail>
<ManageProducerAuth
:visible="openManageProducerAuthDialog"
:record="selectRow"
@manageProducerAuthDialog="closeManageProducerAuthDialog"
></ManageProducerAuth>
<ManageConsumerAuth
:visible="openManageConsumerAuthDialog"
:record="selectRow"
@manageConsumerAuthDialog="closeManageConsumerAuthDialog"
></ManageConsumerAuth>
<AddAuth
:visible="openAddAuthDialog"
:record="selectRow"
@addAuthDialog="closeAddAuthDialog"
></AddAuth>
</div>
</a-spin>
</div>
</div>
</template>
@@ -165,7 +168,7 @@ import ManageConsumerAuth from "@/views/acl/ManageConsumerAuth";
import AddAuth from "@/views/acl/AddAuth";
import AclDetail from "@/views/acl/AclDetail";
import UserDetail from "@/views/acl/UserDetail";
import Header from "@/components/Header"
export default {
name: "Acl",
components: {
@@ -175,6 +178,7 @@ export default {
AddAuth,
AclDetail,
UserDetail,
Header
},
data() {
return {

View File

@@ -1,43 +1,47 @@
<template>
<div class="content">
<a-spin :spinning="loading">
<div class="body-c">
<div class="cluster-id">
<h3>集群ID{{ clusterId }}</h3>
</div>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<div class="body-c">
<div class="cluster-id">
<h3>集群ID{{ clusterId }}</h3>
</div>
<a-table :columns="columns" :data-source="data" bordered row-key="id">
<div slot="addr" slot-scope="text, record">
{{ record.host }}:{{ record.port }}
</div>
<div slot="controller" slot-scope="text">
<span v-if="text" style="color: red"></span><span v-else></span>
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openBrokerConfigDialog(record, false)"
<a-table :columns="columns" :data-source="data" bordered row-key="id">
<div slot="addr" slot-scope="text, record">
{{ record.host }}:{{ record.port }}
</div>
<div slot="controller" slot-scope="text">
<span v-if="text" style="color: red"></span><span v-else></span>
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openBrokerConfigDialog(record, false)"
>属性配置
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openBrokerConfigDialog(record, true)"
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openBrokerConfigDialog(record, true)"
>日志配置
</a-button>
</div>
</a-table>
</div>
<BrokerConfig
:visible="showBrokerConfigDialog"
:id="this.select.idString"
:is-logger-config="isLoggerConfig"
@closeBrokerConfigDialog="closeBrokerConfigDialog"
></BrokerConfig>
</a-spin>
</a-button>
</div>
</a-table>
</div>
<BrokerConfig
:visible="showBrokerConfigDialog"
:id="this.select.idString"
:is-logger-config="isLoggerConfig"
@closeBrokerConfigDialog="closeBrokerConfigDialog"
></BrokerConfig>
</a-spin>
</div>
</div>
</template>
@@ -46,12 +50,14 @@ import request from "@/utils/request";
import { KafkaClusterApi } from "@/utils/api";
import BrokerConfig from "@/views/cluster/BrokerConfig";
import notification from "ant-design-vue/lib/notification";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default {
name: "Topic",
components: { BrokerConfig },
components: { BrokerConfig, Header },
data() {
return {
manager: isManager(),
data: [],
columns,
loading: false,

View File

@@ -1,144 +1,148 @@
<template>
<div class="content">
<a-spin :spinning="loading">
<div class="topic">
<div id="form-consumer-group-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`消费组`">
<a-input
placeholder="groupId"
class="input-w"
v-decorator="['groupId']"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="`状态`">
<a-checkbox-group
v-decorator="['states']"
style="width: 100%"
>
<a-row>
<a-col :span="8">
<a-checkbox value="Empty"> Empty</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="PreparingRebalance">
PreparingRebalance
</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="CompletingRebalance">
CompletingRebalance
</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="Stable"> Stable</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="Dead"> Dead</a-checkbox>
</a-col>
</a-row>
</a-checkbox-group>
</a-form-item>
</a-col>
<a-col :span="4" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openAddSubscriptionDialog"
>新增订阅</a-button
>
</div>
<a-table
:columns="columns"
:data-source="data"
bordered
row-key="groupId"
>
<div slot="members" slot-scope="text, record">
<a href="#" @click="openConsumerMemberDialog(record.groupId)"
>{{ text }}
</a>
</div>
<div slot="state" slot-scope="text">
{{ text }}
<!-- <span v-if="text" style="color: red"></span><span v-else></span>-->
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-popconfirm
:title="'删除消费组: ' + record.groupId + ''"
ok-text="确认"
cancel-text="取消"
@confirm="deleteGroup(record.groupId)"
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<div class="topic">
<div id="form-consumer-group-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumerMemberDialog(record.groupId)"
>消费端
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumerDetailDialog(record.groupId)"
>消费详情
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openOffsetPartitionDialog(record.groupId)"
>位移分区
</a-button>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`消费组`">
<a-input
placeholder="groupId"
class="input-w"
v-decorator="['groupId']"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="`状态`">
<a-checkbox-group
v-decorator="['states']"
style="width: 100%"
>
<a-row>
<a-col :span="8">
<a-checkbox value="Empty"> Empty</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="PreparingRebalance">
PreparingRebalance
</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="CompletingRebalance">
CompletingRebalance
</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="Stable"> Stable</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="Dead"> Dead</a-checkbox>
</a-col>
</a-row>
</a-checkbox-group>
</a-form-item>
</a-col>
<a-col :span="4" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
</a-table>
<Member
:visible="showConsumerGroupDialog"
:group="selectDetail.resourceName"
@closeConsumerMemberDialog="closeConsumerDialog"
></Member>
<ConsumerDetail
:visible="showConsumerDetailDialog"
:group="selectDetail.resourceName"
@closeConsumerDetailDialog="closeConsumerDetailDialog"
>
</ConsumerDetail>
<AddSupscription
:visible="showAddSubscriptionDialog"
@closeAddSubscriptionDialog="closeAddSubscriptionDialog"
>
</AddSupscription>
<OffsetTopicPartition
:visible="showOffsetPartitionDialog"
:group="selectDetail.resourceName"
@closeOffsetPartitionDialog="closeOffsetPartitionDialog"
></OffsetTopicPartition>
</div>
</a-spin>
<div v-show="manager" class="operation-row-button">
<a-button type="primary" @click="openAddSubscriptionDialog"
>新增订阅</a-button
>
</div>
<a-table
:columns="columns"
:data-source="data"
bordered
row-key="groupId"
>
<div slot="members" slot-scope="text, record">
<a href="#" @click="openConsumerMemberDialog(record.groupId)"
>{{ text }}
</a>
</div>
<div slot="state" slot-scope="text">
{{ text }}
<!-- <span v-if="text" style="color: red"></span><span v-else></span>-->
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-popconfirm
:title="'删除消费组: ' + record.groupId + ''"
ok-text="确认"
cancel-text="取消"
@confirm="deleteGroup(record.groupId)"
>
<a-button v-show="manager" size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumerMemberDialog(record.groupId)"
>消费端
</a-button>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumerDetailDialog(record.groupId)"
>消费详情
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openOffsetPartitionDialog(record.groupId)"
>位移分区
</a-button>
</div>
</a-table>
<Member
:visible="showConsumerGroupDialog"
:group="selectDetail.resourceName"
@closeConsumerMemberDialog="closeConsumerDialog"
></Member>
<ConsumerDetail
:visible="showConsumerDetailDialog"
:group="selectDetail.resourceName"
@closeConsumerDetailDialog="closeConsumerDetailDialog"
>
</ConsumerDetail>
<AddSupscription
:visible="showAddSubscriptionDialog"
@closeAddSubscriptionDialog="closeAddSubscriptionDialog"
>
</AddSupscription>
<OffsetTopicPartition
:visible="showOffsetPartitionDialog"
:group="selectDetail.resourceName"
@closeOffsetPartitionDialog="closeOffsetPartitionDialog"
></OffsetTopicPartition>
</div>
</a-spin>
</div>
</div>
</template>
@@ -150,12 +154,14 @@ import Member from "@/views/group/Member";
import ConsumerDetail from "@/views/group/ConsumerDetail";
import AddSupscription from "@/views/group/AddSupscription";
import OffsetTopicPartition from "@/views/group/OffsetTopicPartition";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default {
name: "ConsumerGroup",
components: { Member, ConsumerDetail, AddSupscription, OffsetTopicPartition },
components: { Member, ConsumerDetail, AddSupscription, OffsetTopicPartition, Header },
data() {
return {
manager: isManager(),
queryParam: {},
data: [],
columns,

View File

@@ -1,35 +1,38 @@
<template>
<div class="home">
<a-card title="控制台默认配置" class="card-style">
<p v-for="(v, k) in config" :key="k">{{ k }}={{ v }}</p>
</a-card>
<p></p>
<hr />
<h3>kafka API 版本兼容性</h3>
<a-spin :spinning="apiVersionInfoLoading">
<a-table
:columns="columns"
:data-source="brokerApiVersionInfo"
bordered
row-key="brokerId"
>
<div slot="operation" slot-scope="record">
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openApiVersionInfoDialog(record)"
<div>
<Header/>
<div class="content">
<a-card title="控制台默认配置" class="card-style">
<p v-for="(v, k) in config" :key="k">{{ k }}={{ v }}</p>
</a-card>
<p></p>
<hr />
<h3>kafka API 版本兼容性</h3>
<a-spin :spinning="apiVersionInfoLoading">
<a-table
:columns="columns"
:data-source="brokerApiVersionInfo"
bordered
row-key="brokerId"
>
<div slot="operation" slot-scope="record">
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openApiVersionInfoDialog(record)"
>详情
</a-button>
</div>
</a-table>
</a-spin>
<VersionInfo
:version-info="apiVersionInfo"
:visible="showApiVersionInfoDialog"
@closeApiVersionInfoDialog="closeApiVersionInfoDialog"
>
</VersionInfo>
</a-button>
</div>
</a-table>
</a-spin>
<VersionInfo
:version-info="apiVersionInfo"
:visible="showApiVersionInfoDialog"
@closeApiVersionInfoDialog="closeApiVersionInfoDialog"
>
</VersionInfo>
</div>
</div>
</template>
@@ -39,9 +42,10 @@ import request from "@/utils/request";
import { KafkaConfigApi, KafkaClusterApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import VersionInfo from "@/views/home/VersionInfo";
import Header from "@/components/Header"
export default {
name: "Home",
components: { VersionInfo },
components: { VersionInfo, Header },
data() {
return {
config: {},

View File

@@ -0,0 +1,99 @@
<template>
<div id="login">
<div class="kafka-console-ui">
<span style="font-size: xxx-large; font-weight: bold">kafka-console-ui</span>
</div>
<div>
<a-form
:form="form"
:label-col="{ span: 10 }"
:wrapper-col="{ span: 4 }"
@submit="handleSubmit"
>
<a-form-item label="账号">
<a-input
v-decorator="[
'username',
{ rules: [{ required: true, message: '请输入账号' }] },
]"
placeholder="请输入账号"
/>
</a-form-item>
<a-form-item label="密码">
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码' }] },
]"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 10, offset: 10 }">
<a-button type="primary" html-type="submit"> 提交 </a-button>
</a-form-item>
</a-form>
</div>
<router-view></router-view>
</div>
</template>
<script>
import notification from "ant-design-vue/lib/notification";
import request from "@/utils/request";
import { DevOpsUserAPi } from "@/utils/api";
export default {
name: 'login',
data(){
return{
form: this.$form.createForm(this, { name: "coordinated" }),
}
},methods:{
handleSubmit(e){
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
if (values.configs) {
const config = {};
values.configs.split("\n").forEach((e) => {
const c = e.split("=");
if (c.length > 1) {
let k = c[0].trim(),
v = c[1].trim();
if (k && v) {
config[k] = v;
}
}
});
values.configs = config;
}
request({
url: DevOpsUserAPi.login.url,
method: DevOpsUserAPi.login.method,
data: values,
}).then((res) => {
if (res.code == 0) {
localStorage.setItem('token', res.data.token)
localStorage.setItem('role', res.data.role)
this.$router.push({ path:'/main'})
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
}
}
}
</script>
<style>
.kafka-console-ui{
text-align: center;
height: 100px;
margin-top: 50px;
margin-bottom: 20px;
}
</style>

View File

@@ -1,18 +1,21 @@
<template>
<div class="content">
<a-spin :spinning="loading">
<a-tabs default-active-key="1" size="large" tabPosition="top">
<a-tab-pane key="1" tab="根据时间查询消息">
<SearchByTime :topic-list="topicList"></SearchByTime>
</a-tab-pane>
<a-tab-pane key="2" tab="根据偏移查询消息">
<SearchByOffset :topic-list="topicList"></SearchByOffset>
</a-tab-pane>
<a-tab-pane key="3" tab="在线发送">
<SendMessage :topic-list="topicList"></SendMessage>
</a-tab-pane>
</a-tabs>
</a-spin>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<a-tabs default-active-key="1" size="large" tabPosition="top">
<a-tab-pane key="1" tab="根据时间查询消息">
<SearchByTime :topic-list="topicList"></SearchByTime>
</a-tab-pane>
<a-tab-pane key="2" tab="根据偏移查询消息">
<SearchByOffset :topic-list="topicList"></SearchByOffset>
</a-tab-pane>
<a-tab-pane key="3" tab="在线发送">
<SendMessage :topic-list="topicList"></SendMessage>
</a-tab-pane>
</a-tabs>
</a-spin>
</div>
</div>
</template>
@@ -23,9 +26,10 @@ import request from "@/utils/request";
import { KafkaTopicApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import SendMessage from "@/views/message/SendMessage";
import Header from "@/components/Header"
export default {
name: "Message",
components: { SearchByTime, SearchByOffset, SendMessage },
components: { SearchByTime, SearchByOffset, SendMessage, Header },
data() {
return {
loading: false,

View File

@@ -1,144 +1,147 @@
<template>
<div class="content">
<div class="content-module">
<a-card title="集群管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openClusterInfoDialog">
集群切换
</a-button>
<label>说明</label>
<span
<div>
<Header/>
<div class="content">
<div class="content-module">
<a-card title="集群管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openClusterInfoDialog">
集群切换
</a-button>
<label>说明</label>
<span
>多集群管理增加删除集群配置切换选中集群为当前操作集群</span
>
</p>
</a-card>
</div>
<div class="content-module">
<a-card title="Broker管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openConfigThrottleDialog">
配置限流
</a-button>
<label>说明</label>
<span
>
</p>
</a-card>
</div>
<div class="content-module" v-show="manager">
<a-card title="Broker管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openConfigThrottleDialog">
配置限流
</a-button>
<label>说明</label>
<span
>设置指定broker上的topic的副本之间数据同步占用的带宽这个设置是broker级别的但是设置后还要去对应的topic上进行限流配置指定对这个topic的相关副本进行限制</span
>
</p>
<p>
<a-button type="primary" @click="openRemoveThrottleDialog">
解除限流
</a-button>
<label>说明</label>
<span>解除指定broker上的topic副本之间数据同步占用的带宽限制</span>
</p>
</a-card>
</div>
<div class="content-module">
<a-card title="副本管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openElectPreferredLeaderDialog">
首选副本作为leader
</a-button>
<label>说明</label>
<span>将集群中所有分区leader副本设置为首选副本</span>
</p>
<p>
<a-button type="primary" @click="openCurrentReassignmentsDialog">
副本变更详情
</a-button>
<label>说明</label>
<span>查看正在进行副本变更/重分配的任务或者将其取消</span>
</p>
<p>
<a-button type="primary" @click="openReplicaReassignDialog">
副本重分配
</a-button>
<label>说明</label>
<span
>
</p>
<p>
<a-button type="primary" @click="openRemoveThrottleDialog">
解除限流
</a-button>
<label>说明</label>
<span>解除指定broker上的topic副本之间数据同步占用的带宽限制</span>
</p>
</a-card>
</div>
<div class="content-module" v-show="manager">
<a-card title="副本管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openElectPreferredLeaderDialog">
首选副本作为leader
</a-button>
<label>说明</label>
<span>将集群中所有分区leader副本设置为首选副本</span>
</p>
<p>
<a-button type="primary" @click="openCurrentReassignmentsDialog">
副本变更详情
</a-button>
<label>说明</label>
<span>查看正在进行副本变更/重分配的任务或者将其取消</span>
</p>
<p>
<a-button type="primary" @click="openReplicaReassignDialog">
副本重分配
</a-button>
<label>说明</label>
<span
>副本所在节点重新分配打个比方集群有6个节点分区1的3个副本在节点123现在将它们重新分配到345</span
>
</p>
</a-card>
</div>
<!-- 隐藏数据同步相关-->
<div class="content-module" v-show="false">
<a-card title="数据同步" style="width: 100%; text-align: left">
<p v-show="true">
<a-button type="primary" @click="openDataSyncSchemeDialog">
数据同步方案
</a-button>
<label>说明</label>
<span>新老集群迁移数据同步解决方案</span>
</p>
<p>
<a-button type="primary" @click="openMinOffsetAlignmentDialog">
最小位移对齐
</a-button>
<label>说明</label>
<span
>
</p>
</a-card>
</div>
<!-- 隐藏数据同步相关-->
<div class="content-module" v-show="false">
<a-card title="数据同步" style="width: 100%; text-align: left">
<p v-show="true">
<a-button type="primary" @click="openDataSyncSchemeDialog">
数据同步方案
</a-button>
<label>说明</label>
<span>新老集群迁移数据同步解决方案</span>
</p>
<p>
<a-button type="primary" @click="openMinOffsetAlignmentDialog">
最小位移对齐
</a-button>
<label>说明</label>
<span
>同步消费位点时需要获取两端集群中订阅分区的最小位移进行消费位点计算如需后面同步消费位点在进行数据同步前先进行最小位移对齐
点击右侧查看</span
><a href="javascript:;" @click="openOffsetAlignmentInfoDialog"
>对齐信息</a
><a href="javascript:;" @click="openOffsetAlignmentInfoDialog"
>对齐信息</a
>
</p>
<p>
<a-button type="primary" @click="openSyncConsumerOffsetDialog">
同步消费位点
</a-button>
<label>说明</label>
<span
</p>
<p>
<a-button type="primary" @click="openSyncConsumerOffsetDialog">
同步消费位点
</a-button>
<label>说明</label>
<span
>同步其它集群中指定消费组与订阅的topic的消费位点到当前集群上该消费组在当前集群已存在且双方订阅的topic分区信息一致</span
>
</p>
</a-card>
>
</p>
</a-card>
</div>
<SyncConsumerOffset
:visible="syncData.showSyncConsumerOffsetDialog"
@closeSyncConsumerOffsetDialog="closeSyncConsumerOffsetDialog"
>
</SyncConsumerOffset>
<MinOffsetAlignment
:visible="syncData.showMinOffsetAlignmentDialog"
@closeMinOffsetAlignmentDialog="closeMinOffsetAlignmentDialog"
>
</MinOffsetAlignment>
<OffsetAlignmentTable
:visible="syncData.showOffsetAlignmentInfoDialog"
@closeOffsetAlignmentInfoDialog="closeOffsetAlignmentInfoDialog"
></OffsetAlignmentTable>
<ElectPreferredLeader
:visible="replicationManager.showElectPreferredLeaderDialog"
@closeElectPreferredLeaderDialog="closeElectPreferredLeaderDialog"
></ElectPreferredLeader>
<DataSyncScheme
:visible="syncData.showDataSyncSchemeDialog"
@closeDataSyncSchemeDialog="closeDataSyncSchemeDialog"
>
</DataSyncScheme>
<ConfigThrottle
:visible="brokerManager.showConfigThrottleDialog"
@closeConfigThrottleDialog="closeConfigThrottleDialog"
>
</ConfigThrottle>
<RemoveThrottle
:visible="brokerManager.showRemoveThrottleDialog"
@closeRemoveThrottleDialog="closeRemoveThrottleDialog"
>
</RemoveThrottle>
<CurrentReassignments
:visible="replicationManager.showCurrentReassignmentsDialog"
@closeCurrentReassignmentsDialog="closeCurrentReassignmentsDialog"
></CurrentReassignments>
<ClusterInfo
:visible="clusterManager.showClusterInfoDialog"
@closeClusterInfoDialog="closeClusterInfoDialog"
></ClusterInfo>
<ReplicaReassign
:visible="replicationManager.showReplicaReassignDialog"
@closeReplicaReassignDialog="closeReplicaReassignDialog"
>
</ReplicaReassign>
</div>
<SyncConsumerOffset
:visible="syncData.showSyncConsumerOffsetDialog"
@closeSyncConsumerOffsetDialog="closeSyncConsumerOffsetDialog"
>
</SyncConsumerOffset>
<MinOffsetAlignment
:visible="syncData.showMinOffsetAlignmentDialog"
@closeMinOffsetAlignmentDialog="closeMinOffsetAlignmentDialog"
>
</MinOffsetAlignment>
<OffsetAlignmentTable
:visible="syncData.showOffsetAlignmentInfoDialog"
@closeOffsetAlignmentInfoDialog="closeOffsetAlignmentInfoDialog"
></OffsetAlignmentTable>
<ElectPreferredLeader
:visible="replicationManager.showElectPreferredLeaderDialog"
@closeElectPreferredLeaderDialog="closeElectPreferredLeaderDialog"
></ElectPreferredLeader>
<DataSyncScheme
:visible="syncData.showDataSyncSchemeDialog"
@closeDataSyncSchemeDialog="closeDataSyncSchemeDialog"
>
</DataSyncScheme>
<ConfigThrottle
:visible="brokerManager.showConfigThrottleDialog"
@closeConfigThrottleDialog="closeConfigThrottleDialog"
>
</ConfigThrottle>
<RemoveThrottle
:visible="brokerManager.showRemoveThrottleDialog"
@closeRemoveThrottleDialog="closeRemoveThrottleDialog"
>
</RemoveThrottle>
<CurrentReassignments
:visible="replicationManager.showCurrentReassignmentsDialog"
@closeCurrentReassignmentsDialog="closeCurrentReassignmentsDialog"
></CurrentReassignments>
<ClusterInfo
:visible="clusterManager.showClusterInfoDialog"
@closeClusterInfoDialog="closeClusterInfoDialog"
></ClusterInfo>
<ReplicaReassign
:visible="replicationManager.showReplicaReassignDialog"
@closeReplicaReassignDialog="closeReplicaReassignDialog"
>
</ReplicaReassign>
</div>
</template>
@@ -153,6 +156,8 @@ import RemoveThrottle from "@/views/op/RemoveThrottle";
import CurrentReassignments from "@/views/op/CurrentReassignments";
import ClusterInfo from "@/views/op/ClusterInfo";
import ReplicaReassign from "@/views/op/ReplicaReassign";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default {
name: "Operation",
components: {
@@ -166,9 +171,11 @@ export default {
CurrentReassignments,
ClusterInfo,
ReplicaReassign,
Header
},
data() {
return {
manager: isManager(),
syncData: {
showSyncConsumerOffsetDialog: false,
showMinOffsetAlignmentDialog: false,

View File

@@ -31,7 +31,7 @@
{{ i }}
</span>
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<div slot="operation" slot-scope="record" v-show="!record.internal && manager">
<a-popconfirm
:title="
'topic: ' +
@@ -68,6 +68,7 @@ import request from "@/utils/request";
import { KafkaOpApi, KafkaTopicApi } from "@/utils/api";
import notification from "ant-design-vue/es/notification";
import moment from "moment";
import {isManager} from "../../utils/role";
export default {
name: "PartitionInfo",
props: {
@@ -82,6 +83,7 @@ export default {
},
data() {
return {
manager: isManager(),
columns: columns,
show: this.visible,
data: [],

View File

@@ -1,176 +1,185 @@
<template>
<div class="content">
<a-spin :spinning="loading">
<div class="topic">
<div id="components-form-topic-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`topic`">
<a-input
placeholder="topic"
class="input-w"
v-decorator="['topic']"
@change="onTopicUpdate"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`类型`">
<a-select
class="type-select"
v-model="type"
placeholder="选择类型"
@change="getTopicList"
>
<a-select-option value="all"> 所有</a-select-option>
<a-select-option value="normal"> 普通</a-select-option>
<a-select-option value="system"> 系统</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 刷新</a-button>
<!-- <a-button :style="{ marginLeft: '8px' }" @click="handleReset">-->
<!-- 重置-->
<!-- </a-button>-->
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openCreateTopicDialog"
>新增</a-button
>
</div>
<a-table
:columns="columns"
:data-source="filteredData"
bordered
row-key="name"
>
<div slot="partitions" slot-scope="text, record">
<a href="#" @click="openPartitionInfoDialog(record.name)"
>{{ text }}
</a>
</div>
<div slot="internal" slot-scope="text">
<span v-if="text" style="color: red"></span><span v-else></span>
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-popconfirm
:title="'删除topic: ' + record.name + ''"
ok-text="确认"
cancel-text="取消"
@confirm="deleteTopic(record.name)"
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<div class="topic">
<div id="components-form-topic-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openPartitionInfoDialog(record.name)"
>分区详情
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openAddPartitionDialog(record.name)"
>增加分区
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumedDetailDialog(record.name)"
>消费详情
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openTopicConfigDialog(record.name)"
>属性配置
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openUpdateReplicaDialog(record.name)"
>变更副本
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openMessageStatsDialog(record.name)"
>发送统计
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openThrottleDialog(record.name)"
>限流
</a-button>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`topic`">
<a-input
placeholder="topic"
class="input-w"
v-decorator="['topic']"
@change="onTopicUpdate"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`类型`">
<a-select
class="type-select"
v-model="type"
placeholder="选择类型"
@change="getTopicList"
>
<a-select-option value="all"> 所有</a-select-option>
<a-select-option value="normal"> 普通</a-select-option>
<a-select-option value="system"> 系统</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 刷新</a-button>
<!-- <a-button :style="{ marginLeft: '8px' }" @click="handleReset">-->
<!-- 重置-->
<!-- </a-button>-->
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
</a-table>
<PartitionInfo
:topic="selectDetail.resourceName"
:visible="showPartitionInfo"
@closePartitionInfoDialog="closePartitionInfoDialog"
></PartitionInfo>
<CreateTopic
:visible="showCreateTopic"
@closeCreateTopicDialog="closeCreateTopicDialog"
>
</CreateTopic>
<AddPartition
:visible="showAddPartition"
:topic="selectDetail.resourceName"
@closeAddPartitionDialog="closeAddPartitionDialog"
></AddPartition>
<ConsumedDetail
:visible="showConsumedDetailDialog"
:topic="selectDetail.resourceName"
@closeConsumedDetailDialog="closeConsumedDetailDialog"
>
</ConsumedDetail>
<TopicConfig
:visible="showTopicConfigDialog"
:topic="selectDetail.resourceName"
@closeTopicConfigDialog="closeTopicConfigDialog"
></TopicConfig>
<UpdateReplica
:visible="showUpdateReplicaDialog"
:topic="selectDetail.resourceName"
@closeUpdateReplicaDialog="closeUpdateReplicaDialog"
></UpdateReplica>
<ConfigTopicThrottle
:visible="showThrottleDialog"
:topic="selectDetail.resourceName"
@closeThrottleDialog="closeThrottleDialog"
></ConfigTopicThrottle>
<SendStats
:visible="showSendStatsDialog"
:topic="selectDetail.resourceName"
@closeMessageStatsDialog="closeMessageStatsDialog"
></SendStats>
</div>
</a-spin>
<div v-show="manager" class="operation-row-button">
<a-button type="primary" @click="openCreateTopicDialog"
>新增</a-button
>
</div>
<a-table
:columns="columns"
:data-source="filteredData"
bordered
row-key="name"
>
<div slot="partitions" slot-scope="text, record">
<a href="#" @click="openPartitionInfoDialog(record.name)"
>{{ text }}
</a>
</div>
<div slot="internal" slot-scope="text">
<span v-if="text" style="color: red"></span><span v-else></span>
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-popconfirm
v-show="manager"
:title="'删除topic: ' + record.name + ''"
ok-text="确认"
cancel-text="取消"
@confirm="deleteTopic(record.name)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openPartitionInfoDialog(record.name)"
>分区详情
</a-button>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openAddPartitionDialog(record.name)"
>增加分区
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumedDetailDialog(record.name)"
>消费详情
</a-button>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openTopicConfigDialog(record.name)"
>属性配置
</a-button>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openUpdateReplicaDialog(record.name)"
>变更副本
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openMessageStatsDialog(record.name)"
>发送统计
</a-button>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openThrottleDialog(record.name)"
>限流
</a-button>
</div>
</a-table>
<PartitionInfo
:topic="selectDetail.resourceName"
:visible="showPartitionInfo"
@closePartitionInfoDialog="closePartitionInfoDialog"
></PartitionInfo>
<CreateTopic
:visible="showCreateTopic"
@closeCreateTopicDialog="closeCreateTopicDialog"
>
</CreateTopic>
<AddPartition
:visible="showAddPartition"
:topic="selectDetail.resourceName"
@closeAddPartitionDialog="closeAddPartitionDialog"
></AddPartition>
<ConsumedDetail
:visible="showConsumedDetailDialog"
:topic="selectDetail.resourceName"
@closeConsumedDetailDialog="closeConsumedDetailDialog"
>
</ConsumedDetail>
<TopicConfig
:visible="showTopicConfigDialog"
:topic="selectDetail.resourceName"
@closeTopicConfigDialog="closeTopicConfigDialog"
></TopicConfig>
<UpdateReplica
:visible="showUpdateReplicaDialog"
:topic="selectDetail.resourceName"
@closeUpdateReplicaDialog="closeUpdateReplicaDialog"
></UpdateReplica>
<ConfigTopicThrottle
:visible="showThrottleDialog"
:topic="selectDetail.resourceName"
@closeThrottleDialog="closeThrottleDialog"
></ConfigTopicThrottle>
<SendStats
:visible="showSendStatsDialog"
:topic="selectDetail.resourceName"
@closeMessageStatsDialog="closeMessageStatsDialog"
></SendStats>
</div>
</a-spin>
</div>
</div>
</template>
@@ -186,7 +195,8 @@ import TopicConfig from "@/views/topic/TopicConfig";
import UpdateReplica from "@/views/topic/UpdateReplica";
import ConfigTopicThrottle from "@/views/topic/ConfigTopicThrottle";
import SendStats from "@/views/topic/SendStats";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default {
name: "Topic",
components: {
@@ -198,9 +208,11 @@ export default {
UpdateReplica,
ConfigTopicThrottle,
SendStats,
Header
},
data() {
return {
manager: isManager(),
queryParam: { type: "normal" },
data: [],
columns,

View File

@@ -0,0 +1,135 @@
<template>
<a-modal
title="新增用户"
:visible="show"
:width="800"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
>
<div>
<a-spin :spinning="loading">
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
>
<a-form-item label="账号">
<a-input
v-decorator="[
'username',
{ rules: [{ required: true, message: '请输入用户名!' }] },
]"
placeholder="请输入用户名"
/>
</a-form-item>
<a-form-item label="密码">
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码!' }] },
]"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item label="角色">
<a-select
option-filter-prop="role"
v-decorator="[
'role',
{ rules: [{ required: true, message: '请选择一个角色!' }] },
]"
placeholder="请选择一个角色"
>
<a-select-option v-for="v in roleList" :key="v" :value="v">
{{ v }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :wrapper-col="{ span: 12, offset: 5 }">
<a-button type="primary" html-type="submit"> 提交 </a-button>
</a-form-item>
</a-form>
</a-spin>
</div>
</a-modal>
</template>
<script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
import {DevOpsUserAPi} from "../../utils/api";
export default {
name: "CreateUser",
props: {
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
show: this.visible,
data: [],
roleList: roleList,
loading: false,
form: this.$form.createForm(this, { name: "coordinated" }),
};
},
watch: {
visible(v) {
this.show = v;
},
},
methods: {
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
if (values.configs) {
const config = {};
values.configs.split("\n").forEach((e) => {
const c = e.split("=");
if (c.length > 1) {
let k = c[0].trim(),
v = c[1].trim();
if (k && v) {
config[k] = v;
}
}
});
values.configs = config;
}
this.loading = true;
request({
url: DevOpsUserAPi.createUser.url,
method: DevOpsUserAPi.createUser.method,
data: values,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeCreateUserDialog", { refresh: true });
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
handleCancel() {
this.data = [];
this.$emit("closeCreateUserDialog", { refresh: false });
},
},
};
const roleList = ["developer", "manager"]
</script>
<style scoped></style>

View File

@@ -0,0 +1,123 @@
<template>
<a-modal
title="重置密码"
:visible="show"
:width="800"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
>
<div>
<a-spin :spinning="loading">
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
>
<a-form-item label="账号">
<a-input
:disabled="true"
v-decorator="['username', { initialValue: username }]"
placeholder="username"
/>
</a-form-item>
<a-form-item label="密码">
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码!' }] },
]"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 12, offset: 5 }">
<a-button type="primary" html-type="submit"> 提交 </a-button>
</a-form-item>
</a-form>
</a-spin>
</div>
</a-modal>
</template>
<script>
import request from "@/utils/request";
import { DevOpsUserAPi } from "@/utils/api";
import notification from "ant-design-vue/es/notification";
export default {
name: "UpdatePassword",
props: {
username: {
type: String,
default: "",
},
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
show: this.visible,
data: [],
loading: false,
form: this.$form.createForm(this, { name: "coordinated" }),
};
},
watch: {
visible(v) {
this.show = v;
},
},
methods: {
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
if (values.assignment) {
const assignment = {};
values.assignment.split("\n").forEach((e) => {
const c = e.split("=");
if (c.length > 1) {
let k = c[0];
let v = c[1];
let arr = v.split(",");
if (arr.length > 0) {
assignment[k] = arr;
}
}
});
values.assignment = assignment;
}
this.loading = true;
request({
url: DevOpsUserAPi.updateUser.url,
method: DevOpsUserAPi.updateUser.method,
data: values,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeResetPasswordDialog", { refresh: true });
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
handleCancel() {
this.data = [];
this.$emit("closeResetPasswordDialog", { refresh: false });
},
},
};
</script>
<style scoped></style>

227
ui/src/views/user/index.vue Normal file
View File

@@ -0,0 +1,227 @@
<template>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<div class="user">
<div class="operation-row-button">
<a-button type="primary" @click="openCreateUserDialog"
>新增</a-button
>
</div>
<a-table
:columns="columns"
:data-source="data"
bordered
row-key="name"
>
<div slot="role" slot-scope="text, record">
<a-select
@change="handleRoleChange(record.username, text)"
v-model="text"
option-filter-prop="role"
v-decorator="['role']"
style="width: 200px"
>
<a-select-option v-for="v in roleList" :key="v" :value="v">
{{ v }}
</a-select-option>
</a-select>
</div>
<div slot="operation" slot-scope="record">
<a-popconfirm
:title="'删除用户: ' + record.username + ' '"
ok-text="确认"
cancel-text="取消"
@confirm="deleteUser(record.id)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="resetPassword(record.username)"
>重置密码
</a-button>
</div>
</a-table>
<CreateUser
:visible="showCreateUser"
@closeCreateUserDialog="closeCreateUserDialog"
>
</CreateUser>
<ResetPassword
:visible="showResetPassword"
:username="selectDetail.resourceName"
@closeResetPasswordDialog="closeResetPasswordDialog"
></ResetPassword>
</div>
</a-spin>
</div>
</div>
</template>
<script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
import CreateUser from "@/views/user/CreateUser";
import ResetPassword from "@/views/user/ResetPassword"
import Header from "@/components/Header"
import {DevOpsUserAPi} from "../../utils/api";
export default {
name: "DevOpsUser",
components: {
CreateUser,
ResetPassword,
Header
},
data() {
return {
queryParam: { type: "normal" },
roleList: ["developer", "manager"],
columns,
showUpdateUser: false,
deleteUserConfirm: false,
selectDetail: {
resourceName: "",
resourceType: "",
username: "",
},
loading: false,
showCreateUser: false,
showResetPassword: false,
type: "normal",
};
},
methods: {
handleRoleChange(username, role) {
this.loading = true;
request({
url: DevOpsUserAPi.updateUser.url,
method: DevOpsUserAPi.updateUser.method,
data: {
"username": username,
"role": role
}
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.getDevOpsUserList();
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
getDevOpsUserList() {
Object.assign(this.queryParam, { type: this.type });
this.loading = true;
request({
url: DevOpsUserAPi.userList.url,
method: DevOpsUserAPi.userList.method,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.data = res.data;
//this.filter();
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
deleteUser(id) {
request({
url: DevOpsUserAPi.deleteUser.url + "?id=" + id,
method: DevOpsUserAPi.deleteUser.method,
}).then((res) => {
if (res.code == 0) {
this.$message.success(res.msg);
this.getDevOpsUserList();
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
openCreateUserDialog() {
this.showCreateUser = true;
},
closeCreateUserDialog(res) {
this.showCreateUser = false;
if (res.refresh) {
this.getDevOpsUserList();
}
},
resetPassword(username) {
this.selectDetail.resourceName = username;
this.showResetPassword = true;
},
closeResetPasswordDialog(res) {
this.showResetPassword = false;
if (res.refresh) {
this.getDevOpsUserList();
}
},
},
created() {
this.getDevOpsUserList();
},
};
const columns = [
{
title: "账号",
dataIndex: "username",
key: "username",
},
{
title: "角色",
dataIndex: "role",
key: "role",
slots: { title: "role" },
scopedSlots: { customRender: "role" },
width: 300
},
{
title: "创建时间",
dataIndex: "createTime",
key: "createTime",
slots: { title: "createTime" },
width: 300
},
{
title: "操作",
key: "operation",
scopedSlots: { customRender: "operation" },
},
];
</script>
<style scoped>
.user {
width: 100%;
height: 100%;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 8px;
}
.operation-btn {
margin-right: 3%;
}
</style>