Compare commits

...
Author SHA1 Message Date
bucketli 7ac605242e fix(canal):fix endless local binlog mode with exception. 2020-02-27 17:56:10 +08:00
Rg 70d8a379f2 add Rust canal client support (#2518) 2020-01-15 20:12:28 +08:00
rewerma b6333f018f Revert "修复kafka消费回滚的bug (#2521)" (#2526)
This reverts commit 31acfc0101.
2020-01-02 17:04:57 +08:00
rewerma 31acfc0101 修复kafka消费回滚的bug (#2521) 2020-01-02 16:15:47 +08:00
rewerma 6d9ea57186 修复子线程阻塞造成不能中止的问题 (#2508) 2019-12-26 19:29:48 +08:00
Li Qiang c4c0255073 Update README.md 2019-11-04 10:20:00 +08:00
agapple 1fa9020599 format 2019-10-15 21:38:48 +08:00
agapple dd7554374f format 2019-10-15 21:35:57 +08:00
shicongyang 65b5325ca6 建议增加一个属性,针对根据某一字段hash时,hash算法可以不根据database算出 #2248 (#2294) 2019-10-15 21:32:42 +08:00
agapple b64a3e301a fixed issue #2286 , ArithmeticException zero 2019-10-10 15:37:17 +08:00
agapple 31562919bb rename canal-admin project to admin 2019-10-09 14:50:08 +08:00
agapple 9c75e25da7 optimizer pom dependency 2019-10-09 14:18:37 +08:00
agapple 7d91850be6 fixed issue #2257 , time 100 parse bugfix 2019-10-09 13:39:22 +08:00
agapple 4af3b24b69 fixed issue #2281 , null reject 2019-10-08 18:50:37 +08:00
craigwu9 e4e66b143c make canal.admin.port canal.admin.user canal.admin.passwd can be override by system env properties (#2274)
* Update SimpleAdminConnectors.java

add more information about which server execute failed

* Update CanalStarter.java

make canal.admin.port canal.admin.user canal.admin.passwd can be override by system env
2019-09-30 20:45:00 +08:00
agapple dd93e2b0c4 fixed issue #2258 , optimizer rabbitmq send perf & matchTopics only match one Topic 2019-09-30 20:42:52 +08:00
agapple d505b5ab9d fixed issue #2258 , optimizer rocketmq send perf 2019-09-30 20:19:25 +08:00
agapple 0fb717a97f fixed issue #2258 , optimizer kafka send perf 2019-09-30 14:24:59 +08:00
一杯Hash c634f98fca bugfix mysql >= 8.0, 缓存层移除,EbeanServer 运行 Fail,Unknown system variable query_cache_size error! (#2264)
threw exception; nested exception is javax.persistence.PersistenceException: java.sql.SQLException: Unknown system variable 'query_cache_size'
2019-09-29 10:51:17 +08:00
rewerma 460479390a bug fix (#2249) 2019-09-26 10:58:28 +08:00
Li Qiang 26013e239d Update README.md 2019-09-25 19:49:22 +08:00
agapple 46fe40ed47 use es6x/es7x instead of elaticsearch project 2019-09-25 19:31:52 +08:00
Koshoooooo 0c27bcf73e Fix issue #2221 (#2242)
fastjson修复了拒绝服务安全问题,升级到当前1.2.58的兼容安全版本1.2.58.sec06
2019-09-25 19:28:13 +08:00
agapple d86b0e6e67 fixed testcase 2019-09-25 19:16:33 +08:00
EdgarScott fff623acb1 Add unit tests (#2235)
* Add Unit Tests for the following classes:

com.alibaba.otter.canal.client.adapter.support.JdbcTypeUtilTest
com.alibaba.otter.canal.client.adapter.support.UtilTest
com.taobao.tddl.dbsync.binlog.JsonConversion_Json_ValueTest
com.taobao.tddl.dbsync.binlog.JsonDiffConversionTest
com.taobao.tddl.dbsync.binlog.LogBufferTest
com.taobao.tddl.dbsync.binlog.LogEventTest
com.taobao.tddl.dbsync.binlog.event.LogHeaderTest
com.taobao.tddl.dbsync.binlog.event.RowsLogBufferTest
com.alibaba.otter.canal.parse.driver.mysql.packets.client.RegisterSlaveCommandPacketTest
com.alibaba.otter.canal.parse.driver.mysql.utils.ByteHelperTest
com.alibaba.otter.canal.parse.inbound.mysql.ddl.DdlResultTest
com.alibaba.otter.canal.parse.inbound.mysql.ddl.DruidDdlParserTest

These tests were written using Diffblue Cover

* Add dependencies and refactor
2019-09-25 18:53:24 +08:00
rewerma f45c0206da Add elasticsearch 7.x version adapter support (#2246)
* init es data sync upgrade

* Add elasticsearch 7.x version adapter support
2019-09-25 18:51:34 +08:00
gaecfov 61726c7e05 fix: ES配置文件监控问题 (#2202)
* fix:
ES监控配置文件时如果第一次因为文件错误新增失败后,再次修改不会新增的问题
ES监控配置文件添加的Config缓存没有带Instance导致匹配不到的问题

* fix:ES配置文件监控不生效
2019-09-17 09:22:55 +08:00
耐小心 79552e0a37 rabbitmq(amqp)MQ支持 (#2156) 2019-09-16 12:43:02 +08:00
agapple 5ec2087257 fixed issue #2197 , startup.bat fix 2019-09-16 09:58:27 +08:00
rewerma 5214257cd4 ES子表含group的where条件拼接 (#2190)
* ES子表含group的where条件拼接

* modify
2019-09-16 09:47:02 +08:00
gaecfov 365ca4d01f fix:当查询列包含常量拼接时解析主键字段进行条件拼接时会多出 where null=null (#2188) 2019-09-16 09:44:59 +08:00
agapple 419a73b3de fixed issue #2168, ignore create database 2019-09-12 09:08:30 +08:00
Jony c093fbb8f6 动态刷新同步锁状态 (#2181)
* 解决修改数据库远程配置,canal-adapter 一直循环destroy、init。

* 解决动态添加instance和对应的adapter无锁状态问题。
2019-09-12 08:52:33 +08:00
rewerma 31bb85bea3 fix bug #2023 and code optimize (#2167)
* 减少等待刷新状态时间

* fix bug #2023
2019-09-12 08:47:22 +08:00
rewerma 803565456f fix modified_time null value error (#2157)
* template config load

* compile vue frontend for product

* fix modified_time null value error

* modify
2019-09-07 21:53:11 +08:00
Li Qiang 520f5771d8 Update README.md 2019-09-06 15:02:34 +08:00
Li Qiang 5fbd6f6606 Update README.md 2019-09-06 14:57:47 +08:00
Li Qiang bc9c7f0f1d Update README.md 2019-09-06 14:54:22 +08:00
Li Qiang 08a2f0aa44 Update README.md 2019-09-06 14:45:33 +08:00
Li Qiang a0387faa67 Update README.md 2019-09-06 14:45:14 +08:00
Li Qiang 1dd4674ccb Update README.md 2019-09-06 13:36:04 +08:00
Jony 60e26d1ddd 解决修改数据库远程配置,canal-adapter 一直循环destroy、init。 (#2137) 2019-09-04 12:25:54 +08:00
agapple ecdab76705 fixed issue #2129 , Negative seek offset 2019-09-03 08:51:21 +08:00
agapple 403fdf997c Update README.md 2019-09-02 19:28:49 +08:00
agapple a9e83463e0 Update README.md 2019-09-02 19:27:50 +08:00
agapple 2bde4327c7 [maven-release-plugin] prepare for next development iteration 2019-09-02 14:24:16 +08:00
agapple d53bfd7ee7 [maven-release-plugin] prepare release canal-1.1.4 2019-09-02 14:24:03 +08:00
agapple ccc45b672c fixed issue #2127 , remove logback stdout 2019-09-02 14:10:29 +08:00
agapple 0d42bdafe2 fixed stdout 2019-09-02 14:04:19 +08:00
agapple 95050ff382 fixed test example 2019-09-02 13:46:35 +08:00
agapple 5addd246ac fixed unused error 2019-09-01 11:20:00 +08:00
agapple cfc95e2b7a fixed docker config 2019-09-01 10:41:00 +08:00
agapple 44187d9e3a fixed docker config 2019-09-01 10:15:58 +08:00
agapple c76e8ba0e4 fixed canal-admin 2019-08-31 15:23:58 +08:00
rewerma 1bcae9dc49 template config load (#2123)
* template config load

* compile vue frontend for product
2019-08-31 14:56:28 +08:00
agapple 3a388308d9 fixed compiler error 2019-08-31 13:01:52 +08:00
agapple a60ec6fa42 fixed tescase failed 2019-08-31 13:00:31 +08:00
agapple 456945fe3a add autoRegister and cluster 2019-08-31 12:59:12 +08:00
agapple ba24c15833 add conflict file 2019-08-31 12:47:27 +08:00
agapple 4e0b8a0278 fixed admin 2019-08-31 12:24:41 +08:00
rewerma 274e91f8aa bug fix (#2122)
* admin端口调整完成

* server端适配调整

* 调整部分功能

* 单机/集群admin调试完成

* 重新发布前端页面

* bug fix

* 解决server端admin配置会被远程覆盖的问题
解决default-instance.xml无法加载远程zkServers配置的问题

* 集群模式下取活动实例的问题
2019-08-31 12:22:16 +08:00
rewerma 89055f7405 canal-admin完善 (#2121)
* admin端口调整完成

* server端适配调整

* 调整部分功能

* 单机/集群admin调试完成

* 重新发布前端页面
2019-08-30 18:09:22 +08:00
rewerma e8dba41216 code optimize (#2108)
* code optimize
2019-08-27 22:52:14 +08:00
agapple f52b2cccd5 fixed testcase 2019-08-27 22:45:27 +08:00
rewerma 7386327af9 code optimize (#2107)
* admin assembly打包

* 修改npm  profile

* code optimize

* code optimize
2019-08-27 22:24:53 +08:00
rewerma c48407b449 修改npm profile (#2106)
* admin assembly打包

* 修改npm  profile
2019-08-27 20:55:15 +08:00
rewerma d92badeb45 admin assembly打包 (#2104) 2019-08-27 19:48:22 +08:00
agapple 7093179cc8 fixed issue #2099 , rename poll to polling 2019-08-27 16:25:42 +08:00
agapple bf89974f7c fixed issue #2099 , support remote config poll 2019-08-27 16:11:20 +08:00
rewerma 2e1b7a5093 通过http从canal-admin中拉配置 (#2093)
* modify

* http拉取配置监听

* revert main pom

* 整理代码
2019-08-26 19:01:08 +08:00
agapple 3d4dbc9a8c fixed issue #2099 , support user/passwd acl and canal admin operator 2019-08-26 18:16:33 +08:00
agapple e376a129ed fixed canal-admin vue 2019-08-24 16:54:27 +08:00
wangwengeek 983ff5fc73 查询数据过长变成byte数组转化为json解决 (#2090)
Caused by: com.alibaba.fastjson.JSONException: syntax error, pos 2, json : [B@406b7f2 #2089
2019-08-23 17:38:22 +08:00
agapple 3a74f6124a fixed issue #1940 #2088 , add canal.register.ip support docker 2019-08-22 23:48:19 +08:00
agapple bd974e3da8 upgrade fastjson 1.2.58 2019-08-22 23:03:22 +08:00
agapple 86cd805a64 upgrade fastsql 973 2019-08-22 22:02:58 +08:00
agapple 8b54b9e596 add python client info 2019-08-22 15:17:33 +08:00
agapple 2d3baeab7d fastsql tsdb testcase 2019-08-22 15:08:36 +08:00
黄大海 7b245c5a64 fix #1762 (#1959) 2019-08-21 14:33:31 +08:00
agapple 78af6b1130 fixed issue #2067 , add example 2019-08-21 13:52:34 +08:00
ever 7547cac3c5 fix compiler problem: upgrade rocketmq to 4.5.2 (#2074) 2019-08-21 12:48:46 +08:00
soocold 066743cfd6 always check whether ringbuffer has an exception in event processor (#2053) 2019-08-21 12:46:17 +08:00
rewerma fdd0763567 新增es adapter rest client和认证机制 (#2046)
* 新增es adapter rest client和认证机制

* 修改getMapping位置

* 代码调整

* 代码调整

* 整理代码

* fix #2048
2019-08-21 12:44:57 +08:00
ThomasPerkins1123 bcdbf75dc5 Add unit tests for com.alibaba.otter.canal.parse.driver.mysql.utils.ByteHelper (#2039)
These tests were written using Diffblue Cover.
2019-08-21 12:44:24 +08:00
jason-liew db23f15b61 fix: hbase etl sql syntax error in some case (#2034) 2019-08-21 12:44:04 +08:00
rewerma 44722fe01c canal-admin-ui 指定profile编译 (#2019)
* canal-admin-ui 指定profile编译

* 启动实例时选择指定节点

* modify

* modify

* 静态页面修改
2019-07-22 09:15:14 +08:00
bucketli be9813142f remove openjdk12 2019-07-20 19:05:49 +08:00
bucketli c48e80a163 remove oraclejdk8 CI config,add oraclejdk12 and openjdk12 2019-07-20 18:34:01 +08:00
bucketli c1bfad4f2d 1.not compile canal-admin module,unless you need it, to be optimize.#1998,2.support JDK 11 & 12 ,3.fix the CI block test 2019-07-20 18:12:07 +08:00
rewerma 10f4ef6acd canal admin (#1998)
* 增加注解

* modify

* 文件大小写问题
2019-07-16 21:47:53 +08:00
Li Qiang 4c0d82fb98 Update README.md 2019-07-16 20:35:37 +08:00
rewerma 7cec0994e9 Merge branch 'master' into master 2019-07-14 19:24:42 +08:00
mcy 483044916d canal admin 2019-07-14 19:18:15 +08:00
mcy c8bb6e200c modify 2019-07-14 09:59:58 +08:00
mcy 9cc6b67426 ddl sql 2019-07-14 09:59:35 +08:00
mcy 62072ea9ab modify 2019-07-14 00:06:25 +08:00
mcy f3daceea6b canal instance manager module finished 2019-07-13 21:34:44 +08:00
rewerma 4efc0f6078 modify 2019-07-13 18:03:28 +08:00
rewerma 40d2a8c6cc modify 2019-07-13 14:33:42 +08:00
rewerma 28da536ec8 modify 2019-07-13 11:47:55 +08:00
mcy 6f129bf524 modify 2019-07-12 21:00:33 +08:00
mcy 31ae308760 增减jmx状态控制 2019-07-12 20:59:20 +08:00
mcy c2ce662f9a modify 2019-07-12 20:08:16 +08:00
mcy 7044e172f5 jmx 客户端 2019-07-12 20:06:58 +08:00
Menng c070757da0 修正文档链接&优化格式 #1965 (#1967) 2019-07-12 10:13:29 +08:00
xingwenge 92dd2bb189 Update README.md (#1981) 2019-07-12 09:53:06 +08:00
rewerma 8639b45fe9 fix #1761 (#1973)
* Message转Dml对null值的判断

* Message转Dml对null值的判断

* Message转Dml对null值的判断

* fix #1761
2019-07-12 09:52:36 +08:00
mcy 6b9487a0a0 modify 2019-07-12 09:16:10 +08:00
rewerma 5741f5a347 modify 2019-07-12 00:42:18 +08:00
rewerma 4c3ef6772c modify 2019-07-12 00:01:37 +08:00
rewerma 64824bc5aa modify 2019-07-11 23:29:49 +08:00
mcy 23209f24ed 增减jmx状态控制 2019-07-11 21:34:12 +08:00
mcy 015026ea91 modify 2019-07-11 21:31:38 +08:00
mcy c7967e989c modify 2019-07-11 20:46:02 +08:00
mcy e4b37f33de modify 2019-07-11 20:44:39 +08:00
mcy 598f15a404 modify 2019-07-11 16:30:50 +08:00
mcy ec5f1eb10d modify 2019-07-11 16:16:32 +08:00
mcy 28c0aac98d modify 2019-07-11 15:11:29 +08:00
mcy 50ea6870dc modify 2019-07-11 15:07:57 +08:00
mcy 317439670c modify 2019-07-11 10:16:12 +08:00
rewerma 64ff140304 modify 2019-07-11 01:05:11 +08:00
rewerma a93af74539 modify 2019-07-11 00:20:56 +08:00
rewerma fc39b2af68 modify 2019-07-10 21:46:38 +08:00
mcy e87a0c3b63 canal-admin init 2019-07-10 16:22:38 +08:00
mcy b43043f4e4 Merge remote-tracking branch 'origin/master' 2019-07-09 19:09:24 +08:00
mcy 60a46ae941 fix #1761 2019-07-09 19:08:35 +08:00
Braavos b5c05c2fd2 Add unit tests for com.alibaba.otter.canal.parse.driver.mysql.utils.MySQLPasswordEncrypter (#1937)
These tests were written using Diffblue Cover.
2019-06-28 14:57:18 +08:00
靳光宇 0cf18976d9 Update HbaseSyncService.java (#1936)
Fix batch update or delete with only affects the first data issue
2019-06-28 14:56:51 +08:00
Li Qiang 803c0607d3 Update README.md 2019-06-27 19:45:26 +08:00
Li Qiang f04e742f4f #1897 , fix DummyEventStore implements new method (#1932) 2019-06-27 09:58:24 +08:00
cjk87927 c81034dda3 增加表字段过滤功能 (#1914)
* 增加表字段过滤功能

* 增加表字段过滤功能

* 更换fastsql版本通过编译

* 1.修改字段配置格式 2.增加字段黑名单配置(冲突时以白名单为准)
2019-06-26 17:45:14 +08:00
agapple a847660a45 fixed issue #1826 , remove kafka transaction send 2019-06-26 16:02:19 +08:00
agapple 4957083493 fixed issue #1897 , memory event store ack null 2019-06-25 17:32:46 +08:00
rewerma 24f0eece42 Merge pull request #1 from alibaba/master
merge
2019-06-19 21:00:31 +08:00
agapple adccc0e912 add logger info for query_log_event 2019-06-19 15:04:22 +08:00
agapple 8728beb4b6 fixed compile 2019-06-18 22:18:30 +08:00
agapple 5bea876ec9 fixed issue #1352, add pkNames for flatMessage partition 2019-06-18 21:44:52 +08:00
agapple 6c494f8892 add rocketmq-client pom version 2019-06-18 21:28:55 +08:00
BillowX 74489d1bba 当MQ配置为Kafka时,支持配置Kerberos认证 (#1895) 2019-06-18 15:02:14 +08:00
Z6N 5ac0f7b710 按主键散列,未设置主键时无限空指针异常 (#1889)
当按注解散列时,如果监控的表未显示设置主键,将会导致flatMessage.getPkNames()为空,导致无限空指针异常
2019-06-18 15:01:00 +08:00
lanxinxu 75b4911caf 线程安全的获取meta (#1866) 2019-06-18 14:59:30 +08:00
lanxinxu 95602f4784 并行解析模式下有丢数据风险 (#1857) 2019-06-18 14:57:59 +08:00
agapple 058e1fc546 fixed delete message send mq bugfix 2019-05-28 00:14:20 +08:00
lulu2panpan e808c6ec70 AbstractCanalInstance的subscribeChange方法,增加对AbstractEventParser的类型判断;MysqlEventParser对Gtid的处理需要考虑兼容性 (#1824)
* AbstractCanalInstance的subscribeChange方法,增加对AbstractEventParser的类型判断,以支持用户的自定义CanalEventParser

* MysqlEventParser对Gtid模式的处理需要考虑兼容性,支持系统平滑的从非gtid模式升级为gtid模式
2019-05-28 00:05:13 +08:00
rewerma ce7948d6e7 fix Message转Dml对null值的bug (#1808)
* Message转Dml对null值的判断

* Message转Dml对null值的判断

* Message转Dml对null值的判断
2019-05-28 00:03:55 +08:00
soso 8df6d6fed8 Fix: 主备切换后没有往Kafka发送数据 (#1810)
发生主备切换时,调用canalMQStarter启停对应的destination。
2019-05-28 00:03:28 +08:00
nieyuan1991 f9989e0856 fix:未捕获kafka事务异常,造成canal ack异常 (#1814) 2019-05-28 00:02:25 +08:00
xiayuan 4075a0623f set the validationQuery while testWhileIdle is true (#1827) 2019-05-27 23:56:25 +08:00
Marsss___ 37fc248ebf 删除操作计算hash值的情况没有考虑,导致删除时的分区和插入时的分区不一致 (#1848) 2019-05-27 23:54:25 +08:00
Heng Du d0a83aad27 Support seamless docking with Alibaba Cloud RocketMQ commercial service (#1849) 2019-05-27 23:51:32 +08:00
agapple 3b9ff26d1a add logo 2019-05-15 10:20:08 +08:00
mcy 4f7842c843 Message转Dml对null值的判断 2019-05-14 17:38:01 +08:00
mcy b870e7ecd0 Message转Dml对null值的判断 2019-05-14 17:35:34 +08:00
mcy 331dd63c5f Message转Dml对null值的判断 2019-05-14 17:31:51 +08:00
JKTerrific 72c4daba5f 1:添加对于查询结果中,MAX( CASE a.key WHEN 'WORD1' THEN a.value ELSE 0 END ) AS… (#1794)
* 1:添加对于查询结果中,MAX( CASE a.key WHEN 'WORD1' THEN a.value ELSE 0 END ) AS WORD1 的竖表转横表的支持
2:添加对于查询条件尾部,使用GROUP BY _id 等分组语句时出现的sql解析异常问题

* fix index out of range
2019-05-11 17:33:59 +08:00
Jet 7427a74612 improve es etl (#1758)
* fix typo in trace log

* improve es etl

* improve es etl log

* improve es etl by add threadCount

* improve es etl by add threadCount

* improve es etl query

* remove comment
2019-05-11 17:26:08 +08:00
paascloud 24fae6fa52 变量定义错误 (#1776)
* 变量定义错误

* 变量定义错误
2019-05-11 17:21:21 +08:00
Sweet 4c690fb5a3 Null point Exception during loading the checksum (#1788)
If you connect to the mysql with version 5.1, the result of command 'select @master_binlog_checksum' is an array with only one Null element .
2019-05-11 17:17:54 +08:00
Jet 8f088cddc0 Fix es bulk (#1743)
thanks
2019-04-25 09:36:09 +08:00
Jet e94ee64316 fix typo in trace log (#1738) 2019-04-24 09:57:05 +08:00
dataccs 3165cb9640 支持用户自定义的CanalAlarmHandler (#1736)
* 支持用户自定义的CanalAlarmHandler

* RowsLogEvent增加对TableMapLogEvent判空检查,防止NPE异常
2019-04-24 09:56:32 +08:00
rewerma 2b738f4e04 fix bugs (#1712)
* kafka 模式默认开启事务异步发送

* fix adapter etl rest sql inject bug

* fix #1704
2019-04-18 20:33:15 +08:00
rewerma 16f335c381 修复rest etl 存在SQL注入风险 (#1685)
* kafka 模式默认开启事务异步发送

* fix adapter etl rest sql inject bug
2019-04-15 23:58:12 +08:00
王超 a8bf2b641f String.split(String regex) bug fix (#1696) 2019-04-15 23:55:03 +08:00
chenxyz707 67faf0e03e resovle config/metrics as a destination folder (#1678) 2019-04-09 21:05:42 +08:00
Xin Wang 8cb85753bb open the switch to get codecov test coverage (#1671)
* add codecov for test coverage
2019-04-09 19:11:48 +08:00
agapple df86f94409 fixed issue template 2019-04-09 15:12:30 +08:00
agapple 80594444f7 fixed issue template 2019-04-09 15:11:14 +08:00
agapple d8996432b9 add issue template 2019-04-09 15:08:43 +08:00
agapple 69df50fe17 fixed issue #1672 , CanalKafkaProducer send 2019-04-08 19:57:00 +08:00
agapple c5736bd5f3 Merge pull request #1670 from lovepoem/update_compile_jdk
update jacoco lib
2019-04-08 12:28:27 +08:00
Xin Wang 55030476ee reverse jdk to 1.8 2019-04-07 19:14:46 +08:00
Xin Wang cd007088db reverse jdk to 1.8 2019-04-07 18:52:35 +08:00
Xin Wang 1fc880f255 reverse jdk to 1.7 2019-04-07 18:45:54 +08:00
Xin Wang 8c68cf833a update jacoco lib 2019-04-07 18:38:50 +08:00
Xin Wang 0de8f4691e update jacoco lib 2019-04-07 18:25:05 +08:00
Xin Wang 5a2efbf9cb update jacoco lib 2019-04-07 18:23:53 +08:00
agapple dfab98fce1 Merge pull request #1669 from lovepoem/add_test_coverage_and_jdk11
add test coverage and jdk11
2019-04-06 23:37:40 +08:00
Xin Wang e5c7bbffbf update pom 2019-04-06 18:13:41 +08:00
Xin Wang e139698f6e update pom 2019-04-06 17:50:05 +08:00
Xin Wang 2b8120b6ed update pom 2019-04-06 17:49:03 +08:00
Xin Wang 68ebb3a957 update skipTests=false 2019-04-06 17:35:16 +08:00
Xin Wang 21309cb065 resove Could not resolve dependencies for project com.alibaba.otter:client-adapter.hbase:jar:1.1.4-SNAPSHOT: Could not find artifact jdk.tools:jdk.tools:jar:1.7 at specified path 2019-04-06 17:23:48 +08:00
Xin Wang 386567a633 resove Could not resolve dependencies for project com.alibaba.otter:client-adapter.hbase:jar:1.1.4-SNAPSHOT: Could not find artifact jdk.tools:jdk.tools:jar:1.7 at specified path 2019-04-06 16:56:03 +08:00
Xin Wang 95c69bb854 add test coverage and jdk11 2019-04-06 16:45:04 +08:00
Xin Wang 6febcf1a0f add test coverage and jdk11 2019-04-06 16:42:45 +08:00
agapple fadfe1d6ca [maven-release-plugin] prepare for next development iteration 2019-04-04 10:25:44 +08:00
agapple a7b12c7098 [maven-release-plugin] prepare release canal-1.1.3 2019-04-04 10:25:29 +08:00
agapple 2c320e8618 Merge pull request #1661 from rewerma/master
flatMessage模式下默认canal.mq.lingerMs=100
2019-04-04 10:08:34 +08:00
machey d76d25df6c flatMessage模式下默认canal.mq.lingerMs=100 2019-04-04 00:14:27 +08:00
agapple b95c0d03cf fixed issue #1660 ,support mysql8.0 caching_sha2_password auth 2019-04-03 23:42:41 +08:00
agapple 08af40221f Merge pull request #1658 from rewerma/master
adapter 日志打印修改
2019-04-03 12:12:56 +08:00
rewerma d13c1eb17c Merge branch 'master' into master 2019-04-03 11:34:10 +08:00
agapple 82ed9db8ae fixed rocketmq partition out of range 2019-04-03 11:26:31 +08:00
agapple d3195ac973 fixed issue #1626 , support alter table id first 2019-04-03 10:58:43 +08:00
agapple 15e29d276a fixed default xml config 2019-03-28 22:05:56 +08:00
agapple d715ff64b4 fixed packet write batch 2019-03-28 21:52:51 +08:00
agapple f63ce4ffe6 fixed mysql auth 2019-03-28 20:45:16 +08:00
agapple a7f9a779ad fixed issue #1640 , support schema partition 2019-03-28 17:01:36 +08:00
agapple 39aeeb8c58 Merge pull request #1641 from wangsaner/master
修改canal.mq.partitionHash散列规则,使其支持同库下的不同表hash至相同分区
2019-03-28 16:58:31 +08:00
wangsaner 9826948ebb 修改canal.mq.partitionHash散列规则,
当指明pk级做分区(schema.table:pk1^pk2)时,由table.hashCode()^column.getValue().hashCode()改为database.hashCode()^column.getValue().hashCode(),从而支持同库下不同表hash至相同分区
2019-03-28 16:01:02 +08:00
wangsaner d6ef66a281 Merge pull request #1 from alibaba/master
merge
2019-03-28 10:37:24 +08:00
agapple 97640b717b fixed compiler 2019-03-28 10:23:33 +08:00
agapple 20e7becfe9 fixed format 2019-03-28 10:19:53 +08:00
agapple 99b34ff632 Merge pull request #1639 from wangsaner/master
fixed issue #1636
2019-03-28 10:17:42 +08:00
wangsaner 739963acd0 fixed issue #1636
非flat模式,当MQ主题为多分区时,数据拆分只取了for循环里面的最后一份数据,
导致消费端只能消费到一个分区的数据
2019-03-27 21:57:04 +08:00
agapple 0ec4699149 fixed format & support Mysql Auth Switch Protocol 2019-03-27 11:54:40 +08:00
mcy c101ae2280 Merge branch 'master' of https://github.com/rewerma/canal 2019-03-26 11:39:41 +08:00
mcy a137deeaff 使用统一log 2019-03-26 11:29:52 +08:00
rewerma c5be5b7a8a Merge pull request #40 from alibaba/master
merge
2019-03-26 00:43:11 +08:00
agapple 95bed6bcf6 fixed fastsql 2019-03-25 19:50:02 +08:00
agapple 661d119265 fixed issue #1625, mq dymaic topic match 2019-03-25 17:54:35 +08:00
agapple 08167c95c7 Merge pull request #1622 from rewerma/master
fix bugs
2019-03-22 18:14:14 +08:00
mcy 37c5168a7d code optimization 2019-03-22 14:49:50 +08:00
mcy 579fa72ae2 kafka consumer connector引入事务隔离级别 fix #1512 2019-03-22 13:44:02 +08:00
rewerma 844c019433 Merge pull request #39 from alibaba/master
merge
2019-03-22 09:10:43 +08:00
agapple c94e3a0024 fixed default properties 2019-03-21 20:59:09 +08:00
agapple f6ca489984 Merge pull request #1620 from rewerma/master
fix bugs
2019-03-21 20:51:10 +08:00
agapple b2b69307f9 fixed rocketmq 2019-03-21 20:50:52 +08:00
agapple 531b535228 fixed bug 2019-03-21 20:26:00 +08:00
mcy 99cccd2113 fix #1608 2019-03-21 20:07:06 +08:00
rewerma 38d8c3d462 Merge branch 'master' into master 2019-03-21 19:42:07 +08:00
agapple 024ffc2aa2 fixed mq partition num priority 2019-03-21 19:39:21 +08:00
agapple a81d2710ff fixed MQ partition 2019-03-21 19:27:39 +08:00
mcy 4bad0e88f8 增加日志配置 fix #1570 2019-03-21 19:04:33 +08:00
mcy 0181dfaa42 sql解析对case when 的支持
fix #1598
2019-03-21 18:35:58 +08:00
rewerma 0411d99e7b Merge pull request #37 from alibaba/master
merge
2019-03-21 18:07:41 +08:00
agapple 8fcdd7f798 Merge pull request #1619 from rewerma/master
fix #1608
2019-03-21 17:58:57 +08:00
mcy b0f4e4ee78 fix #1608 2019-03-21 17:55:35 +08:00
agapple d448455ebf Merge pull request #1613 from KangZhiDong/master
set utf8mb4 to support emoji
2019-03-21 16:49:44 +08:00
agapple e07fdb6426 Update README.md 2019-03-21 16:46:42 +08:00
agapple 38336b249e fixed travis 2019-03-21 16:36:41 +08:00
agapple e7b81cd83a fixed mvn wrapper 2019-03-21 16:30:05 +08:00
agapple 2810e7e815 add mvnw 2019-03-21 16:20:13 +08:00
agapple 024986a195 fixed travlis.yml 2019-03-21 16:16:31 +08:00
kangzhidong 9b2ad0eafd set utf8mb4 to support emoji 2019-03-20 16:29:53 +08:00
康智冬 03d46f150f Merge pull request #2 from alibaba/master
update
2019-03-20 16:21:38 +08:00
agapple b488ca02d8 fixed travlis 2019-03-20 13:55:29 +08:00
agapple 17a6ba9c9c fixed travlis 2019-03-20 13:44:29 +08:00
agapple 3b618740d4 Merge pull request #1607 from rewerma/master
ES同步新增父子文档索引适配
2019-03-20 13:35:25 +08:00
mcy 06bfcda275 code optimization 2019-03-20 10:41:51 +08:00
mcy 5821c1f606 modify 2019-03-20 09:10:15 +08:00
mcy a2360efff9 增加对sql字段 `` 的支持
fix #1559
2019-03-19 19:13:15 +08:00
mcy 51c469aa6f es etl 主键类型bug fix #1559 2019-03-19 17:54:11 +08:00
mcy 6b3bf31404 adapter代码整理优化 2019-03-19 17:49:00 +08:00
mcy 941ed7f60d Merge branch 'feature/es2' 2019-03-18 09:06:58 +08:00
rewerma 9db2bd291b Merge pull request #36 from alibaba/master
merge
2019-03-18 09:05:47 +08:00
agapple 95b26c4d1d fixed #1433, optimzier code 2019-03-17 10:35:16 +08:00
agapple b114e0cb15 Merge pull request #1581 from alexinea/master
CanalSharp has transferred to .NET Core Community
2019-03-17 10:30:03 +08:00
agapple 5c96bf282e Merge pull request #1590 from rewerma/master
fix #1585
2019-03-17 10:29:12 +08:00
agapple 6d280d1048 Merge pull request #1597 from KangZhiDong/master
fix the error log does not contain table name
2019-03-17 10:28:47 +08:00
agapple cf4db1625d Merge pull request #1605 from lizhyocx/master
解决单核服务器无法订阅数据的bug
2019-03-17 10:27:50 +08:00
lizhyocx df98fec945 解决单核服务器无法订阅数据的bug
解决单核服务器c中anal.instance.parser.parallel = true时无法订阅数据的bug
2019-03-15 17:11:14 +08:00
mcy e03705e211 es父子文档索引ETL支持 2019-03-13 17:19:08 +08:00
mcy 4df60a0fb5 es增加父子文档索引同步适配 2019-03-13 16:57:23 +08:00
kangzhidong 078673c6a9 fix the error log does not contain table name 2019-03-13 16:55:38 +08:00
mcy 4bd91a1079 fix bug 2019-03-13 14:36:27 +08:00
mcy 05ad671cc1 es增加relations配置
配置monitor yml加载使用通用加载类
2019-03-13 14:34:31 +08:00
mcy 43045bc672 use system time zone for es datetime field formatter 2019-03-13 10:01:16 +08:00
mcy 971c9a88c2 modify 2019-03-12 17:07:33 +08:00
rewerma 7b6e482827 Merge pull request #35 from alibaba/master
merge
2019-03-12 16:58:05 +08:00
mcy ad39559f8c fix #1585 2019-03-12 16:56:59 +08:00
AlexLEWIS 2707c12570 CanalSharp has transferred to .NET Core Community
CanalSharp has transferred to .NET Core Community(NCC) and became a member project of NCC.
2019-03-11 14:47:14 +08:00
康智冬 7dab5ac6c2 Merge pull request #1 from alibaba/master
update
2019-03-08 10:05:20 +08:00
agapple 9d4b61f083 Merge pull request #1567 from dragontree101/miss_producergroup_properties
producerGroup配置没有读取
2019-03-06 11:07:15 +08:00
dragonlong1986 a34f9ae97b Merge branch 'master' into miss_producergroup_properties 2019-03-05 17:44:35 +08:00
dragonlong1986 64ca62b986 update 2019-03-05 17:39:56 +08:00
agapple 14c1af4c80 Merge pull request #1542 from rewerma/master
fixed #1538
2019-03-04 14:13:14 +08:00
agapple 7436d9f06f Merge pull request #1550 from lee123lee123/master
Issues #1460  修改tsdb中同步数据带表情字符错误问题
2019-03-04 14:12:33 +08:00
agapple c4599df1c2 Merge pull request #1560 from dragontree101/doc_mistake_key
key改成大写,小写不会生效
2019-03-04 14:07:22 +08:00
dragonlong1986 7abf50f233 key改成大写,小写不会生效 2019-03-02 10:25:19 +08:00
machey 1781381187 modify 2019-03-01 00:19:47 +08:00
machey 5d4210bd22 modify 2019-02-28 23:31:07 +08:00
machey 9888fae559 负时区的bug 2019-02-28 23:22:02 +08:00
rewerma 81ab7e9783 Merge branch 'master' into master 2019-02-28 19:37:27 +08:00
mcy b891afa50e 增加时区判断 2019-02-28 19:28:17 +08:00
lee123lee123 8187d7c5df Issues #1460 修改utf8数据带表情字符错误 2019-02-28 19:04:14 +08:00
mcy e7f069850f fixed #1538 2019-02-28 17:29:06 +08:00
agapple d377a92a5d fixed issue #1548, NPE 2019-02-28 10:52:28 +08:00
agapple f664af1af4 fixed issue #1546 , support drop database 2019-02-28 10:47:42 +08:00
agapple 5e2eabb3c2 Merge pull request #1544 from finefuture/MQ-fix-pr
fix issue 1532
2019-02-28 10:32:41 +08:00
agapple f2368e5469 Merge pull request #1540 from wurongjun/master
修复当适配器mapping文件修改时,无法生效的问题
2019-02-28 10:32:07 +08:00
TomlongTK fd4c8db92b fix issue 1532
因为binlog日志同表名大小写的原因导致kafka的日志恢复并resize,报错之后引起集群崩溃
现在修改为投递消息的topic不区分表名大小写
2019-02-26 14:23:29 +08:00
mcy f21abc13af fixed #1538 2019-02-26 10:23:38 +08:00
wurongjun@duiba.com.cn ae474d67ad 修复当适配器mapping文件修改时,无法生效的问题 2019-02-25 17:53:30 +08:00
bigbro 0c4711ab62 Merge pull request #2 from alibaba/master
合并原项目最新代码
2019-02-25 17:51:19 +08:00
agapple 072420c32b fixed 2019-02-25 15:18:04 +08:00
agapple 1473a4a6b5 Merge pull request #1523 from rewerma/master
es adapter新增upsert属性
2019-02-25 14:50:58 +08:00
agapple 0fff514876 Merge pull request #1533 from DaiZZZZZ/master
CanalParameter添加parallel参数,用来配置canalinstance
2019-02-25 14:45:54 +08:00
agapple 4017de5102 Merge pull request #1526 from jiuchengspotorg/master
adapter rdb无法同步ddl更新问题修正
2019-02-25 14:15:47 +08:00
mcy fcc1fc78ae fix #1521 2019-02-22 17:09:50 +08:00
mcy 17efc2ada0 es 批量提交修改 2019-02-22 16:56:58 +08:00
zhikuodai a68bd28852 CanalParameter类添加parallel参数 2019-02-21 22:04:06 +08:00
daizzzzz 1de8b27da1 Merge pull request #3 from alibaba/master
merge
2019-02-21 17:25:55 +08:00
mcy 01dc8ce2b8 YmlConfigBinder代码整理 2019-02-20 15:00:48 +08:00
jiucheng f7e490f7a0 1、adapter/rdb,从库表字段(varchar,char,text,longtext)非空约束时,无法同步问题修正
2、主库存在0000-00-00 00:00:00时间时,从库表字段是非空约束时,无法同步问题修正
2019-02-20 10:10:14 +08:00
jiucheng 8cadd585c6 adapter rdb无法同步ddl更新问题修正
adapter/support/MessageUtil.java
2019-02-20 09:49:15 +08:00
mcy 57c3226e56 Merge branch 'feature/es2' 2019-02-19 15:05:59 +08:00
rewerma 79a3961da1 Merge pull request #34 from alibaba/master
merge
2019-02-19 15:05:34 +08:00
agapple 62338a945e Merge pull request #1496 from rewerma/master
远程配置代码整理
2019-02-18 21:08:02 +08:00
agapple e07db7b178 Merge pull request #1507 from i404/master
修复 FlatMessage  Hash 到不正确的分区的Bug。
2019-02-18 21:05:03 +08:00
mcy 05cc627b5d fix rdb etl bug 2019-02-15 11:39:26 +08:00
i404 44beaf1916 修复当 canal.mq.flatMessage = true 时,MQMessageUtils::messagePartition 数据分区错误的 bug 2019-02-14 18:26:01 +08:00
mcy 476244b26d modify 2019-02-14 15:41:27 +08:00
mcy cfdf509f9a 增加对pk的支持 2019-02-14 15:29:27 +08:00
mcy c14ed724ff update中增加upsert判断 2019-02-13 15:59:46 +08:00
mcy 5a4cfeab46 Merge branch 'master' into feature/es2
# Conflicts:
#	client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/support/ESTemplate.java
2019-02-13 15:51:24 +08:00
mcy 947cf130ab fixed #1504 2019-02-13 15:50:11 +08:00
mcy 0ef21e4155 代码整理 2019-02-13 15:47:48 +08:00
mcy 83b760494d ES adapter代码重新整理
新增upsert属性
2019-02-13 15:19:09 +08:00
mcy aba8df699c Merge branch 'master' into feature/es2
# Conflicts:
#	client-adapter/elasticsearch/src/main/java/com/alibaba/otter/canal/client/adapter/es/config/SchemaItem.java
2019-02-12 17:21:42 +08:00
rewer 0c5f5c128f 整理代码 2019-02-07 13:49:35 +08:00
mcy ad62275cd7 增加kafka自定义properties 2019-02-05 19:20:36 +08:00
mcy f6d03bcb37 fixed #1490 2019-02-05 17:48:08 +08:00
mcy 5284aa9793 fix bugs 2019-02-01 16:20:53 +08:00
mcy 10276e254a mq模式下adapter配置增加groupId属性对应 2019-02-01 15:18:16 +08:00
mcy 597a08ac37 通过groupId过滤对应的adapter配置 2019-02-01 13:51:49 +08:00
mcy 8211ef35e6 修改yml配置读取绑定,遵循spring yml加载方式,支持environment级占位符替换 2019-02-01 10:11:52 +08:00
mcy a20960834b 远程配置代码整理 2019-01-31 09:45:52 +08:00
rewerma 7b6b332463 Merge pull request #33 from alibaba/master
merge
2019-01-31 09:03:37 +08:00
agapple 43f869599d Merge pull request #1484 from liulangwa/master
捕获"rs = query("select @@global.binlog_checksum")" 结果rs为null的异常
2019-01-30 11:45:33 +08:00
agapple e3d05f6201 Merge pull request #1482 from wurongjun/master
修复从数据库加载配置写入文件时因目录不存在出现的问题
2019-01-30 11:43:47 +08:00
codebee 032301102c 捕获"rs = query("select @@global.binlog_checksum")" 结果rs为null的异常 2019-01-29 17:10:26 +08:00
wurongjun@duiba.com.cn bb64d84298 修复从数据库加载配置写入文件时因目录不存在出现的问题 2019-01-29 16:10:26 +08:00
bigbro ccd39e9d62 Merge pull request #1 from alibaba/master
update
2019-01-29 16:03:43 +08:00
agapple 32031da9db Merge pull request #1475 from rewerma/master
整理远程配置, 统一扩展接口
2019-01-29 10:13:03 +08:00
mcy 9a2e890f57 整理adapter端远程配置, 统一扩展接口 2019-01-25 18:50:30 +08:00
mcy 8f76b33214 解析配置修改 2019-01-25 17:51:38 +08:00
mcy a7467539c7 整理server端远程配置, 统一扩展接口 2019-01-25 17:50:50 +08:00
agapple db5495d158 Merge pull request #1464 from KangZhiDong/master
修改注释
2019-01-25 10:41:40 +08:00
kangzhidong b04d948064 修改注释 2019-01-23 11:58:07 +08:00
agapple 5f8eb4d261 Merge pull request #1456 from rewerma/master
新增kafka消息投递事务
2019-01-21 23:45:13 +08:00
mcy 197ca67c8b 测试调整 2019-01-21 09:56:35 +08:00
machey 851637611f modify 2019-01-20 15:02:06 +08:00
machey 4ebf17f57b 代码调整 2019-01-20 14:30:20 +08:00
machey 2f214ce3a6 采用事务批量异步投递kafka flatMessage
fixed #1454
2019-01-20 14:20:06 +08:00
agapple 8d4fbb395b fixed issue #1451 , fixed NPE 2019-01-18 11:00:02 +08:00
agapple d935c39648 Merge pull request #1449 from wdlily/master
fixed a word
2019-01-18 10:24:01 +08:00
wdlily 6e0e3fbef1 fixed a word
change '租' to '组'
2019-01-17 15:52:24 +08:00
rewerma cbe88af2e3 Merge pull request #1442 from rewerma/master
fixed #1430
2019-01-17 15:18:40 +08:00
mcy ba9d9dea6d fixed #1433 2019-01-17 11:27:12 +08:00
mcy 2d88d803ee merge #1440 and fixed #1430 2019-01-17 10:52:27 +08:00
rewerma ff1fef1eb6 Merge pull request #32 from alibaba/master
merge
2019-01-17 10:48:23 +08:00
rewerma 4beb1e6855 Merge branch 'master' into master 2019-01-17 10:48:14 +08:00
rewerma d2bbe9b061 Merge pull request #1440 from wurongjun/master
fix #1430
2019-01-17 10:46:30 +08:00
rewerma 171bedf488 Merge pull request #31 from alibaba/master
merge
2019-01-17 10:43:49 +08:00
mcy e387cfdee5 fixed #1430 2019-01-17 10:22:08 +08:00
wurongjun@duiba.com.cn 47d7914304 停机时清空zk客户端信息,修复重启时报出的zk连接已关闭的异常 2019-01-16 17:28:51 +08:00
agapple e4177ab0ce Merge pull request #1419 from rewerma/master
specified mq dynamic topic
2019-01-16 10:27:25 +08:00
mcy 739f3b4fa1 fixed #1433 2019-01-15 18:39:32 +08:00
mcy 60441d8b31 fixed #1396 2019-01-15 18:20:05 +08:00
mcy ca41be8412 fixed #1420 2019-01-15 11:38:00 +08:00
mcy 18b304f99d specified mq dynamic topic 2019-01-14 14:25:47 +08:00
agapple 7f9dce4b77 Merge pull request #1418 from rewerma/master
fixed #1416
2019-01-14 13:10:32 +08:00
mcy f26adff7c7 fixed #1416 2019-01-14 11:16:33 +08:00
mcy 77646c0185 拼写错误 2019-01-14 10:58:33 +08:00
agapple 7e4912cd2e Merge pull request #1413 from wingerx/master
获取TableMeta信息时,增加一次retry操作
2019-01-12 11:26:30 +08:00
agapple 70a4f0363d Merge pull request #1412 from rewerma/master
fixed #1408 fixed # 1403
2019-01-12 11:26:11 +08:00
winger 725960d46e 获取TableMeta信息时,增加一次retry操作 2019-01-11 14:00:15 +08:00
mcy b00d347eb0 rdb adapter 增加 truncate dml适配 fixed #1408
fixed #1403
2019-01-11 13:40:37 +08:00
rewerma 92666b41e8 Merge pull request #30 from alibaba/master
merge
2019-01-11 12:59:01 +08:00
agapple f1753e79f9 Merge pull request #1407 from lileibuaa/master
fix filter transaction entry logic in EntryEventSink
2019-01-11 10:29:06 +08:00
lileibuaa f1a998b2d5 fix filter transaction entry logic in EntryEventSink 2019-01-10 21:16:25 +08:00
agapple 03e6662ce5 fixed mq 2019-01-08 23:57:16 +08:00
agapple b845a67666 Merge pull request #1391 from rewerma/master
fixed 1390
2019-01-08 23:42:57 +08:00
mcy 690222bde8 dynamic topic 正则注释 2019-01-08 11:28:29 +08:00
mcy 63ad80c8c4 fixed 1390 2019-01-08 10:39:32 +08:00
859 changed files with 42565 additions and 8259 deletions
+6
View File
@@ -0,0 +1,6 @@
coverage:
status:
# pull-requests only
patch:
default:
threshold: 0.1%
@@ -0,0 +1,8 @@
---
name: Any Questions on Canal
about: Ask whatever you want to know or confusion about Canal
---
## Question
<!-- You can ask any question about this project -->
@@ -0,0 +1,29 @@
---
name: Bug Report for Canal
about: Create a report to help us improve
---
- [ ] I have searched the [issues](https://github.com/alibaba/canal/issues) of this repository and believe that this is not a duplicate.
- [ ] I have checked the [FAQ](https://github.com/alibaba/canal/wiki/FAQ) of this repository and believe that this is not a duplicate.
### environment
* canal version
* mysql version
### Issue Description
### Steps to reproduce
### Expected behaviour
### Actual behaviour
If there is an exception, please attach the exception trace:
```
Just put your stack trace here!
```
@@ -0,0 +1,16 @@
---
name: Feature Request of Canal
about: Suggest an idea for this project
---
## Why you need it?
<!-- Is your feature request related to a problem? Please describe in details -->
## How it could be?
<!--A clear and concise description of what you want to happen. For a computer fan, you can explain more about input of the feature, and output of it.-->
## Other related information
<!-- Add any other context or screenshots about the feature request here.-->
-13
View File
@@ -1,13 +0,0 @@
### environment
* canal version
* mysql version
### Issue Description
### Steps to reproduce
### Expected behaviour
### Actual behaviour
+7
View File
@@ -16,3 +16,10 @@ jtester.properties
.DS_Store
*.tar.gz
*.rpm
client-adapter/example/
*.dat
conf/
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/
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip
+23
View File
@@ -0,0 +1,23 @@
language: java
sudo: false # faster builds
jdk:
- oraclejdk12
- openjdk11
- oraclejdk11
- openjdk8
cache:
directories:
- $HOME/.m2
install: true
script:
- travis_wait 30 ./mvnw clean install -DskipTests=false -Dcheckstyle.skip=true -Dmaven.javadoc.skip=true
after_success:
- bash <(curl -s https://codecov.io/bash)
after_failure:
- if [ -f canal.log ]; then echo "------TAIL of canal.log------"; tail -n 1000 canal.log; echo "------END of canal.log------"; fi
+84 -111
View File
@@ -1,126 +1,99 @@
<div class="blog_content">
<div class="iteye-blog-content-contain">
<h1>背景</h1>
<p style="font-size: 14px;"> 早期,阿里巴巴B2B公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于trigger的方式获取增量变更,不过从2010年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&amp;消费的业务,从此开启了一段新纪元。</p>
<p style="font-size: 14px;"> ps. 目前内部版本已经支持mysql和oracle部分版本的日志解析,当前的canal开源版本支持5.7及以下的版本(阿里内部mysql 5.7.13, 5.6.10, mysql 5.5.18和5.1.40/48)</p>
<p style="font-size: 14px;"> </p>
<p style="font-size: 14px;">基于日志增量订阅&amp;消费支持的业务:</p>
<ol style="font-size: 14px;">
<li>数据库镜像</li>
<li>数据库实时备份</li>
<li>多级索引 (卖家和买家各自分库索引)</li>
<li>search build</li>
<li>业务cache刷新</li>
<li>价格变化等重要业务消息</li>
</ol>
<h1>项目介绍</h1>
<p style="font-size: 14px;"> 名称:canal [kə'næl]</p>
<p style="font-size: 14px;"> 译意: 水道/管道/沟渠 </p>
<p style="font-size: 14px;"> 语言: 纯java开发</p>
<p style="font-size: 14px;"> 定位: 基于数据库增量日志解析,提供增量数据订阅&amp;消费,目前主要支持了mysql</p>
<p style="font-size: 14px;"> 关键词: mysql binlog parser / real-time / queue&topic </p>
<p style="font-size: 14px;"> </p>
<h2>工作原理</h2>
<h3 style="font-size: 14px;">mysql主备复制实现</h3>
<p><img src="http://dl.iteye.com/upload/attachment/0080/3086/468c1a14-e7ad-3290-9d3d-44ac501a7227.jpg" alt=""><br> 从上层来看,复制分成三步:
<ol>
<li>master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看);</li>
<li>slave将master的binary log events拷贝到它的中继日志(relay log)</li>
<li>slave重做中继日志中的事件,将改变反映它自己的数据。</li>
</ol>
<h3>canal的工作原理:</h3>
<p><img width="590" src="http://dl.iteye.com/upload/attachment/0080/3107/c87b67ba-394c-3086-9577-9db05be04c95.jpg" alt="" height="273">
<p>原理相对比较简单:</p>
<ol>
<li>canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议</li>
<li>mysql master收到dump请求,开始推送binary log给slave(也就是canal)</li>
<li>canal解析binary log对象(原始为byte流)</li>
</ol>
[![build status](https://travis-ci.com/alibaba/canal.svg?branch=master)](https://travis-ci.com/alibaba/canal)
[![codecov](https://codecov.io/gh/alibaba/canal/branch/master/graph/badge.svg)](https://codecov.io/gh/alibaba/canal)
![maven](https://img.shields.io/maven-central/v/com.alibaba.otter/canal.svg)
![license](https://img.shields.io/github/license/alibaba/canal.svg)
[![average time to resolve an issue](http://isitmaintained.com/badge/resolution/alibaba/canal.svg)](http://isitmaintained.com/project/alibaba/canal "average time to resolve an issue")
[![percentage of issues still open](http://isitmaintained.com/badge/open/alibaba/canal.svg)](http://isitmaintained.com/project/alibaba/canal "percentage of issues still open")
<h1>重要版本更新说明</h1>
## 简介
canal 1.1.x系列,参考release文档:<a href="https://github.com/alibaba/canal/releases">版本发布信息</a>
![](https://img-blog.csdnimg.cn/20191104101735947.png)
1. 整体性能测试&优化,提升了150%. #726 参考: 【[Performance](https://github.com/alibaba/canal/wiki/Performance)】
2. 原生支持prometheus监控 #765 【[Prometheus QuickStart](https://github.com/alibaba/canal/wiki/Prometheus-QuickStart)】
3. 原生支持kafka消息投递 #695 【[Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart)】
4. 原生支持aliyun rds的binlog订阅 (解决自动主备切换/oss binlog离线解析) 参考: 【[Aliyun RDS QuickStart](https://github.com/alibaba/canal/wiki/aliyun-RDS-QuickStart)】
5. 原生支持docker镜像 #801 参考: 【[Docker QuickStart](https://github.com/alibaba/canal/wiki/Docker-QuickStart)】
**canal [kə'næl]**,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费
<h1>相关文档</h1>
早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。
See the wiki page for : <a href="https://github.com/alibaba/canal/wiki" >wiki文档</a>
基于日志增量订阅和消费的业务包括
- 数据库镜像
- 数据库实时备份
- 索引构建和实时维护(拆分异构索引、倒排索引等)
- 业务 cache 刷新
- 带业务逻辑的增量数据处理
<h3><a name="table-of-contents" class="anchor" href="#table-of-contents"><span class="mini-icon mini-icon-link"></span></a>wiki文档列表</h3>
<ul>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/Home">Home</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/Introduction">Introduction</a></li>
<li>
<a class="internal present" href="https://github.com/alibaba/canal/wiki/QuickStart">QuickStart</a>
<ul>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/Docker-QuickStart">Docker QuickStart</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart">Canal Kafka/RocketMQ QuickStart</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/aliyun-RDS-QuickStart">Aliyun RDS QuickStart</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/Prometheus-QuickStart">Prometheus QuickStart</a></li>
</ul>
</li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/AdminGuide">AdminGuide</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/ClientExample">ClientExample</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/ClientAPI">ClientAPI</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/Performance">Performance</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/DevGuide">DevGuide</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/BinlogChange%28mysql5.6%29">BinlogChange(Mysql5.6)</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/BinlogChange%28MariaDB%29">BinlogChange(MariaDB)</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/TableMetaTSDB">TableMetaTSDB</a></li>
<li><a href="http://alibaba.github.com/canal/release.html">ReleaseNotes</a></li>
<li><a href="https://github.com/alibaba/canal/releases">Download</a></li>
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/FAQ">FAQ</a></li>
</ul>
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x
<h1>多语言业务</h1>
## 工作原理
1. canal整体交互协议设计上使用了protobuf3.0,理论上可以支持绝大部分的多语言场景,欢迎大家提交多客户端的PR
* canal java客户端: <a href="https://github.com/alibaba/canal/wiki/ClientExample"> https://github.com/alibaba/canal/wiki/ClientExample </a>
* canal c#客户端开源项目地址<a href="https://github.com/CanalClient/CanalSharp"> https://github.com/CanalSharp/CanalSharp </a>
* canal go客户端开源项目地址:<a href="https://github.com/CanalClient/canal-go"> https://github.com/CanalClient/canal-go </a>
2. canal作为MySQL binlog的增量获取工具,可以将数据投递到MQ系统中,比如Kafka/RocketMQ,可以借助于MQ的多语言能力
* 参考文档: [Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart)
#### MySQL主备复制原理
![](http://dl.iteye.com/upload/attachment/0080/3086/468c1a14-e7ad-3290-9d3d-44ac501a7227.jpg)
<h1>相关资料</h1>
- MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
- MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
* ADC阿里技术嘉年华分享ppt (放在google docs上,可能需要翻墙): <a href="https://docs.google.com/presentation/d/1MkszUPYRDkfVPz9IqOT1LLT5d9tuwde_WC8GZvjaDRg/edit?usp=sharing">ppt下载</href>
* [与阿里巴巴的RocketMQ配合使用](https://github.com/apache/RocketMQ)
#### canal 工作原理
<h1>相关开源</h1>
<ol>
<li>阿里巴巴分布式数据库同步系统(解决中美异地机房)<a href="http://github.com/alibaba/otter">http://github.com/alibaba/otter</a></li>
<li>阿里巴巴去Oracle数据迁移同步工具(目标支持MySQL/DRDS)<a href="http://github.com/alibaba/yugong">http://github.com/alibaba/yugong</a></li>
</ol>
- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
- canal 解析 binary log 对象(原始为 byte 流)
<h1>相关产品</h1>
<ol>
<li><a href="https://www.aliyun.com/product/drds?spm=5176.55326.cloudEssentials.71.69fd227dRPZj9K">阿里云分布式数据库DRDS</a></li>
<li><a href="https://www.aliyun.com/product/dts?spm=5176.7947010.cloudEssentials.80.33f734f4JOAxSP">阿里云数据传输服务DTS</a></li>
<li><a href="https://www.aliyun.com/product/dbs?spm=5176.54487.cloudEssentials.83.34b851a8GmVZg6">阿里云数据库备份服务DBS</a></li>
<li><a href="https://www.aliyun.com/product/dms?spm=5176.169464.cloudEssentials.81.2e1066feC1sBBL">阿里云数据管理服务DMS</a></li>
</ol>
## 重要版本更新说明
<h1>问题反馈</h1>
<ol>
<li>qq交流群: 161559791 </li>
<li>邮件交流: jianghang115@gmail.com </li>
<li>新浪微博: agapple0002 </li>
<li>报告issue<a href="https://github.com/alibaba/canal/issues">issues</a></li>
</ol>
1. canal 1.1.x 版本([release_note](https://github.com/alibaba/canal/releases)),性能与功能层面有较大的突破,重要提升包括:
<h3>最新更新</h3>
<ol>
<li>canal发布重大版本更新1.1.0,具体releaseNode参考:<a href="https://github.com/alibaba/canal/releases/tag/canal-1.1.0">https://github.com/alibaba/canal/releases/tag/canal-1.1.0</a></li>
<li>canal c#客户端开源项目地址<a href="https://github.com/CanalClient/CanalSharp"> https://github.com/CanalClient/CanalSharp </a>,推荐! </li>
<li>canal QQ讨论群已经建立,群号:161559791 ,欢迎加入进行技术讨论。</li>
<li>canal消费端项目开源: Otter(分布式数据库同步系统),地址:<a href="https://github.com/alibaba/otter">https://github.com/alibaba/otter</a></li>
- 整体性能测试&优化,提升了150%. #726 参考: [Performance](https://github.com/alibaba/canal/wiki/Performance)
- 原生支持prometheus监控 #765 [Prometheus QuickStart](https://github.com/alibaba/canal/wiki/Prometheus-QuickStart)
- 原生支持kafka消息投递 #695 [Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-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)
<li>Canal已在阿里云推出商业化版本 <a href="https://www.aliyun.com/product/dts?spm=a2c4g.11186623.cloudEssentials.80.srdwr7">数据传输服务DTS</a>, 开通即用,免去部署维护的昂贵使用成本。DTS针对阿里云RDS、DRDS等产品进行了适配,解决了Binlog日志回收,主备切换、VPC网络切换等场景下的订阅高可用问题。同时,针对RDS进行了针对性的性能优化。出于稳定性、性能及成本的考虑,强烈推荐阿里云用户使用DTS产品。<a href="https://help.aliyun.com/document_detail/26592.html?spm=a2c4g.11174283.6.539.t1Y91E">DTS产品使用文档</a></li>
DTS支持阿里云RDS&DRDS的Binlog日志实时订阅,现推出首月免费体验,限时限量,<a href="https://common-buy.aliyun.com/?commodityCode=dtspre&request=%7b%22dts_function%22%3a%22data_subscribe%22%7d">立即体验>>></a>
</ol>
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)
- [Introduction](https://github.com/alibaba/canal/wiki/Introduction)
- [QuickStart](https://github.com/alibaba/canal/wiki/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")
- [Aliyun RDS for MySQL QuickStart](https://github.com/alibaba/canal/wiki/aliyun-RDS-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)
- [ClientExample](https://github.com/alibaba/canal/wiki/ClientExample)
- [ClientAPI](https://github.com/alibaba/canal/wiki/ClientAPI)
- [Performance](https://github.com/alibaba/canal/wiki/Performance)
- [DevGuide](https://github.com/alibaba/canal/wiki/DevGuide)
- [BinlogChange(MySQL 5.6)](https://github.com/alibaba/canal/wiki/BinlogChange%28mysql5.6%29)
- [BinlogChange(MariaDB)](https://github.com/alibaba/canal/wiki/BinlogChange%28MariaDB%29)
- [TableMetaTSDB](https://github.com/alibaba/canal/wiki/TableMetaTSDB)
- [ReleaseNotes](http://alibaba.github.com/canal/release.html)
- [Download](https://github.com/alibaba/canal/releases)
- [FAQ](https://github.com/alibaba/canal/wiki/FAQ)
## 多语言
canal 特别设计了 client-server 模式,交互协议使用 protobuf 3.0 , client 端可采用不同语言实现不同的消费逻辑,欢迎大家提交 pull request
- canal java 客户端: [https://github.com/alibaba/canal/wiki/ClientExample](https://github.com/alibaba/canal/wiki/ClientExample)
- canal c# 客户端: [https://github.com/dotnetcore/CanalSharp](https://github.com/dotnetcore/CanalSharp)
- 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 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 作为 MySQL binlog 增量获取和解析工具,可将变更记录投递到 MQ 系统中,比如 Kafka/RocketMQ,可以借助于 MQ 的多语言能力
- 参考文档: [Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart)
## 相关开源
- [canal 消费端开源项目: Otter](http://github.com/alibaba/otter)
- [阿里巴巴去 Oracle 数据迁移同步工具: yugong](http://github.com/alibaba/yugong)
## 问题反馈
- 报告 issue: [github issues](https://github.com/alibaba/canal/issues)
+14
View File
@@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
+14
View File
@@ -0,0 +1,14 @@
# just a flag
ENV = 'development'
# base api
VUE_APP_BASE_API = 'http://127.0.0.1:8089/api/v1'
# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
# to control whether the babel-plugin-dynamic-import-node plugin is enabled.
# It only does one thing by converting all import() to require().
# This configuration can significantly increase the speed of hot updates,
# when you have a large number of pages.
# Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
VUE_CLI_BABEL_TRANSPILE_MODULES = true
+6
View File
@@ -0,0 +1,6 @@
# just a flag
ENV = 'production'
# base api
VUE_APP_BASE_API = '/api/v1'
+8
View File
@@ -0,0 +1,8 @@
NODE_ENV = production
# just a flag
ENV = 'staging'
# base api
VUE_APP_BASE_API = '/stage-api'
+4
View File
@@ -0,0 +1,4 @@
build/*.js
src/assets
public
dist
+198
View File
@@ -0,0 +1,198 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true,
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
// add your custom rules here
//it is base on https://github.com/vuejs/eslint-config-vue
rules: {
"vue/max-attributes-per-line": [2, {
"singleline": 10,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"],
"vue/no-v-html": "off",
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ["error", "always", {"null": "ignore"}],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}
+17
View File
@@ -0,0 +1,17 @@
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
tests/**/coverage/
target/
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
+5
View File
@@ -0,0 +1,5 @@
language: node_js
node_js: 10
script: npm run test
notifications:
email: false
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017-present PanJiaChen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}
+35
View File
@@ -0,0 +1,35 @@
const { run } = require('runjs')
const chalk = require('chalk')
const config = require('../vue.config.js')
const rawArgv = process.argv.slice(2)
const args = rawArgv.join(' ')
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
const report = rawArgv.includes('--report')
run(`vue-cli-service build ${args}`)
const port = 9526
const publicPath = config.publicPath
var connect = require('connect')
var serveStatic = require('serve-static')
const app = connect()
app.use(
publicPath,
serveStatic('./target/dist', {
index: ['index.html', '/']
})
)
app.listen(port, function () {
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
if (report) {
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
}
})
} else {
run(`vue-cli-service build ${args}`)
}
+24
View File
@@ -0,0 +1,24 @@
module.exports = {
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: ['jest-serializer-vue'],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
coverageDirectory: '<rootDir>/tests/unit/coverage',
// 'collectCoverage': true,
'coverageReporters': [
'lcov',
'text-summary'
],
testURL: 'http://localhost/'
}
+66
View File
@@ -0,0 +1,66 @@
import Mock from 'mockjs'
import { param2Obj } from '../src/utils'
import user from './user'
import table from './table'
const mocks = [
...user,
...table
]
// for front mock
// please use it cautiously, it will redefine XMLHttpRequest,
// which will cause many of your third-party libraries to be invalidated(like progress event).
export function mockXHR() {
// mock patch
// https://github.com/nuysoft/Mock/issues/300
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
Mock.XHR.prototype.send = function() {
if (this.custom.xhr) {
this.custom.xhr.withCredentials = this.withCredentials || false
if (this.responseType) {
this.custom.xhr.responseType = this.responseType
}
}
this.proxy_send(...arguments)
}
function XHR2ExpressReqWrap(respond) {
return function(options) {
let result = null
if (respond instanceof Function) {
const { body, type, url } = options
// https://expressjs.com/en/4x/api.html#req
result = respond({
method: type,
body: JSON.parse(body),
query: param2Obj(url)
})
} else {
result = respond
}
return Mock.mock(result)
}
}
for (const i of mocks) {
Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
}
}
// for mock server
const responseFake = (url, type, respond) => {
return {
url: new RegExp(`/mock${url}`),
type: type || 'get',
response(req, res) {
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
}
}
}
export default mocks.map(route => {
return responseFake(route.url, route.type, route.response)
})
+68
View File
@@ -0,0 +1,68 @@
const chokidar = require('chokidar')
const bodyParser = require('body-parser')
const chalk = require('chalk')
const path = require('path')
const mockDir = path.join(process.cwd(), 'mock')
function registerRoutes(app) {
let mockLastIndex
const { default: mocks } = require('./index.js')
for (const mock of mocks) {
app[mock.type](mock.url, mock.response)
mockLastIndex = app._router.stack.length
}
const mockRoutesLength = Object.keys(mocks).length
return {
mockRoutesLength: mockRoutesLength,
mockStartIndex: mockLastIndex - mockRoutesLength
}
}
function unregisterRoutes() {
Object.keys(require.cache).forEach(i => {
if (i.includes(mockDir)) {
delete require.cache[require.resolve(i)]
}
})
}
module.exports = app => {
// es6 polyfill
require('@babel/register')
// parse app.body
// https://expressjs.com/en/4x/api.html#req.body
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
extended: true
}))
const mockRoutes = registerRoutes(app)
var mockRoutesLength = mockRoutes.mockRoutesLength
var mockStartIndex = mockRoutes.mockStartIndex
// watch files, hot reload mock server
chokidar.watch(mockDir, {
ignored: /mock-server/,
ignoreInitial: true
}).on('all', (event, path) => {
if (event === 'change' || event === 'add') {
try {
// remove mock routes stack
app._router.stack.splice(mockStartIndex, mockRoutesLength)
// clear routes cache
unregisterRoutes()
const mockRoutes = registerRoutes(app)
mockRoutesLength = mockRoutes.mockRoutesLength
mockStartIndex = mockRoutes.mockStartIndex
console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
} catch (error) {
console.log(chalk.redBright(error))
}
}
})
}
+29
View File
@@ -0,0 +1,29 @@
import Mock from 'mockjs'
const data = Mock.mock({
'items|30': [{
id: '@id',
title: '@sentence(10, 20)',
'status|1': ['published', 'draft', 'deleted'],
author: 'name',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
}]
})
export default [
{
url: '/table/list',
type: 'get',
response: config => {
const items = data.items
return {
code: 20000,
data: {
total: items.length,
items: items
}
}
}
}
]
+84
View File
@@ -0,0 +1,84 @@
const tokens = {
admin: {
token: 'admin-token'
},
editor: {
token: 'editor-token'
}
}
const users = {
'admin-token': {
roles: ['admin'],
introduction: 'I am a super administrator',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Super Admin'
},
'editor-token': {
roles: ['editor'],
introduction: 'I am an editor',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Normal Editor'
}
}
export default [
// user login
{
url: '/user/login',
type: 'post',
response: config => {
const { username } = config.body
const token = tokens[username]
// mock error
if (!token) {
return {
code: 60204,
message: 'Account and password are incorrect.'
}
}
return {
code: 20000,
data: token
}
}
},
// get user info
{
url: '/user/info\.*',
type: 'get',
response: config => {
const { token } = config.query
const info = users[token]
// mock error
if (!info) {
return {
code: 50008,
message: 'Login failed, unable to get user details.'
}
}
return {
code: 20000,
data: info
}
}
},
// user logout
{
url: '/user/logout',
type: 'post',
response: _ => {
return {
code: 20000,
data: 'success'
}
}
}
]
BIN
View File
Binary file not shown.
+65
View File
@@ -0,0 +1,65 @@
{
"name": "canal-admin-ui",
"version": "1.1.4",
"description": "A canal-admin ui with Element UI & axios & iconfont & permission control & lint",
"author": "Machengyuan <rewerma@163.com>",
"license": "MIT",
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
},
"dependencies": {
"axios": "0.18.1",
"element-ui": "2.7.2",
"js-cookie": "2.2.0",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"vue": "2.6.10",
"vue-router": "3.0.6",
"vuex": "3.1.0"
},
"devDependencies": {
"@babel/core": "7.0.0",
"@babel/register": "7.0.0",
"@vue/cli-plugin-babel": "3.6.0",
"@vue/cli-plugin-eslint": "^3.9.1",
"@vue/cli-plugin-unit-jest": "3.6.3",
"@vue/cli-service": "3.6.0",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "^9.5.1",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "10.0.1",
"babel-jest": "23.6.0",
"chalk": "2.4.2",
"connect": "3.6.6",
"eslint": "5.15.3",
"eslint-plugin-vue": "5.2.2",
"html-webpack-plugin": "3.2.0",
"mockjs": "1.0.1-beta3",
"node-sass": "^4.9.0",
"runjs": "^4.3.2",
"sass-loader": "^7.1.0",
"script-ext-html-webpack-plugin": "2.1.3",
"script-loader": "0.7.2",
"serve-static": "^1.13.2",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.2",
"vue-template-compiler": "2.6.10",
"vue2-ace-editor": "^0.0.13"
},
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}
+73
View File
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<parent>
<artifactId>canal.admin</artifactId>
<groupId>com.alibaba.otter</groupId>
<version>1.1.5-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>canal.admin-ui</artifactId>
<name>canal admin-ui module for otter ${project.version}</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<frontend-maven-plugin.version>1.6</frontend-maven-plugin.version>
</properties>
<profiles>
<profile>
<id>npm</id>
<activation>
<property>
<name>env</name>
<value>npm</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>${frontend-maven-plugin.version}</version>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v9.11.1</nodeVersion>
</configuration>
</execution>
<!-- Install all project dependencies -->
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<!-- optional: default phase is "generate-resources" -->
<phase>generate-resources</phase>
<!-- Optional configuration which provides for running any npm command -->
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<!-- Build and minify static files -->
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
+8
View File
@@ -0,0 +1,8 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
'plugins': {
// to edit target browsers: use "browserslist" field in package.json
'autoprefixer': {}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

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

After

Width:  |  Height:  |  Size: 21 KiB

+11
View File
@@ -0,0 +1,11 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
+46
View 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
View 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'
})
}
+81
View File
@@ -0,0 +1,81 @@
import request from '@/utils/request'
export function getCanalInstances(params) {
return request({
url: '/canal/instances',
method: 'get',
params: params
})
}
export function canalInstanceDetail(id) {
return request({
url: '/canal/instance/' + id,
method: 'get'
})
}
export function updateCanalInstance(data) {
return request({
url: '/canal/instance',
method: 'put',
data
})
}
export function addCanalInstance(data) {
return request({
url: '/canal/instance',
method: 'post',
data
})
}
export function deleteCanalInstance(id) {
return request({
url: '/canal/instance/' + id,
method: 'delete'
})
}
export function startInstance(id, nodeId) {
return request({
url: '/canal/instance/start/' + id + '/' + nodeId,
method: 'put'
})
}
export function stopInstance(id, nodeId) {
return request({
url: '/canal/instance/stop/' + id + '/' + nodeId,
method: 'put'
})
}
export function instanceLog(id, nodeId) {
return request({
url: '/canal/instance/log/' + id + '/' + nodeId,
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'
})
}
+60
View File
@@ -0,0 +1,60 @@
import request from '@/utils/request'
export function getNodeServers(params) {
return request({
url: '/nodeServers',
method: 'get',
params: params
})
}
export function addNodeServer(data) {
return request({
url: '/nodeServer',
method: 'post',
data
})
}
export function nodeServerDetail(id) {
return request({
url: '/nodeServer/' + id,
method: 'get'
})
}
export function updateNodeServer(data) {
return request({
url: '/nodeServer',
method: 'put',
data
})
}
export function deleteNodeServer(id) {
return request({
url: '/nodeServer/' + id,
method: 'delete'
})
}
export function startNodeServer(id) {
return request({
url: '/nodeServer/start/' + id,
method: 'put'
})
}
export function stopNodeServer(id) {
return request({
url: '/nodeServer/stop/' + id,
method: 'put'
})
}
export function nodeServerLog(id) {
return request({
url: '/nodeServer/log/' + id,
method: 'get'
})
}
+9
View File
@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getList(params) {
return request({
url: '/table/list',
method: 'get',
params
})
}
+32
View File
@@ -0,0 +1,32 @@
import request from '@/utils/request'
export function login(data) {
return request({
url: '/user/login',
method: 'post',
data
})
}
export function getInfo(token) {
return request({
url: '/user/info',
method: 'get',
params: { token }
})
}
export function logout() {
return request({
url: '/user/logout',
method: 'post'
})
}
export function updateUser(data) {
return request({
url: '/user',
method: 'put',
data
})
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -0,0 +1,78 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
import pathToRegexp from 'path-to-regexp'
export default {
data() {
return {
levelList: null
}
},
watch: {
$route() {
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{ path: '/dashboard', meta: { title: '主页' }}].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
},
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.pathCompile(path))
}
}
}
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>
@@ -0,0 +1,44 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script>
export default {
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
}
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>
@@ -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>
@@ -0,0 +1,62 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>
+9
View File
@@ -0,0 +1,9 @@
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon'// svg component
// register globally
Vue.component('svg-icon', SvgIcon)
const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)
@@ -0,0 +1 @@
<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>

After

Width:  |  Height:  |  Size: 497 B

@@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>

After

Width:  |  Height:  |  Size: 944 B

+1
View File
@@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>

After

Width:  |  Height:  |  Size: 285 B

+1
View File
@@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>

After

Width:  |  Height:  |  Size: 821 B

@@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>

After

Width:  |  Height:  |  Size: 623 B

+1
View File
@@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>

After

Width:  |  Height:  |  Size: 597 B

+1
View File
@@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+1
View File
@@ -0,0 +1 @@
<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>

After

Width:  |  Height:  |  Size: 440 B

+22
View File
@@ -0,0 +1,22 @@
# replace default config
# multipass: true
# full: true
plugins:
# - name
#
# or:
# - name: false
# - name: true
#
# or:
# - name:
# param1: 1
# param2: 2
- removeAttrs:
attrs:
- 'fill'
- 'fill-rule'
@@ -0,0 +1,40 @@
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<router-view :key="key" />
</transition>
</section>
</template>
<script>
export default {
name: 'AppMain',
computed: {
key() {
return this.$route.path
}
}
}
</script>
<style scoped>
.app-main {
/*50 = navbar */
min-height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
}
.fixed-header+.app-main {
padding-top: 50px;
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>
@@ -0,0 +1,142 @@
<template>
<div class="navbar">
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb class="breadcrumb-container" />
<div class="right-menu">
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<!-- avatar+'?imageView2/1/w/80/h/80' -->
<img :src="avat+'?imageView2/1/w/80/h/80'" class="user-avatar">
<i class="el-icon-caret-bottom" />
</div>
<el-dropdown-menu slot="dropdown" class="user-dropdown">
<router-link to="/">
<el-dropdown-item>
主页
</el-dropdown-item>
</router-link>
<router-link to="/sys/user">
<el-dropdown-item>账号管理</el-dropdown-item>
</router-link>
<el-dropdown-item divided>
<span style="display:block;" @click="logout">退出</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
export default {
components: {
Breadcrumb,
Hamburger
},
data() {
return {
avat: './avatar.gif'
}
},
computed: {
...mapGetters([
'sidebar',
'avatar'
])
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
async logout() {
await this.$store.dispatch('user/logout')
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
}
}
</script>
<style lang="scss" scoped>
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color:transparent;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
.breadcrumb-container {
float: left;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
}
.avatar-container {
margin-right: 30px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
</style>
@@ -0,0 +1,26 @@
export default {
computed: {
device() {
return this.$store.state.app.device
}
},
mounted() {
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
// https://github.com/PanJiaChen/vue-element-admin/issues/1135
this.fixBugIniOS()
},
methods: {
fixBugIniOS() {
const $subMenu = this.$refs.subMenu
if ($subMenu) {
const handleMouseleave = $subMenu.handleMouseleave
$subMenu.handleMouseleave = (e) => {
if (this.device === 'mobile') {
return
}
handleMouseleave(e)
}
}
}
}
}
@@ -0,0 +1,29 @@
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const { icon, title } = context.props
const vnodes = []
if (icon) {
vnodes.push(<svg-icon icon-class={icon}/>)
}
if (title) {
vnodes.push(<span slot='title'>{(title)}</span>)
}
return vnodes
}
}
</script>
@@ -0,0 +1,36 @@
<template>
<!-- eslint-disable vue/require-component-is -->
<component v-bind="linkProps(to)">
<slot />
</component>
</template>
<script>
import { isExternal } from '@/utils/validate'
export default {
props: {
to: {
type: String,
required: true
}
},
methods: {
linkProps(url) {
if (isExternal(url)) {
return {
is: 'a',
href: url,
target: '_blank',
rel: 'noopener'
}
}
return {
is: 'router-link',
to: url
}
}
}
}
</script>
@@ -0,0 +1,82 @@
<template>
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo">
<h1 v-else class="sidebar-title">{{ title }} </h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo">
<h1 class="sidebar-title">{{ title }} </h1>
</router-link>
</transition>
</div>
</template>
<script>
export default {
name: 'SidebarLogo',
props: {
collapse: {
type: Boolean,
required: true
}
},
data() {
return {
title: '',
logo: './logo.png'
}
}
}
</script>
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
transition: opacity 1.5s;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
opacity: 0;
}
.sidebar-logo-container {
position: relative;
width: 100%;
height: 50px;
line-height: 50px;
background: #2b2f3a;
text-align: center;
overflow: hidden;
& .sidebar-logo-link {
height: 100%;
width: 100%;
& .sidebar-logo {
width: 55px;
// height: 32px;
vertical-align: middle;
margin-right: 12px;
}
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
}
}
&.collapse {
.sidebar-logo {
margin-right: 0px;
}
}
}
</style>
@@ -0,0 +1,95 @@
<template>
<div v-if="!item.hidden" class="menu-wrapper">
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
// TODO: refactor with render function
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>
@@ -0,0 +1,56 @@
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'sidebar'
]),
routes() {
return this.$router.options.routes
},
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>
@@ -0,0 +1,3 @@
export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
export { default as AppMain } from './AppMain'
+93
View File
@@ -0,0 +1,93 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" />
<div class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar />
</div>
<app-main />
</div>
</div>
</template>
<script>
import { Navbar, Sidebar, AppMain } from './components'
import ResizeMixin from './mixin/ResizeHandler'
export default {
name: 'Layout',
components: {
Navbar,
Sidebar,
AppMain
},
mixins: [ResizeMixin],
computed: {
sidebar() {
return this.$store.state.app.sidebar
},
device() {
return this.$store.state.app.device
},
fixedHeader() {
return this.$store.state.settings.fixedHeader
},
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar{
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px)
}
.mobile .fixed-header {
width: 100%;
}
</style>
@@ -0,0 +1,45 @@
import store from '@/store'
const { body } = document
const WIDTH = 992 // refer to Bootstrap's responsive design
export default {
watch: {
$route(route) {
if (this.device === 'mobile' && this.sidebar.opened) {
store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
},
beforeMount() {
window.addEventListener('resize', this.$_resizeHandler)
},
beforeDestroy() {
window.removeEventListener('resize', this.$_resizeHandler)
},
mounted() {
const isMobile = this.$_isMobile()
if (isMobile) {
store.dispatch('app/toggleDevice', 'mobile')
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_isMobile() {
const rect = body.getBoundingClientRect()
return rect.width - 1 < WIDTH
},
$_resizeHandler() {
if (!document.hidden) {
const isMobile = this.$_isMobile()
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
if (isMobile) {
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
}
}
}
}
+44
View File
@@ -0,0 +1,44 @@
import Vue from 'vue'
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import locale from 'element-ui/lib/locale/lang/en' // lang i18n
import '@/styles/index.scss' // global css
import App from './App'
import store from './store'
import router from './router'
import '@/icons' // icon
import '@/permission' // permission control
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online! ! !
*/
// import { mockXHR } from '../mock'
// if (process.env.NODE_ENV === 'production') {
// mockXHR()
// }
// set ElementUI lang to EN
Vue.use(ElementUI, {
size: 'small',
locale
})
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
+64
View File
@@ -0,0 +1,64 @@
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
const hasGetUserInfo = store.getters.name
if (hasGetUserInfo) {
next()
} else {
try {
// get user info
await store.dispatch('user/getInfo')
next()
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
+153
View File
@@ -0,0 +1,153 @@
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/**
* Note: sub-menu only appear when route children.length >= 1
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
*
* hidden: true if set true, item will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu
* if not set alwaysShow, when item has more than one children route,
* it will becomes nested mode, otherwise not show the root menu
* redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
roles: ['admin','editor'] control the page roles (you can set multiple roles)
title: 'title' the name show in sidebar and breadcrumb (recommend set)
icon: 'svg-name' the icon show in the sidebar
breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
}
*/
/**
* constantRoutes
* a base page that does not have permission requirements
* all roles can be accessed
*/
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: '主页',
component: () => import('@/views/dashboard/index'),
meta: { title: '主页', icon: 'dashboard' }
}],
hidden: true
},
{
path: '/sys',
component: Layout,
redirect: '/user',
children: [{
path: 'user',
name: '用户信息',
component: () => import('@/views/sys/UserInfo'),
meta: { title: '用户信息' }
}],
hidden: true
},
{
path: '/canalServer',
component: Layout,
redirect: '/canalServer/nodeServers',
name: 'Canal Server',
meta: { title: 'Canal Server', icon: 'example' },
children: [
{
path: 'canalClusters',
name: 'Canal 集群管理',
component: () => import('@/views/canalServer/CanalCluster'),
meta: { title: '集群管理', icon: 'tree' }
},
{
path: 'nodeServers',
name: 'Server 状态',
component: () => import('@/views/canalServer/NodeServer'),
meta: { title: 'Server 管理', icon: 'form' }
},
{
path: 'nodeServer/config',
name: 'Server 配置',
component: () => import('@/views/canalServer/CanalConfig'),
meta: { title: 'Server 配置' },
hidden: true
},
{
path: 'canalInstances',
name: 'Instance 管理',
component: () => import('@/views/canalServer/CanalInstance'),
meta: { title: 'Instance 管理', icon: 'nested' }
},
{
path: 'canalInstance/add',
name: '新建Instance配置',
component: () => import('@/views/canalServer/CanalInstanceAdd'),
meta: { title: '新建Instance配置' },
hidden: true
},
{
path: 'canalInstance/modify',
name: '修改Instance配置',
component: () => import('@/views/canalServer/CanalInstanceUpdate'),
meta: { title: '修改Instance配置' },
hidden: true
},
{
path: 'nodeServer/log',
name: 'Server 日志',
component: () => import('@/views/canalServer/CanalLogDetail'),
meta: { title: 'Server 日志' },
hidden: true
},
{
path: 'canalInstance/log',
name: 'Instance 日志',
component: () => import('@/views/canalServer/CanalInstanceLogDetail'),
meta: { title: 'Instance 日志' },
hidden: true
}
]
},
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
+16
View File
@@ -0,0 +1,16 @@
module.exports = {
title: 'Canal Admin',
/**
* @type {boolean} true | false
* @description Whether fix the header
*/
fixedHeader: true,
/**
* @type {boolean} true | false
* @description Whether show the logo in sidebar
*/
sidebarLogo: true
}
+8
View File
@@ -0,0 +1,8 @@
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name
}
export default getters
+19
View File
@@ -0,0 +1,19 @@
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
settings,
user
},
getters
})
export default store
+48
View File
@@ -0,0 +1,48 @@
import Cookies from 'js-cookie'
const state = {
sidebar: {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
withoutAnimation: false
},
device: 'desktop'
}
const mutations = {
TOGGLE_SIDEBAR: state => {
state.sidebar.opened = !state.sidebar.opened
state.sidebar.withoutAnimation = false
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0)
}
},
CLOSE_SIDEBAR: (state, withoutAnimation) => {
Cookies.set('sidebarStatus', 0)
state.sidebar.opened = false
state.sidebar.withoutAnimation = withoutAnimation
},
TOGGLE_DEVICE: (state, device) => {
state.device = device
}
}
const actions = {
toggleSideBar({ commit }) {
commit('TOGGLE_SIDEBAR')
},
closeSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
toggleDevice({ commit }, device) {
commit('TOGGLE_DEVICE', device)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
@@ -0,0 +1,31 @@
import defaultSettings from '@/settings'
const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
const state = {
showSettings: showSettings,
fixedHeader: fixedHeader,
sidebarLogo: sidebarLogo
}
const mutations = {
CHANGE_SETTING: (state, { key, value }) => {
if (state.hasOwnProperty(key)) {
state[key] = value
}
}
}
const actions = {
changeSetting({ commit }, data) {
commit('CHANGE_SETTING', data)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
+90
View File
@@ -0,0 +1,90 @@
import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'
const state = {
token: getToken(),
name: '',
avatar: ''
}
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
}
}
const actions = {
// user login
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// get user info
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
reject('Verification failed, please Login again.')
}
const { name, avatar } = data
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
resolve(data)
}).catch(error => {
reject(error)
})
})
},
// user logout
logout({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
removeToken()
resetRouter()
resolve()
}).catch(error => {
reject(error)
})
})
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
+44
View File
@@ -0,0 +1,44 @@
// cover some element-ui styles
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
}
.el-upload {
input[type="file"] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
// to fixed https://github.com/ElemeFE/element/issues/2461
.el-dialog {
transform: none;
left: 0;
position: relative;
margin: 0 auto;
}
// refine element ui upload
.upload-container {
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
}
// dropdown
.el-dropdown-menu {
a {
display: block
}
}
+74
View File
@@ -0,0 +1,74 @@
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app {
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
div:focus {
outline: none;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
// main-container global css
.app-container {
padding: 20px;
}
.filter-container {
padding-bottom: 10px;
.filter-item {
display: inline-block;
vertical-align: middle;
margin-bottom: 10px;
}
}
+28
View File
@@ -0,0 +1,28 @@
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}
+209
View File
@@ -0,0 +1,209 @@
#app {
.main-container {
min-height: 100%;
transition: margin-left .28s;
margin-left: $sideBarWidth;
position: relative;
}
.sidebar-container {
transition: width 0.28s;
width: $sideBarWidth !important;
background-color: $menuBg;
height: 100%;
position: fixed;
font-size: 0px;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
// reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
}
.el-scrollbar__bar.is-vertical {
right: 0px;
}
.el-scrollbar {
height: 100%;
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 50px);
}
}
.is-horizontal {
display: none;
}
a {
display: inline-block;
width: 100%;
overflow: hidden;
}
.svg-icon {
margin-right: 16px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
// menu hover
.submenu-title-noDropdown,
.el-submenu__title {
&:hover {
background-color: $menuHover !important;
}
}
.is-active>.el-submenu__title {
color: $subMenuActiveText !important;
}
& .nest-menu .el-submenu>.el-submenu__title,
& .el-submenu .el-menu-item {
min-width: $sideBarWidth !important;
background-color: $subMenuBg !important;
&:hover {
background-color: $subMenuHover !important;
}
}
}
.hideSidebar {
.sidebar-container {
width: 54px !important;
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
}
}
.el-submenu {
overflow: hidden;
&>.el-submenu__title {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.el-submenu__icon-arrow {
display: none;
}
}
}
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.el-menu--collapse .el-menu .el-submenu {
min-width: $sideBarWidth !important;
}
// mobile responsive
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform .28s;
width: $sideBarWidth !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-$sideBarWidth, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
// when menu collapsed
.el-menu--vertical {
&>.el-menu {
.svg-icon {
margin-right: 16px;
}
}
.nest-menu .el-submenu>.el-submenu__title,
.el-menu-item {
&:hover {
// you can use $subMenuHover
background-color: $menuHover !important;
}
}
// the scroll bar appears when the subMenu is too long
>.el-menu--popup {
max-height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
}
+48
View File
@@ -0,0 +1,48 @@
// global transition css
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all .5s;
}
.breadcrumb-leave-active {
position: absolute;
}
+25
View File
@@ -0,0 +1,25 @@
// sidebar
$menuText:#bfcbd9;
$menuActiveText:#409EFF;
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
$menuBg:#304156;
$menuHover:#263445;
$subMenuBg:#1f2d3d;
$subMenuHover:#001528;
$sideBarWidth: 210px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
menuText: $menuText;
menuActiveText: $menuActiveText;
subMenuActiveText: $subMenuActiveText;
menuBg: $menuBg;
menuHover: $menuHover;
subMenuBg: $subMenuBg;
subMenuHover: $subMenuHover;
sideBarWidth: $sideBarWidth;
}
+15
View File
@@ -0,0 +1,15 @@
import Cookies from 'js-cookie'
const TokenKey = 'canal_admin_token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token, { maxAge: 0 })
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
@@ -0,0 +1,10 @@
import defaultSettings from '@/settings'
const title = defaultSettings.title || 'Vue Admin Template'
export default function getPageTitle(pageTitle) {
if (pageTitle) {
return `${pageTitle} - ${title}`
}
return `${title}`
}
+110
View File
@@ -0,0 +1,110 @@
/**
* Created by PanJiaChen on 16/11/18.
*/
/**
* Parse the time to string
* @param {(Object|string|number)} time
* @param {string} cFormat
* @returns {string}
*/
export function parseTime(time, cFormat) {
if (arguments.length === 0) {
return null
}
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
time = parseInt(time)
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
})
return time_str
}
/**
* @param {number} time
* @param {string} option
* @returns {string}
*/
export function formatTime(time, option) {
if (('' + time).length === 10) {
time = parseInt(time) * 1000
} else {
time = +time
}
const d = new Date(time)
const now = Date.now()
const diff = (now - d) / 1000
if (diff < 30) {
return '刚刚'
} else if (diff < 3600) {
// less 1 hour
return Math.ceil(diff / 60) + '分钟前'
} else if (diff < 3600 * 24) {
return Math.ceil(diff / 3600) + '小时前'
} else if (diff < 3600 * 24 * 2) {
return '1天前'
}
if (option) {
return parseTime(time, option)
} else {
return (
d.getMonth() +
1 +
'月' +
d.getDate() +
'日' +
d.getHours() +
'时' +
d.getMinutes() +
'分'
)
}
}
/**
* @param {string} url
* @returns {Object}
*/
export function param2Obj(url) {
const search = url.split('?')[1]
if (!search) {
return {}
}
return JSON.parse(
'{"' +
decodeURIComponent(search)
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"')
.replace(/\+/g, ' ') +
'"}'
)
}
+85
View File
@@ -0,0 +1,85 @@
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 60000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['X-Token'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 20000) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// to re-login
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
+50
View 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()
}
+20
View File
@@ -0,0 +1,20 @@
/**
* 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
}
+228
View File
@@ -0,0 +1,228 @@
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
</div>
<div class="bullshit">
<div class="bullshit__oops">OOPS!</div>
<div class="bullshit__info">All rights reserved
<a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
</div>
<div class="bullshit__headline">{{ message }}</div>
<div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
<a href="" class="bullshit__return-home">Back to home</a>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Page404',
computed: {
message() {
return 'The webmaster said that you can not enter this page...'
}
}
}
</script>
<style lang="scss" scoped>
.wscn-http404-container{
transform: translate(-50%,-50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>
@@ -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>
@@ -0,0 +1,120 @@
<template>
<div>
<el-form ref="form" :model="form">
<div style="padding-left: 10px;padding-top: 20px;">
<el-form-item>
{{ form.name }}&nbsp;&nbsp;&nbsp;&nbsp;
<el-button type="primary" @click="onSubmit">保存</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>
</div>
<editor v-model="form.content" lang="properties" theme="chrome" width="100%" :height="800" @init="editorInit" />
</el-form>
</div>
</template>
<script>
import { getCanalConfig, updateCanalConfig, getTemplateConfig } from '@/api/canalConfig'
export default {
components: {
editor: require('vue2-ace-editor')
},
data() {
return {
form: {
id: null,
name: '',
content: '',
serverId: null,
clusterId: null
}
}
},
created() {
this.loadCanalConfig()
},
methods: {
editorInit() {
require('brace/ext/language_tools')
require('brace/mode/html')
require('brace/mode/yaml')
require('brace/mode/properties')
require('brace/mode/javascript')
require('brace/mode/less')
require('brace/theme/chrome')
require('brace/snippets/javascript')
},
loadCanalConfig() {
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
this.form.id = data.id
this.form.name = data.name
this.form.content = data.content
this.form.serverId = this.$route.query.serverId
this.form.clusterId = this.$route.query.clusterId
})
},
onSubmit() {
if (this.form.content === null || this.form.content === '') {
this.$message({
message: '配置内容不能为空',
type: 'error'
})
return
}
this.$confirm(
'修改主配置可能会导致Server重启,是否继续?',
'确定修改',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
updateCanalConfig(this.form).then(response => {
if (response.data === 'success') {
this.$message({
message: '保存成功',
type: 'success'
})
this.loadCanalConfig()
} else {
this.$message({
message: '保存失败',
type: 'error'
})
}
})
})
},
onCancel() {
this.loadCanalConfig()
},
onBack() {
history.go(-1)
},
onLoadTemplate() {
getTemplateConfig().then(res => {
this.form.content = res.data
})
}
}
}
</script>
<style scoped>
.line{
text-align: center;
}
</style>
@@ -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>
&nbsp;&nbsp;
<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>
@@ -0,0 +1,118 @@
<template>
<div>
<el-form ref="form" :model="form">
<div class="filter-container" style="padding-left: 10px;padding-top: 20px;">
<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-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>
</div>
<editor v-model="form.content" lang="properties" theme="chrome" width="100%" :height="800" @init="editorInit" />
</el-form>
</div>
</template>
<script>
import { addCanalInstance, getTemplateInstance } from '@/api/canalInstance'
import { getClustersAndServers } from '@/api/canalCluster'
export default {
components: {
editor: require('vue2-ace-editor')
},
data() {
return {
options: [],
form: {
name: '',
content: '',
clusterServerId: ''
}
}
},
created() {
getClustersAndServers().then((res) => {
this.options = res.data
})
},
methods: {
editorInit() {
require('brace/ext/language_tools')
require('brace/mode/html')
require('brace/mode/yaml')
require('brace/mode/properties')
require('brace/mode/javascript')
require('brace/mode/less')
require('brace/theme/chrome')
require('brace/snippets/javascript')
},
onSubmit() {
if (this.form.name === '') {
this.$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'
})
return
}
this.$confirm(
'确定新建',
'确定新建',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
addCanalInstance(this.form).then(response => {
if (response.data === 'success') {
this.$message({
message: '新建成功',
type: 'success'
})
this.$router.push('/canalServer/canalInstances')
} else {
this.$message({
message: '新建失败',
type: 'error'
})
}
})
})
},
onBack() {
history.go(-1)
},
onLoadTemplate() {
getTemplateInstance().then(res => {
this.form.content = res.data
})
}
}
}
</script>
<style scoped>
.line{
text-align: center;
}
</style>
@@ -0,0 +1,53 @@
<template>
<div>
<el-form ref="form" :model="form">
<div style="padding-left: 10px;padding-right: 10px;padding-top: 20px;">
<el-form-item>
{{ form.instance }}&nbsp;&nbsp;&nbsp;&nbsp;
<el-button type="primary" @click="onRefresh">刷新</el-button>
<el-button type="info" @click="onBack">返回</el-button>
</el-form-item>
<el-input v-model="form.desc" :rows="35" :readonly="'readonly'" type="textarea" />
</div>
</el-form>
</div>
</template>
<script>
import { instanceLog } from '@/api/canalInstance'
export default {
data() {
return {
form: {
instance: '',
desc: ''
}
}
},
created() {
this.fetchData()
},
methods: {
fetchData() {
instanceLog(this.$route.query.id, this.$route.query.nodeId).then(res => {
this.form.instance = res.data.instance + '.log'
this.form.desc = res.data.log
})
},
onRefresh() {
this.fetchData()
},
onBack() {
history.go(-1)
}
}
}
</script>
<style scoped>
.line{
text-align: center;
}
</style>
@@ -0,0 +1,108 @@
<template>
<div>
<el-form ref="form" :model="form">
<div style="padding-left: 10px;padding-top: 20px;">
<el-form-item>
{{ form.name }}&nbsp;&nbsp;&nbsp;&nbsp;
<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="warning" @click="onCancel">重置</el-button>
<el-button type="info" @click="onBack">返回</el-button>
</el-form-item>
</div>
<editor v-model="form.content" lang="properties" theme="chrome" width="100%" :height="800" @init="editorInit" />
</el-form>
</div>
</template>
<script>
import { canalInstanceDetail, updateCanalInstance } from '@/api/canalInstance'
import { getClustersAndServers } from '@/api/canalCluster'
export default {
components: {
editor: require('vue2-ace-editor')
},
data() {
return {
options: [],
form: {
id: null,
name: '',
content: '',
clusterServerId: ''
}
}
},
created() {
this.loadCanalConfig()
getClustersAndServers().then((res) => {
this.options = res.data
})
},
methods: {
editorInit() {
require('brace/ext/language_tools')
require('brace/mode/html')
require('brace/mode/yaml')
require('brace/mode/properties')
require('brace/mode/javascript')
require('brace/mode/less')
require('brace/theme/chrome')
require('brace/snippets/javascript')
},
loadCanalConfig() {
canalInstanceDetail(this.$route.query.id).then(response => {
const data = response.data
this.form.id = data.id
this.form.name = data.name + '/instance.propertios'
this.form.content = data.content
this.form.clusterServerId = data.clusterServerId
})
},
onSubmit() {
this.$confirm(
'修改Instance配置可能会导致重启,是否继续?',
'确定修改',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
updateCanalInstance(this.form).then(response => {
if (response.data === 'success') {
this.$message({
message: '修改成功',
type: 'success'
})
this.loadCanalConfig()
} else {
this.$message({
message: '修改失败',
type: 'error'
})
}
})
})
},
onCancel() {
this.loadCanalConfig()
},
onBack() {
history.go(-1)
}
}
}
</script>
<style scoped>
.line{
text-align: center;
}
</style>
@@ -0,0 +1,51 @@
<template>
<div>
<el-form ref="form" :model="form">
<div style="padding-left: 10px;padding-right: 10px;padding-top: 20px;">
<el-form-item>
canal.log&nbsp;&nbsp;&nbsp;&nbsp;
<el-button type="primary" @click="onRefresh">刷新</el-button>
<el-button type="info" @click="onBack">返回</el-button>
</el-form-item>
<el-input v-model="form.desc" :rows="35" :readonly="'readonly'" type="textarea" />
</div>
</el-form>
</div>
</template>
<script>
import { nodeServerLog } from '@/api/nodeServer'
export default {
data() {
return {
form: {
desc: ''
}
}
},
created() {
this.fetchData()
},
methods: {
fetchData() {
nodeServerLog(this.$route.query.id).then(res => {
this.form.desc = res.data
})
},
onRefresh() {
this.fetchData()
},
onBack() {
history.go(-1)
}
}
}
</script>
<style scoped>
.line{
text-align: center;
}
</style>

Some files were not shown because too many files have changed in this diff Show More