8 Commits

Author SHA1 Message Date
Rinjae f1d6181210 - Playwright 테스트 정리용 internal API 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- /internal/test-cleanup/{org,user,orphans} 신설
- dev 프로필 전용, 토큰+IP allowlist 이중 가드
- 13개 고아 테이블 cascade/스윕 삭제 서비스
- 리포지토리 deleteBy 메서드 추가, 단위테스트 포함
2026-08-21 17:57:51 +09:00
Rinjae bac9a54dee 활성 프로파일 뱃지 추가 - HTTP 세션 로그 설정 제거 - 개인정보 암호화 처리
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-08-21 17:25:41 +09:00
Rinjae 603cba65a7 API 목록 페이징 기능 추가 - 검색 결과 페이지 처리 로직 구현
API 아이콘 스트리밍 엔드포인트 추가 - base64 데이터 URL 캐싱 처리
response 압축 설정 추가 - application.yml에 설정 값 정의
2026-08-21 10:48:47 +09:00
Rinjae 889d77b990 로그 설정 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- JVM 파라미터로 파일, 세션, API 테스터 로그 레벨 제어 추가
- 루트, 세션, API 테스터 로그 레벨 동적 설정 지원
2026-08-20 14:34:01 +09:00
Rinjae bf2dad6ba7 고객센터 연락처 미노출 조건 변경 - 문자열 공백 검사 로직 수정
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-08-19 18:47:54 +09:00
Rinjae 3a1b0cb9c1 고객센터 연락처 미노출 조건 변경 - 문자열 공백 검사 함수 수정
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-08-19 18:25:19 +09:00
Rinjae a13279a777 고객센터 연락처 미노출 조건 추가 - 값 공백 시 th:if로 처리
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-08-19 16:46:58 +09:00
Rinjae 5299352235 CSS hover 색상값 표현 변경 (RGB → 퍼센트 형식)
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-08-19 15:17:41 +09:00
93 changed files with 12931 additions and 839 deletions
+11 -67
View File
@@ -83,10 +83,7 @@ dependencies {
implementation group: 'com.fasterxml.woodstox', name: 'woodstox-core', version: '6.5.1'
// Thymeleaf 3.1 코어가 #temporals 를 내장 제공 → java8time extras 를 함께 두면 표현식 객체가 중복 등록된다.
implementation('org.springframework.boot:spring-boot-starter-thymeleaf') {
exclude group: 'org.thymeleaf.extras', module: 'thymeleaf-extras-java8time'
}
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation('org.springframework.boot:spring-boot-starter-cache')
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
@@ -94,40 +91,33 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// spring-expression 개별 pin 제거: 아래 ext 의 spring-framework.version 이 전 모듈을 일괄 관리한다.
// 개별 pin 이 남아 있으면 다른 spring-* 모듈보다 낮은 버전으로 고정되어 버전이 어긋난다.
implementation 'org.springframework:spring-expression:5.3.30'
implementation group: 'xalan', name: 'xalan', version: '2.7.3'
implementation 'org.hibernate:hibernate-envers:5.6.15.Final'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.4.0' // 3.0.0 은 thymeleaf 3.0 전용
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.0.0'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
// uap-java 제거: 소스 전체에 ua_parser 참조 0건이고, 1.5.3 은 snakeyaml 2.x 에서 삭제된
// SafeConstructor() no-arg 를 호출해 NoSuchMethodError 를 낸다. 되살릴 경우 1.6.1 이상.
implementation 'com.github.ua-parser:uap-java:1.5.3'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'com.navercorp.lucy:lucy-xss-servlet:2.0.1'
implementation 'javax.servlet:javax.servlet-api:4.0.0'
implementation 'org.jasypt:jasypt:1.9.3'
implementation 'xerces:xercesImpl:2.12.2'
// Uncontrolled recursion in ClassUtils.getClass(...) on very long inputs. 3.18.0+ 에서 수정.
implementation 'org.apache.commons:commons-lang3:3.20.0'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'commons-net:commons-net:3.9.0'
// CVE-2025-48734 (PropertyUtilsBean 이 enum 의 declaredClass 프로퍼티 노출 → ClassLoader 접근/RCE).
// 1.11.0 부터 SuppressPropertiesBeanIntrospector 가 기본 활성이라 declaredClass 접근이 차단된다.
// 이 앱의 호출부(PasswordMatchValidator / PasswordRuleValidator / AuthNumberValidator)는
// 어노테이션에 박힌 고정 프로퍼티명만 넘기므로 외부 입력 경로는 없지만 버전은 올려 둔다.
// 1.11.0 = Java 8 바이트코드(major 52), PropertyUtils.getProperty/getNestedProperty API 동일.
implementation('commons-beanutils:commons-beanutils:1.11.0') {
implementation('commons-beanutils:commons-beanutils:1.9.4') {
// exclude group: 'commons-collections', module: 'commons-collections'
}
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
// jackson 개별 pin 제거: 아래 ext 의 jackson-bom.version 이 전 모듈을 일괄 관리한다.
// 개별 pin 은 BOM 보다 우선하므로 남겨 두면 BOM 만 올렸을 때 core/annotations/databind 가
// 옛 버전에 고정돼 버전이 어긋난다(실제로 그런 상태였다).
// WS-2026-0003 (jackson-core async parser DoS, CVSS 7.5) — 2.18.6 에서 수정. JDK8 호환.
implementation 'com.fasterxml.jackson.core:jackson-core:2.18.6'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.18.6'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.6'
implementation group: 'org.apache.velocity', name: 'velocity-engine-core', version: '2.3'
@@ -137,10 +127,7 @@ dependencies {
implementation 'net.bytebuddy:byte-buddy:1.14.5'
// Commons FileUpload (WAS 독립적인 multipart 처리)
// CVE-2025-48976 (멀티파트 파트 헤더 크기 제한 부재 → DoS). 1.6.0 에서 partHeaderSizeMax 도입.
// 주의: 1.6 부터 파트 헤더 총량 기본 상한이 10240 → 512 바이트로 줄었다(DEFAULT_PART_HEADER_SIZE_MAX).
// 한글 파일명은 UTF-8 로 3바이트/자라 Content-Disposition 이 길어질 수 있어 실측으로 여유를 확인했다.
implementation 'commons-fileupload:commons-fileupload:1.6.0'
implementation 'commons-fileupload:commons-fileupload:1.5'
implementation 'commons-io:commons-io:2.15.1'
@@ -158,49 +145,6 @@ ext {
springMavenArtifactVersion = '5.3.30'
encoding = 'UTF-8'
profile = 'local'
// 내장 Tomcat 버전 상향 (Spring Boot 2.7.18 기본값 9.0.83 → 9.0.120).
// Boot BOM 의 tomcat.version 프로퍼티를 덮어써서 tomcat-embed-core/-el/-websocket 이 함께 올라간다.
// 9.0.x 계열 유지 = Servlet 4.0 / javax.* 네임스페이스 그대로, JDK8 호환.
set('tomcat.version', '9.0.120')
// CVE-2022-1471 (snakeyaml Constructor 임의 타입 역직렬화 → RCE). Boot 2.7.18 BOM 기본값 1.30 → 2.6.
// 앱/프레임워크 실경로는 이미 SafeConstructor 계열(OriginTrackedYamlLoader, spring-beans
// FilteringConstructor, swagger-parser DeserializationUtils)이라 익스플로잇 경로는 없었으나
// SCA 는 버전으로 판정하므로 2.x 로 올린다. snakeyaml 2.6 = Java 8 바이트코드(major 52).
// 2.6 인 이유: swagger-core 2.2.52 / swagger-parser-v3 2.1.45 가 요구하는 버전이라 강등이 없다.
// 부수 효과: swagger-parser 가 호출하는 LoaderOptions.setCodePointLimit(1.32+ API) 도 해소.
set('snakeyaml.version', '2.6')
// jackson 전 모듈 버전 통일(Boot 2.7.18 BOM 기본 2.13.5). 2.18.x 는 JDK8 호환 라인이다.
// 이유 3가지
// 1) snakeyaml 2.x 는 ParserImpl(StreamReader) 를 제거했고 jackson-dataformat-yaml 은
// 2.15+ 부터 ParserImpl(StreamReader, LoaderOptions) 를 쓴다 — 위 snakeyaml 상향의 전제.
// 2) WS-2026-0003 (jackson-core async parser DoS, CVSS 7.5) — 2.18.6 에서 수정.
// 3) jackson-databind PolymorphicTypeValidator 우회(제네릭 타입 인자 미검증) — 2.18.8 에서 수정.
// 이 앱은 다형성 역직렬화(activateDefaultTyping/@JsonTypeInfo)를 쓰지 않아 노출 경로는 없다.
// 2.18.x 마지막 패치를 쓴다.
set('jackson-bom.version', '2.18.10')
// Thymeleaf SSTI (≤3.1.3.RELEASE: 표현식 접근 객체 제한 우회 → 템플릿 인젝션). 3.0.x 는 EOL 이라
// 백포트가 없어 3.1.4 로 올린다. JDK8/Spring5 유지: thymeleaf 3.1.4 / thymeleaf-spring5 3.1.4 /
// extras-springsecurity5 3.1.5 / layout-dialect 3.4.0 모두 Java8 바이트코드(major 52), 패키지도
// org.thymeleaf.spring5 + javax.servlet 그대로다.
// Boot 2.7 ThymeleafAutoConfiguration 이 호출하는 setter 는 3.1.4 에 전부 존재함(확인함).
// 주의: 3.1 은 #request/#session/#response/#servletContext 표현식 객체를 제거했다(IllegalArgumentException).
set('thymeleaf.version', '3.1.4.RELEASE')
set('thymeleaf-extras-springsecurity.version', '3.1.5.RELEASE')
// Spring Framework 5.3.x OSS 마지막 릴리스로 통일(Boot 2.7.18 BOM 기본 5.3.31, 일부 5.3.30 혼재였음).
// 5.3.32~5.3.39 구간 CVE 정리용. 5.3.x 는 OSS EOL 이라 이 위로는 상용(Enterprise) 빌드뿐이다.
// 남는 5.3.x 지적(CVE-2026-41855 JMS Jackson 역직렬화, CVE-2016-1000027 HttpInvoker)은
// 5.3 계열에 수정본이 없고 앱이 JMS/HttpInvoker 를 쓰지 않으므로 억제 항목으로 따로 관리한다.
set('spring-framework.version', '5.3.39')
// Spring Security 5.7.x OSS 마지막 릴리스(Boot 2.7.18 BOM 기본 5.7.11).
// CVE-2026-22732(응답 커밋 후 보안 헤더 미기록)는 수정본이 5.7.22/5.8.24 = Enterprise 전용이라
// OSS 로는 올릴 수 없다. 우회책(HeaderWriterFilter.shouldWriteHeadersEagerly=true)은 동작 변경이라 분리 검토.
set('spring-security.version', '5.7.14')
}
-90
View File
@@ -28,94 +28,4 @@
</suppress>
-->
<!-- ================================================================================
Spring 계열 Critical 5건 (2026-08-18 리포트 기준).
공통 배경: Spring Framework 5.3.x / Spring Boot 2.7.x / Spring Security 5.7.x 는 모두 OSS EOL 이라
남은 수정본이 상용(Enterprise/Tanzu)뿐이다. JDK 8 유지 제약상 Boot 3 + Spring 6 이관 전까지는
버전 상향으로 못 없앤다. 아래는 "코드에 트리거 경로가 없음"을 근거로 한 억제이며 만료일을 둔다.
버전 범위를 5.3.x / 2.7.x / 5.7.x 로 고정해 두었으므로, 이관 후에는 억제가 자동으로 풀린다.
재검토 시 확인할 것: (1) 각 근거 grep 이 여전히 0건인지 (2) OSS 수정본이 나왔는지.
================================================================================ -->
<!-- 1. Spring Boot: Cassandra SSL 호스트명 미검증 -->
<suppress until="2027-02-28Z">
<notes><![CDATA[
CVE-2026-40974 는 Spring Boot 의 Cassandra SSL 자동설정이 SSL 번들의 호스트명 검증 설정을
드라이버에 전달하지 않는 문제다. 이 앱은 Cassandra 를 쓰지 않는다
(runtimeClasspath 에 cassandra/datastax 계열 jar 0건, spring-boot-starter-data-cassandra 미선언).
수정본 2.7.33 은 상용(Enterprise) 릴리스라 OSS 로는 올릴 수 없다.
devtools/actuator/spring-boot-admin jar 는 스캔 대상(runtimeClasspath)에는 있으나
배포 산출물에서는 build.gradle 의 localOnlyLibPrefixes 로 제외되어 WAR 에 실리지 않는다.
확인: Rinjae / 2026-08-18 / grep -ri cassandra 0건, WAR 내 devtools/actuator jar 0건.
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.springframework\.boot/.*@2\.7\..*$</packageUrl>
<cve>CVE-2026-40974</cve>
</suppress>
<!-- 2. Thymeleaf SSTI 2건: starter jar 이름 기준 오탐 -->
<suppress>
<notes><![CDATA[
오탐. CVE-2026-40477 / CVE-2026-40478 은 thymeleaf 본체 3.1.3.RELEASE 이하의 표현식 샌드박스
우회 문제이고 3.1.4.RELEASE 에서 수정됐다. 이 프로젝트는 build.gradle 의 ext 에서
thymeleaf.version=3.1.4.RELEASE / thymeleaf-extras-springsecurity.version=3.1.5.RELEASE 로
올려 두었으므로 실제 실리는 jar 는 thymeleaf-3.1.4.RELEASE.jar 다.
Dependency-Check 가 spring-boot-starter-thymeleaf-2.7.18.jar(의존만 선언한 빈 starter)에
thymeleaf CPE 를 잘못 매칭한 결과다. thymeleaf 본체 jar 에 대한 탐지는 억제하지 않는다.
확인: Rinjae / 2026-08-18 / WAR 내 thymeleaf-3.1.4.RELEASE.jar, thymeleaf-spring5-3.1.4.RELEASE.jar.
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.springframework\.boot/spring-boot-starter-thymeleaf@.*$</packageUrl>
<cve>CVE-2026-40477</cve>
<cve>CVE-2026-40478</cve>
</suppress>
<!-- 3. Spring Framework: JMS Jackson 역직렬화 -->
<suppress until="2027-02-28Z">
<notes><![CDATA[
CVE-2026-41855 는 org.springframework.jms.support.converter.MappingJackson2MessageConverter /
JacksonJsonMessageConverter 가 임의 클래스 인스턴스화를 허용하는 문제다(신뢰할 수 없는 JMS 환경 전제).
이 앱은 JMS 를 쓰지 않는다: runtimeClasspath 에 spring-jms 0건, 소스에 javax.jms /
JmsTemplate / MappingJackson2MessageConverter 참조 0건.
5.3.x 는 OSS 수정본이 없다(6.2.19 / 7.0.8 에서만 수정). 이미 5.3.x OSS 마지막인 5.3.39 로 올려 둔 상태다.
확인: Rinjae / 2026-08-18 / grep -rn "javax.jms|JmsTemplate|MappingJackson2MessageConverter" src 0건.
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.springframework/spring-.*@5\.3\..*$</packageUrl>
<cve>CVE-2026-41855</cve>
</suppress>
<!-- 4. spring-web: HttpInvoker 역직렬화 -->
<suppress until="2027-02-28Z">
<notes><![CDATA[
CVE-2016-1000027 은 HttpInvokerServiceExporter 를 노출했을 때만 성립한다. 해당 클래스는
Spring 6.0 에서 제거됐고 5.3.x 에는 수정본이 없다(= 5.3.x 를 쓰는 한 계속 탐지된다).
이 앱은 HttpInvoker 계열을 쓰지 않는다: 소스에 HttpInvoker 참조 0건이고 외부 호출은
RestTemplate / HttpClient 기반이다.
근본 해결은 Spring 6(Boot 3, JDK 17) 이관. 이관 전까지 억제한다.
확인: Rinjae / 2026-08-18 / grep -rn HttpInvoker src 0건.
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.springframework/spring-web@5\.3\..*$</packageUrl>
<cve>CVE-2016-1000027</cve>
</suppress>
<!-- 5. Spring Security: 응답 커밋 시 보안 헤더 미기록 -->
<suppress until="2027-02-28Z">
<notes><![CDATA[
CVE-2026-22732 는 응답이 커밋된 뒤 Spring Security 가 보안 헤더를 기록하지 못하는 문제다.
트리거는 Content-Length 를 setHeader / setIntHeader / addIntHeader 로 지정하는 경로다
(OnCommittedResponseWrapper 는 setContentLength / setContentLengthLong / addHeader 만 추적한다).
(1) 이 코드베이스에는 트리거가 없다: portal / elink-portal-common / elink-online-core-jpa 전체에
setIntHeader, addIntHeader, setHeader("Content-Length") 0건. 파일 다운로드는
response.setContentLength(int) 를 쓰며 실측상 보안 헤더가 정상 기록된다.
(2) 그럼에도 안전망으로 PortalConfigSecurity 에서
HeaderWriterFilter.setShouldWriteHeadersEagerly(true) 를 적용해(spring.io 권고 우회책)
요청 시작 시점에 헤더를 기록하도록 했다.
수정본 5.7.22 / 5.8.24 는 Enterprise 전용이라 OSS 로는 올릴 수 없다. 이미 5.7.x OSS 마지막인
5.7.14 로 올려 둔 상태다.
확인: Rinjae / 2026-08-18 / 동일 스택(Boot 2.7.18 + Security 5.7.14 + Tomcat 9.0.120) 프로브 앱 실측 —
우회책 미적용 시 setHeader/setIntHeader/addIntHeader 경로에서 헤더 누락 재현, 적용 후 정상 기록.
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.springframework\.security/spring-security-.*@5\.7\..*$</packageUrl>
<cve>CVE-2026-22732</cve>
</suppress>
</suppressions>
@@ -15,6 +15,10 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
@@ -79,12 +83,17 @@ public class ApiController {
}
@GetMapping
public String apiList(@ModelAttribute ApiGroupSearch search, Model model) {
public String apiList(@ModelAttribute ApiGroupSearch search, @PageableDefault Pageable pageable, Model model) {
Map<String, Object> searchResult = apiSearchFacade.searchApis(search);
@SuppressWarnings("unchecked")
List<ApiSpecInfoDto> allApis = (List<ApiSpecInfoDto>) searchResult.get("apis");
Page<ApiSpecInfoDto> apiPage = slicePage(allApis, pageable);
model.addAttribute("search", search);
model.addAttribute("services", searchResult.get("services"));
model.addAttribute("apis", searchResult.get("apis"));
model.addAttribute("apis", apiPage.getContent());
model.addAttribute("page", apiPage);
model.addAttribute("totalApiCount", searchResult.get("totalApiCount"));
model.addAttribute("selectedApiCount", searchResult.get("selectedApiCount"));
model.addAttribute("selected", search.getGroupIds().size() > 0 ? search.getGroupIds().get(0) : "-1");
@@ -94,6 +103,19 @@ public class ApiController {
return "apps/apis/mainApiList";
}
/**
* apiSearchFacade.searchApis() 는 다른 소비처(API Status 필터 목록 등)와 계약을 공유하므로
* 항상 전체 List 를 돌려준다. 목록 화면 렌더링에서만 결과를 잘라 Page 로 감싼다.
*/
private Page<ApiSpecInfoDto> slicePage(List<ApiSpecInfoDto> apis, Pageable pageable) {
int total = apis == null ? 0 : apis.size();
int fromIndex = Math.min(pageable.getPageNumber() * pageable.getPageSize(), total);
int toIndex = Math.min(fromIndex + pageable.getPageSize(), total);
List<ApiSpecInfoDto> content = total == 0 ? new ArrayList<>() : apis.subList(fromIndex, toIndex);
return new PageImpl<>(content, pageable, total);
}
@GetMapping("/testbed/api")
public String testbedByApi(@RequestParam(value = "id", required = false) String id, Model model) {
// 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도.
@@ -46,4 +46,6 @@ public class ApiSpecInfoDto {
private String displayRoleCode;
private String apiGroupName;
private String apiGroupId;
}
@@ -115,7 +115,9 @@ public class ApiSearchFacadeImpl implements ApiSearchFacade {
filteredApis.forEach(api -> {
ApiServiceDTO service = apiData.getServicesByApiId().get(api.getApiId());
if (service != null) {
api.setMainIcon(service.getMainIcon());
// mainIcon(CLOB, base64)은 카드 수만큼 복제하지 않는다. 렌더링은
// apiGroupId 기준 /api-services/{id}/icon 스트리밍 엔드포인트를 사용한다.
api.setApiGroupId(service.getId());
api.setApiGroupName(service.getGroupName());
}
});
@@ -5,8 +5,17 @@ import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceTabInfo;
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@@ -20,6 +29,10 @@ import org.springframework.web.servlet.ModelAndView;
@RequiredArgsConstructor
public class ApiServiceController {
// ApiGroup.mainIcon 은 "data:image/png;base64,...." 형태의 Data URL 문자열 그대로 저장돼 있다.
private static final Pattern DATA_URL_PATTERN = Pattern.compile("^data:(image/[a-zA-Z0-9+.-]+);base64,(.+)$", Pattern.DOTALL);
private static final String DEFAULT_ICON_PATH = "/img/api_icon_default.png";
private final ApiServiceService apiServiceService;
@@ -52,4 +65,28 @@ public class ApiServiceController {
return "apps/apiservice/apiServiceDetail";
}
/**
* API 그룹 아이콘 스트리밍. mainIcon(CLOB, Data URL 문자열)을 그대로 HTML에 인라인하면
* 카드 개수만큼 반복 전송돼 응답 용량이 폭증하므로, 그룹 id 당 1회만 내려주고
* 브라우저 캐시로 재사용시킨다.
*/
@GetMapping("/{id}/icon")
public ResponseEntity<byte[]> icon(@PathVariable String id) {
String mainIcon = apiServiceService.getMainIcon(id);
Matcher matcher = mainIcon == null ? null : DATA_URL_PATTERN.matcher(mainIcon);
if (matcher == null || !matcher.matches()) {
return ResponseEntity.status(HttpStatus.FOUND)
.header(HttpHeaders.LOCATION, DEFAULT_ICON_PATH)
.build();
}
byte[] imageBytes = Base64.getDecoder().decode(matcher.group(2));
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(matcher.group(1)))
.cacheControl(CacheControl.maxAge(1, TimeUnit.DAYS).cachePublic())
.body(imageBytes);
}
}
@@ -120,5 +120,14 @@ public class ApiServiceService {
.orElse(null);
}
/**
* 그룹 아이콘(mainIcon)만 가볍게 조회. {@link #getApiGroupById} 는 apiGroupApiList 까지
* 함께 로드해 무거우므로, 아이콘 스트리밍 엔드포인트 전용으로 CLOB 값만 꺼낸다.
*/
public String getMainIcon(String id) {
return apiServiceRepository.findById(id)
.map(ApiGroup::getMainIcon)
.orElse(null);
}
}
@@ -36,7 +36,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.stereotype.Service;
@Service
@@ -156,10 +155,10 @@ public class ApiSpecService {
updateReferences(pathItem, apiId);
String tmpFullPath = StringUtils.isNotEmpty(basePath)
? StringUtils.join(basePath, "/", Strings.CS.removeStart(path, "/"))
? StringUtils.join(basePath, "/", StringUtils.removeStart(path, "/"))
: path;
final String fullPath = tmpFullPath.replaceAll("//+", "/");
final String fullPath = StringUtils.replacePattern(tmpFullPath, "//+", "/");
// HTTP 메소드별로 중복 체크
pathItem.readOperationsMap().forEach((httpMethod, operation) -> {
@@ -295,7 +294,7 @@ public class ApiSpecService {
return StringUtils.EMPTY;
}
Server server = api.getServers().get(0);
return Strings.CS.removeEnd(server.getUrl(), "/");
return StringUtils.removeEnd(server.getUrl(), "/");
}
private void mergeComponents(OpenAPI currentAPI, String apiId, String apiName, Map<String, Object> mergedComponents, ObjectMapper objectMapper) {
@@ -6,6 +6,7 @@ import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository
@EMSDataSource
@@ -16,4 +17,8 @@ public interface PartnershipApplicationRepository extends JpaRepository<Partners
* createdBy 는 PersonalDataEncryptConverter 로 결정적 암호화되므로 평문 사용자 id 로 등가 조회가 가능하다.
*/
List<PartnershipApplication> findTop3ByCreatedByOrderByCreatedDateDesc(String createdBy);
/** createdBy = PortalUser.id (평문 등가 조회 가능한 이유는 위와 동일). */
@Transactional
long deleteByCreatedBy(String createdBy);
}
@@ -7,10 +7,14 @@ import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository
@EMSDataSource
public interface InquiryRepository extends JpaRepository<Inquiry, String>, JpaSpecificationExecutor<Inquiry> {
Optional<Inquiry> findByInquirerAndId(PortalUser inquirer, String id);
@Transactional
long deleteByInquirer_Id(String inquirerId);
}
@@ -111,6 +111,18 @@ public class GlobalControllerAdvice {
return clientGuardService.isDevtoolsGuardEnabled();
}
/**
* 상단 헤더 좌측 노출용 활성 프로파일 배지. prod 프로파일이면 노출하지 않는다(null).
*/
@ModelAttribute("activeProfileBadge")
public String activeProfileBadge() {
if (environment.acceptsProfiles(Profiles.of("prod"))) {
return null;
}
String[] activeProfiles = environment.getActiveProfiles();
return activeProfiles.length == 0 ? "default" : String.join(", ", activeProfiles);
}
/**
* 푸터 고객센터 연락처. PortalProperty(Portal/customer.center.contact)에서 조회.
* 전화번호가 아닐 수도 있으므로 값 그대로 출력하되 템플릿에서 th:text(HTML escape)로 렌더한다.
@@ -2,7 +2,6 @@ package com.eactive.apim.portal.common.exception;
import java.util.Arrays;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@@ -37,27 +36,23 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@RequiredArgsConstructor
public class PortalGlobalExceptionHandler {
private static final String REDIRECT_PREFIX = "redirect:";
private static final String REDIRECT_HOME = REDIRECT_PREFIX + "/";
private static final String REDIRECT_LOGIN_AUTH = REDIRECT_PREFIX + "/login?reason=auth";
private final Logger log = LoggerFactory.getLogger(getClass());
private final PortalProperties portalProperties;
private final Environment environment;
@ExceptionHandler(value = NotFoundException.class)
public ModelAndView handleINotFoundException(HttpServletRequest request, NotFoundException ex) {
return new ModelAndView(REDIRECT_HOME);
return new ModelAndView("redirect:/");
}
@ExceptionHandler(value = MethodArgumentTypeMismatchException.class)
public ModelAndView handleMethodArgumentTypeMismatchException(HttpServletRequest request, MethodArgumentTypeMismatchException ex) {
return new ModelAndView(REDIRECT_HOME);
return new ModelAndView("redirect:/");
}
@ExceptionHandler(value = UserNotLoginException.class)
public ModelAndView handleUserNotLoginException(HttpServletRequest request, UserNotLoginException ex) {
return new ModelAndView(REDIRECT_LOGIN_AUTH);
return new ModelAndView("redirect:/login?reason=auth");
}
@ExceptionHandler(value = AccessDeniedException.class)
@@ -66,7 +61,7 @@ public class PortalGlobalExceptionHandler {
if (!SecurityUtil.isAuthenticated()) {
// 원래 요청 페이지를 세션에 저장 → 로그인+2FA 완료 후 LoginFinalizer 가 복귀시킨다.
savePostLoginRedirect(request);
return new ModelAndView(REDIRECT_LOGIN_AUTH);
return new ModelAndView("redirect:/login?reason=auth");
}
log.warn("접근 권한 없음: loginId={}, uri={}", StringMaskingUtil.maskLoginId(SecurityUtil.getCurrentLoginId()), request.getRequestURI());
ModelAndView modelAndView = new ModelAndView("error");
@@ -127,40 +122,9 @@ public class PortalGlobalExceptionHandler {
|| path.startsWith("/favicon"));
}
/**
* 리다이렉트/뷰 이름으로 허용할 문자. 스킴 구분자(:)·중괄호·달러를 막아
* 외부 URL 이나 Thymeleaf 표현식이 뷰 이름으로 흘러드는 것을 차단한다.
*/
private static final Pattern SAFE_REDIRECT_TARGET = Pattern.compile("[A-Za-z0-9._/?=&%-]+");
@ExceptionHandler(value = PortalRedirectException.class)
public ModelAndView handlePortalRedirectException(HttpServletRequest request, PortalRedirectException ex) {
// PortalRedirectException 은 super(message) 를 호출하지 않으므로 getMessage() 는 항상 null 이다.
// 이전 구현은 그 null 을 뷰 이름으로 넘겨(= 뷰 미지정) 요청 URL 기준으로 뷰가 추론되게 만들었다.
// 실제 대상은 redirectPage 필드다.
String target = ex.getRedirectPage();
if (!isSafeRedirectTarget(target)) {
log.warn("허용되지 않은 리다이렉트 대상 - uri={}, target={}", request.getRequestURI(), target);
return new ModelAndView(REDIRECT_HOME);
}
return new ModelAndView(target);
}
/** 내부 경로/뷰 이름만 허용(외부 URL·프로토콜 상대 URL·표현식 문자 차단) */
private boolean isSafeRedirectTarget(String target) {
if (target == null) {
return false;
}
String path = target;
if (path.startsWith(REDIRECT_PREFIX)) {
path = path.substring(REDIRECT_PREFIX.length());
} else if (path.startsWith("forward:")) {
path = path.substring("forward:".length());
}
if (path.isEmpty() || path.startsWith("//")) { // //evil.example 형태의 외부 리다이렉트 차단
return false;
}
return SAFE_REDIRECT_TARGET.matcher(path).matches();
return new ModelAndView(ex.getMessage());
}
@ExceptionHandler(value = {IllegalArgumentException.class})
@@ -176,7 +140,7 @@ public class PortalGlobalExceptionHandler {
public ModelAndView handleHttpRequestMethodNotSupportedException(HttpServletRequest request, HttpRequestMethodNotSupportedException ex) {
log.error(ex.getMessage());
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName(REDIRECT_HOME);
modelAndView.setViewName("redirect:/");
return modelAndView;
}
@@ -220,7 +184,7 @@ public class PortalGlobalExceptionHandler {
public ModelAndView handleInvalidFileException(HttpServletRequest request, RedirectAttributes redirectAttributes, InvalidFileException ex) {
ModelAndView modelAndView = new ModelAndView();
redirectAttributes.addFlashAttribute("error", ex.getMessage());
modelAndView.setViewName(REDIRECT_PREFIX + request.getRequestURI());
modelAndView.setViewName("redirect:" + request.getRequestURI());
return modelAndView;
}
@@ -231,7 +195,7 @@ public class PortalGlobalExceptionHandler {
String errorMessage = "파일 크기가 허용된 최대 용량(" + maxSize + ")을 초과했습니다.";
redirectAttributes.addFlashAttribute("error", errorMessage);
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName(REDIRECT_PREFIX + request.getRequestURI());
modelAndView.setViewName("redirect:" + request.getRequestURI());
return modelAndView;
}
}
@@ -1,7 +1,6 @@
package com.eactive.apim.portal.common.validator;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
@@ -40,8 +39,14 @@ public class CellPhoneValidator implements ConstraintValidator<CellPhone, String
}
// 기존의 prefix 체크 로직 유지
return Strings.CS.startsWithAny(cellPhone,
"+821", "821", "010", "011", "016", "017", "018", "019");
return StringUtils.startsWith(cellPhone, "+821") ||
StringUtils.startsWith(cellPhone, "821") ||
StringUtils.startsWith(cellPhone, "010") ||
StringUtils.startsWith(cellPhone, "011") ||
StringUtils.startsWith(cellPhone, "016") ||
StringUtils.startsWith(cellPhone, "017") ||
StringUtils.startsWith(cellPhone, "018") ||
StringUtils.startsWith(cellPhone, "019");
}
private boolean isValidPartLength(String prefix, String middle, String last) {
@@ -2,8 +2,6 @@ package com.eactive.apim.portal.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUpload;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
@@ -20,18 +18,6 @@ import org.springframework.web.multipart.commons.CommonsMultipartResolver;
@RequiredArgsConstructor
public class MultipartConfig {
/**
* 멀티파트 파트 1개의 헤더 구간 최대 바이트.
* <p>
* commons-fileupload 1.6.0(CVE-2025-48976 수정)부터 이 상한이 생겼고 기본값이 512 바이트다.
* 한글 파일명은 UTF-8 로 3바이트/자라 Content-Disposition 이 금방 커진다 — 실측상 기본값 512 로는
* 한글 약 137자 이상 파일명에서 업로드가 실패하고, 예외도 "Maximum upload size exceeded" 로 감싸져
* 원인 파악이 어렵다. OS 파일명 상한(255자)이 전부 한글이어도 통과하도록 2048 로 둔다.
* (1.5 이전에는 사실상 10240 이었으므로 이 값도 그보다 5배 엄격하다.)
* </p>
*/
private static final int PART_HEADER_SIZE_MAX = 2048;
private final PortalProperties portalProperties;
/**
@@ -45,14 +31,7 @@ public class MultipartConfig {
*/
@Bean(name = "filterMultipartResolver")
public CommonsMultipartResolver filterMultipartResolver() {
CommonsMultipartResolver resolver = new CommonsMultipartResolver() {
@Override
protected FileUpload newFileUpload(FileItemFactory fileItemFactory) {
FileUpload fileUpload = super.newFileUpload(fileItemFactory);
fileUpload.setPartHeaderSizeMax(PART_HEADER_SIZE_MAX);
return fileUpload;
}
};
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
long maxSizeBytes = portalProperties.getFile().getMaxSizeBytes();
resolver.setMaxUploadSize(maxSizeBytes);
@@ -60,8 +39,8 @@ public class MultipartConfig {
resolver.setMaxInMemorySize((int) Math.min(maxSizeBytes, Integer.MAX_VALUE));
resolver.setDefaultEncoding("UTF-8");
log.info("CommonsMultipartResolver configured with maxUploadSize: {} bytes ({}), partHeaderSizeMax: {} bytes",
maxSizeBytes, portalProperties.getFile().getMaxSize(), PART_HEADER_SIZE_MAX);
log.info("CommonsMultipartResolver configured with maxUploadSize: {} bytes ({})",
maxSizeBytes, portalProperties.getFile().getMaxSize());
return resolver;
}
@@ -7,7 +7,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -17,7 +16,6 @@ import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfException;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -94,23 +92,6 @@ public class PortalConfigSecurity {
http
.authenticationManager(portalAuthenticationManager)
// CVE-2026-22732 우회책. Spring Security 는 기본적으로 보안 헤더를 "응답 커밋 시점"에
// 지연 기록하는데, 응답 래퍼가 Content-Length 를 setHeader/setIntHeader/addIntHeader 로
// 지정하는 경로를 추적하지 못해 그 경우 헤더가 통째로 누락된다
// (X-Content-Type-Options, X-Frame-Options, Cache-Control, Pragma, Expires, X-XSS-Protection).
// 수정본은 5.7.22/5.8.24(Enterprise 전용)뿐이라 OSS 로는 올릴 수 없어 우회책을 적용한다.
// 요청 시작 시점에 헤더를 기록하게 만든다. 앱이 나중에 같은 헤더를 지정하면 앱 값이 남는다
// (실측: FileDownloadController#viewImage 의 Cache-Control: public, max-age=86400 유지됨).
// 현재 앱은 response.setContentLength(int) 만 쓰므로 노출 경로는 없지만,
// 새 코드가 위 메서드를 쓰더라도 헤더가 빠지지 않도록 두는 안전망이다.
// Spring Security 를 수정본(6.5.9+/7.0.4+ 또는 Enterprise 5.7.22+)으로 올리면 제거 가능.
.headers(headers -> headers.addObjectPostProcessor(new ObjectPostProcessor<HeaderWriterFilter>() {
@Override
public <O extends HeaderWriterFilter> O postProcess(O filter) {
filter.setShouldWriteHeadersEagerly(true);
return filter;
}
}))
.formLogin(form -> form
.loginPage("/login")
.usernameParameter("id")
@@ -132,7 +113,10 @@ public class PortalConfigSecurity {
// /internal/menu 는 브라우저 세션이 없는 서버간 호출(admin → portal)이라 CSRF 토큰을 실을 수 없다.
// 대신 InternalApiTokenService 의 공유 토큰 헤더 + 허용 IP 목록으로 통제한다.
// (커스텀 헤더는 cross-site form POST 로 위조할 수 없어 CSRF 경로가 차단된다)
.ignoringRequestMatchers(new AntPathRequestMatcher("/internal/menu/**"))
// /internal/test-cleanup 도 동일 이유(Playwright 등 세션 없는 호출) + 동일 통제 방식.
.ignoringRequestMatchers(
new AntPathRequestMatcher("/internal/menu/**"),
new AntPathRequestMatcher("/internal/test-cleanup/**"))
)
// 로그인 페이지에 오래 머물러 세션(=CSRF 토큰 저장소)이 타임아웃되면
// 로그인 제출 시 CsrfFilter가 AnonymousAuthenticationFilter보다 먼저 예외를 던져
@@ -4,6 +4,7 @@ import com.eactive.apim.portal.qna.entity.InquiryComment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.List;
@@ -17,4 +18,7 @@ public interface InquiryCommentRepository extends JpaRepository<InquiryComment,
+ " group by c.inquiry.id")
List<Object[]> countActiveGroupByInquiry(@Param("inquiryIds") Collection<String> inquiryIds,
@Param("delYn") String delYn);
@Transactional
long deleteByInquiry_Inquirer_Id(String inquirerId);
}
@@ -0,0 +1,190 @@
package com.eactive.apim.portal.djb.testcleanup.controller;
import com.eactive.apim.portal.common.internal.InternalApiTokenService;
import com.eactive.apim.portal.common.util.IpAddressMatcher;
import com.eactive.apim.portal.djb.testcleanup.service.OrphanCleanupService;
import com.eactive.apim.portal.djb.testcleanup.service.TestCleanupResult;
import com.eactive.apim.portal.djb.testcleanup.service.TestCleanupService;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Playwright E2E 테스트 정리용 내부 API — local(개인 dev 프로필, 항상 {@code dev} 동반 활성화)과
* 배포된 dev 서버 양쪽에서만 존재한다({@code @Profile("dev")}, stage/prod 는 빈 자체가 없어 404).
*
* <p>가드는 {@link com.eactive.apim.portal.djb.menu.MenuInternalController} 와 동일하게 두 겹이다.</p>
* <ol>
* <li><b>공유 토큰 헤더</b> — {@link InternalApiTokenService}(menu.reload 와 동일 토큰 재사용).</li>
* <li><b>허용 IP 목록</b> — PTL_PROPERTY {@code Portal / testcleanup.internal.allow-ips}(기본 loopback).
* menu 와 별도 프로퍼티라 Playwright 실행 호스트만 좁게 허용할 수 있다.</li>
* </ol>
*
* <p>파괴적 작업 안전장치: {@code org}/{@code user} 삭제는 구체적 식별자(compRegNo/email)가 필수이며
* 와일드카드/전체삭제 파라미터는 없다. {@code orphans} 스윕은 이미 사라진 org/user 를 참조하는 행만
* 대상이라 살아있는 테스트 데이터를 지울 위험이 없다.</p>
*
* <pre>curl -X POST -H 'X-Internal-Token: &lt;PTL_PROPERTY Portal/internal.api.token&gt;' \
* 'http://127.0.0.1:39130/internal/test-cleanup/org?compRegNo=1234567890'</pre>
*/
@Slf4j
@Profile("dev")
@RestController
@RequestMapping("/internal/test-cleanup")
@RequiredArgsConstructor
public class TestCleanupInternalController {
static final String PROP_GROUP = "Portal";
static final String PROP_ALLOW_IPS = "testcleanup.internal.allow-ips";
static final String DEFAULT_ALLOW_IPS = "127.0.0.1,::1";
static final String PROP_ALLOW_IPS_DESCRIPTION =
"테스트 정리 내부 API(법인/계정/고아데이터 삭제) 허용 IP 목록. 콤마(,)/세미콜론(;)/줄바꿈 구분, "
+ "정확일치·IPv4 CIDR(172.30.1.0/24)·와일드카드(172.30.*.*) 지원";
private final TestCleanupService testCleanupService;
private final OrphanCleanupService orphanCleanupService;
private final PortalPropertyService portalPropertyService;
private final InternalApiTokenService internalApiTokenService;
@PostMapping("/org")
public ResponseEntity<Map<String, Object>> deleteOrg(
@RequestParam String compRegNo, HttpServletRequest request) {
ResponseEntity<Map<String, Object>> guardFailure = checkGuards(request);
if (guardFailure != null) {
return guardFailure;
}
if (StringUtils.isBlank(compRegNo)) {
return badRequest("compRegNo 는 필수입니다.");
}
TestCleanupResult result = testCleanupService.deleteOrgCascade(compRegNo);
log.info("테스트 정리(org) 실행 - compRegNo: {}, from: {}", compRegNo, request.getRemoteAddr());
Map<String, Object> body = baseBody();
body.put("compRegNo", compRegNo);
body.put("orgId", result.getTargetId());
body.put("orgFound", result.isFound());
body.put("deletedCounts", result.getDeletedCounts());
return ResponseEntity.ok(body);
}
@PostMapping("/user")
public ResponseEntity<Map<String, Object>> deleteUser(
@RequestParam String email, HttpServletRequest request) {
ResponseEntity<Map<String, Object>> guardFailure = checkGuards(request);
if (guardFailure != null) {
return guardFailure;
}
if (StringUtils.isBlank(email)) {
return badRequest("email 은 필수입니다.");
}
TestCleanupResult result = testCleanupService.deleteUserCascade(email);
log.info("테스트 정리(user) 실행 - email: {}, from: {}", email, request.getRemoteAddr());
Map<String, Object> body = baseBody();
body.put("email", email);
body.put("userId", result.getTargetId());
body.put("userFound", result.isFound());
body.put("deletedCounts", result.getDeletedCounts());
return ResponseEntity.ok(body);
}
@PostMapping("/orphans")
public ResponseEntity<Map<String, Object>> cleanOrphans(HttpServletRequest request) {
ResponseEntity<Map<String, Object>> guardFailure = checkGuards(request);
if (guardFailure != null) {
return guardFailure;
}
TestCleanupResult result = orphanCleanupService.sweep();
log.info("테스트 정리(orphans) 실행 - from: {}", request.getRemoteAddr());
Map<String, Object> body = baseBody();
body.put("deletedCounts", result.getDeletedCounts());
return ResponseEntity.ok(body);
}
private ResponseEntity<Map<String, Object>> checkGuards(HttpServletRequest request) {
if (!isAllowedIp(request)) {
return denied(HttpStatus.FORBIDDEN,
"허용되지 않은 접근입니다. (" + PROP_GROUP + "/" + PROP_ALLOW_IPS + " 확인)");
}
if (!hasValidToken(request)) {
return denied(HttpStatus.UNAUTHORIZED,
"내부 API 토큰이 유효하지 않습니다. (" + InternalApiTokenService.PROP_GROUP + "/"
+ InternalApiTokenService.PROP_TOKEN + " 확인)");
}
return null;
}
private Map<String, Object> baseBody() {
Map<String, Object> body = new LinkedHashMap<>();
body.put("result", "OK");
body.put("processedAt", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
return body;
}
private ResponseEntity<Map<String, Object>> badRequest(String message) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("result", "BAD_REQUEST");
body.put("message", message);
return ResponseEntity.badRequest().body(body);
}
private ResponseEntity<Map<String, Object>> denied(HttpStatus status, String message) {
Map<String, Object> denied = new LinkedHashMap<>();
denied.put("result", "DENIED");
denied.put("message", message);
return ResponseEntity.status(status).body(denied);
}
/**
* 공유 토큰 헤더 검사. 헤더명은 PTL_PROPERTY 로 바뀔 수 있으므로 매 요청 조회한다(호출 빈도가 낮다).
*/
private boolean hasValidToken(HttpServletRequest request) {
String headerName = internalApiTokenService.ensureHeaderName();
if (!internalApiTokenService.matches(request.getHeader(headerName))) {
log.warn("테스트 정리 내부 API 차단 - 토큰 불일치, remote: {}, header: {}", request.getRemoteAddr(), headerName);
return false;
}
return true;
}
/**
* 허용 IP 검사. 프록시 경유(X-Forwarded-For 존재) 요청은 IP 신뢰 불가로 거부한다.
*/
private boolean isAllowedIp(HttpServletRequest request) {
String remote = IpAddressMatcher.canonicalize(request.getRemoteAddr());
boolean viaProxy = request.getHeader("X-Forwarded-For") != null;
if (viaProxy || !IpAddressMatcher.matches(resolveAllowIps(), remote)) {
log.warn("테스트 정리 내부 API 차단 - remote: {}, viaProxy: {}", remote, viaProxy);
return false;
}
return true;
}
private String resolveAllowIps() {
try {
return portalPropertyService.getOrCreateProperty(PROP_GROUP, PROP_ALLOW_IPS,
DEFAULT_ALLOW_IPS, PROP_ALLOW_IPS_DESCRIPTION);
} catch (Exception e) {
log.warn("허용 IP 목록 조회 실패 - 기본값({}) 사용", DEFAULT_ALLOW_IPS, e);
return DEFAULT_ALLOW_IPS;
}
}
}
@@ -0,0 +1,81 @@
package com.eactive.apim.portal.djb.testcleanup.service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Playwright 반복 실행으로 쌓인, 더 이상 유효한 PTL_ORG/PTL_USER 를 참조하지 않는 잔존(고아) 행을
* 13개 테이블에서 스윕 삭제한다. local(dev 동반 활성화)·dev 서버에서만 동작한다({@link TestCleanupService}
* 참고). 대상이 "이미 사라진 org/user"뿐이라 살아있는 테스트 데이터를 건드릴 위험이 없어 파라미터가 없다.
*/
@Slf4j
@Profile("dev")
@Service
@Transactional
public class OrphanCleanupService {
private final Environment environment;
@PersistenceContext
private EntityManager entityManager;
public OrphanCleanupService(Environment environment) {
this.environment = environment;
}
public TestCleanupResult sweep() {
assertNonProdProfile();
TestCleanupResult result = TestCleanupResult.found(null);
// 자식 -> 부모 순서 (FK 제약은 없으나 논리적 정합성 유지 목적)
result.put("PTL_INQUIRY_COMMENT", exec(
"DELETE FROM PTL_INQUIRY_COMMENT WHERE INQUIRY_ID IN "
+ "(SELECT ID FROM PTL_INQUIRY WHERE INQUIRER_ID IS NOT NULL AND INQUIRER_ID NOT IN (SELECT ID FROM PTL_USER))"));
result.put("PTL_INQUIRY", exec(
"DELETE FROM PTL_INQUIRY WHERE INQUIRER_ID IS NOT NULL AND INQUIRER_ID NOT IN (SELECT ID FROM PTL_USER)"));
result.put("PTL_PARTNERSHIP_APPLICATION", exec(
"DELETE FROM PTL_PARTNERSHIP_APPLICATION WHERE CREATED_BY IS NOT NULL AND CREATED_BY NOT IN (SELECT ID FROM PTL_USER)"));
result.put("PTL_USER_PRIVACY_POLICY_AGREEMENT", exec(
"DELETE FROM PTL_USER_PRIVACY_POLICY_AGREEMENT WHERE CREATED_BY IS NOT NULL AND CREATED_BY NOT IN (SELECT ID FROM PTL_USER)"));
result.put("PTL_USER_PASSWORD_HISTORY", exec(
"DELETE FROM PTL_USER_PASSWORD_HISTORY WHERE USER_ID IS NOT NULL AND USER_ID NOT IN (SELECT ID FROM PTL_USER)"));
result.put("PTL_USER_ROLE_HISTORY", exec(
"DELETE FROM PTL_USER_ROLE_HISTORY WHERE USER_ID IS NOT NULL AND USER_ID NOT IN (SELECT LOGIN_ID FROM PTL_USER)"));
result.put("PTL_USER_LOG", exec(
"DELETE FROM PTL_USER_LOG WHERE LOGIN_ID IS NOT NULL AND LOGIN_ID NOT IN (SELECT LOGIN_ID FROM PTL_USER)"));
result.put("PTL_CREDENTIAL_API", exec(
"DELETE FROM PTL_CREDENTIAL_API WHERE CLIENT_ID IN "
+ "(SELECT CLIENTID FROM PTL_CREDENTIAL WHERE ORGID IS NOT NULL AND ORGID NOT IN (SELECT ID FROM PTL_ORG))"));
result.put("PTL_CREDENTIAL", exec(
"DELETE FROM PTL_CREDENTIAL WHERE ORGID IS NOT NULL AND ORGID NOT IN (SELECT ID FROM PTL_ORG)"));
result.put("PTL_WEBHOOK_REQ_API", exec(
"DELETE FROM PTL_WEBHOOK_REQ_API WHERE WEBHOOK_REQ_ID IN "
+ "(SELECT ID FROM PTL_WEBHOOK_REQ WHERE ORG_ID IS NOT NULL AND ORG_ID NOT IN (SELECT ID FROM PTL_ORG))"));
result.put("PTL_WEBHOOK_REQ_EVENT", exec(
"DELETE FROM PTL_WEBHOOK_REQ_EVENT WHERE WEBHOOK_REQ_ID IN "
+ "(SELECT ID FROM PTL_WEBHOOK_REQ WHERE ORG_ID IS NOT NULL AND ORG_ID NOT IN (SELECT ID FROM PTL_ORG))"));
result.put("PTL_WEBHOOK_REQ", exec(
"DELETE FROM PTL_WEBHOOK_REQ WHERE ORG_ID IS NOT NULL AND ORG_ID NOT IN (SELECT ID FROM PTL_ORG)"));
result.put("PTL_WEBHOOK_SEND_LOG", exec(
"DELETE FROM PTL_WEBHOOK_SEND_LOG WHERE ORG_ID IS NOT NULL AND ORG_ID NOT IN (SELECT ID FROM PTL_ORG)"));
log.info("테스트 정리 - 고아 데이터 스윕 완료: {}", result.getDeletedCounts());
return result;
}
private long exec(String sql) {
return entityManager.createNativeQuery(sql).executeUpdate();
}
private void assertNonProdProfile() {
if (environment.acceptsProfiles(Profiles.of("stage", "prod"))) {
throw new IllegalStateException("stage/prod 환경에서는 테스트 정리 API를 수행할 수 없습니다.");
}
}
}
@@ -0,0 +1,33 @@
package com.eactive.apim.portal.djb.testcleanup.service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;
/**
* 독립 엔티티가 없어 JPA 파생 삭제 메서드를 쓸 수 없는 테이블(PTL_CREDENTIAL_API, PTL_WEBHOOK_SEND_LOG) 전용,
* 특정 org 소유분만 지우는 삭제. EMS 가 {@code @Primary} 이므로 기본 EntityManager 를 그대로 쓴다.
*/
@Profile("dev")
@Repository
class TestCleanupNativeQueries {
@PersistenceContext
private EntityManager entityManager;
int deleteCredentialApiByOrgId(String orgId) {
return entityManager.createNativeQuery(
"DELETE FROM PTL_CREDENTIAL_API WHERE CLIENT_ID IN "
+ "(SELECT CLIENTID FROM PTL_CREDENTIAL WHERE ORGID = :orgId)")
.setParameter("orgId", orgId)
.executeUpdate();
}
int deleteWebhookSendLogByOrgId(String orgId) {
return entityManager.createNativeQuery(
"DELETE FROM PTL_WEBHOOK_SEND_LOG WHERE ORG_ID = :orgId")
.setParameter("orgId", orgId)
.executeUpdate();
}
}
@@ -0,0 +1,45 @@
package com.eactive.apim.portal.djb.testcleanup.service;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 테스트 정리 API 응답용 결과 누적기. 대상 존재 여부 + 테이블별 삭제 건수를 담는다.
*/
public class TestCleanupResult {
private boolean found;
private String targetId;
private final Map<String, Long> deletedCounts = new LinkedHashMap<>();
public static TestCleanupResult notFound() {
return new TestCleanupResult();
}
public static TestCleanupResult found(String targetId) {
TestCleanupResult result = new TestCleanupResult();
result.found = true;
result.targetId = targetId;
return result;
}
public void put(String table, long count) {
deletedCounts.merge(table, count, Long::sum);
}
public void merge(TestCleanupResult other) {
other.deletedCounts.forEach(this::put);
}
public boolean isFound() {
return found;
}
public String getTargetId() {
return targetId;
}
public Map<String, Long> getDeletedCounts() {
return deletedCounts;
}
}
@@ -0,0 +1,141 @@
package com.eactive.apim.portal.djb.testcleanup.service;
import com.eactive.apim.portal.app.repository.CredentialRepository;
import com.eactive.apim.portal.apps.community.partnership.repository.PartnershipApplicationRepository;
import com.eactive.apim.portal.apps.community.qna.repository.InquiryRepository;
import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
import com.eactive.apim.portal.djb.community.qna.comment.repository.InquiryCommentRepository;
import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestApiRepository;
import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestEventRepository;
import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestRepository;
import com.eactive.apim.portal.djb.webhook.repository.entity.WebhookRequest;
import com.eactive.apim.portal.djb.webhook.service.WebhookService;
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.repository.PortalUserPrivacyAgreementRepository;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.apim.portal.portaluser.repository.UserPasswordHistoryRepository;
import com.eactive.apim.portal.portaluser.repository.UserRoleHistoryRepository;
import com.eactive.apim.portal.user.repository.UserLogRepository;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Playwright E2E 테스트가 반복 실행되며 쌓이는 테스트 법인(PTL_ORG)·계정(PTL_USER)과
* 그에 딸린 데이터를 하드 삭제한다. local(dev 동반 활성화)·dev 서버에서만 존재하는 빈이며,
* stage/prod 는 {@code @Profile("dev")} 로 빈 자체가 등록되지 않는다.
*
* <p>모든 대상 테이블이 EMS 스키마(PTL_*)라 무지정 {@code @Transactional}(EMS, {@code @Primary})만 쓴다.</p>
*/
@Slf4j
@Profile("dev")
@Service
@RequiredArgsConstructor
@Transactional
public class TestCleanupService {
private final Environment environment;
private final PortalOrgRepository portalOrgRepository;
private final PortalUserRepository portalUserRepository;
private final InquiryRepository inquiryRepository;
private final InquiryCommentRepository inquiryCommentRepository;
private final PartnershipApplicationRepository partnershipApplicationRepository;
private final UserRoleHistoryRepository userRoleHistoryRepository;
private final PortalUserPrivacyAgreementRepository portalUserPrivacyAgreementRepository;
private final UserPasswordHistoryRepository userPasswordHistoryRepository;
private final UserLogRepository userLogRepository;
private final CredentialRepository credentialRepository;
private final WebhookRequestRepository webhookRequestRepository;
private final WebhookRequestApiRepository webhookRequestApiRepository;
private final WebhookRequestEventRepository webhookRequestEventRepository;
private final WebhookService webhookService;
private final TestCleanupNativeQueries nativeQueries;
/**
* 사업자등록번호로 법인을 찾아, 소속 계정 전원 + org 소유 CREDENTIAL/WEBHOOK + 법인 자체를 하드 삭제한다.
*/
public TestCleanupResult deleteOrgCascade(String compRegNo) {
assertNonProdProfile();
Optional<PortalOrg> orgOpt = portalOrgRepository.findByCompRegNo(compRegNo);
if (!orgOpt.isPresent()) {
return TestCleanupResult.notFound();
}
PortalOrg org = orgOpt.get();
String orgId = org.getId();
TestCleanupResult result = TestCleanupResult.found(orgId);
List<PortalUser> users = portalUserRepository.findAllByPortalOrg_Id(orgId);
for (PortalUser user : users) {
result.merge(deleteUserCascadeInternal(user));
}
result.put("PTL_CREDENTIAL_API", nativeQueries.deleteCredentialApiByOrgId(orgId));
result.put("PTL_CREDENTIAL", credentialRepository.deleteByOrgid(orgId));
Optional<WebhookRequest> webhook = webhookRequestRepository.findByOrgId(orgId);
if (webhook.isPresent()) {
Long webhookReqId = webhook.get().getId();
result.put("PTL_WEBHOOK_REQ_API", webhookRequestApiRepository.findByWebhookReqId(webhookReqId).size());
result.put("PTL_WEBHOOK_REQ_EVENT", webhookRequestEventRepository.findByWebhookReqId(webhookReqId).size());
webhookService.delete(webhookReqId, orgId);
result.put("PTL_WEBHOOK_REQ", 1L);
} else {
result.put("PTL_WEBHOOK_REQ_API", 0L);
result.put("PTL_WEBHOOK_REQ_EVENT", 0L);
result.put("PTL_WEBHOOK_REQ", 0L);
}
result.put("PTL_WEBHOOK_SEND_LOG", nativeQueries.deleteWebhookSendLogByOrgId(orgId));
portalOrgRepository.delete(org);
result.put("PTL_ORG", 1L);
log.info("테스트 정리 - 법인 완전삭제 완료: compRegNo={}, orgId={}, userCount={}", compRegNo, orgId, users.size());
return result;
}
/**
* 이메일로 계정을 찾아 단독 하드 삭제한다(org 소속 여부와 무관 — org 소유 CREDENTIAL/WEBHOOK 은 건드리지 않는다).
*/
public TestCleanupResult deleteUserCascade(String email) {
assertNonProdProfile();
Optional<PortalUser> userOpt = portalUserRepository.findPortalUserByEmailAddr(email);
if (!userOpt.isPresent()) {
return TestCleanupResult.notFound();
}
PortalUser user = userOpt.get();
TestCleanupResult result = deleteUserCascadeInternal(user);
log.info("테스트 정리 - 계정 완전삭제 완료: userId={}", user.getId());
return result;
}
private TestCleanupResult deleteUserCascadeInternal(PortalUser user) {
TestCleanupResult result = TestCleanupResult.found(user.getId());
result.put("PTL_INQUIRY_COMMENT", inquiryCommentRepository.deleteByInquiry_Inquirer_Id(user.getId()));
result.put("PTL_INQUIRY", inquiryRepository.deleteByInquirer_Id(user.getId()));
result.put("PTL_PARTNERSHIP_APPLICATION", partnershipApplicationRepository.deleteByCreatedBy(user.getId()));
// UserRoleHistory.userId 는 loginId 를 저장한다(다른 이력 테이블과 시맨틱이 반대이므로 혼동 주의).
result.put("PTL_USER_ROLE_HISTORY", userRoleHistoryRepository.deleteByUserId(user.getLoginId()));
result.put("PTL_USER_PRIVACY_POLICY_AGREEMENT", portalUserPrivacyAgreementRepository.deleteByCreatedBy(user.getId()));
// UserPasswordHistory.userId 는 PortalUser.id 를 저장한다.
result.put("PTL_USER_PASSWORD_HISTORY", userPasswordHistoryRepository.deleteByUserId(user.getId()));
result.put("PTL_USER_LOG", userLogRepository.deleteByLoginId(user.getLoginId()));
portalUserRepository.delete(user);
result.put("PTL_USER", 1L);
return result;
}
private void assertNonProdProfile() {
if (environment.acceptsProfiles(Profiles.of("stage", "prod"))) {
throw new IllegalStateException("stage/prod 환경에서는 테스트 정리 API를 수행할 수 없습니다.");
}
}
}
+5
View File
@@ -30,6 +30,11 @@ server:
# RequestHeader set X-Forwarded-Proto "https" / X-Forwarded-Port "1443" (mod_wl_ohs 는 미전송)
forward-headers-strategy: native
compression:
enabled: true
mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
min-response-size: 1024
spring:
config:
+19 -4
View File
@@ -19,6 +19,21 @@
<property name="__consoleLevel.FALSE" value="OFF"/>
<property name="CONSOLE_EFFECTIVE_LEVEL" value="${__consoleLevel.${CONSOLE_LOG_ENABLED}}"/>
<!-- 파일 로그(ROLLING/ERROR_FILE 등 root 하위 appender) 레벨 JVM 파라미터 제어 -->
<!-- 사용: -DFILE_LOG_LEVEL=ERROR (전체 미남김: -DFILE_LOG_LEVEL=OFF) -->
<!-- 기본값: dev 프로파일 DEBUG, 그 외 INFO (기존 동작 유지) -->
<property name="FILE_LOG_LEVEL" value="${FILE_LOG_LEVEL:-INFO}"/>
<property name="FILE_LOG_LEVEL_DEV" value="${FILE_LOG_LEVEL:-DEBUG}"/>
<!-- API 테스터 감사 로그(eapim.portal.apitester.audit) on/off JVM 파라미터 제어 -->
<!-- 사용: -DAPI_TESTER_AUDIT_LOG_ENABLED=false / 기본값: true(켜짐, INFO) -->
<property name="API_TESTER_AUDIT_LOG_ENABLED" value="${API_TESTER_AUDIT_LOG_ENABLED:-true}"/>
<property name="__apiTesterAuditLevel.true" value="INFO"/>
<property name="__apiTesterAuditLevel.TRUE" value="INFO"/>
<property name="__apiTesterAuditLevel.false" value="OFF"/>
<property name="__apiTesterAuditLevel.FALSE" value="OFF"/>
<property name="API_TESTER_AUDIT_EFFECTIVE_LEVEL" value="${__apiTesterAuditLevel.${API_TESTER_AUDIT_LOG_ENABLED}}"/>
<appender name="HIBERNATE_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/hibernate.log</file>
@@ -95,23 +110,23 @@
</appender>
<logger name="eapim.portal.session" level="INFO" additivity="false">
<logger name="eapim.portal.session" level="${FILE_LOG_LEVEL}" additivity="false">
<appender-ref ref="HTTP_SESSION" />
</logger>
<logger name="eapim.portal.apitester.audit" level="INFO" additivity="false">
<logger name="eapim.portal.apitester.audit" level="${API_TESTER_AUDIT_EFFECTIVE_LEVEL}" additivity="false">
<appender-ref ref="API_TESTER_AUDIT" />
</logger>
<root level="INFO">
<root level="${FILE_LOG_LEVEL}">
<appender-ref ref="ROLLING"/>
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
<springProfile name="dev">
<root level="DEBUG">
<root level="${FILE_LOG_LEVEL_DEV}">
<appender-ref ref="ROLLING"/>
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ERROR_FILE"/>
+93 -45
View File
@@ -821,6 +821,50 @@ hr {
flex-shrink: 0;
}
.env-badge {
display: inline-flex;
align-items: center;
margin-left: 10px;
padding: 2px 8px;
border-radius: 4px;
background: var(--accent-orange);
color: var(--white);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
line-height: 1.6;
}
@media (max-width: 1024px) {
.env-badge {
display: none;
}
}
.env-strip {
display: none;
}
@media (max-width: 1024px) {
.env-strip {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 16px;
background: var(--accent-orange);
color: var(--white);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
z-index: 1100;
pointer-events: none;
}
}
.logo-wrapper {
display: flex;
align-items: center;
@@ -1192,7 +1236,7 @@ hr {
transition: all 0.3s ease;
}
.mobile-drawer .drawer-welcome .btn-drawer-login:hover {
background: rgb(0, 65.7, 162);
background: rgb(0%, 25.7647058824%, 63.5294117647%);
}
.mobile-drawer .drawer-welcome.authenticated {
flex-direction: row;
@@ -2503,7 +2547,7 @@ hr {
color: #FFFFFF;
}
.btn-success:hover {
background: rgb(83.2897959184, 199.3102040816, 106.493877551);
background: rgb(32.662665066%, 78.1608643457%, 41.762304922%);
transform: translateY(-3px);
}
.btn-danger {
@@ -2511,7 +2555,7 @@ hr {
color: #FFFFFF;
}
.btn-danger:hover {
background: rgb(255, 70.8, 70.8);
background: rgb(100%, 27.7647058824%, 27.7647058824%);
transform: translateY(-3px);
}
.btn-ghost {
@@ -2777,7 +2821,7 @@ hr {
.action-btn-delete:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
background: rgb(255, 88.9, 88.9);
background: rgb(100%, 34.862745098%, 34.862745098%);
}
.action-btn-delete:active {
transform: translateY(0);
@@ -2895,7 +2939,7 @@ hr {
background: #a4d6ea;
}
.btn-input-action.btn-change:hover {
background: rgb(131.6625, 199.4303571429, 226.5375);
background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
}
@@ -2970,7 +3014,7 @@ hr {
border: none;
}
.btn-action-primary:hover {
background: rgb(31.8731707317, 92.7219512195, 205.7268292683);
background: rgb(12.4992826399%, 36.3615494978%, 80.6771879484%);
transform: translateY(-2px);
color: #fff;
}
@@ -3020,7 +3064,7 @@ hr {
}
.status-badge.status-processing {
background: rgba(255, 217, 61, 0.1);
color: rgb(221.2, 177.8721649485, 0);
color: rgb(86.7450980392%, 69.7537901759%, 0%);
}
.status-badge.status-failed {
background: rgba(255, 107, 107, 0.1);
@@ -3060,7 +3104,7 @@ hr {
}
.status-badge-header.status-processing {
background: rgba(255, 217, 61, 0.1);
color: rgb(221.2, 177.8721649485, 0);
color: rgb(86.7450980392%, 69.7537901759%, 0%);
}
.badge-sm {
@@ -4248,7 +4292,7 @@ select.form-control {
.file-upload-wrapper .file-remove-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
background: rgb(255, 88.9, 88.9);
background: rgb(100%, 34.862745098%, 34.862745098%);
}
.file-upload-wrapper .file-remove-btn:active {
transform: translateY(0);
@@ -4569,7 +4613,7 @@ select.form-control {
transition: all 0.3s ease;
}
.form-actions--with-withdrawal .withdrawal-link:hover {
background: rgb(210.2090909091, 232.6045454545, 242.9409090909);
background: rgb(82.4349376114%, 91.2174688057%, 95.2709447415%);
}
.form-actions--with-withdrawal .withdrawal-link img {
width: 22px;
@@ -4724,7 +4768,7 @@ select.form-control {
text-decoration: underline;
}
.notice-content-box a:hover {
color: rgb(0, 65.7, 162);
color: rgb(0%, 25.7647058824%, 63.5294117647%);
}
.form-row--content .form-label-wrapper {
@@ -5660,7 +5704,7 @@ select.form-control {
font-size: 16px;
}
.drawer-logout-btn:hover {
background: rgb(255, 70.8, 70.8);
background: rgb(100%, 27.7647058824%, 27.7647058824%);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(75, 155, 255, 0.15);
}
@@ -6373,7 +6417,7 @@ select.form-control {
color: #64748b;
}
.list-table-btn--default:hover {
background-color: rgb(233.3571428571, 233.3571428571, 231.1928571429);
background-color: rgb(91.512605042%, 91.512605042%, 90.6638655462%);
}
.list-table-btn--primary {
background-color: #ecf0fa;
@@ -6381,7 +6425,7 @@ select.form-control {
color: #2a69de;
}
.list-table-btn--primary:hover {
background-color: rgb(216.7625, 224.8125, 244.9375);
background-color: rgb(85.0049019608%, 88.1617647059%, 96.0539215686%);
}
.list-table-btn--secondary {
background-color: #f5f5f4;
@@ -6389,7 +6433,7 @@ select.form-control {
color: #64748b;
}
.list-table-btn--secondary:hover {
background-color: rgb(233.3571428571, 233.3571428571, 231.1928571429);
background-color: rgb(91.512605042%, 91.512605042%, 90.6638655462%);
}
.list-table-btn--danger {
background-color: #fbe7e9;
@@ -6397,7 +6441,7 @@ select.form-control {
color: #bb1026;
}
.list-table-btn--danger:hover {
background-color: rgb(247.5571428571, 210.3428571429, 214.0642857143);
background-color: rgb(97.081232493%, 82.487394958%, 83.9467787115%);
}
.table-pagination {
@@ -7047,7 +7091,7 @@ select.form-control {
.alert.alert-error {
background: rgba(255, 107, 107, 0.1);
border: 1px solid rgba(255, 107, 107, 0.3);
color: rgb(255, 70.8, 70.8);
color: rgb(100%, 27.7647058824%, 27.7647058824%);
align-items: center;
}
.alert.alert-error svg {
@@ -7061,7 +7105,7 @@ select.form-control {
.alert.alert-success {
background: rgba(107, 207, 127, 0.1);
border: 1px solid rgba(107, 207, 127, 0.3);
color: rgb(61.5183673469, 189.6816326531, 87.1510204082);
color: rgb(24.12484994%, 74.3849539816%, 34.1768707483%);
}
.alert.alert-info {
background: rgba(0, 73, 180, 0.1);
@@ -11432,10 +11476,10 @@ body.index-page-body {
line-height: 20px;
}
.login-button:hover {
background: rgb(25.65, 70.3, 173.85);
background: rgb(10.0588235294%, 27.568627451%, 68.1764705882%);
}
.login-button:active {
background: rgb(24.3, 66.6, 164.7);
background: rgb(9.5294117647%, 26.1176470588%, 64.5882352941%);
}
.login-button:disabled {
opacity: 0.6;
@@ -11478,10 +11522,10 @@ body.index-page-body {
border-bottom-right-radius: 8px;
}
.login-links-container .link-btn:hover {
background: rgb(220.61, 227.85, 245.95);
background: rgb(86.5137254902%, 89.3529411765%, 96.4509803922%);
}
.login-links-container .link-btn:active {
background: rgb(205.22, 215.7, 241.9);
background: rgb(80.4784313725%, 84.5882352941%, 94.862745098%);
}
.login-alert {
@@ -11990,12 +12034,12 @@ body.index-page-body {
}
.auth-request-button:hover,
.auth-verify-button:hover {
background: rgb(37.3117757009, 153.9304672897, 235.0082242991);
background: rgb(14.6320689023%, 60.3648891332%, 92.1600879604%);
transform: none !important;
}
.auth-request-button:active,
.auth-verify-button:active {
background: rgb(21.411588785, 146.3125233645, 233.148411215);
background: rgb(8.3967014843%, 57.3774601429%, 91.4307494961%);
}
.auth-request-button:disabled,
.auth-verify-button:disabled {
@@ -12044,10 +12088,10 @@ body.index-page-body {
background: #f0f2f5;
}
.account-recovery-card .form-actions .cancel-button:hover {
background: rgb(225.45, 229.39, 235.3);
background: rgb(88.4117647059%, 89.9568627451%, 92.2745098039%);
}
.account-recovery-card .form-actions .cancel-button:active {
background: rgb(210.9, 216.78, 225.6);
background: rgb(82.7058823529%, 85.0117647059%, 88.4705882353%);
}
.account-recovery-card .form-actions .submit-button {
color: #FFFFFF;
@@ -12057,7 +12101,7 @@ body.index-page-body {
background: rgb(6, 54, 125);
}
.account-recovery-card .form-actions .submit-button:active {
background: rgb(0, 65.7, 162);
background: rgb(0%, 25.7647058824%, 63.5294117647%);
}
.account-recovery-card .form-actions .submit-button:disabled {
opacity: 0.6;
@@ -12293,7 +12337,7 @@ body.index-page-body {
transition: color 0.3s ease;
}
.result-info-box .info-text .info-link:hover {
color: rgb(0, 65.7, 162);
color: rgb(0%, 25.7647058824%, 63.5294117647%);
}
@media (max-width: 576px) {
.result-info-box .info-text {
@@ -17493,7 +17537,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease;
}
.btn-copy-action:hover {
background: rgb(131.6625, 199.4303571429, 226.5375);
background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
}
@media (max-width: 768px) {
.btn-copy-action {
@@ -17520,7 +17564,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease;
}
.btn-view-secret:hover {
background: rgb(31.8897196262, 151.4130841121, 234.5102803738);
background: rgb(12.5057724024%, 59.377680044%, 91.9648158329%);
}
.btn-view-secret svg {
width: 20px;
@@ -17705,7 +17749,7 @@ input[type=checkbox]:checked + .custom-checkbox {
border-radius: 8px;
}
.btn-copy-action:hover {
background: rgb(131.6625, 199.4303571429, 226.5375);
background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
}
.btn-view-secret {
width: 100% !important;
@@ -17721,7 +17765,7 @@ input[type=checkbox]:checked + .custom-checkbox {
height: 16px;
}
.btn-view-secret:hover {
background: rgb(31.8897196262, 151.4130841121, 234.5102803738);
background: rgb(12.5057724024%, 59.377680044%, 91.9648158329%);
}
#revealedSecretBox {
width: 100%;
@@ -17994,7 +18038,7 @@ input[type=checkbox]:checked + .custom-checkbox {
flex-shrink: 0;
}
.detail-wrap .dt-btn-copy:hover {
background: rgb(189.6, 230.0857142857, 255);
background: rgb(74.3529411765%, 90.2296918768%, 100%);
}
.detail-wrap .dt-btn-copy svg {
color: #2a69de;
@@ -18073,6 +18117,10 @@ input[type=checkbox]:checked + .custom-checkbox {
padding-right: 20px;
flex: 1;
}
.detail-wrap .dt-api-name:hover {
color: #2a69de;
text-decoration: underline;
}
@media (max-width: 768px) {
.detail-wrap .dt-api-name {
font-size: 14px;
@@ -18171,7 +18219,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease;
}
.detail-wrap .dt-btn-gray:hover {
background: rgb(170.6589473684, 183.4378947368, 193.6610526316);
background: rgb(66.9250773994%, 71.9364293086%, 75.9455108359%);
}
.detail-wrap .dt-btn-red {
width: 156px;
@@ -18189,7 +18237,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease;
}
.detail-wrap .dt-btn-red:hover {
background: rgb(255, 70.0915337423, 64.24);
background: rgb(100%, 27.4868759774%, 25.1921568627%);
}
.detail-wrap .dt-btn-blue {
width: 156px;
@@ -19763,7 +19811,7 @@ input[type=checkbox]:checked + .custom-checkbox {
}
}
.btn-inquiry-list:hover {
background: rgb(215.8869565217, 218.8956521739, 224.9130434783);
background: rgb(84.6615515772%, 85.8414322251%, 88.2011935209%);
}
.btn-inquiry-list:active {
transform: scale(0.98);
@@ -19796,7 +19844,7 @@ input[type=checkbox]:checked + .custom-checkbox {
}
}
.btn-inquiry-edit:hover {
background: rgb(0, 69.35, 171);
background: rgb(0%, 27.1960784314%, 67.0588235294%);
}
.btn-inquiry-edit:active {
transform: scale(0.98);
@@ -19829,7 +19877,7 @@ input[type=checkbox]:checked + .custom-checkbox {
}
}
.btn-inquiry-delete:hover {
background: rgb(217.9841772152, 41.3658227848, 58.2873417722);
background: rgb(85.4839910648%, 16.2218912882%, 22.8577810871%);
}
.btn-inquiry-delete:active {
transform: scale(0.98);
@@ -19901,7 +19949,7 @@ input[type=checkbox]:checked + .custom-checkbox {
margin-left: 8px;
}
.file-upload-inline .btn-remove-file-inline:hover {
background: rgb(209.4151898734, 36.2848101266, 52.8721518987);
background: rgb(82.1236038719%, 14.2293373045%, 20.7341772152%);
}
.file-upload-inline .btn-remove-file-inline svg {
width: 12px;
@@ -19933,7 +19981,7 @@ input[type=checkbox]:checked + .custom-checkbox {
}
}
.file-upload-inline .btn-file-attach:hover {
background: rgb(37.3117757009, 153.9304672897, 235.0082242991);
background: rgb(14.6320689023%, 60.3648891332%, 92.1600879604%);
}
.file-upload-inline .btn-file-attach svg {
width: 22px;
@@ -19993,7 +20041,7 @@ input[type=checkbox]:checked + .custom-checkbox {
border: none;
}
.inquiry-form-container .form-actions .btn-secondary:hover {
background: rgb(215.8869565217, 218.8956521739, 224.9130434783);
background: rgb(84.6615515772%, 85.8414322251%, 88.2011935209%);
}
.inquiry-form-container .form-actions .btn-primary {
background: #0049b4;
@@ -20001,7 +20049,7 @@ input[type=checkbox]:checked + .custom-checkbox {
border: none;
}
.inquiry-form-container .form-actions .btn-primary:hover {
background: rgb(0, 69.35, 171);
background: rgb(0%, 27.1960784314%, 67.0588235294%);
}
.inquiry-form-container .file-upload-inline .file-input-display {
min-height: 50px;
@@ -20790,7 +20838,7 @@ input[type=checkbox]:checked + .custom-checkbox {
cursor: pointer;
}
.djb-board-write-container .form-actions .btn-submit:hover {
background-color: rgb(33.643902439, 97.8731707317, 217.156097561);
background-color: rgb(13.193687231%, 38.3816355811%, 85.1592539455%);
}
@media (max-width: 768px) {
.djb-board-write-container .form-actions .btn-submit {
@@ -21326,7 +21374,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: all 0.3s ease;
}
.org-file-remove:hover {
background: rgb(255, 70.8, 70.8);
background: rgb(100%, 27.7647058824%, 27.7647058824%);
}
.org-file-notice {
@@ -22338,7 +22386,7 @@ input[type=checkbox]:checked + .custom-checkbox {
}
.status-indicator.status-active {
background-color: rgba(107, 207, 127, 0.1);
color: rgb(83.2897959184, 199.3102040816, 106.493877551);
color: rgb(32.662665066%, 78.1608643457%, 41.762304922%);
}
.status-indicator.status-active .status-dot {
background-color: #6BCF7F;
File diff suppressed because one or more lines are too long
+491
View File
@@ -0,0 +1,491 @@
class ChartEditor {
constructor(container, layoutManager, dashboard) {
this.rootContainer = container;
this.layoutManager = layoutManager;
this.dashboard = dashboard;
this.targetContainerIndex = -1;
this.targetColumnIndex = -1;
this.tempChart = null;
this.chartPreview = null;
this.containerHeight = 0;
this.chartEditorModel = [
{
'groupName': 'default',
'groupTitle': '기본 정보',
'fields': [
{
'path': 'contentId',
'type': 'text',
'label': 'Chart ID',
'placeholder': 'chartId를 입력하세요'
},
{
'path': 'styles.width',
'type': 'text',
'label': '길이(px, %)',
'placeholder': 'px, % 등 단위를 포함한 길이를 입력하세요'
},
{
'path': 'styles.height',
'type': 'text',
'label': '높이(px, %)',
'placeholder': 'px, % 등 단위를 포함한 높이를 입력하세요'
},
{
'path': 'config.animation',
'type': 'boolean',
'label': 'Animation 사용'
}
]
},
{
'groupName': 'title',
'groupTitle': '제목',
'fields': [
{
'path': 'config.title.text',
'type': 'text',
'label': '차트 제목',
'placeholder': '차트 제목을 입력하세요'
}
]
},
{
'groupName': 'tooltip',
'groupTitle': 'Tooltip',
'fields': [
{
'path': 'config.tooltip.show',
'type': 'boolean',
'label': 'Tooltip 표시'
},
{
'path': 'config.tooltip.trigger',
'type': 'select',
'label': 'Tooltip Trigger',
'placeholder': 'Enter the trigger type',
'options': ['item', 'axis', 'none']
}
]
},
{
'groupName': 'legend',
'groupTitle': 'Legend',
'fields': [
{
'path': 'config.legend.show',
'type': 'boolean',
'label': 'Legend 표시'
}
]
},
{
'groupName': 'grid',
'groupTitle': 'Grid',
'fields': [
{
'path': 'config.grid.show',
'type': 'boolean',
'label': 'Grid 표시'
},
{
'path': 'config.grid.left',
'type': 'text',
'label': 'Grid Left',
'placeholder': '좌측 여백'
},
{
'path': 'config.grid.right',
'type': 'text',
'label': 'Grid Right',
'placeholder': '우측 여백'
},
{
'path': 'config.grid.top',
'type': 'text',
'label': 'Grid Top',
'placeholder': '위쪽 여백'
},
{
'path': 'config.grid.bottom',
'type': 'text',
'label': 'Grid Bottom',
'placeholder': '아래쪽 여백'
},
{
'path': 'config.grid.containLabel',
'type': 'boolean',
'label': 'Contain Label'
}
]
},
{
'groupName': 'xAxis',
'groupTitle': 'X축',
'fields': [
{
'path': 'config.xAxis.show',
'type': 'boolean',
'label': 'X축 표시'
},
{
'path': 'config.xAxis.type',
'type': 'select',
'label': 'X축 유형',
'placeholder': 'X축 유형을 선택하세요',
'options': ['category', 'value', 'time']
}
]
},
{
'groupName': 'yAxis',
'groupTitle': 'Y축',
'fields': [
{
'path': 'config.yAxis[0].show',
'type': 'boolean',
'label': 'Y축 표시'
},
{
'path': 'config.yAxis[0].name',
'type': 'text',
'label': 'Y축 이름'
},
{
'path': 'config.yAxis[0].type',
'type': 'select',
'label': 'Y축 유형',
'placeholder': 'Y축 유형을 선택하세요',
'options': ['category', 'value', 'time']
},
{
'path': 'config.yAxis[0].position',
'type': 'select',
'label': 'Y축 위치',
'placeholder': 'Y축 위치를 선택하세요',
'options': ['left', 'right']
}
]
},
{
'groupName': 'toolbox',
'groupTitle': 'Toolbox',
'fields': [
{
'path': 'config.toolbox.show',
'type': 'boolean',
'label': 'Toolbox 표시',
'placeholder': 'Check to show toolbox'
},
{
'path': 'config.toolbox.feature.saveAsImage.show',
'type': 'boolean',
'label': 'Save as Image 사용'
}
]
}
];
this.seriesConfigModel = [
{
'groupName': 'default',
'groupTitle': '시리즈 정보',
'fields': [
{
'path': 'seriesConfig[0].queryType',
'type': 'select',
'label': 'Query Type',
'options': ['elasticsearch']
},
// {
// 'path': 'seriesConfig[0].type',
// 'type': 'select',
// 'label': '차트 유형',
// 'placeholder': '차트 유형을 선택하세요',
// 'options': ['bar', 'line', 'pie']
// },
{
'path': 'seriesConfig[0].index',
'type': 'text',
'label': 'Elasticsearch Index',
'placeholder': 'Elasticsearch Index를 입력하세요'
},
{
'path': 'seriesConfig[0].query',
'type': 'textarea',
'label': 'Elasticsearch Query'
},
{
'path': 'seriesConfig[0].queryResult',
'type': 'textarea',
'label': 'Elasticsearch Query Result'
},
{
'path': 'seriesConfig[0].dataPath',
'type': 'text',
'label': '데이터 경로',
'placeholder': '데이터 경로를 입력하세요'
}
]
}
];
}
initChartEditor() {
const chartEditorPopup = `
<div class="modal fade" id="chart_editor_modal" tabindex="-1" aria-labelledby="chart_editor_modal" aria-hidden="true">
<div class="modal-dialog modal-fullscreen modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">차트 정보</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="editor_container">
<input type="hidden" name="type" value="" />
<div class="left-panel default-info">
${LayoutUtil.createForm(this.chartEditorModel)}
</div>
<div class="editor-right-panel">
<div class="series-config d-flex flex-column">
<h6>Data Source</h6>
<div class="d-flex flex-row">
<div class="form-floating me-2" style="flex: 1">
<select class="form-select" name="seriesConfig[0].queryType">
<option value="elasticsearch">Elasticsearch</option>
</select>
<label for="queryType">Query Type</label>
</div>
<div class="form-floating me-2" style="flex: 1">
<input class="form-control" type="text" name="seriesConfig[0].index" value="" />
<label for="index">Elasticsearch Index</label>
</div>
</div>
<div class="d-flex flex-row mt-2" style="flex: 1">
<div style="flex: 1">
<div class="form-floating me-2" style="height: 100%">
<textarea class="form-control" type="text" name="seriesConfig[0].query" id="seriesConfigQuery" placeholder="Code goes here..." style="height: 400px;">
</textarea>
</div>
</div>
<div style="flex: 1">
<div class="form-floating me-2" style="height: 100%">
<textarea class="form-control" type="text" name="seriesConfig[0].queryResult" style="height: 400px;" readonly>
</textarea>
<label for="query">요청 결과</label>
</div>
</div>
</div>
<div class="d-flex flex-row mt-2" style="flex: 1">
<div style="flex: 1">
<div class="form-floating me-2" style="height: 100%">
<textarea class="form-control" type="text" name="seriesConfig[0].mapperY" style="height: 400px;">
</textarea>
<label for="mapper">데이터 Mapper</label>
</div>
</div>
<div style="flex: 1">
<div class="form-floating me-2" style="height: 100%">
<textarea class="form-control" type="text" name="seriesConfig[0].mapperX" style="height: 400px;">
</textarea>
<label for="mapper">X축 Mapper</label>
</div>
</div>
</div>
<div class="d-flex flex-row" style="height: 400px">
<div style="flex: 1">
<div class="form-floating me-2" style="height: 100%">
<textarea class="form-control" type="text" id="echartOption" style="width: 100%; height: 400px;" readonly>
</textarea>
<label for="mapper">생성된 Option</label>
</div>
</div>
<div style="flex: 1">
<div class="chart-preview form-control" id="chart-preview" style="height: 400px;">
chart_preview
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" data-bs-dismiss="modal">닫기</button>
<button type="button" class="btn btn-primary btn-sm add-chart">추가</button>
<button type="button" class="btn btn-primary btn-sm save-chart">저장</button>
</div>
</div>
</div>
</div>`;
$(this.rootContainer).parent().append(chartEditorPopup);
//add event to modal show
$('#chart_editor_modal').on('shown.bs.modal', (event) => {
let chartContainer = document.getElementById('chart-preview');
let rect = chartContainer.getBoundingClientRect();
let width = rect.width; // Precise width
let height = rect.height; // Precise height
this.chartPreview = echarts.init(document.getElementById('chart-preview'), 'dark', {
width: width, height: height
});
this.drawCharts();
});
}
showChartEditor(isNew, tempChart, containerIndex, columnIndex) {
this.targetContainerIndex = containerIndex;
this.targetColumnIndex = columnIndex;
this.tempChart = tempChart;
if (isNew) {
$('.add-chart').show();
$('.save-chart').hide();
} else {
$('.add-chart').hide();
$('.save-chart').show();
}
let flatModel = LayoutUtil.flattenObject(tempChart);
$('#chart_editor_modal').find('input, select, textarea').each((index, element) => {
if ($(element).attr('type') === 'checkbox') {
$(element).prop('checked', false);
}
$(element).val('');
});
_.forEach(flatModel, (value, key) => {
const inputSelector = `input[name="${key}"]`;
const selectSelector = `select[name="${key}"]`;
const textareaSelector = `textarea[name="${key}"]`;
const $input = $('#chart_editor_modal').find(inputSelector);
const $select = $('#chart_editor_modal').find(selectSelector);
const $textarea = $('#chart_editor_modal').find(textareaSelector);
if ($input.length > 0 && $input.attr('type') === 'checkbox') {
// If it's a checkbox, update its 'checked' property
$input.prop('checked', value);
} else if ($input.length > 0 || $select.length > 0 || $textarea.length > 0) {
// If any input, select, or textarea is found, update its value
$($input.add($select).add($textarea)).val(value);
} else {
// If no elements are found, log to the console
console.log(`Element not found for name: ${key}`);
}
});
$('#chart_editor_modal').modal('show');
console.log('chart_editor_modal show');
this.bindEvents();
}
addChart() {
$('#chart_editor_modal').find('input, select, textarea').each((index, element) => {
const fieldName = $(element).attr('name');
const newValue = LayoutUtil.getFieldValue(element);
if (fieldName !== '') {
_.set(this.tempChart, fieldName, newValue);
}
});
this.layoutManager.layoutData.children[this.targetContainerIndex].children.push(JSON.parse(JSON.stringify(this.tempChart)));
this.tempChart = null;
this.targetContainerIndex = -1;
this.targetColumnIndex = -1;
this.layoutManager.resetLayout();
$('#chart_editor_modal').modal('hide');
}
cleanUpEvents() {
console.log('cleanUpEvents');
$('.add-chart').off('click');
$('.save-chart').off('click');
}
saveChart() {
console.log('saveChart');
$('#chart_editor_modal').find('input, select, textarea').each((index, element) => {
const fieldName = $(element).attr('name');
const newValue = LayoutUtil.getFieldValue(element);
if (fieldName !== undefined && fieldName !== '') {
_.set(this.tempChart, fieldName, newValue);
}
});
const json = JSON.stringify(this.tempChart, LayoutUtil.echartReplacer, 2);
console.log(json);
this.layoutManager.layoutData.children[this.targetContainerIndex].children[this.targetColumnIndex] = JSON.parse(json);
this.targetContainerIndex = -1;
this.targetColumnIndex = -1;
this.layoutManager.resetLayout();
$('#chart_editor_modal').modal('hide');
}
refreshChart(chartInfo) {
console.log('refreshChart');
return new Promise((resolve, reject) => {
let option = JSON.parse(JSON.stringify(chartInfo.config));
option.series = [];
let self = this;
Promise.all(this.tempChart.seriesConfig.map(config => this.dashboard.requestQuery(config))).then(async (results) => {
await self.dashboard.updateChartSeries(option, results, chartInfo, (index, result) => {
$('#chart_editor_modal').find(`textarea[name='seriesConfig[${index}].queryResult']`).val('result = ' + JSON.stringify(result, null, 2));
});
document.getElementById('echartOption').value = JSON.stringify(option, null, 2);
self.chartPreview.setOption(option);
resolve();
}).catch((error) => {
console.error('Error fetching data for preview: ', error);
reject(error);
});
});
}
drawCharts() {
console.log('drawCharts');
const promises = [this.refreshChart(this.tempChart)];
Promise.all(promises).then(() => console.log('Chart Preview is completed')).catch(error => console.error('An error occurred:', error));
}
bindEvents() {
$('.add-chart').off('click').on('click', (event) => {
this.addChart();
});
$('.save-chart').off('click').on('click', (event) => {
this.saveChart();
});
$('#chart_editor_modal').find('input, select, textarea').each((index, element) => {
$(element).off('change').on('change', (event) => {
console.log('change event');
const fieldName = $(event.currentTarget).attr('name');
const newValue = LayoutUtil.getFieldValue(event.currentTarget);
if (fieldName !== '') {
_.set(this.tempChart, fieldName, newValue);
}
this.refreshChart(this.tempChart);
});
});
$('#chart_editor_modal').on('hidden.bs.modal', (event) => {
if (this.chartPreview != null && this.chartPreview.dispose) {
this.chartPreview.dispose();
this.chartPreview = null;
}
this.cleanUpEvents();
});
}
}
+344
View File
@@ -0,0 +1,344 @@
class Dashboard {
constructor(layoutData) {
this.layoutData = layoutData || {};
this.fromDateTarget = 'fromDate';
this.toDateTarget = 'toDate';
this.charts = [];
this.theme = 'dark';
this.requestUrl = '/mgmt/dashboard/search.do';
}
bindEvents() {
const events = [{selector: '.refresh', action: this.refresh.bind(this)}];
for (let event of events) {
$(document).off('click', event.selector).on('click', event.selector, event.action);
}
}
getDateTemplate(id, label) {
return `
<div class="input-group mb-3">
<div class="form-floating">
<input id="${id}Input" type="text" class="form-control" data-td-target="#${id}"/>
<label for="${id}Input">${label}</label>
</div>
<span class="input-group-text" data-td-target="#${id}" data-td-toggle="datetimepicker">
<span class="fa-solid fa-calendar"></span>
</span>
</div>
`;
}
init(chartAreaId) {
console.log('Dashboard init');
this.chartAreaId = chartAreaId;
this.initDate();
this.initCharts();
this.drawCharts();
window.addEventListener('resize', this.resizeAllCharts.bind(this));
}
initCharts() {
console.log('initCharts');
this.charts = [];
if (this.layoutData.children) {
this.layoutData.children.forEach((row) => {
row.children.forEach((chartInfo) => {
this.charts.push(chartInfo);
});
});
}
}
formatDate(isoString) {
const date = new Date(isoString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // +1 because getMonth() returns 0-11
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
formatTime(isoString) {
const date = new Date(isoString);
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
return `${hour}:${minute}:00`;
}
formatDateTime(isoString) {
return this.formatDate(isoString) + 'T' + this.formatTime(isoString);
}
startOfDay(isoString) {
return this.formatDate(isoString) + 'T00:00:00';
}
initDate() {
const ko = tempusDominus.locales.ko.localization;
const fromDateElement = document.getElementById(this.fromDateTarget);
$(fromDateElement).data('td-target-input', 'nearest');
$(fromDateElement).data('td-target-toggle', 'nearest');
const toDateElement = document.getElementById(this.toDateTarget);
$(toDateElement).data('td-target-input', 'nearest');
$(toDateElement).data('td-target-toggle', 'nearest');
$(fromDateElement).append(this.getDateTemplate(this.fromDateTarget, 'From'));
$(toDateElement).append(this.getDateTemplate(this.toDateTarget, 'To'));
this.fromDate = new tempusDominus.TempusDominus(fromDateElement, {
localization: ko, defaultDate: this.startOfDay(new Date())
});
this.toDate = new tempusDominus.TempusDominus(toDateElement, {
useCurrent: false, localization: ko, defaultDate: new Date()
});
fromDateElement.addEventListener(tempusDominus.Namespace.events.change, (e) => {
this.toDate.updateOptions({
restrictions: {
minDate: e.detail.date
}
});
});
const subscription = this.toDate.subscribe(tempusDominus.Namespace.events.change, (e) => {
this.fromDate.updateOptions({
restrictions: {
maxDate: e.date
}
});
});
}
requestPost(url, data, hds) {
var deferred = $.Deferred();
var headers = $.extend({
'Content-Type': 'application/json; charset=utf-8'
}, hds);
if (headers['Content-Type'].startsWith('application/json')) {
data = JSON.stringify(data);
}
$.ajax({
url: url, async: true, method: 'POST', data: data, headers: headers
}).done((response) => {
deferred.resolve(response);
}).fail((response, textStatus, errorThrown) => {
deferred.reject(response, textStatus, errorThrown);
});
return deferred.promise();
};
requestQuery(seriesConfig) {
let request = null;
try {
if (seriesConfig.queryType === 'elasticsearch') {
if (seriesConfig.query && seriesConfig.index !== '') {
let requestQuery = JSON.parse(seriesConfig.query);
requestQuery.query = requestQuery.query || {};
requestQuery.query.bool = requestQuery.query.bool || {};
requestQuery.query.bool.filter = requestQuery.query.bool.filter || [];
requestQuery.query.bool.filter.push({
'range': {
'@timestamp': {
'gte': this.formatDateTime(this.fromDate.dates.picked[0]),
'lte': this.formatDateTime(this.toDate.dates.picked[0]),
'time_zone': 'Asia/Seoul'
}
}
});
request = {
query: JSON.stringify(requestQuery), index: seriesConfig.index
};
return this.requestPost(this.requestUrl, request, {});
} else {
console.log('query is empty');
return Promise.resolve();
}
}
} catch (error) {
console.error('Error fetching data for ' + seriesConfig.contentId + ': ', error);
return Promise.reject(error);
}
}
resizeAllCharts() {
this.charts.forEach(chartInfo => {
let chartContainer = document.getElementById('chart_' + chartInfo.contentId);
let rect = chartContainer.getBoundingClientRect();
let width = rect.width; // Precise width
let height = rect.height; // Precise height
if (chartInfo.echart) {
chartInfo.echart.resize({width: width, height: height});
}
});
}
initializeChart(chartInfo) {
console.log('initializeChart: ' + chartInfo.contentId);
let chartContainer = document.getElementById('chart_' + chartInfo.contentId);
let rect = chartContainer.getBoundingClientRect();
let width = rect.width; // Precise width
let height = rect.height; // Precise height
chartInfo.echart = echarts.init(document.getElementById('chart_' + chartInfo.contentId), this.theme, {
width: width, height: height
});
}
executeScript(result, script) {
console.log('executeScript');
if (script) {
return new Promise((resolve, reject) => {
let resultFrame = document.getElementById('mapperScript');
window.preRequestComplete = (result, error) => {
if (error) {
console.log('Error executing script: ', error);
reject(error);
} else {
resolve(result);
}
};
window.result = result;
resultFrame.srcdoc = `<script>
function get(obj, path, defaultValue = undefined) {
const keys = Array.isArray(path) ? path : path.replace(/\\[(\\d+)\\]/g, '.$1').split('.');
let result = obj;
for (let key of keys) {
result = result?.[key];
if (result === undefined) {
return defaultValue;
}
}
return result;
}
function formatDate(isoString) {
const date = new Date(isoString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // +1 because getMonth() returns 0-11
const day = String(date.getDate()).padStart(2, '0');
return \`\${year}-\${month}-\${day}\`;
}
function formatTime(isoString) {
const date = new Date(isoString);
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
return \`\${hour}:\${minute}:00\`;
}
function formatDateTime(isoString) {
return formatDate(isoString) + 'T' + formatTime(isoString);
}
function startOfDay(isoString) {
return formatDate(isoString) + 'T00:00:00';
}
window.result = window.parent.result;
try {
const executeFunction = ${script};
const data = executeFunction(window.result);
window.parent.preRequestComplete(data, null);
} catch (error) {
window.parent.preRequestComplete(null, error);
}
</script>`;
});
} else {
console.log('No script to execute');
return Promise.resolve();
}
}
async refresh(event) {
console.log('refresh');
for (let i = 0; i < this.charts.length; i++) {
const chartInfo = this.charts[i];
try {
await this.refreshChart(chartInfo);
} catch (error) {
console.error(`Error refreshing chart ${chartInfo.contentId}:`, error);
}
}
console.log('All Charts requests are complete');
// const promises = this.charts.map(info => this.refreshChart(info));
// Promise.all(promises).then(() => console.log('All Charts requests are complete')).catch(error => console.error('An error occurred:', error));
}
drawCharts() {
console.log('drawCharts');
this.charts.forEach(info => this.initializeChart(info));
this.refresh();
this.bindEvents();
}
async updateChartSeries(option, results, chartInfo, callback) {
console.log('updateChartSeries');
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result){
let config = chartInfo.seriesConfig[i];
let parsedResult = JSON.parse(result);
if (callback) {
callback(i, parsedResult);
}
try {
console.log('start executeScript');
const dataY = await this.executeScript(parsedResult, config.mapperY);
if (Array.isArray(dataY)) {
dataY.forEach(item => {
option.series.push(item);
});
} else {
option.series.push(dataY);
}
const dataX = await this.executeScript(parsedResult, config.mapperX);
option.xAxis.data = dataX;
console.log('stop executeScript');
} catch (error) {
console.error(`Error updating series for chart ${chartInfo.contentId}:`, error);
}
}
}
}
async refreshChart(chartInfo) {
try {
console.log('refreshChart');
let option = JSON.parse(JSON.stringify(chartInfo.config));
option.series = [];
const results = await Promise.all(chartInfo.seriesConfig.map(config => this.requestQuery(config)));
await this.updateChartSeries(option, results, chartInfo);
// Clear the chart and set the new option
chartInfo.echart.clear();
chartInfo.echart.setOption(option);
console.log('Chart refreshed successfully');
} catch (error) {
console.error('Error fetching data for ' + chartInfo.contentId + ': ', error);
throw error; // Rethrow the error if you need to catch it higher up in the call stack
}
}
}
@@ -0,0 +1,643 @@
class LayoutManager {
constructor(container, layoutData) {
this.rootContainer = container;
this.tempContainer = null;
this.editContainerIndex = -1;
this.layoutData = layoutData || {
type: 'root',
styles: {
'display': 'flex',
'flex-direction': 'column',
'justify-content': 'start',
'align-items': 'flex-start',
'gap': '10px',
'width': '100%',
'height': '100vh',
'position': 'relative',
'overflow': 'auto',
'background-color': '#100C2A'
},
children: []
};
this.containerEditorModel = [
{
'groupName': 'default',
'groupTitle': '기본',
'fields': [
{
'path': 'styles.display',
'type': 'select',
'label': 'Display',
'options': ['flex']
},
{
'path': 'styles.flex-direction',
'type': 'select',
'label': 'Flex 방향',
'options': ['row', 'column', 'row-reverse', 'column-reverse']
},
{
'path': 'styles.width',
'type': 'text',
'label': '길이(px, %)',
'placeholder': 'px, % 등 단위를 포함한 길이를 입력하세요'
},
{
'path': 'styles.height',
'type': 'text',
'label': '높이(px, %)',
'placeholder': 'px, % 등 단위를 포함한 높이를 입력하세요'
}
]
}
];
this.dashboardEditorModel = [
{
'groupName': 'default',
'groupTitle': '기본',
'fields': [
{
'path': 'styles.background-color',
'type': 'text',
'label': '색상',
'placeholder': '#로 시작하는 값을 입력하세요'
},
{
'path': 'styles.width',
'type': 'text',
'label': '길이(px)',
'placeholder': 'px 단위를 포함한 길이를 입력하세요'
},
{
'path': 'styles.height',
'type': 'text',
'label': '높이(px)',
'placeholder': 'px 단위를 포함한 높이를 입력하세요'
}
]
}
]
}
// traverse(obj) {
// if (obj.type === 'chart') {
// $(`#chart_${obj.contentId}`).closest('.dashboard-chart').resizable({
// grid: 10,
// ghost: true,
// stop: (event, ui) => {
// const {width, height} = ui.size;
// obj.styles.width = `${width}px`;
// obj.styles.height = `${height}px`;
// this.resetLayout();
// }
// });
// }
//
// if (obj.children) {
// obj.children.forEach(child => {
// this.traverse(child);
// });
// }
// }
init(Dashboard, viewerMode) {
this.viewMode = viewerMode || false;
this.renderRoot(this.layoutData, this.rootContainer);
if (Dashboard) {
this.dashboard = new Dashboard(this.layoutData);
this.dashboard.init('chart_area');
}
if (!this.viewMode) {
this.initContainerEditor();
this.initDashboardEditor();
this.chartEditor = new ChartEditor(this.rootContainer, this, this.dashboard);
this.chartEditor.initChartEditor();
this.bindEvents();
}
}
initContainerEditor() {
const containerEditorPopup = `
<div class="modal fade" id="container_editor_modal" tabindex="-1" aria-labelledby="container_editor_modal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">컨테이너 정보</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="containerOption">
${LayoutUtil.createForm(this.containerEditorModel)}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" data-bs-dismiss="modal">닫기</button>
<button type="button" class="btn btn-primary btn-sm add-container">추가</button>
<button type="button" class="btn btn-primary btn-sm save-container">저장</button>
</div>
</div>
`;
$(this.rootContainer).parent().append(containerEditorPopup);
}
initDashboardEditor() {
const containerEditorPopup = `
<div class="modal fade" id="dashboard_editor_modal" tabindex="-1" aria-labelledby="dashboard_editor_modal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">대시보드 정보</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="containerOption">
${LayoutUtil.createForm(this.dashboardEditorModel)}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" data-bs-dismiss="modal">닫기</button>
<button type="button" class="btn btn-primary btn-sm save-dashboard">적용</button>
</div>
</div>
`;
$(this.rootContainer).parent().append(containerEditorPopup);
}
resetLayout() {
console.log('resetLayout');
$(this.rootContainer).empty();
this.renderRoot(this.layoutData, this.rootContainer);
this.dashboard.initCharts();
this.dashboard.layoutData = this.layoutData;
this.dashboard.drawCharts();
this.editContainerIndex = -1;
this.bindEvents();
}
addNewChart(containerIndex) {
this.editContainerIndex = containerIndex;
let tempChart = {
'contentId': '',
'type': 'chart',
'styles': {
width: '100%', height: '100%'
},
'config': {
'tooltip': {
'trigger': 'axis',
'show': true
},
'legend': {
'show': true
},
'grid': {
'show': true,
'top': 0,
'bottom': 0,
'left': 0,
'right': 0,
'containLabel': true
},
'title': {},
'xAxis': {
'show': true,
'type': 'category'
},
'yAxis': [
{
'show': true,
'name': '',
'type': 'value',
'position': 'left'
}],
'toolbox': {
'show': true
}
},
'seriesConfig': [
{
'type': 'line',
'index': '',
'queryType': 'elasticsearch',
'query': '',
'mapperX': `
function(result) {
let data = [];
return data;
}
`,
'mapperY': `
function(result) {
let data = [{
type : 'line',
data: []
}];
return data;
}
`,
'queryResult': ''
}
]
};
this.chartEditor.showChartEditor(true, tempChart, containerIndex);
}
editChart(editContainerIndex, editColumnIndex) {
console.log('edit-chart');
let tempChart = this.layoutData.children[editContainerIndex].children[editColumnIndex];
this.chartEditor.showChartEditor(false, JSON.parse(JSON.stringify(tempChart, LayoutUtil.echartReplacer)), editContainerIndex, editColumnIndex);
}
renderRoot(layoutData, parent) {
let classList = 'dashboard-root mt-2' + (!this.viewMode ? ' edit' : '');
let children = '';
let style = '';
if (layoutData.styles) {
style = Object.keys(layoutData.styles).map(key => `${key}: ${layoutData.styles[key]};`).join(' ');
}
console.log({style});
if (layoutData.children) {
layoutData.children.forEach((child) => {
if (child.type === 'container') {
children += this.renderContainer(child);
} else if (child.type === 'chart') {
children += this.renderChart(child);
}
});
}
let layoutItem = `<div class="${classList}" style="${style}"> ${children}</div>`;
$(parent).append(layoutItem);
// if (!this.viewMode) {
// this.traverse(this.layoutData);
// }
};
renderChart(layoutData) {
let style = '';
if (layoutData.styles) {
style = Object.keys(layoutData.styles).map(key => `${key}: ${layoutData.styles[key]};`).join(' ');
}
let editButton = this.viewMode ? '' : `<div class="chart-menu-icon" data-content-id="chart_${layoutData.contentId}">
<div class="dropdown">
<span class="" data-bs-toggle="dropdown" aria-expanded="false">⋮</span>
<ul class="dropdown-menu">
<li><span class="dropdown-item edit-chart">차트 편집</span></li>
<li><span class="dropdown-item remove-chart">차트 제거</span></li>
</ul>
</div>
</div>`;
return `
<div class="dashboard-chart" data-content-id="chart_${layoutData.contentId}" style="${style}">
${editButton}
<div id="chart_${layoutData.contentId}" style="width: 100%; height: 100%"></div>
</div>
`;
}
renderContainer(layoutData) {
console.log('renderContainer');
let children = '';
if (layoutData.children) {
layoutData.children.forEach(child => {
if (child.type === 'container') {
children += this.renderContainer(child);
} else if (child.type === 'chart') {
children += this.renderChart(child);
}
});
}
let width = layoutData.styles.width || '100%';
let height = layoutData.styles.height || '100%';
let editButton = this.viewMode ? '' : `
<div class="container-menu-icon">
<div class="dropdown">
<span><i class="fa-solid fa-arrows-up-down-left-right"></i></span>
<span class="" data-bs-toggle="dropdown" aria-expanded="false">⋮</span>
<ul class="dropdown-menu">
<li><span class="dropdown-item edit-container">컨테이너 편집</span></li>
<li><span class="dropdown-item remove-container">컨테이너 제거</span></li>
<li><span class="dropdown-item add-new-chart">차트 추가</span></li>
</ul>
</div>
</div>`;
let style = '';
if (layoutData.styles) {
style = Object.keys(layoutData.styles).map(key =>{
if (key === 'width' || key === 'height') return `${key}: 100%;`;
return `${key}: ${layoutData.styles[key]};`;
} ).join(' ');
}
return `<div class="dashboard-container" style="width: ${width}; height: ${height};">
<div class="handle">
${editButton}
</div>
<div class="dashboard-container-children" style="${style}">
${children}
</div>
</div>`;
}
addNewContainer(event) {
this.tempContainer = {
type: 'container',
styles: {
display: 'flex',
'flex-direction': 'row',
width: '100%',
height: '300px'
},
children: []
};
this.showContainerEditor(true);
}
editContainer(editContainerIndex) {
console.log('edit-container: ' + editContainerIndex);
this.tempContainer = JSON.parse(JSON.stringify(this.layoutData.children[editContainerIndex], LayoutUtil.echartReplacer));
this.showContainerEditor(false);
this.editContainerIndex = editContainerIndex;
}
removeContainer(containerIndex) {
console.log('edit-container: ' + containerIndex);
this.layoutData.children.splice(containerIndex, 1);
this.resetLayout();
}
addContainer(event) {
$('#container_editor_modal').find('input, select, textarea').each((index, element) => {
const fieldName = $(element).attr('name');
const newValue = LayoutUtil.getFieldValue(element);
if (fieldName !== '') {
_.set(this.tempContainer, fieldName, newValue);
}
});
this.layoutData.children.push(JSON.parse(JSON.stringify(this.tempContainer)));
this.tempContainer = null;
this.resetLayout();
$('#container_editor_modal').modal('hide');
}
saveContainer(event) {
console.log('save-container: ' + this.editContainerIndex);
$('#container_editor_modal').find('input, select, textarea').each((index, element) => {
const fieldName = $(element).attr('name');
const newValue = LayoutUtil.getFieldValue(element);
if (fieldName !== '') {
_.set(this.tempContainer, fieldName, newValue);
}
});
this.layoutData.children[this.editContainerIndex] = JSON.parse(JSON.stringify(this.tempContainer));
this.tempContainer = null;
this.resetLayout();
$('#container_editor_modal').modal('hide');
}
removeChart(containerIndex, columnIndex) {
this.layoutData.children[containerIndex].children.splice(columnIndex, 1);
this.resetLayout();
}
onRowsSorted(ui) {
let originalContainerIndex = ui.item.data('original_row_index');
this.updateLayoutDataRows(originalContainerIndex, ui);
}
onColumnsSorted(ui) {
let containerIndex = $('.dashboard-root').children('.dashboard-container').index(ui.item.closest('.dashboard-container'));
let originalContainerIndex = ui.item.data('original_row_index');
if (originalContainerIndex) {
if (originalContainerIndex === containerIndex) { // Check if the column moved from another row
this.updateLayoutDataWithinRow(ui.item);
} else { // The column was sorted within the same row
this.updateLayoutDataColumns(ui.item);
}
this.resetLayout();
}
}
updateLayoutDataRows(originalContainerIndex, ui) {
let targetIndex = $('.dashboard-root').children('.dashboard-container').index(ui.item.closest('.dashboard-container'));
if (targetIndex !== originalContainerIndex) {
this.layoutData.children.splice(targetIndex, 0, this.layoutData.children.splice(originalContainerIndex, 1)[0]);
}
console.log(this.layoutData);
}
updateLayoutDataWithinRow(movedItem) {
let oldContainerIndex = movedItem.data('original_row_index');
let oldIndex = movedItem.data('original_column_index');
if (oldContainerIndex !== -1 && oldIndex !== -1) {
let $row = movedItem.closest('.dashboard-container');
let newIndex = $row.children('.dashboard-chart').index(movedItem);
this.layoutData.children[oldContainerIndex].children.splice(newIndex, 0, this.layoutData.children[oldContainerIndex].children.splice(oldIndex, 1)[0]);
}
}
save() {
const jsonString = JSON.stringify(this.layoutData, LayoutUtil.echartReplacer);
return jsonString;
}
updateLayoutDataColumns(movedItem) {
// 새로운 행과 이전 행의 jQuery 요소를 가져옵니다
let $newRow = movedItem.closest('.dashboard-container');
// 이전 행의 레이아웃 데이터에서 이동된 행과 열의 인덱스를 찾습니다
let oldContainerIndex = movedItem.data('original_row_index');
let movedColumnIndex = movedItem.data('original_column_index');
if (oldContainerIndex !== -1 && movedColumnIndex !== undefined && movedColumnIndex !== -1) {
movedItem.data('original_row_index', -1);
movedItem.data('original_column_index', -1);
// 새 행의 인덱스를 찾습니다
let newContainerIndex = $('.dashboard-root').children('.dashboard-container').index($newRow);
// 이전 행에서 열 데이터를 추출합니다
let movedColumnData = this.layoutData.children[oldContainerIndex].children.splice(movedColumnIndex, 1)[0];
// 아이템이 드롭된 새 열 인덱스를 찾습니다
let newColumnIndex = movedItem.closest('.dashboard-container-children').children('.dashboard-chart').index(movedItem);
// 이동된 열 데이터를 새 행의 열에 새 인덱스에 삽입합니다
// 아이템이 마지막 위치로 이동된 경우 인덱스는 -1이 될 것이므로 이 경우를 처리합니다
if (newColumnIndex === -1) {
this.layoutData.children[newContainerIndex].children.push(movedColumnData);
} else {
this.layoutData.children[newContainerIndex].children.splice(newColumnIndex, 0, movedColumnData);
}
} else {
console.log('called twice on update');
}
}
showContainerEditor(isNew) {
if (isNew) {
$('.add-container').show();
$('.save-container').hide();
} else {
$('.add-container').hide();
$('.save-container').show();
}
let flatModel = LayoutUtil.flattenObject(JSON.parse(JSON.stringify(this.tempContainer)));
$('#container_editor_modal').find('input, select, textarea').each((index, element) => {
if ($(element).attr('type') === 'checkbox') {
$(element).prop('checked', false);
}
$(element).val('');
});
_.forEach(flatModel, (value, key) => {
if ($('#container_editor_modal').find(`input[name="${key}"]`).attr('type') === 'checkbox') {
$('#container_editor_modal').find(`input[name="${key}"]`).prop('checked', value);
} else {
$('#container_editor_modal').find(`input[name="${key}"], select[name="${key}"], textarea[name="${key}"]`).val(value);
}
});
$('#container_editor_modal').modal('show');
}
resize() {
console.log('resize');
this.dashboard.resizeAllCharts();
}
editDashboard(event) {
this.showDashboardEditor();
}
showDashboardEditor() {
let flatModel = LayoutUtil.flattenObject(JSON.parse(JSON.stringify(this.layoutData, LayoutUtil.echartReplacer)));
$('#dashboard_editor_modal').find('input, select, textarea').each((index, element) => {
if ($(element).attr('type') === 'checkbox') {
$(element).prop('checked', false);
}
$(element).val('');
});
_.forEach(flatModel, (value, key) => {
if ($('#dashboard_editor_modal').find(`input[name="${key}"]`).attr('type') === 'checkbox') {
$('#dashboard_editor_modal').find(`input[name="${key}"]`).prop('checked', value);
} else {
$('#dashboard_editor_modal').find(`input[name="${key}"], select[name="${key}"], textarea[name="${key}"]`).val(value);
}
});
$('#dashboard_editor_modal').modal('show');
}
saveDashboard(event) {
$('#dashboard_editor_modal').find('input, select, textarea').each((index, element) => {
const fieldName = $(element).attr('name');
const newValue = LayoutUtil.getFieldValue(element);
if (fieldName !== '') {
_.set(this.layoutData, fieldName, newValue);
}
});
console.log(this.layoutData);
this.resetLayout();
$('#dashboard_editor_modal').modal('hide');
}
bindEvents() {
$('.add-new-container').off('click').on('click', (event) => {
this.addNewContainer(event);
});
$('.edit-dashboard').off('click').on('click', (event) => {
this.editDashboard(event);
});
$('.save-dashboard').off('click').on('click', (event) => {
this.saveDashboard(event);
});
$('.remove-container').off('click').on('click', (event) => {
let containerIndex = $('.dashboard-root').children('.dashboard-container').index(event.target.closest('.dashboard-container'));
this.removeContainer(containerIndex);
});
$('.add-container').off('click').on('click', (event) => {
this.addContainer(event);
});
$('.save-container').off('click').on('click', (event) => {
this.saveContainer(event);
});
$('.edit-container').off('click').on('click', (event) => {
let editContainerIndex = $('.dashboard-root').children('.dashboard-container').index(event.target.closest('.dashboard-container'));
this.editContainer(editContainerIndex);
});
$('.edit-chart').off('click').on('click', (event) => {
let editContainerIndex = $('.dashboard-root').children('.dashboard-container').index(event.target.closest('.dashboard-container'));
let column = event.target.closest('.dashboard-chart');
let editColumnIndex = $(event.target.closest('.dashboard-container-children')).children('.dashboard-chart').index(column);
this.editChart(editContainerIndex, editColumnIndex);
});
$('.add-new-chart').off('click').on('click', (event) => {
let containerIndex = $('.dashboard-root').children('.dashboard-container').index(event.target.closest('.dashboard-container'));
this.addNewChart(containerIndex);
});
$('.remove-chart').off('click').on('click', (event) => {
let column = event.target.closest('.dashboard-chart');
let containerIndex = $('.dashboard-root').children('.dashboard-container').index(event.target.closest('.dashboard-container'));
let columnIndex = $(event.target.closest('.dashboard-container-children')).children('.dashboard-chart').index(column);
this.removeChart(containerIndex, columnIndex);
});
if ($('.dashboard-root').hasClass('ui-sortable')) {
$('.dashboard-root').sortable('destroy');
}
$('.dashboard-root').sortable({
handle: '.handle',
items: '.dashboard-container',
placeholder: 'ui-state-highlight',
start: (event, ui) => {
let originalRowIdx = $('.dashboard-root').children('.dashboard-container').index(ui.item.closest('.dashboard-container'));
ui.item.data('original_row_index', originalRowIdx);
}, update: (event, ui) => {
this.onRowsSorted(ui);
}
});
if ($('.dashboard-container').hasClass('ui-sortable')) {
$('.dashboard-container').sortable('destroy');
}
$('.dashboard-container-children').sortable({
// handle: ".handle",
placeholder: 'ui-state-highlight',
items: '.dashboard-chart',
connectWith: '.dashboard-container-children',
update: (event, ui) => {
this.onColumnsSorted(ui);
},
start: (event, ui) => {
let originalRowIdx = $('.dashboard-root').children('.dashboard-container').index(ui.item.closest('.dashboard-container'));
let originalColumnIdx = ui.item.closest('.dashboard-container-children').children('.dashboard-chart').index(ui.item);
ui.item.data('original_row_index', originalRowIdx);
ui.item.data('original_column_index', originalColumnIdx);
}
}).disableSelection();
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,173 @@
/*
* 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.
*/
/**
* AUTO-GENERATED FILE. DO NOT MODIFY.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports);
} else {
// Browser globals
factory({});
}
})(this, function(exports) {
/**
* Language: English.
*/
var localeObj = {
time: {
month: [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
],
monthAbbr: [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
],
dayOfWeek: [
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
],
dayOfWeekAbbr: [
'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
]
},
legend: {
selector: {
all: 'All',
inverse: 'Inv'
}
},
toolbox: {
brush: {
title: {
rect: 'Box Select',
polygon: 'Lasso Select',
lineX: 'Horizontally Select',
lineY: 'Vertically Select',
keep: 'Keep Selections',
clear: 'Clear Selections'
}
},
dataView: {
title: 'Data View',
lang: ['Data View', 'Close', 'Refresh']
},
dataZoom: {
title: {
zoom: 'Zoom',
back: 'Zoom Reset'
}
},
magicType: {
title: {
line: 'Switch to Line Chart',
bar: 'Switch to Bar Chart',
stack: 'Stack',
tiled: 'Tile'
}
},
restore: {
title: 'Restore'
},
saveAsImage: {
title: 'Save as Image',
lang: ['Right Click to Save Image']
}
},
series: {
typeNames: {
pie: 'Pie chart',
bar: 'Bar chart',
line: 'Line chart',
scatter: 'Scatter plot',
effectScatter: 'Ripple scatter plot',
radar: 'Radar chart',
tree: 'Tree',
treemap: 'Treemap',
boxplot: 'Boxplot',
candlestick: 'Candlestick',
k: 'K line chart',
heatmap: 'Heat map',
map: 'Map',
parallel: 'Parallel coordinate map',
lines: 'Line graph',
graph: 'Relationship graph',
sankey: 'Sankey diagram',
funnel: 'Funnel chart',
gauge: 'Gauge',
pictorialBar: 'Pictorial bar',
themeRiver: 'Theme River Map',
sunburst: 'Sunburst'
}
},
aria: {
general: {
withTitle: 'This is a chart about "{title}"',
withoutTitle: 'This is a chart'
},
series: {
single: {
prefix: '',
withName: ' with type {seriesType} named {seriesName}.',
withoutName: ' with type {seriesType}.'
},
multiple: {
prefix: '. It consists of {seriesCount} series count.',
withName: ' The {seriesId} series is a {seriesType} representing {seriesName}.',
withoutName: ' The {seriesId} series is a {seriesType}.',
separator: {
middle: '',
end: ''
}
}
},
data: {
allData: 'The data is as follows: ',
partialData: 'The first {displayCnt} items are: ',
withName: 'the data for {name} is {value}',
withoutName: '{value}',
separator: {
middle: ', ',
end: '. '
}
}
}
};
for (var key in localeObj) {
if (localeObj.hasOwnProperty(key)) {
exports[key] = localeObj[key];
}
}
});
@@ -0,0 +1,169 @@
/*
* 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.
*/
/**
* AUTO-GENERATED FILE. DO NOT MODIFY.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
/**
* Language: English.
*/
var localeObj = {
time: {
month: [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
],
monthAbbr: [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
],
dayOfWeek: [
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
],
dayOfWeekAbbr: [
'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
]
},
legend: {
selector: {
all: 'All',
inverse: 'Inv'
}
},
toolbox: {
brush: {
title: {
rect: 'Box Select',
polygon: 'Lasso Select',
lineX: 'Horizontally Select',
lineY: 'Vertically Select',
keep: 'Keep Selections',
clear: 'Clear Selections'
}
},
dataView: {
title: 'Data View',
lang: ['Data View', 'Close', 'Refresh']
},
dataZoom: {
title: {
zoom: 'Zoom',
back: 'Zoom Reset'
}
},
magicType: {
title: {
line: 'Switch to Line Chart',
bar: 'Switch to Bar Chart',
stack: 'Stack',
tiled: 'Tile'
}
},
restore: {
title: 'Restore'
},
saveAsImage: {
title: 'Save as Image',
lang: ['Right Click to Save Image']
}
},
series: {
typeNames: {
pie: 'Pie chart',
bar: 'Bar chart',
line: 'Line chart',
scatter: 'Scatter plot',
effectScatter: 'Ripple scatter plot',
radar: 'Radar chart',
tree: 'Tree',
treemap: 'Treemap',
boxplot: 'Boxplot',
candlestick: 'Candlestick',
k: 'K line chart',
heatmap: 'Heat map',
map: 'Map',
parallel: 'Parallel coordinate map',
lines: 'Line graph',
graph: 'Relationship graph',
sankey: 'Sankey diagram',
funnel: 'Funnel chart',
gauge: 'Gauge',
pictorialBar: 'Pictorial bar',
themeRiver: 'Theme River Map',
sunburst: 'Sunburst'
}
},
aria: {
general: {
withTitle: 'This is a chart about "{title}"',
withoutTitle: 'This is a chart'
},
series: {
single: {
prefix: '',
withName: ' with type {seriesType} named {seriesName}.',
withoutName: ' with type {seriesType}.'
},
multiple: {
prefix: '. It consists of {seriesCount} series count.',
withName: ' The {seriesId} series is a {seriesType} representing {seriesName}.',
withoutName: ' The {seriesId} series is a {seriesType}.',
separator: {
middle: '',
end: ''
}
}
},
data: {
allData: 'The data is as follows: ',
partialData: 'The first {displayCnt} items are: ',
withName: 'the data for {name} is {value}',
withoutName: '{value}',
separator: {
middle: ', ',
end: '. '
}
}
}
};
echarts.registerLocale('EN', localeObj);
});
@@ -0,0 +1,173 @@
/*
* 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.
*/
/**
* AUTO-GENERATED FILE. DO NOT MODIFY.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports);
} else {
// Browser globals
factory({});
}
})(this, function(exports) {
/**
* Language: Korean.
*/
var localeObj = {
time: {
month: [
'1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월'
],
monthAbbr: [
'1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월'
],
dayOfWeek: [
'일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'
],
dayOfWeekAbbr: [
'일', '월', '화', '수', '목', '금', '토'
]
},
legend: {
selector: {
all: '모두 선택',
inverse: '선택 범위 반전'
}
},
toolbox: {
brush: {
title: {
rect: '사각형 선택',
polygon: '올가미 선택',
lineX: '수평 선택',
lineY: '수직 선택',
keep: '선택 유지',
clear: '선택 지우기'
}
},
dataView: {
title: '날짜 보기',
lang: ['날짜 보기', '닫기', '새로 고침']
},
dataZoom: {
title: {
zoom: '확대/축소',
back: '확대/축소 초기화'
}
},
magicType: {
title: {
line: '꺽은선 그래프로 변경',
bar: '막대 그래프로 변경',
stack: '스택',
tiled: '타일'
}
},
restore: {
title: '복구'
},
saveAsImage: {
title: '이미지로 저장',
lang: ['이미지를 저장하려면 마우스 오른쪽 버튼을 클릭하세요.']
}
},
series: {
typeNames: {
pie: '원 그래프',
bar: '막대 그래프',
line: '꺽은선 그래프',
scatter: '산점도',
effectScatter: '물결 효과 산점도',
radar: '방사형 그래프',
tree: '트리',
treemap: '트리맵',
boxplot: '상자 수염 그래프',
candlestick: '캔들스틱 차트',
k: 'K 라인 차트',
heatmap: '히트 맵',
map: '지도',
parallel: '평행 좌표 맵',
lines: '선',
graph: '관계 그래프',
sankey: '산키 다이어그램',
funnel: '깔때기형 그래프',
gauge: '계기',
pictorialBar: '픽토그램 차트',
themeRiver: '스트림 그래프',
sunburst: '선버스트 차트'
}
},
aria: {
general: {
withTitle: '"{title}"에 대한 차트입니다.',
withoutTitle: '차트입니다.'
},
series: {
single: {
prefix: '',
withName: ' 차트 유형은 {seriesType}이며 {seriesName}을 표시합니다.',
withoutName: ' 차트 유형은 {seriesType}입니다.'
},
multiple: {
prefix: '. {seriesCount} 하나의 차트 시리즈로 구성됩니다.',
withName: ' {seriesId}번째 시리즈는 {seriesName}을 나타내는 {seriesType} representing.',
withoutName: ' {seriesId}번째 시리즈는 {seriesType}입니다.',
separator: {
middle: '',
end: ''
}
}
},
data: {
allData: '데이터: ',
partialData: '첫번째 {displayCnt} 아이템: ',
withName: '{name}의 데이터는 {value}',
withoutName: '{value}',
separator: {
middle: ', ',
end: '. '
}
}
}
};
for (var key in localeObj) {
if (localeObj.hasOwnProperty(key)) {
exports[key] = localeObj[key];
}
}
});
@@ -0,0 +1,169 @@
/*
* 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.
*/
/**
* AUTO-GENERATED FILE. DO NOT MODIFY.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
/**
* Language: Korean.
*/
var localeObj = {
time: {
month: [
'1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월'
],
monthAbbr: [
'1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월'
],
dayOfWeek: [
'일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'
],
dayOfWeekAbbr: [
'일', '월', '화', '수', '목', '금', '토'
]
},
legend: {
selector: {
all: '모두 선택',
inverse: '선택 범위 반전'
}
},
toolbox: {
brush: {
title: {
rect: '사각형 선택',
polygon: '올가미 선택',
lineX: '수평 선택',
lineY: '수직 선택',
keep: '선택 유지',
clear: '선택 지우기'
}
},
dataView: {
title: '날짜 보기',
lang: ['날짜 보기', '닫기', '새로 고침']
},
dataZoom: {
title: {
zoom: '확대/축소',
back: '확대/축소 초기화'
}
},
magicType: {
title: {
line: '꺽은선 그래프로 변경',
bar: '막대 그래프로 변경',
stack: '스택',
tiled: '타일'
}
},
restore: {
title: '복구'
},
saveAsImage: {
title: '이미지로 저장',
lang: ['이미지를 저장하려면 마우스 오른쪽 버튼을 클릭하세요.']
}
},
series: {
typeNames: {
pie: '원 그래프',
bar: '막대 그래프',
line: '꺽은선 그래프',
scatter: '산점도',
effectScatter: '물결 효과 산점도',
radar: '방사형 그래프',
tree: '트리',
treemap: '트리맵',
boxplot: '상자 수염 그래프',
candlestick: '캔들스틱 차트',
k: 'K 라인 차트',
heatmap: '히트 맵',
map: '지도',
parallel: '평행 좌표 맵',
lines: '선',
graph: '관계 그래프',
sankey: '산키 다이어그램',
funnel: '깔때기형 그래프',
gauge: '계기',
pictorialBar: '픽토그램 차트',
themeRiver: '스트림 그래프',
sunburst: '선버스트 차트'
}
},
aria: {
general: {
withTitle: '"{title}"에 대한 차트입니다.',
withoutTitle: '차트입니다.'
},
series: {
single: {
prefix: '',
withName: ' 차트 유형은 {seriesType}이며 {seriesName}을 표시합니다.',
withoutName: ' 차트 유형은 {seriesType}입니다.'
},
multiple: {
prefix: '. {seriesCount} 하나의 차트 시리즈로 구성됩니다.',
withName: ' {seriesId}번째 시리즈는 {seriesName}을 나타내는 {seriesType} representing.',
withoutName: ' {seriesId}번째 시리즈는 {seriesType}입니다.',
separator: {
middle: '',
end: ''
}
}
},
data: {
allData: '데이터: ',
partialData: '첫번째 {displayCnt} 아이템: ',
withName: '{name}의 데이터는 {value}',
withoutName: '{value}',
separator: {
middle: ', ',
end: '. '
}
}
}
};
echarts.registerLocale('KO', localeObj);
});
@@ -0,0 +1,163 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#f2385a',
'#f5a503',
'#4ad9d9',
'#f7879c',
'#c1d7a8',
'#4dffd2',
'#fccfd7',
'#d5f6f6'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#f2385a'
}
},
visualMap: {
color: ['#f2385a', '#f5a503']
},
toolbox: {
color: ['#f2385a', '#f2385a', '#f2385a', '#f2385a']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#f2385a',
type: 'dashed'
},
crossStyle: {
color: '#f2385a'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#f2385a' // Handle color
},
timeline: {
lineStyle: {
color: '#f2385a'
},
controlStyle: {
color: '#f2385a',
borderColor: '#f2385a'
}
},
candlestick: {
itemStyle: {
color: '#f2385a',
color0: '#f5a503'
},
lineStyle: {
width: 1,
color: '#f2385a',
color0: '#f5a503'
},
areaStyle: {
color: '#c1d7a8',
color0: '#4ad9d9'
}
},
map: {
itemStyle: {
color: '#f2385a'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#f2385a'
},
linkStyle: {
color: '#f2385a'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#f5a503'],
[0.8, '#f2385a'],
[1, '#c1d7a8']
],
width: 8
}
}
}
};
echarts.registerTheme('azul', theme);
});
@@ -0,0 +1,178 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#001727',
'#805500',
'#ffff00',
'#ffd11a',
'#f2d71f',
'#f2be19',
'#f3a81a',
'#fff5cc'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#001727'
}
},
visualMap: {
color: ['#001727', '#805500']
},
toolbox: {
color: ['#001727', '#001727', '#001727', '#001727']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#001727',
type: 'dashed'
},
crossStyle: {
color: '#001727'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#001727' // Handle color
},
timeline: {
lineStyle: {
color: '#001727'
},
controlStyle: {
color: '#001727',
borderColor: '#001727'
}
},
candlestick: {
itemStyle: {
color: '#f3a81a',
color0: '#ffff00'
},
lineStyle: {
width: 1,
color: '#ffff00',
color0: '#f3a81a'
},
areaStyle: {
color: '#805500',
color0: '#ffff00'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#f3a81a',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#805500'
}
},
map: {
itemStyle: {
color: '#ffd11a'
},
areaStyle: {
color: '#f2be19'
},
label: {
color: '#ffd11a'
}
},
graph: {
itemStyle: {
color: '#001727'
},
linkStyle: {
color: '#001727'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#f2d71f'],
[0.8, '#001727'],
[1, '#ffff00']
],
width: 8
}
}
}
};
echarts.registerTheme('bee-inspired', theme);
});
@@ -0,0 +1,178 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#1790cf',
'#1bb2d8',
'#99d2dd',
'#88b0bb',
'#1c7099',
'#038cc4',
'#75abd0',
'#afd6dd'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#1790cf'
}
},
visualMap: {
color: ['#1790cf', '#a2d4e6']
},
toolbox: {
color: ['#1790cf', '#1790cf', '#1790cf', '#1790cf']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#1790cf',
type: 'dashed'
},
crossStyle: {
color: '#1790cf'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#1790cf' // Handle color
},
timeline: {
lineStyle: {
color: '#1790cfa'
},
controlStyle: {
color: '#1790cf',
borderColor: '#1790cf'
}
},
candlestick: {
itemStyle: {
color: '#1bb2d8',
color0: '#99d2dd'
},
lineStyle: {
width: 1,
color: '#1c7099',
color0: '#88b0bb'
},
areaStyle: {
color: '#1790cf',
color0: '#1bb2d8'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#1bb2d8',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#1790cf'
}
},
graph: {
itemStyle: {
color: '#1bb2d8'
},
linkStyle: {
color: '#88b0bb'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '99d2dd'
},
label: {
color: '#c12e34'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#1bb2d8'],
[0.8, '#1790cf'],
[1, '#1c7099']
],
width: 8
}
}
}
};
echarts.registerTheme('blue', theme);
});
@@ -0,0 +1,178 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#fad089',
'#ff9c5b',
'#f5634a',
'#ed303c',
'#3b8183',
'#f7826e',
'#faac9e',
'#fcd5cf'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#fad089'
}
},
visualMap: {
color: ['#fad089', '#a2d4e6']
},
toolbox: {
color: ['#fad089', '#fad089', '#fad089', '#fad089']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#fad089',
type: 'dashed'
},
crossStyle: {
color: '#fad089'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#fad089' // Handle color
},
timeline: {
lineStyle: {
color: '#fad089'
},
controlStyle: {
color: '#fad089',
borderColor: '#fad089'
}
},
candlestick: {
itemStyle: {
color: '#ff9c5b',
color0: '#f5634a'
},
lineStyle: {
width: 1,
color: '#3b8183',
color0: '#ed303c'
},
areaStyle: {
color: '#fad089',
color0: '#ed303c'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#fad089',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#ed303c'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#f5634a'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#f5634a'
},
linkStyle: {
color: '#fad089'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#ff9c5b'],
[0.8, '#fad089'],
[1, '#3b8183']
],
width: 8
}
}
}
};
echarts.registerTheme('caravan', theme);
});
@@ -0,0 +1,163 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#f0d8A8',
'#3d1c00',
'#86b8b1',
'#f2d694',
'#fa2a00',
'#ff8066',
'#ffd5cc',
'#f9edd2'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#f0d8A8'
}
},
visualMap: {
color: ['#f0d8A8', '#3d1c00']
},
toolbox: {
color: ['#f0d8A8', '#f0d8A8', '#f0d8A8', '#f0d8A8']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#f0d8A8',
type: 'dashed'
},
crossStyle: {
color: '#f0d8A8'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#f0d8A8' // Handle color
},
timeline: {
lineStyle: {
color: '#f0dba8'
},
controlStyle: {
color: '#f0dba8',
borderColor: '#f0dba8'
}
},
candlestick: {
itemStyle: {
color: '#3d1c00',
color0: '#86b8b1'
},
lineStyle: {
width: 1,
color: '#fa2a00',
color0: '#f2d694'
},
areaStyle: {
color: '#f0d8A8',
color0: '#86b8b1'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#86b8b1'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#3d1c00'
},
linkStyle: {
color: '#f0d8A8'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#3d1c00'],
[0.8, '#f0d8A8'],
[1, '#fa2a00']
],
width: 8
}
}
}
};
echarts.registerTheme('carp', theme);
});
@@ -0,0 +1,180 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#b21ab4',
'#6f0099',
'#2a2073',
'#0b5ea8',
'#17aecc',
'#b3b3ff',
'#eb99ff',
'#fae6ff',
'#e6f2ff',
'#eeeeee'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#00aecd'
}
},
visualMap: {
color: ['#00aecd', '#a2d4e6']
},
toolbox: {
color: ['#00aecd', '#00aecd', '#00aecd', '#00aecd']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#00aecd',
type: 'dashed'
},
crossStyle: {
color: '#00aecd'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#00aecd' // Handle color
},
timeline: {
lineStyle: {
color: '#00aecd'
},
controlStyle: {
color: '#00aecd',
borderColor: '00aecd'
}
},
candlestick: {
itemStyle: {
color: '#00aecd',
color0: '#a2d4e6'
},
lineStyle: {
width: 1,
color: '#00aecd',
color0: '#a2d4e6'
},
areaStyle: {
color: '#b21ab4',
color0: '#0b5ea8'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#b21ab4',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#0b5ea8'
}
},
graph: {
itemStyle: {
color: '#b21ab4'
},
linkStyle: {
color: '#2a2073'
}
},
map: {
itemStyle: {
color: '#c12e34'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#dddddd'],
[0.8, '#00aecd'],
[1, '#f5ccff']
],
width: 8
}
}
}
};
echarts.registerTheme('cool', theme);
});
@@ -0,0 +1,164 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#eee';
var axisCommon = function() {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
axisTick: {
lineStyle: {
color: contrastColor
}
},
axisLabel: {
color: contrastColor
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#aaa'
}
},
splitArea: {
areaStyle: {
color: contrastColor
}
}
};
};
var colorPalette = [
'#00305a',
'#004b8d',
'#0074d9',
'#4192d9',
'#7abaf2',
'#99cce6',
'#d6ebf5',
'#eeeeee'
];
var theme = {
color: colorPalette,
backgroundColor: '#333',
tooltip: {
axisPointer: {
lineStyle: {
color: contrastColor
},
crossStyle: {
color: contrastColor
}
}
},
legend: {
textStyle: {
color: contrastColor
}
},
title: {
textStyle: {
color: contrastColor
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#00305a' // Handle color
},
timeline: {
itemStyle: {
color: colorPalette[1]
},
lineStyle: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
},
label: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#004b8d'],
[0.8, '#00305a'],
[1, '#7abaf2']
],
width: 8
}
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark-blue', theme);
});
@@ -0,0 +1,164 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#eee';
var axisCommon = function() {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
axisTick: {
lineStyle: {
color: contrastColor
}
},
axisLabel: {
color: contrastColor
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#aaa'
}
},
splitArea: {
areaStyle: {
color: contrastColor
}
}
};
};
var colorPalette = [
'#458c6b',
'#f2da87',
'#d9a86c',
'#d94436',
'#a62424',
'#76bc9b',
'#cce6da',
'#eeeeee'
];
var theme = {
color: colorPalette,
backgroundColor: '#333',
tooltip: {
axisPointer: {
lineStyle: {
color: contrastColor
},
crossStyle: {
color: contrastColor
}
}
},
legend: {
textStyle: {
color: contrastColor
}
},
title: {
textStyle: {
color: contrastColor
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#458c6b' // Handle color
},
timeline: {
itemStyle: {
color: colorPalette[1]
},
lineStyle: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
},
label: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#f2da87'],
[0.8, '#458c6b'],
[1, '#a62424']
],
width: 8
}
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark-bold', theme);
});
@@ -0,0 +1,164 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#eee';
var axisCommon = function() {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
axisTick: {
lineStyle: {
color: contrastColor
}
},
axisLabel: {
color: contrastColor
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#aaa'
}
},
splitArea: {
areaStyle: {
color: contrastColor
}
}
};
};
var colorPalette = [
'#52656b',
'#ff3b77',
'#a3cc00',
'#ffffff',
'#b8b89f',
'#ffccdb',
'#e5ff80',
'#f4f4f0'
];
var theme = {
color: colorPalette,
backgroundColor: '#333',
tooltip: {
axisPointer: {
lineStyle: {
color: contrastColor
},
crossStyle: {
color: contrastColor
}
}
},
legend: {
textStyle: {
color: contrastColor
}
},
title: {
textStyle: {
color: contrastColor
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#52656b' // Handle color
},
timeline: {
itemStyle: {
color: colorPalette[1]
},
lineStyle: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
},
label: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#ff3b77'],
[0.8, '#52656b'],
[1, '#b8b89f']
],
width: 8
}
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark-digerati', theme);
});
@@ -0,0 +1,164 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#eee';
var axisCommon = function() {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
axisTick: {
lineStyle: {
color: contrastColor
}
},
axisLabel: {
color: contrastColor
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#aaa'
}
},
splitArea: {
areaStyle: {
color: contrastColor
}
}
};
};
var colorPalette = [
'#00a8c6',
'#40c0cb',
'#ebd3ad',
'#aee239',
'#8fbe00',
'#33e0ff',
'#b3f4ff',
'#e6ff99'
];
var theme = {
color: colorPalette,
backgroundColor: '#333',
tooltip: {
axisPointer: {
lineStyle: {
color: contrastColor
},
crossStyle: {
color: contrastColor
}
}
},
legend: {
textStyle: {
color: contrastColor
}
},
title: {
textStyle: {
color: contrastColor
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#00a8c6' // Handle color
},
timeline: {
itemStyle: {
color: colorPalette[1]
},
lineStyle: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
},
label: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#40c0cb'],
[0.8, '#00a8c6'],
[1, '#8fbe00']
],
width: 8
}
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark-fresh-cut', theme);
});
@@ -0,0 +1,164 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#eee';
var axisCommon = function() {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
axisTick: {
lineStyle: {
color: contrastColor
}
},
axisLabel: {
color: contrastColor
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#aaa'
}
},
splitArea: {
areaStyle: {
color: contrastColor
}
}
};
};
var colorPalette = [
'#cc0e00',
'#ff1a0a',
'#ff8880',
'#ffc180',
'#ffc2b0',
'#ffffff',
'#ff8880',
'#ffe6e6'
];
var theme = {
color: colorPalette,
backgroundColor: '#333',
tooltip: {
axisPointer: {
lineStyle: {
color: contrastColor
},
crossStyle: {
color: contrastColor
}
}
},
legend: {
textStyle: {
color: contrastColor
}
},
title: {
textStyle: {
color: contrastColor
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#cc0e00' // Handle color
},
timeline: {
itemStyle: {
color: colorPalette[1]
},
lineStyle: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
},
label: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#ff1a0a'],
[0.8, '#cc0e00'],
[1, '#ffc2b0']
],
width: 8
}
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark-mushroom', theme);
});
@@ -0,0 +1,224 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#B9B8CE';
var backgroundColor = '#100C2A';
var axisCommon = function () {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
splitLine: {
lineStyle: {
color: '#484753'
}
},
splitArea: {
areaStyle: {
color: ['rgba(255,255,255,0.02)', 'rgba(255,255,255,0.05)']
}
},
minorSplitLine: {
lineStyle: {
color: '#20203B'
}
}
};
};
var colorPalette = [
'#4992ff',
'#7cffb2',
'#fddd60',
'#ff6e76',
'#58d9f9',
'#05c091',
'#ff8a45',
'#8d48e3',
'#dd79ff'
];
var theme = {
darkMode: true,
color: colorPalette,
backgroundColor: backgroundColor,
axisPointer: {
lineStyle: {
color: '#817f91'
},
crossStyle: {
color: '#817f91'
},
label: {
// TODO Contrast of label backgorundColor
color: '#fff'
}
},
legend: {
textStyle: {
color: contrastColor
}
},
textStyle: {
color: contrastColor
},
title: {
textStyle: {
color: '#EEF1FA'
},
subtextStyle: {
color: '#B9B8CE'
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
dataZoom: {
borderColor: '#71708A',
textStyle: {
color: contrastColor
},
brushStyle: {
color: 'rgba(135,163,206,0.3)'
},
handleStyle: {
color: '#353450',
borderColor: '#C5CBE3'
},
moveHandleStyle: {
color: '#B0B6C3',
opacity: 0.3
},
fillerColor: 'rgba(135,163,206,0.2)',
emphasis: {
handleStyle: {
borderColor: '#91B7F2',
color: '#4D587D'
},
moveHandleStyle: {
color: '#636D9A',
opacity: 0.7
}
},
dataBackground: {
lineStyle: {
color: '#71708A',
width: 1
},
areaStyle: {
color: '#71708A'
}
},
selectedDataBackground: {
lineStyle: {
color: '#87A3CE'
},
areaStyle: {
color: '#87A3CE'
}
}
},
visualMap: {
textStyle: {
color: contrastColor
}
},
timeline: {
lineStyle: {
color: contrastColor
},
label: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
}
},
calendar: {
itemStyle: {
color: backgroundColor
},
dayLabel: {
color: contrastColor
},
monthLabel: {
color: contrastColor
},
yearLabel: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
title: {
color: contrastColor
}
},
candlestick: {
itemStyle: {
color: '#FD1050',
color0: '#0CF49B',
borderColor: '#FD1050',
borderColor0: '#0CF49B'
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark', theme);
});
@@ -0,0 +1,178 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#59535e',
'#e7dcef',
'#f1baf3',
'#5d4970',
'#372049',
'#c0b2cd',
'#ffccff',
'#f2f0f5'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#59535e'
}
},
visualMap: {
color: ['#59535e', '#e7dcef']
},
toolbox: {
color: ['#59535e', '#59535e', '#59535e', '#59535e']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#59535e',
type: 'dashed'
},
crossStyle: {
color: '#59535e'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#59535e' // Handle color
},
timeline: {
lineStyle: {
color: '#59535e'
},
controlStyle: {
color: '#59535e',
borderColor: '#59535e'
}
},
candlestick: {
itemStyle: {
color: '#e7dcef',
color0: '#f1baf3'
},
lineStyle: {
width: 1,
color: '#372049',
color0: '#5d4970'
},
areaStyle: {
color: '#59535e',
color0: '#e7dcef'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#59535e',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#e7dcef'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#f1baf3'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#59535e'
},
linkStyle: {
color: '#59535e'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#e7dcef'],
[0.8, '#59535e'],
[1, '#372049']
],
width: 8
}
}
}
};
echarts.registerTheme('eduardo', theme);
});
@@ -0,0 +1,163 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#313b23',
'#494f2b',
'#606233',
'#d6b77b',
'#0e0e0e',
'#076278',
'#808080',
'#e7d5b1'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#313b23'
}
},
visualMap: {
color: ['#313b23', '#494f2b']
},
toolbox: {
color: ['#313b23', '#313b23', '#313b23', '#313b23']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#313b23',
type: 'dashed'
},
crossStyle: {
color: '#313b23'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#313b23' // Handle color
},
timeline: {
lineStyle: {
color: '#313b23'
},
controlStyle: {
color: '#313b23',
borderColor: '#313b23'
}
},
candlestick: {
itemStyle: {
color: '#494f2b',
color0: '#606233'
},
lineStyle: {
width: 1,
color: '#0e0e0e',
color0: '#d6b77b'
},
areaStyle: {
color: '#494f2b',
color0: '#d6b77b'
}
},
map: {
itemStyle: {
color: '#606233'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#494f2b'
},
linkStyle: {
color: '#313b23'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#494f2b'],
[0.8, '#313b23'],
[1, '0e0e0e']
],
width: 8
}
}
}
};
echarts.registerTheme('forest', theme);
});
@@ -0,0 +1,163 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#00a8c6',
'#40c0cb',
'#f0dec2',
'#aee239',
'#8fbe00',
'#33e0ff',
'#b3f4ff',
'#e6ff99'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#00a8c6'
}
},
visualMap: {
color: ['#00a8c6', '#a2d4e6']
},
toolbox: {
color: ['#00a8c6', '#00a8c6', '#00a8c6', '#00a8c6']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#00a8c6',
type: 'dashed'
},
crossStyle: {
color: '#00a8c6'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#00a8c6' // Handle color
},
timeline: {
lineStyle: {
color: '#00a8c6'
},
controlStyle: {
color: '#00a8c6',
borderColor: '#00a8c6'
}
},
candlestick: {
itemStyle: {
color: '#40c0cb',
color0: '#f0dec2'
},
lineStyle: {
width: 1,
color: '#8fbe00',
color0: '#aee239'
},
areaStyle: {
color: '#00a8c6',
color0: '#aee239'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#f0dec2'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#f0dec2'
},
linkStyle: {
color: '#00a8c6'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#40c0cb'],
[0.8, '#00a8c6'],
[1, '#8fbe00']
],
width: 8
}
}
}
};
echarts.registerTheme('fresh-cut', theme);
});
@@ -0,0 +1,178 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#ffcb6a',
'#ffa850',
'#ffe2c4',
'#e5834e',
'#ffb081',
'#f7826e',
'#faac9e',
'#fcd5cf'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#ffcb6a'
}
},
visualMap: {
color: ['#ffcb6a', '#ffa850']
},
toolbox: {
color: ['#ffcb6a', '#ffcb6a', '#ffcb6a', '#ffcb6a']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#ffcb6a',
type: 'dashed'
},
crossStyle: {
color: '#ffcb6a'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#ffcb6a' // Handle color
},
timeline: {
lineStyle: {
color: '#ffcb6a'
},
controlStyle: {
color: '#ffcb6a',
borderColor: '#ffcb6a'
}
},
candlestick: {
itemStyle: {
color: '#ffa850',
color0: '#ffe2c4'
},
lineStyle: {
width: 1,
color: '#ffb081',
color0: '#e5834e'
},
areaStyle: {
color: '#e5834e',
color0: '#fcd5cf'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#fcd5cf',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#e5834e'
}
},
map: {
itemStyle: {
color: '#ffe2c4'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#f2385a'
},
linkStyle: {
color: '#ffcb6a'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#ffa850'],
[0.8, '#ffcb6a'],
[1, '#ffb081']
],
width: 8
}
}
}
};
echarts.registerTheme('fruit', theme);
});
@@ -0,0 +1,220 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#757575',
'#c7c7c7',
'#dadada',
'#8b8b8b',
'#b5b5b5',
'#e9e9e9'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#757575'
}
},
dataRange: {
color: ['#636363', '#dcdcdc']
},
toolbox: {
color: ['#757575', '#757575', '#757575', '#757575']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#757575',
type: 'dashed'
},
crossStyle: {
color: '#757575'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(117,117,117,0.2)', // Fill the color
handleColor: '#757575' // Handle color
},
grid: {
borderWidth: 0
},
categoryAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#757575'
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
valueAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#757575'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.1)', 'rgba(200,200,200,0.1)']
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
timeline: {
lineStyle: {
color: '#757575'
},
controlStyle: {
color: '#757575',
borderColor: '#757575'
}
},
candlestick: {
itemStyle: {
color: '#8b8b8b',
color0: '#dadada'
},
lineStyle: {
width: 1,
color: '#757575',
color0: '#c7c7c7'
},
areaStyle: {
color: '#757575',
color0: '#e9e9e9'
}
},
map: {
itemStyle: {
color: '#c7c7c7'
},
areaStyle: {
color: 'ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#e9e9e9'
},
linkStyle: {
color: '#757575'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#e9e9e9',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#757575'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#b5b5b5'],
[0.8, '#757575'],
[1, '#5c5c5c']
],
width: 8
}
}
}
};
echarts.registerTheme('gray', theme);
});
@@ -0,0 +1,222 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#408829',
'#68a54a',
'#a9cba2',
'#86b379',
'#397b29',
'#8abb6f',
'#759c6a',
'#bfd3b7'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#408829'
}
},
visualMap: {
color: ['408829', '#a9cba2']
},
toolbox: {
color: ['#408829', '#408829', '#408829', '#408829']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#408829',
type: 'dashed'
},
crossStyle: {
color: '#408829'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(64,136,41,0.2)', // Fill the color
handleColor: '#408829' // Handle color
},
grid: {
borderWidth: 0
},
categoryAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#408829'
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
valueAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#408829'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.1)', 'rgba(200,200,200,0.1)']
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
timeline: {
lineStyle: {
color: '#408829'
},
controlStyle: {
color: '#408829',
borderColor: '#408829'
}
},
candlestick: {
itemStyle: {
color: '#68a54a',
color0: '#a9cba2'
},
lineStyle: {
width: 1,
color: '#408829',
color0: '#86b379'
},
areaStyle: {
color: '#408829',
color0: '#bfd3b7'
}
},
graph: {
itemStyle: {
color: '#bfd3b7'
},
linkStyle: {
color: '#408829'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#bfd3b7',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#408829'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#408829'
},
label: {
color: '#000'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#86b379'],
[0.8, '#68a54a'],
[1, '#408829']
],
width: 8
}
}
}
};
echarts.registerTheme('green', theme);
});
@@ -0,0 +1,263 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#44B7D3',
'#E42B6D',
'#F4E24E',
'#FE9616',
'#8AED35',
'#ff69b4',
'#ba55d3',
'#cd5c5c',
'#ffa500',
'#40e0d0',
'#E95569',
'#ff6347',
'#7b68ee',
'#00fa9a',
'#ffd700',
'#6699FF',
'#ff6666',
'#3cb371',
'#b8860b',
'#30e0e0'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#8A826D'
}
},
dataRange: {
x: 'right',
y: 'center',
itemWidth: 5,
itemHeight: 25,
color: ['#E42B6D', '#F9AD96'],
text: ['High', 'Low'], // Text, default is numeric text
textStyle: {
color: '#8A826D' // Range text color
}
},
toolbox: {
color: ['#E95569', '#E95569', '#E95569', '#E95569'],
effectiveColor: '#ff4500',
itemGap: 8
},
tooltip: {
backgroundColor: 'rgba(138,130,109,0.7)', // Prompt background color, default is black with a transparency of 0.7
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#6B6455',
type: 'dashed'
},
crossStyle: {
color: '#A6A299'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: 'rgba(130,197,209,0.6)', // Data background color
fillerColor: 'rgba(233,84,105,0.1)', // Fill the color
handleColor: 'rgba(107,99,84,0.8)' // Handle color
},
grid: {
borderWidth: 0
},
categoryAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#6B6455'
}
},
splitLine: {
// separate line
show: false
}
},
valueAxis: {
axisLine: {
// Coordinate axis
show: true
},
splitArea: {
show: false
},
splitLine: {
// separate line
lineStyle: {
// Property 'lineStyle' controls line styles
color: ['#FFF'],
type: 'dashed'
}
}
},
polar: {
axisLine: {
// Coordinate axis
lineStyle: {
// // Property 'lineStyle' controls line styles
color: '#ddd'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.2)', 'rgba(200,200,200,0.2)']
}
},
splitLine: {
lineStyle: {
color: '#ddd'
}
}
},
timeline: {
lineStyle: {
color: '#6B6455'
},
controlStyle: {
color: '#6B6455',
borderColor: '#6B6455'
}
},
line: {
smooth: true,
symbol: 'emptyCircle', // Inflection point graphic type
symbolSize: 3 // Inflection point graphic size
},
candlestick: {
itemStyle: {
color: '#e42B6d',
color0: '#44B7d3'
},
lineStyle: {
width: 1,
color: '#e42B6d',
color0: '#44B7d3'
},
areaStyle: {
color: '#fe994e',
color0: '#e42B6d'
}
},
map: {
itemStyle: {
color: '#6b6455'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#e42B6d'
}
},
graph: {
itemStyle: {
color: '#e42B6d'
},
linkStyle: {
color: '#6b6455'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#e42B6d',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#6b6455'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#44B7D3'],
[0.8, '#6B6455'],
[1, '#E42B6D']
],
width: 8
}
}
}
};
echarts.registerTheme('helianthus', theme);
});
@@ -0,0 +1,236 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#C1232B',
'#27727B',
'#FCCE10',
'#E87C25',
'#B5C334',
'#FE8463',
'#9BCA63',
'#FAD860',
'#F3A43B',
'#60C0DD',
'#D7504B',
'#C6E579',
'#F4E001',
'#F0805A',
'#26C0C0'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#27727B'
}
},
visualMap: {
color: ['#C1232B', '#FCCE10']
},
toolbox: {
iconStyle: {
borderColor: colorPalette[0]
}
},
tooltip: {
backgroundColor: 'rgba(50,50,50,0.5)',
axisPointer: {
type: 'line',
lineStyle: {
color: '#27727B',
type: 'dashed'
},
crossStyle: {
color: '#27727B'
},
shadowStyle: {
color: 'rgba(200,200,200,0.3)'
}
}
},
dataZoom: {
dataBackgroundColor: 'rgba(181,195,52,0.3)',
fillerColor: 'rgba(181,195,52,0.2)',
handleColor: '#27727B'
},
categoryAxis: {
axisLine: {
lineStyle: {
color: '#27727B'
}
},
splitLine: {
show: false
}
},
valueAxis: {
axisLine: {
show: false
},
splitArea: {
show: false
},
splitLine: {
lineStyle: {
color: ['#ccc'],
type: 'dashed'
}
}
},
timeline: {
itemStyle: {
color: '#27727B'
},
lineStyle: {
color: '#27727B'
},
controlStyle: {
color: '#27727B',
borderColor: '#27727B'
},
symbol: 'emptyCircle',
symbolSize: 3
},
line: {
itemStyle: {
borderWidth: 2,
borderColor: '#fff',
lineStyle: {
width: 3
}
},
emphasis: {
itemStyle: {
borderWidth: 0
}
},
symbol: 'circle',
symbolSize: 3.5
},
candlestick: {
itemStyle: {
color: '#c1232b',
color0: '#b5c334'
},
lineStyle: {
width: 1,
color: '#c1232b',
color0: '#b5c334'
},
areaStyle: {
color: '#c1232b',
color0: '#27727b'
}
},
graph: {
itemStyle: {
color: '#c1232b'
},
linkStyle: {
color: '#b5c334'
}
},
map: {
itemStyle: {
color: '#f2385a',
areaColor: '#ddd',
borderColor: '#eee'
},
areaStyle: {
color: '#fe994e'
},
label: {
color: '#c1232b'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#B5C334'],
[0.8, '#27727B'],
[1, '#C1232B']
]
}
},
axisTick: {
splitNumber: 2,
length: 5,
lineStyle: {
color: '#fff'
}
},
axisLabel: {
color: '#fff'
},
splitLine: {
length: '5%',
lineStyle: {
color: '#fff'
}
},
title: {
offsetCenter: [0, -20]
}
}
};
echarts.registerTheme('infographic', theme);
});
@@ -0,0 +1,163 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#cc0000',
'#002266',
'#ff9900',
'#006600',
'#8a150f',
'#076278',
'#808080',
'#f07b75'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#cc0000'
}
},
visualMap: {
color: ['#cc0000', '#002266']
},
toolbox: {
color: ['#cc0000', '#cc0000', '#cc0000', '#cc0000']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#cc0000',
type: 'dashed'
},
crossStyle: {
color: '#cc0000'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#cc0000' // Handle color
},
timeline: {
lineStyle: {
color: '#cc0000'
},
controlStyle: {
color: '#cc0000',
borderColor: '#cc0000'
}
},
candlestick: {
itemStyle: {
color: '#002266',
color0: '#ff9900'
},
lineStyle: {
width: 1,
color: '#8a150f',
color0: '#006600'
},
areaStyle: {
color: '#cc0000',
color0: '#ff9900'
}
},
map: {
itemStyle: {
color: '#ff9900'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#ff9900'
},
linkStyle: {
color: '#cc0000'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#002266'],
[0.8, '#cc0000'],
[1, '8a150f']
],
width: 8
}
}
}
};
echarts.registerTheme('inspired', theme);
});
@@ -0,0 +1,163 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#e9e0d1',
'#91a398',
'#33605a',
'#070001',
'#68462b',
'#58a79c',
'#abd3ce',
'#eef6f5'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#e9e0d1'
}
},
visualMap: {
color: ['#e9e0d1', '#91a398']
},
toolbox: {
color: ['#e9e0d1', '#e9e0d1', '#e9e0d1', '#e9e0d1']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#e9e0d1',
type: 'dashed'
},
crossStyle: {
color: '#e9e0d1'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#e9e0d1' // Handle color
},
timeline: {
lineStyle: {
color: '#e9e0d1'
},
controlStyle: {
color: '#e9e0d1',
borderColor: '#e9e0d1'
}
},
candlestick: {
itemStyle: {
color: '#91a398',
color0: '#33605a'
},
lineStyle: {
width: 1,
color: '#68462b',
color0: '#070001'
},
areaStyle: {
color: '#91a398',
color0: '#abd3ce'
}
},
map: {
itemStyle: {
color: '#c12e34'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#33605a'
},
linkStyle: {
color: '#e9e0d1'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#91a398'],
[0.8, '#e9e0d1'],
[1, '#68462b']
],
width: 8
}
}
}
};
echarts.registerTheme('jazz', theme);
});
@@ -0,0 +1,163 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#02151a',
'#043a47',
'#087891',
'#c8c8c8',
'#b31d14',
'#0b9cc1',
'#f2f2f2',
'#f07b75'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#02151a'
}
},
visualMap: {
color: ['#02151a', '#a2d4e6']
},
toolbox: {
color: ['#02151a', '#02151a', '#02151a', '#02151a']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#02151a',
type: 'dashed'
},
crossStyle: {
color: '#02151a'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#02151a' // Handle color
},
timeline: {
lineStyle: {
color: '#02151a'
},
controlStyle: {
color: '#02151a',
borderColor: '#02151a'
}
},
candlestick: {
itemStyle: {
color: '#043a47',
color0: '#087891'
},
lineStyle: {
width: 1,
color: '#b31d14',
color0: '#c8c8c8'
},
areaStyle: {
color: '#087891',
color0: '#c8c8c8'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#087891'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#c12e34'
},
linkStyle: {
color: '#02151a'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#043a47'],
[0.8, '#02151a'],
[1, '#b31d14']
],
width: 8
}
}
}
};
echarts.registerTheme('london', theme);
});
@@ -0,0 +1,242 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#2ec7c9',
'#b6a2de',
'#5ab1ef',
'#ffb980',
'#d87a80',
'#8d98b3',
'#e5cf0d',
'#97b552',
'#95706d',
'#dc69aa',
'#07a2a4',
'#9a7fd1',
'#588dd5',
'#f5994e',
'#c05050',
'#59678c',
'#c9ab00',
'#7eb00a',
'#6f5553',
'#c14089'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#008acd'
}
},
visualMap: {
itemWidth: 15,
color: ['#5ab1ef', '#e0ffff']
},
toolbox: {
iconStyle: {
borderColor: colorPalette[0]
}
},
tooltip: {
borderWidth: 0,
backgroundColor: 'rgba(50,50,50,0.5)',
textStyle: {
color: '#FFF'
},
axisPointer: {
type: 'line',
lineStyle: {
color: '#008acd'
},
crossStyle: {
color: '#008acd'
},
shadowStyle: {
color: 'rgba(200,200,200,0.2)'
}
}
},
dataZoom: {
dataBackgroundColor: '#efefff',
fillerColor: 'rgba(182,162,222,0.2)',
handleColor: '#008acd'
},
grid: {
borderColor: '#eee'
},
categoryAxis: {
axisLine: {
lineStyle: {
color: '#008acd'
}
},
splitLine: {
lineStyle: {
color: ['#eee']
}
}
},
valueAxis: {
axisLine: {
lineStyle: {
color: '#008acd'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.1)', 'rgba(200,200,200,0.1)']
}
},
splitLine: {
lineStyle: {
color: ['#eee']
}
}
},
timeline: {
lineStyle: {
color: '#008acd'
},
controlStyle: {
color: '#008acd',
borderColor: '#008acd'
},
symbol: 'emptyCircle',
symbolSize: 3
},
line: {
smooth: true,
symbol: 'emptyCircle',
symbolSize: 3
},
candlestick: {
itemStyle: {
color: '#d87a80',
color0: '#2ec7c9'
},
lineStyle: {
width: 1,
color: '#d87a80',
color0: '#2ec7c9'
},
areaStyle: {
color: '#2ec7c9',
color0: '#b6a2de'
}
},
scatter: {
symbol: 'circle',
symbolSize: 4
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#fe994e'
},
label: {
color: '#d87a80'
}
},
graph: {
itemStyle: {
color: '#d87a80'
},
linkStyle: {
color: '#2ec7c9'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#2ec7c9'],
[0.8, '#5ab1ef'],
[1, '#d87a80']
],
width: 10
}
},
axisTick: {
splitNumber: 10,
length: 15,
lineStyle: {
color: 'auto'
}
},
splitLine: {
length: 22,
lineStyle: {
color: 'auto'
}
},
pointer: {
width: 5
}
}
};
echarts.registerTheme('macarons', theme);
});
@@ -0,0 +1,246 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#ed9678',
'#e7dac9',
'#cb8e85',
'#f3f39d',
'#c8e49c',
'#f16d7a',
'#f3d999',
'#d3758f',
'#dcc392',
'#2e4783',
'#82b6e9',
'#ff6347',
'#a092f1',
'#0a915d',
'#eaf889',
'#6699FF',
'#ff6666',
'#3cb371',
'#d5b158',
'#38b6b6'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#cb8e85'
}
},
dataRange: {
color: ['#cb8e85', '#e7dac9'], //颜色
//text:['高','低'], // 文本,默认为数值文本
textStyle: {
color: '#333' // 值域文字颜色
}
},
bar: {
barMinHeight: 0, // 最小高度改为0
// barWidth: null, // 默认自适应
barGap: '30%', // 柱间距离,默认为柱形宽度的30%,可设固定值
barCategoryGap: '20%', // 类目间柱形距离,默认为类目间距的20%,可设固定值
label: {
show: false
// position: 默认自适应,水平布局为'top',垂直布局为'right',可选为
// 'inside'|'left'|'right'|'top'|'bottom'
// textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE
},
itemStyle: {
// color: '各异',
barBorderColor: '#fff', // 柱条边线
barBorderRadius: 0, // 柱条边线圆角,单位px,默认为0
barBorderWidth: 1 // 柱条边线线宽,单位px,默认为1
},
emphasis: {
itemStyle: {
// color: '各异',
barBorderColor: 'rgba(0,0,0,0)', // 柱条边线
barBorderRadius: 0, // 柱条边线圆角,单位px,默认为0
barBorderWidth: 1, // 柱条边线线宽,单位px,默认为1
},
label: {
show: false
// position: 默认自适应,水平布局为'top',垂直布局为'right',可选为
// 'inside'|'left'|'right'|'top'|'bottom'
// textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE
}
}
},
line: {
label: {
show: false
// position: 默认自适应,水平布局为'top',垂直布局为'right',可选为
// 'inside'|'left'|'right'|'top'|'bottom'
// textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE
},
itemStyle: {
// color: 各异,
},
emphasis: {
// color: 各异,
label: {
show: false
// position: 默认自适应,水平布局为'top',垂直布局为'right',可选为
// 'inside'|'left'|'right'|'top'|'bottom'
// textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE
}
},
lineStyle: {
width: 2,
type: 'solid',
shadowColor: 'rgba(0,0,0,0)', //默认透明
shadowBlur: 5,
shadowOffsetX: 3,
shadowOffsetY: 3
},
//smooth : false,
//symbol: null, // 拐点图形类型
symbolSize: 2, // 拐点图形大小
//symbolRotate : null, // 拐点图形旋转控制
showAllSymbol: false // 标志图形默认只有主轴显示(随主轴标签间隔隐藏策略)
},
candlestick: {
itemStyle: {
color: '#fe9778',
color0: '#e7dac9'
},
lineStyle: {
width: 1,
color: '#f78766',
color0: '#f1ccb8'
},
areaStyle: {
color: '#e7dac9',
color0: '#c8e49c'
}
},
// 饼图默认参数
pie: {
center: ['50%', '50%'], // 默认全局居中
radius: [0, '75%'],
clockWise: false, // 默认逆时针
startAngle: 90,
minAngle: 0, // 最小角度改为0
selectedOffset: 10, // 选中是扇区偏移量
label: {
show: true,
position: 'outer',
color: '#1b1b1b',
lineStyle: { color: '#1b1b1b' }
// textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE
},
itemStyle: {
// color: 各异,
borderColor: '#fff',
borderWidth: 1
},
labelLine: {
show: true,
length: 20,
lineStyle: {
// color: 各异,
width: 1,
type: 'solid'
}
}
},
map: {
itemStyle: {
color: '#ddd',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
color: '#f3f39d'
},
label: {
show: false,
color: 'rgba(139,69,19,1)'
},
showLegendSymbol: true
},
graph: {
itemStyle: {
color: '#d87a80'
},
linkStyle: {
strokeColor: '#a17e6e'
},
nodeStyle: {
brushType: 'both',
strokeColor: '#a17e6e'
},
label: {
show: false
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#ed9678'],
[0.8, '#e7dac9'],
[1, '#cb8e85']
],
width: 8
}
}
}
};
echarts.registerTheme('macarons2', theme);
});
@@ -0,0 +1,155 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#8aedd5',
'#93bc9e',
'#cef1db',
'#7fe579',
'#a6d7c2',
'#bef0bb',
'#99e2vb',
'#94f8a8',
'#7de5b8',
'#4dfb70'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#8aedd5'
}
},
toolbox: {
color: ['#8aedd5', '#8aedd5', '#8aedd5', '#8aedd5']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#8aedd5',
type: 'dashed'
},
crossStyle: {
color: '#8aedd5'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(64,136,41,0.2)', // Fill the color
handleColor: '#408829' // Handle color
},
dataRange: {
color: ['#93bc92', '#bef0bb']
},
candlestick: {
itemStyle: {
color: '#8aedd5',
color0: '#7fe579'
},
lineStyle: {
width: 1,
color: '#8aedd5',
color0: '#7fe579'
},
areaStyle: {
color: '#8aedd5',
color0: '#93bc9e'
}
},
graph: {
itemStyle: {
color: '#8aedd5'
},
linkStyle: {
color: '#93bc9e'
}
},
map: {
itemStyle: {
color: '#8aedd5'
},
areaStyle: {
color: '#93bc9e'
},
label: {
color: '#cef1db'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#93bc9e'],
[0.8, '#8aedd5'],
[1, '#a6d7c2']
],
width: 8
}
}
}
};
echarts.registerTheme('mint', theme);
});
@@ -0,0 +1,163 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#8b1a2d',
'#a7314b',
'#e6004c',
'#ff8066',
'#8e5c4e',
'#ff1a66',
'#d6c582',
'#f0d4af'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#8b1a2d'
}
},
visualMap: {
color: ['#8b1a2d', '#a7314b']
},
toolbox: {
color: ['#8b1a2d', '#8b1a2d', '#8b1a2d', '#8b1a2d']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#8b1a2d',
type: 'dashed'
},
crossStyle: {
color: '#8b1a2d'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#8b1a2d' // Handle color
},
timeline: {
lineStyle: {
color: '#8b1a2d'
},
controlStyle: {
color: '#8b1a2d',
borderColor: '#8b1a2d'
}
},
candlestick: {
itemStyle: {
color: '#a7314b',
color0: '#d6c582'
},
lineStyle: {
width: 1,
color: '#8e5c4e',
color0: '#f0d4af'
},
areaStyle: {
color: '#8b1a2d',
color0: '#ff8066'
}
},
map: {
itemStyle: {
color: '#8b1a2d'
},
areaStyle: {
color: '#ff8066'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#ff8066'
},
linkStyle: {
color: '#8b1a2d'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#a7314b'],
[0.8, '#8b1a2d'],
[1, '#8e5c4e']
],
width: 8
}
}
}
};
echarts.registerTheme('red-velvet', theme);
});
@@ -0,0 +1,225 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#d8361b',
'#f16b4c',
'#f7b4a9',
'#d26666',
'#99311c',
'#c42703',
'#d07e75'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#d8361b'
}
},
visualMap: {
color: ['#d8361b', '#ffd2d2']
},
dataRange: {
color: ['#bd0707', '#ffd2d2']
},
toolbox: {
color: ['#d8361b', '#d8361b', '#d8361b', '#d8361b']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#d8361b',
type: 'dashed'
},
crossStyle: {
color: '#d8361b'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(216,54,27,0.2)', // Fill the color
handleColor: '#d8361b' // Handle color
},
grid: {
borderWidth: 0
},
categoryAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#d8361b'
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
valueAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#d8361b'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.1)', 'rgba(200,200,200,0.1)']
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
timeline: {
lineStyle: {
color: '#d8361b'
},
controlStyle: {
color: '#d8361b',
borderColor: '#d8361b'
}
},
candlestick: {
itemStyle: {
color: '#f16b4c',
color0: '#f7b4a9'
},
lineStyle: {
width: 1,
color: '#d8361b',
color0: '#d26666'
},
areaStyle: {
color: '#d8361b',
color0: '#d07e75'
}
},
graph: {
itemStyle: {
color: '#d07e75'
},
linkStyle: {
color: '#d8361b'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#d07e75',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#d8361b'
}
},
map: {
itemStyle: {
color: '#d8361b'
},
areaStyle: {
color: '#d07e75'
},
label: {
color: '#c12e34'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#f16b4c'],
[0.8, '#d8361b'],
[1, '#99311c']
],
width: 8
}
}
}
};
echarts.registerTheme('red', theme);
});
@@ -0,0 +1,119 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#E01F54',
'#001852',
'#f5e8c8',
'#b8d2c7',
'#c6b38e',
'#a4d8c2',
'#f3d999',
'#d3758f',
'#dcc392',
'#2e4783',
'#82b6e9',
'#ff6347',
'#a092f1',
'#0a915d',
'#eaf889',
'#6699FF',
'#ff6666',
'#3cb371',
'#d5b158',
'#38b6b6'
];
var theme = {
color: colorPalette,
visualMap: {
color: ['#e01f54', '#e7dbc3'],
textStyle: {
color: '#333'
}
},
candlestick: {
itemStyle: {
color: '#e01f54',
color0: '#001852'
},
lineStyle: {
width: 1,
color: '#f5e8c8',
color0: '#b8d2c7'
},
areaStyle: {
color: '#a4d8c2',
color0: '#f3d999'
}
},
graph: {
itemStyle: {
color: '#a4d8c2'
},
linkStyle: {
color: '#f3d999'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#E01F54'],
[0.8, '#b8d2c7'],
[1, '#001852']
],
width: 8
}
}
}
};
echarts.registerTheme('roma', theme);
});
@@ -0,0 +1,163 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#3f7ea6',
'#993366',
'#408000',
'#8c6f56',
'#a65149',
'#731f17',
'#adc2eb',
'#d9c3b0'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#3f7ea6'
}
},
visualMap: {
color: ['#3f7ea6', '#993366']
},
toolbox: {
color: ['#3f7ea6', '#3f7ea6', '#3f7ea6', '#3f7ea6']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#3f7ea6',
type: 'dashed'
},
crossStyle: {
color: '#3f7ea6'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#3f7ea6' // Handle color
},
timeline: {
lineStyle: {
color: '#3f7ea6'
},
controlStyle: {
color: '#3f7ea6',
borderColor: '#3f7ea6'
}
},
candlestick: {
itemStyle: {
color: '#d9c3b0',
color0: '#8c6f56'
},
lineStyle: {
width: 1,
color: '#731f17',
color0: '#a65149'
},
areaStyle: {
color: '#3f7ea6',
color0: '#993366'
}
},
map: {
itemStyle: {
color: '#d9c3b0'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#993366'
},
linkStyle: {
color: '#3f7ea6'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#d9c3b0'],
[0.8, '#3f7ea6'],
[1, '#731f17']
],
width: 8
}
}
}
};
echarts.registerTheme('royal', theme);
});
@@ -0,0 +1,140 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#e52c3c',
'#f7b1ab',
'#fa506c',
'#f59288',
'#f8c4d8',
'#e54f5c',
'#f06d5c',
'#e54f80',
'#f29c9f',
'#eeb5b7'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#e52c3c'
}
},
visualMap: {
color: ['#e52c3c', '#f7b1ab']
},
dataRange: {
color: ['#e52c3c', '#f7b1ab']
},
candlestick: {
itemStyle: {
color: '#e52c3c',
color0: '#f59288'
},
lineStyle: {
width: 1,
color: '#e52c3c',
color0: '#f59288'
},
areaStyle: {
color: '#fa506c',
color0: '#f8c4d8'
}
},
map: {
itemStyle: {
color: '#e52c3c',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
color: '#ccc'
},
label: {
color: 'rgba(139,69,19,1)',
show: false
}
},
graph: {
itemStyle: {
color: '#f2385a'
},
nodeStyle: {
brushType: 'both',
strokeColor: '#e54f5c'
},
linkStyle: {
color: '#f2385a',
strokeColor: '#e54f5c'
},
label: {
color: '#f2385a',
show: false
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#e52c3c'],
[0.8, '#f7b1ab'],
[1, '#fa506c']
],
width: 8
}
}
}
};
echarts.registerTheme('sakura', theme);
});
@@ -0,0 +1,175 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#c12e34',
'#e6b600',
'#0098d9',
'#2b821d',
'#005eaa',
'#339ca8',
'#cda819',
'#32a487'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal'
}
},
visualMap: {
color: ['#1790cf', '#a2d4e6']
},
toolbox: {
iconStyle: {
borderColor: '#06467c'
}
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.6)'
},
dataZoom: {
dataBackgroundColor: '#dedede',
fillerColor: 'rgba(154,217,247,0.2)',
handleColor: '#005eaa'
},
timeline: {
lineStyle: {
color: '#005eaa'
},
controlStyle: {
color: '#005eaa',
borderColor: '#005eaa'
}
},
candlestick: {
itemStyle: {
color: '#c12e34',
color0: '#2b821d'
},
lineStyle: {
width: 1,
color: '#c12e34',
color0: '#2b821d'
},
areaStyle: {
color: '#e6b600',
color0: '#005eaa'
}
},
graph: {
itemStyle: {
color: '#e6b600'
},
linkStyle: {
color: '#005eaa'
}
},
map: {
itemStyle: {
color: '#f2385a',
borderColor: '#eee',
areaColor: '#ddd'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
gauge: {
axisLine: {
show: true,
lineStyle: {
color: [
[0.2, '#2b821d'],
[0.8, '#005eaa'],
[1, '#c12e34']
],
width: 5
}
},
axisTick: {
splitNumber: 10,
length: 8,
lineStyle: {
color: 'auto'
}
},
axisLabel: {
color: 'auto'
},
splitLine: {
length: 12,
lineStyle: {
color: 'auto'
}
},
pointer: {
length: '90%',
width: 3,
color: 'auto'
},
title: {
color: '#333'
},
detail: {
color: 'auto'
}
}
};
echarts.registerTheme('shine', theme);
});
@@ -0,0 +1,184 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#4d4d4d',
'#3a5897',
'#007bb6',
'#7094db',
'#0080ff',
'#b3b3ff',
'#00bdec',
'#33ccff',
'#ccddff',
'#eeeeee'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#00aecd'
}
},
visualMap: {
color: ['#00aecd', '#a2d4e6']
},
toolbox: {
color: ['#00aecd', '#00aecd', '#00aecd', '#00aecd']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#00aecd',
type: 'dashed'
},
crossStyle: {
color: '#00aecd'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#00aecd' // Handle color
},
timeline: {
lineStyle: {
color: '#00aecd'
},
controlStyle: {
color: '#00aecd',
},
emphasis: {
controlStyle: {
color: '#00aecd'
}
}
},
candlestick: {
itemStyle: {
color: '#ddd',
color0: '#eee'
},
lineStyle: {
width: 1,
color: '#33ccff',
color0: '#1bb4cf'
},
areaStyle: {
color: '#7094db',
color0: '#33ccff'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#7094db',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#33ccff'
}
},
graph: {
itemStyle: {
color: '#7094db'
},
linkStyle: {
color: '#33ccff'
}
},
map: {
itemStyle: {
color: '#7094db'
},
areaStyle: {
color: '#33ccff'
},
label: {
color: '#ddd'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#dddddd'],
[0.8, '#00aecd'],
[1, '#33ccff']
],
width: 8
}
}
}
};
echarts.registerTheme('tech-blue', theme);
});
@@ -0,0 +1,88 @@
/*
* 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.
*/
export default {
title: {
text: 'Area Chart',
left: 'center',
top: '3%',
textStyle: {
fontWeight: 'normal'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '12%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday','Sunday']
},
yAxis: {
type: 'value',
splitNumber: 3
},
dataZoom: {
},
series: [
{
name:'Email',
type:'line',
stack: '总量',
areaStyle: {},
data:[120, 132, 101, 134, 90, 230, 210]
},
{
name:'联盟广告',
type:'line',
stack: '总量',
areaStyle: {},
data:[220, 182, 191, 234, 290, 330, 310]
},
{
name:'视频广告',
type:'line',
stack: '总量',
areaStyle: {},
data:[150, 232, 201, 154, 190, 330, 410]
},
{
name:'直接访问',
type:'line',
stack: '总量',
areaStyle: {},
data:[320, 332, 301, 334, 390, 330, 320]
},
{
name:'搜索引擎',
type:'line',
stack: '总量',
label: {
show: true,
position: 'top'
},
areaStyle: {},
data:[820, 932, 901, 934, 1290, 1330, 1320]
}
]
};
@@ -0,0 +1,107 @@
/*
* 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.
*/
export default {
title: {
text: 'Bar Chart',
left: 'center',
top: '3%',
textStyle: {
fontWeight: 'normal'
}
},
toolbox: {
top: '3%',
feature: {
magicType: {
type: ['line', 'bar', 'stack', 'tiled']
},
restore: {},
dataZoom: {},
saveAsImage: {}
}
},
grid: {
left: '13%',
right: '5%',
bottom: '5%',
textStyle: {
fontWeight: 'normal'
}
},
xAxis: {
type: 'value'
},
yAxis: {
type: 'category',
data: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday','Sunday']
},
series: [
{
name:'直接访问',
type:'bar',
stack: '总量',
label: {
show: true,
position: 'insideRight'
},
data:[320, 302, 301, 334, 390, 330, 320]
},
{
name:'邮件营销',
type:'bar',
stack: '总量',
label: {
show: true,
position: 'insideRight'
},
data:[120, 132, 101, 134, 90, 230, 210]
},
{
name:'联盟广告',
type:'bar',
stack: '总量',
label: {
show: true,
position: 'insideRight'
},
data:[220, 182, 191, 234, 290, 330, 310]
},
{
name:'视频广告',
type:'bar',
stack: '总量',
label: {
show: true,
position: 'insideRight'
},
data:[150, 212, 201, 154, 190, 330, 410]
},
{
name:'搜索引擎',
type:'bar',
stack: '总量',
label: {
show: true,
position: 'insideRight'
},
data:[820, 832, 901, 934, 1290, 1330, 1320]
}
]
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,141 @@
/*
* 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.
*/
export default {
visualMap: {
show: true,
min: 0,
max: 1500,
right: 50,
top: 'middle',
text:['高','低']
// orient: 'horizontal'
},
selectedMode: 'single',
series : [
{
name: 'iphone3',
type: 'map',
map: 'china',
showLegendSymbol: true,
label: {
show: false,
},
emphasis: {
label: {
show: false,
}
},
data:[
{name: '北京',value: 500},
{name: '天津',value: 500},
{name: '上海',value: 500},
{name: '重庆',value: 500},
{name: '河北',value: 500},
{name: '河南',value: 500},
{name: '云南',value: 500},
{name: '辽宁',value: 500},
{name: '黑龙江',value: 500},
{name: '湖南',value: 500},
{name: '安徽',value: 500},
{name: '山东',value: 500},
{name: '新疆',value: 500},
{name: '江苏',value: 500},
{name: '浙江',value: 500},
{name: '江西',value: 500},
{name: '湖北',value: 500},
{name: '广西',value: 500},
{name: '甘肃',value: 500},
{name: '山西',value: 500},
{name: '内蒙古',value: 500},
{name: '陕西',value: 500},
{name: '吉林',value: 500},
{name: '福建',value: 500},
{name: '贵州',value: 500},
{name: '广东',value: 500},
{name: '青海',value: 500},
{name: '西藏',value: 500},
{name: '四川',value: 500},
{name: '宁夏',value: 500},
{name: '海南',value: 500},
{name: '台湾',value: 500},
{name: '香港',value: 500},
{name: '澳门',value: 500}
]
},
{
name: 'iphone4',
type: 'map',
mapType: 'china',
showLegendSymbol: true,
label: {
show: false,
},
emphasis: {
label: {
show: false
}
},
data:[
{name: '北京',value: 500},
{name: '天津',value: 500},
{name: '上海',value: 500},
{name: '重庆',value: 500},
{name: '河北',value: 500},
{name: '安徽',value: 500},
{name: '新疆',value: 500},
{name: '浙江',value: 500},
{name: '江西',value: 500},
{name: '山西',value: 500},
{name: '内蒙古',value: 500},
{name: '吉林',value: 500},
{name: '福建',value: 500},
{name: '广东',value: 500},
{name: '西藏',value: 500},
{name: '四川',value: 500},
{name: '宁夏',value: 500},
{name: '香港',value: 500},
{name: '澳门',value: 500}
]
},
{
name: 'iphone5',
type: 'map',
mapType: 'china',
showLegendSymbol: true,
label: {
show: false,
},
emphasis: {
label: {
show: false
}
},
data:[
{name: '北京',value: 500},
{name: '天津',value: 500},
{name: '上海',value: 500},
{name: '广东',value: 500},
{name: '台湾',value: 500},
{name: '香港',value: 500},
{name: '澳门',value: 500}
]
}
]
};
@@ -0,0 +1,87 @@
/*
* 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.
*/
export default {
legend: {
bottom: '5%',
data: ['rose1', 'rose2', 'rose3', 'rose4']
},
series : [
{
name:'半径模式',
type:'pie',
radius : [20, 80],
center : ['25%', 110],
label: {
show: false,
},
lableLine: {
show: false,
},
emphasis: {
label: {
show: true
},
lableLine: {
show: true
}
},
data:[
{value:10, name:'rose1'},
{value:5, name:'rose2'},
{value:15, name:'rose3'},
{value:25, name:'rose4'},
{value:20, name:'rose5'},
{value:35, name:'rose6'},
{value:30, name:'rose7'},
{value:40, name:'rose8'}
]
},
{
name:'面积模式',
type:'pie',
radius : [30, 80],
center : ['75%', 110],
roseType : 'area',
labelLine: {
length: 5
},
data:[
{value:10, name:'rose1'},
{value:5, name:'rose2'},
{value:15, name:'rose3'},
{value:25, name:'rose4'},
{value:20, name:'rose5'},
{value:35, name:'rose6'},
{value:30, name:'rose7'},
{value:40, name:'rose8'}
]
},
{
name:'仪表盘',
type:'gauge',
radius : 100,
center : ['50%', 280],
detail : {formatter:'{value}%'},
data:[
{value:50, name:'Gauge'}
]
}
]
};
@@ -0,0 +1,201 @@
/*
* 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.
*/
export default {
timeline: {
left: '2%',
right: '2%',
data: [
'2002-01-01','2003-01-01','2004-01-01',
{
value: '2005-01-01',
symbol: 'diamond',
symbolSize: 16
},
'2006-01-01', '2007-01-01','2008-01-01','2009-01-01','2010-01-01',
{
value: '2011-01-01',
symbol: 'diamond',
symbolSize: 18
}
],
label: {
formatter : function(s) {
return (new Date(s)).getFullYear();
}
}
},
options: [{
grid: {
left: '13%',
right: '5%',
bottom: '20%'
},
xAxis: {
type : 'value',
scale:true,
axisLabel : {
formatter: '{value} cm'
}
},
yAxis: {
type : 'value',
scale:true,
axisLabel : {
formatter: '{value} kg'
}
},
series : [
{
name:'女性',
type:'scatter',
data: [[161.2, 51.6], [167.5, 59.0], [159.5, 49.2], [157.0, 63.0], [155.8, 53.6],
[170.0, 59.0], [159.1, 47.6], [166.0, 69.8], [176.2, 66.8], [160.2, 75.2],
[172.5, 55.2], [170.9, 54.2], [172.9, 62.5], [153.4, 42.0], [160.0, 50.0],
[147.2, 49.8], [168.2, 49.2], [175.0, 73.2], [157.0, 47.8], [167.6, 68.8],
[159.5, 50.6], [175.0, 82.5], [166.8, 57.2], [176.5, 87.8], [170.2, 72.8],
[174.0, 54.5], [173.0, 59.8], [179.9, 67.3], [170.5, 67.8], [160.0, 47.0],
[154.4, 46.2], [162.0, 55.0], [176.5, 83.0], [160.0, 54.4], [152.0, 45.8],
[162.1, 53.6], [170.0, 73.2], [160.2, 52.1], [161.3, 67.9], [166.4, 56.6],
[168.9, 62.3], [163.8, 58.5], [167.6, 54.5], [160.0, 50.2], [161.3, 60.3],
[167.6, 58.3], [165.1, 56.2], [160.0, 50.2], [170.0, 72.9], [157.5, 59.8],
[167.6, 61.0], [160.7, 69.1], [163.2, 55.9], [152.4, 46.5], [157.5, 54.3],
[168.3, 54.8], [180.3, 60.7], [165.5, 60.0], [165.0, 62.0], [164.5, 60.3],
[156.0, 52.7], [160.0, 74.3], [163.0, 62.0], [165.7, 73.1], [161.0, 80.0],
[162.0, 54.7], [166.0, 53.2], [174.0, 75.7], [172.7, 61.1], [167.6, 55.7],
[151.1, 48.7], [164.5, 52.3], [163.5, 50.0], [152.0, 59.3], [169.0, 62.5],
[164.0, 55.7], [161.2, 54.8], [155.0, 45.9], [170.0, 70.6], [176.2, 67.2],
[170.0, 69.4], [162.5, 58.2], [170.3, 64.8], [164.1, 71.6], [169.5, 52.8],
[163.2, 59.8], [154.5, 49.0], [159.8, 50.0], [173.2, 69.2], [170.0, 55.9],
[161.4, 63.4], [169.0, 58.2], [166.2, 58.6], [159.4, 45.7], [162.5, 52.2],
[159.0, 48.6], [162.8, 57.8], [159.0, 55.6], [179.8, 66.8], [162.9, 59.4],
[161.0, 53.6], [151.1, 73.2], [168.2, 53.4], [168.9, 69.0], [173.2, 58.4],
[171.8, 56.2], [178.0, 70.6], [164.3, 59.8], [163.0, 72.0], [168.5, 65.2],
[166.8, 56.6], [172.7, 105.2], [163.5, 51.8], [169.4, 63.4], [167.8, 59.0],
[159.5, 47.6], [167.6, 63.0], [161.2, 55.2], [160.0, 45.0], [163.2, 54.0],
[162.2, 50.2], [161.3, 60.2], [149.5, 44.8], [157.5, 58.8], [163.2, 56.4],
[172.7, 62.0], [155.0, 49.2], [156.5, 67.2], [164.0, 53.8], [160.9, 54.4],
[162.8, 58.0], [167.0, 59.8], [160.0, 54.8], [160.0, 43.2], [168.9, 60.5],
[158.2, 46.4], [156.0, 64.4], [160.0, 48.8], [167.1, 62.2], [158.0, 55.5],
[167.6, 57.8], [156.0, 54.6], [162.1, 59.2], [173.4, 52.7], [159.8, 53.2],
[170.5, 64.5], [159.2, 51.8], [157.5, 56.0], [161.3, 63.6], [162.6, 63.2],
[160.0, 59.5], [168.9, 56.8], [165.1, 64.1], [162.6, 50.0], [165.1, 72.3],
[166.4, 55.0], [160.0, 55.9], [152.4, 60.4], [170.2, 69.1], [162.6, 84.5],
[170.2, 55.9], [158.8, 55.5], [172.7, 69.5], [167.6, 76.4], [162.6, 61.4],
[167.6, 65.9], [156.2, 58.6], [175.2, 66.8], [172.1, 56.6], [162.6, 58.6],
[160.0, 55.9], [165.1, 59.1], [182.9, 81.8], [166.4, 70.7], [165.1, 56.8],
[177.8, 60.0], [165.1, 58.2], [175.3, 72.7], [154.9, 54.1], [158.8, 49.1],
[172.7, 75.9], [168.9, 55.0], [161.3, 57.3], [167.6, 55.0], [165.1, 65.5],
[175.3, 65.5], [157.5, 48.6], [163.8, 58.6], [167.6, 63.6], [165.1, 55.2],
[165.1, 62.7], [168.9, 56.6], [162.6, 53.9], [164.5, 63.2], [176.5, 73.6],
[168.9, 62.0], [175.3, 63.6], [159.4, 53.2], [160.0, 53.4], [170.2, 55.0],
[162.6, 70.5], [167.6, 54.5], [162.6, 54.5], [160.7, 55.9], [160.0, 59.0],
[157.5, 63.6], [162.6, 54.5], [152.4, 47.3], [170.2, 67.7], [165.1, 80.9],
[172.7, 70.5], [165.1, 60.9], [170.2, 63.6], [170.2, 54.5], [170.2, 59.1],
[161.3, 70.5], [167.6, 52.7], [167.6, 62.7], [165.1, 86.3], [162.6, 66.4],
[152.4, 67.3], [168.9, 63.0], [170.2, 73.6], [175.2, 62.3], [175.2, 57.7],
[160.0, 55.4], [165.1, 104.1], [174.0, 55.5], [170.2, 77.3], [160.0, 80.5],
[167.6, 64.5], [167.6, 72.3], [167.6, 61.4], [154.9, 58.2], [162.6, 81.8],
[175.3, 63.6], [171.4, 53.4], [157.5, 54.5], [165.1, 53.6], [160.0, 60.0],
[174.0, 73.6], [162.6, 61.4], [174.0, 55.5], [162.6, 63.6], [161.3, 60.9],
[156.2, 60.0], [149.9, 46.8], [169.5, 57.3], [160.0, 64.1], [175.3, 63.6],
[169.5, 67.3], [160.0, 75.5], [172.7, 68.2], [162.6, 61.4], [157.5, 76.8],
[176.5, 71.8], [164.4, 55.5], [160.7, 48.6], [174.0, 66.4], [163.8, 67.3]
],
markPoint : {
data : [
{type : 'max', name: '最大值'},
{type : 'min', name: '最小值'}
]
},
markLine : {
data : [
{type : 'average', name: '平均值'}
]
}
},
{
name:'男性',
type:'scatter',
data: [[174.0, 65.6], [175.3, 71.8], [193.5, 80.7], [186.5, 72.6], [187.2, 78.8],
[181.5, 74.8], [184.0, 86.4], [184.5, 78.4], [175.0, 62.0], [184.0, 81.6],
[180.0, 76.6], [177.8, 83.6], [192.0, 90.0], [176.0, 74.6], [174.0, 71.0],
[184.0, 79.6], [192.7, 93.8], [171.5, 70.0], [173.0, 72.4], [176.0, 85.9],
[176.0, 78.8], [180.5, 77.8], [172.7, 66.2], [176.0, 86.4], [173.5, 81.8],
[178.0, 89.6], [180.3, 82.8], [180.3, 76.4], [164.5, 63.2], [173.0, 60.9],
[183.5, 74.8], [175.5, 70.0], [188.0, 72.4], [189.2, 84.1], [172.8, 69.1],
[170.0, 59.5], [182.0, 67.2], [170.0, 61.3], [177.8, 68.6], [184.2, 80.1],
[186.7, 87.8], [171.4, 84.7], [172.7, 73.4], [175.3, 72.1], [180.3, 82.6],
[182.9, 88.7], [188.0, 84.1], [177.2, 94.1], [172.1, 74.9], [167.0, 59.1],
[169.5, 75.6], [174.0, 86.2], [172.7, 75.3], [182.2, 87.1], [164.1, 55.2],
[163.0, 57.0], [171.5, 61.4], [184.2, 76.8], [174.0, 86.8], [174.0, 72.2],
[177.0, 71.6], [186.0, 84.8], [167.0, 68.2], [171.8, 66.1], [182.0, 72.0],
[167.0, 64.6], [177.8, 74.8], [164.5, 70.0], [192.0, 101.6], [175.5, 63.2],
[171.2, 79.1], [181.6, 78.9], [167.4, 67.7], [181.1, 66.0], [177.0, 68.2],
[174.5, 63.9], [177.5, 72.0], [170.5, 56.8], [182.4, 74.5], [197.1, 90.9],
[180.1, 93.0], [175.5, 80.9], [180.6, 72.7], [184.4, 68.0], [175.5, 70.9],
[180.6, 72.5], [177.0, 72.5], [177.1, 83.4], [181.6, 75.5], [176.5, 73.0],
[175.0, 70.2], [174.0, 73.4], [165.1, 70.5], [177.0, 68.9], [192.0, 102.3],
[176.5, 68.4], [169.4, 65.9], [182.1, 75.7], [179.8, 84.5], [175.3, 87.7],
[184.9, 86.4], [177.3, 73.2], [167.4, 53.9], [178.1, 72.0], [168.9, 55.5],
[157.2, 58.4], [180.3, 83.2], [170.2, 72.7], [177.8, 64.1], [172.7, 72.3],
[165.1, 65.0], [186.7, 86.4], [165.1, 65.0], [174.0, 88.6], [175.3, 84.1],
[185.4, 66.8], [177.8, 75.5], [180.3, 93.2], [180.3, 82.7], [177.8, 58.0],
[177.8, 79.5], [177.8, 78.6], [177.8, 71.8], [177.8, 116.4], [163.8, 72.2],
[188.0, 83.6], [198.1, 85.5], [175.3, 90.9], [166.4, 85.9], [190.5, 89.1],
[166.4, 75.0], [177.8, 77.7], [179.7, 86.4], [172.7, 90.9], [190.5, 73.6],
[185.4, 76.4], [168.9, 69.1], [167.6, 84.5], [175.3, 64.5], [170.2, 69.1],
[190.5, 108.6], [177.8, 86.4], [190.5, 80.9], [177.8, 87.7], [184.2, 94.5],
[176.5, 80.2], [177.8, 72.0], [180.3, 71.4], [171.4, 72.7], [172.7, 84.1],
[172.7, 76.8], [177.8, 63.6], [177.8, 80.9], [182.9, 80.9], [170.2, 85.5],
[167.6, 68.6], [175.3, 67.7], [165.1, 66.4], [185.4, 102.3], [181.6, 70.5],
[172.7, 95.9], [190.5, 84.1], [179.1, 87.3], [175.3, 71.8], [170.2, 65.9],
[193.0, 95.9], [171.4, 91.4], [177.8, 81.8], [177.8, 96.8], [167.6, 69.1],
[167.6, 82.7], [180.3, 75.5], [182.9, 79.5], [176.5, 73.6], [186.7, 91.8],
[188.0, 84.1], [188.0, 85.9], [177.8, 81.8], [174.0, 82.5], [177.8, 80.5],
[171.4, 70.0], [185.4, 81.8], [185.4, 84.1], [188.0, 90.5], [188.0, 91.4],
[182.9, 89.1], [176.5, 85.0], [175.3, 69.1], [175.3, 73.6], [188.0, 80.5],
[188.0, 82.7], [175.3, 86.4], [170.5, 67.7], [179.1, 92.7], [177.8, 93.6],
[175.3, 70.9], [182.9, 75.0], [170.8, 93.2], [188.0, 93.2], [180.3, 77.7],
[177.8, 61.4], [185.4, 94.1], [168.9, 75.0], [185.4, 83.6], [180.3, 85.5],
[174.0, 73.9], [167.6, 66.8], [182.9, 87.3], [160.0, 72.3], [180.3, 88.6],
[167.6, 75.5], [186.7, 101.4], [175.3, 91.1], [175.3, 67.3], [175.9, 77.7],
[175.3, 81.8], [179.1, 75.5], [181.6, 84.5], [177.8, 76.6], [182.9, 85.0],
[177.8, 102.5], [184.2, 77.3], [179.1, 71.8], [176.5, 87.9], [188.0, 94.3],
[174.0, 70.9], [167.6, 64.5], [170.2, 77.3], [167.6, 72.3], [188.0, 87.3],
[174.0, 80.0], [176.5, 82.3], [180.3, 73.6], [167.6, 74.1], [188.0, 85.9],
[180.3, 73.2], [167.6, 76.3], [183.0, 65.9], [183.0, 90.9], [179.1, 89.1],
[170.2, 62.3], [177.8, 82.7], [179.1, 79.1], [190.5, 98.2], [177.8, 84.1],
[180.3, 83.2], [180.3, 83.2]
],
markPoint : {
data : [
{type : 'max', name: '最大值'},
{type : 'min', name: '最小值'}
]
},
markLine : {
data : [
{type : 'average', name: '平均值'}
]
}
}
]
}]
};
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<!--
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.
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="../../dist/echarts.js"></script>
<script src="../../map/js/china.js"></script>
</head>
<body>
<style>
body {
margin: 0;
}
</style>
<div id="main"></div>
<script type="module">
import bar from './option/bar';
import area from './option/area';
import scatter from './option/scatter';
import pie from './option/pie';
import graph from './option/graph';
import map from './option/map';
let options = [bar, area, scatter, pie, graph, map];
let mainDiv = document.querySelector('#main');
let theme = window.location.hash.slice(1);
let scriptTag = document.createElement('script');
scriptTag.src = '../' + theme + '.js';
scriptTag.onload = function () {
options.forEach(option => {
let div = document.createElement('div');
mainDiv.appendChild(div);
div.style.cssText = 'float:left;width:50%;height:400px';
let chart = echarts.init(div, theme);
option.animation = false;
chart.setOption(option);
});
}
document.body.appendChild(scriptTag);
</script>
</body>
</html>
@@ -0,0 +1,63 @@
/*
* 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.
*/
const glob = require('glob');
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
async function wait(time) {
return new Promise(resolve => {
setTimeout(resolve, time);
});
}
async function snapshot(browser, themePath) {
let themeName = path.basename(themePath, '.js');
let code = fs.readFileSync(themePath, 'utf-8');
let page = await browser.newPage();
await page.evaluateOnNewDocument(code);
await page.setViewport({ width: 1200, height: 1200 });
try {
await page.goto('http://localhost/echarts/theme/tool/thumb.html#' + themeName);
await wait(200);
await page.screenshot({ path: __dirname + '/../thumb/' + themeName + '.png' });
}
catch (e) {
console.log(e);
}
await page.close();
console.log('Updated ' + themeName);
}
glob('../*.js', async function (err, themePathList) {
let browser = await puppeteer.launch();
for (let themePath of themePathList) {
try {
await snapshot(browser, themePath);
}
catch(e) {
console.log(e);
}
}
await browser.close();
});
@@ -0,0 +1,63 @@
/*
* 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.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#d87c7c',
'#919e8b',
'#d7ab82',
'#6e7074',
'#61a0a8',
'#efa18d',
'#787464',
'#cc7e63',
'#724e58',
'#4b565b'
];
echarts.registerTheme('vintage', {
color: colorPalette,
backgroundColor: '#fef8ef',
graph: {
color: colorPalette
}
});
});
@@ -156,6 +156,50 @@
}
}
// 활성 Spring 프로파일 표시 (prod 제외)
// 데스크톱: 로고 옆 인라인 뱃지 / 모바일·태블릿: 최상단 floating 바(fixed, 레이아웃 미영향)
.env-badge {
display: inline-flex;
align-items: center;
margin-left: 10px;
padding: 2px 8px;
border-radius: 4px;
background: var(--accent-orange);
color: var(--white);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
line-height: 1.6;
@media (max-width: 1024px) {
display: none;
}
}
.env-strip {
display: none;
@media (max-width: 1024px) {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 16px;
background: var(--accent-orange);
color: var(--white);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
z-index: 1100;
pointer-events: none;
}
}
// Logo Wrapper
.logo-wrapper {
display: flex;
@@ -1198,6 +1198,11 @@
padding-right: 20px;
flex: 1;
&:hover {
color: #2a69de;
text-decoration: underline;
}
@media (max-width: 768px) {
font-size: 14px;
}
@@ -20,14 +20,14 @@
<nav class="service-nav">
<!-- All APIs -->
<a class="service-nav__item"
th:classappend="${selected == '-1' or (param.selected != null and param.selected[0] == '-1')} ? 'service-nav__item--active' : ''"
th:classappend="${selected == '-1' || #request.getParameter('selected') == '-1'} ? 'service-nav__item--active' : ''"
th:href="@{/apis}">
전체
</a>
<!-- Service Categories -->
<a class="service-nav__item" th:each="service : ${services}"
th:classappend="${(param.selected != null ? param.selected[0] : (selected != null ? selected : apiSpecInfo.service)) == service.id} ? 'service-nav__item--active' : ''"
th:classappend="${(#request.getParameter('selected') != null ? #request.getParameter('selected') : (selected != null ? selected : apiSpecInfo.service)) == service.id} ? 'service-nav__item--active' : ''"
th:href="@{/apis(groupIds=${service.id})}">
[[${service.groupName}]]
</a>
@@ -61,6 +61,7 @@
<form id="searchForm" th:action="@{/apis}" method="get">
<input type="hidden" name="groupIds"
th:value="${search.groupIds != null and !search.groupIds.isEmpty() ? search.groupIds[0] : ''}" />
<input type="hidden" name="page" value="1" />
<input type="text" class="search-input" name="keyword" th:value="${search.keyword}"
placeholder="검색어를 입력하세요.">
<button type="submit" class="search-submit-btn" aria-label="검색">
@@ -99,13 +100,17 @@
<!-- API Image (Bottom Right) -->
<div class="api-card-image">
<img th:if="${api.mainIcon}" th:src="${api.mainIcon}" th:alt="${api.apiGroupName}"
onerror="this.style.display='none'">
<img th:if="${api.apiGroupId}" th:src="@{/api-services/{id}/icon(id=${api.apiGroupId})}"
th:alt="${api.apiGroupName}" onerror="this.style.display='none'">
</div>
</div>
</div>
<!-- Pagination -->
<div class="pagination" th:if="${apis != null and !apis.isEmpty()}"
th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
<!-- Empty State -->
<div class="api-empty-state" th:if="${apis == null or apis.isEmpty()}">
<div class="empty-icon">🔍</div>
@@ -121,6 +126,14 @@
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
// fragment/pagination.html 의 인라인 onclick 에서 호출 (전역 스코프 필요)
window.fn_select_page = function (pageNo) {
const form = document.getElementById('searchForm');
const pageInput = form.querySelector('input[name="page"]');
if (pageInput) pageInput.value = pageNo;
form.submit();
};
document.addEventListener('DOMContentLoaded', function () {
// DOM Elements
const sidebar = document.getElementById('apiSidebar');
@@ -128,6 +141,7 @@
const mobileOverlay = document.getElementById('mobileOverlay');
const searchForm = document.getElementById('searchForm');
const groupIdsInput = searchForm.querySelector('input[name="groupIds"]');
const pageInput = searchForm.querySelector('input[name="page"]');
const searchInput = searchForm.querySelector('input[name="keyword"]');
// API Card Click Handlers
@@ -230,6 +244,9 @@
groupIdsInput.value = '';
}
// 카테고리 변경 시 1페이지로 리셋
if (pageInput) pageInput.value = '1';
// Submit form
searchForm.submit();
});
@@ -239,6 +256,7 @@
searchInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
if (pageInput) pageInput.value = '1';
searchForm.submit();
}
});
@@ -238,10 +238,8 @@
</span>
</p>
<div class="card-illustration">
<!-- 서비스별 아이콘 매핑 - base64 인코딩된 이미지 사용 -->
<img
th:src="${service.mainIcon != null and !#strings.isEmpty(service.mainIcon)} ? ${service.mainIcon} : @{/img/api_icon_default.png}"
th:alt="${service.groupName}">
<!-- 서비스별 아이콘: base64 인라인 대신 그룹 id 기준 스트리밍 엔드포인트(캐시 가능) 사용 -->
<img th:src="@{/api-services/{id}/icon(id=${service.id})}" th:alt="${service.groupName}">
</div>
</div>
</div>
@@ -560,9 +558,6 @@
</th:block>
</body>
<th:block layout:fragment="contentScript">
<!-- 메인 페이지 전용 스크립트 모듈 추가 -->
<script th:src="@{/js/main.js}"></script>
<!-- 로그인 후 처리 스크립트 -->
<script th:src="@{/js/login-success-handler.js}"></script>
<script th:inline="javascript">
@@ -164,7 +164,7 @@
<label class="dt-label">API 목록</label>
<div class="dt-api-list-box">
<div class="dt-api-item" th:each="api : ${apiKey.apiList}">
<span class="dt-api-name" th:text="${api.apiDesc}">추가된 API 1</span>
<a class="dt-api-name" th:href="@{/apis/detail(id=${api.apiId})}" th:text="${api.apiDesc}">추가된 API 1</a>
<span class="dt-api-status-badge">승인</span>
</div>
<div class="dt-empty-message" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}">
@@ -19,7 +19,7 @@
th:classappend="${isInvited ? 'input-readonly' : ''}" maxlength="50" th:value="${domain}"
th:readonly="${isInvited}" th:placeholder="#{portalUser.Register.domain}">
</div>
<button type="button" class="btn-action-primary md" th:classappend="${isInvited ? 'hidden' : ''}">
<button type="button" class="btn-action-primary md btn_check_email" th:classappend="${isInvited ? 'hidden' : ''}">
중복체크
</button>
</div>
@@ -31,7 +31,9 @@
<option th:each="site : ${relatedSites}" th:value="${site.url}" th:text="${site.name}"></option>
</select>
</div>
<p class="footer-contact" th:text="'고객센터 ' + ${customerCenterContact}">고객센터 1588-3388</p>
<!-- 고객센터 연락처: PortalProperty(Portal/customer.center.contact). 값이 공백이면 미노출 -->
<p class="footer-contact" th:if="${!#strings.isEmpty(#strings.trim(customerCenterContact))}"
th:text="'고객센터 ' + ${customerCenterContact}">고객센터 1588-3388</p>
</div>
</div>
</div>
@@ -122,14 +122,8 @@
<!--/* LiveReload (개발 전용): DevTools 가 정적 리소스/템플릿 변경을 감지하면 브라우저를 자동 새로고침한다.
localhost 외 IP(예: 172.30.1.14)로 접속해도 동작하도록 접속 호스트 기준으로 livereload.js 를 로드한다.
prod/stage 에는 DevTools 자체가 없으므로(developmentOnly) 개발 프로파일에서만 주입한다. */-->
<script th:if="${@environment.acceptsProfiles('local_rinjaemac','local')}">
// Thymeleaf 3.1 은 #request 표현식 객체를 제거했으므로 호스트는 브라우저에서 읽는다.
(function () {
var s = document.createElement('script');
s.src = '//' + location.hostname + ':35729/livereload.js';
document.head.appendChild(s);
})();
</script>
<script th:if="${@environment.acceptsProfiles('local_rinjaemac','local')}"
th:src="|//${#request.serverName}:35729/livereload.js|"></script>
</head>
</html>
@@ -3,6 +3,8 @@
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<th:block th:fragment="headerFragment(headerClass)">
<!-- 활성 프로파일 표시(prod 제외). 데스크톱은 로고 옆 뱃지, 모바일/태블릿은 최상단 floating 바(env-badge 는 그때 숨김) -->
<div th:if="${activeProfileBadge != null}" class="env-strip" th:text="${activeProfileBadge}">dev</div>
<!-- Global Header Container -->
<header class="global-header" th:classappend="${headerClass}">
<div class="container">
@@ -23,6 +25,7 @@
<img src="/img/logo/logo-djb.png" alt="DJBank" class="mobile-logo">
</a>
<a th:href="@{/}" class="mobile-logo-text" style="font-size: 22px; font-weight: 700; color: #212529; text-decoration: none; margin-left: 8px; vertical-align: middle;">API Portal</a>
<span th:if="${activeProfileBadge != null}" class="env-badge" th:text="${activeProfileBadge}">dev</span>
</div>
</div>
</div>
@@ -1,107 +0,0 @@
package com.eactive.apim.portal.common.compatibility;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import com.eactive.apim.portal.common.util.UserTypeUtil;
import com.eactive.apim.portal.common.validator.CellPhoneValidator;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* Commons Lang 업그레이드 포털에서 사용하는 문자열 처리 결과가 유지되는지 검증한다.
*/
class CommonsLangUpgradeCompatibilityTest {
@AfterEach
void resetUserTypeConfiguration() {
UserTypeUtil.setInternalEmailDomains(null);
}
@Test
@DisplayName("빈 문자열과 공백 판정이 기존 분기 조건을 유지한다")
void keepsEmptyAndBlankBranchingBehavior() {
assertAll(
() -> assertTrue(StringUtils.isEmpty(null)),
() -> assertTrue(StringUtils.isEmpty("")),
() -> assertFalse(StringUtils.isEmpty(" ")),
() -> assertTrue(StringUtils.isBlank(" \t")),
() -> assertFalse(StringUtils.isNotBlank(" \t")),
() -> assertTrue(StringUtils.isNotEmpty(" "))
);
}
@Test
@DisplayName("폼 입력과 상태 설정의 trim 및 기본값 처리가 유지된다")
void keepsTrimmingAndDefaultingBehavior() {
assertAll(
() -> assertEquals("서비스명", StringUtils.trimToEmpty(" 서비스명 ")),
() -> assertEquals("", StringUtils.trimToEmpty(null)),
() -> assertEquals("api-001", StringUtils.defaultIfBlank(" ", "api-001")),
() -> assertEquals("API 이름", StringUtils.defaultIfBlank("API 이름", "api-001")),
() -> assertTrue(Strings.CI.equalsAny(" Y ".trim(), "true", "y", "1"))
);
}
@Test
@DisplayName("API 경로와 메시지 리소스명 문자열 처리가 유지된다")
void keepsPathAndResourceNameBehavior() {
String fullPath = StringUtils.join("/openapi/", "/", Strings.CS.removeStart("/users", "/"));
String normalizedPath = fullPath.replaceAll("//+", "/");
String resource = "/WEB-INF/messages/messages_ko.properties";
String baseName = StringUtils.substringBeforeLast(resource, ".properties");
assertAll(
() -> assertEquals("/openapi/users", normalizedPath),
() -> assertEquals("/openapi", Strings.CS.removeEnd("/openapi/", "/")),
() -> assertEquals("/WEB-INF/messages", StringUtils.substringBeforeLast(baseName, "/")),
() -> assertEquals("messages_ko", StringUtils.substringAfterLast(baseName, "/")),
() -> assertEquals("messages", StringUtils.substringBeforeLast("messages_ko", "_"))
);
}
@SuppressWarnings("deprecation")
@Test
@DisplayName("API와 약관 본문의 HTML 엔티티 디코딩 결과가 유지된다")
void keepsHtmlUnescapeBehavior() {
assertEquals("<p>이용약관 & 안내</p>",
StringEscapeUtils.unescapeHtml4("&lt;p&gt;이용약관 &amp; 안내&lt;/p&gt;"));
}
@Test
@DisplayName("휴대전화 검증의 기존 허용 및 거부 결과가 유지된다")
void keepsCellPhoneValidationBehavior() {
CellPhoneValidator validator = new CellPhoneValidator();
assertAll(
() -> assertTrue(validator.isValid("010-1234-5678", null)),
() -> assertTrue(validator.isValid("821012345678", null)),
() -> assertFalse(validator.isValid("", null)),
() -> assertFalse(validator.isValid("02-1234-5678", null)),
() -> assertFalse(validator.isValid("010-12-5678", null))
);
}
@Test
@DisplayName("내부 사용자 이메일 도메인 정규화와 판별 결과가 유지된다")
void keepsInternalUserDomainBehavior() {
UserTypeUtil.setInternalEmailDomains(Arrays.asList(" DJBANK.com ", "@Partner.COM", " "));
assertAll(
() -> assertEquals(Arrays.asList("@djbank.com", "@partner.com"),
UserTypeUtil.getInternalEmailDomains()),
() -> assertTrue(UserTypeUtil.isInternalEmail(" USER@DJBANK.COM ")),
() -> assertTrue(UserTypeUtil.isInternalEmail("user@partner.com")),
() -> assertFalse(UserTypeUtil.isInternalEmail("user@example.com")),
() -> assertFalse(UserTypeUtil.isInternalEmail(null))
);
}
}
@@ -1,128 +0,0 @@
package com.eactive.apim.portal.common.json;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDate;
import java.time.LocalDateTime;
import com.eactive.apim.portal.djb.apistatus.dto.DailyStatDTO;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
/**
* jackson 2.13.5 2.18.x 상향에 따른 JSON 직렬화 회귀 테스트.
* <p>
* Spring Boot {@link JacksonAutoConfiguration} 만들어 주는 ObjectMapper(= MVC 응답에 실제로 쓰이는 )
* 화면/AJAX 응답의 날짜 표현이 그대로인지 고정한다. 직접 new ObjectMapper Jackson2ObjectMapperBuilder
* WRITE_DATES_AS_TIMESTAMPS 켜져 있어 실제 응답과 다르므로 쓰지 않는다.
* </p>
*/
class JacksonDateSerializationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class));
/** Boot 가 구성한 ObjectMapper 로 검증 로직을 실행한다. */
private void withBootObjectMapper(MapperAssertion assertion) {
contextRunner.run(context -> assertion.accept(context.getBean(ObjectMapper.class)));
}
@FunctionalInterface
interface MapperAssertion {
void accept(ObjectMapper objectMapper) throws Exception;
}
/** 테스트 소스에는 lombok annotation processor 가 걸려 있지 않아 접근자를 직접 둔다. */
public static class SampleDTO {
private LocalDateTime createdAt;
private LocalDate baseDate;
private String name;
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDate getBaseDate() {
return baseDate;
}
public void setBaseDate(LocalDate baseDate) {
this.baseDate = baseDate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Test
@DisplayName("@JsonFormat 지정 필드는 지정 패턴 문자열로 직렬화된다")
void jsonFormatPatternIsHonored() {
withBootObjectMapper(objectMapper -> {
DailyStatDTO dto = new DailyStatDTO();
dto.setStatDate(LocalDate.of(2026, 8, 18));
dto.setUptimeRatio(0.9987);
dto.setStatus("NORMAL");
String json = objectMapper.writeValueAsString(dto);
assertTrue(json.contains("\"statDate\":\"2026-08-18\""), json);
assertTrue(json.contains("\"status\":\"NORMAL\""), json);
});
}
@Test
@DisplayName("어노테이션 없는 날짜 필드는 타임스탬프 숫자가 아니라 ISO-8601 문자열이다")
void plainDatesAreIsoStrings() {
withBootObjectMapper(objectMapper -> {
SampleDTO dto = new SampleDTO();
dto.setCreatedAt(LocalDateTime.of(2026, 8, 18, 9, 30, 0));
dto.setBaseDate(LocalDate.of(2026, 8, 18));
dto.setName("포털");
String json = objectMapper.writeValueAsString(dto);
assertTrue(json.contains("\"createdAt\":\"2026-08-18T09:30:00\""), json);
assertTrue(json.contains("\"baseDate\":\"2026-08-18\""), json);
assertTrue(json.contains("\"name\":\"포털\""), json);
});
}
@Test
@DisplayName("ISO-8601 문자열은 다시 날짜 타입으로 역직렬화된다")
void isoStringsDeserializeBack() {
withBootObjectMapper(objectMapper -> {
String json = "{\"createdAt\":\"2026-08-18T09:30:00\",\"baseDate\":\"2026-08-18\",\"name\":\"포털\"}";
SampleDTO dto = objectMapper.readValue(json, SampleDTO.class);
assertEquals(LocalDateTime.of(2026, 8, 18, 9, 30, 0), dto.getCreatedAt());
assertEquals(LocalDate.of(2026, 8, 18), dto.getBaseDate());
});
}
@Test
@DisplayName("모르는 필드가 있어도 역직렬화가 실패하지 않는다 - Boot 기본 동작 유지")
void unknownPropertiesAreIgnored() {
withBootObjectMapper(objectMapper -> {
String json = "{\"name\":\"포털\",\"unknownField\":123}";
SampleDTO dto = objectMapper.readValue(json, SampleDTO.class);
assertEquals("포털", dto.getName());
});
}
}
@@ -1,56 +0,0 @@
package com.eactive.apim.portal.common.security;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.apache.commons.beanutils.PropertyUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* commons-beanutils 1.11.0(CVE-2025-48734) 상향 회귀 테스트.
* <p>
* 1.11.0 부터 SuppressPropertiesBeanIntrospector 기본 활성이라 enum declaredClass 통해
* ClassLoader 내려가는 경로가 막힌다. 검증기들이 쓰는 일반 프로퍼티 접근은 그대로 동작해야 한다.
* </p>
*/
class BeanUtilsPropertyAccessTest {
enum Kind {
A, B
}
public static class Form {
private String password = "pw1234";
private Kind kind = Kind.A;
public String getPassword() {
return password;
}
public Kind getKind() {
return kind;
}
}
@Test
@DisplayName("일반 프로퍼티 접근은 정상 - PasswordRuleValidator 등이 쓰는 경로")
void plainPropertyAccessWorks() throws Exception {
assertEquals("pw1234", PropertyUtils.getProperty(new Form(), "password"));
assertEquals(Kind.A, PropertyUtils.getProperty(new Form(), "kind"));
}
@Test
@DisplayName("enum 의 declaredClass 접근은 차단된다")
void declaredClassIsSuppressed() {
assertThrows(NoSuchMethodException.class,
() -> PropertyUtils.getProperty(new Form(), "kind.declaredClass"));
}
@Test
@DisplayName("declaredClass 를 거쳐 ClassLoader 로 내려가는 경로도 차단된다")
void classLoaderIsUnreachable() {
assertThrows(NoSuchMethodException.class,
() -> PropertyUtils.getNestedProperty(new Form(), "kind.declaredClass.classLoader"));
}
}
@@ -1,51 +0,0 @@
package com.eactive.apim.portal.common.security;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ByteArrayResource;
/**
* snakeyaml 2.6(CVE-2022-1471) 상향 회귀 테스트.
* <p>
* application.yml 로딩 경로가 정상 동작하는지, 그리고 임의 타입을 지정하는 글로벌 태그(!!java...)
* 거부되는지 확인한다.
* </p>
*/
class YamlLoaderTagTest {
private final YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
private List<PropertySource<?>> load(String yaml) throws IOException {
return loader.load("test", new ByteArrayResource(yaml.getBytes("UTF-8")));
}
@Test
@DisplayName("일반 yml 은 그대로 로딩된다")
void plainYamlLoads() throws Exception {
List<PropertySource<?>> sources = load("server:\n port: 39130\nportal:\n auth-ttl: 300\n");
assertEquals(1, sources.size());
assertEquals(39130, sources.get(0).getProperty("server.port"));
assertEquals(300, sources.get(0).getProperty("portal.auth-ttl"));
}
@Test
@DisplayName("임의 타입을 지정하는 글로벌 태그는 거부된다")
void globalTagIsRejected() {
String payload = "key: !!javax.script.ScriptEngineManager [!!java.net.URL [\"http://127.0.0.1/\"]]\n";
Exception thrown = assertThrows(Exception.class, () -> load(payload));
assertNotNull(thrown.getMessage());
assertEquals(true, thrown.getMessage().contains("Global tag is not allowed"));
}
}
@@ -1,100 +0,0 @@
package com.eactive.apim.portal.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
/**
* commons-fileupload 1.6.0(CVE-2025-48976) 상향에 따른 파트 헤더 상한 회귀 테스트.
* <p>
* 1.6 부터 파트 헤더 총량 기본 상한이 512 바이트라 한글 파일명(UTF-8 3바이트/) 길면 업로드가 깨진다.
* {@link MultipartConfig} 상한을 2048 올려 두는지 파싱 결과로 확인한다.
* </p>
*/
class MultipartConfigTest {
private static final String BOUNDARY = "----portalTestBoundary";
private CommonsMultipartResolver resolver() {
PortalProperties properties = new PortalProperties();
properties.getFile().setMaxSize("8MB");
return new MultipartConfig(properties).filterMultipartResolver();
}
private MockHttpServletRequest multipartRequest(String filename) throws IOException {
String head = "--" + BOUNDARY + "\r\n"
+ "Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"\r\n"
+ "Content-Type: application/octet-stream\r\n\r\n";
ByteArrayOutputStream body = new ByteArrayOutputStream();
body.write(head.getBytes("UTF-8"));
body.write("hello".getBytes("UTF-8"));
body.write(("\r\n--" + BOUNDARY + "--\r\n").getBytes("UTF-8"));
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/file/upload");
request.setContentType("multipart/form-data; boundary=" + BOUNDARY);
request.setCharacterEncoding("UTF-8");
request.setContent(body.toByteArray());
return request;
}
private String korean(int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append('한');
}
return sb.append(".pdf").toString();
}
@Test
@DisplayName("영문 파일명 업로드는 그대로 파싱된다")
void asciiFilename() throws Exception {
MultipartHttpServletRequest parsed = resolver().resolveMultipart(multipartRequest("report.pdf"));
MultipartFile file = parsed.getFile("file");
assertNotNull(file);
assertEquals("report.pdf", file.getOriginalFilename());
assertEquals(5L, file.getSize());
}
@Test
@DisplayName("한글 파일명 137자 이상도 파싱된다 - fileupload 1.6 기본 상한 512 였다면 실패할 길이")
void longKoreanFilenameOverDefaultLimit() throws Exception {
String filename = korean(150);
MultipartHttpServletRequest parsed = resolver().resolveMultipart(multipartRequest(filename));
MultipartFile file = parsed.getFile("file");
assertNotNull(file);
assertEquals(filename, file.getOriginalFilename());
}
@Test
@DisplayName("OS 파일명 상한(255자)이 전부 한글이어도 파싱된다")
void koreanFilenameAtOsLimit() throws Exception {
String filename = korean(255);
MultipartHttpServletRequest parsed = resolver().resolveMultipart(multipartRequest(filename));
MultipartFile file = parsed.getFile("file");
assertNotNull(file);
assertEquals(filename, file.getOriginalFilename());
}
@Test
@DisplayName("파트 헤더가 상한(2048바이트)을 넘으면 거부한다 - DoS 방어가 살아 있음")
void partHeaderOverConfiguredLimitIsRejected() {
assertThrows(MultipartException.class,
() -> resolver().resolveMultipart(multipartRequest(korean(700))));
}
}
@@ -1,70 +0,0 @@
package com.eactive.apim.portal.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.header.writers.CacheControlHeadersWriter;
import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter;
/**
* CVE-2026-22732 우회책 회귀 테스트.
* <p>
* {@code PortalConfigSecurity} HeaderWriterFilter eager 모드로 후처리한다. eager 모드에서는
* 컨트롤러가 실행되기 전에 보안 헤더가 이미 기록돼 있어야 하며, 응답이 먼저 커밋되더라도 헤더가 누락되지 않는다.
* </p>
*/
class SecurityHeaderEagerWriteTest {
private HeaderWriterFilter filter(boolean eager) {
HeaderWriterFilter filter = new HeaderWriterFilter(Arrays.asList(
new XContentTypeOptionsHeaderWriter(),
new CacheControlHeadersWriter()));
filter.setShouldWriteHeadersEagerly(eager);
return filter;
}
@Test
@DisplayName("eager 모드에서는 체인(컨트롤러) 실행 시점에 이미 보안 헤더가 기록돼 있다")
void eagerWritesHeadersBeforeChain() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/file/download");
MockHttpServletResponse response = new MockHttpServletResponse();
AtomicReference<String> seenInChain = new AtomicReference<>();
filter(true).doFilter(request, response, (req, res) -> {
seenInChain.set(((HttpServletResponse) res).getHeader("X-Content-Type-Options"));
new MockFilterChain().doFilter(req, res);
});
assertEquals("nosniff", seenInChain.get(), "체인 진입 시점에 헤더가 있어야 한다");
assertEquals("nosniff", response.getHeader("X-Content-Type-Options"));
assertNotNull(response.getHeader("Cache-Control"));
}
@Test
@DisplayName("기본(lazy) 모드에서는 체인 실행 시점에 헤더가 아직 없다 - eager 설정이 실제로 의미 있음을 고정")
void lazyDoesNotWriteHeadersBeforeChain() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/file/download");
MockHttpServletResponse response = new MockHttpServletResponse();
AtomicReference<String> seenInChain = new AtomicReference<>();
filter(false).doFilter(request, response, (req, res) -> {
seenInChain.set(((HttpServletResponse) res).getHeader("X-Content-Type-Options"));
new MockFilterChain().doFilter(req, res);
});
assertNull(seenInChain.get(), "lazy 모드는 체인 이후에 헤더를 쓴다");
assertEquals("nosniff", response.getHeader("X-Content-Type-Options"));
}
}
@@ -0,0 +1,55 @@
package com.eactive.apim.portal.djb.testcleanup.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.test.util.ReflectionTestUtils;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class OrphanCleanupServiceTest {
@Mock private Environment environment;
@Mock private EntityManager entityManager;
@Mock private Query query;
private OrphanCleanupService service;
@BeforeEach
void setUp() {
service = new OrphanCleanupService(environment);
ReflectionTestUtils.setField(service, "entityManager", entityManager);
when(environment.acceptsProfiles(Profiles.of("stage", "prod"))).thenReturn(false);
when(entityManager.createNativeQuery(org.mockito.ArgumentMatchers.anyString())).thenReturn(query);
when(query.executeUpdate()).thenReturn(0);
}
@Test
void sweep_runsAllThirteenTablesAndReportsCounts() {
when(query.executeUpdate()).thenReturn(3);
TestCleanupResult result = service.sweep();
assertEquals(13, result.getDeletedCounts().size());
result.getDeletedCounts().values().forEach(count -> assertEquals(3L, count));
}
@Test
void sweep_prodProfile_throws() {
when(environment.acceptsProfiles(Profiles.of("stage", "prod"))).thenReturn(true);
assertThrows(IllegalStateException.class, () -> service.sweep());
}
}
@@ -0,0 +1,194 @@
package com.eactive.apim.portal.djb.testcleanup.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.eactive.apim.portal.app.repository.CredentialRepository;
import com.eactive.apim.portal.apps.community.partnership.repository.PartnershipApplicationRepository;
import com.eactive.apim.portal.apps.community.qna.repository.InquiryRepository;
import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
import com.eactive.apim.portal.djb.community.qna.comment.repository.InquiryCommentRepository;
import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestApiRepository;
import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestEventRepository;
import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestRepository;
import com.eactive.apim.portal.djb.webhook.repository.entity.WebhookRequest;
import com.eactive.apim.portal.djb.webhook.service.WebhookService;
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.repository.PortalUserPrivacyAgreementRepository;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.apim.portal.portaluser.repository.UserPasswordHistoryRepository;
import com.eactive.apim.portal.portaluser.repository.UserRoleHistoryRepository;
import com.eactive.apim.portal.user.repository.UserLogRepository;
import java.util.Collections;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class TestCleanupServiceTest {
@Mock private Environment environment;
@Mock private PortalOrgRepository portalOrgRepository;
@Mock private PortalUserRepository portalUserRepository;
@Mock private InquiryRepository inquiryRepository;
@Mock private InquiryCommentRepository inquiryCommentRepository;
@Mock private PartnershipApplicationRepository partnershipApplicationRepository;
@Mock private UserRoleHistoryRepository userRoleHistoryRepository;
@Mock private PortalUserPrivacyAgreementRepository portalUserPrivacyAgreementRepository;
@Mock private UserPasswordHistoryRepository userPasswordHistoryRepository;
@Mock private UserLogRepository userLogRepository;
@Mock private CredentialRepository credentialRepository;
@Mock private WebhookRequestRepository webhookRequestRepository;
@Mock private WebhookRequestApiRepository webhookRequestApiRepository;
@Mock private WebhookRequestEventRepository webhookRequestEventRepository;
@Mock private WebhookService webhookService;
@Mock private TestCleanupNativeQueries nativeQueries;
private TestCleanupService service;
private static final String ORG_ID = "ORG1";
private static final String USER_ID = "USER1";
private static final String LOGIN_ID = "user1@test.com";
private static final String COMP_REG_NO = "1234567890";
private static final String EMAIL = "user1@test.com";
@BeforeEach
void setUp() {
service = new TestCleanupService(environment, portalOrgRepository, portalUserRepository,
inquiryRepository, inquiryCommentRepository, partnershipApplicationRepository,
userRoleHistoryRepository, portalUserPrivacyAgreementRepository, userPasswordHistoryRepository,
userLogRepository, credentialRepository, webhookRequestRepository, webhookRequestApiRepository,
webhookRequestEventRepository, webhookService, nativeQueries);
when(environment.acceptsProfiles(Profiles.of("stage", "prod"))).thenReturn(false);
}
private PortalUser user() {
PortalUser u = new PortalUser();
u.setId(USER_ID);
u.setLoginId(LOGIN_ID);
return u;
}
@Test
void deleteOrgCascade_notFound() {
when(portalOrgRepository.findByCompRegNo(COMP_REG_NO)).thenReturn(Optional.empty());
TestCleanupResult result = service.deleteOrgCascade(COMP_REG_NO);
assertFalse(result.isFound());
assertTrue(result.getDeletedCounts().isEmpty());
verifyNoInteractions(webhookService, portalUserRepository);
}
@Test
void deleteOrgCascade_found_cascadesUsersAndOrg() {
PortalOrg org = org();
PortalUser member = user();
when(portalOrgRepository.findByCompRegNo(COMP_REG_NO)).thenReturn(Optional.of(org));
when(portalUserRepository.findAllByPortalOrg_Id(ORG_ID)).thenReturn(Collections.singletonList(member));
when(webhookRequestRepository.findByOrgId(ORG_ID)).thenReturn(Optional.empty());
when(nativeQueries.deleteCredentialApiByOrgId(ORG_ID)).thenReturn(2);
when(credentialRepository.deleteByOrgid(ORG_ID)).thenReturn(1L);
TestCleanupResult result = service.deleteOrgCascade(COMP_REG_NO);
assertTrue(result.isFound());
assertEquals(ORG_ID, result.getTargetId());
assertEquals(1L, result.getDeletedCounts().get("PTL_USER"));
assertEquals(2L, result.getDeletedCounts().get("PTL_CREDENTIAL_API"));
assertEquals(1L, result.getDeletedCounts().get("PTL_CREDENTIAL"));
assertEquals(1L, result.getDeletedCounts().get("PTL_ORG"));
assertEquals(0L, result.getDeletedCounts().get("PTL_WEBHOOK_REQ"));
verify(inquiryCommentRepository).deleteByInquiry_Inquirer_Id(USER_ID);
verify(inquiryRepository).deleteByInquirer_Id(USER_ID);
verify(userRoleHistoryRepository).deleteByUserId(LOGIN_ID);
verify(userPasswordHistoryRepository).deleteByUserId(USER_ID);
verify(portalUserRepository).delete(member);
verify(webhookService, never()).delete(anyLong(), anyString());
verify(portalOrgRepository).delete(org);
}
@Test
void deleteOrgCascade_withWebhook_delegatesToWebhookService() {
PortalOrg org = org();
WebhookRequest webhook = new WebhookRequest();
webhook.setId(99L);
when(portalOrgRepository.findByCompRegNo(COMP_REG_NO)).thenReturn(Optional.of(org));
when(portalUserRepository.findAllByPortalOrg_Id(ORG_ID)).thenReturn(Collections.emptyList());
when(webhookRequestRepository.findByOrgId(ORG_ID)).thenReturn(Optional.of(webhook));
when(webhookRequestApiRepository.findByWebhookReqId(99L)).thenReturn(Collections.emptyList());
when(webhookRequestEventRepository.findByWebhookReqId(99L)).thenReturn(Collections.emptyList());
TestCleanupResult result = service.deleteOrgCascade(COMP_REG_NO);
verify(webhookService, times(1)).delete(99L, ORG_ID);
assertEquals(1L, result.getDeletedCounts().get("PTL_WEBHOOK_REQ"));
}
@Test
void deleteUserCascade_notFound() {
when(portalUserRepository.findPortalUserByEmailAddr(EMAIL)).thenReturn(Optional.empty());
TestCleanupResult result = service.deleteUserCascade(EMAIL);
assertFalse(result.isFound());
assertTrue(result.getDeletedCounts().isEmpty());
}
@Test
void deleteUserCascade_found_deletesOrphanDataAndUser() {
PortalUser target = user();
when(portalUserRepository.findPortalUserByEmailAddr(EMAIL)).thenReturn(Optional.of(target));
TestCleanupResult result = service.deleteUserCascade(EMAIL);
assertTrue(result.isFound());
assertEquals(USER_ID, result.getTargetId());
verify(inquiryCommentRepository).deleteByInquiry_Inquirer_Id(USER_ID);
verify(inquiryRepository).deleteByInquirer_Id(USER_ID);
verify(partnershipApplicationRepository).deleteByCreatedBy(USER_ID);
verify(userRoleHistoryRepository).deleteByUserId(LOGIN_ID);
verify(portalUserPrivacyAgreementRepository).deleteByCreatedBy(USER_ID);
verify(userPasswordHistoryRepository).deleteByUserId(USER_ID);
verify(userLogRepository).deleteByLoginId(LOGIN_ID);
verify(portalUserRepository).delete(target);
}
@Test
void deleteOrgCascade_prodProfile_throws() {
when(environment.acceptsProfiles(Profiles.of("stage", "prod"))).thenReturn(true);
assertThrows(IllegalStateException.class, () -> service.deleteOrgCascade(COMP_REG_NO));
}
@Test
void deleteUserCascade_prodProfile_throws() {
when(environment.acceptsProfiles(Profiles.of("stage", "prod"))).thenReturn(true);
assertThrows(IllegalStateException.class, () -> service.deleteUserCascade(EMAIL));
}
private PortalOrg org() {
PortalOrg o = new PortalOrg();
o.setId(ORG_ID);
return o;
}
}