Compare commits
438 Commits
canal-1.1.
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87be50e876 | ||
|
|
e20e442446 | ||
|
|
08d736b33f | ||
|
|
cf628769d6 | ||
|
|
cf97b2ae31 | ||
|
|
ac6242bae9 | ||
|
|
c24307c893 | ||
|
|
a97d841737 | ||
|
|
8a5dc38a58 | ||
|
|
5110cfd49d | ||
|
|
da7ab64d55 | ||
|
|
09382c6b91 | ||
|
|
69a06dba09 | ||
|
|
1a265c8960 | ||
|
|
24877fdf03 | ||
|
|
c0de979595 | ||
|
|
fcadf564ba | ||
|
|
54c84b1535 | ||
|
|
4ded2869c9 | ||
|
|
f07a075dce | ||
|
|
dcea8afc93 | ||
|
|
c0619c4217 | ||
|
|
8a4199a7b0 | ||
|
|
79338be02d | ||
|
|
d18bde71c6 | ||
|
|
5a5e60d371 | ||
|
|
af9375440d | ||
|
|
9f2fc7409f | ||
|
|
556d998db1 | ||
|
|
25501db3ac | ||
|
|
8523a51623 | ||
|
|
16831825b8 | ||
|
|
1e37fe2dee | ||
|
|
5607444bad | ||
|
|
3a7223e460 | ||
|
|
553438e227 | ||
|
|
a3ea3d32d9 | ||
|
|
50a570290a | ||
|
|
6b7ddbbe92 | ||
|
|
2b53164253 | ||
|
|
e101287c25 | ||
|
|
b153e61f0d | ||
|
|
9bd4357f37 | ||
|
|
dad209feb8 | ||
|
|
e21e98aa83 | ||
|
|
c998b3f100 | ||
|
|
c15129b0ac | ||
|
|
0741ccda1e | ||
|
|
02fffc9e74 | ||
|
|
5ca4c39313 | ||
|
|
3162d7093d | ||
|
|
f350fbdc0a | ||
|
|
2d0dcf0e5d | ||
|
|
515c05c873 | ||
|
|
7ee551cbf7 | ||
|
|
923675aa49 | ||
|
|
ebb541001c | ||
|
|
44ed76ebb8 | ||
|
|
fe836f7515 | ||
|
|
b30544c750 | ||
|
|
66483ef83f | ||
|
|
b391188baf | ||
|
|
2a7a507987 | ||
|
|
f53749bfc5 | ||
|
|
80ca4366db | ||
|
|
be0d945d7d | ||
|
|
5a968b3715 | ||
|
|
1e5b8a2021 | ||
|
|
ff82fd6532 | ||
|
|
b060f46ccf | ||
|
|
7e31b40531 | ||
|
|
b2b3223d1d | ||
|
|
fea4d8210a | ||
|
|
fb3d2c6811 | ||
|
|
3439c48948 | ||
|
|
2b8a309ec7 | ||
|
|
9373429015 | ||
|
|
9493ff733d | ||
|
|
31530dc9af | ||
|
|
b0f3ff8dcc | ||
|
|
5c61f2fb56 | ||
|
|
7bb619cf12 | ||
|
|
d66a15fe35 | ||
|
|
aa05d8e2a0 | ||
|
|
5ff6449620 | ||
|
|
bbea802914 | ||
|
|
e2219c88dd | ||
|
|
a92e61f5a9 | ||
|
|
eaacc01b77 | ||
|
|
caa46f3672 | ||
|
|
c0ff058fe1 | ||
|
|
059f21208d | ||
|
|
8ca900377a | ||
|
|
6dffcc8573 | ||
|
|
711971599f | ||
|
|
87c7a95523 | ||
|
|
e255baff11 | ||
|
|
d457cd835e | ||
|
|
cf9c64629e | ||
|
|
307547746c | ||
|
|
c7d5ed4ba1 | ||
|
|
b9ed5caf05 | ||
|
|
348d218120 | ||
|
|
e2a64daad8 | ||
|
|
1413cc9196 | ||
|
|
04c11ec8eb | ||
|
|
54145ced5e | ||
|
|
fb35dd4254 | ||
|
|
e233bec66a | ||
|
|
67e0928d80 | ||
|
|
93dac2d335 | ||
|
|
9baeff55e4 | ||
|
|
ca79258e4f | ||
|
|
0e4c5c40d7 | ||
|
|
c65477d97a | ||
|
|
fc3cfc5c0c | ||
|
|
b4074206ca | ||
|
|
98677c5bc8 | ||
|
|
2c7e918f30 | ||
|
|
163f719971 | ||
|
|
55a5e7592b | ||
|
|
259008a7cd | ||
|
|
26ba84e27e | ||
|
|
c8ef9552ec | ||
|
|
bff394042a | ||
|
|
b322201889 | ||
|
|
ea8949298f | ||
|
|
ba924b9f1d | ||
|
|
7e0061fe93 | ||
|
|
6440b744f2 | ||
|
|
066b022bb3 | ||
|
|
738f41049f | ||
|
|
0f6b9d156a | ||
|
|
ed754b5203 | ||
|
|
11f89163c1 | ||
|
|
cda330e49e | ||
|
|
f9f810226d | ||
|
|
d4a09704f2 | ||
|
|
34e9440383 | ||
|
|
d623bea71a | ||
|
|
5ea9259f0d | ||
|
|
a216b55010 | ||
|
|
2786e06ccf | ||
|
|
63dfbf65df | ||
|
|
d90066e01b | ||
|
|
69cfbabb10 | ||
|
|
ab30f3bd8d | ||
|
|
681d03fbe7 | ||
|
|
f13cc4ee9f | ||
|
|
252ca0279f | ||
|
|
7241af84f7 | ||
|
|
159457cccd | ||
|
|
d8a70d0c2f | ||
|
|
ec247114fd | ||
|
|
f067f761c7 | ||
|
|
d4c8780f26 | ||
|
|
894ef6e610 | ||
|
|
cc75d3dda4 | ||
|
|
d8169cbf71 | ||
|
|
fccc6bc816 | ||
|
|
97aa441bca | ||
|
|
b8c2a757a0 | ||
|
|
c8de48bbd5 | ||
|
|
08239ef23f | ||
|
|
04a2b6f941 | ||
|
|
5f667c3574 | ||
|
|
bfb8cf4190 | ||
|
|
18c38ee057 | ||
|
|
82b3aed62b | ||
|
|
a04a1912a8 | ||
|
|
5d7d19fdb1 | ||
|
|
9edbe7e827 | ||
|
|
b52c5d149b | ||
|
|
aada80ba38 | ||
|
|
a10698e4a8 | ||
|
|
9d0a37241f | ||
|
|
41a2a87a91 | ||
|
|
62c4333790 | ||
|
|
ea8fb5e5b8 | ||
|
|
cd798b71e9 | ||
|
|
b937ee613d | ||
|
|
9c6f6f5cdf | ||
|
|
869e831b04 | ||
|
|
f7a4979b15 | ||
|
|
afef45500e | ||
|
|
4c7cdbc7f3 | ||
|
|
279d503cf4 | ||
|
|
272b1d5d11 | ||
|
|
fa0c08c170 | ||
|
|
10ff148aba | ||
|
|
571fa0d6fb | ||
|
|
aa29f12d96 | ||
|
|
3bceb07c44 | ||
|
|
c16d6e7729 | ||
|
|
fb6981356f | ||
|
|
c055737e24 | ||
|
|
ef7e21e4b2 | ||
|
|
ce916ee407 | ||
|
|
92758b8c87 | ||
|
|
7585d9c98b | ||
|
|
9d37bb87b7 | ||
|
|
2faecec749 | ||
|
|
0ef7b28c03 | ||
|
|
41bf3e2289 | ||
|
|
4d4199c801 | ||
|
|
b390e74a18 | ||
|
|
72c0e24cfd | ||
|
|
3dc86330a8 | ||
|
|
eee73dc4b1 | ||
|
|
a96db39420 | ||
|
|
bac1d543f1 | ||
|
|
753fad6639 | ||
|
|
bc4dceea1a | ||
|
|
7aaee7b439 | ||
|
|
e19bf1ecff | ||
|
|
a99dc0cbe8 | ||
|
|
7b8ff04d20 | ||
|
|
7ee246ca5a | ||
|
|
54c645f315 | ||
|
|
6c81ab6fdc | ||
|
|
99aefaa118 | ||
|
|
e2248b3d35 | ||
|
|
eef1ee85fb | ||
|
|
7dfec19c68 | ||
|
|
05a52f1bd3 | ||
|
|
fb0f274ba8 | ||
|
|
d71a208123 | ||
|
|
656a1d23c2 | ||
|
|
a574b91fdf | ||
|
|
e41e903984 | ||
|
|
4a22d72ef4 | ||
|
|
b7eff723a3 | ||
|
|
9b9cfe4037 | ||
|
|
ee51f338e0 | ||
|
|
4315f203a2 | ||
|
|
b12945d4a9 | ||
|
|
885f05fb6a | ||
|
|
591126ee6b | ||
|
|
af432bf10a | ||
|
|
b532e90ff4 | ||
|
|
e48bc35f35 | ||
|
|
11a5f2a0fa | ||
|
|
c775478a56 | ||
|
|
bd1f91cd9c | ||
|
|
67e85d201c | ||
|
|
6965726dfe | ||
|
|
c0001afe8f | ||
|
|
977031a666 | ||
|
|
bd3224eea1 | ||
|
|
33733aef25 | ||
|
|
25be0259a4 | ||
|
|
8114d77a20 | ||
|
|
571e7f4e25 | ||
|
|
ead91433d7 | ||
|
|
bac016f89e | ||
|
|
8804647c30 | ||
|
|
2623d08b8c | ||
|
|
622fed84ce | ||
|
|
9f5e8fd1c4 | ||
|
|
88d472ef1e | ||
|
|
2987d08161 | ||
|
|
b54bea5e33 | ||
|
|
c42ba95eed | ||
|
|
b48c434ffd | ||
|
|
2c892fce9b | ||
|
|
3bb49e19d2 | ||
|
|
f26b84ffc2 | ||
|
|
0a2254eeaf | ||
|
|
40d840375f | ||
|
|
5b6fd8094a | ||
|
|
4ce401d219 | ||
|
|
de95a6a128 | ||
|
|
9e11c3c018 | ||
|
|
d67bea8f3d | ||
|
|
997957d6e0 | ||
|
|
ea20076a32 | ||
|
|
2550d8eb65 | ||
|
|
31a0fdc807 | ||
|
|
d79431c7e5 | ||
|
|
911711248c | ||
|
|
63407dc570 | ||
|
|
3c20fd09ad | ||
|
|
eef3bd92ba | ||
|
|
474063af3b | ||
|
|
5cc85ff2de | ||
|
|
64862503ba | ||
|
|
f9e0dc7daa | ||
|
|
0c399509a9 | ||
|
|
a09cd9adb5 | ||
|
|
1369418c6a | ||
|
|
75174a63af | ||
|
|
17f4ee13e7 | ||
|
|
f0417502f8 | ||
|
|
a7720b51e4 | ||
|
|
21904ef1dc | ||
|
|
94425223bd | ||
|
|
7857df9701 | ||
|
|
64ea4ff222 | ||
|
|
85df89f777 | ||
|
|
a0f8e182ec | ||
|
|
33721d0394 | ||
|
|
91bddfeed6 | ||
|
|
25feab4af8 | ||
|
|
f66baeabf1 | ||
|
|
b1dc3821ea | ||
|
|
f5f79104eb | ||
|
|
e8d138d3bd | ||
|
|
32d9e58915 | ||
|
|
58448e559d | ||
|
|
fede3b3d1d | ||
|
|
5d8ec31bc4 | ||
|
|
82b18ebe62 | ||
|
|
66408782b0 | ||
|
|
846c8870ae | ||
|
|
8cfb07f07e | ||
|
|
d741425895 | ||
|
|
20add85ff6 | ||
|
|
a5c09c521e | ||
|
|
c94276b1ac | ||
|
|
272d575c78 | ||
|
|
693f8d501a | ||
|
|
bb1b6b9862 | ||
|
|
8ed82caaef | ||
|
|
de1dd70609 | ||
|
|
09429c241f | ||
|
|
9a332940d7 | ||
|
|
362974b1c2 | ||
|
|
435db30b7b | ||
|
|
e7de8289cd | ||
|
|
5065fea963 | ||
|
|
6989eb3d2b | ||
|
|
df24596092 | ||
|
|
43c23ffb97 | ||
|
|
e7658879bc | ||
|
|
df60eda679 | ||
|
|
b81e9dfeb8 | ||
|
|
b79af46054 | ||
|
|
90bf9e6de5 | ||
|
|
182163c3bd | ||
|
|
e4726972de | ||
|
|
985892cf7e | ||
|
|
746d51e621 | ||
|
|
cdd077b758 | ||
|
|
72ae7ba8d7 | ||
|
|
b82c8bca83 | ||
|
|
576406a4ce | ||
|
|
b5b08e92e3 | ||
|
|
9f6021cf36 | ||
|
|
0e3d719a39 | ||
|
|
afbf125231 | ||
|
|
b2b2a82554 | ||
|
|
30a1b067a8 | ||
|
|
beaca165d0 | ||
|
|
c21c18458b | ||
|
|
5e761e14fa | ||
|
|
18418ebd98 | ||
|
|
92f04f86cb | ||
|
|
0d5505aa2c | ||
|
|
bd56438eca | ||
|
|
b9a157657f | ||
|
|
523e8b85e6 | ||
|
|
ab74b2c82c | ||
|
|
265a8e8594 | ||
|
|
4fc7e5f7d9 | ||
|
|
9cc8caa834 | ||
|
|
a71375dad5 | ||
|
|
70d8a379f2 | ||
|
|
b6333f018f | ||
|
|
31acfc0101 | ||
|
|
6d9ea57186 | ||
|
|
c4c0255073 | ||
|
|
1fa9020599 | ||
|
|
dd7554374f | ||
|
|
65b5325ca6 | ||
|
|
b64a3e301a | ||
|
|
31562919bb | ||
|
|
9c75e25da7 | ||
|
|
7d91850be6 | ||
|
|
4af3b24b69 | ||
|
|
e4e66b143c | ||
|
|
dd93e2b0c4 | ||
|
|
d505b5ab9d | ||
|
|
0fb717a97f | ||
|
|
c634f98fca | ||
|
|
460479390a | ||
|
|
26013e239d | ||
|
|
46fe40ed47 | ||
|
|
0c27bcf73e | ||
|
|
d86b0e6e67 | ||
|
|
fff623acb1 | ||
|
|
f45c0206da | ||
|
|
61726c7e05 | ||
|
|
79552e0a37 | ||
|
|
5ec2087257 | ||
|
|
5214257cd4 | ||
|
|
365ca4d01f | ||
|
|
419a73b3de | ||
|
|
c093fbb8f6 | ||
|
|
31bb85bea3 | ||
|
|
803565456f | ||
|
|
520f5771d8 | ||
|
|
5fbd6f6606 | ||
|
|
bc9c7f0f1d | ||
|
|
08a2f0aa44 | ||
|
|
a0387faa67 | ||
|
|
1dd4674ccb | ||
|
|
60e26d1ddd | ||
|
|
ecdab76705 | ||
|
|
403fdf997c | ||
|
|
a9e83463e0 | ||
|
|
2bde4327c7 | ||
|
|
d53bfd7ee7 | ||
|
|
ccc45b672c | ||
|
|
0d42bdafe2 | ||
|
|
95050ff382 | ||
|
|
5addd246ac | ||
|
|
cfc95e2b7a | ||
|
|
44187d9e3a | ||
|
|
c76e8ba0e4 | ||
|
|
1bcae9dc49 | ||
|
|
3a388308d9 | ||
|
|
a60ec6fa42 | ||
|
|
456945fe3a | ||
|
|
ba24c15833 | ||
|
|
4e0b8a0278 | ||
|
|
274e91f8aa | ||
|
|
89055f7405 | ||
|
|
e8dba41216 | ||
|
|
f52b2cccd5 | ||
|
|
7386327af9 | ||
|
|
c48407b449 | ||
|
|
d92badeb45 | ||
|
|
7093179cc8 | ||
|
|
bf89974f7c | ||
|
|
2e1b7a5093 | ||
|
|
3d4dbc9a8c | ||
|
|
e376a129ed | ||
|
|
983ff5fc73 |
46
.github/workflows/maven.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
|
||||||
|
|
||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
|
||||||
|
name: Java CI with Maven
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ ubuntu-latest ]
|
||||||
|
java: [ 8, 11, 17, 21 ]
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 16
|
||||||
|
name: Test JDK ${{ matrix.java }}, ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: ${{ matrix.java }}
|
||||||
|
cache: 'maven'
|
||||||
|
- name: Build with Maven if test jdk8
|
||||||
|
if: ${{ matrix.java == '8' || matrix.java == '11'}}
|
||||||
|
run: ./mvnw -Pgen-javadoc clean package -B
|
||||||
|
- name: Build with Maven if test jdk17
|
||||||
|
if: ${{ matrix.java == '17' || matrix.java == '21' }}
|
||||||
|
run: ./mvnw -Penable-for-jdk17+,gen-code-cov clean package -B
|
||||||
12
.gitignore
vendored
@@ -7,6 +7,7 @@ test-output/
|
|||||||
.settings/
|
.settings/
|
||||||
tmp
|
tmp
|
||||||
temp
|
temp
|
||||||
|
logs/
|
||||||
*.log
|
*.log
|
||||||
antx.properties
|
antx.properties
|
||||||
otter.properties
|
otter.properties
|
||||||
@@ -18,7 +19,10 @@ jtester.properties
|
|||||||
*.rpm
|
*.rpm
|
||||||
client-adapter/example/
|
client-adapter/example/
|
||||||
*.dat
|
*.dat
|
||||||
|
.qwen/
|
||||||
canal-admin/canal-admin-ui/dist
|
.vscode/
|
||||||
canal-admin/canal-admin-ui/node
|
conf/
|
||||||
canal-admin/canal-admin-ui/node_modules
|
canal-admin/canal-admin-ui/dist/
|
||||||
|
canal-admin/canal-admin-ui/node/
|
||||||
|
canal-admin/canal-admin-ui/node_modules/
|
||||||
|
canal-admin/canal-admin-server/src/main/resources/conf/
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ cache:
|
|||||||
install: true
|
install: true
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- travis_wait 30 ./mvnw clean install -DskipTests=false -Dcheckstyle.skip=true -Dmaven.javadoc.skip=true
|
- travis_wait 30 ./mvnw -T 1C clean install -DskipTests=false -Dcheckstyle.skip=true -Dmaven.javadoc.skip=true
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|||||||
45
README.md
@@ -4,12 +4,16 @@
|
|||||||

|

|
||||||
[](http://isitmaintained.com/project/alibaba/canal "average time to resolve an issue")
|
[](http://isitmaintained.com/project/alibaba/canal "average time to resolve an issue")
|
||||||
[](http://isitmaintained.com/project/alibaba/canal "percentage of issues still open")
|
[](http://isitmaintained.com/project/alibaba/canal "percentage of issues still open")
|
||||||
|
[](https://opensource.alibaba.com/contribution_leaderboard/details?projectValue=canal)
|
||||||
|
|
||||||
## 背景
|
|
||||||
|
|
||||||
早期,阿里巴巴 B2B 公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求 ,主要是基于trigger的方式获取增量变更。从 2010 年开始,公司开始逐步尝试数据库日志解析,获取增量变更进行同步,由此衍生出了增量订阅和消费业务,从此开启一段新纪元。
|
## 简介
|
||||||
|
|
||||||
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x
|

|
||||||
|
|
||||||
|
**canal [kə'næl]**,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费
|
||||||
|
|
||||||
|
早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。
|
||||||
|
|
||||||
基于日志增量订阅和消费的业务包括
|
基于日志增量订阅和消费的业务包括
|
||||||
- 数据库镜像
|
- 数据库镜像
|
||||||
@@ -18,12 +22,7 @@
|
|||||||
- 业务 cache 刷新
|
- 业务 cache 刷新
|
||||||
- 带业务逻辑的增量数据处理
|
- 带业务逻辑的增量数据处理
|
||||||
|
|
||||||
## 项目介绍
|
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x
|
||||||
|
|
||||||
#### 名称:canal [kə'næl]
|
|
||||||
#### 译意: 水道/管道/沟渠
|
|
||||||
#### 产品定位: 基于数据库增量日志解析,提供增量数据订阅和消费
|
|
||||||
#### 关键词: MySQL binlog parser / real-time / queue&topic / index build
|
|
||||||
|
|
||||||
## 工作原理
|
## 工作原理
|
||||||
|
|
||||||
@@ -35,7 +34,6 @@
|
|||||||
- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
|
- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
|
||||||
|
|
||||||
#### canal 工作原理
|
#### canal 工作原理
|
||||||

|
|
||||||
|
|
||||||
- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
|
- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
|
||||||
- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
|
- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
|
||||||
@@ -43,7 +41,7 @@
|
|||||||
|
|
||||||
## 重要版本更新说明
|
## 重要版本更新说明
|
||||||
|
|
||||||
canal 1.1.x 版本([release_note](https://github.com/alibaba/canal/releases)),性能与功能层面有较大的突破,重要提升包括:
|
1. canal 1.1.x 版本([release_note](https://github.com/alibaba/canal/releases)),性能与功能层面有较大的突破,重要提升包括:
|
||||||
|
|
||||||
- 整体性能测试&优化,提升了150%. #726 参考: [Performance](https://github.com/alibaba/canal/wiki/Performance)
|
- 整体性能测试&优化,提升了150%. #726 参考: [Performance](https://github.com/alibaba/canal/wiki/Performance)
|
||||||
- 原生支持prometheus监控 #765 [Prometheus QuickStart](https://github.com/alibaba/canal/wiki/Prometheus-QuickStart)
|
- 原生支持prometheus监控 #765 [Prometheus QuickStart](https://github.com/alibaba/canal/wiki/Prometheus-QuickStart)
|
||||||
@@ -51,15 +49,22 @@ canal 1.1.x 版本([release_note](https://github.com/alibaba/canal/releases)
|
|||||||
- 原生支持aliyun rds的binlog订阅 (解决自动主备切换/oss binlog离线解析) 参考: [Aliyun RDS QuickStart](https://github.com/alibaba/canal/wiki/aliyun-RDS-QuickStart)
|
- 原生支持aliyun rds的binlog订阅 (解决自动主备切换/oss binlog离线解析) 参考: [Aliyun RDS QuickStart](https://github.com/alibaba/canal/wiki/aliyun-RDS-QuickStart)
|
||||||
- 原生支持docker镜像 #801 参考: [Docker QuickStart](https://github.com/alibaba/canal/wiki/Docker-QuickStart)
|
- 原生支持docker镜像 #801 参考: [Docker QuickStart](https://github.com/alibaba/canal/wiki/Docker-QuickStart)
|
||||||
|
|
||||||
|
2. canal 1.1.4版本,迎来最重要的WebUI能力,引入canal-admin工程,支持面向WebUI的canal动态管理能力,支持配置、任务、日志等在线白屏运维能力,具体文档:[Canal Admin Guide](https://github.com/alibaba/canal/wiki/Canal-Admin-Guide)
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
- [Home](https://github.com/alibaba/canal/wiki/Home)
|
- [Home](https://github.com/alibaba/canal/wiki/Home)
|
||||||
- [Introduction](https://github.com/alibaba/canal/wiki/Introduction)
|
- [Introduction](https://github.com/alibaba/canal/wiki/Introduction)
|
||||||
- [QuickStart](https://github.com/alibaba/canal/wiki/QuickStart)
|
- [QuickStart](https://github.com/alibaba/canal/wiki/QuickStart)
|
||||||
- [Docker QuickStart](https://github.com/alibaba/canal/wiki/Docker-QuickStart)
|
- [Docker QuickStart](https://github.com/alibaba/canal/wiki/Docker-QuickStart)
|
||||||
- [Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart")
|
- [Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart)
|
||||||
- [Aliyun RDS for MySQL QuickStart](https://github.com/alibaba/canal/wiki/aliyun-RDS-QuickStart)
|
- [Aliyun RDS for MySQL QuickStart](https://github.com/alibaba/canal/wiki/aliyun-RDS-QuickStart)
|
||||||
- [Prometheus QuickStart](https://github.com/alibaba/canal/wiki/Prometheus-QuickStart)
|
- [Prometheus QuickStart](https://github.com/alibaba/canal/wiki/Prometheus-QuickStart)
|
||||||
|
- Canal Admin
|
||||||
|
- [Canal Admin QuickStart](https://github.com/alibaba/canal/wiki/Canal-Admin-QuickStart)
|
||||||
|
- [Canal Admin Guide](https://github.com/alibaba/canal/wiki/Canal-Admin-Guide)
|
||||||
|
- [Canal Admin ServerGuide](https://github.com/alibaba/canal/wiki/Canal-Admin-ServerGuide)
|
||||||
|
- [Canal Admin Docker](https://github.com/alibaba/canal/wiki/Canal-Admin-Docker)
|
||||||
- [AdminGuide](https://github.com/alibaba/canal/wiki/AdminGuide)
|
- [AdminGuide](https://github.com/alibaba/canal/wiki/AdminGuide)
|
||||||
- [ClientExample](https://github.com/alibaba/canal/wiki/ClientExample)
|
- [ClientExample](https://github.com/alibaba/canal/wiki/ClientExample)
|
||||||
- [ClientAPI](https://github.com/alibaba/canal/wiki/ClientAPI)
|
- [ClientAPI](https://github.com/alibaba/canal/wiki/ClientAPI)
|
||||||
@@ -81,20 +86,26 @@ canal 特别设计了 client-server 模式,交互协议使用 protobuf 3.0 , c
|
|||||||
- canal go客户端: [https://github.com/CanalClient/canal-go](https://github.com/CanalClient/canal-go)
|
- canal go客户端: [https://github.com/CanalClient/canal-go](https://github.com/CanalClient/canal-go)
|
||||||
- canal php客户端: [https://github.com/xingwenge/canal-php](https://github.com/xingwenge/canal-php)
|
- canal php客户端: [https://github.com/xingwenge/canal-php](https://github.com/xingwenge/canal-php)
|
||||||
- canal Python客户端:[https://github.com/haozi3156666/canal-python](https://github.com/haozi3156666/canal-python)
|
- canal Python客户端:[https://github.com/haozi3156666/canal-python](https://github.com/haozi3156666/canal-python)
|
||||||
|
- canal Rust客户端:[https://github.com/laohanlinux/canal-rs](https://github.com/laohanlinux/canal-rs)
|
||||||
|
- canal Nodejs客户端:[https://github.com/marmot-z/canal-nodejs](https://github.com/marmot-z/canal-nodejs)
|
||||||
|
|
||||||
canal 作为 MySQL binlog 增量获取和解析工具,可将变更记录投递到 MQ 系统中,比如 Kafka/RocketMQ,可以借助于 MQ 的多语言能力
|
canal 作为 MySQL binlog 增量获取和解析工具,可将变更记录投递到 MQ 系统中,比如 Kafka/RocketMQ,可以借助于 MQ 的多语言能力
|
||||||
|
|
||||||
- 参考文档: [Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart)
|
- 参考文档: [Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart)
|
||||||
|
|
||||||
## 相关开源
|
## 基于canal开发的工具
|
||||||
|
|
||||||
|
- canal2sql(基于binlog生成SQL) : [https://github.com/zhuchao941/canal2sql]
|
||||||
|
|
||||||
|
## 相关开源&产品
|
||||||
|
|
||||||
- [canal 消费端开源项目: Otter](http://github.com/alibaba/otter)
|
- [canal 消费端开源项目: Otter](http://github.com/alibaba/otter)
|
||||||
- [阿里巴巴去 Oracle 数据迁移同步工具: yugong](http://github.com/alibaba/yugong)
|
- [阿里巴巴去 Oracle 数据迁移同步工具: yugong](http://github.com/alibaba/yugong)
|
||||||
|
- [阿里巴巴离线同步开源项目 DataX](https://github.com/alibaba/datax)
|
||||||
|
- [阿里巴巴数据库连接池开源项目 Druid](https://github.com/alibaba/druid)
|
||||||
|
- [阿里巴巴实时数据同步工具 DTS](https://www.aliyun.com/product/dts)
|
||||||
|
|
||||||
## 问题反馈
|
## 问题反馈
|
||||||
- 邮件: jianghang115@gmail.com
|
|
||||||
- 报告 issue: [github issues](https://github.com/alibaba/canal/issues)
|
- 报告 issue: [github issues](https://github.com/alibaba/canal/issues)
|
||||||
|
|
||||||
## 其他资料
|
本项目的Issues会被同步沉淀至[阿里云开发者社区](https://developer.aliyun.com/ask)
|
||||||
|
|
||||||
- [ ADC 阿里技术嘉年华分享 ppt ( google docs)](https://docs.google.com/presentation/d/1MkszUPYRDkfVPz9IqOT1LLT5d9tuwde_WC8GZvjaDRg/edit?usp=sharing)
|
|
||||||
|
|||||||
118
SECURITY.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# 漏洞奖励计划
|
||||||
|
## 报告
|
||||||
|
如果您认为自己在本程序中发现了任何安全(技术)漏洞,欢迎您通过 https://security.alibaba.com 向我们提交漏洞报告。
|
||||||
|
如果您报告任何安全漏洞,请注意您可能包含以下信息(合格报告):
|
||||||
|
* git程序URL地址,运行的环境
|
||||||
|
* 包含必要屏幕截图的详细说明
|
||||||
|
* 重现漏洞的步骤以及修复漏洞的建议。
|
||||||
|
* 其他有用信息
|
||||||
|
|
||||||
|
## 处理
|
||||||
|
ASRC(Alibaba Security Response Center阿里安全响应中心)将尽快审核并回复您的提交内容,并在我们努力修复您提交的漏洞时随时通知您。如有必要,我们可能会与您联系以获取更多信息。
|
||||||
|
|
||||||
|
|
||||||
|
## 条款和条件
|
||||||
|
1. 仅接受技术漏洞并对其进行评级
|
||||||
|
2. 出于安全原因,上报者同意与ASRC合作完成他/她提交的漏洞,不向任何第三方透露任何漏洞信息
|
||||||
|
3. 如果不止一个人报告相同的安全漏洞,奖励将给予完成合格报告的第一个人
|
||||||
|
4. 为了保护程序的用户,请在修复之前不要直接提交git的issue,也不要在社区讨论任何漏洞信息
|
||||||
|
5. 所有奖励和声誉积分将提供给仅向ASRC提交其安全漏洞的上报者
|
||||||
|
6. 安全漏洞奖励的解释权利归ASRC所有
|
||||||
|
|
||||||
|
## 收集范围
|
||||||
|
我们的主要收集漏洞类别是:
|
||||||
|
* 服务器端请求伪造(SSRF)
|
||||||
|
* SQL注入
|
||||||
|
* 拒绝服务攻击
|
||||||
|
* 远程执行代码(RCE)
|
||||||
|
* XML外部实体攻击(XXE)
|
||||||
|
* 访问控制问题(不安全的直接对象参考问题等)
|
||||||
|
* 敏感目录遍历问题
|
||||||
|
* 本地文件读取(LFD)
|
||||||
|
* 敏感信息泄露(密钥,Cookie,Session等)
|
||||||
|
|
||||||
|
## 奖励
|
||||||
|
* 可直接导致严重问题的每个漏洞奖励7000元人民币
|
||||||
|
* 存在限制及需要一定特殊环境下才能利用的问题将给予700-5600元人民币不等的奖励,比如需要用户主动点击才会触发的问题或需要admin权限
|
||||||
|
* 只有在指定环境下才可以运行的利用将有可能被收纳但不给予奖励,或直接被忽略,比如只在fastjson+linux特定版本才会出现的问题
|
||||||
|
|
||||||
|
## 不在收集范围的报告
|
||||||
|
* 影响过时浏览器或平台用户的漏洞
|
||||||
|
* Self-XSS
|
||||||
|
* 会话固定
|
||||||
|
* 内容欺骗
|
||||||
|
* 缺少cookie标记
|
||||||
|
* 混合内容警告
|
||||||
|
* SSL / TLS问题
|
||||||
|
* Clickjacking
|
||||||
|
* 基于Flash的漏洞
|
||||||
|
* 反射文件下载攻击(RFD)
|
||||||
|
* 物理或社会工程攻击
|
||||||
|
* 未验证自动化工具或扫描仪的结果
|
||||||
|
* 登录/注销/未认证/低影响CSRF
|
||||||
|
* 需要MITM或物理访问用户设备的攻击
|
||||||
|
* 与网络协议或行业标准相关的问题
|
||||||
|
* 不能用于直接攻击的错误信息泄露
|
||||||
|
* 缺少与安全相关的HTTP标头等
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Vulnerability Reward Program
|
||||||
|
## Reporting
|
||||||
|
If you believe you have found any security (technical) vulnerability in the Program, you are welcomed to submit a vulnerability report to us at https://security.alibaba.com
|
||||||
|
In case of reporting any security vulnerability, please be noted that you may including following information (Qualified Reporting):
|
||||||
|
* The git program URL and running version
|
||||||
|
* A detailed description with necessary screenshots
|
||||||
|
* Steps to reappearance the vulnerability and your advice to fix it
|
||||||
|
* Other useful information
|
||||||
|
|
||||||
|
|
||||||
|
## Processing
|
||||||
|
ASRC (Alibaba Security Response Center) will review and respond as quickly as possible to your submission, and keep you informed as we work to fix the vulnerability you submitted. We may contact you for further information if necessary.
|
||||||
|
|
||||||
|
|
||||||
|
## Terms and Conditions
|
||||||
|
1. ONLY technical vulnerabilities will be accepted and rated.
|
||||||
|
2. With regarding to security reasons, reporters agree to cooperate with ASRC exclusively on the vulnerability he/she submitted and not disclose any information of vulnerability to any third-parties.
|
||||||
|
3. In the case that more than one person report the same security vulnerability, the reward will be given to the first person who accomplish a Qualified Reporting.
|
||||||
|
4. To protect users of the program, please do not directly submit issue on github or discuss anything with the community
|
||||||
|
5. All Rewards and Reputation Credits are given to the reporters who submit his/her security vulnerabilities ONLY to ASRC.
|
||||||
|
6. All rights for the security vulnerability rewards are reserved by ASRC.
|
||||||
|
|
||||||
|
## Scope of Collecting
|
||||||
|
The main categories of vulnerabilities that we are sincerely looking for are:
|
||||||
|
* Server-Side Request Forgery (SSRF)
|
||||||
|
* SQL Injection
|
||||||
|
* Denial of Service Attack
|
||||||
|
* Remote Code Execution (RCE)
|
||||||
|
* XML External Entity Attacks (XXE)
|
||||||
|
* Access Control Issues (Insecure Direct Object Reference issues, etc.)
|
||||||
|
* Directory Traversal Issues
|
||||||
|
* Local File Disclosure (LFD)
|
||||||
|
* Sensitive Information Leakage (Key, Cookie, Session etc.)
|
||||||
|
|
||||||
|
## Reward
|
||||||
|
* $1,000 for one valid report
|
||||||
|
* $100-$800 for Vuls which is limited. For example, Vuls that need user interactions or administrator authority
|
||||||
|
* Vuls which only work on the special version will be accepted but no reward, or directly rejected. For example, Vul runs only on a special linux version
|
||||||
|
|
||||||
|
## Ineligible Reports
|
||||||
|
* Vulnerabilities affecting users of outdated browsers or platforms
|
||||||
|
* "Self" XSS
|
||||||
|
* Session fixation
|
||||||
|
* Content Spoofing
|
||||||
|
* Missing cookie flags
|
||||||
|
* Mixed content warnings
|
||||||
|
* SSL/TLS best practices
|
||||||
|
* Clickjacking/UI redressing
|
||||||
|
* Flash-based vulnerabilities
|
||||||
|
* Reflected file download attacks (RFD)
|
||||||
|
* Physical or social engineering attacks
|
||||||
|
* Unverified Results of automated tools or scanners
|
||||||
|
* Login/logout/unauthenticated/low-impact CSRF
|
||||||
|
* Attacks requiring MITM or physical access to a user's device
|
||||||
|
* Issues related to networking protocols or industry standards
|
||||||
|
* Error information disclosure that cannot be used to make a direct attack
|
||||||
|
* Missing security-related HTTP headers which do not lead directly to a vulnerability
|
||||||
BIN
admin/admin-ui/node/node
Executable file
@@ -1,16 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<parent>
|
<parent>
|
||||||
<artifactId>canal-admin</artifactId>
|
<artifactId>canal.admin</artifactId>
|
||||||
<groupId>com.alibaba.otter</groupId>
|
<groupId>com.alibaba.otter</groupId>
|
||||||
<version>1.1.4-SNAPSHOT</version>
|
<version>1.1.9-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<artifactId>canal-admin-ui</artifactId>
|
<artifactId>canal.admin-ui</artifactId>
|
||||||
|
<name>canal admin-ui module for otter ${project.version}</name>
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
@@ -20,11 +18,11 @@
|
|||||||
|
|
||||||
<profiles>
|
<profiles>
|
||||||
<profile>
|
<profile>
|
||||||
<id>release</id>
|
<id>npm</id>
|
||||||
<activation>
|
<activation>
|
||||||
<property>
|
<property>
|
||||||
<name>env</name>
|
<name>env</name>
|
||||||
<value>release</value>
|
<value>npm</value>
|
||||||
</property>
|
</property>
|
||||||
</activation>
|
</activation>
|
||||||
<build>
|
<build>
|
||||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
46
admin/admin-ui/src/api/canalCluster.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getCanalClusters(params) {
|
||||||
|
return request({
|
||||||
|
url: '/canal/clusters',
|
||||||
|
method: 'get',
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addCanalCluster(data) {
|
||||||
|
return request({
|
||||||
|
url: '/canal/cluster',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canalClusterDetail(id) {
|
||||||
|
return request({
|
||||||
|
url: '/canal/cluster/' + id,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCanalCluster(data) {
|
||||||
|
return request({
|
||||||
|
url: '/canal/cluster',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCanalCluster(id) {
|
||||||
|
return request({
|
||||||
|
url: '/canal/cluster/' + id,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClustersAndServers() {
|
||||||
|
return request({
|
||||||
|
url: '/canal/clustersAndServers',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
23
admin/admin-ui/src/api/canalConfig.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getCanalConfig(clusterId, serverId) {
|
||||||
|
return request({
|
||||||
|
url: '/canal/config/' + clusterId + '/' + serverId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCanalConfig(data) {
|
||||||
|
return request({
|
||||||
|
url: '/canal/config',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTemplateConfig() {
|
||||||
|
return request({
|
||||||
|
url: '/canal/config/template',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -58,3 +58,24 @@ export function instanceLog(id, nodeId) {
|
|||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function instanceStatus(id, option) {
|
||||||
|
return request({
|
||||||
|
url: '/canal/instance/status/' + id + '?option=' + option,
|
||||||
|
method: 'put'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveInstances(serverId) {
|
||||||
|
return request({
|
||||||
|
url: '/canal/active/instances/' + serverId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTemplateInstance() {
|
||||||
|
return request({
|
||||||
|
url: '/canal/instance/template',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
101
admin/admin-ui/src/components/Pagination/index.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="{'hidden':hidden}" class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
:background="background"
|
||||||
|
:current-page.sync="currentPage"
|
||||||
|
:page-size.sync="pageSize"
|
||||||
|
:layout="layout"
|
||||||
|
:page-sizes="pageSizes"
|
||||||
|
:total="total"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { scrollTo } from '@/utils/scrollTo'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Pagination',
|
||||||
|
props: {
|
||||||
|
total: {
|
||||||
|
required: true,
|
||||||
|
type: Number
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 20
|
||||||
|
},
|
||||||
|
pageSizes: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [10, 20, 30, 50]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: String,
|
||||||
|
default: 'total, sizes, prev, pager, next, jumper'
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
autoScroll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
hidden: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentPage: {
|
||||||
|
get() {
|
||||||
|
return this.page
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:page', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageSize: {
|
||||||
|
get() {
|
||||||
|
return this.limit
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:limit', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSizeChange(val) {
|
||||||
|
this.$emit('pagination', { page: this.currentPage, limit: val })
|
||||||
|
if (this.autoScroll) {
|
||||||
|
scrollTo(0, 800)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.$emit('pagination', { page: val, limit: this.pageSize })
|
||||||
|
if (this.autoScroll) {
|
||||||
|
scrollTo(0, 800)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pagination-container {
|
||||||
|
background: #fff;
|
||||||
|
padding: 32px 16px;
|
||||||
|
}
|
||||||
|
.pagination-container.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 497 B After Width: | Height: | Size: 497 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 944 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 285 B After Width: | Height: | Size: 285 B |
|
Before Width: | Height: | Size: 821 B After Width: | Height: | Size: 821 B |
|
Before Width: | Height: | Size: 623 B After Width: | Height: | Size: 623 B |
|
Before Width: | Height: | Size: 597 B After Width: | Height: | Size: 597 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 440 B After Width: | Height: | Size: 440 B |
@@ -77,49 +77,56 @@ export const constantRoutes = [
|
|||||||
meta: { title: 'Canal Server', icon: 'example' },
|
meta: { title: 'Canal Server', icon: 'example' },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'nodeServers',
|
path: 'canalClusters',
|
||||||
name: '节点状态',
|
name: 'Canal 集群管理',
|
||||||
component: () => import('@/views/canalServer/NodeServer'),
|
component: () => import('@/views/canalServer/CanalCluster'),
|
||||||
meta: { title: '节点管理', icon: 'tree' }
|
meta: { title: '集群管理', icon: 'tree' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'config',
|
path: 'nodeServers',
|
||||||
name: 'Canal主配置',
|
name: 'Server 状态',
|
||||||
component: () => import('@/views/canalServer/Config'),
|
component: () => import('@/views/canalServer/NodeServer'),
|
||||||
meta: { title: 'Canal主配置', icon: 'form' }
|
meta: { title: 'Server 管理', icon: 'form' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'nodeServer/config',
|
||||||
|
name: 'Server 配置',
|
||||||
|
component: () => import('@/views/canalServer/CanalConfig'),
|
||||||
|
meta: { title: 'Server 配置' },
|
||||||
|
hidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'canalInstances',
|
path: 'canalInstances',
|
||||||
name: '实例管理',
|
name: 'Instance 管理',
|
||||||
component: () => import('@/views/canalServer/CanalInstance'),
|
component: () => import('@/views/canalServer/CanalInstance'),
|
||||||
meta: { title: '实例管理', icon: 'nested' }
|
meta: { title: 'Instance 管理', icon: 'nested' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'canalInstance/add',
|
path: 'canalInstance/add',
|
||||||
name: '新建实例配置',
|
name: '新建Instance配置',
|
||||||
component: () => import('@/views/canalServer/CanalInstanceAdd'),
|
component: () => import('@/views/canalServer/CanalInstanceAdd'),
|
||||||
meta: { title: '新建实例配置' },
|
meta: { title: '新建Instance配置' },
|
||||||
hidden: true
|
hidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'canalInstance/modify',
|
path: 'canalInstance/modify',
|
||||||
name: '修改实例配置',
|
name: '修改Instance配置',
|
||||||
component: () => import('@/views/canalServer/CanalInstanceUpdate'),
|
component: () => import('@/views/canalServer/CanalInstanceUpdate'),
|
||||||
meta: { title: '修改实例配置' },
|
meta: { title: '修改Instance配置' },
|
||||||
hidden: true
|
hidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'nodeServer/log',
|
path: 'nodeServer/log',
|
||||||
name: 'Canal日志',
|
name: 'Server 日志',
|
||||||
component: () => import('@/views/canalServer/CanalLogDetail'),
|
component: () => import('@/views/canalServer/CanalLogDetail'),
|
||||||
meta: { title: 'Canal日志' },
|
meta: { title: 'Server 日志' },
|
||||||
hidden: true
|
hidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'canalInstance/log',
|
path: 'canalInstance/log',
|
||||||
name: 'Canal Instance日志',
|
name: 'Instance 日志',
|
||||||
component: () => import('@/views/canalServer/CanalInstanceLogDetail'),
|
component: () => import('@/views/canalServer/CanalInstanceLogDetail'),
|
||||||
meta: { title: 'Canal Instance日志' },
|
meta: { title: 'Instance 日志' },
|
||||||
hidden: true
|
hidden: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
50
admin/admin-ui/src/utils/scrollTo.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
Math.easeInOutQuad = function(t, b, c, d) {
|
||||||
|
t /= d / 2
|
||||||
|
if (t < 1) {
|
||||||
|
return c / 2 * t * t + b
|
||||||
|
}
|
||||||
|
t--
|
||||||
|
return -c / 2 * (t * (t - 2) - 1) + b
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
|
||||||
|
var requestAnimFrame = (function() {
|
||||||
|
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
|
||||||
|
})()
|
||||||
|
|
||||||
|
// because it's so fucking difficult to detect the scrolling element, just move them all
|
||||||
|
function move(amount) {
|
||||||
|
document.documentElement.scrollTop = amount
|
||||||
|
document.body.parentNode.scrollTop = amount
|
||||||
|
document.body.scrollTop = amount
|
||||||
|
}
|
||||||
|
|
||||||
|
function position() {
|
||||||
|
return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scrollTo(to, duration, callback) {
|
||||||
|
const start = position()
|
||||||
|
const change = to - start
|
||||||
|
const increment = 20
|
||||||
|
let currentTime = 0
|
||||||
|
duration = (typeof (duration) === 'undefined') ? 500 : duration
|
||||||
|
var animateScroll = function() {
|
||||||
|
// increment the time
|
||||||
|
currentTime += increment
|
||||||
|
// find the value with the quadratic in-out easing function
|
||||||
|
var val = Math.easeInOutQuad(currentTime, start, change, duration)
|
||||||
|
// move the document.body
|
||||||
|
move(val)
|
||||||
|
// do the animation unless its over
|
||||||
|
if (currentTime < duration) {
|
||||||
|
requestAnimFrame(animateScroll)
|
||||||
|
} else {
|
||||||
|
if (callback && typeof (callback) === 'function') {
|
||||||
|
// the animation is done so lets callback
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animateScroll()
|
||||||
|
}
|
||||||
204
admin/admin-ui/src/views/canalServer/CanalCluster.vue
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<div class="filter-container">
|
||||||
|
<!-- <el-input v-model="listQuery.name" placeholder="Server 名称" style="width: 200px;" class="filter-item" />
|
||||||
|
<el-input v-model="listQuery.ip" placeholder="Server IP" style="width: 200px;" class="filter-item" />
|
||||||
|
<el-button class="filter-item" type="primary" icon="el-icon-search" plain @click="fetchData()">查询</el-button> -->
|
||||||
|
<el-button class="filter-item" type="primary" @click="handleCreate()">新建集群</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
v-loading="listLoading"
|
||||||
|
:data="list"
|
||||||
|
element-loading-text="Loading"
|
||||||
|
border
|
||||||
|
fit
|
||||||
|
highlight-current-row
|
||||||
|
>
|
||||||
|
<el-table-column label="集群名称" min-width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.name }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="ZK地址" min-width="300" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.zkHosts }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" prop="created_at" label="操作" min-width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-dropdown trigger="click">
|
||||||
|
<el-button type="primary" size="mini">
|
||||||
|
操作<i class="el-icon-arrow-down el-icon--right" />
|
||||||
|
</el-button>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item @click.native="handleConfig(scope.row)">主配置</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleUpdate(scope.row)">修改集群</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleDelete(scope.row)">删除集群</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleView(scope.row)">查看Server</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-dialog :visible.sync="dialogFormVisible" :title="textMap[dialogStatus]" width="600px">
|
||||||
|
<el-form ref="dataForm" :rules="rules" :model="canalCluster" label-position="left" label-width="120px" style="width: 400px; margin-left:30px;">
|
||||||
|
<el-form-item label="集群名称" prop="name">
|
||||||
|
<el-input v-model="canalCluster.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="ZK地址" prop="zkHosts">
|
||||||
|
<el-input v-model="canalCluster.zkHosts" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="dialogFormVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="dataOperation()">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { addCanalCluster, getCanalClusters, updateCanalCluster, deleteCanalCluster } from '@/api/canalCluster'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
filters: {
|
||||||
|
statusFilter(status) {
|
||||||
|
const statusMap = {
|
||||||
|
'1': 'success',
|
||||||
|
'0': 'gray',
|
||||||
|
'-1': 'danger'
|
||||||
|
}
|
||||||
|
return statusMap[status]
|
||||||
|
},
|
||||||
|
statusLabel(status) {
|
||||||
|
const statusMap = {
|
||||||
|
'1': '启动',
|
||||||
|
'0': '停止',
|
||||||
|
'-1': '断开'
|
||||||
|
}
|
||||||
|
return statusMap[status]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: null,
|
||||||
|
listLoading: true,
|
||||||
|
listQuery: {
|
||||||
|
name: '',
|
||||||
|
ip: ''
|
||||||
|
},
|
||||||
|
dialogFormVisible: false,
|
||||||
|
textMap: {
|
||||||
|
create: '新建集群信息',
|
||||||
|
update: '修改集群信息'
|
||||||
|
},
|
||||||
|
canalCluster: {
|
||||||
|
id: null,
|
||||||
|
name: null,
|
||||||
|
zkHosts: null
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
name: [{ required: true, message: '集群名称不能为空', trigger: 'change' }],
|
||||||
|
zkHosts: [{ required: true, message: 'zk地址不能为空', trigger: 'change' }]
|
||||||
|
},
|
||||||
|
dialogStatus: 'create'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData() {
|
||||||
|
this.listLoading = true
|
||||||
|
getCanalClusters(this.listQuery).then(res => {
|
||||||
|
this.list = res.data
|
||||||
|
}).finally(() => {
|
||||||
|
this.listLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resetModel() {
|
||||||
|
this.canalCluster = {
|
||||||
|
id: null,
|
||||||
|
name: null,
|
||||||
|
zkHosts: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleCreate() {
|
||||||
|
this.resetModel()
|
||||||
|
this.dialogStatus = 'create'
|
||||||
|
this.dialogFormVisible = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs['dataForm'].clearValidate()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
dataOperation() {
|
||||||
|
this.$refs['dataForm'].validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
if (this.dialogStatus === 'create') {
|
||||||
|
addCanalCluster(this.canalCluster).then(res => {
|
||||||
|
this.operationRes(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.dialogStatus === 'update') {
|
||||||
|
updateCanalCluster(this.canalCluster).then(res => {
|
||||||
|
this.operationRes(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
operationRes(res) {
|
||||||
|
if (res.data === 'success') {
|
||||||
|
this.fetchData()
|
||||||
|
this.dialogFormVisible = false
|
||||||
|
this.$message({
|
||||||
|
message: this.textMap[this.dialogStatus] + '成功',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message: this.textMap[this.dialogStatus] + '失败',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleView(row) {
|
||||||
|
this.$router.push('/canalServer/nodeServers?clusterId=' + row.id)
|
||||||
|
},
|
||||||
|
handleConfig(row) {
|
||||||
|
this.$router.push('/canalServer/nodeServer/config?clusterId=' + row.id)
|
||||||
|
},
|
||||||
|
handleUpdate(row) {
|
||||||
|
this.resetModel()
|
||||||
|
this.canalCluster = Object.assign({}, row)
|
||||||
|
this.dialogStatus = 'update'
|
||||||
|
this.dialogFormVisible = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs['dataForm'].clearValidate()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleDelete(row) {
|
||||||
|
this.$confirm('删除集群信息会导致服务停止', '确定删除集群信息', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
deleteCanalCluster(row.id).then((res) => {
|
||||||
|
if (res.data === 'success') {
|
||||||
|
this.fetchData()
|
||||||
|
this.$message({
|
||||||
|
message: '删除集群信息成功',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message: '删除集群信息失败',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -4,8 +4,10 @@
|
|||||||
<div style="padding-left: 10px;padding-top: 20px;">
|
<div style="padding-left: 10px;padding-top: 20px;">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
{{ form.name }}
|
{{ form.name }}
|
||||||
<el-button type="primary" @click="onSubmit">修改</el-button>
|
<el-button type="primary" @click="onSubmit">保存</el-button>
|
||||||
<el-button type="warning" @click="onCancel">重置</el-button>
|
<el-button type="warning" @click="onCancel">重置</el-button>
|
||||||
|
<el-button type="success" @click="onLoadTemplate">载入模板</el-button>
|
||||||
|
<el-button type="info" @click="onBack">返回</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<editor v-model="form.content" lang="properties" theme="chrome" width="100%" :height="800" @init="editorInit" />
|
<editor v-model="form.content" lang="properties" theme="chrome" width="100%" :height="800" @init="editorInit" />
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getCanalConfig, updateCanalConfig } from '@/api/canalConfig'
|
import { getCanalConfig, updateCanalConfig, getTemplateConfig } from '@/api/canalConfig'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -25,7 +27,9 @@ export default {
|
|||||||
form: {
|
form: {
|
||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
content: ''
|
content: '',
|
||||||
|
serverId: null,
|
||||||
|
clusterId: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -44,16 +48,32 @@ export default {
|
|||||||
require('brace/snippets/javascript')
|
require('brace/snippets/javascript')
|
||||||
},
|
},
|
||||||
loadCanalConfig() {
|
loadCanalConfig() {
|
||||||
getCanalConfig().then(response => {
|
let clusterId = 0
|
||||||
|
let serverId = 0
|
||||||
|
if (this.$route.query.clusterId) {
|
||||||
|
clusterId = this.$route.query.clusterId
|
||||||
|
} else if (this.$route.query.serverId) {
|
||||||
|
serverId = this.$route.query.serverId
|
||||||
|
}
|
||||||
|
getCanalConfig(clusterId, serverId).then(response => {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
this.form.id = data.id
|
this.form.id = data.id
|
||||||
this.form.name = data.name
|
this.form.name = data.name
|
||||||
this.form.content = data.content
|
this.form.content = data.content
|
||||||
|
this.form.serverId = this.$route.query.serverId
|
||||||
|
this.form.clusterId = this.$route.query.clusterId
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
|
if (this.form.content === null || this.form.content === '') {
|
||||||
|
this.$message({
|
||||||
|
message: '配置内容不能为空',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$confirm(
|
this.$confirm(
|
||||||
'修改Canal主配置可能会导致Server重启,是否继续?',
|
'修改主配置可能会导致Server重启,是否继续?',
|
||||||
'确定修改',
|
'确定修改',
|
||||||
{
|
{
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
@@ -64,13 +84,13 @@ export default {
|
|||||||
updateCanalConfig(this.form).then(response => {
|
updateCanalConfig(this.form).then(response => {
|
||||||
if (response.data === 'success') {
|
if (response.data === 'success') {
|
||||||
this.$message({
|
this.$message({
|
||||||
message: '修改成功',
|
message: '保存成功',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
})
|
})
|
||||||
this.loadCanalConfig()
|
this.loadCanalConfig()
|
||||||
} else {
|
} else {
|
||||||
this.$message({
|
this.$message({
|
||||||
message: '修改失败',
|
message: '保存失败',
|
||||||
type: 'error'
|
type: 'error'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -79,6 +99,14 @@ export default {
|
|||||||
},
|
},
|
||||||
onCancel() {
|
onCancel() {
|
||||||
this.loadCanalConfig()
|
this.loadCanalConfig()
|
||||||
|
},
|
||||||
|
onBack() {
|
||||||
|
history.go(-1)
|
||||||
|
},
|
||||||
|
onLoadTemplate() {
|
||||||
|
getTemplateConfig().then(res => {
|
||||||
|
this.form.content = res.data
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
228
admin/admin-ui/src/views/canalServer/CanalInstance.vue
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<div class="filter-container">
|
||||||
|
<el-input v-model="listQuery.name" placeholder="Instance 名称" style="width: 200px;" class="filter-item" />
|
||||||
|
<el-select v-model="listQuery.clusterServerId" placeholder="所属集群/主机" class="filter-item">
|
||||||
|
<el-option key="" label="所属集群/主机" value="" />
|
||||||
|
<el-option-group v-for="group in options" :key="group.label" :label="group.label">
|
||||||
|
<el-option v-for="item in group.options" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-option-group>
|
||||||
|
</el-select>
|
||||||
|
<el-button class="filter-item" type="primary" icon="el-icon-search" plain @click="queryData()">查询</el-button>
|
||||||
|
|
||||||
|
<el-button class="filter-item" type="primary" @click="handleCreate()">新建 Instance</el-button>
|
||||||
|
<el-button class="filter-item" type="info" @click="fetchData()">刷新列表</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
v-loading="listLoading"
|
||||||
|
:data="list"
|
||||||
|
element-loading-text="Loading"
|
||||||
|
border
|
||||||
|
fit
|
||||||
|
highlight-current-row
|
||||||
|
>
|
||||||
|
<el-table-column label="Instance 名称" min-width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.name }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="所属集群" min-width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.canalCluster !== null">
|
||||||
|
{{ scope.row.canalCluster.name }}
|
||||||
|
</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="所属主机" min-width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.nodeServer !== null">
|
||||||
|
{{ scope.row.nodeServer.name }}
|
||||||
|
</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column class-name="status-col" label="状态" min-width="150" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.runningStatus | statusFilter">{{ scope.row.runningStatus | statusLabel }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="修改时间" min-width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.modifiedTime }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" prop="created_at" label="操作" min-width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-dropdown trigger="click">
|
||||||
|
<el-button type="primary" size="mini">
|
||||||
|
操作<i class="el-icon-arrow-down el-icon--right" />
|
||||||
|
</el-button>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item @click.native="handleUpdate(scope.row)">修改</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleDelete(scope.row)">删除</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleStart(scope.row)">启动</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleStop(scope.row)">停止</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleLog(scope.row)">日志</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination v-show="count>0" :total="count" :page.sync="listQuery.page" :limit.sync="listQuery.size" @pagination="fetchData()" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getCanalInstances, deleteCanalInstance, instanceStatus } from '@/api/canalInstance'
|
||||||
|
import Pagination from '@/components/Pagination'
|
||||||
|
import { getClustersAndServers } from '@/api/canalCluster'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Pagination },
|
||||||
|
filters: {
|
||||||
|
statusFilter(status) {
|
||||||
|
const statusMap = {
|
||||||
|
'1': 'success',
|
||||||
|
'0': 'gray'
|
||||||
|
}
|
||||||
|
return statusMap[status]
|
||||||
|
},
|
||||||
|
statusLabel(status) {
|
||||||
|
const statusMap = {
|
||||||
|
'1': '启动',
|
||||||
|
'0': '停止'
|
||||||
|
}
|
||||||
|
return statusMap[status]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: null,
|
||||||
|
listLoading: true,
|
||||||
|
dialogFormVisible: false,
|
||||||
|
nodeServices: [],
|
||||||
|
count: 0,
|
||||||
|
options: [],
|
||||||
|
listQuery: {
|
||||||
|
name: '',
|
||||||
|
clusterServerId: '',
|
||||||
|
page: 1,
|
||||||
|
size: 20
|
||||||
|
},
|
||||||
|
currentId: null,
|
||||||
|
rules: {
|
||||||
|
id: [{ required: true, message: '请选择运行Server', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
getClustersAndServers().then((res) => {
|
||||||
|
this.options = res.data
|
||||||
|
})
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
queryData() {
|
||||||
|
this.listQuery.page = 1
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
fetchData() {
|
||||||
|
this.listLoading = true
|
||||||
|
getCanalInstances(this.listQuery).then(res => {
|
||||||
|
this.list = res.data.items
|
||||||
|
this.count = res.data.count
|
||||||
|
}).finally(() => {
|
||||||
|
this.listLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleCreate() {
|
||||||
|
this.$router.push('/canalServer/canalInstance/add')
|
||||||
|
},
|
||||||
|
handleUpdate(row) {
|
||||||
|
this.$router.push('/canalServer/canalInstance/modify?id=' + row.id)
|
||||||
|
},
|
||||||
|
handleDelete(row) {
|
||||||
|
this.$confirm('删除Instance配置会导致停止', '确定删除Instance信息', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
deleteCanalInstance(row.id).then((res) => {
|
||||||
|
if (res.data === 'success') {
|
||||||
|
this.fetchData()
|
||||||
|
this.$message({
|
||||||
|
message: '删除Instance信息成功',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message: '删除Instance信息失败',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleStart(row) {
|
||||||
|
// if (row.runningStatus === '1') {
|
||||||
|
// this.$message({ message: '当前Instance已处于启动状态!', type: 'error' })
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
this.$confirm('启动Instance: ' + row.name, '确定启动Instance服务', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
instanceStatus(row.id, 'start').then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
this.fetchData()
|
||||||
|
this.$message({
|
||||||
|
message: '启动成功, 稍后请刷新列表查看状态',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message: '启动Instance出现异常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleStop(row) {
|
||||||
|
// if (row.runningStatus === '0') {
|
||||||
|
// this.$message({ message: '当前Instance已处于停止状态!', type: 'error' })
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
this.$confirm('停止Instance: ' + row.name, '确定停止Instance服务', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
instanceStatus(row.id, 'stop').then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
this.fetchData()
|
||||||
|
this.$message({
|
||||||
|
message: '停止成功, 稍后请刷新列表查看状态',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message: '停止Instance出现异常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleLog(row) {
|
||||||
|
if (row.nodeId === null) {
|
||||||
|
this.$message({ message: '当前Instance不是启动状态,无法查看日志', type: 'warning' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$router.push('canalInstance/log?id=' + row.id + '&nodeId=' + row.nodeServer.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2,9 +2,14 @@
|
|||||||
<div>
|
<div>
|
||||||
<el-form ref="form" :model="form">
|
<el-form ref="form" :model="form">
|
||||||
<div class="filter-container" style="padding-left: 10px;padding-top: 20px;">
|
<div class="filter-container" style="padding-left: 10px;padding-top: 20px;">
|
||||||
<el-input v-model="form.name" placeholder="实例名称" style="width: 200px;" class="filter-item" />
|
<el-input v-model="form.name" placeholder="Instance名称" style="width: 200px;" class="filter-item" />
|
||||||
|
<el-select v-model="form.clusterServerId" placeholder="所属集群/主机" class="filter-item">
|
||||||
<el-button class="filter-item" type="primary" @click="onSubmit">新建</el-button>
|
<el-option-group v-for="group in options" :key="group.label" :label="group.label">
|
||||||
|
<el-option v-for="item in group.options" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-option-group>
|
||||||
|
</el-select>
|
||||||
|
<el-button class="filter-item" type="primary" @click="onSubmit">保存</el-button>
|
||||||
|
<el-button class="filter-item" type="success" @click="onLoadTemplate">载入模板</el-button>
|
||||||
<el-button class="filter-item" type="info" @click="onBack">返回</el-button>
|
<el-button class="filter-item" type="info" @click="onBack">返回</el-button>
|
||||||
</div>
|
</div>
|
||||||
<editor v-model="form.content" lang="properties" theme="chrome" width="100%" :height="800" @init="editorInit" />
|
<editor v-model="form.content" lang="properties" theme="chrome" width="100%" :height="800" @init="editorInit" />
|
||||||
@@ -13,7 +18,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { addCanalInstance } from '@/api/canalInstance'
|
import { addCanalInstance, getTemplateInstance } from '@/api/canalInstance'
|
||||||
|
import { getClustersAndServers } from '@/api/canalCluster'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -21,13 +27,18 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
options: [],
|
||||||
form: {
|
form: {
|
||||||
name: '',
|
name: '',
|
||||||
content: ''
|
content: '',
|
||||||
|
clusterServerId: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
getClustersAndServers().then((res) => {
|
||||||
|
this.options = res.data
|
||||||
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
editorInit() {
|
editorInit() {
|
||||||
@@ -43,7 +54,21 @@ export default {
|
|||||||
onSubmit() {
|
onSubmit() {
|
||||||
if (this.form.name === '') {
|
if (this.form.name === '') {
|
||||||
this.$message({
|
this.$message({
|
||||||
message: '请输入实例名称',
|
message: '请输入Instance名称',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.form.clusterServerId === '') {
|
||||||
|
this.$message({
|
||||||
|
message: '请选择所属集群/主机',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.form.content === null || this.form.content === '') {
|
||||||
|
this.$message({
|
||||||
|
message: '请输入配置内容',
|
||||||
type: 'error'
|
type: 'error'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -75,6 +100,11 @@ export default {
|
|||||||
},
|
},
|
||||||
onBack() {
|
onBack() {
|
||||||
history.go(-1)
|
history.go(-1)
|
||||||
|
},
|
||||||
|
onLoadTemplate() {
|
||||||
|
getTemplateInstance().then(res => {
|
||||||
|
this.form.content = res.data
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,11 @@
|
|||||||
<div style="padding-left: 10px;padding-top: 20px;">
|
<div style="padding-left: 10px;padding-top: 20px;">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
{{ form.name }}
|
{{ form.name }}
|
||||||
|
<el-select v-model="form.clusterServerId" placeholder="所属集群/主机" class="filter-item">
|
||||||
|
<el-option-group v-for="group in options" :key="group.label" :label="group.label">
|
||||||
|
<el-option v-for="item in group.options" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-option-group>
|
||||||
|
</el-select>
|
||||||
<el-button type="primary" @click="onSubmit">修改</el-button>
|
<el-button type="primary" @click="onSubmit">修改</el-button>
|
||||||
<el-button type="warning" @click="onCancel">重置</el-button>
|
<el-button type="warning" @click="onCancel">重置</el-button>
|
||||||
<el-button type="info" @click="onBack">返回</el-button>
|
<el-button type="info" @click="onBack">返回</el-button>
|
||||||
@@ -16,6 +21,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { canalInstanceDetail, updateCanalInstance } from '@/api/canalInstance'
|
import { canalInstanceDetail, updateCanalInstance } from '@/api/canalInstance'
|
||||||
|
import { getClustersAndServers } from '@/api/canalCluster'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -23,15 +29,20 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
options: [],
|
||||||
form: {
|
form: {
|
||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
content: ''
|
content: '',
|
||||||
|
clusterServerId: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.loadCanalConfig()
|
this.loadCanalConfig()
|
||||||
|
getClustersAndServers().then((res) => {
|
||||||
|
this.options = res.data
|
||||||
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
editorInit() {
|
editorInit() {
|
||||||
@@ -48,13 +59,14 @@ export default {
|
|||||||
canalInstanceDetail(this.$route.query.id).then(response => {
|
canalInstanceDetail(this.$route.query.id).then(response => {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
this.form.id = data.id
|
this.form.id = data.id
|
||||||
this.form.name = data.name + '/instance.propertios'
|
this.form.name = data.name + '/instance.properties'
|
||||||
this.form.content = data.content
|
this.form.content = data.content
|
||||||
|
this.form.clusterServerId = data.clusterServerId
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.$confirm(
|
this.$confirm(
|
||||||
'修改Canal实例配置可能会导致实例重启,是否继续?',
|
'修改Instance配置可能会导致重启,是否继续?',
|
||||||
'确定修改',
|
'确定修改',
|
||||||
{
|
{
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
459
admin/admin-ui/src/views/canalServer/NodeServer.vue
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<div class="filter-container">
|
||||||
|
<!--<el-input v-model="listQuery.name" placeholder="Server 名称" style="width: 200px;" class="filter-item" />-->
|
||||||
|
<el-select v-model="listQuery.clusterId" placeholder="所属集群" class="filter-item">
|
||||||
|
<el-option key="" label="所属集群" value="" />
|
||||||
|
<el-option key="-1" label="单机" value="-1" />
|
||||||
|
<el-option v-for="item in canalClusters" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-input v-model="listQuery.ip" placeholder="Server IP" style="width: 200px;" class="filter-item" />
|
||||||
|
<el-button class="filter-item" type="primary" icon="el-icon-search" plain @click="queryData()">查询</el-button>
|
||||||
|
<el-button class="filter-item" type="primary" @click="handleCreate()">新建Server</el-button>
|
||||||
|
<el-button class="filter-item" type="info" @click="fetchData()">刷新列表</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
v-loading="listLoading"
|
||||||
|
:data="list"
|
||||||
|
element-loading-text="Loading"
|
||||||
|
border
|
||||||
|
fit
|
||||||
|
highlight-current-row
|
||||||
|
>
|
||||||
|
<el-table-column label="所属集群" min-width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.canalCluster !== null">
|
||||||
|
{{ scope.row.canalCluster.name }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
-
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Server 名称" min-width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.name }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Server IP" min-width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.ip }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="admin 端口" min-width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.adminPort }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="tcp 端口" min-width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.tcpPort }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="metric 端口" min-width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.metricPort }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column class-name="status-col" label="状态" min-width="150" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.status | statusFilter">{{ scope.row.status | statusLabel }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" prop="created_at" label="操作" min-width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-dropdown trigger="click">
|
||||||
|
<el-button type="primary" size="mini">
|
||||||
|
操作<i class="el-icon-arrow-down el-icon--right" />
|
||||||
|
</el-button>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item @click.native="handleConfig(scope.row)">配置</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleUpdate(scope.row)">修改</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleDelete(scope.row)">删除</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleStart(scope.row)">启动</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleStop(scope.row)">停止</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleInstances(scope.row)">详情</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleLog(scope.row)">日志</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination v-show="count>0" :total="count" :page.sync="listQuery.page" :limit.sync="listQuery.size" @pagination="fetchData()" />
|
||||||
|
<el-dialog :visible.sync="dialogFormVisible" :title="textMap[dialogStatus]" width="600px">
|
||||||
|
<el-form ref="dataForm" :rules="rules" :model="nodeModel" label-position="left" label-width="120px" style="width: 400px; margin-left:30px;">
|
||||||
|
<el-form-item label="所属集群" prop="clusterId">
|
||||||
|
<el-select v-if="dialogStatus === 'create'" v-model="nodeModel.clusterId" placeholder="选择所属集群">
|
||||||
|
<el-option key="" label="单机" value="" />
|
||||||
|
<el-option v-for="item in canalClusters" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-else v-model="nodeModel.clusterId" placeholder="选择所属集群" disabled="disabled">
|
||||||
|
<el-option key="" label="单机" value="" />
|
||||||
|
<el-option v-for="item in canalClusters" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Server 名称" prop="name">
|
||||||
|
<el-input v-model="nodeModel.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Server IP" prop="ip">
|
||||||
|
<el-input v-model="nodeModel.ip" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="admin 端口" prop="adminPort">
|
||||||
|
<el-input v-model="nodeModel.adminPort" placeholder="11110" type="number" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="tcp 端口" prop="tcpPort">
|
||||||
|
<el-input v-model="nodeModel.tcpPort" placeholder="11111" type="number" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="metric 端口" prop="metricPort">
|
||||||
|
<el-input v-model="nodeModel.metricPort" placeholder="11112" type="number" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="dialogFormVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="dataOperation()">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog :visible.sync="dialogInstances" title="instance 列表" width="800px">
|
||||||
|
<div class="filter-container">
|
||||||
|
<el-button class="filter-item" type="info" @click="activeInstances()">刷新列表</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
v-loading="listLoading2"
|
||||||
|
:data="instanceList"
|
||||||
|
element-loading-text="Loading"
|
||||||
|
border
|
||||||
|
fit
|
||||||
|
highlight-current-row
|
||||||
|
>
|
||||||
|
<el-table-column label="Instance 名称" min-width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.name }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" min-width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.runningStatus | statusFilter">{{ scope.row.runningStatus | statusLabel }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" min-width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-dropdown trigger="click">
|
||||||
|
<el-button type="primary" size="mini">
|
||||||
|
操作<i class="el-icon-arrow-down el-icon--right" />
|
||||||
|
</el-button>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item @click.native="handleStartInstance(scope.row)">启动</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleStopInstance(scope.row)">停止</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { addNodeServer, getNodeServers, updateNodeServer, deleteNodeServer, startNodeServer, stopNodeServer } from '@/api/nodeServer'
|
||||||
|
import { getActiveInstances, stopInstance, startInstance } from '@/api/canalInstance'
|
||||||
|
import { getCanalClusters } from '@/api/canalCluster'
|
||||||
|
import Pagination from '@/components/Pagination'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Pagination },
|
||||||
|
filters: {
|
||||||
|
statusFilter(status) {
|
||||||
|
const statusMap = {
|
||||||
|
'1': 'success',
|
||||||
|
'0': 'gray',
|
||||||
|
'-1': 'danger'
|
||||||
|
}
|
||||||
|
return statusMap[status]
|
||||||
|
},
|
||||||
|
statusLabel(status) {
|
||||||
|
const statusMap = {
|
||||||
|
'1': '启动',
|
||||||
|
'0': '停止',
|
||||||
|
'-1': '断开'
|
||||||
|
}
|
||||||
|
return statusMap[status]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: null,
|
||||||
|
instanceList: null,
|
||||||
|
listLoading: true,
|
||||||
|
listLoading2: true,
|
||||||
|
serverIdTmp: null,
|
||||||
|
canalClusters: [],
|
||||||
|
count: 0,
|
||||||
|
listQuery: {
|
||||||
|
name: '',
|
||||||
|
ip: '',
|
||||||
|
clusterId: null,
|
||||||
|
page: 1,
|
||||||
|
size: 20
|
||||||
|
},
|
||||||
|
dialogFormVisible: false,
|
||||||
|
dialogInstances: false,
|
||||||
|
textMap: {
|
||||||
|
create: '新建Server信息',
|
||||||
|
update: '修改Server信息'
|
||||||
|
},
|
||||||
|
nodeModel: {
|
||||||
|
id: undefined,
|
||||||
|
clusterId: null,
|
||||||
|
name: null,
|
||||||
|
ip: null,
|
||||||
|
adminPort: 11110,
|
||||||
|
tcpPort: 11111,
|
||||||
|
metricPort: 11112
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
name: [{ required: true, message: 'Server 名称不能为空', trigger: 'change' }],
|
||||||
|
ip: [{ required: true, message: 'Server IP不能为空', trigger: 'change' }],
|
||||||
|
adminPort: [{ required: true, message: 'Server admin端口不能为空', trigger: 'change' }]
|
||||||
|
},
|
||||||
|
dialogStatus: 'create'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// { min: 2, max: 5, message: '长度在 2 到 5 个字符', trigger: 'change' }
|
||||||
|
created() {
|
||||||
|
getCanalClusters().then((res) => {
|
||||||
|
this.canalClusters = res.data
|
||||||
|
})
|
||||||
|
if (this.$route.query.clusterId) {
|
||||||
|
try {
|
||||||
|
this.listQuery.clusterId = Number(this.$route.query.clusterId)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData() {
|
||||||
|
this.listLoading = true
|
||||||
|
getNodeServers(this.listQuery).then(res => {
|
||||||
|
this.list = res.data.items
|
||||||
|
this.count = res.data.count
|
||||||
|
}).finally(() => {
|
||||||
|
this.listLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
queryData() {
|
||||||
|
this.listQuery.page = 1
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
resetModel() {
|
||||||
|
this.nodeModel = {
|
||||||
|
id: undefined,
|
||||||
|
clusterId: null,
|
||||||
|
name: null,
|
||||||
|
ip: null,
|
||||||
|
adminPort: null,
|
||||||
|
tcpPort: null,
|
||||||
|
metricPort: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleCreate() {
|
||||||
|
this.resetModel()
|
||||||
|
this.dialogStatus = 'create'
|
||||||
|
this.dialogFormVisible = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs['dataForm'].clearValidate()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleInstances(row) {
|
||||||
|
this.serverIdTmp = row.id
|
||||||
|
this.activeInstances()
|
||||||
|
},
|
||||||
|
activeInstances() {
|
||||||
|
this.listLoading2 = true
|
||||||
|
this.dialogInstances = true
|
||||||
|
getActiveInstances(this.serverIdTmp).then(res => {
|
||||||
|
this.instanceList = res.data
|
||||||
|
}).finally(() => {
|
||||||
|
this.listLoading2 = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
dataOperation() {
|
||||||
|
this.$refs['dataForm'].validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
if (this.dialogStatus === 'create') {
|
||||||
|
addNodeServer(this.nodeModel).then(res => {
|
||||||
|
this.operationRes(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.dialogStatus === 'update') {
|
||||||
|
updateNodeServer(this.nodeModel).then(res => {
|
||||||
|
this.operationRes(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
operationRes(res) {
|
||||||
|
if (res.data === 'success') {
|
||||||
|
this.fetchData()
|
||||||
|
this.dialogFormVisible = false
|
||||||
|
this.$message({
|
||||||
|
message: this.textMap[this.dialogStatus] + '成功',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message: this.textMap[this.dialogStatus] + '失败',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleConfig(row) {
|
||||||
|
if (row.canalCluster !== null) {
|
||||||
|
this.$message({ message: '集群模式Server不允许单独变更配置,请在集群配置变更', type: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$router.push('/canalServer/nodeServer/config?serverId=' + row.id)
|
||||||
|
},
|
||||||
|
handleUpdate(row) {
|
||||||
|
this.resetModel()
|
||||||
|
this.nodeModel = Object.assign({}, row)
|
||||||
|
this.dialogStatus = 'update'
|
||||||
|
this.dialogFormVisible = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs['dataForm'].clearValidate()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleDelete(row) {
|
||||||
|
this.$confirm('删除Server信息会导致节点服务停止', '确定删除Server信息', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
deleteNodeServer(row.id).then((res) => {
|
||||||
|
if (res.data === 'success') {
|
||||||
|
this.fetchData()
|
||||||
|
this.$message({
|
||||||
|
message: '删除Server信息成功',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message: '删除Server信息失败',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleStart(row) {
|
||||||
|
if (row.status !== '0') {
|
||||||
|
this.$message({ message: '当前Server不是停止状态,无法启动', type: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$confirm('启动Server服务', '确定启动Server服务', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
startNodeServer(row.id).then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
this.fetchData()
|
||||||
|
this.$message({
|
||||||
|
message: '启动成功',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message: '启动Server服务出现异常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleStop(row) {
|
||||||
|
if (row.status !== '1') {
|
||||||
|
this.$message({ message: '当前Server不是启动状态,无法停止', type: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$confirm('停止Server服务会导致所有Instance都停止服务', '确定停止Server服务', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
stopNodeServer(row.id).then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
this.fetchData()
|
||||||
|
this.$message({
|
||||||
|
message: '停止成功',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message: '停止Server服务出现异常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleLog(row) {
|
||||||
|
this.$router.push('nodeServer/log?id=' + row.id)
|
||||||
|
},
|
||||||
|
handleStartInstance(row) {
|
||||||
|
if (row.runningStatus !== '0') {
|
||||||
|
this.$message({ message: '当前Instance不是停止状态,无法启动', type: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$confirm('启动Instance服务', '确定启动Instance服务', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
startInstance(row.id, this.serverIdTmp).then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
this.activeInstances()
|
||||||
|
this.$message({
|
||||||
|
message: '启动成功, 稍后请刷新列表查看状态',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message: '启动Instance服务出现异常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleStopInstance(row) {
|
||||||
|
if (row.runningStatus !== '1') {
|
||||||
|
this.$message({ message: '当前Instance不是运行状态,无法停止', type: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$confirm('集群模式下停止实例其它主机将会抢占执行该实例', '停止 Instance 服务', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
stopInstance(row.id, this.serverIdTmp).then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
this.activeInstances()
|
||||||
|
this.$message({
|
||||||
|
message: '停止成功, 稍后请刷新列表查看状态',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
message: '停止Instance服务出现异常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -66,8 +66,8 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const validatePassword = (rule, value, callback) => {
|
const validatePassword = (rule, value, callback) => {
|
||||||
if (value.length < 6) {
|
if (value.length < 5) {
|
||||||
callback(new Error('The password can not be less than 6 digits'))
|
callback(new Error('The password can not be less than 5 digits'))
|
||||||
} else {
|
} else {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||