diff --git a/README.md b/README.md index 65ce0332..2ce00450 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ * 下图简单描述了MQCloud大概的功能: - ![mqcloud](mq-cloud/src/main/resources/static/img/intro/mqcloud.png) + ![mqcloud](mq-cloud/src/main/resources/static/wiki/intro/img/mqcloud.png) ---------- @@ -21,47 +21,47 @@ ## 特性概览 * 用户topic列表-不同用户看到不同的topic,管理员可以管理所有topic - ![用户topic列表](mq-cloud/src/main/resources/static/img/intro/index.png) + ![用户topic列表](mq-cloud/src/main/resources/static/wiki/intro/img/index.png) * topic详情-分三块 基本信息,今日流程,拓扑 - ![topic详情](mq-cloud/src/main/resources/static/img/intro/topicDetail.png) + ![topic详情](mq-cloud/src/main/resources/static/wiki/intro/img/topicDetail.png) * 生产详情 - ![生产详情](mq-cloud/src/main/resources/static/img/intro/produceDetail2.png) + ![生产详情](mq-cloud/src/main/resources/static/wiki/intro/img/produceDetail2.png) * 消费详情 - ![消费详情](mq-cloud/src/main/resources/static/img/intro/consumeDetail2.png) + ![消费详情](mq-cloud/src/main/resources/static/wiki/intro/img/consumeDetail2.png) * 某个消费者具体的消费详情-可以查询重试消息和死消息 - ![消费详情](mq-cloud/src/main/resources/static/img/intro/consumeRetry.png) + ![消费详情](mq-cloud/src/main/resources/static/wiki/intro/img/consumeRetry.png) * 消息 - ![消息](mq-cloud/src/main/resources/static/img/intro/msgSearch.png) + ![消息](mq-cloud/src/main/resources/static/wiki/intro/img/msgSearch.png) * 消息消费情况 - ![msgconsume](mq-cloud/src/main/resources/static/img/intro/msgTrack.png) + ![msgconsume](mq-cloud/src/main/resources/static/wiki/intro/img/msgTrack.png) * 集群发现 - ![admin](mq-cloud/src/main/resources/static/img/intro/nameServer.png) + ![admin](mq-cloud/src/main/resources/static/wiki/intro/img/nameServer.png) * 集群管理 - ![admin](mq-cloud/src/main/resources/static/img/intro/cluster.png) + ![admin](mq-cloud/src/main/resources/static/wiki/intro/img/cluster.png) * 集群流量 - ![admin](mq-cloud/src/main/resources/static/img/intro/clusterTraffic.png) + ![admin](mq-cloud/src/main/resources/static/wiki/intro/img/clusterTraffic.png) * 创建broker - ![addBroker](mq-cloud/src/main/resources/static/img/intro/addBroker.png) + ![addBroker](mq-cloud/src/main/resources/static/wiki/intro/img/addBroker.png) ---------- diff --git a/mq-client-common-open/pom.xml b/mq-client-common-open/pom.xml index 11d8100a..c11358b7 100644 --- a/mq-client-common-open/pom.xml +++ b/mq-client-common-open/pom.xml @@ -6,7 +6,7 @@ com.sohu.tv mq - 1.7 + 1.8 mq-client-common-open diff --git a/mq-client-common-open/src/main/java/com/sohu/tv/mq/util/Version.java b/mq-client-common-open/src/main/java/com/sohu/tv/mq/util/Version.java index 4ffeacbc..310a9792 100644 --- a/mq-client-common-open/src/main/java/com/sohu/tv/mq/util/Version.java +++ b/mq-client-common-open/src/main/java/com/sohu/tv/mq/util/Version.java @@ -7,6 +7,6 @@ public class Version { public static String get() { - return "1.7"; + return "1.8"; } } diff --git a/mq-client-open/pom.xml b/mq-client-open/pom.xml index 6a9f56d3..cca2a270 100644 --- a/mq-client-open/pom.xml +++ b/mq-client-open/pom.xml @@ -6,7 +6,7 @@ com.sohu.tv mq - 1.7 + 1.8 mq-client-open diff --git a/mq-cloud-common/pom.xml b/mq-cloud-common/pom.xml index c8b0f57c..246cda40 100644 --- a/mq-cloud-common/pom.xml +++ b/mq-cloud-common/pom.xml @@ -6,7 +6,7 @@ com.sohu.tv mq - 1.7 + 1.8 mq-cloud-common diff --git a/mq-cloud/pom.xml b/mq-cloud/pom.xml index 9ebc696a..f2f9982e 100644 --- a/mq-cloud/pom.xml +++ b/mq-cloud/pom.xml @@ -5,7 +5,7 @@ com.sohu.tv mq - 1.7 + 1.8 mq-cloud @@ -121,6 +121,14 @@ junit test + + + + com.vladsch.flexmark + flexmark + 0.42.12 + + diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ClientVersion.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ClientVersion.java index 1f4713c6..190db6cc 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ClientVersion.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/ClientVersion.java @@ -1,6 +1,8 @@ package com.sohu.tv.mq.cloud.bo; import java.util.Date; +import java.util.HashSet; +import java.util.Set; /** * 客户端版本 * @Description: @@ -8,6 +10,8 @@ * @date 2018年8月31日 */ public class ClientVersion { + public static final int PRODUCER = 1; + public static final int CONSUMER = 2; // topic name private String topic; // producer or consumer @@ -18,6 +22,8 @@ public class ClientVersion { private String version; private Date createDate; private Date updateTime; + // 客户端归属的用户 + private Set owners; public String getTopic() { return topic; } @@ -60,6 +66,32 @@ public Date getUpdateTime() { public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } + public String getOwnersString() { + if(owners == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for(User u : owners) { + sb.append(u.notBlankName()); + sb.append(","); + } + if(sb.length() > 0) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + public boolean addOwner(User owner) { + if(owners == null) { + owners = new HashSet<>(); + } + return owners.add(owner); + } + public Set getOwners() { + return owners; + } + public void setOwners(Set owners) { + this.owners = owners; + } @Override public String toString() { return "ClientVersion [topic=" + topic + ", client=" + client + ", role=" + role + ", version=" + version diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/User.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/User.java index b08b33c8..be90ec40 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/User.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/bo/User.java @@ -129,6 +129,31 @@ public String notBlankName(){ return getName(); } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((email == null) ? 0 : email.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + User other = (User) obj; + if (email == null) { + if (other.email != null) + return false; + } else if (!email.equals(other.email)) + return false; + return true; + } + @Override public String toString() { return "User [id=" + id + ", name=" + name + ", email=" + email + ", mobile=" + mobile + ", type=" + type diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/ServerStatusDao.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/ServerStatusDao.java index 7734d9ca..247c7538 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/ServerStatusDao.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/ServerStatusDao.java @@ -35,7 +35,7 @@ public interface ServerStatusDao { * @return @ServerInfo */ @Select("select * from server s left join server_stat ss on ss.ip = s.ip and ss.cdate=#{cdate} and ss.ctime in " - + "(select max(ctime) from server_stat where ip = s.ip and cdate=#{cdate})") + + "(select max(ctime) from server_stat where cdate=#{cdate})") public List queryAllServer(@Param("cdate") String date); /** diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserDao.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserDao.java index 07305d99..eb04b0f1 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserDao.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/dao/UserDao.java @@ -111,4 +111,26 @@ public interface UserDao { */ @Update("update user set password=#{password} where id = #{uid}") public Integer resetPassword(@Param("uid") long uid, @Param("password") String password); + + /** + * 根据producer批量查询用户 + * @param producerList + * @return List + */ + @Select("") + public List selectByProducerList(@Param("producerList") Collection producerList); + + /** + * 根据consumer批量查询用户 + * @param producerList + * @return List + */ + @Select("") + public List selectByConsumerList(@Param("consumerList") Collection consumerList); } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ClientVersionService.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ClientVersionService.java index a8f728e1..ca19737f 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ClientVersionService.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/service/ClientVersionService.java @@ -1,14 +1,18 @@ package com.sohu.tv.mq.cloud.service; +import java.util.ArrayList; import java.util.List; +import org.apache.commons.collections.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.sohu.tv.mq.cloud.bo.ClientVersion; +import com.sohu.tv.mq.cloud.bo.User; import com.sohu.tv.mq.cloud.dao.ClientVersionDao; +import com.sohu.tv.mq.cloud.dao.UserDao; import com.sohu.tv.mq.cloud.util.Result; /** @@ -25,6 +29,9 @@ public class ClientVersionService { @Autowired private ClientVersionDao clientVersionDao; + @Autowired + private UserDao userDao; + /** * 保存客户端版本 * @param clientVersion @@ -49,10 +56,67 @@ public Result> queryAll(){ List list = null; try { list = clientVersionDao.selectAll(); + setOwners(list); } catch (Exception e) { logger.error("queryAll", e); return Result.getDBErrorResult(e); } return Result.getResult(list); } + + /** + * 设置客户端归属的用户 + * @param list + */ + private void setOwners(List list) { + setOwners(list, ClientVersion.PRODUCER); + setOwners(list, ClientVersion.CONSUMER); + } + + /** + * 设置客户端归属的用户 + * @param list + * @param role + */ + private void setOwners(List list, int role) { + List clientList = getClientList(list, role); + if(CollectionUtils.isEmpty(clientList)) { + return; + } + List userList = null; + if(ClientVersion.PRODUCER == role) { + userList = userDao.selectByProducerList(clientList); + } else { + userList = userDao.selectByConsumerList(clientList); + } + for(ClientVersion cv : list) { + if(cv.getRole() != role) { + continue; + } + for(User user : userList) { + if(cv.getClient().equals(user.getPassword())) { + cv.addOwner(user); + } + } + } + } + + /** + * 获取producer/consumer名字列表 + * @param list + * @param role + * @return + */ + private List getClientList(List list, int role){ + if(list == null) { + return null; + } + List producerList = new ArrayList<>(); + for(ClientVersion cv : list) { + if(role == cv.getRole()) { + producerList.add(cv.getClient()); + } + } + return producerList; + } } diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/MonitorService.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/MonitorService.java index 2cc81d8f..57270578 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/MonitorService.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/MonitorService.java @@ -162,16 +162,29 @@ public void doMonitorWork() throws RemotingException, MQClientException, Interru if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { String consumerGroup = topic.substring(MixAll.RETRY_GROUP_TOPIC_PREFIX.length()); + // 链接在线检测 + ConsumerConnection cc = null; try { - this.reportUndoneMsgs(consumerGroup); + cc = defaultMQAdminExt.examineConsumerConnectionInfo(consumerGroup); } catch (Exception e) { - // log.error("reportUndoneMsgs Exception", e); + if(logger.isDebugEnabled()) { + logger.debug("examineConsumerConnectionInfo consumerGroup:{}, err:{}", consumerGroup, e.getMessage()); + } + } + if(cc == null) { + return; + } + + try { + this.reportUndoneMsgs(consumerGroup, cc); + } catch (Exception e) { + logger.warn("reportUndoneMsgs Exception", e); } try { - this.reportConsumerRunningInfo(consumerGroup); + this.reportConsumerRunningInfo(consumerGroup, cc); } catch (Exception e) { - // log.error("reportConsumerRunningInfo Exception", e); + logger.warn("reportConsumerRunningInfo Exception", e); } } } @@ -180,19 +193,7 @@ public void doMonitorWork() throws RemotingException, MQClientException, Interru logger.info("{} monitor use: {}ms", clusterName, spentTimeMills); } - private void reportUndoneMsgs(final String consumerGroup) { - ConsumerConnection cc = null; - try { - cc = defaultMQAdminExt.examineConsumerConnectionInfo(consumerGroup); - } catch (Exception e) { - if(logger.isDebugEnabled()) { - logger.debug("examineConsumerConnectionInfo consumerGroup:{}, err:{}", consumerGroup, e.getMessage()); - } - return; - } - if(cc == null) { - return; - } + private void reportUndoneMsgs(String consumerGroup, ConsumerConnection cc) { if(cc.getMessageModel() == MessageModel.CLUSTERING) { ConsumeStats cs = null; try { @@ -276,9 +277,8 @@ private void reportUndoneMsgs(final String consumerGroup) { } } - public void reportConsumerRunningInfo(final String consumerGroup) throws InterruptedException, + public void reportConsumerRunningInfo(String consumerGroup, ConsumerConnection cc) throws InterruptedException, MQBrokerException, RemotingException, MQClientException { - ConsumerConnection cc = defaultMQAdminExt.examineConsumerConnectionInfo(consumerGroup); TreeMap infoMap = new TreeMap(); for (Connection c : cc.getConnectionSet()) { String clientId = c.getClientId(); diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/SohuMonitorListener.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/SohuMonitorListener.java index 04963d48..cbae5f19 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/SohuMonitorListener.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/SohuMonitorListener.java @@ -83,13 +83,8 @@ public void beginRound() { public void reportUndoneMsgs(UndoneMsgs undoneMsgs) { String topic = undoneMsgs.getTopic(); // 忽略topic - if(mqCloudConfigHelper.getIgnoreTopic() != null) { - String[] topics = mqCloudConfigHelper.getIgnoreTopic().split(","); - for (int i = 0; i < topics.length; i++) { - if (topic.equals(topics[i])) { - return; - } - } + if(mqCloudConfigHelper.isIgnoreTopic(topic)) { + return; } try { //保存堆积消息的consumer的状态 diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/util/MQCloudConfigHelper.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/util/MQCloudConfigHelper.java index e154adeb..f9eedbe5 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/util/MQCloudConfigHelper.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/util/MQCloudConfigHelper.java @@ -108,6 +108,8 @@ public class MQCloudConfigHelper implements ApplicationEventPublisherAware { private Integer isOpenRegister; // 忽略的topic private String ignoreTopic; + // 忽略的topic + private String[] ignoreTopicArray; // rocketmq安装文件路径 private String rocketmqFilePath; @@ -359,9 +361,22 @@ public void setMailTimeout(int mailTimeout) { public String getIgnoreTopic() { return ignoreTopic; } + + public boolean isIgnoreTopic(String topic) { + if(ignoreTopicArray == null) { + return false; + } + for (int i = 0; i < ignoreTopicArray.length; i++) { + if (topic.equals(ignoreTopicArray[i])) { + return true; + } + } + return false; + } public void setIgnoreTopic(String ignoreTopic) { this.ignoreTopic = ignoreTopic; + this.ignoreTopicArray = ignoreTopic.split(","); } public String getRocketmqFilePath() { diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/WikiController.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/WikiController.java new file mode 100644 index 00000000..6d78d262 --- /dev/null +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/WikiController.java @@ -0,0 +1,88 @@ +package com.sohu.tv.mq.cloud.web.controller; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.sohu.tv.mq.cloud.util.MQCloudConfigHelper; +import com.sohu.tv.mq.cloud.util.Result; +import com.sohu.tv.mq.util.Version; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.ast.Document; + +/** + * wiki + * + * @author yongfeigao + * @date 2019年6月10日 + */ +@Controller +@RequestMapping("/wiki") +public class WikiController { + + @Autowired + private MQCloudConfigHelper mqCloudConfigHelper; + + @RequestMapping("/{path}/{filename}") + public String subPages(@PathVariable String path, @PathVariable String filename, + Map map) throws Exception { + String html = markdown2html(path + "/" + filename, ".md"); + html = html.replace("${clientArtifactId}", mqCloudConfigHelper.getClientArtifactId()); + html = html.replace("${version}", Version.get()); + html = html.replace("${nexusDomain}", mqCloudConfigHelper.getNexusDomain()); + html = html.replace("${producer}", mqCloudConfigHelper.getProducerClass()); + html = html.replace("${consumer}", mqCloudConfigHelper.getConsumerClass()); + Result.setResult(map, html); + + // toc + String toc = markdown2html(path + "/" + filename, ".toc.md"); + if(toc != null) { + map.put("toc", toc); + } + return "wikiTemplate"; + } + + private String markdown2html(String filename, String suffix) throws Exception { + String templatePath = "static/wiki/" + filename + suffix; + InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(templatePath); + if(inputStream == null) { + return null; + } + String markdown = new String(read(inputStream)); + Document document = Parser.builder().build().parse(markdown); + String html = HtmlRenderer.builder().build().render(document); + return html; + } + + private byte[] read(InputStream inputStream) throws IOException { + byte[] buffer = new byte[1024]; + int len = 0; + ByteArrayOutputStream bos = null; + try { + bos = new ByteArrayOutputStream(); + while((len = inputStream.read(buffer)) != -1) { + bos.write(buffer, 0, len); + } + } finally { + if(bos != null) { + try { + bos.close(); + } catch (IOException e) {} + } + if(inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) {} + } + } + return bos.toByteArray(); + } + +} diff --git a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/param/BrokerParam.java b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/param/BrokerParam.java index f3fb969c..6f809b1e 100644 --- a/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/param/BrokerParam.java +++ b/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/web/controller/param/BrokerParam.java @@ -103,7 +103,7 @@ public String toConfig(String nameServerDomain, Cluster cluster) { + "\nfetchNamesrvAddrByAddressServer=true" + "\nautoCreateTopicEnable=false" + "\nclusterTopicEnable=false" - + "\nautoCreateSubscriptionGroup=true" + + "\nautoCreateSubscriptionGroup=false" + "\nslaveReadEnable=true" + "\nstorePathRootDir=" + MQDeployer.MQ_CLOUD_DIR + getDir() + "/data" + "\nstorePathCommitLog=" + MQDeployer.MQ_CLOUD_DIR + getDir() + "/data/commitlog" diff --git a/mq-cloud/src/main/resources/application.yml b/mq-cloud/src/main/resources/application.yml index 1b4f5ec7..2088b42e 100644 --- a/mq-cloud/src/main/resources/application.yml +++ b/mq-cloud/src/main/resources/application.yml @@ -56,7 +56,7 @@ spring: mqcloud: nexusDomain: mqcloud.com clientArtifactId: mq-client-open - producerClass: com.sohu.tv.mq.rocketmq.RocketMQProducer - consumerClass: com.sohu.tv.mq.rocketmq.RocketMQConsumer + producerClass: RocketMQProducer + consumerClass: RocketMQConsumer ticketKey: ticket #cas登录返回后的key,用户名密码可以忽略 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/css/admin.css b/mq-cloud/src/main/resources/static/css/admin.css index ce0534ee..4ec092d6 100644 --- a/mq-cloud/src/main/resources/static/css/admin.css +++ b/mq-cloud/src/main/resources/static/css/admin.css @@ -85,10 +85,13 @@ body { /* * Main content */ - +.admin-row{ + margin: 0px; +} .main { - padding: 20px; - padding-left: 180px; + padding-top: 20px; + padding-left: 161px; + padding-right: 2px; } .main .page-header { diff --git a/mq-cloud/src/main/resources/static/css/githubmd.css b/mq-cloud/src/main/resources/static/css/githubmd.css new file mode 100644 index 00000000..f730888c --- /dev/null +++ b/mq-cloud/src/main/resources/static/css/githubmd.css @@ -0,0 +1,177 @@ +.markdown-body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word +} + +.markdown-body::before { + display: table; + content: "" +} + +.markdown-body::after { + display: table; + clear: both; + content: "" +} + +.markdown-body>*:first-child { + margin-top: 0 !important +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none +} + +.markdown-body .absent { + color: #cb2431 +} + +.markdown-body .anchor { + float: left; + padding-right: 4px; + margin-left: -20px; + line-height: 1 +} + +.markdown-body .anchor:focus { + outline: none +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre { + margin-top: 0; + margin-bottom: 16px +} + +.markdown-body hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0 +} + +.markdown-body blockquote { + padding: 0 1em; + color: #6a737d; + border-left: 0.25em solid #dfe2e5 +} + +.markdown-body blockquote>:first-child { + margin-top: 0 +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0 +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: solid 1px #c6cbd1; + border-bottom-color: #959da5; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #959da5 +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25 +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #1b1f23; + vertical-align: middle; + visibility: hidden +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible +} + +.markdown-body h1 tt, +.markdown-body h1 code, +.markdown-body h2 tt, +.markdown-body h2 code, +.markdown-body h3 tt, +.markdown-body h3 code, +.markdown-body h4 tt, +.markdown-body h4 code, +.markdown-body h5 tt, +.markdown-body h5 code, +.markdown-body h6 tt, +.markdown-body h6 code { + font-size: inherit +} + +.markdown-body h1 { + padding-bottom: 0.3em; + font-size: 2em; + border-bottom: 1px solid #eaecef +} + +.markdown-body h2 { + padding-bottom: 0.3em; + font-size: 1.5em; + border-bottom: 1px solid #eaecef +} + +.markdown-body h3 { + font-size: 1.25em +} + +.markdown-body h4 { + font-size: 1em +} + +.markdown-body h5 { + font-size: 0.875em +} + +.markdown-body h6 { + font-size: 0.85em; + color: #6a737d +} diff --git a/mq-cloud/src/main/resources/static/img/intro/clientStats.png b/mq-cloud/src/main/resources/static/img/intro/clientStats.png deleted file mode 100644 index cf4e3dc5..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/clientStats.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/clientStats_design1.png b/mq-cloud/src/main/resources/static/img/intro/clientStats_design1.png deleted file mode 100644 index 57c2d791..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/clientStats_design1.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/clientStats_design2.png b/mq-cloud/src/main/resources/static/img/intro/clientStats_design2.png deleted file mode 100644 index 82e3689b..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/clientStats_design2.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/cluster.png b/mq-cloud/src/main/resources/static/img/intro/cluster.png deleted file mode 100644 index 29097ac7..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/cluster.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/clusterTraffic.png b/mq-cloud/src/main/resources/static/img/intro/clusterTraffic.png deleted file mode 100644 index 50cf3a8d..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/clusterTraffic.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/consume.png b/mq-cloud/src/main/resources/static/img/intro/consume.png deleted file mode 100644 index ac371a16..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/consume.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/consumeDetail.png b/mq-cloud/src/main/resources/static/img/intro/consumeDetail.png deleted file mode 100644 index 635a2532..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/consumeDetail.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/consumeDetail2.png b/mq-cloud/src/main/resources/static/img/intro/consumeDetail2.png deleted file mode 100644 index aca5d02d..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/consumeDetail2.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/index.png b/mq-cloud/src/main/resources/static/img/intro/index.png deleted file mode 100644 index 2943e826..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/index.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/intro.png b/mq-cloud/src/main/resources/static/img/intro/intro.png deleted file mode 100644 index e71f3017..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/intro.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/msgSearch.png b/mq-cloud/src/main/resources/static/img/intro/msgSearch.png deleted file mode 100644 index 353a1438..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/msgSearch.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/oldUser1.png b/mq-cloud/src/main/resources/static/img/intro/oldUser1.png deleted file mode 100644 index 1bef22d9..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/oldUser1.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/oldUser4.png b/mq-cloud/src/main/resources/static/img/intro/oldUser4.png deleted file mode 100644 index ce8647f4..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/oldUser4.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/produce.png b/mq-cloud/src/main/resources/static/img/intro/produce.png deleted file mode 100644 index acfa189f..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/produce.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/produceDetail2.png b/mq-cloud/src/main/resources/static/img/intro/produceDetail2.png deleted file mode 100644 index f3bba029..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/produceDetail2.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/produceTraffic.png b/mq-cloud/src/main/resources/static/img/intro/produceTraffic.png deleted file mode 100644 index 1b362b98..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/produceTraffic.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/topicDesc.png b/mq-cloud/src/main/resources/static/img/intro/topicDesc.png deleted file mode 100644 index 37c4d48c..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/topicDesc.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/topicDetail.png b/mq-cloud/src/main/resources/static/img/intro/topicDetail.png deleted file mode 100644 index 7b57049f..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/topicDetail.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/topicTraffic.png b/mq-cloud/src/main/resources/static/img/intro/topicTraffic.png deleted file mode 100644 index 6404435a..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/topicTraffic.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/topology.png b/mq-cloud/src/main/resources/static/img/intro/topology.png deleted file mode 100644 index fa6f5909..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/topology.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/trace.png b/mq-cloud/src/main/resources/static/img/intro/trace.png deleted file mode 100644 index 5c677e6e..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/trace.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/trace_after.png b/mq-cloud/src/main/resources/static/img/intro/trace_after.png deleted file mode 100644 index d9c6820a..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/trace_after.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/img/intro/trace_producer.png b/mq-cloud/src/main/resources/static/img/intro/trace_producer.png deleted file mode 100644 index 5db6ebbf..00000000 Binary files a/mq-cloud/src/main/resources/static/img/intro/trace_producer.png and /dev/null differ diff --git a/mq-cloud/src/main/resources/static/js/lineChart.js b/mq-cloud/src/main/resources/static/js/lineChart.js index 13268dc8..fd779743 100644 --- a/mq-cloud/src/main/resources/static/js/lineChart.js +++ b/mq-cloud/src/main/resources/static/js/lineChart.js @@ -44,7 +44,10 @@ function drawLineChart(lineName){ } divComponent.empty(); for(var i = 0; i < data.result.length; ++i){ - var chart = data.result[i] + var chart = data.result[i]; + if ("pstats" == lineName){ + delete chart.subtitle["useHTML"]; + } if(chart.url){ url = chart.url; chart.plotOptions = {}; @@ -100,6 +103,8 @@ function drawLineChart(lineName){ divComponent.append(div); } new Highcharts.Chart(chart); + // 兼容滚动插件 + $("body").getNiceScroll().resize(); } }, 'json'); } diff --git a/mq-cloud/src/main/resources/static/resources/bootstrap-select/js/bootstrap-select.min.js b/mq-cloud/src/main/resources/static/resources/bootstrap-select/js/bootstrap-select.min.js index d4173834..d4997426 100644 --- a/mq-cloud/src/main/resources/static/resources/bootstrap-select/js/bootstrap-select.min.js +++ b/mq-cloud/src/main/resources/static/resources/bootstrap-select/js/bootstrap-select.min.js @@ -5,5 +5,4 @@ * Licensed under MIT (https://github.com/silviomoreto/bootstrap-select/blob/master/LICENSE) */ !function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof module&&module.exports?module.exports=b(require("jquery")):b(a.jQuery)}(this,function(a){!function(a){"use strict";function b(b){var c=[{re:/[\xC0-\xC6]/g,ch:"A"},{re:/[\xE0-\xE6]/g,ch:"a"},{re:/[\xC8-\xCB]/g,ch:"E"},{re:/[\xE8-\xEB]/g,ch:"e"},{re:/[\xCC-\xCF]/g,ch:"I"},{re:/[\xEC-\xEF]/g,ch:"i"},{re:/[\xD2-\xD6]/g,ch:"O"},{re:/[\xF2-\xF6]/g,ch:"o"},{re:/[\xD9-\xDC]/g,ch:"U"},{re:/[\xF9-\xFC]/g,ch:"u"},{re:/[\xC7-\xE7]/g,ch:"c"},{re:/[\xD1]/g,ch:"N"},{re:/[\xF1]/g,ch:"n"}];return a.each(c,function(){b=b?b.replace(this.re,this.ch):""}),b}function c(b){var c=arguments,d=b;[].shift.apply(c);var e,f=this.each(function(){var b=a(this);if(b.is("select")){var f=b.data("selectpicker"),g="object"==typeof d&&d;if(f){if(g)for(var h in g)g.hasOwnProperty(h)&&(f.options[h]=g[h])}else{var i=a.extend({},l.DEFAULTS,a.fn.selectpicker.defaults||{},b.data(),g);i.template=a.extend({},l.DEFAULTS.template,a.fn.selectpicker.defaults?a.fn.selectpicker.defaults.template:{},b.data().template,g.template),b.data("selectpicker",f=new l(this,i))}"string"==typeof d&&(e=f[d]instanceof Function?f[d].apply(f,c):f.options[d])}});return"undefined"!=typeof e?e:f}String.prototype.includes||!function(){var a={}.toString,b=function(){try{var a={},b=Object.defineProperty,c=b(a,a,a)&&b}catch(a){}return c}(),c="".indexOf,d=function(b){if(null==this)throw new TypeError;var d=String(this);if(b&&"[object RegExp]"==a.call(b))throw new TypeError;var e=d.length,f=String(b),g=f.length,h=arguments.length>1?arguments[1]:void 0,i=h?Number(h):0;i!=i&&(i=0);var j=Math.min(Math.max(i,0),e);return!(g+j>e)&&c.call(d,f,i)!=-1};b?b(String.prototype,"includes",{value:d,configurable:!0,writable:!0}):String.prototype.includes=d}(),String.prototype.startsWith||!function(){var a=function(){try{var a={},b=Object.defineProperty,c=b(a,a,a)&&b}catch(a){}return c}(),b={}.toString,c=function(a){if(null==this)throw new TypeError;var c=String(this);if(a&&"[object RegExp]"==b.call(a))throw new TypeError;var d=c.length,e=String(a),f=e.length,g=arguments.length>1?arguments[1]:void 0,h=g?Number(g):0;h!=h&&(h=0);var i=Math.min(Math.max(h,0),d);if(f+i>d)return!1;for(var j=-1;++j":">",'"':""","'":"'","`":"`"},h={"&":"&","<":"<",">":">",""":'"',"'":"'","`":"`"},i=function(a){var b=function(b){return a[b]},c="(?:"+Object.keys(a).join("|")+")",d=RegExp(c),e=RegExp(c,"g");return function(a){return a=null==a?"":""+a,d.test(a)?a.replace(e,b):a}},j=i(g),k=i(h),l=function(b,c){d.useDefault||(a.valHooks.select.set=d._set,d.useDefault=!0),this.$element=a(b),this.$newElement=null,this.$button=null,this.$menu=null,this.$lis=null,this.options=c,null===this.options.title&&(this.options.title=this.$element.attr("title"));var e=this.options.windowPadding;"number"==typeof e&&(this.options.windowPadding=[e,e,e,e]),this.val=l.prototype.val,this.render=l.prototype.render,this.refresh=l.prototype.refresh,this.setStyle=l.prototype.setStyle,this.selectAll=l.prototype.selectAll,this.deselectAll=l.prototype.deselectAll,this.destroy=l.prototype.destroy,this.remove=l.prototype.remove,this.show=l.prototype.show,this.hide=l.prototype.hide,this.init()};l.VERSION="1.12.4",l.DEFAULTS={noneSelectedText:"Nothing selected",noneResultsText:"No results matched {0}",countSelectedText:function(a,b){return 1==a?"{0} item selected":"{0} items selected"},maxOptionsText:function(a,b){return[1==a?"Limit reached ({n} item max)":"Limit reached ({n} items max)",1==b?"Group limit reached ({n} item max)":"Group limit reached ({n} items max)"]},selectAllText:"Select All",deselectAllText:"Deselect All",doneButton:!1,doneButtonText:"Close",multipleSeparator:", ",styleBase:"btn",style:"btn-default",size:"auto",title:null,selectedTextFormat:"values",width:!1,container:!1,hideDisabled:!1,showSubtext:!1,showIcon:!0,showContent:!0,dropupAuto:!0,header:!1,liveSearch:!1,liveSearchPlaceholder:null,liveSearchNormalize:!1,liveSearchStyle:"contains",actionsBox:!1,iconBase:"glyphicon",tickIcon:"glyphicon-ok",showTick:!1,template:{caret:''},maxOptions:!1,mobile:!1,selectOnTab:!1,dropdownAlignRight:!1,windowPadding:0},l.prototype={constructor:l,init:function(){var b=this,c=this.$element.attr("id");this.$element.addClass("bs-select-hidden"),this.liObj={},this.multiple=this.$element.prop("multiple"),this.autofocus=this.$element.prop("autofocus"),this.$newElement=this.createView(),this.$element.after(this.$newElement).appendTo(this.$newElement),this.$button=this.$newElement.children("button"),this.$menu=this.$newElement.children(".dropdown-menu"),this.$menuInner=this.$menu.children(".inner"),this.$searchbox=this.$menu.find("input"),this.$element.removeClass("bs-select-hidden"),this.options.dropdownAlignRight===!0&&this.$menu.addClass("dropdown-menu-right"),"undefined"!=typeof c&&(this.$button.attr("data-id",c),a('label[for="'+c+'"]').click(function(a){a.preventDefault(),b.$button.focus()})),this.checkDisabled(),this.clickListener(),this.options.liveSearch&&this.liveSearchListener(),this.render(),this.setStyle(),this.setWidth(),this.options.container&&this.selectPosition(),this.$menu.data("this",this),this.$newElement.data("this",this),this.options.mobile&&this.mobile(),this.$newElement.on({"hide.bs.dropdown":function(a){b.$menuInner.attr("aria-expanded",!1),b.$element.trigger("hide.bs.select",a)},"hidden.bs.dropdown":function(a){b.$element.trigger("hidden.bs.select",a)},"show.bs.dropdown":function(a){b.$menuInner.attr("aria-expanded",!0),b.$element.trigger("show.bs.select",a)},"shown.bs.dropdown":function(a){b.$element.trigger("shown.bs.select",a)}}),b.$element[0].hasAttribute("required")&&this.$element.on("invalid",function(){b.$button.addClass("bs-invalid"),b.$element.on({"focus.bs.select":function(){b.$button.focus(),b.$element.off("focus.bs.select")},"shown.bs.select":function(){b.$element.val(b.$element.val()).off("shown.bs.select")},"rendered.bs.select":function(){this.validity.valid&&b.$button.removeClass("bs-invalid"),b.$element.off("rendered.bs.select")}}),b.$button.on("blur.bs.select",function(){b.$element.focus().blur(),b.$button.off("blur.bs.select")})}),setTimeout(function(){b.$element.trigger("loaded.bs.select")})},createDropdown:function(){var b=this.multiple||this.options.showTick?" show-tick":"",c=this.$element.parent().hasClass("input-group")?" input-group-btn":"",d=this.autofocus?" autofocus":"",e=this.options.header?'
'+this.options.header+"
":"",f=this.options.liveSearch?'':"",g=this.multiple&&this.options.actionsBox?'
":"",h=this.multiple&&this.options.doneButton?'
":"",i='
";return a(i)},createView:function(){var a=this.createDropdown(),b=this.createLi();return a.find("ul")[0].innerHTML=b,a},reloadLi:function(){var a=this.createLi();this.$menuInner[0].innerHTML=a},createLi:function(){var c=this,d=[],e=0,f=document.createElement("option"),g=-1,h=function(a,b,c,d){return""+a+""},i=function(d,e,f,g){return''+d+''};if(this.options.title&&!this.multiple&&(g--,!this.$element.find(".bs-title-option").length)){var k=this.$element[0];f.className="bs-title-option",f.innerHTML=this.options.title,f.value="",k.insertBefore(f,k.firstChild);var l=a(k.options[k.selectedIndex]);void 0===l.attr("selected")&&void 0===this.$element.data("selected")&&(f.selected=!0)}var m=this.$element.find("option");return m.each(function(b){var f=a(this);if(g++,!f.hasClass("bs-title-option")){var k,l=this.className||"",n=j(this.style.cssText),o=f.data("content")?f.data("content"):f.html(),p=f.data("tokens")?f.data("tokens"):null,q="undefined"!=typeof f.data("subtext")?''+f.data("subtext")+"":"",r="undefined"!=typeof f.data("icon")?' ':"",s=f.parent(),t="OPTGROUP"===s[0].tagName,u=t&&s[0].disabled,v=this.disabled||u;if(""!==r&&v&&(r=""+r+""),c.options.hideDisabled&&(v&&!t||u))return k=f.data("prevHiddenIndex"),f.next().data("prevHiddenIndex",void 0!==k?k:b),void g--;if(f.data("content")||(o=r+''+o+q+""),t&&f.data("divider")!==!0){if(c.options.hideDisabled&&v){if(void 0===s.data("allOptionsDisabled")){var w=s.children();s.data("allOptionsDisabled",w.filter(":disabled").length===w.length)}if(s.data("allOptionsDisabled"))return void g--}var x=" "+s[0].className||"";if(0===f.index()){e+=1;var y=s[0].label,z="undefined"!=typeof s.data("subtext")?''+s.data("subtext")+"":"",A=s.data("icon")?' ':"";y=A+''+j(y)+z+"",0!==b&&d.length>0&&(g++,d.push(h("",null,"divider",e+"div"))),g++,d.push(h(y,null,"dropdown-header"+x,e))}if(c.options.hideDisabled&&v)return void g--;d.push(h(i(o,"opt "+l+x,n,p),b,"",e))}else if(f.data("divider")===!0)d.push(h("",b,"divider"));else if(f.data("hidden")===!0)k=f.data("prevHiddenIndex"),f.next().data("prevHiddenIndex",void 0!==k?k:b),d.push(h(i(o,l,n,p),b,"hidden is-hidden"));else{var B=this.previousElementSibling&&"OPTGROUP"===this.previousElementSibling.tagName;if(!B&&c.options.hideDisabled&&(k=f.data("prevHiddenIndex"),void 0!==k)){var C=m.eq(k)[0].previousElementSibling;C&&"OPTGROUP"===C.tagName&&!C.disabled&&(B=!0)}B&&(g++,d.push(h("",null,"divider",e+"div"))),d.push(h(i(o,l,n,p),b))}c.liObj[b]=g}}),this.multiple||0!==this.$element.find("option:selected").length||this.options.title||this.$element.find("option").eq(0).prop("selected",!0).attr("selected","selected"),d.join("")},findLis:function(){return null==this.$lis&&(this.$lis=this.$menu.find("li")),this.$lis},render:function(b){var c,d=this,e=this.$element.find("option");b!==!1&&e.each(function(a){var b=d.findLis().eq(d.liObj[a]);d.setDisabled(a,this.disabled||"OPTGROUP"===this.parentNode.tagName&&this.parentNode.disabled,b),d.setSelected(a,this.selected,b)}),this.togglePlaceholder(),this.tabIndex();var f=e.map(function(){if(this.selected){if(d.options.hideDisabled&&(this.disabled||"OPTGROUP"===this.parentNode.tagName&&this.parentNode.disabled))return;var b,c=a(this),e=c.data("icon")&&d.options.showIcon?' ':"";return b=d.options.showSubtext&&c.data("subtext")&&!d.multiple?' '+c.data("subtext")+"":"","undefined"!=typeof c.attr("title")?c.attr("title"):c.data("content")&&d.options.showContent?c.data("content").toString():e+c.html()+b}}).toArray(),g=this.multiple?f.join(this.options.multipleSeparator):f[0];if(this.multiple&&this.options.selectedTextFormat.indexOf("count")>-1){var h=this.options.selectedTextFormat.split(">");if(h.length>1&&f.length>h[1]||1==h.length&&f.length>=2){c=this.options.hideDisabled?", [disabled]":"";var i=e.not('[data-divider="true"], [data-hidden="true"]'+c).length,j="function"==typeof this.options.countSelectedText?this.options.countSelectedText(f.length,i):this.options.countSelectedText;g=j.replace("{0}",f.length.toString()).replace("{1}",i.toString())}}void 0==this.options.title&&(this.options.title=this.$element.attr("title")),"static"==this.options.selectedTextFormat&&(g=this.options.title),g||(g="undefined"!=typeof this.options.title?this.options.title:this.options.noneSelectedText),this.$button.attr("title",k(a.trim(g.replace(/<[^>]*>?/g,"")))),this.$button.children(".filter-option").html(g),this.$element.trigger("rendered.bs.select")},setStyle:function(a,b){this.$element.attr("class")&&this.$newElement.addClass(this.$element.attr("class").replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi,""));var c=a?a:this.options.style;"add"==b?this.$button.addClass(c):"remove"==b?this.$button.removeClass(c):(this.$button.removeClass(this.options.style),this.$button.addClass(c))},liHeight:function(b){if(b||this.options.size!==!1&&!this.sizeInfo){var c=document.createElement("div"),d=document.createElement("div"),e=document.createElement("ul"),f=document.createElement("li"),g=document.createElement("li"),h=document.createElement("a"),i=document.createElement("span"),j=this.options.header&&this.$menu.find(".popover-title").length>0?this.$menu.find(".popover-title")[0].cloneNode(!0):null,k=this.options.liveSearch?document.createElement("div"):null,l=this.options.actionsBox&&this.multiple&&this.$menu.find(".bs-actionsbox").length>0?this.$menu.find(".bs-actionsbox")[0].cloneNode(!0):null,m=this.options.doneButton&&this.multiple&&this.$menu.find(".bs-donebutton").length>0?this.$menu.find(".bs-donebutton")[0].cloneNode(!0):null;if(i.className="text",c.className=this.$menu[0].parentNode.className+" open",d.className="dropdown-menu open",e.className="dropdown-menu inner",f.className="divider",i.appendChild(document.createTextNode("Inner text")),h.appendChild(i),g.appendChild(h),e.appendChild(g),e.appendChild(f),j&&d.appendChild(j),k){var n=document.createElement("input");k.className="bs-searchbox",n.className="form-control",k.appendChild(n),d.appendChild(k)}l&&d.appendChild(l),d.appendChild(e),m&&d.appendChild(m),c.appendChild(d),document.body.appendChild(c);var o=h.offsetHeight,p=j?j.offsetHeight:0,q=k?k.offsetHeight:0,r=l?l.offsetHeight:0,s=m?m.offsetHeight:0,t=a(f).outerHeight(!0),u="function"==typeof getComputedStyle&&getComputedStyle(d),v=u?null:a(d),w={vert:parseInt(u?u.paddingTop:v.css("paddingTop"))+parseInt(u?u.paddingBottom:v.css("paddingBottom"))+parseInt(u?u.borderTopWidth:v.css("borderTopWidth"))+parseInt(u?u.borderBottomWidth:v.css("borderBottomWidth")),horiz:parseInt(u?u.paddingLeft:v.css("paddingLeft"))+parseInt(u?u.paddingRight:v.css("paddingRight"))+parseInt(u?u.borderLeftWidth:v.css("borderLeftWidth"))+parseInt(u?u.borderRightWidth:v.css("borderRightWidth"))},x={vert:w.vert+parseInt(u?u.marginTop:v.css("marginTop"))+parseInt(u?u.marginBottom:v.css("marginBottom"))+2,horiz:w.horiz+parseInt(u?u.marginLeft:v.css("marginLeft"))+parseInt(u?u.marginRight:v.css("marginRight"))+2};document.body.removeChild(c),this.sizeInfo={liHeight:o,headerHeight:p,searchHeight:q,actionsHeight:r,doneButtonHeight:s,dividerHeight:t,menuPadding:w,menuExtras:x}}},setSize:function(){if(this.findLis(),this.liHeight(),this.options.header&&this.$menu.css("padding-top",0),this.options.size!==!1){var b,c,d,e,f,g,h,i,j=this,k=this.$menu,l=this.$menuInner,m=a(window),n=this.$newElement[0].offsetHeight,o=this.$newElement[0].offsetWidth,p=this.sizeInfo.liHeight,q=this.sizeInfo.headerHeight,r=this.sizeInfo.searchHeight,s=this.sizeInfo.actionsHeight,t=this.sizeInfo.doneButtonHeight,u=this.sizeInfo.dividerHeight,v=this.sizeInfo.menuPadding,w=this.sizeInfo.menuExtras,x=this.options.hideDisabled?".disabled":"",y=function(){var b,c=j.$newElement.offset(),d=a(j.options.container);j.options.container&&!d.is("body")?(b=d.offset(),b.top+=parseInt(d.css("borderTopWidth")),b.left+=parseInt(d.css("borderLeftWidth"))):b={top:0,left:0};var e=j.options.windowPadding;f=c.top-b.top-m.scrollTop(),g=m.height()-f-n-b.top-e[2],h=c.left-b.left-m.scrollLeft(),i=m.width()-h-o-b.left-e[1],f-=e[0],h-=e[3]};if(y(),"auto"===this.options.size){var z=function(){var m,n=function(b,c){return function(d){return c?d.classList?d.classList.contains(b):a(d).hasClass(b):!(d.classList?d.classList.contains(b):a(d).hasClass(b))}},u=j.$menuInner[0].getElementsByTagName("li"),x=Array.prototype.filter?Array.prototype.filter.call(u,n("hidden",!1)):j.$lis.not(".hidden"),z=Array.prototype.filter?Array.prototype.filter.call(x,n("dropdown-header",!0)):x.filter(".dropdown-header");y(),b=g-w.vert,c=i-w.horiz,j.options.container?(k.data("height")||k.data("height",k.height()),d=k.data("height"),k.data("width")||k.data("width",k.width()),e=k.data("width")):(d=k.height(),e=k.width()),j.options.dropupAuto&&j.$newElement.toggleClass("dropup",f>g&&b-w.verti&&c-w.horiz3?3*p+w.vert-2:0,k.css({"max-height":b+"px",overflow:"hidden","min-height":m+q+r+s+t+"px"}),l.css({"max-height":b-q-r-s-t-v.vert+"px","overflow-y":"auto","min-height":Math.max(m-v.vert,0)+"px"})};z(),this.$searchbox.off("input.getSize propertychange.getSize").on("input.getSize propertychange.getSize",z),m.off("resize.getSize scroll.getSize").on("resize.getSize scroll.getSize",z)}else if(this.options.size&&"auto"!=this.options.size&&this.$lis.not(x).length>this.options.size){var A=this.$lis.not(".divider").not(x).children().slice(0,this.options.size).last().parent().index(),B=this.$lis.slice(0,A+1).filter(".divider").length;b=p*this.options.size+B*u+v.vert,j.options.container?(k.data("height")||k.data("height",k.height()),d=k.data("height")):d=k.height(),j.options.dropupAuto&&this.$newElement.toggleClass("dropup",f>g&&b-w.vert');var b,c,d,e=this,f=a(this.options.container),g=function(a){e.$bsContainer.addClass(a.attr("class").replace(/form-control|fit-width/gi,"")).toggleClass("dropup",a.hasClass("dropup")),b=a.offset(),f.is("body")?c={top:0,left:0}:(c=f.offset(),c.top+=parseInt(f.css("borderTopWidth"))-f.scrollTop(),c.left+=parseInt(f.css("borderLeftWidth"))-f.scrollLeft()),d=a.hasClass("dropup")?0:a[0].offsetHeight,e.$bsContainer.css({top:b.top-c.top+d,left:b.left-c.left,width:a[0].offsetWidth})};this.$button.on("click",function(){var b=a(this);e.isDisabled()||(g(e.$newElement),e.$bsContainer.appendTo(e.options.container).toggleClass("open",!b.hasClass("open")).append(e.$menu))}),a(window).on("resize scroll",function(){g(e.$newElement)}),this.$element.on("hide.bs.select",function(){e.$menu.data("height",e.$menu.height()),e.$bsContainer.detach()})},setSelected:function(a,b,c){c||(this.togglePlaceholder(),c=this.findLis().eq(this.liObj[a])),c.toggleClass("selected",b).find("a").attr("aria-selected",b)},setDisabled:function(a,b,c){c||(c=this.findLis().eq(this.liObj[a])),b?c.addClass("disabled").children("a").attr("href","#").attr("tabindex",-1).attr("aria-disabled",!0):c.removeClass("disabled").children("a").removeAttr("href").attr("tabindex",0).attr("aria-disabled",!1)},isDisabled:function(){return this.$element[0].disabled},checkDisabled:function(){var a=this;this.isDisabled()?(this.$newElement.addClass("disabled"),this.$button.addClass("disabled").attr("tabindex",-1).attr("aria-disabled",!0)):(this.$button.hasClass("disabled")&&(this.$newElement.removeClass("disabled"),this.$button.removeClass("disabled").attr("aria-disabled",!1)),this.$button.attr("tabindex")!=-1||this.$element.data("tabindex")||this.$button.removeAttr("tabindex")),this.$button.click(function(){return!a.isDisabled()})},togglePlaceholder:function(){var a=this.$element.val();this.$button.toggleClass("bs-placeholder",null===a||""===a||a.constructor===Array&&0===a.length)},tabIndex:function(){this.$element.data("tabindex")!==this.$element.attr("tabindex")&&this.$element.attr("tabindex")!==-98&&"-98"!==this.$element.attr("tabindex")&&(this.$element.data("tabindex",this.$element.attr("tabindex")),this.$button.attr("tabindex",this.$element.data("tabindex"))),this.$element.attr("tabindex",-98)},clickListener:function(){var b=this,c=a(document);c.data("spaceSelect",!1),this.$button.on("keyup",function(a){/(32)/.test(a.keyCode.toString(10))&&c.data("spaceSelect")&&(a.preventDefault(),c.data("spaceSelect",!1))}),this.$button.on("click",function(){b.setSize()}),this.$element.on("shown.bs.select",function(){if(b.options.liveSearch||b.multiple){if(!b.multiple){var a=b.liObj[b.$element[0].selectedIndex];if("number"!=typeof a||b.options.size===!1)return;var c=b.$lis.eq(a)[0].offsetTop-b.$menuInner[0].offsetTop;c=c-b.$menuInner[0].offsetHeight/2+b.sizeInfo.liHeight/2,b.$menuInner[0].scrollTop=c}}else b.$menuInner.find(".selected a").focus()}),this.$menuInner.on("click","li a",function(c){var d=a(this),f=d.parent().data("originalIndex"),g=b.$element.val(),h=b.$element.prop("selectedIndex"),i=!0;if(b.multiple&&1!==b.options.maxOptions&&c.stopPropagation(),c.preventDefault(),!b.isDisabled()&&!d.parent().hasClass("disabled")){var j=b.$element.find("option"),k=j.eq(f),l=k.prop("selected"),m=k.parent("optgroup"),n=b.options.maxOptions,o=m.data("maxOptions")||!1;if(b.multiple){if(k.prop("selected",!l),b.setSelected(f,!l),d.blur(),n!==!1||o!==!1){var p=n');t[2]&&(u=u.replace("{var}",t[2][n>1?0:1]),v=v.replace("{var}",t[2][o>1?0:1])),k.prop("selected",!1),b.$menu.append(w),n&&p&&(w.append(a("
"+u+"
")),i=!1,b.$element.trigger("maxReached.bs.select")),o&&q&&(w.append(a("
"+v+"
")),i=!1,b.$element.trigger("maxReachedGrp.bs.select")),setTimeout(function(){b.setSelected(f,!1)},10),w.delay(750).fadeOut(300,function(){a(this).remove()})}}}else j.prop("selected",!1),k.prop("selected",!0),b.$menuInner.find(".selected").removeClass("selected").find("a").attr("aria-selected",!1),b.setSelected(f,!0);!b.multiple||b.multiple&&1===b.options.maxOptions?b.$button.focus():b.options.liveSearch&&b.$searchbox.focus(),i&&(g!=b.$element.val()&&b.multiple||h!=b.$element.prop("selectedIndex")&&!b.multiple)&&(e=[f,k.prop("selected"),l],b.$element.triggerNative("change"))}}),this.$menu.on("click","li.disabled a, .popover-title, .popover-title :not(.close)",function(c){c.currentTarget==this&&(c.preventDefault(),c.stopPropagation(),b.options.liveSearch&&!a(c.target).hasClass("close")?b.$searchbox.focus():b.$button.focus())}),this.$menuInner.on("click",".divider, .dropdown-header",function(a){a.preventDefault(),a.stopPropagation(),b.options.liveSearch?b.$searchbox.focus():b.$button.focus()}),this.$menu.on("click",".popover-title .close",function(){b.$button.click()}),this.$searchbox.on("click",function(a){a.stopPropagation()}),this.$menu.on("click",".actions-btn",function(c){b.options.liveSearch?b.$searchbox.focus():b.$button.focus(),c.preventDefault(),c.stopPropagation(),a(this).hasClass("bs-select-all")?b.selectAll():b.deselectAll()}),this.$element.change(function(){b.render(!1),b.$element.trigger("changed.bs.select",e),e=null})},liveSearchListener:function(){var c=this,d=a('
  • ');this.$button.on("click.dropdown.data-api",function(){c.$menuInner.find(".active").removeClass("active"),c.$searchbox.val()&&(c.$searchbox.val(""),c.$lis.not(".is-hidden").removeClass("hidden"),d.parent().length&&d.remove()),c.multiple||c.$menuInner.find(".selected").addClass("active"),setTimeout(function(){c.$searchbox.focus()},10)}),this.$searchbox.on("click.dropdown.data-api focus.dropdown.data-api touchend.dropdown.data-api",function(a){a.stopPropagation()}),this.$searchbox.on("input propertychange",function(){if(c.$lis.not(".is-hidden").removeClass("hidden"),c.$lis.filter(".active").removeClass("active"),d.remove(),c.$searchbox.val()){var e,f=c.$lis.not(".is-hidden, .divider, .dropdown-header");if(e=c.options.liveSearchNormalize?f.not(":a"+c._searchStyle()+'("'+b(c.$searchbox.val())+'")'):f.not(":"+c._searchStyle()+'("'+c.$searchbox.val()+'")'),e.length===f.length)d.html(c.options.noneResultsText.replace("{0}",'"'+j(c.$searchbox.val())+'"')),c.$menuInner.append(d),c.$lis.addClass("hidden");else{e.addClass("hidden");var g,h=c.$lis.not(".hidden");h.each(function(b){var c=a(this);c.hasClass("divider")?void 0===g?c.addClass("hidden"):(g&&g.addClass("hidden"),g=c):c.hasClass("dropdown-header")&&h.eq(b+1).data("optgroup")!==c.data("optgroup")?c.addClass("hidden"):g=null}),g&&g.addClass("hidden"),f.not(".hidden").first().addClass("active"),c.$menuInner.scrollTop(0)}}})},_searchStyle:function(){var a={begins:"ibegins",startsWith:"ibegins"};return a[this.options.liveSearchStyle]||"icontains"},val:function(a){return"undefined"!=typeof a?(this.$element.val(a),this.render(),this.$element):this.$element.val()},changeAll:function(b){if(this.multiple){"undefined"==typeof b&&(b=!0),this.findLis();var c=this.$element.find("option"),d=this.$lis.not(".divider, .dropdown-header, .disabled, .hidden"),e=d.length,f=[];if(b){if(d.filter(".selected").length===d.length)return}else if(0===d.filter(".selected").length)return;d.toggleClass("selected",b);for(var g=0;g=48&&b.keyCode<=57||b.keyCode>=96&&b.keyCode<=105||b.keyCode>=65&&b.keyCode<=90))return i.options.container?i.$button.trigger("click"):(i.setSize(),i.$menu.parent().addClass("open"),f=!0),void i.$searchbox.focus();if(i.options.liveSearch&&/(^9$|27)/.test(b.keyCode.toString(10))&&f&&(b.preventDefault(),b.stopPropagation(),i.$menuInner.click(),i.$button.focus()),/(38|40)/.test(b.keyCode.toString(10))){if(c=i.$lis.filter(j),!c.length)return;d=i.options.liveSearch?c.index(c.filter(".active")):c.index(c.find("a").filter(":focus").parent()),e=i.$menuInner.data("prevIndex"),38==b.keyCode?(!i.options.liveSearch&&d!=e||d==-1||d--,d<0&&(d+=c.length)):40==b.keyCode&&((i.options.liveSearch||d==e)&&d++,d%=c.length),i.$menuInner.data("prevIndex",d),i.options.liveSearch?(b.preventDefault(),g.hasClass("dropdown-toggle")||(c.removeClass("active").eq(d).addClass("active").children("a").focus(),g.focus())):c.eq(d).children("a").focus()}else if(!g.is("input")){var l,m,n=[];c=i.$lis.filter(j),c.each(function(c){a.trim(a(this).children("a").text().toLowerCase()).substring(0,1)==k[b.keyCode]&&n.push(c)}),l=a(document).data("keycount"),l++,a(document).data("keycount",l),m=a.trim(a(":focus").text().toLowerCase()).substring(0,1),m!=k[b.keyCode]?(l=1,a(document).data("keycount",l)):l>=n.length&&(a(document).data("keycount",0),l>n.length&&(l=1)),c.eq(n[l-1]).children("a").focus()}if((/(13|32)/.test(b.keyCode.toString(10))||/(^9$)/.test(b.keyCode.toString(10))&&i.options.selectOnTab)&&f){if(/(32)/.test(b.keyCode.toString(10))||b.preventDefault(),i.options.liveSearch)/(32)/.test(b.keyCode.toString(10))||(i.$menuInner.find(".active a").click(),g.focus());else{var o=a(":focus");o.click(),o.focus(),b.preventDefault(),a(document).data("spaceSelect",!0)}a(document).data("keycount",0)}(/(^9$|27)/.test(b.keyCode.toString(10))&&f&&(i.multiple||i.options.liveSearch)||/(27)/.test(b.keyCode.toString(10))&&!f)&&(i.$menu.parent().removeClass("open"),i.options.container&&i.$newElement.removeClass("open"),i.$button.focus())},mobile:function(){this.$element.addClass("mobile-device")},refresh:function(){this.$lis=null,this.liObj={},this.reloadLi(),this.render(),this.checkDisabled(),this.liHeight(!0),this.setStyle(), -this.setWidth(),this.$lis&&this.$searchbox.trigger("propertychange"),this.$element.trigger("refreshed.bs.select")},hide:function(){this.$newElement.hide()},show:function(){this.$newElement.show()},remove:function(){this.$newElement.remove(),this.$element.remove()},destroy:function(){this.$newElement.before(this.$element).remove(),this.$bsContainer?this.$bsContainer.remove():this.$menu.remove(),this.$element.off(".bs.select").removeData("selectpicker").removeClass("bs-select-hidden selectpicker")}};var m=a.fn.selectpicker;a.fn.selectpicker=c,a.fn.selectpicker.Constructor=l,a.fn.selectpicker.noConflict=function(){return a.fn.selectpicker=m,this},a(document).data("keycount",0).on("keydown.bs.select",'.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role="listbox"], .bs-searchbox input',l.prototype.keydown).on("focusin.modal",'.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role="listbox"], .bs-searchbox input',function(a){a.stopPropagation()}),a(window).on("load.bs.select.data-api",function(){a(".selectpicker").each(function(){var b=a(this);c.call(b,b.data())})})}(a)}); -//# sourceMappingURL=bootstrap-select.js.map \ No newline at end of file +this.setWidth(),this.$lis&&this.$searchbox.trigger("propertychange"),this.$element.trigger("refreshed.bs.select")},hide:function(){this.$newElement.hide()},show:function(){this.$newElement.show()},remove:function(){this.$newElement.remove(),this.$element.remove()},destroy:function(){this.$newElement.before(this.$element).remove(),this.$bsContainer?this.$bsContainer.remove():this.$menu.remove(),this.$element.off(".bs.select").removeData("selectpicker").removeClass("bs-select-hidden selectpicker")}};var m=a.fn.selectpicker;a.fn.selectpicker=c,a.fn.selectpicker.Constructor=l,a.fn.selectpicker.noConflict=function(){return a.fn.selectpicker=m,this},a(document).data("keycount",0).on("keydown.bs.select",'.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role="listbox"], .bs-searchbox input',l.prototype.keydown).on("focusin.modal",'.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role="listbox"], .bs-searchbox input',function(a){a.stopPropagation()}),a(window).on("load.bs.select.data-api",function(){a(".selectpicker").each(function(){var b=a(this);c.call(b,b.data())})})}(a)}); \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/resources/jsonview/jquery.jsonview.css b/mq-cloud/src/main/resources/static/resources/jsonview/jquery.jsonview.css index 53d9c839..4cbeba9b 100644 --- a/mq-cloud/src/main/resources/static/resources/jsonview/jquery.jsonview.css +++ b/mq-cloud/src/main/resources/static/resources/jsonview/jquery.jsonview.css @@ -47,6 +47,4 @@ margin: 0 0 0 2em; padding: 0; } .jsonview h1 { - font-size: 1.2em; } - -/*# sourceMappingURL=jquery.jsonview.css.map */ + font-size: 1.2em; } \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/resources/nicescroll/jquery.nicescroll.min.js b/mq-cloud/src/main/resources/static/resources/nicescroll/jquery.nicescroll.min.js new file mode 100644 index 00000000..bd1086ee --- /dev/null +++ b/mq-cloud/src/main/resources/static/resources/nicescroll/jquery.nicescroll.min.js @@ -0,0 +1,3729 @@ +/* jquery.nicescroll +-- version 3.7.6 +-- copyright 2017-07-19 InuYaksa*2017 +-- licensed under the MIT +-- +-- https://nicescroll.areaaperta.com/ +-- https://github.com/inuyaksa/jquery.nicescroll +-- +*/ + +/* jshint expr: true */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS. + module.exports = factory(require('jquery')); + } else { + // Browser globals. + factory(jQuery); + } +}(function (jQuery) { + + "use strict"; + + // globals + var domfocus = false, + mousefocus = false, + tabindexcounter = 0, + ascrailcounter = 2000, + globalmaxzindex = 0; + + var $ = jQuery, // sandbox + _doc = document, + _win = window, + $window = $(_win); + + var delegatevents = []; + + // http://stackoverflow.com/questions/2161159/get-script-path + function getScriptPath() { + var scripts = _doc.currentScript || (function () { var s = _doc.getElementsByTagName('script'); return (s.length) ? s[s.length - 1] : false; })(); + var path = scripts ? scripts.src.split('?')[0] : ''; + return (path.split('/').length > 0) ? path.split('/').slice(0, -1).join('/') + '/' : ''; + } + + // based on code by Paul Irish https://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ + var setAnimationFrame = _win.requestAnimationFrame || _win.webkitRequestAnimationFrame || _win.mozRequestAnimationFrame || false; + var clearAnimationFrame = _win.cancelAnimationFrame || _win.webkitCancelAnimationFrame || _win.mozCancelAnimationFrame || false; + + if (!setAnimationFrame) { + var anilasttime = 0; + setAnimationFrame = function (callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - anilasttime)); + var id = _win.setTimeout(function () { callback(currTime + timeToCall); }, + timeToCall); + anilasttime = currTime + timeToCall; + return id; + }; + clearAnimationFrame = function (id) { + _win.clearTimeout(id); + }; + } else { + if (!_win.cancelAnimationFrame) clearAnimationFrame = function (id) { }; + } + + var ClsMutationObserver = _win.MutationObserver || _win.WebKitMutationObserver || false; + + var now = Date.now || function () { return new Date().getTime(); }; + + var _globaloptions = { + zindex: "auto", + cursoropacitymin: 0, + cursoropacitymax: 1, + cursorcolor: "#424242", + cursorwidth: "6px", + cursorborder: "1px solid #fff", + cursorborderradius: "5px", + scrollspeed: 40, + mousescrollstep: 9 * 3, + touchbehavior: false, // deprecated + emulatetouch: false, // replacing touchbehavior + hwacceleration: true, + usetransition: true, + boxzoom: false, + dblclickzoom: true, + gesturezoom: true, + grabcursorenabled: true, + autohidemode: true, + background: "", + iframeautoresize: true, + cursorminheight: 32, + preservenativescrolling: true, + railoffset: false, + railhoffset: false, + bouncescroll: true, + spacebarenabled: true, + railpadding: { + top: 0, + right: 0, + left: 0, + bottom: 0 + }, + disableoutline: true, + horizrailenabled: true, + railalign: "right", + railvalign: "bottom", + enabletranslate3d: true, + enablemousewheel: true, + enablekeyboard: true, + smoothscroll: true, + sensitiverail: true, + enablemouselockapi: true, + // cursormaxheight:false, + cursorfixedheight: false, + directionlockdeadzone: 6, + hidecursordelay: 400, + nativeparentscrolling: true, + enablescrollonselection: true, + overflowx: true, + overflowy: true, + cursordragspeed: 0.3, + rtlmode: "auto", + cursordragontouch: false, + oneaxismousemode: "auto", + scriptpath: getScriptPath(), + preventmultitouchscrolling: true, + disablemutationobserver: false, + enableobserver: true, + scrollbarid: false, + scrollCLass: false + }; + + var browserdetected = false; + + var getBrowserDetection = function () { + + if (browserdetected) return browserdetected; + + var _el = _doc.createElement('DIV'), + _style = _el.style, + _agent = navigator.userAgent, + _platform = navigator.platform, + d = {}; + + d.haspointerlock = "pointerLockElement" in _doc || "webkitPointerLockElement" in _doc || "mozPointerLockElement" in _doc; + + d.isopera = ("opera" in _win); // 12- + d.isopera12 = (d.isopera && ("getUserMedia" in navigator)); + d.isoperamini = (Object.prototype.toString.call(_win.operamini) === "[object OperaMini]"); + + d.isie = (("all" in _doc) && ("attachEvent" in _el) && !d.isopera); //IE10- + d.isieold = (d.isie && !("msInterpolationMode" in _style)); // IE6 and older + d.isie7 = d.isie && !d.isieold && (!("documentMode" in _doc) || (_doc.documentMode === 7)); + d.isie8 = d.isie && ("documentMode" in _doc) && (_doc.documentMode === 8); + d.isie9 = d.isie && ("performance" in _win) && (_doc.documentMode === 9); + d.isie10 = d.isie && ("performance" in _win) && (_doc.documentMode === 10); + d.isie11 = ("msRequestFullscreen" in _el) && (_doc.documentMode >= 11); // IE11+ + + d.ismsedge = ("msCredentials" in _win); // MS Edge 14+ + + d.ismozilla = ("MozAppearance" in _style); + + d.iswebkit = !d.ismsedge && ("WebkitAppearance" in _style); + + d.ischrome = d.iswebkit && ("chrome" in _win); + d.ischrome38 = (d.ischrome && ("touchAction" in _style)); // behavior changed in touch emulation + d.ischrome22 = (!d.ischrome38) && (d.ischrome && d.haspointerlock); + d.ischrome26 = (!d.ischrome38) && (d.ischrome && ("transition" in _style)); // issue with transform detection (maintain prefix) + + d.cantouch = ("ontouchstart" in _doc.documentElement) || ("ontouchstart" in _win); // with detection for Chrome Touch Emulation + d.hasw3ctouch = (_win.PointerEvent || false) && ((navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)); //IE11 pointer events, following W3C Pointer Events spec + d.hasmstouch = (!d.hasw3ctouch) && (_win.MSPointerEvent || false); // IE10 pointer events + + d.ismac = /^mac$/i.test(_platform); + + d.isios = d.cantouch && /iphone|ipad|ipod/i.test(_platform); + d.isios4 = d.isios && !("seal" in Object); + d.isios7 = d.isios && ("webkitHidden" in _doc); //iOS 7+ + d.isios8 = d.isios && ("hidden" in _doc); //iOS 8+ + d.isios10 = d.isios && _win.Proxy; //iOS 10+ + + d.isandroid = (/android/i.test(_agent)); + + d.haseventlistener = ("addEventListener" in _el); + + d.trstyle = false; + d.hastransform = false; + d.hastranslate3d = false; + d.transitionstyle = false; + d.hastransition = false; + d.transitionend = false; + + d.trstyle = "transform"; + d.hastransform = ("transform" in _style) || (function () { + var check = ['msTransform', 'webkitTransform', 'MozTransform', 'OTransform']; + for (var a = 0, c = check.length; a < c; a++) { + if (_style[check[a]] !== undefined) { + d.trstyle = check[a]; + break; + } + } + d.hastransform = (!!d.trstyle); + })(); + + if (d.hastransform) { + _style[d.trstyle] = "translate3d(1px,2px,3px)"; + d.hastranslate3d = /translate3d/.test(_style[d.trstyle]); + } + + d.transitionstyle = "transition"; + d.prefixstyle = ''; + d.transitionend = "transitionend"; + + d.hastransition = ("transition" in _style) || (function () { + + d.transitionend = false; + var check = ['webkitTransition', 'msTransition', 'MozTransition', 'OTransition', 'OTransition', 'KhtmlTransition']; + var prefix = ['-webkit-', '-ms-', '-moz-', '-o-', '-o', '-khtml-']; + var evs = ['webkitTransitionEnd', 'msTransitionEnd', 'transitionend', 'otransitionend', 'oTransitionEnd', 'KhtmlTransitionEnd']; + for (var a = 0, c = check.length; a < c; a++) { + if (check[a] in _style) { + d.transitionstyle = check[a]; + d.prefixstyle = prefix[a]; + d.transitionend = evs[a]; + break; + } + } + if (d.ischrome26) d.prefixstyle = prefix[1]; // always use prefix + + d.hastransition = (d.transitionstyle); + + })(); + + function detectCursorGrab() { + var lst = ['grab', '-webkit-grab', '-moz-grab']; + if ((d.ischrome && !d.ischrome38) || d.isie) lst = []; // force setting for IE returns false positive and chrome cursor bug + for (var a = 0, l = lst.length; a < l; a++) { + var p = lst[a]; + _style.cursor = p; + if (_style.cursor == p) return p; + } + return 'url(https://cdnjs.cloudflare.com/ajax/libs/slider-pro/1.3.0/css/images/openhand.cur),n-resize'; // thanks to https://cdnjs.com/ for the openhand cursor! + } + d.cursorgrabvalue = detectCursorGrab(); + + d.hasmousecapture = ("setCapture" in _el); + + d.hasMutationObserver = (ClsMutationObserver !== false); + + _el = null; //memory released + + browserdetected = d; + + return d; + }; + + var NiceScrollClass = function (myopt, me) { + + var self = this; + + this.version = '3.7.6'; + this.name = 'nicescroll'; + + this.me = me; + + var $body = $("body"); + + var opt = this.opt = { + doc: $body, + win: false + }; + + $.extend(opt, _globaloptions); // clone opts + + // Options for internal use + opt.snapbackspeed = 80; + + if (myopt || false) { + for (var a in opt) { + if (myopt[a] !== undefined) opt[a] = myopt[a]; + } + } + + if (opt.disablemutationobserver) ClsMutationObserver = false; + + this.doc = opt.doc; + this.iddoc = (this.doc && this.doc[0]) ? this.doc[0].id || '' : ''; + this.ispage = /^BODY|HTML/.test((opt.win) ? opt.win[0].nodeName : this.doc[0].nodeName); + this.haswrapper = (opt.win !== false); + this.win = opt.win || (this.ispage ? $window : this.doc); + this.docscroll = (this.ispage && !this.haswrapper) ? $window : this.win; + this.body = $body; + this.viewport = false; + + this.isfixed = false; + + this.iframe = false; + this.isiframe = ((this.doc[0].nodeName == 'IFRAME') && (this.win[0].nodeName == 'IFRAME')); + + this.istextarea = (this.win[0].nodeName == 'TEXTAREA'); + + this.forcescreen = false; //force to use screen position on events + + this.canshowonmouseevent = (opt.autohidemode != "scroll"); + + // Events jump table + this.onmousedown = false; + this.onmouseup = false; + this.onmousemove = false; + this.onmousewheel = false; + this.onkeypress = false; + this.ongesturezoom = false; + this.onclick = false; + + // Nicescroll custom events + this.onscrollstart = false; + this.onscrollend = false; + this.onscrollcancel = false; + + this.onzoomin = false; + this.onzoomout = false; + + // Let's start! + this.view = false; + this.page = false; + + this.scroll = { + x: 0, + y: 0 + }; + this.scrollratio = { + x: 0, + y: 0 + }; + this.cursorheight = 20; + this.scrollvaluemax = 0; + + // http://dev.w3.org/csswg/css-writing-modes-3/#logical-to-physical + // http://dev.w3.org/csswg/css-writing-modes-3/#svg-writing-mode + if (opt.rtlmode == "auto") { + var target = this.win[0] == _win ? this.body : this.win; + var writingMode = target.css("writing-mode") || target.css("-webkit-writing-mode") || target.css("-ms-writing-mode") || target.css("-moz-writing-mode"); + + if (writingMode == "horizontal-tb" || writingMode == "lr-tb" || writingMode === "") { + this.isrtlmode = (target.css("direction") == "rtl"); + this.isvertical = false; + } else { + this.isrtlmode = (writingMode == "vertical-rl" || writingMode == "tb" || writingMode == "tb-rl" || writingMode == "rl-tb"); + this.isvertical = (writingMode == "vertical-rl" || writingMode == "tb" || writingMode == "tb-rl"); + } + } else { + this.isrtlmode = (opt.rtlmode === true); + this.isvertical = false; + } + // this.checkrtlmode = false; + + this.scrollrunning = false; + + this.scrollmom = false; + + this.observer = false; // observer div changes + this.observerremover = false; // observer on parent for remove detection + this.observerbody = false; // observer on body for position change + + if (opt.scrollbarid !== false) { + this.id = opt.scrollbarid; + } else { + do { + this.id = "ascrail" + (ascrailcounter++); + } while (_doc.getElementById(this.id)); + } + + this.rail = false; + this.cursor = false; + this.cursorfreezed = false; + this.selectiondrag = false; + + this.zoom = false; + this.zoomactive = false; + + this.hasfocus = false; + this.hasmousefocus = false; + + //this.visibility = true; + this.railslocked = false; // locked by resize + this.locked = false; // prevent lost of locked status sets by user + this.hidden = false; // rails always hidden + this.cursoractive = true; // user can interact with cursors + + this.wheelprevented = false; //prevent mousewheel event + + this.overflowx = opt.overflowx; + this.overflowy = opt.overflowy; + + this.nativescrollingarea = false; + this.checkarea = 0; + + this.events = []; // event list for unbind + + this.saved = {}; // style saved + + this.delaylist = {}; + this.synclist = {}; + + this.lastdeltax = 0; + this.lastdeltay = 0; + + this.detected = getBrowserDetection(); + + var cap = $.extend({}, this.detected); + + this.canhwscroll = (cap.hastransform && opt.hwacceleration); + this.ishwscroll = (this.canhwscroll && self.haswrapper); + + if (!this.isrtlmode) { + this.hasreversehr = false; + } else if (this.isvertical) { // RTL mode with reverse horizontal axis + this.hasreversehr = !(cap.iswebkit || cap.isie || cap.isie11); + } else { + this.hasreversehr = !(cap.iswebkit || (cap.isie && !cap.isie10 && !cap.isie11)); + } + + this.istouchcapable = false; // desktop devices with touch screen support + + //## Check WebKit-based desktop with touch support + //## + Firefox 18 nightly build (desktop) false positive (or desktop with touch support) + + if (!cap.cantouch && (cap.hasw3ctouch || cap.hasmstouch)) { // desktop device with multiple input + this.istouchcapable = true; + } else if (cap.cantouch && !cap.isios && !cap.isandroid && (cap.iswebkit || cap.ismozilla)) { + this.istouchcapable = true; + } + + //## disable MouseLock API on user request + if (!opt.enablemouselockapi) { + cap.hasmousecapture = false; + cap.haspointerlock = false; + } + + this.debounced = function (name, fn, tm) { + if (!self) return; + var dd = self.delaylist[name] || false; + if (!dd) { + self.delaylist[name] = { + h: setAnimationFrame(function () { + self.delaylist[name].fn.call(self); + self.delaylist[name] = false; + }, tm) + }; + fn.call(self); + } + self.delaylist[name].fn = fn; + }; + + + this.synched = function (name, fn) { + if (self.synclist[name]) self.synclist[name] = fn; + else { + self.synclist[name] = fn; + setAnimationFrame(function () { + if (!self) return; + self.synclist[name] && self.synclist[name].call(self); + self.synclist[name] = null; + }); + } + }; + + this.unsynched = function (name) { + if (self.synclist[name]) self.synclist[name] = false; + }; + + this.css = function (el, pars) { // save & set + for (var n in pars) { + self.saved.css.push([el, n, el.css(n)]); + el.css(n, pars[n]); + } + }; + + this.scrollTop = function (val) { + return (val === undefined) ? self.getScrollTop() : self.setScrollTop(val); + }; + + this.scrollLeft = function (val) { + return (val === undefined) ? self.getScrollLeft() : self.setScrollLeft(val); + }; + + // derived by by Dan Pupius www.pupius.net + var BezierClass = function (st, ed, spd, p1, p2, p3, p4) { + + this.st = st; + this.ed = ed; + this.spd = spd; + + this.p1 = p1 || 0; + this.p2 = p2 || 1; + this.p3 = p3 || 0; + this.p4 = p4 || 1; + + this.ts = now(); + this.df = ed - st; + }; + BezierClass.prototype = { + B2: function (t) { + return 3 * (1 - t) * (1 - t) * t; + }, + B3: function (t) { + return 3 * (1 - t) * t * t; + }, + B4: function (t) { + return t * t * t; + }, + getPos: function () { + return (now() - this.ts) / this.spd; + }, + getNow: function () { + var pc = (now() - this.ts) / this.spd; + var bz = this.B2(pc) + this.B3(pc) + this.B4(pc); + return (pc >= 1) ? this.ed : this.st + (this.df * bz) | 0; + }, + update: function (ed, spd) { + this.st = this.getNow(); + this.ed = ed; + this.spd = spd; + this.ts = now(); + this.df = this.ed - this.st; + return this; + } + }; + + //derived from http://stackoverflow.com/questions/11236090/ + function getMatrixValues() { + var tr = self.doc.css(cap.trstyle); + if (tr && (tr.substr(0, 6) == "matrix")) { + return tr.replace(/^.*\((.*)\)$/g, "$1").replace(/px/g, '').split(/, +/); + } + return false; + } + + if (this.ishwscroll) { // hw accelerated scroll + + this.doc.translate = { + x: 0, + y: 0, + tx: "0px", + ty: "0px" + }; + + //this one can help to enable hw accel on ios6 http://indiegamr.com/ios6-html-hardware-acceleration-changes-and-how-to-fix-them/ + if (cap.hastranslate3d && cap.isios) this.doc.css("-webkit-backface-visibility", "hidden"); // prevent flickering http://stackoverflow.com/questions/3461441/ + + this.getScrollTop = function (last) { + if (!last) { + var mtx = getMatrixValues(); + if (mtx) return (mtx.length == 16) ? -mtx[13] : -mtx[5]; //matrix3d 16 on IE10 + if (self.timerscroll && self.timerscroll.bz) return self.timerscroll.bz.getNow(); + } + return self.doc.translate.y; + }; + + this.getScrollLeft = function (last) { + if (!last) { + var mtx = getMatrixValues(); + if (mtx) return (mtx.length == 16) ? -mtx[12] : -mtx[4]; //matrix3d 16 on IE10 + if (self.timerscroll && self.timerscroll.bh) return self.timerscroll.bh.getNow(); + } + return self.doc.translate.x; + }; + + this.notifyScrollEvent = function (el) { + var e = _doc.createEvent("UIEvents"); + e.initUIEvent("scroll", false, false, _win, 1); + e.niceevent = true; + el.dispatchEvent(e); + }; + + var cxscrollleft = (this.isrtlmode) ? 1 : -1; + + if (cap.hastranslate3d && opt.enabletranslate3d) { + this.setScrollTop = function (val, silent) { + self.doc.translate.y = val; + self.doc.translate.ty = (val * -1) + "px"; + self.doc.css(cap.trstyle, "translate3d(" + self.doc.translate.tx + "," + self.doc.translate.ty + ",0)"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + this.setScrollLeft = function (val, silent) { + self.doc.translate.x = val; + self.doc.translate.tx = (val * cxscrollleft) + "px"; + self.doc.css(cap.trstyle, "translate3d(" + self.doc.translate.tx + "," + self.doc.translate.ty + ",0)"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + } else { + this.setScrollTop = function (val, silent) { + self.doc.translate.y = val; + self.doc.translate.ty = (val * -1) + "px"; + self.doc.css(cap.trstyle, "translate(" + self.doc.translate.tx + "," + self.doc.translate.ty + ")"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + this.setScrollLeft = function (val, silent) { + self.doc.translate.x = val; + self.doc.translate.tx = (val * cxscrollleft) + "px"; + self.doc.css(cap.trstyle, "translate(" + self.doc.translate.tx + "," + self.doc.translate.ty + ")"); + if (!silent) self.notifyScrollEvent(self.win[0]); + }; + } + } else { // native scroll + + this.getScrollTop = function () { + return self.docscroll.scrollTop(); + }; + this.setScrollTop = function (val) { + self.docscroll.scrollTop(val); + }; + + this.getScrollLeft = function () { + var val; + if (!self.hasreversehr) { + val = self.docscroll.scrollLeft(); + } else if (self.detected.ismozilla) { + val = self.page.maxw - Math.abs(self.docscroll.scrollLeft()); + } else { + val = self.page.maxw - self.docscroll.scrollLeft(); + } + return val; + }; + this.setScrollLeft = function (val) { + return setTimeout(function () { + if (!self) return; + if (self.hasreversehr) { + if (self.detected.ismozilla) { + val = -(self.page.maxw - val); + } else { + val = self.page.maxw - val; + } + } + return self.docscroll.scrollLeft(val); + }, 1); + }; + } + + this.getTarget = function (e) { + if (!e) return false; + if (e.target) return e.target; + if (e.srcElement) return e.srcElement; + return false; + }; + + this.hasParent = function (e, id) { + if (!e) return false; + var el = e.target || e.srcElement || e || false; + while (el && el.id != id) { + el = el.parentNode || false; + } + return (el !== false); + }; + + function getZIndex() { + var dom = self.win; + if ("zIndex" in dom) return dom.zIndex(); // use jQuery UI method when available + while (dom.length > 0) { + if (dom[0].nodeType == 9) return false; + var zi = dom.css('zIndex'); + if (!isNaN(zi) && zi !== 0) return parseInt(zi); + dom = dom.parent(); + } + return false; + } + + //inspired by http://forum.jquery.com/topic/width-includes-border-width-when-set-to-thin-medium-thick-in-ie + var _convertBorderWidth = { + "thin": 1, + "medium": 3, + "thick": 5 + }; + + function getWidthToPixel(dom, prop, chkheight) { + var wd = dom.css(prop); + var px = parseFloat(wd); + if (isNaN(px)) { + px = _convertBorderWidth[wd] || 0; + var brd = (px == 3) ? ((chkheight) ? (self.win.outerHeight() - self.win.innerHeight()) : (self.win.outerWidth() - self.win.innerWidth())) : 1; //DON'T TRUST CSS + if (self.isie8 && px) px += 1; + return (brd) ? px : 0; + } + return px; + } + + this.getDocumentScrollOffset = function () { + return { + top: _win.pageYOffset || _doc.documentElement.scrollTop, + left: _win.pageXOffset || _doc.documentElement.scrollLeft + }; + }; + + this.getOffset = function () { + if (self.isfixed) { + var ofs = self.win.offset(); // fix Chrome auto issue (when right/bottom props only) + var scrl = self.getDocumentScrollOffset(); + ofs.top -= scrl.top; + ofs.left -= scrl.left; + return ofs; + } + var ww = self.win.offset(); + if (!self.viewport) return ww; + var vp = self.viewport.offset(); + return { + top: ww.top - vp.top, + left: ww.left - vp.left + }; + }; + + this.updateScrollBar = function (len) { + var pos, off; + if (self.ishwscroll) { + self.rail.css({ + height: self.win.innerHeight() - (opt.railpadding.top + opt.railpadding.bottom) + }); + if (self.railh) self.railh.css({ + width: self.win.innerWidth() - (opt.railpadding.left + opt.railpadding.right) + }); + } else { + var wpos = self.getOffset(); + pos = { + top: wpos.top, + left: wpos.left - (opt.railpadding.left + opt.railpadding.right) + }; + pos.top += getWidthToPixel(self.win, 'border-top-width', true); + pos.left += (self.rail.align) ? self.win.outerWidth() - getWidthToPixel(self.win, 'border-right-width') - self.rail.width : getWidthToPixel(self.win, 'border-left-width'); + + off = opt.railoffset; + if (off) { + if (off.top) pos.top += off.top; + if (off.left) pos.left += off.left; + } + + if (!self.railslocked) self.rail.css({ + top: pos.top, + left: pos.left, + height: ((len) ? len.h : self.win.innerHeight()) - (opt.railpadding.top + opt.railpadding.bottom) + }); + + if (self.zoom) { + self.zoom.css({ + top: pos.top + 1, + left: (self.rail.align == 1) ? pos.left - 20 : pos.left + self.rail.width + 4 + }); + } + + if (self.railh && !self.railslocked) { + pos = { + top: wpos.top, + left: wpos.left + }; + off = opt.railhoffset; + if (off) { + if (off.top) pos.top += off.top; + if (off.left) pos.left += off.left; + } + var y = (self.railh.align) ? pos.top + getWidthToPixel(self.win, 'border-top-width', true) + self.win.innerHeight() - self.railh.height : pos.top + getWidthToPixel(self.win, 'border-top-width', true); + var x = pos.left + getWidthToPixel(self.win, 'border-left-width'); + self.railh.css({ + top: y - (opt.railpadding.top + opt.railpadding.bottom), + left: x, + width: self.railh.width + }); + } + + } + }; + + this.doRailClick = function (e, dbl, hr) { + var fn, pg, cur, pos; + + if (self.railslocked) return; + + self.cancelEvent(e); + + if (!("pageY" in e)) { + e.pageX = e.clientX + _doc.documentElement.scrollLeft; + e.pageY = e.clientY + _doc.documentElement.scrollTop; + } + + if (dbl) { + fn = (hr) ? self.doScrollLeft : self.doScrollTop; + cur = (hr) ? ((e.pageX - self.railh.offset().left - (self.cursorwidth / 2)) * self.scrollratio.x) : ((e.pageY - self.rail.offset().top - (self.cursorheight / 2)) * self.scrollratio.y); + self.unsynched("relativexy"); + fn(cur|0); + } else { + fn = (hr) ? self.doScrollLeftBy : self.doScrollBy; + cur = (hr) ? self.scroll.x : self.scroll.y; + pos = (hr) ? e.pageX - self.railh.offset().left : e.pageY - self.rail.offset().top; + pg = (hr) ? self.view.w : self.view.h; + fn((cur >= pos) ? pg : -pg); + } + + }; + + self.newscrolly = self.newscrollx = 0; + + self.hasanimationframe = ("requestAnimationFrame" in _win); + self.hascancelanimationframe = ("cancelAnimationFrame" in _win); + + self.hasborderbox = false; + + this.init = function () { + + self.saved.css = []; + + if (cap.isoperamini) return true; // SORRY, DO NOT WORK! + if (cap.isandroid && !("hidden" in _doc)) return true; // Android 3- SORRY, DO NOT WORK! + + opt.emulatetouch = opt.emulatetouch || opt.touchbehavior; // mantain compatibility with "touchbehavior" + + self.hasborderbox = _win.getComputedStyle && (_win.getComputedStyle(_doc.body)['box-sizing'] === "border-box"); + + var _scrollyhidden = { 'overflow-y': 'hidden' }; + if (cap.isie11 || cap.isie10) _scrollyhidden['-ms-overflow-style'] = 'none'; // IE 10 & 11 is always a world apart! + + if (self.ishwscroll) { + this.doc.css(cap.transitionstyle, cap.prefixstyle + 'transform 0ms ease-out'); + if (cap.transitionend) self.bind(self.doc, cap.transitionend, self.onScrollTransitionEnd, false); //I have got to do something usefull!! + } + + self.zindex = "auto"; + if (!self.ispage && opt.zindex == "auto") { + self.zindex = getZIndex() || "auto"; + } else { + self.zindex = opt.zindex; + } + + if (!self.ispage && self.zindex != "auto" && self.zindex > globalmaxzindex) { + globalmaxzindex = self.zindex; + } + + if (self.isie && self.zindex === 0 && opt.zindex == "auto") { // fix IE auto == 0 + self.zindex = "auto"; + } + + if (!self.ispage || !cap.isieold) { + + var cont = self.docscroll; + if (self.ispage) cont = (self.haswrapper) ? self.win : self.doc; + + self.css(cont, _scrollyhidden); + + if (self.ispage && (cap.isie11 || cap.isie)) { // IE 7-11 + self.css($("html"), _scrollyhidden); + } + + if (cap.isios && !self.ispage && !self.haswrapper) self.css($body, { + "-webkit-overflow-scrolling": "touch" + }); //force hw acceleration + + var cursor = $(_doc.createElement('div')); + cursor.css({ + position: "relative", + top: 0, + "float": "right", + width: opt.cursorwidth, + height: 0, + 'background-color': opt.cursorcolor, + border: opt.cursorborder, + 'background-clip': 'padding-box', + '-webkit-border-radius': opt.cursorborderradius, + '-moz-border-radius': opt.cursorborderradius, + 'border-radius': opt.cursorborderradius + }); + + cursor.addClass('nicescroll-cursors'); + + self.cursor = cursor; + + var rail = $(_doc.createElement('div')); + rail.attr('id', self.id); + rail.addClass('nicescroll-rails nicescroll-rails-vr'); + + if (opt.scrollCLass) { + rail.addClass(opt.scrollCLass); + } + + var v, a, kp = ["left", "right", "top", "bottom"]; //** + for (var n in kp) { + a = kp[n]; + v = opt.railpadding[a] || 0; + v && rail.css("padding-" + a, v + "px"); + } + + rail.append(cursor); + + rail.width = Math.max(parseFloat(opt.cursorwidth), cursor.outerWidth()); + rail.css({ + width: rail.width + "px", + zIndex: self.zindex, + background: opt.background, + cursor: "default" + }); + + rail.visibility = true; + rail.scrollable = true; + + rail.align = (opt.railalign == "left") ? 0 : 1; + + self.rail = rail; + + self.rail.drag = false; + + var zoom = false; + if (opt.boxzoom && !self.ispage && !cap.isieold) { + zoom = _doc.createElement('div'); + + self.bind(zoom, "click", self.doZoom); + self.bind(zoom, "mouseenter", function () { + self.zoom.css('opacity', opt.cursoropacitymax); + }); + self.bind(zoom, "mouseleave", function () { + self.zoom.css('opacity', opt.cursoropacitymin); + }); + + self.zoom = $(zoom); + self.zoom.css({ + cursor: "pointer", + zIndex: self.zindex, + backgroundImage: 'url(' + opt.scriptpath + 'zoomico.png)', + height: 18, + width: 18, + backgroundPosition: '0 0' + }); + if (opt.dblclickzoom) self.bind(self.win, "dblclick", self.doZoom); + if (cap.cantouch && opt.gesturezoom) { + self.ongesturezoom = function (e) { + if (e.scale > 1.5) self.doZoomIn(e); + if (e.scale < 0.8) self.doZoomOut(e); + return self.cancelEvent(e); + }; + self.bind(self.win, "gestureend", self.ongesturezoom); + } + } + + // init HORIZ + + self.railh = false; + var railh; + + if (opt.horizrailenabled) { + + self.css(cont, { + overflowX: 'hidden' + }); + + cursor = $(_doc.createElement('div')); + cursor.css({ + position: "absolute", + top: 0, + height: opt.cursorwidth, + width: 0, + backgroundColor: opt.cursorcolor, + border: opt.cursorborder, + backgroundClip: 'padding-box', + '-webkit-border-radius': opt.cursorborderradius, + '-moz-border-radius': opt.cursorborderradius, + 'border-radius': opt.cursorborderradius + }); + + if (cap.isieold) cursor.css('overflow', 'hidden'); //IE6 horiz scrollbar issue + + cursor.addClass('nicescroll-cursors'); + + self.cursorh = cursor; + + railh = $(_doc.createElement('div')); + railh.attr('id', self.id + '-hr'); + railh.addClass('nicescroll-rails nicescroll-rails-hr'); + if (opt.scrollCLass) { + railh.addClass(opt.scrollCLass); + } + + railh.height = Math.max(parseFloat(opt.cursorwidth), cursor.outerHeight()); + railh.css({ + height: railh.height + "px", + 'zIndex': self.zindex, + "background": opt.background + }); + + railh.append(cursor); + + railh.visibility = true; + railh.scrollable = true; + + railh.align = (opt.railvalign == "top") ? 0 : 1; + + self.railh = railh; + + self.railh.drag = false; + + } + + if (self.ispage) { + + rail.css({ + position: "fixed", + top: 0, + height: "100%" + }); + + rail.css((rail.align) ? { right: 0 } : { left: 0 }); + + self.body.append(rail); + if (self.railh) { + railh.css({ + position: "fixed", + left: 0, + width: "100%" + }); + + railh.css((railh.align) ? { bottom: 0 } : { top: 0 }); + + self.body.append(railh); + } + } else { + if (self.ishwscroll) { + if (self.win.css('position') == 'static') self.css(self.win, { 'position': 'relative' }); + var bd = (self.win[0].nodeName == 'HTML') ? self.body : self.win; + $(bd).scrollTop(0).scrollLeft(0); // fix rail position if content already scrolled + if (self.zoom) { + self.zoom.css({ + position: "absolute", + top: 1, + right: 0, + "margin-right": rail.width + 4 + }); + bd.append(self.zoom); + } + rail.css({ + position: "absolute", + top: 0 + }); + rail.css((rail.align) ? { right: 0 } : { left: 0 }); + bd.append(rail); + if (railh) { + railh.css({ + position: "absolute", + left: 0, + bottom: 0 + }); + railh.css((railh.align) ? { bottom: 0 } : { top: 0 }); + bd.append(railh); + } + } else { + self.isfixed = (self.win.css("position") == "fixed"); + var rlpos = (self.isfixed) ? "fixed" : "absolute"; + + if (!self.isfixed) self.viewport = self.getViewport(self.win[0]); + if (self.viewport) { + self.body = self.viewport; + if (!(/fixed|absolute/.test(self.viewport.css("position")))) self.css(self.viewport, { + "position": "relative" + }); + } + + rail.css({ + position: rlpos + }); + if (self.zoom) self.zoom.css({ + position: rlpos + }); + self.updateScrollBar(); + self.body.append(rail); + if (self.zoom) self.body.append(self.zoom); + if (self.railh) { + railh.css({ + position: rlpos + }); + self.body.append(railh); + } + } + + if (cap.isios) self.css(self.win, { + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)', + '-webkit-touch-callout': 'none' + }); // prevent grey layer on click + + if (opt.disableoutline) { + if (cap.isie) self.win.attr("hideFocus", "true"); // IE, prevent dotted rectangle on focused div + if (cap.iswebkit) self.win.css('outline', 'none'); // Webkit outline + } + + } + + if (opt.autohidemode === false) { + self.autohidedom = false; + self.rail.css({ + opacity: opt.cursoropacitymax + }); + if (self.railh) self.railh.css({ + opacity: opt.cursoropacitymax + }); + } else if ((opt.autohidemode === true) || (opt.autohidemode === "leave")) { + self.autohidedom = $().add(self.rail); + if (cap.isie8) self.autohidedom = self.autohidedom.add(self.cursor); + if (self.railh) self.autohidedom = self.autohidedom.add(self.railh); + if (self.railh && cap.isie8) self.autohidedom = self.autohidedom.add(self.cursorh); + } else if (opt.autohidemode == "scroll") { + self.autohidedom = $().add(self.rail); + if (self.railh) self.autohidedom = self.autohidedom.add(self.railh); + } else if (opt.autohidemode == "cursor") { + self.autohidedom = $().add(self.cursor); + if (self.railh) self.autohidedom = self.autohidedom.add(self.cursorh); + } else if (opt.autohidemode == "hidden") { + self.autohidedom = false; + self.hide(); + self.railslocked = false; + } + + if (cap.cantouch || self.istouchcapable || opt.emulatetouch || cap.hasmstouch) { + + self.scrollmom = new ScrollMomentumClass2D(self); + + var delayedclick = null; + + self.ontouchstart = function (e) { + + if (self.locked) return false; + + //if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false; + if (e.pointerType && (e.pointerType === 'mouse' || e.pointerType === e.MSPOINTER_TYPE_MOUSE)) return false; // need test on surface!! + + self.hasmoving = false; + + if (self.scrollmom.timer) { + self.triggerScrollEnd(); + self.scrollmom.stop(); + } + + if (!self.railslocked) { + var tg = self.getTarget(e); + + if (tg) { + var skp = (/INPUT/i.test(tg.nodeName)) && (/range/i.test(tg.type)); + if (skp) return self.stopPropagation(e); + } + + var ismouse = (e.type === "mousedown"); + + if (!("clientX" in e) && ("changedTouches" in e)) { + e.clientX = e.changedTouches[0].clientX; + e.clientY = e.changedTouches[0].clientY; + } + + if (self.forcescreen) { + var le = e; + e = { + "original": (e.original) ? e.original : e + }; + e.clientX = le.screenX; + e.clientY = le.screenY; + } + + self.rail.drag = { + x: e.clientX, + y: e.clientY, + sx: self.scroll.x, + sy: self.scroll.y, + st: self.getScrollTop(), + sl: self.getScrollLeft(), + pt: 2, + dl: false, + tg: tg + }; + + if (self.ispage || !opt.directionlockdeadzone) { + + self.rail.drag.dl = "f"; + + } else { + + var view = { + w: $window.width(), + h: $window.height() + }; + + var page = self.getContentSize(); + + var maxh = page.h - view.h; + var maxw = page.w - view.w; + + if (self.rail.scrollable && !self.railh.scrollable) self.rail.drag.ck = (maxh > 0) ? "v" : false; + else if (!self.rail.scrollable && self.railh.scrollable) self.rail.drag.ck = (maxw > 0) ? "h" : false; + else self.rail.drag.ck = false; + + } + + if (opt.emulatetouch && self.isiframe && cap.isie) { + var wp = self.win.position(); + self.rail.drag.x += wp.left; + self.rail.drag.y += wp.top; + } + + self.hasmoving = false; + self.lastmouseup = false; + self.scrollmom.reset(e.clientX, e.clientY); + + if (tg&&ismouse) { + + var ip = /INPUT|SELECT|BUTTON|TEXTAREA/i.test(tg.nodeName); + if (!ip) { + if (cap.hasmousecapture) tg.setCapture(); + if (opt.emulatetouch) { + if (tg.onclick && !(tg._onclick || false)) { // intercept DOM0 onclick event + tg._onclick = tg.onclick; + tg.onclick = function (e) { + if (self.hasmoving) return false; + tg._onclick.call(this, e); + }; + } + return self.cancelEvent(e); + } + return self.stopPropagation(e); + } + + if (/SUBMIT|CANCEL|BUTTON/i.test($(tg).attr('type'))) { + self.preventclick = { + "tg": tg, + "click": false + }; + } + + } + } + + }; + + self.ontouchend = function (e) { + + if (!self.rail.drag) return true; + + if (self.rail.drag.pt == 2) { + //if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false; + if (e.pointerType && (e.pointerType === 'mouse' || e.pointerType === e.MSPOINTER_TYPE_MOUSE)) return false; + + self.rail.drag = false; + + var ismouse = (e.type === "mouseup"); + + if (self.hasmoving) { + self.scrollmom.doMomentum(); + self.lastmouseup = true; + self.hideCursor(); + if (cap.hasmousecapture) _doc.releaseCapture(); + if (ismouse) return self.cancelEvent(e); + } + + } + else if (self.rail.drag.pt == 1) { + return self.onmouseup(e); + } + + }; + + var moveneedoffset = (opt.emulatetouch && self.isiframe && !cap.hasmousecapture); + + var locktollerance = opt.directionlockdeadzone * 0.3 | 0; + + self.ontouchmove = function (e, byiframe) { + + if (!self.rail.drag) return true; + + if (e.targetTouches && opt.preventmultitouchscrolling) { + if (e.targetTouches.length > 1) return true; // multitouch + } + + //if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false; + if (e.pointerType && (e.pointerType === 'mouse' || e.pointerType === e.MSPOINTER_TYPE_MOUSE)) return true; + + if (self.rail.drag.pt == 2) { + + if (("changedTouches" in e)) { + e.clientX = e.changedTouches[0].clientX; + e.clientY = e.changedTouches[0].clientY; + } + + var ofy, ofx; + ofx = ofy = 0; + + if (moveneedoffset && !byiframe) { + var wp = self.win.position(); + ofx = -wp.left; + ofy = -wp.top; + } + + var fy = e.clientY + ofy; + var my = (fy - self.rail.drag.y); + var fx = e.clientX + ofx; + var mx = (fx - self.rail.drag.x); + + var ny = self.rail.drag.st - my; + + if (self.ishwscroll && opt.bouncescroll) { + if (ny < 0) { + ny = Math.round(ny / 2); + } else if (ny > self.page.maxh) { + ny = self.page.maxh + Math.round((ny - self.page.maxh) / 2); + } + } else { + if (ny < 0) { + ny = 0; + fy = 0; + } + else if (ny > self.page.maxh) { + ny = self.page.maxh; + fy = 0; + } + if (fy === 0 && !self.hasmoving) { + if (!self.ispage) self.rail.drag = false; + return true; + } + } + + var nx = self.getScrollLeft(); + + if (self.railh && self.railh.scrollable) { + nx = (self.isrtlmode) ? mx - self.rail.drag.sl : self.rail.drag.sl - mx; + + if (self.ishwscroll && opt.bouncescroll) { + if (nx < 0) { + nx = Math.round(nx / 2); + } else if (nx > self.page.maxw) { + nx = self.page.maxw + Math.round((nx - self.page.maxw) / 2); + } + } else { + if (nx < 0) { + nx = 0; + fx = 0; + } + if (nx > self.page.maxw) { + nx = self.page.maxw; + fx = 0; + } + } + + } + + + if (!self.hasmoving) { + + if (self.rail.drag.y === e.clientY && self.rail.drag.x === e.clientX) return self.cancelEvent(e); // prevent first useless move event + + var ay = Math.abs(my); + var ax = Math.abs(mx); + var dz = opt.directionlockdeadzone; + + if (!self.rail.drag.ck) { + if (ay > dz && ax > dz) self.rail.drag.dl = "f"; + else if (ay > dz) self.rail.drag.dl = (ax > locktollerance) ? "f" : "v"; + else if (ax > dz) self.rail.drag.dl = (ay > locktollerance) ? "f" : "h"; + } + else if (self.rail.drag.ck == "v") { + if (ax > dz && ay <= locktollerance) { + self.rail.drag = false; + } + else if (ay > dz) self.rail.drag.dl = "v"; + + } + else if (self.rail.drag.ck == "h") { + + if (ay > dz && ax <= locktollerance) { + self.rail.drag = false; + } + else if (ax > dz) self.rail.drag.dl = "h"; + + } + + if (!self.rail.drag.dl) return self.cancelEvent(e); + + self.triggerScrollStart(e.clientX, e.clientY, 0, 0, 0); + self.hasmoving = true; + } + + if (self.preventclick && !self.preventclick.click) { + self.preventclick.click = self.preventclick.tg.onclick || false; + self.preventclick.tg.onclick = self.onpreventclick; + } + + if (self.rail.drag.dl) { + if (self.rail.drag.dl == "v") nx = self.rail.drag.sl; + else if (self.rail.drag.dl == "h") ny = self.rail.drag.st; + } + + self.synched("touchmove", function () { + if (self.rail.drag && (self.rail.drag.pt == 2)) { + if (self.prepareTransition) self.resetTransition(); + if (self.rail.scrollable) self.setScrollTop(ny); + self.scrollmom.update(fx, fy); + if (self.railh && self.railh.scrollable) { + self.setScrollLeft(nx); + self.showCursor(ny, nx); + } else { + self.showCursor(ny); + } + if (cap.isie10) _doc.selection.clear(); + } + }); + + return self.cancelEvent(e); + + } + else if (self.rail.drag.pt == 1) { // drag on cursor + return self.onmousemove(e); + } + + }; + + self.ontouchstartCursor = function (e, hronly) { + if (self.rail.drag && self.rail.drag.pt != 3) return; + if (self.locked) return self.cancelEvent(e); + self.cancelScroll(); + self.rail.drag = { + x: e.touches[0].clientX, + y: e.touches[0].clientY, + sx: self.scroll.x, + sy: self.scroll.y, + pt: 3, + hr: (!!hronly) + }; + var tg = self.getTarget(e); + if (!self.ispage && cap.hasmousecapture) tg.setCapture(); + if (self.isiframe && !cap.hasmousecapture) { + self.saved.csspointerevents = self.doc.css("pointer-events"); + self.css(self.doc, { "pointer-events": "none" }); + } + return self.cancelEvent(e); + }; + + self.ontouchendCursor = function (e) { + if (self.rail.drag) { + if (cap.hasmousecapture) _doc.releaseCapture(); + if (self.isiframe && !cap.hasmousecapture) self.doc.css("pointer-events", self.saved.csspointerevents); + if (self.rail.drag.pt != 3) return; + self.rail.drag = false; + return self.cancelEvent(e); + } + }; + + self.ontouchmoveCursor = function (e) { + if (self.rail.drag) { + if (self.rail.drag.pt != 3) return; + + self.cursorfreezed = true; + + if (self.rail.drag.hr) { + self.scroll.x = self.rail.drag.sx + (e.touches[0].clientX - self.rail.drag.x); + if (self.scroll.x < 0) self.scroll.x = 0; + var mw = self.scrollvaluemaxw; + if (self.scroll.x > mw) self.scroll.x = mw; + } else { + self.scroll.y = self.rail.drag.sy + (e.touches[0].clientY - self.rail.drag.y); + if (self.scroll.y < 0) self.scroll.y = 0; + var my = self.scrollvaluemax; + if (self.scroll.y > my) self.scroll.y = my; + } + + self.synched('touchmove', function () { + if (self.rail.drag && (self.rail.drag.pt == 3)) { + self.showCursor(); + if (self.rail.drag.hr) self.doScrollLeft(Math.round(self.scroll.x * self.scrollratio.x), opt.cursordragspeed); + else self.doScrollTop(Math.round(self.scroll.y * self.scrollratio.y), opt.cursordragspeed); + } + }); + + return self.cancelEvent(e); + } + + }; + + } + + self.onmousedown = function (e, hronly) { + if (self.rail.drag && self.rail.drag.pt != 1) return; + if (self.railslocked) return self.cancelEvent(e); + self.cancelScroll(); + self.rail.drag = { + x: e.clientX, + y: e.clientY, + sx: self.scroll.x, + sy: self.scroll.y, + pt: 1, + hr: hronly || false + }; + var tg = self.getTarget(e); + + if (cap.hasmousecapture) tg.setCapture(); + if (self.isiframe && !cap.hasmousecapture) { + self.saved.csspointerevents = self.doc.css("pointer-events"); + self.css(self.doc, { + "pointer-events": "none" + }); + } + self.hasmoving = false; + return self.cancelEvent(e); + }; + + self.onmouseup = function (e) { + if (self.rail.drag) { + if (self.rail.drag.pt != 1) return true; + + if (cap.hasmousecapture) _doc.releaseCapture(); + if (self.isiframe && !cap.hasmousecapture) self.doc.css("pointer-events", self.saved.csspointerevents); + self.rail.drag = false; + self.cursorfreezed = false; + if (self.hasmoving) self.triggerScrollEnd(); + return self.cancelEvent(e); + } + }; + + self.onmousemove = function (e) { + if (self.rail.drag) { + if (self.rail.drag.pt !== 1) return; + + if (cap.ischrome && e.which === 0) return self.onmouseup(e); + + self.cursorfreezed = true; + + if (!self.hasmoving) self.triggerScrollStart(e.clientX, e.clientY, 0, 0, 0); + + self.hasmoving = true; + + if (self.rail.drag.hr) { + self.scroll.x = self.rail.drag.sx + (e.clientX - self.rail.drag.x); + if (self.scroll.x < 0) self.scroll.x = 0; + var mw = self.scrollvaluemaxw; + if (self.scroll.x > mw) self.scroll.x = mw; + } else { + self.scroll.y = self.rail.drag.sy + (e.clientY - self.rail.drag.y); + if (self.scroll.y < 0) self.scroll.y = 0; + var my = self.scrollvaluemax; + if (self.scroll.y > my) self.scroll.y = my; + } + + self.synched('mousemove', function () { + + if (self.cursorfreezed) { + self.showCursor(); + + if (self.rail.drag.hr) { + self.scrollLeft(Math.round(self.scroll.x * self.scrollratio.x)); + } else { + self.scrollTop(Math.round(self.scroll.y * self.scrollratio.y)); + } + + } + }); + + return self.cancelEvent(e); + } + else { + self.checkarea = 0; + } + }; + + if (cap.cantouch || opt.emulatetouch) { + + self.onpreventclick = function (e) { + if (self.preventclick) { + self.preventclick.tg.onclick = self.preventclick.click; + self.preventclick = false; + return self.cancelEvent(e); + } + }; + + self.onclick = (cap.isios) ? false : function (e) { // it needs to check IE11 ??? + if (self.lastmouseup) { + self.lastmouseup = false; + return self.cancelEvent(e); + } else { + return true; + } + }; + + if (opt.grabcursorenabled && cap.cursorgrabvalue) { + self.css((self.ispage) ? self.doc : self.win, { + 'cursor': cap.cursorgrabvalue + }); + self.css(self.rail, { + 'cursor': cap.cursorgrabvalue + }); + } + + } else { + + var checkSelectionScroll = function (e) { + if (!self.selectiondrag) return; + + if (e) { + var ww = self.win.outerHeight(); + var df = (e.pageY - self.selectiondrag.top); + if (df > 0 && df < ww) df = 0; + if (df >= ww) df -= ww; + self.selectiondrag.df = df; + } + if (self.selectiondrag.df === 0) return; + + var rt = -(self.selectiondrag.df*2/6)|0; + self.doScrollBy(rt); + + self.debounced("doselectionscroll", function () { + checkSelectionScroll(); + }, 50); + }; + + if ("getSelection" in _doc) { // A grade - Major browsers + self.hasTextSelected = function () { + return (_doc.getSelection().rangeCount > 0); + }; + } else if ("selection" in _doc) { //IE9- + self.hasTextSelected = function () { + return (_doc.selection.type != "None"); + }; + } else { + self.hasTextSelected = function () { // no support + return false; + }; + } + + self.onselectionstart = function (e) { + // More testing - severe chrome issues + /* + if (!self.haswrapper&&(e.which&&e.which==2)) { // fool browser to manage middle button scrolling + self.win.css({'overflow':'auto'}); + setTimeout(function(){ + self.win.css({'overflow':'hidden'}); + },10); + return true; + } + */ + if (self.ispage) return; + self.selectiondrag = self.win.offset(); + }; + + self.onselectionend = function (e) { + self.selectiondrag = false; + }; + self.onselectiondrag = function (e) { + if (!self.selectiondrag) return; + if (self.hasTextSelected()) self.debounced("selectionscroll", function () { + checkSelectionScroll(e); + }, 250); + }; + } + + if (cap.hasw3ctouch) { //IE11+ + self.css((self.ispage) ? $("html") : self.win, { 'touch-action': 'none' }); + self.css(self.rail, { + 'touch-action': 'none' + }); + self.css(self.cursor, { + 'touch-action': 'none' + }); + self.bind(self.win, "pointerdown", self.ontouchstart); + self.bind(_doc, "pointerup", self.ontouchend); + self.delegate(_doc, "pointermove", self.ontouchmove); + } else if (cap.hasmstouch) { //IE10 + self.css((self.ispage) ? $("html") : self.win, { '-ms-touch-action': 'none' }); + self.css(self.rail, { + '-ms-touch-action': 'none' + }); + self.css(self.cursor, { + '-ms-touch-action': 'none' + }); + self.bind(self.win, "MSPointerDown", self.ontouchstart); + self.bind(_doc, "MSPointerUp", self.ontouchend); + self.delegate(_doc, "MSPointerMove", self.ontouchmove); + self.bind(self.cursor, "MSGestureHold", function (e) { + e.preventDefault(); + }); + self.bind(self.cursor, "contextmenu", function (e) { + e.preventDefault(); + }); + } else if (cap.cantouch) { // smartphones/touch devices + self.bind(self.win, "touchstart", self.ontouchstart, false, true); + self.bind(_doc, "touchend", self.ontouchend, false, true); + self.bind(_doc, "touchcancel", self.ontouchend, false, true); + self.delegate(_doc, "touchmove", self.ontouchmove, false, true); + } + + if (opt.emulatetouch) { + self.bind(self.win, "mousedown", self.ontouchstart, false, true); + self.bind(_doc, "mouseup", self.ontouchend, false, true); + self.bind(_doc, "mousemove", self.ontouchmove, false, true); + } + + if (opt.cursordragontouch || (!cap.cantouch && !opt.emulatetouch)) { + + self.rail.css({ + cursor: "default" + }); + self.railh && self.railh.css({ + cursor: "default" + }); + + self.jqbind(self.rail, "mouseenter", function () { + if (!self.ispage && !self.win.is(":visible")) return false; + if (self.canshowonmouseevent) self.showCursor(); + self.rail.active = true; + }); + self.jqbind(self.rail, "mouseleave", function () { + self.rail.active = false; + if (!self.rail.drag) self.hideCursor(); + }); + + if (opt.sensitiverail) { + self.bind(self.rail, "click", function (e) { + self.doRailClick(e, false, false); + }); + self.bind(self.rail, "dblclick", function (e) { + self.doRailClick(e, true, false); + }); + self.bind(self.cursor, "click", function (e) { + self.cancelEvent(e); + }); + self.bind(self.cursor, "dblclick", function (e) { + self.cancelEvent(e); + }); + } + + if (self.railh) { + self.jqbind(self.railh, "mouseenter", function () { + if (!self.ispage && !self.win.is(":visible")) return false; + if (self.canshowonmouseevent) self.showCursor(); + self.rail.active = true; + }); + self.jqbind(self.railh, "mouseleave", function () { + self.rail.active = false; + if (!self.rail.drag) self.hideCursor(); + }); + + if (opt.sensitiverail) { + self.bind(self.railh, "click", function (e) { + self.doRailClick(e, false, true); + }); + self.bind(self.railh, "dblclick", function (e) { + self.doRailClick(e, true, true); + }); + self.bind(self.cursorh, "click", function (e) { + self.cancelEvent(e); + }); + self.bind(self.cursorh, "dblclick", function (e) { + self.cancelEvent(e); + }); + } + + } + + } + + if (opt.cursordragontouch && (this.istouchcapable || cap.cantouch)) { + self.bind(self.cursor, "touchstart", self.ontouchstartCursor); + self.bind(self.cursor, "touchmove", self.ontouchmoveCursor); + self.bind(self.cursor, "touchend", self.ontouchendCursor); + self.cursorh && self.bind(self.cursorh, "touchstart", function (e) { + self.ontouchstartCursor(e, true); + }); + self.cursorh && self.bind(self.cursorh, "touchmove", self.ontouchmoveCursor); + self.cursorh && self.bind(self.cursorh, "touchend", self.ontouchendCursor); + } + +// if (!cap.cantouch && !opt.emulatetouch) { + if (!opt.emulatetouch && !cap.isandroid && !cap.isios) { + + self.bind((cap.hasmousecapture) ? self.win : _doc, "mouseup", self.onmouseup); + self.bind(_doc, "mousemove", self.onmousemove); + if (self.onclick) self.bind(_doc, "click", self.onclick); + + self.bind(self.cursor, "mousedown", self.onmousedown); + self.bind(self.cursor, "mouseup", self.onmouseup); + + if (self.railh) { + self.bind(self.cursorh, "mousedown", function (e) { + self.onmousedown(e, true); + }); + self.bind(self.cursorh, "mouseup", self.onmouseup); + } + + if (!self.ispage && opt.enablescrollonselection) { + self.bind(self.win[0], "mousedown", self.onselectionstart); + self.bind(_doc, "mouseup", self.onselectionend); + self.bind(self.cursor, "mouseup", self.onselectionend); + if (self.cursorh) self.bind(self.cursorh, "mouseup", self.onselectionend); + self.bind(_doc, "mousemove", self.onselectiondrag); + } + + if (self.zoom) { + self.jqbind(self.zoom, "mouseenter", function () { + if (self.canshowonmouseevent) self.showCursor(); + self.rail.active = true; + }); + self.jqbind(self.zoom, "mouseleave", function () { + self.rail.active = false; + if (!self.rail.drag) self.hideCursor(); + }); + } + + } else { + + self.bind((cap.hasmousecapture) ? self.win : _doc, "mouseup", self.ontouchend); + if (self.onclick) self.bind(_doc, "click", self.onclick); + + if (opt.cursordragontouch) { + self.bind(self.cursor, "mousedown", self.onmousedown); + self.bind(self.cursor, "mouseup", self.onmouseup); + self.cursorh && self.bind(self.cursorh, "mousedown", function (e) { + self.onmousedown(e, true); + }); + self.cursorh && self.bind(self.cursorh, "mouseup", self.onmouseup); + } else { + self.bind(self.rail, "mousedown", function (e) { e.preventDefault(); }); // prevent text selection + self.railh && self.bind(self.railh, "mousedown", function (e) { e.preventDefault(); }); + } + + } + + + if (opt.enablemousewheel) { + if (!self.isiframe) self.mousewheel((cap.isie && self.ispage) ? _doc : self.win, self.onmousewheel); + self.mousewheel(self.rail, self.onmousewheel); + if (self.railh) self.mousewheel(self.railh, self.onmousewheelhr); + } + + if (!self.ispage && !cap.cantouch && !(/HTML|^BODY/.test(self.win[0].nodeName))) { + if (!self.win.attr("tabindex")) self.win.attr({ + "tabindex": ++tabindexcounter + }); + + self.bind(self.win, "focus", function (e) { // better using native events + domfocus = (self.getTarget(e)).id || self.getTarget(e) || false; + self.hasfocus = true; + if (self.canshowonmouseevent) self.noticeCursor(); + }); + self.bind(self.win, "blur", function (e) { // * + domfocus = false; + self.hasfocus = false; + }); + + self.bind(self.win, "mouseenter", function (e) { // * + mousefocus = (self.getTarget(e)).id || self.getTarget(e) || false; + self.hasmousefocus = true; + if (self.canshowonmouseevent) self.noticeCursor(); + }); + self.bind(self.win, "mouseleave", function (e) { // * + mousefocus = false; + self.hasmousefocus = false; + if (!self.rail.drag) self.hideCursor(); + }); + + } + + + //Thanks to http://www.quirksmode.org !! + self.onkeypress = function (e) { + if (self.railslocked && self.page.maxh === 0) return true; + + e = e || _win.event; + var tg = self.getTarget(e); + if (tg && /INPUT|TEXTAREA|SELECT|OPTION/.test(tg.nodeName)) { + var tp = tg.getAttribute('type') || tg.type || false; + if ((!tp) || !(/submit|button|cancel/i.tp)) return true; + } + + if ($(tg).attr('contenteditable')) return true; + + if (self.hasfocus || (self.hasmousefocus && !domfocus) || (self.ispage && !domfocus && !mousefocus)) { + var key = e.keyCode; + + if (self.railslocked && key != 27) return self.cancelEvent(e); + + var ctrl = e.ctrlKey || false; + var shift = e.shiftKey || false; + + var ret = false; + switch (key) { + case 38: + case 63233: //safari + self.doScrollBy(24 * 3); + ret = true; + break; + case 40: + case 63235: //safari + self.doScrollBy(-24 * 3); + ret = true; + break; + case 37: + case 63232: //safari + if (self.railh) { + (ctrl) ? self.doScrollLeft(0) : self.doScrollLeftBy(24 * 3); + ret = true; + } + break; + case 39: + case 63234: //safari + if (self.railh) { + (ctrl) ? self.doScrollLeft(self.page.maxw) : self.doScrollLeftBy(-24 * 3); + ret = true; + } + break; + case 33: + case 63276: // safari + self.doScrollBy(self.view.h); + ret = true; + break; + case 34: + case 63277: // safari + self.doScrollBy(-self.view.h); + ret = true; + break; + case 36: + case 63273: // safari + (self.railh && ctrl) ? self.doScrollPos(0, 0) : self.doScrollTo(0); + ret = true; + break; + case 35: + case 63275: // safari + (self.railh && ctrl) ? self.doScrollPos(self.page.maxw, self.page.maxh) : self.doScrollTo(self.page.maxh); + ret = true; + break; + case 32: + if (opt.spacebarenabled) { + (shift) ? self.doScrollBy(self.view.h) : self.doScrollBy(-self.view.h); + ret = true; + } + break; + case 27: // ESC + if (self.zoomactive) { + self.doZoom(); + ret = true; + } + break; + } + if (ret) return self.cancelEvent(e); + } + }; + + if (opt.enablekeyboard) self.bind(_doc, (cap.isopera && !cap.isopera12) ? "keypress" : "keydown", self.onkeypress); + + self.bind(_doc, "keydown", function (e) { + var ctrl = e.ctrlKey || false; + if (ctrl) self.wheelprevented = true; + }); + self.bind(_doc, "keyup", function (e) { + var ctrl = e.ctrlKey || false; + if (!ctrl) self.wheelprevented = false; + }); + self.bind(_win, "blur", function (e) { + self.wheelprevented = false; + }); + + self.bind(_win, 'resize', self.onscreenresize); + self.bind(_win, 'orientationchange', self.onscreenresize); + + self.bind(_win, "load", self.lazyResize); + + if (cap.ischrome && !self.ispage && !self.haswrapper) { //chrome void scrollbar bug - it persists in version 26 + var tmp = self.win.attr("style"); + var ww = parseFloat(self.win.css("width")) + 1; + self.win.css('width', ww); + self.synched("chromefix", function () { + self.win.attr("style", tmp); + }); + } + + + // Trying a cross-browser implementation - good luck! + + self.onAttributeChange = function (e) { + self.lazyResize(self.isieold ? 250 : 30); + }; + + if (opt.enableobserver) { + + if ((!self.isie11) && (ClsMutationObserver !== false)) { // IE11 crashes #568 + self.observerbody = new ClsMutationObserver(function (mutations) { + mutations.forEach(function (mut) { + if (mut.type == "attributes") { + return ($body.hasClass("modal-open") && $body.hasClass("modal-dialog") && !$.contains($('.modal-dialog')[0], self.doc[0])) ? self.hide() : self.show(); // Support for Bootstrap modal; Added check if the nice scroll element is inside a modal + } + }); + if (self.me.clientWidth != self.page.width || self.me.clientHeight != self.page.height) return self.lazyResize(30); + }); + self.observerbody.observe(_doc.body, { + childList: true, + subtree: true, + characterData: false, + attributes: true, + attributeFilter: ['class'] + }); + } + + if (!self.ispage && !self.haswrapper) { + + var _dom = self.win[0]; + + // redesigned MutationObserver for Chrome18+/Firefox14+/iOS6+ with support for: remove div, add/remove content + if (ClsMutationObserver !== false) { + self.observer = new ClsMutationObserver(function (mutations) { + mutations.forEach(self.onAttributeChange); + }); + self.observer.observe(_dom, { + childList: true, + characterData: false, + attributes: true, + subtree: false + }); + self.observerremover = new ClsMutationObserver(function (mutations) { + mutations.forEach(function (mo) { + if (mo.removedNodes.length > 0) { + for (var dd in mo.removedNodes) { + if (!!self && (mo.removedNodes[dd] === _dom)) return self.remove(); + } + } + }); + }); + self.observerremover.observe(_dom.parentNode, { + childList: true, + characterData: false, + attributes: false, + subtree: false + }); + } else { + self.bind(_dom, (cap.isie && !cap.isie9) ? "propertychange" : "DOMAttrModified", self.onAttributeChange); + if (cap.isie9) _dom.attachEvent("onpropertychange", self.onAttributeChange); //IE9 DOMAttrModified bug + self.bind(_dom, "DOMNodeRemoved", function (e) { + if (e.target === _dom) self.remove(); + }); + } + } + + } + + // + + if (!self.ispage && opt.boxzoom) self.bind(_win, "resize", self.resizeZoom); + if (self.istextarea) { + self.bind(self.win, "keydown", self.lazyResize); + self.bind(self.win, "mouseup", self.lazyResize); + } + + self.lazyResize(30); + + } + + if (this.doc[0].nodeName == 'IFRAME') { + var oniframeload = function () { + self.iframexd = false; + var doc; + try { + doc = 'contentDocument' in this ? this.contentDocument : this.contentWindow._doc; + var a = doc.domain; + } catch (e) { + self.iframexd = true; + doc = false; + } + + if (self.iframexd) { + if ("console" in _win) console.log('NiceScroll error: policy restriced iframe'); + return true; //cross-domain - I can't manage this + } + + self.forcescreen = true; + + if (self.isiframe) { + self.iframe = { + "doc": $(doc), + "html": self.doc.contents().find('html')[0], + "body": self.doc.contents().find('body')[0] + }; + self.getContentSize = function () { + return { + w: Math.max(self.iframe.html.scrollWidth, self.iframe.body.scrollWidth), + h: Math.max(self.iframe.html.scrollHeight, self.iframe.body.scrollHeight) + }; + }; + self.docscroll = $(self.iframe.body); + } + + if (!cap.isios && opt.iframeautoresize && !self.isiframe) { + self.win.scrollTop(0); // reset position + self.doc.height(""); //reset height to fix browser bug + var hh = Math.max(doc.getElementsByTagName('html')[0].scrollHeight, doc.body.scrollHeight); + self.doc.height(hh); + } + self.lazyResize(30); + + self.css($(self.iframe.body), _scrollyhidden); + + if (cap.isios && self.haswrapper) { + self.css($(doc.body), { + '-webkit-transform': 'translate3d(0,0,0)' + }); // avoid iFrame content clipping - thanks to http://blog.derraab.com/2012/04/02/avoid-iframe-content-clipping-with-css-transform-on-ios/ + } + + if ('contentWindow' in this) { + self.bind(this.contentWindow, "scroll", self.onscroll); //IE8 & minor + } else { + self.bind(doc, "scroll", self.onscroll); + } + + if (opt.enablemousewheel) { + self.mousewheel(doc, self.onmousewheel); + } + + if (opt.enablekeyboard) self.bind(doc, (cap.isopera) ? "keypress" : "keydown", self.onkeypress); + + if (cap.cantouch) { + self.bind(doc, "touchstart", self.ontouchstart); + self.bind(doc, "touchmove", self.ontouchmove); + } + else if (opt.emulatetouch) { + self.bind(doc, "mousedown", self.ontouchstart); + self.bind(doc, "mousemove", function (e) { + return self.ontouchmove(e, true); + }); + if (opt.grabcursorenabled && cap.cursorgrabvalue) self.css($(doc.body), { + 'cursor': cap.cursorgrabvalue + }); + } + + self.bind(doc, "mouseup", self.ontouchend); + + if (self.zoom) { + if (opt.dblclickzoom) self.bind(doc, 'dblclick', self.doZoom); + if (self.ongesturezoom) self.bind(doc, "gestureend", self.ongesturezoom); + } + }; + + if (this.doc[0].readyState && this.doc[0].readyState === "complete") { + setTimeout(function () { + oniframeload.call(self.doc[0], false); + }, 500); + } + self.bind(this.doc, "load", oniframeload); + + } + + }; + + this.showCursor = function (py, px) { + if (self.cursortimeout) { + clearTimeout(self.cursortimeout); + self.cursortimeout = 0; + } + if (!self.rail) return; + if (self.autohidedom) { + self.autohidedom.stop().css({ + opacity: opt.cursoropacitymax + }); + self.cursoractive = true; + } + + if (!self.rail.drag || self.rail.drag.pt != 1) { + if (py !== undefined && py !== false) { + self.scroll.y = (py / self.scrollratio.y) | 0; + } + if (px !== undefined) { + self.scroll.x = (px / self.scrollratio.x) | 0; + } + } + + self.cursor.css({ + height: self.cursorheight, + top: self.scroll.y + }); + if (self.cursorh) { + var lx = (self.hasreversehr) ? self.scrollvaluemaxw - self.scroll.x : self.scroll.x; + self.cursorh.css({ + width: self.cursorwidth, + left: (!self.rail.align && self.rail.visibility) ? lx + self.rail.width : lx + }); + self.cursoractive = true; + } + + if (self.zoom) self.zoom.stop().css({ + opacity: opt.cursoropacitymax + }); + }; + + this.hideCursor = function (tm) { + if (self.cursortimeout) return; + if (!self.rail) return; + if (!self.autohidedom) return; + + if (self.hasmousefocus && opt.autohidemode === "leave") return; + self.cursortimeout = setTimeout(function () { + if (!self.rail.active || !self.showonmouseevent) { + self.autohidedom.stop().animate({ + opacity: opt.cursoropacitymin + }); + if (self.zoom) self.zoom.stop().animate({ + opacity: opt.cursoropacitymin + }); + self.cursoractive = false; + } + self.cursortimeout = 0; + }, tm || opt.hidecursordelay); + }; + + this.noticeCursor = function (tm, py, px) { + self.showCursor(py, px); + if (!self.rail.active) self.hideCursor(tm); + }; + + this.getContentSize = + (self.ispage) ? + function () { + return { + w: Math.max(_doc.body.scrollWidth, _doc.documentElement.scrollWidth), + h: Math.max(_doc.body.scrollHeight, _doc.documentElement.scrollHeight) + }; + } : (self.haswrapper) ? + function () { + return { + w: self.doc[0].offsetWidth, + h: self.doc[0].offsetHeight + }; + } : function () { + return { + w: self.docscroll[0].scrollWidth, + h: self.docscroll[0].scrollHeight + }; + }; + + this.onResize = function (e, page) { + + if (!self || !self.win) return false; + + var premaxh = self.page.maxh, + premaxw = self.page.maxw, + previewh = self.view.h, + previeww = self.view.w; + + self.view = { + w: (self.ispage) ? self.win.width() : self.win[0].clientWidth, + h: (self.ispage) ? self.win.height() : self.win[0].clientHeight + }; + + self.page = (page) ? page : self.getContentSize(); + + self.page.maxh = Math.max(0, self.page.h - self.view.h); + self.page.maxw = Math.max(0, self.page.w - self.view.w); + + if ((self.page.maxh == premaxh) && (self.page.maxw == premaxw) && (self.view.w == previeww) && (self.view.h == previewh)) { + // test position + if (!self.ispage) { + var pos = self.win.offset(); + if (self.lastposition) { + var lst = self.lastposition; + if ((lst.top == pos.top) && (lst.left == pos.left)) return self; //nothing to do + } + self.lastposition = pos; + } else { + return self; //nothing to do + } + } + + if (self.page.maxh === 0) { + self.hideRail(); + self.scrollvaluemax = 0; + self.scroll.y = 0; + self.scrollratio.y = 0; + self.cursorheight = 0; + self.setScrollTop(0); + if (self.rail) self.rail.scrollable = false; + } else { + self.page.maxh -= (opt.railpadding.top + opt.railpadding.bottom); + self.rail.scrollable = true; + } + + if (self.page.maxw === 0) { + self.hideRailHr(); + self.scrollvaluemaxw = 0; + self.scroll.x = 0; + self.scrollratio.x = 0; + self.cursorwidth = 0; + self.setScrollLeft(0); + if (self.railh) { + self.railh.scrollable = false; + } + } else { + self.page.maxw -= (opt.railpadding.left + opt.railpadding.right); + if (self.railh) self.railh.scrollable = (opt.horizrailenabled); + } + + self.railslocked = (self.locked) || ((self.page.maxh === 0) && (self.page.maxw === 0)); + if (self.railslocked) { + if (!self.ispage) self.updateScrollBar(self.view); + return false; + } + + if (!self.hidden) { + if (!self.rail.visibility) self.showRail(); + if (self.railh && !self.railh.visibility) self.showRailHr(); + } + + if (self.istextarea && self.win.css('resize') && self.win.css('resize') != 'none') self.view.h -= 20; + + self.cursorheight = Math.min(self.view.h, Math.round(self.view.h * (self.view.h / self.page.h))); + self.cursorheight = (opt.cursorfixedheight) ? opt.cursorfixedheight : Math.max(opt.cursorminheight, self.cursorheight); + + self.cursorwidth = Math.min(self.view.w, Math.round(self.view.w * (self.view.w / self.page.w))); + self.cursorwidth = (opt.cursorfixedheight) ? opt.cursorfixedheight : Math.max(opt.cursorminheight, self.cursorwidth); + + self.scrollvaluemax = self.view.h - self.cursorheight - (opt.railpadding.top + opt.railpadding.bottom); + if (!self.hasborderbox) self.scrollvaluemax -= self.cursor[0].offsetHeight - self.cursor[0].clientHeight; + + if (self.railh) { + self.railh.width = (self.page.maxh > 0) ? (self.view.w - self.rail.width) : self.view.w; + self.scrollvaluemaxw = self.railh.width - self.cursorwidth - (opt.railpadding.left + opt.railpadding.right); + } + + if (!self.ispage) self.updateScrollBar(self.view); + + self.scrollratio = { + x: (self.page.maxw / self.scrollvaluemaxw), + y: (self.page.maxh / self.scrollvaluemax) + }; + + var sy = self.getScrollTop(); + if (sy > self.page.maxh) { + self.doScrollTop(self.page.maxh); + } else { + self.scroll.y = (self.getScrollTop() / self.scrollratio.y) | 0; + self.scroll.x = (self.getScrollLeft() / self.scrollratio.x) | 0; + if (self.cursoractive) self.noticeCursor(); + } + + if (self.scroll.y && (self.getScrollTop() === 0)) self.doScrollTo((self.scroll.y * self.scrollratio.y)|0); + + return self; + }; + + this.resize = self.onResize; + + var hlazyresize = 0; + + this.onscreenresize = function(e) { + clearTimeout(hlazyresize); + + var hiderails = (!self.ispage && !self.haswrapper); + if (hiderails) self.hideRails(); + + hlazyresize = setTimeout(function () { + if (self) { + if (hiderails) self.showRails(); + self.resize(); + } + hlazyresize=0; + }, 120); + }; + + this.lazyResize = function (tm) { // event debounce + + clearTimeout(hlazyresize); + + tm = isNaN(tm) ? 240 : tm; + + hlazyresize = setTimeout(function () { + self && self.resize(); + hlazyresize=0; + }, tm); + + return self; + + }; + + // derived by MDN https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/wheel + function _modernWheelEvent(dom, name, fn, bubble) { + self._bind(dom, name, function (e) { + e = e || _win.event; + var event = { + original: e, + target: e.target || e.srcElement, + type: "wheel", + deltaMode: e.type == "MozMousePixelScroll" ? 0 : 1, + deltaX: 0, + deltaZ: 0, + preventDefault: function () { + e.preventDefault ? e.preventDefault() : e.returnValue = false; + return false; + }, + stopImmediatePropagation: function () { + (e.stopImmediatePropagation) ? e.stopImmediatePropagation() : e.cancelBubble = true; + } + }; + + if (name == "mousewheel") { + e.wheelDeltaX && (event.deltaX = -1 / 40 * e.wheelDeltaX); + e.wheelDeltaY && (event.deltaY = -1 / 40 * e.wheelDeltaY); + !event.deltaY && !event.deltaX && (event.deltaY = -1 / 40 * e.wheelDelta); + } else { + event.deltaY = e.detail; + } + + return fn.call(dom, event); + }, bubble); + } + + + + this.jqbind = function (dom, name, fn) { // use jquery bind for non-native events (mouseenter/mouseleave) + self.events.push({ + e: dom, + n: name, + f: fn, + q: true + }); + $(dom).on(name, fn); + }; + + this.mousewheel = function (dom, fn, bubble) { // bind mousewheel + var el = ("jquery" in dom) ? dom[0] : dom; + if ("onwheel" in _doc.createElement("div")) { // Modern browsers support "wheel" + self._bind(el, "wheel", fn, bubble || false); + } else { + var wname = (_doc.onmousewheel !== undefined) ? "mousewheel" : "DOMMouseScroll"; // older Webkit+IE support or older Firefox + _modernWheelEvent(el, wname, fn, bubble || false); + if (wname == "DOMMouseScroll") _modernWheelEvent(el, "MozMousePixelScroll", fn, bubble || false); // Firefox legacy + } + }; + + var passiveSupported = false; + + if (cap.haseventlistener) { // W3C standard event model + + // thanks to https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + try { var options = Object.defineProperty({}, "passive", { get: function () { passiveSupported = !0; } }); _win.addEventListener("test", null, options); } catch (err) { } + + this.stopPropagation = function (e) { + if (!e) return false; + e = (e.original) ? e.original : e; + e.stopPropagation(); + return false; + }; + + this.cancelEvent = function(e) { + if (e.cancelable) e.preventDefault(); + e.stopImmediatePropagation(); + if (e.preventManipulation) e.preventManipulation(); // IE10+ + return false; + }; + + } else { + + // inspired from https://gist.github.com/jonathantneal/2415137 + + Event.prototype.preventDefault = function () { + this.returnValue = false; + }; + + Event.prototype.stopPropagation = function () { + this.cancelBubble = true; + }; + + _win.constructor.prototype.addEventListener = _doc.constructor.prototype.addEventListener = Element.prototype.addEventListener = function (type, listener, useCapture) { + this.attachEvent("on" + type, listener); + }; + _win.constructor.prototype.removeEventListener = _doc.constructor.prototype.removeEventListener = Element.prototype.removeEventListener = function (type, listener, useCapture) { + this.detachEvent("on" + type, listener); + }; + + // Thanks to http://www.switchonthecode.com !! + this.cancelEvent = function (e) { + e = e || _win.event; + if (e) { + e.cancelBubble = true; + e.cancel = true; + e.returnValue = false; + } + return false; + }; + + this.stopPropagation = function (e) { + e = e || _win.event; + if (e) e.cancelBubble = true; + return false; + }; + + } + + this.delegate = function (dom, name, fn, bubble, active) { + + var de = delegatevents[name] || false; + + if (!de) { + + de = { + a: [], + l: [], + f: function (e) { + var lst = de.l, l = lst.length - 1; + var r = false; + for (var a = l; a >= 0; a--) { + r = lst[a].call(e.target, e); + if (r === false) return false; + } + return r; + } + }; + + self.bind(dom, name, de.f, bubble, active); + + delegatevents[name] = de; + + } + + if (self.ispage) { + de.a = [self.id].concat(de.a); + de.l = [fn].concat(de.l); + } else { + de.a.push(self.id); + de.l.push(fn); + } + + }; + + this.undelegate = function (dom, name, fn, bubble, active) { + var de = delegatevents[name]||false; + if (de&&de.l) { // quick fix #683 + for (var a=0,l=de.l.length;a 0) return dd; + dom = (dom.parentNode) ? dom.parentNode : false; + } + return false; + }; + + this.triggerScrollStart = function (cx, cy, rx, ry, ms) { + + if (self.onscrollstart) { + var info = { + type: "scrollstart", + current: { + x: cx, + y: cy + }, + request: { + x: rx, + y: ry + }, + end: { + x: self.newscrollx, + y: self.newscrolly + }, + speed: ms + }; + self.onscrollstart.call(self, info); + } + + }; + + this.triggerScrollEnd = function () { + if (self.onscrollend) { + + var px = self.getScrollLeft(); + var py = self.getScrollTop(); + + var info = { + type: "scrollend", + current: { + x: px, + y: py + }, + end: { + x: px, + y: py + } + }; + + self.onscrollend.call(self, info); + + } + + }; + + var scrolldiry = 0, scrolldirx = 0, scrolltmr = 0, scrollspd = 1; + + function doScrollRelative(px, py, chkscroll, iswheel) { + + if (!self.scrollrunning) { + self.newscrolly = self.getScrollTop(); + self.newscrollx = self.getScrollLeft(); + scrolltmr = now(); + } + + var gap = (now() - scrolltmr); + scrolltmr = now(); + + if (gap > 350) { + scrollspd = 1; + } else { + scrollspd += (2 - scrollspd) / 10; + } + + px = px * scrollspd | 0; + py = py * scrollspd | 0; + + if (px) { + + if (iswheel) { // mouse-only + if (px < 0) { // fix apple magic mouse swipe back/forward + if (self.getScrollLeft() >= self.page.maxw) return true; + } else { + if (self.getScrollLeft() <= 0) return true; + } + } + + var dx = px > 0 ? 1 : -1; + + if (scrolldirx !== dx) { + if (self.scrollmom) self.scrollmom.stop(); + self.newscrollx = self.getScrollLeft(); + scrolldirx = dx; + } + + self.lastdeltax -= px; + + } + + if (py) { + + var chk = (function () { + var top = self.getScrollTop(); + if (py < 0) { + if (top >= self.page.maxh) return true; + } else { + if (top <= 0) return true; + } + })(); + + if (chk) { + if (opt.nativeparentscrolling && chkscroll && !self.ispage && !self.zoomactive) return true; + var ny = self.view.h >> 1; + if (self.newscrolly < -ny) { self.newscrolly = -ny; py = -1; } + else if (self.newscrolly > self.page.maxh + ny) { self.newscrolly = self.page.maxh + ny; py = 1; } + else py = 0; + } + + var dy = py > 0 ? 1 : -1; + + if (scrolldiry !== dy) { + if (self.scrollmom) self.scrollmom.stop(); + self.newscrolly = self.getScrollTop(); + scrolldiry = dy; + } + + self.lastdeltay -= py; + + } + + if (py || px) { + self.synched("relativexy", function () { + + var dty = self.lastdeltay + self.newscrolly; + self.lastdeltay = 0; + + var dtx = self.lastdeltax + self.newscrollx; + self.lastdeltax = 0; + + if (!self.rail.drag) self.doScrollPos(dtx, dty); + + }); + } + + } + + var hasparentscrollingphase = false; + + function execScrollWheel(e, hr, chkscroll) { + var px, py; + + if (!chkscroll && hasparentscrollingphase) return true; + + if (e.deltaMode === 0) { // PIXEL + px = -(e.deltaX * (opt.mousescrollstep / (18 * 3))) | 0; + py = -(e.deltaY * (opt.mousescrollstep / (18 * 3))) | 0; + } else if (e.deltaMode === 1) { // LINE + px = -(e.deltaX * opt.mousescrollstep * 50 / 80) | 0; + py = -(e.deltaY * opt.mousescrollstep * 50 / 80) | 0; + } + + if (hr && opt.oneaxismousemode && (px === 0) && py) { // classic vertical-only mousewheel + browser with x/y support + px = py; + py = 0; + + if (chkscroll) { + var hrend = (px < 0) ? (self.getScrollLeft() >= self.page.maxw) : (self.getScrollLeft() <= 0); + if (hrend) { // preserve vertical scrolling + py = px; + px = 0; + } + } + + } + + // invert horizontal direction for rtl mode + if (self.isrtlmode) px = -px; + + var chk = doScrollRelative(px, py, chkscroll, true); + + if (chk) { + if (chkscroll) hasparentscrollingphase = true; + } else { + hasparentscrollingphase = false; + e.stopImmediatePropagation(); + return e.preventDefault(); + } + + } + + this.onmousewheel = function (e) { + if (self.wheelprevented||self.locked) return false; + if (self.railslocked) { + self.debounced("checkunlock", self.resize, 250); + return false; + } + if (self.rail.drag) return self.cancelEvent(e); + + if (opt.oneaxismousemode === "auto" && e.deltaX !== 0) opt.oneaxismousemode = false; // check two-axis mouse support (not very elegant) + + if (opt.oneaxismousemode && e.deltaX === 0) { + if (!self.rail.scrollable) { + if (self.railh && self.railh.scrollable) { + return self.onmousewheelhr(e); + } else { + return true; + } + } + } + + var nw = now(); + var chk = false; + if (opt.preservenativescrolling && ((self.checkarea + 600) < nw)) { + self.nativescrollingarea = self.isScrollable(e); + chk = true; + } + self.checkarea = nw; + if (self.nativescrollingarea) return true; // this isn't my business + var ret = execScrollWheel(e, false, chk); + if (ret) self.checkarea = 0; + return ret; + }; + + this.onmousewheelhr = function (e) { + if (self.wheelprevented) return; + if (self.railslocked || !self.railh.scrollable) return true; + if (self.rail.drag) return self.cancelEvent(e); + + var nw = now(); + var chk = false; + if (opt.preservenativescrolling && ((self.checkarea + 600) < nw)) { + self.nativescrollingarea = self.isScrollable(e); + chk = true; + } + self.checkarea = nw; + if (self.nativescrollingarea) return true; // this is not my business + if (self.railslocked) return self.cancelEvent(e); + + return execScrollWheel(e, true, chk); + }; + + this.stop = function () { + self.cancelScroll(); + if (self.scrollmon) self.scrollmon.stop(); + self.cursorfreezed = false; + self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y)); + self.noticeCursor(); + return self; + }; + + this.getTransitionSpeed = function (dif) { + + return 80 + (dif / 72) * opt.scrollspeed |0; + + }; + + if (!opt.smoothscroll) { + this.doScrollLeft = function (x, spd) { //direct + var y = self.getScrollTop(); + self.doScrollPos(x, y, spd); + }; + this.doScrollTop = function (y, spd) { //direct + var x = self.getScrollLeft(); + self.doScrollPos(x, y, spd); + }; + this.doScrollPos = function (x, y, spd) { //direct + var nx = (x > self.page.maxw) ? self.page.maxw : x; + if (nx < 0) nx = 0; + var ny = (y > self.page.maxh) ? self.page.maxh : y; + if (ny < 0) ny = 0; + self.synched('scroll', function () { + self.setScrollTop(ny); + self.setScrollLeft(nx); + }); + }; + this.cancelScroll = function () { }; // direct + + } else if (self.ishwscroll && cap.hastransition && opt.usetransition && !!opt.smoothscroll) { + + var lasttransitionstyle = ''; + + this.resetTransition = function () { + lasttransitionstyle = ''; + self.doc.css(cap.prefixstyle + 'transition-duration', '0ms'); + }; + + this.prepareTransition = function (dif, istime) { + var ex = (istime) ? dif : self.getTransitionSpeed(dif); + var trans = ex + 'ms'; + if (lasttransitionstyle !== trans) { + lasttransitionstyle = trans; + self.doc.css(cap.prefixstyle + 'transition-duration', trans); + } + return ex; + }; + + this.doScrollLeft = function (x, spd) { //trans + var y = (self.scrollrunning) ? self.newscrolly : self.getScrollTop(); + self.doScrollPos(x, y, spd); + }; + + this.doScrollTop = function (y, spd) { //trans + var x = (self.scrollrunning) ? self.newscrollx : self.getScrollLeft(); + self.doScrollPos(x, y, spd); + }; + + this.cursorupdate = { + running: false, + start: function () { + var m = this; + + if (m.running) return; + m.running = true; + + var loop = function () { + if (m.running) setAnimationFrame(loop); + self.showCursor(self.getScrollTop(), self.getScrollLeft()); + self.notifyScrollEvent(self.win[0]); + }; + + setAnimationFrame(loop); + }, + stop: function () { + this.running = false; + } + }; + + this.doScrollPos = function (x, y, spd) { //trans + + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + + if (((self.newscrolly - py) * (y - py) < 0) || ((self.newscrollx - px) * (x - px) < 0)) self.cancelScroll(); //inverted movement detection + + if (!opt.bouncescroll) { + if (y < 0) y = 0; + else if (y > self.page.maxh) y = self.page.maxh; + if (x < 0) x = 0; + else if (x > self.page.maxw) x = self.page.maxw; + } else { + if (y < 0) y = y / 2 | 0; + else if (y > self.page.maxh) y = self.page.maxh + (y - self.page.maxh) / 2 | 0; + if (x < 0) x = x / 2 | 0; + else if (x > self.page.maxw) x = self.page.maxw + (x - self.page.maxw) / 2 | 0; + } + + if (self.scrollrunning && x == self.newscrollx && y == self.newscrolly) return false; + + self.newscrolly = y; + self.newscrollx = x; + + var top = self.getScrollTop(); + var lft = self.getScrollLeft(); + + var dst = {}; + dst.x = x - lft; + dst.y = y - top; + + var dd = Math.sqrt((dst.x * dst.x) + (dst.y * dst.y)) | 0; + + var ms = self.prepareTransition(dd); + + if (!self.scrollrunning) { + self.scrollrunning = true; + self.triggerScrollStart(lft, top, x, y, ms); + self.cursorupdate.start(); + } + + self.scrollendtrapped = true; + + if (!cap.transitionend) { + if (self.scrollendtrapped) clearTimeout(self.scrollendtrapped); + self.scrollendtrapped = setTimeout(self.onScrollTransitionEnd, ms); // simulate transitionend event + } + + self.setScrollTop(self.newscrolly); + self.setScrollLeft(self.newscrollx); + + }; + + this.cancelScroll = function () { + if (!self.scrollendtrapped) return true; + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + self.scrollrunning = false; + if (!cap.transitionend) clearTimeout(cap.transitionend); + self.scrollendtrapped = false; + self.resetTransition(); + self.setScrollTop(py); // fire event onscroll + if (self.railh) self.setScrollLeft(px); + if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm); + self.timerscroll = false; + + self.cursorfreezed = false; + + self.cursorupdate.stop(); + self.showCursor(py, px); + return self; + }; + + this.onScrollTransitionEnd = function () { + + if (!self.scrollendtrapped) return; + + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + + if (py < 0) py = 0; + else if (py > self.page.maxh) py = self.page.maxh; + if (px < 0) px = 0; + else if (px > self.page.maxw) px = self.page.maxw; + if ((py != self.newscrolly) || (px != self.newscrollx)) return self.doScrollPos(px, py, opt.snapbackspeed); + + if (self.scrollrunning) self.triggerScrollEnd(); + self.scrollrunning = false; + + self.scrollendtrapped = false; + self.resetTransition(); + self.timerscroll = false; + self.setScrollTop(py); // fire event onscroll + if (self.railh) self.setScrollLeft(px); // fire event onscroll left + + self.cursorupdate.stop(); + self.noticeCursor(false, py, px); + + self.cursorfreezed = false; + + }; + + } else { + + this.doScrollLeft = function (x, spd) { //no-trans + var y = (self.scrollrunning) ? self.newscrolly : self.getScrollTop(); + self.doScrollPos(x, y, spd); + }; + + this.doScrollTop = function (y, spd) { //no-trans + var x = (self.scrollrunning) ? self.newscrollx : self.getScrollLeft(); + self.doScrollPos(x, y, spd); + }; + + this.doScrollPos = function (x, y, spd) { //no-trans + + var py = self.getScrollTop(); + var px = self.getScrollLeft(); + + if (((self.newscrolly - py) * (y - py) < 0) || ((self.newscrollx - px) * (x - px) < 0)) self.cancelScroll(); //inverted movement detection + + var clipped = false; + + if (!self.bouncescroll || !self.rail.visibility) { + if (y < 0) { + y = 0; + clipped = true; + } else if (y > self.page.maxh) { + y = self.page.maxh; + clipped = true; + } + } + if (!self.bouncescroll || !self.railh.visibility) { + if (x < 0) { + x = 0; + clipped = true; + } else if (x > self.page.maxw) { + x = self.page.maxw; + clipped = true; + } + } + + if (self.scrollrunning && (self.newscrolly === y) && (self.newscrollx === x)) return true; + + self.newscrolly = y; + self.newscrollx = x; + + self.dst = {}; + self.dst.x = x - px; + self.dst.y = y - py; + self.dst.px = px; + self.dst.py = py; + + var dd = Math.sqrt((self.dst.x * self.dst.x) + (self.dst.y * self.dst.y)) | 0; + var ms = self.getTransitionSpeed(dd); + + self.bzscroll = {}; + + var p3 = (clipped) ? 1 : 0.58; + self.bzscroll.x = new BezierClass(px, self.newscrollx, ms, 0, 0, p3, 1); + self.bzscroll.y = new BezierClass(py, self.newscrolly, ms, 0, 0, p3, 1); + + var loopid = now(); + + var loop = function () { + + if (!self.scrollrunning) return; + var x = self.bzscroll.y.getPos(); + + self.setScrollLeft(self.bzscroll.x.getNow()); + self.setScrollTop(self.bzscroll.y.getNow()); + + if (x <= 1) { + self.timer = setAnimationFrame(loop); + } else { + self.scrollrunning = false; + self.timer = 0; + self.triggerScrollEnd(); + } + + }; + + if (!self.scrollrunning) { + self.triggerScrollStart(px, py, x, y, ms); + self.scrollrunning = true; + self.timer = setAnimationFrame(loop); + } + + }; + + this.cancelScroll = function () { + if (self.timer) clearAnimationFrame(self.timer); + self.timer = 0; + self.bzscroll = false; + self.scrollrunning = false; + return self; + }; + + } + + this.doScrollBy = function (stp, relative) { + doScrollRelative(0, stp); + }; + + this.doScrollLeftBy = function (stp, relative) { + doScrollRelative(stp, 0); + }; + + this.doScrollTo = function (pos, relative) { + var ny = (relative) ? Math.round(pos * self.scrollratio.y) : pos; + if (ny < 0) ny = 0; + else if (ny > self.page.maxh) ny = self.page.maxh; + self.cursorfreezed = false; + self.doScrollTop(pos); + }; + + this.checkContentSize = function () { + var pg = self.getContentSize(); + if ((pg.h != self.page.h) || (pg.w != self.page.w)) self.resize(false, pg); + }; + + self.onscroll = function (e) { + if (self.rail.drag) return; + if (!self.cursorfreezed) { + self.synched('scroll', function () { + self.scroll.y = Math.round(self.getScrollTop() / self.scrollratio.y); + if (self.railh) self.scroll.x = Math.round(self.getScrollLeft() / self.scrollratio.x); + self.noticeCursor(); + }); + } + }; + self.bind(self.docscroll, "scroll", self.onscroll); + + this.doZoomIn = function (e) { + if (self.zoomactive) return; + self.zoomactive = true; + + self.zoomrestore = { + style: {} + }; + var lst = ['position', 'top', 'left', 'zIndex', 'backgroundColor', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight']; + var win = self.win[0].style; + for (var a in lst) { + var pp = lst[a]; + self.zoomrestore.style[pp] = (win[pp] !== undefined) ? win[pp] : ''; + } + + self.zoomrestore.style.width = self.win.css('width'); + self.zoomrestore.style.height = self.win.css('height'); + + self.zoomrestore.padding = { + w: self.win.outerWidth() - self.win.width(), + h: self.win.outerHeight() - self.win.height() + }; + + if (cap.isios4) { + self.zoomrestore.scrollTop = $window.scrollTop(); + $window.scrollTop(0); + } + + self.win.css({ + position: (cap.isios4) ? "absolute" : "fixed", + top: 0, + left: 0, + zIndex: globalmaxzindex + 100, + margin: 0 + }); + var bkg = self.win.css("backgroundColor"); + if ("" === bkg || /transparent|rgba\(0, 0, 0, 0\)|rgba\(0,0,0,0\)/.test(bkg)) self.win.css("backgroundColor", "#fff"); + self.rail.css({ + zIndex: globalmaxzindex + 101 + }); + self.zoom.css({ + zIndex: globalmaxzindex + 102 + }); + self.zoom.css('backgroundPosition', '0 -18px'); + self.resizeZoom(); + + if (self.onzoomin) self.onzoomin.call(self); + + return self.cancelEvent(e); + }; + + this.doZoomOut = function (e) { + if (!self.zoomactive) return; + self.zoomactive = false; + + self.win.css("margin", ""); + self.win.css(self.zoomrestore.style); + + if (cap.isios4) { + $window.scrollTop(self.zoomrestore.scrollTop); + } + + self.rail.css({ + "z-index": self.zindex + }); + self.zoom.css({ + "z-index": self.zindex + }); + self.zoomrestore = false; + self.zoom.css('backgroundPosition', '0 0'); + self.onResize(); + + if (self.onzoomout) self.onzoomout.call(self); + + return self.cancelEvent(e); + }; + + this.doZoom = function (e) { + return (self.zoomactive) ? self.doZoomOut(e) : self.doZoomIn(e); + }; + + this.resizeZoom = function () { + if (!self.zoomactive) return; + + var py = self.getScrollTop(); //preserve scrolling position + self.win.css({ + width: $window.width() - self.zoomrestore.padding.w + "px", + height: $window.height() - self.zoomrestore.padding.h + "px" + }); + self.onResize(); + + self.setScrollTop(Math.min(self.page.maxh, py)); + }; + + this.init(); + + $.nicescroll.push(this); + + }; + + // Inspired by the work of Kin Blas + // http://webpro.host.adobe.com/people/jblas/momentum/includes/jquery.momentum.0.7.js + var ScrollMomentumClass2D = function (nc) { + var self = this; + this.nc = nc; + + this.lastx = 0; + this.lasty = 0; + this.speedx = 0; + this.speedy = 0; + this.lasttime = 0; + this.steptime = 0; + this.snapx = false; + this.snapy = false; + this.demulx = 0; + this.demuly = 0; + + this.lastscrollx = -1; + this.lastscrolly = -1; + + this.chkx = 0; + this.chky = 0; + + this.timer = 0; + + this.reset = function (px, py) { + self.stop(); + self.steptime = 0; + self.lasttime = now(); + self.speedx = 0; + self.speedy = 0; + self.lastx = px; + self.lasty = py; + self.lastscrollx = -1; + self.lastscrolly = -1; + }; + + this.update = function (px, py) { + var tm = now(); + self.steptime = tm - self.lasttime; + self.lasttime = tm; + var dy = py - self.lasty; + var dx = px - self.lastx; + var sy = self.nc.getScrollTop(); + var sx = self.nc.getScrollLeft(); + var newy = sy + dy; + var newx = sx + dx; + self.snapx = (newx < 0) || (newx > self.nc.page.maxw); + self.snapy = (newy < 0) || (newy > self.nc.page.maxh); + self.speedx = dx; + self.speedy = dy; + self.lastx = px; + self.lasty = py; + }; + + this.stop = function () { + self.nc.unsynched("domomentum2d"); + if (self.timer) clearTimeout(self.timer); + self.timer = 0; + self.lastscrollx = -1; + self.lastscrolly = -1; + }; + + this.doSnapy = function (nx, ny) { + var snap = false; + + if (ny < 0) { + ny = 0; + snap = true; + } else if (ny > self.nc.page.maxh) { + ny = self.nc.page.maxh; + snap = true; + } + + if (nx < 0) { + nx = 0; + snap = true; + } else if (nx > self.nc.page.maxw) { + nx = self.nc.page.maxw; + snap = true; + } + + (snap) ? self.nc.doScrollPos(nx, ny, self.nc.opt.snapbackspeed) : self.nc.triggerScrollEnd(); + }; + + this.doMomentum = function (gp) { + var t = now(); + var l = (gp) ? t + gp : self.lasttime; + + var sl = self.nc.getScrollLeft(); + var st = self.nc.getScrollTop(); + + var pageh = self.nc.page.maxh; + var pagew = self.nc.page.maxw; + + self.speedx = (pagew > 0) ? Math.min(60, self.speedx) : 0; + self.speedy = (pageh > 0) ? Math.min(60, self.speedy) : 0; + + var chk = l && (t - l) <= 60; + + if ((st < 0) || (st > pageh) || (sl < 0) || (sl > pagew)) chk = false; + + var sy = (self.speedy && chk) ? self.speedy : false; + var sx = (self.speedx && chk) ? self.speedx : false; + + if (sy || sx) { + var tm = Math.max(16, self.steptime); //timeout granularity + + if (tm > 50) { // do smooth + var xm = tm / 50; + self.speedx *= xm; + self.speedy *= xm; + tm = 50; + } + + self.demulxy = 0; + + self.lastscrollx = self.nc.getScrollLeft(); + self.chkx = self.lastscrollx; + self.lastscrolly = self.nc.getScrollTop(); + self.chky = self.lastscrolly; + + var nx = self.lastscrollx; + var ny = self.lastscrolly; + + var onscroll = function () { + var df = ((now() - t) > 600) ? 0.04 : 0.02; + + if (self.speedx) { + nx = Math.floor(self.lastscrollx - (self.speedx * (1 - self.demulxy))); + self.lastscrollx = nx; + if ((nx < 0) || (nx > pagew)) df = 0.10; + } + + if (self.speedy) { + ny = Math.floor(self.lastscrolly - (self.speedy * (1 - self.demulxy))); + self.lastscrolly = ny; + if ((ny < 0) || (ny > pageh)) df = 0.10; + } + + self.demulxy = Math.min(1, self.demulxy + df); + + self.nc.synched("domomentum2d", function () { + + if (self.speedx) { + var scx = self.nc.getScrollLeft(); + // if (scx != self.chkx) self.stop(); + self.chkx = nx; + self.nc.setScrollLeft(nx); + } + + if (self.speedy) { + var scy = self.nc.getScrollTop(); + // if (scy != self.chky) self.stop(); + self.chky = ny; + self.nc.setScrollTop(ny); + } + + if (!self.timer) { + self.nc.hideCursor(); + self.doSnapy(nx, ny); + } + + }); + + if (self.demulxy < 1) { + self.timer = setTimeout(onscroll, tm); + } else { + self.stop(); + self.nc.hideCursor(); + self.doSnapy(nx, ny); + } + }; + + onscroll(); + + } else { + self.doSnapy(self.nc.getScrollLeft(), self.nc.getScrollTop()); + } + + }; + + }; + + + // override jQuery scrollTop + var _scrollTop = jQuery.fn.scrollTop; // preserve original function + + jQuery.cssHooks.pageYOffset = { + get: function (elem, computed, extra) { + var nice = $.data(elem, '__nicescroll') || false; + return (nice && nice.ishwscroll) ? nice.getScrollTop() : _scrollTop.call(elem); + }, + set: function (elem, value) { + var nice = $.data(elem, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollTop(parseInt(value)) : _scrollTop.call(elem, value); + return this; + } + }; + + jQuery.fn.scrollTop = function (value) { + if (value === undefined) { + var nice = (this[0]) ? $.data(this[0], '__nicescroll') || false : false; + return (nice && nice.ishwscroll) ? nice.getScrollTop() : _scrollTop.call(this); + } else { + return this.each(function () { + var nice = $.data(this, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollTop(parseInt(value)) : _scrollTop.call($(this), value); + }); + } + }; + + // override jQuery scrollLeft + var _scrollLeft = jQuery.fn.scrollLeft; // preserve original function + + $.cssHooks.pageXOffset = { + get: function (elem, computed, extra) { + var nice = $.data(elem, '__nicescroll') || false; + return (nice && nice.ishwscroll) ? nice.getScrollLeft() : _scrollLeft.call(elem); + }, + set: function (elem, value) { + var nice = $.data(elem, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollLeft(parseInt(value)) : _scrollLeft.call(elem, value); + return this; + } + }; + + jQuery.fn.scrollLeft = function (value) { + if (value === undefined) { + var nice = (this[0]) ? $.data(this[0], '__nicescroll') || false : false; + return (nice && nice.ishwscroll) ? nice.getScrollLeft() : _scrollLeft.call(this); + } else { + return this.each(function () { + var nice = $.data(this, '__nicescroll') || false; + (nice && nice.ishwscroll) ? nice.setScrollLeft(parseInt(value)) : _scrollLeft.call($(this), value); + }); + } + }; + + var NiceScrollArray = function (doms) { + var self = this; + this.length = 0; + this.name = "nicescrollarray"; + + this.each = function (fn) { + $.each(self, fn); + return self; + }; + + this.push = function (nice) { + self[self.length] = nice; + self.length++; + }; + + this.eq = function (idx) { + return self[idx]; + }; + + if (doms) { + for (var a = 0; a < doms.length; a++) { + var nice = $.data(doms[a], '__nicescroll') || false; + if (nice) { + this[this.length] = nice; + this.length++; + } + } + } + + return this; + }; + + function mplex(el, lst, fn) { + for (var a = 0, l = lst.length; a < l; a++) fn(el, lst[a]); + } + mplex( + NiceScrollArray.prototype, ['show', 'hide', 'toggle', 'onResize', 'resize', 'remove', 'stop', 'doScrollPos'], + function (e, n) { + e[n] = function () { + var args = arguments; + return this.each(function () { + this[n].apply(this, args); + }); + }; + } + ); + + jQuery.fn.getNiceScroll = function (index) { + if (index === undefined) { + return new NiceScrollArray(this); + } else { + return this[index] && $.data(this[index], '__nicescroll') || false; + } + }; + + var pseudos = jQuery.expr.pseudos || jQuery.expr[':']; // jQuery 3 migration + pseudos.nicescroll = function (a) { + return $.data(a, '__nicescroll') !== undefined; + }; + + $.fn.niceScroll = function (wrapper, _opt) { + if (_opt === undefined && typeof wrapper == "object" && !("jquery" in wrapper)) { + _opt = wrapper; + wrapper = false; + } + + var ret = new NiceScrollArray(); + + this.each(function () { + var $this = $(this); + + var opt = $.extend({}, _opt); // cloning + + if (wrapper || false) { + var wrp = $(wrapper); + opt.doc = (wrp.length > 1) ? $(wrapper, $this) : wrp; + opt.win = $this; + } + var docundef = !("doc" in opt); + if (!docundef && !("win" in opt)) opt.win = $this; + + var nice = $this.data('__nicescroll') || false; + if (!nice) { + opt.doc = opt.doc || $this; + nice = new NiceScrollClass(opt, $this); + $this.data('__nicescroll', nice); + } + ret.push(nice); + }); + + return (ret.length === 1) ? ret[0] : ret; + }; + + _win.NiceScroll = { + getjQuery: function () { + return jQuery; + } + }; + + if (!$.nicescroll) { + $.nicescroll = new NiceScrollArray(); + $.nicescroll.options = _globaloptions; + } + +})); \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/resources/toastr/toastr.min.js b/mq-cloud/src/main/resources/static/resources/toastr/toastr.min.js index 4b5f34a0..0f0889e5 100644 --- a/mq-cloud/src/main/resources/static/resources/toastr/toastr.min.js +++ b/mq-cloud/src/main/resources/static/resources/toastr/toastr.min.js @@ -4,4 +4,3 @@ * make sure you copy the url from the website since the url may change between versions. * */ !function(e){e(["jquery"],function(e){return function(){function t(e,t,n){return g({type:O.error,iconClass:m().iconClasses.error,message:e,optionsOverride:n,title:t})}function n(t,n){return t||(t=m()),v=e("#"+t.containerId),v.length?v:(n&&(v=d(t)),v)}function o(e,t,n){return g({type:O.info,iconClass:m().iconClasses.info,message:e,optionsOverride:n,title:t})}function s(e){C=e}function i(e,t,n){return g({type:O.success,iconClass:m().iconClasses.success,message:e,optionsOverride:n,title:t})}function a(e,t,n){return g({type:O.warning,iconClass:m().iconClasses.warning,message:e,optionsOverride:n,title:t})}function r(e,t){var o=m();v||n(o),u(e,o,t)||l(o)}function c(t){var o=m();return v||n(o),t&&0===e(":focus",t).length?void h(t):void(v.children().length&&v.remove())}function l(t){for(var n=v.children(),o=n.length-1;o>=0;o--)u(e(n[o]),t)}function u(t,n,o){var s=!(!o||!o.force)&&o.force;return!(!t||!s&&0!==e(":focus",t).length)&&(t[n.hideMethod]({duration:n.hideDuration,easing:n.hideEasing,complete:function(){h(t)}}),!0)}function d(t){return v=e("
    ").attr("id",t.containerId).addClass(t.positionClass),v.appendTo(e(t.target)),v}function p(){return{tapToDismiss:!0,toastClass:"toast",containerId:"toast-container",debug:!1,showMethod:"fadeIn",showDuration:300,showEasing:"swing",onShown:void 0,hideMethod:"fadeOut",hideDuration:1e3,hideEasing:"swing",onHidden:void 0,closeMethod:!1,closeDuration:!1,closeEasing:!1,closeOnHover:!0,extendedTimeOut:1e3,iconClasses:{error:"toast-error",info:"toast-info",success:"toast-success",warning:"toast-warning"},iconClass:"toast-info",positionClass:"toast-top-right",timeOut:5e3,titleClass:"toast-title",messageClass:"toast-message",escapeHtml:!1,target:"body",closeHtml:'',closeClass:"toast-close-button",newestOnTop:!0,preventDuplicates:!1,progressBar:!1,progressClass:"toast-progress",rtl:!1}}function f(e){C&&C(e)}function g(t){function o(e){return null==e&&(e=""),e.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function s(){c(),u(),d(),p(),g(),C(),l(),i()}function i(){var e="";switch(t.iconClass){case"toast-success":case"toast-info":e="polite";break;default:e="assertive"}I.attr("aria-live",e)}function a(){E.closeOnHover&&I.hover(H,D),!E.onclick&&E.tapToDismiss&&I.click(b),E.closeButton&&j&&j.click(function(e){e.stopPropagation?e.stopPropagation():void 0!==e.cancelBubble&&e.cancelBubble!==!0&&(e.cancelBubble=!0),E.onCloseClick&&E.onCloseClick(e),b(!0)}),E.onclick&&I.click(function(e){E.onclick(e),b()})}function r(){I.hide(),I[E.showMethod]({duration:E.showDuration,easing:E.showEasing,complete:E.onShown}),E.timeOut>0&&(k=setTimeout(b,E.timeOut),F.maxHideTime=parseFloat(E.timeOut),F.hideEta=(new Date).getTime()+F.maxHideTime,E.progressBar&&(F.intervalId=setInterval(x,10)))}function c(){t.iconClass&&I.addClass(E.toastClass).addClass(y)}function l(){E.newestOnTop?v.prepend(I):v.append(I)}function u(){if(t.title){var e=t.title;E.escapeHtml&&(e=o(t.title)),M.append(e).addClass(E.titleClass),I.append(M)}}function d(){if(t.message){var e=t.message;E.escapeHtml&&(e=o(t.message)),B.append(e).addClass(E.messageClass),I.append(B)}}function p(){E.closeButton&&(j.addClass(E.closeClass).attr("role","button"),I.prepend(j))}function g(){E.progressBar&&(q.addClass(E.progressClass),I.prepend(q))}function C(){E.rtl&&I.addClass("rtl")}function O(e,t){if(e.preventDuplicates){if(t.message===w)return!0;w=t.message}return!1}function b(t){var n=t&&E.closeMethod!==!1?E.closeMethod:E.hideMethod,o=t&&E.closeDuration!==!1?E.closeDuration:E.hideDuration,s=t&&E.closeEasing!==!1?E.closeEasing:E.hideEasing;if(!e(":focus",I).length||t)return clearTimeout(F.intervalId),I[n]({duration:o,easing:s,complete:function(){h(I),clearTimeout(k),E.onHidden&&"hidden"!==P.state&&E.onHidden(),P.state="hidden",P.endTime=new Date,f(P)}})}function D(){(E.timeOut>0||E.extendedTimeOut>0)&&(k=setTimeout(b,E.extendedTimeOut),F.maxHideTime=parseFloat(E.extendedTimeOut),F.hideEta=(new Date).getTime()+F.maxHideTime)}function H(){clearTimeout(k),F.hideEta=0,I.stop(!0,!0)[E.showMethod]({duration:E.showDuration,easing:E.showEasing})}function x(){var e=(F.hideEta-(new Date).getTime())/F.maxHideTime*100;q.width(e+"%")}var E=m(),y=t.iconClass||E.iconClass;if("undefined"!=typeof t.optionsOverride&&(E=e.extend(E,t.optionsOverride),y=t.optionsOverride.iconClass||y),!O(E,t)){T++,v=n(E,!0);var k=null,I=e("
    "),M=e("
    "),B=e("
    "),q=e("
    "),j=e(E.closeHtml),F={intervalId:null,hideEta:null,maxHideTime:null},P={toastId:T,state:"visible",startTime:new Date,options:E,map:t};return s(),r(),a(),f(P),E.debug&&console&&console.log(P),I}}function m(){return e.extend({},p(),b.options)}function h(e){v||(v=n()),e.is(":visible")||(e.remove(),e=null,0===v.children().length&&(v.remove(),w=void 0))}var v,C,w,T=0,O={error:"error",info:"info",success:"success",warning:"warning"},b={clear:r,remove:c,error:t,getContainer:n,info:o,options:{},subscribe:s,success:i,version:"2.1.3",warning:a};return b}()})}("function"==typeof define&&define.amd?define:function(e,t){"undefined"!=typeof module&&module.exports?module.exports=t(require("jquery")):window.toastr=t(window.jQuery)}); -//# sourceMappingURL=toastr.js.map diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/api.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/api.md new file mode 100644 index 00000000..84b461db --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/api.md @@ -0,0 +1,233 @@ +## 一、topic相关的api + +1. 创建topic:**MQAdminExt.createAndUpdateTopicConfig(String addr, TopicConfig config)** + + **释义**:在broker上创建topic。 + + **用途**:新建topic使用。 + +2. 删除topic: + + 1. **MQAdminExt.deleteTopicInBroker(Set addrs, String topic)** + + **释义**:在broker上删除topic。 + + **用途**:删除topic使用。 + + 2. **MQAdminExt.deleteTopicInNameServer(Set addrs, String topic)** + + **释义**:在name server上删除topic。 + + **用途**:删除topic使用。 + +3. 查询topic路由:MQAdminExt.examineTopicRouteInfo(String topic) + + **释义**:从name server获取topic的路由信息,返回如下数据结构: + + ![](img/5.1.png) + + **用途**:用于获取topic路由信息的所有地方。 + +4. 获取所有topic的列表:MQAdminExt.fetchAllTopicList() + + **释义**:获取broker上的所有的topic。 + + **用途**:适用于全量获取topic。 + +5. 获取所有topic的配置:MQAdminExt.getAllTopicGroup(String brokerAddr, long timeoutMillis) + + **释义**:获取broker上的所有的topic及配置。 + + **用途**:适用于全量获取topic,比如部署新broker实例时,copy所有topic的配置。 + +6. 获取topic的生产情况:MQAdminExt.examineTopicStats(String topic) + + **释义**:实时获取broker端的topic的偏移量(数据来自于消费者的主动上报)。 + + **用途**:实时展示生产者的消息量,最新生产时间等。 + +## 二、生产者相关的api + +1. 诊断生产者链接:MQAdminExt.examineProducerConnectionInfo(String producerGroup, String topic) + + **释义**:获取生产者的链接信息。 + + **用途**:适用于展示生产者链接状况。 + +## 三、消费者相关的api + +1. 创建消费者:**MQAdminExt.createAndUpdateSubscriptionGroupConfig(String addr, SubscriptionGroupConfig config)** + + **释义**:在broker上创建消费者订阅。 + + **用途**:新建消费者使用。 + +2. 删除消费者:**MQAdminExt.deleteSubscriptionGroup(String addr, String groupName)** + + **释义**:删除broker上的订阅组。 + + **用途**:消费者下线使用。 + +3. 消费者偏移量重置: + + 1. 消费者在线:**MQAdminExt.resetOffsetByTimestamp(String topic, String group, long timestamp, boolean isForce)** + + **释义**:broker回调客户端,重置客户端的偏移量至timestamp。 + + **用途**:适用于客户端在线的情况,即客户端还在运行着(不管客户端是广播消费还是集群消费此种方式都可以直接重置偏移量)。 + + 2. 消费者不在线:**MQAdminExt.resetOffsetByTimestampOld(String consumerGroup, String topic, long timestamp, boolean force)** + + **释义**:重置客户端的偏移量至timestamp。 + + **用途**:适用于客户端离线的情况,因为它直接重置的是broker端的消费者的偏移量,不适用于广播消费模式。 + +4. 获取消费者偏移量:MQAdminExt.examineConsumeStats(String consumerGroup) + + **释义**:实时获取broker端的消费者的偏移量(数据来自于消费者的主动上报)。 + + **用途**:实时展示消费者的堆积,延迟等指标。 + +5. 获取消费者状态:MQAdminExt.getConsumeStatus(String topic, String group, String clientAddr) + + **释义**:broker回调客户端,获取客户端统计的每个队列的消费的偏移量(可以用于广播消费模式,因为广播消费不会上报偏移量到broker,broker端不知道广播模式的消费状况)。 + + **用途**:实时展示消费者的堆积,延迟等指标。 + +6. 获取消费者链接:MQAdminExt.examineConsumerConnectionInfo(String consumerGroup) + + **释义**:获取消费者的链接,返回结果包括订阅信息和链接clientId等,详细如下: + + ``` + public class ConsumerConnection { + private HashSet connectionSet; + private ConcurrentMap subscriptionTable; + private ConsumeType consumeType; + private MessageModel messageModel; + private ConsumeFromWhere consumeFromWhere; + } + // 链接信息 + public class Connection { + private String clientId; + private String clientAddr; + private LanguageCode language; + private int version; + } + // 订阅信息 + public class SubscriptionData { + public final static String SUB_ALL = "*"; + private String topic; + private String subString; + } + ``` + + **用途**:获取消费者的所有实例的链接信息。 + +7. 获取消费者客户端实例的运行时信息:MQAdminExt.getConsumerRunningInfo(String consumerGroup, String clientId, boolean jstack) + + **释义**:broker回调客户端,获取客户端运行时信息,其返回数据结构如下,详细[参见](https://github.com/apache/rocketmq/blob/master/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumerRunningInfo.java): + + ``` + public class ConsumerRunningInfo extends RemotingSerializable { + private Properties properties = new Properties(); + + private TreeSet subscriptionSet = new TreeSet(); + + private TreeMap mqTable = new TreeMap(); + + private TreeMap statusTable = new TreeMap(); + + private String jstack; + } + ``` + + **用途**:MQCloud用于分析订阅关系,客户端阻塞情况等。 + +8. 获取所有的消费者配置:MQAdminExt.getAllSubscriptionGroup(String brokerAddr, long timeoutMillis) + + **释义**:获取broker上的所有的消费者订阅及配置。 + + **用途**:适用于全量获取订阅信息,比如部署新broker实例时,copy所有订阅的配置。 + +9. 获取topic的消费者:MQAdminExt.queryTopicConsumeByWho(String topic) + + **释义**:从broker上查询topic有哪些消费者。 + + **用途**:适用于从topic反查消费者的情况。 + +## 四、消息相关的api + +1. MQAdmin.queryMessage(String topic, String key, int maxNum, long begin, long end) + + **释义**:按照key查询topic的消息。 + + **用途**:适用于发送消息时传递了参数keys的topic。 + +2. MQAdmin.viewMessage(String topic, String msgId) + + **释义**:按照msgId查询topic的消息。 + + **用途**:适用于发送消息成功后,记录的消息id的情况。 + +## 五、broker相关的api + +1. MQAdminExt.examineBrokerClusterInfo() + + **释义**:获取集群下所有的broker的信息,返回如下数据结构: + + ``` + // 集群信息 + public class ClusterInfo { + private HashMap brokerAddrTable; + private HashMap> clusterAddrTable; + } + // broker路由 + public class BrokerData { + private String cluster; + private String brokerName; + private HashMap brokerAddrs; + } + ``` + + 例子如下: + + ![](img/5.0.png) + + **用途**:需要集群数据的地方。 + +2. MQAdminExt.viewBrokerStatsData(String brokerAddr, String statsName, String statsKey) + + **释义**:获取broker上的统计数据,broker端统计介绍[参见](./statMonitorWarning)。 + + **用途**:统计topic的生产和消费流量,并用于预警。 + +3. MQAdminExt.fetchBrokerRuntimeStats(String brokerAddr) + + **释义**:获取broker运行时状况。 + + **用途**:监测broker是否存活。 + +4. **MQAdminExt.wipeWritePermOfBroker(final String namesrvAddr, String brokerName)** + + **释义**:擦除name server上broker的写权限。 + + **用途**:broker下线时,先将broker写权限擦除。客户端每30秒会拉取一次topic队列的路由信息,然后判断broker是否可写。这样,客户端写流量逐渐停止,broker便可以安全切下线。 + +## 六、NameServer相关的api + +1. MQAdminExt.getNameServerAddressList() + + **释义**:获取name server列表的地址,该api本质没有远程调用,name server列表维护在客户端的缓存中。 + + **用途**:需要name server列表地址的地方,MQCloud用于构建一个私有的PullConsumer拉取消息。 + +2. MQAdminExt.getNameServerConfig(List nameServers) + + **释义**:获取name sever配置。 + + **用途**:监控name server是否存活。 + + + + + diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/api.toc.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/api.toc.md new file mode 100644 index 00000000..922d9d89 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/api.toc.md @@ -0,0 +1,8 @@ +##### 目录 + +- [一、topic相关的api](#topic) +- [二、生产者相关的api](#producer) +- [三、消费者相关的api](#consumer) +- [四、消息相关的api](#message) +- [五、broker相关的api](#broker) +- [六、NameServer相关的api](#nameserver) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/client.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/client.md new file mode 100644 index 00000000..bd10f936 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/client.md @@ -0,0 +1,158 @@ +## 一、简介 + +MQCloud对RocketMQ原生API进行了封装,提供了一个[增强版的SDK](../userGuide/client): [mq-client-open](https://github.com/sohutv/mqcloud/tree/master/mq-client-open)。它提供了类似特性:耗时、异常等统计、上报,序列化,自动配置,降级隔离,trace发送单独集群等等,与MQCloud配合使用,将极大提升开发效率,并能实时监控客户端的情况,下面将会一一进行介绍(当然MQCloud并不强制使用提供的SDK,只使用MQCloud做监控也同样支持)。 + +## 二、启动 + +mq-client-open启动时将会往MQCloud发送http请求,拉取生产者或消费者的配置信息,并同时上报自己使用的版本。 + +对于生产者来说,目前的配置信息有: + +1. 集群id +2. 是否开启trace(*MQCloud1.6后支持RocketMQ4.4.0的trace特性*) +3. 是否启用vip通道(*所谓vip通道,是RocketMQ3.5.8以后新增的特性,即broker会启动一个端口(配置端口-2)作为vip通道,生产者可以把消息发往此通道,消费者从旧端口消费,实现生产和消费通道分离*)。 +4. unitName(用于发现不同的集群,具体可以参考NameServer寻址部分) + +对于消费者来说,目前的配置信息有: + +1. 集群id +2. 是否开启trace +3. 消费方式(*广播或集群*)。 +4. unitName + +为什么要启动时候拉取这些信息呢? + +1. 客户端可以只用配置topic名字和group名字即可,剩余的信息可以自动配置。 + +2. 校验客户端是否配置错误,配置错误将一直循环请求配置信息,卡主启动流程,防止业务方配置错误无法生产或消费消息但是却不知道,将这种明显的问题扼杀在早期。 + +3. vip通道作为新特性,如果broker版本较低,客户端版本较高,可能导致发送消息失败,此时需要设置vipChannelEnabled=false,而mq-client-open可以知道集群的情况,自动设置此参数。 + +4. 由于MQCloud是可以运维多个集群的,所以需要具体知道某个topic在哪个集群上,从而可以根据所在集群发现集群信息,具体参见NameServer寻址。 + + 有人可能会问,这样是不是必须要求topic在所有集群全局唯一?我们觉得这不是问题,因为通过MQCloud创建topic建议是:小组名-业务名-topic,MQCloud也会在数据层面保证唯一性。 + + 另外,ConsumerGroup也必须是唯一的,如果不唯一,可能导致消费问题,具体参见[这里](https://blog.csdn.net/a417930422/article/details/50663639)。 + +## 三、序列化 + +RocketMQ的原生客户端是不提供序列化的(*可能官方认为序列化属于业务的事情*)。MQCloud的mq-client-common-open中默认是提供了[protostuff](https://protostuff.github.io/docs/)作为序列化工具,类似如下: + +``` +/** + * 构建消息 + * @param messageObject 消息 + * @param tags tags + * @param keys key + * @param delayLevel 延时级别 + * @return + * @throws Exception + */ +public Message buildMessage(Object messageObject, String tags, String keys, MessageDelayLevel delayLevel) + throws Exception { + byte[] bytes = getMessageSerializer().serialize(messageObject); + Message message = new Message(topic, tags, keys, bytes); + if (delayLevel != null) { + message.setDelayTimeLevel(delayLevel.getLevel()); + } + return message; +} +``` + +protostuff其内部是protobuf,而protobuf源自于google,其压缩能力和性能在此不再多说。 + +但是,由于RocketMQ是有两种角色的,生产者和消费者。如果生产者使用了此种方式序列化消息,那么消费者就没得选择。所以,MQCloud又提供了一种额外的扩展`com.sohu.tv.mq.serializable.StringSerializer`,其就是String.getBytes。这样客户端就可以直接传输String了,比如流行的JSON。 + +*这里需要注意一点,生产者和消费者要约定好使用何种序列化,并且此种序列化针对某个topic后续基本不可改了,否则会导致消费者消费消息时无法反序列化。* + +## 四、生产者 + +1. 发送方式 + + MQCloud对同步发送,异步发送,oneway发送,顺序发送,事务发送均作了相应封装,详细的可以运行起MQCloud后参见生产者接入,里面有详细的使用介绍。 + + *这里说明一点,关于顺序发送,MQCloud提供了如下封装的API:* + + ``` + /** + * 相同的id发送到同一个队列 + * hash方法:id % 队列数 + */ + class IDHashMessageQueueSelector implements MessageQueueSelector { + public MessageQueue select(List mqs, Message msg, Object idObject) { + long id = (Long) idObject; + int size = mqs.size(); + int index = (int) (id % size); + return mqs.get(index); + } + } + // 设置到producer + producer.setMessageQueueSelector(new IDHashMessageQueueSelector()); + // 消息发送 + long id = 123L; + Map map = new HashMap(); + map.put("id", id); + Result sendResult = producer.publishOrder(map, String.valueOf(id), id); + ``` + + *需要发送者检查返回结果,保障消息一定发送成功才行。* + + *另外,这只是发送到某个队列的时候保障顺序,如果要求严格的全局有序,即使broker宕掉的情况下,也不能乱序,那么就需要在创建topic的时候指定全局有序,RocketMQ会将topic路由存储在NameServer上,不会随着broker变化而变化了,这样将牺牲可用性来换取一致性,可以参考[这里的顺序消息部分](https://blog.csdn.net/a417930422/article/details/52585495)。* + +2. 延迟容错 + + 默认MQCloud会启用RocketMQ提供的延迟容错配置:`producer.setSendLatencyFaultEnable(true)`。 + + 即通过统计每个队列的发送耗时和异常提供策略来选择broker发送消息。 + +3. 耗时异常统计 + + 详细参考统计监控预警中的介绍。 + +4. 熔断机制 + + RocketMQ作为中间件,可能会发生整体集群不可用的极端情况,针对这种情况,有些业务如果对MQ不是强依赖,可以使用MQCloud提供的隔离版api,内部采用[hystrix](https://github.com/Netflix/Hystrix/releases)做熔断隔离,保障集群故障时不影响业务方。 + +## 五、消费者 + +1. 消息拉取方式 + + 因为pull模式需要业务自己拉取消息及存储offset,所以MQCloud只封装了push模式,即从外部看来是消息从broker源源不断的push到客户端,内部实际是RocketMQ帮忙pull过来的,push模式已经满足大部分的业务使用了。 + +2. 消息消费方式 + + 广播模式&集群模式MQCloud做了统一的封装,这个在MQCloud平台申请消费消息时,会让使用者选择`广播模式`或`集群模式`。然后使用MQCloud提供的[客户端](https://github.com/sohutv/mqcloud/blob/master/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/RocketMQConsumer.java)启动时,会自行从MQCloud拉取配置信息,并进行相关设置,MQCloud在[代码层面](https://github.com/sohutv/mqcloud/blob/master/mq-client-open/src/main/java/com/sohu/tv/mq/rocketmq/MessageConsumer.java)屏蔽了这两种消费方式在RocketMQ api层面的差异,使用者只需设置topic和consumer group和回调方法即可。 + + *对于**广播模式消费**,默认的offset存储于~/.rocketmq_offsets下,对于使用docker的用户,每次镜像重构会导致offset丢失,丢失后默认会使用broker队列的最大offset。如果不想丢失,可以指定jvm参数`-Drocketmq.client.localOffsetStoreDir=`为持久化存储来存储目录 即可。* + +3. 消息的顺序 + + 对于要求顺序消费消息的业务,回调方法与普通的一样,只是设置此参数为true即可:`com.sohu.tv.mq.rocketmq.RocketMQConsumer.consumeOrderly`。 + + 设置该参数后,RocketMQ保障同一时刻,某个topic的某个队列,只能被同一consumerGroup的消费者的一个client进行消费,其采用对topic的队列进行远程加锁和本地加锁的方式实现,[源码参考](https://github.com/apache/rocketmq/blob/master/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyService.java)。 + + *注意,默认情况下,为了保障顺序性,消费失败的消息会进行本地重试消费,至到消费成功或达到最大重试次数。* + +4. 消息重试 + + RocketMQ从4.x在push模式消费增加了consumeTimeout,默认如果该消息从拉取下来超过15分钟未被消费,那么将会被发到重试队列进行重试。对于接到消息后长时间执行任务的业务来说显然是不太合适的,MQCloud将此时间增加到了2小时。 + +## 六、Client ID + +持有同样ClientID的客户端只能跟同一个集群交互,那么如果用户想发送消息到两个集群改怎么办? + +首先说一下RocketMQ ClientID的生成规则: + +`ip@instanceName@unitName` + +释义,共有三部分组成: + +1. ip,即客户端的ip +2. instanceName,默认为固定字符串'default',如果用户没有更改,RocketMQ会转换为jvm的进程号。 +3. unitName,默认为空,但是MQCloud里使用unitName代表了集群id + +所以,我们在MQCloud里看到一些客户端链接的时候,都是类似下面的三段: + +![](img/2.9.png) + +如果使用MQCloud提供的客户端,MQCloud会设置unitName为集群id,所以可以自动区分不同的集群。 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/client.toc.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/client.toc.md new file mode 100644 index 00000000..a96dc419 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/client.toc.md @@ -0,0 +1,8 @@ +##### 目录 + +- [一、简介](#intro) +- [二、启动](#start) +- [三、序列化](#serial) +- [四、生产者](#producer) +- [五、消费者](#consumer) +- [六、Client ID](#clientId) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.0.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.0.png new file mode 100644 index 00000000..72468b15 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.0.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.2.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.2.png new file mode 100644 index 00000000..5db203a2 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.2.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.3.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.3.png new file mode 100644 index 00000000..e5189ba8 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.3.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.4.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.4.png new file mode 100644 index 00000000..0b864065 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.4.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.5.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.5.png new file mode 100644 index 00000000..9839275a Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/1.5.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.1.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.1.png new file mode 100644 index 00000000..3a9d4e02 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.1.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.2.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.2.png new file mode 100644 index 00000000..2a2ea4cf Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.2.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.3.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.3.png new file mode 100644 index 00000000..44a7fcc1 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.3.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.4.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.4.png new file mode 100644 index 00000000..7e353176 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.4.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.5.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.5.png new file mode 100644 index 00000000..a95353be Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.5.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.6.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.6.png new file mode 100644 index 00000000..75ac9a2f Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.6.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.7.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.7.png new file mode 100644 index 00000000..c8232900 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.7.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.8.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.8.png new file mode 100644 index 00000000..79ec0858 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.8.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.9.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.9.png new file mode 100644 index 00000000..62adbb00 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/2.9.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/3.1.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/3.1.png new file mode 100644 index 00000000..c501def3 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/3.1.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/3.2.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/3.2.png new file mode 100644 index 00000000..a85047a5 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/3.2.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/3.3.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/3.3.png new file mode 100644 index 00000000..81450850 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/3.3.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.1.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.1.png new file mode 100644 index 00000000..dee5f91e Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.1.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.2.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.2.png new file mode 100644 index 00000000..e3864c35 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.2.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.3.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.3.png new file mode 100644 index 00000000..0283acc0 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.3.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.4.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.4.png new file mode 100644 index 00000000..4c309599 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.4.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.5.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.5.png new file mode 100644 index 00000000..57bd9e58 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.5.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.6.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.6.png new file mode 100644 index 00000000..f08536af Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.6.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.7.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.7.png new file mode 100644 index 00000000..f6d08cf1 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/4.7.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/5.0.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/5.0.png new file mode 100644 index 00000000..15e625ac Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/5.0.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/img/5.1.png b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/5.1.png new file mode 100644 index 00000000..494da48b Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/developerGuide/img/5.1.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/intro.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/intro.md new file mode 100644 index 00000000..c61e5565 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/intro.md @@ -0,0 +1,18 @@ +## 一、背景 + +由于消息队列的特性导致其相对于其他中间件较为复杂,尤其是针对部署,迁移,监控,预警及生产消费状况等。 + +而MQCloud是具备客户端SDK,运维,监控,预警等功能的一站式RocketMQ服务平台,能够提升开发和运维效率。 + +鉴于此,有必要介绍MQCloud是到底如何做到上述的功能的。 + +## 二、目的 + +对MQCloud关键设计和规范进行介绍,以便于让大家更好的了解和使用MQCloud,进而促进MQCloud进一步发展。 + +## 三、适合人群 + +1. 使用RocketMQ作为消息中间件的开发者和运维开发人员。 +2. 对MQCloud原理感兴趣的人员。 + +阅读此系列文章需要了解一些RocketMQ的基本概念和运作原理。 diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/messageTrace.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/messageTrace.md new file mode 100644 index 00000000..deeeaa85 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/messageTrace.md @@ -0,0 +1,63 @@ +## 一、背景 + +RocketMQ从4.4.0起提供了消息的trace功能(不支持事务消息),默认其使用了与普通topic在同一集群的Name Server集群。而broker端如果启用了trace功能,默认创建一个RMQ_SYS_TRACE_TOPIC的topic,trace的数据将会发送到此topic中。 + +而MQCloud是通过域名方式发现RocketMQ集群的,它可以管理多个Name Server集群,所以MQCloud对官方的trace功能进行了部分适配,以便能够自动的将trace数据发到单独的broker集群,而且和MQCloud无缝整合。 + +## 二、后台适配 + +管理员可以通过`集群发现`菜单,创建一个新的Name Server集群,指定开启trace: + +![](./img/4.2.png) + +之后通过`集群管理`菜单正常部署broker即可。 + +## 三、前台适配 + +针对用户申请新建topic时,通过`我要生产消息`菜单,其中的如下选项: + +![](img/4.1.png) + +用户开启后,将会把用户的业务topic对应的trace topic创建在trace集群上,具体步骤参见:[7.有关Trace消息](https://github.com/sohutv/mqcloud/wiki/7.%E6%9C%89%E5%85%B3Trace%E6%B6%88%E6%81%AF) + +同样,消费者消费消息时,也会提供相应是否开启trace的选项: + +![](img/4.3.png) + +用户开启后,将会在客户端进行自动trace。 + +## 四、客户端适配 + +生产者或消费者启动时,将会从MQCloud拉取配置信息,其中就包括了是否开启了trace支持。针对开启了trace要求的客户端,MQCloud会根据用户指定的topic构建一个对应的trace topic生产者,因为规则是固定的: + +![](img/4.4.png) + +然后再根据trace topic到MQCloud查询其配置信息,进行初始化,之后用来替换官方提供的`org.apache.rocketmq.client.trace.AsyncTraceDispatcher`中的`DefaultMQProducer`,从而实现完美适配。 + +另外,MQCloud针对生产者和消费者的trace客户端单独设置了instanceName,在构建clientId(参见[2.客户端]里的Client ID部分)时将其构建了进去,这样,同一个jvm里针对同一个topic的trace客户端可以很容易区分出来: + +![](img/4.5.png) + +释义:各项含义是 ip@pid@role@clusterId + +其中role=1表示为生产者的trace客户端,role=2表示为消费者的trace客户端 + +## 五、消息查询 + +RocketMQ的原生console支持trace消息查询,而MQCloud做了进一步的优化。使用者可以通过自己的业务topic来查询,举个例子说明一下: + +比如使用者的topic是mqcloud-test-topic,位于集群1。 + +而该trace的trace topic:mqcloud-test-trace-topic位于集群2。 + +那么使用者并不关心trace数据在哪,他只需要通过mqcloud-test-topic的查询页面查询到trace数据就可以,而MQCloud正是这样设计的。 + +使用者可以通过`消息`tab,查询方式选择`trace`,选择好查询的时间段,输入key进行查询即可: + +![](img/4.6.png) + +这里展示了trace的详细信息,点击后面的`详情`的小眼睛,可以看到trace的格式化后的数据: + +![](img/4.7.png) + +另外,也可以通过其他查询方式查到结果后,点击`序号`字段,快速跳到trace页面。 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/messageTrace.toc.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/messageTrace.toc.md new file mode 100644 index 00000000..07a1d223 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/messageTrace.toc.md @@ -0,0 +1,7 @@ +##### 目录 + +- [一、背景](#background) +- [二、后台适配](#admin) +- [三、前台适配](#front) +- [四、客户端适配](#client) +- [五、消息查询](#message) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/nameServer.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/nameServer.md new file mode 100644 index 00000000..0650a79c --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/nameServer.md @@ -0,0 +1,163 @@ +## 一、Name Server + +先看一张RocketMQ的部署图: + +![](./img/1.0.png) + +Name Server的重要性: + +1. 生产者或消费者从NameServer获取topic路由信息,继而可以跟broker交互。 +2. broker定时向NameServer注册,包含broker地址和topic信息。 + +了解NameServer请[参考](https://blog.csdn.net/a417930422/article/details/52585414)。这里不再过多介绍NameServer。 + +## 二、Name Server寻址方式 + +先看一下官方的文档介绍: + +``` +RocketMQ有多种配置方式可以令客户端找到Name Server, 然后通过Name Server再找到Broker,分别如下: +优先级由高到低,高优先级会覆盖低优先级。 + +一、 代码中指定 Name Server地址 +producer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876"); +或 +consumer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876"); + +二、 Java 启动参数中指定Name Server地址 +-Drocketmq.namesrv.addr=192.168.0.1:9876;192.168.0.2:9876 + +三、 环境变量量指定 Name Server地址 +export NAMESRV_ADDR=192.168.0.1:9876;192.168.0.2:9876 + +四、 HTTP 静态服务器寻址 +客户端启动后,会定时访问一个静态 HTTP 服务器,地址如下: +http://jmenv.tbsite.net:8080/rocketmq/nsaddr +返个URL的返回内容如下: +192.168.0.1:9876;192.168.0.2:9876 +客户端默认每隔2分钟访问一次返个 HTTP 服务器,并更新本地的 Name Server 地址。 +URL已经在代码中写死,可通过修改/etc/hosts文件来配置为要访问的服务器。 +推荐使用 HTTP 静态服务器寻址方式,好处是客户端部署简单,且 Name Server 集群可以热升级。 +``` + +*由于Name Server集群中每个节点之间是不进行通信的,也就是Name Server集群每个节点是孤立的,所以客户端必须知道Name Server集群中所有的节点。采用http域名方式可以动态修改节点个数,从而实现动态扩容,缩容,上下线等运维操作。* + +MQCloud采用了官方推荐的方式:使用域名的方式的寻址,方便生产环境节点迁移等运维。具体来说,其实包括两部分: + +1. 客户端的寻址,包括生产者和消费者如何发现Name Server集群。 +2. broker端的寻址,broker需要向Name Server集群定时注册。 + +但是这两部分的寻址代码都是一样的,下面会进行说明。 + +## 三、Name Server Http服务器-MQCloud + +MQCloud使用自身作为发现Name Server的http服务器。如果按照[wiki](https://github.com/sohutv/mqcloud/wiki)部署过MQCloud,那么访问如下类似url: + +``` +http://127.0.0.1:8080/rocketmq/nsaddr-1 +``` + +就会发现其返回了Name Server集群的列表: + +``` +127.0.0.1:9876;127.0.0.2:9876; +``` + +URL解释: + +1. `http://127.0.0.1:8080`MQCloud域名,具体线上使用时建议采用域名 +2. `/rocketmq/nsaddr-{unitName}`RocketMQ按照http寻址拼装的路径,下面有详细解释。 + +MQCloud是支持多集群Name Server发现的,其实就是借用了RocketMQ的http寻址方式中的unitName。 + +说到这里,不得不说RocketMQ根据http寻址的代码(这里将代码进行简化,只列出关键代码): + +``` +// 第一步.获取系统配置域名的变量 +String wsDomainName = System.getProperty("rocketmq.namesrv.domain", DEFAULT_NAMESRV_ADDR_LOOKUP); +// 第二步.获取域名后的子路径 +String wsDomainSubgroup = System.getProperty("rocketmq.namesrv.domain.subgroup", "nsaddr"); +String url = "http://" + wsDomainName + ":8080/rocketmq/" + wsDomainSubgroup; +if (wsDomainName.indexOf(":") > 0) { + // 第三步.如果域名包含冒号,则改变拼装路径 + url = "http://" + wsDomainName + "/rocketmq/" + wsDomainSubgroup; +} +// 第四步.如果设置了unitName,则改变拼装路径 +if (!UtilAll.isBlank(this.unitName)) { + url = wsAddr + "-" + this.unitName + "?nofix=1"; +} +``` + +详细解释(以域名为127.0.0.1为例): + +1. 第一步中,MQCloud会设置环境变量setProperty("rocketmq.namesrv.domain", "127.0.0.1:80")。 + +2. 第三步中,由于wsDomainName携带了冒号,所有url会变成:`http://127.0.0.1:80/rocketmq/nsaddr` + +3. 第四步中,MQCloud会进行类似设置:clientConfig.setUnitName("clusterId");,这样,RocketMQ的Name Server http url就会携带集群id,类似如下: + + ``` + http://127.0.0.1:8080/rocketmq/nsaddr-{clusterId} + ``` + + 那么,MQCloud只要配置好对应路径的controller映射,即可根据集群id从数据库查询Name Server列表了,代码类似如下: + + ![](img/1.2.png) + +## 四、针对客户端的配置 + +这里的客户端是指生产者和消费者,MQCloud会自动进行如下设置,[使用参见](https://github.com/sohutv/mqcloud/wiki/4.%E5%AE%A2%E6%88%B7%E7%AB%AF%E7%9A%84%E4%BD%BF%E7%94%A8): + +``` +// 设置Name Server域名 +setProperty("rocketmq.namesrv.domain", "mqcloud域名:80"); +// 设置单元名 +clientConfig.setUnitName("集群id"); +``` + +其中,`mqcloud域名`是需要写死的。而`集群id`是客户端启动时候自动从MQCloud上查询而来的,类似如下: + +``` +访问:http://mqcloud域名/cluster/info?topic=mail-test&group=mail-media-group-test&role=1 +返回值: +{ + status: 200, + message: "OK", + result: { + clusterId: 1, // 集群id + vipChannelEnabled: true, // 开启虚拟通道 + broadcast: false, // 广播消费 + traceEnabled: false // 开启trace + } +} +``` + +这里不过多介绍此机制,后续在客户端里进行详细介绍。 + +## 五、针对broker的配置 + +broker的配置方式与客户端不同,但是和客户端使用的代码都是一样的,具体如下: + +``` +rmqAddressServerDomain=mqcloud域名:80 +rmqAddressServerSubGroup=nsaddr-集群id +fetchNamesrvAddrByAddressServer=true +``` + +如果使用MQCloud部署,这些设置都会是自动的。由此可以看到http方式发现Name Server方式域名的重要性,所以[MQCloud使用](https://github.com/sohutv/mqcloud/wiki/3.%E5%88%9B%E5%BB%BA%E9%9B%86%E7%BE%A4)里,一开始就强调了这项配置: + +![](img/1.3.png) + +而集群id,我们使用MQCloud一开始就需要创建集群记录: + +![](img/1.4.png) + +这样就会在数据库的cluster表增加一条记录,然后使用`+NameServer`或者`关联NameServer`功能,就可以将NameServer列表和cluster建立关系,从而可以实现根据clusterId查找NameServer列表的功能。 + +## 六、MQCloud高可用部署 + +*如果没有使用MQCloud提供的客户端和部署RocketMQ,仅使用其监控,统计,预警功能,那么此步骤可以跳过。* + +如果使用MQCloud部署了RocketMQ,那么MQCloud肩负了Name Server http方式寻址的重要责任,我们需要保证MQCloud的高可用,由于MQCloud其实仅仅是一个web应用,所以按照普通web应用高可用的方式部署即可,部署参照如下: + +![](img/1.5.png) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/nameServer.toc.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/nameServer.toc.md new file mode 100644 index 00000000..b95f2fc4 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/nameServer.toc.md @@ -0,0 +1,8 @@ +##### 目录 + +- [一、Name Server](#ns) +- [二、Name Server寻址方式](#addressing) +- [三、Name Server Http服务器-MQCloud](#http) +- [四、针对客户端的配置](#client) +- [五、针对broker的配置](#broker) +- [六、MQCloud高可用部署](#deploy) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/statMonitorWarning.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/statMonitorWarning.md new file mode 100644 index 00000000..e41e7c97 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/statMonitorWarning.md @@ -0,0 +1,263 @@ +## 一、统计 + +要想做监控,必须先做统计,为了更好的知道RocketMQ集群的运行状况,MQCloud做了大量的统计工作,主要包括如下几项: + +1. 每分钟topic的生产流量:用于绘制topic生产流量图及监控预警。 +2. 每分钟消费者流量:用于绘制消费流量图及监控预警。 +3. 每10分钟topic生产流量:用于按照流量展示topic排序。 +4. 每分钟broker生产、消费流量:用于绘制broker生产消费流量图。 +5. 每分钟broker集群生产、消费流量:用于绘制broker集群的生产流量图。 +6. 每分钟生产者百分位耗时、异常统计:以ip维度绘制每个生产者的耗时流量图及监控预警。 +7. 机器的cpu,内存,io,网络流量,网络连接等统计:用于服务器的状况图和监控预警。 + +下面来分别介绍每项统计是如何收集的: + +1. **每分钟topic的生产流量** + + 此数据来自于RocketMQ broker端[BrokerStatsManager](https://github.com/apache/rocketmq/blob/master/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStatsManager.java),其提供了统计功能,统计项如下: + + 1. TOPIC_PUT_NUMS:某topic消息生产条数,向某个topic写入消息成功才算 + + *写入成功包括四种状态:PUT_OK,FLUSH_DISK_TIMEOUT,FLUSH_SLAVE_TIMEOUT,SLAVE_NOT_AVAILABLE* + + 2. TOPIC_PUT_SIZE:某topic消息生产大小,向某个topic写入消息成功才算 + + RocketMQ实现的统计逻辑较为精巧,这里做简单描述,首先介绍几个对象: + + 1. StatsItemSet主要字段及方法如下: + + ``` + ConcurrentMap statsItemTable; // statsKey<->StatsItem + // 针对某个数据项进行记录 + public void addValue(final String statsKey, final int incValue, final int incTimes) { + StatsItem statsItem = this.getAndCreateStatsItem(statsKey); + statsItem.getValue().addAndGet(incValue); + statsItem.getTimes().addAndGet(incTimes); + } + // 获取并创建StatsItem + public StatsItem getAndCreateStatsItem(final String statsKey) { + StatsItem statsItem = this.statsItemTable.get(statsKey); + if (null == statsItem) { + statsItem = new StatsItem(this.statsName, statsKey); + this.statsItemTable.put(statsKey, statsItem); + } + return statsItem; + } + ``` + + 2. StatsItem主要字段及方法如下: + + ``` + AtomicLong value; // 统计数据:比如消息条数,消息大小 + AtomicLong times; // 次数 + LinkedList csListMinute; // 每分钟快照数据 + LinkedList csListHour; // 每小时快照数据 + LinkedList csListDay; // 每天快照数据 + // 分钟采样 + public void samplingInSeconds() { + synchronized (csListMinute) { + csListMinute.add(new CallSnapshot(System.currentTimeMillis(), times.get(), value.get())); + if (csListMinute.size() > 7) { + csListMinute.removeFirst(); + } + } + } + // 小时采样 + public void samplingInMinutes() { + // ...代码省略 + } + // 天采样 + public void samplingInHour() { + // ...代码省略 + } + ``` + + 3. CallSnapshot主要字段如下: + + ``` + long times; // 次数快照 + long value; // 统计数据快照 + long timestamp; //快照时间戳 + ``` + + 上面三个对象如何配合进行数据统计呢?举个例子,比如统计topic名字为test_topic的消息生产大小: + + 只要进行类似如下调用即可: + + ``` + StatsItemSet.addValue("test_topic", 123125123, 1) + ``` + + 即表示发送了1次消息到test_topic,消息大小为123125123。 + + 那如何进行数据采样呢?StatsItemSet内置了定时任务,比如其每10秒调用一次StatsItem.samplingInSeconds()。这样StatsItem就会持有60秒的数据,类似如下结构: + + ![](img/3.1.png) + + 那么,最后一个10秒的快照 - 第一个10秒的快照 = 当前60秒的数据,当然根据时间戳差值可以得到耗时了。 + + 类似,小时数据每10分钟进行一次快照,类似如下结构: + + ![](img/3.2.png) + + 天数据每1小时进行一次快照,类似如下结构: + + ![](img/3.3.png) + + MQCloud每分钟遍历查询集群下所有broker,通过api:[MQAdminExt.viewBrokerStatsData(String brokerAddr, String statsName, String statsKey)](api#viewBrokerStatsData)来查询RocketMQ统计好的分钟数据,然后进行存储。 + +2. **每分钟消费者流量** + + 与[每分钟topic的生产流量](#topicTraffic)一样,也采用RocketMQ统计好的数据。 + +3. **每10分钟topic生产流量** + + 采用数据库已经统计好的每分钟topic流量进行累加,统计出10分钟流量。 + +4. **每分钟broker生产、消费流量** + + 由于统计[1.每分钟topic的生产流量](#topicTraffic)和[2.每分钟消费者流量](#consumerTraffic)时是跟broker交互获取的,所以知道broker ip,故直接按照broker维度存储一份数据即可。 + +5. **每分钟broker集群生产、消费流量** + + 采用[4.每分钟broker生产、消费流量](#brokerTraffic)数据,按照集群求和即可。 + +6. **每分钟生产者百分位耗时、异常统计** + + 由于RocketMQ并没有提供生产者的流量统计(*只提供了topic,但是并不知道每个生产者的情况*),所以MQCloud实现了对生产者数据进行统计(*通过RocketMQ的回调钩子实现*),主要统计如下信息: + + 1. 客户端ip->broker ip + 2. 发送消息耗时 + 3. 消息数量 + 4. 发送异常 + + 统计完成后,定时发送到MQCloud进行存储,并做实时监控和展示。 + + 关于统计部分有一点说明,一般耗时统计有最大,最小和平均值,而通常99%(即99%的请求耗时都低于此数值)的请求的耗时情况才能反映真实响应情况。99%请求耗时统计最大的问题是如何控制内存占用,因为需要对某段时间内所有的耗时做排序后才能统计出这段时间的99%的耗时状况。而对于流式数据做这样的统计是有一些算法和数据结构的,例如[t-digest](https://github.com/tdunning/t-digest),但是MQCloud采用了非精确的但是较为简单的[分段统计](https://github.com/sohutv/mqcloud/blob/master/mq-client-common-open/src/main/java/com/sohu/tv/mq/stats/TimeSectionStats.java)的方法,具体如下: + + 1. 创建一个按照最大耗时预哈希的时间跨度不同的**耗时分段数组**: + + 1. 第一段:耗时范围0ms~10ms,时间跨度为1ms。 + + ![](img/2.1.png) + + 2. 第二组:耗时范围11ms~100ms,时间跨度5ms。 + + ![](img/2.2.png) + + 3. 第三组:耗时范围101ms~3500ms,时间跨度50ms。 + + ![](img/2.3.png) + + *优点:此种分段方法占用内存是固定的,比如最大耗时如果为3500ms,那么只需要空间大小为96的数组即可* + + *缺点:分段精度需要提前设定好,且不可更改* + + 2. 针对上面的分段数组,创建一个大小对应的AtomicLong的**计数数组**,支持并发统计: + + ![](img/2.4.png) + + 3. 耗时统计时,计算耗时对应的**耗时分段数组**下标,然后调用**计数数组**进行统计即可,参考下图: + + ![](img/2.5.png) + + 1. 例如某次耗时为18ms,首先找到它所属的区间,即归属于[16~20]ms之间,对应的数组下标为12。 + 2. 根据第一步找到的数组下标12,获取对应的计数数组下标12。 + 3. 获取对应的计数器进行+1操作,即表示18ms发生了一次调用。 + + 这样,从**计数数组**就可以得到实时耗时统计,类似如下: + + ![](./img/2.6.png) + + 4. 然后定时采样任务会每分钟对**计数数组**进行快照,产生如下**耗时数据**: + + ![](img/2.7.png) + + 5. 由于上面的**耗时数据**天然就是排好序的,可以很容易计算99%耗时,90%耗时,平均耗时等数据了。 + + *另外提一点,由于RocketMQ 4.4.0新增的trace功能也使用hook来实现,与MQCloud的统计有冲突,MQCloud已经做了兼容。* + + *Trace和统计是两种维度,trace反映的是消息从生产->存储->消费的流程,而MQCloud做的是针对生产者状况的统计,有了这些统计数据,才可以做到生产耗时情况展示,生产异常情况预警等功能。* + +7. **机器统计** + + 关于集群状况收集主要采用了将[nmon](http://nmon.sourceforge.net/pmwiki.php)自动放置到/tmp目录,定时采用ssh连接到机器执行nmon命令,解析返回的数据,然后进行存储。 + +## 二、消费堆积预警 + +RocketMQ针对消费者的消费情况提供了一种[监控实现](https://github.com/apache/rocketmq/blob/master/tools/src/main/java/org/apache/rocketmq/tools/monitor/MonitorService.java),MQCloud基于此增加了多Name Server集群的[支持](https://github.com/sohutv/mqcloud/blob/master/mq-cloud/src/main/java/com/sohu/tv/mq/cloud/task/monitor/MonitorService.java)。这里来阐明一下此种监控方式的实现流程: + +1. 从Name Server获取到所有的topic。(api为:[MQAdminExt.fetchAllTopicList()](api#fetchAllTopicList)) + +2. 遍历找出其中%RETRY%开头的topic + + *集群消费模式,会自动创建一个%RETRY%ConsumerGroup的topic,用于消费者消费失败时发送重试消息,所以此监控方式**只监控集群消费模式**的消费者。* + +3. 检测消息堆积情况 + + 1. 检测此消费者的链接状况:(api为[MQAdminExt.examineConsumerConnectionInfo(String consumerGroup)](api#examineConsumerConnectionInfo)) + + *该api本质是从broker上查找消费者的订阅信息,链接信息等,消费者通过心跳向broker上报这些数据。* + + 2. 获取此消费者订阅topic的每个队列的如下信息:(api为[MQAdminExt.examineConsumeStats(String consumerGroup](api#examineConsumeStats)) + + 1. broker端偏移量 + 2. 消费者偏移量 + 3. 消费者消费的最新的消息的存储时间(*可以认为消费者消费到了此时间*) + + 据此,可以计算出消费者消费的消息堆积了多少,堆积了多久。 + + *该维度主要是通过broker上的数据情况来检测消费者消息的堆积情况。* + + +## 三、客户端阻塞预警 + +与[二、消费堆积预警](#consumeAccumulate)检测方式一样,MQCloud通过如下项来进行客户端阻塞检测: + +1. 从Name Server获取到所有的topic。(api为:MQAdminExt.fetchAllTopicList();) + +2. 遍历找出其中%RETRY%开头的topic + +3. 检测客户端情况 + + 获取客户端运行时信息,类似如下:(api为[MQAdminExt.getConsumerRunningInfo](api#getConsumerRunningInfo) *该api本质为broker反调客户端,主要获取客户端的*) + 1. 订阅信息(*检测该项可以得知一个消费者是否订阅多个topic*) + + 2. 每个队列的消息消费时间 + + 据此,可以检测一个消费者订阅多个topic的情况,也可以检测客户端阻塞的情况。 + + *该维度主要是通过消费者自身的数据情况来检测消费者的阻塞情况。* + +[二、消费堆积预警](#consumeAccumulate)和[三、客户端阻塞预警](#clientBlock)两个维度的预警虽然一个从broker出发,一个从consumer出发,但是一般同时发生。因为如果消费者消费的慢,或者消费阻塞时,会直接导致消息的堆积,所以经常会有业务会同时收到**消费堆积**和**客户端阻塞**的预警。 + +## 四、生产失败预警 + +使用了[6.每分钟生产者百分位耗时、异常统计](#clientStat)中的统计数据,MQCloud会定时扫描异常表,然后进行预警。 + +## 五、消费失败预警 + +MQCloud使用了broker端[BrokerStatsManager](https://github.com/apache/rocketmq/blob/master/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStatsManager.java)提供的状态统计数据,数据项为SNDBCK_PUT_NUMS:该项代表某个消费者发送重试消息的条数。MQCloud定时获取该值,并进行预警(消费者需要开启重试机制)。 + +## 六、偏移量错误预警 + +RocketMQ内置一个名字为OFFSET_MOVED_EVENT的topic,当消费者拉取消息时,发生下面几种情况,RocketMQ会往该topic发送消息: + +1. 对于某个队列来说,消费者请求的最小偏移量小于broker上的最小偏移量。 +2. 对于某个队列来说,消费者请求的最大偏移量大于broker上的最大偏移量。 + +上面的情况都是消费者请求的偏移量发生了错误,不可能拉取到消息。MQCloud参考RocketMQ的[监控实现](https://github.com/apache/rocketmq/blob/master/tools/src/main/java/org/apache/rocketmq/tools/monitor/MonitorService.java),针对这种偏移量错误进行预警。 + +## 七、其他监控预警 + +1. Name Server集群监控 + + 针对每个Name Server,通过调用api:[MQAdminExt.getNameServerConfig(List nameServers)](api#getNameServerConfig)来探测Name Server是否可以响应。 + +2. Broker集群监控 + + 通过每个broker,通过调用api:[MQAdminExt.fetchBrokerRuntimeStats(String brokerAddr)](api#fetchBrokerRuntimeStats)来探测broker是否可以响应。 + +3. 服务器监控 + + 针对采集的服务器状态数据,检测各个状态值是否超过后台配置的服务器阈值,然后进行预警。 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/developerGuide/statMonitorWarning.toc.md b/mq-cloud/src/main/resources/static/wiki/developerGuide/statMonitorWarning.toc.md new file mode 100644 index 00000000..dd2025c1 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/developerGuide/statMonitorWarning.toc.md @@ -0,0 +1,9 @@ +##### 目录 + +- [一、统计](#stat) +- [二、消费堆积预警](#consumeAccumulate) +- [三、客户端阻塞预警](#clientBlock) +- [四、生产失败预警](#produceError) +- [五、消费失败预警](#consumeError) +- [六、偏移量错误预警](#offsetError) +- [七、其他监控预警](#otherWarn) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/img/intro/addBroker.png b/mq-cloud/src/main/resources/static/wiki/intro/img/addBroker.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/addBroker.png rename to mq-cloud/src/main/resources/static/wiki/intro/img/addBroker.png diff --git a/mq-cloud/src/main/resources/static/wiki/intro/img/cluster.png b/mq-cloud/src/main/resources/static/wiki/intro/img/cluster.png new file mode 100644 index 00000000..554b1a82 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/intro/img/cluster.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/intro/img/clusterTraffic.png b/mq-cloud/src/main/resources/static/wiki/intro/img/clusterTraffic.png new file mode 100644 index 00000000..a7ad1a82 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/intro/img/clusterTraffic.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/intro/img/consumeDetail2.png b/mq-cloud/src/main/resources/static/wiki/intro/img/consumeDetail2.png new file mode 100644 index 00000000..75ffc37a Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/intro/img/consumeDetail2.png differ diff --git a/mq-cloud/src/main/resources/static/img/intro/consumeRetry.png b/mq-cloud/src/main/resources/static/wiki/intro/img/consumeRetry.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/consumeRetry.png rename to mq-cloud/src/main/resources/static/wiki/intro/img/consumeRetry.png diff --git a/mq-cloud/src/main/resources/static/wiki/intro/img/index.png b/mq-cloud/src/main/resources/static/wiki/intro/img/index.png new file mode 100644 index 00000000..6c7a2a8b Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/intro/img/index.png differ diff --git a/mq-cloud/src/main/resources/static/img/intro/mqcloud.png b/mq-cloud/src/main/resources/static/wiki/intro/img/mqcloud.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/mqcloud.png rename to mq-cloud/src/main/resources/static/wiki/intro/img/mqcloud.png diff --git a/mq-cloud/src/main/resources/static/wiki/intro/img/msgSearch.png b/mq-cloud/src/main/resources/static/wiki/intro/img/msgSearch.png new file mode 100644 index 00000000..8c1787a5 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/intro/img/msgSearch.png differ diff --git a/mq-cloud/src/main/resources/static/img/intro/msgTrack.png b/mq-cloud/src/main/resources/static/wiki/intro/img/msgTrack.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/msgTrack.png rename to mq-cloud/src/main/resources/static/wiki/intro/img/msgTrack.png diff --git a/mq-cloud/src/main/resources/static/img/intro/nameServer.png b/mq-cloud/src/main/resources/static/wiki/intro/img/nameServer.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/nameServer.png rename to mq-cloud/src/main/resources/static/wiki/intro/img/nameServer.png diff --git a/mq-cloud/src/main/resources/static/wiki/intro/img/produceDetail2.png b/mq-cloud/src/main/resources/static/wiki/intro/img/produceDetail2.png new file mode 100644 index 00000000..9763bd8e Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/intro/img/produceDetail2.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/intro/img/topicDetail.png b/mq-cloud/src/main/resources/static/wiki/intro/img/topicDetail.png new file mode 100644 index 00000000..2d50a6d5 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/intro/img/topicDetail.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/intro/index.md b/mq-cloud/src/main/resources/static/wiki/intro/index.md new file mode 100644 index 00000000..3a597266 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/intro/index.md @@ -0,0 +1,80 @@ +## MQCloud-集客户端SDK,运维,监控预警等功能的[RocketMQ](https://github.com/apache/rocketmq)企业级一站式服务平台 +**它具备以下特性:** + +* 跨集群:可以同时管理多个集群,对使用者透明。 + +* 预警功能:针对生产或消费堆积,失败,异常等情况预警。 + +* 简单明了:用户视图-拓扑、流量、消费状况等指标直接展示;管理员视图-集群运维、监控、流程审批等。 + +* 安全:用户隔离,操作审批,数据安全。 + +* 更多特性正在开发中。 + +* 下图简单描述了MQCloud大概的功能: + + ![mqcloud](img/mqcloud.png) + + +---------- + +## 特性概览 +* 用户topic列表-不同用户看到不同的topic,管理员可以管理所有topic + + ![用户topic列表](img/index.png) + +* topic详情-分三块 基本信息,今日流程,拓扑 + + ![topic详情](img/topicDetail.png) + +* 生产详情 + + ![生产详情](img/produceDetail2.png) + +* 消费详情 + + ![消费详情](img/consumeDetail2.png) + +* 某个消费者具体的消费详情-可以查询重试消息和死消息 + + ![消费详情](img/consumeRetry.png) + +* 消息 + + ![消息](img/msgSearch.png) + +* 消息消费情况 + + ![msgconsume](img/msgTrack.png) + +* 集群发现 + + ![admin](img/nameServer.png) + +* 集群管理 + + ![admin](img/cluster.png) + +* 集群流量 + + ![admin](img/clusterTraffic.png) + +* 创建broker + + ![addBroker](img/addBroker.png) + +---------- + +## 目前运维的规模 +1. 服务器:40台+ +2. 集群:5个+ +3. topic:370个+ +4. 生产消费消息量/日:10亿条+ +5. 生产消费消息大小/日:1T+ +---------- + +## 联系方式 + +MQCloud QQ交流群:474960759 + +使用方式请参考[wiki](https://github.com/sohutv/sohu-tv-mq/wiki)。 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/intro/index.toc.md b/mq-cloud/src/main/resources/static/wiki/intro/index.toc.md new file mode 100644 index 00000000..d5c6b5c2 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/intro/index.toc.md @@ -0,0 +1,6 @@ +##### 目录 + +- [MQCloud-集客户端SDK,运维,监控预警等功能的RocketMQ企业级一站式服务平台](#title) +- [特性概览](#future) +- [目前运维的规模](#situation) +- [联系方式](#contract) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/troubleshooting/broadcastingOffsetMovedEvent.md b/mq-cloud/src/main/resources/static/wiki/troubleshooting/broadcastingOffsetMovedEvent.md new file mode 100644 index 00000000..3909fd90 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/troubleshooting/broadcastingOffsetMovedEvent.md @@ -0,0 +1,183 @@ +## 一、现象 +mqcloud持续发送topic为digg-topic的消费者digg-group发生偏移量错误的预警邮件,详细预警如下: + +![](img/1_offset_warn.png) + +即:digg-group请求从偏移量**156798**开始消费,但是broker上最小的消息偏移量是**172289**,也就是说,**消费者想请求消费的消息,在broker上已经不存在了。** + +*解释:rocketmq会将此种情况当做一个事件消息发送到内置的topic:OFFSET_MOVED_EVENT中,mqcloud会订阅并消费该topic,并会以固定频率进行预警。* + +## 二、mqcloud监控情况 + +顺着预警邮件的链接,到mqcloud里看下消费者的具体情况,发现digg-group消费有堆积,详细如下: + +![](img/1_consume_detail.png) + +点开查看每个客户端的消费情况,定位到某个机器消费有堆积: + +![](img/1_consume_detail_ip.png) + +## 三、broker表现 + +找到对应的broker的某个实例,查看broker.log日志,发现很多类似如下的日志: + +``` +2019-01-18 19:24:01 INFO PullMessageThread_49 - the request offset too small. group=digg-group, topic=digg-topic, requestOffset=156798, brokerMinOffset=172289, clientIp=/10.*.*.*:54437 +2019-01-18 19:24:01 WARN PullMessageThread_49 - PULL_OFFSET_MOVED:correction offset. topic=digg-topic, groupId=digg-group, requestOffset=156798, newOffset=172289, suggestBrokerId=0 +2019-01-18 19:24:18 INFO ClientManageThread_27 - subscription changed, group: digg-group OLD: SubscriptionData [classFilterMode=false, topic=digg-topic, subString=*, tagsSet=[], codeSet=[], + subVersion=1547810638588, expressionType=null] NEW: SubscriptionData [classFilterMode=false, topic=digg-topic, subString=*, tagsSet=[], codeSet=[], subVersion=1547810658600, expressionType +=null] +。。。 。。。 +2019-01-18 19:24:38 INFO ClientManageThread_29 - subscription changed, group: digg-group OLD: SubscriptionData [classFilterMode=false, topic=digg-topic, subString=*, tagsSet=[], codeSet=[], + subVersion=1547810658600, expressionType=null] NEW: SubscriptionData [classFilterMode=false, topic=digg-topic, subString=*, tagsSet=[], codeSet=[], subVersion=1547810678611, expressionType +=null] +``` + +第一条日志含义: + +``` +the request offset too small. group=digg-group, topic=digg-topic, requestOffset=156798, brokerMinOffset=172289, clientIp=/10.*.*.*:54437 +``` + +客户端请求消费的消息offset太小了,即消息不存在。 + +第二条日志含义: + +``` +PULL_OFFSET_MOVED:correction offset. topic=digg-topic, groupId=digg-group, requestOffset=156798, newOffset=172289, suggestBrokerId=0 +``` + +与第一条相同,只是把这次请求事件当做一条消息发送到了OFFSET_MOVED_EVENT中。 + +第三条日志含义:(简化下日志) + +``` +subscription changed, group: digg-group OLD: SubscriptionData [topic=digg-topic, subVersion=1547810638588] NEW: SubscriptionData [topic=digg-topic, subVersion=1547810658600] +``` + +此日志表明消费者的订阅关系发生了改变,其上报心跳到broker,broker检测到关系变化,打印该日志。 + +中间省略符号: + +省略了重复的日志。 + +第四条日志含义: + +在20秒过后,又重复打印类似第三条日志内容,该日志对应broker的`org.apache.rocketmq.broker.client.ConsumerGroupInfo` 的如下代码: + +![](img/1_update_subscription.png) + +证明消费者的subVersion发生了更新。 + +综上:**消费者确实发送的offset过小,之后消费者更新了subVersion。** + +但是,有几个问题: + +1. **为什么消费者的subVersion会每隔20秒,就发生一次更新?** +2. **broker告诉消费者正确的offset后,消费者为什么没有采用,还是发送之前错误的offset?** + +## 四、消费者代码跟踪 + +带着上面两个问题,来捋一下消费者的代码。 + +首先看第2个问题:`2. broker告诉消费者正确的offset后,消费者为什么没有采用,还是发送之前错误的offset?` ,针对consumer请求偏移量过小的情况,broker会响应ResponseCode.PULL_OFFSET_MOVED的状态码,消费者会转换为PullStatus.OFFSET_ILLEGAL,`DefaultMQPushConsumer`对应的行为是更新`OffsetStore`为broker返回的正确值,接着标记该`ProcessQueue`下线,再从处理队列中移除掉,即不再消费这个队列,对应代码如下: + +![](img/1_offset_illegal.png) + +那么,不消费这个队列的话,肯定会导致堆积,那怎么才能重新消费这个队列呢? + +奥秘就在于再平衡过程,查看`org.apache.rocketmq.client.impl.consumer.RebalanceService` ,它会将topic的队列分配给对应的`ProcessQueue` 对象,然后封装成PullRequest进行消息拉取,具体如下图: + +1. rebalance过程![](https://img-blog.csdn.net/20160919143010798?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center) + +2. 相关对象结构 + + ![](https://img-blog.csdn.net/20160919143024654?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center) + +对这块有疑问的同学请参考我之前的文章:[8.consumer](https://blog.csdn.net/a417930422/article/details/52585548)。 + +了解这块代码就可以知道,无论是路由关系:topic<->broker<->队列关系发生变化,还是consumer的消费关系:比如移除`ProcessQueue` 发生变化,都会进行再平衡:即对所有队列进行重新分配消费,包括有问题的队列。 + +那么还是之前的问题,**为什么重新消费有问题的队列offset取的还是旧的呢?** + +难道是重新分配消费时没有取到正确的offset吗?看下rebalance对应的代码:`org.apache.rocketmq.client.impl.consumer.RebalanceImpl.updateProcessQueueTableInRebalance` + +![](img/1_rebalance_pullRequest.png) + +关键的代码是`long nextOffset = this.computePullFromWhere(mq)`,它会从`OffsetStore`读取offset并作为起始偏移量进行消息消费,对于broadcasting模式的消费者来说,offset存储在本地文件,即`LocalFileOffsetStore`,存储位置默认为启动程序的用户主目录下的`~/.rocketmq_offsets`下。来跟踪一下`computePullFromWhere(mq)`,简化代码如下: + +``` +long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE); +``` + +offsetStore.readOffset简化代码如下: + +``` +public long readOffset(final MessageQueue mq, final ReadOffsetType type) { + switch (type) { + case READ_FROM_STORE: { + OffsetSerializeWrapper offsetSerializeWrapper; + try { + offsetSerializeWrapper = this.readLocalOffset(); + } catch (MQClientException e) { + return -1; + } + if (offsetSerializeWrapper != null && offsetSerializeWrapper.getOffsetTable() != null) { + AtomicLong offset = offsetSerializeWrapper.getOffsetTable().get(mq); + if (offset != null) { + this.updateOffset(mq, offset.get(), false); + return offset.get(); + } + } + } + } +} +``` + +this.readLocalOffset代码如下: + +``` +private OffsetSerializeWrapper readLocalOffset() throws MQClientException { + String content = null; + try { + content = MixAll.file2String(this.storePath); + } catch (IOException e) { + log.warn("Load local offset store file exception", e); + } + if (null == content || content.length() == 0) { + return this.readLocalOffsetBak(); + } else { + OffsetSerializeWrapper offsetSerializeWrapper = null; + try { + offsetSerializeWrapper = + OffsetSerializeWrapper.fromJson(content, OffsetSerializeWrapper.class); + } catch (Exception e) { + log.warn("readLocalOffset Exception, and try to correct", e); + return this.readLocalOffsetBak(); + } + + return offsetSerializeWrapper; + } +} +``` + +即直接从`~/.rocketmq_offsets`下读取对应的文件,返回相应的偏移量。 + +我们已经知道broker如果响应`ResponseCode.PULL_OFFSET_MOVED`,消费者会更新`OffsetStore`为broker返回正确值。而`OffsetStore`是会定时将内存的offset持久化到硬盘上的,也就是说**可能持久化有问题,导致偏移量无法更新**。 + +## 五、消费者表现 + +**联系业务同学,发现rocketmq offset文件所在磁盘故障,导致offset无法写入**。由于消费者没有增加rocketmq相关的日志配置,所以没有更详细的日志输出,建议消费者一定加上[日志配置](https://github.com/sohutv/sohu-tv-mq/tree/master/mq-cloud/src/main/resources/static/file)。 + +到这里,还有一个遗留问题,就是`1. 为什么消费者的subVersion每隔20秒,就发生一次更新?`查看`org.apache.rocketmq.client.impl.consumer.RebalanceService` 果然发现其执行频率为20秒一次: + +`long waitInterval = Long.parseLong(System.getProperty("rocketmq.client.rebalance.waitInterval", "20000"))`。 + +当`四、消费者代码跟踪`中发生再平衡后,会导致订阅关系发生更新,参考`org.apache.rocketmq.client.impl.consumer.RebalancePushImpl`如下代码: + +![](img/1_message_queue_changed.png) + +## 六、总结 + +1. broadcasting模式消费者,偏移量存储在本地,如果本地磁盘有问题,导致不可写,此时重启的话,offset会从本地磁盘读取,可能会导致消费停滞。 +2. 另外,针对docker,建议设置`rocketmq.client.localOffsetStoreDir=/持久化路径`即,将offset写到可以持久化的存储里,否则重启消费者会导致消费时跳过部分消息。 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/troubleshooting/broadcastingOffsetMovedEvent.toc.md b/mq-cloud/src/main/resources/static/wiki/troubleshooting/broadcastingOffsetMovedEvent.toc.md new file mode 100644 index 00000000..3932e6ef --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/troubleshooting/broadcastingOffsetMovedEvent.toc.md @@ -0,0 +1,8 @@ +##### 目录 + +- [一、现象](#state) +- [二、mqcloud监控情况](#monitor) +- [三、broker表现](#broker) +- [四、消费者代码跟踪](#consumerCode) +- [五、消费者表现](#consumer) +- [六、总结](#conclusion) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_consume_detail.png b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_consume_detail.png new file mode 100644 index 00000000..8e23ad60 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_consume_detail.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_consume_detail_ip.png b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_consume_detail_ip.png new file mode 100644 index 00000000..39e23213 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_consume_detail_ip.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_message_queue_changed.png b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_message_queue_changed.png new file mode 100644 index 00000000..363b68f8 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_message_queue_changed.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_offset_illegal.png b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_offset_illegal.png new file mode 100644 index 00000000..7778fa45 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_offset_illegal.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_offset_warn.png b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_offset_warn.png new file mode 100644 index 00000000..89cb9037 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_offset_warn.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_rebalance_pullRequest.png b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_rebalance_pullRequest.png new file mode 100644 index 00000000..3b3860f0 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_rebalance_pullRequest.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_update_subscription.png b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_update_subscription.png new file mode 100644 index 00000000..c45d9bd2 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/troubleshooting/img/1_update_subscription.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/client.md b/mq-cloud/src/main/resources/static/wiki/userGuide/client.md new file mode 100644 index 00000000..1c09d280 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/client.md @@ -0,0 +1,86 @@ +## 一、pom依赖 + +``` + + com.sohu.tv + ${clientArtifactId} + ${version} + + + sohu.nexus + http://${nexusDomain}/nexus/content/groups/public + +``` + +## 二、日志配置-logback[可选] + +``` + + ${LOGS_DIR}/rocketmq.log + + ${LOGS_DIR}/otherdays/rocketmq.log.%d{yyyy-MM-dd} + + 40 + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + +``` + +## 三、日志配置-log4j[可选] + +``` + + + + + + + + + + + + + + + + +``` + +## 三、日志配置-log4j2[可选] + +``` + + + + + + + + + + + + + + + + + + + + + +``` diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/client.toc.md b/mq-cloud/src/main/resources/static/wiki/userGuide/client.toc.md new file mode 100644 index 00000000..9b0bb42d --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/client.toc.md @@ -0,0 +1,6 @@ +##### 目录 + +- [一、pom依赖](#pom) +- [二、日志配置-logback](#logback) +- [三、日志配置-log4j](#log4j) +- [四、日志配置-log4j2](#log4j2) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/clientConsumer.md b/mq-cloud/src/main/resources/static/wiki/userGuide/clientConsumer.md new file mode 100644 index 00000000..9789953a --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/clientConsumer.md @@ -0,0 +1,106 @@ +## 一、初始化之spring-boot方式 + +**老用户请先通过“我是老用户”入口关联消费者** + +``` +@Configuration +public class MQConfiguration { + + @Value("${flushCache.consumer}") + private String flushCacheConsumer; + + @Value("${flushCache.topic}") + private String flushCacheTopic; + + @Bean(initMethod = "start", destroyMethod = "shutdown") + public ${consumer} flushCacheConsumer(FlushCacheConsumerCallback consumerCallback) { + ${consumer} consumer = new ${consumer}(flushCacheConsumer, flushCacheTopic); + consumer.setConsumerCallback(consumerCallback); + return consumer; + } +} +``` + +## 二、 初始化之spring xml方式 + +``` + + + + + + + + +``` + +## 三、初始化之java方式 + +``` +// 消费者初始化 注意:只用初始化一次 +${consumer} consumer = new ${consumer}("xxx-consumer", "xxx-topic"); +// 设置消费回调 +consumer.setConsumerCallback(new ConsumerCallback, MessageExt>() { + public void call(Map t, MessageExt k) { + try { + // 消费逻辑 + } catch (Exception e) { + logger.error("consume err, msgid:{}, msg:{}", k.getMsgId(), t, e); + // 如果需要重新消费,这里需要把异常抛出,消费失败的消息将发回rocketmq,重试消费 + throw e; + } + } +}); +// 注意,只用启动一次 +consumer.start(); +// 应用退出时 +consumer.shutdown(); +``` + +## 四、广播模式消费者需要注意 + +广播模式offset默认存储在应用服务器~/.rocketmq_offsets文件夹下,如果应用部署在docker上,重新部署会导致offset文件丢失,丢失后默认会从broker上拉取最新的offset,那么可能会导致部分消息消费不到。可以通过单独指定offset存储的目录来防止这种情况: + +``` +-Drocketmq.client.localOffsetStoreDir=/data/logs/.rocketmq_offsets +``` + +## 五、Consumer部分参数释义【如非有特殊需求不必修改】: + +``` +/** + * 消费线程数,默认20 + * + * @param num + */ +public void setConsumeThreadMin(int num) { + if (num <= 0) { + return; + } + consumer.setConsumeThreadMin(num); +} +/** + * 消费线程数,默认64 + * + * @param num + */ +public void setConsumeThreadMax(int num) { + if (num <= 0) { + return; + } + consumer.setConsumeThreadMax(num); +} +/** + * queue中缓存多少个消息时进行流控 ,默认1000 + * + * @param size + */ +public void setPullThresholdForQueue(int size) { + if (size < 0) { + return; + } + consumer.setPullThresholdForQueue(size); +} +``` + diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/clientConsumer.toc.md b/mq-cloud/src/main/resources/static/wiki/userGuide/clientConsumer.toc.md new file mode 100644 index 00000000..f98a96e2 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/clientConsumer.toc.md @@ -0,0 +1,7 @@ +##### 目录 + +- [一、初始化之spring-boot方式](#spring-boot) +- [二、初始化之spring xml方式](#spring-xml) +- [三、初始化之java方式](#java) +- [四、广播模式消费者需要注意](#offset) +- [五、Consumer部分参数释义](#explain) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/clientProducer.md b/mq-cloud/src/main/resources/static/wiki/userGuide/clientProducer.md new file mode 100644 index 00000000..3472719f --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/clientProducer.md @@ -0,0 +1,150 @@ +## 一、初始化之spring-boot方式 + +**老用户请先通过“我是老用户”入口关联生产者** + +``` +@Configuration +public class MQConfiguration { + @Value("${flushCache.producer}") + private String flushCacheProducer; + + @Value("${flushCache.topic}") + private String flushCacheTopic; + + @Bean(initMethod = "start", destroyMethod = "shutdown") + public ${producer} flushCacheProducer() { + return new ${producer}(flushCacheProducer, flushCacheTopic); + } +} +``` + +## 二、初始化之spring xml方式 + +``` + + + + + +``` + +## 三、初始化之java方式 + +``` +// 生产者初始化 注意:只用初始化一次 +${producer} producer = new ${producer}("xxx-producer", "xxx-topic"); +// 注意,只用启动一次 +producer.start(); +// 应用退出时 +producer.shutdown(); +``` + +## 四、发送普通消息示例: + +``` +Map message = new HashMap(); +message.put("vid", "123456"); +message.put("aid", "789172"); +//这个例子message使用map,当然也可以使用json +//建议设置keys(多个key用空格分隔)参数(也可以忽略该参数),比如keys指定为vid,那么就可以根据vid查询消息 +Result sendResult = producer.publish(message); +if(!sendResult.isSuccess){ + //失败消息处理 +} +``` + +## 五、发送有序消息示例 + +``` +/** + * 相同的id发送到同一个队列 + * hash方法:id % 队列数 + */ +class IDHashMessageQueueSelector implements MessageQueueSelector { + public MessageQueue select(List mqs, Message msg, Object idObject) { + long id = (Long) idObject; + int size = mqs.size(); + int index = (int) (id % size); + return mqs.get(index); + } +} +// 设置到producer +producer.setMessageQueueSelector(new IDHashMessageQueueSelector()); +// 消息发送 +long id = 123L; +Map map = new HashMap(); +map.put("id", id); +Result sendResult = producer.publishOrder(map, String.valueOf(id), id); +``` + +## 六、 发送事务消息示例 + +``` +// 1.定义实现事务回调接口 +TransactionListener transactionListener = new TransactionListener() { + /** + * 在此方法执行本地事务 + */ + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + // arg可以传业务id + int id = (Integer) arg; + // 确定事务状态,未知返回:UNKNOW,回滚返回:ROLLBACK_MESSAGE,成功返回:COMMIT_MESSAGE,抛出异常默认为:UNKNOW + return LocalTransactionState.COMMIT_MESSAGE; + } + + /** + * 如果executeLocalTransaction返回UNKNOW,rocketmq会回调此方法查询事务状态,默认每分钟查一次,最多查询15次,状态还是UNKNOW的话,丢弃消息 + */ + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + String key = msg.getKeys(); + int id = Integer.valueOf(key); + return LocalTransactionState.COMMIT_MESSAGE; + } +}; + +// 2.发送事务消息 +// 初始化 +${producer} producer = new ${producer}(producerGroup, topic, transactionListener); +// 组装消息 +int id = 123; +Map map = new HashMap(); +map.put("id", id); +map.put("msg", "msg" + id); +// 发送 +Result sendResult = producer.publishTransaction(JSON.toJSONString(map), String.valueOf(id), id); +if(!sendResult.isSuccess){ + //失败消息处理 +} +``` + +## 七、 隔离发送消息示例【hystrix版:MQ集群如果出现故障,将会拖慢发送方,故提供了hystrix版,以保证即使MQ集群整体不可用,也不会拖死发送方】 + +``` +Map map = new HashMap(); +map.put("aid", "123456"); +map.put("vid", "765432"); +// 1.oneway方式 - 此种方式发送效率最高,但是无法获取返回的结果 +new PublishOnewayCommand(producer, map).execute(); +// 2.async方式 - 此种方式发送效率高于普通方式,可以通过异步回调的方式校验返回结果 +SendCallback sendCallback = new SendCallback() { + public void onSuccess(SendResult sendResult) { + // 成功回调 + } + public void onException(Throwable e) { + // 失败回调 + } + }; +new PublishAsyncCommand(producer, map, sendCallback).execute(); +// 3.普通方式 - 此种方式即为普通方式的hystrix封装,与普通发送方式无异 +Result result = new PublishCommand(producer, map).execute(); +``` + +注意:hystrix配置默认采用线程池隔离,容量为30,超时时间为rocketmq客户端默认超时3s,如果使用hystrix版,还需要显示依赖hystrix,如下: + +``` + + com.netflix.hystrix + hystrix-core + 1.3.20 + +``` diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/clientProducer.toc.md b/mq-cloud/src/main/resources/static/wiki/userGuide/clientProducer.toc.md new file mode 100644 index 00000000..f300c4a6 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/clientProducer.toc.md @@ -0,0 +1,9 @@ +##### 目录 + +- [一、初始化之spring-boot方式](#spring-boot) +- [二、初始化之spring xml方式](#spring-xml) +- [三、初始化之java方式](#java) +- [四、发送普通消息示例](#produceMessage) +- [五、发送有序消息示例](#produceOrderMessage) +- [六、发送事务消息示例](#produceTransMessage) +- [七、隔离发送消息示例](#hystrix) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/faq.md b/mq-cloud/src/main/resources/static/wiki/userGuide/faq.md new file mode 100644 index 00000000..48815f59 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/faq.md @@ -0,0 +1,67 @@ +#### 1. 能否用一个topic发送多种消息? + +最好一个topic只负责一类消息,topic的数量对MQ集群几乎无影响。 + +#### 2. 能否用同样的producer名(即producer group)往多个topic发送消息? + +可以发送成功,但是不要这样做。 + +#### 3. 能否用同样的consumer名(即consumer group)消费多个topic的消息? + +不可以,consumer group与topic的关系是,多对一。 + +#### 4. 消息采用什么格式发送? + +可以使用json或者map。 + +#### 5. 三种消息发送方式的应用场景? + +1. 如果是通知类型消息,即消息可以丢失,推荐采用oneway方式发送。 +2. 如果需要知道消息是否发送成功,但是不能阻塞主流程,推荐采用asyn方式发送。 +3. 如果消息必须发送成功,不在乎是否阻塞主流程,推荐采用普通方式发送。 +4. 以上三种方式都有对应的hystrix隔离版,可以在MQ集群故障时保障客户端主流程不阻塞。 + +#### 6. 生产者注意事项: + +检查发送消息后的返回值,针对失败的消息进行重试发送或降级处理。 + +#### 7. 【集群模式】消费者注意事项: + +针对需要重试的消息,消费失败需要抛出异常,这样会将失败的消息发回重试队列。 + +#### 8. 能否使用tags? + +不建议使用tags,理由如下: + +1 说起tags不得不说consumer group,其必须在整个集群中全局唯一,否则会在消费时导致部分消息丢失的问题:[参见测试](https://blog.csdn.net/a417930422/article/details/50663639),而MQCloud在业务层面保证了这个唯一性。 + +2 那么跟tags有什么关系?关系就是同一个topic,同样的consumer group,使用不同的tags,会导致和consumer group一样的问题。 + +也就是说**topic<->consumer group<->tags需要一一对应!** + +3 引起这两个问题的原因都跟rocketmq心跳机制有关,具体类可以参见[ConsumerManager](https://github.com/apache/rocketmq/blob/master/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerManager.java),中的结构: + +``` +private final ConcurrentMap consumerTable = new ConcurrentHashMap(1024); +``` + +4 如果自己能确保上述的一一对应关系,可以参考如下相关代码: + +``` +// 生产者:注意一条消息只支持设置一个tag +producer.publish(msg, tags, null, null); +// 消费者:在启动之前设置 +consumer.setSubExpression("tagA || tagB"); +``` + +5 tags替代方案:消息体增加type字段,各个消费者自己过滤。 + +#### 9. 已知问题: + +1 org.apache.rocketmq.client.exception.MQBrokerException: CODE: 25 DESC: the consumer's subscription not latest。 + +该问题是rocketmq4.2版本的bug,拉取消息流程控制不严格导致,但是并不影响消息消费,在4.2版本出现,在4.3版本修复,[参见](https://github.com/apache/rocketmq/issues/370)。 + +2 org.apache.rocketmq.client.exception.MQBrokerException: CODE: 2 DESC: [TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while + +该问题是由于rocketmq4.1之后broker针对处理发送过来的请求增加了快速失败机制,对于响应超过200ms的请求移除队列。默认broker端采用单线程和spin lock来处理。引起的原因可能是SYN_FLUSH,SYN_MASTER,gc,iops过高等,[参考1](https://stackoverflow.com/questions/43154365/rocketmqmqbrokerexception-code-2-desc-timeout-clean-queue),[参考2](https://issues.apache.org/jira/browse/ROCKETMQ-311)。 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/1.0.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/1.0.png new file mode 100644 index 00000000..1a777ab1 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/1.0.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/1.1.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/1.1.png new file mode 100644 index 00000000..175d8d9a Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/1.1.png differ diff --git a/mq-cloud/src/main/resources/static/img/intro/oldUser2.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/1.2.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/oldUser2.png rename to mq-cloud/src/main/resources/static/wiki/userGuide/img/1.2.png diff --git a/mq-cloud/src/main/resources/static/img/intro/oldUser3.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/1.3.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/oldUser3.png rename to mq-cloud/src/main/resources/static/wiki/userGuide/img/1.3.png diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.0.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.0.png new file mode 100644 index 00000000..582189fb Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.0.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.1.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.1.png new file mode 100644 index 00000000..ea46b563 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.1.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.2.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.2.png new file mode 100644 index 00000000..f7d65541 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.2.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.3.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.3.png new file mode 100644 index 00000000..55923a54 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.3.png differ diff --git a/mq-cloud/src/main/resources/static/img/intro/produceDetail.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.4.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/produceDetail.png rename to mq-cloud/src/main/resources/static/wiki/userGuide/img/3.4.png diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.5.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.5.png new file mode 100644 index 00000000..bd845173 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.5.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.6.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.6.png new file mode 100644 index 00000000..e75e79f5 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.6.png differ diff --git a/mq-cloud/src/main/resources/static/img/intro/consumeTraffic.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.7.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/consumeTraffic.png rename to mq-cloud/src/main/resources/static/wiki/userGuide/img/3.7.png diff --git a/mq-cloud/src/main/resources/static/img/intro/topicList.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.8.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/topicList.png rename to mq-cloud/src/main/resources/static/wiki/userGuide/img/3.8.png diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.9.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.9.png new file mode 100644 index 00000000..c8d950be Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/3.9.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.0.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.0.png new file mode 100644 index 00000000..610dedfc Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.0.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.1.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.1.png new file mode 100644 index 00000000..12035792 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.1.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.2.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.2.png new file mode 100644 index 00000000..ef9cc60e Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.2.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.3.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.3.png new file mode 100644 index 00000000..24c8f4db Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.3.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.4.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.4.png new file mode 100644 index 00000000..14616fa6 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.4.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.5.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.5.png new file mode 100644 index 00000000..77fa6811 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.5.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.6.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.6.png new file mode 100644 index 00000000..6a1eaa8e Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.6.png differ diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.7.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.7.png new file mode 100644 index 00000000..8795a624 Binary files /dev/null and b/mq-cloud/src/main/resources/static/wiki/userGuide/img/4.7.png differ diff --git a/mq-cloud/src/main/resources/static/img/intro/warn_accumulate.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/5.0.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/warn_accumulate.png rename to mq-cloud/src/main/resources/static/wiki/userGuide/img/5.0.png diff --git a/mq-cloud/src/main/resources/static/img/intro/warn_block.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/5.1.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/warn_block.png rename to mq-cloud/src/main/resources/static/wiki/userGuide/img/5.1.png diff --git a/mq-cloud/src/main/resources/static/img/intro/clientException.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/5.2.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/clientException.png rename to mq-cloud/src/main/resources/static/wiki/userGuide/img/5.2.png diff --git a/mq-cloud/src/main/resources/static/img/intro/warn_consume_fail.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/5.3.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/warn_consume_fail.png rename to mq-cloud/src/main/resources/static/wiki/userGuide/img/5.3.png diff --git a/mq-cloud/src/main/resources/static/img/intro/warn_offset_err.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/5.4.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/warn_offset_err.png rename to mq-cloud/src/main/resources/static/wiki/userGuide/img/5.4.png diff --git a/mq-cloud/src/main/resources/static/img/intro/warn_subscribe_more.png b/mq-cloud/src/main/resources/static/wiki/userGuide/img/5.5.png similarity index 100% rename from mq-cloud/src/main/resources/static/img/intro/warn_subscribe_more.png rename to mq-cloud/src/main/resources/static/wiki/userGuide/img/5.5.png diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/messageQuery.md b/mq-cloud/src/main/resources/static/wiki/userGuide/messageQuery.md new file mode 100644 index 00000000..a7af8bcc --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/messageQuery.md @@ -0,0 +1,91 @@ +## 一、查询方式 + +目前支持以下查询方式: + +1. 按照偏移量查询 +2. 按照key查询 +3. 按照时间段查询 +4. 按照消息id查询 +5. 按照trace查询 + +下面进行一一介绍。 + +## 二、按照偏移量查询 + +*由于必须输入broker偏移量,适用于精确查询。* + +![](img/4.0.png) + +**起始偏移量**:参考生产详情里broker的最小偏移量。 + +**结束偏移量**:参考生产详情里broker的最大偏移量。 + +**关键字**:如果期望查到的消息包含某些字符,可以在此填入。 + +**筛选条件**:可以选择在某个broker查询,或者broker的某个队列。 + +**消息重发**:针对查出来的消息,可以单击选中(支持shfit或ctrl多选),点击重发按钮,即会把选中消息的重发请求发送到管理后台,管理员审核通过后进行重发。 + +**详情**:点击详情的眼睛图标,可以看到消息的生产消费轨迹(非trace): + +![](img/4.7.png) + +## 三、按照key查询 + +*适用于生产消息时发送了keys字段的topic* + +![](img/4.1.png) + +**开始时间**:消息的最小时间。 + +**结束时间**:消息的最大时间。 + +**消息key**:发送时传的keys参数。 + +**建议消费发送方传递参数keys,因为RocketMQ会根据keys建立索引,这样根据keys查询时会很快。** + +**例如,keys可以指定为订单号,那么在这里查询时,输入一个订单号,所有此订单相关的消息均会查到。** + +## 四、按照时间段查询 + +*适用于没有传递keys,但是想根据时间来检索消息* + +![](img/4.2.png) + +**开始时间**:消息的最小时间。 + +**结束时间**:消息的最大时间。 + +**关键字**:如果期望查到的消息包含某些字符,可以在此填入。 + +*与按照key查询方式不同,这样查询消息没有索引,耗时会比较长。* + +*另外,按照时间查询实际是根据时间来查找对应的存储文件,再从存储文件检索消息的过程。由于RocketMQ存储的特点,按照时间查询可能会查询不到某些消息,尤其是对于消息量少的集群。* + +## 五、按照消息id查询 + +*适用于用户知道消息id的情况* + +![](img/4.3.png) + +**消息id**:此处的消息id是[SendResult](https://github.com/apache/rocketmq/blob/master/client/src/main/java/org/apache/rocketmq/client/producer/SendResult.java)里的offsetMsgId。 + +## 六、按照trace查询 + +*适用于申请新建topic时,开启了trace功能。* + +![](img/4.4.png) + +如果该topic开启了trace功能,可以从之前4种查询方式的消息结果页,通过点击消息前的序号跳到此trace页面。 + +点击详情的眼睛图标,可以看到trace的详细数据: + +**生产者trace详情**: + +![](img/4.5.png) + + + +**消费者trace详情**: + +![](img/4.6.png) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/messageQuery.toc.md b/mq-cloud/src/main/resources/static/wiki/userGuide/messageQuery.toc.md new file mode 100644 index 00000000..2ebebbb2 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/messageQuery.toc.md @@ -0,0 +1,8 @@ +##### 目录 + +- [一、查询方式](#queryWay) +- [二、按照偏移量查询](#offset) +- [三、按照key查询](#key) +- [四、按照时间段查询](#time) +- [五、按照消息id查询](#msgId) +- [六、按照trace查询](#trace) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/monitorAndWarn.md b/mq-cloud/src/main/resources/static/wiki/userGuide/monitorAndWarn.md new file mode 100644 index 00000000..326423eb --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/monitorAndWarn.md @@ -0,0 +1,39 @@ +## 一、消费堆积预警 + +![](img/5.0.png) + +*只针对集群消费方式进行预警,默认每5分钟消息堆积量达到10000条,预警一次,一小时最多预警一次。* + +## 二、客户端阻塞 + +![](img/5.1.png) + +*只针对push方式局部有序的消费者,默认每5分钟阻塞达到10秒,预警一次,一小时最多预警一次。* + +## 三、客户端异常 + +![](img/5.2.png) + +*使用MQCloud提供的客户端,消费失败的消息会每5分钟预警一次。* + +## 四、消费失败 + +![](img/5.3.png) + +*针对集群消费方式的消费者,每小时消费失败量达到10次,预警一次。* + +## 五、偏移量错误 + +![](img/5.4.png) + +*消费者消费的消息在broker上不存在时,一般是偏移量错误,此时会进行预警,预警频率:实时。* + +## 六、订阅错误 + +![](img/5.5.png) + +*一个消费者订阅了多个topic时,进行预警。* + +## 七、统计,监控,预警 + +关于这块的内容,感兴趣的可以参考开发指南的[统计监控预警](../developerGuide/statMonitorWarning)部分。 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/monitorAndWarn.toc.md b/mq-cloud/src/main/resources/static/wiki/userGuide/monitorAndWarn.toc.md new file mode 100644 index 00000000..475ccf94 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/monitorAndWarn.toc.md @@ -0,0 +1,9 @@ +##### 目录 + +- [一、消费堆积预警](#consume) +- [二、客户端阻塞预警](#clientBlock) +- [三、客户端异常](#clientException) +- [四、消费失败](#consumeError) +- [五、偏移量错误](#offset) +- [六、订阅错误](#subError) +- [七、统计,监控,预警](#statMonitorWarning) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/produceAndConsume.md b/mq-cloud/src/main/resources/static/wiki/userGuide/produceAndConsume.md new file mode 100644 index 00000000..bde4c762 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/produceAndConsume.md @@ -0,0 +1,86 @@ +## 一、创建topic + +创建topic的申请是通过`我要生产消息`模块发起的,申请时的选项如下图: + +![](img/1.0.png) + +各个选项释义如下: + +**组名**:业务组的名字,比如用户组可以填user。 + +**业务名**:具体的业务名字,比如用户订单业务可以填order。 + +**Topic**:组名-业务名-topic。 + +*为了规范命名,MQCloud自动根据`组名`和`业务名`命名topic和producerGroup的名字。* + +**队列数量**:默认为一个broker8个队列,如非特殊需求,不建议修改,后期可以动态扩容。 + +**生产者**:producerGroup的名字,组名-业务名-topic-producer。 + +**消息量**:请根据业务预估量填写,单位 `条/天`。 + +**高峰消息量**:请根据业务预估量填写,单位 `条/秒`。 + +**使用环境**:如果是测试使用,请选择测试环境,将会在测试集群创建此topic。 + +**申请用途**:此topic在业务里的用途,该项同样会展示在topic的`用途`信息里。 + +**消息顺序**:默认为局部有序,如果需要全局有序,请更改此选项,全局有序将丧失高可用性。 + +**开启Trace**:默认不开启trace,如果开启trace后,使用MQCloud提供的客户端将自动对消息进行trace,并可以在`消息查询`模块查看trace情况。 + +**支持事务**:默认不支持事务,如果勾选支持事务,将会在事务集群创建此topic。 + +**消息延迟**:该选项的意义是告知MQCloud从哪里获取统计数据,由于RocketMQ的延迟消息与普通消息统计方式不同,MQCloud需要知道此topic是否用于发送延迟消息。 + + + +## 二、消费消息 + +消费某个topic的申请是通过`我要消费消息`模块发起的,申请时的选项如下图: + +![](img/1.1.png) + +各个选项释义如下: + +**Topic**:选择想要消费的topic。 + +**消费者**:就是consumerGroup,这里建议采用:组名-业务名-部分topic名-consumer 的命名方式。 + +**消费方式**: + +1. 集群消费:所有的消费者均分消息进行消费。 +2. 广播消息:每个消费者会消费所有的消息。 + +**申请原因**:根据业务填写。 + + + +## 三、老用户入口 + +MQCloud可以管理已有的集群,但MQCloud并不知道以前的集群中已经存在的topic归属于谁,所以用户需要通过**我是老用户**入口, 与生产者或消费者进行关联,这样才能让MQCloud为您服务。 + +#### 1 关联生产者 + +![](img/1.2.png) + +各个选项释义如下: + +**Topic**:选择想要关联的topic。 + +**我是**:选择**生产者**。 + +**生产者**:输入**producerGroup**即可,一般查看之前的代码配置就知道了。 + +#### 2 关联消费者 + +![](img/1.3.png) + +各个选项释义如下: + +**Topic**:选择想要关联的topic。 + +**我是**:选择**消费者**。 + +**消费者**:选择即可,如果consumer group列表没有,请联系管理员添加。 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/produceAndConsume.toc.md b/mq-cloud/src/main/resources/static/wiki/userGuide/produceAndConsume.toc.md new file mode 100644 index 00000000..418890a3 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/produceAndConsume.toc.md @@ -0,0 +1,5 @@ +##### 目录 + +- [一、创建topic](#createTopic) +- [二、消费消息](#consumeTopic) +- [三、老用户入口](#oldUser) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/topic.md b/mq-cloud/src/main/resources/static/wiki/userGuide/topic.md new file mode 100644 index 00000000..dcf3150f --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/topic.md @@ -0,0 +1,95 @@ +## 一、topic列表 + +![](img/3.8.png) + +## 二、topic详情 + +**1 topic概要** + +![](img/3.0.png) + +各项释义如下: + +**topic**:topic名字。 + +**所属集群**:topic归属的集群。 + +**每个broker上的队列数**:该topic在单个broker上的队列数。 + +**整个集群队列数**:该topic在整个集群上总的队列数。 + +**消息顺序**:创建topic时指定的选项。 + +**用途**:创建topic时填入的用途。 + +**所属用户**:该topic的申请者,通过下面的+号,可以关联其他生产用户,关联后其他用户可以看到此topic。 + +**生产者**:即producer group,应用代码里初始化时需要使用此选项。 + +**2 今日流量** + +![](img/3.1.png) + +展示今日生产和消费的流量占比。此图由于有两个消费者,所以消费流量是生产流量的2倍。 + +*对于集群消费,消费流量=所有消费组流量之和。* + +*对于广播消费,消费流量=所有消费实例流量之和。* + +**3 拓扑** + +![](img/3.2.png) + +各项释义如下: + +从左至右依次为 **生产者**->**topic**->**消费者**。 + +**生产者和topic,topic和消费者之间连线**:展示的是每分钟的次数和大小流量。 + +**生产者**和**消费者**:鼠标放到图标上会弹出所有链接的实例,格式为:ip@pid@集群id和版本。 + +在**生产者**图标上点击,会弹出相关统计数据,如下: + +![](img/3.3.png) + +## 三、生产详情 + +**1 生产概览** + +![](img/3.4.png) + +展示发往该topic的每个broker上的消息量及最后更新时间。 + +**2 生产流量** + +![](img/3.5.png) + +展示发往该topic的流量,数据来自于broker端的统计。 + +## 四、消费详情 + +**1 消费概览** + +![](img/3.6.png) + +展示消费该topic的每个消费者的消费情况,支持跳过堆积,消息回溯等。 + +**2 消费详情** + +点击某个消费者的名字,会弹出如下消费详情: + +![](img/3.9.png) + +从上至下分为三部分: + +第一部分:如果该消费者产生过死消息,这里将进行展示,支持查询死消息。 + +第二部分:如果该消费者产生过重试消息,这里进行展示,支持查询重试消息。 + +第三部分:该消费者每个客户端ip消费每个队列的详情。 + +**3 消费流量** + +![](img/3.7.png) + +展示消费该topic的每个消费者的流量。 \ No newline at end of file diff --git a/mq-cloud/src/main/resources/static/wiki/userGuide/topic.toc.md b/mq-cloud/src/main/resources/static/wiki/userGuide/topic.toc.md new file mode 100644 index 00000000..4f40b800 --- /dev/null +++ b/mq-cloud/src/main/resources/static/wiki/userGuide/topic.toc.md @@ -0,0 +1,6 @@ +##### 目录 + +- [一、topic列表](#list) +- [二、topic详情](#detail) +- [三、生产详情](#produce) +- [四、消费详情](#consume) \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/admin/audit/list.html b/mq-cloud/src/main/resources/templates/admin/audit/list.html index bd635826..c0942a54 100644 --- a/mq-cloud/src/main/resources/templates/admin/audit/list.html +++ b/mq-cloud/src/main/resources/templates/admin/audit/list.html @@ -70,7 +70,7 @@ <#if response.empty> - + 暂无数据 diff --git a/mq-cloud/src/main/resources/templates/admin/client/list.html b/mq-cloud/src/main/resources/templates/admin/client/list.html index 8ea3d5fc..28bd7061 100644 --- a/mq-cloud/src/main/resources/templates/admin/client/list.html +++ b/mq-cloud/src/main/resources/templates/admin/client/list.html @@ -8,10 +8,11 @@ - - + + + @@ -22,6 +23,7 @@ + @@ -29,7 +31,7 @@ <#if response.empty> - @@ -41,6 +43,7 @@ + diff --git a/mq-cloud/src/main/resources/templates/adminTemplate.html b/mq-cloud/src/main/resources/templates/adminTemplate.html index 9238c848..f6b352f4 100644 --- a/mq-cloud/src/main/resources/templates/adminTemplate.html +++ b/mq-cloud/src/main/resources/templates/adminTemplate.html @@ -7,7 +7,7 @@ <#include "inc/adminNav.html">
    -
    +
    <#include "inc/left.html"> <#include "${view}.html">
    diff --git a/mq-cloud/src/main/resources/templates/inc/foot.html b/mq-cloud/src/main/resources/templates/inc/foot.html index fbeb1726..737acf9a 100644 --- a/mq-cloud/src/main/resources/templates/inc/foot.html +++ b/mq-cloud/src/main/resources/templates/inc/foot.html @@ -3,3 +3,13 @@ 搜狐视频
    + diff --git a/mq-cloud/src/main/resources/templates/inc/include.html b/mq-cloud/src/main/resources/templates/inc/include.html index 4d9b20dc..90a329d4 100644 --- a/mq-cloud/src/main/resources/templates/inc/include.html +++ b/mq-cloud/src/main/resources/templates/inc/include.html @@ -52,6 +52,8 @@ + + \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/intro/header.html b/mq-cloud/src/main/resources/templates/intro/header.html deleted file mode 100644 index 2183de0c..00000000 --- a/mq-cloud/src/main/resources/templates/intro/header.html +++ /dev/null @@ -1,12 +0,0 @@ -
    -
    -

    MQCloud是RocketMQ的管理平台,它具备以下特性:

    -
      -
    • 一,跨集群:同时管理多个集群,对使用者透明。
    • -
    • 二,预警功能:针对消费堆积,失败,异常等情况预警,处理。
    • -
    • 三,简单明了:使用者视图:对拓扑,流量,消费状况等指标进行直接展示;管理员视图:集群维护监控,流程审批等。
    • -
    • 四,安全:用户隔离,操作审批,数据安全。
    • -
    • 五,更多特性正在开发中。
    • -
    -
    -
    \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/intro/index.html b/mq-cloud/src/main/resources/templates/intro/index.html deleted file mode 100644 index 45625db6..00000000 --- a/mq-cloud/src/main/resources/templates/intro/index.html +++ /dev/null @@ -1,608 +0,0 @@ - - - - -
    -
    -

    引导页&首页

    -

    1. 引导页主要用于新用户注册。

    -

    2. 首页主要展示用户的topic,并提供新建,消费,删除,关联等功能。

    -

    查看详情 »

    -
    -
    -

    生产和消费

    -

    1. 新建topic和producer group。

    -

    2. 消费消息及细节设置。

    -

    查看详情 »

    -
    -
    -

    我是老用户

    -

    - MQCloud并不知道之前的topic及消费者归属于谁,所以使用者需要通过我是老用户入口, - 将用户与生产者或消费者进行关联,这样才能让MQCloud为您服务。 -

    -

    查看详情 »

    -
    -
    -
    -
    -

    topic详情

    -

    包括topic概要,拓扑,流量,消息查询功能。

    -

    查看详情 »

    -
    -
    -

    预警功能

    -

    - 针对消费失败,堆积,客户端阻塞等情况进行预警。 -

    -

    查看详情 »

    -
    -
    -

    客户端接入

    -

    提供生产和消费者接入指导,并提供部分编码样例。

    -

    查看详情 »

    -
    -
    - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/msg/idSearch.html b/mq-cloud/src/main/resources/templates/msg/idSearch.html index cb5afa2a..907fb331 100644 --- a/mq-cloud/src/main/resources/templates/msg/idSearch.html +++ b/mq-cloud/src/main/resources/templates/msg/idSearch.html @@ -21,4 +21,5 @@ \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/msg/index.html b/mq-cloud/src/main/resources/templates/msg/index.html index 4e870c69..7de559ac 100644 --- a/mq-cloud/src/main/resources/templates/msg/index.html +++ b/mq-cloud/src/main/resources/templates/msg/index.html @@ -244,6 +244,7 @@ } // 展示数据 $("#page_"+pageNum).show().siblings("tbody").hide(); + $("body").getNiceScroll().resize(); } // 上一页 function previous(){ diff --git a/mq-cloud/src/main/resources/templates/msg/keySearch.html b/mq-cloud/src/main/resources/templates/msg/keySearch.html index 5a2d8beb..3916999c 100644 --- a/mq-cloud/src/main/resources/templates/msg/keySearch.html +++ b/mq-cloud/src/main/resources/templates/msg/keySearch.html @@ -29,4 +29,5 @@ \ No newline at end of file diff --git a/mq-cloud/src/main/resources/templates/msg/traceSearch.html b/mq-cloud/src/main/resources/templates/msg/traceSearch.html index 4c499bb7..70708eca 100644 --- a/mq-cloud/src/main/resources/templates/msg/traceSearch.html +++ b/mq-cloud/src/main/resources/templates/msg/traceSearch.html @@ -95,6 +95,7 @@ $(function(){ $("[data-toggle='tooltip']").tooltip({container: 'body'}); + $("body").getNiceScroll().resize(); }) function showTraceDetail(index){ diff --git a/mq-cloud/src/main/resources/templates/user/topicTopology.html b/mq-cloud/src/main/resources/templates/user/topicTopology.html index 2910ca2e..7882112d 100644 --- a/mq-cloud/src/main/resources/templates/user/topicTopology.html +++ b/mq-cloud/src/main/resources/templates/user/topicTopology.html @@ -748,6 +748,8 @@ $('#producerStatsModal').on('shown.bs.modal', function() { producerStats(); }); + + setTimeout("$('body').getNiceScroll().resize()", 500); }); // 准备删除生产者 diff --git a/mq-cloud/src/main/resources/templates/wikiTemplate.html b/mq-cloud/src/main/resources/templates/wikiTemplate.html new file mode 100644 index 00000000..98ed76f4 --- /dev/null +++ b/mq-cloud/src/main/resources/templates/wikiTemplate.html @@ -0,0 +1,36 @@ + + + + <#include "inc/include.html"> + + + + MQCloud + + + <#include "inc/nav.html"> +
    +
    +
    + <#include "inc/wikiLeft.html"> +
    +
    + ${response.result} +
    +
    +
    + + + <#include "inc/foot.html"> + + diff --git a/pom.xml b/pom.xml index a78e8633..f903d53d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ mq pom mq - 1.7 + 1.8 mq-client-common-open
    客户端 类型 版本归属于 创建日期 修改时间
    + 暂无数据
    ${client.client} ${client.roleDesc} ${client.version}${client.ownersString} ${client.createDate?string("yyyy-MM-dd")} ${client.updateTime?string("yyyy-MM-dd HH:mm:ss")}