lenboo
5 years ago
51 changed files with 1257 additions and 270 deletions
@ -0,0 +1,57 @@
|
||||
/* |
||||
* Licensed to the Apache Software Foundation (ASF) under one or more |
||||
* contributor license agreements. See the NOTICE file distributed with |
||||
* this work for additional information regarding copyright ownership. |
||||
* The ASF licenses this file to You under the Apache License, Version 2.0 |
||||
* (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package cn.escheduler.alert.manager; |
||||
|
||||
import cn.escheduler.alert.utils.Constants; |
||||
import cn.escheduler.alert.utils.EnterpriseWeChatUtils; |
||||
import cn.escheduler.dao.model.Alert; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.Arrays; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* Enterprise WeChat Manager |
||||
*/ |
||||
public class EnterpriseWeChatManager { |
||||
private static final Logger logger = LoggerFactory.getLogger(MsgManager.class); |
||||
/** |
||||
* Enterprise We Chat send |
||||
* @param alert |
||||
*/ |
||||
public Map<String,Object> send(Alert alert, String token){ |
||||
Map<String,Object> retMap = new HashMap<>(); |
||||
retMap.put(Constants.STATUS, false); |
||||
String agentId = EnterpriseWeChatUtils.enterpriseWeChatAgentId; |
||||
String users = EnterpriseWeChatUtils.enterpriseWeChatUsers; |
||||
List<String> userList = Arrays.asList(users.split(",")); |
||||
logger.info("send message {}",alert); |
||||
String msg = EnterpriseWeChatUtils.makeUserSendMsg(userList, agentId,EnterpriseWeChatUtils.markdownByAlert(alert)); |
||||
try { |
||||
EnterpriseWeChatUtils.sendEnterpriseWeChat(Constants.UTF_8, msg, token); |
||||
} catch (IOException e) { |
||||
logger.error(e.getMessage(),e); |
||||
} |
||||
retMap.put(Constants.STATUS, true); |
||||
return retMap; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,248 @@
|
||||
/* |
||||
* Licensed to the Apache Software Foundation (ASF) under one or more |
||||
* contributor license agreements. See the NOTICE file distributed with |
||||
* this work for additional information regarding copyright ownership. |
||||
* The ASF licenses this file to You under the Apache License, Version 2.0 |
||||
* (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package cn.escheduler.alert.utils; |
||||
|
||||
import cn.escheduler.common.enums.ShowType; |
||||
import cn.escheduler.dao.model.Alert; |
||||
import com.alibaba.fastjson.JSON; |
||||
|
||||
import com.google.common.reflect.TypeToken; |
||||
import org.apache.commons.lang3.StringUtils; |
||||
import org.apache.http.HttpEntity; |
||||
import org.apache.http.client.methods.CloseableHttpResponse; |
||||
import org.apache.http.client.methods.HttpGet; |
||||
import org.apache.http.client.methods.HttpPost; |
||||
import org.apache.http.entity.StringEntity; |
||||
import org.apache.http.impl.client.CloseableHttpClient; |
||||
import org.apache.http.impl.client.HttpClients; |
||||
import org.apache.http.util.EntityUtils; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.*; |
||||
|
||||
import static cn.escheduler.alert.utils.PropertyUtils.getString; |
||||
|
||||
/** |
||||
* Enterprise WeChat utils |
||||
*/ |
||||
public class EnterpriseWeChatUtils { |
||||
|
||||
public static final Logger logger = LoggerFactory.getLogger(EnterpriseWeChatUtils.class); |
||||
|
||||
private static final String enterpriseWeChatCorpId = getString(Constants.ENTERPRISE_WECHAT_CORP_ID); |
||||
|
||||
private static final String enterpriseWeChatSecret = getString(Constants.ENTERPRISE_WECHAT_SECRET); |
||||
|
||||
private static final String enterpriseWeChatTokenUrl = getString(Constants.ENTERPRISE_WECHAT_TOKEN_URL); |
||||
private static String enterpriseWeChatTokenUrlReplace = enterpriseWeChatTokenUrl |
||||
.replaceAll("\\$corpId", enterpriseWeChatCorpId) |
||||
.replaceAll("\\$secret", enterpriseWeChatSecret); |
||||
|
||||
private static final String enterpriseWeChatPushUrl = getString(Constants.ENTERPRISE_WECHAT_PUSH_URL); |
||||
|
||||
private static final String enterpriseWeChatTeamSendMsg = getString(Constants.ENTERPRISE_WECHAT_TEAM_SEND_MSG); |
||||
|
||||
private static final String enterpriseWeChatUserSendMsg = getString(Constants.ENTERPRISE_WECHAT_USER_SEND_MSG); |
||||
|
||||
public static final String enterpriseWeChatAgentId = getString(Constants.ENTERPRISE_WECHAT_AGENT_ID); |
||||
|
||||
public static final String enterpriseWeChatUsers = getString(Constants.ENTERPRISE_WECHAT_USERS); |
||||
|
||||
/** |
||||
* get Enterprise WeChat token info |
||||
* @return token string info |
||||
* @throws IOException |
||||
*/ |
||||
public static String getToken() throws IOException { |
||||
String resp; |
||||
|
||||
CloseableHttpClient httpClient = HttpClients.createDefault(); |
||||
HttpGet httpGet = new HttpGet(enterpriseWeChatTokenUrlReplace); |
||||
CloseableHttpResponse response = httpClient.execute(httpGet); |
||||
try { |
||||
HttpEntity entity = response.getEntity(); |
||||
resp = EntityUtils.toString(entity, Constants.UTF_8); |
||||
EntityUtils.consume(entity); |
||||
} finally { |
||||
response.close(); |
||||
} |
||||
|
||||
Map<String, Object> map = JSON.parseObject(resp, |
||||
new TypeToken<Map<String, Object>>() { |
||||
}.getType()); |
||||
return map.get("access_token").toString(); |
||||
} |
||||
|
||||
/** |
||||
* make team single Enterprise WeChat message |
||||
* @param toParty |
||||
* @param agentId |
||||
* @param msg |
||||
* @return Enterprise WeChat send message |
||||
*/ |
||||
public static String makeTeamSendMsg(String toParty, String agentId, String msg) { |
||||
return enterpriseWeChatTeamSendMsg.replaceAll("\\$toParty", toParty) |
||||
.replaceAll("\\$agentId", agentId) |
||||
.replaceAll("\\$msg", msg); |
||||
} |
||||
|
||||
/** |
||||
* make team multi Enterprise WeChat message |
||||
* @param toParty |
||||
* @param agentId |
||||
* @param msg |
||||
* @return Enterprise WeChat send message |
||||
*/ |
||||
public static String makeTeamSendMsg(Collection<String> toParty, String agentId, String msg) { |
||||
String listParty = FuncUtils.mkString(toParty, "|"); |
||||
return enterpriseWeChatTeamSendMsg.replaceAll("\\$toParty", listParty) |
||||
.replaceAll("\\$agentId", agentId) |
||||
.replaceAll("\\$msg", msg); |
||||
} |
||||
|
||||
/** |
||||
* make team single user message |
||||
* @param toUser |
||||
* @param agentId |
||||
* @param msg |
||||
* @return Enterprise WeChat send message |
||||
*/ |
||||
public static String makeUserSendMsg(String toUser, String agentId, String msg) { |
||||
return enterpriseWeChatUserSendMsg.replaceAll("\\$toUser", toUser) |
||||
.replaceAll("\\$agentId", agentId) |
||||
.replaceAll("\\$msg", msg); |
||||
} |
||||
|
||||
/** |
||||
* make team multi user message |
||||
* @param toUser |
||||
* @param agentId |
||||
* @param msg |
||||
* @return Enterprise WeChat send message |
||||
*/ |
||||
public static String makeUserSendMsg(Collection<String> toUser, String agentId, String msg) { |
||||
String listUser = FuncUtils.mkString(toUser, "|"); |
||||
return enterpriseWeChatUserSendMsg.replaceAll("\\$toUser", listUser) |
||||
.replaceAll("\\$agentId", agentId) |
||||
.replaceAll("\\$msg", msg); |
||||
} |
||||
|
||||
/** |
||||
* send Enterprise WeChat |
||||
* @param charset |
||||
* @param data |
||||
* @param token |
||||
* @return Enterprise WeChat resp, demo: {"errcode":0,"errmsg":"ok","invaliduser":""} |
||||
* @throws IOException |
||||
*/ |
||||
public static String sendEnterpriseWeChat(String charset, String data, String token) throws IOException { |
||||
String enterpriseWeChatPushUrlReplace = enterpriseWeChatPushUrl.replaceAll("\\$token", token); |
||||
|
||||
CloseableHttpClient httpclient = HttpClients.createDefault(); |
||||
HttpPost httpPost = new HttpPost(enterpriseWeChatPushUrlReplace); |
||||
httpPost.setEntity(new StringEntity(data, charset)); |
||||
CloseableHttpResponse response = httpclient.execute(httpPost); |
||||
String resp; |
||||
try { |
||||
HttpEntity entity = response.getEntity(); |
||||
resp = EntityUtils.toString(entity, charset); |
||||
EntityUtils.consume(entity); |
||||
} finally { |
||||
response.close(); |
||||
} |
||||
logger.info("Enterprise WeChat send [{}], param:{}, resp:{}", enterpriseWeChatPushUrl, data, resp); |
||||
return resp; |
||||
} |
||||
|
||||
/** |
||||
* convert table to markdown style |
||||
* @param title |
||||
* @param content |
||||
* @return |
||||
*/ |
||||
public static String markdownTable(String title,String content){ |
||||
List<LinkedHashMap> mapItemsList = JSONUtils.toList(content, LinkedHashMap.class); |
||||
StringBuilder contents = new StringBuilder(200); |
||||
for (LinkedHashMap mapItems : mapItemsList){ |
||||
|
||||
Set<Map.Entry<String, String>> entries = mapItems.entrySet(); |
||||
|
||||
Iterator<Map.Entry<String, String>> iterator = entries.iterator(); |
||||
|
||||
StringBuilder t = new StringBuilder(String.format("`%s`%s",title,Constants.MARKDOWN_ENTER)); |
||||
while (iterator.hasNext()){ |
||||
|
||||
Map.Entry<String, String> entry = iterator.next(); |
||||
t.append(Constants.MARKDOWN_QUOTE); |
||||
t.append(entry.getKey()).append(":").append(entry.getValue()); |
||||
t.append(Constants.MARKDOWN_ENTER); |
||||
} |
||||
|
||||
contents.append(t); |
||||
} |
||||
return contents.toString(); |
||||
} |
||||
|
||||
/** |
||||
* convert text to markdown style |
||||
* @param title |
||||
* @param content |
||||
* @return |
||||
*/ |
||||
public static String markdownText(String title,String content){ |
||||
if (StringUtils.isNotEmpty(content)){ |
||||
List<String> list; |
||||
try { |
||||
list = JSONUtils.toList(content,String.class); |
||||
}catch (Exception e){ |
||||
logger.error("json format exception",e); |
||||
return null; |
||||
} |
||||
|
||||
StringBuilder contents = new StringBuilder(100); |
||||
contents.append(String.format("`%s`\n",title)); |
||||
for (String str : list){ |
||||
contents.append(Constants.MARKDOWN_QUOTE); |
||||
contents.append(str); |
||||
contents.append(Constants.MARKDOWN_ENTER); |
||||
} |
||||
|
||||
return contents.toString(); |
||||
|
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Determine the mardown style based on the show type of the alert |
||||
* @param alert |
||||
* @return |
||||
*/ |
||||
public static String markdownByAlert(Alert alert){ |
||||
String result = ""; |
||||
if (alert.getShowType() == ShowType.TABLE) { |
||||
result = markdownTable(alert.getTitle(),alert.getContent()); |
||||
}else if(alert.getShowType() == ShowType.TEXT){ |
||||
result = markdownText(alert.getTitle(),alert.getContent()); |
||||
} |
||||
return result; |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,34 @@
|
||||
/* |
||||
* Licensed to the Apache Software Foundation (ASF) under one or more |
||||
* contributor license agreements. See the NOTICE file distributed with |
||||
* this work for additional information regarding copyright ownership. |
||||
* The ASF licenses this file to You under the Apache License, Version 2.0 |
||||
* (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package cn.escheduler.alert.utils; |
||||
|
||||
public class FuncUtils { |
||||
|
||||
static public String mkString(Iterable<String> list, String split) { |
||||
StringBuilder sb = new StringBuilder(); |
||||
boolean first = true; |
||||
for (String item : list) { |
||||
if (first) |
||||
first = false; |
||||
else |
||||
sb.append(split); |
||||
sb.append(item); |
||||
} |
||||
return sb.toString(); |
||||
} |
||||
|
||||
} |
@ -1,42 +0,0 @@
|
||||
<!-- Logback configuration. See http://logback.qos.ch/manual/index.html --> |
||||
<configuration scan="true" scanPeriod="120 seconds"> |
||||
<logger name="org.apache.zookeeper" level="WARN"/> |
||||
<logger name="org.apache.hbase" level="WARN"/> |
||||
<logger name="org.apache.hadoop" level="WARN"/> |
||||
|
||||
<property name="log.base" value="logs" /> |
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
||||
<encoder> |
||||
<pattern> |
||||
[%level] %date{yyyy-MM-dd HH:mm:ss.SSS} %logger{96}:[%line] - %msg%n |
||||
</pattern> |
||||
<charset>UTF-8</charset> |
||||
</encoder> |
||||
</appender> |
||||
|
||||
<appender name="APISERVERLOGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> |
||||
<!-- Log level filter --> |
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter"> |
||||
<level>INFO</level> |
||||
</filter> |
||||
<file>${log.base}/escheduler-api-server.log</file> |
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> |
||||
<fileNamePattern>${log.base}/escheduler-api-server.%d{yyyy-MM-dd_HH}.%i.log</fileNamePattern> |
||||
<maxHistory>168</maxHistory> |
||||
<maxFileSize>64MB</maxFileSize> |
||||
</rollingPolicy> |
||||
|
||||
<encoder> |
||||
<pattern> |
||||
[%level] %date{yyyy-MM-dd HH:mm:ss.SSS} %logger{96}:[%line] - %msg%n |
||||
</pattern> |
||||
<charset>UTF-8</charset> |
||||
</encoder> |
||||
|
||||
</appender> |
||||
|
||||
<root level="INFO"> |
||||
<appender-ref ref="STDOUT" /> |
||||
</root> |
||||
</configuration> |
@ -0,0 +1,70 @@
|
||||
/* |
||||
* Licensed to the Apache Software Foundation (ASF) under one or more |
||||
* contributor license agreements. See the NOTICE file distributed with |
||||
* this work for additional information regarding copyright ownership. |
||||
* The ASF licenses this file to You under the Apache License, Version 2.0 |
||||
* (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package cn.escheduler.common.utils; |
||||
|
||||
|
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
|
||||
/** |
||||
* http utils |
||||
*/ |
||||
public class IpUtils { |
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(IpUtils.class); |
||||
public static final String DOT = "."; |
||||
|
||||
/** |
||||
* ip str to long <p> |
||||
* |
||||
* @param ipStr ip string |
||||
*/ |
||||
public static Long ipToLong(String ipStr) { |
||||
String[] ipSet = ipStr.split("\\" + DOT); |
||||
|
||||
return Long.parseLong(ipSet[0]) << 24 | Long.parseLong(ipSet[1]) << 16 | Long.parseLong(ipSet[2]) << 8 | Long.parseLong(ipSet[3]); |
||||
} |
||||
|
||||
/** |
||||
* long to ip |
||||
* @param ipLong the long number converted from IP |
||||
* @return String |
||||
*/ |
||||
public static String longToIp(long ipLong) { |
||||
long[] ipNumbers = new long[4]; |
||||
long tmp = 0xFF; |
||||
ipNumbers[0] = ipLong >> 24 & tmp; |
||||
ipNumbers[1] = ipLong >> 16 & tmp; |
||||
ipNumbers[2] = ipLong >> 8 & tmp; |
||||
ipNumbers[3] = ipLong & tmp; |
||||
|
||||
StringBuilder sb = new StringBuilder(16); |
||||
sb.append(ipNumbers[0]).append(DOT) |
||||
.append(ipNumbers[1]).append(DOT) |
||||
.append(ipNumbers[2]).append(DOT) |
||||
.append(ipNumbers[3]); |
||||
return sb.toString(); |
||||
} |
||||
|
||||
|
||||
|
||||
public static void main(String[] args){ |
||||
long ipLong = ipToLong("11.3.4.5"); |
||||
logger.info(longToIp(ipLong)); |
||||
} |
||||
} |
@ -0,0 +1,116 @@
|
||||
<template> |
||||
<div class="starting-params-dag-index"> |
||||
<template v-if="isView && isActive"> |
||||
<div class="box"> |
||||
<p class="box-hd"><i class="fa fa-chevron-circle-right"></i><b>{{$t('Startup parameter')}}</b></p> |
||||
<ul class="box-bd"> |
||||
<li><span>{{$t('Startup type')}}:</span><span>{{_rtRunningType(startupParam.commandType)}}</span></li> |
||||
<li><span>{{$t('Complement range')}}:</span><span v-if="startupParam.commandParam && startupParam.commandParam.complementStartDate">{{startupParam.commandParam.complementStartDate}}-{{startupParam.commandParam.complementEndDate}}</span><span v-else>-</span></li> |
||||
<li><span>{{$t('Failure Strategy')}}:</span><span>{{startupParam.failureStrategy === 'END' ? $t('End') : $t('Continue')}}</span></li> |
||||
<li><span>{{$t('Process priority')}}:</span><span>{{startupParam.processInstancePriority}}</span></li> |
||||
<li><span>{{$t('Worker group')}}:</span><span v-if="workerGroupList.length">{{_rtWorkerGroupName(startupParam.workerGroupId)}}</span></li> |
||||
<li><span>{{$t('Notification strategy')}}:</span><span>{{_rtWarningType(startupParam.warningType)}}</span></li> |
||||
<li><span>{{$t('Notification group')}}:</span><span v-if="notifyGroupList.length">{{_rtNotifyGroupName(startupParam.warningGroupId)}}</span></li> |
||||
<li><span>{{$t('Recipient')}}:</span><span>{{startupParam.receivers || '-'}}</span></li> |
||||
<li><span>{{$t('Cc')}}:</span><span>{{startupParam.receiversCc || '-'}}</span></li> |
||||
</ul> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
</template> |
||||
<script> |
||||
import store from '@/conf/home/store' |
||||
import { runningType } from '@/conf/home/pages/dag/_source/config' |
||||
import { warningTypeList } from '@/conf/home/pages/projects/pages/definition/pages/list/_source/util' |
||||
|
||||
export default { |
||||
name: 'starting-params-dag-index', |
||||
data () { |
||||
return { |
||||
store, |
||||
startupParam: store.state.dag.startup, |
||||
isView: false, |
||||
isActive: true, |
||||
notifyGroupList: null, |
||||
workerGroupList: null |
||||
} |
||||
}, |
||||
methods: { |
||||
_toggleParam () { |
||||
this.isView = !this.isView |
||||
}, |
||||
_rtRunningType (code) { |
||||
return _.filter(runningType, v => v.code === code)[0].desc |
||||
}, |
||||
_rtWarningType (id) { |
||||
return _.filter(warningTypeList, v => v.id === id)[0].code |
||||
}, |
||||
_rtNotifyGroupName (id) { |
||||
let o = _.filter(this.notifyGroupList, v => v.id === id) |
||||
if (o && o.length) { |
||||
return o[0].code |
||||
} |
||||
return '-' |
||||
}, |
||||
_rtWorkerGroupName (id) { |
||||
let o = _.filter(this.workerGroupList, v => v.id === id) |
||||
if (o && o.length) { |
||||
return o[0].name |
||||
} |
||||
return '-' |
||||
}, |
||||
_getNotifyGroupList () { |
||||
let notifyGroupListS = _.cloneDeep(this.store.state.dag.notifyGroupListS) || [] |
||||
if (!notifyGroupListS.length) { |
||||
this.store.dispatch('dag/getNotifyGroupList').then(res => { |
||||
this.notifyGroupList = res |
||||
}) |
||||
} else { |
||||
this.notifyGroupList = notifyGroupListS |
||||
} |
||||
}, |
||||
_getWorkerGroupList () { |
||||
let stateWorkerGroupsList = this.store.state.security.workerGroupsListAll || [] |
||||
if (!stateWorkerGroupsList.length) { |
||||
this.store.dispatch('security/getWorkerGroupsAll').then(res => { |
||||
this.workerGroupList = res |
||||
}) |
||||
} else { |
||||
this.workerGroupList = stateWorkerGroupsList |
||||
} |
||||
} |
||||
}, |
||||
watch: { |
||||
'$route': { |
||||
deep: true, |
||||
handler () { |
||||
this.isActive = false |
||||
this.notifyGroupList = null |
||||
this.workerGroupList = null |
||||
this.$nextTick(() => (this.isActive = true)) |
||||
} |
||||
} |
||||
}, |
||||
mounted () { |
||||
this._getNotifyGroupList() |
||||
this._getWorkerGroupList() |
||||
} |
||||
} |
||||
</script> |
||||
<style lang="scss"> |
||||
.starting-params-dag-index { |
||||
.box { |
||||
padding: 5px 10px 10px; |
||||
.box-hd { |
||||
.fa { |
||||
color: #0097e0; |
||||
margin-right: 4px; |
||||
} |
||||
font-size: 16px; |
||||
} |
||||
.box-bd { |
||||
margin-left: 20px; |
||||
} |
||||
} |
||||
} |
||||
</style> |
Loading…
Reference in new issue