diff --git a/Deployment.md b/Deployment.md index 527fe2704..9312f42cc 100644 --- a/Deployment.md +++ b/Deployment.md @@ -1,14 +1,58 @@ --- -title: 部署 +部署 --- -## 0 demo +# 0. 在线体验 Demo - http://datart-demo.retech.cc - 用户名:demo - 密码:123456 -## 1 环境准备 +# 1. Docker 部署 + +```shell +docker run -p 8080:8080 datart/datart +``` +启动后可访问 +默认账户:用户名`demo`,密码`123456` + +## 1.1. 配置外部数据库 +在没有外部数据库配置的情况下,Datart使用H2作为应用程序数据库。 强烈建议您将自己的Mysql数据库配置为应用程序数据库。 + +创建空文件 `datart.conf` ,将以下内容粘贴到到文件中。 + +```shell +# 数据库连接配置 +datasource.ip= +datasource.port= +datasource.database= +datasource.username= +datasource.password= + +# server +server.port=8080 +server.address=0.0.0.0 + +# datart config +datart.address=http://127.0.0.1 +datart.send-mail=false +datart.webdriver-path=http://127.0.0.1:4444/wd/hub +``` + +运行 `docker run -d --name datart -v your_path/datart.conf:/datart/config/datart.conf -p 8080:8080 datart/datart` + +## 1.2. 将用户文件挂载到外部 + +默认配置下,用户文件(头像,文件数据源等)保存在 `files` 文件夹下,将这个路径挂载到外部,以在进行应用升级时,能够保留这些文件。 + +在配置文件中增加参数 `-v your_path/files:/datart/files` 即可。以下是完整命令 + +`docker run -d --name datart -v your_path/datart.conf:/datart/config/datart.conf -v your_path/files:/datart/files -p 8080:8080 datart/datart` + +***更多配置,访问 *** + +# 2. 本地部署 +## 2.1. 环境准备 - JDK 1.8+ - MySql5.7+ @@ -40,35 +84,49 @@ unzip datart-server-1.0.0-beta.x-install.zip ``` -## 2 初始化数据库 +## 2.2. 以独立模式运行 + +安装包解压后,即可运行 ./bin/datart-server.sh start 来启动datart,启动后默认访问地址是: ,默认用户`demo/123456` + +***独立模式使用内置数据库作为应用数据库,数据的安全性和数据迁移无法保证,建议配置外部数据库作为应用数据库*** + +## 2.3. 配置外部数据库,要求Mysql5.7及以上版本。 -- 创建数据库,并将bin/datart.sql导入到数据库中 +- 创建数据库,指定数据库编码为utf8 ```bash mysql> CREATE DATABASE `datart` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'; -mysql> use datart; -mysql> source bin/datart.sql ``` -## 3 修改配置文件 +***注意:1.0.0-beta.2版本以前,需要手动执行`bin/datart.sql`来初始化数据库。此版本及以上版本,创建好数据库即可,在初次连接时会自动初始化数据库*** + +***首次连接数据库(或者版本升级)时,建议使用一个权限较高的数据库账号登录(如root账号)。因为首次连接会执行数据库初始化脚本,如果使用的数据库账号权限太低,会导致数据库初始化失败*** + +- 基础配置:配置文件位于 config/datart.conf -- 配置文件位于 config/application-config.yml.example,先重命名为application-config.yml ```bash - mv ${DATART_HOME}/config/application-config.yml.example ${DATART_HOME}/config/application-config.yml - - 需要修改的配置项: - 1. 数据库连接信息(必须) - 2. 邮件配置(注册需邮箱激活时必须) - 3. 浏览器截图驱动(可选-需要使用定时任务邮件发送图表截图时可配置) - 4. Redis(可选-需要使用缓存时可配置) - 具体配置见下述: - + 数据库配置(必填): + 1. datasource.ip(数据库IP地址) + 2. datasource.port(数据库端口数据库端口) + 3. datasource.database(指定数据库) + 4. datasource.username(用户名) + 5. datasource.password(密码) + + 其它配置(选填): + 1. server.port(应用绑定端口地址,默认8080) + 2. server.address(应用绑定IP地址,默认 0.0.0.0) + 3. datart.address(datart 外部可访问地址,默认http://127.0.0.1) + 4. datart.send-mail(用户注册是否使用邮件激活,默认 false ) + 5. datart.webdriver-path(截图驱动) ``` +## 2.4. 高级配置 (可选) : 配置文件位于 config/profiles/application-config.yml -### 3.1 配置文件信息 +***高级配置文件格式是yml格式,配置错误会导致程序无法启动。配置时一定要严格遵循yml格式。*** -***注:请务必保留连接串中的`allowMultiQueries=true`参数*** +***application-config.yml直接由spring-boot处理,其中的oauth2,redis,mail等配置项完全遵循spring-boot-autoconfigure配置*** + +### 2.4.1 配置文件信息 ```yaml spring: @@ -133,18 +191,17 @@ datart: env: file-path: ${user.dir}/files # 服务端文件保存位置 -# 可选配置 如需配置请参照 [3.2 截图配置 [ChromeWebDriver]-可选] + # 可选配置 如需配置请参照 [3.2 截图配置 [ChromeWebDriver]-可选] screenshot: timeout-seconds: 60 webdriver-type: CHROME - webdriver-path: "http://youip:4444/wd/hub" + webdriver-path: "http://youip:4444/wd/hub" ``` *注意:加密密钥每个服务端部署前应该进行修改,且部署后不能再次修改。如果是集群部署,同一个集群内的secret要保持统一* - -### 3.2 截图配置 [ChromeWebDriver]-可选 +### 2.4.2 截图配置 [ChromeWebDriver]-可选 ```bash @@ -154,7 +211,7 @@ docker run -p 4444:4444 -d --name selenium-chrome --shm-size="2g" selenium/stand ``` -### 4 启动服务 +### 2.5. 启动服务 *注意:启动脚本 已更新了 start|stop|status|restart* @@ -162,7 +219,7 @@ docker run -p 4444:4444 -d --name selenium-chrome --shm-size="2g" selenium/stand ${DATART_HOME}/bin/datart-server.sh (start|stop|status|restart) ``` -### 5 访问服务 +### 2.5 访问服务 *注意:没有默认用户 直接注册 成功后直接登录即可* diff --git a/Dockerfile b/Dockerfile index d986d1d69..2b96657f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ FROM java:8 LABEL "author"="tl" RUN mkdir /datart -COPY ./bin/* /datart/bin/ -COPY ./config/* /datart/config/ -COPY ./lib/* /datart/lib/ +COPY ./bin/ /datart/bin/ +COPY ./config/ /datart/config/ +COPY ./lib/ /datart/lib/ COPY static /datart/static ENV TZ=Asia/Shanghai -EXPOSE 58080 +EXPOSE 8080 WORKDIR /datart ENTRYPOINT java -cp "lib/*" datart.DatartServerApplication \ No newline at end of file diff --git a/bin/datart-demo.cmd b/bin/datart-demo.cmd deleted file mode 100644 index 9a429512c..000000000 --- a/bin/datart-demo.cmd +++ /dev/null @@ -1,29 +0,0 @@ -@echo off - -REM Datart -REM

-REM Copyright 2021 -REM

-REM Licensed under the Apache License, Version 2.0 (the "License"); -REM you may not use this file except in compliance with the License. -REM You may obtain a copy of the License at -REM

-REM http://www.apache.org/licenses/LICENSE-2.0 -REM

-REM Unless required by applicable law or agreed to in writing, software -REM distributed under the License is distributed on an "AS IS" BASIS, -REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -REM See the License for the specific language governing permissions and -REM limitations under the License. - - -if "%1"=="start" goto START - - -:START - -cd /d %~dp0 - -cd .. - -java -server -Xms2G -Xmx2G -Dspring.profiles.active=demo -Dfile.encoding=UTF-8 -cp ".\lib\*" datart.DatartServerApplication \ No newline at end of file diff --git a/bin/datart-demo.sh b/bin/datart-demo.sh deleted file mode 100644 index cf6be4161..000000000 --- a/bin/datart-demo.sh +++ /dev/null @@ -1,112 +0,0 @@ -#!/bin/bash - -# Datart -#

-# Copyright 2021 -#

-# Licensed 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. - -BASE_DIR=$(cd "$(dirname "$0")/.."; pwd -P) - -echo "working dir ${BASE_DIR}" - -cd "${BASE_DIR}" - -CLASS_PATH="${BASE_DIR}/lib/*" - -START_CLASS="datart.DatartServerApplication" - -# java -server -Xms2G -Xmx2G -Dspring.profiles.active=demo -Dfile.encoding=UTF-8 -cp "${CLASS_PATH}" datart.DatartServerApplication - -datart_status(){ - result=`ps -ef | awk '/DatartServerApplication/ && !/awk/{print $2}' | wc -l` - - if [[ $result -eq 0 ]]; then - return 0 - else - return 1 - fi - } - -datart_start(){ - source ~/.bashrc - datart_status >/dev/null 2>&1 - if [[ $? -eq 0 ]]; then - - nohup java -server -Xms2G -Xmx2G -Dspring.profiles.active=demo -Dfile.encoding=UTF-8 -cp "${CLASS_PATH}" "${START_CLASS}" & - - else - echo "" - PID=`ps -ef | awk '/DatartServerApplication/ && !/awk/{print $2}'` - echo "Datart is Running Now..... PID is ${PID} " - fi -} - - -datart_stop(){ - datart_status >/dev/null 2>&1 - if [[ $? -eq 0 ]]; then - echo "" - echo "Datart is not Running....." - echo "" - else - ps -ef | awk '/DatartServerApplication/ && !/awk/{print $2}'| xargs kill -9 - - fi -} - - -case $1 in - start ) - echo "" - echo "Datart Starting........... " - echo "" - datart_start - ;; - - stop ) - echo "" - - echo "Datart Stoping.......... " - - echo "" - datart_stop - ;; - - restart ) - echo "Datart is Restarting.......... " - datart_stop - echo "" - datart_start - echo "Datart is Starting.......... " - - ;; - - status ) - datart_status>/dev/null 2>&1 - if [[ $? -eq 0 ]]; then - echo "" - echo "Datart is not Running......" - echo "" - else - echo "" - PID=`ps -ef | awk '/DatartServerApplication/ && !/awk/{print $2}'` - echo "Datart is Running..... PID is ${PID}" - echo "" - fi - ;; - - * ) - echo "Usage: datart-demo.sh (start|stop|status|restart)" - -esac diff --git a/bin/h2/datart.demo.mv.db b/bin/h2/datart.demo.mv.db index 4eec49f80..7eb302ba9 100644 Binary files a/bin/h2/datart.demo.mv.db and b/bin/h2/datart.demo.mv.db differ diff --git a/bin/h2/datart.demo.trace.db b/bin/h2/datart.demo.trace.db deleted file mode 100644 index e69de29bb..000000000 diff --git a/config/application-config.yml.example b/config/application-config.yml.example deleted file mode 100644 index ff2f40b29..000000000 --- a/config/application-config.yml.example +++ /dev/null @@ -1,67 +0,0 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - type: com.alibaba.druid.pool.DruidDataSource - url: jdbc:mysql://{IP:PORT}/datart?&allowMultiQueries=true - username: { USERNAME } - password: { PASSWORD } - -# mail config - -# mail: -# host: { 邮箱服务地址 } -# port: { 端口号 } -# username: { 邮箱地址 } -# fromAddress: -# password: { 邮箱服务密码 } -# senderName: { 发送者昵称 } -# -# properties: -# smtp: -# starttls: -# enable: true -# required: true -# auth: true -# mail: -# smtp: -# ssl: -# enable: true - - -# redis config - -# redis: -# port: 6379 -# host: { HOST } - - -server: - port: { PORT } - address: { IP } - - # 开启 gzip 压缩,加快请求和响应速度 - compression: - enabled: true - mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* - - -datart: - server: - address: http://{IP/域名}:{端口} - - user: - active: - send-mail: true # 注册用户时是否需要邮件验证激活 - - security: - token: - secret: "d@a$t%a^r&a*t" #加密密钥 - timeout-min: 30 # 登录会话有效时长,单位:分钟。 - - env: - file-path: ${user.dir}/files # 服务端文件保存位置 - - screenshot: - timeout-seconds: 60 - webdriver-type: CHROME - webdriver-path: { Web Driver Path } diff --git a/config/datart.conf b/config/datart.conf new file mode 100644 index 000000000..e27b77cdd --- /dev/null +++ b/config/datart.conf @@ -0,0 +1,17 @@ +# this file has the highest priority, if val is not blank, then will replace the config + +# datasource config +datasource.ip= +datasource.port= +datasource.database= +datasource.username= +datasource.password= + +# server +server.port=8080 +server.address=0.0.0.0 + +# datart config +datart.address=http://127.0.0.1 +datart.send-mail=false +datart.webdriver-path=http://127.0.0.1:4444/wd/hub \ No newline at end of file diff --git a/config/jdbc-driver-ext.yml b/config/jdbc-driver-ext.yml index cea52bf9a..82ebec295 100644 --- a/config/jdbc-driver-ext.yml +++ b/config/jdbc-driver-ext.yml @@ -14,4 +14,18 @@ IMPALA: identifier-quote: "`" adapter-class: "datart.data.provider.jdbc.adapters.ImpalaDataProviderAdapter" url-prefix: "jdbc:impala://" - sql-dialect: "datart.data.provider.calcite.dialect.ImpalaSqlDialectSupport" \ No newline at end of file + sql-dialect: "datart.data.provider.calcite.dialect.ImpalaSqlDialectSupport" + + +ORACLE: + quote-identifiers: false + +DORIS: + db-type: "doris" + name: "doris" + driver-class: "com.mysql.cj.jdbc.Driver" + literal-quote: "'" + identifier-quote: "`" + adapter-class: "datart.data.provider.jdbc.adapters.DorisDataProviderAdapter" + url-prefix: "jdbc:mysql://" + diff --git a/config/logback.xml b/config/logback.xml index 4d0e315b7..750c358f6 100644 --- a/config/logback.xml +++ b/config/logback.xml @@ -1,6 +1,7 @@ + @@ -22,13 +23,27 @@ + + + ${LOG_HOME}/%d{yyyy-MM-dd}-sql.log + 30 + + + UTF-8 + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} : %msg%n + + + - - + + + + + \ No newline at end of file diff --git a/config/profiles/application-config.yml b/config/profiles/application-config.yml new file mode 100644 index 000000000..0e94a00a9 --- /dev/null +++ b/config/profiles/application-config.yml @@ -0,0 +1,85 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + type: com.alibaba.druid.pool.DruidDataSource + url: jdbc:mysql://${datasource.ip:null}:${datasource.port:3306}/${datasource.database:datart}?&allowMultiQueries=true&characterEncoding=utf-8 + username: ${datasource.username:root} + password: ${datasource.password:123456} + +# security: +# oauth2: +# client: +# registration: +# cas: +# provider: cas +# client-id: "xxxxx" +# client-name: "Sign in with CAS" +# client-secret: "xxx" +# authorization-grant-type: authorization_code +# client-authentication-method: post +# redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" +# scope: userinfo +# provider: +# cas: +# authorization-uri: https://cas.xxx.com/cas/oauth2.0/authorize +# token-uri: https://cas.xxx.com/cas/oauth2.0/accessToken +# user-info-uri: https://cas.xxx.com/cas/oauth2.0/profile +# user-name-attribute: id +# userMapping: +# email: "attributes.email" +# name: "attributes.name" +# avatar: "attributes.avatar" + +# mail config + +# mail: +# host: { 邮箱服务地址 } +# port: { 端口号 } +# username: { 邮箱地址 } +# fromAddress: +# password: { 邮箱服务密码 } +# senderName: { 发送者昵称 } +# +# properties: +# smtp: +# starttls: +# enable: true +# required: true +# auth: true +# mail: +# smtp: +# ssl: +# enable: true + + +# redis config + +# redis: +# port: 6379 +# host: { HOST } + + +server: + port: ${server.port:8080} + address: ${server.ip:0.0.0.0} + +datart: + server: + address: ${datart.address:http://127.0.0.1:8080} + + user: + active: + send-mail: ${datart.send-mail:false} # 注册用户时是否需要邮件验证激活 + + security: + token: + secret: "d@a$t%a^r&a*t" #加密密钥 + timeout-min: 30 # 登录会话有效时长,单位:分钟。 + + env: + file-path: ${user.dir}/files # 服务端文件保存位置 + + screenshot: + timeout-seconds: 60 + webdriver-type: CHROME + webdriver-path: ${datart.webdriver-path:} diff --git a/core/pom.xml b/core/pom.xml index d970d1048..2d8d20934 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -5,7 +5,7 @@ datart-parent datart - 1.0.0-beta.1 + 1.0.0-beta.2 4.0.0 diff --git a/core/src/main/java/datart/core/base/consts/Const.java b/core/src/main/java/datart/core/base/consts/Const.java index e1c15f88a..db17bb176 100644 --- a/core/src/main/java/datart/core/base/consts/Const.java +++ b/core/src/main/java/datart/core/base/consts/Const.java @@ -31,6 +31,11 @@ public class Const { public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final String FILE_SUFFIX_DATE_FORMAT = "yyyy-MM-dd_HH-mm-ss-SSS"; + + // 数据库schema最短同步时间间隔 + public static final Integer MINIMUM_SYNC_INTERVAL = 60; + /** * 正则表达式 */ diff --git a/core/src/main/java/datart/core/base/consts/Currency.java b/core/src/main/java/datart/core/base/consts/Currency.java new file mode 100644 index 000000000..1aae77861 --- /dev/null +++ b/core/src/main/java/datart/core/base/consts/Currency.java @@ -0,0 +1,33 @@ +package datart.core.base.consts; + +public enum Currency { + CNY("¥"), + USD("$", "US"), + GBP("£"), + AUD("$", "AU"), + EUR("€"), + JPY("¥", "JP"), + CAD("$", "CA"); + + private String unit; + + private String prefix; + + Currency(String unit) { + this.unit = unit; + this.prefix = ""; + } + + Currency(String unit, String prefix) { + this.unit = unit; + this.prefix = prefix; + } + + public String getPrefix() { + return prefix; + } + + public String getUnit() { + return unit; + } +} diff --git a/core/src/main/java/datart/core/base/consts/JavaType.java b/core/src/main/java/datart/core/base/consts/JavaType.java index c64584456..387ea2382 100644 --- a/core/src/main/java/datart/core/base/consts/JavaType.java +++ b/core/src/main/java/datart/core/base/consts/JavaType.java @@ -18,8 +18,14 @@ public enum JavaType { DOUBLE, + BIGDECIMAL, + STRING, DATE, + LOCALDATE, + + LOCALDATETIME + } diff --git a/core/src/main/java/datart/core/base/consts/UnitKey.java b/core/src/main/java/datart/core/base/consts/UnitKey.java new file mode 100644 index 000000000..edf1b4eb5 --- /dev/null +++ b/core/src/main/java/datart/core/base/consts/UnitKey.java @@ -0,0 +1,45 @@ +package datart.core.base.consts; + +public enum UnitKey { + + NONE("none", 1, ""), + THOUSAND("thousand", 1000, "K"), + TEN_THOUSAND("wan", 10_000, "万"), + MILLION("million", 1000_000, "M"), + HUNDRED_MILLION("yi", 100_000_000, "亿"), + BILLION("billion", 1000_000_000, "B"); + + private String value; + + private int unit; + + private String fmt; + + UnitKey(String value, int i, String str) { + this.value = value; + this.unit = i; + this.fmt = str; + } + + public String getValue() { + return value; + } + + public int getUnit() { + return unit; + } + + public String getFmt() { + return fmt; + } + + public static UnitKey getUnitKeyByValue(String value){ + for (UnitKey unitKey : UnitKey.values()) { + if (unitKey.getValue().equals(value)){ + return unitKey; + } + } + return NONE; + } + +} diff --git a/core/src/main/java/datart/core/common/Application.java b/core/src/main/java/datart/core/common/Application.java index 70e510a55..be0437c88 100644 --- a/core/src/main/java/datart/core/common/Application.java +++ b/core/src/main/java/datart/core/common/Application.java @@ -71,7 +71,7 @@ public static String getWebRootURL() { } public static String getApiPrefix() { - return getProperty("datart.path-prefix"); + return getProperty("datart.server.path-prefix"); } public static String getTokenSecret() { diff --git a/core/src/main/java/datart/core/common/CSVParse.java b/core/src/main/java/datart/core/common/CSVParse.java index ea109589c..70f52d03d 100644 --- a/core/src/main/java/datart/core/common/CSVParse.java +++ b/core/src/main/java/datart/core/common/CSVParse.java @@ -74,7 +74,7 @@ public List> parse() throws IOException { .collect(Collectors.toList()); // remove utf-8-with-bom char String start = values.get(0).get(0).toString(); - if (start.charAt(0) == '\uFEFF') { + if (start.length() > 0 && start.charAt(0) == '\uFEFF') { values.get(0).set(0, start.substring(1)); } return values; diff --git a/core/src/main/java/datart/core/common/POIUtils.java b/core/src/main/java/datart/core/common/POIUtils.java index bbd1711f5..ddf69ee19 100644 --- a/core/src/main/java/datart/core/common/POIUtils.java +++ b/core/src/main/java/datart/core/common/POIUtils.java @@ -18,24 +18,30 @@ package datart.core.common; import datart.core.base.consts.FileFormat; -import datart.core.base.exception.BaseException; +import datart.core.base.consts.JavaType; import datart.core.base.exception.Exceptions; import datart.core.data.provider.Column; import datart.core.data.provider.Dataframe; +import datart.core.entity.poi.ColumnSetting; +import datart.core.entity.poi.POISettings; +import datart.core.entity.poi.format.PoiNumFormat; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xssf.streaming.SXSSFWorkbook; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.poi.xssf.usermodel.*; import java.io.*; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; +import java.math.BigDecimal; +import java.util.*; @Slf4j public class POIUtils { + private static IndexedColorMap indexedColorMap = new DefaultIndexedColorMap(); + public static void save(Workbook workbook, String path, boolean cover) throws IOException { if (workbook == null || path == null) { return; @@ -58,7 +64,7 @@ public static void save(Workbook workbook, String path, boolean cover) throws IO public static Workbook fromTableData(Dataframe dataframe) { Workbook workbook = new SXSSFWorkbook(); Sheet sheet = workbook.createSheet(); - fillSheet(sheet, dataframe); + fillSheet(sheet, dataframe, 1); return workbook; } @@ -66,28 +72,87 @@ public static Workbook createEmpty() { return new SXSSFWorkbook(); } - public static void withSheet(Workbook workbook, String sheetName, Dataframe sheetData) { - fillSheet(workbook.createSheet(sheetName), sheetData); + public static void withSheet(Workbook workbook, String sheetName, Dataframe sheetData, POISettings poiSettings) { + Sheet sheet = workbook.createSheet(sheetName); + int rowNum = writeHeaderRows(sheet, poiSettings.getHeaderRows()); + fillSheetWithSetting(sheet, sheetData, poiSettings.getColumnSetting(), rowNum+1); + mergeSheetCell(sheet, poiSettings.getMergeCells()); + setColumnWidth(sheet, poiSettings.getColumnSetting()); + } + + private static void setColumnWidth(Sheet sheet, Map columnSetting) { + for (Integer num : columnSetting.keySet()) { + ColumnSetting setting = columnSetting.get(num); + sheet.setColumnWidth(setting.getIndex(), setting.getWidth()); + } + } + + private static int writeHeaderRows(Sheet sheet, Map> headerRows) { + CellStyle cellStyle = getHeaderCellStyle(sheet); + for (int i = 0; i < headerRows.size(); i++) { + writeHeader(sheet, headerRows.get(i), i, cellStyle); + } + return headerRows.size()-1; } - private static void fillSheet(Sheet sheet, Dataframe data) { - writeHeader(data.getColumns(), sheet); - int rowIndex = 1; + private static void writeHeader(Sheet sheet, List columns, int rowNum, CellStyle cellStyle) { + Row row = sheet.createRow(rowNum); + for (int i = 0; i < columns.size(); i++) { + Cell cell = row.createCell(i); + cell.setCellValue(columns.get(i).getName()); + cell.setCellStyle(cellStyle); + } + } + + private static void fillSheet(Sheet sheet, Dataframe data, int rowIndex) { for (List dataRow : data.getRows()) { Row row = sheet.createRow(rowIndex); - int columnIndex = 0; - for (Object val : dataRow) { - row.createCell(columnIndex).setCellValue(val == null ? null : val.toString()); - columnIndex++; + for (int i = 0; i < dataRow.size(); i++) { + Object val = dataRow.get(i); + Cell cell = row.createCell(i); + cell.setCellStyle(getCellStyle(sheet, val, "")); + setCellValue(cell, val); } rowIndex++; } } - private static void writeHeader(List columns, Sheet sheet) { - Row row = sheet.createRow(0); - for (int i = 0; i < columns.size(); i++) { - row.createCell(i).setCellValue(columns.get(i).getName()); + private static void fillSheetWithSetting(Sheet sheet, Dataframe data, Map columnSetting, int rowIndex) { + for (List dataRow : data.getRows()) { + Row row = sheet.createRow(rowIndex); + for (int i = 0; i < dataRow.size(); i++) { + Object val = dataRow.get(i); + int columnIndex = i; + String fmt = ""; + CellStyle cellStyle = null; + if (columnSetting.containsKey(i)){ + ColumnSetting setting = columnSetting.get(i); + columnIndex = setting.getIndex(); + PoiNumFormat numFormat = setting.getNumFormat(); + fmt = numFormat.getFormat(); + setting.setLength(val==null ? 0 : Math.max(val.toString().length(), setting.getLength())); + val = numFormat.parseValue(val); + cellStyle = setting.getCellStyle()==null ? getCellStyle(sheet, val, fmt) : setting.getCellStyle(); + setting.setCellStyle(cellStyle); + } else { + ColumnSetting setting = new ColumnSetting(); + setting.setIndex(i); + setting.setNumFormat(new PoiNumFormat()); + setting.setLength(val==null ? 0 : val.toString().length()); + setting.setCellStyle(getCellStyle(sheet, val, fmt)); + columnSetting.put(i, setting); + } + Cell cell = row.createCell(columnIndex); + cell.setCellStyle(cellStyle); + setCellValue(cell, val); + } + rowIndex++; + } + } + + private static void mergeSheetCell(Sheet sheet, List mergeCells) { + for (CellRangeAddress cellRangeAddress : mergeCells) { + sheet.addMergedRegion(cellRangeAddress); } } @@ -132,6 +197,9 @@ private static Object readCellValue(Cell cell) { } switch (cell.getCellType()) { case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)){ + return cell.getDateCellValue(); + } return cell.getNumericCellValue(); case BOOLEAN: return cell.getBooleanCellValue(); @@ -140,4 +208,67 @@ private static Object readCellValue(Cell cell) { } } + private static CellStyle getCellStyle(Sheet sheet, Object val, String fmt){ + CellStyle cellStyle = sheet.getWorkbook().createCellStyle(); + DataFormat dataFormat = sheet.getWorkbook().createDataFormat(); + if (StringUtils.isNotBlank(fmt)){ + cellStyle.setDataFormat(dataFormat.getFormat(fmt)); + } else if (val instanceof Number){ + cellStyle.setDataFormat(dataFormat.getFormat("0")); + } else if (val instanceof Date){ + cellStyle.setDataFormat(dataFormat.getFormat(DateUtils.inferDateFormat(val.toString()))); + } else { + cellStyle.setDataFormat(dataFormat.getFormat("General")); + } + return cellStyle; + } + + private static CellStyle getHeaderCellStyle(Sheet sheet){ + XSSFCellStyle cellStyle = (XSSFCellStyle) sheet.getWorkbook().createCellStyle(); + cellStyle.setBorderTop(BorderStyle.THIN); + cellStyle.setBorderRight(BorderStyle.THIN); + cellStyle.setBorderBottom(BorderStyle.THIN); + cellStyle.setBorderLeft(BorderStyle.THIN); + XSSFColor grayColor = new XSSFColor(new java.awt.Color(220, 220, 220), indexedColorMap); + XSSFColor whiteColor = new XSSFColor(new java.awt.Color(240, 240, 240), indexedColorMap); + cellStyle.setTopBorderColor(whiteColor); + cellStyle.setRightBorderColor(whiteColor); + cellStyle.setBottomBorderColor(whiteColor); + cellStyle.setLeftBorderColor(whiteColor); + cellStyle.setFillForegroundColor(grayColor); + cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + cellStyle.setVerticalAlignment(VerticalAlignment.CENTER); + return cellStyle; + } + + private static void setCellValue(Cell cell, Object val){ + if (val == null) { + cell.setCellValue(""); + return ; + } + try { + JavaType javaType = JavaType.valueOf(val.getClass().getSimpleName().toUpperCase()); + switch (javaType) { + case BIGDECIMAL: + case BYTE: + case SHORT: + case INTEGER: + case LONG: + case FLOAT: + case DOUBLE: + cell.setCellValue(new BigDecimal(val.toString()).doubleValue()); + break; + case BOOLEAN: + cell.setCellValue((Boolean) val); + break; + case DATE: + default: + cell.setCellValue(val.toString()); + break; + } + } catch (IllegalArgumentException e) { + cell.setCellValue(val.toString()); + } + } + } diff --git a/core/src/main/java/datart/core/common/RequestContext.java b/core/src/main/java/datart/core/common/RequestContext.java index 6e2680475..de7421ca2 100644 --- a/core/src/main/java/datart/core/common/RequestContext.java +++ b/core/src/main/java/datart/core/common/RequestContext.java @@ -25,6 +25,10 @@ public class RequestContext { private static final InheritableThreadLocal> exceptions = new InheritableThreadLocal<>(); + private static final InheritableThreadLocal sql = new InheritableThreadLocal<>(); + + private static final InheritableThreadLocal scriptPermission = new InheritableThreadLocal<>(); + public static void putWarning(String name, Exception exception) { Map exceptionMap = exceptions.get(); if (exceptionMap == null) { @@ -40,6 +44,26 @@ public static Map getWarnings() { public static void clean() { exceptions.remove(); + sql.set(null); + scriptPermission.set(null); + } + + public static void setSql(String sqlStr) { + if (scriptPermission.get() != null && scriptPermission.get()) { + sql.set(sqlStr); + } + } + + public static void setScriptPermission(boolean permission) { + scriptPermission.set(permission); + } + + public static String getSql() { + return sql.get(); + } + + public static Boolean getScriptPermission() { + return scriptPermission.get(); } } diff --git a/core/src/main/java/datart/core/data/provider/Column.java b/core/src/main/java/datart/core/data/provider/Column.java index 6d84c71e2..63b09f528 100644 --- a/core/src/main/java/datart/core/data/provider/Column.java +++ b/core/src/main/java/datart/core/data/provider/Column.java @@ -22,6 +22,7 @@ import lombok.Data; import java.io.Serializable; +import java.util.List; @Data public class Column implements Serializable { @@ -32,11 +33,7 @@ public class Column implements Serializable { private String fmt; - private String pkDatabase; - - private String pkTable; - - private String pkColumn; + private List foreignKeys; public Column(String name, ValueType type) { this.name = name; diff --git a/server/src/main/java/datart/server/config/DubboConfig.java b/core/src/main/java/datart/core/data/provider/ForeignKey.java similarity index 73% rename from server/src/main/java/datart/server/config/DubboConfig.java rename to core/src/main/java/datart/core/data/provider/ForeignKey.java index 97f9988f7..6aff676f4 100644 --- a/server/src/main/java/datart/server/config/DubboConfig.java +++ b/core/src/main/java/datart/core/data/provider/ForeignKey.java @@ -16,12 +16,17 @@ * limitations under the License. */ -package datart.server.config; +package datart.core.data.provider; -//import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; -//import org.springframework.context.annotation.Configuration; +import lombok.Data; + +@Data +public class ForeignKey { + + private String database; + + private String table; + + private String column; -//@Configuration -//@EnableDubbo -public class DubboConfig { } diff --git a/core/src/main/java/datart/core/data/provider/SchemaInfo.java b/core/src/main/java/datart/core/data/provider/SchemaInfo.java new file mode 100644 index 000000000..875ffdaec --- /dev/null +++ b/core/src/main/java/datart/core/data/provider/SchemaInfo.java @@ -0,0 +1,40 @@ +/* + * Datart + *

+ * Copyright 2021 + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package datart.core.data.provider; + +import lombok.Data; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +@Data +public class SchemaInfo { + + private List schemaItems; + + private Date updateTime; + + public static SchemaInfo empty() { + SchemaInfo schemaInfo = new SchemaInfo(); + schemaInfo.setSchemaItems(Collections.emptyList()); + return schemaInfo; + } + +} diff --git a/core/src/main/java/datart/core/data/provider/SchemaItem.java b/core/src/main/java/datart/core/data/provider/SchemaItem.java new file mode 100644 index 000000000..dc6bf2f0a --- /dev/null +++ b/core/src/main/java/datart/core/data/provider/SchemaItem.java @@ -0,0 +1,32 @@ +/* + * Datart + *

+ * Copyright 2021 + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package datart.core.data.provider; + +import lombok.Data; + +import java.util.List; + +@Data +public class SchemaItem { + + private String dbName; + + private List tables; + +} diff --git a/server/src/main/java/datart/server/base/dto/TableInfo.java b/core/src/main/java/datart/core/data/provider/TableInfo.java similarity index 53% rename from server/src/main/java/datart/server/base/dto/TableInfo.java rename to core/src/main/java/datart/core/data/provider/TableInfo.java index 488382a13..0590002b4 100644 --- a/server/src/main/java/datart/server/base/dto/TableInfo.java +++ b/core/src/main/java/datart/core/data/provider/TableInfo.java @@ -1,19 +1,19 @@ -package datart.server.base.dto; +package datart.core.data.provider; -import datart.core.data.provider.Column; import lombok.Data; import java.util.List; +import java.util.Set; @Data public class TableInfo { - private String dbName; +// private String dbName; private String tableName; private List primaryKeys; - private List columns; + private Set columns; } diff --git a/core/src/main/java/datart/core/entity/Folder.java b/core/src/main/java/datart/core/entity/Folder.java index 37e1bd3e8..6d68ba99c 100644 --- a/core/src/main/java/datart/core/entity/Folder.java +++ b/core/src/main/java/datart/core/entity/Folder.java @@ -12,8 +12,12 @@ public class Folder extends BaseEntity { private String relType; + private String subType; + private String relId; + private String avatar; + private String parentId; private Double index; diff --git a/core/src/main/java/datart/core/entity/SourceSchemas.java b/core/src/main/java/datart/core/entity/SourceSchemas.java new file mode 100644 index 000000000..e5a25545c --- /dev/null +++ b/core/src/main/java/datart/core/entity/SourceSchemas.java @@ -0,0 +1,12 @@ +package datart.core.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class SourceSchemas extends BaseEntity { + private String sourceId; + + private String schemas; +} \ No newline at end of file diff --git a/core/src/main/java/datart/core/entity/ext/SourceDetail.java b/core/src/main/java/datart/core/entity/ext/SourceDetail.java new file mode 100644 index 000000000..8db6bf60f --- /dev/null +++ b/core/src/main/java/datart/core/entity/ext/SourceDetail.java @@ -0,0 +1,36 @@ +/* + * Datart + *

+ * Copyright 2021 + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package datart.core.entity.ext; + +import datart.core.entity.Source; +import lombok.Data; +import org.springframework.beans.BeanUtils; + +import java.util.Date; + +@Data +public class SourceDetail extends Source { + + private Date schemaUpdateDate; + + public SourceDetail(Source source) { + BeanUtils.copyProperties(source, this); + } + +} diff --git a/core/src/main/java/datart/core/entity/poi/ColumnSetting.java b/core/src/main/java/datart/core/entity/poi/ColumnSetting.java new file mode 100644 index 000000000..940144b8a --- /dev/null +++ b/core/src/main/java/datart/core/entity/poi/ColumnSetting.java @@ -0,0 +1,26 @@ +package datart.core.entity.poi; + +import datart.core.entity.poi.format.PoiNumFormat; +import lombok.Data; +import org.apache.poi.ss.usermodel.CellStyle; + +@Data +public class ColumnSetting { + + private int index; + + private PoiNumFormat numFormat; + + private int length; + + private CellStyle cellStyle; + + public void setLength(int length) { + this.length = Math.max(length, 8); + this.length = Math.min(this.length, 80); + } + + public int getWidth() { + return (numFormat.getFixLength()+length) * 300; + } +} diff --git a/core/src/main/java/datart/core/entity/poi/POISettings.java b/core/src/main/java/datart/core/entity/poi/POISettings.java new file mode 100644 index 000000000..c6b235b95 --- /dev/null +++ b/core/src/main/java/datart/core/entity/poi/POISettings.java @@ -0,0 +1,20 @@ +package datart.core.entity.poi; + +import datart.core.data.provider.Column; +import lombok.Data; +import org.apache.poi.ss.util.CellRangeAddress; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Data +public class POISettings { + + private Map columnSetting = new HashMap<>(); + + private Map> headerRows = new HashMap<>(); + + private List mergeCells = new ArrayList<>(); +} diff --git a/core/src/main/java/datart/core/entity/poi/format/CurrencyFormat.java b/core/src/main/java/datart/core/entity/poi/format/CurrencyFormat.java new file mode 100644 index 000000000..5e8af00bb --- /dev/null +++ b/core/src/main/java/datart/core/entity/poi/format/CurrencyFormat.java @@ -0,0 +1,56 @@ +package datart.core.entity.poi.format; + +import datart.core.base.consts.Currency; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +@Data +public class CurrencyFormat extends PoiNumFormat { + + public static final String type = "currency"; + + /** 前缀 */ + private String prefix; + + /** 货币类型 */ + private String currency; + + public CurrencyFormat() { + this.setUseThousandSeparator(true); + } + + public void setCurrency(String currency) { + this.currency = currency; + this.prefix = ""; + if (StringUtils.isNotBlank(this.currency)){ + Currency currencyType = Currency.valueOf(currency); + this.currency = currencyType.getUnit(); + this.prefix = currencyType.getPrefix(); + } + } + + @Override + public String getFormat() { + String formatStr = ""; + if (StringUtils.isNotBlank(prefix)){ + formatStr += "\""+getPrefix()+"\""; + } + if (StringUtils.isNotBlank(currency)){ + formatStr += getCurrency(); + } + formatStr += this.getUseThousandSeparator(); + formatStr += this.getDecimalPlaces(); + if (StringUtils.isNotBlank(this.getUnitKey())){ + formatStr += " \""+this.getUnitKey()+"\""; + } + return formatStr; + } + + @Override + public int getFixLength() { + if (StringUtils.isNotBlank(currency)) { + return super.getFixLength()+2; + } + return super.getFixLength(); + } +} diff --git a/core/src/main/java/datart/core/entity/poi/format/NumericFormat.java b/core/src/main/java/datart/core/entity/poi/format/NumericFormat.java new file mode 100644 index 000000000..9d92eb2db --- /dev/null +++ b/core/src/main/java/datart/core/entity/poi/format/NumericFormat.java @@ -0,0 +1,37 @@ +package datart.core.entity.poi.format; + +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +@Data +public class NumericFormat extends PoiNumFormat { + + public static final String type = "numeric"; + + /** 前缀 */ + private String prefix; + /** 后缀 */ + private String suffix; + + @Override + public String getFormat() { + String formatStr = ""; + if (StringUtils.isNotBlank(prefix)) { + formatStr += "\""+prefix+"\""; + } + formatStr += this.getUseThousandSeparator(); + formatStr += this.getDecimalPlaces(); + if (StringUtils.isNotBlank(this.getUnitKey())) { + formatStr += "\""+this.getUnitKey()+"\""; + } + if (StringUtils.isNotBlank(suffix)) { + formatStr += "\""+suffix+"\""; + } + return formatStr; + } + + @Override + public int getFixLength() { + return super.getFixLength()+prefix.length()+suffix.length(); + } +} diff --git a/core/src/main/java/datart/core/entity/poi/format/PercentageFormat.java b/core/src/main/java/datart/core/entity/poi/format/PercentageFormat.java new file mode 100644 index 000000000..a323a6abe --- /dev/null +++ b/core/src/main/java/datart/core/entity/poi/format/PercentageFormat.java @@ -0,0 +1,15 @@ +package datart.core.entity.poi.format; + +import datart.core.entity.poi.format.PoiNumFormat; +import lombok.Data; + +@Data +public class PercentageFormat extends PoiNumFormat { + + public static final String type = "percentage"; + + @Override + public String getFormat() { + return this.getDecimalPlaces() + "%"; + } +} diff --git a/core/src/main/java/datart/core/entity/poi/format/PoiNumFormat.java b/core/src/main/java/datart/core/entity/poi/format/PoiNumFormat.java new file mode 100644 index 000000000..5dc2a3474 --- /dev/null +++ b/core/src/main/java/datart/core/entity/poi/format/PoiNumFormat.java @@ -0,0 +1,65 @@ +package datart.core.entity.poi.format; + +import datart.core.base.consts.UnitKey; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; + +import java.math.BigDecimal; + +@Data +public class PoiNumFormat { + + /** 小数位数 */ + private String decimalPlaces; + /** 启用分隔符 */ + private boolean useThousandSeparator = false; + /** 单位 */ + private String unitKey; + + public String getUseThousandSeparator() { + return useThousandSeparator ? "#,##" : ""; + } + + public int getDecimalPlacesNum() { + return StringUtils.isNumeric(decimalPlaces) ? NumberUtils.toInt(decimalPlaces) : 0; + } + + public String getUnitKey() { + if (StringUtils.isNotBlank(this.unitKey)){ + UnitKey unitKey = UnitKey.getUnitKeyByValue(this.unitKey); + return unitKey.getFmt(); + } + return unitKey; + } + + public String getDecimalPlaces() { + String format = "0"; + String decimalStr = ""; + for (int i = 0; i < getDecimalPlacesNum(); i++) { + decimalStr += "0"; + } + return StringUtils.isBlank(decimalStr) ? format : format+"."+decimalStr; + } + + public Object parseValue(Object obj){ + if (obj!=null && StringUtils.isNotBlank(this.unitKey)){ + UnitKey unitKey = UnitKey.getUnitKeyByValue(this.unitKey); + BigDecimal val = new BigDecimal(obj.toString()).divide(new BigDecimal(unitKey.getUnit())); + obj = val.setScale(getDecimalPlacesNum(), BigDecimal.ROUND_HALF_UP); + } + return obj; + } + + public int getFixLength(){ + int num = getDecimalPlacesNum(); + if (StringUtils.isNotBlank(unitKey)){ + num = num+1; + } + return num; + } + + public String getFormat(){ + return ""; + }; +} diff --git a/core/src/main/java/datart/core/entity/poi/format/ScientificNotationFormat.java b/core/src/main/java/datart/core/entity/poi/format/ScientificNotationFormat.java new file mode 100644 index 000000000..f710e0104 --- /dev/null +++ b/core/src/main/java/datart/core/entity/poi/format/ScientificNotationFormat.java @@ -0,0 +1,14 @@ +package datart.core.entity.poi.format; + +import lombok.Data; + +@Data +public class ScientificNotationFormat extends PoiNumFormat { + + public static final String type = "scientificNotation"; + + @Override + public String getFormat() { + return this.getDecimalPlaces() + "E+0"; + } +} diff --git a/core/src/main/java/datart/core/mappers/FolderMapper.java b/core/src/main/java/datart/core/mappers/FolderMapper.java index 1a26befa2..aa84e270e 100644 --- a/core/src/main/java/datart/core/mappers/FolderMapper.java +++ b/core/src/main/java/datart/core/mappers/FolderMapper.java @@ -22,11 +22,13 @@ public interface FolderMapper extends CRUDMapper { @Insert({ "insert into folder (id, `name`, ", "org_id, rel_type, ", - "rel_id, parent_id, ", + "sub_type, rel_id, ", + "avatar, parent_id, ", "`index`)", "values (#{id,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, ", "#{orgId,jdbcType=VARCHAR}, #{relType,jdbcType=VARCHAR}, ", - "#{relId,jdbcType=VARCHAR}, #{parentId,jdbcType=VARCHAR}, ", + "#{subType,jdbcType=VARCHAR}, #{relId,jdbcType=VARCHAR}, ", + "#{avatar,jdbcType=VARCHAR}, #{parentId,jdbcType=VARCHAR}, ", "#{index,jdbcType=DOUBLE})" }) int insert(Folder record); @@ -36,7 +38,7 @@ public interface FolderMapper extends CRUDMapper { @Select({ "select", - "id, `name`, org_id, rel_type, rel_id, parent_id, `index`", + "id, `name`, org_id, rel_type, sub_type, rel_id, avatar, parent_id, `index`", "from folder", "where id = #{id,jdbcType=VARCHAR}" }) @@ -45,7 +47,9 @@ public interface FolderMapper extends CRUDMapper { @Result(column="name", property="name", jdbcType=JdbcType.VARCHAR), @Result(column="org_id", property="orgId", jdbcType=JdbcType.VARCHAR), @Result(column="rel_type", property="relType", jdbcType=JdbcType.VARCHAR), + @Result(column="sub_type", property="subType", jdbcType=JdbcType.VARCHAR), @Result(column="rel_id", property="relId", jdbcType=JdbcType.VARCHAR), + @Result(column="avatar", property="avatar", jdbcType=JdbcType.VARCHAR), @Result(column="parent_id", property="parentId", jdbcType=JdbcType.VARCHAR), @Result(column="index", property="index", jdbcType=JdbcType.DOUBLE) }) @@ -59,7 +63,9 @@ public interface FolderMapper extends CRUDMapper { "set `name` = #{name,jdbcType=VARCHAR},", "org_id = #{orgId,jdbcType=VARCHAR},", "rel_type = #{relType,jdbcType=VARCHAR},", + "sub_type = #{subType,jdbcType=VARCHAR},", "rel_id = #{relId,jdbcType=VARCHAR},", + "avatar = #{avatar,jdbcType=VARCHAR},", "parent_id = #{parentId,jdbcType=VARCHAR},", "`index` = #{index,jdbcType=DOUBLE}", "where id = #{id,jdbcType=VARCHAR}" diff --git a/core/src/main/java/datart/core/mappers/FolderSqlProvider.java b/core/src/main/java/datart/core/mappers/FolderSqlProvider.java index f7f0a3a8d..671a92a47 100644 --- a/core/src/main/java/datart/core/mappers/FolderSqlProvider.java +++ b/core/src/main/java/datart/core/mappers/FolderSqlProvider.java @@ -24,10 +24,18 @@ public String insertSelective(Folder record) { sql.VALUES("rel_type", "#{relType,jdbcType=VARCHAR}"); } + if (record.getSubType() != null) { + sql.VALUES("sub_type", "#{subType,jdbcType=VARCHAR}"); + } + if (record.getRelId() != null) { sql.VALUES("rel_id", "#{relId,jdbcType=VARCHAR}"); } + if (record.getAvatar() != null) { + sql.VALUES("avatar", "#{avatar,jdbcType=VARCHAR}"); + } + if (record.getParentId() != null) { sql.VALUES("parent_id", "#{parentId,jdbcType=VARCHAR}"); } @@ -55,10 +63,18 @@ public String updateByPrimaryKeySelective(Folder record) { sql.SET("rel_type = #{relType,jdbcType=VARCHAR}"); } + if (record.getSubType() != null) { + sql.SET("sub_type = #{subType,jdbcType=VARCHAR}"); + } + if (record.getRelId() != null) { sql.SET("rel_id = #{relId,jdbcType=VARCHAR}"); } + if (record.getAvatar() != null) { + sql.SET("avatar = #{avatar,jdbcType=VARCHAR}"); + } + if (record.getParentId() != null) { sql.SET("parent_id = #{parentId,jdbcType=VARCHAR}"); } diff --git a/core/src/main/java/datart/core/mappers/SourceSchemasMapper.java b/core/src/main/java/datart/core/mappers/SourceSchemasMapper.java new file mode 100644 index 000000000..49ba8edce --- /dev/null +++ b/core/src/main/java/datart/core/mappers/SourceSchemasMapper.java @@ -0,0 +1,58 @@ +package datart.core.mappers; + +import datart.core.entity.SourceSchemas; +import datart.core.mappers.ext.CRUDMapper; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.InsertProvider; +import org.apache.ibatis.annotations.Result; +import org.apache.ibatis.annotations.Results; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import org.apache.ibatis.annotations.UpdateProvider; +import org.apache.ibatis.type.JdbcType; + +public interface SourceSchemasMapper extends CRUDMapper { + @Delete({ + "delete from source_schemas", + "where id = #{id,jdbcType=VARCHAR}" + }) + int deleteByPrimaryKey(String id); + + @Insert({ + "insert into source_schemas (id, source_id, ", + "`schemas`, update_time)", + "values (#{id,jdbcType=VARCHAR}, #{sourceId,jdbcType=VARCHAR}, ", + "#{schemas,jdbcType=VARCHAR}, #{updateTime,jdbcType=TIMESTAMP})" + }) + int insert(SourceSchemas record); + + @InsertProvider(type=SourceSchemasSqlProvider.class, method="insertSelective") + int insertSelective(SourceSchemas record); + + @Select({ + "select", + "id, source_id, `schemas`, update_time", + "from source_schemas", + "where id = #{id,jdbcType=VARCHAR}" + }) + @Results({ + @Result(column="id", property="id", jdbcType=JdbcType.VARCHAR, id=true), + @Result(column="source_id", property="sourceId", jdbcType=JdbcType.VARCHAR), + @Result(column="schemas", property="schemas", jdbcType=JdbcType.VARCHAR), + @Result(column="update_time", property="updateTime", jdbcType=JdbcType.TIMESTAMP) + }) + SourceSchemas selectByPrimaryKey(String id); + + @UpdateProvider(type=SourceSchemasSqlProvider.class, method="updateByPrimaryKeySelective") + int updateByPrimaryKeySelective(SourceSchemas record); + + @Update({ + "update source_schemas", + "set source_id = #{sourceId,jdbcType=VARCHAR},", + "`schemas` = #{schemas,jdbcType=VARCHAR},", + "update_time = #{updateTime,jdbcType=TIMESTAMP}", + "where id = #{id,jdbcType=VARCHAR}" + }) + int updateByPrimaryKey(SourceSchemas record); +} \ No newline at end of file diff --git a/core/src/main/java/datart/core/mappers/SourceSchemasSqlProvider.java b/core/src/main/java/datart/core/mappers/SourceSchemasSqlProvider.java new file mode 100644 index 000000000..04bf2ba4f --- /dev/null +++ b/core/src/main/java/datart/core/mappers/SourceSchemasSqlProvider.java @@ -0,0 +1,50 @@ +package datart.core.mappers; + +import datart.core.entity.SourceSchemas; +import org.apache.ibatis.jdbc.SQL; + +public class SourceSchemasSqlProvider { + public String insertSelective(SourceSchemas record) { + SQL sql = new SQL(); + sql.INSERT_INTO("source_schemas"); + + if (record.getId() != null) { + sql.VALUES("id", "#{id,jdbcType=VARCHAR}"); + } + + if (record.getSourceId() != null) { + sql.VALUES("source_id", "#{sourceId,jdbcType=VARCHAR}"); + } + + if (record.getSchemas() != null) { + sql.VALUES("`schemas`", "#{schemas,jdbcType=VARCHAR}"); + } + + if (record.getUpdateTime() != null) { + sql.VALUES("update_time", "#{updateTime,jdbcType=TIMESTAMP}"); + } + + return sql.toString(); + } + + public String updateByPrimaryKeySelective(SourceSchemas record) { + SQL sql = new SQL(); + sql.UPDATE("source_schemas"); + + if (record.getSourceId() != null) { + sql.SET("source_id = #{sourceId,jdbcType=VARCHAR}"); + } + + if (record.getSchemas() != null) { + sql.SET("`schemas` = #{schemas,jdbcType=VARCHAR}"); + } + + if (record.getUpdateTime() != null) { + sql.SET("update_time = #{updateTime,jdbcType=TIMESTAMP}"); + } + + sql.WHERE("id = #{id,jdbcType=VARCHAR}"); + + return sql.toString(); + } +} \ No newline at end of file diff --git a/core/src/main/java/datart/core/mappers/ext/SourceSchemasMapperExt.java b/core/src/main/java/datart/core/mappers/ext/SourceSchemasMapperExt.java new file mode 100644 index 000000000..7278ea10a --- /dev/null +++ b/core/src/main/java/datart/core/mappers/ext/SourceSchemasMapperExt.java @@ -0,0 +1,36 @@ +/* + * Datart + *

+ * Copyright 2021 + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package datart.core.mappers.ext; + +import datart.core.entity.SourceSchemas; +import datart.core.mappers.SourceSchemasMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; + +@Mapper +public interface SourceSchemasMapperExt extends SourceSchemasMapper { + + @Select("SELECT * FROM source_schemas where source_id = #{sourceId}") + SourceSchemas selectBySource(String sourceId); + + @Select("SELECT update_time FROM source_schemas where source_id = #{sourceId}") + Date selectUpdateDateBySource(String sourceId); + +} diff --git a/data-providers/file-data-provider/pom.xml b/data-providers/file-data-provider/pom.xml index fb3dd489b..b68855c31 100644 --- a/data-providers/file-data-provider/pom.xml +++ b/data-providers/file-data-provider/pom.xml @@ -5,7 +5,7 @@ datart-parent datart - 1.0.0-beta.1 + 1.0.0-beta.2 ../../pom.xml 4.0.0 diff --git a/data-providers/file-data-provider/src/main/java/datart/data/provider/FileDataProvider.java b/data-providers/file-data-provider/src/main/java/datart/data/provider/FileDataProvider.java index 8b11daae1..96f80553b 100644 --- a/data-providers/file-data-provider/src/main/java/datart/data/provider/FileDataProvider.java +++ b/data-providers/file-data-provider/src/main/java/datart/data/provider/FileDataProvider.java @@ -128,11 +128,13 @@ private List inferHeader(List> values) { boolean isHeader = typedValues.stream() .allMatch(typedValue -> typedValue instanceof String); if (isHeader) { - for (Object value : typedValues) { + typedValues = values.size() > 1 ? values.get(1) : typedValues; + for (int i = 0; i < typedValues.size(); i++) { Column column = new Column(); - ValueType valueType = DataTypeUtils.javaType2DataType(value); + ValueType valueType = DataTypeUtils.javaType2DataType(typedValues.get(i)); column.setType(valueType); - column.setName(value.toString()); + String name = values.get(0).get(i).toString(); + column.setName(StringUtils.isBlank(values.get(0).get(i).toString()) ? "col" + i : name); columns.add(column); } values.remove(0); diff --git a/data-providers/http-data-provider/pom.xml b/data-providers/http-data-provider/pom.xml index 5b3e1ca49..358c0edc0 100644 --- a/data-providers/http-data-provider/pom.xml +++ b/data-providers/http-data-provider/pom.xml @@ -5,7 +5,7 @@ datart-parent datart - 1.0.0-beta.1 + 1.0.0-beta.2 ../../pom.xml diff --git a/data-providers/jdbc-data-provider/pom.xml b/data-providers/jdbc-data-provider/pom.xml index 540f5b4fb..4e6c0e54e 100644 --- a/data-providers/jdbc-data-provider/pom.xml +++ b/data-providers/jdbc-data-provider/pom.xml @@ -5,7 +5,7 @@ datart-parent datart - 1.0.0-beta.1 + 1.0.0-beta.2 ../../pom.xml 4.0.0 diff --git a/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/JdbcDataProvider.java b/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/JdbcDataProvider.java index e0f6c2c34..238e8976e 100644 --- a/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/JdbcDataProvider.java +++ b/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/JdbcDataProvider.java @@ -18,6 +18,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.calcite.sql.SqlDialect; import org.apache.commons.lang3.StringUtils; +import org.springframework.util.CollectionUtils; import org.yaml.snakeyaml.Yaml; import javax.sql.DataSource; @@ -270,8 +271,17 @@ private static List loadDriverInfoFromResource() { //Build in database types Map> buildIn = loadYml(JDBC_DRIVER_BUILD_IN); // user ext database types - - buildIn.putAll(loadYml(new File(FileUtils.concatPath(System.getProperty("user.dir"), JDBC_DRIVER_EXT)))); + Map> extDrivers = loadYml(new File(FileUtils.concatPath(System.getProperty("user.dir"), JDBC_DRIVER_EXT))); + if (!CollectionUtils.isEmpty(extDrivers)) { + for (String key : extDrivers.keySet()) { + Map driver = buildIn.get(key); + if (driver == null) { + buildIn.put(key, extDrivers.get(key)); + } else { + driver.putAll(extDrivers.get(key)); + } + } + } return buildIn.entrySet().stream().map(entry -> { try { @@ -279,6 +289,10 @@ private static List loadDriverInfoFromResource() { if (StringUtils.isBlank(jdbcDriverInfo.getAdapterClass())) { jdbcDriverInfo.setAdapterClass(DEFAULT_ADAPTER); } + // default to quote all identifiers , for support special column names and most databases + if (jdbcDriverInfo.getQuoteIdentifiers() == null) { + jdbcDriverInfo.setQuoteIdentifiers(true); + } jdbcDriverInfo.setDbType(jdbcDriverInfo.getDbType().toUpperCase()); return jdbcDriverInfo; } catch (Exception e) { diff --git a/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/DataSourceFactoryDruidImpl.java b/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/DataSourceFactoryDruidImpl.java index 1c937b309..143692369 100644 --- a/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/DataSourceFactoryDruidImpl.java +++ b/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/DataSourceFactoryDruidImpl.java @@ -59,16 +59,6 @@ private Properties configDataSource(JdbcProperties properties) { System.setProperty("druid.mysql.usePingMethod", "false"); - // url properties -// pro.setProperty(DruidDataSourceFactory.PROP_CONNECTIONPROPERTIES, "useUnicode=true;characterEncoding=utf8;characterSetResults=utf8"); - - // wall config -// pro.setProperty("druid.wall.updateAllow", "false"); -// pro.setProperty("druid.wall.deleteAllow", "false"); -// pro.setProperty("druid.wall.insertAllow", "false"); -// pro.setProperty("druid.wall.multiStatementAllow", "true"); -// pro.setProperty("druid.failFast", "true"); - //opt config pro.putAll(properties.getProperties()); return pro; diff --git a/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/adapters/DorisDataProviderAdapter.java b/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/adapters/DorisDataProviderAdapter.java new file mode 100644 index 000000000..458134655 --- /dev/null +++ b/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/adapters/DorisDataProviderAdapter.java @@ -0,0 +1,38 @@ +/* + * Datart + *

+ * Copyright 2021 + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package datart.data.provider.jdbc.adapters; + +import datart.core.data.provider.Dataframe; +import datart.core.data.provider.ExecuteParam; +import datart.core.data.provider.QueryScript; +import datart.core.data.provider.sql.OrderOperator; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.Collections; + +public class DorisDataProviderAdapter extends JdbcDataProviderAdapter { + + @Override + protected Dataframe executeOnSource(QueryScript script, ExecuteParam executeParam) throws Exception { + if (CollectionUtils.isEmpty(executeParam.getOrders())) { + executeParam.setOrders(Collections.singletonList(new OrderOperator())); + } + return super.executeOnSource(script, executeParam); + } +} diff --git a/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/adapters/JdbcDataProviderAdapter.java b/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/adapters/JdbcDataProviderAdapter.java index f4a60aee6..47353d050 100644 --- a/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/adapters/JdbcDataProviderAdapter.java +++ b/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/adapters/JdbcDataProviderAdapter.java @@ -141,16 +141,11 @@ public Set readTableColumn(String database, String table) throws SQLExce try (Connection conn = getConn()) { Set columnSet = new HashSet<>(); DatabaseMetaData metadata = conn.getMetaData(); - Map> importedKeys = getImportedKeys(metadata, database, table); + Map> importedKeys = getImportedKeys(metadata, database, table); ResultSet columns = metadata.getColumns(database, null, table, null); while (columns.next()) { Column column = readTableColumn(columns); - Map pKeys = importedKeys.get(column.getName()); - if (pKeys != null) { - column.setPkDatabase(pKeys.get(PKTABLE_CAT)); - column.setPkTable(pKeys.get(PKTABLE_NAME)); - column.setPkColumn(pKeys.get(PKCOLUMN_NAME)); - } + column.setForeignKeys(importedKeys.get(column.getName())); columnSet.add(column); } return columnSet; @@ -167,15 +162,15 @@ protected Column readTableColumn(ResultSet columnMetadata) throws SQLException { /** * 获取表的外键关系 */ - protected Map> getImportedKeys(DatabaseMetaData metadata, String database, String table) throws SQLException { - HashMap> keyMap = new HashMap<>(); + protected Map> getImportedKeys(DatabaseMetaData metadata, String database, String table) throws SQLException { + HashMap> keyMap = new HashMap<>(); ResultSet importedKeys = metadata.getImportedKeys(database, null, table); while (importedKeys.next()) { - HashMap keys = new HashMap<>(); - keys.put(PKTABLE_CAT, importedKeys.getString(PKTABLE_CAT)); - keys.put(PKTABLE_NAME, importedKeys.getString(PKTABLE_NAME)); - keys.put(PKCOLUMN_NAME, importedKeys.getString(PKCOLUMN_NAME)); - keyMap.put(importedKeys.getString(FKCOLUMN_NAME), keys); + ForeignKey foreignKey = new ForeignKey(); + foreignKey.setDatabase(importedKeys.getString(PKTABLE_CAT)); + foreignKey.setTable(importedKeys.getString(PKTABLE_NAME)); + foreignKey.setColumn(importedKeys.getString(PKCOLUMN_NAME)); + keyMap.computeIfAbsent(importedKeys.getString(FKCOLUMN_NAME), key->new ArrayList<>()).add(foreignKey); } return keyMap; } diff --git a/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/adapters/OracleDataProviderAdapter.java b/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/adapters/OracleDataProviderAdapter.java index 94691b27d..671bd79a3 100644 --- a/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/adapters/OracleDataProviderAdapter.java +++ b/data-providers/jdbc-data-provider/src/main/java/datart/data/provider/jdbc/adapters/OracleDataProviderAdapter.java @@ -34,7 +34,7 @@ @Slf4j public class OracleDataProviderAdapter extends JdbcDataProviderAdapter { - private static final String PAGE_SQL = "SELECT * FROM (SELECT ROWNUM V_R_N,V_T0.* FROM (%s) V_T0 WHERE ROWNUM < %d) WHERE V_R_N>=%d"; + private static final String PAGE_SQL = "SELECT * FROM (SELECT ROWNUM V_R_N,V_T0.* FROM (%s) V_T0 WHERE ROWNUM <= %d) WHERE V_R_N>%d"; public Set readAllDatabases() { return Collections.singleton(jdbcProperties.getUser()); diff --git a/data-providers/jdbc-data-provider/src/main/resources/jdbc-data-provider.json b/data-providers/jdbc-data-provider/src/main/resources/jdbc-data-provider.json index 2d06dfc0a..f37b1344c 100644 --- a/data-providers/jdbc-data-provider/src/main/resources/jdbc-data-provider.json +++ b/data-providers/jdbc-data-provider/src/main/resources/jdbc-data-provider.json @@ -44,6 +44,18 @@ "required": false, "defaultValue": false }, + { + "name": "enableSyncSchemas", + "type": "bool", + "required": false, + "defaultValue": true + }, + { + "name": "syncInterval", + "type": "string", + "required": false, + "defaultValue": "60" + }, { "name": "properties", "type": "object", diff --git a/data-providers/pom.xml b/data-providers/pom.xml index 44ed52b29..2d401ad58 100644 --- a/data-providers/pom.xml +++ b/data-providers/pom.xml @@ -5,7 +5,7 @@ datart-parent datart - 1.0.0-beta.1 + 1.0.0-beta.2 4.0.0 diff --git a/data-providers/src/main/java/codegen/Parser.jj b/data-providers/src/main/java/codegen/Parser.jj index f82aed517..1d9f247a6 100644 --- a/data-providers/src/main/java/codegen/Parser.jj +++ b/data-providers/src/main/java/codegen/Parser.jj @@ -6636,6 +6636,7 @@ String KeywordFunctionNameSupport() : | | | + | ) { return unquotedIdentifier(); diff --git a/data-providers/src/main/java/datart/data/provider/DefaultDataProvider.java b/data-providers/src/main/java/datart/data/provider/DefaultDataProvider.java index 5c6083ae5..b1609a246 100644 --- a/data-providers/src/main/java/datart/data/provider/DefaultDataProvider.java +++ b/data-providers/src/main/java/datart/data/provider/DefaultDataProvider.java @@ -226,6 +226,9 @@ protected List> parseValues(List> values, List } break; case DATE: + if (val instanceof Date){ + break; + } String fmt = columns.get(i).getFmt(); if (StringUtils.isBlank(fmt)) { fmt = DateUtils.inferDateFormat(val.toString()); diff --git a/data-providers/src/main/java/datart/data/provider/calcite/SqlBuilder.java b/data-providers/src/main/java/datart/data/provider/calcite/SqlBuilder.java index d3089680e..e33c041c9 100644 --- a/data-providers/src/main/java/datart/data/provider/calcite/SqlBuilder.java +++ b/data-providers/src/main/java/datart/data/provider/calcite/SqlBuilder.java @@ -52,6 +52,8 @@ public class SqlBuilder { private boolean withPage; + private boolean quoteIdentifiers; + private SqlBuilder() { } @@ -64,8 +66,8 @@ public SqlBuilder withBaseSql(String sql) { if (StringUtils.isNotBlank(sql)) { sql = removeEndDelimiter(sql); } - sql = StringUtils.appendIfMissing(sql," "," "); - sql = StringUtils.prependIfMissing(sql," "," "); + sql = StringUtils.appendIfMissing(sql, " ", " "); + sql = StringUtils.prependIfMissing(sql, " ", " "); this.srcSql = sql; return this; } @@ -88,6 +90,11 @@ public SqlBuilder withPage(boolean withPage) { return this; } + public SqlBuilder withQuoteIdentifiers(boolean quoteIdentifiers) { + this.quoteIdentifiers = quoteIdentifiers; + return this; + } + /** * 根据页面操作生成的Aggregator,Filter,Group By, Order By等操作符,重新构建SQL。 @@ -222,7 +229,7 @@ public String build() throws SqlParseException { offset, fetch, null); - return SqlNodeUtils.toSql(sqlSelect, this.dialect); + return SqlNodeUtils.toSql(sqlSelect, this.dialect, quoteIdentifiers); } private SqlNode createAggNode(AggregateOperator.SqlOperator sqlOperator, String column, String alias) { diff --git a/data-providers/src/main/java/datart/data/provider/calcite/SqlFragment.java b/data-providers/src/main/java/datart/data/provider/calcite/SqlFragment.java index 9694996fa..b31bdcf6d 100644 --- a/data-providers/src/main/java/datart/data/provider/calcite/SqlFragment.java +++ b/data-providers/src/main/java/datart/data/provider/calcite/SqlFragment.java @@ -13,7 +13,7 @@ public class SqlFragment extends SqlLiteral { * @param value String value */ public SqlFragment(String value) { - super(value, SqlTypeName.ANY, SqlParserPos.ZERO); + super(value, SqlTypeName.MULTISET, SqlParserPos.ZERO); } @Override diff --git a/data-providers/src/main/java/datart/data/provider/calcite/SqlFunctionRegisterVisitor.java b/data-providers/src/main/java/datart/data/provider/calcite/SqlFunctionRegisterVisitor.java index aae4682e0..36cda65a3 100644 --- a/data-providers/src/main/java/datart/data/provider/calcite/SqlFunctionRegisterVisitor.java +++ b/data-providers/src/main/java/datart/data/provider/calcite/SqlFunctionRegisterVisitor.java @@ -39,6 +39,10 @@ public Object visit(SqlCall call) { private void registerIfNotExists(SqlFunction sqlFunction) { SqlStdOperatorTable opTab = SqlStdOperatorTable.instance(); LinkedList list = new LinkedList<>(); + // built-in functions have no identifier and no registration required + if (sqlFunction.getSqlIdentifier() == null) { + return; + } opTab.lookupOperatorOverloads(sqlFunction.getSqlIdentifier(), null, SqlSyntax.FUNCTION, list, SqlNameMatchers.withCaseSensitive(sqlFunction.getSqlIdentifier().isComponentQuoted(0))); if (list.size() > 0) { diff --git a/data-providers/src/main/java/datart/data/provider/calcite/SqlNodeUtils.java b/data-providers/src/main/java/datart/data/provider/calcite/SqlNodeUtils.java index 650876bc1..b4b308bcb 100644 --- a/data-providers/src/main/java/datart/data/provider/calcite/SqlNodeUtils.java +++ b/data-providers/src/main/java/datart/data/provider/calcite/SqlNodeUtils.java @@ -127,13 +127,20 @@ public static SqlNode createSqlNode(SingleTypedValue value) { return createSqlNode(value, null); } - public static String toSql(SqlNode sqlNode, SqlDialect dialect) { + + /** + * SQL 输出时,字段名称要默认加上引号,否则对于特殊字段名称无法处理,以及pg数据库无法正常执行等问题。 + * 但是在oracle数据库中,加上引号的字段小写会导致列名无法识别的问题,此时需要用户SQL中使用大写列名,或者可在jdbc-driver文件中配置为默认不加引号。 + */ + public static String toSql(SqlNode sqlNode, SqlDialect dialect, boolean quoteIdentifiers) { return sqlNode.toSqlString( config -> config.withDialect(dialect) - .withQuoteAllIdentifiers(false) + .withQuoteAllIdentifiers(quoteIdentifiers) .withAlwaysUseParentheses(false) .withSelectListItemsOnSeparateLines(false) .withUpdateSetListNewline(false) .withIndentation(0)).getSql(); } + + } diff --git a/data-providers/src/main/java/datart/data/provider/calcite/SqlParserUtils.java b/data-providers/src/main/java/datart/data/provider/calcite/SqlParserUtils.java index caef57e72..671424f56 100644 --- a/data-providers/src/main/java/datart/data/provider/calcite/SqlParserUtils.java +++ b/data-providers/src/main/java/datart/data/provider/calcite/SqlParserUtils.java @@ -1,6 +1,7 @@ package datart.data.provider.calcite; import datart.data.provider.calcite.parser.impl.SqlParserImpl; +import org.apache.calcite.avatica.util.Casing; import org.apache.calcite.avatica.util.Quoting; import org.apache.calcite.sql.SqlDialect; import org.apache.calcite.sql.SqlNode; @@ -19,7 +20,10 @@ public static SqlNode parseSnippet(String snippet) throws SqlParseException { String sql = String.format(SELECT_SQL, snippet); SqlParser.Config config = SqlParser.config() .withParserFactory(SqlParserImpl.FACTORY) + .withQuotedCasing(Casing.UNCHANGED) + .withUnquotedCasing(Casing.UNCHANGED) .withConformance(SqlConformanceEnum.LENIENT) + .withCaseSensitive(true) .withQuoting(Quoting.BRACKET); return SqlParser.create(sql, config).parseQuery(); diff --git a/data-providers/src/main/java/datart/data/provider/calcite/SqlValidateUtils.java b/data-providers/src/main/java/datart/data/provider/calcite/SqlValidateUtils.java index 05295359b..076abf518 100644 --- a/data-providers/src/main/java/datart/data/provider/calcite/SqlValidateUtils.java +++ b/data-providers/src/main/java/datart/data/provider/calcite/SqlValidateUtils.java @@ -115,7 +115,7 @@ private static String firstWord(String src) { if (src == null) { return null; } - return src.trim().split(" ", 2)[0]; + return src.trim().split("\\s", 2)[0]; } } diff --git a/data-providers/src/main/java/datart/data/provider/freemarker/FreemarkerContext.java b/data-providers/src/main/java/datart/data/provider/freemarker/FreemarkerContext.java index a61779483..b12e8a6d5 100644 --- a/data-providers/src/main/java/datart/data/provider/freemarker/FreemarkerContext.java +++ b/data-providers/src/main/java/datart/data/provider/freemarker/FreemarkerContext.java @@ -48,8 +48,6 @@ public static String process(String content, Map dataModel) { return writer.toString(); } catch (Exception e) { log.error("freemarker parse error", e); - } finally { - StringTemplateLoader.SCRIPT_MAP.remove(key); } return content; } diff --git a/data-providers/src/main/java/datart/data/provider/freemarker/StringTemplateLoader.java b/data-providers/src/main/java/datart/data/provider/freemarker/StringTemplateLoader.java index f9985c8a7..26c3a50f0 100644 --- a/data-providers/src/main/java/datart/data/provider/freemarker/StringTemplateLoader.java +++ b/data-providers/src/main/java/datart/data/provider/freemarker/StringTemplateLoader.java @@ -18,16 +18,16 @@ package datart.data.provider.freemarker; import freemarker.cache.TemplateLoader; +import org.apache.commons.collections4.map.LRUMap; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; public class StringTemplateLoader implements TemplateLoader { - public static final Map SCRIPT_MAP = new ConcurrentHashMap<>(); + public static final Map SCRIPT_MAP = new LRUMap<>(1000); @Override public Object findTemplateSource(String name) throws IOException { diff --git a/data-providers/src/main/java/datart/data/provider/jdbc/DataTypeUtils.java b/data-providers/src/main/java/datart/data/provider/jdbc/DataTypeUtils.java index b938bce2e..68c2319e4 100644 --- a/data-providers/src/main/java/datart/data/provider/jdbc/DataTypeUtils.java +++ b/data-providers/src/main/java/datart/data/provider/jdbc/DataTypeUtils.java @@ -65,25 +65,6 @@ public static SqlTypeName javaType2SqlType(String javaTypeSimpleName) { return SqlTypeName.VARCHAR; } } -// -// public static ValueType javaType2DataType(String javaTypeSimpleName) { -// JavaType javaType = JavaType.valueOf(javaTypeSimpleName.toUpperCase()); -// switch (javaType) { -// case BYTE: -// case SHORT: -// case INTEGER: -// case LONG: -// case FLOAT: -// case DOUBLE: -// return ValueType.NUMERIC; -// case DATE: -// return ValueType.DATE; -// case BOOLEAN: -// return ValueType.BOOLEAN; -// default: -// return ValueType.STRING; -// } -// } public static ValueType javaType2DataType(Object obj) { if (obj instanceof Number) { diff --git a/data-providers/src/main/java/datart/data/provider/jdbc/JdbcDriverInfo.java b/data-providers/src/main/java/datart/data/provider/jdbc/JdbcDriverInfo.java index 9a3023a00..c2b684cb3 100644 --- a/data-providers/src/main/java/datart/data/provider/jdbc/JdbcDriverInfo.java +++ b/data-providers/src/main/java/datart/data/provider/jdbc/JdbcDriverInfo.java @@ -52,4 +52,6 @@ public class JdbcDriverInfo { private String urlPrefix; + private Boolean quoteIdentifiers; + } diff --git a/data-providers/src/main/java/datart/data/provider/jdbc/RegexVariableResolver.java b/data-providers/src/main/java/datart/data/provider/jdbc/RegexVariableResolver.java index 15c206686..4ce4b3a60 100644 --- a/data-providers/src/main/java/datart/data/provider/jdbc/RegexVariableResolver.java +++ b/data-providers/src/main/java/datart/data/provider/jdbc/RegexVariableResolver.java @@ -78,6 +78,8 @@ private static List createPlaceholder(SqlDialect sqlDialect placeholders.add(new SimpleVariablePlaceholder(variable, sqlDialect, variableFragment)); } } + } else { + placeholders.add(new SimpleVariablePlaceholder(variable, sqlDialect, variableFragment)); } return placeholders; } diff --git a/data-providers/src/main/java/datart/data/provider/jdbc/SimpleVariablePlaceholder.java b/data-providers/src/main/java/datart/data/provider/jdbc/SimpleVariablePlaceholder.java index f842a914f..78ddf2b86 100644 --- a/data-providers/src/main/java/datart/data/provider/jdbc/SimpleVariablePlaceholder.java +++ b/data-providers/src/main/java/datart/data/provider/jdbc/SimpleVariablePlaceholder.java @@ -38,10 +38,6 @@ public class SimpleVariablePlaceholder extends VariablePlaceholder { private SqlIdentifier identifier; - private SimpleVariablePlaceholder(List variables, SqlDialect sqlDialect, SqlCall sqlCall, String originalSqlFragment) { - super(variables, sqlDialect, sqlCall, originalSqlFragment); - } - public SimpleVariablePlaceholder(ScriptVariable variable, SqlDialect sqlDialect, String originalSqlFragment) { super(null, sqlDialect, null, originalSqlFragment); this.variable = variable; diff --git a/data-providers/src/main/java/datart/data/provider/jdbc/SqlScriptRender.java b/data-providers/src/main/java/datart/data/provider/jdbc/SqlScriptRender.java index d61e7fb53..9f9fa04cb 100644 --- a/data-providers/src/main/java/datart/data/provider/jdbc/SqlScriptRender.java +++ b/data-providers/src/main/java/datart/data/provider/jdbc/SqlScriptRender.java @@ -29,10 +29,9 @@ import datart.core.data.provider.ScriptVariable; import datart.data.provider.base.DataProviderException; import datart.data.provider.calcite.SqlBuilder; -import datart.data.provider.calcite.SqlValidateUtils; import datart.data.provider.calcite.SqlParserUtils; +import datart.data.provider.calcite.SqlValidateUtils; import datart.data.provider.freemarker.FreemarkerContext; -import datart.data.provider.local.LocalDB; import datart.data.provider.script.ReplacementPair; import datart.data.provider.script.ScriptRender; import datart.data.provider.script.VariablePlaceholder; @@ -70,21 +69,24 @@ public class SqlScriptRender extends ScriptRender { private final SqlDialect sqlDialect; // special sql execute permission config from datasource - private boolean enableSpecialSQL; + private final boolean enableSpecialSQL; - public SqlScriptRender(QueryScript queryScript, ExecuteParam executeParam) { - super(queryScript, executeParam); - this.sqlDialect = LocalDB.SQL_DIALECT; - } + // default all identifiers + private final boolean quoteIdentifiers; public SqlScriptRender(QueryScript queryScript, ExecuteParam executeParam, SqlDialect sqlDialect) { - super(queryScript, executeParam); - this.sqlDialect = sqlDialect; + this(queryScript, executeParam, sqlDialect, false); } public SqlScriptRender(QueryScript queryScript, ExecuteParam executeParam, SqlDialect sqlDialect, boolean enableSpecialSQL) { - this(queryScript, executeParam, sqlDialect); + this(queryScript, executeParam, sqlDialect, enableSpecialSQL, true); + } + + public SqlScriptRender(QueryScript queryScript, ExecuteParam executeParam, SqlDialect sqlDialect, boolean enableSpecialSQL, boolean quoteIdentifiers) { + super(queryScript, executeParam); + this.sqlDialect = sqlDialect; this.enableSpecialSQL = enableSpecialSQL; + this.quoteIdentifiers = quoteIdentifiers; } @@ -122,6 +124,7 @@ public String render(boolean withExecuteParam, boolean withPage, boolean onlySel .withDialect(sqlDialect) .withBaseSql(selectSql) .withPage(withPage) + .withQuoteIdentifiers(quoteIdentifiers) .build(); } @@ -131,7 +134,11 @@ public String render(boolean withExecuteParam, boolean withPage, boolean onlySel selectSql = replaceVariables(selectSql); - return onlySelectStatement ? selectSql : script.replace(selectSql0, selectSql); + selectSql = onlySelectStatement ? selectSql : script.replace(selectSql0, selectSql); + + RequestContext.setSql(selectSql); + + return selectSql; } @@ -169,7 +176,7 @@ public String replaceVariables(String selectSql) throws SqlParseException { if (CollectionUtils.isNotEmpty(placeholders)) { for (VariablePlaceholder placeholder : placeholders) { ReplacementPair replacementPair = placeholder.replacementPair(); - selectSql = StringUtils.replaceIgnoreCase(selectSql,replacementPair.getPattern(),replacementPair.getReplacement()); + selectSql = StringUtils.replaceIgnoreCase(selectSql, replacementPair.getPattern(), replacementPair.getReplacement()); } } diff --git a/data-providers/src/main/java/datart/data/provider/local/LocalDB.java b/data-providers/src/main/java/datart/data/provider/local/LocalDB.java index e8dc8a923..d38ae201d 100644 --- a/data-providers/src/main/java/datart/data/provider/local/LocalDB.java +++ b/data-providers/src/main/java/datart/data/provider/local/LocalDB.java @@ -43,16 +43,12 @@ import java.util.concurrent.ConcurrentHashMap; @Slf4j -public class -LocalDB { +public class LocalDB { private static final String MEM_URL = "jdbc:h2:mem:/"; private static final String H2_PARAM = ";LOG=0;DATABASE_TO_UPPER=false;MODE=MySQL;CASE_INSENSITIVE_IDENTIFIERS=TRUE;CACHE_SIZE=65536;LOCK_MODE=0;UNDO_LOG=0"; - private static String fileUrl; - - private static final String TABLE_CREATE_SQL_TEMPLATE = "CREATE TABLE `%s` ( %s )"; public static final SqlDialect SQL_DIALECT = new H2Dialect(); @@ -283,6 +279,8 @@ private static Dataframe execute(Connection connection, QueryScript queryScript, String sql = render.render(true, false, false); + log.debug(sql); + ResultSet resultSet = connection.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY).executeQuery(sql); PageInfo pageInfo = executeParam.getPageInfo(); resultSet.last(); @@ -312,7 +310,7 @@ private static String getDatabaseUrl(String database) { } else { database = toDatabase(database); } - return fileUrl = String.format("jdbc:h2:file:%s/%s" + H2_PARAM, getDbFileBasePath(), database); + return String.format("jdbc:h2:file:%s/%s" + H2_PARAM, getDbFileBasePath(), database); } private static String getDbFileBasePath() { diff --git a/data-providers/src/main/java/datart/data/provider/script/ScriptRender.java b/data-providers/src/main/java/datart/data/provider/script/ScriptRender.java index ca2b9e50a..e3b3ed9b1 100644 --- a/data-providers/src/main/java/datart/data/provider/script/ScriptRender.java +++ b/data-providers/src/main/java/datart/data/provider/script/ScriptRender.java @@ -38,12 +38,9 @@ public class ScriptRender { protected ExecuteParam executeParam; -// protected String variableQuote; - public ScriptRender(QueryScript queryScript, ExecuteParam executeParam) { this.queryScript = queryScript; this.executeParam = executeParam; -// this.variableQuote = Const.DEFAULT_VARIABLE_QUOTE; } } \ No newline at end of file diff --git a/data-providers/src/main/java/datart/data/provider/script/VariablePlaceholder.java b/data-providers/src/main/java/datart/data/provider/script/VariablePlaceholder.java index a0e436d1a..1b662a88b 100644 --- a/data-providers/src/main/java/datart/data/provider/script/VariablePlaceholder.java +++ b/data-providers/src/main/java/datart/data/provider/script/VariablePlaceholder.java @@ -217,7 +217,7 @@ private ReplacementPair replacePermissionVariable(List variables for (ScriptVariable variable : variables) { replaceVariable(sqlCall, variable); } - return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(sqlCall, sqlDialect)); + return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(sqlCall, sqlDialect, false)); } ScriptVariable variable = variables.get(0); @@ -231,11 +231,11 @@ private ReplacementPair replacePermissionVariable(List variables if (variable.getValues().size() == 1) { replaceVariable(sqlCall, variable); - return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(sqlCall, sqlDialect)); + return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(sqlCall, sqlDialect, false)); } SqlCall fixSqlCall = autoFixSqlCall(variable); - return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(fixSqlCall, sqlDialect)); + return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(fixSqlCall, sqlDialect, false)); } catch (ParamReplaceException e) { return replaceAsSting(); } @@ -257,20 +257,20 @@ private ReplacementPair replaceQueryVariable(List variables) { for (ScriptVariable variable : variables) { replaceVariable(sqlCall, variable); } - return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(sqlCall, sqlDialect)); + return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(sqlCall, sqlDialect, false)); } ScriptVariable variable = variables.get(0); if (CollectionUtils.isEmpty(variable.getValues())) { log.warn("The query variable [" + variable.getName() + "] do not have default values, which may cause SQL syntax errors"); SqlCall isNullSqlCall = createIsNullSqlCall(sqlCall.getOperandList().get(0)); - return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(isNullSqlCall, sqlDialect)); + return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(isNullSqlCall, sqlDialect, false)); } if (variable.getValues().size() > 1 && SqlValidateUtils.isLogicExpressionSqlCall(sqlCall)) { SqlCall fixedCall = autoFixSqlCall(variable); - return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(fixedCall, sqlDialect)); + return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(fixedCall, sqlDialect, false)); } else { replaceVariable(sqlCall, variable); - return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(sqlCall, sqlDialect)); + return new ReplacementPair(originalSqlFragment, SqlNodeUtils.toSql(sqlCall, sqlDialect, false)); } } catch (ParamReplaceException e) { return replaceAsSting(); @@ -309,20 +309,19 @@ protected String formatValue(ScriptVariable variable) { } protected String formatWithoutQuote(Set values) { - if (org.springframework.util.CollectionUtils.isEmpty(values)) { + if (CollectionUtils.isEmpty(values)) { return ""; } return String.join(",", values); } protected String formatWithQuote(Set values) { - if (org.springframework.util.CollectionUtils.isEmpty(values)) { + if (CollectionUtils.isEmpty(values)) { return ""; } return values.stream().map(SqlSimpleStringLiteral::new) - .map(node -> SqlNodeUtils.toSql(node, sqlDialect)) + .map(node -> SqlNodeUtils.toSql(node, sqlDialect, false)) .collect(Collectors.joining(",")); } - } \ No newline at end of file diff --git a/data-providers/src/test/java/datart/data/provider/DataProviderTestApplication.java b/data-providers/src/test/java/datart/data/provider/DataProviderTestApplication.java new file mode 100644 index 000000000..a01501acb --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/DataProviderTestApplication.java @@ -0,0 +1,14 @@ +package datart.data.provider; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ComponentScan(basePackages = {"datart.core.mappers","datart.core.common"}) +public class DataProviderTestApplication { + + public static void main(String[] args) { + SpringApplication.run(DataProviderTestApplication.class); + } +} diff --git a/data-providers/src/test/java/datart/data/provider/ParamFactory.java b/data-providers/src/test/java/datart/data/provider/ParamFactory.java deleted file mode 100644 index 64a7a4e66..000000000 --- a/data-providers/src/test/java/datart/data/provider/ParamFactory.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Datart - *

- * Copyright 2021 - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package datart.data.provider; - -import com.google.common.collect.Lists; -import datart.core.base.consts.ValueType; -import datart.core.base.consts.VariableTypeEnum; -import datart.core.common.UUIDGenerator; -import datart.core.data.provider.QueryScript; -import datart.core.data.provider.ScriptVariable; -import org.apache.commons.compress.utils.Sets; - -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -public class ParamFactory { - - private final static Set scripts = Sets.newHashSet( - "SELECT * FROM test_table where 部门=$部门$ and `age`>$age$" - , "SELECT * FROM test_table where 部门 IN ($部门$)" - , "SELECT * FROM test_table where age between $min$ and $max$" - , "SELECT * FROM `test_table` where age between $min$ and $max$" - ); - - private final static Set errorScripts = Sets.newHashSet( - "_SELECT * _FROM test_table where 部门=$部门$ and `age`>$age$" - , "_SELECT * _FROM test_table where 部门 IN ($部门$)" - , "_SELECT * _FROM test_table where age between $min$ and $max$" - , "_SELECT * _FROM `test_table` where age between $min$ and $max$" - ); - - private final static List variables = Lists.newArrayList( - variable("部门", ValueType.STRING, VariableTypeEnum.PERMISSION, false, "销售部") - , variable("age", ValueType.NUMERIC, VariableTypeEnum.QUERY, false, "20") - , variable("max", ValueType.NUMERIC, VariableTypeEnum.QUERY, false, "100") - , variable("min", ValueType.NUMERIC, VariableTypeEnum.QUERY, false, "0") - ); - - public static List getQueryScriptExamples() { - return scripts.stream().map(script -> { - return QueryScript.builder().script(script) - .sourceId(UUIDGenerator.generate()) - .variables(new LinkedList<>(variables)) - .viewId(UUIDGenerator.generate()) - .test(false) - .build(); - }).collect(Collectors.toList()); - } - - public static List getErrorQueryScriptExamples() { - return errorScripts.stream().map(script -> { - return QueryScript.builder().script(script) - .sourceId(UUIDGenerator.generate()) - .variables(new LinkedList<>(variables)) - .viewId(UUIDGenerator.generate()) - .test(false) - .build(); - }).collect(Collectors.toList()); - } - - private static ScriptVariable variable(String name, ValueType type, VariableTypeEnum variableType, boolean expression, String... values) { - ScriptVariable scriptVariable = new ScriptVariable(); - scriptVariable.setName(name); - scriptVariable.setValueType(type); - scriptVariable.setType(variableType); - scriptVariable.setExpression(expression); - scriptVariable.setValues(Sets.newHashSet(values)); - return scriptVariable; - } - -} diff --git a/data-providers/src/test/java/datart/data/provider/function/SqlFunctionExamples.java b/data-providers/src/test/java/datart/data/provider/function/SqlFunctionExamples.java new file mode 100644 index 000000000..db1fd2ba2 --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/function/SqlFunctionExamples.java @@ -0,0 +1,76 @@ +package datart.data.provider.function; + +import java.util.ArrayList; +import java.util.List; + +public class SqlFunctionExamples { + + public static List functionList = new ArrayList<>(); + + static { + init(); + } + + private static void init(){ + functionList.add("SUM(num)"); + functionList.add("MAX(num)"); + functionList.add("MIN(num)"); + functionList.add("AVG(num)"); + functionList.add("distinct id"); + functionList.add("var(num)"); + functionList.add("STDDEV(num)"); + functionList.add("MEDIAN(num)"); + functionList.add("TRIM(str)"); + //functionList.add("TRIM(`name`)"); + functionList.add("LTRIM(str)"); + functionList.add("RTRIM(str)"); + functionList.add("count(*)"); + functionList.add("abs(num)"); + functionList.add("POWER(num)"); + functionList.add("floor(num)"); + functionList.add("ceiling(num)"); + functionList.add("round(num)"); + functionList.add("lower(num)"); + functionList.add("upper(num)"); + functionList.add("LENGTH(string)"); + functionList.add("concat(concat('2',concat('3','6')),'1')"); + functionList.add("REPLACE('teststr', 'str', 'Str')"); + functionList.add("NOW()"); + functionList.add("SECOND(cr_time)"); + functionList.add("MINUTE(cr_time)"); + functionList.add("HOUR(cr_time)"); + functionList.add("QUARTER(cr_time)"); + //functionList.add("DAY(cr_time)"); + functionList.add("WEEK(cr_time)"); + functionList.add("MONTH(cr_time)"); + functionList.add("YEAR(cr_time)"); + functionList.add("DAY_OF_WEEK(cr_time)"); + functionList.add("DAY_OF_MONTH(cr_time)"); + functionList.add("DAY_OF_YEAR(cr_time)"); + functionList.add("CASE WHEN AGE>=18 THEN '成年' ELSE '未成年' end"); + functionList.add("IFNULL(str)"); + functionList.add("IF(500<1000, 5, 10)"); + functionList.add("substr('string',1,3)"); + functionList.add("SQRT(num)"); + functionList.add("EXP(num)"); + functionList.add("LOG10(num)"); + functionList.add("LN(num)"); + functionList.add("MOD(29,3)"); + functionList.add("RAND(10)"); + functionList.add("DEGREES(10)"); + functionList.add("RADIANS(100)"); + functionList.add("TRUNC(12.345,2)"); + functionList.add("TRUNC(12.345,2)"); + functionList.add("SIGN(12.345,2)"); + functionList.add("ACOS(num)"); + functionList.add("ASIN(num)"); + functionList.add("ATAN(num)"); + functionList.add("ATAN2(num)"); + functionList.add("SIN(num)"); + functionList.add("COS(num)"); + functionList.add("TAN(num)"); + functionList.add("COT(num)"); + functionList.add("LENGTH(name)"); + //functionList.add("date_add(date1 ,interval - day(date2) + 1 day)"); + } +} diff --git a/data-providers/src/test/java/datart/data/provider/function/SqlFunctionValidateTest.java b/data-providers/src/test/java/datart/data/provider/function/SqlFunctionValidateTest.java new file mode 100644 index 000000000..6d95c6e9b --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/function/SqlFunctionValidateTest.java @@ -0,0 +1,30 @@ +package datart.data.provider.function; + +import datart.core.base.exception.Exceptions; +import datart.data.provider.calcite.SqlParserUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class SqlFunctionValidateTest { + + @Test + public void testFunctionValidate() { + List errorStr = new ArrayList<>(); + for (String s : SqlFunctionExamples.functionList) { + try { + SqlParserUtils.parseSnippet(s); + } catch (Exception e) { + errorStr.add(s); + } + } + if (errorStr.size()>0){ + Exceptions.msg("Functions validate failed: "+ StringUtils.join(errorStr, ",")); + } + log.info("Functions validate passed"); + } +} diff --git a/data-providers/src/test/java/datart/data/provider/sql/SqlScriptRenderTest.java b/data-providers/src/test/java/datart/data/provider/sql/SqlScriptRenderTest.java index bec35689c..4445bc4bd 100644 --- a/data-providers/src/test/java/datart/data/provider/sql/SqlScriptRenderTest.java +++ b/data-providers/src/test/java/datart/data/provider/sql/SqlScriptRenderTest.java @@ -18,25 +18,89 @@ package datart.data.provider.sql; +import datart.core.base.exception.Exceptions; import datart.core.data.provider.QueryScript; -import datart.data.provider.ParamFactory; +import datart.data.provider.DataProviderTestApplication; +import datart.data.provider.base.DataProviderException; import datart.data.provider.jdbc.SqlScriptRender; -import org.apache.calcite.sql.dialect.MysqlSqlDialect; +import datart.data.provider.sql.entity.SqlTestEntity; +import datart.data.provider.sql.common.ParamFactory; +import datart.data.provider.sql.examples.*; +import lombok.extern.slf4j.Slf4j; import org.apache.calcite.sql.parser.SqlParseException; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import java.util.List; + +@SpringBootTest(classes = DataProviderTestApplication.class) +@Slf4j public class SqlScriptRenderTest { + @Test + public void testNormalSqlTest() throws SqlParseException { + validateTestSql(NormalSqlExamples.sqlList, false); + log.info("NormalSqlScripts validate passed"); + } + + @Test + public void testSqlWithExecParam() throws SqlParseException { + validateTestSql(ExecParamSqlExamples.sqlList, false); + log.info("SqlWithExecParamSqlScripts validate passed"); + } + + @Test + public void testVariableSql() throws SqlParseException { + validateTestSql(VariableSqlExamples.sqlList, false); + log.info("VariableSqlScripts validate passed"); + } + + @Test + public void testFallbackSql() throws SqlParseException { + validateTestSql(FallbackSqlExamples.sqlList, false); + log.info("FallbackSqlScripts validate passed"); + } @Test - void testParamReplace() throws SqlParseException { + public void testForbiddenSql() throws SqlParseException { + validateForbiddenSql(ForbiddenSqlExamples.sqlList, false); + log.info("ForbiddenSqlScripts validate passed"); + } - for (QueryScript queryScriptExample : ParamFactory.getQueryScriptExamples()) { - SqlScriptRender render = new SqlScriptRender(queryScriptExample, null, new MysqlSqlDialect(MysqlSqlDialect.DEFAULT_CONTEXT)); - String sql = render.render(false, false, false); - System.out.println(sql); + @Test + public void testSpecialSql() throws SqlParseException { + validateTestSql(SpecialSqlExamples.sqlList, true); + log.info("SpecialSqlScripts validate passed"); + } + + private void validateTestSql(List list, boolean enableSpecialSql) throws SqlParseException { + for (SqlTestEntity sqlTest : list) { + QueryScript queryScript = ParamFactory.getQueryScriptExample(sqlTest.getSql()); + SqlScriptRender render = new SqlScriptRender(queryScript, sqlTest.getExecuteParam(), sqlTest.getSqlDialect(), enableSpecialSql); + boolean withExecParam = sqlTest.getExecuteParam()!=null; + String parsedSql = render.render(withExecParam, false, false); + boolean result = parsedSql.equals(sqlTest.getDesireSql()); + if (!result){ + Exceptions.msg("sql validate failed! \n" + sqlTest + + " the parsed sql: "+parsedSql); + } } + } + private void validateForbiddenSql(List list, boolean enableSpecialSql) throws SqlParseException { + for (SqlTestEntity sqlTest : list) { + QueryScript queryScript = ParamFactory.getQueryScriptExample(sqlTest.getSql()); + SqlScriptRender render = new SqlScriptRender(queryScript, null, sqlTest.getSqlDialect(), enableSpecialSql); + try { + render.render(false, false, false); + } catch (DataProviderException e) { + if ("message.sql.op.forbidden".equals(e.getMessage())){ + continue; + } + Exceptions.e(e); + } + Exceptions.msg("The forbidden sql test passed, should be forbid, enableSpecialSqlState: "+enableSpecialSql+"\n"+sqlTest); + } } } diff --git a/data-providers/src/test/java/datart/data/provider/sql/TestFallback.java b/data-providers/src/test/java/datart/data/provider/sql/TestFallback.java deleted file mode 100644 index 96cc98aa1..000000000 --- a/data-providers/src/test/java/datart/data/provider/sql/TestFallback.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Datart - *

- * Copyright 2021 - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package datart.data.provider.sql; - -import datart.core.data.provider.QueryScript; -import datart.data.provider.ParamFactory; -import datart.data.provider.jdbc.SqlScriptRender; -import org.apache.calcite.sql.dialect.MysqlSqlDialect; -import org.apache.calcite.sql.parser.SqlParseException; -import org.junit.jupiter.api.Test; - -public class TestFallback { - -// @Test - void testParamReplace() throws SqlParseException { - for (QueryScript queryScriptExample : ParamFactory.getErrorQueryScriptExamples()) { - SqlScriptRender render = new SqlScriptRender(queryScriptExample, null, new MysqlSqlDialect(MysqlSqlDialect.DEFAULT_CONTEXT)); - String sql = render.render(false, false, false); - System.out.println(sql); - } - - } - -} diff --git a/data-providers/src/test/java/datart/data/provider/sql/common/ParamFactory.java b/data-providers/src/test/java/datart/data/provider/sql/common/ParamFactory.java new file mode 100644 index 000000000..ae8fc45c0 --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/sql/common/ParamFactory.java @@ -0,0 +1,120 @@ +/* + * Datart + *

+ * Copyright 2021 + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package datart.data.provider.sql.common; + +import com.google.common.collect.Lists; +import datart.core.base.PageInfo; +import datart.core.base.consts.ValueType; +import datart.core.base.consts.VariableTypeEnum; +import datart.core.common.UUIDGenerator; +import datart.core.data.provider.ExecuteParam; +import datart.core.data.provider.QueryScript; +import datart.core.data.provider.ScriptVariable; +import datart.core.data.provider.sql.AggregateOperator; +import datart.core.data.provider.sql.FunctionColumn; +import datart.core.data.provider.sql.GroupByOperator; +import datart.core.data.provider.sql.OrderOperator; +import org.apache.commons.compress.utils.Sets; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +public class ParamFactory { + + private final static List variables = Lists.newArrayList( + variable("部门", ValueType.STRING, VariableTypeEnum.PERMISSION, false, "销售部") + , variable("age", ValueType.NUMERIC, VariableTypeEnum.QUERY, false, "20") + , variable("max", ValueType.NUMERIC, VariableTypeEnum.QUERY, false, "100") + , variable("min", ValueType.NUMERIC, VariableTypeEnum.QUERY, false, "0") + , variable("str", ValueType.STRING, VariableTypeEnum.QUERY, false, "content") + , variable("datetime", ValueType.STRING, VariableTypeEnum.QUERY, false, "2020-01-01 00:00:00") + , variable("date", ValueType.DATE, VariableTypeEnum.QUERY, false, "2020-01-01 00:00:00") + , variable("where", ValueType.FRAGMENT, VariableTypeEnum.QUERY, false, "1=1") + ); + + public static QueryScript getQueryScriptExample(String script) { + return QueryScript.builder().script(script) + .sourceId(UUIDGenerator.generate()) + .variables(new LinkedList<>(variables)) + .viewId(UUIDGenerator.generate()) + .test(false) + .build(); + } + + public static ExecuteParam getExecuteScriptExample() { + List columns = new ArrayList<>(); + columns.add("name"); + columns.add("age"); + + List functionColumns = new ArrayList<>(); + FunctionColumn functionColumn = new FunctionColumn(); + functionColumn.setAlias("ageNum"); + functionColumn.setSnippet("MAX(age)"); + functionColumns.add(functionColumn); + + List aggregateOperators = new ArrayList<>(); + AggregateOperator aggregateOperator = new AggregateOperator(); + aggregateOperator.setColumn("val"); + aggregateOperator.setSqlOperator(AggregateOperator.SqlOperator.SUM); + aggregateOperators.add(aggregateOperator); + + List groupByOperators = new ArrayList<>(); + GroupByOperator group = new GroupByOperator(); + group.setColumn("id"); + groupByOperators.add(group); + + List orderOperators = new ArrayList<>(); + OrderOperator orderOperator = new OrderOperator(); + orderOperator.setColumn("age"); + orderOperator.setOperator(OrderOperator.SqlOperator.DESC); + orderOperator.setAggOperator(AggregateOperator.SqlOperator.COUNT); + orderOperators.add(orderOperator); + + PageInfo pageInfo = new PageInfo(); + pageInfo.setPageNo(1); + pageInfo.setPageSize(100); + pageInfo.setTotal(234); + pageInfo.setCountTotal(false); + + return ExecuteParam.builder() + .columns(columns) + .functionColumns(functionColumns) + .aggregators(aggregateOperators) + .groups(groupByOperators) + .orders(orderOperators) + .includeColumns(new HashSet<>(columns)) + .pageInfo(pageInfo) + .concurrencyOptimize(false) + .serverAggregate(false) + .build(); + } + + private static ScriptVariable variable(String name, ValueType type, VariableTypeEnum variableType, boolean expression, String... values) { + ScriptVariable scriptVariable = new ScriptVariable(); + scriptVariable.setName(name); + scriptVariable.setValueType(type); + scriptVariable.setType(variableType); + scriptVariable.setExpression(expression); + scriptVariable.setValues(Sets.newHashSet(values)); + return scriptVariable; + } + +} diff --git a/data-providers/src/test/java/datart/data/provider/sql/common/TestSqlDialects.java b/data-providers/src/test/java/datart/data/provider/sql/common/TestSqlDialects.java new file mode 100644 index 000000000..840e7e973 --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/sql/common/TestSqlDialects.java @@ -0,0 +1,12 @@ +package datart.data.provider.sql.common; + +import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.dialect.MysqlSqlDialect; +import org.apache.calcite.sql.dialect.OracleSqlDialect; + +public class TestSqlDialects { + + public final static SqlDialect MYSQL = new MysqlSqlDialect(MysqlSqlDialect.DEFAULT_CONTEXT); + public final static SqlDialect ORACLE = new OracleSqlDialect(OracleSqlDialect.DEFAULT_CONTEXT); + +} diff --git a/data-providers/src/test/java/datart/data/provider/sql/entity/SqlTestEntity.java b/data-providers/src/test/java/datart/data/provider/sql/entity/SqlTestEntity.java new file mode 100644 index 000000000..d66952dc9 --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/sql/entity/SqlTestEntity.java @@ -0,0 +1,42 @@ +package datart.data.provider.sql.entity; + +import datart.core.data.provider.ExecuteParam; +import lombok.Data; +import org.apache.calcite.sql.SqlDialect; + +@Data +public class SqlTestEntity { + + private SqlDialect sqlDialect; + + private String sql; + + private String desireSql; + + private ExecuteParam executeParam; + + public SqlTestEntity() { + } + + public static SqlTestEntity createForbiddenSql(SqlDialect sqlDialect, String sql) { + SqlTestEntity rec = new SqlTestEntity(); + rec.setSqlDialect(sqlDialect); + rec.setSql(sql); + return rec; + } + + public static SqlTestEntity createValidateSql(SqlDialect sqlDialect, String sql, String desireSql) { + SqlTestEntity rec = new SqlTestEntity(); + rec.setSqlDialect(sqlDialect); + rec.setSql(sql); + rec.setDesireSql(desireSql); + return rec; + } + + @Override + public String toString() { + return " sqlDialect: " + sqlDialect.getDatabaseProduct().name() + '\n' + + " the origin sql: " + sql + '\n' + + " the desire sql: " + desireSql + '\n'; + } +} diff --git a/data-providers/src/test/java/datart/data/provider/sql/examples/ExecParamSqlExamples.java b/data-providers/src/test/java/datart/data/provider/sql/examples/ExecParamSqlExamples.java new file mode 100644 index 000000000..1ff16d173 --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/sql/examples/ExecParamSqlExamples.java @@ -0,0 +1,54 @@ +package datart.data.provider.sql.examples; + +import datart.core.data.provider.ExecuteParam; +import datart.data.provider.sql.entity.SqlTestEntity; +import datart.data.provider.sql.common.ParamFactory; +import datart.data.provider.sql.common.TestSqlDialects; +import org.junit.platform.commons.util.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ExecParamSqlExamples { + + public static List sqlList = new ArrayList<>(); + + private static Map execParamTemplate = new HashMap<>(); + + static { + initExecTemplateMap(); + initScripts(); + } + + private static void initScripts() { + ExecuteParam executeParam = ParamFactory.getExecuteScriptExample(); + sqlList.add(dealExecParam(executeParam, SqlTestEntity.createValidateSql(TestSqlDialects.MYSQL, + "SELECT * FROM test_table WHERE name not like 'a' and id <> '123' and age != 0 and year between 1990 and 2000 ", + "SELECT * FROM test_table WHERE name not like 'a' and id <> '123' and age != 0 and year between 1990 and 2000"))); + sqlList.add(dealExecParam(executeParam, SqlTestEntity.createValidateSql(TestSqlDialects.MYSQL, + "SELECT * FROM test_table where 部门=$部门$ and `age`>$age$ ", + "SELECT * FROM test_table where 部门 = '销售部' and `age` > 20"))); + + } + + private static SqlTestEntity dealExecParam(ExecuteParam executeParam, SqlTestEntity sqlTest){ + String template = execParamTemplate.getOrDefault(sqlTest.getSqlDialect().getDatabaseProduct().name(), ""); + if (StringUtils.isBlank(template)) { + return sqlTest; + } + sqlTest.setExecuteParam(executeParam); + sqlTest.setDesireSql(template.replace("&#%xxx%#&",sqlTest.getDesireSql())); + return sqlTest; + } + + private static void initExecTemplateMap() { + execParamTemplate.put(TestSqlDialects.MYSQL.getDatabaseProduct().name(), "SELECT `DATART_VTABLE`.`name` AS `name`, `DATART_VTABLE`.`age` AS `age`, SUM(`DATART_VTABLE`.`val`) AS `SUM(val)`, `DATART_VTABLE`.`id` " + + "FROM ( &#%xxx%#& ) AS `DATART_VTABLE` GROUP BY `DATART_VTABLE`.`id` ORDER BY COUNT(`DATART_VTABLE`.`age`) DESC"); + execParamTemplate.put(TestSqlDialects.ORACLE.getDatabaseProduct().name(), "SELECT \"DATART_VTABLE\".\"name\" \"name\", \"DATART_VTABLE\".\"age\" \"age\", SUM(\"DATART_VTABLE\".\"val\") \"SUM(val)\", \"DATART_VTABLE\".\"id\" " + + "FROM ( &#%xxx%#& ) \"DATART_VTABLE\" GROUP BY \"DATART_VTABLE\".\"id\" ORDER BY COUNT(\"DATART_VTABLE\".\"age\") DESC"); + } + + +} diff --git a/data-providers/src/test/java/datart/data/provider/sql/examples/FallbackSqlExamples.java b/data-providers/src/test/java/datart/data/provider/sql/examples/FallbackSqlExamples.java new file mode 100644 index 000000000..c9c4b75bc --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/sql/examples/FallbackSqlExamples.java @@ -0,0 +1,47 @@ +package datart.data.provider.sql.examples; + +import datart.data.provider.sql.entity.SqlTestEntity; +import datart.data.provider.sql.common.TestSqlDialects; +import org.apache.calcite.sql.SqlDialect; + +import java.util.ArrayList; +import java.util.List; + +public class FallbackSqlExamples { + + public static List sqlList = new ArrayList<>(); + + static { + initScripts(TestSqlDialects.MYSQL, TestSqlDialects.ORACLE); + } + + private static void initScripts(SqlDialect... sqlDialects){ + for (SqlDialect sqlDialect : sqlDialects) { + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * _FROM test_table where 部门=$部门$ and `age`>$age$ ", + "SELECT * _FROM test_table where 部门 = '销售部' and `age` > 20")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * _FROM test_table where 部门 IN ($部门$) ", + "SELECT * _FROM test_table where 部门 IN ('销售部')")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * _FROM test_table where 部门 = ($部门$) and 部门 = ($部门$) ", + "SELECT * _FROM test_table where 部门 = ('销售部') and 部门 = ('销售部')")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * _FROM test_table where age not like $min$% and name is not null", + "SELECT * _FROM test_table where age NOT LIKE 0% and name is not null")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * _FROM test_table where age between $min$ and $max$ ", + "SELECT * _FROM test_table where age between 0 and 100")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * _FROM test_table order by $部门$ limit $max$", + "SELECT * _FROM test_table order by '销售部' limit 100")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT name,age,$部门$ _FROM test_table order by $部门$ limit $max$", + "SELECT name,age,'销售部' _FROM test_table order by '销售部' limit 100")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT content,count(1) _FROM test_table group by $str$", + "SELECT content,count(1) _FROM test_table group by 'content'")); + } + } + +} \ No newline at end of file diff --git a/data-providers/src/test/java/datart/data/provider/sql/examples/ForbiddenSqlExamples.java b/data-providers/src/test/java/datart/data/provider/sql/examples/ForbiddenSqlExamples.java new file mode 100644 index 000000000..87148e39d --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/sql/examples/ForbiddenSqlExamples.java @@ -0,0 +1,66 @@ +package datart.data.provider.sql.examples; + +import datart.data.provider.sql.entity.SqlTestEntity; +import datart.data.provider.sql.common.TestSqlDialects; +import org.apache.calcite.sql.SqlDialect; + +import java.util.ArrayList; +import java.util.List; + +public class ForbiddenSqlExamples { + + public static List sqlList = new ArrayList<>(); + + static { + initScripts(TestSqlDialects.MYSQL, TestSqlDialects.ORACLE); + initMysqlScripts(); + initOracleScripts(); + } + + private static void initScripts(SqlDialect... sqlDialects) { + for (SqlDialect sqlDialect : sqlDialects) { + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "create database test")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "drop database test")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "use test; delete from test_table")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "create table test_table(id int)")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "alter table test_table add age int")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "drop table test_table")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "insert into test_table value(1)")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "update test_table set age=18 where id=1")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "delete from test_table where id=1")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "commit")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "rollback")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "CREATE INDEX indexName ON table_name (column_name)")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "ALTER table tableName ADD INDEX indexName(columnName)")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "DROP INDEX [indexName] ON mytable")); + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "ALTER table tableName ADD INDEX indexName(columnName)")); + } + } + + private static void initMysqlScripts(){ + SqlDialect sqlDialect = TestSqlDialects.MYSQL; + sqlList.add(SqlTestEntity.createForbiddenSql(sqlDialect, + "replace INTO test_table (id) VALUES(123)")); + } + + private static void initOracleScripts(){ + SqlDialect sqlDialect = TestSqlDialects.ORACLE; + + } + +} diff --git a/data-providers/src/test/java/datart/data/provider/sql/examples/NormalSqlExamples.java b/data-providers/src/test/java/datart/data/provider/sql/examples/NormalSqlExamples.java new file mode 100644 index 000000000..d9afaa529 --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/sql/examples/NormalSqlExamples.java @@ -0,0 +1,74 @@ +package datart.data.provider.sql.examples; + +import datart.data.provider.sql.common.TestSqlDialects; +import datart.data.provider.sql.entity.SqlTestEntity; +import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.dialect.ClickHouseSqlDialect; + +import java.util.ArrayList; +import java.util.List; + +public class NormalSqlExamples { + + public static List sqlList = new ArrayList<>(); + + static { + initScripts(TestSqlDialects.MYSQL, TestSqlDialects.ORACLE); + initMysqlScripts(); + initOracleScripts(); + } + + private static void initScripts(SqlDialect... sqlDialects){ + for (SqlDialect sqlDialect : sqlDialects) { + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "-- comment\n" + "SELECT t.name FROM test_table t ", + "SELECT t.name FROM test_table t")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "/*test\n" + "multiline \n" + "comment*/" + "SELECT * FROM test_table WHERE name='a' ORDER BY id DESC ", + "SELECT * FROM test_table WHERE name='a' ORDER BY id DESC")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * FROM test_table WHERE name not like 'a' and id <> '123' and age != 0 and year between 1990 and 2000", + "SELECT * FROM test_table WHERE name not like 'a' and id <> '123' and age != 0 and year between 1990 and 2000")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * FROM test_table WHERE name not like 'a' and id <> '123' and age != 0 and year between 1990 and 2000 ", + "SELECT * FROM test_table WHERE name not like 'a' and id <> '123' and age != 0 and year between 1990 and 2000")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "select `date` from tableName", + "select `date` from tableName")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "select IFNULL(id),SUM(num),MAX(age),AVG(score),TRIM(content) from tableName", + "select IFNULL(id),SUM(num),MAX(age),AVG(score),TRIM(content) from tableName")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "select distinct age from tableName union select distinct age from tableName2", + "select distinct age from tableName union select distinct age from tableName2")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "select concat(concat('1', age), id) from test_table", + "select concat(concat('1', age), id) from test_table")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "with RECURSIVE c(n) as " + + " (select 1 union all select n + 1 from c where n < 10) " + + " select n from c", + "with RECURSIVE c(n) as " + + " (select 1 union all select n + 1 from c where n < 10) " + + " select n from c")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "select INSERT('Football',0,4,'Play') AS col1 from test_table", + "select INSERT('Football',0,4,'Play') AS col1 from test_table")); + } + } + + private static void initMysqlScripts() { + SqlDialect sqlDialect = new ClickHouseSqlDialect(ClickHouseSqlDialect.DEFAULT_CONTEXT); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "select date_add(oclife_time, interval-day(oclife_time)+1 day) as dt from ttt", + "select date_add(oclife_time, interval-day(oclife_time)+1 day) as dt from ttt")); + } + + private static void initOracleScripts() { + SqlDialect sqlDialect = TestSqlDialects.ORACLE; + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "select * from test_table where age between 0 and 20", + "select * from test_table where age between 0 and 20")); + } + +} \ No newline at end of file diff --git a/data-providers/src/test/java/datart/data/provider/sql/examples/SpecialSqlExamples.java b/data-providers/src/test/java/datart/data/provider/sql/examples/SpecialSqlExamples.java new file mode 100644 index 000000000..52047d6ce --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/sql/examples/SpecialSqlExamples.java @@ -0,0 +1,40 @@ +package datart.data.provider.sql.examples; + +import datart.data.provider.sql.entity.SqlTestEntity; +import datart.data.provider.sql.common.TestSqlDialects; +import org.apache.calcite.sql.SqlDialect; + +import java.util.ArrayList; +import java.util.List; + +public class SpecialSqlExamples { + + public static List sqlList = new ArrayList<>(); + + static { + initScripts(TestSqlDialects.MYSQL, TestSqlDialects.ORACLE); + initMysqlScripts(); + initOracleScripts(); + } + + private static void initScripts(SqlDialect... sqlDialects) { + for (SqlDialect sqlDialect : sqlDialects) { + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "Special sql", + "Special sql")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "replace INTO test_table (id) VALUES(123)", + "replace INTO test_table (id) VALUES(123)")); + } + } + + private static void initMysqlScripts(){ + SqlDialect sqlDialect = TestSqlDialects.MYSQL; + + } + + private static void initOracleScripts() { + SqlDialect sqlDialect = TestSqlDialects.ORACLE; + + } +} diff --git a/data-providers/src/test/java/datart/data/provider/sql/examples/VariableSqlExamples.java b/data-providers/src/test/java/datart/data/provider/sql/examples/VariableSqlExamples.java new file mode 100644 index 000000000..86da62da8 --- /dev/null +++ b/data-providers/src/test/java/datart/data/provider/sql/examples/VariableSqlExamples.java @@ -0,0 +1,73 @@ +package datart.data.provider.sql.examples; + +import datart.data.provider.sql.entity.SqlTestEntity; +import datart.data.provider.sql.common.TestSqlDialects; +import org.apache.calcite.sql.SqlDialect; + +import java.util.ArrayList; +import java.util.List; + +public class VariableSqlExamples { + + public static List sqlList = new ArrayList<>(); + + static { + initScripts(TestSqlDialects.MYSQL, TestSqlDialects.ORACLE); + initMysqlScripts(); + initOracleScripts(); + } + + private static void initScripts(SqlDialect... sqlDialects){ + for (SqlDialect sqlDialect : sqlDialects) { + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * FROM test_table where 部门=$部门$ and `age`>$age$ ", + "SELECT * FROM test_table where 部门 = '销售部' and `age` > 20")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * FROM test_table where 部门 IN ($部门$) ", + "SELECT * FROM test_table where 部门 IN ('销售部')")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * FROM test_table where 部门 IN ($部门$) and 部门 IN ($部门$) and 部门 = $部门$", + "SELECT * FROM test_table where 部门 IN ('销售部') and 部门 IN ('销售部') and 部门 = '销售部'")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT name,age,$部门$ FROM test_table order by $部门$ limit $max$", + "SELECT name,age,'销售部' FROM test_table order by '销售部' limit 100")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT content,count(1) FROM test_table group by $str$", + "SELECT content,count(1) FROM test_table group by 'content'")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * FROM `test_table` where AGE BETWEEN $min$ AND $max$ ", + "SELECT * FROM `test_table` where AGE BETWEEN 0 AND 100")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "select date_format(create_time, '%Y-%m') as month, date_add(create_time, interval-day(create_time)+1 day) dt, sum(price) price,bitmap_count(bitmap_union(id)) as num " + + "from db.test_table " + + "where create_time >= $datetime$ and id in (10000, 10001) group by month,dt,id", + "select date_format(create_time, '%Y-%m') as month, date_add(create_time, interval-day(create_time)+1 day) dt, sum(price) price,bitmap_count(bitmap_union(id)) as num " + + "from db.test_table " + + "where create_time >= '2020-01-01 00:00:00' and id in (10000, 10001) group by month,dt,id")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "select INSERT('Football',$min$,4,'Play') AS col1 from test_table limit $max$", + "select INSERT('Football',0,4,'Play') AS col1 from test_table limit 100")); + } + } + + private static void initMysqlScripts() { + SqlDialect sqlDialect = TestSqlDialects.MYSQL; + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "select * from test_table where $where$ and create_time > $date$", + "select * from test_table where 1=1 and create_time > TIMESTAMP '2020-01-01 00:00:00'")); + } + + private static void initOracleScripts() { + SqlDialect sqlDialect = TestSqlDialects.ORACLE; + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * FROM test_table where age BETWEEN $min$ AND 100 ", + "SELECT * FROM test_table where AGE BETWEEN 0 AND 100")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "SELECT * FROM 'test_table' where age BETWEEN $min$ AND 100 ", + "SELECT * FROM 'test_table' where age BETWEEN 0 AND 100")); + sqlList.add(SqlTestEntity.createValidateSql(sqlDialect, + "select * from test_table where $where$ and create_time > $date$", + "select * from test_table where 1=1 and CREATE_TIME > TO_TIMESTAMP('2020-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS.FF')")); + } + +} \ No newline at end of file diff --git a/data-providers/src/test/resources/log4j.properties b/data-providers/src/test/resources/log4j.properties new file mode 100644 index 000000000..674dfd37b --- /dev/null +++ b/data-providers/src/test/resources/log4j.properties @@ -0,0 +1,4 @@ +log4j.rootLogger=WARN,CONSOLE +log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender +log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout +log4j.appender.CONSOLE.layout.ConversionPattern=[frame] %d{yyyy-MM-dd HH:mm:ss,SSS} - %-4r %-5p [%t] %C:%L %x - %m%n \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index 40ec22b91..674475b6a 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -36,3 +36,4 @@ yarn-error.log* /types /public/task +/public/antd/theme.less \ No newline at end of file diff --git a/frontend/craco.config.js b/frontend/craco.config.js index 6b1b071fd..c83746b92 100644 --- a/frontend/craco.config.js +++ b/frontend/craco.config.js @@ -1,6 +1,5 @@ const path = require('path'); const fs = require('fs'); -const CracoLessPlugin = require('craco-less'); const WebpackBar = require('webpackbar'); const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); // const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); @@ -32,29 +31,6 @@ module.exports = { babel: { plugins: ['babel-plugin-styled-components'], }, - plugins: [ - { - plugin: CracoLessPlugin, - options: { - lessLoaderOptions: { - lessOptions: { - modifyVars: { - '@primary-color': '#1B9AEE', - '@success-color': '#15AD31', - '@processing-color': '#1B9AEE', - '@error-color': '#E62412', - '@highlight-color': '#E62412', - '@warning-color': '#FA8C15', - '@text-color': '#212529', - '@text-color-secondary': '#495057', - '@heading-color': '#212529', - }, - javascriptEnabled: true, - }, - }, - }, - }, - ], webpack: { alias: {}, plugins: [ @@ -133,6 +109,14 @@ module.exports = { return webpackConfig; }, }, + jest: { + configure: (jestConfig, { env, paths, resolve, rootDir }) => { + return Object.assign(jestConfig, { + setupFiles: ['jest-canvas-mock'], + }); + }, + modulePaths: ['../'], + }, devServer: { before: function (app, server, compiler) { diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 53f4ff53f..5b7eede55 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -19,6 +19,6 @@ const { createJestConfig } = require('@craco/craco'); const cracoConfig = require('./craco.config.js'); -const jestConfig = createJestConfig(cracoConfig, {}, { displayName: 'Datart' }); +const jestConfig = createJestConfig(cracoConfig, {}); module.exports = jestConfig; diff --git a/frontend/package.json b/frontend/package.json index 04db09e38..582490174 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,7 +15,9 @@ "license": "Apache-2.0", "scripts": { "bootstrap": "npm install --legacy-peer-deps", + "prestart": "npm run extractAntdTheme", "start": "craco start", + "prebuild": "npm run extractAntdTheme", "build": "cross-env GENERATE_SOURCEMAP=false craco build", "build:task": "rollup -c", "build:all": "npm run build:task && npm run build", @@ -31,7 +33,8 @@ "prepare": "cd .. && husky install frontend/.husky", "eject": "react-scripts eject", "doc:types": "tsc --project tsconfig.dHelper.json", - "doc:html": "jsdoc -c jsdoc.config.json" + "doc:html": "jsdoc -c jsdoc.config.json", + "extractAntdTheme": "node scripts/extractAntdTheme" }, "eslintConfig": { "extends": [ @@ -95,10 +98,11 @@ "@reduxjs/toolkit": "^1.7.1", "@types/react-color": "^3.0.5", "antd": "4.16.13", + "antd-theme-generator": "1.2.11", "axios": "^0.21.1", "classnames": "^2.3.1", "debounce-promise": "3.1.2", - "echarts": "^5.1.1", + "echarts": "5.3.1", "echarts-wordcloud": "^2.0.0", "file-saver": "^2.0.5", "flexlayout-react": "^0.5.12", @@ -114,6 +118,7 @@ "quilljs-markdown": "1.1.10", "react": "^17.0.1", "react-app-polyfill": "^2.0.0", + "react-beautiful-dnd": "^13.1.0", "react-color": "^2.19.3", "react-dev-inspector": "^1.6.0", "react-dnd": "^14.0.2", @@ -135,7 +140,8 @@ "reveal.js": "^4.1.0", "split.js": "^1.6.4", "sql-formatter": "^4.0.2", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "video-react": "^0.15.0" }, "devDependencies": { "@babel/core": "^7.15.8", @@ -160,6 +166,7 @@ "@types/react-dom": "^17.0.1", "@types/react-grid-layout": "^1.1.1", "@types/react-redux": "^7.1.16", + "@types/react-resizable": "^1.7.4", "@types/react-router-dom": "^5.1.7", "@types/react-test-renderer": "^17.0.1", "@types/styled-components": "5.1.20", @@ -168,7 +175,6 @@ "@types/webpack-env": "1.15.2", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.0", "babel-plugin-styled-components": "2.0.2", - "craco-less": "^1.17.1", "cross-env": "^7.0.3", "cz-conventional-changelog": "^3.3.0", "docdash": "^1.2.0", @@ -179,6 +185,7 @@ "eslint-plugin-react-hooks": "^4.2.0", "html2canvas": "^1.3.2", "husky": "^6.0.0", + "jest-canvas-mock": "^2.3.1", "jest-styled-components": "7.0.8", "jsdoc": "^3.6.10", "lint-staged": "^10.5.4", @@ -210,4 +217,4 @@ "path": "cz-conventional-changelog" } } -} +} \ No newline at end of file diff --git a/frontend/public/antd/less.min.js b/frontend/public/antd/less.min.js new file mode 100644 index 000000000..dc1636d43 --- /dev/null +++ b/frontend/public/antd/less.min.js @@ -0,0 +1,17 @@ +/*! + * Less - Leaner CSS v2.7.2 + * http://lesscss.org + * + * Copyright (c) 2009-2017, Alexis Sellier + * Licensed under the Apache-2.0 License. + * + */ + + /** * @license Apache-2.0 + */ + + !function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.less=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g0||b.isFileProtocol?"development":"production");var c=/!dumpLineNumbers:(comments|mediaquery|all)/.exec(a.location.hash);c&&(b.dumpLineNumbers=c[1]),void 0===b.useFileCache&&(b.useFileCache=!0),void 0===b.onReady&&(b.onReady=!0)}},{"./browser":3,"./utils":10}],2:[function(a,b,c){function d(a){a.filename&&console.warn(a),e.async||h.removeChild(i)}a("promise/polyfill.js");var e=window.less||{};a("./add-default-options")(window,e);var f=b.exports=a("./index")(window,e);window.less=f;var g,h,i;e.onReady&&(/!watch/.test(window.location.hash)&&f.watch(),e.async||(g="body { display: none !important }",h=document.head||document.getElementsByTagName("head")[0],i=document.createElement("style"),i.type="text/css",i.styleSheet?i.styleSheet.cssText=g:i.appendChild(document.createTextNode(g)),h.appendChild(i)),f.registerStylesheetsImmediately(),f.pageLoadFinished=f.refresh("development"===f.env).then(d,d))},{"./add-default-options":1,"./index":8,"promise/polyfill.js":97}],3:[function(a,b,c){var d=a("./utils");b.exports={createCSS:function(a,b,c){var e=c.href||"",f="less:"+(c.title||d.extractId(e)),g=a.getElementById(f),h=!1,i=a.createElement("style");i.setAttribute("type","text/css"),c.media&&i.setAttribute("media",c.media),i.id=f,i.styleSheet||(i.appendChild(a.createTextNode(b)),h=null!==g&&g.childNodes.length>0&&i.childNodes.length>0&&g.firstChild.nodeValue===i.firstChild.nodeValue);var j=a.getElementsByTagName("head")[0];if(null===g||h===!1){var k=c&&c.nextSibling||null;k?k.parentNode.insertBefore(i,k):j.appendChild(i)}if(g&&h===!1&&g.parentNode.removeChild(g),i.styleSheet)try{i.styleSheet.cssText=b}catch(l){throw new Error("Couldn't reassign styleSheet.cssText.")}},currentScript:function(a){var b=a.document;return b.currentScript||function(){var a=b.getElementsByTagName("script");return a[a.length-1]}()}}},{"./utils":10}],4:[function(a,b,c){b.exports=function(a,b,c){var d=null;if("development"!==b.env)try{d="undefined"==typeof a.localStorage?null:a.localStorage}catch(e){}return{setCSS:function(a,b,e,f){if(d){c.info("saving "+a+" to cache.");try{d.setItem(a,f),d.setItem(a+":timestamp",b),e&&d.setItem(a+":vars",JSON.stringify(e))}catch(g){c.error('failed to save "'+a+'" to local storage for caching.')}}},getCSS:function(a,b,c){var e=d&&d.getItem(a),f=d&&d.getItem(a+":timestamp"),g=d&&d.getItem(a+":vars");if(c=c||{},f&&b.lastModified&&new Date(b.lastModified).valueOf()===new Date(f).valueOf()&&(!c&&!g||JSON.stringify(c)===g))return e}}}},{}],5:[function(a,b,c){var d=a("./utils"),e=a("./browser");b.exports=function(a,b,c){function f(b,f){var g,h,i="less-error-message:"+d.extractId(f||""),j='

  • {content}
  • ',k=a.document.createElement("div"),l=[],m=b.filename||f,n=m.match(/([^\/]+(\?.*)?)$/)[1];k.id=i,k.className="less-error-message",h="

    "+(b.type||"Syntax")+"Error: "+(b.message||"There is an error in your .less file")+'

    in '+n+" ";var o=function(a,b,c){void 0!==a.extract[b]&&l.push(j.replace(/\{line\}/,(parseInt(a.line,10)||0)+(b-1)).replace(/\{class\}/,c).replace(/\{content\}/,a.extract[b]))};b.extract&&(o(b,0,""),o(b,1,"line"),o(b,2,""),h+="on line "+b.line+", column "+(b.column+1)+":

      "+l.join("")+"
    "),b.stack&&(b.extract||c.logLevel>=4)&&(h+="
    Stack Trace
    "+b.stack.split("\n").slice(1).join("
    ")),k.innerHTML=h,e.createCSS(a.document,[".less-error-message ul, .less-error-message li {","list-style-type: none;","margin-right: 15px;","padding: 4px 0;","margin: 0;","}",".less-error-message label {","font-size: 12px;","margin-right: 15px;","padding: 4px 0;","color: #cc7777;","}",".less-error-message pre {","color: #dd6666;","padding: 4px 0;","margin: 0;","display: inline-block;","}",".less-error-message pre.line {","color: #ff0000;","}",".less-error-message h3 {","font-size: 20px;","font-weight: bold;","padding: 15px 0 5px 0;","margin: 0;","}",".less-error-message a {","color: #10a","}",".less-error-message .error {","color: red;","font-weight: bold;","padding-bottom: 2px;","border-bottom: 1px dashed red;","}"].join("\n"),{title:"error-message"}),k.style.cssText=["font-family: Arial, sans-serif","border: 1px solid #e00","background-color: #eee","border-radius: 5px","-webkit-border-radius: 5px","-moz-border-radius: 5px","color: #e00","padding: 15px","margin-bottom: 15px"].join(";"),"development"===c.env&&(g=setInterval(function(){var b=a.document,c=b.body;c&&(b.getElementById(i)?c.replaceChild(k,b.getElementById(i)):c.insertBefore(k,c.firstChild),clearInterval(g))},10))}function g(b){var c=a.document.getElementById("less-error-message:"+d.extractId(b));c&&c.parentNode.removeChild(c)}function h(a){}function i(a){c.errorReporting&&"html"!==c.errorReporting?"console"===c.errorReporting?h(a):"function"==typeof c.errorReporting&&c.errorReporting("remove",a):g(a)}function j(a,d){var e="{line} {content}",f=a.filename||d,g=[],h=(a.type||"Syntax")+"Error: "+(a.message||"There is an error in your .less file")+" in "+f+" ",i=function(a,b,c){void 0!==a.extract[b]&&g.push(e.replace(/\{line\}/,(parseInt(a.line,10)||0)+(b-1)).replace(/\{class\}/,c).replace(/\{content\}/,a.extract[b]))};a.extract&&(i(a,0,""),i(a,1,"line"),i(a,2,""),h+="on line "+a.line+", column "+(a.column+1)+":\n"+g.join("\n")),a.stack&&(a.extract||c.logLevel>=4)&&(h+="\nStack Trace\n"+a.stack),b.logger.error(h)}function k(a,b){c.errorReporting&&"html"!==c.errorReporting?"console"===c.errorReporting?j(a,b):"function"==typeof c.errorReporting&&c.errorReporting("add",a,b):f(a,b)}return{add:k,remove:i}}},{"./browser":3,"./utils":10}],6:[function(a,b,c){b.exports=function(b,c){function d(){if(window.XMLHttpRequest&&!("file:"===window.location.protocol&&"ActiveXObject"in window))return new XMLHttpRequest;try{return new ActiveXObject("Microsoft.XMLHTTP")}catch(a){return c.error("browser doesn't support AJAX."),null}}var e=a("../less/environment/abstract-file-manager.js"),f={},g=function(){};return g.prototype=new e,g.prototype.alwaysMakePathsAbsolute=function(){return!0},g.prototype.join=function(a,b){return a?this.extractUrlParts(b,a).path:b},g.prototype.doXHR=function(a,e,f,g){function h(b,c,d){b.status>=200&&b.status<300?c(b.responseText,b.getResponseHeader("Last-Modified")):"function"==typeof d&&d(b.status,a)}var i=d(),j=!b.isFileProtocol||b.fileAsync;"function"==typeof i.overrideMimeType&&i.overrideMimeType("text/css"),c.debug("XHR: Getting '"+a+"'"),i.open("GET",a,j),i.setRequestHeader("Accept",e||"text/x-less, text/css; q=0.9, */*; q=0.5"),i.send(null),b.isFileProtocol&&!b.fileAsync?0===i.status||i.status>=200&&i.status<300?f(i.responseText):g(i.status,a):j?i.onreadystatechange=function(){4==i.readyState&&h(i,f,g)}:h(i,f,g)},g.prototype.supports=function(a,b,c,d){return!0},g.prototype.clearFileCache=function(){f={}},g.prototype.loadFile=function(a,b,c,d,e){b&&!this.isPathAbsolute(a)&&(a=b+a),c=c||{};var g=this.extractUrlParts(a,window.location.href),h=g.url;if(c.useFileCache&&f[h])try{var i=f[h];e(null,{contents:i,filename:h,webInfo:{lastModified:new Date}})}catch(j){e({filename:h,message:"Error loading file "+h+" error was "+j.message})}else this.doXHR(h,c.mime,function(a,b){f[h]=a,e(null,{contents:a,filename:h,webInfo:{lastModified:b}})},function(a,b){e({type:"File",message:"'"+b+"' wasn't found ("+a+")",href:h})})},g}},{"../less/environment/abstract-file-manager.js":15}],7:[function(a,b,c){b.exports=function(){function b(){throw{type:"Runtime",message:"Image size functions are not supported in browser version of less"}}var c=a("./../less/functions/function-registry"),d={"image-size":function(a){return b(this,a),-1},"image-width":function(a){return b(this,a),-1},"image-height":function(a){return b(this,a),-1}};c.addMultiple(d)}},{"./../less/functions/function-registry":22}],8:[function(a,b,c){var d=a("./utils").addDataAttr,e=a("./browser");b.exports=function(b,c){function f(a){return c.postProcessor&&"function"==typeof c.postProcessor&&(a=c.postProcessor.call(a,a)||a),a}function g(a){var b={};for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}function h(a,b){var c=Array.prototype.slice.call(arguments,2);return function(){var d=c.concat(Array.prototype.slice.call(arguments,0));return a.apply(b,d)}}function i(a){for(var b,d=m.getElementsByTagName("style"),e=0;e=c&&console.log(a)},info:function(a){b.logLevel>=d&&console.log(a)},warn:function(a){b.logLevel>=e&&console.warn(a)},error:function(a){b.logLevel>=f&&console.error(a)}}]);for(var g=0;g0&&(a=a.slice(0,b)),b=a.lastIndexOf("/"),b<0&&(b=a.lastIndexOf("\\")),b<0?"":a.slice(0,b+1)},d.prototype.tryAppendExtension=function(a,b){return/(\.[a-z]*$)|([\?;].*)$/.test(a)?a:a+b},d.prototype.tryAppendLessExtension=function(a){return this.tryAppendExtension(a,".less")},d.prototype.supportsSync=function(){return!1},d.prototype.alwaysMakePathsAbsolute=function(){return!1},d.prototype.isPathAbsolute=function(a){return/^(?:[a-z-]+:|\/|\\|#)/i.test(a)},d.prototype.join=function(a,b){return a?a+b:b},d.prototype.pathDiff=function(a,b){var c,d,e,f,g=this.extractUrlParts(a),h=this.extractUrlParts(b),i="";if(g.hostPart!==h.hostPart)return"";for(d=Math.max(h.directories.length,g.directories.length),c=0;c0&&(h.splice(c-1,2),c-=2)}return g.hostPart=f[1],g.directories=h,g.path=(f[1]||"")+h.join("/"),g.fileUrl=g.path+(f[4]||""),g.url=g.fileUrl+(f[5]||""),g},b.exports=d},{}],16:[function(a,b,c){var d=a("../logger"),e=function(a,b){this.fileManagers=b||[],a=a||{};for(var c=["encodeBase64","mimeLookup","charsetLookup","getSourceMapGenerator"],d=[],e=d.concat(c),f=0;f=0;h--){var i=g[h];if(i[f?"supportsSync":"supports"](a,b,c,e))return i}return null},e.prototype.addFileManager=function(a){this.fileManagers.push(a)},e.prototype.clearFileManagers=function(){this.fileManagers=[]},b.exports=e},{"../logger":33}],17:[function(a,b,c){function d(a,b,c){var d,f,g,h,i=b.alpha,j=c.alpha,k=[];g=j+i*(1-j);for(var l=0;l<3;l++)d=b.rgb[l]/255,f=c.rgb[l]/255,h=a(d,f),g&&(h=(j*f+i*(d-j*(d+f-h)))/g),k[l]=255*h;return new e(k,g)}var e=a("../tree/color"),f=a("./function-registry"),g={multiply:function(a,b){return a*b},screen:function(a,b){return a+b-a*b},overlay:function(a,b){return a*=2,a<=1?g.multiply(a,b):g.screen(a-1,b)},softlight:function(a,b){var c=1,d=a;return b>.5&&(d=1,c=a>.25?Math.sqrt(a):((16*a-12)*a+4)*a),a-(1-2*b)*d*(c-a)},hardlight:function(a,b){return g.overlay(b,a)},difference:function(a,b){return Math.abs(a-b)},exclusion:function(a,b){return a+b-2*a*b},average:function(a,b){return(a+b)/2},negation:function(a,b){return 1-Math.abs(a+b-1)}};for(var h in g)g.hasOwnProperty(h)&&(d[h]=d.bind(null,g[h]));f.addMultiple(d)},{"../tree/color":50,"./function-registry":22}],18:[function(a,b,c){function d(a){return Math.min(1,Math.max(0,a))}function e(a){return h.hsla(a.h,a.s,a.l,a.a)}function f(a){if(a instanceof i)return parseFloat(a.unit.is("%")?a.value/100:a.value);if("number"==typeof a)return a;throw{type:"Argument",message:"color functions take numbers as parameters"}}function g(a,b){return a instanceof i&&a.unit.is("%")?parseFloat(a.value*b/100):f(a)}var h,i=a("../tree/dimension"),j=a("../tree/color"),k=a("../tree/quoted"),l=a("../tree/anonymous"),m=a("./function-registry");h={rgb:function(a,b,c){return h.rgba(a,b,c,1)},rgba:function(a,b,c,d){var e=[a,b,c].map(function(a){return g(a,255)});return d=f(d),new j(e,d)},hsl:function(a,b,c){return h.hsla(a,b,c,1)},hsla:function(a,b,c,e){function g(a){return a=a<0?a+1:a>1?a-1:a,6*a<1?i+(j-i)*a*6:2*a<1?j:3*a<2?i+(j-i)*(2/3-a)*6:i}var i,j;return a=f(a)%360/360,b=d(f(b)),c=d(f(c)),e=d(f(e)),j=c<=.5?c*(b+1):c+b-c*b,i=2*c-j,h.rgba(255*g(a+1/3),255*g(a),255*g(a-1/3),e)},hsv:function(a,b,c){return h.hsva(a,b,c,1)},hsva:function(a,b,c,d){a=f(a)%360/360*360,b=f(b),c=f(c),d=f(d);var e,g;e=Math.floor(a/60%6),g=a/60-e;var i=[c,c*(1-b),c*(1-g*b),c*(1-(1-g)*b)],j=[[0,3,1],[2,0,1],[1,0,3],[1,2,0],[3,1,0],[0,1,2]];return h.rgba(255*i[j[e][0]],255*i[j[e][1]],255*i[j[e][2]],d)},hue:function(a){return new i(a.toHSL().h)},saturation:function(a){return new i(100*a.toHSL().s,"%")},lightness:function(a){return new i(100*a.toHSL().l,"%")},hsvhue:function(a){return new i(a.toHSV().h)},hsvsaturation:function(a){return new i(100*a.toHSV().s,"%")},hsvvalue:function(a){return new i(100*a.toHSV().v,"%")},red:function(a){return new i(a.rgb[0])},green:function(a){return new i(a.rgb[1])},blue:function(a){return new i(a.rgb[2])},alpha:function(a){return new i(a.toHSL().a)},luma:function(a){return new i(a.luma()*a.alpha*100,"%")},luminance:function(a){var b=.2126*a.rgb[0]/255+.7152*a.rgb[1]/255+.0722*a.rgb[2]/255;return new i(b*a.alpha*100,"%")},saturate:function(a,b,c){if(!a.rgb)return null;var f=a.toHSL();return f.s+="undefined"!=typeof c&&"relative"===c.value?f.s*b.value/100:b.value/100,f.s=d(f.s),e(f)},desaturate:function(a,b,c){var f=a.toHSL();return f.s-="undefined"!=typeof c&&"relative"===c.value?f.s*b.value/100:b.value/100,f.s=d(f.s),e(f)},lighten:function(a,b,c){var f=a.toHSL();return f.l+="undefined"!=typeof c&&"relative"===c.value?f.l*b.value/100:b.value/100,f.l=d(f.l),e(f)},darken:function(a,b,c){var f=a.toHSL();return f.l-="undefined"!=typeof c&&"relative"===c.value?f.l*b.value/100:b.value/100,f.l=d(f.l),e(f)},fadein:function(a,b,c){var f=a.toHSL();return f.a+="undefined"!=typeof c&&"relative"===c.value?f.a*b.value/100:b.value/100,f.a=d(f.a),e(f)},fadeout:function(a,b,c){var f=a.toHSL();return f.a-="undefined"!=typeof c&&"relative"===c.value?f.a*b.value/100:b.value/100,f.a=d(f.a),e(f)},fade:function(a,b){var c=a.toHSL();return c.a=b.value/100,c.a=d(c.a),e(c)},spin:function(a,b){var c=a.toHSL(),d=(c.h+b.value)%360;return c.h=d<0?360+d:d,e(c)},mix:function(a,b,c){a.toHSL&&b.toHSL||(console.log(b.type),console.dir(b)),c||(c=new i(50));var d=c.value/100,e=2*d-1,f=a.toHSL().a-b.toHSL().a,g=((e*f==-1?e:(e+f)/(1+e*f))+1)/2,h=1-g,k=[a.rgb[0]*g+b.rgb[0]*h,a.rgb[1]*g+b.rgb[1]*h,a.rgb[2]*g+b.rgb[2]*h],l=a.alpha*d+b.alpha*(1-d);return new j(k,l)},greyscale:function(a){return h.desaturate(a,new i(100))},contrast:function(a,b,c,d){if(!a.rgb)return null;if("undefined"==typeof c&&(c=h.rgba(255,255,255,1)),"undefined"==typeof b&&(b=h.rgba(0,0,0,1)),b.luma()>c.luma()){var e=c;c=b,b=e}return d="undefined"==typeof d?.43:f(d),a.luma()=t&&this.context.ieCompat!==!1?(g.warn("Skipped data-uri embedding of "+i+" because its size ("+s.length+" characters) exceeds IE8-safe "+t+" characters!"),f(this,e||a)):new d(new c('"'+s+'"',s,(!1),this.index,this.currentFileInfo),this.index,this.currentFileInfo)})}},{"../logger":33,"../tree/quoted":73,"../tree/url":80,"./function-registry":22}],20:[function(a,b,c){var d=a("../tree/keyword"),e=a("./function-registry"),f={eval:function(){var a=this.value_,b=this.error_;if(b)throw b;if(null!=a)return a?d.True:d.False},value:function(a){this.value_=a},error:function(a){this.error_=a},reset:function(){this.value_=this.error_=null}};e.add("default",f.eval.bind(f)),b.exports=f},{"../tree/keyword":65,"./function-registry":22}],21:[function(a,b,c){var d=a("../tree/expression"),e=function(a,b,c,d){this.name=a.toLowerCase(),this.index=c,this.context=b,this.currentFileInfo=d,this.func=b.frames[0].functionRegistry.get(this.name)};e.prototype.isValid=function(){return Boolean(this.func)},e.prototype.call=function(a){return Array.isArray(a)&&(a=a.filter(function(a){return"Comment"!==a.type}).map(function(a){if("Expression"===a.type){var b=a.value.filter(function(a){return"Comment"!==a.type});return 1===b.length?b[0]:new d(b)}return a})),this.func.apply(this,a)},b.exports=e},{"../tree/expression":59}],22:[function(a,b,c){function d(a){return{_data:{},add:function(a,b){a=a.toLowerCase(),this._data.hasOwnProperty(a),this._data[a]=b},addMultiple:function(a){Object.keys(a).forEach(function(b){this.add(b,a[b])}.bind(this))},get:function(b){return this._data[b]||a&&a.get(b)},inherit:function(){return d(this)}}}b.exports=d(null)},{}],23:[function(a,b,c){b.exports=function(b){var c={functionRegistry:a("./function-registry"),functionCaller:a("./function-caller")};return a("./default"),a("./color"),a("./color-blending"),a("./data-uri")(b),a("./math"),a("./number"),a("./string"),a("./svg")(b),a("./types"),c}},{"./color":18,"./color-blending":17,"./data-uri":19,"./default":20,"./function-caller":21,"./function-registry":22,"./math":25,"./number":26,"./string":27,"./svg":28,"./types":29}],24:[function(a,b,c){var d=a("../tree/dimension"),e=function(){};e._math=function(a,b,c){if(!(c instanceof d))throw{type:"Argument",message:"argument must be a number"};return null==b?b=c.unit:c=c.unify(),new d(a(parseFloat(c.value)),b)},b.exports=e},{"../tree/dimension":56}],25:[function(a,b,c){var d=a("./function-registry"),e=a("./math-helper.js"),f={ceil:null,floor:null,sqrt:null,abs:null,tan:"",sin:"",cos:"",atan:"rad",asin:"rad",acos:"rad"};for(var g in f)f.hasOwnProperty(g)&&(f[g]=e._math.bind(null,Math[g],f[g]));f.round=function(a,b){var c="undefined"==typeof b?0:b.value;return e._math(function(a){return a.toFixed(c)},null,a)},d.addMultiple(f)},{"./function-registry":22,"./math-helper.js":24}],26:[function(a,b,c){var d=a("../tree/dimension"),e=a("../tree/anonymous"),f=a("./function-registry"),g=a("./math-helper.js"),h=function(a,b){switch(b=Array.prototype.slice.call(b),b.length){case 0:throw{type:"Argument",message:"one or more arguments required"}}var c,f,g,h,i,j,k,l,m=[],n={};for(c=0;ci.value)&&(m[f]=g);else{if(void 0!==k&&j!==k)throw{type:"Argument",message:"incompatible types"};n[j]=m.length,m.push(g)}else Array.isArray(b[c].value)&&Array.prototype.push.apply(b,Array.prototype.slice.call(b[c].value));return 1==m.length?m[0]:(b=m.map(function(a){return a.toCSS(this.context)}).join(this.context.compress?",":", "),new e((a?"min":"max")+"("+b+")"))};f.addMultiple({min:function(){return h(!0,arguments)},max:function(){return h(!1,arguments)},convert:function(a,b){return a.convertTo(b.value)},pi:function(){return new d(Math.PI)},mod:function(a,b){return new d(a.value%b.value,a.unit)},pow:function(a,b){if("number"==typeof a&&"number"==typeof b)a=new d(a),b=new d(b);else if(!(a instanceof d&&b instanceof d))throw{type:"Argument",message:"arguments must be numbers"};return new d(Math.pow(a.value,b.value),a.unit)},percentage:function(a){var b=g._math(function(a){return 100*a},"%",a);return b}})},{"../tree/anonymous":46,"../tree/dimension":56,"./function-registry":22,"./math-helper.js":24}],27:[function(a,b,c){var d=a("../tree/quoted"),e=a("../tree/anonymous"),f=a("../tree/javascript"),g=a("./function-registry");g.addMultiple({e:function(a){return new e(a instanceof f?a.evaluated:a.value)},escape:function(a){return new e(encodeURI(a.value).replace(/=/g,"%3D").replace(/:/g,"%3A").replace(/#/g,"%23").replace(/;/g,"%3B").replace(/\(/g,"%28").replace(/\)/g,"%29"))},replace:function(a,b,c,e){var f=a.value;return c="Quoted"===c.type?c.value:c.toCSS(),f=f.replace(new RegExp(b.value,e?e.value:""),c),new d(a.quote||"",f,a.escaped)},"%":function(a){for(var b=Array.prototype.slice.call(arguments,1),c=a.value,e=0;e",k=0;k";return j+="',j=encodeURIComponent(j),j="data:image/svg+xml,"+j,new g(new f("'"+j+"'",j,(!1),this.index,this.currentFileInfo),this.index,this.currentFileInfo)})}},{"../tree/color":50,"../tree/dimension":56,"../tree/expression":59,"../tree/quoted":73,"../tree/url":80,"./function-registry":22}],29:[function(a,b,c){var d=a("../tree/keyword"),e=a("../tree/detached-ruleset"),f=a("../tree/dimension"),g=a("../tree/color"),h=a("../tree/quoted"),i=a("../tree/anonymous"),j=a("../tree/url"),k=a("../tree/operation"),l=a("./function-registry"),m=function(a,b){return a instanceof b?d.True:d.False},n=function(a,b){if(void 0===b)throw{type:"Argument",message:"missing the required second argument to isunit."};if(b="string"==typeof b.value?b.value:b,"string"!=typeof b)throw{type:"Argument",message:"Second argument to isunit should be a unit or a string."};return a instanceof f&&a.unit.is(b)?d.True:d.False},o=function(a){var b=Array.isArray(a.value)?a.value:Array(a);return b};l.addMultiple({isruleset:function(a){return m(a,e)},iscolor:function(a){return m(a,g)},isnumber:function(a){return m(a,f)},isstring:function(a){return m(a,h)},iskeyword:function(a){return m(a,d)},isurl:function(a){return m(a,j)},ispixel:function(a){return n(a,"px")},ispercentage:function(a){return n(a,"%")},isem:function(a){return n(a,"em")},isunit:n,unit:function(a,b){if(!(a instanceof f))throw{type:"Argument",message:"the first argument to unit must be a number"+(a instanceof k?". Have you forgotten parenthesis?":"")};return b=b?b instanceof d?b.value:b.toCSS():"",new f(a.value,b)},"get-unit":function(a){return new i(a.unit)},extract:function(a,b){return b=b.value-1,o(a)[b]},length:function(a){return new f(o(a).length)}})},{"../tree/anonymous":46,"../tree/color":50,"../tree/detached-ruleset":55,"../tree/dimension":56,"../tree/keyword":65,"../tree/operation":71,"../tree/quoted":73,"../tree/url":80,"./function-registry":22}],30:[function(a,b,c){var d=a("./contexts"),e=a("./parser/parser"),f=a("./plugins/function-importer");b.exports=function(a){var b=function(a,b){this.rootFilename=b.filename,this.paths=a.paths||[],this.contents={},this.contentsIgnoredChars={},this.mime=a.mime,this.error=null,this.context=a,this.queue=[],this.files={}};return b.prototype.push=function(b,c,g,h,i){var j=this;this.queue.push(b);var k=function(a,c,d){j.queue.splice(j.queue.indexOf(b),1);var e=d===j.rootFilename;h.optional&&a?i(null,{rules:[]},!1,null):(j.files[d]=c,a&&!j.error&&(j.error=a),i(a,c,e,d))},l={relativeUrls:this.context.relativeUrls,entryPath:g.entryPath,rootpath:g.rootpath,rootFilename:g.rootFilename},m=a.getFileManager(b,g.currentDirectory,this.context,a);if(!m)return void k({message:"Could not find a file-manager for "+b});c&&(b=m.tryAppendExtension(b,h.plugin?".js":".less"));var n=function(a){var b=a.filename,c=a.contents.replace(/^\uFEFF/,"");l.currentDirectory=m.getPath(b),l.relativeUrls&&(l.rootpath=m.join(j.context.rootpath||"",m.pathDiff(l.currentDirectory,l.entryPath)),!m.isPathAbsolute(l.rootpath)&&m.alwaysMakePathsAbsolute()&&(l.rootpath=m.join(l.entryPath,l.rootpath))),l.filename=b;var i=new d.Parse(j.context);i.processImports=!1,j.contents[b]=c,(g.reference||h.reference)&&(l.reference=!0),h.plugin?new f(i,l).eval(c,function(a,c){k(a,c,b)}):h.inline?k(null,c,b):new e(i,j,l).parse(c,function(a,c){k(a,c,b)})},o=m.loadFile(b,g.currentDirectory,this.context,a,function(a,b){a?k(a):n(b)});o&&o.then(n,k)},b}},{"./contexts":11,"./parser/parser":38,"./plugins/function-importer":40}],31:[function(a,b,c){b.exports=function(b,c){var d,e,f,g,h,i={version:[2,7,2],data:a("./data"),tree:a("./tree"),Environment:h=a("./environment/environment"),AbstractFileManager:a("./environment/abstract-file-manager"),environment:b=new h(b,c),visitors:a("./visitors"),Parser:a("./parser/parser"),functions:a("./functions")(b),contexts:a("./contexts"),SourceMapOutput:d=a("./source-map-output")(b),SourceMapBuilder:e=a("./source-map-builder")(d,b),ParseTree:f=a("./parse-tree")(e),ImportManager:g=a("./import-manager")(b),render:a("./render")(b,f,g),parse:a("./parse")(b,f,g),LessError:a("./less-error"),transformTree:a("./transform-tree"),utils:a("./utils"),PluginManager:a("./plugin-manager"),logger:a("./logger")};return i}},{"./contexts":11,"./data":13,"./environment/abstract-file-manager":15,"./environment/environment":16,"./functions":23,"./import-manager":30,"./less-error":32,"./logger":33,"./parse":35,"./parse-tree":34,"./parser/parser":38,"./plugin-manager":39,"./render":41,"./source-map-builder":42,"./source-map-output":43,"./transform-tree":44,"./tree":62,"./utils":83,"./visitors":87}],32:[function(a,b,c){var d=a("./utils"),e=b.exports=function(a,b,c){Error.call(this);var e=a.filename||c;if(b&&e){var f=b.contents[e],g=d.getLocation(a.index,f),h=g.line,i=g.column,j=a.call&&d.getLocation(a.call,f).line,k=f.split("\n");this.type=a.type||"Syntax",this.filename=e,this.index=a.index,this.line="number"==typeof h?h+1:null,this.callLine=j+1,this.callExtract=k[j],this.column=i,this.extract=[k[h-1],k[h],k[h+1]]}this.message=a.message,this.stack=a.stack};if("undefined"==typeof Object.create){var f=function(){};f.prototype=Error.prototype,e.prototype=new f}else e.prototype=Object.create(Error.prototype);e.prototype.constructor=e},{"./utils":83}],33:[function(a,b,c){b.exports={error:function(a){this._fireEvent("error",a)},warn:function(a){this._fireEvent("warn",a)},info:function(a){this._fireEvent("info",a)},debug:function(a){this._fireEvent("debug",a)},addListener:function(a){this._listeners.push(a)},removeListener:function(a){for(var b=0;b=97&&j<=122||j<34))switch(j){case 40:o++,e=h;continue;case 41:if(--o<0)return b("missing opening `(`",h);continue;case 59:o||c();continue;case 123:n++,d=h;continue;case 125:if(--n<0)return b("missing opening `{`",h);n||o||c();continue;case 92:if(h96)){if(k==j){l=1;break}if(92==k){if(h==m-1)return b("unescaped `\\`",h);h++}}if(l)continue;return b("unmatched `"+String.fromCharCode(j)+"`",i);case 47:if(o||h==m-1)continue;if(k=a.charCodeAt(h+1),47==k)for(h+=2;hd&&g>f?b("missing closing `}` or `*/`",d):b("missing closing `}`",d):0!==o?b("missing closing `)`",e):(c(!0),p)}},{}],37:[function(a,b,c){var d=a("./chunker");b.exports=function(){function a(d){for(var e,f,j,p=k.i,q=c,s=k.i-i,t=k.i+h.length-s,u=k.i+=d,v=b;k.i=0){j={index:k.i,text:v.substr(k.i,x+2-k.i),isLineComment:!1},k.i+=j.text.length-1,k.commentStore.push(j);continue}}break}if(e!==l&&e!==n&&e!==m&&e!==o)break}if(h=h.slice(d+k.i-u+s),i=k.i,!h.length){if(ce||k.i===e&&a&&!f)&&(e=k.i,f=a);var b=j.pop();h=b.current,i=k.i=b.i,c=b.j},k.forget=function(){j.pop()},k.isWhitespace=function(a){var c=k.i+(a||0),d=b.charCodeAt(c);return d===l||d===o||d===m||d===n},k.$re=function(b){k.i>i&&(h=h.slice(k.i-i),i=k.i);var c=b.exec(h);return c?(a(c[0].length),"string"==typeof c?c:1===c.length?c[0]:c):null},k.$char=function(c){return b.charAt(k.i)!==c?null:(a(1),c)},k.$str=function(c){for(var d=c.length,e=0;es||a=b.length;return k.i=b.length-1,furthestChar:b[k.i]}},k}},{"./chunker":36}],38:[function(a,b,c){var d=a("../less-error"),e=a("../tree"),f=a("../visitors"),g=a("./parser-input"),h=a("../utils"),i=function j(a,b,c){function i(a,e){throw new d({index:o.i,filename:c.filename,type:e||"Syntax",message:a},b)}function k(a,b,c){var d=a instanceof Function?a.call(n):o.$re(a);return d?d:void i(b||("string"==typeof a?"expected '"+a+"' got '"+o.currentChar()+"'":"unexpected token"))}function l(a,b){return o.$char(a)?a:void i(b||"expected '"+a+"' got '"+o.currentChar()+"'")}function m(a){var b=c.filename;return{lineNumber:h.getLocation(a,o.getInput()).line+1,fileName:b}}var n,o=g();return{parse:function(g,h,i){var k,l,m,n,p=null,q="";if(l=i&&i.globalVars?j.serializeVars(i.globalVars)+"\n":"",m=i&&i.modifyVars?"\n"+j.serializeVars(i.modifyVars):"",a.pluginManager)for(var r=a.pluginManager.getPreProcessors(),s=0;s1&&(b=new e.Value(g)),d.push(b),g=[])}return o.forget(),a?d:f},literal:function(){return this.dimension()||this.color()||this.quoted()||this.unicodeDescriptor()},assignment:function(){var a,b;return o.save(),(a=o.$re(/^\w+(?=\s?=)/i))&&o.$char("=")&&(b=n.entity())?(o.forget(),new e.Assignment(a,b)):void o.restore()},url:function(){var a,b=o.i;return o.autoCommentAbsorb=!1,o.$str("url(")?(a=this.quoted()||this.variable()||o.$re(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/)||"",o.autoCommentAbsorb=!0,l(")"),new e.URL(null!=a.value||a instanceof e.Variable?a:new e.Anonymous(a),b,c)):void(o.autoCommentAbsorb=!0)},variable:function(){var a,b=o.i;if("@"===o.currentChar()&&(a=o.$re(/^@@?[\w-]+/)))return new e.Variable(a,b,c)},variableCurly:function(){var a,b=o.i;if("@"===o.currentChar()&&(a=o.$re(/^@\{([\w-]+)\}/)))return new e.Variable("@"+a[1],b,c)},color:function(){var a;if("#"===o.currentChar()&&(a=o.$re(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))){var b=a.input.match(/^#([\w]+).*/);return b=b[1],b.match(/^[A-Fa-f0-9]+$/)||i("Invalid HEX color code"),new e.Color(a[1],(void 0),"#"+b)}},colorKeyword:function(){o.save();var a=o.autoCommentAbsorb;o.autoCommentAbsorb=!1;var b=o.$re(/^[_A-Za-z-][_A-Za-z0-9-]+/);if(o.autoCommentAbsorb=a,!b)return void o.forget();o.restore();var c=e.Color.fromKeyword(b);return c?(o.$str(b),c):void 0},dimension:function(){if(!o.peekNotNumeric()){var a=o.$re(/^([+-]?\d*\.?\d+)(%|[a-z_]+)?/i);return a?new e.Dimension(a[1],a[2]):void 0}},unicodeDescriptor:function(){var a;if(a=o.$re(/^U\+[0-9a-fA-F?]+(\-[0-9a-fA-F?]+)?/))return new e.UnicodeDescriptor(a[0])},javascript:function(){var a,b=o.i;o.save();var d=o.$char("~"),f=o.$char("`");return f?(a=o.$re(/^[^`]*`/))?(o.forget(),new e.JavaScript(a.substr(0,a.length-1),Boolean(d),b,c)):void o.restore("invalid javascript definition"):void o.restore()}},variable:function(){var a;if("@"===o.currentChar()&&(a=o.$re(/^(@[\w-]+)\s*:/)))return a[1]},rulesetCall:function(){var a;if("@"===o.currentChar()&&(a=o.$re(/^(@[\w-]+)\(\s*\)\s*;/)))return new e.RulesetCall(a[1])},extend:function(a){var b,d,f,g,h,j=o.i;if(o.$str(a?"&:extend(":":extend(")){do{for(f=null,b=null;!(f=o.$re(/^(all)(?=\s*(\)|,))/))&&(d=this.element());)b?b.push(d):b=[d];f=f&&f[1],b||i("Missing target selector for :extend()."),h=new e.Extend(new e.Selector(b),f,j,c),g?g.push(h):g=[h]}while(o.$char(","));return k(/^\)/),a&&k(/^;/),g}},extendRule:function(){return this.extend(!0)},mixin:{call:function(){var a,b,d,f,g,h,i=o.currentChar(),j=!1,k=o.i;if("."===i||"#"===i){for(o.save();;){if(a=o.i,f=o.$re(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/),!f)break;d=new e.Element(g,f,a,c),b?b.push(d):b=[d],g=o.$char(">")}return b&&(o.$char("(")&&(h=this.args(!0).args,l(")")),n.important()&&(j=!0),n.end())?(o.forget(),new e.mixin.Call(b,h,k,c,j)):void o.restore()}},args:function(a){var b,c,d,f,g,h,j,k=n.entities,l={args:null,variadic:!1},m=[],p=[],q=[];for(o.save();;){if(a)h=n.detachedRuleset()||n.expression();else{if(o.commentStore.length=0,o.$str("...")){l.variadic=!0,o.$char(";")&&!b&&(b=!0),(b?p:q).push({variadic:!0});break}h=k.variable()||k.literal()||k.keyword()}if(!h)break;f=null,h.throwAwayComments&&h.throwAwayComments(),g=h;var r=null;if(a?h.value&&1==h.value.length&&(r=h.value[0]):r=h,r&&r instanceof e.Variable)if(o.$char(":")){if(m.length>0&&(b&&i("Cannot mix ; and , as delimiter types"),c=!0),g=n.detachedRuleset()||n.expression(),!g){if(!a)return o.restore(),l.args=[],l;i("could not understand value for named argument")}f=d=r.name}else if(o.$str("...")){if(!a){l.variadic=!0,o.$char(";")&&!b&&(b=!0),(b?p:q).push({name:h.name,variadic:!0});break}j=!0}else a||(d=f=r.name,g=null);g&&m.push(g),q.push({name:f,value:g,expand:j}),o.$char(",")||(o.$char(";")||b)&&(c&&i("Cannot mix ; and , as delimiter types"),b=!0,m.length>1&&(g=new e.Value(m)),p.push({name:d,value:g,expand:j}),d=null,m=[],c=!1)}return o.forget(),l.args=b?p:q,l},definition:function(){var a,b,c,d,f=[],g=!1;if(!("."!==o.currentChar()&&"#"!==o.currentChar()||o.peek(/^[^{]*\}/)))if(o.save(),b=o.$re(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/)){a=b[1];var h=this.args(!1);if(f=h.args,g=h.variadic,!o.$char(")"))return void o.restore("Missing closing ')'");if(o.commentStore.length=0,o.$str("when")&&(d=k(n.conditions,"expected condition")),c=n.block())return o.forget(),new e.mixin.Definition(a,f,c,d,g);o.restore()}else o.forget()}},entity:function(){var a=this.entities;return this.comment()||a.literal()||a.variable()||a.url()||a.call()||a.keyword()||a.javascript()},end:function(){return o.$char(";")||o.peek("}")},alpha:function(){var a;if(o.$re(/^opacity=/i))return a=o.$re(/^\d+/),a||(a=k(this.entities.variable,"Could not parse alpha")),l(")"),new e.Alpha(a)},element:function(){var a,b,d,f=o.i;if(b=this.combinator(),a=o.$re(/^(?:\d+\.\d+|\d+)%/)||o.$re(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/)||o.$char("*")||o.$char("&")||this.attribute()||o.$re(/^\([^&()@]+\)/)||o.$re(/^[\.#:](?=@)/)||this.entities.variableCurly(),a||(o.save(),o.$char("(")?(d=this.selector())&&o.$char(")")?(a=new e.Paren(d),o.forget()):o.restore("Missing closing ')'"):o.forget()),a)return new e.Element(b,a,f,c)},combinator:function(){var a=o.currentChar();if("/"===a){o.save();var b=o.$re(/^\/[a-z]+\//i);if(b)return o.forget(),new e.Combinator(b);o.restore()}if(">"===a||"+"===a||"~"===a||"|"===a||"^"===a){for(o.i++,"^"===a&&"^"===o.currentChar()&&(a="^^",o.i++);o.isWhitespace();)o.i++;return new e.Combinator(a)}return new e.Combinator(o.isWhitespace(-1)?" ":null)},lessSelector:function(){return this.selector(!0)},selector:function(a){for(var b,d,f,g,h,j,l,m=o.i;(a&&(d=this.extend())||a&&(j=o.$str("when"))||(g=this.element()))&&(j?l=k(this.conditions,"expected condition"):l?i("CSS guard can only be used at the end of selector"):d?h=h?h.concat(d):d:(h&&i("Extend can only be used at the end of selector"),f=o.currentChar(),b?b.push(g):b=[g],g=null),"{"!==f&&"}"!==f&&";"!==f&&","!==f&&")"!==f););return b?new e.Selector(b,h,l,m,c):void(h&&i("Extend must be used to extend a selector, it cannot be used on its own"))},attribute:function(){if(o.$char("[")){var a,b,c,d=this.entities;return(a=d.variableCurly())||(a=k(/^(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\.)+/)),c=o.$re(/^[|~*$^]?=/),c&&(b=d.quoted()||o.$re(/^[0-9]+%/)||o.$re(/^[\w-]+/)||d.variableCurly()),l("]"),new e.Attribute(a,c,b)}},block:function(){var a;if(o.$char("{")&&(a=this.primary())&&o.$char("}"))return a},blockRuleset:function(){var a=this.block();return a&&(a=new e.Ruleset(null,a)),a},detachedRuleset:function(){var a=this.blockRuleset();if(a)return new e.DetachedRuleset(a)},ruleset:function(){var b,c,d,f;for(o.save(),a.dumpLineNumbers&&(f=m(o.i));;){if(c=this.lessSelector(),!c)break;if(b?b.push(c):b=[c],o.commentStore.length=0,c.condition&&b.length>1&&i("Guards are only currently allowed on a single selector."),!o.$char(","))break;c.condition&&i("Guards are only currently allowed on a single selector."),o.commentStore.length=0}if(b&&(d=this.block())){o.forget();var g=new e.Ruleset(b,d,a.strictImports);return a.dumpLineNumbers&&(g.debugInfo=f),g}o.restore()},rule:function(b){var d,f,g,h,i,j=o.i,k=o.currentChar();if("."!==k&&"#"!==k&&"&"!==k&&":"!==k)if(o.save(),d=this.variable()||this.ruleProperty()){if(i="string"==typeof d,i&&(f=this.detachedRuleset()),o.commentStore.length=0,!f){h=!i&&d.length>1&&d.pop().value;var l=!b&&(a.compress||i);if(l&&(f=this.value()),!f&&(f=this.anonymousValue()))return o.forget(),new e.Rule(d,f,(!1),h,j,c);l||f||(f=this.value()),g=this.important()}if(f&&this.end())return o.forget(),new e.Rule(d,f,g,h,j,c);if(o.restore(),f&&!b)return this.rule(!0)}else o.forget()},anonymousValue:function(){var a=o.$re(/^([^@+\/'"*`(;{}-]*);/);if(a)return new e.Anonymous(a[1])},"import":function(){var a,b,d=o.i,f=o.$re(/^@import?\s+/);if(f){var g=(f?this.importOptions():null)||{};if(a=this.entities.quoted()||this.entities.url())return b=this.mediaFeatures(),o.$char(";")||(o.i=d,i("missing semi-colon or unrecognised media features on import")),b=b&&new e.Value(b),new e.Import(a,b,g,d,c);o.i=d,i("malformed import statement")}},importOptions:function(){var a,b,c,d={};if(!o.$char("("))return null;do if(a=this.importOption()){switch(b=a,c=!0,b){case"css":b="less",c=!1;break;case"once":b="multiple",c=!1}if(d[b]=c,!o.$char(","))break}while(a);return l(")"),d},importOption:function(){var a=o.$re(/^(less|css|multiple|once|inline|reference|optional)/);if(a)return a[1]},mediaFeature:function(){var a,b,d=this.entities,f=[];o.save();do a=d.keyword()||d.variable(),a?f.push(a):o.$char("(")&&(b=this.property(),a=this.value(),o.$char(")")?b&&a?f.push(new e.Paren(new e.Rule(b,a,null,null,o.i,c,(!0)))):a?f.push(new e.Paren(a)):i("badly formed media feature definition"):i("Missing closing ')'","Parse"));while(a);if(o.forget(),f.length>0)return new e.Expression(f)},mediaFeatures:function(){var a,b=this.entities,c=[];do if(a=this.mediaFeature()){if(c.push(a),!o.$char(","))break}else if(a=b.variable(),a&&(c.push(a),!o.$char(",")))break;while(a);return c.length>0?c:null},media:function(){var b,d,f,g,h=o.i;return a.dumpLineNumbers&&(g=m(h)),o.save(),o.$str("@media")?(b=this.mediaFeatures(),d=this.block(),d||i("media definitions require block statements after any features"),o.forget(),f=new e.Media(d,b,h,c),a.dumpLineNumbers&&(f.debugInfo=g),f):void o.restore()},plugin:function(){var a,b=o.i,d=o.$re(/^@plugin?\s+/);if(d){var f={plugin:!0};if(a=this.entities.quoted()||this.entities.url())return o.$char(";")||(o.i=b,i("missing semi-colon on plugin")),new e.Import(a,null,f,b,c);o.i=b,i("malformed plugin statement")}},directive:function(){var b,d,f,g,h,j,k,l=o.i,n=!0,p=!0;if("@"===o.currentChar()){if(d=this["import"]()||this.plugin()||this.media())return d;if(o.save(),b=o.$re(/^@[a-z-]+/)){switch(g=b,"-"==b.charAt(1)&&b.indexOf("-",2)>0&&(g="@"+b.slice(b.indexOf("-",2)+1)),g){case"@charset":h=!0,n=!1;break;case"@namespace":j=!0,n=!1;break;case"@keyframes":case"@counter-style":h=!0;break;case"@document":case"@supports":k=!0,p=!1;break;default:k=!0}return o.commentStore.length=0,h?(d=this.entity(),d||i("expected "+b+" identifier")):j?(d=this.expression(),d||i("expected "+b+" expression")):k&&(d=(o.$re(/^[^{;]+/)||"").trim(),n="{"==o.currentChar(),d&&(d=new e.Anonymous(d))),n&&(f=this.blockRuleset()),f||!n&&d&&o.$char(";")?(o.forget(),new e.Directive(b,d,f,l,c,a.dumpLineNumbers?m(l):null,p)):void o.restore("directive options not recognised")}}},value:function(){var a,b=[];do if(a=this.expression(),a&&(b.push(a),!o.$char(",")))break;while(a);if(b.length>0)return new e.Value(b)},important:function(){if("!"===o.currentChar())return o.$re(/^! *important/)},sub:function(){var a,b;return o.save(),o.$char("(")?(a=this.addition(),a&&o.$char(")")?(o.forget(),b=new e.Expression([a]),b.parens=!0,b):void o.restore("Expected ')'")):void o.restore()},multiplication:function(){var a,b,c,d,f;if(a=this.operand()){for(f=o.isWhitespace(-1);;){if(o.peek(/^\/[*\/]/))break;if(o.save(),c=o.$char("/")||o.$char("*"),!c){o.forget();break}if(b=this.operand(),!b){o.restore();break}o.forget(),a.parensInOp=!0,b.parensInOp=!0,d=new e.Operation(c,[d||a,b],f),f=o.isWhitespace(-1)}return d||a}},addition:function(){var a,b,c,d,f;if(a=this.multiplication()){for(f=o.isWhitespace(-1);;){if(c=o.$re(/^[-+]\s+/)||!f&&(o.$char("+")||o.$char("-")),!c)break;if(b=this.multiplication(),!b)break;a.parensInOp=!0,b.parensInOp=!0,d=new e.Operation(c,[d||a,b],f),f=o.isWhitespace(-1)}return d||a}},conditions:function(){var a,b,c,d=o.i;if(a=this.condition()){for(;;){if(!o.peek(/^,\s*(not\s*)?\(/)||!o.$char(","))break;if(b=this.condition(),!b)break;c=new e.Condition("or",c||a,b,d)}return c||a}},condition:function(){function a(){return o.$str("or")}var b,c,d;if(b=this.conditionAnd(this)){if(c=a()){if(d=this.condition(),!d)return;b=new e.Condition(c,b,d)}return b}},conditionAnd:function(){function a(a){return a.negatedCondition()||a.parenthesisCondition()}function b(){return o.$str("and")}var c,d,f;if(c=a(this)){if(d=b()){if(f=this.conditionAnd(),!f)return;c=new e.Condition(d,c,f)}return c}},negatedCondition:function(){if(o.$str("not")){var a=this.parenthesisCondition();return a&&(a.negate=!a.negate),a}},parenthesisCondition:function(){function a(a){var b;return o.save(),(b=a.condition())&&o.$char(")")?(o.forget(),b):void o.restore()}var b;return o.save(),o.$str("(")?(b=a(this))?(o.forget(),b):(b=this.atomicCondition())?o.$char(")")?(o.forget(),b):void o.restore("expected ')' got '"+o.currentChar()+"'"):void o.restore():void o.restore()},atomicCondition:function(){var a,b,c,d,f=this.entities,g=o.i;if(a=this.addition()||f.keyword()||f.quoted())return o.$char(">")?d=o.$char("=")?">=":">":o.$char("<")?d=o.$char("=")?"<=":"<":o.$char("=")&&(d=o.$char(">")?"=>":o.$char("<")?"=<":"="),d?(b=this.addition()||f.keyword()||f.quoted(),b?c=new e.Condition(d,a,b,g,(!1)):i("expected expression")):c=new e.Condition("=",a,new e.Keyword("true"),g,(!1)),c},operand:function(){var a,b=this.entities;o.peek(/^-[@\(]/)&&(a=o.$char("-"));var c=this.sub()||b.dimension()||b.color()||b.variable()||b.call()||b.colorKeyword();return a&&(c.parensInOp=!0,c=new e.Negative(c)),c},expression:function(){var a,b,c=[];do a=this.comment(),a?c.push(a):(a=this.addition()||this.entity(),a&&(c.push(a),o.peek(/^\/[\/*]/)||(b=o.$char("/"),b&&c.push(new e.Anonymous(b)))));while(a);if(c.length>0)return new e.Expression(c)},property:function(){var a=o.$re(/^(\*?-?[_a-zA-Z0-9-]+)\s*:/);if(a)return a[1]},ruleProperty:function(){function a(a){var b=o.i,c=o.$re(a);if(c)return g.push(b),f.push(c[1])}var b,d,f=[],g=[];o.save();var h=o.$re(/^([_a-zA-Z0-9-]+)\s*:/);if(h)return f=[new e.Keyword(h[1])],o.forget(),f;for(a(/^(\*?)/);;)if(!a(/^((?:[\w-]+)|(?:@\{[\w-]+\}))/))break;if(f.length>1&&a(/^((?:\+_|\+)?)\s*:/)){for(o.forget(),""===f[0]&&(f.shift(),g.shift()),d=0;d=b);c++);this.preProcessors.splice(c,0,{preProcessor:a,priority:b})},d.prototype.addPostProcessor=function(a,b){var c;for(c=0;c=b);c++);this.postProcessors.splice(c,0,{postProcessor:a,priority:b})},d.prototype.addFileManager=function(a){this.fileManagers.push(a)},d.prototype.getPreProcessors=function(){for(var a=[],b=0;b0){var d,e=JSON.stringify(this._sourceMapGenerator.toJSON());this.sourceMapURL?d=this.sourceMapURL:this._sourceMapFilename&&(d=this._sourceMapFilename),this.sourceMapURL=d,this.sourceMap=e}return this._css.join("")},b}},{}],44:[function(a,b,c){var d=a("./contexts"),e=a("./visitors"),f=a("./tree");b.exports=function(a,b){b=b||{};var c,g=b.variables,h=new d.Eval(b);"object"!=typeof g||Array.isArray(g)||(g=Object.keys(g).map(function(a){var b=g[a];return b instanceof f.Value||(b instanceof f.Expression||(b=new f.Expression([b])),b=new f.Value([b])),new f.Rule("@"+a,b,(!1),null,0)}),h.frames=[new f.Ruleset(null,g)]);var i,j=[],k=[new e.JoinSelectorVisitor,new e.MarkVisibleSelectorsVisitor((!0)),new e.ExtendVisitor,new e.ToCSSVisitor({compress:Boolean(b.compress)})];if(b.pluginManager){var l=b.pluginManager.getVisitors();for(i=0;i.5?j/(2-g-h):j/(g+h),g){case c:a=(d-e)/j+(d="===a||"=<"===a||"<="===a;case 1:return">"===a||">="===a;default:return!1}}}(this.op,this.lvalue.eval(a),this.rvalue.eval(a));return this.negate?!b:b},b.exports=e},{"./node":70}],54:[function(a,b,c){var d=function(a,b,c){var e="";if(a.dumpLineNumbers&&!a.compress)switch(a.dumpLineNumbers){case"comments":e=d.asComment(b);break;case"mediaquery":e=d.asMediaQuery(b);break;case"all":e=d.asComment(b)+(c||"")+d.asMediaQuery(b)}return e};d.asComment=function(a){return"/* line "+a.debugInfo.lineNumber+", "+a.debugInfo.fileName+" */\n"},d.asMediaQuery=function(a){var b=a.debugInfo.fileName;return/^[a-z]+:\/\//i.test(b)||(b="file://"+b),"@media -sass-debug-info{filename{font-family:"+b.replace(/([.:\/\\])/g,function(a){return"\\"==a&&(a="/"),"\\"+a})+"}line{font-family:\\00003"+a.debugInfo.lineNumber+"}}\n"},b.exports=d},{}],55:[function(a,b,c){var d=a("./node"),e=a("../contexts"),f=function(a,b){this.ruleset=a,this.frames=b};f.prototype=new d,f.prototype.type="DetachedRuleset",f.prototype.evalFirst=!0,f.prototype.accept=function(a){this.ruleset=a.visit(this.ruleset)},f.prototype.eval=function(a){var b=this.frames||a.frames.slice(0);return new f(this.ruleset,b)},f.prototype.callEval=function(a){return this.ruleset.eval(this.frames?new e.Eval(a,this.frames.concat(a.frames)):a)},b.exports=f},{"../contexts":11,"./node":70}],56:[function(a,b,c){var d=a("./node"),e=a("../data/unit-conversions"),f=a("./unit"),g=a("./color"),h=function(a,b){this.value=parseFloat(a),this.unit=b&&b instanceof f?b:new f(b?[b]:void 0)};h.prototype=new d,h.prototype.type="Dimension",h.prototype.accept=function(a){this.unit=a.visit(this.unit)},h.prototype.eval=function(a){return this},h.prototype.toColor=function(){return new g([this.value,this.value,this.value])},h.prototype.genCSS=function(a,b){if(a&&a.strictUnits&&!this.unit.isSingular())throw new Error("Multiple units in dimension. Correct the units or use the unit function. Bad unit: "+this.unit.toString());var c=this.fround(a,this.value),d=String(c);if(0!==c&&c<1e-6&&c>-1e-6&&(d=c.toFixed(20).replace(/0+$/,"")),a&&a.compress){if(0===c&&this.unit.isLength())return void b.add(d);c>0&&c<1&&(d=d.substr(1))}b.add(d),this.unit.genCSS(a,b)},h.prototype.operate=function(a,b,c){var d=this._operate(a,b,this.value,c.value),e=this.unit.clone();if("+"===b||"-"===b)if(0===e.numerator.length&&0===e.denominator.length)e=c.unit.clone(),this.unit.backupUnit&&(e.backupUnit=this.unit.backupUnit);else if(0===c.unit.numerator.length&&0===e.denominator.length);else{if(c=c.convertTo(this.unit.usedUnits()),a.strictUnits&&c.unit.toString()!==e.toString())throw new Error("Incompatible units. Change the units or use the unit function. Bad units: '"+e.toString()+"' and '"+c.unit.toString()+"'.");d=this._operate(a,b,this.value,c.value)}else"*"===b?(e.numerator=e.numerator.concat(c.unit.numerator).sort(),e.denominator=e.denominator.concat(c.unit.denominator).sort(),e.cancel()):"/"===b&&(e.numerator=e.numerator.concat(c.unit.denominator).sort(),e.denominator=e.denominator.concat(c.unit.numerator).sort(),e.cancel());return new h(d,e)},h.prototype.compare=function(a){var b,c;if(a instanceof h){if(this.unit.isEmpty()||a.unit.isEmpty())b=this,c=a;else if(b=this.unify(),c=a.unify(),0!==b.unit.compare(c.unit))return;return d.numericCompare(b.value,c.value)}},h.prototype.unify=function(){return this.convertTo({length:"px",duration:"s",angle:"rad"})},h.prototype.convertTo=function(a){var b,c,d,f,g,i=this.value,j=this.unit.clone(),k={};if("string"==typeof a){for(b in e)e[b].hasOwnProperty(a)&&(k={},k[b]=a);a=k}g=function(a,b){return d.hasOwnProperty(a)?(b?i/=d[a]/d[f]:i*=d[a]/d[f],f):a};for(c in a)a.hasOwnProperty(c)&&(f=a[c],d=e[c],j.map(g));return j.cancel(),new h(i,j)},b.exports=h},{"../data/unit-conversions":14,"./color":50,"./node":70,"./unit":79}],57:[function(a,b,c){var d=a("./node"),e=a("./selector"),f=a("./ruleset"),g=function(a,b,c,d,f,g,h,i){var j;if(this.name=a,this.value=b,c)for(Array.isArray(c)?this.rules=c:(this.rules=[c],this.rules[0].selectors=new e([],null,null,this.index,f).createEmptySelectors()),j=0;j1?b=new g(this.value.map(function(b){return b.eval(a)})):1===this.value.length?(this.value[0].parens&&!this.value[0].parensInOp&&(d=!0),b=this.value[0].eval(a)):b=this,c&&a.outOfParenthesis(),this.parens&&this.parensInOp&&!a.isMathOn()&&!d&&(b=new e(b)),b},g.prototype.genCSS=function(a,b){for(var c=0;c0&&c.length&&""===c[0].combinator.value&&(c[0].combinator.value=" "),d=d.concat(a[b].elements);this.selfSelectors=[new e(d)],this.selfSelectors[0].copyVisibilityInfo(this.visibilityInfo())},b.exports=f},{"./node":70,"./selector":77}],61:[function(a,b,c){var d=a("./node"),e=a("./media"),f=a("./url"),g=a("./quoted"),h=a("./ruleset"),i=a("./anonymous"),j=function(a,b,c,d,e,f){if(this.options=c,this.index=d,this.path=a,this.features=b,this.currentFileInfo=e,this.allowRoot=!0,void 0!==this.options.less||this.options.inline)this.css=!this.options.less||this.options.inline;else{var g=this.getPath();g&&/[#\.\&\?\/]css([\?;].*)?$/.test(g)&&(this.css=!0)}this.copyVisibilityInfo(f)};j.prototype=new d,j.prototype.type="Import",j.prototype.accept=function(a){this.features&&(this.features=a.visit(this.features)),this.path=a.visit(this.path),this.options.plugin||this.options.inline||!this.root||(this.root=a.visit(this.root))},j.prototype.genCSS=function(a,b){this.css&&void 0===this.path.currentFileInfo.reference&&(b.add("@import ",this.currentFileInfo,this.index),this.path.genCSS(a,b),this.features&&(b.add(" "),this.features.genCSS(a,b)),b.add(";"))},j.prototype.getPath=function(){return this.path instanceof f?this.path.value.value:this.path.value},j.prototype.isVariableImport=function(){var a=this.path;return a instanceof f&&(a=a.value),!(a instanceof g)||a.containsVariables()},j.prototype.evalForImport=function(a){var b=this.path;return b instanceof f&&(b=b.value),new j(b.eval(a),this.features,this.options,this.index,this.currentFileInfo,this.visibilityInfo())},j.prototype.evalPath=function(a){var b=this.path.eval(a),c=this.currentFileInfo&&this.currentFileInfo.rootpath;if(!(b instanceof f)){if(c){var d=b.value;d&&a.isPathRelative(d)&&(b.value=c+d)}b.value=a.normalizePath(b.value)}return b},j.prototype.eval=function(a){var b=this.doEval(a);return(this.options.reference||this.blocksVisibility())&&(b.length||0===b.length?b.forEach(function(a){a.addVisibilityBlock()}):b.addVisibilityBlock()),b},j.prototype.doEval=function(a){var b,c,d=this.features&&this.features.eval(a);if(this.options.plugin)return c=a.frames[0]&&a.frames[0].functionRegistry,c&&this.root&&this.root.functions&&c.addMultiple(this.root.functions),[];if(this.skip&&("function"==typeof this.skip&&(this.skip=this.skip()),this.skip))return[];if(this.options.inline){var f=new i(this.root,0,{filename:this.importedFilename,reference:this.path.currentFileInfo&&this.path.currentFileInfo.reference},(!0),(!0));return this.features?new e([f],this.features.value):[f]}if(this.css){var g=new j(this.evalPath(a),d,this.options,this.index);if(!g.css&&this.error)throw this.error;return g}return b=new h(null,this.root.rules.slice(0)),b.evalImports(a),this.features?new e(b.rules,this.features.value):b.rules},b.exports=j},{"./anonymous":46,"./media":66,"./node":70,"./quoted":73,"./ruleset":76,"./url":80}],62:[function(a,b,c){var d={};d.Node=a("./node"),d.Alpha=a("./alpha"),d.Color=a("./color"),d.Directive=a("./directive"),d.DetachedRuleset=a("./detached-ruleset"),d.Operation=a("./operation"),d.Dimension=a("./dimension"),d.Unit=a("./unit"),d.Keyword=a("./keyword"),d.Variable=a("./variable"),d.Ruleset=a("./ruleset"),d.Element=a("./element"),d.Attribute=a("./attribute"),d.Combinator=a("./combinator"),d.Selector=a("./selector"),d.Quoted=a("./quoted"),d.Expression=a("./expression"),d.Rule=a("./rule"),d.Call=a("./call"),d.URL=a("./url"),d.Import=a("./import"),d.mixin={Call:a("./mixin-call"),Definition:a("./mixin-definition")},d.Comment=a("./comment"),d.Anonymous=a("./anonymous"),d.Value=a("./value"),d.JavaScript=a("./javascript"),d.Assignment=a("./assignment"),d.Condition=a("./condition"),d.Paren=a("./paren"),d.Media=a("./media"),d.UnicodeDescriptor=a("./unicode-descriptor"),d.Negative=a("./negative"),d.Extend=a("./extend"),d.RulesetCall=a("./ruleset-call"),b.exports=d},{"./alpha":45,"./anonymous":46,"./assignment":47,"./attribute":48,"./call":49,"./color":50,"./combinator":51,"./comment":52,"./condition":53,"./detached-ruleset":55,"./dimension":56,"./directive":57,"./element":58,"./expression":59,"./extend":60,"./import":61,"./javascript":63,"./keyword":65,"./media":66,"./mixin-call":67,"./mixin-definition":68,"./negative":69,"./node":70,"./operation":71,"./paren":72,"./quoted":73,"./rule":74,"./ruleset":76,"./ruleset-call":75,"./selector":77,"./unicode-descriptor":78,"./unit":79,"./url":80,"./value":81,"./variable":82}],63:[function(a,b,c){var d=a("./js-eval-node"),e=a("./dimension"),f=a("./quoted"),g=a("./anonymous"),h=function(a,b,c,d){this.escaped=b,this.expression=a,this.index=c,this.currentFileInfo=d};h.prototype=new d,h.prototype.type="JavaScript",h.prototype.eval=function(a){var b=this.evaluateJavaScript(this.expression,a);return"number"==typeof b?new e(b):"string"==typeof b?new f('"'+b+'"',b,this.escaped,this.index):new g(Array.isArray(b)?b.join(", "):b)},b.exports=h},{"./anonymous":46,"./dimension":56,"./js-eval-node":64,"./quoted":73}],64:[function(a,b,c){var d=a("./node"),e=a("./variable"),f=function(){};f.prototype=new d,f.prototype.evaluateJavaScript=function(a,b){var c,d=this,f={};if(void 0!==b.javascriptEnabled&&!b.javascriptEnabled)throw{message:"You are using JavaScript, which has been disabled.",filename:this.currentFileInfo.filename,index:this.index};a=a.replace(/@\{([\w-]+)\}/g,function(a,c){return d.jsify(new e("@"+c,d.index,d.currentFileInfo).eval(b))});try{a=new Function("return ("+a+")")}catch(g){throw{message:"JavaScript evaluation error: "+g.message+" from `"+a+"`",filename:this.currentFileInfo.filename,index:this.index}}var h=b.frames[0].variables();for(var i in h)h.hasOwnProperty(i)&&(f[i.slice(1)]={value:h[i].value,toJS:function(){return this.value.eval(b).toCSS()}});try{c=a.call(f)}catch(g){throw{message:"JavaScript evaluation error: '"+g.name+": "+g.message.replace(/["]/g,"'")+"'",filename:this.currentFileInfo.filename,index:this.index}}return c},f.prototype.jsify=function(a){return Array.isArray(a.value)&&a.value.length>1?"["+a.value.map(function(a){return a.toCSS()}).join(", ")+"]":a.toCSS()},b.exports=f},{"./node":70,"./variable":82}],65:[function(a,b,c){var d=a("./node"),e=function(a){this.value=a};e.prototype=new d,e.prototype.type="Keyword",e.prototype.genCSS=function(a,b){if("%"===this.value)throw{type:"Syntax",message:"Invalid % without number"};b.add(this.value)},e.True=new e("true"),e.False=new e("false"),b.exports=e},{"./node":70}],66:[function(a,b,c){var d=a("./ruleset"),e=a("./value"),f=a("./selector"),g=a("./anonymous"),h=a("./expression"),i=a("./directive"),j=function(a,b,c,g,h){this.index=c,this.currentFileInfo=g;var i=new f([],null,null,this.index,this.currentFileInfo).createEmptySelectors();this.features=new e(b),this.rules=[new d(i,a)],this.rules[0].allowImports=!0,this.copyVisibilityInfo(h),this.allowRoot=!0};j.prototype=new i,j.prototype.type="Media",j.prototype.isRulesetLike=!0,j.prototype.accept=function(a){this.features&&(this.features=a.visit(this.features)),this.rules&&(this.rules=a.visitArray(this.rules))},j.prototype.genCSS=function(a,b){b.add("@media ",this.currentFileInfo,this.index),this.features.genCSS(a,b),this.outputRuleset(a,b,this.rules)},j.prototype.eval=function(a){a.mediaBlocks||(a.mediaBlocks=[],a.mediaPath=[]);var b=new j(null,[],this.index,this.currentFileInfo,this.visibilityInfo());this.debugInfo&&(this.rules[0].debugInfo=this.debugInfo,b.debugInfo=this.debugInfo);var c=!1;a.strictMath||(c=!0,a.strictMath=!0);try{b.features=this.features.eval(a)}finally{c&&(a.strictMath=!1)}return a.mediaPath.push(b),a.mediaBlocks.push(b),this.rules[0].functionRegistry=a.frames[0].functionRegistry.inherit(),a.frames.unshift(this.rules[0]),b.rules=[this.rules[0].eval(a)],a.frames.shift(),a.mediaPath.pop(),0===a.mediaPath.length?b.evalTop(a):b.evalNested(a)},j.prototype.evalTop=function(a){var b=this;if(a.mediaBlocks.length>1){var c=new f([],null,null,this.index,this.currentFileInfo).createEmptySelectors();b=new d(c,a.mediaBlocks),b.multiMedia=!0,b.copyVisibilityInfo(this.visibilityInfo())}return delete a.mediaBlocks,delete a.mediaPath,b},j.prototype.evalNested=function(a){var b,c,f=a.mediaPath.concat([this]);for(b=0;b0;b--)a.splice(b,0,new g("and"));return new h(a)})),new d([],[])},j.prototype.permute=function(a){if(0===a.length)return[];if(1===a.length)return a[0];for(var b=[],c=this.permute(a.slice(1)),d=0;d0){for(n=!0,k=0;k0)p=B;else if(p=A,q[A]+q[B]>1)throw{type:"Runtime",message:"Ambiguous use of `default()` found when matching for `"+this.format(t)+"`",index:this.index,filename:this.currentFileInfo.filename};for(k=0;kthis.params.length)return!1}c=Math.min(f,this.arity);for(var g=0;gb?1:void 0},d.prototype.blocksVisibility=function(){return null==this.visibilityBlocks&&(this.visibilityBlocks=0),0!==this.visibilityBlocks},d.prototype.addVisibilityBlock=function(){null==this.visibilityBlocks&&(this.visibilityBlocks=0),this.visibilityBlocks=this.visibilityBlocks+1},d.prototype.removeVisibilityBlock=function(){null==this.visibilityBlocks&&(this.visibilityBlocks=0),this.visibilityBlocks=this.visibilityBlocks-1},d.prototype.ensureVisibility=function(){this.nodeVisible=!0},d.prototype.ensureInvisibility=function(){this.nodeVisible=!1},d.prototype.isVisible=function(){return this.nodeVisible},d.prototype.visibilityInfo=function(){return{visibilityBlocks:this.visibilityBlocks,nodeVisible:this.nodeVisible}},d.prototype.copyVisibilityInfo=function(a){a&&(this.visibilityBlocks=a.visibilityBlocks,this.nodeVisible=a.nodeVisible)},b.exports=d},{}],71:[function(a,b,c){var d=a("./node"),e=a("./color"),f=a("./dimension"),g=function(a,b,c){this.op=a.trim(),this.operands=b,this.isSpaced=c};g.prototype=new d,g.prototype.type="Operation",g.prototype.accept=function(a){this.operands=a.visit(this.operands)},g.prototype.eval=function(a){var b=this.operands[0].eval(a),c=this.operands[1].eval(a);if(a.isMathOn()){if(b instanceof f&&c instanceof e&&(b=b.toColor()),c instanceof f&&b instanceof e&&(c=c.toColor()),!b.operate)throw{type:"Operation",message:"Operation on an invalid type"};return b.operate(a,this.op,c)}return new g(this.op,[b,c],this.isSpaced)},g.prototype.genCSS=function(a,b){this.operands[0].genCSS(a,b),this.isSpaced&&b.add(" "),b.add(this.op),this.isSpaced&&b.add(" "),this.operands[1].genCSS(a,b)},b.exports=g},{"./color":50,"./dimension":56,"./node":70}],72:[function(a,b,c){var d=a("./node"),e=function(a){this.value=a};e.prototype=new d,e.prototype.type="Paren",e.prototype.genCSS=function(a,b){b.add("("),this.value.genCSS(a,b),b.add(")")},e.prototype.eval=function(a){return new e(this.value.eval(a))},b.exports=e},{"./node":70}],73:[function(a,b,c){var d=a("./node"),e=a("./js-eval-node"),f=a("./variable"),g=function(a,b,c,d,e){this.escaped=null==c||c,this.value=b||"",this.quote=a.charAt(0),this.index=d,this.currentFileInfo=e};g.prototype=new e,g.prototype.type="Quoted",g.prototype.genCSS=function(a,b){this.escaped||b.add(this.quote,this.currentFileInfo,this.index),b.add(this.value),this.escaped||b.add(this.quote)},g.prototype.containsVariables=function(){return this.value.match(/(`([^`]+)`)|@\{([\w-]+)\}/)},g.prototype.eval=function(a){function b(a,b,c){var d=a;do a=d,d=a.replace(b,c);while(a!==d);return d}var c=this,d=this.value,e=function(b,d){return String(c.evaluateJavaScript(d,a))},h=function(b,d){var e=new f("@"+d,c.index,c.currentFileInfo).eval(a,!0);return e instanceof g?e.value:e.toCSS()};return d=b(d,/`([^`]+)`/g,e),d=b(d,/@\{([\w-]+)\}/g,h),new g(this.quote+d+this.quote,d,this.escaped,this.index,this.currentFileInfo)},g.prototype.compare=function(a){return"Quoted"!==a.type||this.escaped||a.escaped?a.toCSS&&this.toCSS()===a.toCSS()?0:void 0:d.numericCompare(this.value,a.value)},b.exports=g},{"./js-eval-node":64,"./node":70,"./variable":82}],74:[function(a,b,c){function d(a,b){var c,d="",e=b.length,f={add:function(a){d+=a}};for(c=0;cd){if(!c||c(h)){e=h.find(new f(a.elements.slice(d)),b,c);for(var j=0;j0&&b.add(k),a.firstSelector=!0,h[0].genCSS(a,b),a.firstSelector=!1,e=1;e0?(e=a.slice(0),f=e.pop(),h=d.createDerived(f.elements.slice(0))):h=d.createDerived([]),b.length>0){var i=c.combinator,j=b[0].elements[0];i.emptyOrWhitespace&&!j.combinator.emptyOrWhitespace&&(i=j.combinator),h.elements.push(new g(i,j.value,c.index,c.currentFileInfo)),h.elements=h.elements.concat(b[0].elements.slice(1))}if(0!==h.elements.length&&e.push(h),b.length>1){var k=b.slice(1);k=k.map(function(a){return a.createDerived(a.elements,[])}),e=e.concat(k)}return e}function j(a,b,c,d,e){var f;for(f=0;f0?d[d.length-1]=d[d.length-1].createDerived(d[d.length-1].elements.concat(a)):d.push(new f(a))}}function l(a,b,c){function f(a){var b;return"Paren"!==a.value.type?null:(b=a.value.value,"Selector"!==b.type?null:b)}var h,m,n,o,p,q,r,s,t,u,v=!1;for(o=[],p=[[]],h=0;h0&&r[0].elements.push(new g(s.combinator,"",s.index,s.currentFileInfo)),q.push(r);else for(n=0;n0&&(a.push(p[h]),u=p[h][t-1],p[h][t-1]=u.createDerived(u.elements,c.extendList));return v}function m(a,b){var c=b.createDerived(b.elements,b.extendList,b.evaldCondition);return c.copyVisibilityInfo(a),c}var n,o,p;if(o=[],p=l(o,b,c),!p)if(b.length>0)for(o=[],n=0;n0)for(b=0;b=0&&"\n"!==b.charAt(c);)e++;return"number"==typeof a&&(d=(b.slice(0,a).match(/\n/g)||"").length),{line:d,column:e}}}},{}],84:[function(a,b,c){var d=a("../tree"),e=a("./visitor"),f=a("../logger"),g=function(){this._visitor=new e(this),this.contexts=[],this.allExtendsStack=[[]]};g.prototype={run:function(a){return a=this._visitor.visit(a),a.allExtends=this.allExtendsStack[0],a},visitRule:function(a,b){b.visitDeeper=!1},visitMixinDefinition:function(a,b){b.visitDeeper=!1},visitRuleset:function(a,b){if(!a.root){var c,e,f,g,h=[],i=a.rules,j=i?i.length:0;for(c=0;c=0||(i=[k.selfSelectors[0]],g=n.findMatch(j,i),g.length&&(j.hasFoundMatches=!0,j.selfSelectors.forEach(function(a){var b=k.visibilityInfo();h=n.extendSelector(g,i,a,j.isVisible()),l=new d.Extend(k.selector,k.option,0,k.currentFileInfo,b),l.selfSelectors=h,h[h.length-1].extendList=[l],m.push(l),l.ruleset=k.ruleset,l.parent_ids=l.parent_ids.concat(k.parent_ids,j.parent_ids),k.firstExtendOnThisSelectorPath&&(l.firstExtendOnThisSelectorPath=!0,k.ruleset.paths.push(h))})));if(m.length){if(this.extendChainCount++,c>100){var o="{unable to calculate}",p="{unable to calculate}";try{o=m[0].selfSelectors[0].toCSS(),p=m[0].selector.toCSS()}catch(q){}throw{message:"extend circular reference detected. One of the circular extends is currently:"+o+":extend("+p+")"}}return m.concat(n.doExtendChaining(m,b,c+1))}return m},visitRule:function(a,b){b.visitDeeper=!1},visitMixinDefinition:function(a,b){b.visitDeeper=!1},visitSelector:function(a,b){b.visitDeeper=!1},visitRuleset:function(a,b){if(!a.root){var c,d,e,f,g=this.allExtendsStack[this.allExtendsStack.length-1],h=[],i=this;for(e=0;e0&&k[i.matched].combinator.value!==g?i=null:i.matched++,i&&(i.finished=i.matched===k.length,i.finished&&!a.allowAfter&&(e+1k&&l>0&&(m[m.length-1].elements=m[m.length-1].elements.concat(b[k].elements.slice(l)),l=0,k++),j=g.elements.slice(l,i.index).concat([h]).concat(c.elements.slice(1)),k===i.pathIndex&&f>0?m[m.length-1].elements=m[m.length-1].elements.concat(j):(m=m.concat(b.slice(k,i.pathIndex)),m.push(new d.Selector(j))),k=i.endPathIndex,l=i.endPathElementIndex,l>=b[k].elements.length&&(l=0,k++);return k0&&(m[m.length-1].elements=m[m.length-1].elements.concat(b[k].elements.slice(l)),k++),m=m.concat(b.slice(k,b.length)),m=m.map(function(a){var b=a.createDerived(a.elements);return e?b.ensureVisibility():b.ensureInvisibility(),b})},visitMedia:function(a,b){var c=a.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);c=c.concat(this.doExtendChaining(c,a.allExtends)),this.allExtendsStack.push(c)},visitMediaOut:function(a){var b=this.allExtendsStack.length-1;this.allExtendsStack.length=b},visitDirective:function(a,b){var c=a.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);c=c.concat(this.doExtendChaining(c,a.allExtends)),this.allExtendsStack.push(c)},visitDirectiveOut:function(a){var b=this.allExtendsStack.length-1;this.allExtendsStack.length=b}},b.exports=h},{"../logger":33,"../tree":62,"./visitor":91}],85:[function(a,b,c){function d(a){this.imports=[],this.variableImports=[],this._onSequencerEmpty=a,this._currentDepth=0}d.prototype.addImport=function(a){var b=this,c={callback:a,args:null,isReady:!1};return this.imports.push(c),function(){c.args=Array.prototype.slice.call(arguments,0),c.isReady=!0,b.tryRun()}},d.prototype.addVariableImport=function(a){this.variableImports.push(a)},d.prototype.tryRun=function(){this._currentDepth++;try{for(;;){for(;this.imports.length>0;){var a=this.imports[0];if(!a.isReady)return; + this.imports=this.imports.slice(1),a.callback.apply(null,a.args)}if(0===this.variableImports.length)break;var b=this.variableImports[0];this.variableImports=this.variableImports.slice(1),b()}}finally{this._currentDepth--}0===this._currentDepth&&this._onSequencerEmpty&&this._onSequencerEmpty()},b.exports=d},{}],86:[function(a,b,c){var d=a("../contexts"),e=a("./visitor"),f=a("./import-sequencer"),g=function(a,b){this._visitor=new e(this),this._importer=a,this._finish=b,this.context=new d.Eval,this.importCount=0,this.onceFileDetectionMap={},this.recursionDetector={},this._sequencer=new f(this._onSequencerEmpty.bind(this))};g.prototype={isReplacing:!1,run:function(a){try{this._visitor.visit(a)}catch(b){this.error=b}this.isFinished=!0,this._sequencer.tryRun()},_onSequencerEmpty:function(){this.isFinished&&this._finish(this.error)},visitImport:function(a,b){var c=a.options.inline;if(!a.css||c){var e=new d.Eval(this.context,this.context.frames.slice(0)),f=e.frames[0];this.importCount++,a.isVariableImport()?this._sequencer.addVariableImport(this.processImportNode.bind(this,a,e,f)):this.processImportNode(a,e,f)}b.visitDeeper=!1},processImportNode:function(a,b,c){var d,e=a.options.inline;try{d=a.evalForImport(b)}catch(f){f.filename||(f.index=a.index,f.filename=a.currentFileInfo.filename),a.css=!0,a.error=f}if(!d||d.css&&!e)this.importCount--,this.isFinished&&this._sequencer.tryRun();else{d.options.multiple&&(b.importMultiple=!0);for(var g=void 0===d.css,h=0;h0},resolveVisibility:function(a,b){if(!a.blocksVisibility()){if(this.isEmpty(a)&&!this.containsSilentNonBlockedChild(b))return;return a}var c=a.rules[0];if(this.keepOnlyVisibleChilds(c),!this.isEmpty(c))return a.ensureVisibility(),a.removeVisibilityBlock(),a},isVisibleRuleset:function(a){return!!a.firstRoot||!this.isEmpty(a)&&!(!a.root&&!this.hasVisibleSelector(a))}};var g=function(a){this._visitor=new e(this),this._context=a,this.utils=new f(a)};g.prototype={isReplacing:!0,run:function(a){return this._visitor.visit(a)},visitRule:function(a,b){if(!a.blocksVisibility()&&!a.variable)return a},visitMixinDefinition:function(a,b){a.frames=[]},visitExtend:function(a,b){},visitComment:function(a,b){if(!a.blocksVisibility()&&!a.isSilent(this._context))return a},visitMedia:function(a,b){var c=a.rules[0].rules;return a.accept(this._visitor),b.visitDeeper=!1,this.utils.resolveVisibility(a,c)},visitImport:function(a,b){if(!a.blocksVisibility())return a},visitDirective:function(a,b){return a.rules&&a.rules.length?this.visitDirectiveWithBody(a,b):this.visitDirectiveWithoutBody(a,b)},visitDirectiveWithBody:function(a,b){function c(a){var b=a.rules;return 1===b.length&&(!b[0].paths||0===b[0].paths.length)}function d(a){var b=a.rules;return c(a)?b[0].rules:b}var e=d(a);return a.accept(this._visitor),b.visitDeeper=!1,this.utils.isEmpty(a)||this._mergeRules(a.rules[0].rules),this.utils.resolveVisibility(a,e)},visitDirectiveWithoutBody:function(a,b){if(!a.blocksVisibility()){if("@charset"===a.name){if(this.charset){if(a.debugInfo){var c=new d.Comment("/* "+a.toCSS(this._context).replace(/\n/g,"")+" */\n");return c.debugInfo=a.debugInfo,this._visitor.visit(c)}return}this.charset=!0}return a}},checkValidNodes:function(a,b){if(a)for(var c=0;c0?a.accept(this._visitor):a.rules=null,b.visitDeeper=!1}return a.rules&&(this._mergeRules(a.rules),this._removeDuplicateRules(a.rules)),this.utils.isVisibleRuleset(a)&&(a.ensureVisibility(),d.splice(0,0,a)),1===d.length?d[0]:d},_compileRulesetPaths:function(a){a.paths&&(a.paths=a.paths.filter(function(a){var b;for(" "===a[0].elements[0].combinator.value&&(a[0].elements[0].combinator=new d.Combinator("")),b=0;b=0;e--)if(c=a[e],c instanceof d.Rule)if(f[c.name]){b=f[c.name],b instanceof d.Rule&&(b=f[c.name]=[f[c.name].toCSS(this._context)]);var g=c.toCSS(this._context);b.indexOf(g)!==-1?a.splice(e,1):b.push(g)}else f[c.name]=c}},_mergeRules:function(a){if(a){for(var b,c,e,f={},g=0;g1){c=b[0];var h=[],i=[];b.map(function(a){"+"===a.merge&&(i.length>0&&h.push(e(i)),i=[]),i.push(a)}),h.push(e(i)),c.value=g(h)}})}},visitAnonymous:function(a,b){if(!a.blocksVisibility())return a.accept(this._visitor),a}},b.exports=g},{"../tree":62,"./visitor":91}],91:[function(a,b,c){function d(a){return a}function e(a,b){var c,d;for(c in a)if(a.hasOwnProperty(c))switch(d=a[c],typeof d){case"function":d.prototype&&d.prototype.type&&(d.prototype.typeIndex=b++);break;case"object":b=e(d,b)}return b}var f=a("../tree"),g={visitDeeper:!0},h=!1,i=function(a){this._implementation=a,this._visitFnCache=[],h||(e(f,1),h=!0)};i.prototype={visit:function(a){if(!a)return a;var b=a.typeIndex;if(!b)return a;var c,e=this._visitFnCache,f=this._implementation,h=b<<1,i=1|h,j=e[h],k=e[i],l=g;if(l.visitDeeper=!0,j||(c="visit"+a.type,j=f[c]||d,k=f[c+"Out"]||d,e[h]=j,e[i]=k),j!==d){var m=j.call(f,a,l);f.isReplacing&&(a=m)}return l.visitDeeper&&a&&a.accept&&a.accept(this),k!=d&&k.call(f,a),a},visitArray:function(a,b){if(!a)return a;var c,d=a.length;if(b||!this._implementation.isReplacing){for(c=0;ck){for(var b=0,c=h.length-j;b Datart + +
    + + `, error); + } + return results; + }); + + const [visible, setVisible] = useState(false); + const [dataSource, setDataSource] = useState< + ScorecardConditionalStyleFormValues[] + >( + myData?.value?.filter(item => + allItems.find(ac => ac.key === item.metricKey), + ) || [], + ); + + const [currentItem, setCurrentItem] = + useState( + {} as ScorecardConditionalStyleFormValues, + ); + const onEditItem = (values: ScorecardConditionalStyleFormValues) => { + setCurrentItem(CloneValueDeep(values)); + openConditionalStyle(); + }; + const onRemoveItem = (values: ScorecardConditionalStyleFormValues) => { + const result: ScorecardConditionalStyleFormValues[] = dataSource.filter( + item => item.uid !== values.uid, + ); + + setDataSource(result); + onChange?.(ancestors, { + ...myData, + value: result, + }); + }; + + const tableColumnsSettings: ColumnsType = + [ + { + title: t('viz.palette.data.metrics', true), + dataIndex: 'metricKey', + render: key => { + return allItems.find(v => v.key === key)?.label; + }, + }, + { + title: t('conditionalStyleTable.header.operator'), + dataIndex: 'operator', + }, + { + title: t('conditionalStyleTable.header.value'), + dataIndex: 'value', + render: (_, { value }) => <>{JSON.stringify(value)}, + }, + { + title: t('conditionalStyleTable.header.color.title'), + dataIndex: 'value', + render: (_, { color }) => ( + <> + + {t('conditionalStyleTable.header.color.text')} + + + {t('conditionalStyleTable.header.color.background')} + + + ), + }, + { + title: t('conditionalStyleTable.header.action'), + dataIndex: 'action', + width: 140, + render: (_, record) => { + return [ + , + onRemoveItem(record)} + > + + , + ]; + }, + }, + ]; + + const openConditionalStyle = () => { + setVisible(true); + }; + const closeConditionalStyleModal = () => { + setVisible(false); + setCurrentItem({} as ScorecardConditionalStyleFormValues); + }; + const submitConditionalStyleModal = ( + values: ScorecardConditionalStyleFormValues, + ) => { + let result: ScorecardConditionalStyleFormValues[] = []; + + if (values.uid) { + result = dataSource.map(item => { + if (item.uid === values.uid) { + return values; + } + return item; + }); + } else { + result = [...dataSource, { ...values, uid: uuidv4() }]; + } + + setDataSource(result); + closeConditionalStyleModal(); + onChange?.(ancestors, { + ...myData, + value: result, + }); + }; + + return ( + + + + + + bordered={true} + size="small" + pagination={false} + rowKey={record => record.uid!} + columns={tableColumnsSettings} + dataSource={dataSource} + /> + + + + + ); + }, + itemLayoutComparer, +); + +const StyledScorecardConditionalStylePanel = styled(Space)` + width: 100%; + margin-top: 10px; + overflow: hidden; +`; + +export default ScorecardConditionalStyle; diff --git a/frontend/src/app/components/FormGenerator/Customize/ScorecardConditionalStyle/add.tsx b/frontend/src/app/components/FormGenerator/Customize/ScorecardConditionalStyle/add.tsx new file mode 100644 index 000000000..cc4f7a2ac --- /dev/null +++ b/frontend/src/app/components/FormGenerator/Customize/ScorecardConditionalStyle/add.tsx @@ -0,0 +1,322 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ +import { Col, Form, Input, InputNumber, Modal, Row, Select } from 'antd'; +import { ColorPickerPopover } from 'app/components/ColorPicker'; +import { ColumnTypes } from 'app/pages/MainPage/pages/ViewPage/constants'; +import { memo, useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { G70 } from 'styles/StyleConstants'; +import { isEmpty } from 'utils/object'; +import { + ConditionalOperatorTypes, + OperatorTypes, + OperatorTypesLocale, +} from '../ConditionalStyle/types'; +import { ScorecardConditionalStyleFormValues } from './types'; + +interface AddProps { + context?: any; + allItems?: any[]; + translate?: (title: string, options?: any) => string; + visible: boolean; + values: ScorecardConditionalStyleFormValues; + onOk: (values: ScorecardConditionalStyleFormValues) => void; + onCancel: () => void; +} + +export default function Add({ + translate: t = title => title, + values, + visible, + onOk, + onCancel, + context, + allItems, +}: AddProps) { + const [colors] = useState([ + { + name: 'textColor', + label: t('conditionalStyleTable.header.color.text'), + value: G70, + }, + { + name: 'background', + label: t('conditionalStyleTable.header.color.background'), + value: 'transparent', + }, + ]); + const [operatorSelect, setOperatorSelect] = useState< + { label: string; value: string }[] + >([]); + const [operatorValue, setOperatorValue] = useState( + OperatorTypes.Equal, + ); + const [form] = Form.useForm(); + const [type] = useState(ColumnTypes.Number); + + useEffect(() => { + if (type) { + setOperatorSelect( + ConditionalOperatorTypes[type]?.map(item => ({ + label: `${OperatorTypesLocale[item]} [${item}]`, + value: item, + })), + ); + } else { + setOperatorSelect([]); + } + }, []); + + useEffect(() => { + // !重置form + if (visible) { + const result: Partial = + Object.keys(values).length === 0 + ? { + operator: OperatorTypes.Equal, + color: { + background: 'transparent', + textColor: G70, + }, + metricKey: allItems?.[0]?.value, + } + : values; + form.setFieldsValue(result); + setOperatorValue(result.operator ?? OperatorTypes.Equal); + } + }, [form, visible, values, allItems]); + + const modalOk = () => { + form.validateFields().then(values => { + onOk({ + ...values, + target: { + name: context?.label, + type: context?.type, + }, + }); + }); + }; + + const operatorChange = (value: OperatorTypes) => { + setOperatorValue(value); + }; + + const renderValueNode = () => { + let DefaultNode = <>; + switch (type) { + case ColumnTypes.Number: + DefaultNode = ; + break; + default: + DefaultNode = ; + break; + } + + switch (operatorValue) { + case OperatorTypes.In: + case OperatorTypes.NotIn: + return ( + + + + + + + + + - handleCopyToClipboard(getFullShareLinkPath(shareLink)) + {shareLink?.usePassword ? ( + + + + handleCopyToClipboard( + t('share.link') + + `: ` + + `${getFullShareLinkPath(shareLink)}` + + ` ` + + t('share.password') + + `: ` + + `${shareLink?.password}`, + ) + } + /> } /> - } - /> - - {shareLink?.usePassword && ( - - handleCopyToClipboard(shareLink?.password)} - /> - } - /> + + + ) : ( + + + + handleCopyToClipboard(getFullShareLinkPath(shareLink)) + } + /> + } + /> + )} diff --git a/frontend/src/app/hooks/useCacheWidthHeight.ts b/frontend/src/app/hooks/useCacheWidthHeight.ts index 1d93108ec..95d9f68c3 100644 --- a/frontend/src/app/hooks/useCacheWidthHeight.ts +++ b/frontend/src/app/hooks/useCacheWidthHeight.ts @@ -16,7 +16,7 @@ * limitations under the License. */ -import { useEffect, useState } from 'react'; +import { useLayoutEffect, useState } from 'react'; import useResizeObserver from './useResizeObserver'; export const useCacheWidthHeight = ( @@ -33,7 +33,7 @@ export const useCacheWidthHeight = ( refreshMode: 'debounce', refreshRate: 20, }); - useEffect(() => { + useLayoutEffect(() => { if (width > 0) { setCacheW(width); setCacheH(height); diff --git a/frontend/src/app/hooks/useComputedState.ts b/frontend/src/app/hooks/useComputedState.ts index 6f8aecaec..dd7c7f25d 100644 --- a/frontend/src/app/hooks/useComputedState.ts +++ b/frontend/src/app/hooks/useComputedState.ts @@ -22,7 +22,7 @@ import { useEffect, useState } from 'react'; * Lazy initialized state when dependency object has correct value. * @param {*} stateTransformer required, get/transform to new state value * @param {*} shouldUpdate required, if update new state compare function - * with previous and current dependecyvalue + * with previous and current dependency value * @param {*} dependency required, dependency value * @param {*} defaultState optional, default state value * @return {*} [state, setState] diff --git a/frontend/src/app/hooks/useDebouncedFormValue.ts b/frontend/src/app/hooks/useDebouncedFormValue.ts new file mode 100644 index 000000000..ee1d7cf2b --- /dev/null +++ b/frontend/src/app/hooks/useDebouncedFormValue.ts @@ -0,0 +1,47 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import debounce from 'lodash/debounce'; +import { useMemo, useState } from 'react'; + +export default function useDebouncedFormValue( + initValue: T, + options: { + ancestors: number[]; + needRefresh?: boolean; + delay: number; + }, + onChange?: (ancestors: number[], value: T, needRefresh?: boolean) => void, +) { + const [cachedValue, setCachedValue] = useState(initValue); + + const valueChange = newValue => { + setCachedValue(newValue); + debouncedValueChange(newValue); + }; + + const debouncedValueChange = useMemo( + () => + debounce(newValue => { + onChange?.(options.ancestors, newValue, options?.needRefresh); + }, options.delay), + [options.ancestors, options.delay, onChange, options?.needRefresh], + ); + + return [cachedValue, valueChange]; +} diff --git a/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts b/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts index fd29904b3..d2733bba8 100644 --- a/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts +++ b/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts @@ -25,7 +25,7 @@ import { ChartDTO } from 'app/types/ChartDTO'; import { getDistinctFields } from 'app/utils/fetch'; import useMount from './useMount'; -export const useFetchFilterDataByCondtion = ( +export const useFetchFilterDataByCondition = ( viewId?: string, condition?: FilterCondition, onFinish?: (datas: RelationFilterValue[]) => void, @@ -51,4 +51,4 @@ export const useFetchFilterDataByCondtion = ( }); }; -export default useFetchFilterDataByCondtion; +export default useFetchFilterDataByCondition; diff --git a/frontend/src/app/hooks/useFieldActionModal.tsx b/frontend/src/app/hooks/useFieldActionModal.tsx index f3ec0eff0..264b94141 100644 --- a/frontend/src/app/hooks/useFieldActionModal.tsx +++ b/frontend/src/app/hooks/useFieldActionModal.tsx @@ -32,7 +32,7 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { const t = useI18NPrefix(i18nPrefix); const [show, contextHolder] = useStateModal({ initState: {} }); - const getConent = ( + const getContent = ( actionType, config?: ChartDataSectionField, dataset?: ChartDataSetDTO, @@ -106,7 +106,7 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { title: t(actionType), modalSize: modalSize || _modalSize, content: onChange => - getConent( + getContent( actionType, currentConfig, dataset, diff --git a/frontend/src/app/hooks/useGetVizIcon.tsx b/frontend/src/app/hooks/useGetVizIcon.tsx index 192357dd3..2ac5b8ab5 100644 --- a/frontend/src/app/hooks/useGetVizIcon.tsx +++ b/frontend/src/app/hooks/useGetVizIcon.tsx @@ -4,18 +4,76 @@ import { FolderOpenFilled, FundFilled, } from '@ant-design/icons'; -import { useCallback } from 'react'; +import ChartManager from 'app/pages/ChartWorkbenchPage/models/ChartManager'; +import React, { useCallback } from 'react'; +import styled from 'styled-components/macro'; +import { FONT_SIZE_TITLE } from 'styles/StyleConstants'; function useGetVizIcon() { - return useCallback(({ relType }) => { - switch (relType) { - case 'DASHBOARD': - return ; - case 'DATACHART': - return ; - default: - return p => (p.expanded ? : ); - } - }, []); + const chartManager = ChartManager.instance(); + const chartIcons = chartManager.getAllChartIcons(); + + return useCallback( + ({ relType, avatar, subType }) => { + switch (relType) { + case 'DASHBOARD': + return subType !== null ? ( + renderIcon(subType === 'free' ? 'CombinedShape' : 'kanban') + ) : ( + + ); + case 'DATACHART': + return avatar ? renderIcon(chartIcons[avatar]) : ; + default: + return p => (p.expanded ? : ); + } + }, + [chartIcons], + ); } + export default useGetVizIcon; + +export const renderIcon = (iconStr: string) => { + if (/^; + } + if (/svg\+xml;base64/.test(iconStr)) { + return ; + } + return ; +}; + +const SVGFontIconRender = ({ iconStr }) => { + return ( + + ); +}; + +const SVGImageRender = ({ iconStr }) => { + return ( + + ); +}; + +const Base64ImageRender = ({ iconStr }) => { + return ( + + ); +}; + +const StyledInlineSVGIcon = styled.img``; + +const StyledSVGFontIcon = styled.i``; + +const StyledBase64Icon = styled.img``; diff --git a/frontend/src/app/hooks/useStateModal.tsx b/frontend/src/app/hooks/useStateModal.tsx index 55e853ce1..1e7fba754 100644 --- a/frontend/src/app/hooks/useStateModal.tsx +++ b/frontend/src/app/hooks/useStateModal.tsx @@ -126,6 +126,7 @@ function useStateModal({ initState }: { initState?: any }) { onCancel: handleClickCancelButton, maskClosable: true, icon: null, + centered: true, }); }; diff --git a/frontend/src/app/hooks/useWidgetRowHeight.ts b/frontend/src/app/hooks/useWidgetRowHeight.ts index fb49cc5b9..3a4c93332 100644 --- a/frontend/src/app/hooks/useWidgetRowHeight.ts +++ b/frontend/src/app/hooks/useWidgetRowHeight.ts @@ -21,11 +21,18 @@ import { BASE_VIEW_WIDTH, MIN_ROW_HEIGHT, } from 'app/pages/DashBoardPage/constants'; -import { useMemo } from 'react'; -import { useCacheWidthHeight } from './useCacheWidthHeight'; +import { useLayoutEffect, useMemo, useState } from 'react'; +import useResizeObserver from './useResizeObserver'; export const useWidgetRowHeight = () => { - const { ref, cacheW } = useCacheWidthHeight(); + const [cacheW, setCacheW] = useState(0); + const { ref, width = 0 } = useResizeObserver(); + + useLayoutEffect(() => { + if (width > 0) { + setCacheW(width); + } + }, [width]); const widgetRowHeight = useMemo(() => { let dynamicHeight = (cacheW * BASE_ROW_HEIGHT) / BASE_VIEW_WIDTH; return Math.max(dynamicHeight, MIN_ROW_HEIGHT); diff --git a/frontend/src/app/index.tsx b/frontend/src/app/index.tsx index 07a06e458..bbeabf37c 100644 --- a/frontend/src/app/index.tsx +++ b/frontend/src/app/index.tsx @@ -26,7 +26,7 @@ import { Helmet } from 'react-helmet-async'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { BrowserRouter, Route, Switch } from 'react-router-dom'; -import { GlobalStyle, OverriddenStyle } from 'styles/globalStyles'; +import { GlobalStyles } from 'styles/globalStyles'; import { getToken } from 'utils/auth'; import useI18NPrefix from './hooks/useI18NPrefix'; import { LoginAuthRoute } from './LoginAuthRoute'; @@ -83,8 +83,7 @@ export function App() { /> - - + ); diff --git a/frontend/src/app/migration/BoardConfig/migrateBoardConfig.ts b/frontend/src/app/migration/BoardConfig/migrateBoardConfig.ts index 09dbec745..23fed1c27 100644 --- a/frontend/src/app/migration/BoardConfig/migrateBoardConfig.ts +++ b/frontend/src/app/migration/BoardConfig/migrateBoardConfig.ts @@ -15,14 +15,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { MIN_MARGIN, MIN_PADDING } from 'app/pages/DashBoardPage/constants'; import { BoardTypeMap, DashboardConfig, } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { getInitBoardConfig } from 'app/pages/DashBoardPage/utils/board'; -import { VERSION_BETA_0, VERSION_LIST } from '../constants'; +import { versionCanDo } from '../utils'; +import { VERSION_BETA_0, VERSION_BETA_1, VERSION_BETA_2 } from './../constants'; export const parseBoardConfig = (boardConfig: string) => { let borderTypes = Object.values(BoardTypeMap); @@ -40,11 +40,7 @@ export const parseBoardConfig = (boardConfig: string) => { }; export const beta0 = (config: DashboardConfig) => { - config.version = config.version || VERSION_BETA_0; - const canHandleVersions = VERSION_LIST.slice(0, 1); - // 此函数只能处理 beta0以及 beta0之前的版本 - if (!canHandleVersions.includes(config.version)) return config; - + if (!versionCanDo(VERSION_BETA_0, config.version)) return config; // 1. initialQuery 新增属性 检测没有这个属性就设置为 true,如果已经设置为false,则保持false if (!config.hasOwnProperty('initialQuery')) { config.initialQuery = true; @@ -62,9 +58,29 @@ export const beta0 = (config: DashboardConfig) => { config.hasQueryControl = Boolean(config.hasQueryControl); config.hasResetControl = Boolean(config.hasQueryControl); + // reset config.version + config.version = VERSION_BETA_0; + return config; +}; + +export const beta1 = (config: DashboardConfig) => { + if (!versionCanDo(VERSION_BETA_1, config.version)) return config; + config.version = VERSION_BETA_1; + return config; +}; +export const beta2 = (config: DashboardConfig) => { + if (!versionCanDo(VERSION_BETA_1, config.version)) return config; + // allowOverlap in autoBoard + if (!config.allowOverlap) { + config.allowOverlap = false; + } + config.version = VERSION_BETA_2; return config; }; export const migrateBoardConfig = (boardConfig: string) => { let config = parseBoardConfig(boardConfig); - return beta0(config); + config = beta0(config); + config = beta1(config); + config = beta2(config); + return config; }; diff --git a/frontend/src/app/migration/WidgetConfig/migrateWidgets.ts b/frontend/src/app/migration/BoardConfig/migrateWidgets.ts similarity index 81% rename from frontend/src/app/migration/WidgetConfig/migrateWidgets.ts rename to frontend/src/app/migration/BoardConfig/migrateWidgets.ts index 427aff955..d5b09055c 100644 --- a/frontend/src/app/migration/WidgetConfig/migrateWidgets.ts +++ b/frontend/src/app/migration/BoardConfig/migrateWidgets.ts @@ -15,7 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { ControllerWidgetContent, Relation, @@ -27,7 +26,8 @@ import { fontDefault, VALUE_SPLITTER, } from 'app/pages/DashBoardPage/utils/widget'; -import { VERSION_BETA_0, VERSION_LIST } from '../constants'; +import { versionCanDo } from '../utils'; +import { VERSION_BETA_0, VERSION_BETA_1, VERSION_BETA_2 } from './../constants'; /** * @@ -60,11 +60,7 @@ export const convertWidgetRelationsToObj = ( */ export const beta0 = (widget?: Widget) => { if (!widget) return undefined; - const canHandleVersions = ['', undefined, null].concat( - VERSION_LIST.slice(0, 1), - ); - // 此函数只能处理 beta0以及 beta0之前的版本 - if (!canHandleVersions.includes(widget.config.version)) return widget; + if (!versionCanDo(VERSION_BETA_0, widget?.config.version)) return widget; // 1.放弃了 filter type 新的是 controller if ((widget.config.type as any) === 'filter') { @@ -89,14 +85,20 @@ export const beta0 = (widget?: Widget) => { return widget; }; -/** - * - * TODO beta1 - * @param {Widget} [widget] - * @return {*} - */ export const beta1 = (widget?: Widget) => { if (!widget) return undefined; + if (!versionCanDo(VERSION_BETA_1, widget?.config.version)) return widget; + widget.config.version = VERSION_BETA_1; + return widget; +}; +export const beta2 = (widget?: Widget) => { + if (!widget) return undefined; + if (!versionCanDo(VERSION_BETA_2, widget?.config.version)) return widget; + // widget.lock + if (!widget.config.lock) { + widget.config.lock = false; + } + widget.config.version = VERSION_BETA_2; return widget; }; /** @@ -133,13 +135,9 @@ export const migrateWidgets = (widgets: ServerWidget[]) => { }) .filter(widget => !!widget) .map(widget => { - let sourceVersion = widget?.config.version || VERSION_BETA_0; - if (!VERSION_LIST.includes(sourceVersion)) { - widget!.config.version = VERSION_BETA_0; - } - let resWidget: Widget | undefined; - resWidget = beta1(beta0(widget)); - if (!resWidget) return null; + let resWidget = beta0(widget); + resWidget = beta1(resWidget); + resWidget = beta2(resWidget); return resWidget; }) .filter(widget => !!widget); diff --git a/frontend/src/app/migration/MigrationEvent.ts b/frontend/src/app/migration/MigrationEvent.ts new file mode 100644 index 000000000..c34511285 --- /dev/null +++ b/frontend/src/app/migration/MigrationEvent.ts @@ -0,0 +1,62 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { APP_VERSION_INIT } from './constants'; +import { IDomainEvent, Task } from './types'; + +/** + * Migration Event + * @class MigrationTaskEvent + * @template TDomainModel + */ +class MigrationEvent + implements IDomainEvent +{ + version: string = APP_VERSION_INIT; + task?: Task; + + constructor(version: string, task: Task) { + this.version = version; + this.task = task; + } + + public run(model?: TDomainModel) { + if (!this.task) { + throw new Error('Please register migration task function first!'); + } + + try { + const result = this.task.call(Object.create(null), model); + if (!!result) { + // auto update version when on error occur + result.version = this.version; + } + return result; + } catch (error) { + console.log( + '%c Datart Migration Error | Version: %s | Please Contract Administrator! ', + 'background: red; color: #fafafa', + this.version, + ); + console.log('Migration Event Error | Stack Trace: %o', error); + throw error; + } + } +} + +export default MigrationEvent; diff --git a/frontend/src/app/migration/MigrationEventDispatcher.ts b/frontend/src/app/migration/MigrationEventDispatcher.ts new file mode 100644 index 000000000..08ddce6ec --- /dev/null +++ b/frontend/src/app/migration/MigrationEventDispatcher.ts @@ -0,0 +1,87 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { CloneValueDeep } from 'utils/object'; +import { APP_SEMANTIC_VERSIONS } from './constants'; +import { IDomainEvent } from './types'; + +/** + * Migration Event Dispatcher + * [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html) + * @class EventDispatcher + * @template TDomainModel + */ +class MigrationEventDispatcher { + domainEvents: IDomainEvent[] = []; + + constructor(...events: IDomainEvent[]) { + this.domainEvents = events; + } + + public process(model: TDomainModel, drawback?: TDomainModel) { + const drawbackModel = drawback || CloneValueDeep(model); + + try { + return this.domainEvents.reduce((acc, event) => { + if (this.shouldDoEventSourcing(acc.version, event.version)) { + return event.run(acc) || acc; + } + return acc; + }, model); + } catch { + return drawbackModel; + } + } + + private shouldDoEventSourcing( + modelVersion?: string, + eventVersion?: string, + ): boolean { + const modelVerIndex = APP_SEMANTIC_VERSIONS.findIndex( + v => v === modelVersion, + ); + const currentEventVerIndex = APP_SEMANTIC_VERSIONS.findIndex( + v => v === eventVersion, + ); + if (currentEventVerIndex === 0) { + return modelVerIndex < currentEventVerIndex; + } else if (currentEventVerIndex > 0) { + const domainEventIndex = this.domainEvents.findIndex( + dEvent => dEvent.version === eventVersion, + ); + if (domainEventIndex <= 0) { + return modelVerIndex < currentEventVerIndex; + } else { + const prevDomainEventIndex = domainEventIndex - 1; + const prevEventVersion = + this.domainEvents[prevDomainEventIndex].version; + + const prevEventVerIndex = APP_SEMANTIC_VERSIONS.findIndex( + v => v === prevEventVersion, + ); + return ( + prevEventVerIndex <= modelVerIndex && + modelVerIndex < currentEventVerIndex + ); + } + } + return false; + } +} + +export default MigrationEventDispatcher; diff --git a/frontend/src/app/migration/StoryConfig/migrateStoryConfig.ts b/frontend/src/app/migration/StoryConfig/migrateStoryConfig.ts new file mode 100644 index 000000000..bd6b9ec8b --- /dev/null +++ b/frontend/src/app/migration/StoryConfig/migrateStoryConfig.ts @@ -0,0 +1,57 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ +import { StoryConfig } from 'app/pages/StoryBoardPage/slice/types'; +import { getInitStoryConfig } from 'app/pages/StoryBoardPage/utils'; +import { versionCanDo } from '../utils'; +import { VERSION_BETA_0, VERSION_BETA_1, VERSION_BETA_2 } from './../constants'; + +export const parseStoryConfig = (storyConfig: string) => { + if (!storyConfig) { + return getInitStoryConfig(); + } + try { + let nextConfig: StoryConfig = JSON.parse(storyConfig); + return nextConfig; + } catch (error) { + console.log('解析 story.config 出错'); + return getInitStoryConfig(); + } +}; + +export const beta0 = (config: StoryConfig) => { + if (!versionCanDo(VERSION_BETA_0, config.version)) return config; + config.version = VERSION_BETA_0; + return config; +}; +export const beta1 = (config: StoryConfig) => { + if (!versionCanDo(VERSION_BETA_1, config.version)) return config; + config.version = VERSION_BETA_1; + return config; +}; +export const beta2 = (config: StoryConfig) => { + if (!versionCanDo(VERSION_BETA_2, config.version)) return config; + config.version = VERSION_BETA_2; + return config; +}; +export const migrateStoryConfig = (boardConfig: string) => { + let config = parseStoryConfig(boardConfig); + config = beta0(config); + config = beta1(config); + config = beta2(config); + return config; +}; diff --git a/frontend/src/app/migration/StoryConfig/migrateStoryPageConfig.ts b/frontend/src/app/migration/StoryConfig/migrateStoryPageConfig.ts new file mode 100644 index 000000000..3d8fc5f05 --- /dev/null +++ b/frontend/src/app/migration/StoryConfig/migrateStoryPageConfig.ts @@ -0,0 +1,57 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ +import { StoryPageConfig } from 'app/pages/StoryBoardPage/slice/types'; +import { getInitStoryPageConfig } from 'app/pages/StoryBoardPage/utils'; +import { versionCanDo } from '../utils'; +import { VERSION_BETA_0, VERSION_BETA_1, VERSION_BETA_2 } from './../constants'; + +export const parseStoryPageConfig = (storyConfig: string) => { + try { + let nextConfig: StoryPageConfig = JSON.parse(storyConfig); + return nextConfig; + } catch (error) { + let nextConfig = getInitStoryPageConfig(); + return nextConfig; + } +}; + +export const beta0 = (config: StoryPageConfig) => { + const curVersion = VERSION_BETA_0; + if (!versionCanDo(curVersion, config.version)) return config; + config.version = curVersion; + return config; +}; +export const beta1 = (config: StoryPageConfig) => { + const curVersion = VERSION_BETA_1; + if (!versionCanDo(curVersion, config.version)) return config; + config.version = curVersion; + return config; +}; +export const beta2 = (config: StoryPageConfig) => { + const curVersion = VERSION_BETA_2; + if (!versionCanDo(curVersion, config.version)) return config; + config.version = curVersion; + return config; +}; +export const migrateStoryPageConfig = (configStr: string) => { + let config = parseStoryPageConfig(configStr); + config = beta0(config); + config = beta1(config); + config = beta2(config); + return config; +}; diff --git a/frontend/src/app/migration/ViewConfig/migrationViewDetailConfig.ts b/frontend/src/app/migration/ViewConfig/migrationViewDetailConfig.ts new file mode 100644 index 000000000..da221c67d --- /dev/null +++ b/frontend/src/app/migration/ViewConfig/migrationViewDetailConfig.ts @@ -0,0 +1,33 @@ +import { APP_VERSION_BETA_2 } from '../constants'; +import MigrationEvent from '../MigrationEvent'; +import MigrationEventDispatcher from '../MigrationEventDispatcher'; + +export const beta2 = viewConfig => { + if (!viewConfig) { + return viewConfig; + } + + try { + if (viewConfig) { + viewConfig.expensiveQuery = false; + } + + return viewConfig; + } catch (error) { + console.error('Migration ViewConfig Errors | beta.2 | ', error); + return viewConfig; + } +}; + +export const migrateViewConfig = (viewConfig: string) => { + if (!viewConfig?.trim().length) { + return viewConfig; + } + + const config = JSON.parse(viewConfig); + const event2 = new MigrationEvent(APP_VERSION_BETA_2, beta2); + const dispatcher = new MigrationEventDispatcher(event2); + const result = dispatcher.process(config); + + return JSON.stringify(result); +}; diff --git a/frontend/src/app/migration/ViewConfig/migrationViewModelConfig.ts b/frontend/src/app/migration/ViewConfig/migrationViewModelConfig.ts new file mode 100644 index 000000000..c46cc4a76 --- /dev/null +++ b/frontend/src/app/migration/ViewConfig/migrationViewModelConfig.ts @@ -0,0 +1,74 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { CloneValueDeep } from 'utils/object'; +import { APP_VERSION_BETA_2 } from '../constants'; +import MigrationEvent from '../MigrationEvent'; +import MigrationEventDispatcher from '../MigrationEventDispatcher'; + +/** + * Initialize method to setup version used by @see MigrationEvent + * + * @param {object} [model] + * @return {*} {(object | undefined)} + */ +const init = (model?: object): object | undefined => { + return model; +}; + +/** + * Migrate @see View config in beta.2 version + * Changes: + * - migrate model to ... + * - .... + * + * @param {object} [model] + * @return {*} {(object | undefined)} + */ +const beta2 = (model?: object): object | undefined => { + const clonedModel = CloneValueDeep(model) || {}; + if (model) { + Object.keys(clonedModel).forEach(name => { + clonedModel[name] = { ...clonedModel[name], name }; + }); + model = { + hierarchy: clonedModel, + columns: clonedModel, + }; + } + return model; +}; + +/** + * main entry point of migration + * + * @param {string} model + * @return {string} + */ +const beginViewModelMigration = (model: string): string => { + if (!model?.trim().length) { + return model; + } + const modelObj = JSON.parse(model); + const event2 = new MigrationEvent(APP_VERSION_BETA_2, beta2); + const dispatcher = new MigrationEventDispatcher(event2); + const result = dispatcher.process(modelObj); + return JSON.stringify(result); +}; + +export default beginViewModelMigration; diff --git a/frontend/src/app/migration/__tests__/MigrationEventDispatcher.test.ts b/frontend/src/app/migration/__tests__/MigrationEventDispatcher.test.ts new file mode 100644 index 000000000..b5aeb7674 --- /dev/null +++ b/frontend/src/app/migration/__tests__/MigrationEventDispatcher.test.ts @@ -0,0 +1,194 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { + APP_SEMANTIC_VERSIONS, + APP_VERSION_BETA_0, + APP_VERSION_BETA_1, + APP_VERSION_BETA_2, + APP_VERSION_INIT, +} from '../constants'; +import MigrationEvent from '../MigrationEvent'; +import MigrationEventDispatcher from '../MigrationEventDispatcher'; + +describe('MigrationEventDispatcher Tests', () => { + test('app semantic version tests', () => { + expect(APP_SEMANTIC_VERSIONS).toEqual( + expect.arrayContaining([ + '0.0.0', + '1.0.0-beta.0', + '1.0.0-beta.1', + '1.0.0-beta.2', + ]), + ); + }); + + test('should set version after eventinvoked', () => { + const event0 = new MigrationEvent(APP_VERSION_INIT, m => m); + const dispatcher = new MigrationEventDispatcher(event0); + expect(dispatcher.process({ version: '', id: 1 } as any)).toEqual({ + id: 1, + version: APP_VERSION_INIT, + }); + }); + + test('should process events one by one and set last version', () => { + const event0 = new MigrationEvent(APP_VERSION_INIT, m => m); + const event1 = new MigrationEvent(APP_VERSION_BETA_0, m => m); + const dispatcher = new MigrationEventDispatcher(event0, event1); + expect(dispatcher.process({ version: '', id: 1 } as any)).toEqual({ + id: 1, + version: APP_VERSION_BETA_0, + }); + }); + + test('should process events with no init version', () => { + const event2 = new MigrationEvent(APP_VERSION_BETA_2, m => m); + const dispatcher = new MigrationEventDispatcher(event2); + expect(dispatcher.process({ version: '', id: 1 } as any)).toEqual({ + id: 1, + version: APP_VERSION_BETA_2, + }); + }); + + test('should return initialize model value if exception happen', () => { + const event0 = new MigrationEvent(APP_VERSION_INIT, (m: any) => { + m.id = 9; + return m; + }); + const event1 = new MigrationEvent(APP_VERSION_BETA_1, m => { + throw new Error('some error occor'); + }); + const event2 = new MigrationEvent(APP_VERSION_BETA_2, m => m); + const dispatcher = new MigrationEventDispatcher(event0, event1, event2); + expect(dispatcher.process({ version: '', id: 1 } as any)).toEqual({ + id: 1, + version: '', + }); + }); + + test('should return drawback model value if exception happen', () => { + const event0 = new MigrationEvent(APP_VERSION_INIT, (m: any) => { + m.id = 9; + return m; + }); + const event1 = new MigrationEvent(APP_VERSION_BETA_1, m => { + throw new Error('some error occor'); + }); + const event2 = new MigrationEvent(APP_VERSION_BETA_2, m => m); + const dispatcher = new MigrationEventDispatcher(event0, event1, event2); + expect( + dispatcher.process({ version: '', id: 1 } as any, { + version: 'drawback version', + id: 999, + }), + ).toEqual({ + id: 999, + version: 'drawback version', + }); + }); + + test('should migrate change by some events', () => { + const event1 = new MigrationEvent(APP_VERSION_BETA_1, m => { + m.beta = 3; + return m; + }); + + const dispatcher = new MigrationEventDispatcher(event1); + expect(dispatcher.process({ id: 0 } as any)).toMatchObject({ + beta: 3, + version: APP_VERSION_BETA_1, + }); + }); + + test('should migrate change by all merge events', () => { + const eventInit = new MigrationEvent(APP_VERSION_INIT, m => { + m.id = 1; + return m; + }); + const event0 = new MigrationEvent(APP_VERSION_BETA_0, m => { + m.beta = 2; + return m; + }); + const event1 = new MigrationEvent(APP_VERSION_BETA_1, m => { + m.beta = 3; + return m; + }); + const event2 = new MigrationEvent(APP_VERSION_BETA_2, m => { + m.beta2 = 3; + return m; + }); + + const dispatcher = new MigrationEventDispatcher( + eventInit, + event0, + event1, + event2, + ); + expect(dispatcher.process({ id: 0 } as any)).toMatchObject({ + id: 1, + beta: 3, + beta2: 3, + version: APP_VERSION_BETA_2, + }); + }); + + test('should migrate change by registered events', () => { + const eventInit = new MigrationEvent(APP_VERSION_INIT, m => { + m.id = 1; + return m; + }); + const event1 = new MigrationEvent(APP_VERSION_BETA_1, m => { + m.beta = 3; + return m; + }); + const dispatcher = new MigrationEventDispatcher(eventInit, event1); + expect(dispatcher.process({ id: 0 } as any)).toMatchObject({ + id: 1, + beta: 3, + version: APP_VERSION_BETA_1, + }); + }); + + test('should migrate change with specific version', () => { + const eventInit = new MigrationEvent(APP_VERSION_INIT, m => { + m.id = 1; + return m; + }); + const event1 = new MigrationEvent(APP_VERSION_BETA_1, m => { + m.beta = 3; + return m; + }); + const event2 = new MigrationEvent(APP_VERSION_BETA_2, m => { + m.id = 999; + return m; + }); + const dispatcher = new MigrationEventDispatcher(eventInit, event1, event2); + expect( + dispatcher.process({ + id: 0, + beta: 2, + version: APP_VERSION_BETA_1, + } as any), + ).toMatchObject({ + id: 999, + beta: 2, + version: APP_VERSION_BETA_2, + }); + }); +}); diff --git a/frontend/src/app/migration/__tests__/migrateBoardConfig.test.ts b/frontend/src/app/migration/__tests__/migrateBoardConfig.test.ts index 6eec585da..f3a8e8c81 100644 --- a/frontend/src/app/migration/__tests__/migrateBoardConfig.test.ts +++ b/frontend/src/app/migration/__tests__/migrateBoardConfig.test.ts @@ -23,8 +23,7 @@ import { migrateBoardConfig, parseBoardConfig, } from '../BoardConfig/migrateBoardConfig'; -import { VERSION_BETA_0, VERSION_BETA_1 } from '../constants'; - +import { CURRENT_VERSION, VERSION_BETA_0, VERSION_BETA_1 } from '../constants'; describe('test migrateBoard ', () => { test('parse board.config', () => { const config = '{}'; @@ -90,7 +89,7 @@ describe('test migrateBoard ', () => { const config = '{}'; expect(migrateBoardConfig(config)).toMatchObject({ type: 'auto', - version: VERSION_BETA_0, + version: CURRENT_VERSION, } as DashboardConfig); }); }); diff --git a/frontend/src/app/pages/DashBoardPage/contexts/WidgetDataContext.ts b/frontend/src/app/migration/__tests__/migrateStoryConfig.test.ts similarity index 71% rename from frontend/src/app/pages/DashBoardPage/contexts/WidgetDataContext.ts rename to frontend/src/app/migration/__tests__/migrateStoryConfig.test.ts index 68fe21225..7806951d3 100644 --- a/frontend/src/app/pages/DashBoardPage/contexts/WidgetDataContext.ts +++ b/frontend/src/app/migration/__tests__/migrateStoryConfig.test.ts @@ -15,8 +15,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { WidgetData } from 'app/pages/DashBoardPage/pages/Board/slice/types'; -import { createContext } from 'react'; -export const WidgetDataContext = createContext<{ data: WidgetData }>({ - data: { id: '', columns: [], rows: [] } as WidgetData, + +import { parseStoryConfig } from '../StoryConfig/migrateStoryConfig'; + +describe('test migrateBoard ', () => { + test('handle parseStoryConfig is empty', () => { + expect(parseStoryConfig('')).toMatchObject({ + version: '', + }); + }); }); diff --git a/frontend/src/app/migration/__tests__/migrateStoryPageConfig.test.ts b/frontend/src/app/migration/__tests__/migrateStoryPageConfig.test.ts new file mode 100644 index 000000000..5701d27a9 --- /dev/null +++ b/frontend/src/app/migration/__tests__/migrateStoryPageConfig.test.ts @@ -0,0 +1,26 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ +import { parseStoryPageConfig } from '../StoryConfig/migrateStoryPageConfig'; + +describe('test migrateBoard ', () => { + test('handle parseStoryPageConfig is empty', () => { + expect(parseStoryPageConfig('')).toMatchObject({ + version: '', + }); + }); +}); diff --git a/frontend/src/app/migration/__tests__/migrateWidgets.test.ts b/frontend/src/app/migration/__tests__/migrateWidgets.test.ts index 28b590c7f..55c620c41 100644 --- a/frontend/src/app/migration/__tests__/migrateWidgets.test.ts +++ b/frontend/src/app/migration/__tests__/migrateWidgets.test.ts @@ -27,7 +27,8 @@ import { beta0, convertWidgetRelationsToObj, migrateWidgets, -} from '../WidgetConfig/migrateWidgets'; +} from '../BoardConfig/migrateWidgets'; +import { CURRENT_VERSION, VERSION_BETA_0 } from '../constants'; describe('test migrateWidgets ', () => { test('should return undefined when widget.config.type === filter', () => { @@ -54,7 +55,7 @@ describe('test migrateWidgets ', () => { const widget2 = { config: { nameConfig: fontDefault, - version: '1.0.0-beta.0', + version: VERSION_BETA_0, }, } as Widget; expect(beta0(widget1 as Widget)).toMatchObject(widget2); @@ -107,7 +108,7 @@ describe('test migrateWidgets ', () => { config: '{}', } as ServerWidget; const widget2 = { - config: '{"version":"1.0.0-beta.0"}', + config: `{"version":"1.0.0-beta.0"}`, } as ServerWidget; const widget3 = { config: '{"version":"rrr"}', @@ -117,13 +118,13 @@ describe('test migrateWidgets ', () => { } as ServerWidget; const resWidget = { config: { - version: '1.0.0-beta.0', + version: CURRENT_VERSION, }, relations: [] as Relation[], } as Widget; const resWidget2 = { config: { - version: '1.0.0-beta.1', + version: CURRENT_VERSION, }, relations: [] as Relation[], } as Widget; diff --git a/frontend/src/app/migration/__tests__/migrationViewDetailConfig.test.ts b/frontend/src/app/migration/__tests__/migrationViewDetailConfig.test.ts new file mode 100644 index 000000000..f8fe5bf8f --- /dev/null +++ b/frontend/src/app/migration/__tests__/migrationViewDetailConfig.test.ts @@ -0,0 +1,77 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { APP_VERSION_BETA_2 } from '../constants'; +import { migrateViewConfig } from '../ViewConfig/migrationViewDetailConfig'; + +describe('migrateViewConfig Test', () => { + test('Test when config is null', () => { + const config = JSON.stringify(null); + expect(migrateViewConfig(config)).toEqual('null'); + }); + + test('Test when config is empty', () => { + const config = ''; + expect(migrateViewConfig(config)).toEqual(''); + }); + + test('Test config without expensiveQuery variable', () => { + const config = + '{"concurrencyControl":true,"concurrencyControlMode":"DIRTYREAD","cache":false,"cacheExpires":0}'; + expect(migrateViewConfig(config)).toEqual( + `{"concurrencyControl":true,"concurrencyControlMode":"DIRTYREAD","cache":false,"cacheExpires":0,"expensiveQuery":false,"version":"${APP_VERSION_BETA_2}"}`, + ); + expect(migrateViewConfig(config)).toMatch(/"version":/); + }); + + test('Test when there is a version number and expensiveQuery = false', () => { + const config = `{"concurrencyControl":true,"concurrencyControlMode":"DIRTYREAD","cache":false,"cacheExpires":0,"expensiveQuery":false,"version":"${APP_VERSION_BETA_2}"}`; + expect(migrateViewConfig(config)).toEqual(config); + expect(migrateViewConfig(config)).toMatch(/"version":/); + }); + + test('Test when there is a version number and expensiveQuery = true', () => { + const config = `{"concurrencyControl":true,"concurrencyControlMode":"DIRTYREAD","cache":false,"cacheExpires":0,"expensiveQuery":true,"version":"${APP_VERSION_BETA_2}"}`; + expect(migrateViewConfig(config)).toEqual(config); + expect(migrateViewConfig(config)).toMatch(/"version":/); + }); + + /** + * The case of dirty data + */ + test('Test when config is null', () => { + const config = + '{"expensiveQuery":false,"concurrencyControl":true,"concurrencyControlMode":"DIRTYREAD","cache":false,"cacheExpires":0}'; + expect(migrateViewConfig(config)).toEqual( + `{"expensiveQuery":false,"concurrencyControl":true,"concurrencyControlMode":"DIRTYREAD","cache":false,"cacheExpires":0,"version":"${APP_VERSION_BETA_2}"}`, + ); + expect(migrateViewConfig(config)).toMatch(/"version":/); + }); + + /** + * The case of dirty data + */ + test('Test for dirty data', () => { + const config = + '{"expensiveQuery":true,"concurrencyControl":true,"concurrencyControlMode":"DIRTYREAD","cache":false,"cacheExpires":0}'; + expect(migrateViewConfig(config)).toEqual( + `{"expensiveQuery":false,"concurrencyControl":true,"concurrencyControlMode":"DIRTYREAD","cache":false,"cacheExpires":0,"version":"${APP_VERSION_BETA_2}"}`, + ); + expect(migrateViewConfig(config)).toMatch(/"version":/); + }); +}); diff --git a/frontend/src/app/migration/__tests__/migrationViewModelConfig.test.ts b/frontend/src/app/migration/__tests__/migrationViewModelConfig.test.ts new file mode 100644 index 000000000..ac93769e6 --- /dev/null +++ b/frontend/src/app/migration/__tests__/migrationViewModelConfig.test.ts @@ -0,0 +1,69 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { APP_VERSION_BETA_2 } from '../constants'; +import beginViewModelMigration from '../ViewConfig/migrationViewModelConfig'; + +describe('migrationViewModelConfig Test', () => { + test('should get latest version after migration', () => { + const model = JSON.stringify({}); + expect(beginViewModelMigration(model)).toEqual( + JSON.stringify({ + hierarchy: {}, + columns: {}, + version: APP_VERSION_BETA_2, + }), + ); + }); + + test('should get latest version even model is empty', () => { + expect(beginViewModelMigration('')).toEqual(''); + }); + + test('should migrate model to nested model object', () => { + const originalModel = { + column1: { name: 'column1', role: 'role', type: 'STRING' }, + column2: { name: 'column2', role: 'role', type: 'NUMBER' }, + }; + const migrationResultObj = JSON.parse( + beginViewModelMigration(JSON.stringify(originalModel)), + ); + expect(migrationResultObj.columns).toMatchObject(originalModel); + expect(migrationResultObj.hierarchy).toMatchObject(originalModel); + expect(migrationResultObj.version).toEqual(APP_VERSION_BETA_2); + }); + + test('should migrate model name to columns', () => { + const originalModel = { + column1: { role: 'role', type: 'STRING' }, + column2: { role: 'role', type: 'NUMBER' }, + }; + const migrationResultObj = JSON.parse( + beginViewModelMigration(JSON.stringify(originalModel)), + ); + expect(migrationResultObj.columns).toMatchObject({ + column1: { name: 'column1', role: 'role', type: 'STRING' }, + column2: { name: 'column2', role: 'role', type: 'NUMBER' }, + }); + expect(migrationResultObj.hierarchy).toMatchObject({ + column1: { name: 'column1', role: 'role', type: 'STRING' }, + column2: { name: 'column2', role: 'role', type: 'NUMBER' }, + }); + expect(migrationResultObj.version).toEqual(APP_VERSION_BETA_2); + }); +}); diff --git a/frontend/src/app/pages/DashBoardPage/contexts/WidgetChartContext.ts b/frontend/src/app/migration/__tests__/utils.test.ts similarity index 52% rename from frontend/src/app/pages/DashBoardPage/contexts/WidgetChartContext.ts rename to frontend/src/app/migration/__tests__/utils.test.ts index 56cd5b8b8..123da089e 100644 --- a/frontend/src/app/pages/DashBoardPage/contexts/WidgetChartContext.ts +++ b/frontend/src/app/migration/__tests__/utils.test.ts @@ -15,8 +15,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DataChart } from 'app/pages/DashBoardPage/pages/Board/slice/types'; -import { createContext } from 'react'; -export const WidgetChartContext = createContext( - {} as DataChart, -); +import { VERSION_LIST } from '../constants'; +import { versionCanDo } from '../utils'; +describe('test versionCanDo ', () => { + test(`no testVersion `, () => { + expect(versionCanDo('v1', undefined)).toBe(true); + }); + const v1 = VERSION_LIST[0]; + const v2 = VERSION_LIST[1]; + test(`v2 canDo v1 `, () => { + expect(versionCanDo(v2, v1)).toBe(true); + }); + test(`v1 not canDo v2 `, () => { + expect(versionCanDo(v1, v2)).toBe(false); + }); + test(`v2 canDo v2 `, () => { + expect(versionCanDo(v2, v2)).toBe(true); + }); +}); diff --git a/frontend/src/app/migration/constants.ts b/frontend/src/app/migration/constants.ts index dea46a685..427ac14b4 100644 --- a/frontend/src/app/migration/constants.ts +++ b/frontend/src/app/migration/constants.ts @@ -18,4 +18,19 @@ export const VERSION_BETA_0 = '1.0.0-beta.0'; export const VERSION_BETA_1 = '1.0.0-beta.1'; -export const VERSION_LIST = [VERSION_BETA_0, VERSION_BETA_1]; +export const VERSION_BETA_2 = '1.0.0-beta.2'; +export const VERSION_LIST = [VERSION_BETA_0, VERSION_BETA_1, VERSION_BETA_2]; +export const CURRENT_VERSION = VERSION_BETA_2; + +export const APP_VERSION_INIT = '0.0.0'; +export const APP_VERSION_BETA_0 = '1.0.0-beta.0'; +export const APP_VERSION_BETA_1 = '1.0.0-beta.1'; +export const APP_VERSION_BETA_2 = '1.0.0-beta.2'; +export const APP_SEMANTIC_VERSIONS = [ + APP_VERSION_INIT, + APP_VERSION_BETA_0, + APP_VERSION_BETA_1, + APP_VERSION_BETA_2, +]; +export const APP_CURRENT_VERSION = + APP_SEMANTIC_VERSIONS[APP_SEMANTIC_VERSIONS.length - 1]; diff --git a/frontend/src/app/pages/DashBoardPage/contexts/WidgetInfoContext.ts b/frontend/src/app/migration/types.ts similarity index 75% rename from frontend/src/app/pages/DashBoardPage/contexts/WidgetInfoContext.ts rename to frontend/src/app/migration/types.ts index 3c68a6bef..1f6f7e8dd 100644 --- a/frontend/src/app/pages/DashBoardPage/contexts/WidgetInfoContext.ts +++ b/frontend/src/app/migration/types.ts @@ -15,6 +15,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { WidgetInfo } from 'app/pages/DashBoardPage/pages/Board/slice/types'; -import { createContext } from 'react'; -export const WidgetInfoContext = createContext({} as WidgetInfo); + +export type Task = (v?: T) => T | undefined; + +export type IDomainEvent = { + version: string; + run: Task; +}; diff --git a/frontend/src/app/migration/utils.ts b/frontend/src/app/migration/utils.ts new file mode 100644 index 000000000..0e1eab9ec --- /dev/null +++ b/frontend/src/app/migration/utils.ts @@ -0,0 +1,26 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { VERSION_LIST } from './constants'; + +export const versionCanDo = (curVersion: string, testVersion?: string) => { + let testVersionIndex = VERSION_LIST.indexOf(testVersion || ''); + if (testVersionIndex === -1) return true; + let curVersionIndex = VERSION_LIST.indexOf(curVersion); + return curVersionIndex >= testVersionIndex; +}; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx index ecab07391..32388e8fb 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx @@ -15,13 +15,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { DownloadOutlined } from '@ant-design/icons'; import { Button, Space } from 'antd'; import SaveToDashboard from 'app/components/SaveToDashboard'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; -import { backendChartSelector } from 'app/pages/ChartWorkbenchPage/slice/workbenchSlice'; +import useMount from 'app/hooks/useMount'; +import { + backendChartSelector, + selectChartEditorDownloadPolling, + useWorkbenchSlice, +} from 'app/pages/ChartWorkbenchPage/slice/workbenchSlice'; +import { DownloadListPopup } from 'app/pages/MainPage/Navbar/DownloadListPopup'; +import { loadTasks } from 'app/pages/MainPage/Navbar/service'; +import { selectHasVizFetched } from 'app/pages/MainPage/pages/VizPage/slice/selectors'; +import { getFolders } from 'app/pages/MainPage/pages/VizPage/slice/thunks'; +import { downloadFile } from 'app/utils/fetch'; import { FC, memo, useCallback, useState } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { FONT_SIZE_ICON_SM, @@ -39,7 +49,7 @@ const ChartHeaderPanel: FC<{ container?: string; onSaveChart?: () => void; onGoBack?: () => void; - onSaveChartToDashBoard?: (dashboardId) => void; + onSaveChartToDashBoard?: (dashboardId, dashboardType) => void; }> = memo( ({ chartName, @@ -50,12 +60,16 @@ const ChartHeaderPanel: FC<{ onSaveChartToDashBoard, }) => { const t = useI18NPrefix(`viz.workbench.header`); + const hasVizFetched = useSelector(selectHasVizFetched); const [isModalVisible, setIsModalVisible] = useState(false); const backendChart = useSelector(backendChartSelector); + const downloadPolling = useSelector(selectChartEditorDownloadPolling); + const dispatch = useDispatch(); + const { actions } = useWorkbenchSlice(); const handleModalOk = useCallback( - (dashboardId: string) => { - onSaveChartToDashBoard?.(dashboardId); + (dashboardId: string, dashboardType: string) => { + onSaveChartToDashBoard?.(dashboardId, dashboardType); setIsModalVisible(true); }, [onSaveChartToDashBoard], @@ -65,13 +79,40 @@ const ChartHeaderPanel: FC<{ setIsModalVisible(false); }, []); + const onSetPolling = useCallback( + (polling: boolean) => { + dispatch(actions.setChartEditorDownloadPolling(polling)); + }, + [dispatch, actions], + ); + + useMount(() => { + if (!hasVizFetched) { + // Request data when there is no data + dispatch(getFolders(orgId as string)); + } + }); + return (

    {chartName}

    - + { + if (item.id) { + downloadFile(item.id).then(() => { + dispatch(actions.setChartEditorDownloadPolling(true)); + }); + } + }} + renderDom={ + + } + /> + @@ -106,6 +147,7 @@ const Wrapper = styled.div` flex-shrink: 0; align-items: center; padding: ${SPACE_SM} ${SPACE_MD} ${SPACE_SM} ${SPACE_SM}; + background-color: ${p => p.theme.componentBackground}; border-bottom: 1px solid ${p => p.theme.borderColorSplit}; h1 { diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx index 80908a9eb..a787de681 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx @@ -26,7 +26,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import styled from 'styled-components/macro'; import ChartDatasetContext from '../../contexts/ChartDatasetContext'; import ChartDataViewContext from '../../contexts/ChartDataViewContext'; -import layoutCnofig, { LayoutComponentType } from './ChartOperationPanelLayout'; +import layoutConfig, { LayoutComponentType } from './ChartOperationPanelLayout'; import ChartConfigPanel from './components/ChartConfigPanel/ChartConfigPanel'; import ChartDataViewPanel from './components/ChartDataViewPanel'; import ChartPresentWrapper from './components/ChartPresentWrapper'; @@ -35,23 +35,26 @@ const ChartOperationPanel: FC<{ chart?: IChart; chartConfig?: ChartConfig; defaultViewId?: string; + allowQuery: boolean; onChartChange: (chart: IChart) => void; onChartConfigChange: (type, payload) => void; onDataViewChange?: () => void; + onCreateDownloadDataTask?: () => void; }> = memo( ({ chart, chartConfig, defaultViewId, + allowQuery, onChartChange, onChartConfigChange, onDataViewChange, + onCreateDownloadDataTask, }) => { - const { dataset } = useContext(ChartDatasetContext); - const { dataView } = useContext(ChartDataViewContext); - + const { dataset, onRefreshDataset } = useContext(ChartDatasetContext); + const { dataView, expensiveQuery } = useContext(ChartDataViewContext); const [layout, setLayout] = useState(() => - Model.fromJson(layoutCnofig), + Model.fromJson(layoutConfig), ); const layoutFactory = node => { @@ -72,6 +75,7 @@ const ChartOperationPanel: FC<{ ); @@ -87,8 +91,12 @@ const ChartOperationPanel: FC<{ } chart={chart} dataset={dataset} + expensiveQuery={expensiveQuery} + allowQuery={allowQuery} chartConfig={chartConfig} onChartChange={onChartChange} + onRefreshDataset={onRefreshDataset} + onCreateDownloadDataTask={onCreateDownloadDataTask} /> ); } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanelLayout.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanelLayout.ts index 41e667896..d52e15dcb 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanelLayout.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanelLayout.ts @@ -25,7 +25,7 @@ export enum LayoutComponentType { CONFIG = 'ChartConfigPanel', } -const layoutCnofig: IJsonModel = { +const layoutConfig: IJsonModel = { global: { tabEnableFloat: true, tabEnableClose: false, @@ -75,4 +75,4 @@ const layoutCnofig: IJsonModel = { }, }; -export default layoutCnofig; +export default layoutConfig; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartConfigPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartConfigPanel.tsx index 949129adc..f4376b547 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartConfigPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartConfigPanel.tsx @@ -58,11 +58,12 @@ const CONFIG_PANEL_TABS = { }; const ChartConfigPanel: FC<{ - chartId: string; + chartId?: string; chartConfig?: ChartConfig; + expensiveQuery?: boolean; onChange: (type: string, payload: ChartConfigPayloadType) => void; }> = memo( - ({ chartId, chartConfig, onChange }) => { + ({ chartId, chartConfig, expensiveQuery, onChange }) => { const t = useI18NPrefix(`viz.palette`); const [tabActiveKey, setTabActiveKey] = useComputedState( () => { @@ -163,6 +164,7 @@ const ChartConfigPanel: FC<{ @@ -187,7 +189,9 @@ const ChartConfigPanel: FC<{ ); }, (prev, next) => - prev.chartConfig === next.chartConfig && prev.chartId === next.chartId, + prev.chartConfig === next.chartConfig && + prev.chartId === next.chartId && + prev.expensiveQuery === next.expensiveQuery, ); export default ChartConfigPanel; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartDataConfigPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartDataConfigPanel.tsx index d9b72bd29..640333be7 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartDataConfigPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartDataConfigPanel.tsx @@ -26,13 +26,14 @@ import PaletteDataConfig from '../ChartDataConfigSection'; const ChartDataConfigPanel: FC<{ dataConfigs?: ChartDataConfig[]; + expensiveQuery?: boolean; onChange: ( ancestors: number[], config: ChartDataConfig, needRefresh?: boolean, ) => void; }> = memo( - ({ dataConfigs, onChange }) => { + ({ dataConfigs, expensiveQuery, onChange }) => { const translate = useI18NPrefix(`viz.palette.data`); const { aggregation } = useContext(ChartAggregationContext); @@ -43,6 +44,7 @@ const ChartDataConfigPanel: FC<{ config, translate, aggregation, + expensiveQuery, onConfigChanged: (ancestors, config, needRefresh?: boolean) => { onChange?.(ancestors, config, needRefresh); }, @@ -75,7 +77,10 @@ const ChartDataConfigPanel: FC<{ ); }, (prev, next) => { - return prev.dataConfigs === next.dataConfigs; + return ( + prev.dataConfigs === next.dataConfigs && + prev.expensiveQuery === next.expensiveQuery + ); }, ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartSettingConfigPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartSettingConfigPanel.tsx index bc0b9762f..4668bea20 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartSettingConfigPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartSettingConfigPanel.tsx @@ -44,7 +44,7 @@ const ChartSettingConfigPanel: FC<{ mode={ c.comType === 'group' ? GroupLayoutMode.INNER - : GroupLayoutMode.OUTTER + : GroupLayoutMode.OUTER } data={c} translate={t} diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartStyleConfigPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartStyleConfigPanel.tsx index 43cfd12f9..893bdbf18 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartStyleConfigPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartConfigPanel/ChartStyleConfigPanel.tsx @@ -49,7 +49,7 @@ const ChartStyleConfigPanel: FC<{ mode={ c.comType === 'group' ? GroupLayoutMode.INNER - : GroupLayoutMode.OUTTER + : GroupLayoutMode.OUTER } data={c} translate={t} diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/BaseDataConfigSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/BaseDataConfigSection.tsx index b49795fe2..c4823fccf 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/BaseDataConfigSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/BaseDataConfigSection.tsx @@ -51,4 +51,5 @@ const StyledBaseDataConfigSection = styled.div` const StyledBaseDataConfigSectionTitle = styled.div` color: ${p => p.theme.textColor}; + user-select: none; `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/FilterTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/FilterTypeSection.tsx index 2c0ac4a1a..b427c560f 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/FilterTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/FilterTypeSection.tsx @@ -73,7 +73,7 @@ const FilterTypeSection: FC = memo( onConfigChanged(ancestors, config, true); }; - const hanldShowExtraFunctionDialog = () => { + const handleShowExtraFunctionDialog = () => { const props = { config: currentConfig, onConfigChange: handleExtraConfigChange, @@ -112,7 +112,7 @@ const FilterTypeSection: FC = memo( diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/utils.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/utils.ts index c9e660438..a484a203a 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/utils.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/utils.ts @@ -26,7 +26,8 @@ export function dataConfigSectionComparer( if ( prevProps.translate !== nextProps.translate || prevProps.config !== nextProps.config || - prevProps.aggregation !== nextProps.aggregation + prevProps.aggregation !== nextProps.aggregation || + prevProps.expensiveQuery !== nextProps.expensiveQuery ) { return false; } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/ChartDataViewPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/ChartDataViewPanel.tsx index 005ece342..1b3d73f7b 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/ChartDataViewPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/ChartDataViewPanel.tsx @@ -325,6 +325,7 @@ const StyledChartDataViewPanel = styled.div` display: flex; flex-direction: column; height: 100%; + background-color: ${p => p.theme.componentBackground}; `; const Header = styled.div` diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartComputedFieldEditor/ChartComputedFieldEditor.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartComputedFieldEditor/ChartComputedFieldEditor.tsx index f17f950cc..1def2b1db 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartComputedFieldEditor/ChartComputedFieldEditor.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartComputedFieldEditor/ChartComputedFieldEditor.tsx @@ -18,9 +18,9 @@ import { Divider, Row } from 'antd'; import { - ChartCompoutedFieldHandle, + ChartComputedFieldHandle, FunctionDescription, -} from 'app/types/CompoutedFieldEditor'; +} from 'app/types/ComputedFieldEditor'; import debounce from 'lodash/debounce'; import { forwardRef, @@ -35,7 +35,7 @@ import ChartComputedFieldEditorDarkTheme from './ChartComputedFieldEditorDarkThe import DatartQueryLanguageSpecification from './DatartQueryLanguageSpecification'; const ChartComputedFieldEditor: ForwardRefRenderFunction< - ChartCompoutedFieldHandle, + ChartComputedFieldHandle, { value?: string; functionDescriptions?: FunctionDescription[]; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartComputedFieldSettingPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartComputedFieldSettingPanel.tsx index e8a085e38..4a351d005 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartComputedFieldSettingPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartComputedFieldSettingPanel.tsx @@ -25,7 +25,7 @@ import { ChartDataViewFieldType, } from 'app/types/ChartDataView'; import { ChartDataViewMeta } from 'app/types/ChartDataViewMeta'; -import { ChartCompoutedFieldHandle } from 'app/types/CompoutedFieldEditor'; +import { ChartComputedFieldHandle } from 'app/types/ComputedFieldEditor'; import { FC, useRef, useState } from 'react'; import styled from 'styled-components/macro'; import ChartComputedFieldEditor from './ChartComputedFieldEditor/ChartComputedFieldEditor'; @@ -61,7 +61,7 @@ const ChartComputedFieldSettingPanel: FC<{ }) => { const t = useI18NPrefix(`viz.workbench.dataview`); const defaultFunctionCategory = 'all'; - const editorRef = useRef(null); + const editorRef = useRef(null); const myComputedFieldRef = useRef(computedField); const [selectedFunctionCategory, setSelectedFunctionCategory] = useState( defaultFunctionCategory, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartSearchableList.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartSearchableList.tsx index b686793cb..04201c0d1 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartSearchableList.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataViewPanel/components/ChartSearchableList.tsx @@ -26,13 +26,13 @@ const ChartSearchableList: FC<{ source: Array<{ value: string; label: string }>; onItemSelected: (itemKey) => void; }> = memo(({ source, onItemSelected }) => { - const [listItems, setListItmes] = useState(source); + const [listItems, setListItems] = useState(source); const [searchValue, setSearchValue] = useState(); const t = useI18NPrefix(`viz.workbench.dataview`); useEffect(() => { setSearchValue(''); - setListItmes(source); + setListItems(source); }, [source]); const handleListItemClick = itemKey => { @@ -42,12 +42,12 @@ const ChartSearchableList: FC<{ const handleSearch = debounce((value: string) => { setSearchValue(value); if (!value || !value.trim()) { - setListItmes(source); + setListItems(source); } const newListItems = source?.filter(item => item?.label.toUpperCase().includes(value.toUpperCase()), ); - setListItmes(newListItems); + setListItems(newListItems); }, 100); return ( diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableElement.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableElement.tsx index dc224b288..45d35f197 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableElement.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableElement.tsx @@ -220,6 +220,7 @@ const Content = styled.div` .title { flex: 1; + color: ${p => p.theme.white}; } .action { diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableSourceContainer.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableSourceContainer.tsx index ffd044076..c23b91c19 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableSourceContainer.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableSourceContainer.tsx @@ -42,11 +42,11 @@ import { FONT_WEIGHT_MEDIUM, INFO, SPACE, - SPACE_SM, SPACE_TIMES, SUCCESS, WARNING, } from 'styles/StyleConstants'; +import { stopPPG } from 'utils/utils'; export const ChartDraggableSourceContainer: FC< { @@ -159,18 +159,20 @@ export const ChartDraggableSourceContainer: FC< <> {icon}

    {colName}

    - - } - iconSize={FONT_SIZE_BASE} - className="setting" - onClick={e => e.preventDefault()} - /> - +
    + + } + iconSize={FONT_SIZE_BASE} + className="setting" + onClick={e => e.preventDefault()} + /> + +
    ); }, [type, colName, onDeleteComputedField, onEditComputedField, category, t]); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableSourceGroupContainer.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableSourceGroupContainer.tsx index 91af372f6..06b385691 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableSourceGroupContainer.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableSourceGroupContainer.tsx @@ -18,7 +18,7 @@ import { List } from 'antd'; import { ChartDataViewMeta } from 'app/types/ChartDataViewMeta'; -import { FC, memo, useState } from 'react'; +import { FC, memo, useCallback, useState } from 'react'; import styled from 'styled-components/macro'; import { stopPPG } from 'utils/utils'; import { ChartDraggableSourceContainer } from './ChartDraggableSourceContainer'; @@ -34,7 +34,7 @@ export const ChartDraggableSourceGroupContainer: FC<{ onEditComputedField, }) { const [selectedItems, setSelectedItems] = useState([]); - const [selectedItemsIds, setselectedItemsIds] = useState>([]); + const [selectedItemsIds, setSelectedItemsIds] = useState>([]); const [activeItemId, setActiveItemId] = useState(''); const onDataItemSelectionChange = ( @@ -78,19 +78,27 @@ export const ChartDraggableSourceGroupContainer: FC<{ interimSelectedItemsIds.includes(c.id), ); - setselectedItemsIds(interimSelectedItemsIds); + setSelectedItemsIds(interimSelectedItemsIds); setActiveItemId(interimActiveItemId); setSelectedItems(selectedCards); }; const onClearCheckedList = () => { if (selectedItems?.length > 0) { - setselectedItemsIds([]); + setSelectedItemsIds([]); setActiveItemId(''); setSelectedItems([]); } }; + const handleEditComputedField = useCallback( + fieldName => { + onEditComputedField(fieldName); + setSelectedItems([]); + }, + [onEditComputedField], + ); + return ( {/* 拖动层组件 */} @@ -108,7 +116,7 @@ export const ChartDraggableSourceGroupContainer: FC<{ expression={item.expression} type={item.type} onDeleteComputedField={onDeleteComputedField} - onEditComputedField={onEditComputedField} + onEditComputedField={handleEditComputedField} onSelectionChange={onDataItemSelectionChange} onClearCheckedList={onClearCheckedList} isActive={selectedItemsIds.includes(item.id)} diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableTargetContainer.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableTargetContainer.tsx index f86675a10..1b0f2bbae 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableTargetContainer.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableTargetContainer.tsx @@ -113,6 +113,7 @@ export const ChartDraggableTargetContainer: FC = currentConfig?.rows || [], draft => { draft.splice(originItemIndex, 1); + item.aggregate = getDefaultAggregate(item); return draft.splice(item?.index!, 0, item); }, ); @@ -121,6 +122,7 @@ export const ChartDraggableTargetContainer: FC = const currentColumns = updateBy( currentConfig?.rows || [], draft => { + item.aggregate = getDefaultAggregate(item); return draft.splice(item?.index!, 0, item); }, ); @@ -297,9 +299,9 @@ export const ChartDraggableTargetContainer: FC = )} - {aggregation - ? getColumnRenderName(columnConfig) - : columnConfig.colName} + {aggregation === false + ? columnConfig.colName + : getColumnRenderName(columnConfig)}
    {enableActionsIcons(columnConfig)} diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AggregationColorizeAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AggregationColorizeAction.tsx index c22a55fbb..253d33198 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AggregationColorizeAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AggregationColorizeAction.tsx @@ -120,7 +120,7 @@ const AggregationColorizeAction: FC<{ visible={selColorBoxStatus} trigger="click" placement="bottomRight" - overlayClassName="aggregation-colorpopover" + overlayClassName="datart-aggregation-colorpopover" content={ void; }> = memo(({ config, onConfigChange }) => { @@ -40,12 +40,12 @@ const ArrangeFilterAcion: FC<{ }); const handleRelationChange = relation => { - const relationConddition = new ConditionBuilder(relation).asRelation( + const relationCondition = new ConditionBuilder(relation).asRelation( relation.value, relation.children, ); - setRelation(relationConddition); - config.fieldRelation = relationConddition; + setRelation(relationCondition); + config.fieldRelation = relationCondition; onConfigChange(config); }; @@ -58,4 +58,4 @@ const ArrangeFilterAcion: FC<{ ); }); -export default ArrangeFilterAcion; +export default ArrangeFilterAction; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterAction/FilterAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterAction/FilterAction.tsx index 2a3ca264a..d039f17b3 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterAction/FilterAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterAction/FilterAction.tsx @@ -20,7 +20,7 @@ import { ChartDataConfig, ChartDataSectionField } from 'app/types/ChartConfig'; import ChartDataSetDTO from 'app/types/ChartDataSet'; import ChartDataView from 'app/types/ChartDataView'; import { FC, memo } from 'react'; -import FilterControllPanel from '../FilterControlPanel'; +import FilterControlPanel from '../FilterControlPanel'; const FilterAction: FC<{ config: ChartDataSectionField; @@ -35,11 +35,11 @@ const FilterAction: FC<{ }> = memo( ({ config, dataset, dataView, dataConfig, onConfigChange, aggregation }) => { const handleFetchDataFromField = async fieldId => { - // TODO: tobe implement to get fields + // TODO: to be implement to get fields return await Promise.resolve(['a', 'b', 'c'].map(f => `${fieldId}-${f}`)); }; return ( - void; onDeleteSelfFilter?: () => void; - onConditionChange: (condtion: ChartFilterCondition) => void; + onConditionChange: (condition: ChartFilterCondition) => void; }> = memo( ({ rowName, @@ -41,6 +41,7 @@ const SingleFilter: FC<{ const t = useI18NPrefix('viz.common.enum.filterOperator'); const operators = [ FilterSqlOperator.Between, + FilterSqlOperator.NotBetween, FilterSqlOperator.Equal, FilterSqlOperator.NotEqual, FilterSqlOperator.GreaterThanOrEqual, @@ -76,7 +77,10 @@ const SingleFilter: FC<{ if (op === FilterSqlOperator.Null || op === FilterSqlOperator.NotNull) { return null; } - if (op === FilterSqlOperator.Between) { + if ( + op === FilterSqlOperator.Between || + op === FilterSqlOperator.NotBetween + ) { return ( <> option.label.includes(inputValue), + (inputValue, option) => option.label?.includes(inputValue) || false, [], ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionEditableTable.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionEditableTable.tsx index 45d2841d5..1076d612e 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionEditableTable.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionEditableTable.tsx @@ -150,7 +150,7 @@ const CategoryConditionEditableTable: FC< const handleRowStateUpdate = (row: RelationFilterValue) => { const newRows = [...rows]; - const targetIndex = newRows.findIndex(r => r.index === row.index); + const targetIndex = newRows.findIndex(r => r.key === row.key); newRows.splice(targetIndex, 1, row); handleFilterConditionChange(newRows); }; @@ -188,8 +188,7 @@ const CategoryConditionEditableTable: FC< const convertToList = (collection, selectedKeys) => { const items: string[] = (collection || []).flatMap(c => c); const uniqueKeys = Array.from(new Set(items)); - return uniqueKeys.map((item, index) => ({ - index: index, + return uniqueKeys.map(item => ({ key: item, label: item, isSelected: selectedKeys.includes(item), diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/DateConditionConfiguration.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/DateConditionConfiguration.tsx index fd4aded63..caf23d3a0 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/DateConditionConfiguration.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/DateConditionConfiguration.tsx @@ -32,7 +32,7 @@ import TimeSelector from '../../ChartTimeSelector'; const DateConditionConfiguration: FC< { condition?: ChartFilterCondition; - onChange: (confconditionig: ChartFilterCondition) => void; + onChange: (condition: ChartFilterCondition) => void; } & I18NComponentProps > = memo(({ i18nPrefix, condition, onChange: onConditionChange }) => { const t = useI18NPrefix(i18nPrefix); @@ -78,7 +78,7 @@ const DateConditionConfiguration: FC< /> - .ant-row { diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterFacadeConfiguration.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterFacadeConfiguration.tsx index 67dac5932..ca15787b8 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterFacadeConfiguration.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterFacadeConfiguration.tsx @@ -30,14 +30,14 @@ import styled from 'styled-components/macro'; import { IsKeyIn } from 'utils/object'; const isDisableSingleDropdownListFacade = condition => { - let isDisableSignleDropdownList = true; + let isDisableSingleDropdownList = true; if (Array.isArray(condition?.value)) { if (IsKeyIn(condition?.value?.[0], 'key')) { - isDisableSignleDropdownList = + isDisableSingleDropdownList = condition?.value?.filter(n => n.isSelected)?.length > 1; } } - return isDisableSignleDropdownList; + return isDisableSingleDropdownList; }; const getFacadeOptions = (condition, category) => { diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/index.ts index 60b77f07d..8535e25ac 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/index.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/index.ts @@ -16,8 +16,8 @@ * limitations under the License. */ import CategoryConditionRelationSelector from './CategoryConditionRelationSelector'; -import FilterControllPanel from './FilterControlPanel'; +import FilterControlPanel from './FilterControlPanel'; export { CategoryConditionRelationSelector }; -export default FilterControllPanel; +export default FilterControlPanel; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/SortAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/SortAction.tsx index 672e2af72..bd57b0878 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/SortAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/SortAction.tsx @@ -42,7 +42,9 @@ const SortAction: FC<{ ? true : Boolean(options?.backendSort); const t = useI18NPrefix(`viz.palette.data.actions`); - const [direction, setDirection] = useState(config?.sort?.type); + const [direction, setDirection] = useState( + config?.sort?.type || SortActionType.NONE, + ); const [sortValue, setSortValue] = useState(() => { const objDataColumns = transformToDataSet(dataset?.rows, dataset?.columns); return ( diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphIcon.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphIcon.tsx index 083d6d6ea..db4a00bf7 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphIcon.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphIcon.tsx @@ -66,7 +66,7 @@ const ChartGraphIcon: FC<{ return ; }; - const renderChartRequirments = requirements => { + const renderChartRequirements = requirements => { const lintMessages = requirements?.flatMap((requirement, index) => { return [ChartDataSectionType.GROUP, ChartDataSectionType.AGGREGATE].map( type => { @@ -101,8 +101,8 @@ const ChartGraphIcon: FC<{ key={chart?.meta?.id} title={ <> - {t(chart?.meta?.name, true)} - {renderChartRequirments(chart?.meta?.requirements)} + {t(chart?.meta?.name!, true)} + {renderChartRequirements(chart?.meta?.requirements)} } > diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/ChartPresentPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/ChartPresentPanel.tsx index 4fa6891e7..9fa5da857 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/ChartPresentPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/ChartPresentPanel.tsx @@ -16,6 +16,7 @@ * limitations under the License. */ +import { ReloadOutlined } from '@ant-design/icons'; import { Table } from 'antd'; import { ChartIFrameContainerDispatcher } from 'app/components/ChartIFrameContainer'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; @@ -24,9 +25,16 @@ import { IChart } from 'app/types/Chart'; import { ChartConfig } from 'app/types/ChartConfig'; import ChartDataSetDTO from 'app/types/ChartDataSet'; import { FC, memo, useState } from 'react'; +import { useSelector } from 'react-redux'; import styled from 'styled-components/macro'; -import { BORDER_RADIUS, SPACE_LG, SPACE_MD } from 'styles/StyleConstants'; +import { + BORDER_RADIUS, + LINE_HEIGHT_ICON_XXL, + SPACE_LG, + SPACE_MD, +} from 'styles/StyleConstants'; import { Debugger } from 'utils/debugger'; +import { datasetLoadingSelector } from '../../../../slice/workbenchSlice'; import Chart404Graph from './components/Chart404Graph'; import ChartTypeSelector, { ChartPresentType, @@ -40,11 +48,26 @@ const ChartPresentPanel: FC<{ chart?: IChart; dataset?: ChartDataSetDTO; chartConfig?: ChartConfig; + expensiveQuery: boolean; + allowQuery: boolean; + onRefreshDataset?: () => void; + onCreateDownloadDataTask?: () => void; }> = memo( - ({ containerHeight, containerWidth, chart, dataset, chartConfig }) => { + ({ + containerHeight, + containerWidth, + chart, + dataset, + chartConfig, + expensiveQuery, + allowQuery, + onRefreshDataset, + onCreateDownloadDataTask, + }) => { const translate = useI18NPrefix(`viz.palette.present`); const chartDispatcher = ChartIFrameContainerDispatcher.instance(); const [chartType, setChartType] = useState(ChartPresentType.GRAPH); + const datasetLoadingStatus = useSelector(datasetLoadingSelector); useMount(undefined, () => { Debugger.instance.measure(`ChartPresentPanel | Dispose Event`, () => { @@ -114,14 +137,25 @@ const ChartPresentPanel: FC<{ return ( ); }; return ( + {expensiveQuery && allowQuery && ( + + + + )} + {renderChartTypeSelector()} {renderReusableChartContainer()} @@ -137,6 +171,7 @@ const StyledChartPresentPanel = styled.div` flex-direction: column; background-color: ${p => p.theme.componentBackground}; border-radius: ${BORDER_RADIUS}; + position: relative; `; const TableWrapper = styled.div` @@ -150,8 +185,25 @@ const SqlWrapper = styled.div` overflow-y: auto; background-color: ${p => p.theme.emphasisBackground}; border-radius: ${BORDER_RADIUS}; - > code { color: ${p => p.theme.textColorSnd}; } `; + +const ReloadMask = styled.div` + position: absolute; + top: 0; + left: 0; + z-index: 10; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.9); + .fetchDataIcon { + cursor: pointer; + color: ${p => p.theme.primary}; + font-size: ${LINE_HEIGHT_ICON_XXL}; + } +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/components/Chart404Graph.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/components/Chart404Graph.tsx index 92f125700..e6ae6470a 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/components/Chart404Graph.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/components/Chart404Graph.tsx @@ -30,7 +30,7 @@ const Chart404Graph: FC<{ }> = memo(({ chart, chartConfig }) => { const t = useI18NPrefix(`viz.palette`); - const renderChartLimition = () => { + const renderChartLimitation = () => { const sections = chartConfig?.datas ?.filter(s => reachLowerBoundCount(s?.limit, s.rows?.length) > 0) .map(s => { @@ -51,7 +51,7 @@ const Chart404Graph: FC<{ - {renderChartLimition()} + {renderChartLimitation()} ); }); @@ -61,8 +61,8 @@ export default Chart404Graph; const StyledChart404Graph = styled.div` display: flex; flex-flow: column; - justify-content: center; align-items: center; + justify-content: center; height: 100%; opacity: 0.3; `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/components/ChartTypeSelector.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/components/ChartTypeSelector.tsx index 975b7a7e5..d673829e0 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/components/ChartTypeSelector.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentPanel/components/ChartTypeSelector.tsx @@ -18,10 +18,13 @@ import { AreaChartOutlined, + CloudDownloadOutlined, ConsoleSqlOutlined, TableOutlined, } from '@ant-design/icons'; +import { Popconfirm } from 'antd'; import { IW } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import classnames from 'classnames'; import { FC, memo, useCallback } from 'react'; import styled from 'styled-components/macro'; @@ -31,46 +34,68 @@ export enum ChartPresentType { GRAPH = 'graph', RAW = 'raw', SQL = 'sql', + DOWNLOAD = 'download', } const ChartTypeSelector: FC<{ type; - onChange: (value) => void; translate: (title: string) => string; -}> = memo(({ type, onChange, translate = title => title }) => { - const typeChange = useCallback( - type => () => { - onChange(type); - }, - [onChange], - ); + onChange: (value) => void; + onCreateDownloadDataTask?: () => void; +}> = memo( + ({ + type, + onChange, + translate = title => title, + onCreateDownloadDataTask, + }) => { + const t = useI18NPrefix(`viz.action.common`); + const typeChange = useCallback( + type => () => { + onChange(type); + }, + [onChange], + ); - return ( - - - - - - - - - - - - ); -}); + return ( + + + + + + + + + + + + + + + + + ); + }, +); export default ChartTypeSelector; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentWrapper.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentWrapper.tsx index 63a2faabc..158b0c5f0 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentWrapper.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentWrapper.tsx @@ -33,15 +33,23 @@ const ChartPresentWrapper: FC<{ chart?: IChart; dataset?: ChartDataSetDTO; chartConfig?: ChartConfig; + expensiveQuery: boolean; + allowQuery: boolean; onChartChange: (c: IChart) => void; + onRefreshDataset?: () => void; + onCreateDownloadDataTask?: () => void; }> = memo( ({ containerHeight, containerWidth, chart, dataset, + expensiveQuery, chartConfig, + allowQuery, onChartChange, + onRefreshDataset, + onCreateDownloadDataTask, }) => { const { ref: ChartGraphPanelRef } = useResizeObserver({ refreshMode: 'debounce', @@ -71,7 +79,11 @@ const ChartPresentWrapper: FC<{ containerWidth={(containerWidth || 0) - borderWidth} chart={chart} dataset={dataset} + expensiveQuery={expensiveQuery} + allowQuery={allowQuery} chartConfig={chartConfig} + onRefreshDataset={onRefreshDataset} + onCreateDownloadDataTask={onCreateDownloadDataTask} /> diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/MannualRangeTimeSelector.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/ManualRangeTimeSelector.tsx similarity index 97% rename from frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/MannualRangeTimeSelector.tsx rename to frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/ManualRangeTimeSelector.tsx index d9e9b7210..af199760a 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/MannualRangeTimeSelector.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/ManualRangeTimeSelector.tsx @@ -28,7 +28,7 @@ import ChartFilterCondition, { import CurrentRangeTime from './CurrentRangeTime'; import ManualSingleTimeSelector from './ManualSingleTimeSelector'; -const MannualRangeTimeSelector: FC< +const ManualRangeTimeSelector: FC< { condition?: FilterCondition; onConditionChange: (filter: ChartFilterCondition) => void; @@ -89,4 +89,4 @@ const MannualRangeTimeSelector: FC< ); }); -export default MannualRangeTimeSelector; +export default ManualRangeTimeSelector; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/index.ts index 58212ab6a..9aaa5903c 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/index.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/index.ts @@ -17,12 +17,12 @@ */ import CurrentRangeTime from './CurrentRangeTime'; -import MannualRangeTimeSelector from './MannualRangeTimeSelector'; +import ManualRangeTimeSelector from './ManualRangeTimeSelector'; import ManualSingleTimeSelector from './ManualSingleTimeSelector'; import RecommendRangeTimeSelector from './RecommendRangeTimeSelector'; const TimeSelector = { - MannualRangeTimeSelector, + ManualRangeTimeSelector, ManualSingleTimeSelector, RecommendRangeTimeSelector, CurrentRangeTime, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartToolbar/Aggregation.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartToolbar/Aggregation.tsx index 6278b0ff8..a2549ed02 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartToolbar/Aggregation.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartToolbar/Aggregation.tsx @@ -54,4 +54,6 @@ const AggregationOperationMenu: FC<{ export default AggregationOperationMenu; -const Aggregation = styled.div``; +const Aggregation = styled.div` + color: ${p => p.theme.textColor}; +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx index 81256920e..c13be97f8 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx @@ -41,18 +41,22 @@ const ChartWorkbench: FC<{ chart?: IChart; aggregation?: boolean; defaultViewId?: string; + expensiveQuery: boolean; + allowQuery: boolean; header?: { name?: string; orgId?: string; container?: string; onSaveChart?: () => void; - onSaveChartToDashBoard?: (dashboardId) => void; + onSaveChartToDashBoard?: (dashboardId, dashboardType) => void; onGoBack?: () => void; onChangeAggregation?: () => void; }; onChartChange: (c: IChart) => void; onChartConfigChange: (type, payload) => void; onDataViewChange?: () => void; + onRefreshDataset?: () => void; + onCreateDownloadDataTask?: () => void; }> = memo( ({ dataset, @@ -62,9 +66,13 @@ const ChartWorkbench: FC<{ aggregation, header, defaultViewId, + expensiveQuery, + allowQuery, onChartChange, onChartConfigChange, onDataViewChange, + onRefreshDataset, + onCreateDownloadDataTask, }) => { const language = useSelector(languageSelector); const dateFormat = useSelector(dateFormatSelector); @@ -75,8 +83,15 @@ const ChartWorkbench: FC<{ onChangeAggregation: header?.onChangeAggregation, }} > - - + + @@ -96,9 +111,11 @@ const ChartWorkbench: FC<{ chart={chart} defaultViewId={defaultViewId} chartConfig={chartConfig} + allowQuery={allowQuery} onChartChange={onChartChange} onChartConfigChange={onChartConfigChange} onDataViewChange={onDataViewChange} + onCreateDownloadDataTask={onCreateDownloadDataTask} /> @@ -124,10 +141,11 @@ const StyledChartWorkbench = styled.div` flex: 1; flex-flow: column; overflow: hidden; - background-color: ${p => p.theme.componentBackground}; + background-color: ${p => p.theme.bodyBackground}; .flexlayout__tab { overflow: hidden; + background-color: ${p => p.theme.bodyBackground}; } .flexlayout__splitter { diff --git a/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartDataViewContext.ts b/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartDataViewContext.ts index 90e304fc9..3687673b9 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartDataViewContext.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartDataViewContext.ts @@ -19,8 +19,12 @@ import ChartDataView from 'app/types/ChartDataView'; import { createContext } from 'react'; -const VizDataViewContext = createContext<{ dataView?: ChartDataView }>({ +const ChartDataViewContext = createContext<{ + dataView?: ChartDataView; + expensiveQuery: boolean; +}>({ dataView: {} as ChartDataView, + expensiveQuery: false, }); -export default VizDataViewContext; +export default ChartDataViewContext; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartDatasetContext.ts b/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartDatasetContext.ts index 227ccd719..ecbcd6bcd 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartDatasetContext.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartDatasetContext.ts @@ -19,8 +19,12 @@ import ChartDataSetDTO from 'app/types/ChartDataSet'; import { createContext } from 'react'; -const ChartDatasetContext = createContext<{ dataset?: ChartDataSetDTO }>({ +const ChartDatasetContext = createContext<{ + dataset?: ChartDataSetDTO; + onRefreshDataset?: () => void; +}>({ dataset: {}, + onRefreshDataset: undefined, }); export default ChartDatasetContext; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts index d62ead2e4..8cd7937ec 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts @@ -164,7 +164,7 @@ export class ConditionBuilder { asRecommendTime(name?, sqlType?) { this.condition.type = FilterConditionType.RecommendTime; - this.condition.operator = FilterSqlOperator.Equal; + this.condition.operator = FilterSqlOperator.Between; this.condition.name = name || this.condition.name; this.condition.visualType = sqlType || this.condition.visualType; return this.condition; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts index 3b915443f..9f35be995 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts @@ -35,6 +35,7 @@ import { PivotSheetChart, RoseChart, ScatterOutlineMapChart, + Scorecard, ScoreChart, StackAreaChart, StackBarChart, @@ -75,6 +76,13 @@ class ChartManager { return this._charts || []; } + public getAllChartIcons() { + return this._charts.reduce((acc, cur) => { + acc[cur.meta.id] = cur.meta.icon; + return acc; + }, {}); + } + public getById(id?: string) { if (id === null || id === undefined) { return; @@ -83,7 +91,7 @@ class ChartManager { } public getDefaultChart() { - return this._charts[0]; + return CloneValueDeep(this._charts[0]); } private async _loadCustomizeCharts(paths: string[]) { @@ -104,6 +112,7 @@ class ChartManager { new MingXiTableChart(), new PivotSheetChart(), new ScoreChart(), + new Scorecard(), new ClusterColumnChart(), new ClusterBarChart(), new StackColumnChart(), diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/PluginChartLoader.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/PluginChartLoader.ts index 3bf77a4f4..84666250d 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/PluginChartLoader.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/PluginChartLoader.ts @@ -19,7 +19,6 @@ import Chart from 'app/components/ChartGraph/models/Chart'; import * as datartChartHelper from 'app/utils/chartHelper'; import { fetchPluginChart } from 'app/utils/fetch'; -import * as datartChartNumber from 'app/utils/number'; import { cond, Omit } from 'utils/object'; const pureFuncLoader = ({ path, result }) => { @@ -49,7 +48,7 @@ class PluginChartLoader { return Promise.resolve(result); } - /* Known Issue: file path only allow in src folder by create-react-app file scope limition by CRA + /* Known Issue: file path only allow in src folder by create-react-app file scope limitation by CRA * Git Issue: https://github.com/facebook/create-react-app/issues/5563 * Suggestions: Use es6 `import` api to load file and compatible with ES Modules */ diff --git a/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts b/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts index 39873f046..95f1757ca 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts @@ -23,6 +23,7 @@ import { PayloadAction, } from '@reduxjs/toolkit'; import { migrateChartConfig } from 'app/migration'; +import { migrateViewConfig } from 'app/migration/ViewConfig/migrationViewDetailConfig'; import ChartManager from 'app/pages/ChartWorkbenchPage/models/ChartManager'; import { ResourceTypes } from 'app/pages/MainPage/pages/PermissionPage/constants'; import { ChartConfig } from 'app/types/ChartConfig'; @@ -76,6 +77,8 @@ export type WorkbenchState = { backendChart?: ChartDTO; backendChartId?: string; aggregation?: boolean; + datasetLoading: boolean; + chartEditorDownloadPolling: boolean; }; const initState: WorkbenchState = { @@ -83,6 +86,9 @@ const initState: WorkbenchState = { dateFormat: 'LLL', dataviews: [], dataset: {}, + aggregation: true, + datasetLoading: false, + chartEditorDownloadPolling: false, }; // Selectors @@ -134,6 +140,15 @@ export const aggregationSelector = createSelector( wb => wb.aggregation, ); +export const datasetLoadingSelector = createSelector( + workbenchSelector, + wb => wb.datasetLoading, +); + +export const selectChartEditorDownloadPolling = createSelector( + workbenchSelector, + wb => wb.chartEditorDownloadPolling, +); // Effects export const initWorkbenchAction = createAsyncThunk( 'workbench/initWorkbenchAction', @@ -257,7 +272,7 @@ export const refreshDatasetAction = createAsyncThunk( const requestParams = builder .addExtraSorters(arg?.sorter ? [arg?.sorter as any] : []) .build(); - thunkAPI.dispatch(fetchDataSetAction(requestParams)); + return thunkAPI.dispatch(fetchDataSetAction(requestParams)); }, ); @@ -434,6 +449,9 @@ const workbenchSlice = createSlice({ resetWorkbenchState: (state, action) => { return initState; }, + setChartEditorDownloadPolling(state, { payload }: PayloadAction) { + state.chartEditorDownloadPolling = payload; + }, }, extraReducers: builder => { builder @@ -452,6 +470,7 @@ const workbenchSlice = createSlice({ if (index !== undefined) { state.currentDataView = { ...payload, + config: migrateViewConfig(payload.config), meta: transformMeta(payload.model), computedFields, }; @@ -460,6 +479,7 @@ const workbenchSlice = createSlice({ }) .addCase(fetchDataSetAction.fulfilled, (state, { payload }) => { state.dataset = payload as any; + state.datasetLoading = false; }) .addCase(fetchChartAction.fulfilled, (state, { payload }) => { if (!payload) { @@ -487,11 +507,20 @@ const workbenchSlice = createSlice({ chartConfigDTO.aggregation === undefined ? true : chartConfigDTO.aggregation; - }) - .addMatcher( - isMySliceRejectedAction(workbenchSlice.name), - rejectedActionMessageHandler, - ); + }); + + builder.addCase(fetchDataSetAction.pending, (state, action) => { + state.datasetLoading = true; + }); + + builder.addCase(fetchDataSetAction.rejected, (state, action) => { + state.datasetLoading = false; + }); + + builder.addMatcher( + isMySliceRejectedAction(workbenchSlice.name), + rejectedActionMessageHandler, + ); }, }); diff --git a/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx b/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx index f75e191e7..f7359d9fb 100644 --- a/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx @@ -26,7 +26,8 @@ import { import { Menu, Popconfirm } from 'antd'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { memo, useContext, useMemo } from 'react'; -import { BoardContext } from '../contexts/BoardContext'; +import { BoardContext } from './BoardProvider/BoardProvider'; + export interface BoardOverLayProps { onOpenShareLink?: () => void; onBoardToDownLoad: () => void; diff --git a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx index 645240bae..564914b20 100644 --- a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx @@ -20,14 +20,8 @@ import { PageInfo } from 'app/pages/MainPage/pages/ViewPage/slice/types'; import { useSaveAsViz } from 'app/pages/MainPage/pages/VizPage/hooks/useSaveAsViz'; import { generateShareLinkAsync } from 'app/utils/fetch'; import debounce from 'lodash/debounce'; -import React, { FC, useContext } from 'react'; +import React, { createContext, FC, useContext } from 'react'; import { useDispatch } from 'react-redux'; -import { - BoardActionContext, - BoardActionContextProps, -} from '../../contexts/BoardActionContext'; -import { BoardConfigContext } from '../../contexts/BoardConfigContext'; -import { BoardContext } from '../../contexts/BoardContext'; import { boardActions } from '../../pages/Board/slice'; import { boardDownLoadAction, @@ -40,6 +34,7 @@ import { } from '../../pages/Board/slice/thunk'; import { Widget } from '../../pages/Board/slice/types'; import { editBoardStackActions } from '../../pages/BoardEditor/slice'; +import { clearActiveWidgets } from '../../pages/BoardEditor/slice/actions/actions'; import { editWidgetsQueryAction } from '../../pages/BoardEditor/slice/actions/controlActions'; import { getEditChartWidgetDataAsync, @@ -50,16 +45,33 @@ import { getCascadeControllers, getNeedRefreshWidgetsByController, } from '../../utils/widget'; +import { BoardConfigContext } from './BoardConfigProvider'; +import { BoardContext } from './BoardProvider'; +export interface BoardActionContextProps { + widgetUpdate: (widget: Widget) => void; + refreshWidgetsByController: (widget: Widget) => void; + updateBoard?: (callback: () => void) => void; + onGenerateShareLink?: (date, usePwd) => any; + onBoardToDownLoad: () => any; + onWidgetsQuery: () => any; + onWidgetsReset: () => any; + onSaveAsVizs: () => any; + boardToggleAllowOverlap: (allow: boolean) => void; + onClearActiveWidgets: () => void; +} +export const BoardActionContext = createContext( + {} as BoardActionContextProps, +); export const BoardActionProvider: FC<{ id: string }> = ({ id: boardId, children, }) => { const dispatch = useDispatch(); const { editing, renderMode } = useContext(BoardContext); - const { config: boardConfig } = useContext(BoardConfigContext); + const { hasQueryControl } = useContext(BoardConfigContext); const saveAsViz = useSaveAsViz(); - const { hasQueryControl } = boardConfig; + const actions: BoardActionContextProps = { widgetUpdate: (widget: Widget) => { if (editing) { @@ -68,7 +80,9 @@ export const BoardActionProvider: FC<{ id: string }> = ({ dispatch(boardActions.updateWidget(widget)); } }, - + boardToggleAllowOverlap: (allow: boolean) => { + dispatch(editBoardStackActions.toggleAllowOverlap(allow)); + }, onWidgetsQuery: debounce(() => { if (editing) { dispatch(editWidgetsQueryAction({ boardId })); @@ -147,6 +161,9 @@ export const BoardActionProvider: FC<{ id: string }> = ({ onSaveAsVizs: () => { saveAsViz(boardId, 'DASHBOARD'); }, + onClearActiveWidgets: () => { + dispatch(clearActiveWidgets()); + }, }; return ( diff --git a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardConfigProvider.tsx b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardConfigProvider.tsx index 31afe70ca..83599ef1e 100644 --- a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardConfigProvider.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardConfigProvider.tsx @@ -16,21 +16,17 @@ * limitations under the License. */ -import React, { FC, memo } from 'react'; -import { - BoardConfigContext, - BoardConfigContextProps, -} from '../../contexts/BoardConfigContext'; +import React, { createContext, FC, memo } from 'react'; import { DashboardConfig } from '../../pages/Board/slice/types'; +export const BoardConfigContext = createContext( + {} as DashboardConfig, +); + export const BoardConfigProvider: FC<{ config: DashboardConfig }> = memo( ({ config, children }) => { - const boardConfigValue: BoardConfigContextProps = { - config: config, - }; - return ( - + {children} ); diff --git a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardInfoProvider.tsx b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardInfoProvider.tsx index 20f8075b3..154248626 100644 --- a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardInfoProvider.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardInfoProvider.tsx @@ -16,13 +16,13 @@ * limitations under the License. */ -import React, { FC, memo } from 'react'; +import React, { createContext, FC, memo } from 'react'; import { useSelector } from 'react-redux'; -import { BoardInfoContext } from '../../contexts/BoardInfoContext'; import { selectBoardInfoById } from '../../pages/Board/slice/selector'; -import { BoardState } from '../../pages/Board/slice/types'; +import { BoardInfo, BoardState } from '../../pages/Board/slice/types'; import { boardInfoState } from '../../pages/BoardEditor/slice/selectors'; +export const BoardInfoContext = createContext({} as BoardInfo); export const BoardInfoProvider: FC<{ id: string; editing: boolean }> = memo( ({ id, editing, children }) => { const editBoardInfo = useSelector(boardInfoState); diff --git a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardProvider.tsx b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardProvider.tsx index 8faf9a527..c643786fb 100644 --- a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardProvider.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardProvider.tsx @@ -17,17 +17,42 @@ */ import produce from 'immer'; -import React, { FC, memo, useCallback, useMemo } from 'react'; +import React, { createContext, FC, memo, useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; -import { BoardContext, BoardContextProps } from '../../contexts/BoardContext'; import { renderedWidgetAsync } from '../../pages/Board/slice/thunk'; -import { Dashboard, VizRenderMode } from '../../pages/Board/slice/types'; +import { + BoardType, + Dashboard, + VizRenderMode, +} from '../../pages/Board/slice/types'; import { renderedEditWidgetAsync } from '../../pages/BoardEditor/slice/thunk'; import { adaptBoardImageUrl } from '../../utils'; import { BoardActionProvider } from './BoardActionProvider'; import { BoardConfigProvider } from './BoardConfigProvider'; import { BoardInfoProvider } from './BoardInfoProvider'; +export interface BoardContextProps { + name: string; + renderMode?: VizRenderMode; + boardId: string; + orgId: string; + boardType: BoardType; + status: number; + editing: boolean; + thumbnail?: string; + autoFit?: boolean | undefined; + hideTitle?: boolean | undefined; + allowDownload?: boolean; + allowShare?: boolean; + allowManage?: boolean; + queryVariables: Dashboard['queryVariables']; + // methods + renderedWidgetById: (wid: string) => void; +} + +export const BoardContext = createContext( + {} as BoardContextProps, +); export const BoardProvider: FC<{ board: Dashboard; renderMode: VizRenderMode; diff --git a/frontend/src/app/pages/DashBoardPage/components/FreeBoardBackground.tsx b/frontend/src/app/pages/DashBoardPage/components/FreeBoardBackground.tsx index e509eb398..157bd5d8a 100644 --- a/frontend/src/app/pages/DashBoardPage/components/FreeBoardBackground.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/FreeBoardBackground.tsx @@ -15,11 +15,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BoardConfigContext } from 'app/pages/DashBoardPage/contexts/BoardConfigContext'; + import React, { createContext, useContext, useMemo } from 'react'; import styled from 'styled-components/macro'; -import { BoardContext } from '../contexts/BoardContext'; import StyledBackground from '../pages/Board/components/StyledBackground'; +import { BoardConfigContext } from './BoardProvider/BoardConfigProvider'; +import { BoardContext } from './BoardProvider/BoardProvider'; + export const scaleContext = createContext<[number, number]>([1, 1]); export interface IProps { scale: [number, number]; @@ -27,7 +29,10 @@ export interface IProps { } const SlideBackground: React.FC = props => { const { - config: { width: slideWidth, height: slideHeight, scaleMode, background }, + width: slideWidth, + height: slideHeight, + scaleMode, + background, } = useContext(BoardConfigContext); const { editing } = useContext(BoardContext); const { scale, slideTranslate } = props; @@ -70,6 +75,5 @@ export default SlideBackground; const Warp = styled(StyledBackground)<{ editing: boolean }>` position: relative; box-shadow: ${p => (p.editing ? '0px 1px 8px 2px #8cb4be;' : '')}; - transition: all ease-in 200ms; transform-origin: 0 0; `; diff --git a/frontend/src/app/pages/DashBoardPage/components/FullScreenPanel.tsx b/frontend/src/app/pages/DashBoardPage/components/FullScreenPanel.tsx index 3a1f3b237..26f7c5a13 100644 --- a/frontend/src/app/pages/DashBoardPage/components/FullScreenPanel.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/FullScreenPanel.tsx @@ -18,7 +18,6 @@ import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import { Button, Layout, Menu } from 'antd'; import { WidgetAllProvider } from 'app/pages/DashBoardPage/components/WidgetProvider/WidgetAllProvider'; -import { BoardContext } from 'app/pages/DashBoardPage/contexts/BoardContext'; import { boardActions } from 'app/pages/DashBoardPage/pages/Board/slice'; import { makeSelectBoardFullScreenPanelById, @@ -30,6 +29,7 @@ import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { G90, WHITE } from 'styles/StyleConstants'; import { CanFullScreenWidgetTypes } from '../constants'; +import { BoardContext } from './BoardProvider/BoardProvider'; import { WidgetCore } from './WidgetCore'; const { Header } = Layout; diff --git a/frontend/src/app/pages/DashBoardPage/components/SaveToStoryBoard.tsx b/frontend/src/app/pages/DashBoardPage/components/SaveToStoryBoard.tsx index 9ff92958c..65a3ad48d 100644 --- a/frontend/src/app/pages/DashBoardPage/components/SaveToStoryBoard.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/SaveToStoryBoard.tsx @@ -53,7 +53,7 @@ const SaveToStoryBoard: FC = memo( let dashboardIds: any = []; let dashboardData = vizData?.filter(v => { const path = getPath( - [v] as Array<{ id: string; parentId: string }>, + vizData as Array<{ id: string; parentId: string }>, { id: v.id, parentId: v.parentId }, VizResourceSubTypes.Folder, ); diff --git a/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx b/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx index 497794f8e..7f8513f08 100644 --- a/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx @@ -45,10 +45,10 @@ import { SPACE_LG, SPACE_SM, } from 'styles/StyleConstants'; -import { BoardActionContext } from '../contexts/BoardActionContext'; -import { BoardContext } from '../contexts/BoardContext'; -import { BoardInfoContext } from '../contexts/BoardInfoContext'; import { BoardOverLay } from './BoardOverLay'; +import { BoardActionContext } from './BoardProvider/BoardActionProvider'; +import { BoardInfoContext } from './BoardProvider/BoardInfoProvider'; +import { BoardContext } from './BoardProvider/BoardProvider'; import SaveToStoryBoard from './SaveToStoryBoard'; interface TitleHeaderProps { @@ -76,6 +76,7 @@ const TitleHeader: FC = memo( onSyncData, }) => { const t = useI18NPrefix(`viz.action`); + const { onClearActiveWidgets } = useContext(BoardActionContext); const [showShareLinkModal, setShowShareLinkModal] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); const { @@ -98,8 +99,11 @@ const TitleHeader: FC = memo( }, []); const onUpdateBoard = useCallback(() => { - updateBoard?.(() => toggleBoardEditor?.(false)); - }, [toggleBoardEditor, updateBoard]); + onClearActiveWidgets(); + setImmediate(() => { + updateBoard?.(() => toggleBoardEditor?.(false)); + }); + }, [onClearActiveWidgets, toggleBoardEditor, updateBoard]); const closeBoardEditor = () => { toggleBoardEditor?.(false); diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ButtonWidget/QueryWidget.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ButtonWidget/QueryWidget.tsx index ddf342089..523986078 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ButtonWidget/QueryWidget.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ButtonWidget/QueryWidget.tsx @@ -15,12 +15,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BoardActionContext } from 'app/pages/DashBoardPage/contexts/BoardActionContext'; -import { WidgetContext } from 'app/pages/DashBoardPage/contexts/WidgetContext'; + import { FontConfig } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { darken, getLuminance, lighten } from 'polished'; import React, { useContext } from 'react'; import styled from 'styled-components/macro'; +import { BoardActionContext } from '../../BoardProvider/BoardActionProvider'; +import { WidgetContext } from '../../WidgetProvider/WidgetProvider'; export interface CompProps {} export const QueryWidget: React.FC = () => { @@ -42,15 +43,14 @@ export const QueryWidget: React.FC = () => { }; const Wrap = styled.div` - cursor: pointer; - flex: 1; display: flex; + flex: 1; align-items: center; justify-content: center; font: ${p => `${p.fontStyle} ${p.fontWeight} ${p.fontSize}px ${p.fontFamily}`}; color: ${p => p.color}; - + cursor: pointer; &:hover { background: ${p => getLuminance(p.background) > 0.5 diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ButtonWidget/ResetWidget.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ButtonWidget/ResetWidget.tsx index a1f66e35b..1ac2ae8aa 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ButtonWidget/ResetWidget.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ButtonWidget/ResetWidget.tsx @@ -15,12 +15,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BoardActionContext } from 'app/pages/DashBoardPage/contexts/BoardActionContext'; -import { WidgetContext } from 'app/pages/DashBoardPage/contexts/WidgetContext'; + import { FontConfig } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { darken, getLuminance, lighten } from 'polished'; import React, { useContext } from 'react'; import styled from 'styled-components/macro'; +import { BoardActionContext } from '../../BoardProvider/BoardActionProvider'; +import { WidgetContext } from '../../WidgetProvider/WidgetProvider'; export interface CompProps {} export const ResetWidget: React.FC = () => { @@ -42,15 +43,14 @@ export const ResetWidget: React.FC = () => { }; const Wrap = styled.div` - cursor: pointer; - flex: 1; display: flex; + flex: 1; align-items: center; justify-content: center; font: ${p => `${p.fontStyle} ${p.fontWeight} ${p.fontSize}px ${p.fontFamily}`}; color: ${p => p.color}; - + cursor: pointer; &:hover { background: ${p => getLuminance(p.background) > 0.5 diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/TabWidget/WidgetOfTab.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/TabWidget/WidgetOfTab.tsx index 92ced0e7c..4022ce933 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/TabWidget/WidgetOfTab.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/TabWidget/WidgetOfTab.tsx @@ -15,16 +15,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { WidgetContext } from 'app/pages/DashBoardPage/contexts/WidgetContext'; -import { WidgetInfoContext } from 'app/pages/DashBoardPage/contexts/WidgetInfoContext'; + import { ContainerItem } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import React, { useContext, useMemo } from 'react'; import styled from 'styled-components/macro'; import { INFO, SUCCESS } from 'styles/StyleConstants'; import { WidgetCore } from '../..'; -import { BoardContext } from '../../../../contexts/BoardContext'; import SubMaskLayer from '../../../../pages/BoardEditor/components/SubMaskLayer'; +import { BoardContext } from '../../../BoardProvider/BoardProvider'; +import { WidgetInfoContext } from '../../../WidgetProvider/WidgetInfoProvider'; +import { WidgetContext } from '../../../WidgetProvider/WidgetProvider'; import WidgetToolBar from '../../../WidgetToolBar'; + export interface IProps { tabItem: ContainerItem; } @@ -82,10 +84,10 @@ interface WrapProps { const Wrap = styled.div` position: relative; box-sizing: border-box; - width: 100%; - height: 100%; display: flex; flex: 1; + width: 100%; + height: 100%; border: ${p => p.border}; & .widget-tool-bar { @@ -102,8 +104,8 @@ const Wrap = styled.div` `; const ItemContainer = styled.div` position: absolute; - display: flex; z-index: 10; + display: flex; width: 100%; height: 100%; `; diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/TabWidget/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/TabWidget/index.tsx index f898e2a35..775a3cc7c 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/TabWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/TabWidget/index.tsx @@ -16,17 +16,17 @@ * limitations under the License. */ import { Tabs } from 'antd'; -import { WidgetContext } from 'app/pages/DashBoardPage/contexts/WidgetContext'; -import { WidgetInfoContext } from 'app/pages/DashBoardPage/contexts/WidgetInfoContext'; import { ContainerWidgetContent } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import React, { useCallback, useContext, useState } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components/macro'; import { PRIMARY } from 'styles/StyleConstants'; import { uuidv4 } from 'utils/utils'; -import { BoardContext } from '../../../../contexts/BoardContext'; import { editBoardStackActions } from '../../../../pages/BoardEditor/slice'; +import { BoardContext } from '../../../BoardProvider/BoardProvider'; import { WidgetAllProvider } from '../../../WidgetProvider/WidgetAllProvider'; +import { WidgetInfoContext } from '../../../WidgetProvider/WidgetInfoProvider'; +import { WidgetContext } from '../../../WidgetProvider/WidgetProvider'; import DropHolder from './DropHolder'; import TabWidgetContainer from './WidgetOfTab'; diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/index.tsx index 9c3cca3d1..831bf29f2 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ContainerWidget/index.tsx @@ -15,9 +15,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { WidgetContext } from 'app/pages/DashBoardPage/contexts/WidgetContext'; + import { ContainerWidgetType } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { useContext } from 'react'; +import { WidgetContext } from '../../WidgetProvider/WidgetProvider'; import { TabWidget } from './TabWidget'; export const ContainerWidget: React.FC<{}> = () => { diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/ControllerWidgetCore.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/ControllerWidgetCore.tsx index 76c5a5f1d..af38f67ae 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/ControllerWidgetCore.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/ControllerWidgetCore.tsx @@ -17,10 +17,6 @@ */ import { Form } from 'antd'; -import { BoardActionContext } from 'app/pages/DashBoardPage/contexts/BoardActionContext'; -import { BoardContext } from 'app/pages/DashBoardPage/contexts/BoardContext'; -import { WidgetContext } from 'app/pages/DashBoardPage/contexts/WidgetContext'; -import { WidgetDataContext } from 'app/pages/DashBoardPage/contexts/WidgetDataContext'; import { ControllerWidgetContent } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { ControllerConfig, @@ -42,6 +38,10 @@ import React, { useMemo, } from 'react'; import styled from 'styled-components/macro'; +import { BoardActionContext } from '../../BoardProvider/BoardActionProvider'; +import { BoardContext } from '../../BoardProvider/BoardProvider'; +import { WidgetDataContext } from '../../WidgetProvider/WidgetDataProvider'; +import { WidgetContext } from '../../WidgetProvider/WidgetProvider'; import { LabelName } from '../WidgetName/WidgetName'; import { CheckboxGroupControllerForm } from './Controller/CheckboxGroupController'; import { MultiSelectControllerForm } from './Controller/MultiSelectController'; diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/DataChartWidget/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/DataChartWidget/index.tsx index 5dbc42b26..aa168e578 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/DataChartWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/DataChartWidget/index.tsx @@ -19,10 +19,6 @@ import { ChartIFrameContainer } from 'app/components/ChartIFrameContainer'; import { useCacheWidthHeight } from 'app/hooks/useCacheWidthHeight'; import { migrateChartConfig } from 'app/migration'; import ChartManager from 'app/pages/ChartWorkbenchPage/models/ChartManager'; -import { WidgetChartContext } from 'app/pages/DashBoardPage/contexts/WidgetChartContext'; -import { WidgetContext } from 'app/pages/DashBoardPage/contexts/WidgetContext'; -import { WidgetDataContext } from 'app/pages/DashBoardPage/contexts/WidgetDataContext'; -import { WidgetMethodContext } from 'app/pages/DashBoardPage/contexts/WidgetMethodContext'; import { Widget } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { ChartMouseEventParams, IChart } from 'app/types/Chart'; import { ChartConfig } from 'app/types/ChartConfig'; @@ -38,6 +34,11 @@ import React, { useRef, } from 'react'; import styled from 'styled-components/macro'; +import { WidgetChartContext } from '../../WidgetProvider/WidgetChartProvider'; +import { WidgetDataContext } from '../../WidgetProvider/WidgetDataProvider'; +import { WidgetMethodContext } from '../../WidgetProvider/WidgetMethodProvider'; +import { WidgetContext } from '../../WidgetProvider/WidgetProvider'; + export const DataChartWidget: React.FC<{}> = memo(() => { const dataChart = useContext(WidgetChartContext); const { data } = useContext(WidgetDataContext); @@ -45,6 +46,7 @@ export const DataChartWidget: React.FC<{}> = memo(() => { const { id: widgetId } = widget; const { widgetChartClick } = useContext(WidgetMethodContext); const { ref, cacheW, cacheH } = useCacheWidthHeight(); + const widgetRef = useRef(widget); useEffect(() => { widgetRef.current = widget; @@ -61,6 +63,7 @@ export const DataChartWidget: React.FC<{}> = memo(() => { ); const chart = useMemo(() => { + // console.log('_dataChartID', dataChart?.id); if (!dataChart) { return null; } @@ -114,7 +117,9 @@ export const DataChartWidget: React.FC<{}> = memo(() => { }), [data], ); + const chartFrame = useMemo(() => { + if (cacheH <= 1 || cacheW <= 1) return null; if (!dataChart) { return `not found dataChart`; } @@ -165,12 +170,12 @@ export const DataChartWidget: React.FC<{}> = memo(() => { }); const ChartFrameBox = styled.div` position: absolute; - height: 100%; width: 100%; + height: 100%; overflow: hidden; `; const Wrap = styled.div` + position: relative; display: flex; flex: 1; - position: relative; `; diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/IframeWidget/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/IframeWidget/index.tsx index 481e3952c..176ca03cf 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/IframeWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/IframeWidget/index.tsx @@ -16,16 +16,15 @@ * limitations under the License. */ import { Button, Form, Input } from 'antd'; -import { BoardActionContext } from 'app/pages/DashBoardPage/contexts/BoardActionContext'; -import { WidgetContext } from 'app/pages/DashBoardPage/contexts/WidgetContext'; -import { WidgetInfoContext } from 'app/pages/DashBoardPage/contexts/WidgetInfoContext'; import { editWidgetInfoActions } from 'app/pages/DashBoardPage/pages/BoardEditor/slice'; import produce from 'immer'; import React, { useContext, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components/macro'; -import { G20 } from 'styles/StyleConstants'; import { MediaWidgetContent } from '../../../../pages/Board/slice/types'; +import { BoardActionContext } from '../../../BoardProvider/BoardActionProvider'; +import { WidgetInfoContext } from '../../../WidgetProvider/WidgetInfoProvider'; +import { WidgetContext } from '../../../WidgetProvider/WidgetProvider'; const IframeWidget: React.FC<{}> = () => { const widget = useContext(WidgetContext); @@ -92,6 +91,6 @@ const Wrap = styled.div` .wrap-form { padding: 4px; margin-bottom: 4px; - background-color: ${G20}; + background-color: ${p => p.theme.emphasisBackground}; } `; diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/ImageWidget/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/ImageWidget/index.tsx index 19e8592b5..7e6e98e8e 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/ImageWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/ImageWidget/index.tsx @@ -17,15 +17,15 @@ */ import { Empty } from 'antd'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; -import { BoardActionContext } from 'app/pages/DashBoardPage/contexts/BoardActionContext'; -import { WidgetContext } from 'app/pages/DashBoardPage/contexts/WidgetContext'; -import { WidgetInfoContext } from 'app/pages/DashBoardPage/contexts/WidgetInfoContext'; import useClientRect from 'app/pages/DashBoardPage/hooks/useClientRect'; import { MediaWidgetContent } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { UploadDragger } from 'app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ImageUpload'; import produce from 'immer'; import React, { useCallback, useContext, useMemo } from 'react'; import styled from 'styled-components/macro'; +import { BoardActionContext } from '../../../BoardProvider/BoardActionProvider'; +import { WidgetInfoContext } from '../../../WidgetProvider/WidgetInfoProvider'; +import { WidgetContext } from '../../../WidgetProvider/WidgetProvider'; const widgetSize: React.CSSProperties = { width: '100%', diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/index.tsx index 9652c0e2b..2f527710c 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/index.tsx @@ -15,20 +15,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Tooltip } from 'antd'; +import { Modal } from 'antd'; +import { + CustomColor, + QuillPalette, +} from 'app/components/ChartGraph/BasicRichText/RichTextPluginLoader/CustomColor'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { MediaWidgetContent, Widget, WidgetInfo, } from 'app/pages/DashBoardPage/pages/Board/slice/types'; -import { FONT_FAMILIES, FONT_SIZES } from 'globalConstants'; +import { editBoardStackActions } from 'app/pages/DashBoardPage/pages/BoardEditor/slice'; +import produce from 'immer'; import { DeltaStatic } from 'quill'; import { ImageDrop } from 'quill-image-drop-module'; // 拖动加载图片组件。 import QuillMarkdown from 'quilljs-markdown'; import 'quilljs-markdown/dist/quilljs-markdown-common-style.css'; import React, { useCallback, + useContext, useEffect, + useLayoutEffect, useMemo, useRef, useState, @@ -38,14 +46,22 @@ import ReactQuill, { Quill } from 'react-quill'; import 'react-quill/dist/quill.snow.css'; import { useDispatch } from 'react-redux'; import styled from 'styled-components/macro'; -import { editBoardStackActions } from '../../../../pages/BoardEditor/slice'; +import { SPACE_TIMES } from 'styles/StyleConstants'; +import { BoardActionContext } from '../../../BoardProvider/BoardActionProvider'; +import { BoardContext } from '../../../BoardProvider/BoardProvider'; import { MarkdownOptions } from './configs/MarkdownOptions'; import TagBlot from './configs/TagBlot'; import { Formats } from './Formats'; - +// import produce from 'immer'; Quill.register('modules/imageDrop', ImageDrop); Quill.register('formats/tag', TagBlot); +const CUSTOM_COLOR = 'custom-color'; +const CUSTOM_COLOR_INIT = { + background: 'transparent', + color: '#000', +}; + type RichTextWidgetProps = { widgetConfig: Widget; widgetInfo: WidgetInfo; @@ -54,7 +70,10 @@ export const RichTextWidget: React.FC = ({ widgetConfig, widgetInfo, }) => { + const t = useI18NPrefix(); const dispatch = useDispatch(); + const { editing: boardEditing } = useContext(BoardContext); + const { onClearActiveWidgets } = useContext(BoardActionContext); const initContent = useMemo(() => { return (widgetConfig.config.content as MediaWidgetContent).richTextConfig ?.content; @@ -65,33 +84,52 @@ export const RichTextWidget: React.FC = ({ const [containerId, setContainerId] = useState(); const [quillModules, setQuillModules] = useState(null); + const [customColorVisible, setCustomColorVisible] = useState(false); + const [customColor, setCustomColor] = useState<{ + background: string; + color: string; + }>({ ...QuillPalette.RICH_TEXT_CUSTOM_COLOR_INIT }); + const [customColorType, setCustomColorType] = useState< + 'color' | 'background' + >('color'); + const [contentSavable, setContentSavable] = useState(false); + useEffect(() => { - setQuillValue(initContent); - }, [initContent]); + if (widgetInfo.editing) { + setQuillValue(initContent); + } + }, [initContent, widgetInfo.editing]); useEffect(() => { - if (widgetInfo.editing === false) { + if (widgetInfo.editing === false && contentSavable && boardEditing) { if (quillRef.current) { let contents = quillRef.current?.getEditor().getContents(); const strContents = JSON.stringify(contents); if (strContents !== JSON.stringify(initContent)) { + const nextMediaWidgetContent = produce( + widgetConfig.config.content, + draft => { + (draft as MediaWidgetContent).richTextConfig = { + content: JSON.parse(strContents), + }; + }, + ) as MediaWidgetContent; + dispatch( editBoardStackActions.changeMediaWidgetConfig({ id: widgetConfig.id, - mediaWidgetConfig: { - ...(widgetConfig.config.content as MediaWidgetContent), - richTextConfig: { - content: JSON.parse(strContents), - }, - }, + mediaWidgetContent: nextMediaWidgetContent, }), ); + setContentSavable(false); } } } }, [ + boardEditing, dispatch, initContent, + contentSavable, widgetConfig.config.content, widgetConfig.id, widgetInfo.editing, @@ -103,6 +141,22 @@ export const RichTextWidget: React.FC = ({ const modules = { toolbar: { container: `#${newId}`, + handlers: { + color: function (value) { + if (value === QuillPalette.RICH_TEXT_CUSTOM_COLOR) { + setCustomColorType('color'); + setCustomColorVisible(true); + } + quillRef.current!.getEditor().format('color', value); + }, + background: function (value) { + if (value === QuillPalette.RICH_TEXT_CUSTOM_COLOR) { + setCustomColorType('background'); + setCustomColorVisible(true); + } + quillRef.current!.getEditor().format('background', value); + }, + }, }, imageDrop: true, }; @@ -111,12 +165,63 @@ export const RichTextWidget: React.FC = ({ const quillRef = useRef(null); - useEffect(() => { + useLayoutEffect(() => { if (quillRef.current) { + quillRef.current + .getEditor() + .on('selection-change', (r: { index: number; length: number }) => { + if (!r?.index) return; + try { + const index = r.length === 0 ? r.index - 1 : r.index; + const length = r.length === 0 ? 1 : r.length; + const delta = quillRef + .current!.getEditor() + .getContents(index, length); + + if (delta.ops?.length === 1 && delta.ops[0]?.attributes) { + const { background, color } = delta.ops[0].attributes; + setCustomColor({ + background: background || CUSTOM_COLOR_INIT.background, + color: color || CUSTOM_COLOR_INIT.color, + }); + + const colorNode = document.querySelector( + '.ql-color .ql-color-label', + ); + const backgroundNode = document.querySelector( + '.ql-background .ql-color-label', + ); + if (color && !colorNode?.getAttribute('style')) { + colorNode!.setAttribute('style', `stroke: ${color}`); + } + if (background && !backgroundNode?.getAttribute('style')) { + backgroundNode!.setAttribute('style', `fill: ${background}`); + } + } else { + setCustomColor({ ...CUSTOM_COLOR_INIT }); + } + } catch (error) { + console.error('selection-change callback | error', error); + } + }); new QuillMarkdown(quillRef.current.getEditor(), MarkdownOptions); } }, [quillModules]); + useEffect(() => { + let palette: QuillPalette | null = null; + if (quillRef.current && containerId) { + palette = new QuillPalette(quillRef.current, { + toolbarId: containerId, + onChange: setCustomColor, + }); + } + + return () => { + palette?.destroy(); + }; + }, [containerId]); + const ssp = e => { e.stopPropagation(); }; @@ -129,149 +234,103 @@ export const RichTextWidget: React.FC = ({ }, []); const toolbar = useMemo( - () => ( -
    - - - - -
    + } + >
    { state.deleteSourceLoading = false; }); + + // syncSourceSchema + builder.addCase(syncSourceSchema.pending, state => { + state.syncSourceSchemaLoading = true; + }); + builder.addCase(syncSourceSchema.fulfilled, (state, action) => { + state.syncSourceSchemaLoading = false; + }); + builder.addCase(syncSourceSchema.rejected, state => { + state.syncSourceSchemaLoading = false; + }); }, }); diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/selectors.ts b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/selectors.ts index 04effa454..c026ba09f 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/selectors.ts +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/selectors.ts @@ -67,3 +67,8 @@ export const selectDeleteSourceLoading = createSelector( [selectDomain], sourceState => sourceState.deleteSourceLoading, ); + +export const selectSyncSourceSchemaLoading = createSelector( + [selectDomain], + sourceState => sourceState.syncSourceSchemaLoading, +); diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/thunks.ts b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/thunks.ts index 51f591dc6..ecb5f7954 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/thunks.ts +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/thunks.ts @@ -20,7 +20,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { getLoggedInUserPermissions } from 'app/pages/MainPage/slice/thunks'; import { RootState } from 'types'; -import { request } from 'utils/request'; +import { request, request2 } from 'utils/request'; import { errorHandle } from 'utils/utils'; import { AddSourceParams, @@ -154,3 +154,14 @@ export const deleteSource = createAsyncThunk( } }, ); + +export const syncSourceSchema = createAsyncThunk( + 'source/syncSourceSchema', + async ({ sourceId }) => { + const { data } = await request2({ + url: `/sources/sync/schemas/${sourceId}`, + method: 'GET', + }); + return data; + }, +); diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/types.ts b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/types.ts index d43f4a481..32a0ee89f 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/types.ts +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/types.ts @@ -26,6 +26,7 @@ export interface SourceState { saveSourceLoading: boolean; unarchiveSourceLoading: boolean; deleteSourceLoading: boolean; + syncSourceSchemaLoading: boolean; } export interface Source { @@ -39,6 +40,7 @@ export interface Source { type: string; updateBy: string; updateTime: string; + schemaUpdateDate: string; } export interface SourceFormModel extends Pick { diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Container.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Container.tsx index 5afe8022c..f877973a5 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Container.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Container.tsx @@ -85,7 +85,6 @@ export function Container() { />
    { type: CommonFormTypes.Edit, visible: true, parentIdLabel: t('folder'), + initialValues: { + name: '', + parentId: '', + config: {}, + }, onSave: (values, onClose) => { let index = getInsertedNodeIndex(values, viewsData); @@ -130,7 +135,6 @@ export const SQLEditor = memo(() => { dispatch( getEditorProvideCompletionItems({ resolve: getItems => { - editorCompletionItemProviderRef?.current?.dispose(); const providerRef = editor.languages.registerCompletionItemProvider( 'sql', { @@ -176,6 +180,7 @@ export const SQLEditor = memo(() => { editorInstance?.layout(); return () => { editorInstance?.dispose(); + editorCompletionItemProviderRef?.current?.dispose(); }; }, [editorInstance]); diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Results.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Results.tsx index 855cbb2f9..635cea22d 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Results.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Results.tsx @@ -24,11 +24,13 @@ import { import { Tooltip } from 'antd'; import { Popup, ToolbarButton, Tree } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { APP_CURRENT_VERSION } from 'app/migration/constants'; import classnames from 'classnames'; import { memo, useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { FONT_FAMILY, FONT_SIZE_BASE } from 'styles/StyleConstants'; +import { CloneValueDeep, isEmptyArray } from 'utils/object'; import { uuidv4 } from 'utils/utils'; import { selectRoles } from '../../../MemberPage/slice/selectors'; import { SubjectTypes } from '../../../PermissionPage/constants'; @@ -39,7 +41,7 @@ import { selectCurrentEditingViewAttr } from '../../slice/selectors'; import { Column, ColumnPermission, - Model, + HierarchyModel, ViewViewModel, } from '../../slice/types'; @@ -58,7 +60,7 @@ export const Results = memo(({ height = 0, width = 0 }: ResultsProps) => { ) as string; const model = useSelector(state => selectCurrentEditingViewAttr(state, { name: 'model' }), - ) as Model; + ) as HierarchyModel; const columnPermissions = useSelector(state => selectCurrentEditingViewAttr(state, { name: 'columnPermissions' }), ) as ColumnPermission[]; @@ -86,9 +88,29 @@ export const Results = memo(({ height = 0, width = 0 }: ResultsProps) => { } else { value = { ...column, type: key }; } + const clonedHierarchyModel = CloneValueDeep(model.hierarchy || {}); + if (columnName in clonedHierarchyModel) { + clonedHierarchyModel[columnName] = value; + } else { + Object.values(clonedHierarchyModel) + .filter(col => !isEmptyArray(col.children)) + .forEach(col => { + const targetChildColumnIndex = col.children!.findIndex( + child => child.name === columnName, + ); + if (targetChildColumnIndex > -1) { + col.children![targetChildColumnIndex] = value; + } + }); + } + dispatch( actions.changeCurrentEditingView({ - model: { ...model, [columnName]: value }, + model: { + ...model, + hierarchy: clonedHierarchyModel, + version: APP_CURRENT_VERSION, + }, }), ); }, @@ -108,7 +130,7 @@ export const Results = memo(({ height = 0, width = 0 }: ResultsProps) => { const checkRoleColumnPermission = useCallback( columnName => checkedKeys => { - const fullPermissions = Object.keys(model); + const fullPermissions = Object.keys(model?.columns || {}); dispatch( actions.changeCurrentEditingView({ columnPermissions: roleDropdownData.reduce( @@ -231,7 +253,8 @@ export const Results = memo(({ height = 0, width = 0 }: ResultsProps) => { { const { actions } = useViewSlice(); @@ -53,7 +49,7 @@ export const ColumnPermissions = memo(() => { ) as ViewStatus; const model = useSelector(state => selectCurrentEditingViewAttr(state, { name: 'model' }), - ) as Model; + ) as HierarchyModel; const columnPermissions = useSelector(state => selectCurrentEditingViewAttr(state, { name: 'columnPermissions' }), ) as ColumnPermission[]; @@ -73,7 +69,9 @@ export const ColumnPermissions = memo(() => { if (index >= 0) { if ( - Object.keys(model).sort().join(',') !== checkedKeys.sort().join(',') + Object.keys(model?.columns || {}) + .sort() + .join(',') !== checkedKeys.sort().join(',') ) { dispatch( actions.changeCurrentEditingView({ @@ -115,8 +113,12 @@ export const ColumnPermissions = memo(() => { const columnDropdownData = useMemo( () => - Object.keys(model).map(name => ({ key: name, title: name, value: name })), - [model], + Object.keys(model?.columns || {}).map(name => ({ + key: name, + title: name, + value: name, + })), + [model?.columns], ); const renderItem = useCallback( @@ -170,8 +172,7 @@ export const ColumnPermissions = memo(() => { ); return ( - - + { ); }); -const Container = styled.div` - display: flex; - flex: 1; - flex-direction: column; - width: ${SPACE_TIMES(100)}; - min-height: 0; - border-left: 1px solid ${p => p.theme.borderColorSplit}; -`; - const Searchbar = styled(Row)` .input { padding-bottom: ${SPACE_XS}; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Container.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Container.tsx new file mode 100644 index 000000000..10b0eaf6a --- /dev/null +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Container.tsx @@ -0,0 +1,49 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { Skeleton } from 'antd'; +import { ListTitle } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { FC, memo } from 'react'; +import styled from 'styled-components/macro'; +import { SPACE_TIMES } from 'styles/StyleConstants'; + +const Container: FC = memo(props => { + const t = useI18NPrefix('view.properties'); + const { title, children, isLoading, ...rest } = props; + + return ( + + + + {children} + + + ); +}); + +export default Container; + +const StyledContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + width: ${SPACE_TIMES(100)}; + min-height: 0; + border-left: 1px solid ${p => p.theme.borderColorSplit}; +`; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/DataModelBranch.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/DataModelBranch.tsx new file mode 100644 index 000000000..f9a5ff050 --- /dev/null +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/DataModelBranch.tsx @@ -0,0 +1,174 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { + DeleteOutlined, + EditOutlined, + FolderOpenOutlined, +} from '@ant-design/icons'; +import { Tooltip } from 'antd'; +import { IW, ToolbarButton } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { FC, memo, useState } from 'react'; +import { Draggable, Droppable } from 'react-beautiful-dnd'; +import styled from 'styled-components/macro'; +import { + FONT_SIZE_BASE, + FONT_SIZE_HEADING, + INFO, + SPACE_UNIT, + YELLOW, +} from 'styles/StyleConstants'; +import { Column } from '../../../slice/types'; +import { TreeNodeHierarchy } from './constant'; +import DataModelNode from './DataModelNode'; + +const DataModelBranch: FC<{ + node: Column; + getPermissionButton: (name) => JSX.Element; + onNodeTypeChange: (type: any, name: string) => void; + onMoveToHierarchy: (node: Column) => void; + onEditBranch; + onDelete: (node: Column) => void; +}> = memo( + ({ + node, + getPermissionButton, + onNodeTypeChange, + onMoveToHierarchy, + onEditBranch, + onDelete, + }) => { + const t = useI18NPrefix('view.model'); + const [isHover, setIsHover] = useState(false); + + const renderNode = (node, isDragging) => { + let icon = ( + + ); + + return ( + <> +
    { + setIsHover(true); + }} + onMouseLeave={() => { + setIsHover(false); + }} + > + {icon} + {node.name} +
    + {isHover && !isDragging && ( + + onEditBranch(node)} + icon={} + /> + + )} + {isHover && !isDragging && ( + + onDelete(node)} + icon={} + /> + + )} +
    +
    +
    + {node?.children?.map(childNode => ( + + ))} +
    + + ); + }; + + return ( + + {(draggableProvided, draggableSnapshot) => { + return ( + + + {(droppableProvided, droppableSnapshot) => ( +
    + {renderNode(node, draggableSnapshot.isDragging)} + {droppableProvided.placeholder} +
    + )} +
    +
    + ); + }} +
    + ); + }, +); + +export default DataModelBranch; + +const StyledDataModelBranch = styled.div<{}>` + line-height: 32px; + margin: ${SPACE_UNIT}; + user-select: 'none'; + font-size: ${FONT_SIZE_BASE}; + + & .content { + display: flex; + } + + & .children { + margin-left: 40px; + } + + & .action { + display: flex; + flex: 1; + justify-content: flex-end; + padding-right: ${FONT_SIZE_BASE}px; + } +`; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/DataModelNode.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/DataModelNode.tsx new file mode 100644 index 000000000..da65da525 --- /dev/null +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/DataModelNode.tsx @@ -0,0 +1,218 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { + BranchesOutlined, + CalendarOutlined, + FieldStringOutlined, + NumberOutlined, + SisternodeOutlined, + SwapOutlined, +} from '@ant-design/icons'; +import { Dropdown, Menu, Tooltip } from 'antd'; +import { IW, ToolbarButton } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { FC, memo, useState } from 'react'; +import { Draggable } from 'react-beautiful-dnd'; +import styled from 'styled-components/macro'; +import { + FONT_SIZE_BASE, + FONT_SIZE_HEADING, + INFO, + SPACE, + SPACE_UNIT, + SUCCESS, + WARNING, +} from 'styles/StyleConstants'; +import { ColumnCategories, ColumnTypes } from '../../../constants'; +import { Column } from '../../../slice/types'; +import { ALLOW_COMBINE_COLUMN_TYPES } from './constant'; + +const DataModelNode: FC<{ + node: Column; + getPermissionButton: (name) => JSX.Element; + onNodeTypeChange: (type: any, name: string) => void; + onMoveToHierarchy: (node: Column) => void; + onCreateHierarchy?: (node: Column) => void; +}> = memo( + ({ + node, + getPermissionButton, + onCreateHierarchy, + onMoveToHierarchy, + onNodeTypeChange, + }) => { + const t = useI18NPrefix('view.model'); + const tg = useI18NPrefix('global'); + const [isHover, setIsHover] = useState(false); + const hasCategory = true; + + const renderNode = (node, isDragging) => { + let icon; + switch (node.type) { + case ColumnTypes.Number: + icon = ( + + ); + break; + case ColumnTypes.String: + icon = ( + + ); + break; + default: + icon = ( + + ); + break; + } + + const isAllowCreateHierarchy = node => { + return ALLOW_COMBINE_COLUMN_TYPES.includes(node.type); + }; + + return ( +
    { + setIsHover(true); + }} + onMouseLeave={() => { + setIsHover(false); + }} + > + {icon} + {node.name} +
    + {isHover && !isDragging && ( + onNodeTypeChange(key, node?.name)} + > + {Object.values(ColumnTypes).map(t => ( + + {tg(`columnType.${t.toLowerCase()}`)} + + ))} + {hasCategory && ( + <> + + + {Object.values(ColumnCategories).map(t => ( + + {tg(`columnCategory.${t.toLowerCase()}`)} + + ))} + + + )} + + } + > + + } + /> + + + )} + {isHover && !isDragging && getPermissionButton(node?.name)} + {isHover && + !isDragging && + isAllowCreateHierarchy(node) && + onCreateHierarchy && ( + + onCreateHierarchy(node)} + icon={} + /> + + )} + {isHover && !isDragging && isAllowCreateHierarchy(node) && ( + + onMoveToHierarchy(node)} + icon={} + /> + + )} +
    +
    + ); + }; + + return ( + + {(draggableProvided, draggableSnapshot) => ( + + {renderNode(node, draggableSnapshot.isDragging)} + + )} + + ); + }, +); + +export default DataModelNode; + +const StyledDataModelNode = styled.div<{ + isDragging: boolean; +}>` + line-height: 32px; + margin: ${SPACE}; + user-select: 'none'; + background: ${p => + p.isDragging ? p.theme.highlightBackground : 'transparent'}; + font-size: ${FONT_SIZE_BASE}; + + & .content { + display: flex; + } + + & .action { + display: flex; + flex: 1; + justify-content: flex-end; + padding-right: ${FONT_SIZE_BASE}px; + } +`; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/DataModelTree.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/DataModelTree.tsx new file mode 100644 index 000000000..7683dc5d6 --- /dev/null +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/DataModelTree.tsx @@ -0,0 +1,639 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; +import { Form, Input, Select, Tooltip } from 'antd'; +import { Popup, ToolbarButton, Tree } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import useStateModal, { StateModalSize } from 'app/hooks/useStateModal'; +import { APP_CURRENT_VERSION } from 'app/migration/constants'; +import { selectRoles } from 'app/pages/MainPage/pages/MemberPage/slice/selectors'; +import { SubjectTypes } from 'app/pages/MainPage/pages/PermissionPage/constants'; +import classnames from 'classnames'; +import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { DragDropContext, Droppable } from 'react-beautiful-dnd'; +import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components/macro'; +import { FONT_SIZE_BASE, INFO } from 'styles/StyleConstants'; +import { Nullable } from 'types'; +import { CloneValueDeep, isEmpty, isEmptyArray } from 'utils/object'; +import { uuidv4 } from 'utils/utils'; +import { ColumnTypes, ViewViewModelStages } from '../../../constants'; +import { useViewSlice } from '../../../slice'; +import { + selectCurrentEditingView, + selectCurrentEditingViewAttr, +} from '../../../slice/selectors'; +import { + Column, + ColumnPermission, + ColumnRole, + Model, +} from '../../../slice/types'; +import { dataModelColumnSorter } from '../../../utils'; +import Container from '../Container'; +import { + ALLOW_COMBINE_COLUMN_TYPES, + ROOT_CONTAINER_ID, + TreeNodeHierarchy, +} from './constant'; +import DataModelBranch from './DataModelBranch'; +import DataModelNode from './DataModelNode'; + +const DataModelTree: FC = memo(() => { + const t = useI18NPrefix('view'); + const { actions } = useViewSlice(); + const dispatch = useDispatch(); + const [openStateModal, contextHolder] = useStateModal({}); + const viewId = useSelector(state => + selectCurrentEditingViewAttr(state, { name: 'id' }), + ) as string; + const currentEditingView = useSelector(selectCurrentEditingView); + const stage = useSelector(state => + selectCurrentEditingViewAttr(state, { name: 'stage' }), + ) as ViewViewModelStages; + const roles = useSelector(selectRoles); + const columnPermissions = useSelector(state => + selectCurrentEditingViewAttr(state, { name: 'columnPermissions' }), + ) as ColumnPermission[]; + const [hierarchy, setHierarchy] = useState>(); + + useEffect(() => { + setHierarchy(currentEditingView?.model?.hierarchy); + }, [currentEditingView?.model?.hierarchy]); + + const tableColumns = useMemo(() => { + return Object.entries(hierarchy || {}) + .map(([name, column], index) => { + return Object.assign({ index }, column, { name }); + }) + .sort(dataModelColumnSorter); + }, [hierarchy]); + + const roleDropdownData = useMemo( + () => + roles.map(({ id, name }) => ({ + key: id, + title: name, + value: id, + isLeaf: true, + })), + [roles], + ); + + const checkRoleColumnPermission = useCallback( + columnName => checkedKeys => { + const fullPermissions = Object.keys(hierarchy || {}); + dispatch( + actions.changeCurrentEditingView({ + columnPermissions: roleDropdownData.reduce( + (updated, { key }) => { + const permission = columnPermissions.find( + ({ subjectId }) => subjectId === key, + ); + const checkOnCurrentRole = checkedKeys.includes(key); + if (permission) { + if (checkOnCurrentRole) { + const updatedColumnPermission = Array.from( + new Set(permission.columnPermission.concat(columnName)), + ); + return fullPermissions.sort().join(',') !== + updatedColumnPermission.sort().join(',') + ? updated.concat({ + ...permission, + columnPermission: updatedColumnPermission, + }) + : updated; + } else { + return updated.concat({ + ...permission, + columnPermission: permission.columnPermission.filter( + c => c !== columnName, + ), + }); + } + } else { + return !checkOnCurrentRole + ? updated.concat({ + id: uuidv4(), + viewId, + subjectId: key, + subjectType: SubjectTypes.Role, + columnPermission: fullPermissions.filter( + c => c !== columnName, + ), + }) + : updated; + } + }, + [], + ), + }), + ); + }, + [dispatch, actions, viewId, hierarchy, columnPermissions, roleDropdownData], + ); + + const handleDeleteBranch = (node: Column) => { + const newHierarchy = deleteBranch(tableColumns, node); + handleDataModelHierarchyChange(newHierarchy); + }; + + const handleNodeTypeChange = (type, name) => { + const targetNode = tableColumns?.find(n => n.name === name); + if (targetNode) { + let newNode; + if (type.includes('category')) { + const category = type.split('-')[1]; + newNode = { ...targetNode, category }; + } else { + newNode = { ...targetNode, type: type }; + } + const newHierarchy = updateNode(tableColumns, newNode, targetNode.index); + handleDataModelHierarchyChange(newHierarchy); + return; + } + const targetBranch = tableColumns?.find(b => { + if (b.children) { + return b.children?.find(bn => bn.name === name); + } + return false; + }); + if (!!targetBranch) { + const newNodeIndex = targetBranch.children?.findIndex( + bn => bn.name === name, + ); + if (newNodeIndex !== undefined && newNodeIndex > -1) { + const newTargetBranch = CloneValueDeep(targetBranch); + if (newTargetBranch.children) { + let newNode = newTargetBranch.children[newNodeIndex]; + if (type.includes('category')) { + const category = type.split('-')[1]; + newNode = { ...newNode, category }; + } else { + newNode = { ...newNode, type: type }; + } + newTargetBranch.children[newNodeIndex] = newNode; + const newHierarchy = updateNode( + tableColumns, + newTargetBranch, + newTargetBranch.index, + ); + handleDataModelHierarchyChange(newHierarchy); + } + } + } + }; + + const handleDataModelHierarchyChange = hierarchy => { + setHierarchy(hierarchy); + dispatch( + actions.changeCurrentEditingView({ + model: { + ...currentEditingView?.model, + hierarchy, + version: APP_CURRENT_VERSION, + }, + }), + ); + }; + + const handleDragEnd = result => { + if (Boolean(result.destination) && isEmpty(result?.combine)) { + const newHierarchy = reorderNode( + CloneValueDeep(tableColumns), + { name: result.draggableId }, + { + name: result.destination.droppableId, + index: result.destination.index, + }, + ); + return handleDataModelHierarchyChange(newHierarchy); + } + if (!Boolean(result.destination) && !isEmpty(result?.combine)) { + const clonedTableColumns = CloneValueDeep(tableColumns); + const sourceNode = clonedTableColumns?.find( + c => c.name === result.draggableId, + ); + const targetNode = clonedTableColumns?.find( + c => c.name === result.combine.draggableId, + ); + if ( + sourceNode && + sourceNode.role !== ColumnRole.Hierarchy && + targetNode && + targetNode.role !== ColumnRole.Hierarchy && + ALLOW_COMBINE_COLUMN_TYPES.includes(sourceNode.type) && + ALLOW_COMBINE_COLUMN_TYPES.includes(targetNode.type) + ) { + return openCreateHierarchyModal(sourceNode, targetNode); + } else if ( + sourceNode && + sourceNode.role !== ColumnRole.Hierarchy && + targetNode && + targetNode.role === ColumnRole.Hierarchy && + ALLOW_COMBINE_COLUMN_TYPES.includes(sourceNode.type) + ) { + const newHierarchy = reorderNode( + clonedTableColumns, + { name: result.draggableId }, + { + name: result.combine.draggableId, + index: -1, + }, + ); + return handleDataModelHierarchyChange(newHierarchy); + } + } + }; + + const openCreateHierarchyModal = (...nodes: Column[]) => { + return (openStateModal as Function)({ + title: t('model.newHierarchy'), + modalSize: StateModalSize.XSMALL, + onOk: hierarchyName => { + if (!hierarchyName) { + return; + } + const hierarchyNode: Column = { + name: hierarchyName, + type: ColumnTypes.String, + role: ColumnRole.Hierarchy, + children: nodes, + }; + const newHierarchy = insertNode(tableColumns, hierarchyNode, nodes); + handleDataModelHierarchyChange(newHierarchy); + }, + content: onChangeEvent => { + const allNodeNames = tableColumns?.flatMap(c => { + if (!isEmptyArray(c.children)) { + return [c.name].concat(c.children?.map(cc => cc.name) || []); + } + return c.name; + }); + return ( + ({ + validator(_, value) { + if (!allNodeNames.includes(getFieldValue('hierarchyName'))) { + return Promise.resolve(value); + } + return Promise.reject(new Error('名称重复,请检查!')); + }, + }), + ]} + > + onChangeEvent(e.target?.value)} /> + + ); + }, + }); + }; + + const openMoveToHierarchyModal = (node: Column) => { + const currentHierarchies = tableColumns?.filter( + c => + c.role === ColumnRole.Hierarchy && + !c?.children?.find(cn => cn.name === node.name), + ); + + return (openStateModal as Function)({ + title: t('model.addToHierarchy'), + modalSize: StateModalSize.XSMALL, + onOk: hierarchyName => { + if (currentHierarchies?.find(h => h.name === hierarchyName)) { + let newHierarchy = moveNode( + tableColumns, + node, + currentHierarchies, + hierarchyName, + ); + handleDataModelHierarchyChange(newHierarchy); + } + }, + content: onChangeEvent => { + return ( + + + + ); + }, + }); + }; + + const openEditBranchModal = (node: Column) => { + const allNodeNames = tableColumns + ?.flatMap(c => { + if (!isEmptyArray(c.children)) { + return c.children?.map(cc => cc.name); + } + return c.name; + }) + .filter(n => n !== node.name); + + return (openStateModal as Function)({ + title: t('model.rename'), + modalSize: StateModalSize.XSMALL, + onOk: newName => { + if (!newName) { + return; + } + const newHierarchy = updateNode( + tableColumns, + { ...node, name: newName }, + node.index, + ); + handleDataModelHierarchyChange(newHierarchy); + }, + content: onChangeEvent => { + return ( + ({ + validator(_, value) { + if (!allNodeNames.includes(getFieldValue('rename'))) { + return Promise.resolve(value); + } + return Promise.reject(new Error('名称重复,请检查!')); + }, + }), + ]} + > + { + onChangeEvent(e.target?.value); + }} + /> + + ); + }, + }); + }; + + const reorderNode = ( + columns: Column[], + source: { name: string }, + target: { name: string; index: number }, + ) => { + let sourceItem: Column | undefined; + const removeIndex = columns.findIndex(c => c.name === source.name); + if (removeIndex > -1) { + sourceItem = columns.splice(removeIndex, 1)?.[0]; + } else { + const branchNode = columns.filter( + c => + c.role === ColumnRole.Hierarchy && + c.children?.find(cc => cc.name === source.name), + )?.[0]; + if (!branchNode) { + return toModel(columns); + } + const removeIndex = branchNode.children!.findIndex( + c => c.name === source.name, + ); + if (removeIndex === -1) { + return toModel(columns); + } + sourceItem = branchNode.children?.splice(removeIndex, 1)?.[0]; + } + + if (!sourceItem) { + return toModel(columns); + } + + if (target.name === ROOT_CONTAINER_ID) { + columns.splice(target.index, 0, sourceItem); + } else { + const branchNode = columns.filter( + c => c.role === ColumnRole.Hierarchy && c.name === target.name, + )?.[0]; + if (!branchNode) { + return toModel(columns); + } + if (target.index === -1) { + branchNode.children!.push(sourceItem); + } else { + branchNode.children!.splice(target.index, 0, sourceItem); + } + } + return toModel(columns); + }; + + const insertNode = (columns: Column[], newNode, nodes: Column[]) => { + const newColumns = columns.filter( + c => !nodes.map(n => n.name).includes(c.name), + ); + newColumns.unshift(newNode); + return toModel(newColumns); + }; + + const updateNode = (columns: Column[], newNode, updateIndex) => { + columns[updateIndex] = newNode; + return toModel(columns); + }; + + const deleteBranch = (columns: Column[], node: Column) => { + const deletedBranchIndex = columns.findIndex(c => c.name === node.name); + if (deletedBranchIndex > -1) { + const branch = columns[deletedBranchIndex]; + const children = branch?.children || []; + columns.splice(deletedBranchIndex, 1); + return toModel(columns, ...children); + } + }; + + const moveNode = ( + columns: Column[], + node: Column, + currentHierarchies: Column[], + hierarchyName, + ) => { + const nodeIndex = columns?.findIndex(c => c.name === node.name); + if (nodeIndex !== undefined && nodeIndex > -1) { + columns.splice(nodeIndex, 1); + } else { + const branch = columns?.find(c => + c.children?.find(cc => cc.name === node.name), + ); + if (branch) { + branch.children = + branch.children?.filter(bc => bc.name !== node.name) || []; + } + } + const targetHierarchy = currentHierarchies?.find( + h => h.name === hierarchyName, + ); + const clonedHierarchy = CloneValueDeep(targetHierarchy!); + clonedHierarchy.children = (clonedHierarchy.children || []).concat([node]); + return updateNode( + columns, + clonedHierarchy, + columns.findIndex(c => c.name === clonedHierarchy.name), + ); + }; + + const toModel = (columns: Column[], ...additional) => { + return columns.concat(...additional)?.reduce((acc, cur, newIndex) => { + if (cur?.role === ColumnRole.Hierarchy && isEmptyArray(cur?.children)) { + return acc; + } + if (cur?.role === ColumnRole.Hierarchy && !isEmptyArray(cur?.children)) { + const orderedChildren = cur.children?.map((child, newIndex) => { + return { + ...child, + index: newIndex, + }; + }); + acc[cur.name] = Object.assign({}, cur, { + index: newIndex, + children: orderedChildren, + }); + } else { + acc[cur.name] = Object.assign({}, cur, { index: newIndex }); + } + return acc; + }, {}); + }; + + const getPermissionButton = useCallback( + (name: string) => { + // 没有记录相当于对所有字段都有权限 + const checkedKeys = + columnPermissions.length > 0 + ? roleDropdownData.reduce((selected, { key }) => { + const permission = columnPermissions.find( + ({ subjectId }) => subjectId === key, + ); + if (permission) { + return permission.columnPermission.includes(name) + ? selected.concat(key) + : selected; + } else { + return selected.concat(key); + } + }, []) + : roleDropdownData.map(({ key }) => key); + + return ( + + } + > + + 0 ? ( + + ) : ( + + ) + } + /> + + + ); + }, + [columnPermissions, roleDropdownData, checkRoleColumnPermission, t], + ); + + return ( + + + + {(droppableProvided, droppableSnapshot) => ( + + {tableColumns.map(col => { + return col.role === ColumnRole.Hierarchy ? ( + + ) : ( + + ); + })} + {droppableProvided.placeholder} + + )} + + + {contextHolder} + + ); +}); + +export default DataModelTree; + +const StyledDroppableContainer = styled.div<{ isDraggingOver }>` + user-select: 'none'; +`; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/constant.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/constant.ts new file mode 100644 index 000000000..cde1e2032 --- /dev/null +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/constant.ts @@ -0,0 +1,32 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { DATARTSEPERATOR } from 'globalConstants'; +import { ColumnTypes } from '../../../constants'; + +export const ROOT_CONTAINER_ID = `${DATARTSEPERATOR}data-model-root-id`; + +export const ALLOW_COMBINE_COLUMN_TYPES = [ + ColumnTypes.String, + ColumnTypes.Date, +]; + +export enum TreeNodeHierarchy { + Root = 'root', + Branch = 'branch', +} diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/index.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/index.tsx new file mode 100644 index 000000000..1334f079e --- /dev/null +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/DataModelTree/index.tsx @@ -0,0 +1,21 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import DataModelTree from './DataModelTree'; + +export default DataModelTree; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Resource.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Resource.tsx index 9e0adc902..cf258bd4b 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Resource.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Resource.tsx @@ -25,60 +25,82 @@ import { TableOutlined, } from '@ant-design/icons'; import { Col, Input, Row } from 'antd'; -import { ListTitle, Tree } from 'app/components'; +import { Tree } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import useResizeObserver from 'app/hooks/useResizeObserver'; import { useSearchAndExpand } from 'app/hooks/useSearchAndExpand'; -import { selectDataProviderDatabaseListLoading } from 'app/pages/MainPage/slice/selectors'; -import { getDataProviderDatabases } from 'app/pages/MainPage/slice/thunks'; import { DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; -import { memo, useCallback, useContext, useEffect } from 'react'; +import { memo, useCallback, useContext, useEffect, useMemo } from 'react'; import { monaco } from 'react-monaco-editor'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; -import { SPACE_MD, SPACE_TIMES, SPACE_XS } from 'styles/StyleConstants'; +import { SPACE_MD, SPACE_XS } from 'styles/StyleConstants'; import { RootState } from 'types'; -import { request } from 'utils/request'; -import { errorHandle } from 'utils/utils'; import { ColumnTypes } from '../../constants'; import { EditorContext } from '../../EditorContext'; -import { useViewSlice } from '../../slice'; import { selectCurrentEditingViewAttr, - selectDatabases, + selectDatabaseSchemaLoading, + selectSourceDatabaseSchemas, } from '../../slice/selectors'; import { getEditorProvideCompletionItems } from '../../slice/thunks'; -import { Schema } from '../../slice/types'; +import { DatabaseSchema } from '../../slice/types'; +import { buildAntdTreeNodeModel } from '../../utils'; +import Container from './Container'; export const Resource = memo(() => { - const { actions } = useViewSlice(); + const t = useI18NPrefix('view.resource'); const dispatch = useDispatch(); const { editorCompletionItemProviderRef } = useContext(EditorContext); + const isDatabaseSchemaLoading = useSelector(selectDatabaseSchemaLoading); const sourceId = useSelector(state => selectCurrentEditingViewAttr(state, { name: 'sourceId' }), ) as string; - const databases = useSelector(state => - selectDatabases(state, { name: sourceId }), - ); - const databaseListLoading = useSelector( - selectDataProviderDatabaseListLoading, + const databaseSchemas = useSelector(state => + selectSourceDatabaseSchemas(state, { id: sourceId }), ); - const t = useI18NPrefix('view.resource'); + + const { height, ref: treeWrapperRef } = useResizeObserver({ + refreshMode: 'debounce', + refreshRate: 200, + }); + + const buildTableNode = useCallback((database: DatabaseSchema) => { + const children = + database?.tables?.map(table => { + return buildTableColumnNode([database.dbName], table); + }) || []; + + return buildAntdTreeNodeModel([], database.dbName, children, false); + }, []); + + const buildTableColumnNode = (ancestors: string[] = [], table) => { + const children = + table?.columns?.map(column => { + return buildAntdTreeNodeModel( + ancestors.concat(table.tableName), + column?.name, + [], + true, + ); + }) || []; + return buildAntdTreeNodeModel(ancestors, table.tableName, children, false); + }; + + const databaseTreeModel = useMemo(() => { + return (databaseSchemas || []).map(buildTableNode); + }, [buildTableNode, databaseSchemas]); const { filteredData, onExpand, debouncedSearch, expandedRowKeys } = useSearchAndExpand( - databases, + databaseTreeModel, (keywords, data) => (data.title as string).includes(keywords), DEFAULT_DEBOUNCE_WAIT, true, ); - useEffect(() => { - if (sourceId && !databases) { - dispatch(getDataProviderDatabases(sourceId)); - } - }, [dispatch, sourceId, databases]); useEffect(() => { - if (databases && editorCompletionItemProviderRef) { + if (databaseTreeModel && editorCompletionItemProviderRef) { editorCompletionItemProviderRef.current?.dispose(); dispatch( getEditorProvideCompletionItems({ @@ -92,54 +114,7 @@ export const Resource = memo(() => { }), ); } - }, [dispatch, sourceId, databases, editorCompletionItemProviderRef]); - - const loadData = useCallback( - ({ value }) => - new Promise((resolve, reject) => { - try { - const [database, table] = value; - if (table) { - request( - `/data-provider/${sourceId}/${database}/${table}/columns`, - ).then(({ data }) => { - dispatch( - actions.addSchema({ - sourceId, - databaseName: database, - tableName: table, - schema: data, - }), - ); - resolve(); - }); - } else { - request( - `/data-provider/${sourceId}/${database}/tables`, - ).then(({ data }) => { - dispatch( - actions.addTables({ - sourceId, - databaseName: database, - tables: data.sort((a, b) => - a.toLowerCase() < b.toLowerCase() - ? -1 - : a.toLowerCase() > b.toLowerCase() - ? 1 - : 0, - ), - }), - ); - resolve(); - }); - } - } catch (error) { - errorHandle(error); - reject(); - } - }), - [dispatch, sourceId, actions], - ); + }, [dispatch, sourceId, databaseTreeModel, editorCompletionItemProviderRef]); const renderIcon = useCallback(({ value }) => { if (Array.isArray(value)) { @@ -162,8 +137,7 @@ export const Resource = memo(() => { }, []); return ( - - + { /> - + @@ -191,15 +165,6 @@ export const Resource = memo(() => { ); }); -const Container = styled.div` - display: flex; - flex: 1; - flex-direction: column; - width: ${SPACE_TIMES(100)}; - min-height: 0; - border-left: 1px solid ${p => p.theme.borderColorSplit}; -`; - const Searchbar = styled(Row)` .input { padding-bottom: ${SPACE_XS}; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Variables.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Variables.tsx index 529fa7392..255ad420b 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Variables.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Variables.tsx @@ -25,7 +25,7 @@ import { TeamOutlined, } from '@ant-design/icons'; import { Button, List, Popconfirm } from 'antd'; -import { ListItem, ListTitle } from 'app/components'; +import { ListItem } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { getRoles } from 'app/pages/MainPage/pages/MemberPage/slice/thunks'; import { @@ -56,7 +56,7 @@ import { import { monaco } from 'react-monaco-editor'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; -import { SPACE_MD, SPACE_TIMES, SPACE_XS } from 'styles/StyleConstants'; +import { SPACE_MD, SPACE_XS } from 'styles/StyleConstants'; import { errorHandle, uuidv4 } from 'utils/utils'; import { selectVariables } from '../../../VariablePage/slice/selectors'; import { getVariables } from '../../../VariablePage/slice/thunks'; @@ -67,6 +67,7 @@ import { selectCurrentEditingViewAttr } from '../../slice/selectors'; import { getEditorProvideCompletionItems } from '../../slice/thunks'; import { VariableHierarchy } from '../../slice/types'; import { comparePermissionChange } from '../../utils'; +import Container from './Container'; export const Variables = memo(() => { const { actions } = useViewSlice(); @@ -293,7 +294,7 @@ export const Variables = memo(() => { const titleProps = useMemo( () => ({ - title: t('title'), + title: 'variable', search: true, add: { items: [{ key: 'variable', text: t('add') }], @@ -304,8 +305,7 @@ export const Variables = memo(() => { ); return ( - - + { ); }); -const Container = styled.div` - display: flex; - flex: 1; - flex-direction: column; - width: ${SPACE_TIMES(100)}; - min-height: 0; - border-left: 1px solid ${p => p.theme.borderColorSplit}; -`; - const ListWrapper = styled.div` flex: 1; padding-bottom: ${SPACE_MD}; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/index.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/index.tsx index 3a84c12a7..8b2177f09 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/index.tsx @@ -17,13 +17,14 @@ */ import { + ApartmentOutlined, DatabaseOutlined, FunctionOutlined, SafetyCertificateOutlined, } from '@ant-design/icons'; import { PaneWrapper } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; -import React, { +import { memo, useCallback, useContext, @@ -32,8 +33,10 @@ import React, { useState, } from 'react'; import styled from 'styled-components/macro'; +import { STICKY_LEVEL } from 'styles/StyleConstants'; import { EditorContext } from '../../EditorContext'; import { ColumnPermissions } from './ColumnPermissions'; +import DataModelTree from './DataModelTree/DataModelTree'; import { Resource } from './Resource'; import { Variables } from './Variables'; import { VerticalTabs } from './VerticalTabs'; @@ -55,6 +58,7 @@ export const Properties = memo(({ allowManage }: PropertiesProps) => { () => [ { name: 'reference', title: t('reference'), icon: }, { name: 'variable', title: t('variable'), icon: }, + { name: 'model', title: t('model'), icon: }, { name: 'columnPermissions', title: t('columnPermissions'), @@ -76,6 +80,9 @@ export const Properties = memo(({ allowManage }: PropertiesProps) => { + + + @@ -88,4 +95,5 @@ const Container = styled.div` display: flex; flex-shrink: 0; background-color: ${p => p.theme.componentBackground}; + z-index: ${STICKY_LEVEL}; `; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Tabs.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Tabs.tsx index 097a0dcd5..0949b7b9e 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Tabs.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Tabs.tsx @@ -25,7 +25,7 @@ import { Button, Space } from 'antd'; import { Confirm, TabPane, Tabs as TabsComponent } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; -import React, { memo, useCallback, useContext, useState } from 'react'; +import { memo, useCallback, useContext, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import styled, { css } from 'styled-components/macro'; @@ -137,13 +137,7 @@ export const Tabs = memo(() => { - } + icon={} footer={ diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Workbench.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Workbench.tsx index 0870135e2..55fa36d41 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Workbench.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Workbench.tsx @@ -26,7 +26,7 @@ import React, { useEffect, useMemo, } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { getPath } from 'utils/utils'; import { @@ -36,11 +36,13 @@ import { import { UNPERSISTED_ID_PREFIX } from '../constants'; import { EditorContext } from '../EditorContext'; import { selectCurrentEditingViewAttr, selectViews } from '../slice/selectors'; +import { getSchemaBySourceId } from '../slice/thunks'; import { Editor } from './Editor'; import { Outputs } from './Outputs'; import { Properties } from './Properties'; export const Workbench = memo(() => { + const dispatch = useDispatch(); const { editorInstance } = useContext(EditorContext); const views = useSelector(selectViews); const id = useSelector(state => @@ -49,6 +51,15 @@ export const Workbench = memo(() => { const parentId = useSelector(state => selectCurrentEditingViewAttr(state, { name: 'parentId' }), ) as string; + const sourceId = useSelector(state => + selectCurrentEditingViewAttr(state, { name: 'sourceId' }), + ) as string; + + useEffect(() => { + if (sourceId) { + dispatch(getSchemaBySourceId(sourceId)); + } + }, [dispatch, sourceId]); const path = useMemo( () => diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/SaveForm.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/SaveForm.tsx index 6f717bc95..a7843bcf2 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/SaveForm.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/SaveForm.tsx @@ -19,6 +19,7 @@ import { DoubleRightOutlined } from '@ant-design/icons'; import { Button, + Checkbox, Form, FormInstance, Input, @@ -29,6 +30,7 @@ import { } from 'antd'; import { ModalForm, ModalFormProps } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { APP_CURRENT_VERSION } from 'app/migration/constants'; import debounce from 'debounce-promise'; import { DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; import { @@ -64,7 +66,7 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { const [concurrencyControl, setConcurrencyControl] = useState(true); const [cache, setCache] = useState(false); const selectViewFolderTree = useMemo(makeSelectViewFolderTree, []); - + const [expensiveQuery, setExpensiveQuery] = useState(false); // beta.2 add expensiveQuery const { type, visible, @@ -114,7 +116,13 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { const save = useCallback( values => { - onSave(values, onCancel); + onSave( + { + ...values, + config: { version: APP_CURRENT_VERSION, ...values.config }, + }, + onCancel, + ); }, [onSave, onCancel], ); @@ -125,12 +133,14 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { setConcurrencyControl(true); setCache(false); onAfterClose && onAfterClose(); + setExpensiveQuery(false); }, [onAfterClose]); return ( - {!simple && ( + {!simple && initialValues?.config && ( <> + + {t('expensiveQuery')} + )} diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/FolderTree.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/FolderTree.tsx index 69a712130..0d31da9f7 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/FolderTree.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/FolderTree.tsx @@ -119,7 +119,7 @@ export const FolderTree = memo(({ treeData }: FolderTreeProps) => { ); const moreMenuClick = useCallback( - ({ id, name, parentId, index }) => + ({ id, name, parentId, index, isFolder }) => ({ key, domEvent }) => { domEvent.stopPropagation(); switch (key) { @@ -127,7 +127,7 @@ export const FolderTree = memo(({ treeData }: FolderTreeProps) => { showSaveForm({ type: CommonFormTypes.Edit, visible: true, - simple: true, + simple: isFolder, initialValues: { id, name, diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/index.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/index.tsx index 9e687e13c..b37b8f0ca 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/index.tsx @@ -228,20 +228,23 @@ const Wrapper = styled.div<{ isDragging: boolean; width: number; }>` - transition: ${p => (!p.isDragging ? 'width 0.3s ease' : 'none')}; height: 100%; + transition: ${p => (!p.isDragging ? 'width 0.3s ease' : 'none')}; &.close { - width: ${SPACE_TIMES(7.5)} !important; - border-right: 1px solid #e9ecef; position: absolute; + width: ${SPACE_TIMES(7.5)} !important; height: 100%; background: #fff; + border-right: 1px solid #e9ecef; .menuUnfoldOutlined { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } + > div { + display: ${p => (p.sliderVisible ? 'none' : 'flex')}; + } &:hover { width: ${p => p.width + '%'} !important; .menuUnfoldOutlined { @@ -255,18 +258,15 @@ const Wrapper = styled.div<{ } } } - > div { - display: ${p => (p.sliderVisible ? 'none' : 'flex')}; - } `; const ListNavWrapper = styled(ListNav)` + position: relative; + z-index: ${NAV_LEVEL}; display: flex; flex-direction: column; flex-shrink: 0; + height: 100%; padding: ${SPACE_XS} 0; background-color: ${p => p.theme.componentBackground}; border-right: 1px solid ${p => p.theme.borderColorSplit}; - height: 100%; - position: relative; - z-index: ${NAV_LEVEL}; `; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/__tests__/utils.test.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/__tests__/utils.test.ts new file mode 100644 index 000000000..be44a4a13 --- /dev/null +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/__tests__/utils.test.ts @@ -0,0 +1,233 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { ColumnTypes } from '../constants'; +import { Column, ColumnRole } from '../slice/types'; +import { dataModelColumnSorter, diffMergeHierarchyModel } from '../utils'; + +describe('dataModelColumnSorter test', () => { + test('should sort by alphabet with the STRING column type', () => { + const columns: Column[] = [ + { name: 'c', type: ColumnTypes.String }, + { name: 'b', type: ColumnTypes.String }, + { name: 'a', type: ColumnTypes.String }, + ]; + expect(columns.sort(dataModelColumnSorter)[0].name).toEqual('a'); + expect(columns.sort(dataModelColumnSorter)[1].name).toEqual('b'); + expect(columns.sort(dataModelColumnSorter)[2].name).toEqual('c'); + }); + + test('should sort by alphabet with the Numeric column type', () => { + const columns: Column[] = [ + { name: 'c', type: ColumnTypes.Number }, + { name: 'b', type: ColumnTypes.Number }, + { name: 'a', type: ColumnTypes.Number }, + ]; + expect(columns.sort(dataModelColumnSorter)[0].name).toEqual('a'); + expect(columns.sort(dataModelColumnSorter)[1].name).toEqual('b'); + expect(columns.sort(dataModelColumnSorter)[2].name).toEqual('c'); + }); + + test('should sort by alphabet with string and date column type', () => { + const columns: Column[] = [ + { name: 'c', type: ColumnTypes.String }, + { name: 'b', type: ColumnTypes.Date }, + { name: 'a', type: ColumnTypes.Date }, + ]; + expect(columns.sort(dataModelColumnSorter)[0].name).toEqual('a'); + expect(columns.sort(dataModelColumnSorter)[1].name).toEqual('b'); + expect(columns.sort(dataModelColumnSorter)[2].name).toEqual('c'); + }); + + test('should sort by column type when column type with STRING, Numeric, DATE', () => { + const columns: Column[] = [ + { name: 'c', type: ColumnTypes.String }, + { name: 'b', type: ColumnTypes.Number }, + { name: 'a', type: ColumnTypes.Date }, + { name: 'd', type: ColumnTypes.Date }, + { name: 'e', type: ColumnTypes.Number }, + { name: 'f', type: ColumnTypes.String }, + ]; + expect(columns.sort(dataModelColumnSorter)[0].name).toEqual('a'); + expect(columns.sort(dataModelColumnSorter)[1].name).toEqual('c'); + expect(columns.sort(dataModelColumnSorter)[2].name).toEqual('d'); + expect(columns.sort(dataModelColumnSorter)[3].name).toEqual('f'); + expect(columns.sort(dataModelColumnSorter)[4].name).toEqual('b'); + expect(columns.sort(dataModelColumnSorter)[5].name).toEqual('e'); + }); + + test('should sort by column type with multiple column types and hierarchy columns', () => { + const columns: Column[] = [ + { name: 'e', type: ColumnTypes.String, role: ColumnRole.Hierarchy }, + { name: 'c', type: ColumnTypes.String }, + { name: 'b', type: ColumnTypes.Number }, + { name: 'a', type: ColumnTypes.Date }, + { name: 'f', type: ColumnTypes.Date, role: ColumnRole.Hierarchy }, + ]; + expect(columns.sort(dataModelColumnSorter)[0].name).toEqual('e'); + expect(columns.sort(dataModelColumnSorter)[1].name).toEqual('f'); + expect(columns.sort(dataModelColumnSorter)[2].name).toEqual('a'); + expect(columns.sort(dataModelColumnSorter)[3].name).toEqual('c'); + expect(columns.sort(dataModelColumnSorter)[4].name).toEqual('b'); + }); +}); + +describe('diffMergeHierarchyModel test', () => { + test('should append all new column to hierarchy without children', () => { + const model = { + columns: { + id: { name: 'id', type: 'STRING' }, + age: { name: 'age', type: 'NUMBER' }, + }, + hierarchy: {}, + }; + expect(diffMergeHierarchyModel(model as any)).toMatchObject({ + columns: { + id: { name: 'id', type: 'STRING' }, + age: { name: 'age', type: 'NUMBER' }, + }, + hierarchy: { + id: { name: 'id', type: 'STRING' }, + age: { name: 'age', type: 'NUMBER' }, + }, + }); + }); + + test('should append new column to hierarchy without children', () => { + const model = { + columns: { + id: { name: 'id', type: 'STRING' }, + age: { name: 'age', type: 'NUMBER' }, + address: { name: 'address', type: 'STRING' }, + }, + hierarchy: { + age: { name: 'age', type: 'NUMBER' }, + }, + }; + expect(diffMergeHierarchyModel(model as any)).toMatchObject({ + columns: model.columns, + hierarchy: { + id: { name: 'id', type: 'STRING' }, + age: { name: 'age', type: 'NUMBER' }, + address: { name: 'address', type: 'STRING' }, + }, + }); + }); + + test('should remove column in hierarchy which not exist in columns', () => { + const model = { + columns: { + id: { name: 'id', type: 'STRING' }, + }, + hierarchy: { + id: { name: 'id', type: 'STRING' }, + age: { name: 'age', type: 'NUMBER' }, + address: { name: 'address', type: 'STRING' }, + }, + }; + expect(diffMergeHierarchyModel(model as any)).toMatchObject({ + columns: model.columns, + hierarchy: { + id: { name: 'id', type: 'STRING' }, + }, + }); + }); + + test('should remove child column in hierarchy', () => { + const model = { + columns: { + id: { name: 'id', type: 'STRING' }, + age: { name: 'age', type: 'NUMBER' }, + }, + hierarchy: { + dealers: { + name: 'dealers', + children: [ + { name: 'id', type: 'STRING' }, + { name: 'age', type: 'NUMBER' }, + { name: 'address', type: 'STRING' }, + ], + }, + }, + }; + expect(diffMergeHierarchyModel(model as any)).toMatchObject({ + columns: model.columns, + hierarchy: { + dealers: { + name: 'dealers', + children: [ + { name: 'id', type: 'STRING' }, + { name: 'age', type: 'NUMBER' }, + ], + }, + }, + }); + }); + + test('should delete branch node in hierarchy when child is not in columns', () => { + const model = { + columns: { + id: { name: 'id', type: 'STRING' }, + age: { name: 'age', type: 'NUMBER' }, + address: { name: 'address', type: 'STRING' }, + }, + hierarchy: { + dealers: { + name: 'dealers', + children: [{ name: 'unkown', type: 'STRING' }], + }, + }, + }; + expect(diffMergeHierarchyModel(model as any)).toMatchObject({ + columns: model.columns, + hierarchy: {}, + }); + }); + + test('should delete and add new to hierarchy model', () => { + const model = { + columns: { + id: { name: 'id', type: 'STRING' }, + age: { name: 'age', type: 'NUMBER' }, + address: { name: 'address', type: 'STRING' }, + newId: { name: 'newId', type: 'STRING' }, + }, + hierarchy: { + age: { name: 'age', type: 'NUMBER' }, + dealers: { + name: 'dealers', + children: [ + { name: 'address', type: 'STRING' }, + { name: 'post', type: 'STRING' }, + ], + }, + }, + }; + expect(diffMergeHierarchyModel(model as any)).toMatchObject({ + columns: model.columns, + hierarchy: { + age: { name: 'age', type: 'NUMBER' }, + newId: { name: 'newId', type: 'STRING' }, + dealers: { + name: 'dealers', + children: [{ name: 'address', type: 'STRING' }], + }, + }, + }); + }); +}); diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/components/SchemaTable.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/components/SchemaTable.tsx index ddea2ecef..5b681392f 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/components/SchemaTable.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/components/SchemaTable.tsx @@ -36,13 +36,14 @@ import { import { uuidv4 } from 'utils/utils'; import { ColumnCategories, ColumnTypes } from '../constants'; import { Column, Model } from '../slice/types'; -import { getColumnWidthMap } from '../utils'; +import { getColumnWidthMap, getHierarchyColumn } from '../utils'; const ROW_KEY = 'DATART_ROW_KEY'; interface SchemaTableProps extends TableProps { height: number; width: number; model: Model; + hierarchy: Model; dataSource?: object[]; hasCategory?: boolean; getExtraHeaderActions?: ( @@ -60,6 +61,7 @@ export const SchemaTable = memo( height, width: propsWidth, model, + hierarchy, dataSource, hasCategory, getExtraHeaderActions, @@ -86,11 +88,13 @@ export const SchemaTable = memo( } = useMemo(() => { let tableWidth = 0; const columns = Object.entries(model).map(([name, column]) => { + const hierarchyColumn = getHierarchyColumn(name, hierarchy) || column; + const width = columnWidthMap[name]; tableWidth += width; let icon; - switch (column.type) { + switch (hierarchyColumn.type) { case ColumnTypes.Number: icon = ; break; @@ -103,7 +107,7 @@ export const SchemaTable = memo( } const extraActions = - getExtraHeaderActions && getExtraHeaderActions(name, column); + getExtraHeaderActions && getExtraHeaderActions(name, hierarchyColumn); const title = ( <> @@ -112,9 +116,12 @@ export const SchemaTable = memo( trigger={['click']} overlay={ {Object.values(ColumnTypes).map(t => ( @@ -166,6 +173,7 @@ export const SchemaTable = memo( return { columns, tableWidth }; }, [ model, + hierarchy, columnWidthMap, hasCategory, getExtraHeaderActions, diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/hooks/useSaveAsView.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/hooks/useSaveAsView.tsx index f53316257..85d501b7b 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/hooks/useSaveAsView.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/hooks/useSaveAsView.tsx @@ -16,6 +16,7 @@ * limitations under the License. */ import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { migrateViewConfig } from 'app/migration/ViewConfig/migrationViewDetailConfig'; import { CommonFormTypes } from 'globalConstants'; import { useCallback, useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -38,6 +39,7 @@ export function useSaveAsView() { const getViewData = useCallback(async (viewId): Promise => { try { const { data } = await request(`/views/${viewId}`); + data.config = migrateViewConfig(data.config); return data; } catch (error) { errorHandle(error); @@ -59,7 +61,7 @@ export function useSaveAsView() { initialValues: { name: name + '_' + tg('copy'), parentId, - config: config, + config, }, parentIdLabel: t('folder'), onSave: (values, onClose) => { diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/hooks/useStartAnalysis.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/hooks/useStartAnalysis.tsx index 17f5bd124..8e5668397 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/hooks/useStartAnalysis.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/hooks/useStartAnalysis.tsx @@ -28,12 +28,7 @@ export function useStartAnalysis() { viewId => { history.push({ pathname: `/organizations/${orgId}/vizs/chartEditor`, - state: { - dataChartId: '', - chartType: 'dataChart', - container: 'dataChart', - defaultViewId: viewId, - }, + search: `dataChartId=&chartType=dataChart&container=dataChart&defaultViewId=${viewId}`, }); }, [history, orgId], diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/index.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/index.ts index 9a073fd4a..f5e233764 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/index.ts +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/index.ts @@ -19,11 +19,17 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { getDataProviderDatabases } from 'app/pages/MainPage/slice/thunks'; import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; +import { isMySliceRejectedAction } from 'utils/@reduxjs/toolkit'; +import { rejectedActionMessageHandler } from 'utils/notification'; import { ViewViewModelStages } from '../constants'; -import { transformQueryResultToModelAndDataSource } from '../utils'; +import { + diffMergeHierarchyModel, + transformQueryResultToModelAndDataSource, +} from '../utils'; import { deleteView, getArchivedViews, + getSchemaBySourceId, getViewDetail, getViews, runSql, @@ -42,8 +48,10 @@ export const initialState: ViewState = { editingViews: [], currentEditingView: '', sourceDatabases: {}, + sourceDatabaseSchema: {}, saveViewLoading: false, unarchiveLoading: false, + databaseSchemaLoading: false, }; const slice = createSlice({ @@ -224,7 +232,7 @@ const slice = createSlice({ action.payload, currentEditingView.model, ); - currentEditingView.model = model; + currentEditingView.model = diffMergeHierarchyModel(model); currentEditingView.previewResults = dataSource; if (!action.meta.arg.isFragment) { currentEditingView.stage = ViewViewModelStages.Saveable; @@ -392,6 +400,27 @@ const slice = createSlice({ value: [name], })); }); + + // getSchemaBySourceId + builder.addCase(getSchemaBySourceId.pending, state => { + state.databaseSchemaLoading = true; + }); + builder.addCase(getSchemaBySourceId.rejected, state => { + state.databaseSchemaLoading = false; + }); + builder.addCase(getSchemaBySourceId.fulfilled, (state, action) => { + state.databaseSchemaLoading = false; + if (!action.payload?.data?.schemaItems) { + return; + } + state.sourceDatabaseSchema[action.payload?.sourceId] = + action.payload.data.schemaItems; + }); + + builder.addMatcher( + isMySliceRejectedAction(slice.name), + rejectedActionMessageHandler, + ); }, }); diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/selectors.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/selectors.ts index 319d7255e..ef22e117e 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/selectors.ts +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/selectors.ts @@ -22,6 +22,7 @@ import { listToTree } from 'utils/utils'; import { initialState } from '.'; import { ResourceTypes } from '../../PermissionPage/constants'; import { + DatabaseSchema, SelectViewFolderTreeProps, SelectViewTreeProps, ViewViewModel, @@ -94,6 +95,11 @@ export const selectSourceDatabases = createSelector( viewState => viewState.sourceDatabases, ); +export const selectSourceDatabaseSchemas = createSelector( + [selectDomain, (_, props: { id: string }) => props.id], + (viewState, id) => viewState.sourceDatabaseSchema?.[id], +); + export const selectDatabases = createSelector( [selectSourceDatabases, (_, props: { name?: string }) => props.name], (sourceDatabases, name) => (name ? sourceDatabases[name] : void 0), @@ -108,3 +114,8 @@ export const selectArchivedListLoading = createSelector( [selectDomain], viewState => viewState.archivedListLoading, ); + +export const selectDatabaseSchemaLoading = createSelector( + [selectDomain], + viewState => viewState.databaseSchemaLoading, +); diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/thunks.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/thunks.ts index 319f50edb..21714404f 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/thunks.ts +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/thunks.ts @@ -18,11 +18,13 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import sqlReservedWords from 'app/assets/javascripts/sqlReservedWords'; +import { migrateViewConfig } from 'app/migration/ViewConfig/migrationViewDetailConfig'; +import beginViewModelMigration from 'app/migration/ViewConfig/migrationViewModelConfig'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import i18n from 'i18next'; import { monaco } from 'react-monaco-editor'; import { RootState } from 'types'; -import { request } from 'utils/request'; +import { request, request2 } from 'utils/request'; import { errorHandle, rejectHandle } from 'utils/utils'; import { viewActions } from '.'; import { View } from '../../../../../types/View'; @@ -40,8 +42,8 @@ import { selectCurrentEditingView, selectCurrentEditingViewAttr, selectCurrentEditingViewKey, - selectDatabases, selectEditingViews, + selectSourceDatabaseSchemas, selectViews, } from './selectors'; import { @@ -121,6 +123,8 @@ export const getViewDetail = createAsyncThunk< try { const { data } = await request(`/views/${viewId}`); + data.config = migrateViewConfig(data.config); + data.model = beginViewModelMigration(data?.model); return transformModelToViewModel(data, tempViewModel); } catch (error) { return rejectHandle(error, rejectWithValue); @@ -128,6 +132,26 @@ export const getViewDetail = createAsyncThunk< }, ); +export const getSchemaBySourceId = createAsyncThunk( + 'source/getSchemaBySourceId', + async (sourceId, { getState }) => { + const sourceSchemas = selectSourceDatabaseSchemas(getState() as RootState, { + id: sourceId, + }); + if (sourceSchemas) { + return; + } + const { data } = await request2({ + url: `/sources/schemas/${sourceId}`, + method: 'GET', + }); + return { + sourceId, + data, + }; + }, +); + export const runSql = createAsyncThunk< QueryResult, { id: string; isFragment: boolean }, @@ -176,7 +200,7 @@ export const saveView = createAsyncThunk< SaveViewParams, { state: RootState } >('view/saveView', async ({ resolve, isSaveAs, currentView }, { getState }) => { - const currentEditingView = isSaveAs + let currentEditingView = isSaveAs ? (currentView as ViewViewModel) : (selectCurrentEditingView(getState()) as ViewViewModel); const orgId = selectOrgId(getState()); @@ -334,13 +358,15 @@ export const getEditorProvideCompletionItems = createAsyncThunk< const variableKeywords = new Set(); if (sourceId) { - const databases = selectDatabases(getState(), { name: sourceId }); - databases?.forEach(db => { - dbKeywords.add(db.title as string); - db.children?.forEach(table => { - tableKeywords.add(table.title as string); - table.children?.forEach(column => { - schemaKeywords.add(column.title as string); + const currentDBSchemas = selectSourceDatabaseSchemas(getState(), { + id: sourceId, + }); + currentDBSchemas?.forEach(db => { + dbKeywords.add(db.dbName); + db.tables?.forEach(table => { + tableKeywords.add(table.tableName); + table.columns?.forEach(column => { + schemaKeywords.add(column.name as string); }); }); }); diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/types.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/types.ts index a258d2d4d..d5a41c00a 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/types.ts +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/types.ts @@ -38,9 +38,27 @@ export interface ViewState { sourceDatabases: { [name: string]: TreeDataNode[]; }; + sourceDatabaseSchema: { + [name: string]: DatabaseSchema[]; + }; saveViewLoading: boolean; unarchiveLoading: boolean; -} + databaseSchemaLoading: boolean; +} + +export type DatabaseSchema = { + dbName: string; + tables: Array<{ + primaryKeys: string[]; + tableName: string; + columns: Array<{ + fmt: string; + foreignKeys: Array<{ column: string; database: string; table: string }>; + name: string; + type: string; + }>; + }>; +}; export interface ViewBase { id: string; @@ -65,7 +83,7 @@ export interface ViewViewModel description?: string; index: number | null; isFolder?: boolean; - model: Model; + model: HierarchyModel; config: object; originVariables: VariableHierarchy[]; variables: VariableHierarchy[]; @@ -102,14 +120,29 @@ export interface Schema { type: ColumnTypes; } +export enum ColumnRole { + Role = 'role', + Hierarchy = 'hierachy', +} + export interface Column extends Schema { - category: ColumnCategories; + category?: ColumnCategories; + index?: number; + + role?: ColumnRole; + children?: Column[]; } export interface Model { - [key: string]: Omit; + [key: string]: Column; } +export type HierarchyModel = { + version?: string; + hierarchy?: Model; + columns?: Model; +}; + export interface ColumnPermissionRaw { id: string; viewId: string; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/utils.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/utils.tsx index 5ae16de47..e7ed60696 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/utils.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/utils.tsx @@ -16,15 +16,27 @@ * limitations under the License. */ +import { TreeDataNode } from 'antd'; +import { APP_CURRENT_VERSION } from 'app/migration/constants'; import { FONT_WEIGHT_MEDIUM, SPACE_UNIT } from 'styles/StyleConstants'; +import { Nullable } from 'types'; +import { isEmptyArray } from 'utils/object'; import { getDiffParams, getTextWidth } from 'utils/utils'; import { ColumnCategories, + ColumnTypes, DEFAULT_PREVIEW_SIZE, UNPERSISTED_ID_PREFIX, ViewViewModelStages, } from './constants'; -import { Column, Model, QueryResult, ViewViewModel } from './slice/types'; +import { + Column, + ColumnRole, + HierarchyModel, + Model, + QueryResult, + ViewViewModel, +} from './slice/types'; export function generateEditingView( attrs?: Partial, @@ -36,7 +48,9 @@ export function generateEditingView( index: null, script: '', config: {}, - model: {}, + model: { + version: APP_CURRENT_VERSION, + }, originVariables: [], variables: [], originColumnPermissions: [], @@ -80,30 +94,50 @@ export function isNewView(id: string | undefined): boolean { export function transformQueryResultToModelAndDataSource( data: QueryResult, - lastModel: Model, + lastModel: HierarchyModel, ): { - model: Model; + model: HierarchyModel; dataSource: object[]; } { const { rows, columns } = data; - const model = columns.reduce( - (obj, { name, type, primaryKey }) => ({ + const newColumns = columns.reduce((obj, { name, type, primaryKey }) => { + const hierarchyColumn = getHierarchyColumn( + name, + lastModel?.hierarchy || {}, + ); + return { ...obj, [name]: { - type: lastModel[name]?.type || type, + type: hierarchyColumn?.type || type, primaryKey, - category: lastModel[name]?.category || ColumnCategories.Uncategorized, // FIXME: model 重构时一起改 + category: hierarchyColumn?.category || ColumnCategories.Uncategorized, // FIXME: model 重构时一起改 }, - }), - {}, - ); + }; + }, {}); const dataSource = rows.map(arr => arr.reduce( (obj, val, index) => ({ ...obj, [columns[index].name]: val }), {}, ), ); - return { model, dataSource }; + return { + model: { ...lastModel, columns: newColumns }, + dataSource, + }; +} + +export function getHierarchyColumn( + columnName: string, + hierarchyModel: Model, +): Nullable { + return Object.entries(hierarchyModel) + .flatMap(([name, value]) => { + if (!isEmptyArray(value.children)) { + return value.children; + } + return value; + }) + ?.find(col => col?.name === columnName); } export function getColumnWidthMap( @@ -293,3 +327,76 @@ export function transformModelToViewModel( })), }; } + +export const dataModelColumnSorter = (prev: Column, next: Column): number => { + const columnTypePriority = { + [ColumnTypes.Date]: 1, + [ColumnTypes.String]: 1, + [ColumnTypes.Number]: 2, + }; + const hierarchyPriority = { + [ColumnRole.Hierarchy]: 10, + [ColumnRole.Role]: 100, + }; + const calcPriority = (column: Column) => { + return ( + columnTypePriority[column?.type || ColumnTypes.String] * + hierarchyPriority[column?.role || ColumnRole.Role] + ); + }; + return ( + calcPriority(prev) - calcPriority(next) || + (prev?.name || '').localeCompare(next?.name || '') + ); +}; + +export const diffMergeHierarchyModel = (model: HierarchyModel) => { + const hierarchy = model?.hierarchy || {}; + const columns = model?.columns || {}; + const allHierarchyColumnNames = Object.keys(hierarchy).flatMap(name => { + if (!isEmptyArray(hierarchy[name].children)) { + return hierarchy[name].children!.map(child => child.name); + } + return name; + }); + const additionalObjs = Object.keys(columns).reduce((acc, name) => { + if (allHierarchyColumnNames.includes(name)) { + return acc; + } + acc[name] = columns[name]; + return acc; + }, {}); + const newHierarchy = Object.keys(hierarchy).reduce((acc, name) => { + if (name in columns) { + acc[name] = hierarchy[name]; + } else if (!isEmptyArray(hierarchy[name]?.children)) { + const hierarchyColumn = hierarchy[name]; + hierarchyColumn.children = hierarchyColumn.children?.filter(child => + Object.keys(columns).includes(child.name), + ); + if (hierarchyColumn.children?.length) { + acc[name] = hierarchyColumn; + } + } + return acc; + }, additionalObjs); + model.hierarchy = newHierarchy; + return model; +}; + +export function buildAntdTreeNodeModel( + ancestors: string[] = [], + nodeName: string, + children?: T[], + isLeaf?: boolean, +): T { + const TREE_HIERARCHY_SEPERATOR = String.fromCharCode(0); + const fullNames = ancestors.concat(nodeName); + return { + key: fullNames.join(TREE_HIERARCHY_SEPERATOR), + title: nodeName, + value: fullNames, + children, + isLeaf, + } as any; +} diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/ChartPreview.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/ChartPreview.tsx index 8557aafc5..ecbc706dd 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/ChartPreview.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/ChartPreview.tsx @@ -158,11 +158,7 @@ const ChartPreviewBoard: FC<{ const handleGotoWorkbenchPage = () => { history.push({ pathname: `/organizations/${orgId}/vizs/chartEditor`, - state: { - dataChartId: backendChartId, - chartType: 'dataChart', - container: 'dataChart', - }, + search: `dataChartId=${backendChartId}&chartType=dataChart&container=dataChart`, }); }; @@ -203,9 +199,20 @@ const ChartPreviewBoard: FC<{ false, chartPreview?.backendChart?.config?.aggregation, ); + dispatch( makeDownloadDataTask({ - downloadParams: [builder.build()], + downloadParams: [ + { + ...builder.build(), + ...{ + vizId: chartPreview?.backendChart?.id, + vizName: chartPreview?.backendChart?.name, + analytics: false, + vizType: 'dataChart', + }, + }, + ], fileName: chartPreview?.backendChart?.name || 'chart', resolve: () => { dispatch(actions.setDownloadPolling(true)); @@ -248,7 +255,7 @@ const ChartPreviewBoard: FC<{ }, [dispatch, backendChartId]); const handleAddToDashBoard = useCallback( - dashboardId => { + (dashboardId, dashboardType) => { const currentChartPreview = previewCharts.find( c => c.backendChartId === backendChartId, ); @@ -261,6 +268,7 @@ const ChartPreviewBoard: FC<{ chartType: '', dataChart: currentChartPreview?.backendChart, dataview: currentChartPreview?.backendChart?.view, + dashboardType, }), }, }); @@ -349,7 +357,6 @@ const StyledChartPreviewBoard = styled.div` flex: 1; flex-flow: column; height: 100%; - iframe { flex-grow: 1000; } @@ -360,16 +367,16 @@ const PreviewBlock = styled.div` flex-direction: column; height: 100%; padding: ${SPACE_LG}; - box-shadow: ${p => p.theme.shadowBlock}; overflow: hidden; + box-shadow: ${p => p.theme.shadowBlock}; `; const ChartWrapper = styled.div` + position: relative; display: flex; flex: 1; background-color: ${p => p.theme.componentBackground}; border-radius: ${BORDER_RADIUS}; - position: relative; .spinWrapper { width: 100%; height: 100%; diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/ControllerPanel.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/ControllerPanel.tsx index 0c188d175..c21f6a310 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/ControllerPanel.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/ControllerPanel.tsx @@ -17,13 +17,13 @@ */ import { Col, Form, Row } from 'antd'; -import { ChartDTO } from "app/types/ChartDTO"; import { ChartConfig, ChartDataSectionField, ChartDataSectionType, FilterFieldAction, } from 'app/types/ChartConfig'; +import { ChartDTO } from 'app/types/ChartDTO'; import { ControllerFacadeTypes, ControllerVisibilityTypes, diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/DropdownListFilter.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/DropdownListFilter.tsx index 22eaf4300..a0b6ecb4b 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/DropdownListFilter.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/DropdownListFilter.tsx @@ -17,7 +17,7 @@ */ import { Select } from 'antd'; -import useFetchFilterDataByCondtion from 'app/hooks/useFetchFilterDataByCondtion'; +import useFetchFilterDataByCondition from 'app/hooks/useFetchFilterDataByCondtion'; import { RelationFilterValue } from 'app/types/ChartConfig'; import { updateBy } from 'app/utils/mutation'; import { FC, memo, useState } from 'react'; @@ -42,7 +42,7 @@ const DropdownListFilter: FC = memo( } }); - useFetchFilterDataByCondtion(viewId, condition, setOriginalNodes, view); + useFetchFilterDataByCondition(viewId, condition, setOriginalNodes, view); const handleSelectedChange = value => { const newCondition = updateBy(condition!, draft => { diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/MultiDropdownListFilter.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/MultiDropdownListFilter.tsx index 77094b054..2b660f09e 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/MultiDropdownListFilter.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/MultiDropdownListFilter.tsx @@ -17,7 +17,7 @@ */ import { TreeSelect } from 'antd'; -import useFetchFilterDataByCondtion from 'app/hooks/useFetchFilterDataByCondtion'; +import useFetchFilterDataByCondition from 'app/hooks/useFetchFilterDataByCondtion'; import { RelationFilterValue } from 'app/types/ChartConfig'; import { updateBy } from 'app/utils/mutation'; import { FC, memo, useMemo, useState } from 'react'; @@ -31,7 +31,7 @@ const MultiDropdownListFilter: FC = memo( condition?.value as RelationFilterValue[], ); - useFetchFilterDataByCondtion(viewId, condition, setOriginalNodes, view); + useFetchFilterDataByCondition(viewId, condition, setOriginalNodes, view); const handleSelectedChange = (keys: any) => { const newCondition = updateBy(condition!, draft => { diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RadioGroupFilter.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RadioGroupFilter.tsx index b6c6d5a01..fe69409e2 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RadioGroupFilter.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RadioGroupFilter.tsx @@ -17,7 +17,7 @@ */ import { Radio } from 'antd'; -import useFetchFilterDataByCondtion from 'app/hooks/useFetchFilterDataByCondtion'; +import useFetchFilterDataByCondition from 'app/hooks/useFetchFilterDataByCondtion'; import { RelationFilterValue } from 'app/types/ChartConfig'; import { ControllerRadioFacadeTypes } from 'app/types/FilterControlPanel'; import { updateBy } from 'app/utils/mutation'; @@ -42,7 +42,7 @@ const RadioGroupFilter: FC = memo( } }); - useFetchFilterDataByCondtion(viewId, condition, setOriginalNodes, view); + useFetchFilterDataByCondition(viewId, condition, setOriginalNodes, view); const handleSelectedNodeChange = (nodeKey: string) => { if (selectedNode === nodeKey) { diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangValueFilter.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangValueFilter.tsx index 810b5285a..254c14520 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangValueFilter.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangValueFilter.tsx @@ -68,6 +68,7 @@ export default RangValueFilter; const StyledRangeValueFilter = styled(Row)` .text-center { + line-height: 32px; text-align: center; } diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/index.ts b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/index.ts index 9d1a994e0..630c6baca 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/index.ts +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/index.ts @@ -16,8 +16,8 @@ * limitations under the License. */ -import { ChartDTO } from "app/types/ChartDTO"; import { FilterCondition } from 'app/types/ChartConfig'; +import { ChartDTO } from 'app/types/ChartDTO'; import DropdownListFilter from './DropdownListFilter'; import MultiDropdownListFilter from './MultiDropdownListFilter'; import RadioGroupFilter from './RadioGroupFilter'; diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/Main/index.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/Main/index.tsx index 868ab6aca..489f5243c 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/Main/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/Main/index.tsx @@ -196,7 +196,7 @@ export function Main({ sliderVisible }: { sliderVisible: boolean }) { /> } + render={() => } /> diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/SaveForm.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/SaveForm.tsx index 146e57c2b..0b01a020d 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/SaveForm.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/SaveForm.tsx @@ -1,12 +1,11 @@ import { Form, FormInstance, Input, Radio, TreeSelect } from 'antd'; import { ModalForm, ModalFormProps } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; -import useMount from 'app/hooks/useMount'; import { BoardTypeMap } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import debounce from 'debounce-promise'; import { CommonFormTypes, DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; import { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { request } from 'utils/request'; import { getCascadeAccess } from '../../Access'; @@ -22,7 +21,6 @@ import { selectSaveFolderLoading, selectSaveStoryboardLoading, } from './slice/selectors'; -import { getFolders } from './slice/thunks'; type SaveFormProps = Omit; @@ -45,7 +43,7 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { const formRef = useRef(); const t = useI18NPrefix('viz.saveForm'); const tg = useI18NPrefix('global'); - const dispatch = useDispatch(); + const getDisabled = useCallback( (_, path: string[]) => !getCascadeAccess( @@ -62,16 +60,6 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { selectVizFolderTree(state, { id: initialValues?.id, getDisabled }), ); - useMount(() => { - dispatch(getFolders(orgId)); - }); - - useEffect(() => { - if (initialValues) { - formRef.current?.setFieldsValue(initialValues); - } - }, [initialValues]); - const save = useCallback( values => { onSave(values, onCancel); @@ -84,6 +72,22 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { onAfterClose && onAfterClose(); }, [onAfterClose]); + useEffect(() => { + if (initialValues) { + formRef.current?.setFieldsValue(initialValues); + } + }, [initialValues]); + + const boardTips = () => { + return ( + <> + {t('boardType.autoTips')} +
    + {t('boardType.freeTips')} + + ); + }; + return ( )} {vizType === 'DASHBOARD' && type === CommonFormTypes.Add && ( - + {t('boardType.auto')} diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/index.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/index.tsx index 55ad2d73e..9b3389b22 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/index.tsx @@ -7,7 +7,6 @@ import { ListNav, ListPane, ListTitle } from 'app/components'; import { useDebouncedSearch } from 'app/hooks/useDebouncedSearch'; import useGetVizIcon from 'app/hooks/useGetVizIcon'; import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import { BoardTypeMap } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { dispatchResize } from 'app/utils/dispatchResize'; import { CommonFormTypes } from 'globalConstants'; @@ -17,7 +16,7 @@ import { useHistory } from 'react-router'; import styled from 'styled-components/macro'; import { SPACE_XS } from 'styles/StyleConstants'; import { useAddViz } from '../../hooks/useAddViz'; -import { SaveFormContext, SaveFormModel } from '../../SaveFormContext'; +import { SaveFormContext } from '../../SaveFormContext'; import { makeSelectVizTree, selectArchivedDashboardLoading, @@ -29,7 +28,7 @@ import { getArchivedDashboards, getArchivedDatacharts, } from '../../slice/thunks'; -import { FolderViewModel, VizType } from '../../slice/types'; +import { FolderViewModel } from '../../slice/types'; import { Recycle } from '../Recycle'; import { FolderTree } from './FolderTree'; @@ -55,15 +54,6 @@ export const Folders = memo( const history = useHistory(); const { showSaveForm } = useContext(SaveFormContext); const addVizFn = useAddViz({ showSaveForm }); - const getInitValues = useCallback((relType: VizType) => { - if (relType === 'DASHBOARD') { - return { - name: '', - boardType: BoardTypeMap.auto, - } as SaveFormModel; - } - return undefined; - }, []); const getIcon = useGetVizIcon(); @@ -82,10 +72,10 @@ export const Folders = memo( ); const archivedDatacharts = useSelector(selectArchivedDatacharts); const archivedDashboards = useSelector(selectArchivedDashboards); - const archivedDatachartloading = useSelector( + const archivedDataChartLoading = useSelector( selectArchivedDatachartLoading, ); - const archivedDashboardloading = useSelector( + const archivedDashboardLoading = useSelector( selectArchivedDashboardLoading, ); const { filteredData: filteredListData, debouncedSearch: listSearch } = @@ -104,11 +94,7 @@ export const Folders = memo( if (key === 'DATACHART') { history.push({ pathname: `/organizations/${orgId}/vizs/chartEditor`, - state: { - dataChartId: '', - chartType: 'dataChart', - container: 'dataChart', - }, + search: `dataChartId=&chartType=dataChart&container=dataChart`, }); return false; } @@ -117,10 +103,10 @@ export const Folders = memo( vizType: key, type: CommonFormTypes.Add, visible: true, - initialValues: getInitValues(key), + initialValues: undefined, }); }, - [getInitValues, orgId, history, addVizFn], + [orgId, history, addVizFn], ); const titles = useMemo( @@ -194,7 +180,7 @@ export const Folders = memo( type="viz" orgId={orgId} list={filteredListData} - listLoading={archivedDashboardloading || archivedDatachartloading} + listLoading={archivedDashboardLoading || archivedDataChartLoading} selectedId={selectedId} onInit={recycleInit} /> diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/hooks/useAddViz.ts b/frontend/src/app/pages/MainPage/pages/VizPage/hooks/useAddViz.ts index 577ff984b..16a4999b3 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/hooks/useAddViz.ts +++ b/frontend/src/app/pages/MainPage/pages/VizPage/hooks/useAddViz.ts @@ -15,7 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BoardTypeMap } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { getInitBoardConfig } from 'app/pages/DashBoardPage/utils/board'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { CommonFormTypes } from 'globalConstants'; @@ -50,7 +49,7 @@ export function useAddViz({ showSaveForm }) { if (relType === 'DASHBOARD') { try { dataValues.config = JSON.stringify( - getInitBoardConfig(values.boardType || BoardTypeMap.auto), + getInitBoardConfig(values.boardType), ); } catch (error) { throw error; @@ -76,6 +75,8 @@ export function useAddViz({ showSaveForm }) { ...dataValues, orgId: orgId, index: index, + subType: vizType === 'DASHBOARD' ? dataValues.boardType : null, + avatar: vizType === 'DATACHART' ? initialValues.avatar : null, }, type: vizType, }), diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/index.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/index.tsx index 3a0e8937c..714e491c5 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/index.tsx @@ -74,8 +74,8 @@ export function VizPage() { width={400} formProps={{ labelAlign: 'left', - labelCol: { offset: 1, span: 6 }, - wrapperCol: { span: 15 }, + labelCol: { offset: 1, span: 7 }, + wrapperCol: { span: 14 }, }} okText={tg('button.save')} /> diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/slice/index.ts b/frontend/src/app/pages/MainPage/pages/VizPage/slice/index.ts index 8afdb47bc..6f2f16b30 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/slice/index.ts +++ b/frontend/src/app/pages/MainPage/pages/VizPage/slice/index.ts @@ -31,6 +31,7 @@ import { ArchivedViz, VizState, VizTab } from './types'; import { transferChartConfig } from './utils'; export const initialState: VizState = { vizs: [], + hasVizFetched: false, storyboards: [], vizListLoading: false, storyboardListLoading: false, @@ -114,10 +115,12 @@ const slice = createSlice({ }); builder.addCase(getFolders.fulfilled, (state, action) => { state.vizListLoading = false; + state.hasVizFetched = true; state.vizs = action.payload.map(f => ({ ...f, deleteLoading: false })); }); builder.addCase(getFolders.rejected, state => { state.vizListLoading = false; + state.hasVizFetched = true; }); // getStoryboards diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/slice/selectors.ts b/frontend/src/app/pages/MainPage/pages/VizPage/slice/selectors.ts index fefee8fc5..12aadc82f 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/slice/selectors.ts +++ b/frontend/src/app/pages/MainPage/pages/VizPage/slice/selectors.ts @@ -120,3 +120,8 @@ export const selectPreviewCharts = createSelector( [selectDomain], vizState => vizState.chartPreviews, ); + +export const selectHasVizFetched = createSelector( + [selectDomain], + vizState => vizState.hasVizFetched, +); diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/slice/types.ts b/frontend/src/app/pages/MainPage/pages/VizPage/slice/types.ts index 34904a30b..fbff9bb05 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/slice/types.ts +++ b/frontend/src/app/pages/MainPage/pages/VizPage/slice/types.ts @@ -13,6 +13,7 @@ export type VizType = [ export interface VizState { vizs: FolderViewModel[]; + hasVizFetched: boolean; storyboards: StoryboardViewModel[]; vizListLoading: boolean; storyboardListLoading: boolean; diff --git a/frontend/src/app/pages/SharePage/ChartForShare.tsx b/frontend/src/app/pages/SharePage/ChartForShare.tsx index eff955e46..22dc16c7b 100644 --- a/frontend/src/app/pages/SharePage/ChartForShare.tsx +++ b/frontend/src/app/pages/SharePage/ChartForShare.tsx @@ -132,7 +132,16 @@ const ChartForShare: FC<{ false, chartPreview?.backendChart?.config?.aggregation, ); - const downloadParams = [builder.build()]; + + const downloadParams = [ + { + ...builder.build(), + analytics: false, + vizType: 'dataChart', + vizName: chartPreview?.backendChart?.name || 'chart', + vizId: chartPreview?.backendChart?.id, + }, + ]; const fileName = chartPreview?.backendChart?.name || 'chart'; onCreateDataChartDownloadTask(downloadParams, fileName); }; diff --git a/frontend/src/app/pages/StoryBoardPage/Preview/Preview.tsx b/frontend/src/app/pages/StoryBoardPage/Preview/Preview.tsx index d1f26d010..f3fad70a4 100644 --- a/frontend/src/app/pages/StoryBoardPage/Preview/Preview.tsx +++ b/frontend/src/app/pages/StoryBoardPage/Preview/Preview.tsx @@ -19,6 +19,7 @@ import { Layout, message } from 'antd'; import { Split } from 'app/components'; import usePrefixI18N from 'app/hooks/useI18NPrefix'; import { useSplitSizes } from 'app/hooks/useSplitSizes'; +import { fetchBoardDetail } from 'app/pages/DashBoardPage/pages/Board/slice/thunk'; import { selectPublishLoading } from 'app/pages/MainPage/pages/VizPage/slice/selectors'; import { deleteViz, @@ -69,10 +70,6 @@ export const StoryPagePreview: React.FC<{ makeSelectStoryPagesById(state, storyId), ); - const onCloseEditor = useCallback(() => { - history.push(`/organizations/${orgId}/vizs/${storyId}`); - }, [history, orgId, storyId]); - const sortedPages = useMemo(() => { const sortedPages = Object.values(pageMap).sort( (a, b) => a.config.index - b.config.index, @@ -80,18 +77,6 @@ export const StoryPagePreview: React.FC<{ return sortedPages; }, [pageMap]); - const curPageId = useMemo(() => { - return sortedPages[currentPageIndex]?.id || ''; - }, [currentPageIndex, sortedPages]); - - const toggleEdit = useCallback(() => { - history.push(`/organizations/${orgId}/vizs/${storyId}/storyEditor`); - }, [history, orgId, storyId]); - - const playStory = useCallback(() => { - window.open(`${storyId}/storyPlay`, '_blank'); - }, [storyId]); - const onPageClick = useCallback( (index: number, pageId: string, multiple: boolean) => { setCurrentPageIndex(index); @@ -105,6 +90,27 @@ export const StoryPagePreview: React.FC<{ }, [dispatch, storyId], ); + const currentPage = useMemo(() => { + const currentPage = sortedPages[currentPageIndex]; + return currentPage; + }, [currentPageIndex, sortedPages]); + + const onCloseStoryEditor = useCallback(() => { + history.replace(`/organizations/${orgId}/vizs/${storyId}`); + if (!currentPage?.id) return; + onPageClick(0, currentPage?.id, false); + if (currentPage.relType === 'DASHBOARD' && currentPage.relId) { + dispatch(fetchBoardDetail({ dashboardRelId: currentPage.relId })); + } + }, [currentPage, dispatch, history, onPageClick, orgId, storyId]); + + const toggleEdit = useCallback(() => { + history.push(`/organizations/${orgId}/vizs/${storyId}/storyEditor`); + }, [history, orgId, storyId]); + + const playStory = useCallback(() => { + window.open(`${storyId}/storyPlay`, '_blank'); + }, [storyId]); const onPublish = useCallback(() => { if (storyBoard) { @@ -240,7 +246,7 @@ export const StoryPagePreview: React.FC<{ {sortedPages.map(page => ( - + ( - + )} /> diff --git a/frontend/src/app/pages/StoryBoardPage/slice/actions.ts b/frontend/src/app/pages/StoryBoardPage/slice/actions.ts index af5e2b19e..16ca800db 100644 --- a/frontend/src/app/pages/StoryBoardPage/slice/actions.ts +++ b/frontend/src/app/pages/StoryBoardPage/slice/actions.ts @@ -33,8 +33,8 @@ export const handleServerStoryAction = }) => async dispatch => { const { data, storyId } = params; - const pages = getStoryPage(data.storypages || []); let story = formatStory(data); + const pages = getStoryPage(data.storypages || []); const storyPageMap = getStoryPageMap(pages); const storyPageInfoMap = getInitStoryPageInfoMap(pages); dispatch(storyActions.setStoryBoard(story)); diff --git a/frontend/src/app/pages/StoryBoardPage/slice/thunks.ts b/frontend/src/app/pages/StoryBoardPage/slice/thunks.ts index 407984aa5..36ed2a52a 100644 --- a/frontend/src/app/pages/StoryBoardPage/slice/thunks.ts +++ b/frontend/src/app/pages/StoryBoardPage/slice/thunks.ts @@ -17,13 +17,14 @@ */ import { createAsyncThunk } from '@reduxjs/toolkit'; +import { migrateStoryPageConfig } from 'app/migration/StoryConfig/migrateStoryPageConfig'; import { getBoardDetail } from 'app/pages/DashBoardPage/pages/Board/slice/thunk'; import { selectVizs } from 'app/pages/MainPage/pages/VizPage/slice/selectors'; import { ExecuteToken } from 'app/pages/SharePage/slice/types'; import { RootState } from 'types'; import { request2 } from 'utils/request'; import { storyActions } from '.'; -import { getInitStoryPageConfig, getStoryPageConfig } from '../utils'; +import { getInitStoryPageConfig } from '../utils'; import { handleServerStoryAction } from './actions'; import { makeSelectStoryPagesById } from './selectors'; import { @@ -139,7 +140,7 @@ export const addStoryPage = createAsyncThunk< }); const page = { ...data, - config: getStoryPageConfig(data.config), + config: migrateStoryPageConfig(data.config), } as StoryPage; dispatch(storyActions.addStoryPage(page)); return null; diff --git a/frontend/src/app/pages/StoryBoardPage/slice/types.ts b/frontend/src/app/pages/StoryBoardPage/slice/types.ts index 0c78e9354..09f681164 100644 --- a/frontend/src/app/pages/StoryBoardPage/slice/types.ts +++ b/frontend/src/app/pages/StoryBoardPage/slice/types.ts @@ -36,6 +36,7 @@ export interface ServerStoryBoard extends Omit { storypages?: StoryPageOfServer[]; } export interface StoryConfig { + version: string; autoPlay: { auto: boolean; delay: number; @@ -53,6 +54,7 @@ export interface StoryPageOfServer extends Omit { } export type StoryPageRelType = Extract<'DASHBOARD' | 'DATACHART', VizType>; export interface StoryPageConfig { + version: string; name?: string; thumbnail?: string; index: number; diff --git a/frontend/src/app/pages/StoryBoardPage/utils.ts b/frontend/src/app/pages/StoryBoardPage/utils.ts index 5864c62ad..eb2ff443a 100644 --- a/frontend/src/app/pages/StoryBoardPage/utils.ts +++ b/frontend/src/app/pages/StoryBoardPage/utils.ts @@ -16,6 +16,8 @@ * limitations under the License. */ +import { migrateStoryConfig } from 'app/migration/StoryConfig/migrateStoryConfig'; +import { migrateStoryPageConfig } from 'app/migration/StoryConfig/migrateStoryPageConfig'; import { generateShareLinkAsync } from 'app/utils/fetch'; import { ServerStoryBoard, @@ -28,19 +30,13 @@ import { } from './slice/types'; export const formatStory = (data: ServerStoryBoard) => { - let story = {} as StoryBoard; - delete data.storypages; - let config; - if (data.config && Object.keys(data.config).length > 0) { - config = JSON.parse(data.config); - } else { - config = getInitStoryConfig(); - } - story = { ...data, config: config }; + let config = migrateStoryConfig(data.config); + let story = { ...data, config: config } as StoryBoard; return story; }; export const getInitStoryConfig = (): StoryConfig => { return { + version: '', autoPlay: { auto: false, delay: 1, @@ -73,6 +69,7 @@ export const getInitStoryPageInfo = (id?: string): StoryPageInfo => { }; export const getInitStoryPageConfig = (index?: number): StoryPageConfig => { return { + version: '', name: '', thumbnail: '', index: index || 0, @@ -86,19 +83,9 @@ export const getInitStoryPageConfig = (index?: number): StoryPageConfig => { export const getStoryPage = (pages: StoryPageOfServer[]) => { return pages.map(page => ({ ...page, - config: getStoryPageConfig(page.config), + config: migrateStoryPageConfig(page.config), })); }; -export const getStoryPageConfig = (configStr: string | undefined) => { - if (!configStr) { - return getInitStoryPageConfig(0); - } - try { - return JSON.parse(configStr); - } catch (error) { - return getInitStoryPageConfig(0); - } -}; export const generateShareLink = async ( expireDate, diff --git a/frontend/src/app/share.tsx b/frontend/src/app/share.tsx index 3994aa508..f5f3b0061 100644 --- a/frontend/src/app/share.tsx +++ b/frontend/src/app/share.tsx @@ -23,7 +23,7 @@ import { antdLocales } from 'locales/i18n'; import { Helmet } from 'react-helmet-async'; import { useTranslation } from 'react-i18next'; import { BrowserRouter } from 'react-router-dom'; -import { GlobalStyle, OverriddenStyle } from 'styles/globalStyles'; +import { GlobalStyles } from 'styles/globalStyles'; import { LazySharePage } from './pages/SharePage/Loadable'; registerTheme('default', echartsDefaultTheme); @@ -41,8 +41,7 @@ export function Share() { - - + ); diff --git a/frontend/src/app/slice/index.ts b/frontend/src/app/slice/index.ts index afb850a50..609af9297 100644 --- a/frontend/src/app/slice/index.ts +++ b/frontend/src/app/slice/index.ts @@ -19,6 +19,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; import { + getOauth2Clients, getSystemInfo, getUserInfoByToken, login, @@ -37,6 +38,7 @@ export const initialState: AppState = { registerLoading: false, saveProfileLoading: false, modifyPasswordLoading: false, + oauth2Clients: [], }; const slice = createSlice({ @@ -116,6 +118,13 @@ const slice = createSlice({ builder.addCase(getSystemInfo.fulfilled, (state, action) => { state.systemInfo = action.payload; }); + + builder.addCase(getOauth2Clients.fulfilled, (state, action) => { + state.oauth2Clients = action.payload.map(x => ({ + name: Object.keys(x)[0], + value: x[Object.keys(x)[0]], + })); + }); }, }); diff --git a/frontend/src/app/slice/selectors.ts b/frontend/src/app/slice/selectors.ts index 523febb24..d87da95f1 100644 --- a/frontend/src/app/slice/selectors.ts +++ b/frontend/src/app/slice/selectors.ts @@ -56,3 +56,8 @@ export const selectModifyPasswordLoading = createSelector( [selectDomain], appState => appState.modifyPasswordLoading, ); + +export const selectOauth2Clients = createSelector( + [selectDomain], + appState => appState.oauth2Clients, +); diff --git a/frontend/src/app/slice/thunks.ts b/frontend/src/app/slice/thunks.ts index 7cfd30842..91111acd1 100644 --- a/frontend/src/app/slice/thunks.ts +++ b/frontend/src/app/slice/thunks.ts @@ -185,3 +185,36 @@ export const getSystemInfo = createAsyncThunk( } }, ); + +export const getOauth2Clients = createAsyncThunk<[]>( + 'app/getOauth2Clients', + async () => { + try { + const { data } = await request<[]>({ + url: '/tpa/getOauth2Clients', + method: 'GET', + }); + return data; + } catch (error) { + errorHandle(error); + throw error; + } + }, +); + +export const tryOauth = createAsyncThunk('app/tryOauth', async () => { + try { + const { data } = await request({ + url: '/tpa/oauth2login', + method: 'POST', + }); + localStorage.setItem(StorageKeys.LoggedInUser, JSON.stringify(data)); + setTimeout(() => { + window.location.href = '/'; + }); + return data; + } catch (error) { + errorHandle(error); + throw error; + } +}); diff --git a/frontend/src/app/slice/types.ts b/frontend/src/app/slice/types.ts index 0b56e48c6..26457da6a 100644 --- a/frontend/src/app/slice/types.ts +++ b/frontend/src/app/slice/types.ts @@ -22,6 +22,7 @@ export interface AppState { registerLoading: boolean; saveProfileLoading: boolean; modifyPasswordLoading: boolean; + oauth2Clients: Array<{name:string,value:string}>; } export interface User { diff --git a/frontend/src/app/types/Chart.d.ts b/frontend/src/app/types/Chart.d.ts index 5a8018529..36e59b99e 100644 --- a/frontend/src/app/types/Chart.d.ts +++ b/frontend/src/app/types/Chart.d.ts @@ -17,6 +17,8 @@ */ import ChartDataSetDTO from 'app/types/ChartDataSet'; +import ChartConfig from './ChartConfig'; +import ChartMetadata from './ChartMetadata'; export type ChartStatus = | 'init' diff --git a/frontend/src/app/types/ChartConfig.ts b/frontend/src/app/types/ChartConfig.ts index ade7fdd90..8f16134bb 100644 --- a/frontend/src/app/types/ChartConfig.ts +++ b/frontend/src/app/types/ChartConfig.ts @@ -190,6 +190,8 @@ export const ChartStyleSectionComponentType = { FONTFAMILY: 'fontFamily', FONTSIZE: 'fontSize', FONTCOLOR: 'fontColor', + FONTSTYLE: 'fontStyle', + FONTWEIGHT: 'fontWeight', INPUTNUMBER: 'inputNumber', INPUTPERCENTAGE: 'inputPercentage', SLIDER: 'slider', @@ -201,11 +203,15 @@ export const ChartStyleSectionComponentType = { LINE: 'line', MARGIN_WIDTH: 'marginWidth', TEXT: 'text', - CONDITIONSTYLE: 'conditionStylePanel', + CONDITIONALSTYLE: 'conditionalStylePanel', RADIO: 'radio', // Customize Component FontAlignment: 'fontAlignment', + NameLocation: 'nameLocation', + LegendType: 'legendType', + ScorecardListTemplate: 'scorecardListTemplate', + ScorecardConditionalStyle: 'scorecardConditionalStyle', }; export type ChartConfigBase = { @@ -326,6 +332,7 @@ export type ChartStyleSectionRowOption = { min?: number | string; max?: number | string; step?: number | string; + dots?: boolean; type?: string; editable?: boolean; modalSize?: string | number; @@ -336,10 +343,14 @@ export type ChartStyleSectionRowOption = { getItems?: (cols) => Array; needRefresh?: boolean; fontFamilies?: string[]; + showFontSize?: boolean; + showLineHeight?: boolean; + showFontStyle?: boolean; + showFontColor?: boolean; /** - * Suppport Components: @see BasicRadio, @see BasicSelector and etc - * Default is false for now, will be change in futrue version + * Support Components: @see BasicRadio, @see BasicSelector and etc + * Default is false for now, will be change in future version */ translateItemLabel?: boolean; }; diff --git a/frontend/src/app/types/ChartConfigDTO.d.ts b/frontend/src/app/types/ChartConfigDTO.d.ts index 7d55c8b50..c1fbfc8e7 100644 --- a/frontend/src/app/types/ChartConfigDTO.d.ts +++ b/frontend/src/app/types/ChartConfigDTO.d.ts @@ -18,6 +18,7 @@ import { ChartDataConfig } from 'app/types/ChartConfig'; import { ChartDataViewMeta } from 'app/types/ChartDataViewMeta'; +import { ECharts } from 'echarts'; export type ChartStyleConfigDTO = { key: string; @@ -40,3 +41,20 @@ export type ChartDetailConfigDTO = { computedFields: ChartDataViewMeta[]; aggregation: boolean; }; + +export interface ChartCommonConfig { + chart: ECharts; + // 官方ts定义过于复杂 所以用any + // x轴options + xAxis: any; + // y轴options + yAxis: any; + // 布局 + grid: any; + // y轴 字段索引名 + yAxisNames: string[]; + // 图表系列 + series: any[]; + // 水平还是垂直 + horizon?: boolean; +} diff --git a/frontend/src/app/types/ChartDataConfigSection.ts b/frontend/src/app/types/ChartDataConfigSection.ts index 9e4d4d462..39c7f3a06 100644 --- a/frontend/src/app/types/ChartDataConfigSection.ts +++ b/frontend/src/app/types/ChartDataConfigSection.ts @@ -26,9 +26,10 @@ export interface ChartDataConfigSectionProps { config: ChartDataConfig; modalSize?: StateModalSize; category?: Lowercase; + aggregation?: boolean; + expensiveQuery?: boolean; extra?: () => ReactNode; translate?: (title: string) => string; - aggregation?: boolean; onConfigChanged: ( ancestors: number[], config: ChartDataConfig, diff --git a/frontend/src/app/types/ChartDataRequest.ts b/frontend/src/app/types/ChartDataRequest.ts index 14acf0bda..f22884842 100644 --- a/frontend/src/app/types/ChartDataRequest.ts +++ b/frontend/src/app/types/ChartDataRequest.ts @@ -46,6 +46,9 @@ export type ChartDataRequest = { concurrencyControl?: boolean; concurrencyControlMode?: string; params?: Record; + vizId?: string; + vizName?: string; + analytics?: Boolean; }; export type ChartDataRequestFilter = { diff --git a/frontend/src/app/types/ChartDataSet.d.ts b/frontend/src/app/types/ChartDataSet.d.ts index ddc9848bf..290f2fa0e 100644 --- a/frontend/src/app/types/ChartDataSet.d.ts +++ b/frontend/src/app/types/ChartDataSet.d.ts @@ -41,6 +41,10 @@ export interface IChartDataSet extends Array> { getFieldIndex(field: ChartDataSectionField): number; sortBy(dataConfigs: ChartDataConfig[]): void; + + groupBy(field: ChartDataSectionField): { + [groupKey in string]: IChartDataSetRow[]; + }; } export type ChartDataSetDTO = { diff --git a/frontend/src/app/types/CompoutedFieldEditor.ts b/frontend/src/app/types/ComputedFieldEditor.ts similarity index 94% rename from frontend/src/app/types/CompoutedFieldEditor.ts rename to frontend/src/app/types/ComputedFieldEditor.ts index ea786c60c..46e27f21c 100644 --- a/frontend/src/app/types/CompoutedFieldEditor.ts +++ b/frontend/src/app/types/ComputedFieldEditor.ts @@ -23,6 +23,6 @@ export interface FunctionDescription { syntax: string; } -export interface ChartCompoutedFieldHandle { +export interface ChartComputedFieldHandle { insertField: (value, funcDesc?: FunctionDescription) => void; } diff --git a/frontend/src/app/utils/ChartDtoHelper.ts b/frontend/src/app/utils/ChartDtoHelper.ts index 3abc218bd..186652624 100644 --- a/frontend/src/app/utils/ChartDtoHelper.ts +++ b/frontend/src/app/utils/ChartDtoHelper.ts @@ -22,8 +22,8 @@ import { ChartDTO } from 'app/types/ChartDTO'; import { mergeChartDataConfigs, mergeChartStyleConfigs, + transformMeta, } from 'app/utils/internalChartHelper'; -import { transformMeta } from 'app/utils/internalChartHelper'; import { Omit } from 'utils/object'; export function convertToChartDTO(data): ChartDTO { @@ -63,6 +63,7 @@ export function buildUpdateChartRequest({ viewId: viewId, config: stringifyConfig, permissions: [], + avatar: graphId, }; } @@ -82,6 +83,7 @@ function getStyleValueModel(styles?: ChartStyleConfig[]) { label: s.label, key: s.key, value: s.value, + comType: s.comType, rows: s.template ? s.rows : getStyleValueModel(s.rows), }; }); diff --git a/frontend/src/app/utils/__tests__/chartHelper.test.ts b/frontend/src/app/utils/__tests__/chartHelper.test.ts index 09bfbfded..141980896 100644 --- a/frontend/src/app/utils/__tests__/chartHelper.test.ts +++ b/frontend/src/app/utils/__tests__/chartHelper.test.ts @@ -18,12 +18,19 @@ import { ChartDataSetRow } from 'app/components/ChartGraph/models/ChartDataSet'; import { + ChartDataSectionField, + IFieldFormatConfig, +} from '../../types/ChartConfig'; +import { + getColorizeGroupSeriesColumns, getColumnRenderName, getStyles, getValue, isMatchRequirement, + toFormattedValue, transformToDataSet, transformToObjectArray, + valueFormatter, } from '../chartHelper'; describe('Chart Helper ', () => { @@ -409,6 +416,63 @@ describe('Chart Helper ', () => { }); }); + describe('getColorizeGroupSeriesColumns Test', () => { + test('should group dataset', () => { + const columns = [ + ['stephen', 'engineer', '36'], + ['jack', 'sales', '28'], + ['tom', 'engineer', '30'], + ['john', 'sales', '32'], + ]; + const metas = [ + { name: 'name' }, + { name: 'current(profession)' }, + { name: 'age' }, + ]; + const chartDataSet = transformToDataSet(columns, metas, [ + { + rows: [ + { + colName: 'name', + }, + { + colName: 'profession', + aggregate: 'current', + }, + + { + colName: 'age', + }, + ], + }, + ] as any); + + expect( + JSON.stringify( + getColorizeGroupSeriesColumns(chartDataSet, { + colName: 'profession', + aggregate: 'current', + } as any), + ), + ).toBe( + JSON.stringify([ + { + engineer: [ + ['stephen', 'engineer', '36'], + ['tom', 'engineer', '30'], + ], + }, + { + sales: [ + ['jack', 'sales', '28'], + ['john', 'sales', '32'], + ], + }, + ]), + ); + }); + }); + describe('transformToDataSet Test', () => { test('should get dataset model with ignore case compare', () => { const columns = [ @@ -494,4 +558,171 @@ describe('Chart Helper ', () => { }); }); }); + + describe.each([ + [1, undefined, 1], + [ + 2, + { + type: 'numeric', + numeric: { + decimalPlaces: 3, + unitKey: 'thousand', + useThousandSeparator: true, + prefix: 'a', + suffix: 'b', + }, + }, + 'a0.002Kb', + ], + [ + 111, + { + type: 'numeric', + numeric: { + decimalPlaces: 3, + unitKey: 'thousand', + useThousandSeparator: true, + prefix: '', + suffix: 'b', + }, + }, + '0.111Kb', + ], + [ + 333, + { + type: 'numeric', + numeric: { + decimalPlaces: 3, + unitKey: '', + useThousandSeparator: true, + prefix: '', + suffix: 'b', + }, + }, + '333.000b', + ], + [ + 3, + { + type: 'currency', + currency: { + decimalPlaces: 3, + unitKey: 'thousand', + useThousandSeparator: true, + currency: 'CNY', + }, + }, + '¥0.003 K', + ], + [ + 4, + { + type: 'percentage', + percentage: { + decimalPlaces: 2, + }, + }, + '400.00%', + ], + [ + 50, + { + type: 'scientificNotation', + scientificNotation: { + decimalPlaces: 2, + }, + }, + `5.00e+1`, + ], + [ + 55, + { + type: 'scientificNotation', + scientificNotation: { + decimalPlaces: 3, + }, + }, + `5.500e+1`, + ], + ])('toFormattedValue Test - ', (value, format, expected) => { + test(`format aggregate data`, () => { + expect(toFormattedValue(value, format as IFieldFormatConfig)).toEqual( + expected, + ); + }); + }); + + describe.each([ + [undefined, undefined, `[unknown]: -`], + [ + { + alias: { + name: 'aa', + }, + aggregate: 'SUM', + colName: 'name', + type: 'STRING', + category: 'field', + }, + undefined, + `aa: -`, + ], + [ + { + alias: { + name: 'bb', + }, + aggregate: '', + colName: 'name', + type: 'STRING', + category: 'field', + }, + undefined, + `bb: -`, + ], + [ + { + aggregate: '', + colName: 'name', + type: 'STRING', + category: 'field', + }, + 55, + `name: 55`, + ], + [ + { + aggregate: 'SUM', + colName: 'name', + type: 'STRING', + category: 'field', + }, + 55, + `SUM(name): 55`, + ], + [ + { + format: { + type: 'scientificNotation', + scientificNotation: { + decimalPlaces: 3, + }, + }, + aggregate: 'SUM', + colName: 'name', + type: 'STRING', + category: 'field', + }, + 55, + `SUM(name): 5.500e+1`, + ], + ])('valueFormatter Test - ', (config, value, expected) => { + test(`Get chart render string with field name and value`, () => { + expect(valueFormatter(config as ChartDataSectionField, value)).toEqual( + expected, + ); + }); + }); }); diff --git a/frontend/src/app/utils/__tests__/overflowFuncs.test.ts b/frontend/src/app/utils/__tests__/overflowFuncs.test.ts new file mode 100644 index 000000000..e4c0f2a3d --- /dev/null +++ b/frontend/src/app/utils/__tests__/overflowFuncs.test.ts @@ -0,0 +1,179 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { + getAutoFunnelTopPosition, + getIntervalShow, + hadAxisLabelOverflowConfig, +} from '../chartHelper'; + +describe('test getIntervalShow return boolean', () => { + it('getIntervalShow return true when arg is number', () => { + expect(getIntervalShow(0)).toBeTruthy(); + }); + + it('1. getIntervalShow return true when arg is string', () => { + expect(getIntervalShow('0')).toBeTruthy(); + }); + + it('getIntervalShow return false when arg is "auto"', () => { + expect(getIntervalShow('auto')).toBeFalsy(); + }); + + // Interval 已经经过处理,不可能为 undefined + it('getIntervalShow return false when arg is null', () => { + expect(getIntervalShow(null)).toBeFalsy(); + }); +}); + +describe('test hadAxisLabelOverflowConfig return boolean', () => { + const axisLabel = { + overflow: 'break', + interval: 0, + show: true, + }; + + const getOptions = (label: any = null, horizon = false) => ({ + [horizon ? 'yAxis' : 'xAxis']: label ? [label] : null, + }); + + it('hadAxisLabelOverflowConfig return false when options is null', () => { + expect(hadAxisLabelOverflowConfig(getOptions())).toBeFalsy(); + }); + + it('hadAxisLabelOverflowConfig return false when options.xAxis[0].axisLabel is null', () => { + expect(hadAxisLabelOverflowConfig(getOptions({}))).toBeFalsy(); + }); + + it('hadAxisLabelOverflowConfig return false when get options.yAxis opts', () => { + expect(hadAxisLabelOverflowConfig(getOptions({}, true))).toBeFalsy(); + }); + + it('hadAxisLabelOverflowConfig return true when get options.xAxis', () => { + expect(hadAxisLabelOverflowConfig(getOptions({ axisLabel }))).toBeTruthy(); + }); + + it('hadAxisLabelOverflowConfig return false when get options.yAxis opts', () => { + expect(hadAxisLabelOverflowConfig(getOptions({}, true))).toBeFalsy(); + }); + + it('hadAxisLabelOverflowConfig return true when get options.yAxis', () => { + expect( + hadAxisLabelOverflowConfig(getOptions({ axisLabel }, true), true), + ).toBeTruthy(); + }); + + it('hadAxisLabelOverflowConfig return false when show false', () => { + expect( + hadAxisLabelOverflowConfig( + getOptions({ + axisLabel: { + axisLabel, + show: false, + }, + }), + ), + ).toBeFalsy(); + }); + + it('hadAxisLabelOverflowConfig return false when interval "auto"', () => { + expect( + hadAxisLabelOverflowConfig( + getOptions({ + axisLabel: { + axisLabel, + interval: 'auto', + }, + }), + ), + ).toBeFalsy(); + }); + + it('hadAxisLabelOverflowConfig return false when overflow null"', () => { + expect( + hadAxisLabelOverflowConfig( + getOptions({ + axisLabel: { + axisLabel, + overflow: null, + }, + }), + ), + ).toBeFalsy(); + }); +}); + +describe('test getAutoFunnelTopPosition return number', () => { + it('getAutoFunnelTopPosition return 8 when legendPos is not left or right', () => { + expect( + getAutoFunnelTopPosition({ + legendPos: 'top', + } as any), + ).toEqual(8); + }); + it('getAutoFunnelTopPosition return 16 when legendPos is left or right and height is 0 or null', () => { + expect( + getAutoFunnelTopPosition({ + legendPos: 'left', + } as any), + ).toEqual(16); + + expect( + getAutoFunnelTopPosition({ + legendPos: 'left', + height: 0, + } as any), + ).toEqual(16); + }); + + it('getAutoFunnelTopPosition return 16 when sort is ascending', () => { + expect( + getAutoFunnelTopPosition({ + legendPos: 'left', + height: 32, + sort: 'ascending', + } as any), + ).toEqual(16); + }); + + it('getAutoFunnelTopPosition return 16 when chart.getHeight return null/0', () => { + expect( + getAutoFunnelTopPosition({ + legendPos: 'left', + height: 32, + sort: 'none', + chart: { + getHeight: () => 0, + }, + } as any), + ).toEqual(16); + }); + + it('getAutoFunnelTopPosition return chart.getHeight - height - marginBottom', () => { + expect( + getAutoFunnelTopPosition({ + legendPos: 'left', + height: 100, + sort: 'none', + chart: { + getHeight: () => 800, + }, + } as any), + ).toEqual(800 - 100 - 24); + }); +}); diff --git a/frontend/src/app/utils/chartHelper.ts b/frontend/src/app/utils/chartHelper.ts index db9a6b64d..eecb5089f 100644 --- a/frontend/src/app/utils/chartHelper.ts +++ b/frontend/src/app/utils/chartHelper.ts @@ -31,19 +31,25 @@ import { IFieldFormatConfig, SortActionType, } from 'app/types/ChartConfig'; -import { ChartStyleConfigDTO } from 'app/types/ChartConfigDTO'; +import { + ChartCommonConfig, + ChartStyleConfigDTO, +} from 'app/types/ChartConfigDTO'; import { ChartDatasetMeta, IChartDataSet, IChartDataSetRow, } from 'app/types/ChartDataSet'; import ChartMetadata from 'app/types/ChartMetadata'; +import { ECharts } from 'echarts'; +import { ECBasicOption } from 'echarts/types/dist/shared'; import { NumberUnitKey, NumericUnitDescriptions } from 'globalConstants'; import moment from 'moment'; import { Debugger } from 'utils/debugger'; import { isEmpty, isEmptyArray, meanValue, pipe } from 'utils/object'; import { flattenHeaderRowsWithoutGroupRow, + getAxisLengthByConfig, getColumnRenderOriginName, getRequiredAggregatedSections, getRequiredGroupedSections, @@ -500,7 +506,12 @@ export function getReference2( dataConfig, isHorizonDisplay, ), - markArea: getMarkArea2(referenceTabs, dataSetRows, isHorizonDisplay), + markArea: getMarkArea2( + referenceTabs, + dataSetRows, + dataConfig, + isHorizonDisplay, + ), }; } @@ -676,8 +687,10 @@ function getMarkAreaData2( valueTypeKey, constantValueKey, metricKey, + dataConfig, isHorizonDisplay, ) { + const metric = getSettingValue(mark.rows, metricKey, 'value'); const valueKey = isHorizonDisplay ? 'xAxis' : 'yAxis'; const show = getSettingValue(mark.rows, 'showLabel', 'value'); const enableMarkArea = getSettingValue(mark.rows, 'enableMarkArea', 'value'); @@ -692,8 +705,10 @@ function getMarkAreaData2( ); const name = mark.value; const valueType = getSettingValue(mark.rows, valueTypeKey, 'value'); - const metric = getSettingValue(mark.rows, metricKey, 'value'); - const metricDatas = dataSetRows.map(d => +d.getCellByKey(metric)); + const metricDatas = + dataConfig.uid === metric + ? dataSetRows.map(d => +d.getCell(dataConfig)) + : []; const constantValue = getSettingValue(mark.rows, constantValueKey, 'value'); let yAxis = 0; switch (valueType) { @@ -711,8 +726,8 @@ function getMarkAreaData2( break; } - if (!enableMarkArea) { - return null; + if (!enableMarkArea || !Number.isFinite(yAxis) || Number.isNaN(yAxis)) { + return; } return { @@ -826,12 +841,12 @@ function getMarkArea(refTabs, dataColumns, isHorizonDisplay) { function getMarkArea2( refTabs, dataSetRows: IChartDataSetRow[], + dataConfig, isHorizonDisplay, ) { const refAreas = refTabs?.reduce((acc, cur) => { const markLineConfigs = cur?.rows?.filter(r => r.key === 'markArea'); - acc.push(...markLineConfigs); - return acc; + return acc.concat(markLineConfigs); }, []); return { data: refAreas @@ -844,13 +859,14 @@ function getMarkArea2( `${prefix}ValueType`, `${prefix}ConstantValue`, `${prefix}Metric`, + dataConfig, isHorizonDisplay, ); }) .filter(Boolean); return markAreaData; }) - .filter(m => Boolean(m?.length)), + .filter(m => m?.length === 2), }; } @@ -866,11 +882,13 @@ export function getAxisLabel( font: { fontFamily; fontSize; color }, interval = null, rotate = null, + overflow = null, ) { return { show, interval, rotate, + overflow, ...font, }; } @@ -957,7 +975,7 @@ export function transformToObjectArray( 'transformToObjectArray', () => { const result: any[] = Array.apply(null, Array(columns.length)); - for (let j = 0, outterLength = result.length; j < outterLength; j++) { + for (let j = 0, outerLength = result.length; j < outerLength; j++) { let objCol: any = {}; for (let i = 0, innerLength = metas.length; i < innerLength; i++) { const key = metas?.[i]?.name; @@ -1069,6 +1087,7 @@ export function getSeriesTooltips4Rectangular2( chartDataSet: IChartDataSet, tooltipParam: { componentType: string; + seriesName?: string; data: { name: string; rowData: { [key: string]: any }; @@ -1083,7 +1102,7 @@ export function getSeriesTooltips4Rectangular2( if (tooltipParam?.componentType !== 'series') { return ''; } - const aggConfigName = tooltipParam?.data?.name; + const aggConfigName = tooltipParam?.data?.name || tooltipParam?.seriesName; const row = tooltipParam?.data?.rowData || {}; const tooltips: string[] = ([] as any[]) @@ -1221,7 +1240,6 @@ export function getExtraSeriesRowData(data) { rowData: data?.convertToCaseSensitiveObject(), }; } - return { rowData: data, }; @@ -1235,36 +1253,13 @@ export function getExtraSeriesDataFormat(format?: IFieldFormatConfig) { export function getColorizeGroupSeriesColumns( chartDataSet: IChartDataSet, - groupByKey: string, - xAxisColumnName: string, - aggregateKeys: string[], - infoColumnNames: string[], + groupConfig: ChartDataSectionField, ) { - const groupedDataColumnObject = chartDataSet?.reduce((acc, cur) => { - const colKey = cur.getCellByKey(groupByKey) || 'defaultGroupKey'; - - if (!acc[colKey]) { - acc[colKey] = []; - } - const value = aggregateKeys - .concat([xAxisColumnName]) - .concat(infoColumnNames || []) - .concat([groupByKey]) - .reduce((a, k) => { - a[k] = cur.getCellByKey(k); - return a; - }, {}); - acc[colKey].push(value); - return acc; - }, {}); - - let collection = [] as any; - Object.entries(groupedDataColumnObject).forEach(([k, v]) => { + return Object.entries(chartDataSet.groupBy(groupConfig)).map(([k, v]) => { let a = {}; a[k] = v; - collection.push(a); + return a; }); - return collection; } /** @@ -1333,3 +1328,126 @@ export function isMatchRequirement( ); }); } + +// 获取是否展示刻度 +export const getIntervalShow = interval => + interval !== 'auto' && interval !== null; + +// 判断overflow 条件是否已生效 +export function hadAxisLabelOverflowConfig( + options?: ECBasicOption, + horizon: boolean = false, +) { + if (!options) return false; + const axisName = !horizon ? 'xAxis' : 'yAxis'; + + const axisLabelOpts = (options as unknown as any)[axisName]?.[0]?.axisLabel; + if (!axisLabelOpts) return false; + + const { overflow, interval, show } = axisLabelOpts; + + return show && overflow && getIntervalShow(interval); +} + +// 处理溢出情况 +export function setOptionsByAxisLabelOverflow(config: ChartCommonConfig) { + const { chart, xAxis, yAxis, grid, series, horizon = false } = config; + + const commonOpts = { + grid, + xAxis, + yAxis, + series, + }; + + // 如果是x轴需要截断,则取x轴数据 + const axisOpts = !horizon ? xAxis : yAxis; + const axisName = !horizon ? 'xAxis' : 'yAxis'; + + const data = axisOpts.data || []; + + const dataLength = data.length; + + // 拿到截断配置 + const overflow = axisOpts.axisLabel?.overflow; + const show = axisOpts.axisLabel?.show; + // 是否展示刻度,非刻度使用默认样式 + + const showInterval = getIntervalShow(axisOpts.axisLabel?.interval); + + // 不展示刻度 + if (!show) return commonOpts; + // 数据为空 + if (!dataLength) return commonOpts; + + commonOpts[axisName].axisLabel.hideOverlap = true; + commonOpts[axisName].axisLabel.overflow = overflow; + + // 如果overflow为截断,则使用每段刻度来响应tooltip + // 不破坏原有展示逻辑 + if (showInterval && overflow === 'truncate') { + commonOpts[axisName].axisPointer = { + show: true, + type: 'shadow', + }; + } + + // 获取x/y轴在model上的信息 + // @ts-ignore + const axisModel = chart.getModel()?.getComponent(axisName); + + // 处理 每个刻度宽度 + const setWidth = width => { + // 水平图表使用默认宽度 + if (horizon) return 40; + return parseInt(String((width - dataLength * 8) / dataLength)); + }; + // model 渲染未完成的兼容性方案,一般只在图表初始化阶段,还没有拿到model。 + // 一般只会运行一次 + // 拿到model后就可使用更加精确的坐标轴宽高度等信息,所以处理可以略粗略 + const handlerWhenChartUnFinished = () => { + commonOpts[axisName].axisLabel.width = showInterval + ? setWidth(getAxisLengthByConfig(config)) + : void 0; + return commonOpts; + }; + + // model未获取到,原因: 未渲染完成 + if (!axisModel) { + handlerWhenChartUnFinished(); + return commonOpts; + } + // @ts-ignore + const axisView = chart.getViewOfComponentModel(axisModel); + + const axisRect = axisView?.group?.getBoundingRect(); + + if (!axisRect) { + handlerWhenChartUnFinished(); + return commonOpts; + } + + commonOpts[axisName].axisLabel.width = showInterval + ? setWidth(axisRect.width) + : void 0; + + return commonOpts; +} + +export const getAutoFunnelTopPosition = (config: { + chart: ECharts; + height: number; + sort: 'ascending' | 'descending' | 'none'; + legendPos: string; +}) => { + const { chart, height, sort, legendPos } = config; + if (legendPos !== 'left' && legendPos !== 'right') return 8; + if (!height) return 16; + // 升序 + if (sort === 'ascending') return 16; + + const chartHeight = chart.getHeight(); + if (!chartHeight) return 16; + // 24 marginBottom + return chartHeight - 24 - height; +}; diff --git a/frontend/src/app/utils/internalChartHelper.ts b/frontend/src/app/utils/internalChartHelper.ts index e2a73748b..3aa56a19b 100644 --- a/frontend/src/app/utils/internalChartHelper.ts +++ b/frontend/src/app/utils/internalChartHelper.ts @@ -24,7 +24,10 @@ import { ChartDataSectionType, ChartStyleConfig, } from 'app/types/ChartConfig'; -import { ChartStyleConfigDTO } from 'app/types/ChartConfigDTO'; +import { + ChartCommonConfig, + ChartStyleConfigDTO, +} from 'app/types/ChartConfigDTO'; import { ChartDataViewFieldCategory, ChartDataViewFieldType, @@ -297,8 +300,6 @@ const transferMixedToOther = ( return targetConfig!; }; -const balanceAssignConfigRows = sources => {}; - export function isInRange(limit?: ChartDataConfig['limit'], count: number = 0) { return cond( [isEmpty, true], @@ -381,11 +382,22 @@ export function transformMeta(model?: string) { return undefined; } const jsonObj = JSON.parse(model); - return Object.keys(jsonObj).map(colKey => ({ - ...jsonObj[colKey], - id: colKey, - category: ChartDataViewFieldCategory.Field, - })); + const HierarchyModel = 'hierarchy' in jsonObj ? jsonObj.hierarchy : jsonObj; + return Object.keys(HierarchyModel || {}).flatMap(colKey => { + const column = HierarchyModel[colKey]; + if (!isEmptyArray(column?.children)) { + return column.children.map(c => ({ + ...c, + id: c.name, + category: ChartDataViewFieldCategory.Field, + })); + } + return { + ...column, + id: colKey, + category: ChartDataViewFieldCategory.Field, + }; + }); } export function mergeChartStyleConfigs( @@ -413,12 +425,7 @@ export function mergeChartStyleConfigs( const sEle = 'key' in tEle ? source?.find(s => s?.key === tEle.key) : source?.[index]; - if (!isUndefined(sEle?.['value']) && (!sEle?.comType || !tEle.comType)) { - tEle['value'] = sEle?.['value']; - } else if ( - !isUndefined(sEle?.['value']) && - sEle?.comType === tEle.comType - ) { + if (!isUndefined(sEle?.['value'])) { tEle['value'] = sEle?.['value']; } if (!isEmptyArray(tEle?.rows)) { @@ -486,3 +493,82 @@ export const filterSqlOperatorName = (requestParams, widgetData) => { }); return widgetData; }; + +// 获取当前echart坐标轴区域的宽度 +export function getAxisLengthByConfig(config: ChartCommonConfig) { + const { chart, xAxis, yAxis, grid, series, yAxisNames, horizon } = config; + const axisOpts = !horizon ? xAxis : yAxis; + // datart 布局配置分为百分比和像素 + const getPositionLengthInfo = ( + positionConfig: string | number, + ): { + length: number; + type: 'percent' | 'px'; + } => { + if (typeof positionConfig === 'string') { + const lengthPercentInt = parseInt(positionConfig.replace('%', ''), 10); + if (isNaN(lengthPercentInt)) { + throw new Error(`${positionConfig} is not a number`); + } + return { + length: lengthPercentInt / 100, + type: 'percent', + }; + } + return { + length: positionConfig, + type: 'px', + }; + }; + + // 获取坐标轴宽度 + const getAxisWidth = (YAxisLength: number): number => { + return (Array.isArray(axisOpts) ? axisOpts : [axisOpts]).reduce( + (prev, item) => { + const { fontSize, show } = item.axisLabel; + // 预留一个字符长度 + const axisLabelMaxWidth = show ? (YAxisLength + 1) * fontSize : 0; + prev += axisLabelMaxWidth; + return prev; + }, + 0, + ); + }; + + const { containerLabel, left, right } = grid; + + // 找到轴上最大的数字长度 + let foundMaxAxisLength = 0; + + if (containerLabel && !horizon) { + foundMaxAxisLength = series.reduce((prev, sery) => { + sery?.data?.forEach(item => { + yAxisNames.forEach(name => { + if (item.name === name) { + const yNumStr = `${item[0]}`; + if (yNumStr.length > prev) { + prev = yNumStr.length; + } + } + }); + }); + return prev; + }, 0); + } + + const axisLabelMaxWidth = getAxisWidth(foundMaxAxisLength); + + const left_ = getPositionLengthInfo(left); + const right_ = getPositionLengthInfo(right); + + const containerWidth = chart.getWidth(); + + // 左右边距 + const leftWidth = + left_.type === 'px' ? left_.length : containerWidth * left_.length; + const rightWidth = + right_.type === 'px' ? right_.length : containerWidth * right_.length; + + // 坐标轴区域宽度 = 容器宽度 - 最大字符所占长度 - 左右边距 + return containerWidth - axisLabelMaxWidth - leftWidth - rightWidth; +} diff --git a/frontend/src/app/utils/mutation.ts b/frontend/src/app/utils/mutation.ts index 238569a45..ff1b6207c 100644 --- a/frontend/src/app/utils/mutation.ts +++ b/frontend/src/app/utils/mutation.ts @@ -24,9 +24,9 @@ export interface Action { value: T; } -export function updateBy(base: T, updator: (draft: Draft) => void) { +export function updateBy(base: T, updater: (draft: Draft) => void) { return produce(base, draft => { - updator(draft); + updater(draft); }); } diff --git a/frontend/src/assets/svgs/mention.svg b/frontend/src/assets/svgs/mention.svg deleted file mode 100644 index 22c45abc1..000000000 --- a/frontend/src/assets/svgs/mention.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/globalConstants.ts b/frontend/src/globalConstants.ts index 8fa03a27f..c1b5dbfc5 100644 --- a/frontend/src/globalConstants.ts +++ b/frontend/src/globalConstants.ts @@ -27,6 +27,7 @@ export enum StorageKeys { LoggedInUser = 'LOGGED_IN_USER', ShareClientId = 'SHARE_CLIENT_ID', Locale = 'LOCALE', + Theme = 'THEME', } export const BASE_API_URL = '/api/v1'; export const BASE_RESOURCE_URL = '/'; @@ -46,23 +47,53 @@ export const DEFAULT_DEBOUNCE_WAIT = 300; export const FONT_SIZES = [ 12, 13, 14, 15, 16, 18, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 96, 128, ]; +export const FONT_LINE_HEIGHT = [ + { + name: 'viz.palette.style.lineHeight.default', + value: 1, + }, + 1.5, + 2, + 2.5, + 3, + 3.5, + 4, + 4.5, + 5, + 5.5, + 6, + 6.5, + 7, + 7.5, + 8, + 8.5, + 9, + 9.5, + 10, +]; export const FONT_FAMILIES = [ - { name: '默认字体', value: FONT_FAMILY }, - { name: '微软雅黑', value: 'Microsoft YaHei' }, - { name: '宋体', value: 'SimSun' }, - { name: '黑体', value: 'SimHei' }, - { name: 'Helvetica Neue', value: '"Helvetica Neue"' }, - { name: 'Helvetica', value: 'Helvetica' }, - { name: 'Arial', value: 'Arial' }, - { name: 'sans-serif', value: 'sans-serif' }, + { name: 'viz.palette.style.fontFamily.default', value: FONT_FAMILY }, + { + name: 'viz.palette.style.fontFamily.microsoftYaHei', + value: 'Microsoft YaHei', + }, + { name: 'viz.palette.style.fontFamily.simSun', value: 'SimSun' }, + { name: 'viz.palette.style.fontFamily.simHei', value: 'SimHei' }, + { + name: 'viz.palette.style.fontFamily.helveticaNeue', + value: 'Helvetica Neue', + }, + { name: 'viz.palette.style.fontFamily.helvetica', value: 'Helvetica' }, + { name: 'viz.palette.style.fontFamily.arial', value: 'Arial' }, + { name: 'viz.palette.style.fontFamily.sansSerif', value: 'sans-serif' }, ]; export const FONT_WEIGHT = [ - { name: '常规字号', value: 'normal' }, - { name: '粗体', value: 'bold' }, - { name: '特粗体', value: 'bolder' }, - { name: '细体', value: 'lighter' }, + { name: 'viz.palette.style.fontWeight.normal', value: 'normal' }, + { name: 'viz.palette.style.fontWeight.bold', value: 'bold' }, + { name: 'viz.palette.style.fontWeight.bolder', value: 'bolder' }, + { name: 'viz.palette.style.fontWeight.lighter', value: 'lighter' }, { name: '100', value: '100' }, { name: '200', value: '200' }, { name: '300', value: '300' }, @@ -75,15 +106,26 @@ export const FONT_WEIGHT = [ ]; export const FONT_STYLE = [ - { name: '常规体', value: 'normal' }, - { name: '斜体', value: 'italic' }, - { name: '偏斜体', value: 'oblique' }, + { name: 'viz.palette.style.fontStyle.normal', value: 'normal' }, + { name: 'viz.palette.style.fontStyle.italic', value: 'italic' }, + { name: 'viz.palette.style.fontStyle.oblique', value: 'oblique' }, ]; export const CHART_LINE_STYLES = [ - { name: '实线', value: 'solid' }, - { name: '虚线', value: 'dashed' }, - { name: '点', value: 'dotted' }, + { name: 'viz.palette.style.lineStyles.solid', value: 'solid' }, + { name: 'viz.palette.style.lineStyles.dashed', value: 'dashed' }, + { name: 'viz.palette.style.lineStyles.dotted', value: 'dotted' }, +]; + +export const CHART_NAME_LOCATION = [ + { name: 'nameLocation.start', value: 'start' }, + { name: 'nameLocation.center', value: 'center' }, + { name: 'nameLocation.end', value: 'end' }, +]; + +export const CHART_LEGEND_TYPE = [ + { name: 'legendType.plain', value: 'plain' }, + { name: 'legendType.scroll', value: 'scroll' }, ]; export const CHART_LINE_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; @@ -137,6 +179,7 @@ export enum FilterSqlOperator { NotSuffixContain = 'SUFFIX_NOT_LIKE', Between = 'BETWEEN', + NotBetween = 'NOT_BETWEEN', In = 'IN', NotIn = 'NOT_IN', LessThan = 'LT', diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index a7e5481fe..ec815f9a2 100755 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -16,7 +16,7 @@ * limitations under the License. */ -import 'antd/dist/antd.less'; +import 'antd/dist/antd.min.css'; import { App } from 'app'; import 'app/assets/fonts/iconfont.css'; import 'core-js/features/string/replace-all'; diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index 0345b82c5..c442749d0 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -162,7 +162,12 @@ "confirmPassword": "Confirm password" }, "switchLanguage": { - "title": "language" + "title": "Language" + }, + "switchTheme": { + "title": "Theme", + "light": "Light", + "dark": "Dark" }, "logout": { "title": "Logout" @@ -304,6 +309,7 @@ "SUFFIX_LIKE": "Suffix Contain", "SUFFIX_NOT_LIKE": "Not Suffix Contain", "BETWEEN": "Between", + "NOT_BETWEEN": "Not Between", "IN": "In", "NOT_IN": "Not In", "LT": "Less Than", @@ -448,6 +454,7 @@ "open": "Open", "close": "Close", "cancel": "Cancel", + "downloadList": "Download list", "lang": { "zh": "zh", "en": "en" @@ -500,7 +507,7 @@ } } }, - "conditionStyleTable": { + "conditionalStyleTable": { "btn": { "add": "Add", "edit": "Edit", @@ -508,7 +515,7 @@ "confirm": "Confirm" }, "modal": { - "title": "Condition Style", + "title": "Conditional Style", "notFoundContent": "Multiple values can be added (enter the content and press enter to complete the addition)" }, "header": { @@ -544,6 +551,44 @@ "bottom": "Bottom Margin", "top": "Top Margin", "containLabel": "Contain Label" + }, + "lineStyles": { + "solid": "Solid", + "dashed": "Dashed", + "dotted": "Dotted" + }, + "nameLocation": { + "start": "Start", + "center": "Center", + "end": "End" + }, + "legendType": { + "plain": "Plain", + "scroll": "Scroll" + }, + "fontFamily": { + "default": "Default", + "microsoftYaHei": "Microsoft YaHei", + "simSun": "SimSun", + "simHei": "SimHei", + "helveticaNeue": "Helvetica Neue", + "helvetica": "Helvetica", + "arial": "Arial", + "sansSerif": "sans-serif" + }, + "lineHeight": { + "default": "Default Line Height" + }, + "fontWeight": { + "normal": "Normal", + "bold": "Bold", + "bolder": "Bolder", + "lighter": "Lighter" + }, + "fontStyle": { + "normal": "Normal", + "italic": "Italic", + "oblique": "Oblique" } }, "data": { @@ -644,11 +689,20 @@ "endMetric": "End Reference Metric", "font": "Font", "showLabel": "Show", - "position": "Position", + "position": { + "title": "Position", + "start": "Start", + "middle": "Middle", + "end": "End" + }, "lineStyle": "Line Style", "opacity": "Opacity", "backgroundColor": "Background Color", - "borderStyle": "Border Style" + "borderStyle": "Border Style", + "constant": "Constant", + "avg": "AVG", + "max": "Max", + "min": "Min" }, "paging": { "title": "Common", @@ -722,6 +776,7 @@ "share": { "link": "Link", "password": "Password", + "link_password": "Link Password", "expireDate": "Expire Date", "enablePassword": "Password", "generateLink": "Generate Link", @@ -752,7 +807,9 @@ "delete": "Delete", "copy": "Copy", "paste": "Paste", - "deviceSwitch": "Device Switch" + "deviceSwitch": "Device Switch", + "allowOverlap": "Allow Overlap", + "forbidOverlap": "Forbid Overlap" }, "controlTypes": { "common": "Common", @@ -805,7 +862,8 @@ "autoPlay": "Auto Play", "duration": "Duration (s) ", "delPagesTip": "Confirm to delete all selected story pages?", - "delPageTip": "Confirm to delete this story page?" + "delPageTip": "Confirm to delete this story page?", + "enterHere": "Enter here" } }, "widget": { @@ -832,6 +890,7 @@ "action": { "refresh": "Refresh", "fullScreen": "FullScreen", + "lock": "Lock", "edit": "Edit", "delete": "Delete", "confirmDel": "Confirm Delete", @@ -841,6 +900,12 @@ "closeLinkage": "Close Linkage", "makeJump": "Set Jump", "closeJump": "Close Jump" + }, + "tips": { + "unlock": "LockIng drag and click to unlock", + "waiting": "Waiting for loadData", + "cancelLinkage": "Cancel Linkage", + "canLinkage": "Click the Widget to link" } }, "linkage": { @@ -849,7 +914,8 @@ "associatedWidgets": "Associated Widgets", "associatedFields": "Associated Fields", "selectTriggers": "Select the trigger field", - "selectLinker": "Select linkage field" + "selectLinker": "Select linkage field", + "linkageError": "The linkage chart has been deleted, please reconfigure" }, "jump": { "title": "Jump Settings", @@ -859,7 +925,8 @@ "URL": "URL", "parameters": "Parameters", "controller": "Associated Controllers", - "associatedFields": "Associated Fields" + "associatedFields": "Associated Fields", + "jumpError": "The jump target has been invalidated or deleted, please reconfigure" }, "associate": { "title": "Please associate fields/variables", @@ -937,9 +1004,12 @@ "name": "Name", "description": "Description", "boardType": { - "label": "Board type", + "label": "Layout", "auto": "Auto", - "free": "Free" + "free": "Free", + "autoTips": "Auto: Responsive, fluid layout", + "freeTips": "Free: Precise, fixed layout", + "requiredMessage": "Select a Layout Type" }, "parent": "Parent", "root": "Root", @@ -993,6 +1063,7 @@ "properties": { "reference": "Source reference", "variable": "Variables", + "model": "Data Model", "columnPermissions": "Column Permissions" }, "resource": { @@ -1006,6 +1077,16 @@ "prefix": "[Public]", "suffix": "duplicate" }, + "model": { + "type": "Type", + "rename": "Rename", + "delete": "Delete", + "typeAndCategory": "Type & Category", + "permission": "Column Permissions", + "newHierarchy": "New Hierarchy", + "addToHierarchy": "Add To Hierarchy", + "hierarchyName": "Hierarchy Name" + }, "columnPermission": { "title": "Column permissions", "search": "Search role keywords", @@ -1033,7 +1114,8 @@ "dirtyread": "Dirty read", "fastfailover": "Fast failover", "cache": "Cache", - "cacheExpires": "Expires(s)" + "cacheExpires": "Expires(s)", + "expensiveQuery": "This is an expensive query" }, "schemaTable": { "category": "Category", @@ -1048,6 +1130,9 @@ "archived": "Archived ", "noPermission": "You do not have permission to access this page", "creatView": "Creat View", + "syncDatabase": "Sync Database", + "syncDatabaseSchemaSuccess": "Sync database schema successfully", + "lastUpdateTime": "Last Update Time", "sidebar": { "title": "Sources", "add": "Create source", @@ -1098,6 +1183,7 @@ "remove": "Remove" }, "form": { + "email": "Email", "search": "Search or paste the emails of invited members", "needConfirm": "Need email confirmation" }, diff --git a/frontend/src/locales/i18n.ts b/frontend/src/locales/i18n.ts index b0fc6c0ef..995d7a1fd 100644 --- a/frontend/src/locales/i18n.ts +++ b/frontend/src/locales/i18n.ts @@ -29,6 +29,7 @@ export const changeLang = lang => { lang === 'zh' ? 'zh-CN' : 'en-US'; // FIXME locale localStorage.setItem(StorageKeys.Locale, lang); moment.locale(lang === 'zh' ? 'zh-cn' : 'en-us'); // FIXME locale + window.location && window.location.reload(); }; const initialLocale = getInitialLocale(); diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json index 8cf42ffb1..0122ada7a 100644 --- a/frontend/src/locales/zh/translation.json +++ b/frontend/src/locales/zh/translation.json @@ -162,7 +162,12 @@ "confirmPassword": "确定新密码" }, "switchLanguage": { - "title": "切换语言" + "title": "语言" + }, + "switchTheme": { + "title": "主题", + "light": "明亮", + "dark": "黑暗" }, "logout": { "title": "退出登录" @@ -303,7 +308,8 @@ "PREFIX_NOT_LIKE": "前缀不包含", "SUFFIX_LIKE": "后缀包含", "SUFFIX_NOT_LIKE": "后缀不包含", - "BETWEEN": "区间", + "BETWEEN": "区间内", + "NOT_BETWEEN": "区间外", "IN": "包括", "NOT_IN": "不包括", "LT": "小于", @@ -448,6 +454,7 @@ "open": "开启", "close": "关闭", "cancel": "取消", + "downloadList": "下载列表", "lang": { "zh": "中文", "en": "英文" @@ -500,7 +507,7 @@ } } }, - "conditionStyleTable": { + "conditionalStyleTable": { "btn": { "add": "新增", "edit": "编辑", @@ -544,6 +551,44 @@ "bottom": "底边距", "top": "上边距", "containLabel": "包含标题" + }, + "lineStyles": { + "solid": "实线", + "dashed": "虚线", + "dotted": "点" + }, + "nameLocation": { + "start": "开始", + "center": "中间", + "end": "结束" + }, + "legendType": { + "plain": "普通", + "scroll": "滚动" + }, + "fontFamily": { + "default": "默认字体", + "microsoftYaHei": "微软雅黑", + "simSun": "宋体", + "simHei": "黑体", + "helveticaNeue": "Helvetica Neue", + "helvetica": "Helvetica", + "arial": "Arial", + "sansSerif": "sans-serif" + }, + "fontWeight": { + "normal": "常规字号", + "bold": "粗体", + "bolder": "特粗体", + "lighter": "细体" + }, + "fontStyle": { + "normal": "常规体", + "italic": "斜体", + "oblique": "偏斜体" + }, + "lineHeight": { + "default": "默认行高" } }, "data": { @@ -644,11 +689,20 @@ "endMetric": "结束关联指标", "font": "字体", "showLabel": "显示标签", - "position": "标签位置", + "position": { + "title": "标签位置", + "start": "开始", + "middle": "中间", + "end": "结尾" + }, "lineStyle": "线条样式", "opacity": "透明度", "backgroundColor": "背景颜色", - "borderStyle": "边框样式" + "borderStyle": "边框样式", + "constant": "常量", + "avg": "平均值", + "max": "最大值", + "min": "最小值" }, "paging": { "title": "常规", @@ -722,6 +776,7 @@ "share": { "link": "链接", "password": "密码", + "link_password": "链接&密码", "expireDate": "截止日期", "enablePassword": "启用密码", "generateLink": "生成链接", @@ -752,7 +807,9 @@ "delete": "删除", "copy": "复制", "paste": "粘贴", - "deviceSwitch": "设备切换" + "deviceSwitch": "设备切换", + "allowOverlap": "允许组件重叠", + "forbidOverlap": "禁止组件重叠" }, "controlTypes": { "common": "常规", @@ -805,7 +862,8 @@ "autoPlay": "自动播放", "duration": "停留时间(秒)", "delPagesTip": "确认删除所有选中的故事页", - "delPageTip": "确认删除此故事页?" + "delPageTip": "确认删除此故事页?", + "enterHere": "请输入" } }, "widget": { @@ -832,6 +890,7 @@ "action": { "refresh": "同步数据", "fullScreen": "全屏", + "lock": "锁定拖拽", "edit": "编辑", "delete": "删除", "confirmDel": "确认删除", @@ -841,6 +900,12 @@ "closeLinkage": "关闭联动", "makeJump": "跳转设置", "closeJump": "关闭跳转" + }, + "tips": { + "unlock": "已锁定拖拽,点击解锁", + "waiting": "等待加载", + "cancelLinkage": "取消联动", + "canLinkage": "点击图表可联动" } }, "linkage": { @@ -849,7 +914,8 @@ "associatedWidgets": "关联组件", "associatedFields": "关联字段", "selectTriggers": "选择触发字段", - "selectLinker": "选择联动字段" + "selectLinker": "选择联动字段", + "linkageError": "联动的图表已经被删除,请重新配置" }, "jump": { "title": "跳转设置", @@ -859,7 +925,8 @@ "URL": "URL", "parameters": "Parameters", "controller": "关联控制器", - "associatedFields": "关联字段" + "associatedFields": "关联字段", + "jumpError": "跳转的目标已失效或被删除,请重新配置" }, "associate": { "title": "关联字段/变量", @@ -938,8 +1005,11 @@ "description": "描述", "boardType": { "label": "布局类型", - "auto": "自动", - "free": "自由" + "auto": "自动布局", + "free": "自由布局", + "autoTips": "自动布局:流式布局,可适配多终端", + "freeTips": "自由布局:固定布局,用于制作大屏", + "requiredMessage": "请选择一种布局方式" }, "parent": "所属目录", "root": "根目录", @@ -993,6 +1063,7 @@ "properties": { "reference": "数据源信息", "variable": "变量配置", + "model": "数据模型", "columnPermissions": "列权限" }, "resource": { @@ -1006,6 +1077,16 @@ "prefix": "[公共]", "suffix": "重复" }, + "model": { + "category": "分类", + "rename": "重命名", + "delete": "删除", + "typeAndCategory": "类型与分类", + "permission": "列权限", + "newHierarchy": "新建层次结构", + "addToHierarchy": "添加到层次结构", + "hierarchyName": "层级名称" + }, "columnPermission": { "title": "列权限", "search": "搜索角色关键字", @@ -1033,7 +1114,8 @@ "dirtyread": "延迟更新", "fastfailover": "快速失败", "cache": "缓存", - "cacheExpires": "有效期(秒)" + "cacheExpires": "有效期(秒)", + "expensiveQuery": "这是一个消耗资源的查询" }, "schemaTable": { "category": "分类", @@ -1048,6 +1130,9 @@ "archived": "已归档", "noPermission": "您没有权限访问该页面", "creatView": "去新建数据视图", + "syncDatabase": "同步数据库模式", + "syncDatabaseSchemaSuccess": "已成功获取数据库模式", + "lastUpdateTime": "最近同步时间", "sidebar": { "title": "数据源列表", "add": "新建数据源", @@ -1098,6 +1183,7 @@ "remove": "移除" }, "form": { + "email": "邮箱", "search": "请搜索或粘贴被邀请成员邮箱", "needConfirm": "需要被邀请成员邮件确认" }, diff --git a/frontend/src/share.tsx b/frontend/src/share.tsx index 2ee9ae7d6..fb5be600b 100644 --- a/frontend/src/share.tsx +++ b/frontend/src/share.tsx @@ -16,7 +16,7 @@ * limitations under the License. */ -import 'antd/dist/antd.less'; +import 'antd/dist/antd.min.css'; import 'app/assets/fonts/iconfont.css'; import { Share } from 'app/share'; import 'core-js/features/string/replace-all'; diff --git a/frontend/src/styles/StyleConstants.ts b/frontend/src/styles/StyleConstants.ts index 4a0a42b81..6e3ed5cd5 100644 --- a/frontend/src/styles/StyleConstants.ts +++ b/frontend/src/styles/StyleConstants.ts @@ -19,7 +19,6 @@ export const MODAL_LEVEL = 1000; // base color export const BLUE = '#1B9AEE'; -export const BLUERGB = '27, 154, 238'; export const GREEN = '#15AD31'; export const ORANGE = '#FA8C15'; export const YELLOW = '#FAD414'; @@ -38,20 +37,29 @@ export const RED = '#E62412'; * G80 - font */ export const WHITE = '#FFFFFF'; -export const G10 = '#f8f9fa'; -export const G20 = '#e9ecef'; -export const G30 = '#dee2e6'; -export const G40 = '#ced4da'; -export const G50 = '#adb5bd'; -export const G60 = '#6c757d'; -export const G70 = '#495057'; -export const G80 = '#343a40'; -export const G90 = '#212529'; +export const G10 = '#F5F8FA'; +export const G20 = '#EFF2F5'; +export const G30 = '#E4E6EF'; +export const G40 = '#B5B5C3'; +export const G50 = '#A1A5B7'; +export const G60 = '#7E8299'; +export const G70 = '#5E6278'; +export const G80 = '#3F4254'; +export const G90 = '#181C32'; export const BLACK = '#000000'; +export const DG10 = '#1b1b29'; +export const DG20 = '#2B2B40'; +export const DG30 = '#323248'; +export const DG40 = '#474761'; +export const DG50 = '#565674'; +export const DG60 = '#6D6D80'; +export const DG70 = '#92929F'; +export const DG80 = '#CDCDDE'; +export const DG90 = '#FFFFFF'; + // theme color export const PRIMARY = BLUE; -export const PRIMARYRGB = BLUERGB; export const INFO = PRIMARY; export const SUCCESS = GREEN; export const PROCESSING = BLUE; diff --git a/frontend/src/styles/__tests__/media.test.ts b/frontend/src/styles/__tests__/media.test.ts index cb8dfd96d..b020a20e7 100644 --- a/frontend/src/styles/__tests__/media.test.ts +++ b/frontend/src/styles/__tests__/media.test.ts @@ -3,12 +3,14 @@ import { media, sizes } from '../media'; describe('media', () => { it('should return media query in css', () => { - const mediaQuery = media.small`color: red;`.join(''); + const mediaQuery = media.small`color: red;`.join('').replace(/ /g, ''); const cssVersion = css` @media (min-width: ${sizes.small}px) { color: red; } - `.join(''); + ` + .join('') + .replace(/ /g, ''); expect(mediaQuery).toEqual(cssVersion); }); }); diff --git a/frontend/src/styles/antd/variables.less b/frontend/src/styles/antd/variables.less new file mode 100644 index 000000000..1f19d10dd --- /dev/null +++ b/frontend/src/styles/antd/variables.less @@ -0,0 +1 @@ +@import "~antd/lib/style/index.less"; \ No newline at end of file diff --git a/frontend/src/styles/globalStyles.ts b/frontend/src/styles/globalStyles.ts deleted file mode 100644 index f66f31038..000000000 --- a/frontend/src/styles/globalStyles.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { createGlobalStyle } from 'styled-components'; -import { - EMPHASIS_LEVEL, - FONT_FAMILY, - FONT_SIZE_BODY, - MODAL_LEVEL, - SPACE_SM, - SPACE_TIMES, - SPACE_XS, -} from './StyleConstants'; -/* istanbul ignore next */ -export const GlobalStyle = createGlobalStyle` - body { - font-size: ${FONT_SIZE_BODY}; - font-family: ${FONT_FAMILY}; - background-color: ${p => p.theme.bodyBackground}; - } - - h1,h2,h3,h4,h5,h6 { - margin: 0; - font-weight: inherit; - color: inherit; - } - - p, figure { - margin: 0; - } - - ul { - padding: 0; - margin: 0; - } - - li { - list-style-type: none; - } - - input { - padding: 0; - } - - table th { - padding: 0; - text-align: center; - } - - * { - -webkit-overflow-scrolling: touch; - } -`; - -export const OverriddenStyle = createGlobalStyle` - /* app/components/Popup */ - .datart-popup { - z-index: ${MODAL_LEVEL - 1}; - - &.on-modal { - z-index: ${MODAL_LEVEL + 30}; - } - - .ant-popover-arrow { - display: none; - } - .ant-popover-inner-content { - padding: 0; - } - .ant-dropdown-menu { - box-shadow: none; - } - &.ant-popover-placement-bottom, - &.ant-popover-placement-bottomLeft, - &.ant-popover-placement-bottomRight { - padding-top: 0; - } - } - - .ant-form-item-label > label { - color: ${p => p.theme.textColorLight}; - } - - .ant-form-item-label-left > label { - padding-left: ${SPACE_SM}; - - &:before { - position: absolute; - left: 0; - } - } - - .ant-popover-inner { - box-shadow: ${p => p.theme.shadow3}; - } - - /* react-split */ - .datart-split { - min-width: 0; - min-height: 0; - - .gutter-horizontal { - &:before { - width: 2px; - height: 100%; - transform: translate(-50%, 0); - } - &:after { - width: ${SPACE_TIMES(2)}; - height: 100%; - cursor: ew-resize; - transform: translate(-50%, 0); - } - } - - .gutter-vertical { - &:before { - width: 100%; - height: 2px; - transform: translate(0, -50%); - } - &:after { - width: 100%; - height: ${SPACE_TIMES(2)}; - cursor: ns-resize; - transform: translate(0, -50%); - } - } - - .gutter-horizontal, - .gutter-vertical{ - position: relative; - - &:before { - position: absolute; - z-index: ${EMPHASIS_LEVEL}; - content: ''; - } - &:after { - position: absolute; - z-index: ${EMPHASIS_LEVEL}; - content: ''; - } - &:hover, - &:active { - &:before { - background-color: ${p => p.theme.primary}; - } - } - } - } - - /* react-grid-layout */ - .react-grid-item.react-grid-placeholder { - background-color: ${p => p.theme.textColorDisabled} !important; - } - - - /* schema table header action dropdown menu */ - .datart-schema-table-header-menu { - min-width: ${SPACE_TIMES(40)}; - - .ant-dropdown-menu-submenu-selected { - .ant-dropdown-menu-submenu-title { - color: ${p => p.theme.textColor}; - } - } - } - - /* config panel */ - .datart-config-panel { - &.ant-collapse > - .ant-collapse-item > - .ant-collapse-header { - padding: ${SPACE_XS} 0; - color: ${p => p.theme.textColor}; - - .ant-collapse-arrow { - margin-right: ${SPACE_XS}; - } - } - - .ant-collapse-content > - .ant-collapse-content-box { - padding: ${SPACE_XS} 0 ${SPACE_SM} !important; - } - } - - /* data config section dropdown */ - .datart-data-section-dropdown { - z-index: ${MODAL_LEVEL - 1}; - } - .aggregation-colorpopover{ - .ant-popover-arrow{ - display:none; - } - } - - /* fix antd bugs #32919 */ - .ant-tabs-dropdown-menu-item { - display: flex; - align-items: center; - - > span { - flex: 1; - white-space: nowrap; - } - - &-remove { - flex: none; - margin-left: 12px; - color: #00000073; - font-size: 12px; - background: 0 0; - border: 0; - cursor: pointer; - - &:hover { - color: #1B9AEE; - } - } - } - - /* 覆盖antd 默认样式 */ - @media (max-width: 575px) { - .datart-viz .ant-form .ant-form-item .ant-form-item-label, - .datart-viz .ant-form .ant-form-item .ant-form-item-control { - flex: 1; - } - } -`; diff --git a/frontend/src/styles/globalStyles/base.ts b/frontend/src/styles/globalStyles/base.ts new file mode 100644 index 000000000..a868ba8f6 --- /dev/null +++ b/frontend/src/styles/globalStyles/base.ts @@ -0,0 +1,61 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { createGlobalStyle } from 'styled-components/macro'; +import { FONT_FAMILY, FONT_SIZE_BODY } from 'styles/StyleConstants'; + +/* istanbul ignore next */ +export const Base = createGlobalStyle` + body { + font-family: ${FONT_FAMILY}; + font-size: ${FONT_SIZE_BODY}; + background-color: ${p => p.theme.bodyBackground}; + + h1,h2,h3,h4,h5,h6 { + margin: 0; + font-weight: inherit; + color: inherit; + } + + p, figure { + margin: 0; + } + + ul { + padding: 0; + margin: 0; + } + + li { + list-style-type: none; + } + + input { + padding: 0; + } + + table th { + padding: 0; + text-align: center; + } + + * { + -webkit-overflow-scrolling: touch; + } + } +`; diff --git a/frontend/src/styles/globalStyles/index.tsx b/frontend/src/styles/globalStyles/index.tsx new file mode 100644 index 000000000..223e98952 --- /dev/null +++ b/frontend/src/styles/globalStyles/index.tsx @@ -0,0 +1,19 @@ +import { Base } from './base'; +import { Form } from './overwritten/form'; +import { GlobalOverlays } from './overwritten/globalOverlays'; +import { Hardcoded } from './overwritten/hardcoded'; +import { Viz } from './overwritten/viz'; +import { ReactSplit } from './reactSplit'; + +export function GlobalStyles() { + return ( + <> + + + + + + + + ); +} diff --git a/frontend/src/styles/globalStyles/overwritten/form.ts b/frontend/src/styles/globalStyles/overwritten/form.ts new file mode 100644 index 000000000..dc50dd88b --- /dev/null +++ b/frontend/src/styles/globalStyles/overwritten/form.ts @@ -0,0 +1,109 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { createGlobalStyle } from 'styled-components/macro'; +import { BORDER_RADIUS } from 'styles/StyleConstants'; + +export const Form = createGlobalStyle` + .datart-ant-input { + &.ant-input { + color: ${p => p.theme.textColorSnd}; + background-color: ${p => p.theme.bodyBackground}; + border-color: ${p => p.theme.bodyBackground}; + box-shadow: none; + + &:hover, + &:focus { + border-color: ${p => p.theme.bodyBackground}; + box-shadow: none; + } + + &:focus { + background-color: ${p => p.theme.emphasisBackground}; + } + } + } + + .datart-ant-select { + &.ant-select { + color: ${p => p.theme.textColorSnd}; + } + + &.ant-select:not(.ant-select-customize-input) .ant-select-selector { + background-color: ${p => p.theme.bodyBackground}; + border-color: ${p => p.theme.bodyBackground} !important; + border-radius: ${BORDER_RADIUS}; + box-shadow: none !important; + } + } + + .datart-ant-input-number { + &.ant-input-number { + width: 100%; + background-color: ${p => p.theme.bodyBackground}; + border-color: ${p => p.theme.bodyBackground}; + border-radius: ${BORDER_RADIUS}; + box-shadow: none; + + &:hover, + &:focus { + border-color: ${p => p.theme.bodyBackground}; + box-shadow: none; + } + + &:focus { + background-color: ${p => p.theme.bodyBackground}; + } + } + + .ant-input-number-input { + color: ${p => p.theme.textColorSnd}; + } + + .ant-input-number-handler-wrap { + background-color: ${p => p.theme.bodyBackground}; + } + + .ant-input-number-disabled { + background-color: ${p => p.theme.textColorDisabled}; + } + } + + .datart-ant-upload { + &.ant-upload.ant-upload-drag { + background-color: ${p => p.theme.bodyBackground} !important; + border-color: transparent !important; + border-radius: ${BORDER_RADIUS}; + } + } + + .datart-modal-button { + &.ant-btn { + color: ${p => p.theme.textColorSnd}; + background-color: ${p => p.theme.bodyBackground}; + border: 0; + border-radius: ${BORDER_RADIUS}; + + &:hover, + &:active { + color: ${p => p.theme.textColorSnd}; + background-color: ${p => p.theme.emphasisBackground}; + } + } + } +`; diff --git a/frontend/src/styles/globalStyles/overwritten/globalOverlays.ts b/frontend/src/styles/globalStyles/overwritten/globalOverlays.ts new file mode 100644 index 000000000..82b0c6279 --- /dev/null +++ b/frontend/src/styles/globalStyles/overwritten/globalOverlays.ts @@ -0,0 +1,94 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { createGlobalStyle } from 'styled-components/macro'; +import { + MODAL_LEVEL, + SPACE_SM, + SPACE_TIMES, + SPACE_XS, +} from 'styles/StyleConstants'; + +export const GlobalOverlays = createGlobalStyle` + /* app/components/Popup */ + .datart-popup { + z-index: ${MODAL_LEVEL - 1}; + + &.on-modal { + z-index: ${MODAL_LEVEL + 30}; + } + + .ant-popover-arrow { + display: none; + } + .ant-popover-inner-content { + padding: 0; + } + .ant-dropdown-menu { + box-shadow: none; + } + &.ant-popover-placement-bottom, + &.ant-popover-placement-bottomLeft, + &.ant-popover-placement-bottomRight { + padding-top: 0; + } + } + + + /* schema table header action dropdown menu */ + .datart-schema-table-header-menu { + min-width: ${SPACE_TIMES(40)}; + + .ant-dropdown-menu-submenu-selected { + .ant-dropdown-menu-submenu-title { + color: ${p => p.theme.textColor}; + } + } + } + + /* config panel */ + .datart-config-panel { + &.ant-collapse > + .ant-collapse-item > + .ant-collapse-header { + padding: ${SPACE_XS} 0; + color: ${p => p.theme.textColor}; + + .ant-collapse-arrow { + margin-right: ${SPACE_XS}; + } + } + + .ant-collapse-content > + .ant-collapse-content-box { + padding: ${SPACE_XS} 0 ${SPACE_SM} !important; + } + } + + /* data config section dropdown */ + .datart-data-section-dropdown { + z-index: ${MODAL_LEVEL - 1}; + } + + /* color popover */ + .datart-aggregation-colorpopover{ + .ant-popover-arrow{ + display:none; + } + } +`; diff --git a/frontend/src/styles/globalStyles/overwritten/hardcoded.ts b/frontend/src/styles/globalStyles/overwritten/hardcoded.ts new file mode 100644 index 000000000..e42100f07 --- /dev/null +++ b/frontend/src/styles/globalStyles/overwritten/hardcoded.ts @@ -0,0 +1,74 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { createGlobalStyle } from 'styled-components/macro'; +import { FONT_SIZE_LABEL, SPACE_SM } from 'styles/StyleConstants'; + +export const Hardcoded = createGlobalStyle` + body { + .ant-form-item-label > label { + color: ${p => p.theme.textColorLight}; + } + + .ant-form-item-label-left > label { + padding-left: ${SPACE_SM}; + + &:before { + position: absolute; + left: 0; + } + } + + .ant-popover-inner { + box-shadow: ${p => p.theme.shadow3}; + } + .ant-popover.ant-popconfirm { + z-index: 1060; + } + + /* fix antd bugs #32919 */ + .ant-tabs-dropdown-menu-item { + display: flex; + align-items: center; + + > span { + flex: 1; + white-space: nowrap; + } + + &-remove { + flex: none; + margin-left: ${SPACE_SM}; + font-size: ${FONT_SIZE_LABEL}; + color: ${p => p.theme.textColorLight}; + cursor: pointer; + background: 0 0; + border: 0; + + &:hover { + color: ${p => p.theme.primary}; + } + } + } + } + + /* react-grid-layout */ + .react-grid-item.react-grid-placeholder { + background-color: ${p => p.theme.textColorDisabled} !important; + } +`; diff --git a/frontend/src/styles/globalStyles/overwritten/viz.ts b/frontend/src/styles/globalStyles/overwritten/viz.ts new file mode 100644 index 000000000..1142bd3c5 --- /dev/null +++ b/frontend/src/styles/globalStyles/overwritten/viz.ts @@ -0,0 +1,29 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { createGlobalStyle } from 'styled-components/macro'; + +export const Viz = createGlobalStyle` + /* 覆盖antd 默认样式 */ + @media (max-width: 575px) { + .datart-viz .ant-form .ant-form-item .ant-form-item-label, + .datart-viz .ant-form .ant-form-item .ant-form-item-control { + flex: 1; + } + } +`; diff --git a/frontend/src/styles/globalStyles/reactSplit.ts b/frontend/src/styles/globalStyles/reactSplit.ts new file mode 100644 index 000000000..d3955fa17 --- /dev/null +++ b/frontend/src/styles/globalStyles/reactSplit.ts @@ -0,0 +1,78 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed 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. + */ + +import { createGlobalStyle } from 'styled-components/macro'; +import { EMPHASIS_LEVEL, SPACE_TIMES } from 'styles/StyleConstants'; + +export const ReactSplit = createGlobalStyle` + /* react-split */ + .datart-split { + min-width: 0; + min-height: 0; + + .gutter-horizontal { + &:before { + width: 2px; + height: 100%; + transform: translate(-50%, 0); + } + &:after { + width: ${SPACE_TIMES(2)}; + height: 100%; + cursor: ew-resize; + transform: translate(-50%, 0); + } + } + + .gutter-vertical { + &:before { + width: 100%; + height: 2px; + transform: translate(0, -50%); + } + &:after { + width: 100%; + height: ${SPACE_TIMES(2)}; + cursor: ns-resize; + transform: translate(0, -50%); + } + } + + .gutter-horizontal, + .gutter-vertical{ + position: relative; + + &:before { + position: absolute; + z-index: ${EMPHASIS_LEVEL}; + content: ''; + } + &:after { + position: absolute; + z-index: ${EMPHASIS_LEVEL}; + content: ''; + } + &:hover, + &:active { + &:before { + background-color: ${p => p.theme.primary}; + } + } + } + } +`; diff --git a/frontend/src/styles/theme/ThemeProvider.tsx b/frontend/src/styles/theme/ThemeProvider.tsx index 5af026b2e..a37720d25 100644 --- a/frontend/src/styles/theme/ThemeProvider.tsx +++ b/frontend/src/styles/theme/ThemeProvider.tsx @@ -1,13 +1,20 @@ -import React from 'react'; +import React, { useLayoutEffect } from 'react'; import { useSelector } from 'react-redux'; import { ThemeProvider as OriginalThemeProvider } from 'styled-components'; import { useThemeSlice } from './slice'; -import { selectTheme } from './slice/selectors'; +import { selectTheme, selectThemeKey } from './slice/selectors'; +import { changeAntdTheme } from './utils'; export const ThemeProvider = (props: { children: React.ReactChild }) => { useThemeSlice(); const theme = useSelector(selectTheme); + const themeKey = useSelector(selectThemeKey); + + useLayoutEffect(() => { + changeAntdTheme(themeKey); + }, []); // eslint-disable-line + return ( {React.Children.only(props.children)} diff --git a/frontend/src/styles/theme/slice/index.ts b/frontend/src/styles/theme/slice/index.ts index 81e58f62c..29e0468fe 100644 --- a/frontend/src/styles/theme/slice/index.ts +++ b/frontend/src/styles/theme/slice/index.ts @@ -5,10 +5,10 @@ import { getThemeFromStorage } from '../utils'; import { ThemeKeyType, ThemeState } from './types'; export const initialState: ThemeState = { - selected: getThemeFromStorage() || 'light', + selected: getThemeFromStorage(), }; -const slice = createSlice({ +const themeSlice = createSlice({ name: 'theme', initialState, reducers: { @@ -18,9 +18,11 @@ const slice = createSlice({ }, }); -export const { actions: themeActions, reducer } = slice; +export default themeSlice; + +export const { actions: themeActions, reducer } = themeSlice; export const useThemeSlice = () => { - useInjectReducer({ key: slice.name, reducer: slice.reducer }); - return { actions: slice.actions }; + useInjectReducer({ key: themeSlice.name, reducer: themeSlice.reducer }); + return { actions: themeSlice.actions }; }; diff --git a/frontend/src/styles/theme/themes.ts b/frontend/src/styles/theme/themes.ts index 52023f709..9cc5ed35d 100644 --- a/frontend/src/styles/theme/themes.ts +++ b/frontend/src/styles/theme/themes.ts @@ -2,6 +2,15 @@ import { lighten, rgba } from 'polished'; import { BLACK, BLUE, + DG10, + DG20, + DG30, + DG40, + DG50, + DG60, + DG70, + DG80, + DG90, ERROR, G10, G20, @@ -18,7 +27,6 @@ import { NORMAL, ORANGE, PRIMARY, - PRIMARYRGB, PROCESSING, RED, SUCCESS, @@ -29,7 +37,6 @@ import { const common = { primary: PRIMARY, - primaryRgb: PRIMARYRGB, info: INFO, success: SUCCESS, processing: PROCESSING, @@ -53,8 +60,7 @@ const lightTheme = { emphasisBackground: G20, highlightBackground: G30, textColor: G90, - textColorSub: G80, - textColorSnd: G70, + textColorSnd: G80, textColorLight: G60, textColorDisabled: G50, iconColorHover: rgba(BLACK, 0.75), @@ -71,18 +77,17 @@ const lightTheme = { const darkTheme: Theme = { bodyBackground: BLACK, - componentBackground: rgba(WHITE, 0.08), - emphasisBackground: rgba(WHITE, 0.12), - highlightBackground: rgba(WHITE, 0.16), - textColor: rgba(WHITE, 0.85), - textColorSub: rgba(WHITE, 0.75), - textColorSnd: rgba(WHITE, 0.65), - textColorLight: rgba(WHITE, 0.45), - textColorDisabled: rgba(WHITE, 0.3), - iconColorHover: rgba(WHITE, 0.75), - borderColorBase: '#434343', - borderColorEmphasis: '#373737', - borderColorSplit: '#303030', + componentBackground: DG10, + emphasisBackground: DG20, + highlightBackground: DG30, + textColor: DG90, + textColorSnd: DG80, + textColorLight: DG60, + textColorDisabled: DG50, + iconColorHover: DG70, + borderColorBase: DG40, + borderColorEmphasis: DG30, + borderColorSplit: DG20, shadow1: `0 1px 5px 0 ${rgba(BLACK, 0.1)}`, shadow2: `0 6px 18px 0 ${rgba(BLACK, 0.4)}`, shadow3: `0 10px 32px 0 ${rgba(BLACK, 0.54)}`, diff --git a/frontend/src/styles/theme/utils.ts b/frontend/src/styles/theme/utils.ts index 93869a786..54b04f76f 100644 --- a/frontend/src/styles/theme/utils.ts +++ b/frontend/src/styles/theme/utils.ts @@ -1,4 +1,8 @@ +import darkTheme from 'antd/dist/dark-theme'; +import lightTheme from 'antd/dist/default-theme'; +import { StorageKeys } from 'globalConstants'; import { ThemeKeyType } from './slice/types'; +import { themes } from './themes'; /* istanbul ignore next line */ export const isSystemDark = window?.matchMedia @@ -6,12 +10,53 @@ export const isSystemDark = window?.matchMedia : undefined; export function saveTheme(theme: ThemeKeyType) { - window.localStorage && localStorage.setItem('selectedTheme', theme); + window.localStorage && localStorage.setItem(StorageKeys.Theme, theme); } /* istanbul ignore next line */ -export function getThemeFromStorage(): ThemeKeyType | null { - return window.localStorage - ? (localStorage.getItem('selectedTheme') as ThemeKeyType) || null - : null; +export function getThemeFromStorage(): ThemeKeyType { + let theme = 'light' as ThemeKeyType; + try { + const storedTheme = + window.localStorage && localStorage.getItem(StorageKeys.Theme); + if (storedTheme) { + theme = storedTheme as ThemeKeyType; + } + } catch (error) { + throw error; + } + return theme; +} + +export function getTokenVariableMapping(themeKey: string) { + const currentTheme = themes[themeKey]; + return { + '@primary-color': currentTheme.primary, + '@success-color': currentTheme.success, + '@processing-color': currentTheme.processing, + '@error-color': currentTheme.error, + '@highlight-color': currentTheme.highlight, + '@warning-color': currentTheme.warning, + '@body-background': currentTheme.bodyBackground, + '@text-color': currentTheme.textColor, + '@text-color-secondary': currentTheme.textColorLight, + '@heading-color': currentTheme.textColor, + '@disabled-color': currentTheme.textColorDisabled, + }; +} + +export function getVarsToBeModified(themeKey: string) { + const tokenVariableMapping = getTokenVariableMapping(themeKey); + return { + ...(themeKey === 'light' ? lightTheme : darkTheme), + ...tokenVariableMapping, + }; +} + +export async function changeAntdTheme(themeKey: string) { + try { + await (window as any).less.modifyVars(getVarsToBeModified(themeKey)); + } catch (error) { + throw error; + } } diff --git a/frontend/src/task.ts b/frontend/src/task.ts index fb543fc29..2ce1af771 100644 --- a/frontend/src/task.ts +++ b/frontend/src/task.ts @@ -19,7 +19,7 @@ // organize-imports-ignore polyfill/stable must in the first import 'react-app-polyfill/stable'; import 'core-js/features/string/replace-all'; -import { migrateWidgets } from 'app/migration/WidgetConfig/migrateWidgets'; +import { migrateWidgets } from 'app/migration/BoardConfig/migrateWidgets'; import { ChartDataRequestBuilder } from 'app/pages/ChartWorkbenchPage/models/ChartDataRequestBuilder'; import { DataChart, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index df98273fb..c06aeed47 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -57,3 +57,5 @@ export declare type Currency = { }; export type ValueOf = T[keyof T]; + +export type Nullable = T | null | undefined; diff --git a/frontend/src/utils/object.ts b/frontend/src/utils/object.ts index 355b525a4..669b4975f 100644 --- a/frontend/src/utils/object.ts +++ b/frontend/src/utils/object.ts @@ -233,7 +233,7 @@ export function mergeDefaultToValue( export function cleanChartConfigValueByDefaultValue( configs?: ChartStyleConfig[], ): ChartStyleConfig[] { - return (configs || []).map(c => { + return (configs || []).filter(Boolean).map(c => { if (c.comType !== 'group') { c.value = c.default; } diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index e9778dffc..85d25fea0 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -94,6 +94,7 @@ export function listToTree< getIcon?: (o: T) => ReactElement | ((props: TreeNodeProps) => ReactElement); getDisabled?: (o: T, path: string[]) => boolean; getSelectable?: (o: T) => boolean; + filter?: (path: string[], o: T) => boolean; }, ): undefined | any[] { if (!list) { @@ -104,8 +105,11 @@ export function listToTree< const childrenList: T[] = []; list.forEach(o => { + const path = parentPath.concat(o.id); + if (options?.filter && !options.filter(path, o)) { + return false; + } if (o.parentId === parentId) { - const path = parentPath.concat(o.id); treeNodes.push({ ...o, key: o.id, diff --git a/pom.xml b/pom.xml index bebda0dc6..1a54b24bc 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ datart datart-parent pom - 1.0.0-beta.1 + 1.0.0-beta.2 diff --git a/security/pom.xml b/security/pom.xml index e40e525de..2ccbd0715 100644 --- a/security/pom.xml +++ b/security/pom.xml @@ -5,7 +5,7 @@ datart-parent datart - 1.0.0-beta.1 + 1.0.0-beta.2 4.0.0 diff --git a/server/pom.xml b/server/pom.xml index 8b8708e5f..984aff957 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -5,7 +5,7 @@ datart-parent datart - 1.0.0-beta.1 + 1.0.0-beta.2 4.0.0 @@ -66,6 +66,26 @@ spring-boot-starter-data-redis + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + org.springframework.boot + spring-boot-starter-data-ldap + + + + com.jayway.jsonpath + json-path + 2.7.0 + + + org.flywaydb + flyway-core + + datart datart-core @@ -151,19 +171,13 @@ io.springfox springfox-swagger2 - 2.6.1 - - - guava - com.google.guava - - + 2.7.0 io.springfox springfox-swagger-ui - 2.6.1 + 2.7.0 @@ -196,7 +210,7 @@ - application-config.yml + application-dev.yml @@ -224,7 +238,7 @@ org.apache.maven.plugins maven-surefire-plugin - true + @@ -303,7 +317,8 @@ ${project.parent.basedir}/frontend/build/task/index.js - ${project.basedir}/src/main/resources/javascript/parser.js + ${project.basedir}/src/main/resources/javascript/parser.js + diff --git a/server/src/main/java/datart/server/base/dto/chart/ChartColumn.java b/server/src/main/java/datart/server/base/dto/chart/ChartColumn.java new file mode 100644 index 000000000..2437b67d6 --- /dev/null +++ b/server/src/main/java/datart/server/base/dto/chart/ChartColumn.java @@ -0,0 +1,115 @@ +package datart.server.base.dto.chart; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import datart.core.entity.poi.format.PoiNumFormat; +import datart.core.entity.poi.format.CurrencyFormat; +import datart.core.entity.poi.format.NumericFormat; +import datart.core.entity.poi.format.PercentageFormat; +import datart.core.entity.poi.format.ScientificNotationFormat; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class ChartColumn { + + private String uid = ""; + + private String colName = ""; + + private String category = ""; + + private String type = ""; + + private String label = ""; + + private ChartColumn.Alias alias = new ChartColumn.Alias(); + + private String desc = ""; + + private boolean isGroup; + + private String aggregate = ""; + + private List children = new ArrayList<>(); + + private JSONObject format; + + private int leafNum = 1; + + private int deepNum = 1; + + public void setChildren(List children) { + this.children = children; + this.leafNum = calLeafNum(); + this.deepNum = calDeepNum(); + } + + public String getColName() { + return isGroup ? label : + StringUtils.isNotBlank(aggregate) ? aggregate+"("+colName+")" : colName; + } + + public PoiNumFormat getNumFormat(){ + PoiNumFormat numFormat = new PoiNumFormat(); + if (format == null || !format.containsKey("type")){ + return numFormat; + } + String type = format.getString("type"); + switch (type) { + case NumericFormat.type: + numFormat = JSON.parseObject(format.getString(type), NumericFormat.class); + break; + case CurrencyFormat.type: + numFormat = JSON.parseObject(format.getString(type), CurrencyFormat.class); + break; + case PercentageFormat.type: + numFormat = JSON.parseObject(format.getString(type), PercentageFormat.class); + break; + case ScientificNotationFormat.type: + numFormat = JSON.parseObject(format.getString(type), ScientificNotationFormat.class); + break; + default: + break; + } + return numFormat; + } + + public List getLeafNodes(){ + List leafNodes = new ArrayList<>(); + if (this.leafNum == 1 && !this.isGroup){ + leafNodes.add(this); + } + for (ChartColumn child : children) { + leafNodes.addAll(child.getLeafNodes()); + } + return leafNodes; + } + + private int calLeafNum() { + int num = 0; + for (ChartColumn child : children) { + num += child.getLeafNum(); + } + return num; + } + + private int calDeepNum() { + int num = 0; + for (ChartColumn child : children) { + if (child.getDeepNum()>num){ + num = child.getDeepNum(); + } + } + return num+1; + } + + @Data + public class Alias { + private String name; + } + +} diff --git a/server/src/main/java/datart/server/base/dto/chart/ChartConfigDTO.java b/server/src/main/java/datart/server/base/dto/chart/ChartConfigDTO.java new file mode 100644 index 000000000..76c1ec2e4 --- /dev/null +++ b/server/src/main/java/datart/server/base/dto/chart/ChartConfigDTO.java @@ -0,0 +1,43 @@ +package datart.server.base.dto.chart; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONValidator; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class ChartConfigDTO { + + private ChartDetailConfigDTO chartConfig = new ChartDetailConfigDTO(); + + private String chartGraphId; + + private Boolean aggregation; + + private static final String COLUMN_KEY = "mixed"; + + private static final String TITLE_KEY = "header"; + + private static final String MODAL_KEY = "modal"; + + private static final String HEADER_KEY = "tableHeaders"; + + public List getColumnSettings(){ + ChartDataConfigDTO mixed = this.chartConfig.getDatas().stream() + .filter(item -> COLUMN_KEY.equals(item.getKey())).findFirst().orElse(new ChartDataConfigDTO()); + return mixed.getRows(); + } + + public List getDataHeaders(){ + List chartHeaders = new ArrayList<>(); + ChartStyleConfigDTO title = this.chartConfig.getStyles().stream().filter(item -> TITLE_KEY.equals(item.getKey())).findFirst().orElse(new ChartStyleConfigDTO()); + ChartStyleConfigDTO header = title.getRows().stream().filter(item -> MODAL_KEY.equals(item.getKey())).findFirst().orElse(new ChartStyleConfigDTO()); + ChartStyleConfigDTO chartStyleConfigDTO = header.getRows().stream().filter(item -> HEADER_KEY.equals(item.getKey())).findFirst().orElse(new ChartStyleConfigDTO()); + if (chartStyleConfigDTO.getValue()!=null && JSONValidator.from(chartStyleConfigDTO.getValue().toString()).validate()){ + chartHeaders = JSON.parseArray(chartStyleConfigDTO.getValue().toString(), ChartColumn.class); + } + return chartHeaders; + } +} diff --git a/server/src/main/java/datart/server/base/dto/chart/ChartDataConfigDTO.java b/server/src/main/java/datart/server/base/dto/chart/ChartDataConfigDTO.java new file mode 100644 index 000000000..6ea953ecd --- /dev/null +++ b/server/src/main/java/datart/server/base/dto/chart/ChartDataConfigDTO.java @@ -0,0 +1,20 @@ +package datart.server.base.dto.chart; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class ChartDataConfigDTO { + + private String label; + + private String key; + + private Boolean required; + + private String type; + + private List rows = new ArrayList<>(); +} diff --git a/server/src/main/java/datart/server/base/dto/chart/ChartDetailConfigDTO.java b/server/src/main/java/datart/server/base/dto/chart/ChartDetailConfigDTO.java new file mode 100644 index 000000000..27b38e36c --- /dev/null +++ b/server/src/main/java/datart/server/base/dto/chart/ChartDetailConfigDTO.java @@ -0,0 +1,16 @@ +package datart.server.base.dto.chart; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class ChartDetailConfigDTO { + + private List datas = new ArrayList<>(); + + private List styles = new ArrayList<>(); + + private List settings = new ArrayList<>(); +} diff --git a/server/src/main/java/datart/server/base/dto/chart/ChartStyleConfigDTO.java b/server/src/main/java/datart/server/base/dto/chart/ChartStyleConfigDTO.java new file mode 100644 index 000000000..5d9567782 --- /dev/null +++ b/server/src/main/java/datart/server/base/dto/chart/ChartStyleConfigDTO.java @@ -0,0 +1,19 @@ +package datart.server.base.dto.chart; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class ChartStyleConfigDTO { + + private String key; + + private String label; + + private Object value; + + private List rows = new ArrayList<>(); + +} diff --git a/server/src/main/java/datart/server/base/dto/chart/WidgetConfig.java b/server/src/main/java/datart/server/base/dto/chart/WidgetConfig.java new file mode 100644 index 000000000..768372ea5 --- /dev/null +++ b/server/src/main/java/datart/server/base/dto/chart/WidgetConfig.java @@ -0,0 +1,20 @@ +package datart.server.base.dto.chart; + +import com.alibaba.fastjson.JSONObject; +import lombok.Data; + +@Data +public class WidgetConfig { + + private JSONObject content = new JSONObject(); + + public String getChartConfig(){ + if (content.containsKey("dataChart")){ + JSONObject obj = content.getJSONObject("dataChart"); + if (obj.containsKey("config")){ + return obj.getString("config"); + } + } + return ""; + } +} diff --git a/server/src/main/java/datart/server/base/params/ViewExecuteParam.java b/server/src/main/java/datart/server/base/params/ViewExecuteParam.java index fde0e82c4..147b72c7c 100644 --- a/server/src/main/java/datart/server/base/params/ViewExecuteParam.java +++ b/server/src/main/java/datart/server/base/params/ViewExecuteParam.java @@ -34,6 +34,8 @@ public class ViewExecuteParam { private String vizName; + private String vizType; + private String viewId; private List keywords; @@ -64,15 +66,12 @@ public class ViewExecuteParam { private boolean script; + private boolean analytics; + public boolean isEmpty() { return CollectionUtils.isEmpty(columns) - && CollectionUtils.isEmpty(keywords) - && CollectionUtils.isEmpty(params) - && CollectionUtils.isEmpty(functionColumns) && CollectionUtils.isEmpty(aggregators) - && CollectionUtils.isEmpty(filters) - && CollectionUtils.isEmpty(groups) - && CollectionUtils.isEmpty(orders); + && CollectionUtils.isEmpty(groups); } } \ No newline at end of file diff --git a/server/src/main/java/datart/server/base/params/VizCreateParam.java b/server/src/main/java/datart/server/base/params/VizCreateParam.java index 6e85b121b..024b11f07 100644 --- a/server/src/main/java/datart/server/base/params/VizCreateParam.java +++ b/server/src/main/java/datart/server/base/params/VizCreateParam.java @@ -34,6 +34,10 @@ public class VizCreateParam extends BaseCreateParam { private Short status; + private String subType; + + private String avatar; + private List permissions; } diff --git a/server/src/main/java/datart/server/base/params/VizUpdateParam.java b/server/src/main/java/datart/server/base/params/VizUpdateParam.java index eb11bae47..595864dc6 100644 --- a/server/src/main/java/datart/server/base/params/VizUpdateParam.java +++ b/server/src/main/java/datart/server/base/params/VizUpdateParam.java @@ -27,4 +27,8 @@ public class VizUpdateParam extends BaseUpdateParam { private Short status; + private String subType; + + private String avatar; + } diff --git a/server/src/main/java/datart/server/common/PoiConvertUtils.java b/server/src/main/java/datart/server/common/PoiConvertUtils.java new file mode 100644 index 000000000..2148ac7ca --- /dev/null +++ b/server/src/main/java/datart/server/common/PoiConvertUtils.java @@ -0,0 +1,143 @@ +package datart.server.common; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONValidator; +import datart.core.base.consts.ValueType; +import datart.core.data.provider.Column; +import datart.core.data.provider.Dataframe; +import datart.core.entity.poi.ColumnSetting; +import datart.core.entity.poi.POISettings; +import datart.server.base.dto.chart.ChartColumn; +import datart.server.base.dto.chart.ChartConfigDTO; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.ss.util.CellRangeAddress; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +public class PoiConvertUtils { + + public static POISettings covertToPoiSetting(String chartConfigStr, Dataframe dataframe){ + ChartConfigDTO chartConfigDTO = JSONValidator.from(chartConfigStr).validate() ? + JSON.parseObject(chartConfigStr, ChartConfigDTO.class) : new ChartConfigDTO(); + List dataColumns = chartConfigDTO.getColumnSettings(); + List dataStyles = chartConfigDTO.getDataHeaders(); + POISettings poiSettings = new POISettings(); + Map aliasMap = getAliasMap(dataColumns); + dealHeaderDataSetting(poiSettings, dataStyles, dataframe, dataColumns, aliasMap); + List realColumnSortList = getRealColumnSortList(dataColumns, dataStyles); + Map columnSettingMap = dealColumnSetting(dataframe.getColumns(), realColumnSortList); + poiSettings.setColumnSetting(columnSettingMap); + return poiSettings; + } + + private static Map dealColumnSetting(List columns, List dataColumns) { + Map resultMap = new HashMap<>(columns.size()); + Map oriSort = new HashMap<>(columns.size()); + for (int i = 0; i < columns.size(); i++) { + oriSort.put(columns.get(i).getName(), i); + } + for (int i = 0; i < dataColumns.size(); i++) { + ChartColumn dataColumn = dataColumns.get(i); + Integer index = oriSort.get(dataColumn.getColName()); + ColumnSetting res = new ColumnSetting(); + res.setIndex(i); + res.setNumFormat(dataColumn.getNumFormat()); + res.setLength(dataColumn.getColName().length()); + resultMap.put(index, res); + } + return resultMap; + } + + /** + * 处理表头信息、列别名及合并信息 + */ + private static void dealHeaderDataSetting(POISettings poiSettings, List dataStyles, Dataframe dataframe, List dataColumns, Map aliasMap){ + Map> rowMap = new HashMap<>(); + List cellRangeAddresses = new ArrayList<>(); + if (dataStyles.size()>0){ + int deepNum = dataStyles.stream().map(ChartColumn::getDeepNum).max(Comparator.comparingInt(Integer::intValue)).get(); + for (int i = 0; i < deepNum; i++) { + rowMap.put(i, new ArrayList<>()); + } + convertGroupHeaderData(dataStyles, rowMap, 0, cellRangeAddresses, aliasMap); + } else { + if (CollectionUtils.isEmpty(dataColumns)){ + rowMap.put(0, dataframe.getColumns()); + }else { + dataColumns.stream().forEach(item -> { + putDataIntoListMap(rowMap, 0, item); + }); + rowMap.get(0).stream().forEach(item -> item.setName(aliasMap.getOrDefault(item.getName(), item.getName()))); + } + } + poiSettings.setHeaderRows(rowMap); + poiSettings.setMergeCells(cellRangeAddresses); + } + + /** + * 获取列别名 + */ + private static Map getAliasMap(List dataColumns){ + Map aliasMap = new HashMap<>(); + dataColumns.stream().forEach(item -> { + String aliasName = StringUtils.isNotBlank(item.getAlias().getName()) ? item.getAlias().getName() : item.getColName(); + aliasMap.put(item.getColName(), aliasName); + }); + return aliasMap; + } + + private static void convertGroupHeaderData(List dataStyles, Map> rowMap, int rowNum, List cellRangeAddresses, Map aliasMap){ + for (ChartColumn dataStyle : dataStyles) { + int columnNum = putDataIntoListMap(rowMap, rowNum, dataStyle); + if (dataStyle.getLeafNum()<=1 && !dataStyle.isGroup()){ + Column column = rowMap.get(rowNum).get(columnNum); + column.setName(aliasMap.getOrDefault(column.getName(), column.getName())); + for (int i = rowNum+1; i < rowMap.size(); i++) { + putDataIntoListMap(rowMap, i, new ChartColumn()); + } + if (rowMap.size()-1 > rowNum){ + cellRangeAddresses.add(new CellRangeAddress(rowNum, rowMap.size()-1, columnNum, columnNum)); + } + } else if (dataStyle.getLeafNum()>1){ + for (int i = 1; i < dataStyle.getLeafNum(); i++) { + putDataIntoListMap(rowMap, rowNum, new ChartColumn()); + } + cellRangeAddresses.add(new CellRangeAddress(rowNum, rowNum, columnNum, columnNum+dataStyle.getLeafNum()-1)); + } + if (dataStyle.getChildren().size()>0){ + int row = rowNum+1; + convertGroupHeaderData(dataStyle.getChildren(), rowMap, row, cellRangeAddresses, aliasMap); + } + } + } + + private static List getRealColumnSortList(List dataColumns, List dataHeaders){ + if (CollectionUtils.isEmpty(dataHeaders)){ + return dataColumns; + } + List list = new ArrayList<>(); + for (ChartColumn dataHeader : dataHeaders) { + list.addAll(dataHeader.getLeafNodes()); + } + Map columnMap = dataColumns.stream().collect(Collectors.toMap(ChartColumn::getUid, item -> item)); + for (ChartColumn chartColumn : list) { + if (columnMap.containsKey(chartColumn.getUid())){ + chartColumn.setFormat(columnMap.get(chartColumn.getUid()).getFormat()); + } + } + return list; + } + + private static int putDataIntoListMap(Map> rowMap, Integer key, ChartColumn val){ + if (!rowMap.containsKey(key)) { + rowMap.put(key, new ArrayList<>()); + } + Column column = new Column(); + column.setName(val.getColName()); + column.setType(ValueType.STRING); + rowMap.get(key).add(column); + return rowMap.get(key).size()-1; + } +} diff --git a/server/src/main/java/datart/server/config/ControllerResponseAdvice.java b/server/src/main/java/datart/server/config/ControllerResponseAdvice.java index 05d1c4f4f..1762c204c 100644 --- a/server/src/main/java/datart/server/config/ControllerResponseAdvice.java +++ b/server/src/main/java/datart/server/config/ControllerResponseAdvice.java @@ -13,7 +13,6 @@ import java.util.LinkedList; import java.util.Map; -import java.util.Objects; @ControllerAdvice public class ControllerResponseAdvice implements ResponseBodyAdvice { @@ -34,9 +33,7 @@ public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaTy } ((ResponseData) o).setWarnings(msg); } - return o; - } else { - return ResponseData.success(o); } + return o; } } \ No newline at end of file diff --git a/server/src/main/java/datart/server/config/CustomConfigValidateBean.java b/server/src/main/java/datart/server/config/CustomConfigValidateBean.java new file mode 100644 index 000000000..4994e1b52 --- /dev/null +++ b/server/src/main/java/datart/server/config/CustomConfigValidateBean.java @@ -0,0 +1,36 @@ +package datart.server.config; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotBlank; + +/** + * 校验配置文件中的key规则 + */ +@Data +@Validated +public class CustomConfigValidateBean { + + @NotBlank + @JSONField(name = "datasource.ip") + private String datasourceIp; + + @NotBlank + @JSONField(name = "datasource.port") + private String datasourcePort; + + @NotBlank + @JSONField(name = "datasource.database") + private String datasourceDatabase; + + @NotBlank + @JSONField(name = "datasource.username") + private String datasourceUsername; + + @NotBlank + @JSONField(name = "datasource.password") + private String datasourcePassword; + +} diff --git a/server/src/main/java/datart/server/config/CustomPropertiesValidate.java b/server/src/main/java/datart/server/config/CustomPropertiesValidate.java new file mode 100644 index 000000000..04bd7cfdc --- /dev/null +++ b/server/src/main/java/datart/server/config/CustomPropertiesValidate.java @@ -0,0 +1,126 @@ +package datart.server.config; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.annotation.JSONField; +import datart.core.base.exception.Exceptions; +import datart.core.common.FileUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.data.util.ReflectionUtils; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.util.*; + +@Order(Integer.MIN_VALUE) +public class CustomPropertiesValidate implements EnvironmentPostProcessor { + + private static final String CONFIG_HOME = "config/datart.conf"; + + private static final String DATABASE_URL = "spring.datasource.url"; + + private static final String CONFIG_DATABASE_URL = "datasource.ip"; + + private static final String DEFAULT_APPLICATION_CONFIG = "config/profiles/application-config.yml"; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + MutablePropertySources propertySources = environment.getPropertySources(); + Properties properties = loadCustomProperties(); + //this.validateConfig(properties); + propertySources.addFirst(new PropertiesPropertySource("datartConfig", properties)); + switchProfile(environment); + } + + private Properties loadCustomProperties() { + Properties properties = new Properties(); + File file = new File(FileUtils.concatPath(System.getProperty("user.dir"), CONFIG_HOME)); + try (InputStream inputStream = new FileInputStream(file)) { + properties.load(inputStream); + } catch (Exception e) { + System.err.println("Failed to load the datart configuration (config/datart.conf), use application-config.yml"); + return new Properties(); + } + List removeKeys = new ArrayList<>(); + for (Map.Entry entry : properties.entrySet()) { + String val = String.valueOf(entry.getValue()).trim(); + if (StringUtils.isBlank(val)) { + removeKeys.add(entry.getKey()); + } + entry.setValue(val); + } + for (Object key : removeKeys) { + properties.remove(key); + } + return properties; + } + + private void validateConfig(Properties properties) { + CustomConfigValidateBean customConfigValidateBean = JSON.parseObject(JSON.toJSONString(properties), CustomConfigValidateBean.class); + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + Set> validate = validator.validate(customConfigValidateBean); + LinkedList errorMessages = new LinkedList<>(); + for (ConstraintViolation violation : validate) { + String configName = getConfigName(violation.getRootBeanClass(), violation.getPropertyPath().toString()); + errorMessages.add(configName + violation.getMessage()); + } + if (errorMessages.size() > 0) { + String msg = "Failed to get the necessary parameters, please check the configuration in the file(config/datart.conf)\nThe reasons: "; + msg = msg + errorMessages.getFirst(); + errorMessages.removeFirst(); + errorMessages.addFirst(msg); + Exceptions.msg(StringUtils.join(errorMessages, ", ")); + } + } + + private String getConfigName(Class clazz, String fieldName) { + Field field = ReflectionUtils.findRequiredField(clazz, fieldName); + JSONField annotation = field.getAnnotation(JSONField.class); + if (annotation != null) { + return annotation.name(); + } + return fieldName; + } + + private void switchProfile(ConfigurableEnvironment environment) { + String url = getDefaultDBUrl(environment); + if (url == null || (url.contains("null") && environment.getProperty(CONFIG_DATABASE_URL) == null)) { + environment.setActiveProfiles("demo"); + System.err.println("【********* Invalid database configuration. Datart is running in demo mode *********】"); + } + } + + private String getDefaultDBUrl(ConfigurableEnvironment environment) { + List activeProfiles = Arrays.asList(environment.getActiveProfiles()); + if (activeProfiles.size() > 0 && !Arrays.asList("demo", "config").containsAll(activeProfiles)) { + // running other profiles + return ""; + } + try { + List> propertySources = new YamlPropertySourceLoader().load(DEFAULT_APPLICATION_CONFIG, new FileSystemResource(DEFAULT_APPLICATION_CONFIG)); + if (CollectionUtils.isEmpty(propertySources)) { + System.err.println("Default config application-config not found "); + return null; + } + return (String) propertySources.get(0).getProperty(DATABASE_URL); + } catch (Exception e) { + System.err.println("Default config application-config not found "); + } + return null; + } + +} diff --git a/server/src/main/java/datart/server/config/LocalConfiguration.java b/server/src/main/java/datart/server/config/LocalConfiguration.java deleted file mode 100644 index 12e23fcae..000000000 --- a/server/src/main/java/datart/server/config/LocalConfiguration.java +++ /dev/null @@ -1,16 +0,0 @@ -package datart.server.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.LocaleResolver; -import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; - -//@Configuration -//public class LocalConfiguration { -// -// @Bean -// public LocaleResolver localeResolver() { -// return new AcceptHeaderLocaleResolver(); -// } -// -//} diff --git a/server/src/main/java/datart/server/config/SwaggerConfiguration.java b/server/src/main/java/datart/server/config/SwaggerConfiguration.java index a90c15239..b92d17ef1 100644 --- a/server/src/main/java/datart/server/config/SwaggerConfiguration.java +++ b/server/src/main/java/datart/server/config/SwaggerConfiguration.java @@ -20,11 +20,9 @@ package datart.server.config; import com.fasterxml.classmate.ResolvedType; -import com.fasterxml.classmate.TypeResolver; -import com.fasterxml.classmate.types.ResolvedObjectType; +import com.google.common.collect.Lists; import datart.server.base.dto.ResponseData; import org.apache.commons.lang3.reflect.TypeUtils; -import org.assertj.core.util.Lists; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,13 +30,13 @@ import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.schema.ModelReference; import springfox.documentation.schema.ResolvedTypes; import springfox.documentation.schema.TypeNameExtractor; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.ApiKey; import springfox.documentation.service.ResponseMessage; import springfox.documentation.spi.DocumentationType; + import springfox.documentation.spi.schema.contexts.ModelContext; import springfox.documentation.spi.service.OperationBuilderPlugin; import springfox.documentation.spi.service.contexts.OperationContext; @@ -46,7 +44,6 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; @@ -54,22 +51,16 @@ @EnableSwagger2 public class SwaggerConfiguration { - @Autowired - private TypeNameExtractor nameExtractor; +// @Autowired +// private TypeNameExtractor nameExtractor; - @Autowired - private TypeResolver typeResolver; +// @Autowired +// private TypeResolver typeResolver; @Bean public Docket createRestApi() { List responseMessageList = new ArrayList<>(); -// responseMessageList.add(new ResponseMessageBuilder().code(HttpCodeEnum.OK.getCode()).message(HttpCodeEnum.OK.getMessage()).build()); -// responseMessageList.add(new ResponseMessageBuilder().code(HttpCodeEnum.FAIL.getCode()).message(HttpCodeEnum.FAIL.getMessage()).build()); -// responseMessageList.add(new ResponseMessageBuilder().code(HttpCodeEnum.UNAUTHORIZED.getCode()).message(HttpCodeEnum.UNAUTHORIZED.getMessage()).build()); -// responseMessageList.add(new ResponseMessageBuilder().code(HttpCodeEnum.FORBIDDEN.getCode()).message(HttpCodeEnum.FORBIDDEN.getMessage()).build()); -// responseMessageList.add(new ResponseMessageBuilder().code(HttpCodeEnum.SERVER_ERROR.getCode()).message(HttpCodeEnum.SERVER_ERROR.getMessage()).build()); - return new Docket(DocumentationType.SWAGGER_2) .globalResponseMessage(RequestMethod.GET, responseMessageList) .globalResponseMessage(RequestMethod.POST, responseMessageList) @@ -82,43 +73,29 @@ public Docket createRestApi() { .build() .securitySchemes(Lists.newArrayList(apiKey())); } -// + + // @Bean -// public OperationModelsProviderPlugin operationModelsProviderPlugin() { -// return new OperationModelsProviderPlugin() { +// public OperationBuilderPlugin operationBuilderPlugin() { +// return new OperationBuilderPlugin() { +// // @Override -// public boolean supports(DocumentationType documentationType) { -// return true; +// public void apply(OperationContext context) { +// ResolvedType returnType = context.getReturnType(); +// returnType = context.alternateFor(returnType); +// ParameterizedType parameterize = TypeUtils.parameterize(ResponseData.class, returnType); +// returnType = typeResolver.resolve(parameterize); +// ModelContext modelContext = ModelContext.returnValue(returnType, context.getDocumentationType(), context.getAlternateTypeProvider(), context.getGenericsNamingStrategy(), context.getIgnorableParameterTypes()); +// context.operationBuilder().responseModel(ResolvedTypes.modelRefFactory(modelContext, SwaggerConfiguration.this.nameExtractor).apply(returnType)); // } // // @Override -// public void apply(RequestMappingContext context) { -// System.out.println(context); +// public boolean supports(DocumentationType documentationType) { +// return true; // } // }; // } - @Bean - public OperationBuilderPlugin operationBuilderPlugin() { - return new OperationBuilderPlugin() { - - @Override - public void apply(OperationContext context) { - ResolvedType returnType = context.getReturnType(); - returnType = context.alternateFor(returnType); - ParameterizedType parameterize = TypeUtils.parameterize(ResponseData.class, returnType); - returnType = typeResolver.resolve(parameterize); - ModelContext modelContext = ModelContext.returnValue(returnType, context.getDocumentationType(), context.getAlternateTypeProvider(), context.getGenericsNamingStrategy(), context.getIgnorableParameterTypes()); - context.operationBuilder().responseModel(ResolvedTypes.modelRefFactory(modelContext, SwaggerConfiguration.this.nameExtractor).apply(returnType)); - } - - @Override - public boolean supports(DocumentationType documentationType) { - return true; - } - }; - } - private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("datart api") diff --git a/server/src/main/java/datart/server/config/WebExceptionHandler.java b/server/src/main/java/datart/server/config/WebExceptionHandler.java index b082c1e47..96d55f014 100644 --- a/server/src/main/java/datart/server/config/WebExceptionHandler.java +++ b/server/src/main/java/datart/server/config/WebExceptionHandler.java @@ -19,12 +19,11 @@ package datart.server.config; import datart.core.common.MessageResolver; +import datart.core.common.RequestContext; +import datart.core.data.provider.Dataframe; import datart.security.exception.AuthException; -import datart.security.exception.PermissionDeniedException; import datart.server.base.dto.ResponseData; -import datart.core.base.exception.NotFoundException; -import datart.core.base.exception.ParamException; -import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.validation.BindException; @@ -41,8 +40,8 @@ public class WebExceptionHandler { @ResponseBody @ResponseStatus(code = HttpStatus.UNAUTHORIZED) - @ExceptionHandler(ExpiredJwtException.class) - public ResponseData exceptionHandler(ExpiredJwtException e) { + @ExceptionHandler(JwtException.class) + public ResponseData exceptionHandler(JwtException e) { ResponseData.ResponseDataBuilder builder = ResponseData.builder(); return builder.success(false) .message(MessageResolver.getMessage("login.session.timeout")) @@ -50,29 +49,6 @@ public ResponseData exceptionHandler(ExpiredJwtException e) { .build(); } - @ResponseBody - @ResponseStatus(code = HttpStatus.BAD_REQUEST) - @ExceptionHandler(ParamException.class) - public ResponseData exceptionHandler(ParamException e) { - ResponseData.ResponseDataBuilder builder = ResponseData.builder(); - return builder.success(false) - .message(e.getMessage()) - .errCode(e.getErrCode()) - .exception(e) - .build(); - } - - @ResponseBody - @ResponseStatus(code = HttpStatus.BAD_REQUEST) - @ExceptionHandler(NotFoundException.class) - public ResponseData exceptionHandler(NotFoundException e) { - ResponseData.ResponseDataBuilder builder = ResponseData.builder(); - return builder.success(false) - .message(e.getMessage()) - .errCode(e.getErrCode()) - .exception(e).build(); - } - @ResponseBody @ResponseStatus(code = HttpStatus.UNAUTHORIZED) @ExceptionHandler(AuthException.class) @@ -84,16 +60,6 @@ public ResponseData exceptionHandler(AuthException e) { .exception(e).build(); } - @ResponseBody - @ResponseStatus(code = HttpStatus.BAD_REQUEST) - @ExceptionHandler(PermissionDeniedException.class) - public ResponseData exceptionHandler(PermissionDeniedException e) { - ResponseData.ResponseDataBuilder builder = ResponseData.builder(); - return builder.success(false) - .message(e.getMessage()) - .exception(e).build(); - } - @ResponseBody @ResponseStatus(code = HttpStatus.BAD_REQUEST) @ExceptionHandler(BindException.class) @@ -110,7 +76,16 @@ public ResponseData validateException(BindException e) { @ResponseBody @ResponseStatus(code = HttpStatus.BAD_REQUEST) @ExceptionHandler(Exception.class) - public ResponseData exceptionHandler(Exception e) { + public ResponseData exceptionHandler(Exception e) { + Object data = null; + if (RequestContext.getScriptPermission() != null) { + Dataframe df = Dataframe.empty(); + if (RequestContext.getScriptPermission()) { + df.setScript(RequestContext.getSql()); + } + data = df; + } + String msg = null; msg = e.getMessage(); if (msg == null) { @@ -120,9 +95,10 @@ public ResponseData exceptionHandler(Exception e) { } } log.error(msg, e); - ResponseData.ResponseDataBuilder builder = ResponseData.builder(); + ResponseData.ResponseDataBuilder builder = ResponseData.builder(); return builder.success(false) .message(msg) + .data(data) .exception(e) .build(); } diff --git a/server/src/main/java/datart/server/config/WebMvcConfig.java b/server/src/main/java/datart/server/config/WebMvcConfig.java index 2530fdf36..d95e454ae 100644 --- a/server/src/main/java/datart/server/config/WebMvcConfig.java +++ b/server/src/main/java/datart/server/config/WebMvcConfig.java @@ -49,7 +49,6 @@ public WebMvcConfig(LoginInterceptor loginInterceptor) { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor).addPathPatterns(getPathPrefix() + "/**"); //i18n locale interceptor -// registry.addInterceptor(new LocaleChangeInterceptor()); registry.addInterceptor(new BasicValidRequestInterceptor()).addPathPatterns("/**"); } diff --git a/server/src/main/java/datart/server/config/WebSecurityConfig.java b/server/src/main/java/datart/server/config/WebSecurityConfig.java new file mode 100644 index 000000000..48a0e4b55 --- /dev/null +++ b/server/src/main/java/datart/server/config/WebSecurityConfig.java @@ -0,0 +1,41 @@ +package datart.server.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +import static datart.core.common.Application.getApiPrefix; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + private OAuth2ClientProperties oAuth2ClientProperties; + + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers(getApiPrefix() + "/tpa"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable(); + if (this.oAuth2ClientProperties != null) { + http + .authorizeRequests() + .antMatchers(getApiPrefix() + "/tpa").permitAll() + .and().oauth2Login().loginPage("/") + .and().logout().logoutUrl("/tpa/oauth2/logout").permitAll(); + } + } + + @Autowired(required = false) + public void setoAuth2ClientProperties(OAuth2ClientProperties properties) { + this.oAuth2ClientProperties = properties; + } + +} diff --git a/server/src/main/java/datart/server/config/interceptor/BasicValidRequestInterceptor.java b/server/src/main/java/datart/server/config/interceptor/BasicValidRequestInterceptor.java index 04562d8f0..d98bac0bf 100644 --- a/server/src/main/java/datart/server/config/interceptor/BasicValidRequestInterceptor.java +++ b/server/src/main/java/datart/server/config/interceptor/BasicValidRequestInterceptor.java @@ -53,6 +53,7 @@ private boolean isValidRequest(HttpServletRequest request) { || requestURI.startsWith("/swagger") || requestURI.startsWith("/webjars") || requestURI.startsWith("/custom-chart-plugins") + || requestURI.startsWith("/antd") || requestURI.startsWith("/v2/") || requestURI.startsWith("/share") || requestURI.startsWith(staticPath); diff --git a/server/src/main/java/datart/server/config/interceptor/LoginInterceptor.java b/server/src/main/java/datart/server/config/interceptor/LoginInterceptor.java index be49edef8..54500b05c 100644 --- a/server/src/main/java/datart/server/config/interceptor/LoginInterceptor.java +++ b/server/src/main/java/datart/server/config/interceptor/LoginInterceptor.java @@ -38,11 +38,9 @@ public class LoginInterceptor implements HandlerInterceptor { private final DatartSecurityManager securityManager; - private final MessageResolver messageResolver; - public LoginInterceptor(DatartSecurityManager securityManager, MessageResolver messageResolver) { + public LoginInterceptor(DatartSecurityManager securityManager) { this.securityManager = securityManager; - this.messageResolver = messageResolver; } @Override diff --git a/server/src/main/java/datart/server/controller/SourceController.java b/server/src/main/java/datart/server/controller/SourceController.java index cd22121d1..a77b899c5 100644 --- a/server/src/main/java/datart/server/controller/SourceController.java +++ b/server/src/main/java/datart/server/controller/SourceController.java @@ -19,6 +19,7 @@ package datart.server.controller; +import datart.core.data.provider.SchemaInfo; import datart.core.entity.Source; import datart.server.base.dto.ResponseData; import datart.server.base.params.CheckNameParam; @@ -98,4 +99,16 @@ public ResponseData unarchive(@PathVariable String sourceId) { return ResponseData.success(sourceService.unarchive(sourceId)); } + @ApiOperation(value = "get source schemas ") + @GetMapping(value = "/schemas/{sourceId}") + public ResponseData getSourceSchemas(@PathVariable String sourceId) { + return ResponseData.success(sourceService.getSourceSchemaInfo(sourceId)); + } + + @ApiOperation(value = "sync source schemas ") + @GetMapping(value = "/sync/schemas/{sourceId}") + public ResponseData syncSourceSchemas(@PathVariable String sourceId) throws Exception{ + return ResponseData.success(sourceService.syncSourceSchema(sourceId)); + } + } \ No newline at end of file diff --git a/server/src/main/java/datart/server/controller/ThirdPartyAuthController.java b/server/src/main/java/datart/server/controller/ThirdPartyAuthController.java new file mode 100644 index 000000000..9df22c30c --- /dev/null +++ b/server/src/main/java/datart/server/controller/ThirdPartyAuthController.java @@ -0,0 +1,90 @@ +package datart.server.controller; + +import datart.core.base.annotations.SkipLogin; +import datart.core.base.consts.Const; +import datart.core.entity.User; +import datart.core.entity.ext.UserBaseInfo; +import datart.security.base.PasswordToken; +import datart.security.util.JwtUtils; +import datart.server.base.dto.ResponseData; +import datart.server.service.UserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +@Api +@Slf4j +@RestController +@RequestMapping(value = "/tpa") +public class ThirdPartyAuthController extends BaseController { + + private final UserService userService; + + public ThirdPartyAuthController(UserService userService) { + this.userService = userService; + } + + private ClientRegistrationRepository clientRegistrationRepository; + + @ApiOperation(value = "Get Oauth2 clents") + @GetMapping(value = "getOauth2Clients", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @SkipLogin + public ResponseData>> getOauth2Clients(HttpServletRequest request) { + if (clientRegistrationRepository == null) { + return ResponseData.success(Collections.emptyList()); + } + Iterable clientRegistrations = (Iterable) clientRegistrationRepository; + List> clients = new ArrayList<>(); + clientRegistrations.forEach(registration -> { + HashMap map = new HashMap<>(); + map.put(registration.getClientName(), OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + registration.getRegistrationId() + "?redirect_url=/"); + clients.add(map); + }); + + return ResponseData.success(clients); + } + + @ApiOperation(value = "External Login") + @SkipLogin + @PostMapping(value = "oauth2login", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseData externalLogin(Principal principal, HttpServletResponse response) { + if (principal instanceof OAuth2AuthenticationToken) { + User user = userService.externalRegist((OAuth2AuthenticationToken) principal); + PasswordToken passwordToken = new PasswordToken(user.getUsername(), + null, + System.currentTimeMillis()); + + passwordToken.setPassword(user.getPassword()); + String token = JwtUtils.toJwtString(passwordToken); + response.setHeader(Const.TOKEN, token); + response.setStatus(200); + return ResponseData.success(new UserBaseInfo(user)); + } + response.setStatus(401); + return ResponseData.failure("oauth2登录失败"); + } + + + @Autowired(required = false) + public void setClientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) { + this.clientRegistrationRepository = clientRegistrationRepository; + } +} diff --git a/server/src/main/java/datart/server/service/impl/EmailJob.java b/server/src/main/java/datart/server/job/EmailJob.java similarity index 96% rename from server/src/main/java/datart/server/service/impl/EmailJob.java rename to server/src/main/java/datart/server/job/EmailJob.java index 08c24f7c9..22ba5ee11 100644 --- a/server/src/main/java/datart/server/service/impl/EmailJob.java +++ b/server/src/main/java/datart/server/job/EmailJob.java @@ -15,12 +15,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package datart.server.service.impl; +package datart.server.job; import datart.core.common.Application; import datart.server.base.dto.ScheduleJobConfig; import datart.server.service.MailService; -import datart.server.service.ScheduleJob; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.mail.javamail.MimeMessageHelper; diff --git a/server/src/main/java/datart/server/service/ScheduleJob.java b/server/src/main/java/datart/server/job/ScheduleJob.java similarity index 94% rename from server/src/main/java/datart/server/service/ScheduleJob.java rename to server/src/main/java/datart/server/job/ScheduleJob.java index 466aa0885..387f00f22 100644 --- a/server/src/main/java/datart/server/service/ScheduleJob.java +++ b/server/src/main/java/datart/server/job/ScheduleJob.java @@ -1,4 +1,4 @@ -package datart.server.service; +package datart.server.job; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -13,6 +13,7 @@ import datart.core.entity.Schedule; import datart.core.entity.ScheduleLog; import datart.core.entity.User; +import datart.core.entity.poi.POISettings; import datart.core.mappers.ext.ScheduleLogMapperExt; import datart.core.mappers.ext.ScheduleMapperExt; import datart.core.mappers.ext.UserMapperExt; @@ -26,6 +27,8 @@ import datart.server.base.params.ShareCreateParam; import datart.server.base.params.ShareToken; import datart.server.base.params.ViewExecuteParam; +import datart.server.service.*; +import datart.server.common.PoiConvertUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; @@ -174,9 +177,11 @@ private void downloadExcel(DownloadCreateParam downloadParams) throws Exception ViewExecuteParam viewExecuteParam = downloadParams.getDownloadParams().get(i); viewExecuteParam.setPageInfo(PageInfo.builder().pageNo(1) .pageSize(Integer.MAX_VALUE).build()); - String vizName = viewExecuteParam.getVizName(); Dataframe dataframe = dataProviderService.execute(downloadParams.getDownloadParams().get(i)); - POIUtils.withSheet(workbook, StringUtils.isEmpty(vizName) ? "Sheet" + i : vizName, dataframe); + String chartConfigStr = vizService.getChartConfigByVizId(viewExecuteParam.getVizId(), viewExecuteParam.getVizType()); + POISettings poiSettings = PoiConvertUtils.covertToPoiSetting(chartConfigStr, dataframe); + String sheetName = StringUtils.isNotBlank(viewExecuteParam.getVizName()) ? viewExecuteParam.getVizName() : "Sheet"+i; + POIUtils.withSheet(workbook, sheetName, dataframe, poiSettings); } File tempFile = File.createTempFile(UUIDGenerator.generate(), ".xlsx"); POIUtils.save(workbook, tempFile.getPath(), true); diff --git a/server/src/main/java/datart/server/job/SchemaSyncJob.java b/server/src/main/java/datart/server/job/SchemaSyncJob.java new file mode 100644 index 000000000..f23ac5a27 --- /dev/null +++ b/server/src/main/java/datart/server/job/SchemaSyncJob.java @@ -0,0 +1,108 @@ +/* + * Datart + *

    + * Copyright 2021 + *

    + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

    + * http://www.apache.org/licenses/LICENSE-2.0 + *

    + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package datart.server.job; + +import com.fasterxml.jackson.databind.ObjectMapper; +import datart.core.common.Application; +import datart.core.common.UUIDGenerator; +import datart.core.data.provider.SchemaItem; +import datart.core.data.provider.TableInfo; +import datart.core.entity.SourceSchemas; +import datart.core.mappers.ext.SourceSchemasMapperExt; +import datart.server.service.DataProviderService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import java.io.Closeable; +import java.io.IOException; +import java.util.*; + +@Slf4j +public class SchemaSyncJob implements Job, Closeable { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + public static final String SOURCE_ID = "SOURCE_ID"; + + @Override + public void close() throws IOException { + } + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + String sourceId = (String) context.getMergedJobDataMap().get(SOURCE_ID); + try { + execute(sourceId); + } catch (Exception e) { + log.error("source schema sync error ", e); + } + } + + public boolean execute(String sourceId) throws Exception { + List schemaItems = new LinkedList<>(); + DataProviderService dataProviderService = Application.getBean(DataProviderService.class); + Set databases = dataProviderService.readAllDatabases(sourceId); + if (CollectionUtils.isNotEmpty(databases)) { + for (String database : databases) { + SchemaItem schemaItem = new SchemaItem(); + schemaItems.add(schemaItem); + schemaItem.setDbName(database); + schemaItem.setTables(new LinkedList<>()); + Set tables = dataProviderService.readTables(sourceId, database); + if (CollectionUtils.isNotEmpty(tables)) { + for (String table : tables) { + TableInfo tableInfo = new TableInfo(); + schemaItem.getTables().add(tableInfo); + tableInfo.setTableName(table); + tableInfo.setColumns(dataProviderService.readTableColumns(sourceId, database, table)); + } + } + } + } + return upsertSchemaInfo(sourceId, schemaItems); + } + + private boolean upsertSchemaInfo(String sourceId, List schemaItems) { + try { + SourceSchemasMapperExt mapper = Application.getBean(SourceSchemasMapperExt.class); + SourceSchemas sourceSchemas = mapper.selectBySource(sourceId); + if (sourceSchemas == null) { + sourceSchemas = new SourceSchemas(); + sourceSchemas.setId(UUIDGenerator.generate()); + sourceSchemas.setSourceId(sourceId); + sourceSchemas.setUpdateTime(new Date()); + sourceSchemas.setSchemas(OBJECT_MAPPER.writeValueAsString(schemaItems)); + mapper.insert(sourceSchemas); + } else { + sourceSchemas.setUpdateTime(new Date()); + sourceSchemas.setSchemas(OBJECT_MAPPER.writeValueAsString(schemaItems)); + mapper.updateByPrimaryKey(sourceSchemas); + } + return true; + } catch (Exception e) { + log.error("source schema parse error ", e); + return false; + } + } + + +} diff --git a/server/src/main/java/datart/server/service/impl/WeChartJob.java b/server/src/main/java/datart/server/job/WeChartJob.java similarity index 96% rename from server/src/main/java/datart/server/service/impl/WeChartJob.java rename to server/src/main/java/datart/server/job/WeChartJob.java index da738e480..91128e20b 100644 --- a/server/src/main/java/datart/server/service/impl/WeChartJob.java +++ b/server/src/main/java/datart/server/job/WeChartJob.java @@ -15,9 +15,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package datart.server.service.impl; +package datart.server.job; -import datart.server.service.ScheduleJob; import lombok.extern.slf4j.Slf4j; import org.springframework.util.Base64Utils; import org.springframework.util.CollectionUtils; diff --git a/server/src/main/java/datart/server/service/BaseService.java b/server/src/main/java/datart/server/service/BaseService.java index 0033e9d3f..ba4d74fe7 100644 --- a/server/src/main/java/datart/server/service/BaseService.java +++ b/server/src/main/java/datart/server/service/BaseService.java @@ -18,6 +18,7 @@ package datart.server.service; +import com.fasterxml.jackson.databind.ObjectMapper; import datart.core.common.Application; import datart.core.common.MessageResolver; import datart.core.entity.BaseEntity; @@ -31,6 +32,8 @@ public class BaseService extends MessageResolver { + protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + protected DatartSecurityManager securityManager; protected AsyncAccessLogService accessLogService; diff --git a/server/src/main/java/datart/server/service/DataProviderService.java b/server/src/main/java/datart/server/service/DataProviderService.java index 01e95e0c5..03cbfad72 100644 --- a/server/src/main/java/datart/server/service/DataProviderService.java +++ b/server/src/main/java/datart/server/service/DataProviderService.java @@ -2,6 +2,7 @@ import datart.core.data.provider.*; +import datart.core.entity.Source; import datart.server.base.params.ViewExecuteParam; import datart.server.base.params.TestExecuteParam; @@ -37,4 +38,6 @@ public interface DataProviderService { void updateSource(DataProviderSource source); + DataProviderSource parseDataProviderConfig(Source source); + } diff --git a/server/src/main/java/datart/server/service/FolderService.java b/server/src/main/java/datart/server/service/FolderService.java index e74a4775f..113554a9f 100644 --- a/server/src/main/java/datart/server/service/FolderService.java +++ b/server/src/main/java/datart/server/service/FolderService.java @@ -12,4 +12,6 @@ public interface FolderService extends BaseCRUDService { boolean checkUnique(ResourceType type, String orgId, String parentId, String name); + Folder getVizFolder(String vizId,String relType); + } \ No newline at end of file diff --git a/server/src/main/java/datart/server/service/SourceService.java b/server/src/main/java/datart/server/service/SourceService.java index 7c78fed9f..1cf6167b5 100644 --- a/server/src/main/java/datart/server/service/SourceService.java +++ b/server/src/main/java/datart/server/service/SourceService.java @@ -18,9 +18,9 @@ package datart.server.service; +import datart.core.data.provider.SchemaInfo; import datart.core.entity.Source; import datart.core.mappers.ext.SourceMapperExt; -import datart.security.exception.PermissionDeniedException; import java.util.List; @@ -28,4 +28,8 @@ public interface SourceService extends BaseCRUDService List listSources(String orgId,boolean active); + SchemaInfo getSourceSchemaInfo(String sourceId); + + SchemaInfo syncSourceSchema(String sourceId) throws Exception; + } diff --git a/server/src/main/java/datart/server/service/UserService.java b/server/src/main/java/datart/server/service/UserService.java index b36d4bff7..5322d480b 100644 --- a/server/src/main/java/datart/server/service/UserService.java +++ b/server/src/main/java/datart/server/service/UserService.java @@ -19,12 +19,14 @@ package datart.server.service; import datart.core.base.consts.UserIdentityType; +import datart.core.base.exception.ServerException; import datart.core.entity.User; import datart.core.entity.ext.UserBaseInfo; import datart.core.mappers.ext.UserMapperExt; import datart.security.base.PasswordToken; import datart.server.base.dto.UserProfile; import datart.server.base.params.*; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import javax.mail.MessagingException; import java.io.UnsupportedEncodingException; @@ -55,4 +57,6 @@ public interface UserService extends BaseCRUDService { boolean resetPassword(UserResetPasswordParam passwordParam); + User externalRegist(OAuth2AuthenticationToken oauthAuthToken) throws ServerException; + } diff --git a/server/src/main/java/datart/server/service/VizService.java b/server/src/main/java/datart/server/service/VizService.java index 7ce3a2ce2..a9ad79bae 100644 --- a/server/src/main/java/datart/server/service/VizService.java +++ b/server/src/main/java/datart/server/service/VizService.java @@ -73,4 +73,6 @@ public interface VizService { boolean unpublish(ResourceType resourceType, String vizId); + String getChartConfigByVizId(String vizId, String vizType); + } diff --git a/server/src/main/java/datart/server/service/impl/DataProviderServiceImpl.java b/server/src/main/java/datart/server/service/impl/DataProviderServiceImpl.java index c7c6e212e..9bc6c097f 100644 --- a/server/src/main/java/datart/server/service/impl/DataProviderServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/DataProviderServiceImpl.java @@ -19,6 +19,7 @@ package datart.server.service.impl; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -29,6 +30,7 @@ import datart.core.base.consts.VariableTypeEnum; import datart.core.base.exception.BaseException; import datart.core.base.exception.Exceptions; +import datart.core.common.RequestContext; import datart.core.data.provider.*; import datart.core.entity.RelSubjectColumns; import datart.core.entity.Source; @@ -121,23 +123,23 @@ public Object testConnection(DataProviderSource source) throws Exception { @Override public Set readAllDatabases(String sourceId) throws SQLException { Source source = retrieve(sourceId, Source.class, false); - return dataProviderManager.readAllDatabases(toDataProviderConfig(source)); + return dataProviderManager.readAllDatabases(parseDataProviderConfig(source)); } @Override public Set readTables(String sourceId, String database) throws SQLException { Source source = retrieve(sourceId, Source.class, false); - return dataProviderManager.readTables(toDataProviderConfig(source), database); + return dataProviderManager.readTables(parseDataProviderConfig(source), database); } @Override public Set readTableColumns(String sourceId, String database, String table) throws SQLException { Source source = retrieve(sourceId, Source.class, false); - return dataProviderManager.readTableColumns(toDataProviderConfig(source), database, table); + return dataProviderManager.readTableColumns(parseDataProviderConfig(source), database, table); } - private DataProviderSource toDataProviderConfig(Source source) { + public DataProviderSource parseDataProviderConfig(Source source) { DataProviderSource providerSource = new DataProviderSource(); try { providerSource.setSourceId(source.getId()); @@ -171,16 +173,15 @@ private DataProviderSource toDataProviderConfig(Source source) { @Override public Dataframe testExecute(TestExecuteParam testExecuteParam) throws Exception { Source source = retrieve(testExecuteParam.getSourceId(), Source.class, true); - List variables = getOrgVariables(source.getOrgId()); if (!CollectionUtils.isEmpty(testExecuteParam.getVariables())) { - for (ScriptVariable variable : testExecuteParam.getVariables()) { - if (variable.isExpression()) { - variable.setValueType(ValueType.FRAGMENT); - } - } variables.addAll(testExecuteParam.getVariables()); } + for (ScriptVariable variable : variables) { + if (variable.isExpression()) { + variable.setValueType(ValueType.FRAGMENT); + } + } if (securityManager.isOrgOwner(source.getOrgId())) { disablePermissionVariables(variables); } @@ -190,7 +191,7 @@ public Dataframe testExecute(TestExecuteParam testExecuteParam) throws Exception .script(testExecuteParam.getScript()) .variables(variables) .build(); - DataProviderSource providerSource = toDataProviderConfig(source); + DataProviderSource providerSource = parseDataProviderConfig(source); ExecuteParam executeParam = ExecuteParam .builder() @@ -211,7 +212,15 @@ public Dataframe execute(ViewExecuteParam viewExecuteParam) throws Exception { //datasource and view View view = retrieve(viewExecuteParam.getViewId(), View.class, true); Source source = retrieve(view.getSourceId(), Source.class, false); - DataProviderSource providerSource = toDataProviderConfig(source); + DataProviderSource providerSource = parseDataProviderConfig(source); + + boolean scriptPermission = true; + try { + viewService.requirePermission(view, Const.MANAGE); + } catch (Exception e) { + scriptPermission = false; + } + RequestContext.setScriptPermission(scriptPermission); //permission and variables Set columns = parseColumnPermission(view); @@ -233,10 +242,6 @@ public Dataframe execute(ViewExecuteParam viewExecuteParam) throws Exception { viewExecuteParam.getPageInfo().setPageNo(1); } - if (viewExecuteParam.getPageInfo().getPageSize() == 0) { - viewExecuteParam.getPageInfo().setPageSize(10_000); - } - viewExecuteParam.getPageInfo().setPageSize(Math.min(viewExecuteParam.getPageInfo().getPageSize(), Integer.MAX_VALUE)); ExecuteParam queryParam = ExecuteParam.builder() @@ -257,13 +262,7 @@ public Dataframe execute(ViewExecuteParam viewExecuteParam) throws Exception { Dataframe dataframe = dataProviderManager.execute(providerSource, queryScript, queryParam); - if (viewExecuteParam.isScript()) { - try { - viewService.requirePermission(view, Const.MANAGE); - } catch (Exception e) { - dataframe.setScript(null); - } - } else { + if (!viewExecuteParam.isScript() || !scriptPermission) { dataframe.setScript(null); } return dataframe; @@ -274,7 +273,7 @@ public Set supportedStdFunctions(String sourceId) { Source source = retrieve(sourceId, Source.class, false); - DataProviderSource dataProviderSource = toDataProviderConfig(source); + DataProviderSource dataProviderSource = parseDataProviderConfig(source); return dataProviderManager.supportedStdFunctions(dataProviderSource); } @@ -282,7 +281,7 @@ public Set supportedStdFunctions(String sourceId) { @Override public boolean validateFunction(String sourceId, String snippet) { Source source = retrieve(sourceId, Source.class); - DataProviderSource dataProviderSource = toDataProviderConfig(source); + DataProviderSource dataProviderSource = parseDataProviderConfig(source); return dataProviderManager.validateFunction(dataProviderSource, snippet); } @@ -414,9 +413,32 @@ private Map parseSchema(String model) { } JSONObject jsonObject = JSON.parseObject(model); - for (String key : jsonObject.keySet()) { - ValueType type = ValueType.valueOf(jsonObject.getJSONObject(key).getString("type")); - schema.put(key, new Column(key, type)); + try { + if (jsonObject.containsKey("hierarchy")) { + jsonObject = jsonObject.getJSONObject("hierarchy"); + for (String key : jsonObject.keySet()) { + JSONObject item = jsonObject.getJSONObject(key); + if (item.containsKey("children")) { + JSONArray children = item.getJSONArray("children"); + if (children != null && children.size() > 0) { + for (int i = 0; i < children.size(); i++) { + JSONObject child = children.getJSONObject(i); + schema.put(child.getString("name"), new Column(child.getString("name"), ValueType.valueOf(child.getString("type")))); + } + } + } else { + schema.put(key, new Column(key, ValueType.valueOf(item.getString("type")))); + } + } + } else { + // 兼容1.0.0-beta.1以前的版本 + for (String key : jsonObject.keySet()) { + ValueType type = ValueType.valueOf(jsonObject.getJSONObject(key).getString("type")); + schema.put(key, new Column(key, type)); + } + } + } catch (Exception e) { + log.error("view model parse error", e); } return schema; } diff --git a/server/src/main/java/datart/server/service/impl/DatachartServiceImpl.java b/server/src/main/java/datart/server/service/impl/DatachartServiceImpl.java index 65bbae93c..e07809991 100644 --- a/server/src/main/java/datart/server/service/impl/DatachartServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/DatachartServiceImpl.java @@ -33,7 +33,9 @@ import datart.core.base.exception.NotFoundException; import datart.core.base.exception.ParamException; import datart.server.base.params.BaseCreateParam; +import datart.server.base.params.BaseUpdateParam; import datart.server.base.params.DatachartCreateParam; +import datart.server.base.params.VizUpdateParam; import datart.server.service.*; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; @@ -148,6 +150,9 @@ public Folder createWithFolder(BaseCreateParam createParam) { BeanUtils.copyProperties(createParam, folder); folder.setRelType(ResourceType.DATACHART.name()); folder.setRelId(datachart.getId()); + folder.setSubType(param.getSubType()); + folder.setAvatar(param.getAvatar()); + folderService.requirePermission(folder, Const.CREATE); folderMapper.insert(folder); diff --git a/server/src/main/java/datart/server/service/impl/DownloadServiceImpl.java b/server/src/main/java/datart/server/service/impl/DownloadServiceImpl.java index 523fcf774..c0b52127f 100644 --- a/server/src/main/java/datart/server/service/impl/DownloadServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/DownloadServiceImpl.java @@ -21,6 +21,7 @@ import datart.core.base.consts.Const; import datart.core.base.consts.FileOwner; import datart.core.base.exception.Exceptions; +import datart.core.base.exception.NotAllowedException; import datart.core.common.FileUtils; import datart.core.common.POIUtils; import datart.core.common.TaskExecutor; @@ -28,22 +29,22 @@ import datart.core.data.provider.Dataframe; import datart.core.entity.Download; import datart.core.entity.View; +import datart.core.entity.poi.POISettings; import datart.core.mappers.ext.DownloadMapperExt; -import datart.core.base.exception.NotAllowedException; import datart.server.base.params.DownloadCreateParam; import datart.server.base.params.ViewExecuteParam; -import datart.server.service.BaseService; -import datart.server.service.DataProviderService; -import datart.server.service.DownloadService; -import datart.server.service.OrgSettingService; +import datart.server.service.*; +import datart.server.common.PoiConvertUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.poi.ss.usermodel.Workbook; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.IOException; +import java.util.Calendar; import java.util.Date; import java.util.List; @@ -59,12 +60,16 @@ public class DownloadServiceImpl extends BaseService implements DownloadService private final OrgSettingService orgSettingService; + private final VizService vizService; + public DownloadServiceImpl(DownloadMapperExt downloadMapper, DataProviderService dataProviderService, - OrgSettingService orgSettingService) { + OrgSettingService orgSettingService, + VizService vizService) { this.downloadMapper = downloadMapper; this.dataProviderService = dataProviderService; this.orgSettingService = orgSettingService; + this.vizService = vizService; } @Override @@ -102,16 +107,19 @@ public Download submitDownloadTask(DownloadCreateParam downloadParams, String cl securityManager.runAs(downloadUser); String fileName = downloadParams.getFileName(); - String path = FileUtils.concatPath(FileOwner.DOWNLOAD.getPath(), StringUtils.isEmpty(fileName) ? "download" : fileName + "-" + System.currentTimeMillis() + XLSX); + String fileSuffix = DateFormatUtils.format(Calendar.getInstance(), Const.FILE_SUFFIX_DATE_FORMAT); + String path = FileUtils.concatPath(FileOwner.DOWNLOAD.getPath(), StringUtils.isEmpty(fileName) ? "download" : fileName + "_" + fileSuffix + XLSX); try { Workbook workbook = POIUtils.createEmpty(); for (int i = 0; i < downloadParams.getDownloadParams().size(); i++) { ViewExecuteParam viewExecuteParam = downloadParams.getDownloadParams().get(i); View view = retrieve(viewExecuteParam.getViewId(), View.class, false); viewExecuteParam.setPageInfo(PageInfo.builder().pageNo(1).pageSize(orgSettingService.getDownloadRecordLimit(view.getOrgId())).build()); - String vizName = viewExecuteParam.getVizName(); Dataframe dataframe = dataProviderService.execute(downloadParams.getDownloadParams().get(i)); - POIUtils.withSheet(workbook, StringUtils.isEmpty(vizName) ? "Sheet" + i : vizName, dataframe); + String chartConfigStr = vizService.getChartConfigByVizId(viewExecuteParam.getVizId(), viewExecuteParam.getVizType()); + POISettings poiSettings = PoiConvertUtils.covertToPoiSetting(chartConfigStr, dataframe); + String sheetName = StringUtils.isNotBlank(viewExecuteParam.getVizName()) ? viewExecuteParam.getVizName() : "Sheet"+i; + POIUtils.withSheet(workbook, sheetName, dataframe, poiSettings); } try { POIUtils.save(workbook, FileUtils.withBasePath(path), true); diff --git a/server/src/main/java/datart/server/service/impl/FolderServiceImpl.java b/server/src/main/java/datart/server/service/impl/FolderServiceImpl.java index e9e4c0438..e361f5964 100644 --- a/server/src/main/java/datart/server/service/impl/FolderServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/FolderServiceImpl.java @@ -163,6 +163,11 @@ public boolean checkUnique(ResourceType resourceType, String orgId, String paren return false; } + @Override + public Folder getVizFolder(String vizId, String relType) { + return folderMapper.selectByRelTypeAndId(relType, vizId); + } + @Override @Transactional public boolean update(BaseUpdateParam baseUpdateParam) { diff --git a/server/src/main/java/datart/server/service/impl/ScheduleServiceImpl.java b/server/src/main/java/datart/server/service/impl/ScheduleServiceImpl.java index bf758f896..ee8480097 100644 --- a/server/src/main/java/datart/server/service/impl/ScheduleServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/ScheduleServiceImpl.java @@ -41,8 +41,10 @@ import datart.server.base.params.ScheduleUpdateParam; import datart.server.service.BaseService; import datart.server.service.RoleService; -import datart.server.service.ScheduleJob; +import datart.server.job.EmailJob; +import datart.server.job.ScheduleJob; import datart.server.service.ScheduleService; +import datart.server.job.WeChartJob; import lombok.extern.slf4j.Slf4j; import org.quartz.*; import org.springframework.beans.BeanUtils; @@ -195,6 +197,7 @@ public boolean start(String scheduleId) throws SchedulerException { .startAt(schedule.getStartDate()) .endAt(schedule.getEndDate()) .build(); + scheduler.scheduleJob(createJobDetail(schedule), trigger); schedule.setActive(true); scheduleMapper.updateByPrimaryKey(schedule); diff --git a/server/src/main/java/datart/server/service/impl/SourceServiceImpl.java b/server/src/main/java/datart/server/service/impl/SourceServiceImpl.java index a43b2088c..e60fab668 100644 --- a/server/src/main/java/datart/server/service/impl/SourceServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/SourceServiceImpl.java @@ -24,12 +24,17 @@ import datart.core.base.consts.Const; import datart.core.base.consts.FileOwner; import datart.core.base.exception.Exceptions; +import datart.core.common.TaskExecutor; +import datart.core.data.provider.SchemaInfo; import datart.core.data.provider.DataProviderConfigTemplate; import datart.core.data.provider.DataProviderSource; +import datart.core.data.provider.SchemaItem; import datart.core.entity.Role; import datart.core.entity.Source; -import datart.core.mappers.ext.RelRoleResourceMapperExt; +import datart.core.entity.SourceSchemas; +import datart.core.entity.ext.SourceDetail; import datart.core.mappers.ext.SourceMapperExt; +import datart.core.mappers.ext.SourceSchemasMapperExt; import datart.security.base.PermissionInfo; import datart.security.base.ResourceType; import datart.security.base.SubjectType; @@ -41,9 +46,12 @@ import datart.server.base.params.BaseUpdateParam; import datart.server.base.params.SourceCreateParam; import datart.server.base.params.SourceUpdateParam; +import datart.server.job.SchemaSyncJob; import datart.server.service.*; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.quartz.*; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; @@ -56,6 +64,10 @@ @Service public class SourceServiceImpl extends BaseService implements SourceService { + private static final String ENABLE_SYNC_SCHEMAS = "enableSyncSchemas"; + + private static final String SYNC_INTERVAL = "syncInterval"; + private final SourceMapperExt sourceMapper; private final DataProviderService dataProviderService; @@ -64,18 +76,21 @@ public class SourceServiceImpl extends BaseService implements SourceService { private final FileService fileService; - private final RelRoleResourceMapperExt rrrMapper; + private final Scheduler scheduler; + + private final SourceSchemasMapperExt sourceSchemasMapper; public SourceServiceImpl(SourceMapperExt sourceMapper, DataProviderService dataProviderService, RoleService roleService, FileService fileService, - RelRoleResourceMapperExt rrrMapper) { + Scheduler scheduler, SourceSchemasMapperExt sourceSchemasMapper) { this.sourceMapper = sourceMapper; this.dataProviderService = dataProviderService; this.roleService = roleService; this.fileService = fileService; - this.rrrMapper = rrrMapper; + this.scheduler = scheduler; + this.sourceSchemasMapper = sourceSchemasMapper; } @Override @@ -92,6 +107,29 @@ public List listSources(String orgId, boolean active) throws PermissionD }).collect(Collectors.toList()); } + @Override + public SchemaInfo getSourceSchemaInfo(String sourceId) { + SourceSchemas sourceSchemas = sourceSchemasMapper.selectBySource(sourceId); + if (sourceSchemas == null || StringUtils.isBlank(sourceSchemas.getSchemas())) { + return SchemaInfo.empty(); + } + SchemaInfo schemaInfo = new SchemaInfo(); + try { + schemaInfo.setUpdateTime(sourceSchemas.getUpdateTime()); + schemaInfo.setSchemaItems(OBJECT_MAPPER.readerForListOf(SchemaItem.class).readValue(sourceSchemas.getSchemas())); + } catch (Exception e) { + log.error("source schema parse error ", e); + } + return schemaInfo; + } + + @Override + public SchemaInfo syncSourceSchema(String sourceId) throws Exception { + SchemaSyncJob schemaSyncJob = new SchemaSyncJob(); + schemaSyncJob.execute(sourceId); + return getSourceSchemaInfo(sourceId); + } + @Override public void requirePermission(Source source, int permission) { if (securityManager.isOrgOwner(source.getOrgId())) { @@ -127,7 +165,7 @@ public Source create(BaseCreateParam createParam) { Source source = SourceService.super.create(createParam); grantDefaultPermission(source); - + updateJdbcSourceSyncJob(source); return source; } @@ -147,7 +185,9 @@ public boolean update(BaseUpdateParam updateParam) { providerSource.setType(sourceUpdateParam.getType()); providerSource.setName(sourceUpdateParam.getName()); dataProviderService.updateSource(providerSource); - return SourceService.super.update(updateParam); + boolean success = SourceService.super.update(updateParam); + updateJdbcSourceSyncJob(retrieve(updateParam.getId())); + return success; } @Override @@ -190,8 +230,60 @@ private String encryptConfig(String type, String config) throws Exception { return config; } + @Override + public Source retrieve(String id) { + SourceDetail sourceDetail = new SourceDetail(SourceService.super.retrieve(id)); + sourceDetail.setSchemaUpdateDate(sourceSchemasMapper.selectUpdateDateBySource(id)); + return sourceDetail; + } + @Override public void deleteStaticFiles(Source source) { fileService.deleteFiles(FileOwner.DATA_SOURCE, source.getId()); } + + private void updateJdbcSourceSyncJob(Source source) { + TaskExecutor.submit(() -> { + try { + new SchemaSyncJob().execute(source.getId()); + } catch (Exception e) { + log.error("source schema sync error", e); + } + }); + try { + DataProviderSource dataProviderSource = dataProviderService.parseDataProviderConfig(source); + + JobKey jobKey = new JobKey(source.getName(), source.getId()); + + Object enable = dataProviderSource.getProperties().get(ENABLE_SYNC_SCHEMAS); + if (enable != null && "true".equals(enable.toString())) { + Object interval = dataProviderSource.getProperties().get(SYNC_INTERVAL); + if (interval == null || !NumberUtils.isDigits(interval.toString())) { + Exceptions.msg("sync interval must be a number"); + } + int intervalMin = Math.max(Integer.parseInt(interval.toString()), Const.MINIMUM_SYNC_INTERVAL); + + scheduler.deleteJob(jobKey); + Trigger trigger = TriggerBuilder.newTrigger() + .withIdentity(source.getId()) + .withSchedule(SimpleScheduleBuilder.repeatMinutelyForever(intervalMin)) + .startNow() + .build(); + JobDetail jobDetail = JobBuilder.newJob() + .withIdentity(jobKey) + .ofType(SchemaSyncJob.class) + .build(); + jobDetail.getJobDataMap().put(SchemaSyncJob.SOURCE_ID, source.getId()); + scheduler.scheduleJob(jobDetail, trigger); + log.info("jdbc source schema job has been created {} - {} - interval {} ", source.getId(), source.getName(), intervalMin); + } else { + scheduler.deleteJob(jobKey); + log.info("jdbc source schema job has been deleted {} - {}", source.getId(), source.getName()); + } + } catch (Exception e) { + log.error("schema sync job update error ", e); + } + } + + } diff --git a/server/src/main/java/datart/server/service/impl/UserServiceImpl.java b/server/src/main/java/datart/server/service/impl/UserServiceImpl.java index ffc878bc7..4ec9fed88 100644 --- a/server/src/main/java/datart/server/service/impl/UserServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/UserServiceImpl.java @@ -18,9 +18,13 @@ package datart.server.service.impl; +import com.alibaba.fastjson.JSONObject; +import com.jayway.jsonpath.JsonPath; import datart.core.base.consts.UserIdentityType; import datart.core.base.exception.BaseException; import datart.core.base.exception.Exceptions; +import datart.core.base.exception.ParamException; +import datart.core.base.exception.ServerException; import datart.core.common.UUIDGenerator; import datart.core.entity.Organization; import datart.core.entity.User; @@ -32,8 +36,6 @@ import datart.security.util.SecurityUtils; import datart.server.base.dto.OrganizationBaseInfo; import datart.server.base.dto.UserProfile; -import datart.core.base.exception.NotFoundException; -import datart.core.base.exception.ParamException; import datart.server.base.params.ChangeUserPasswordParam; import datart.server.base.params.UserRegisterParam; import datart.server.base.params.UserResetPasswordParam; @@ -46,6 +48,8 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.bcrypt.BCrypt; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -56,6 +60,8 @@ import java.util.List; import java.util.stream.Collectors; +import static datart.core.common.Application.getProperty; + @Service @Slf4j public class UserServiceImpl extends BaseService implements UserService { @@ -68,7 +74,7 @@ public class UserServiceImpl extends BaseService implements UserService { private final MailService mailService; - @Value("${datart.user.active.send-mail:true}") + @Value("${datart.user.active.send-mail:false}") private boolean sendEmail; public UserServiceImpl(UserMapperExt userMapper, @@ -294,6 +300,44 @@ public boolean resetPassword(UserResetPasswordParam passwordParam) { return userMapper.updateByPrimaryKeySelective(update) == 1; } + @Override + public User externalRegist(OAuth2AuthenticationToken oauthAuthToken) throws ServerException { + OAuth2User oauthUser = oauthAuthToken.getPrincipal(); + + User user = getUserByName(oauthUser.getName()); + if (user != null) { + return user; + } + user = new User(); + + String emailMapping = getProperty(String.format("spring.security.oauth2.client.provider.%s.userMapping.email", oauthAuthToken.getAuthorizedClientRegistrationId())); + String nameMapping = getProperty(String.format("spring.security.oauth2.client.provider.%s.userMapping.name", oauthAuthToken.getAuthorizedClientRegistrationId())); + String avatarMapping = getProperty(String.format("spring.security.oauth2.client.provider.%s.userMapping.avatar", oauthAuthToken.getAuthorizedClientRegistrationId())); + JSONObject jsonObj = new JSONObject(oauthUser.getAttributes()); + + user.setId(UUIDGenerator.generate()); + user.setCreateBy(user.getId()); + user.setCreateTime(new Date()); + user.setName(JsonPath.read(jsonObj, nameMapping)); + user.setUsername(oauthUser.getName()); + user.setActive(true); + //todo: oauth2登录后需要设置随机密码,此字段作为密文,显然无法对应原文,即不会有任何密码对应以下值 + user.setPassword(BCrypt.hashpw("xxx", BCrypt.gensalt())); + if (emailMapping != null) { + user.setEmail(JsonPath.read(jsonObj, emailMapping)); + } + if (avatarMapping != null) { + user.setAvatar(JsonPath.read(jsonObj, avatarMapping)); + } + int insert = userMapper.insert(user); + if (insert > 0) { + return user; + } else { + log.info("regist fail: {}", oauthUser.getName()); + throw new ServerException("regist fail: unspecified error"); + } + } + @Override public void requirePermission(User entity, int permission) { diff --git a/server/src/main/java/datart/server/service/impl/VizServiceImpl.java b/server/src/main/java/datart/server/service/impl/VizServiceImpl.java index 1a65a04b4..e75b5f766 100644 --- a/server/src/main/java/datart/server/service/impl/VizServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/VizServiceImpl.java @@ -17,6 +17,7 @@ */ package datart.server.service.impl; +import com.alibaba.fastjson.JSON; import datart.core.base.consts.Const; import datart.core.base.consts.VariableTypeEnum; import datart.core.base.exception.Exceptions; @@ -24,9 +25,11 @@ import datart.core.entity.*; import datart.security.base.ResourceType; import datart.server.base.dto.*; +import datart.server.base.dto.chart.WidgetConfig; import datart.server.base.params.*; import datart.server.service.*; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -237,15 +240,48 @@ public StoryboardDetail getStoryboard(String storyboardId) { return storyboardService.getStoryboard(storyboardId); } + @Override + public String getChartConfigByVizId(String vizId, String vizType) { + String result = ""; + try { + if (StringUtils.isNotBlank(vizId)) { + switch (vizType) { + case "dataChart": + return retrieve(vizId, Datachart.class).getConfig(); + case "widget": + String config = retrieve(vizId, Widget.class).getConfig(); + WidgetConfig widgetConfig = JSON.parseObject(config, WidgetConfig.class); + return widgetConfig.getChartConfig(); + default: + return result; + } + } + } catch (Exception e) { + log.warn("query chart("+vizId+") config fail, download with none style."); + } + return result; + } + @Override @Transactional public boolean updateDatachart(DatachartUpdateParam updateParam) { + // update folder + Folder vizFolder = folderService.getVizFolder(updateParam.getId(), ResourceType.DATACHART.name()); + vizFolder.setAvatar(updateParam.getAvatar()); + vizFolder.setSubType(updateParam.getSubType()); + folderService.getDefaultMapper().updateByPrimaryKey(vizFolder); + return datachartService.update(updateParam); } @Override @Transactional public boolean updateDashboard(DashboardUpdateParam updateParam) { + // update folder + Folder vizFolder = folderService.getVizFolder(updateParam.getId(), ResourceType.DASHBOARD.name()); + vizFolder.setAvatar(updateParam.getAvatar()); + vizFolder.setSubType(updateParam.getSubType()); + folderService.getDefaultMapper().updateByPrimaryKey(vizFolder); return dashboardService.update(updateParam); } diff --git a/server/src/main/resources/META-INF/spring.factories b/server/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..a93867bf1 --- /dev/null +++ b/server/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.env.EnvironmentPostProcessor=datart.server.config.CustomPropertiesValidate \ No newline at end of file diff --git a/config/application-demo.yml b/server/src/main/resources/application-demo.yml similarity index 79% rename from config/application-demo.yml rename to server/src/main/resources/application-demo.yml index 34bc0c99c..e1f93a4ad 100644 --- a/config/application-demo.yml +++ b/server/src/main/resources/application-demo.yml @@ -2,20 +2,21 @@ spring: datasource: driver-class-name: org.h2.Driver type: com.alibaba.druid.pool.DruidDataSource - url: jdbc:h2:file:./bin/h2/datart.demo;MODE=MYSQL;DATABASE_TO_UPPER=false + url: jdbc:h2:file:./bin/h2/datart.demo;MODE=MySQL;DATABASE_TO_LOWER=TRUE;IGNORECASE=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;IFEXISTS=TRUE username: password: + flyway: + enabled: false + server: port: 8080 address: 0.0.0.0 - - # 开启 gzip 压缩,加快请求和响应速度 + compression: - enabled: true + enabled: true mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* - datart: server: address: http://127.0.0.1:8080 diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index 4ad0f42e2..8d8760032 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -2,11 +2,21 @@ spring: application: name: datart-server + flyway: + enabled: true + baseline-on-migrate: true + baseline-description: BASE_LINE + baseline-version: "2022.02.18" + clean-disabled: true + clean-on-validation-error: false + validate-on-migrate: false + main: banner-mode: off profiles: - include: config + active: demo,config + servlet: multipart: max-file-size: 1024MB @@ -64,7 +74,15 @@ shiro: web: enabled: false - datart: server: - path-prefix: /api/v1 \ No newline at end of file + path-prefix: /api/v1 + + +server: + port: 8080 + address: 0.0.0.0 + + compression: + enabled: true + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* diff --git a/server/src/main/resources/assembly/assembly.xml b/server/src/main/resources/assembly/assembly.xml index 39b8648b9..43e5a7751 100644 --- a/server/src/main/resources/assembly/assembly.xml +++ b/server/src/main/resources/assembly/assembly.xml @@ -9,6 +9,26 @@ ${project.parent.basedir}/bin bin + unix + + datart-server.sh + + + + ${project.parent.basedir}/bin + bin + dos + + datart-server.cmd + + + + ${project.parent.basedir}/bin/h2 + bin/h2 + + + ${project.parent.basedir}/bin/migrations + bin/migrations ${project.parent.basedir}/config @@ -19,13 +39,15 @@ static - ${project.parent.basedir} - ./ - - Dockerfile - - - + ${project.parent.basedir} + ./ + + Dockerfile + LICENSE + READEME.md + Deployment.md + + diff --git a/server/src/main/resources/db/migration/V2022.02.18__baseline.sql b/server/src/main/resources/db/migration/V2022.02.18__baseline.sql new file mode 100644 index 000000000..bde7218ee --- /dev/null +++ b/server/src/main/resources/db/migration/V2022.02.18__baseline.sql @@ -0,0 +1,666 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for QRTZ_BLOB_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_BLOB_TRIGGERS`; +CREATE TABLE `QRTZ_BLOB_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `BLOB_DATA` blob NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE, + CONSTRAINT `qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for QRTZ_CALENDARS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_CALENDARS`; +CREATE TABLE `QRTZ_CALENDARS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `CALENDAR_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `CALENDAR` blob NOT NULL, + PRIMARY KEY (`SCHED_NAME`, `CALENDAR_NAME`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for QRTZ_CRON_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_CRON_TRIGGERS`; +CREATE TABLE `QRTZ_CRON_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `CRON_EXPRESSION` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TIME_ZONE_ID` varchar(80) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE, + CONSTRAINT `qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for QRTZ_FIRED_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_FIRED_TRIGGERS`; +CREATE TABLE `QRTZ_FIRED_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `ENTRY_ID` varchar(95) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `INSTANCE_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `FIRED_TIME` bigint(13) NOT NULL, + `SCHED_TIME` bigint(13) NOT NULL, + `PRIORITY` int(11) NOT NULL, + `STATE` varchar(16) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `JOB_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `JOB_GROUP` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `IS_NONCONCURRENT` varchar(1) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `REQUESTS_RECOVERY` varchar(1) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + PRIMARY KEY (`SCHED_NAME`, `ENTRY_ID`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for QRTZ_JOB_DETAILS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_JOB_DETAILS`; +CREATE TABLE `QRTZ_JOB_DETAILS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `JOB_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `JOB_GROUP` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `DESCRIPTION` varchar(250) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `JOB_CLASS_NAME` varchar(250) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `IS_DURABLE` varchar(1) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `IS_NONCONCURRENT` varchar(1) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `IS_UPDATE_DATA` varchar(1) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `REQUESTS_RECOVERY` varchar(1) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `JOB_DATA` blob NULL, + PRIMARY KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for QRTZ_LOCKS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_LOCKS`; +CREATE TABLE `QRTZ_LOCKS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `LOCK_NAME` varchar(40) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + PRIMARY KEY (`SCHED_NAME`, `LOCK_NAME`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for QRTZ_PAUSED_TRIGGER_GRPS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_PAUSED_TRIGGER_GRPS`; +CREATE TABLE `QRTZ_PAUSED_TRIGGER_GRPS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_GROUP`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for QRTZ_SCHEDULER_STATE +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_SCHEDULER_STATE`; +CREATE TABLE `QRTZ_SCHEDULER_STATE` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `INSTANCE_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `LAST_CHECKIN_TIME` bigint(13) NOT NULL, + `CHECKIN_INTERVAL` bigint(13) NOT NULL, + PRIMARY KEY (`SCHED_NAME`, `INSTANCE_NAME`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for QRTZ_SIMPLE_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_SIMPLE_TRIGGERS`; +CREATE TABLE `QRTZ_SIMPLE_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `REPEAT_COUNT` bigint(7) NOT NULL, + `REPEAT_INTERVAL` bigint(12) NOT NULL, + `TIMES_TRIGGERED` bigint(10) NOT NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE, + CONSTRAINT `qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for QRTZ_SIMPROP_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_SIMPROP_TRIGGERS`; +CREATE TABLE `QRTZ_SIMPROP_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `STR_PROP_1` varchar(512) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `STR_PROP_2` varchar(512) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `STR_PROP_3` varchar(512) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `INT_PROP_1` int(11) NULL DEFAULT NULL, + `INT_PROP_2` int(11) NULL DEFAULT NULL, + `LONG_PROP_1` bigint(20) NULL DEFAULT NULL, + `LONG_PROP_2` bigint(20) NULL DEFAULT NULL, + `DEC_PROP_1` decimal(13, 4) NULL DEFAULT NULL, + `DEC_PROP_2` decimal(13, 4) NULL DEFAULT NULL, + `BOOL_PROP_1` varchar(1) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `BOOL_PROP_2` varchar(1) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE, + CONSTRAINT `qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for QRTZ_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_TRIGGERS`; +CREATE TABLE `QRTZ_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `JOB_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `JOB_GROUP` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `DESCRIPTION` varchar(250) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `NEXT_FIRE_TIME` bigint(13) NULL DEFAULT NULL, + `PREV_FIRE_TIME` bigint(13) NULL DEFAULT NULL, + `PRIORITY` int(11) NULL DEFAULT NULL, + `TRIGGER_STATE` varchar(16) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `TRIGGER_TYPE` varchar(8) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `START_TIME` bigint(13) NOT NULL, + `END_TIME` bigint(13) NULL DEFAULT NULL, + `CALENDAR_NAME` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `MISFIRE_INSTR` smallint(2) NULL DEFAULT NULL, + `JOB_DATA` blob NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE, + INDEX `SCHED_NAME`(`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) USING BTREE, + CONSTRAINT `qrtz_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for access_log +-- ---------------------------- +DROP TABLE IF EXISTS `access_log`; +CREATE TABLE `access_log` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `user` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `resource_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `resource_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `access_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `access_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), + `duration` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for dashboard +-- ---------------------------- +DROP TABLE IF EXISTS `dashboard`; +CREATE TABLE `dashboard` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `config` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `thumbnail` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `status` tinyint(6) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`) USING BTREE, + INDEX `org_id`(`org_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for datachart +-- ---------------------------- +DROP TABLE IF EXISTS `datachart`; +CREATE TABLE `datachart` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `view_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `config` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `thumbnail` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `status` tinyint(6) NULL DEFAULT 1, + PRIMARY KEY (`id`) USING BTREE, + INDEX `view_id`(`view_id`) USING BTREE, + INDEX `org_id`(`org_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for download +-- ---------------------------- +DROP TABLE IF EXISTS `download`; +CREATE TABLE `download` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `path` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `last_download_time` timestamp(0) NULL DEFAULT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `create_by` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `status` tinyint(6) NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `create_by`(`create_by`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for folder +-- ---------------------------- +DROP TABLE IF EXISTS `folder`; +CREATE TABLE `folder` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `rel_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `rel_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `parent_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `index` double(16, 8) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `name_unique`(`name`, `org_id`, `parent_id`) USING BTREE, + INDEX `org_id`(`org_id`) USING BTREE, + INDEX `rel_id`(`rel_id`) USING BTREE, + INDEX `parent_id`(`parent_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for link +-- ---------------------------- +DROP TABLE IF EXISTS `link`; +CREATE TABLE `link` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `rel_type` varchar(128) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `rel_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `expiration` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for org_settings +-- ---------------------------- +DROP TABLE IF EXISTS `org_settings`; +CREATE TABLE `org_settings` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `type` varchar(128) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `config` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `org_id`(`org_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for organization +-- ---------------------------- +DROP TABLE IF EXISTS `organization`; +CREATE TABLE `organization` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `description` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `create_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `update_time` datetime(0) NULL DEFAULT NULL, + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `orgName`(`name`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for rel_role_resource +-- ---------------------------- +DROP TABLE IF EXISTS `rel_role_resource`; +CREATE TABLE `rel_role_resource` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `resource_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `resource_type` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `permission` int(11) NOT NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `role_id_2`(`role_id`, `resource_id`, `resource_type`) USING BTREE, + INDEX `role_id`(`role_id`) USING BTREE, + INDEX `resource_id`(`resource_id`) USING BTREE, + INDEX `resource_type`(`resource_type`) USING BTREE, + INDEX `org_id`(`org_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for rel_role_user +-- ---------------------------- +DROP TABLE IF EXISTS `rel_role_user`; +CREATE TABLE `rel_role_user` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `user_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL, + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `user_role`(`user_id`, `role_id`) USING BTREE, + INDEX `user_id`(`user_id`) USING BTREE, + INDEX `role_id`(`role_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for rel_subject_columns +-- ---------------------------- +DROP TABLE IF EXISTS `rel_subject_columns`; +CREATE TABLE `rel_subject_columns` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `view_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `subject_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `subject_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `column_permission` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL, + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + PRIMARY KEY (`id`) USING BTREE, + INDEX `view_id`(`view_id`) USING BTREE, + INDEX `subject_id`(`subject_id`) USING BTREE, + INDEX `subject_type`(`subject_type`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for rel_user_organization +-- ---------------------------- +DROP TABLE IF EXISTS `rel_user_organization`; +CREATE TABLE `rel_user_organization` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `user_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL, + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `org_user`(`org_id`, `user_id`) USING BTREE, + INDEX `user_id`(`user_id`) USING BTREE, + INDEX `org_id`(`org_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for rel_variable_subject +-- ---------------------------- +DROP TABLE IF EXISTS `rel_variable_subject`; +CREATE TABLE `rel_variable_subject` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `variable_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `subject_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `subject_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `value` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `create_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), + `use_default_value` tinyint(4) NOT NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `user_var`(`variable_id`, `subject_type`, `subject_id`) USING BTREE, + INDEX `variable_id`(`variable_id`) USING BTREE, + INDEX `subject_id`(`subject_id`) USING BTREE, + INDEX `subject_type`(`subject_type`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for rel_widget_element +-- ---------------------------- +DROP TABLE IF EXISTS `rel_widget_element`; +CREATE TABLE `rel_widget_element` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `widget_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `rel_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `rel_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `rel_id`(`rel_id`) USING BTREE, + INDEX `rel_type`(`rel_type`) USING BTREE, + INDEX `widget_id`(`widget_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for rel_widget_widget +-- ---------------------------- +DROP TABLE IF EXISTS `rel_widget_widget`; +CREATE TABLE `rel_widget_widget` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `source_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `target_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `config` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `source_id`(`source_id`) USING BTREE, + INDEX `target_id`(`target_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for role +-- ---------------------------- +DROP TABLE IF EXISTS `role`; +CREATE TABLE `role` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `type` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL, + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `ord_and_name`(`org_id`, `name`) USING BTREE, + INDEX `org_id`(`org_id`) USING BTREE, + INDEX `type`(`type`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for schedule +-- ---------------------------- +DROP TABLE IF EXISTS `schedule`; +CREATE TABLE `schedule` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `type` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `active` tinyint(4) NOT NULL, + `cron_expression` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `start_date` timestamp(0) NULL DEFAULT NULL, + `end_date` timestamp(0) NULL DEFAULT NULL, + `config` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `create_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `parent_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `is_folder` tinyint(1) NULL DEFAULT NULL, + `index` int(11) NULL DEFAULT NULL, + `status` tinyint(6) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`) USING BTREE, + INDEX `org_id`(`org_id`) USING BTREE, + INDEX `create_by`(`create_by`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for schedule_log +-- ---------------------------- +DROP TABLE IF EXISTS `schedule_log`; +CREATE TABLE `schedule_log` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `schedule_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `start` timestamp(0) NULL DEFAULT NULL, + `end` timestamp(0) NULL DEFAULT NULL, + `status` int(11) NOT NULL, + `message` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `schedule_id`(`schedule_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for source +-- ---------------------------- +DROP TABLE IF EXISTS `source`; +CREATE TABLE `source` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `config` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `type` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL, + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `status` tinyint(6) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `org_name`(`name`, `org_id`) USING BTREE, + INDEX `org_id`(`org_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for storyboard +-- ---------------------------- +DROP TABLE IF EXISTS `storyboard`; +CREATE TABLE `storyboard` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `config` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL, + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `status` tinyint(6) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`) USING BTREE, + INDEX `org_id`(`org_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for storypage +-- ---------------------------- +DROP TABLE IF EXISTS `storypage`; +CREATE TABLE `storypage` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `storyboard_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `rel_type` varchar(128) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `rel_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `config` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `storyboard_id`(`storyboard_id`) USING BTREE, + INDEX `rel_type`(`rel_type`) USING BTREE, + INDEX `rel_id`(`rel_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for user +-- ---------------------------- +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `active` tinyint(1) NULL DEFAULT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `username`(`username`) USING BTREE, + UNIQUE INDEX `email`(`email`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for user_settings +-- ---------------------------- +DROP TABLE IF EXISTS `user_settings`; +CREATE TABLE `user_settings` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `user_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `rel_type` varchar(128) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `rel_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL, + `config` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for variable +-- ---------------------------- +DROP TABLE IF EXISTS `variable`; +CREATE TABLE `variable` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `view_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `type` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `value_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `permission` int(11) NULL DEFAULT NULL, + `encrypt` tinyint(4) NULL DEFAULT NULL, + `label` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `default_value` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `expression` tinyint(4) NULL DEFAULT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `org_id`(`org_id`, `view_id`, `name`) USING BTREE, + INDEX `org_id_2`(`org_id`) USING BTREE, + INDEX `view_id`(`view_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for view +-- ---------------------------- +DROP TABLE IF EXISTS `view`; +CREATE TABLE `view` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `org_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `source_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `script` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `model` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `config` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL, + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `parent_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `is_folder` tinyint(1) NULL DEFAULT NULL, + `index` double(16, 8) NULL DEFAULT NULL, + `status` tinyint(6) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_name`(`name`, `org_id`, `parent_id`) USING BTREE, + INDEX `org_id`(`org_id`) USING BTREE, + INDEX `source_id`(`source_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for widget +-- ---------------------------- +DROP TABLE IF EXISTS `widget`; +CREATE TABLE `widget` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `dashboard_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `config` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `parent_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `create_time` timestamp(0) NULL DEFAULT NULL, + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` timestamp(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + PRIMARY KEY (`id`) USING BTREE, + INDEX `dashboard_id`(`dashboard_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/server/src/main/resources/db/migration/V2022.02.19__1.0.0.beta.2.sql b/server/src/main/resources/db/migration/V2022.02.19__1.0.0.beta.2.sql new file mode 100644 index 000000000..dfb10ff8e --- /dev/null +++ b/server/src/main/resources/db/migration/V2022.02.19__1.0.0.beta.2.sql @@ -0,0 +1,21 @@ +SET NAMES utf8; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for source_schemas +-- ---------------------------- +DROP TABLE IF EXISTS `source_schemas`; +CREATE TABLE `source_schemas` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `source_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `schemas` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL, + `update_time` datetime(0) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; + + +ALTER TABLE `folder` + ADD COLUMN `sub_type` varchar(255) NULL AFTER `rel_type`, + ADD COLUMN `avatar` varchar(255) NULL AFTER `rel_id`; \ No newline at end of file diff --git a/server/src/main/resources/i18n/datart_i18n.properties b/server/src/main/resources/i18n/datart_i18n.properties index c63b3bd96..6abc80d9a 100644 --- a/server/src/main/resources/i18n/datart_i18n.properties +++ b/server/src/main/resources/i18n/datart_i18n.properties @@ -116,6 +116,10 @@ message.provider.sql.parse.failed=SQL解析异常 message.provider.permission.variable.usage.error=用法错误[{0}],权限变量只能在布尔表达式中使用! config.template.jdbc.enableSpecialSQL=允许未识别SQL执行 config.template.jdbc.enableSpecialSQL.desc=Datart默认只允许DQL执行,禁止DML和DDL执行。其它类型的SQL(如存储过程)是否允许执行,可通过该选项进行配置。 +config.template.jdbc.enableSyncSchemas=定时同步数据库库表信息 +config.template.jdbc.syncInterval=定时同步时间间隔(分钟) +config.template.jdbc.enableSyncSchemas.desc=开启后,Datart将按照指定时间间隔定时同步数据库库表信息 +config.template.jdbc.serverAggregate.desc=服务端聚合会拉取源表的全量数据到服务端,然后在服务端执行SQL计算 diff --git a/server/src/main/resources/i18n/datart_i18n_en.properties b/server/src/main/resources/i18n/datart_i18n_en.properties index ab9922f86..0372d717c 100644 --- a/server/src/main/resources/i18n/datart_i18n_en.properties +++ b/server/src/main/resources/i18n/datart_i18n_en.properties @@ -115,5 +115,9 @@ message.provider.sql.parse.failed=sql parse failed message.provider.permission.variable.usage.error=useage error[{0}],Permission variables can only be used in Boolean expressions config.template.jdbc.enableSpecialSQL=Allow unrecognized SQL execution config.template.jdbc.enableSpecialSQL.desc=By default, Datart allows only DQL execution. DML and DDL execution are not allowed. You can configure whether other types of SQL(such as stored procedures) can be executed. +config.template.jdbc.enableSyncSchemas=Periodically synchronize the database schemas +config.template.jdbc.syncInterval=Timing synchronization interval (minutes) +config.template.jdbc.enableSyncSchemas.desc=When enabled, Datart periodically synchronizes database database schemas at specified intervals +config.template.jdbc.serverAggregate.desc=Server-side aggregation pulls the full amount of data from the source table to the server, where SQL calculations are performed diff --git a/server/src/main/resources/i18n/datart_i18n_zh.properties b/server/src/main/resources/i18n/datart_i18n_zh.properties index aa136e130..ba5d67099 100644 --- a/server/src/main/resources/i18n/datart_i18n_zh.properties +++ b/server/src/main/resources/i18n/datart_i18n_zh.properties @@ -115,4 +115,8 @@ message.provider.sql.parse.failed=SQL解析异常 message.provider.permission.variable.usage.error=用法错误[{0}],权限变量只能在布尔表达式中使用! config.template.jdbc.enableSpecialSQL=允许未识别SQL执行 config.template.jdbc.enableSpecialSQL.desc=Datart默认只允许DQL执行,禁止DML和DDL执行。其它类型的SQL(如存储过程)是否允许执行,可通过该选项进行配置。 +config.template.jdbc.enableSyncSchemas=定时同步数据库库表信息 +config.template.jdbc.syncInterval=定时同步时间间隔(分钟) +config.template.jdbc.enableSyncSchemas.desc=开启后,Datart将按照指定时间间隔定时同步数据库库表信息 +config.template.jdbc.serverAggregate.desc=服务端聚合会拉取源表的全量数据到服务端,然后在服务端执行SQL计算