diff --git a/Deployment.md b/Deployment.md index 41438a85d..527fe2704 100644 --- a/Deployment.md +++ b/Deployment.md @@ -12,7 +12,7 @@ title: 部署 - JDK 1.8+ - MySql5.7+ -- datart安装包(datart-server-1.0.0-alpha.3-install.zip) +- datart安装包(datart-server-1.0.0-beta.x-install.zip) - Mail Server (可选) - [ChromeWebDriver](https://chromedriver.chromium.org/) (可选) - Redis (可选) @@ -20,7 +20,7 @@ title: 部署 方式1 :解压安装包 (官方提供的包) ```bash -unzip datart-server-1.0.0-alpha.3-install.zip +unzip datart-server-1.0.0-beta.x-install.zip ``` 方式2 :自行编译 @@ -32,11 +32,11 @@ cd datart mvn clean package -Dmaven.test.skip=true -cp ./datart-server-1.0.0-alpha.3-install.zip ${deployment_basedir} +cp ./datart-server-1.0.0-beta.x-install.zip ${deployment_basedir} cd ${deployment_basedir} -unzip datart-server-1.0.0-alpha.3-install.zip +unzip datart-server-1.0.0-beta.x-install.zip ``` diff --git a/README.md b/README.md index 5638be87b..f3c375433 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ datart 可作为独立平台使用,但不仅限于此,为了更好支持快 ![](https://running-elephant.github.io/datart-docs/images/about/wechat-group.jpeg) #### 插件示例仓库 -[示例仓库 v1.0.0](https://github.com/Cuiyansong/datart-extension-charts) +[示例仓库 v1.0.0](https://github.com/running-elephan/datart-extension-charts) ### 参与贡献 Contributing 非常欢迎和感谢参与贡献,如何参与可参见 [Contributing]() diff --git a/README_zh.md b/README_zh.md index 021653427..477ca58a8 100644 --- a/README_zh.md +++ b/README_zh.md @@ -42,7 +42,7 @@ datart 是新一代数据可视化开放平台,支持各类企业数据可视 参见 [User Guide](http://running-elephant.gitee.io/datart-docs/docs/source.html) ### 最新版本 Latest Release -参见 [Latest Release](https://gitee.com/running-elephant/datart/releases/1.0.0-alpha.3) +参见 [Latest Release](https://gitee.com/running-elephant/datart/releases) ## Community ### 社区支持 Support diff --git a/bin/h2/datart.demo.mv.db b/bin/h2/datart.demo.mv.db index de2611c3d..4eec49f80 100644 Binary files a/bin/h2/datart.demo.mv.db and b/bin/h2/datart.demo.mv.db differ diff --git a/bin/migrations/migration.1.0.0-beta.0.sql b/bin/migrations/migration.1.0.0-beta.0.sql new file mode 100644 index 000000000..b6b0b9374 --- /dev/null +++ b/bin/migrations/migration.1.0.0-beta.0.sql @@ -0,0 +1,2 @@ +ALTER TABLE `datachart` + MODIFY COLUMN `config` mediumtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL AFTER `org_id`; \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index 08241e2d0..11f2acdeb 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -5,7 +5,7 @@ datart-parent datart - 1.0.0-alpha.3 + 1.0.0-beta.0 4.0.0 @@ -157,39 +157,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - org.mybatis.generator - mybatis-generator-maven-plugin - 1.4.0 + org.apache.maven.plugins + maven-compiler-plugin - src/main/resources/mybatis-generator/generatorConfig.xml - true - true - true + true - - - Generate MyBatis Artifacts - - generate - - install - - - - - mysql - mysql-connector-java - 5.1.49 - - - datart - datart-core - ${project.version} - - - 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 b4f6f2b18..708b92985 100644 --- a/core/src/main/java/datart/core/base/consts/Const.java +++ b/core/src/main/java/datart/core/base/consts/Const.java @@ -33,7 +33,7 @@ public class Const { * 正则表达式 */ - public static final String REG_EMAIL = "^[a-z_0-9.-]{1,64}@([a-z0-9-]{1,200}.){1,5}[a-z]{1,6}$"; + public static final String REG_EMAIL = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"; public static final String REG_USER_PASSWORD = ".{6,20}"; @@ -45,7 +45,8 @@ public class Const { */ //默认的变量引用符号 public static final String DEFAULT_VARIABLE_QUOTE = "$"; - + //变量匹配符 + public static final String VARIABLE_EXP = "\\$\\w+\\$"; /** * 权限变量 */ diff --git a/core/src/main/java/datart/core/common/CSVParse.java b/core/src/main/java/datart/core/common/CSVParse.java index 6a63639d1..11e41058e 100644 --- a/core/src/main/java/datart/core/common/CSVParse.java +++ b/core/src/main/java/datart/core/common/CSVParse.java @@ -17,9 +17,7 @@ */ package datart.core.common; -import datart.core.base.consts.Const; import datart.core.base.consts.ValueType; -import datart.core.base.exception.BaseException; import datart.core.base.exception.Exceptions; import lombok.Data; import org.apache.commons.csv.CSVFormat; @@ -31,7 +29,6 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.LinkedList; @@ -40,16 +37,8 @@ public class CSVParse { - public static final ParseConfig DEFAULT_CONFIG = new ParseConfig(); - - static { - DEFAULT_CONFIG.setDateFormat(Const.DEFAULT_DATE_FORMAT); - } - private String path; - private ParseConfig parseConfig; - private ValueType[] types; private SimpleDateFormat simpleDateFormat; @@ -57,7 +46,6 @@ public class CSVParse { public static CSVParse create(String path, ParseConfig parseConfig) { CSVParse csvParse = new CSVParse(); csvParse.path = path; - csvParse.parseConfig = parseConfig; csvParse.simpleDateFormat = new SimpleDateFormat(parseConfig.getDateFormat()); return csvParse; } @@ -65,7 +53,6 @@ public static CSVParse create(String path, ParseConfig parseConfig) { public static CSVParse create(String path) { CSVParse csvParse = new CSVParse(); csvParse.path = path; - csvParse.parseConfig = DEFAULT_CONFIG; return csvParse; } @@ -111,32 +98,11 @@ private List extractValues(CSVRecord record) { } LinkedList values = new LinkedList<>(); for (int i = 0; i < record.size(); i++) { - Object val; - try { - val = parseValue(record.get(i), types[i]); - } catch (Exception e) { - val = record.get(i); - } - values.add(val); + values.add(record.get(i)); } return values; } - private Object parseValue(String val, ValueType valueType) throws ParseException { - switch (valueType) { - case DATE: - return simpleDateFormat.parse(val); - case NUMERIC: - if (NumberUtils.isDigits(val)) { - return Long.parseLong(val); - } else { - return Double.parseDouble(val); - } - default: - return val; - } - } - @Data public static class ParseConfig { private String dateFormat; diff --git a/core/src/main/java/datart/core/common/CacheFactory.java b/core/src/main/java/datart/core/common/CacheFactory.java index b2269d951..021f85b4e 100644 --- a/core/src/main/java/datart/core/common/CacheFactory.java +++ b/core/src/main/java/datart/core/common/CacheFactory.java @@ -1,12 +1,15 @@ package datart.core.common; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; @Slf4j public class CacheFactory { private static final String CACHE_IMPL_CLASS_NAME = "cacheImpl"; + private static final String DEFAULT_CACHE = "datart.server.service.impl.RedisCacheImpl"; + private static Cache cache; public static Cache getCache() { @@ -15,6 +18,9 @@ public static Cache getCache() { } try { String className = Application.getProperty(CACHE_IMPL_CLASS_NAME); + if (StringUtils.isBlank(className)) { + className = DEFAULT_CACHE; + } cache = (Cache) Application.getBean(Class.forName(className)); return cache; } catch (Exception e) { diff --git a/core/src/main/java/datart/core/common/DateUtils.java b/core/src/main/java/datart/core/common/DateUtils.java new file mode 100644 index 000000000..e354241e2 --- /dev/null +++ b/core/src/main/java/datart/core/common/DateUtils.java @@ -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. + */ + +package datart.core.common; + +public class DateUtils { + + private static final String[] FMT = {"y", "M", "d", "H", "m", "s", "S"}; + + public static String inferDateFormat(String src) { + int fmtIdx = 0; + boolean findMatch = false; + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < src.length(); i++) { + char chr = src.charAt(i); + if (Character.isDigit(chr)) { + findMatch = true; + stringBuilder.append(FMT[fmtIdx]); + } else { + if (findMatch) { + fmtIdx++; + findMatch = false; + } + stringBuilder.append(chr); + } + if (fmtIdx == FMT.length - 1 && i < src.length() - 1) { + stringBuilder.append(src.substring(i + 1)); + break; + } + } + return stringBuilder.toString(); + } + +} diff --git a/core/src/main/java/datart/core/common/JavascriptUtils.java b/core/src/main/java/datart/core/common/JavascriptUtils.java index 8eb370d56..89e2f6b9c 100644 --- a/core/src/main/java/datart/core/common/JavascriptUtils.java +++ b/core/src/main/java/datart/core/common/JavascriptUtils.java @@ -18,13 +18,14 @@ package datart.core.common; -import datart.core.base.exception.BaseException; import datart.core.base.exception.Exceptions; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; +import javax.script.ScriptException; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -36,7 +37,14 @@ public class JavascriptUtils { engineFactory = new NashornScriptEngineFactory(); } - public static Object invoke(String path, String functionName, Object... args) throws Exception { + public static Object invoke(Invocable invocable, String functionName, Object... args) throws Exception { + if (invocable != null) { + return invocable.invokeFunction(functionName, args); + } + return null; + } + + public static Invocable load(String path) throws IOException, ScriptException { InputStream stream = JavascriptUtils.class.getClassLoader().getResourceAsStream(path); if (stream == null) { Exceptions.notFound(path); @@ -45,10 +53,10 @@ public static Object invoke(String path, String functionName, Object... args) th ScriptEngine engine = engineFactory.getScriptEngine(); engine.eval(reader); if (engine instanceof Invocable) { - Invocable invocable = (Invocable) engine; - return invocable.invokeFunction(functionName, args); + return (Invocable) engine; } return null; } } + } diff --git a/core/src/main/java/datart/core/common/WebUtils.java b/core/src/main/java/datart/core/common/WebUtils.java index 35a9355d1..3ec3bafde 100644 --- a/core/src/main/java/datart/core/common/WebUtils.java +++ b/core/src/main/java/datart/core/common/WebUtils.java @@ -70,19 +70,19 @@ public static T screenShot(String url, OutputType outputType, int imageWi ExpectedCondition ConditionOfHeight = ExpectedConditions.presenceOfElementLocated(By.id("height")); wait.until(ExpectedConditions.and(ConditionOfSign, ConditionOfWidth, ConditionOfHeight)); - int contentWidth = Integer.parseInt(webDriver.findElement(By.id("width")).getAttribute("value")); + Double contentWidth = Double.parseDouble(webDriver.findElement(By.id("width")).getAttribute("value")); - int contentHeight = Integer.parseInt(webDriver.findElement(By.id("height")).getAttribute("value")); + Double contentHeight = Double.parseDouble(webDriver.findElement(By.id("height")).getAttribute("value")); if (imageWidth != contentWidth) { // scale the window - webDriver.manage().window().setSize(new Dimension(imageWidth, contentHeight)); + webDriver.manage().window().setSize(new Dimension(imageWidth, contentHeight.intValue())); Thread.sleep(1000); } // scale the window again - contentWidth = Integer.parseInt(webDriver.findElement(By.id("width")).getAttribute("value")); - contentHeight = Integer.parseInt(webDriver.findElement(By.id("height")).getAttribute("value")); - webDriver.manage().window().setSize(new Dimension(contentWidth, contentHeight)); + contentWidth = Double.parseDouble(webDriver.findElement(By.id("width")).getAttribute("value")); + contentHeight = Double.parseDouble(webDriver.findElement(By.id("height")).getAttribute("value")); + webDriver.manage().window().setSize(new Dimension(contentWidth.intValue(), contentHeight.intValue())); Thread.sleep(1000); TakesScreenshot screenshot = (TakesScreenshot) webDriver; 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 bbfa6fb7a..7de431686 100644 --- a/core/src/main/java/datart/core/data/provider/Column.java +++ b/core/src/main/java/datart/core/data/provider/Column.java @@ -29,6 +29,8 @@ public class Column implements Serializable { private String name; private ValueType type; + + private String fmt; public Column(String name, ValueType type) { this.name = name; diff --git a/core/src/main/java/datart/core/data/provider/DataProvider.java b/core/src/main/java/datart/core/data/provider/DataProvider.java index 0fd475333..4bd59da3e 100644 --- a/core/src/main/java/datart/core/data/provider/DataProvider.java +++ b/core/src/main/java/datart/core/data/provider/DataProvider.java @@ -26,7 +26,6 @@ import java.io.InputStream; import java.sql.SQLException; import java.util.Collections; -import java.util.List; import java.util.Set; public abstract class DataProvider extends AutoCloseBean { @@ -76,6 +75,9 @@ public DataProviderConfigTemplate getConfigTemplate() throws IOException { } } + public abstract String getConfigDisplayName(String name); + + public abstract String getConfigDescription(String name); public abstract Dataframe execute(DataProviderSource config, QueryScript script, ExecuteParam executeParam) throws Exception; @@ -119,4 +121,5 @@ public Set supportedStdFunctions(DataProviderSource source) { public void resetSource(DataProviderSource source) { } + } \ No newline at end of file diff --git a/core/src/main/java/datart/core/data/provider/DataProviderConfigTemplate.java b/core/src/main/java/datart/core/data/provider/DataProviderConfigTemplate.java index 9fd98f7fb..01b60dcdc 100644 --- a/core/src/main/java/datart/core/data/provider/DataProviderConfigTemplate.java +++ b/core/src/main/java/datart/core/data/provider/DataProviderConfigTemplate.java @@ -29,6 +29,8 @@ public class DataProviderConfigTemplate implements Serializable { private String name; + private String displayName; + private List attributes; @Data @@ -52,7 +54,7 @@ public static class Attribute implements Serializable { private List options; - private Object children; + private List children; } diff --git a/core/src/main/java/datart/core/data/provider/QueryScript.java b/core/src/main/java/datart/core/data/provider/QueryScript.java index a99dd530f..4d7ba6ccf 100644 --- a/core/src/main/java/datart/core/data/provider/QueryScript.java +++ b/core/src/main/java/datart/core/data/provider/QueryScript.java @@ -45,11 +45,10 @@ public class QueryScript implements Serializable { private List variables; - private Map schema; + private Map schema; public String toQueryKey() { return 'Q' + DigestUtils.md5Hex(JSON.toJSONString(this)); } - } \ No newline at end of file diff --git a/core/src/main/java/datart/core/data/provider/ScriptVariable.java b/core/src/main/java/datart/core/data/provider/ScriptVariable.java index 115055757..8727aa158 100644 --- a/core/src/main/java/datart/core/data/provider/ScriptVariable.java +++ b/core/src/main/java/datart/core/data/provider/ScriptVariable.java @@ -18,10 +18,12 @@ package datart.core.data.provider; +import datart.core.base.consts.Const; import datart.core.base.consts.ValueType; import datart.core.base.consts.VariableTypeEnum; import lombok.Data; import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.StringUtils; import java.util.Set; @@ -35,6 +37,8 @@ public class ScriptVariable extends TypedValue { private Set values; + private String nameWithQuote; + private boolean expression; @Override @@ -56,4 +60,12 @@ public ScriptVariable(String name, VariableTypeEnum type, ValueType valueType, S this.expression = expression; } + public String getNameWithQuote() { + if (nameWithQuote != null) { + return nameWithQuote; + } + nameWithQuote = StringUtils.prependIfMissing(name, Const.DEFAULT_VARIABLE_QUOTE); + nameWithQuote = StringUtils.appendIfMissing(nameWithQuote, Const.DEFAULT_VARIABLE_QUOTE); + return nameWithQuote; + } } \ No newline at end of file diff --git a/core/src/main/java/datart/core/data/provider/SingleTypedValue.java b/core/src/main/java/datart/core/data/provider/SingleTypedValue.java index 8659e5ac0..5cb9dcfe0 100644 --- a/core/src/main/java/datart/core/data/provider/SingleTypedValue.java +++ b/core/src/main/java/datart/core/data/provider/SingleTypedValue.java @@ -20,8 +20,10 @@ import datart.core.base.consts.ValueType; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor public class SingleTypedValue extends TypedValue { private Object value; diff --git a/core/src/main/java/datart/core/mappers/ext/RelRoleResourceMapperExt.java b/core/src/main/java/datart/core/mappers/ext/RelRoleResourceMapperExt.java index 99f66cea8..38aa9a208 100644 --- a/core/src/main/java/datart/core/mappers/ext/RelRoleResourceMapperExt.java +++ b/core/src/main/java/datart/core/mappers/ext/RelRoleResourceMapperExt.java @@ -63,7 +63,7 @@ public interface RelRoleResourceMapperExt extends RelRoleResourceMapper { @Select({ "" }) - int countUserPermission(String resourceId, String userId); + int countRolePermission(String resourceId, String roleId); @Select({ "", }) int batchInsert(List elements); + + @Delete({ + "DELETE FROM variable where view_id=#{viewId}" + }) + int deleteByView(String viewId); } diff --git a/core/src/main/resources/mybatis-generator/generatorConfig.xml b/core/src/main/resources/mybatis-generator/generatorConfig.xml index fde6c3d08..7069717e1 100644 --- a/core/src/main/resources/mybatis-generator/generatorConfig.xml +++ b/core/src/main/resources/mybatis-generator/generatorConfig.xml @@ -17,8 +17,8 @@ + connectionURL="jdbc:mysql://127.0.0.1:3306/datart" userId="root" + password=""> diff --git a/data-providers/file-data-provider/pom.xml b/data-providers/file-data-provider/pom.xml index e3e1d310e..18ee29208 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-alpha.3 + 1.0.0-beta.0 ../../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 c3fb8b576..8b11daae1 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 @@ -21,10 +21,7 @@ import datart.core.base.consts.ValueType; import datart.core.base.exception.BaseException; import datart.core.base.exception.Exceptions; -import datart.core.common.CSVParse; -import datart.core.common.FileUtils; -import datart.core.common.POIUtils; -import datart.core.common.UUIDGenerator; +import datart.core.common.*; import datart.core.data.provider.Column; import datart.core.data.provider.DataProviderSource; import datart.core.data.provider.Dataframe; @@ -46,6 +43,23 @@ public class FileDataProvider extends DefaultDataProvider { public static final String FILE_PATH = "path"; + private static final String I18N_PREFIX = "config.template.file."; + + @Override + public String getConfigDisplayName(String name) { + return MessageResolver.getMessage(I18N_PREFIX + name); + } + + @Override + public String getConfigDescription(String name) { + String message = MessageResolver.getMessage(I18N_PREFIX + name + ".desc"); + if (message.startsWith(I18N_PREFIX)) { + return null; + } else { + return message; + } + } + @Override public List loadFullDataFromSource(DataProviderSource config) throws Exception { Map properties = config.getProperties(); diff --git a/data-providers/file-data-provider/src/main/resources/file-data-provider.json b/data-providers/file-data-provider/src/main/resources/file-data-provider.json index b468bd49b..33a0530bc 100644 --- a/data-providers/file-data-provider/src/main/resources/file-data-provider.json +++ b/data-providers/file-data-provider/src/main/resources/file-data-provider.json @@ -20,19 +20,11 @@ "required": true, "defaultValue": "", "type": "string", - "description": "文件格式,目前支持excel 和 csv。", "options": [ "XLSX", "CSV" ] }, - { - "name": "path", - "required": true, - "defaultValue": "", - "type": "string", - "description": "文件路径,上传后自动生成" - }, { "name": "columns", "defaultValue": "", @@ -44,15 +36,13 @@ "name": "cacheEnable", "required": false, "defaultValue": true, - "type": "bool", - "description": "是否开启本地缓存。开启后,文件解析结果将被缓存。" + "type": "bool" }, { "name": "cacheTimeout", "required": false, "type": "string", - "defaultValue": "30", - "description": "缓存超时时间(分钟)" + "defaultValue": "30" } ] } \ No newline at end of file diff --git a/data-providers/http-data-provider/pom.xml b/data-providers/http-data-provider/pom.xml index ccbd396f7..5a564262b 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-alpha.3 + 1.0.0-beta.0 ../../pom.xml diff --git a/data-providers/http-data-provider/src/main/java/datart/data/provider/HttpDataProvider.java b/data-providers/http-data-provider/src/main/java/datart/data/provider/HttpDataProvider.java index 22933a477..1e55b0a27 100644 --- a/data-providers/http-data-provider/src/main/java/datart/data/provider/HttpDataProvider.java +++ b/data-providers/http-data-provider/src/main/java/datart/data/provider/HttpDataProvider.java @@ -20,7 +20,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import datart.core.common.MessageResolver; import datart.core.common.UUIDGenerator; +import datart.core.data.provider.DataProviderConfigTemplate; import datart.core.data.provider.DataProviderSource; import datart.core.data.provider.Dataframe; import lombok.extern.slf4j.Slf4j; @@ -29,11 +31,9 @@ import org.springframework.util.CollectionUtils; import java.io.IOException; +import java.io.InputStream; import java.net.URISyntaxException; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; @Slf4j public class HttpDataProvider extends DefaultDataProvider { @@ -64,6 +64,8 @@ public class HttpDataProvider extends DefaultDataProvider { private static final String CONTENT_TYPE = "contentType"; + private static final String I18N_PREFIX = "config.template.http."; + private final static ObjectMapper MAPPER; static { @@ -102,6 +104,21 @@ public String getConfigFile() { return "http-data-provider.json"; } + @Override + public String getConfigDisplayName(String name) { + return MessageResolver.getMessage(I18N_PREFIX + name); + } + + @Override + public String getConfigDescription(String name) { + String message = MessageResolver.getMessage(I18N_PREFIX + name + ".desc"); + if (message.startsWith(I18N_PREFIX)) { + return null; + } else { + return message; + } + } + private HttpRequestParam convert2RequestParam(Map schema) throws ClassNotFoundException { HttpRequestParam httpRequestParam = new HttpRequestParam(); diff --git a/data-providers/http-data-provider/src/main/resources/http-data-provider.json b/data-providers/http-data-provider/src/main/resources/http-data-provider.json index a589800a5..65f65bd24 100644 --- a/data-providers/http-data-provider/src/main/resources/http-data-provider.json +++ b/data-providers/http-data-provider/src/main/resources/http-data-provider.json @@ -29,7 +29,6 @@ { "name": "property", "defaultValue": "", - "description": "Http返回结果中,JSON数组的属性名称。嵌套结构用 . 隔开。如 data.list", "type": "string" }, { @@ -50,13 +49,11 @@ { "name": "timeout", "defaultValue": 0, - "description": "请求超时时间", "type": "number" }, { "name": "responseParser", "defaultValue": "", - "description": "请求结果解析器,自定义解析器时,指定解析器的全类名", "type": "string" }, { @@ -82,15 +79,13 @@ "name": "cacheEnable", "required": false, "defaultValue": true, - "type": "bool", - "description": "是否开启本地缓存。开启后,HTTP请求结果将会缓存到服务端。" + "type": "bool" }, { "name": "cacheTimeout", "required": false, "type": "string", - "defaultValue": "5", - "description": "缓存超时时间(分钟)" + "defaultValue": "5" } ] } \ No newline at end of file diff --git a/data-providers/jdbc-data-provider/pom.xml b/data-providers/jdbc-data-provider/pom.xml index 79328b2b5..8e5bdd357 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-alpha.3 + 1.0.0-beta.0 ../../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 e0f83fd54..239141e99 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 @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy; import datart.core.base.exception.Exceptions; import datart.core.common.FileUtils; +import datart.core.common.MessageResolver; import datart.core.data.provider.*; import datart.data.provider.base.DataProviderException; import datart.data.provider.jdbc.JdbcDriverInfo; @@ -48,6 +49,8 @@ public class JdbcDataProvider extends DataProvider { public static final String DRIVER_CLASS = "driverClass"; + private static final String I18N_PREFIX = "config.template.jdbc."; + /** * 获取连接时最大等待时间(毫秒) */ @@ -157,6 +160,7 @@ public boolean validateFunction(DataProviderSource source, String snippet) { public DataProviderConfigTemplate getConfigTemplate() throws IOException { DataProviderConfigTemplate configTemplate = super.getConfigTemplate(); for (DataProviderConfigTemplate.Attribute attribute : configTemplate.getAttributes()) { + attribute.setDisplayName(MessageResolver.getMessage("config.template.jdbc." + attribute.getName())); if (attribute.getName().equals("dbType")) { List jdbcDriverInfos = ProviderFactory.loadDriverInfoFromResource(); List dbInfos = jdbcDriverInfos.stream().map(info -> { @@ -172,6 +176,21 @@ public DataProviderConfigTemplate getConfigTemplate() throws IOException { return configTemplate; } + @Override + public String getConfigDisplayName(String name) { + return MessageResolver.getMessage(I18N_PREFIX + name); + } + + @Override + public String getConfigDescription(String name) { + String message = MessageResolver.getMessage(I18N_PREFIX + name + ".desc"); + if (message.startsWith(I18N_PREFIX)) { + return null; + } else { + return message; + } + } + @Override public void close() throws IOException { 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 00c200fc9..791b17d7e 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 @@ -46,7 +46,6 @@ public void destroy(DataSource dataSource) { private Properties configDataSource(JdbcProperties properties) { Properties pro = new Properties(); - //connect params pro.setProperty(DruidDataSourceFactory.PROP_DRIVERCLASSNAME, properties.getDriverClass()); pro.setProperty(DruidDataSourceFactory.PROP_URL, properties.getUrl()); 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 fe76b5a76..61340240f 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 @@ -199,7 +199,7 @@ protected Dataframe execute(String selectSql, PageInfo pageInfo) throws SQLExcep public Dataframe execute(QueryScript script, ExecuteParam executeParam) throws Exception { //If server aggregation is enabled, query the full data before performing server aggregation if (executeParam.isServerAggregate()) { - return executeLocally(script, executeParam); + return executeInLocal(script, executeParam); } else { return executeOnSource(script, executeParam); } @@ -240,7 +240,6 @@ public SqlDialect getSqlDialect() { if (sqlDialect != null) { return sqlDialect; } - if (StringUtils.isNotBlank(driverInfo.getSqlDialect())) { try { Class> clz = Class.forName(driverInfo.getSqlDialect()); @@ -305,7 +304,7 @@ protected List getColumns(ResultSet rs) throws SQLException { /** * 本地执行,从数据源拉取全量数据,在本地执行聚合操作 */ - protected Dataframe executeLocally(QueryScript script, ExecuteParam executeParam) throws Exception { + protected Dataframe executeInLocal(QueryScript script, ExecuteParam executeParam) throws Exception { SqlScriptRender render = new SqlScriptRender(script , executeParam , getSqlDialect() 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 7d5bdce45..6595cea12 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 @@ -1,28 +1,19 @@ { "type": "JDBC", "name": "jdbc-data-provider", - "syntax": { - "name": "SQL", - "keywords": [ - "SELECT", - "FROM", - "JOIN" - ] - }, "attributes": [ { + "code": "dbType", "name": "dbType", "type": "string", "required": true, - "defaultValue": "", - "description": "database type" + "defaultValue": "" }, { "name": "url", "type": "string", "required": true, - "defaultValue": "", - "description": "connect url" + "defaultValue": "" }, { "name": "user", @@ -45,15 +36,13 @@ "name": "serverAggregate", "type": "bool", "required": false, - "defaultValue": false, - "description": "是否开启服务端聚合" + "defaultValue": false }, { "name": "properties", "type": "object", "required": false, - "defaultValue": "", - "description": "Druid连接池其它配置参数" + "defaultValue": "" } ] } \ No newline at end of file diff --git a/data-providers/pom.xml b/data-providers/pom.xml index 70030b755..950981ee4 100644 --- a/data-providers/pom.xml +++ b/data-providers/pom.xml @@ -5,7 +5,7 @@ datart-parent datart - 1.0.0-alpha.3 + 1.0.0-beta.0 4.0.0 diff --git a/data-providers/src/main/java/codegen/Parser.jj b/data-providers/src/main/java/codegen/Parser.jj index c9090f9a5..99043f31a 100644 --- a/data-providers/src/main/java/codegen/Parser.jj +++ b/data-providers/src/main/java/codegen/Parser.jj @@ -1847,9 +1847,9 @@ SqlLiteral JoinType() : | { joinType = JoinType.INNER; } | - [ ] { joinType = JoinType.LEFT; } + [ || ] { joinType = JoinType.LEFT; } | - [ ] { joinType = JoinType.RIGHT; } + [ || ] { joinType = JoinType.RIGHT; } | [ ] { joinType = JoinType.FULL; } | @@ -2071,6 +2071,10 @@ SqlNode TableRef2(boolean lateral) : } { ( + // datart: function as table + LOOKAHEAD(256,TableFunctionCall(getPos())) + tableRef = TableFunctionCall(getPos()) + | LOOKAHEAD(2) tableRef = TableRefWithHintsOpt() [ @@ -2133,6 +2137,7 @@ SqlNode TableRef2(boolean lateral) : } | tableRef = ExtendedTableRef() + ) [ tableRef = Pivot(tableRef) 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 cdcc488bb..5c6083ae5 100644 --- a/data-providers/src/main/java/datart/data/provider/DefaultDataProvider.java +++ b/data-providers/src/main/java/datart/data/provider/DefaultDataProvider.java @@ -20,17 +20,20 @@ import datart.core.base.PageInfo; import datart.core.base.consts.ValueType; import datart.core.base.exception.Exceptions; +import datart.core.common.DateUtils; import datart.core.data.provider.*; -import datart.data.provider.base.DataProviderException; import datart.data.provider.calcite.SqlParserUtils; import datart.data.provider.local.LocalDB; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateParser; +import org.apache.commons.lang3.time.FastDateFormat; import org.springframework.util.CollectionUtils; import java.io.IOException; import java.sql.SQLException; +import java.text.ParseException; import java.util.*; import java.util.stream.Collectors; @@ -195,9 +198,9 @@ protected List> parseValues(List> values, List return values; } if (values.get(0).size() != columns.size()) { - Exceptions.msg( "message.provider.default.schema", values.get(0).size() + ":" + columns.size()); + Exceptions.msg("message.provider.default.schema", values.get(0).size() + ":" + columns.size()); } - values.parallelStream().forEach(vals -> { + values.stream().forEach(vals -> { for (int i = 0; i < vals.size(); i++) { Object val = vals.get(i); if (val == null) { @@ -216,10 +219,28 @@ protected List> parseValues(List> values, List val = null; } else if (NumberUtils.isDigits(val.toString())) { val = Long.parseLong(val.toString()); - } else { + } else if (NumberUtils.isNumber(val.toString())) { val = Double.parseDouble(val.toString()); + } else { + val = null; } break; + case DATE: + String fmt = columns.get(i).getFmt(); + if (StringUtils.isBlank(fmt)) { + fmt = DateUtils.inferDateFormat(val.toString()); + columns.get(i).setFmt(fmt); + } + if (StringUtils.isNotBlank(fmt)) { + DateParser parser = FastDateFormat.getInstance(fmt); + try { + val = parser.parse(val.toString()); + } catch (ParseException e) { + val = null; + } + } else { + val = null; + } default: } vals.set(i, val); diff --git a/data-providers/src/main/java/datart/data/provider/ProviderManager.java b/data-providers/src/main/java/datart/data/provider/ProviderManager.java index 1d49e8768..c8ee2673c 100644 --- a/data-providers/src/main/java/datart/data/provider/ProviderManager.java +++ b/data-providers/src/main/java/datart/data/provider/ProviderManager.java @@ -19,6 +19,7 @@ package datart.data.provider; import datart.core.base.exception.Exceptions; +import datart.core.common.MessageResolver; import datart.core.data.provider.*; import datart.data.provider.optimize.DataProviderExecuteOptimizer; import lombok.extern.slf4j.Slf4j; @@ -53,7 +54,21 @@ public List getSupportedDataProviders() { @Override public DataProviderConfigTemplate getSourceConfigTemplate(String type) throws IOException { - return getDataProviderService(type).getConfigTemplate(); + DataProvider providerService = getDataProviderService(type); + DataProviderConfigTemplate configTemplate = providerService.getConfigTemplate(); + if (!CollectionUtils.isEmpty(configTemplate.getAttributes())) { + for (DataProviderConfigTemplate.Attribute attribute : configTemplate.getAttributes()) { + attribute.setDisplayName(providerService.getConfigDisplayName(attribute.getName())); + attribute.setDescription(providerService.getConfigDescription(attribute.getName())); + if (!CollectionUtils.isEmpty(attribute.getChildren())) { + for (DataProviderConfigTemplate.Attribute child : attribute.getChildren()) { + child.setDisplayName(providerService.getConfigDisplayName(child.getName())); + child.setDescription(providerService.getConfigDescription(child.getName())); + } + } + } + } + return configTemplate; } @Override @@ -120,36 +135,28 @@ public void updateSource(DataProviderSource source) { providerService.resetSource(source); } - private void excludeColumns(Dataframe data, Set columns) { + private void excludeColumns(Dataframe data, Set include) { if (data == null || CollectionUtils.isEmpty(data.getColumns()) - || columns == null - || columns.size() == 0 - || columns.contains("*")) { + || include == null + || include.size() == 0 + || include.contains("*")) { return; } List excludeIndex = new LinkedList<>(); for (int i = 0; i < data.getColumns().size(); i++) { Column column = data.getColumns().get(i); - if (!columns.contains(column.getName())) { + if (!include.contains(column.getName())) { excludeIndex.add(i); - data.getColumns().remove(column); } } if (excludeIndex.size() > 0) { - List> rows = data.getRows().parallelStream().map(row -> { - List r = new LinkedList<>(); - for (int i = 0; i < row.size(); i++) { - if (excludeIndex.size() > 0 && i == excludeIndex.get(0)) { - excludeIndex.remove(0); - } else { - r.add(row.get(i)); - } + data.getRows().parallelStream().forEach(row -> { + for (Integer index : excludeIndex) { + row.set(index, null); } - return r; - }).collect(Collectors.toList()); - data.setRows(rows); + }); } } 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 e37c0d231..aae4682e0 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 @@ -18,11 +18,12 @@ package datart.data.provider.calcite; -import org.apache.calcite.sql.SqlCall; -import org.apache.calcite.sql.SqlFunction; -import org.apache.calcite.sql.SqlOperator; +import org.apache.calcite.sql.*; import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.apache.calcite.sql.util.SqlBasicVisitor; +import org.apache.calcite.sql.validate.SqlNameMatchers; + +import java.util.LinkedList; public class SqlFunctionRegisterVisitor extends SqlBasicVisitor { @@ -30,13 +31,20 @@ public class SqlFunctionRegisterVisitor extends SqlBasicVisitor { public Object visit(SqlCall call) { SqlOperator operator = call.getOperator(); if (operator instanceof SqlFunction) { - registerIfNotExists(operator); + registerIfNotExists((SqlFunction) operator); } return operator.acceptCall(this, call); } - private void registerIfNotExists(SqlOperator sqlFunction) { - SqlStdOperatorTable.instance().register(sqlFunction); + private void registerIfNotExists(SqlFunction sqlFunction) { + SqlStdOperatorTable opTab = SqlStdOperatorTable.instance(); + LinkedList list = new LinkedList<>(); + opTab.lookupOperatorOverloads(sqlFunction.getSqlIdentifier(), null, SqlSyntax.FUNCTION, list, + SqlNameMatchers.withCaseSensitive(sqlFunction.getSqlIdentifier().isComponentQuoted(0))); + if (list.size() > 0) { + return; + } + opTab.register(sqlFunction); } } 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 0118b8c40..dc21af313 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 @@ -38,9 +38,7 @@ public static boolean validateQuery(SqlNode sqlCall) { return true; } - if (sqlCall instanceof SqlDdl || sqlCall instanceof SqlDelete || sqlCall instanceof SqlUpdate) { - Exceptions.tr(DataProviderException.class, "message.sql.op.forbidden", sqlCall.getKind() + ":" + sqlCall); - } + Exceptions.tr(DataProviderException.class, "message.sql.op.forbidden", sqlCall.getKind() + ":" + sqlCall); return false; } diff --git a/data-providers/src/main/java/datart/data/provider/calcite/SqlVariableVisitor.java b/data-providers/src/main/java/datart/data/provider/calcite/SqlVariableVisitor.java index c55cc8a93..d9f3e5d29 100644 --- a/data-providers/src/main/java/datart/data/provider/calcite/SqlVariableVisitor.java +++ b/data-providers/src/main/java/datart/data/provider/calcite/SqlVariableVisitor.java @@ -107,7 +107,6 @@ private VariablePlaceholder createVariablePlaceholder(SqlCall sqlCall, String va return new TrueVariablePlaceholder(originalSqlFragment); } - variable.setName(variableName); if (VariableTypeEnum.PERMISSION.equals(variable.getType())) { return new PermissionVariablePlaceholder(variable, sqlDialect, sqlCall, originalSqlFragment); } else { diff --git a/data-providers/src/main/java/datart/data/provider/calcite/dialect/ImpalaSqlDialectSupport.java b/data-providers/src/main/java/datart/data/provider/calcite/dialect/ImpalaSqlDialectSupport.java index b9d8a735e..7ebf4d4df 100644 --- a/data-providers/src/main/java/datart/data/provider/calcite/dialect/ImpalaSqlDialectSupport.java +++ b/data-providers/src/main/java/datart/data/provider/calcite/dialect/ImpalaSqlDialectSupport.java @@ -19,6 +19,7 @@ package datart.data.provider.calcite.dialect; import datart.data.provider.jdbc.JdbcDriverInfo; +import org.apache.calcite.sql.SqlAbstractDateTimeLiteral; import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.SqlWriter; @@ -32,4 +33,9 @@ public ImpalaSqlDialectSupport(JdbcDriverInfo driverInfo) { public void unparseOffsetFetch(SqlWriter writer, SqlNode offset, SqlNode fetch) { super.unparseFetchUsingLimit(writer, offset, fetch); } + + @Override + public void unparseDateTimeLiteral(SqlWriter writer, SqlAbstractDateTimeLiteral literal, int leftPrec, int rightPrec) { + writer.literal("'" + literal.toFormattedString() + "'"); + } } 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 ff0ca8a88..49309352d 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,7 +29,6 @@ import datart.data.provider.calcite.SqlValidateUtils; import datart.data.provider.calcite.SqlParserUtils; import datart.data.provider.calcite.SqlVariableVisitor; -import datart.data.provider.calcite.parser.impl.SqlParserImpl; import datart.data.provider.freemarker.FreemarkerContext; import datart.data.provider.local.LocalDB; import datart.data.provider.script.ReplacementPair; @@ -37,12 +36,9 @@ import datart.data.provider.script.VariablePlaceholder; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; -import org.apache.calcite.config.Lex; import org.apache.calcite.sql.SqlDialect; import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.parser.SqlParseException; -import org.apache.calcite.sql.parser.SqlParser; -import org.apache.calcite.sql.validate.SqlConformanceEnum; import org.apache.commons.lang3.CharUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.util.CollectionUtils; @@ -85,7 +81,7 @@ public String replaceVariables(String selectSql) { Map variableMap = queryScript.getVariables() .stream() - .collect(Collectors.toMap(v -> getVariablePattern(v.getName()), variable -> variable)); + .collect(Collectors.toMap(ScriptVariable::getNameWithQuote, variable -> variable)); String srcSql = selectSql; SqlNode sqlNode = null; try { @@ -128,15 +124,14 @@ public String render(boolean withExecuteParam, boolean withPage, boolean onlySel if (size != 1) { Exceptions.tr(DataProviderException.class, "message.provider.variable.expression.size", size + ":" + variable.getValues()); } - script = script.replace(getVariablePattern(variable.getName()), Iterables.get(variable.getValues(), 0)); + script = script.replace(variable.getNameWithQuote(), Iterables.get(variable.getValues(), 0)); } } - // find select sql final String selectSql0 = findSelectSql(script); if (StringUtils.isEmpty(selectSql0)) { - Exceptions.tr(DataProviderException.class,"message.no.valid.sql"); + Exceptions.tr(DataProviderException.class, "message.no.valid.sql"); } String selectSql = cleanupSql(selectSql0); @@ -177,14 +172,6 @@ private String findSelectSql(String script) { return selectSql; } - private SqlParser sqlParser() { - SqlParser.Config config = SqlParser.config() - .withLex(Lex.MYSQL) - .withParserFactory(SqlParserImpl.FACTORY) - .withConformance(SqlConformanceEnum.LENIENT); - return SqlParser.create("", config); - } - private SqlNode parseSql(String sql) throws SqlParseException { return SqlParserUtils.createParser(sql, sqlDialect).parseQuery(); } 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 549c77fe9..13e6840d4 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 @@ -17,7 +17,6 @@ */ package datart.data.provider.local; -import com.google.common.collect.Lists; import datart.core.base.PageInfo; import datart.core.base.consts.Const; import datart.core.base.exception.Exceptions; @@ -34,24 +33,22 @@ import org.apache.calcite.sql.SqlDialect; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateFormatUtils; +import org.h2.jdbc.JdbcSQLNonTransientException; import org.h2.tools.DeleteDbFiles; import org.h2.tools.SimpleResultSet; import java.sql.*; -import java.sql.Date; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; @Slf4j public class LocalDB { - private static final String MEM_URL = "jdbc:h2:mem:/LOG=0;DATABASE_TO_UPPER=false;CACHE_SIZE=65536;LOCK_MODE=0;UNDO_LOG=0"; + 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; @@ -61,12 +58,8 @@ public class LocalDB { private static final String SELECT_START_SQL = "SELECT * FROM `%s` "; - private static final String INSERT_SQL = "INSERT INTO `%s` VALUES %s"; - private static final String CREATE_TEMP_TABLE = "CREATE TABLE IF NOT EXISTS `%s` AS (SELECT * FROM FUNCTION_TABLE('%s'))"; - private static final int MAX_INSERT_BATCH = 5_000; - private static final String CACHE_EXPIRE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS `cache_expire` ( `source_id` VARCHAR(128),`expire_time` DATETIME )"; private static final String SET_EXPIRE_SQL = "INSERT INTO `cache_expire` VALUES( '%s', PARSEDATETIME('%s','%s')) "; @@ -146,7 +139,11 @@ private static void registerDataAsTable(Dataframe dataframe, Connection connecti TEMP_RS_CACHE.put(dataframe.getId(), dataframe); // register temporary table String sql = String.format(CREATE_TEMP_TABLE, dataframe.getName(), dataframe.getId()); - connection.prepareStatement(sql).execute(); + try { + connection.prepareStatement(sql).execute(); + } catch (JdbcSQLNonTransientException e) { + //忽略重复创建表导致的异常 + } } /** @@ -185,6 +182,7 @@ public static Dataframe executeLocalQuery(QueryScript queryScript, ExecuteParam queryScript = new QueryScript(); queryScript.setScript(String.format(SELECT_START_SQL, srcData.get(0).getName())); queryScript.setVariables(Collections.emptyList()); + queryScript.setSourceId(srcData.get(0).getName()); } return persistent ? executeInLocalDB(queryScript, executeParam, srcData, expire) : executeInMemDB(queryScript, executeParam, srcData); } @@ -296,29 +294,26 @@ private static Dataframe execute(Connection connection, QueryScript queryScript, } - private static void createTable(String tableName, List columns, Connection connection) throws SQLException { - String sql = tableCreateSQL(tableName, columns); - connection.createStatement().execute(sql); - } - - private static void insertTableData(Dataframe dataframe, Connection connection) throws SQLException { - if (dataframe == null) { - return; - } -// DeleteDbFiles.execute(); - createTable(dataframe.getName(), dataframe.getColumns(), connection); - - List values = createInsertValues(dataframe.getRows(), dataframe.getColumns()); - - List> partition = Lists.partition(values, MAX_INSERT_BATCH); - for (List vals : partition) { - String insertSql = String.format(INSERT_SQL, dataframe.getName(), String.join(",", vals)); - connection.createStatement().execute(insertSql); - } - } +// private static void createTable(String tableName, List columns, Connection connection) throws SQLException { +// String sql = tableCreateSQL(tableName, columns); +// connection.createStatement().execute(sql); +// } + +// private static void insertTableData(Dataframe dataframe, Connection connection) throws SQLException { +// if (dataframe == null) { +// return; +// } +// createTable(dataframe.getName(), dataframe.getColumns(), connection); +// List values = createInsertValues(dataframe.getRows(), dataframe.getColumns()); +// List> partition = Lists.partition(values, MAX_INSERT_BATCH); +// for (List vals : partition) { +// String insertSql = String.format(INSERT_SQL, dataframe.getName(), String.join(",", vals)); +// connection.createStatement().execute(insertSql); +// } +// } private static Connection getConnection(boolean persistent, String database) throws SQLException { - String url = persistent ? getDatabaseUrl(database) : MEM_URL; + String url = persistent ? getDatabaseUrl(database) : MEM_URL + "DB" +database + H2_PARAM; return DriverManager.getConnection(url); } @@ -331,43 +326,43 @@ private static String tableCreateSQL(String name, List columns) { return String.format(TABLE_CREATE_SQL_TEMPLATE, name, sj); } - private static List createInsertValues(List> data, List columns) { - return data.parallelStream().map(row -> { - StringJoiner stringJoiner = new StringJoiner(",", "(", ")"); - for (int i = 0; i < row.size(); i++) { - Object val = row.get(i); - if (val == null || StringUtils.isBlank(val.toString())) { - stringJoiner.add(null); - continue; - } - Column column = columns.get(i); - switch (column.getType()) { - case NUMERIC: - stringJoiner.add(val.toString()); - break; - case DATE: - String valStr; - if (val instanceof Timestamp) { - valStr = DateFormatUtils.format((Timestamp) val, Const.DEFAULT_DATE_FORMAT); - } else if (val instanceof Date) { - valStr = DateFormatUtils.format((Date) val, Const.DEFAULT_DATE_FORMAT); - } else if (val instanceof LocalDateTime) { - valStr = ((LocalDateTime) val).format(DateTimeFormatter.ofPattern(Const.DEFAULT_DATE_FORMAT)); - } else { - valStr = null; - } - if (valStr != null) { - valStr = "PARSEDATETIME('" + valStr + "','" + Const.DEFAULT_DATE_FORMAT + "')"; - } - stringJoiner.add(valStr); - break; - default: - stringJoiner.add("'" + StringEscapeUtils.escapeSql(val.toString()) + "'"); - } - } - return stringJoiner.toString(); - }).collect(Collectors.toList()); - } +// private static List createInsertValues(List> data, List columns) { +// return data.parallelStream().map(row -> { +// StringJoiner stringJoiner = new StringJoiner(",", "(", ")"); +// for (int i = 0; i < row.size(); i++) { +// Object val = row.get(i); +// if (val == null || StringUtils.isBlank(val.toString())) { +// stringJoiner.add(null); +// continue; +// } +// Column column = columns.get(i); +// switch (column.getType()) { +// case NUMERIC: +// stringJoiner.add(val.toString()); +// break; +// case DATE: +// String valStr; +// if (val instanceof Timestamp) { +// valStr = DateFormatUtils.format((Timestamp) val, Const.DEFAULT_DATE_FORMAT); +// } else if (val instanceof Date) { +// valStr = DateFormatUtils.format((Date) val, Const.DEFAULT_DATE_FORMAT); +// } else if (val instanceof LocalDateTime) { +// valStr = ((LocalDateTime) val).format(DateTimeFormatter.ofPattern(Const.DEFAULT_DATE_FORMAT)); +// } else { +// valStr = null; +// } +// if (valStr != null) { +// valStr = "PARSEDATETIME('" + valStr + "','" + Const.DEFAULT_DATE_FORMAT + "')"; +// } +// stringJoiner.add(valStr); +// break; +// default: +// stringJoiner.add("'" + StringEscapeUtils.escapeSql(val.toString()) + "'"); +// } +// } +// return stringJoiner.toString(); +// }).collect(Collectors.toList()); +// } private static String getDatabaseUrl(String database) { if (database == null) { @@ -375,7 +370,7 @@ private static String getDatabaseUrl(String database) { } else { database = toDatabase(database); } - return fileUrl = String.format("jdbc:h2:file:%s/%s;LOG=0;DATABASE_TO_UPPER=false;CACHE_SIZE=65536;LOCK_MODE=0;UNDO_LOG=0", getDbFileBasePath(), database); + return fileUrl = 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/optimize/DataProviderExecuteOptimizer.java b/data-providers/src/main/java/datart/data/provider/optimize/DataProviderExecuteOptimizer.java index a77163580..a5991ebac 100644 --- a/data-providers/src/main/java/datart/data/provider/optimize/DataProviderExecuteOptimizer.java +++ b/data-providers/src/main/java/datart/data/provider/optimize/DataProviderExecuteOptimizer.java @@ -54,20 +54,24 @@ public Dataframe runOptimize(String queryKey, DataProviderSource source, QuerySc } } - public Dataframe getFromCache(String queryKey) { - Cache cache = CacheFactory.getCache(); - if (cache != null) { - return cache.get(queryKey); - } else { - return null; + try { + Cache cache = CacheFactory.getCache(); + if (cache != null) { + return cache.get(queryKey); + } + } catch (Exception e) { } + return null; } public void setCache(String queryKey, Dataframe dataframe, int cacheExpires) { - Cache cache = CacheFactory.getCache(); - if (cache != null) { - cache.put(queryKey, dataframe, cacheExpires); + try { + Cache cache = CacheFactory.getCache(); + if (cache != null) { + cache.put(queryKey, dataframe, cacheExpires); + } + } catch (Exception e) { } } 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 52a38590f..dc1b0ff36 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 @@ -52,17 +52,4 @@ public ScriptRender(QueryScript queryScript, ExecuteParam executeParam, String v this.variableQuote = variableQuote; } - protected String getVariablePattern(String variableName) { - variableName = StringUtils.prependIfMissing(variableName, variableQuote); - variableName = StringUtils.appendIfMissing(variableName, variableQuote); - return variableName; - } - -// private String variableValueString(ScriptVariable variable) { -// if (variable == null || CollectionUtils.isEmpty(variable.getValues())) { -// return ""; -// } else { -// return String.join(",", variable.getValues()); -// } -// } } \ 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 8e34a983a..c0fe4b7e8 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 @@ -141,14 +141,16 @@ protected void replaceVariable(SqlCall sqlCall) { } if (sqlNode instanceof SqlCall) { replaceVariable((SqlCall) sqlNode); + } else if (sqlNode instanceof SqlLiteral) { + // pass } else if (sqlNode instanceof SqlIdentifier) { - if (sqlNode.toString().equals(variable.getName())) { + if (sqlNode.toString().equals(variable.getNameWithQuote())) { sqlCall.setOperand(i, SqlNodeUtils.toSingleSqlLiteral(variable, sqlNode.getParserPosition())); } } else if (sqlNode instanceof SqlNodeList) { SqlNodeList nodeList = (SqlNodeList) sqlNode; List otherNodes = Arrays.stream(nodeList.toArray()) - .filter(node -> !node.toString().equals(variable.getName())) + .filter(node -> !node.toString().equals(variable.getNameWithQuote())) .collect(Collectors.toList()); if (otherNodes.size() == nodeList.size()) { diff --git a/docker-compose.yml.example b/docker-compose.yml.example deleted file mode 100644 index 52693f538..000000000 --- a/docker-compose.yml.example +++ /dev/null @@ -1,15 +0,0 @@ -version: '3' -services: - datart: - image: java:8 - hostname: datart - container_name: datart - restart: always - volumes: - - "{datart application root path}:/datart" - entrypoint: [ "sh","/datart/bin/datart-server.sh" ] - environment: - - TZ=Asia/Shanghai - logging: - options: - max-size: "1g" \ No newline at end of file diff --git a/frontend/.babelrc b/frontend/.babelrc new file mode 100644 index 000000000..50144f1f2 --- /dev/null +++ b/frontend/.babelrc @@ -0,0 +1,23 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + // 避免转换成 CommonJS + "modules": false, + // 使用 loose 模式,避免产生副作用 + "loose": true + } + ] + ], + "plugins": [ + "@babel/plugin-external-helpers", + [ + // 开启 babel 各依赖联动,由此插件负责自动导入 helper 辅助函数,从而形成沙箱 polyfill + "@babel/plugin-transform-runtime", + { + "useESModules": true // 关闭 esm 转化,交由 rollup 处理,同上防止冲突 + } + ] + ] + } \ No newline at end of file diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 1b2d2016b..420ad9f53 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -10,6 +10,21 @@ module.exports = { plugins: ['prettier'], rules: { 'prettier/prettier': ['error', prettierOptions], + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: 'lodash', + message: 'suggest import xxx from `lodash/xxx`', + }, + { + name: 'uuid', + message: 'suggest import xxx from `uuid/dist/xxx`', + }, + ], + }, + ], }, parserOptions: { ecmaVersion: 2018, diff --git a/frontend/.gitignore b/frontend/.gitignore index 978b23d43..ca2ab7ebe 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -31,3 +31,5 @@ yarn-error.log* # vscode .vscode + +/public/task diff --git a/frontend/craco.config.js b/frontend/craco.config.js index c75f1ce8e..6b1b071fd 100644 --- a/frontend/craco.config.js +++ b/frontend/craco.config.js @@ -3,6 +3,7 @@ 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'); const { when, whenDev, @@ -56,7 +57,11 @@ module.exports = { ], webpack: { alias: {}, - plugins: [new WebpackBar(), new MonacoWebpackPlugin({ languages: [''] })], + plugins: [ + new WebpackBar(), + new MonacoWebpackPlugin({ languages: [''] }), + // new BundleAnalyzerPlugin(), + ], configure: (webpackConfig, { env, paths }) => { // paths.appPath='public' // paths.appBuild = 'dist'; // 配合输出打包修改文件目录 diff --git a/frontend/package.json b/frontend/package.json index f837e24cb..cd134a53c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,9 +14,12 @@ "author": "", "license": "Apache-2.0", "scripts": { - "analyze": "craco build && source-map-explorer 'build/static/js/*.js'", + "bootstrap": "npm install --legacy-peer-deps", "start": "craco start", "build": "cross-env GENERATE_SOURCEMAP=false craco build", + "build:task": "rollup -c", + "build:all": "npm run build:task && npm run build", + "build:analyze": "craco build && source-map-explorer 'build/static/js/*.js'", "test": "craco test", "test:coverage": "npm run test -- --watchAll=false --coverage", "checkTs": "tsc --noEmit", @@ -84,6 +87,8 @@ "dependencies": { "@ant-design/icons": "^4.5.0", "@ant-design/pro-table": "2.60.1", + "@antv/s2": "^1.3.0", + "@antv/s2-react": "^1.3.0", "@dinero.js/currencies": "^2.0.0-alpha.8", "@reduxjs/toolkit": "^1.5.0", "@types/react-color": "^3.0.5", @@ -114,7 +119,6 @@ "react-dnd-html5-backend": "^14.0.0", "react-dom": "^17.0.1", "react-draggable": "^4.4.3", - "react-frame-component": "^5.1.0", "react-grid-layout": "^1.2.4", "react-helmet-async": "^1.0.7", "react-hotkeys-hook": "^3.4.0", @@ -125,6 +129,7 @@ "react-resizable": "^1.11.1", "react-resize-detector": "^6.7.6", "react-router-dom": "^5.2.0", + "react-window": "^1.8.6", "redux-undo": "^1.0.1", "reveal.js": "^4.1.0", "split.js": "^1.6.4", @@ -132,9 +137,17 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@babel/core": "^7.15.8", + "@babel/preset-env": "^7.15.8", "@commitlint/cli": "^12.0.1", "@commitlint/config-conventional": "^12.0.1", "@craco/craco": "^6.1.1", + "@rollup/plugin-babel": "^5.3.0", + "@rollup/plugin-commonjs": "^21.0.1", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^13.0.6", + "@rollup/plugin-replace": "^2.4.2", + "@rollup/plugin-typescript": "^8.3.0", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.8.0", @@ -170,6 +183,8 @@ "prettier-plugin-organize-imports": "^2.3.3", "react-scripts": "4.0.3", "react-test-renderer": "^17.0.1", + "rollup": "^2.62.0", + "rollup-plugin-cleanup": "^3.2.1", "serve": "^11.3.2", "source-map-explorer": "^2.5.2", "styled-components": "^5.3.0", diff --git a/frontend/public/custom-chart-plugins/demo-custom-line-chart.js b/frontend/public/custom-chart-plugins/demo-custom-line-chart.js index 16b9bbace..65c4f76dd 100644 --- a/frontend/public/custom-chart-plugins/demo-custom-line-chart.js +++ b/frontend/public/custom-chart-plugins/demo-custom-line-chart.js @@ -17,6 +17,8 @@ */ function DemoCustomLineChart({ dHelper }) { + const svgIcon = ``; + return { config: { datas: [ @@ -408,7 +410,26 @@ function DemoCustomLineChart({ dHelper }) { ], }, ], - settings: [], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -470,9 +491,6 @@ function DemoCustomLineChart({ dHelper }) { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, }, }, ], @@ -482,7 +500,7 @@ function DemoCustomLineChart({ dHelper }) { meta: { id: 'demo-custom-line-chart', name: '[DEMO]用户自定义折线图', - icon: 'chart', + icon: svgIcon, requirements: [ { group: 1, @@ -530,7 +548,7 @@ function DemoCustomLineChart({ dHelper }) { .filter(c => c.type === 'aggregate') .flatMap(config => config.rows || []); - const objDataColumns = dHelper.transfromToObjectArray( + const objDataColumns = dHelper.transformToObjectArray( dataset.rows, dataset.columns, ); diff --git a/frontend/public/custom-chart-plugins/demo-d3js-scatter-chart.js b/frontend/public/custom-chart-plugins/demo-d3js-scatter-chart.js index 650ad9466..a68407762 100644 --- a/frontend/public/custom-chart-plugins/demo-d3js-scatter-chart.js +++ b/frontend/public/custom-chart-plugins/demo-d3js-scatter-chart.js @@ -47,6 +47,26 @@ function D3JSScatterChart({ dHelper }) { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -173,7 +193,7 @@ function D3JSScatterChart({ dHelper }) { .flatMap(config => config.rows || []); // 数据转换,根据Datart提供了Helper转换工具 - const objDataColumns = dHelper.transfromToObjectArray( + const objDataColumns = dHelper.transformToObjectArray( dataset.rows, dataset.columns, ); diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js new file mode 100644 index 000000000..30e83916e --- /dev/null +++ b/frontend/rollup.config.js @@ -0,0 +1,41 @@ +/* eslint-disable import/no-anonymous-default-export */ +import { babel } from '@rollup/plugin-babel'; +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import replace from '@rollup/plugin-replace'; +import typescript from '@rollup/plugin-typescript'; +import path from 'path'; +import cleanup from 'rollup-plugin-cleanup'; +export default { + input: 'src/task.ts', // 打包入口 + output: { + // 打包出口 + name: 'getQueryData', // namespace + file: path.resolve(__dirname, 'public/task/index.js'), // 最终打包出来的文件路径和文件名 + format: 'umd', // umd/amd/cjs/iife + }, + plugins: [ + json(), + nodeResolve({ + extensions: ['.js', '.ts'], + }), + // 解析TypeScript + typescript({ + tsconfig: path.resolve(__dirname, 'tsconfig.json'), + }), + // 将 CommonJS 转换成 ES2015 模块供 Rollup 处理 + commonjs(), + // es6--> es5 + babel({ + babelHelpers: 'runtime', + exclude: 'node_modules/**', + presets: [['@babel/preset-env', { modules: false }]], + comments: false, + }), + cleanup(), + replace({ + 'console.log': '//console.log', + }), + ], +}; diff --git a/frontend/src/app/LoginAuthRoute.tsx b/frontend/src/app/LoginAuthRoute.tsx index 8214d0df5..3dcaf77f1 100644 --- a/frontend/src/app/LoginAuthRoute.tsx +++ b/frontend/src/app/LoginAuthRoute.tsx @@ -1,3 +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 { AuthorizedRoute } from 'app/components'; import { getToken } from 'utils/auth'; import { LazyMainPage } from './pages/MainPage/Loadable'; diff --git a/frontend/src/app/assets/fonts/iconfont.css b/frontend/src/app/assets/fonts/iconfont.css index c5b40a831..a19e73c73 100644 --- a/frontend/src/app/assets/fonts/iconfont.css +++ b/frontend/src/app/assets/fonts/iconfont.css @@ -1,8 +1,8 @@ @font-face { - font-family: 'iconfont'; /* Project id 2869064 */ - src: url('iconfont.woff2?t=1637912668357') format('woff2'), - url('iconfont.woff?t=1637912668357') format('woff'), - url('iconfont.ttf?t=1637912668357') format('truetype'); + font-family: 'iconfont'; + src: url('iconfont.woff2?t=1639456509648') format('woff2'), + url('iconfont.woff?t=1639456509648') format('woff'), + url('iconfont.ttf?t=1639456509648') format('truetype'); } .iconfont { @@ -14,6 +14,10 @@ -moz-osx-font-smoothing: grayscale; } +.icon-rich-text:before { + content: '\e7a4'; +} + .icon-graph-circular:before { content: '\e7d0'; } diff --git a/frontend/src/app/assets/fonts/iconfont.ttf b/frontend/src/app/assets/fonts/iconfont.ttf index c15a98170..abe9580e1 100644 Binary files a/frontend/src/app/assets/fonts/iconfont.ttf and b/frontend/src/app/assets/fonts/iconfont.ttf differ diff --git a/frontend/src/app/assets/fonts/iconfont.woff b/frontend/src/app/assets/fonts/iconfont.woff index 41636bee4..39a91360e 100644 Binary files a/frontend/src/app/assets/fonts/iconfont.woff and b/frontend/src/app/assets/fonts/iconfont.woff differ diff --git a/frontend/src/app/assets/fonts/iconfont.woff2 b/frontend/src/app/assets/fonts/iconfont.woff2 index 6bbb94360..6d5446701 100644 Binary files a/frontend/src/app/assets/fonts/iconfont.woff2 and b/frontend/src/app/assets/fonts/iconfont.woff2 differ diff --git a/frontend/src/app/assets/theme/colorsConfig.ts b/frontend/src/app/assets/theme/colorsConfig.ts new file mode 100644 index 000000000..2868f6efd --- /dev/null +++ b/frontend/src/app/assets/theme/colorsConfig.ts @@ -0,0 +1,476 @@ +/** + * 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 { BLACK, WHITE } from 'styles/StyleConstants'; +export const defaultPalette = [ + '#FAFAFA', + '#9E9E9E', + '#E3F2FD', + '#FFF3E0', + '#FFEBEE', + '#E0F2F1', + '#E8F5E9', + '#FFF8E1', + '#EDE7F6', + '#FCE4EC', + '#EFEBE9', + '#F5F5F5', + '#757575', + '#BBDEFB', + '#FFE0B2', + '#FFCDD2', + '#B2DFDB', + '#C8E6C9', + '#FFECB3', + '#D1C4E9', + '#F8BBD0', + '#D7CCC8', + '#EEEEEE', + '#616161', + '#64B5F6', + '#FFB74D', + '#E57373', + '#64FFDA', + '#81C784', + '#FFD740', + '#9575CD', + '#FF4081', + '#A1887F', + '#E0E0E0', + '#424242', + '#1976D2', + '#F57C00', + '#D32F2F', + '#1DE9B6', + '#388E3C', + '#FFC400', + '#512DA8', + '#F50057', + '#5D4037', + '#BDBDBD', + '#212121', + '#0D47A1', + '#E65100', + '#B71C1C', + '#00BFA5', + '#1B5E20', + '#FFAB00', + '#311B92', + '#C51162', + '#3E2723', +]; +export const colorThemes = [ + { + id: 'default', + colors: [ + '#448aff', + '#ffab40', + '#ff5252', + '#a7ffeb', + '#4caf50', + '#ffecb3', + '#7c4dff', + '#f8bbd0', + '#795548', + '#f5f5f5', + ], + en: { + title: 'Default', + }, + zh: { + title: '默认', + }, + }, + { + id: 'default20', + colors: [ + '#448aff', + '#e3f2fd', + '#ffab40', + '#fff3e0', + '#4caf50', + '#b9f6ca', + '#ffd740', + '#ffecb3', + '#009688', + '#a7ffeb', + '#ff5252', + '#ffcdd2', + '#9e9e9e', + '#f5f5f5', + '#FF4081', + '#f8bbd0', + '#7c4dff', + '#b388ff', + '#795548', + '#d7ccc8', + ], + en: { + title: 'Default 20', + }, + zh: { + title: '默认20色', + }, + }, + { + id: 'spectrum', + colors: [ + '#16b4bb', + '#3f28c9', + '#e17315', + '#cf167d', + '#7d6ff9', + '#40e15c', + '#2068e7', + '#5a20a2', + '#d8b509', + '#bf5b0e', + '#217d59', + '#8ced43', + ], + en: { + title: 'Spectrum', + }, + zh: { + title: 'Spectrum', + }, + }, + { + id: 'retrometro', + colors: [ + '#b33dc6', + '#27aeef', + '#87bc45', + '#bdcf32', + '#ede15b', + '#edbf33', + '#ef9b20', + '#f46a9b', + '#ea5545', + ], + en: { + title: 'Retro Metro', + }, + zh: { + title: 'Retro Metro', + }, + }, + { + id: 'dutchfield', + colors: [ + '#e60049', + '#0bb4ff', + '#50e991', + '#e6d800', + '#9b19f5', + '#ffa300', + '#dc0ab4', + '#b3d4ff', + '#00bfa0', + ], + en: { + title: 'Dutch Field', + }, + zh: { + title: 'Dutch Field', + }, + }, + { + id: 'rivernights', + colors: [ + '#b30000', + '#7c1158', + '#4421af', + '#1a53ff', + '#0d88e6', + '#00b7c7', + '#5ad45a', + '#8be04e', + '#ebdc78', + ], + en: { + title: 'River Nights', + }, + zh: { + title: 'River Nights', + }, + }, + { + id: 'springpastels', + colors: [ + '#fd7f6f', + '#7eb0d5', + '#b2e061', + '#bd7ebe', + '#ffb55a', + '#ffee65', + '#beb9db', + '#fdcce5', + '#8bd3c7', + ], + en: { + title: 'Spring Pastels', + }, + zh: { + title: 'Spring Pastels', + }, + }, + { + id: 'echarts', + colors: [ + '#5470c6', + '#91cc75', + '#fac858', + '#ee6666', + '#73c0de', + '#3ba272', + '#fc8452', + '#9a60b4', + '#ea7ccc', + ], + en: { + title: 'Echarts', + }, + zh: { + title: 'Echarts', + }, + }, + { + id: 'vintage', + colors: [ + '#d87c7c', + '#919e8b', + '#d7ab82', + '#6e7074', + '#61a0a8', + '#efa18d', + '#787464', + '#cc7e63', + '#724e58', + '#4b565b', + ], + en: { + title: 'Vintage', + }, + zh: { + title: '怀旧', + }, + }, + { + id: 'dark', + colors: [ + '#dd6b66', + '#759aa0', + '#e69d87', + '#8dc1a9', + '#ea7e53', + '#eedd78', + '#73a373', + '#73b9bc', + '#7289ab', + '#91ca8c', + '#f49f42', + ], + en: { + title: 'Dark', + }, + zh: { + title: '暗色', + }, + }, + { + id: 'westeros', + colors: ['#516b91', '#59c4e6', '#edafda', '#93b7e3', '#a5e7f0', '#cbb0e3'], + en: { + title: 'Westeros', + }, + zh: { + title: 'Westeros', + }, + }, + { + id: 'essos', + colors: ['#893448', '#d95850', '#eb8146', '#ffb248', '#f2d643', '#ebdba4'], + en: { + title: 'Essos', + }, + zh: { + title: 'Essos', + }, + }, + { + id: 'wonderland', + colors: ['#4ea397', '#22c3aa', '#7bd9a5', '#d0648a', '#f58db2', '#f2b3c9'], + en: { + title: 'Wonderland', + }, + zh: { + title: 'Wonderland', + }, + }, + { + id: 'walden', + colors: ['#3fb1e3', '#6be6c1', '#626c91', '#a0a7e6', '#c4ebad', '#96dee8'], + en: { + title: 'Walden', + }, + zh: { + title: 'Walden', + }, + }, + { + id: 'chalk', + colors: [ + '#fc97af', + '#87f7cf', + '#f7f494', + '#72ccff', + '#f7c5a0', + '#d4a4eb', + '#d2f5a6', + '#76f2f2', + ], + en: { + title: 'Chalk', + }, + zh: { + title: '粉笔', + }, + }, + { + id: 'infographic', + colors: [ + '#c1232b', + '#27727b', + '#fcce10', + '#e87c25', + '#b5c334', + '#fe8463', + '#9bca63', + '#fad860', + '#f3a43b', + '#60c0dd', + '#d7504b', + '#c6e579', + '#f4e001', + '#f0805a', + '#26c0c0', + ], + en: { + title: 'Infographic', + }, + zh: { + title: '信息图', + }, + }, + { + id: 'macarons', + colors: [ + '#2ec7c9', + '#b6a2de', + '#5ab1ef', + '#ffb980', + '#d87a80', + '#8d98b3', + '#e5cf0d', + '#97b552', + '#95706d', + '#dc69aa', + '#07a2a4', + '#9a7fd1', + '#588dd5', + '#f5994e', + '#c05050', + '#59678c', + '#c9ab00', + '#7eb00a', + '#6f5553', + '#c14089', + ], + en: { + title: 'Macarons', + }, + zh: { + title: '马卡龙', + }, + }, + { + id: 'roma', + colors: [ + '#e01f54', + '#001852', + '#f5e8c8', + '#b8d2c7', + '#c6b38e', + '#a4d8c2', + '#f3d999', + '#d3758f', + '#dcc392', + '#2e4783', + '#82b6e9', + '#ff6347', + '#a092f1', + '#0a915d', + '#eaf889', + '#6699ff', + '#ff6666', + '#3cb371', + '#d5b158', + '#38b6b6', + ], + en: { + title: 'Roma', + }, + zh: { + title: '罗马', + }, + }, + { + id: 'shine', + colors: [ + '#c12e34', + '#e6b600', + '#0098d9', + '#2b821d', + '#005eaa', + '#339ca8', + '#cda819', + '#32a487', + ], + en: { + title: 'Shine', + }, + zh: { + title: '阳光', + }, + }, + { + id: 'purplepassion', + colors: ['#9b8bba', '#e098c7', '#8fd3e8', '#71669e', '#cc70af', '#7cb4cc'], + en: { + title: 'Purple Passion', + }, + zh: { + title: '热情', + }, + }, +]; +export const defaultThemes = [ + WHITE, + BLACK, + ...colorThemes[0].colors.slice(0, colorThemes[0].colors.length - 1), +]; diff --git a/frontend/src/app/components/ChartEditor.tsx b/frontend/src/app/components/ChartEditor.tsx index fadb372e8..c836f26ec 100644 --- a/frontend/src/app/components/ChartEditor.tsx +++ b/frontend/src/app/components/ChartEditor.tsx @@ -20,6 +20,7 @@ import { ExclamationCircleOutlined } from '@ant-design/icons'; import { Modal } from 'antd'; import useMount from 'app/hooks/useMount'; import workbenchSlice, { + aggregationSelector, BackendChart, backendChartSelector, ChartConfigReducerActionType, @@ -31,6 +32,7 @@ import workbenchSlice, { shadowChartConfigSelector, updateChartAction, updateChartConfigAndRefreshDatasetAction, + updateRichTextAction, useWorkbenchSlice, } from 'app/pages/ChartWorkbenchPage/slice/workbenchSlice'; import { transferChartConfigs } from 'app/utils/internalChartHelper'; @@ -82,12 +84,17 @@ export const ChartEditor: React.FC = ({ const chartConfig = useSelector(chartConfigSelector); const shadowChartConfig = useSelector(shadowChartConfigSelector); const backendChart = useSelector(backendChartSelector); + const aggregation = useSelector(aggregationSelector); const [chart, setChart] = useState(); useMount( () => { - const currentChart = ChartManager.instance().getDefaultChart(); - handleChartChange(currentChart); + if (!dataChartId && !originChart) { + // Note: add default chart if new to editor + const currentChart = ChartManager.instance().getDefaultChart(); + handleChartChange(currentChart); + } + if (container === 'dataChart') { dispatch( initWorkbenchAction({ @@ -104,6 +111,9 @@ export const ChartEditor: React.FC = ({ backendChart: originChart as BackendChart, }), ); + if (!originChart) { + dispatch(actions.updateChartAggregation(true)); + } } else { // chartType === 'dataChart' dispatch( @@ -129,7 +139,7 @@ export const ChartEditor: React.FC = ({ setChart(currentChart); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [backendChart]); + }, [backendChart?.config?.chartGraphId]); const handleChartChange = (c: Chart) => { registerChartEvents(c); @@ -166,15 +176,19 @@ export const ChartEditor: React.FC = ({ const currentChart = ChartManager.instance().getDefaultChart(); registerChartEvents(currentChart); setChart(currentChart); - let clonedState = CloneValueDeep(currentChart.config); + let targetChartConfig = CloneValueDeep(currentChart.config); + const finalChartConfig = transferChartConfigs( + targetChartConfig, + targetChartConfig, + ); + + dispatch(workbenchSlice.actions.updateShadowChartConfig({})); dispatch( workbenchSlice.actions.updateChartConfig({ type: ChartConfigReducerActionType.INIT, payload: { - init: { - ...clonedState, - }, + init: finalChartConfig, }, }), ); @@ -185,11 +199,12 @@ export const ChartEditor: React.FC = ({ chartConfig: chartConfig!, chartGraphId: chart?.meta.id!, computedFields: dataview?.computedFields || [], + aggregation, }; const dataChart: DataChart = { id: dataChartId, - name: backendChart?.name || 'widget_chart', + name: backendChart?.name || '', viewId: dataview?.id || '', orgId: orgId, config: dataChartConfig, @@ -206,6 +221,7 @@ export const ChartEditor: React.FC = ({ dataview, onSaveInWidget, orgId, + aggregation, ]); const saveChart = useCallback(async () => { @@ -218,6 +234,7 @@ export const ChartEditor: React.FC = ({ chartId: dataChartId, index: 0, parentId: 0, + aggregation: aggregation, }), ); onSaveInDataChart?.(orgId, dataChartId); @@ -238,6 +255,7 @@ export const ChartEditor: React.FC = ({ chartId: dataChartId, index: 0, parentId: 0, + aggregation, }), ); saveToWidget(); @@ -259,6 +277,7 @@ export const ChartEditor: React.FC = ({ orgId, chartType, saveToWidget, + aggregation, ]); const registerChartEvents = chart => { @@ -266,25 +285,55 @@ export const ChartEditor: React.FC = ({ { name: 'click', callback: param => { - if (param.seriesName === 'paging') { - const page = param.value?.page; - dispatch(refreshDatasetAction({ pageInfo: { pageNo: page } })); + if ( + param.componentType === 'table' && + param.seriesType === 'paging-sort-filter' + ) { + dispatch( + refreshDatasetAction({ + sorter: { + column: param?.seriesName!, + operator: param?.value?.direction, + }, + pageInfo: { + pageNo: param?.value?.pageNo, + }, + }), + ); + return; + } + if (param.seriesName === 'richText') { + dispatch(updateRichTextAction(param.value)); return; } - }, - }, - { - name: 'dblclick', - callback: param => { - console.log( - '//TODO: to be remove | mouse db click event ----> ', - param, - ); }, }, ]); }; + const handleAggregationState = state => { + const currentChart = ChartManager.instance().getById(chart?.meta?.id); + let targetChartConfig = CloneValueDeep(currentChart?.config); + registerChartEvents(currentChart); + setChart(currentChart); + + const finalChartConfig = transferChartConfigs( + targetChartConfig, + targetChartConfig, + ); + + dispatch(actions.updateChartAggregation(state)); + dispatch(workbenchSlice.actions.updateShadowChartConfig({})); + dispatch( + workbenchSlice.actions.updateChartConfig({ + type: ChartConfigReducerActionType.INIT, + payload: { + init: finalChartConfig, + }, + }), + ); + }; + return ( = ({ onGoBack: () => { onClose?.(); }, + onChangeAggregation: handleAggregationState, }} + aggregation={aggregation} chart={chart} dataset={dataset} dataview={dataview} diff --git a/frontend/src/app/components/ColorPicker/ChromeColorPicker.tsx b/frontend/src/app/components/ColorPicker/ChromeColorPicker.tsx new file mode 100644 index 000000000..d5d48fdae --- /dev/null +++ b/frontend/src/app/components/ColorPicker/ChromeColorPicker.tsx @@ -0,0 +1,88 @@ +/** + * 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 { Button } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import React, { useState } from 'react'; +import { ChromePicker, ColorResult } from 'react-color'; +import styled from 'styled-components/macro'; +import { SPACE_TIMES } from 'styles/StyleConstants'; +import { colorSelectionPropTypes } from './slice/types'; + +const toChangeValue = (data: ColorResult) => { + const { r, g, b, a } = data.rgb; + return `rgba(${r}, ${g}, ${b}, ${a})`; +}; + +/** + * 单色选择组件 + * @param onChange + * @param color + * @returns 返回一个新的颜色值 + */ +function ChromeColorPicker({ color, onChange }: colorSelectionPropTypes) { + const [selectColor, setSelectColor] = useState(color); + const t = useI18NPrefix('components.colorPicker'); + + return ( + + { + let colorRgb = toChangeValue(color); + setSelectColor(colorRgb); + }} + /> + + { + onChange?.(false); + }} + > + {t('cancel')} + + { + onChange?.(selectColor); + }} + > + {t('ok')} + + + + ); +} + +export default ChromeColorPicker; + +const ChromeColorWrap = styled.div` + .chrome-picker { + box-shadow: none !important; + } +`; + +const BtnWrap = styled.div` + text-align: right; + margin-top: ${SPACE_TIMES(2.5)}; + > button:first-child { + margin-right: ${SPACE_TIMES(2.5)}; + } +`; diff --git a/frontend/src/app/components/ColorPicker/ColorPickerPopover.tsx b/frontend/src/app/components/ColorPicker/ColorPickerPopover.tsx new file mode 100644 index 000000000..2ab262b93 --- /dev/null +++ b/frontend/src/app/components/ColorPicker/ColorPickerPopover.tsx @@ -0,0 +1,54 @@ +import { Popover, PopoverProps } from 'antd'; +import { FC, useCallback, useMemo, useState } from 'react'; +import { SketchPickerProps } from 'react-color'; +import { ColorPicker } from './ColorTag'; +import SingleColorSelection from './SingleColorSelection'; + +interface ColorPickerPopoverProps { + popoverProps?: PopoverProps; + defaultValue?: string; + onSubmit?: (color) => void; + onChange?: (color) => void; + colorPickerClass?: string; + colors?: SketchPickerProps['presetColors']; +} +export const ColorPickerPopover: FC = ({ + children, + defaultValue, + popoverProps, + onSubmit, + onChange, + colorPickerClass, +}) => { + const [visible, setVisible] = useState(false); + const [color] = useState(defaultValue); + + const onCancel = useCallback(() => { + setVisible(false); + }, []); + const onColorChange = useCallback( + color => { + onSubmit?.(color); + onChange?.(color); + onCancel(); + }, + [onSubmit, onCancel, onChange], + ); + const _popoverProps = useMemo(() => { + return typeof popoverProps === 'object' ? popoverProps : {}; + }, [popoverProps]); + return ( + } + trigger="click" + placement="right" + > + {children || ( + + )} + + ); +}; diff --git a/frontend/src/app/components/ReactColorPicker/ColorTag.tsx b/frontend/src/app/components/ColorPicker/ColorTag.tsx similarity index 100% rename from frontend/src/app/components/ReactColorPicker/ColorTag.tsx rename to frontend/src/app/components/ColorPicker/ColorTag.tsx diff --git a/frontend/src/app/components/ColorPicker/SingleColorSelection.tsx b/frontend/src/app/components/ColorPicker/SingleColorSelection.tsx new file mode 100644 index 000000000..09614e3ba --- /dev/null +++ b/frontend/src/app/components/ColorPicker/SingleColorSelection.tsx @@ -0,0 +1,162 @@ +/** + * 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 { Popover } from 'antd'; +import { defaultPalette, defaultThemes } from 'app/assets/theme/colorsConfig'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import React, { useState } from 'react'; +import styled from 'styled-components/macro'; +import { + BORDER_RADIUS, + FONT_SIZE_BODY, + G40, + G80, + SPACE_TIMES, + WHITE, +} from 'styles/StyleConstants'; +import ChromeColorPicker from './ChromeColorPicker'; +import { colorSelectionPropTypes } from './slice/types'; + +/** + * 单色选择组件 + * @param onChange + * @param color + * @returns 返回一个新的颜色值 + */ +function SingleColorSelection({ color, onChange }: colorSelectionPropTypes) { + const [moreStatus, setMoreStatus] = useState(false); + const [selectColor, setSelectColor] = useState(color); + const t = useI18NPrefix('components.colorPicker'); + + //更多颜色里的回调函数 + const moreCallBackFn = value => { + if (value) { + setSelectColor(value); + onChange?.(value); + } + setMoreStatus(false); + }; + const selectColorFn = (color: string) => { + setSelectColor(color); + onChange?.(color); + }; + return ( + + + {defaultThemes.map((color, i) => { + return ( + { + selectColorFn(color); + }} + color={color} + key={i} + className={selectColor === color ? 'active' : ''} + > + ); + })} + + + {defaultPalette.map((color, i) => { + return ( + { + selectColorFn(color); + }} + color={color} + key={i} + className={selectColor === color ? 'active' : ''} + > + ); + })} + + } + > + { + setMoreStatus(true); + }} + > + {t('more')} + + + + ); +} + +export default SingleColorSelection; + +const ColorWrap = styled.div` + background-color: ${WHITE}; + width: 426px; + min-width: 426px; + // max-width: 426px; +`; + +const ThemeColorWrap = styled.div` + border-bottom: 1px solid ${G40}; + padding-bottom: ${SPACE_TIMES(1.5)}; + margin: ${SPACE_TIMES(2.5)} 0; +`; + +const ColorBlock = styled.span<{ color: string }>` + display: inline-block; + min-width: ${SPACE_TIMES(6)}; + min-height: ${SPACE_TIMES(6)}; + background-color: ${p => p.color}; + border-radius: ${BORDER_RADIUS}; + cursor: pointer; + transition: all 0.2s; + margin-right: ${SPACE_TIMES(4)}; + border: 1px solid ${G40}; + &:last-child { + margin-right: 0px; + } + &:hover { + opacity: 0.7; + } + &.active { + border: 1px solid ${p => p.theme.primary}; + } +`; + +const ColorPalette = styled.div` + border-bottom: 1px solid ${G40}; + padding-bottom: ${SPACE_TIMES(1.5)}; + > span:nth-child(11n) { + margin-right: 0px; + } +`; + +const MoreColor = styled.div` + text-align: center; + cursor: pointer; + margin-top: ${SPACE_TIMES(2.5)}; + font-size: ${FONT_SIZE_BODY}; + color: ${G80}; + &:hover { + color: ${p => p.theme.primary}; + } +`; diff --git a/frontend/src/app/components/ColorPicker/ThemeColorSelection.tsx b/frontend/src/app/components/ColorPicker/ThemeColorSelection.tsx new file mode 100644 index 000000000..9885c2266 --- /dev/null +++ b/frontend/src/app/components/ColorPicker/ThemeColorSelection.tsx @@ -0,0 +1,121 @@ +/** + * 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 { List, Popover } from 'antd'; +import { colorThemes } from 'app/assets/theme/colorsConfig'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components/macro'; +import { FONT_SIZE_BODY, G10, SPACE_TIMES } from 'styles/StyleConstants'; +import { themeColorPropTypes } from './slice/types'; + +/** + * @param callbackFn 回调函数返回一个颜色数组 + * @param children 点击弹出按钮的文字 支持文字和html类型 + */ +function ThemeColorSelection({ children, callbackFn }: themeColorPropTypes) { + const [switchStatus, setSwitchStatus] = useState(false); + const [colors] = useState(colorThemes); + const { i18n } = useTranslation(); + + return ( + + ( + { + callbackFn(item.colors); + setSwitchStatus(false); + }} + > + {item[i18n.language].title} + + {item.colors.map((v, i) => { + return ; + })} + + + )} + /> + + } + > + { + setSwitchStatus(!switchStatus); + }} + > + {children} + + + ); +} + +export default ThemeColorSelection; + +const ChooseTheme = styled.div` + display: inline-block; + width: 100%; + text-align: right; + margin-bottom: ${SPACE_TIMES(1)}; +`; +const ChooseThemeSpan = styled.div` + cursor: pointer; + font-size: ${FONT_SIZE_BODY}; + display: inline-block; + width: max-content; + &:hover { + color: ${p => p.theme.primary}; + } +`; +const ColorWrapAlert = styled.div` + width: 350px; + max-height: 300px; + overflow-y: auto; + .ant-list-item { + display: flex; + flex-direction: column; + align-items: flex-start; + cursor: pointer; + padding: ${SPACE_TIMES(2.5)}; + &:hover { + background-color: ${G10}; + } + } +`; +const ColorTitle = styled.span``; +const ColorBlockWrap = styled.div` + display: flex; + flex-wrap: wrap; + margin-top: ${SPACE_TIMES(1)}; +`; +const ColorBlock = styled.span<{ color: string }>` + display: inline-block; + min-width: ${SPACE_TIMES(6)}; + min-height: ${SPACE_TIMES(6)}; + background-color: ${p => p.color}; +`; diff --git a/frontend/src/app/components/ColorPicker/index.tsx b/frontend/src/app/components/ColorPicker/index.tsx new file mode 100644 index 000000000..e98c9046a --- /dev/null +++ b/frontend/src/app/components/ColorPicker/index.tsx @@ -0,0 +1,28 @@ +/** + * 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 { ColorPickerPopover } from './ColorPickerPopover'; +import { ColorTag } from './ColorTag'; +import SingleColorSelection from './SingleColorSelection'; +import ThemeColorSelection from './ThemeColorSelection'; + +export { + SingleColorSelection, + ThemeColorSelection, + ColorTag, + ColorPickerPopover, +}; diff --git a/frontend/src/app/components/ColorPicker/slice/types.ts b/frontend/src/app/components/ColorPicker/slice/types.ts new file mode 100644 index 000000000..f240fca44 --- /dev/null +++ b/frontend/src/app/components/ColorPicker/slice/types.ts @@ -0,0 +1,10 @@ +import { ReactNode } from 'react'; + +export interface colorSelectionPropTypes { + color?: string; + onChange?: (color) => void; +} +export interface themeColorPropTypes { + children: ReactNode; + callbackFn: (Array) => void; +} diff --git a/frontend/src/app/components/Configuration.tsx b/frontend/src/app/components/Configuration.tsx index e21bff043..4a816a40a 100644 --- a/frontend/src/app/components/Configuration.tsx +++ b/frontend/src/app/components/Configuration.tsx @@ -1,7 +1,7 @@ import { EditableProTable, ProColumns } from '@ant-design/pro-table'; import { useCallback, useMemo, useState } from 'react'; import { css } from 'styled-components/macro'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; const tableStyle = css` .ant-card-body { diff --git a/frontend/src/app/components/DragSortEditTable.tsx b/frontend/src/app/components/DragSortEditTable.tsx index dfbc3ea86..e8696c7a8 100644 --- a/frontend/src/app/components/DragSortEditTable.tsx +++ b/frontend/src/app/components/DragSortEditTable.tsx @@ -18,7 +18,7 @@ import { Form, Input, Table, TableProps } from 'antd'; import { FormInstance } from 'antd/lib/form'; -import { FilterValueOption } from 'app/types/ChartConfig'; +import { RelationFilterValue } from 'app/types/ChartConfig'; import { createContext, useCallback, @@ -36,9 +36,9 @@ interface EditableCellProps { title: React.ReactNode; editable: boolean; children: React.ReactNode; - dataIndex: keyof FilterValueOption; - record: FilterValueOption; - handleSave: (record: FilterValueOption) => void; + dataIndex: keyof RelationFilterValue; + record: RelationFilterValue; + handleSave: (record: RelationFilterValue) => void; } interface EditableRowProps { diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicColorSelector.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicColorSelector.tsx index a0c3824d5..501ec5bf0 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicColorSelector.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicColorSelector.tsx @@ -17,7 +17,7 @@ */ import { Col, Row } from 'antd'; -import { ColorPickerPopover } from 'app/components/ReactColorPicker'; +import { ColorPickerPopover } from 'app/components/ColorPicker'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; import { FC, memo } from 'react'; import styled from 'styled-components/macro'; @@ -46,7 +46,7 @@ const BasicColorSelector: FC> = memo( ({ ancestors, translate: t = title => title, data: row, onChange }) => { const { comType, options, ...rest } = row; - const hanldePickerSelect = value => { + const handlePickerSelect = value => { onChange?.(ancestors, value); }; @@ -63,7 +63,7 @@ const BasicColorSelector: FC> = memo( {...options} colors={COLORS} defaultValue={getColor()} - onSubmit={hanldePickerSelect} + onSubmit={handlePickerSelect} > @@ -85,4 +85,5 @@ const StyledColor = styled.div` height: 24px; background-color: ${props => props.color}; border: ${props => (props.color === 'transparent' ? '1px solid red' : '0px')}; + cursor: pointer; `; diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicFont.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicFont.tsx index 42c796639..8de5a8055 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicFont.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicFont.tsx @@ -17,7 +17,7 @@ */ import { Select } from 'antd'; -import { ColorPickerPopover } from 'app/components/ReactColorPicker'; +import { ColorPickerPopover } from 'app/components/ColorPicker'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; import { updateByKey } from 'app/utils/mutation'; import { @@ -36,7 +36,7 @@ const BasicFont: FC> = memo( ({ ancestors, translate: t = title => title, data, onChange }) => { const { comType, options, ...rest } = data; - const hanldePickerSelect = value => { + const handlePickerSelect = value => { handleSettingChange('color')(value); }; @@ -50,19 +50,22 @@ const BasicFont: FC> = memo( - {FONT_FAMILIES.map(o => ( - - {o.name} + {(options?.fontFamilies || FONT_FAMILIES).map(o => ( + + {typeof o === 'string' ? o : o.name} ))} @@ -76,7 +79,7 @@ const BasicFont: FC> = memo( @@ -87,7 +90,7 @@ const BasicFont: FC> = memo( ))} @@ -102,7 +105,7 @@ const BasicFont: FC> = memo( {...rest} {...options} defaultValue={data.value?.color} - onSubmit={hanldePickerSelect} + onSubmit={handlePickerSelect} /> diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicFontFamilySelector.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicFontFamilySelector.tsx index 93f8aa196..68ffccaf0 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicFontFamilySelector.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicFontFamilySelector.tsx @@ -36,7 +36,7 @@ const BasicFontFamilySelector: FC> = dropdownMatchSelectWidth {...rest} {...options} - placeholder={t('pleaseSelect')} + placeholder={t('select')} onChange={value => onChange?.(ancestors, value)} > {FONT_FAMILIES.map(o => ( diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicFontSizeSelector.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicFontSizeSelector.tsx index af2f01278..36994b514 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicFontSizeSelector.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicFontSizeSelector.tsx @@ -36,7 +36,7 @@ const BasicFontSizeSelector: FC> = dropdownMatchSelectWidth {...rest} {...options} - placeholder={t('pleaseSelect')} + placeholder={t('select')} onChange={value => onChange?.(ancestors, value)} > {FONT_SIZES.map(o => ( diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicInput.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicInput.tsx index d44287f24..a5039586f 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicInput.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicInput.tsx @@ -18,23 +18,38 @@ import { Input } from 'antd'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; -import { FC, memo } from 'react'; +import debounce from 'lodash/debounce'; +import { FC, memo, useMemo, useState } from 'react'; import styled from 'styled-components/macro'; import { ItemLayoutProps } from '../types'; import { itemLayoutComparer } from '../utils'; import { BW } from './components/BasicWrapper'; const BasicInput: FC> = memo( - ({ ancestors, translate: t = title => title, data: row, onChange }) => { - const { comType, options, ...rest } = row; - - const handleChangeByEvent = e => { - onChange?.(ancestors, e.target?.value); - }; + ({ ancestors, translate: t = title => title, data, onChange }) => { + const [cache, setCache] = useState(data); + const { comType, options, ...rest } = cache; + const debouncedDataChange = useMemo( + () => + debounce(value => { + onChange?.(ancestors, value, options?.needRefresh); + }, 500), + [ancestors, onChange, options?.needRefresh], + ); return ( - - + + { + const newCache = Object.assign({}, cache, { + value: value.target?.value, + }); + setCache(newCache); + debouncedDataChange(newCache.value); + }} + /> ); }, diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicInputNumber.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicInputNumber.tsx index 70a590005..60ee831b3 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicInputNumber.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicInputNumber.tsx @@ -18,7 +18,8 @@ import { InputNumber } from 'antd'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; -import { FC, memo } from 'react'; +import debounce from 'lodash/debounce'; +import { FC, memo, useMemo, useState } from 'react'; import styled from 'styled-components/macro'; import { BORDER_RADIUS } from 'styles/StyleConstants'; import { ItemLayoutProps } from '../types'; @@ -26,16 +27,29 @@ import { itemLayoutComparer } from '../utils'; import { BW } from './components/BasicWrapper'; const BasicInputNumber: FC> = memo( - ({ ancestors, translate: t = title => title, data: row, onChange }) => { - const { comType, options, ...rest } = row; + ({ ancestors, translate: t = title => title, data, onChange }) => { + const [cache, setCache] = useState(data); + const { comType, options, ...rest } = cache; + + const debouncedDataChange = useMemo( + () => + debounce(value => { + onChange?.(ancestors, value, options?.needRefresh); + }, 500), + [ancestors, onChange, options?.needRefresh], + ); return ( - + onChange?.(ancestors, value)} - defaultValue={rest?.default} + onChange={value => { + const newCache = Object.assign({}, cache, { value }); + setCache(newCache); + debouncedDataChange(newCache.value); + }} + defaultValue={cache?.default} /> ); diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicLine.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicLine.tsx index 6b37edd90..8540bf3f4 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicLine.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicLine.tsx @@ -17,7 +17,7 @@ */ import { Select } from 'antd'; -import { ColorPickerPopover } from 'app/components/ReactColorPicker'; +import { ColorPickerPopover } from 'app/components/ColorPicker'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; import { updateByKey } from 'app/utils/mutation'; import { CHART_LINE_STYLES, CHART_LINE_WIDTH } from 'globalConstants'; @@ -47,7 +47,7 @@ const BasicLine: FC> = memo( > = memo( ))} > = memo( + ({ ancestors, translate: t = title => title, data: row, onChange }) => { + const { value, comType, options, ...rest } = row; + const items = options?.items || []; + const needTranslate = !!options?.translateItemLabel; + + const handleValueChange = e => { + const newValue = e.target.value; + onChange?.(ancestors, newValue, options?.needRefresh); + }; + + return ( + + + {items?.map(o => { + return ( + + {needTranslate ? t(o.label) : o?.label} + + ); + })} + + + ); + }, + itemLayoutComparer, +); + +export default BasicRadio; + +const StyledBasicRadio = styled(BW)``; diff --git a/frontend/src/app/components/FormGenerator/Basic/BaiscSelector.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicSelector.tsx similarity index 88% rename from frontend/src/app/components/FormGenerator/Basic/BaiscSelector.tsx rename to frontend/src/app/components/FormGenerator/Basic/BasicSelector.tsx index d3aae288a..5c510a352 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BaiscSelector.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicSelector.tsx @@ -26,7 +26,7 @@ import { ItemLayoutProps } from '../types'; import { itemLayoutComparer } from '../utils'; import { BW } from './components/BasicWrapper'; -const BaiscSelector: FC> = memo( +const BasicSelector: FC> = memo( ({ ancestors, translate: t = title => title, @@ -36,6 +36,7 @@ const BaiscSelector: FC> = memo( }) => { const { comType, options, ...rest } = row; const hideLabel = !!options?.hideLabel; + const needTranslate = !!options?.translateItemLabel; const handleSelectorValueChange = value => { onChange?.(ancestors, value, options?.needRefresh); @@ -51,7 +52,10 @@ const BaiscSelector: FC> = memo( try { results = typeof row?.options?.getItems === 'function' - ? row?.options?.getItems.call(null, getDataConfigs()) || [] + ? row?.options?.getItems.call( + Object.create(null), + getDataConfigs(), + ) || [] : row?.options?.items || []; } catch (error) { console.error( @@ -70,7 +74,7 @@ const BaiscSelector: FC> = memo( {...rest} {...options} defaultValue={rest.default} - placeholder={t('pleaseSelect')} + placeholder={t('select')} onChange={handleSelectorValueChange} > {safeInvokeAction()?.map((o, index) => { @@ -79,7 +83,7 @@ const BaiscSelector: FC> = memo( const value = isEmpty(o['value']) ? o : o.value; return ( - {label} + {needTranslate ? t(label) : label} ); })} @@ -90,7 +94,7 @@ const BaiscSelector: FC> = memo( itemLayoutComparer, ); -export default BaiscSelector; +export default BasicSelector; const Wrapper = styled(BW)` .ant-select { diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicSwitch.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicSwitch.tsx index 3725fb893..1e9b9c2bb 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicSwitch.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicSwitch.tsx @@ -52,7 +52,7 @@ const BasicSwitch: FC> = memo( onChange?.(ancestors, newRow, needRefresh); }; - const hanldeSwitchChange = value => { + const handleSwitchChange = value => { const newRow = updateByKey(row, 'value', value); onChange?.(ancestors, newRow); }; @@ -74,7 +74,7 @@ const BasicSwitch: FC> = memo( {...rest} {...options} checked={row.value} - onChange={hanldeSwitchChange} + onChange={handleSwitchChange} /> diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicUnControlledTabPanel.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicUnControlledTabPanel.tsx index e5ed5366d..40ad11f91 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicUnControlledTabPanel.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicUnControlledTabPanel.tsx @@ -33,7 +33,7 @@ import { isEmpty, resetValue, } from 'utils/object'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import GroupLayout from '../Layout/GroupLayout'; import { GroupLayoutMode, ItemLayoutProps } from '../types'; import { itemLayoutComparer } from '../utils'; diff --git a/frontend/src/app/components/FormGenerator/Basic/index.ts b/frontend/src/app/components/FormGenerator/Basic/index.ts index d7a7fd385..9e11f3ee8 100644 --- a/frontend/src/app/components/FormGenerator/Basic/index.ts +++ b/frontend/src/app/components/FormGenerator/Basic/index.ts @@ -15,8 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export { default as BaiscSelector } from './BaiscSelector'; export { default as BasicCheckbox } from './BasicCheckbox'; export { default as BasicColorSelector } from './BasicColorSelector'; export { default as BasicFont } from './BasicFont'; @@ -27,6 +25,8 @@ export { default as BasicInputNumber } from './BasicInputNumber'; export { default as BasicInputPercentage } from './BasicInputPercentage'; export { default as BasicLine } from './BasicLine'; export { default as BasicMarginWidth } from './BasicMarginWidth'; +export { default as BasicRadio } from './BasicRadio'; +export { default as BasicSelector } from './BasicSelector'; export { default as BasicSlider } from './BasicSlider'; export { default as BasicSwitch } from './BasicSwitch'; export { default as BasicText } from './BasicText'; diff --git a/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/ConditionalStylePanel.tsx b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/ConditionalStylePanel.tsx new file mode 100644 index 000000000..9d33faf5c --- /dev/null +++ b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/ConditionalStylePanel.tsx @@ -0,0 +1,186 @@ +/** + * 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 { Button, Col, Popconfirm, Row, Space, Table, Tag } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; +import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; +import { FC, memo, useState } from 'react'; +import styled from 'styled-components/macro'; +import { CloneValueDeep } from 'utils/object'; +import { uuidv4 } from 'utils/utils'; +import { ItemLayoutProps } from '../../types'; +import { itemLayoutComparer } from '../../utils'; +import AddModal from './add'; +import { ConditionStyleFormValues } from './types'; + +const ConditionStylePanel: FC> = memo( + ({ + ancestors, + translate: t = title => title, + data, + onChange, + dataConfigs, + context, + }) => { + const [myData] = useState(() => CloneValueDeep(data)); + const [visible, setVisible] = useState(false); + const [dataSource, setDataSource] = useState( + myData.value || [], + ); + + const [currentItem, setCurrentItem] = useState( + {} as ConditionStyleFormValues, + ); + const onEditItem = (values: ConditionStyleFormValues) => { + setCurrentItem(CloneValueDeep(values)); + openConditionStyle(); + }; + const onRemoveItem = (values: ConditionStyleFormValues) => { + const result: ConditionStyleFormValues[] = dataSource.filter( + item => item.uid !== values.uid, + ); + + setDataSource(result); + onChange?.(ancestors, { + ...myData, + value: result, + }); + }; + + const tableColumnsSettings: ColumnsType = [ + { + title: t('conditionStyleTable.header.range.title'), + dataIndex: 'range', + width: 100, + render: (_, { range }) => ( + {t(`conditionStyleTable.header.range.${range}`)} + ), + }, + { + title: t('conditionStyleTable.header.operator'), + dataIndex: 'operator', + }, + { + title: t('conditionStyleTable.header.value'), + dataIndex: 'value', + render: (_, { value }) => <>{JSON.stringify(value)}>, + }, + { + title: t('conditionStyleTable.header.color.title'), + dataIndex: 'value', + render: (_, { color }) => ( + <> + + {t('conditionStyleTable.header.color.background')} + + + {t('conditionStyleTable.header.color.text')} + + > + ), + }, + { + title: t('conditionStyleTable.header.action'), + dataIndex: 'action', + width: 140, + render: (_, record) => { + return [ + onEditItem(record)}> + {t('conditionStyleTable.btn.edit')} + , + onRemoveItem(record)} + > + + {t('conditionStyleTable.btn.remove')} + + , + ]; + }, + }, + ]; + + const openConditionStyle = () => { + setVisible(true); + }; + const closeConditionStyleModal = () => { + setVisible(false); + setCurrentItem({} as ConditionStyleFormValues); + }; + const submitConditionStyleModal = (values: ConditionStyleFormValues) => { + let result: ConditionStyleFormValues[] = []; + + if (values.uid) { + result = dataSource.map(item => { + if (item.uid === values.uid) { + return values; + } + return item; + }); + } else { + result = [...dataSource, { ...values, uid: uuidv4() }]; + } + + setDataSource(result); + closeConditionStyleModal(); + onChange?.(ancestors, { + ...myData, + value: result, + }); + }; + + return ( + + + {t('conditionStyleTable.btn.add')} + + + + + bordered={true} + size="small" + pagination={false} + rowKey={record => record.uid!} + columns={tableColumnsSettings} + dataSource={dataSource} + /> + + + + + ); + }, + itemLayoutComparer, +); + +const StyledConditionStylePanel = styled(Space)` + width: 100%; + margin-top: 10px; +`; + +export default ConditionStylePanel; diff --git a/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/add.tsx b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/add.tsx new file mode 100644 index 000000000..f9394bde6 --- /dev/null +++ b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/add.tsx @@ -0,0 +1,294 @@ +import { Col, Form, Input, InputNumber, Modal, Radio, 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 { + ConditionOperatorTypes, + ConditionStyleFormValues, + ConditionStyleRange, + OperatorTypes, + OperatorTypesLocale, +} from './types'; + +interface AddProps { + context?: any; + translate?: (title: string, options?: any) => string; + visible: boolean; + values: ConditionStyleFormValues; + onOk: (values: ConditionStyleFormValues) => void; + onCancel: () => void; +} + +export default function Add({ + translate: t = title => title, + values, + visible, + onOk, + onCancel, + context: { label, type }, +}: AddProps) { + const [colors] = useState([ + { + name: 'background', + label: t('conditionStyleTable.header.color.background'), + value: undefined, + }, + { + name: 'textColor', + label: t('conditionStyleTable.header.color.text'), + value: undefined, + }, + ]); + const [operatorSelect, setOperatorSelect] = useState< + { label: string; value: string }[] + >([]); + const [operatorValue, setOperatorValue] = useState( + OperatorTypes.Equal, + ); + const [form] = Form.useForm(); + + useEffect(() => { + if (type) { + setOperatorSelect( + ConditionOperatorTypes[type]?.map(item => ({ + label: `${OperatorTypesLocale[item]} [${item}]`, + value: item, + })), + ); + } else { + setOperatorSelect([]); + } + }, [type]); + + useEffect(() => { + // !重置form + if (visible) { + const result: Partial = + Object.keys(values).length === 0 + ? { + range: ConditionStyleRange.Cell, + operator: OperatorTypes.Equal, + } + : values; + + form.setFieldsValue(result); + setOperatorValue(result.operator ?? OperatorTypes.Equal); + } + }, [form, visible, values, label]); + + const modalOk = () => { + form.validateFields().then(values => { + onOk({ + ...values, + target: { + name: label, + 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 ( + {t('conditionStyleTable.modal.notFoundContent')}> + } + /> + ); + case OperatorTypes.Between: + return ; + default: + return DefaultNode; + } + }; + + return ( + + + + + + + + + + {t('conditionStyleTable.header.range.cell')} + + + {t('conditionStyleTable.header.range.row')} + + + + + + + + + {operatorValue !== OperatorTypes.IsNull ? ( + + {renderValueNode()} + + ) : null} + + + + {colors.map(({ label, value, name }) => ( + + + + ))} + + + + + ); +} + +const ColorSelector = memo( + ({ + label, + value, + onChange, + }: { + label: string; + value?: string; + onChange?: (value: any) => void; + }) => { + return ( + <> + {label} + + + + + + > + ); + }, +); + +const InputNumberScope = memo( + ({ + value, + onChange, + }: { + value?: [number, number]; + onChange?: (value: any) => void; + }) => { + const [[min, max], setState] = useState< + [number | undefined, number | undefined] + >([undefined, undefined]); + const [index, setIndex] = useState(0); + + useEffect(() => { + setIndex(index + 1); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (Array.isArray(value)) { + setState([Number(value[0]), Number(value[1])]); + } else { + setState([value, undefined]); + } + }, [value]); + + const inputNumberScopeChange = state => { + setState(state); + const result = state.filter(num => typeof num === 'number'); + if (result.length === 2) { + onChange?.(state); + } else { + onChange?.(undefined); + } + }; + + const minChange = (value: number) => inputNumberScopeChange([value, max]); + const maxChange = (value: number) => inputNumberScopeChange([min, value]); + + return ( + + + + + - + + + + + ); + }, +); + +const StyledColor = styled.div` + width: 16px; + height: 16px; + background-color: ${props => props.color}; + position: relative; + cursor: pointer; + ::after { + position: absolute; + top: -7px; + left: -7px; + display: inline-block; + width: 30px; + height: 30px; + border-radius: 5px; + border: 1px solid #d9d9d9; + content: ''; + } +`; diff --git a/frontend/src/locales/moment.ts b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/index.tsx similarity index 79% rename from frontend/src/locales/moment.ts rename to frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/index.tsx index bbd0a4353..f071c67bc 100644 --- a/frontend/src/locales/moment.ts +++ b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/index.tsx @@ -16,10 +16,7 @@ * limitations under the License. */ -// TODO: add more language here -import moment from 'moment'; -import 'moment/locale/zh-cn'; +import ConditionStylePanel from './ConditionalStylePanel'; -export function setLocale(locale) { - moment.locale(locale); -} +export * from './types'; +export default ConditionStylePanel; diff --git a/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/types.ts b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/types.ts new file mode 100644 index 000000000..7bf87dc10 --- /dev/null +++ b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/types.ts @@ -0,0 +1,74 @@ +import { ColumnTypes } from 'app/pages/MainPage/pages/ViewPage/constants'; + +export interface ConditionStyleFormValues { + uid: string; + target: { name: string; type: any }; + range: ConditionStyleRange; + operator: OperatorTypes; + value: string; + color: { background: string; textColor: string }; +} + +export enum ConditionStyleRange { + Cell = 'cell', + Row = 'row', +} + +export enum OperatorTypes { + Equal = '=', + NotEqual = '!=', + Contain = 'like', + NotContain = 'not like', + Between = 'between', + In = 'in', + NotIn = 'not in', + LessThan = '<', + GreaterThan = '>', + LessThanOrEqual = '<=', + GreaterThanOrEqual = '>=', + IsNull = 'is null', +} + +export const OperatorTypesLocale = { + [OperatorTypes.Equal]: '等于', + [OperatorTypes.NotEqual]: '不等于', + [OperatorTypes.Contain]: '包含', + [OperatorTypes.NotContain]: '不包含', + [OperatorTypes.In]: '在……范围内', + [OperatorTypes.NotIn]: '不在……范围内', + [OperatorTypes.Between]: '在……之间', + [OperatorTypes.LessThan]: '小于', + [OperatorTypes.GreaterThan]: '大于', + [OperatorTypes.LessThanOrEqual]: '小于等于', + [OperatorTypes.GreaterThanOrEqual]: '大于等于', + [OperatorTypes.IsNull]: '空值', +}; + +export const ConditionOperatorTypes = { + [ColumnTypes.String]: [ + OperatorTypes.Equal, + OperatorTypes.NotEqual, + OperatorTypes.Contain, + OperatorTypes.NotContain, + OperatorTypes.In, + OperatorTypes.NotIn, + OperatorTypes.IsNull, + ], + [ColumnTypes.Number]: [ + OperatorTypes.Equal, + OperatorTypes.NotEqual, + OperatorTypes.Between, + OperatorTypes.LessThan, + OperatorTypes.GreaterThan, + OperatorTypes.LessThanOrEqual, + OperatorTypes.GreaterThanOrEqual, + OperatorTypes.IsNull, + ], + [ColumnTypes.Date]: [ + OperatorTypes.Equal, + OperatorTypes.NotEqual, + OperatorTypes.In, + OperatorTypes.NotIn, + OperatorTypes.IsNull, + ], +}; diff --git a/frontend/src/app/components/FormGenerator/Customize/DataCachePanel.tsx b/frontend/src/app/components/FormGenerator/Customize/DataCachePanel.tsx deleted file mode 100644 index f6cb5f3f1..000000000 --- a/frontend/src/app/components/FormGenerator/Customize/DataCachePanel.tsx +++ /dev/null @@ -1,87 +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. - */ - -import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; -import { updateByKey } from 'app/utils/mutation'; -import { FC, memo, useEffect } from 'react'; -import { CloneValueDeep, mergeDefaultToValue } from 'utils/object'; -import { GroupLayout } from '../Layout'; -import { GroupLayoutMode, ItemLayoutProps } from '../types'; -import { itemLayoutComparer } from '../utils'; - -const defaultRows = [ - { - label: 'displayCount', - key: 'displayCount', - default: 10, - comType: 'inputNumber', - }, - { - label: 'autoLoad', - key: 'autoLoad', - default: true, - comType: 'switch', - }, - { - label: 'enableRaw', - key: 'enableRaw', - default: false, - comType: 'switch', - }, -]; - -const DataCachePanel: FC> = memo( - ({ - ancestors, - translate: t = title => title, - data: row, - dataConfigs, - onChange, - }) => { - useEffect(() => { - if (!row.rows || row.rows.length === 0) { - onChange?.( - ancestors, - updateByKey( - row, - 'rows', - mergeDefaultToValue(CloneValueDeep(defaultRows)), - ), - ); - } - }, [row]); - - const handleOnChange = (ancestors, value) => { - onChange?.(ancestors, value, true); - }; - - return ( - - ); - }, - itemLayoutComparer, -); - -export default DataCachePanel; diff --git a/frontend/src/app/components/FormGenerator/Customize/FontAlignment.tsx b/frontend/src/app/components/FormGenerator/Customize/FontAlignment.tsx new file mode 100644 index 000000000..ec503e9f7 --- /dev/null +++ b/frontend/src/app/components/FormGenerator/Customize/FontAlignment.tsx @@ -0,0 +1,76 @@ +/** + * 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 { ChartStyleSectionConfig } from 'app/types/ChartConfig'; +import { FC, memo } from 'react'; +import { ItemLayout } from '../Layout'; +import { ItemLayoutProps } from '../types'; +import { itemLayoutComparer } from '../utils'; + +const template = { + label: '@global@.viz.common.enum.fontAlignment.alignment', + key: 'align', + default: 'left', + comType: 'select', + options: { + translateItemLabel: true, + items: [ + { + label: '@global@.viz.common.enum.fontAlignment.left', + value: 'left', + }, + { + label: '@global@.viz.common.enum.fontAlignment.center', + value: 'center', + }, + { + label: '@global@.viz.common.enum.fontAlignment.right', + value: 'right', + }, + ], + }, +}; + +const FontAlignment: FC> = memo( + ({ + ancestors, + translate: t = title => title, + data, + dataConfigs, + onChange, + }) => { + const props = { + ancestors, + data: Object.assign({}, data, { + label: data?.label || template.label, + key: data?.key || template.key, + default: data?.default || template.default, + options: data?.options || template.options, + comType: 'select', + }), + translate: t, + onChange, + dataConfigs, + }; + + return ; + }, + itemLayoutComparer, +); + +export default FontAlignment; diff --git a/frontend/src/app/components/FormGenerator/Customize/ListTemplatePanel.tsx b/frontend/src/app/components/FormGenerator/Customize/ListTemplatePanel.tsx index b96b72c96..451b90fb4 100644 --- a/frontend/src/app/components/FormGenerator/Customize/ListTemplatePanel.tsx +++ b/frontend/src/app/components/FormGenerator/Customize/ListTemplatePanel.tsx @@ -136,6 +136,7 @@ const ListTemplatePanel: FC> = memo( data={r} translate={t} onChange={handleChildComponentUpdate(r.key)} + context={currentSelectedItem} /> ); }; @@ -146,7 +147,7 @@ const ListTemplatePanel: FC> = memo( diff --git a/frontend/src/app/components/FormGenerator/Customize/UnControlledTableHeaderPanel.tsx b/frontend/src/app/components/FormGenerator/Customize/UnControlledTableHeaderPanel.tsx index 303474368..d2a3d7cc2 100644 --- a/frontend/src/app/components/FormGenerator/Customize/UnControlledTableHeaderPanel.tsx +++ b/frontend/src/app/components/FormGenerator/Customize/UnControlledTableHeaderPanel.tsx @@ -22,22 +22,22 @@ import { CheckOutlined, DeleteOutlined, EditOutlined, + RedoOutlined, } from '@ant-design/icons'; import { Button, Col, Input, Row, Space, Table } from 'antd'; import { + ChartDataSectionConfig, ChartDataSectionType, ChartStyleSectionConfig, } from 'app/types/ChartConfig'; import { - diffHeaderRows, - flattenHeaderRowsWithoutGroupRow, getColumnRenderName, + getUnusedHeaderRows, } from 'app/utils/chartHelper'; import { DATARTSEPERATOR } from 'globalConstants'; import { FC, memo, useState } from 'react'; import styled from 'styled-components'; import { CloneValueDeep } from 'utils/object'; -import { BaiscSelector, BasicColorSelector, BasicFont } from '../Basic'; import { ItemLayoutProps } from '../types'; import { itemLayoutComparer } from '../utils'; @@ -57,6 +57,18 @@ interface RowValue { children?: RowValue[]; } +const getFlattenHeaders = (dataConfigs: ChartDataSectionConfig[] = []) => { + const newDataConfigs = CloneValueDeep(dataConfigs); + return newDataConfigs + .filter( + c => + ChartDataSectionType.AGGREGATE === c.type || + ChartDataSectionType.GROUP === c.type || + ChartDataSectionType.MIXED === c.type, + ) + .flatMap(config => config.rows || []); +}; + const UnControlledTableHeaderPanel: FC< ItemLayoutProps > = memo( @@ -70,43 +82,13 @@ const UnControlledTableHeaderPanel: FC< const [selectedRowUids, setSelectedRowUids] = useState([]); const [myData, setMyData] = useState(() => CloneValueDeep(data)); const [tableDataSource, setTableDataSource] = useState(() => { - const currentHeaderRows = (CloneValueDeep(dataConfigs) || []) - .filter( - c => - ChartDataSectionType.AGGREGATE === c.type || - ChartDataSectionType.GROUP === c.type || - ChartDataSectionType.MIXED === c.type, - ) - .flatMap(config => config.rows || []); - - const oldGroupedHeaderRows: RowValue[] = myData?.value || []; - const oldFlattenedHeaderRows: RowValue[] = oldGroupedHeaderRows.flatMap( - row => flattenHeaderRowsWithoutGroupRow(row), - ); - const isChanged = diffHeaderRows( - oldFlattenedHeaderRows, + const originalFlattenHeaderRows = getFlattenHeaders(dataConfigs); + const currentHeaderRows: RowValue[] = myData?.value || []; + const unusedHeaderRows = getUnusedHeaderRows( + originalFlattenHeaderRows || [], currentHeaderRows, ); - if (!isChanged) { - oldFlattenedHeaderRows.forEach(oldRow => { - const current = currentHeaderRows?.find(v => v.uid === oldRow.uid); - Object.assign(oldRow, current); - }); - return oldGroupedHeaderRows; - } - - return (CloneValueDeep(dataConfigs) || []) - .filter( - c => - ChartDataSectionType.AGGREGATE === c.type || - ChartDataSectionType.GROUP === c.type || - ChartDataSectionType.MIXED === c.type, - ) - .flatMap(config => config.rows || []) - .map(r => { - const previous = oldFlattenedHeaderRows?.find(v => v.uid === r.uid); - return { ...previous, ...r }; - }); + return currentHeaderRows.concat(unusedHeaderRows); }); const mergeRowToGroup = () => { @@ -120,7 +102,6 @@ const UnControlledTableHeaderPanel: FC< mergeSameLineageAncesterRows(lineageRowUids); const ancestorsRows = makeSameLinageRows(noDuplicateLineageRows); const newDataSource = groupTreeNode(ancestorsRows, tableDataSource); - handleConfigChange([...newDataSource]); }; @@ -175,7 +156,7 @@ const UnControlledTableHeaderPanel: FC< }; const groupTreeNode = (rowAncestors, collection) => { - if (rowAncestors && rowAncestors.length <= 1) { + if (rowAncestors && rowAncestors.length < 1) { return collection; } @@ -201,7 +182,7 @@ const UnControlledTableHeaderPanel: FC< const groupRow = { uid: groupRowUid, colName: groupRowUid, - label: 'Please input header name', + label: t('table.header.newName'), isGroup: true, children: selectedRows, }; @@ -239,6 +220,11 @@ const UnControlledTableHeaderPanel: FC< }); }; + const handleRollback = () => { + const originalFlattenHeaders = getFlattenHeaders(dataConfigs); + handleConfigChange?.(originalFlattenHeaders); + }; + const handleTableRowChange = rowUid => style => prop => (_, value) => { const brotherRows = findRowBrothers(rowUid, tableDataSource); const row = brotherRows.find(r => r.uid === rowUid); @@ -295,110 +281,22 @@ const UnControlledTableHeaderPanel: FC< const { label, isGroup, uid } = record; return isGroup ? ( <> + handleDeleteGroupRow(uid)} + /> handleTableRowChange(uid)(undefined)('label')([], value) } /> - handleDeleteGroupRow(uid)} /> > ) : ( getColumnRenderName(record) ); }, }, - { - title: t('table.header.backgroundColor'), - dataIndex: 'backgroundColor', - key: 'backgroundColor', - width: 100, - render: (_, record) => { - const { style, uid } = record; - const row = { - label: 'column.backgroundColor', - key: 'backgroundColor', - comType: 'fontColor', - value: style?.backgroundColor, - options: { - hideLabel: true, - }, - }; - return ( - - ); - }, - }, - { - title: t('table.header.font'), - dataIndex: 'font', - key: 'font', - width: 500, - render: (_, record) => { - const { style, uid } = record; - const row = { - label: 'column.font', - key: 'font', - comType: 'font', - value: style?.font?.value, - options: { - hideLabel: true, - }, - default: { - fontFamily: 'PingFang SC', - fontSize: '12', - fontWeight: 'normal', - fontStyle: 'normal', - color: 'black', - }, - }; - return ( - - ); - }, - }, - { - title: t('table.header.align.title'), - dataIndex: 'align', - key: 'align', - width: 150, - render: (_, record) => { - const { style, uid } = record; - const row = { - label: 'column.align', - key: 'align', - comType: 'select', - default: 'left', - value: style?.align, - options: { - hideLabel: true, - items: [ - { label: t('table.header.align.left'), value: 'left' }, - { label: t('table.header.align.center'), value: 'center' }, - { label: t('table.header.align.right'), value: 'right' }, - ], - }, - }; - return ( - - ); - }, - }, ]; const rowSelection = { @@ -411,39 +309,43 @@ const UnControlledTableHeaderPanel: FC< return ( - - - {t('table.header.merge')} - - + + + {t('table.header.merge')} + + } + onClick={handleRowMoveUp} + > + {t('table.header.moveUp')} + + } + onClick={handleRowMoveDown} + > + {t('table.header.moveDown')} + + + + - - } - onClick={handleRowMoveUp} - > - {t('table.header.moveUp')} - - } - onClick={handleRowMoveDown} - > - {t('table.header.moveDown')} - - + } onClick={handleRollback}> + {t('table.header.reset')} + {label} } onClick={() => setIsEditing(true)} > @@ -495,9 +398,13 @@ const EditableLabel: FC<{ ); }; - return render(); + return {render()}; }); +const StyledEditableLabel = styled.div` + display: inline-block; +`; + const StyledUnControlledTableHeaderPanel = styled(Space)` width: 100%; margin-top: 10px; diff --git a/frontend/src/app/components/FormGenerator/Customize/index.ts b/frontend/src/app/components/FormGenerator/Customize/index.ts index 5d7fdb711..bcdb6f591 100644 --- a/frontend/src/app/components/FormGenerator/Customize/index.ts +++ b/frontend/src/app/components/FormGenerator/Customize/index.ts @@ -16,7 +16,8 @@ * limitations under the License. */ -export { default as DataCachePanel } from './DataCachePanel'; +export { default as ConditionStylePanel } from './ConditionStylePanel'; export { default as DataReferencePanel } from './DataReferencePanel'; +export { default as FontAlignment } from './FontAlignment'; export { default as ListTemplatePanel } from './ListTemplatePanel'; export { default as UnControlledTableHeaderPanel } from './UnControlledTableHeaderPanel'; diff --git a/frontend/src/app/components/FormGenerator/Layout/CollectionLayout.tsx b/frontend/src/app/components/FormGenerator/Layout/CollectionLayout.tsx index 587d63bf8..48abeb565 100644 --- a/frontend/src/app/components/FormGenerator/Layout/CollectionLayout.tsx +++ b/frontend/src/app/components/FormGenerator/Layout/CollectionLayout.tsx @@ -41,34 +41,46 @@ import { groupLayoutComparer } from '../utils'; import ItemLayout from './ItemLayout'; const CollectionLayout: FC> = - memo(({ ancestors, translate, data, dataConfigs, flatten, onChange }) => { - const getDependencyValue = useCallback((watcher, children) => { - if (watcher?.deps) { - // Note: only support depend on one property for now. - const dependencyKey = watcher?.deps?.[0]; - return children?.find(r => r.key === dependencyKey)?.value; - } - }, []); + memo( + ({ + ancestors, + translate, + data, + dataConfigs, + flatten, + onChange, + context, + }) => { + const getDependencyValue = useCallback((watcher, children) => { + if (watcher?.deps) { + // Note: only support depend on one property for now. + const dependencyKey = watcher?.deps?.[0]; + return children?.find(r => r.key === dependencyKey)?.value; + } + }, []); - return ( - - {data?.rows - ?.filter(r => Boolean(!r.hide)) - .map((r, index) => ( - - ))} - - ); - }, groupLayoutComparer); + return ( + + {data?.rows + ?.filter(r => Boolean(!r.hide)) + .map((r, index) => ( + + ))} + + ); + }, + groupLayoutComparer, + ); export default CollectionLayout; diff --git a/frontend/src/app/components/FormGenerator/Layout/GroupLayout.tsx b/frontend/src/app/components/FormGenerator/Layout/GroupLayout.tsx index f3242cb91..a25dea80f 100644 --- a/frontend/src/app/components/FormGenerator/Layout/GroupLayout.tsx +++ b/frontend/src/app/components/FormGenerator/Layout/GroupLayout.tsx @@ -17,7 +17,7 @@ */ import { Button, Collapse } from 'antd'; -import useStateModal from 'app/hooks/useStateModal'; +import useStateModal, { StateModalSize } from 'app/hooks/useStateModal'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; import { FC, memo, useState } from 'react'; import styled from 'styled-components/macro'; @@ -41,10 +41,13 @@ const GroupLayout: FC> = memo( dataConfigs, flatten, onChange, + context, }) => { const [openStateModal, contextHolder] = useStateModal({}); const [type] = useState(data?.options?.type || 'default'); - const [modalSize] = useState(data?.options?.modalSize || ''); + const [modalSize] = useState( + data?.options?.modalSize || StateModalSize.SMALL, + ); const [expand] = useState(!!data?.options?.expand); const handleConfrimModalDialogOrDataUpdate = ( @@ -112,6 +115,7 @@ const GroupLayout: FC> = memo( dataConfigs={dataConfigs} flatten={flatten} onChange={onChangeEvent} + context={context} /> ); }; diff --git a/frontend/src/app/components/FormGenerator/Layout/ItemLayout.tsx b/frontend/src/app/components/FormGenerator/Layout/ItemLayout.tsx index 309c5e9a1..1d980fb29 100644 --- a/frontend/src/app/components/FormGenerator/Layout/ItemLayout.tsx +++ b/frontend/src/app/components/FormGenerator/Layout/ItemLayout.tsx @@ -32,7 +32,6 @@ import { } from 'utils/object'; import { GroupLayout } from '.'; import { - BaiscSelector, BasicCheckbox, BasicColorSelector, BasicFont, @@ -43,14 +42,17 @@ import { BasicInputPercentage, BasicLine, BasicMarginWidth, + BasicRadio, + BasicSelector, BasicSlider, BasicSwitch, BasicText, BasicUnControlledTabPanel, } from '../Basic'; import { - DataCachePanel, + ConditionStylePanel, DataReferencePanel, + FontAlignment, ListTemplatePanel, UnControlledTableHeaderPanel, } from '../Customize'; @@ -68,6 +70,7 @@ const ItemLayout: FC> = memo( onChange, dataConfigs, flatten, + context, }) => { useEffect(() => { const key = data?.watcher?.deps?.[0] as string; @@ -96,6 +99,7 @@ const ItemLayout: FC> = memo( }); onChange?.(ancestors, newData); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [dependency]); const handleDataChange = ( @@ -116,6 +120,7 @@ const ItemLayout: FC> = memo( translate, onChange: handleDataChange, dataConfigs, + context, }; switch (data.comType) { @@ -126,7 +131,7 @@ const ItemLayout: FC> = memo( case ChartStyleSectionComponentType.INPUT: return ; case ChartStyleSectionComponentType.SELECT: - return ; + return ; case ChartStyleSectionComponentType.TABS: return ; case ChartStyleSectionComponentType.FONT: @@ -149,16 +154,20 @@ const ItemLayout: FC> = memo( return ; case ChartStyleSectionComponentType.LINE: return ; - case ChartStyleSectionComponentType.CACHE: - return ; case ChartStyleSectionComponentType.REFERENCE: return ; case ChartStyleSectionComponentType.TABLEHEADER: return ; + case ChartStyleSectionComponentType.CONDITIONSTYLE: + return ; case ChartStyleSectionComponentType.GROUP: return ; case ChartStyleSectionComponentType.TEXT: return ; + case ChartStyleSectionComponentType.RADIO: + return ; + case ChartStyleSectionComponentType.FontAlignment: + return ; default: return {`no matched component comType of ${data.comType}`}; } diff --git a/frontend/src/app/components/FormGenerator/types.ts b/frontend/src/app/components/FormGenerator/types.ts index 539ad3a3f..cddf38daa 100644 --- a/frontend/src/app/components/FormGenerator/types.ts +++ b/frontend/src/app/components/FormGenerator/types.ts @@ -20,6 +20,7 @@ export interface ItemLayoutProps { ) => void; dataConfigs?: ChartDataSectionConfig[]; flatten?: boolean; + context?: any; } export interface FormGeneratorLayoutProps extends ItemLayoutProps { diff --git a/frontend/src/app/components/From/FormItemEx.tsx b/frontend/src/app/components/From/FormItemEx.tsx index ab326cdb9..2672671b2 100644 --- a/frontend/src/app/components/From/FormItemEx.tsx +++ b/frontend/src/app/components/From/FormItemEx.tsx @@ -38,4 +38,8 @@ const StyledFromItemEx = styled(Form.Item)` .ant-form-item-explain { padding-left: 10px; } + + .ant-form-item-control-input { + width: 100%; + } `; diff --git a/frontend/src/app/components/ListItem.tsx b/frontend/src/app/components/ListItem.tsx index cf79298a9..40d21fd9d 100644 --- a/frontend/src/app/components/ListItem.tsx +++ b/frontend/src/app/components/ListItem.tsx @@ -47,11 +47,6 @@ const StyledItem = styled(Item)` color: ${p => p.theme.textColorSnd}; text-overflow: ellipsis; white-space: nowrap; - - > span { - margin-right: ${SPACE_XS}; - color: ${p => p.theme.textColorDisabled}; - } } &.with-avatar { diff --git a/frontend/src/app/components/ListTitle/index.tsx b/frontend/src/app/components/ListTitle/index.tsx index ed9c30825..8e097f5bd 100644 --- a/frontend/src/app/components/ListTitle/index.tsx +++ b/frontend/src/app/components/ListTitle/index.tsx @@ -1,6 +1,7 @@ import { LeftOutlined, MoreOutlined, SearchOutlined } from '@ant-design/icons'; import { Input, Menu, Space, Tooltip } from 'antd'; import { MenuListItem, Popup, ToolbarButton } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { ReactElement, useCallback, useState } from 'react'; import styled from 'styled-components/macro'; import { @@ -60,7 +61,7 @@ export function ListTitle({ onNext, }: ListTitleProps) { const [searchbarVisible, setSearchbarVisible] = useState(false); - + const t = useI18NPrefix('components.listTitle'); const toggleSearchbar = useCallback(() => { setSearchbarVisible(!searchbarVisible); }, [searchbarVisible]); @@ -88,7 +89,7 @@ export function ListTitle({ {subTitle && {subTitle}} {search && ( - + } @@ -129,7 +130,7 @@ export function ListTitle({ } - placeholder="搜索名称关键字" + placeholder={t('searchValue')} bordered={false} onChange={onSearch} /> @@ -157,17 +158,23 @@ const Title = styled.div` h3 { flex: 1; + overflow: hidden; padding: ${SPACE_MD} 0; font-size: ${FONT_SIZE_TITLE}; font-weight: ${FONT_WEIGHT_MEDIUM}; + text-overflow: ellipsis; + white-space: nowrap; } h5 { flex: 1; + overflow: hidden; padding: ${SPACE_XS} 0; font-size: ${FONT_SIZE_SUBTITLE}; font-weight: ${FONT_WEIGHT_MEDIUM}; color: ${p => p.theme.textColorLight}; + text-overflow: ellipsis; + white-space: nowrap; } .back { diff --git a/frontend/src/app/components/ModalForm.tsx b/frontend/src/app/components/ModalForm.tsx index 6f8b193f7..49bea9a8a 100644 --- a/frontend/src/app/components/ModalForm.tsx +++ b/frontend/src/app/components/ModalForm.tsx @@ -1,5 +1,6 @@ import { Form, FormProps, Modal, ModalProps } from 'antd'; -import { CommonFormTypes, COMMON_FORM_TITLE_PREFIX } from 'globalConstants'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { CommonFormTypes } from 'globalConstants'; import { forwardRef, ReactNode, useCallback, useImperativeHandle } from 'react'; export interface ModalFormProps extends ModalProps { @@ -15,6 +16,7 @@ export const ModalForm = forwardRef( ref, ) => { const [form] = Form.useForm(); + const tg = useI18NPrefix('global'); useImperativeHandle(ref, () => form); const onOk = useCallback(() => { @@ -29,7 +31,7 @@ export const ModalForm = forwardRef( return ( diff --git a/frontend/src/app/components/Popup/MenuListItem.tsx b/frontend/src/app/components/Popup/MenuListItem.tsx index a943f91d2..62c518892 100644 --- a/frontend/src/app/components/Popup/MenuListItem.tsx +++ b/frontend/src/app/components/Popup/MenuListItem.tsx @@ -1,5 +1,5 @@ import { Menu, MenuItemProps } from 'antd'; -import React, { cloneElement, ReactElement } from 'react'; +import React, { cloneElement, ReactElement, ReactNode } from 'react'; import styled, { css } from 'styled-components/macro'; import { LINE_HEIGHT_HEADING, SPACE, SPACE_XS } from 'styles/StyleConstants'; import { mergeClassNames } from 'utils/utils'; @@ -10,36 +10,68 @@ const WrapperStyle = css` &.selected { background-color: ${p => p.theme.emphasisBackground}; } + + .ant-dropdown-menu-submenu-title { + line-height: ${LINE_HEIGHT_HEADING}; + } `; interface MenuListItemProps extends Omit { prefix?: ReactElement; suffix?: ReactElement; + sub?: boolean; } export function MenuListItem({ prefix, suffix, + sub, ...menuProps }: MenuListItemProps) { - return ( + return sub ? ( + + {menuProps.title} + + } + > + {menuProps.children} + + ) : ( - - {prefix && - cloneElement(prefix, { - className: mergeClassNames(prefix.props.className, 'prefix'), - })} + {menuProps.children} - {suffix && - cloneElement(suffix, { - className: mergeClassNames(suffix.props.className, 'suffix'), - })} ); } -const ListItem = styled.div` +interface ListItemProps { + prefix?: ReactElement; + suffix?: ReactElement; + children?: ReactNode; +} + +function ListItem({ prefix, suffix, children }: ListItemProps) { + return ( + + {prefix && + cloneElement(prefix, { + className: mergeClassNames(prefix.props.className, 'prefix'), + })} + {children} + {suffix && + cloneElement(suffix, { + className: mergeClassNames(suffix.props.className, 'suffix'), + })} + + ); +} + +const StyledListItem = styled.div` display: flex; align-items: center; diff --git a/frontend/src/app/components/ReactColorPicker/ColorPanel.tsx b/frontend/src/app/components/ReactColorPicker/ColorPanel.tsx deleted file mode 100644 index e69f9ce0a..000000000 --- a/frontend/src/app/components/ReactColorPicker/ColorPanel.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { FC, useCallback } from 'react'; -import { ColorResult, SketchPicker, SketchPickerProps } from 'react-color'; -import styled from 'styled-components/macro'; -type ValueType = string | undefined; -export interface ColorPanelProps { - value?: ValueType; - onChange?: (value: ValueType, colorResult?: ColorResult) => void; - colors?: SketchPickerProps['presetColors']; -} -const toChangeValue = (data: ColorResult) => { - const { r, g, b, a } = data.rgb; - return `rgba(${r}, ${g}, ${b}, ${a})`; -}; -export const ReactColorPicker: FC = ({ value, onChange }) => { - const onChangeComplete = useCallback( - v => { - const rgbaValue = toChangeValue(v); - onChange?.(rgbaValue, v); - }, - [onChange], - ); - - return ( - - ); -}; - -const SketchPickerPanel = styled(SketchPicker)` - width: 260px !important; - padding: 0 !important; - border-radius: 0 !important; - box-shadow: none !important; -`; - -export type { ColorResult } from 'react-color'; diff --git a/frontend/src/app/components/ReactColorPicker/ColorPickerPopover.tsx b/frontend/src/app/components/ReactColorPicker/ColorPickerPopover.tsx deleted file mode 100644 index cced4054c..000000000 --- a/frontend/src/app/components/ReactColorPicker/ColorPickerPopover.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Button, Popover, PopoverProps, Row } from 'antd'; -import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components/macro'; -import { SPACE_MD } from 'styles/StyleConstants'; -import { ColorPanelProps, ColorResult, ReactColorPicker } from './ColorPanel'; -import { ColorPicker } from './ColorTag'; - -interface ColorPickerPopoverProps extends ColorPanelProps { - popoverProps?: PopoverProps; - defaultValue?: string; - onSubmit?: ColorPanelProps['onChange']; - colorPickerClass?: string; -} -export const ColorPickerPopover: FC> = - ({ children, defaultValue, popoverProps, onSubmit, colorPickerClass }) => { - const [visible, setVisible] = useState(false); - const [color, setColor] = useState(defaultValue); - const [colorResult, setColorResult] = useState(); - - useEffect(() => { - if (visible) { - setColor(defaultValue); - } - }, [visible, defaultValue]); - const onCancel = useCallback(() => { - setVisible(false); - }, []); - const onSure = useCallback(() => { - onSubmit?.(color, colorResult); - onCancel(); - }, [onSubmit, color, colorResult, onCancel]); - const onColorChange = useCallback((color, result) => { - setColor(color); - setColorResult(result); - }, []); - const _popoverProps = useMemo(() => { - return typeof popoverProps === 'object' ? popoverProps : {}; - }, [popoverProps]); - return ( - - - - - 取消 - - - 确定 - - - - } - trigger="click" - placement="right" - > - {children || ( - - )} - - ); - }; - -const ContentWrapper = styled.div` - .ant-btn-primary { - margin-left: ${SPACE_MD}; - } -`; diff --git a/frontend/src/app/components/ReactColorPicker/index.tsx b/frontend/src/app/components/ReactColorPicker/index.tsx deleted file mode 100644 index 489d25afb..000000000 --- a/frontend/src/app/components/ReactColorPicker/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { ReactColorPicker } from './ColorPanel'; -import { ColorPickerPopover } from './ColorPickerPopover'; -import { ColorTag } from './ColorTag'; -export { ReactColorPicker, ColorPickerPopover, ColorTag }; diff --git a/frontend/src/app/components/ReactFrameComponent/Content.jsx b/frontend/src/app/components/ReactFrameComponent/Content.jsx new file mode 100644 index 000000000..47b78362d --- /dev/null +++ b/frontend/src/app/components/ReactFrameComponent/Content.jsx @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types'; +import { Children, Component } from 'react'; + +export default class Content extends Component { + static propTypes = { + children: PropTypes.element.isRequired, + contentDidMount: PropTypes.func.isRequired, + contentDidUpdate: PropTypes.func.isRequired, + }; + + componentDidMount() { + this.props.contentDidMount(); + } + + componentDidUpdate() { + this.props.contentDidUpdate(); + } + + render() { + return Children.only(this.props.children); + } +} diff --git a/frontend/src/app/components/ReactFrameComponent/Context.jsx b/frontend/src/app/components/ReactFrameComponent/Context.jsx new file mode 100644 index 000000000..80c4cf24d --- /dev/null +++ b/frontend/src/app/components/ReactFrameComponent/Context.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +let doc; +let win; +if (typeof document !== 'undefined') { + doc = document; +} +if (typeof window !== 'undefined') { + win = window; +} + +export const FrameContext = React.createContext({ document: doc, window: win }); + +export const useFrame = () => React.useContext(FrameContext); + +export const { + Provider: FrameContextProvider, + Consumer: FrameContextConsumer, +} = FrameContext; diff --git a/frontend/src/app/components/ReactFrameComponent/Frame.jsx b/frontend/src/app/components/ReactFrameComponent/Frame.jsx new file mode 100644 index 000000000..4e412c5c2 --- /dev/null +++ b/frontend/src/app/components/ReactFrameComponent/Frame.jsx @@ -0,0 +1,134 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Content from './Content'; +import { FrameContextProvider } from './Context'; + +export class Frame extends Component { + // React warns when you render directly into the body since browser extensions + // also inject into the body and can mess up React. For this reason + // initialContent is expected to have a div inside of the body + // element that we render react into. + static propTypes = { + style: PropTypes.object, + head: PropTypes.node, + mountTarget: PropTypes.string, + contentDidMount: PropTypes.func, + contentDidUpdate: PropTypes.func, + children: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.arrayOf(PropTypes.element), + ]), + }; + + static defaultProps = { + style: {}, + head: null, + children: undefined, + mountTarget: undefined, + contentDidMount: () => {}, + contentDidUpdate: () => {}, + }; + + constructor(props, context) { + super(props, context); + this._isMounted = false; + this.nodeRef = React.createRef(); + this.state = { iframeLoaded: false }; + } + + componentDidMount() { + this._isMounted = true; + const doc = this.getDoc(); + if (doc && doc.readyState === 'complete') { + this.forceUpdate(); + } else { + this.nodeRef.current.addEventListener('load', this.handleLoad); + } + } + + componentWillUnmount() { + this._isMounted = false; + this.nodeRef.current.removeEventListener('load', this.handleLoad); + } + + getDoc() { + return this.nodeRef.current ? this.nodeRef.current.contentDocument : null; // eslint-disable-line + } + + getMountTarget() { + const doc = this.getDoc(); + if (this.props.mountTarget) { + return doc.querySelector(this.props.mountTarget); + } + return doc.body; + } + + setRef = node => { + this.nodeRef.current = node; + if (!this.nodeRef.current?.contentWindow) { + return; + } + }; + + handleLoad = () => { + this.setState({ iframeLoaded: true }); + }; + + renderFrameContents() { + if (!this._isMounted) { + return null; + } + const doc = this.getDoc(); + const contentDidMount = this.props.contentDidMount; + const contentDidUpdate = this.props.contentDidUpdate; + + const win = doc.defaultView || doc.parentView; + const contents = ( + + + {this.props.children} + + + ); + + const mountTarget = this.getMountTarget(); + const res = [ + ReactDOM.createPortal(this.props.head, this.getDoc().head), + ReactDOM.createPortal(contents, mountTarget), + ]; + + return res; + } + + render() { + const props = { + ...this.props, + children: undefined, // The iframe isn't ready so we drop children from props here. #12, #17 + }; + + delete props.head; + /** + * Because ios wechat browser issue which not support srcDoc props + * we remove this props and also to avoid performance issue with + * document.write function, but PR is still welcome! + */ + delete props.srcDoc; + delete props.mountTarget; + delete props.contentDidMount; + delete props.contentDidUpdate; + return ( + + {this.renderFrameContents()} + + ); + } +} diff --git a/frontend/src/app/components/ReactFrameComponent/index.js b/frontend/src/app/components/ReactFrameComponent/index.js new file mode 100644 index 000000000..47f08f142 --- /dev/null +++ b/frontend/src/app/components/ReactFrameComponent/index.js @@ -0,0 +1,2 @@ +export { FrameContext, FrameContextConsumer, useFrame } from './Context.jsx'; +export { Frame } from './Frame'; diff --git a/frontend/src/app/components/VirtualTable.tsx b/frontend/src/app/components/VirtualTable.tsx new file mode 100644 index 000000000..691192c2d --- /dev/null +++ b/frontend/src/app/components/VirtualTable.tsx @@ -0,0 +1,164 @@ +/** + * 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 { Empty, Table, TableProps } from 'antd'; +import classNames from 'classnames'; +import React, { + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { VariableSizeGrid as Grid } from 'react-window'; +import { SPACE_TIMES } from 'styles/StyleConstants'; + +interface VirtualTableProps extends TableProps { + width: number; + scroll: { x: number; y: number }; + columns: any; +} + +/** + * Table组件中使用了虚拟滚动条 渲染的速度变快 基于(react-windows) + * 使用方法:import { VirtualTable } from 'app/components/VirtualTable'; + * + */ +export const VirtualTable = memo((props: VirtualTableProps) => { + const { columns, scroll, width: boxWidth, dataSource } = props; + const widthColumns = columns.map(v => v.width); + const gridRef: any = useRef(); + const isFull = useRef(false); + const widthColumnCount = widthColumns.filter(width => !width).length; + const [connectObject] = useState(() => { + const obj = {}; + Object.defineProperty(obj, 'scrollLeft', { + get: () => null, + set: scrollLeft => { + if (gridRef.current) { + gridRef.current.scrollTo({ + scrollLeft, + }); + } + }, + }); + return obj; + }); + isFull.current = boxWidth > scroll.x; + + if (isFull.current === true) { + widthColumns.forEach((v, i) => { + return (widthColumns[i] = + widthColumns[i] + (boxWidth - scroll.x) / widthColumns.length); + }); + } + + const mergedColumns = useMemo(() => { + return columns.map((column, i) => { + return { + ...column, + width: column.width + ? widthColumns[i] + : Math.floor(boxWidth / widthColumnCount), + }; + }); + }, [boxWidth, columns, widthColumnCount, widthColumns]); + + const resetVirtualGrid = useCallback(() => { + gridRef.current?.resetAfterIndices({ + columnIndex: 0, + shouldForceUpdate: true, + }); + }, [gridRef]); + + useEffect(() => resetVirtualGrid, [boxWidth, dataSource, resetVirtualGrid]); + + const renderVirtualList = useCallback( + (rawData, { scrollbarSize, ref, onScroll }) => { + ref.current = connectObject; + const totalHeight = rawData.length * 39; + + if (!dataSource?.length) { + //如果数据为空 If the data is empty + return ; + } + + return ( + { + const { width } = mergedColumns[index]; + return totalHeight > scroll.y && index === mergedColumns.length - 1 + ? width - scrollbarSize - 16 + : width; + }} + height={scroll.y} + rowCount={rawData.length} + rowHeight={() => 39} + width={boxWidth} + onScroll={({ scrollLeft }) => { + onScroll({ + scrollLeft, + }); + }} + > + {({ rowIndex, columnIndex, style }) => { + style = { + padding: `${SPACE_TIMES(2)}`, + textAlign: mergedColumns[columnIndex].align, + ...style, + borderBottom: '1px solid #f0f0f0', + }; + return ( + + {rawData[rowIndex][mergedColumns[columnIndex].dataIndex]} + + ); + }} + + ); + }, + [mergedColumns, boxWidth, connectObject, dataSource, scroll], + ); + + return ( + + ); +}); diff --git a/frontend/src/app/components/VizHeader/VizHeader.tsx b/frontend/src/app/components/VizHeader/VizHeader.tsx index cf7d870b1..aa84fcfcd 100644 --- a/frontend/src/app/components/VizHeader/VizHeader.tsx +++ b/frontend/src/app/components/VizHeader/VizHeader.tsx @@ -29,12 +29,11 @@ import { VizOperationMenu, } from 'app/components/VizOperationMenu'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; -import { FC, memo, useState } from 'react'; +import { TITLE_SUFFIX } from 'globalConstants'; +import { FC, memo, useMemo, useState } from 'react'; import styled from 'styled-components/macro'; import { DetailPageHeader } from '../DetailPageHeader'; -const TITLE_SUFFIX = ['[已归档]', '[未发布]']; - const VizHeader: FC<{ chartName?: string; status?: number; @@ -63,7 +62,7 @@ const VizHeader: FC<{ allowShare, allowManage, }) => { - const t = useI18NPrefix(`viz.chartPreview`); + const t = useI18NPrefix(`viz.action`); const [showShareLinkModal, setShowShareLinkModal] = useState(false); const handleCloseShareLinkModal = () => { @@ -85,7 +84,13 @@ const VizHeader: FC<{ ); }; - const title = `${chartName || ''} ${TITLE_SUFFIX[Number(status)] || ''}`; + const title = useMemo(() => { + const base = chartName || ''; + const suffix = TITLE_SUFFIX[Number(status)] + ? `[${t(TITLE_SUFFIX[Number(status)])}]` + : ''; + return base + suffix; + }, [chartName, status, t]); const isArchived = Number(status) === 0; return ( diff --git a/frontend/src/app/components/VizOperationMenu/VizOperationMenu.tsx b/frontend/src/app/components/VizOperationMenu/VizOperationMenu.tsx index 0ce61ed8d..184710121 100644 --- a/frontend/src/app/components/VizOperationMenu/VizOperationMenu.tsx +++ b/frontend/src/app/components/VizOperationMenu/VizOperationMenu.tsx @@ -34,7 +34,7 @@ const VizOperationMenu: FC<{ allowDownload, allowShare, }) => { - const t = useI18NPrefix(`viz.chartPreview`); + const t = useI18NPrefix(`viz.action`); const moreActionMenu = () => { const menus: any[] = []; diff --git a/frontend/src/app/components/VizOperationMenu/components/ShareLinkModal.tsx b/frontend/src/app/components/VizOperationMenu/components/ShareLinkModal.tsx index d307da90b..622545ca9 100644 --- a/frontend/src/app/components/VizOperationMenu/components/ShareLinkModal.tsx +++ b/frontend/src/app/components/VizOperationMenu/components/ShareLinkModal.tsx @@ -35,7 +35,7 @@ const ShareLinkModal: FC<{ onOk?; onCancel?; }> = memo(({ visibility, onGenerateShareLink, onOk, onCancel }) => { - const t = useI18NPrefix(`viz.chartPreview`); + const t = useI18NPrefix(`viz.action`); const [expireDate, setExpireDate] = useState(); const [enablePassword, setEnablePassword] = useState(false); const [shareLink, setShareLink] = useState<{ @@ -44,7 +44,7 @@ const ShareLinkModal: FC<{ usePassword?: boolean; }>(); - const hanldeCopyToClipboard = value => { + const handleCopyToClipboard = value => { const ta = document.createElement('textarea'); ta.innerText = value; document.body.appendChild(ta); @@ -63,7 +63,7 @@ const ShareLinkModal: FC<{ }`; }; - const hanldeGenerateShareLink = async (expireDate, enablePassword) => { + const handleGenerateShareLink = async (expireDate, enablePassword) => { const result = await onGenerateShareLink?.(expireDate, enablePassword); setShareLink(result); }; @@ -106,7 +106,7 @@ const ShareLinkModal: FC<{ htmlType="button" disabled={!expireDate} onClick={() => - hanldeGenerateShareLink?.(expireDate, enablePassword) + handleGenerateShareLink?.(expireDate, enablePassword) } > {t('share.generateLink')} @@ -119,7 +119,7 @@ const ShareLinkModal: FC<{ addonAfter={ - hanldeCopyToClipboard(getFullShareLinkPath(shareLink)) + handleCopyToClipboard(getFullShareLinkPath(shareLink)) } /> } @@ -132,7 +132,7 @@ const ShareLinkModal: FC<{ value={shareLink?.password} addonAfter={ hanldeCopyToClipboard(shareLink?.password)} + onClick={() => handleCopyToClipboard(shareLink?.password)} /> } /> diff --git a/frontend/src/app/constants.ts b/frontend/src/app/constants.ts deleted file mode 100644 index 6e94ac58a..000000000 --- a/frontend/src/app/constants.ts +++ /dev/null @@ -1,39 +0,0 @@ -const REGS = { - password: /^[0-9a-zA-Z][0-9a-zA-Z_]{5,19}$/, -}; -export const RULES = { - password: [ - { - required: true, - message: '密码不能为空', - }, - { - validator(_, value) { - if (value && !REGS.password.test(value)) { - return Promise.reject(new Error('由6-20位字母、数字、下划线组成')); - } - return Promise.resolve(); - }, - }, - ], - getConfirmRule: (filed = 'newPassword') => { - return [ - { required: true, message: '密码不能为空' }, - ({ getFieldValue }) => { - return { - validator(_, value) { - if (value && !REGS.password.test(value)) { - return Promise.reject( - new Error('由6-20位字母、数字、下划线组成'), - ); - } - if (value && getFieldValue(filed) !== value) { - return Promise.reject(new Error('两次输入的密码不一致')); - } - return Promise.resolve(); - }, - }; - }, - ]; - }, -}; diff --git a/frontend/src/app/hooks/useCacheWidthHeight.ts b/frontend/src/app/hooks/useCacheWidthHeight.ts new file mode 100644 index 000000000..f4a55bd93 --- /dev/null +++ b/frontend/src/app/hooks/useCacheWidthHeight.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 { useEffect, useState } from 'react'; +import useResizeObserver from './useResizeObserver'; + +export const useCacheWidthHeight = ( + initWidth: number = 400, + initHeight: number = 300, +) => { + const [cacheW, setCacheW] = useState(initWidth); + const [cacheH, setCacheH] = useState(initHeight); + const { + ref, + width = initWidth, + height = initHeight, + } = useResizeObserver({ + refreshMode: 'debounce', + refreshRate: 500, + }); + useEffect(() => { + if (width !== 0 && height !== 0) { + setCacheW(width); + setCacheH(height); + } + }, [width, height]); + return { + ref, + cacheW, + cacheH, + }; +}; diff --git a/frontend/src/app/hooks/useDebouncedSearch.ts b/frontend/src/app/hooks/useDebouncedSearch.ts index 6d887cedf..dd380a6e3 100644 --- a/frontend/src/app/hooks/useDebouncedSearch.ts +++ b/frontend/src/app/hooks/useDebouncedSearch.ts @@ -25,15 +25,15 @@ export function useDebouncedSearch( dataSource: T[] | undefined, filterFunc: (keywords: string, data: T) => boolean, wait: number = DEFAULT_DEBOUNCE_WAIT, + filterLeaf: boolean = false, ) { const [keywords, setKeywords] = useState(''); - const filteredData = useMemo( () => dataSource && keywords.trim() - ? filterListOrTree(dataSource, keywords, filterFunc) + ? filterListOrTree(dataSource, keywords, filterFunc, filterLeaf) : dataSource, - [dataSource, keywords, filterFunc], + [dataSource, keywords, filterFunc, filterLeaf], ); const debouncedSearch = useMemo(() => { diff --git a/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts b/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts index ae4dc42b0..192cbcbd7 100644 --- a/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts +++ b/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts @@ -19,7 +19,7 @@ import { FilterCondition, FilterConditionType, - FilterValueOption, + RelationFilterValue, } from 'app/types/ChartConfig'; import { BackendChart } from 'app/pages/ChartWorkbenchPage/slice/workbenchSlice'; import { getDistinctFields } from 'app/utils/fetch'; @@ -28,7 +28,7 @@ import useMount from './useMount'; export const useFetchFilterDataByCondtion = ( viewId?: string, condition?: FilterCondition, - onFinish?: (datas: FilterValueOption[]) => void, + onFinish?: (datas: RelationFilterValue[]) => void, view?: BackendChart['view'], ) => { useMount(() => { diff --git a/frontend/src/app/hooks/useFieldActionModal.tsx b/frontend/src/app/hooks/useFieldActionModal.tsx index 3949f3c05..f85bc9316 100644 --- a/frontend/src/app/hooks/useFieldActionModal.tsx +++ b/frontend/src/app/hooks/useFieldActionModal.tsx @@ -39,6 +39,7 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { dataView?: ChartDataView, dataConfig?: ChartDataSectionConfig, onChange?, + aggregation?: boolean, ) => { if (!config) { return null; @@ -50,8 +51,9 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { dataView, dataConfig, onConfigChange: onChange, + aggregation, + i18nPrefix, }; - switch (actionType) { case ChartDataSectionFieldActionType.Sortable: return ; @@ -91,13 +93,14 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { dataset?: ChartDataset, dataView?: ChartDataView, modalSize?: string, + aggregation?: boolean, ) => { const currentConfig = dataConfig.rows?.find(c => c.uid === columnUid); - let _modalSize = StateModalSize.Middle; + let _modalSize = StateModalSize.MIDDLE; if (actionType === ChartDataSectionFieldActionType.Colorize) { - _modalSize = StateModalSize.Small; + _modalSize = StateModalSize.XSMALL; } else if (actionType === ChartDataSectionFieldActionType.ColorizeSingle) { - _modalSize = StateModalSize.Small; + _modalSize = StateModalSize.XSMALL; } return (show as Function)({ title: t(actionType), @@ -110,6 +113,7 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { dataView, dataConfig, onChange, + aggregation, ), onOk: handleOk(onConfigChange, columnUid), maskClosable: true, diff --git a/frontend/src/app/hooks/useI18NPrefix.ts b/frontend/src/app/hooks/useI18NPrefix.ts index b359fb339..7dfc73dbd 100644 --- a/frontend/src/app/hooks/useI18NPrefix.ts +++ b/frontend/src/app/hooks/useI18NPrefix.ts @@ -17,6 +17,8 @@ */ import ChartI18NContext from 'app/pages/ChartWorkbenchPage/contexts/Chart18NContext'; +import { DATART_TRANSLATE_HOLDER } from 'globalConstants'; +import i18n from 'i18next'; import get from 'lodash/get'; import { useCallback, useContext } from 'react'; import { useTranslation } from 'react-i18next'; @@ -25,6 +27,10 @@ export interface I18NComponentProps { i18nPrefix?: string; } +export function prefixI18N(key) { + return i18n.t(key); +} + function usePrefixI18N(prefix?: string) { const { t, i18n } = useTranslation(); const { i18NConfigs: vizI18NConfigs } = useContext(ChartI18NContext); @@ -41,10 +47,14 @@ function usePrefixI18N(prefix?: string) { if (contextTranslation) { return contextTranslation; } + if (key.includes(DATART_TRANSLATE_HOLDER)) { + const newKey = key.replace(`${DATART_TRANSLATE_HOLDER}.`, ''); + return t.call(Object.create(null), `${newKey}`, options) as string; + } if (disablePrefix) { - return t.call(null, `${key}`, options) as string; + return t.call(Object.create(null), `${key}`, options) as string; } - return t.call(null, `${prefix}.${key}`, options) as string; + return t.call(Object.create(null), `${prefix}.${key}`, options) as string; }, [i18n.language, prefix, t, vizI18NConfigs], ); diff --git a/frontend/src/app/hooks/useMount.ts b/frontend/src/app/hooks/useMount.ts index 12dabc7b7..2e1be0b4d 100644 --- a/frontend/src/app/hooks/useMount.ts +++ b/frontend/src/app/hooks/useMount.ts @@ -24,6 +24,7 @@ const useMount = (fn?: () => void, dispose?: () => void) => { return () => { dispose?.(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); }; diff --git a/frontend/src/app/hooks/useSearchAndExpand.ts b/frontend/src/app/hooks/useSearchAndExpand.ts index a86e3cc84..0c63abef7 100644 --- a/frontend/src/app/hooks/useSearchAndExpand.ts +++ b/frontend/src/app/hooks/useSearchAndExpand.ts @@ -26,6 +26,7 @@ export function useSearchAndExpand( dataSource: T[] | undefined, filterFunc: (keywords: string, data: T) => boolean, wait: number = DEFAULT_DEBOUNCE_WAIT, + filterLeaf: boolean = false, ) { const [expandedRowKeys, setExpandedRowKeys] = useState([]); @@ -33,6 +34,7 @@ export function useSearchAndExpand( dataSource, filterFunc, wait, + filterLeaf, ); const filteredExpandedRowKeys = useMemo( diff --git a/frontend/src/app/hooks/useStateModal.tsx b/frontend/src/app/hooks/useStateModal.tsx index b0a20833a..8b8e293bf 100644 --- a/frontend/src/app/hooks/useStateModal.tsx +++ b/frontend/src/app/hooks/useStateModal.tsx @@ -18,33 +18,26 @@ import { Form, Modal } from 'antd'; import { useRef } from 'react'; -import useI18NPrefix from './useI18NPrefix'; export interface IStateModalContentProps { onChange: (o: any) => void; } export enum StateModalSize { - Small = 600, - Middle = 1000, - Large = 1600, - XLarge = 2000, + XSMALL = 520, + SMALL = 600, + MIDDLE = 1000, + LARGE = 1600, + XLARGE = 2000, } const defaultBodyStyle: React.CSSProperties = { maxHeight: 1000, - overflowY: 'scroll', + overflowY: 'auto', overflowX: 'auto', }; -function useStateModal({ - i18nPrefix, - initState, -}: { - i18nPrefix?: string; - initState?: any; -}) { - const t = useI18NPrefix(i18nPrefix); +function useStateModal({ initState }: { initState?: any }) { const [form] = Form.useForm(); const [modal, contextHolder] = Modal.useModal(); const okCallbackRef = useRef(); @@ -64,19 +57,19 @@ function useStateModal({ Object.keys(stateRef.current || {}).length > 0 ? stateRef.current : []; - okCallbackRef.current?.call(null, ...spreadParmas); + okCallbackRef.current?.call(Object.create(null), ...spreadParmas); } catch (e) { console.error('useStateModal | exception message ---> ', e); } return closeFn; }) .catch(info => { - return Promise.reject(); + return Promise.reject(info); }); }; const handleClickCancelButton = () => { - cancelCallbackRef.current?.call(null, null); + cancelCallbackRef.current?.call(Object.create(null), null); }; const FormWrapper = content => { @@ -87,13 +80,26 @@ function useStateModal({ ); }; + const getModalSize = (size?: string | number | StateModalSize): number => { + if (!size) { + return StateModalSize.MIDDLE; + } + if (!isNaN(+size)) { + return +size; + } + if (typeof size === 'string' && StateModalSize[size.toUpperCase()]) { + return StateModalSize[size.toUpperCase()]; + } + return StateModalSize.MIDDLE; + }; + const showModal = (props: { title: string; content: ( cacheOnChangeValue: typeof handleSaveCacheValue, ) => React.ReactElement; bodyStyle?: React.CSSProperties; - modalSize?: StateModalSize; + modalSize?: string | number | StateModalSize; onOk?: typeof handleClickOKButton; onCancel?: typeof handleClickCancelButton; }) => { @@ -106,9 +112,11 @@ function useStateModal({ return modal.confirm({ title: props.title, - width: props.modalSize || StateModalSize.Small, + width: getModalSize(props?.modalSize), bodyStyle: props.bodyStyle || defaultBodyStyle, - content: FormWrapper(props?.content?.call(null, handleSaveCacheValue)), + content: FormWrapper( + props?.content?.call(Object.create(null), handleSaveCacheValue), + ), onOk: handleClickOKButton, onCancel: handleClickCancelButton, maskClosable: true, diff --git a/frontend/src/app/index.tsx b/frontend/src/app/index.tsx index 39744cf16..07a06e458 100644 --- a/frontend/src/app/index.tsx +++ b/frontend/src/app/index.tsx @@ -1,15 +1,26 @@ /** + * Datart * - * App + * Copyright 2021 * - * This component is the skeleton around the actual pages, and should only - * contain code that should be seen on all pages. (e.g. navigation bar) + * 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 { message } from 'antd'; +import { ConfigProvider, message } from 'antd'; import echartsDefaultTheme from 'app/assets/theme/echarts_default_theme.json'; import { registerTheme } from 'echarts'; import { StorageKeys } from 'globalConstants'; +import { antdLocales } from 'locales/i18n'; import { useEffect, useLayoutEffect } from 'react'; import { Helmet } from 'react-helmet-async'; import { useTranslation } from 'react-i18next'; @@ -17,6 +28,7 @@ import { useDispatch } from 'react-redux'; import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { GlobalStyle, OverriddenStyle } from 'styles/globalStyles'; import { getToken } from 'utils/auth'; +import useI18NPrefix from './hooks/useI18NPrefix'; import { LoginAuthRoute } from './LoginAuthRoute'; import { LazyActivePage } from './pages/ActivePage/Loadable'; import { LazyAuthorizationPage } from './pages/AuthorizationPage/Loadable'; @@ -32,6 +44,7 @@ export function App() { const dispatch = useDispatch(); const { i18n } = useTranslation(); const logged = !!getToken(); + const t = useI18NPrefix('global'); useAppSlice(); useLayoutEffect(() => { @@ -39,35 +52,40 @@ export function App() { dispatch(setLoggedInUser()); } else { if (localStorage.getItem(StorageKeys.LoggedInUser)) { - message.warning('会话过期,请重新登录'); + message.warning(t('tokenExpired')); } dispatch(logout()); } - }, [dispatch, logged]); + }, [dispatch, t, logged]); useEffect(() => { dispatch(getSystemInfo()); }, [dispatch]); return ( - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + ); } diff --git a/frontend/src/app/migration/alpha3.ts b/frontend/src/app/migration/alpha3.ts index e58203885..d8b809c68 100644 --- a/frontend/src/app/migration/alpha3.ts +++ b/frontend/src/app/migration/alpha3.ts @@ -17,7 +17,7 @@ */ import { ChartConfig } from 'app/types/ChartConfig'; -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; export const hasWrongDimensionName = (config?: ChartConfig) => { if (!config) { diff --git a/frontend/src/app/pages/ActivePage/Loadable.tsx b/frontend/src/app/pages/ActivePage/Loadable.tsx index 205f3aa05..af9b98fc0 100644 --- a/frontend/src/app/pages/ActivePage/Loadable.tsx +++ b/frontend/src/app/pages/ActivePage/Loadable.tsx @@ -15,9 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** - * Asynchronously loads the component for NotFoundPage - */ import { defaultLazyLoad } from 'utils/loadable'; diff --git a/frontend/src/app/pages/ActivePage/index.tsx b/frontend/src/app/pages/ActivePage/index.tsx index 951d204ab..4e38411cc 100644 --- a/frontend/src/app/pages/ActivePage/index.tsx +++ b/frontend/src/app/pages/ActivePage/index.tsx @@ -1,4 +1,5 @@ import { EmptyFiller } from 'app/components/EmptyFiller'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { getUserInfoByToken } from 'app/slice/thunks'; import { useCallback, useEffect } from 'react'; import { useDispatch } from 'react-redux'; @@ -9,6 +10,8 @@ import { activeAccount } from './service'; export const ActivePage = () => { const history = useHistory(); const dispatch = useDispatch(); + const t = useI18NPrefix('active'); + const onActiveUser = useCallback( token => { activeAccount(token).then(loginToken => { @@ -37,7 +40,7 @@ export const ActivePage = () => { }, []); return ( - + ); }; diff --git a/frontend/src/app/pages/AuthorizationPage/Loadable.tsx b/frontend/src/app/pages/AuthorizationPage/Loadable.tsx index 252622fab..aeb3fe23a 100644 --- a/frontend/src/app/pages/AuthorizationPage/Loadable.tsx +++ b/frontend/src/app/pages/AuthorizationPage/Loadable.tsx @@ -15,9 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** - * Asynchronously loads the component for NotFoundPage - */ import { defaultLazyLoad } from 'utils/loadable'; diff --git a/frontend/src/app/pages/AuthorizationPage/index.tsx b/frontend/src/app/pages/AuthorizationPage/index.tsx index 47f3632fd..e0a6ee723 100644 --- a/frontend/src/app/pages/AuthorizationPage/index.tsx +++ b/frontend/src/app/pages/AuthorizationPage/index.tsx @@ -1,4 +1,5 @@ import { EmptyFiller } from 'app/components/EmptyFiller'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { getUserInfoByToken } from 'app/slice/thunks'; import { useCallback, useEffect } from 'react'; import { useDispatch } from 'react-redux'; @@ -10,9 +11,12 @@ export const AuthorizationPage = () => { const history = useHistory(); const paramsMatch = useRouteMatch<{ token: string }>(); const token = paramsMatch.params.token; + const t = useI18NPrefix('authorization'); + const toApp = useCallback(() => { history.replace('/'); }, [history]); + useEffect(() => { if (token) { dispatch(getUserInfoByToken({ token, resolve: toApp })); @@ -20,7 +24,7 @@ export const AuthorizationPage = () => { }, [token, dispatch, toApp]); return ( - + ); }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx index 8c8d669d6..683a77e4a 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx @@ -16,10 +16,10 @@ * limitations under the License. */ -import { LeftOutlined } from '@ant-design/icons'; -import { Button, Space } from 'antd'; +import { LeftOutlined, MoreOutlined } from '@ant-design/icons'; +import { Button, Dropdown, Space } from 'antd'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; -import { FC, memo } from 'react'; +import { FC, memo, useCallback, useContext } from 'react'; import styled from 'styled-components/macro'; import { FONT_SIZE_ICON_SM, @@ -30,13 +30,28 @@ import { SPACE_TIMES, SPACE_XS, } from 'styles/StyleConstants'; +import ChartAggregationContext from '../../contexts/ChartAggregationContext'; +import AggregationOperationMenu from './components/AggregationOperationMenu'; const ChartHeaderPanel: FC<{ chartName?: string; onSaveChart?: () => void; onGoBack?: () => void; -}> = memo(({ chartName, onSaveChart, onGoBack }) => { + onChangeAggregation?: (state: boolean) => void; +}> = memo(({ chartName, onSaveChart, onGoBack, onChangeAggregation }) => { const t = useI18NPrefix(`viz.workbench.header`); + const { aggregation } = useContext(ChartAggregationContext); + + const getOverlays = useCallback(() => { + return ( + { + onChangeAggregation?.(e); + }} + > + ); + }, [aggregation, onChangeAggregation]); return ( @@ -47,29 +62,12 @@ const ChartHeaderPanel: FC<{ )} {chartName} - {/* - {t('lang.zh')} - {t('lang.en')} - - - - {t('format.local')} - - - {t('format.date')} - - {t('format.ll')} - {t('format.lll')} - */} {t('save')} + + } /> + ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/components/AggregationOperationMenu.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/components/AggregationOperationMenu.tsx new file mode 100644 index 000000000..69a125cf0 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/components/AggregationOperationMenu.tsx @@ -0,0 +1,51 @@ +/** + * 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 { Menu, Modal, Switch } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { FC, memo, useMemo } from 'react'; + +const AggregationOperationMenu: FC<{ + defaultValue?: boolean; + onChangeAggregation: (value: boolean) => void; +}> = memo(({ defaultValue = true, onChangeAggregation }) => { + const checkedValue = useMemo(() => defaultValue, [defaultValue]); + const t = useI18NPrefix(`viz.workbench.header`); + + const onChange = value => { + Modal.confirm({ + icon: <>>, + content: t('aggregationSwitchTip'), + okText: checkedValue ? t('close') : t('open'), + // cancelText: t('close'), + onOk() { + onChangeAggregation(value); + }, + }); + }; + return ( + + + {t('aggregationSwitch')}{' '} + + + + ); +}); + +export default AggregationOperationMenu; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx index 6e80f6ecc..1ccf37347 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx @@ -73,6 +73,12 @@ const ChartOperationPanel: FC<{ if (component === LayoutComponentType.PRESENT) { return ( = memo( ({ dataConfigs, onChange }) => { const translate = useI18NPrefix(`viz.palette.data`); + const { aggregation } = useContext(ChartAggregationContext); const getSectionComponent = (config, index) => { const props = { - key: index, + key: config?.key || index, ancestors: [index], config, translate, + aggregation, onConfigChanged: (ancestors, config, needRefresh?: boolean) => { onChange?.(ancestors, config, needRefresh); }, }; + switch (props.config?.type) { case ChartDataSectionType.GROUP: return ; @@ -67,7 +71,11 @@ const ChartDataConfigPanel: FC<{ } }; - return {(dataConfigs || []).map(getSectionComponent)}; + return ( + + {(dataConfigs || []).map(getSectionComponent)} + + ); }, (prev, next) => { return prev.dataConfigs === next.dataConfigs; @@ -76,7 +84,7 @@ const ChartDataConfigPanel: FC<{ export default ChartDataConfigPanel; -const Wrapper = styled.div` +const StyledChartDataConfigPanel = styled.div` display: flex; flex: 1; flex-direction: column; 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 929719da4..bc34e52cb 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 @@ -37,25 +37,26 @@ const ChartStyleConfigPanel: FC<{ }> = memo( ({ configs, dataConfigs, onChange }) => { const t = useI18NPrefix(`viz.palette.style`); - return ( - {configs?.map((c, index) => ( - - - - ))} + {configs + ?.filter(c => !Boolean(c.hidden)) + .map((c, index) => ( + + + + ))} ); }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/AggregateTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/AggregateTypeSection.tsx index 232e8b911..63c31782e 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/AggregateTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/AggregateTypeSection.tsx @@ -17,15 +17,15 @@ */ import { ChartDataSectionFieldActionType } from 'app/types/ChartConfig'; -import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; -import { dataConfigSectionComparer } from './utils'; +import { dataConfigSectionComparer, handleDefaultConfig } from './utils'; const AggregateTypeSection: FC = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( { allowSameField: true, }, @@ -49,6 +49,10 @@ const AggregateTypeSection: FC = memo( }, config, ); + + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } return ; }, dataConfigSectionComparer, 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 14d994f8d..bc098615d 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 @@ -26,18 +26,18 @@ import { dataConfigSectionComparer } from './utils'; const BaseDataConfigSection: FC = memo( ({ modalSize, config, extra, translate = title => title, ...rest }) => { return ( - - + + {translate(config.label)} {extra?.()} - + - + ); }, dataConfigSectionComparer, @@ -45,10 +45,10 @@ const BaseDataConfigSection: FC = memo( export default BaseDataConfigSection; -const Container = styled.div` +const StyledBaseDataConfigSection = styled.div` padding: ${SPACE} 0; `; -const Title = styled.div` +const StyledBaseDataConfigSectionTitle = styled.div` color: ${p => p.theme.textColor}; `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/ColorTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/ColorTypeSection.tsx index f31b8e56b..4111ac83a 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/ColorTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/ColorTypeSection.tsx @@ -21,11 +21,11 @@ import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; -import { dataConfigSectionComparer } from './utils'; +import { dataConfigSectionComparer, handleDefaultConfig } from './utils'; const ColorTypeSection: FC = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( {}, { actions: { @@ -37,7 +37,9 @@ const ColorTypeSection: FC = memo( }, config, ); - + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } return ; }, dataConfigSectionComparer, 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 053e0e5ac..384162e79 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 @@ -24,21 +24,28 @@ import { ChartDataSectionConfig, ChartDataSectionFieldActionType, } from 'app/types/ChartConfig'; -import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo, useState } from 'react'; import { CloneValueDeep } from 'utils/object'; import BaseDataConfigSection from './BaseDataConfigSection'; import { dataConfigSectionComparer } from './utils'; const FilterTypeSection: FC = memo( - ({ ancestors, config, translate = title => title, onConfigChanged }) => { + ({ + ancestors, + config, + translate = title => title, + onConfigChanged, + aggregation, + }) => { const [currentConfig, setCurrentConfig] = useState(config); const [originalConfig, setOriginalConfig] = useState(config); const [enableExtraAction] = useState(false); const extendedConfig = Object.assign( { allowSameField: true, + disableAggregate: false, }, { actions: { @@ -96,7 +103,7 @@ const FilterTypeSection: FC = memo( return ( = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( {}, { actions: { @@ -45,6 +45,9 @@ const GroupTypeSection: FC = memo( }, config, ); + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } return ; }, dataConfigSectionComparer, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/InfoTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/InfoTypeSection.tsx index b810e6941..fa4e9ab7c 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/InfoTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/InfoTypeSection.tsx @@ -17,15 +17,15 @@ */ import { ChartDataSectionFieldActionType } from 'app/types/ChartConfig'; -import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; -import { dataConfigSectionComparer } from './utils'; +import { dataConfigSectionComparer, handleDefaultConfig } from './utils'; const InfoTypeSection: FC = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( { allowSameField: true, }, @@ -33,13 +33,18 @@ const InfoTypeSection: FC = memo( actions: { [ChartDataViewFieldType.NUMERIC]: [ ChartDataSectionFieldActionType.Aggregate, - ChartDataSectionFieldActionType.Alias, ChartDataSectionFieldActionType.Format, + ChartDataSectionFieldActionType.Alias, ], }, }, config, ); + + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } + return ; }, dataConfigSectionComparer, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/MixedTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/MixedTypeSection.tsx index 992b24715..4663053e9 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/MixedTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/MixedTypeSection.tsx @@ -17,32 +17,41 @@ */ import { ChartDataSectionFieldActionType } from 'app/types/ChartConfig'; -import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; -import { dataConfigSectionComparer } from './utils'; +import { dataConfigSectionComparer, handleDefaultConfig } from './utils'; const MixedTypeSection: FC = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( {}, { actions: { [ChartDataViewFieldType.NUMERIC]: [ + ChartDataSectionFieldActionType.Aggregate, ChartDataSectionFieldActionType.Alias, ChartDataSectionFieldActionType.Format, + ChartDataSectionFieldActionType.Sortable, ], [ChartDataViewFieldType.STRING]: [ ChartDataSectionFieldActionType.Alias, + ChartDataSectionFieldActionType.Sortable, ], [ChartDataViewFieldType.DATE]: [ ChartDataSectionFieldActionType.Alias, + ChartDataSectionFieldActionType.Sortable, ], }, }, config, ); + + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } + return ; }, dataConfigSectionComparer, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SizeTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SizeTypeSection.tsx index ff871bf01..80945cd9e 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SizeTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SizeTypeSection.tsx @@ -21,11 +21,11 @@ import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; -import { dataConfigSectionComparer } from './utils'; +import { dataConfigSectionComparer, handleDefaultConfig } from './utils'; const SizeTypeSection: FC = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( {}, { actions: { @@ -38,6 +38,11 @@ const SizeTypeSection: FC = memo( }, config, ); + + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } + return ; }, dataConfigSectionComparer, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SortTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SortTypeSection.tsx index 82142e8e6..d9f9d9482 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SortTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SortTypeSection.tsx @@ -17,8 +17,8 @@ */ import { ChartDataSectionFieldActionType } from 'app/types/ChartConfig'; -import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; import { dataConfigSectionComparer } from './utils'; 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 427f0a938..c9e660438 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 @@ -16,17 +16,46 @@ * limitations under the License. */ +import { ChartDataSectionType } from 'app/types/ChartConfig'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; - +import produce from 'immer'; export function dataConfigSectionComparer( prevProps: ChartDataConfigSectionProps, nextProps: ChartDataConfigSectionProps, ) { if ( prevProps.translate !== nextProps.translate || - prevProps.config !== nextProps.config + prevProps.config !== nextProps.config || + prevProps.aggregation !== nextProps.aggregation ) { return false; } return true; } + +export function handleDefaultConfig(defaultConfig, configType): any { + const nextConfig = produce(defaultConfig, draft => { + let _actions = {}; + + draft.rows?.forEach((row, i) => { + draft.rows[i].aggregate = undefined; + }); + + if (configType === ChartDataSectionType.AGGREGATE) { + delete draft.actions.STRING; + } + + if (configType === ChartDataSectionType.GROUP) { + delete draft.actions.NUMERIC; + } + + for (let key in draft.actions) { + _actions[key] = draft.actions[key].filter( + v => v !== 'aggregate' && v !== 'aggregateLimit', + ); + } + + draft.actions = _actions; + }); + return nextConfig; +} 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 c74823cfe..94b2f5d92 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 @@ -144,7 +144,7 @@ const ChartDataViewPanel: FC<{ const handleAddOrEditComputedField = field => { (showModal as Function)({ title: t('createComputedFields'), - modalSize: StateModalSize.Middle, + modalSize: StateModalSize.MIDDLE, content: onChange => ( { const fieldConfig = config.rows?.find(c => c.uid === uid)!; + const options = config?.options?.[actionName]; if (actionName === ChartDataSectionFieldActionType.Sortable) { return ( { handleFieldConfigChanged(uid, config, needRefresh); }} + options={options} mode="menu" /> ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragLayer.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragLayer.tsx new file mode 100644 index 000000000..3a35ef37b --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragLayer.tsx @@ -0,0 +1,81 @@ +/** + * 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 React from 'react'; +import { DragLayer } from 'react-dnd'; +import styled from 'styled-components'; +import ChartDragPreview from './ChartDragPreview'; + +const collect = monitor => { + return { + item: monitor.getItem(), + itemType: monitor.getItemType(), + currentOffset: monitor.getSourceClientOffset(), + isDragging: monitor.isDragging(), + }; +}; + +const getItemStyles = currentOffset => { + if (!currentOffset) { + return { + display: 'none', + }; + } + const { x, y } = currentOffset; + return { + transform: `translate(${x}px, ${y}px)`, + }; +}; + +function CardDragLayer(props) { + const { item, itemType, currentOffset, isDragging } = props; + + /** + * zh: 如果不是正在拖动或者拖动的数据项不是一个数组则不执行 + * en: If it is not being dragged or the data item being dragged is not an array, do not execute + */ + if (!isDragging || !Array.isArray(item)) { + return null; + } + + const renderItem = (type, item) => { + switch (type) { + case 'dataset_column': + return ( + + + + ); + default: + return null; + } + }; + + return {renderItem(itemType, item)}; +} +export default DragLayer(collect)(CardDragLayer); + +const LayerStyles = styled.div` + position: fixed; + pointer-events: none; + z-index: 100; + left: 0; + top: 0; + right: 0; + bottom: 0; +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragPreview.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragPreview.tsx new file mode 100644 index 000000000..fc7fd2e86 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragPreview.tsx @@ -0,0 +1,54 @@ +/** + * 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 React from 'react'; +import styled from 'styled-components/macro'; +import ChartDraggableSourceContainer from './ChartDraggableSourceContainer'; + +const DragPreview = ({ dataItem }) => { + return ( + + {dataItem?.slice(0, 3).map((v, i) => ( + + + + ))} + + ); +}; + +export default DragPreview; + +const Preview = styled.div` + border: 1px solid #fff; + background: #f2f2f2; + width: 256px; + position: absolute; + transform-origin: bottom left; + -webkit-backface-visibility: hidden; +`; 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 f6f9efb53..dc224b288 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 @@ -54,7 +54,11 @@ interface ChartDraggableElementProps { config: ChartDataSectionField; connectDragSource: ConnectDragSource; connectDropTarget: ConnectDropTarget; - moveCard: (dragIndex: number, hoverIndex: number) => void; + moveCard: ( + dragIndex: number, + hoverIndex: number, + config?: ChartDataSectionField, + ) => void; onDelete: () => void; } @@ -65,7 +69,7 @@ interface ChartDraggableElementInstance { const ChartDraggableElement = forwardRef< HTMLDivElement, ChartDraggableElementProps ->(function Card( +>(function ChartDraggableElement( { content, isDragging, @@ -104,7 +108,7 @@ const ChartDraggableElement = forwardRef< }); export default DropTarget( - CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN, + [CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN], { hover( props: ChartDraggableElementProps, @@ -120,7 +124,9 @@ export default DropTarget( return null; } - const dragIndex = monitor.getItem().index; + const dragItem = monitor.getItem(); + + const dragIndex = dragItem.index; const hoverIndex = props.index; // Don't replace items with themselves @@ -205,7 +211,7 @@ const StyledChartDraggableElement = styled.div<{ background: ${p => p.type === ChartDataViewFieldType.NUMERIC ? p.theme.success : p.theme.info}; border-radius: ${BORDER_RADIUS}; - opacity: ${p => (p.isDragging ? 0 : 1)}; + opacity: ${p => (p.isDragging ? 0.2 : 1)}; `; const Content = styled.div` 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 fc16fcf0b..b4cb0176c 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 @@ -50,8 +50,10 @@ import { export const ChartDraggableSourceContainer: FC< { - onDeleteComputedField: (fieldName) => void; - onEditComputedField: (fieldName) => void; + onDeleteComputedField?: (fieldName) => void; + onEditComputedField?: (fieldName) => void; + onSelectionChange?: (dataItemId, cmdKeyActive, shiftKeyActive) => void; + onClearCheckedList?: () => void; } & ChartDataViewMeta > = memo(function ChartDraggableSourceContainer({ id, @@ -61,23 +63,45 @@ export const ChartDraggableSourceContainer: FC< expression, onDeleteComputedField, onEditComputedField, + onSelectionChange, + selectedItems, + isActive, + onClearCheckedList, }) { const t = useI18NPrefix(`viz.workbench.dataview`); const [, drag] = useDrag( () => ({ type: CHART_DRAG_ELEMENT_TYPE.DATASET_COLUMN, canDrag: true, - item: { colName, type, category }, + item: selectedItems?.length + ? selectedItems.map(item => ({ + colName: item.id, + type: item.type, + category: item.category, + })) + : { colName, type, category }, + collect: monitor => ({ + isDragging: monitor.isDragging(), + }), + end: onClearCheckedList, }), - [], + [selectedItems], ); + const styleClasses: Array = useMemo(() => { + let styleArr: Array = []; + if (isActive) { + styleArr.push('container-active'); + } + return styleArr; + }, [isActive]); + const renderContent = useMemo(() => { const _handleMenuClick = (e, fieldName) => { if (e.key === 'delete') { - onDeleteComputedField(fieldName); + onDeleteComputedField?.(fieldName); } else { - onEditComputedField(fieldName); + onEditComputedField?.(fieldName); } }; @@ -151,7 +175,17 @@ export const ChartDraggableSourceContainer: FC< ); }, [type, colName, onDeleteComputedField, onEditComputedField, category, t]); - return {renderContent}; + return ( + { + onSelectionChange?.(colName, e.metaKey || e.ctrlKey, e.shiftKey); + }} + ref={drag} + className={styleClasses.join(' ')} + > + {renderContent} + + ); }); export default ChartDraggableSourceContainer; @@ -165,7 +199,9 @@ const Container = styled.div` font-weight: ${FONT_WEIGHT_MEDIUM}; color: ${p => p.theme.textColorSnd}; cursor: pointer; - + &.container-active { + background-color: #f8f9fa; + } > p { flex: 1; } 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 4ffc389aa..481d0b9b5 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 @@ -16,13 +16,13 @@ * limitations under the License. */ -import { Checkbox, List } from 'antd'; +import { List } from 'antd'; import { ChartDataViewMeta } from 'app/types/ChartDataView'; -import { CHART_DRAG_ELEMENT_TYPE } from 'globalConstants'; import { FC, memo, useState } from 'react'; -import { DragSourceMonitor, useDrag } from 'react-dnd'; import styled from 'styled-components/macro'; +import { stopPPG } from 'utils/utils'; import { ChartDraggableSourceContainer } from './ChartDraggableSourceContainer'; +import ChartDragLayer from './ChartDragLayer'; export const ChartDraggableSourceGroupContainer: FC<{ meta?: ChartDataViewMeta[]; @@ -33,79 +33,73 @@ export const ChartDraggableSourceGroupContainer: FC<{ onDeleteComputedField, onEditComputedField, }) { - const [indeterminate, setIndeterminate] = useState(false); - const [checkedList, setCheckedList] = useState([]); - const [isCheckAll, setIsCheckAll] = useState(false); + const [selectedItems, setSelectedItems] = useState([]); + const [selectedItemsIds, setselectedItemsIds] = useState>([]); + const [activeItemId, setActiveItemId] = useState(''); - const [, drag] = useDrag( - () => ({ - type: CHART_DRAG_ELEMENT_TYPE.DATASET_GROUP_COLUMNS, - canDrag: true, - item: checkedList.map(item => ({ - colName: item.id, - type: item.type, - category: item.category, - })), - collect: (monitor: DragSourceMonitor) => ({ - isDragging: monitor.isDragging(), - }), - }), - [checkedList], - ); + const onDataItemSelectionChange = ( + dataItemId: string, + cmdKeyActive: boolean, + shiftKeyActive: boolean, + ) => { + let interimSelectedItemsIds: Array = []; + let interimActiveItemId = ''; + const dataViewMeta = meta?.slice() || []; + const previousSelectedItemsIds: Array = selectedItemsIds.slice(); + const previousActiveItemId = activeItemId; - const handleListItemChecked = item => checked => { - if ( - !!checked && - !checkedList.find(checkedItem => checkedItem.id === item.id) - ) { - const updatedList = checkedList.concat([item]); - setCheckedList(updatedList); - setIsCheckAll(meta?.length === updatedList.length); - setIndeterminate( - !!updatedList.length && (meta || []).length > updatedList.length, - ); - } else if (!checked) { - const updatedList = checkedList.filter( - checkedItem => checkedItem.id !== item.id, - ); - setCheckedList(updatedList); - setIsCheckAll(meta?.length === updatedList.length); - setIndeterminate( - !!updatedList.length && (meta || []).length > updatedList.length, + if (cmdKeyActive) { + if ( + previousSelectedItemsIds.indexOf(dataItemId) > -1 && + dataItemId !== previousActiveItemId + ) { + interimSelectedItemsIds = previousSelectedItemsIds.filter( + id => id !== dataItemId, + ); + } else { + interimSelectedItemsIds = [...previousSelectedItemsIds, dataItemId]; + } + } else if (shiftKeyActive && dataItemId !== previousActiveItemId) { + const activeCardIndex: any = dataViewMeta.findIndex( + c => c.id === previousActiveItemId, ); + const cardIndex = dataViewMeta.findIndex(c => c.id === dataItemId); + const lowerIndex = Math.min(activeCardIndex, cardIndex); + const upperIndex = Math.max(activeCardIndex, cardIndex); + interimSelectedItemsIds = dataViewMeta + .slice(lowerIndex, upperIndex + 1) + .map(c => c.id); + } else { + interimSelectedItemsIds = [dataItemId]; + interimActiveItemId = dataItemId; } + + const selectedCards = dataViewMeta.filter(c => + interimSelectedItemsIds.includes(c.id), + ); + + setselectedItemsIds(interimSelectedItemsIds); + setActiveItemId(interimActiveItemId); + setSelectedItems(selectedCards); }; - const handleCheckAll = checked => { - setCheckedList(checked ? meta || [] : []); - setIndeterminate(false); - setIsCheckAll(checked); + const onClearCheckedList = () => { + if (selectedItems?.length > 0) { + setselectedItemsIds([]); + setActiveItemId(''); + setSelectedItems([]); + } }; return ( - + + {/* 拖动层组件 */} + item.id} - // header={ - // handleCheckAll(e.target?.checked)} - // > - // Allow MultiDraggable (Drag Me) - // - // } renderItem={item => ( - - {(isCheckAll || indeterminate) && ( - checkedItem.id === item.id) - } - onChange={e => handleListItemChecked(item)(e.target?.checked)} - /> - )} + )} 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 55d448af1..c3762f498 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 @@ -54,9 +54,14 @@ import { SPACE_SM, } from 'styles/StyleConstants'; import { ValueOf } from 'types'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; +import ChartAggregationContext from '../../../../contexts/ChartAggregationContext'; import ChartDataConfigSectionActionMenu from './ChartDataConfigSectionActionMenu'; -import VizDraggableItem from './ChartDraggableElement'; +import ChartDraggableElement from './ChartDraggableElement'; + +type DragItem = { + index?: number; +}; export const ChartDraggableTargetContainer: FC = memo(function ChartDraggableTargetContainer({ @@ -72,68 +77,88 @@ export const ChartDraggableTargetContainer: FC = const [showModal, contextHolder] = useFieldActionModal({ i18nPrefix: 'viz.palette.data.enum.actionType', }); + const { aggregation } = useContext(ChartAggregationContext); const [{ isOver, canDrop }, drop] = useDrop( () => ({ accept: [ - CHART_DRAG_ELEMENT_TYPE.DATASET_GROUP_COLUMNS, CHART_DRAG_ELEMENT_TYPE.DATASET_COLUMN, CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN, ], - drop(item: ChartDataSectionField, monitor) { - let items = [item]; + drop(item: ChartDataSectionField & DragItem, monitor) { + let items = Array.isArray(item) ? item : [item]; + let needDelete = true; if ( - monitor.getItemType() === - CHART_DRAG_ELEMENT_TYPE.DATASET_GROUP_COLUMNS + monitor.getItemType() === CHART_DRAG_ELEMENT_TYPE.DATASET_COLUMN ) { - items = item as any; - } - if ( - monitor.getItemType() === CHART_DRAG_ELEMENT_TYPE.DATASET_COLUMN || - monitor.getItemType() === CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN - ) { - let currentColumns: ChartDataSectionField[] = ( + const currentColumns: ChartDataSectionField[] = ( currentConfig.rows || [] ).concat( - items.map(i => ({ + items.map(val => ({ uid: uuidv4(), - colName: i.colName, - category: i.category, - type: i.type, - aggregate: getDefaultAggregate(item), + colName: val.colName, + category: val.category, + type: val.type, + aggregate: getDefaultAggregate(val), })), ); - const newCurrentConfig = updateByKey( - currentConfig, - 'rows', - currentColumns, + updateCurrentConfigColumns(currentConfig, currentColumns, true); + } else if ( + monitor.getItemType() === CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN + ) { + const originItemIndex = (currentConfig.rows || []).findIndex( + r => r.uid === item.uid, ); - setCurrentConfig(newCurrentConfig); - onConfigChanged?.(ancestors, newCurrentConfig, true); + if (originItemIndex > -1) { + needDelete = false; + const currentColumns = updateBy( + currentConfig?.rows || [], + draft => { + draft.splice(originItemIndex, 1); + return draft.splice(item?.index!, 0, item); + }, + ); + updateCurrentConfigColumns(currentConfig, currentColumns); + } else { + const currentColumns = updateBy( + currentConfig?.rows || [], + draft => { + return draft.splice(item?.index!, 0, item); + }, + ); + updateCurrentConfigColumns(currentConfig, currentColumns); + } } - return { delete: true }; + return { delete: needDelete }; }, canDrop: (item: ChartDataSectionField, monitor) => { + let items = Array.isArray(item) ? item : [item]; + if ( typeof currentConfig.actions === 'object' && - !(item.type in currentConfig.actions) + !items.every(val => val.type in (currentConfig.actions || {})) ) { + //zh: 判断现在拖动的数据项是否可以拖动到当前容器中 en: Determine whether the currently dragged data item can be dragged into the current container return false; } + // if ( + // typeof currentConfig.actions === 'object' && + // !(item.type in currentConfig.actions) + // ) { + // return false; + // } + if (currentConfig.allowSameField) { return true; } - let items = [item]; if ( - monitor.getItemType() === - CHART_DRAG_ELEMENT_TYPE.DATASET_GROUP_COLUMNS + monitor.getItemType() === CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN ) { - items = item as any; + return true; } - const exists = currentConfig.rows?.map(col => col.colName); return items.every(i => !exists?.includes(i.colName)); }, @@ -149,47 +174,77 @@ export const ChartDraggableTargetContainer: FC = setCurrentConfig(config); }, [config]); + const updateCurrentConfigColumns = ( + currentConfig, + newColumns, + refreshDataset = false, + ) => { + const newCurrentConfig = updateByKey(currentConfig, 'rows', newColumns); + setCurrentConfig(newCurrentConfig); + onConfigChanged?.(ancestors, newCurrentConfig, refreshDataset); + }; + const getDefaultAggregate = item => { if ( currentConfig?.type === ChartDataSectionType.AGGREGATE || currentConfig?.type === ChartDataSectionType.SIZE || - currentConfig?.type === ChartDataSectionType.INFO + currentConfig?.type === ChartDataSectionType.INFO || + currentConfig?.type === ChartDataSectionType.MIXED ) { + if (currentConfig.disableAggregate) { + return; + } if ( - item.category !== + item.category === (ChartDataViewFieldCategory.AggregateComputedField as string) ) { - let aggType: string = ''; - if (currentConfig?.actions instanceof Array) { - currentConfig?.actions?.find( - type => - type === ChartDataSectionFieldActionType.Aggregate || - type === ChartDataSectionFieldActionType.AggregateLimit, - ); - } else if (currentConfig?.actions instanceof Object) { - aggType = currentConfig?.actions?.[item?.type]?.find( - type => - type === ChartDataSectionFieldActionType.Aggregate || - type === ChartDataSectionFieldActionType.AggregateLimit, - ); - } - if (aggType) { - return AggregateFieldSubAggregateType?.[aggType]?.[0]; - } + return; + } + + let aggType: string = ''; + if (currentConfig?.actions instanceof Array) { + currentConfig?.actions?.find( + type => + type === ChartDataSectionFieldActionType.Aggregate || + type === ChartDataSectionFieldActionType.AggregateLimit, + ); + } else if (currentConfig?.actions instanceof Object) { + aggType = currentConfig?.actions?.[item?.type]?.find( + type => + type === ChartDataSectionFieldActionType.Aggregate || + type === ChartDataSectionFieldActionType.AggregateLimit, + ); + } + if (aggType) { + return AggregateFieldSubAggregateType?.[aggType]?.[0]; } } }; const onDraggableItemMove = (dragIndex: number, hoverIndex: number) => { const draggedItem = currentConfig.rows?.[dragIndex]; - - if (draggedItem && !currentConfig?.rows?.length) { + if (draggedItem) { const newCurrentConfig = updateBy(currentConfig, draft => { const columns = draft.rows || []; columns.splice(dragIndex, 1); columns.splice(hoverIndex, 0, draggedItem); }); setCurrentConfig(newCurrentConfig); + } else { + // const placeholder = { + // uid: CHARTCONFIG_FIELD_PLACEHOLDER_UID, + // colName: 'Placeholder', + // category: 'field', + // type: 'STRING', + // } as any; + // const newCurrentConfig = updateBy(currentConfig, draft => { + // const columns = draft.rows || []; + // if (dragIndex) { + // columns.splice(dragIndex, 1); + // } + // columns.splice(hoverIndex, 0, placeholder); + // }); + // setCurrentConfig(newCurrentConfig); } }; @@ -221,7 +276,7 @@ export const ChartDraggableTargetContainer: FC = return currentConfig.rows?.map((columnConfig, index) => { return ( - = {currentConfig?.actions && ( )} - {getColumnRenderName(columnConfig)} + + {aggregation + ? getColumnRenderName(columnConfig) + : columnConfig.colName} + {enableActionsIcons(columnConfig)} @@ -254,7 +313,7 @@ export const ChartDraggableTargetContainer: FC = }} moveCard={onDraggableItemMove} onDelete={handleOnDeleteItem(columnConfig.uid)} - > + > ); }); }; @@ -287,6 +346,7 @@ export const ChartDraggableTargetContainer: FC = dataset, dataView, modalSize, + aggregation, ); }; 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 23e4683d4..525c2b103 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 @@ -16,9 +16,14 @@ * limitations under the License. */ -import { Col, Row } from 'antd'; +import { Col, Popover, Row } from 'antd'; import Theme from 'app/assets/theme/echarts_default_theme.json'; -import { ColorTag, ReactColorPicker } from 'app/components/ReactColorPicker'; +import { + ColorTag, + SingleColorSelection, + ThemeColorSelection, +} from 'app/components/ColorPicker'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; import { updateBy } from 'app/utils/mutation'; @@ -33,39 +38,16 @@ const AggregationColorizeAction: FC<{ config: ChartDataSectionField, needRefresh?: boolean, ) => void; -}> = memo(({ config, dataset, onConfigChange }) => { + i18nPrefix?: string; +}> = memo(({ config, dataset, onConfigChange, i18nPrefix }) => { const actionNeedNewRequest = true; - const [themeColors] = useState(Theme.color); - const [colors, setColors] = useState(() => { - const colorizedColumnName = config.colName; - const colorizeIndex = - dataset?.columns?.findIndex(r => r.name === colorizedColumnName) || 0; - const colorizedGroupValues = Array.from( - new Set(dataset?.rows?.map(r => r[colorizeIndex])), - ); - const originalColors = config.color?.colors || []; - const themeColorTotalCount = themeColors.length; - return colorizedGroupValues.filter(Boolean).map((k, index) => { - return { - key: k!, - value: - originalColors.find(pc => pc.key === k)?.value || - themeColors[index % themeColorTotalCount], - }; - }); - }); + const [themeColors, setThemeColors] = useState(Theme.color); + const [colors, setColors] = useState<{ key: string; value: string }[]>( + setColorFn(config, dataset, themeColors), + ); const [selectColor, setSelectColor] = useState(colors[0]); - - // useMount(() => { - // if (!config?.color) { - // onConfigChange( - // updateBy(config, draft => { - // draft.color = { colors: colors as any }; - // }), - // actionNeedNewRequest, - // ); - // } - // }); + const [selColorBoxStatus, setSelColorBoxStatus] = useState(false); + const t = useI18NPrefix(i18nPrefix); const handleColorChange = value => { if (selectColor) { @@ -79,7 +61,6 @@ const AggregationColorizeAction: FC<{ setColors(newColors); setSelectColor(currentSelectColor); - onConfigChange( updateBy(config, draft => { draft.color = { colors: newColors }; @@ -87,13 +68,21 @@ const AggregationColorizeAction: FC<{ actionNeedNewRequest, ); } + + setSelColorBoxStatus(false); }; const renderGroupColors = () => { return ( <> {colors.map(c => ( - setSelectColor(c)}> + { + setSelColorBoxStatus(true); + setSelectColor(c); + }} + > {' '} {c.key} @@ -104,24 +93,76 @@ const AggregationColorizeAction: FC<{ ); }; + const selectThemeColorFn = colorArr => { + let selectColor1 = setColorFn(config, dataset, colorArr, true); + + setThemeColors(colorArr); + setColors(selectColor1); + onConfigChange( + updateBy(config, draft => { + draft.color = { colors: selectColor1 }; + }), + actionNeedNewRequest, + ); + }; return ( - - - {renderGroupColors()} - - - - - + <> + + {t('chooseTheme')} + + + + {renderGroupColors()} + + + } + > + + > ); }); export default AggregationColorizeAction; +function setColorFn( + config, + dataset, + themeColors, + need = false, +): { key: string; value: string }[] { + const colorizedColumnName = config.colName; + const colorizeIndex = + dataset?.columns?.findIndex(r => r.name === colorizedColumnName) || 0; + const colorizedGroupValues = Array.from( + new Set(dataset?.rows?.map(r => String(r[colorizeIndex]))), + ); + const originalColors = config.color?.colors || []; + const themeColorTotalCount = themeColors.length; + + return colorizedGroupValues + .filter(Boolean) + .map((k, index): { key: string; value: string } => { + return { + key: k! as string, + value: need + ? themeColors[index % themeColorTotalCount] + : originalColors.find(pc => pc.key === k)?.value || + themeColors[index % themeColorTotalCount], + }; + }); +} + const StyledUL = styled.ul` padding-inline-start: 0; max-height: 300px; @@ -132,7 +173,7 @@ const StyledUL = styled.ul` flex-wrap: nowrap; align-items: center; justify-content: flex-start; - padding: 0 ${SPACE_MD}; + padding-right: ${SPACE_MD}; cursor: pointer; .text-span { margin-left: ${SPACE_XS}; @@ -140,5 +181,8 @@ const StyledUL = styled.ul` text-overflow: ellipsis; white-space: nowrap; } + &:hover { + background-color: rgba(27, 154, 238, 0.05); + } } `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AliasAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AliasAction.tsx index 50e385d57..8a087f6d7 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AliasAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AliasAction.tsx @@ -16,16 +16,24 @@ * limitations under the License. */ -import { Input } from 'antd'; +import { Input, Space } from 'antd'; +import { FormItemEx } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField } from 'app/types/ChartConfig'; +import { getColumnRenderOriginName } from 'app/utils/chartHelper'; import { updateBy } from 'app/utils/mutation'; import { FC, useState } from 'react'; +import styled from 'styled-components/macro'; const AliasAction: FC<{ config: ChartDataSectionField; onConfigChange: (config: ChartDataSectionField) => void; }> = ({ config, onConfigChange }) => { + const formItemLayout = { + labelAlign: 'right' as any, + labelCol: { span: 8 }, + wrapperCol: { span: 8 }, + }; const t = useI18NPrefix(`viz.palette.data.actions`); const [aliasName, setAliasName] = useState(config?.alias?.name); const [nameDesc, setNameDesc] = useState(config?.alias?.desc); @@ -40,23 +48,32 @@ const AliasAction: FC<{ }; return ( - - {t('alias.name')} - { - onChange(value, nameDesc); - }} - /> - {t('alias.description')} - { - onChange(aliasName, value); - }} - /> - + + + {getColumnRenderOriginName(config)} + + + { + onChange(value, nameDesc); + }} + /> + + + { + onChange(aliasName, value); + }} + /> + + ); }; export default AliasAction; + +const StyledAliasAction = styled(Space)` + width: 100%; +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeRangeAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeRangeAction.tsx index 86daff594..404f85e9e 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeRangeAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeRangeAction.tsx @@ -17,8 +17,9 @@ */ import { Checkbox, Col, Row } from 'antd'; +import { ColorPickerPopover } from 'app/components/ColorPicker'; +import { ColorPicker } from 'app/components/ColorPicker/ColorTag'; import { FormItemEx } from 'app/components/From'; -import { ReactColorPicker } from 'app/components/ReactColorPicker'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; @@ -54,7 +55,7 @@ const ColorizeRangeAction: FC<{ onConfigChange?.(newConfig, actionNeedNewRequest); }; - const hanldeEnableColorChecked = checked => { + const handleEnableColorChecked = checked => { if (Boolean(checked)) { handleColorRangeChange('#7567bd', '#7567bd'); } else { @@ -67,7 +68,7 @@ const ColorizeRangeAction: FC<{ hanldeEnableColorChecked(e.target?.checked)} + onChange={e => handleEnableColorChecked(e.target?.checked)} > {t('color.enable')} @@ -79,13 +80,15 @@ const ColorizeRangeAction: FC<{ name="StartColor" rules={[{ required: true }]} initialValue={colorRange?.start} + className="form-item-ex" > - { handleColorRangeChange(v, colorRange?.end); }} - /> + > + + @@ -96,13 +99,15 @@ const ColorizeRangeAction: FC<{ name="EndColor" rules={[{ required: true }]} initialValue={colorRange?.end} + className="form-item-ex" > - { handleColorRangeChange(colorRange?.start, v); }} - /> + > + + @@ -114,4 +119,13 @@ export default ColorizeRangeAction; const StyledColorizeRangeAction = styled(Row)` justify-content: center; + .ColorPicker { + border: 1px solid ${p => p.theme.borderColorBase}; + } + .form-item-ex { + width: 100%; + .ant-form-item-control-input { + width: auto; + } + } `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeSingleAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeSingleAction.tsx index 7cc9f8316..479dd19e7 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeSingleAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeSingleAction.tsx @@ -17,7 +17,7 @@ */ import { Checkbox, Col, Row } from 'antd'; -import { ReactColorPicker } from 'app/components/ReactColorPicker'; +import { SingleColorSelection } from 'app/components/ColorPicker'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; @@ -55,7 +55,7 @@ const ColorizeSingleAction: FC<{ onConfigChange?.(newConfig, actionNeedNewRequest); }; - const hanldeEnableColorChecked = checked => { + const handleEnableColorChecked = checked => { if (Boolean(checked)) { handleColorChange('#7567bd'); } else { @@ -65,19 +65,19 @@ const ColorizeSingleAction: FC<{ return ( - + hanldeEnableColorChecked(e.target?.checked)} + onChange={e => handleEnableColorChecked(e.target?.checked)} > {t('color.enable')} - handleColorChange(v)} + 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 37483db53..eeb83212e 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 @@ -30,25 +30,29 @@ const FilterAction: FC<{ dataset?: ChartDataset; dataView?: ChartDataView; dataConfig?: ChartDataSectionConfig; + aggregation?: boolean; onConfigChange: ( config: ChartDataSectionField, needRefresh?: boolean, ) => void; -}> = memo(({ config, dataset, dataView, dataConfig, onConfigChange }) => { - const handleFetchDataFromField = async fieldId => { - // TODO: tobe implement to get fields - return await Promise.resolve(['a', 'b', 'c'].map(f => `${fieldId}-${f}`)); - }; - return ( - - ); -}); +}> = memo( + ({ config, dataset, dataView, dataConfig, onConfigChange, aggregation }) => { + const handleFetchDataFromField = async fieldId => { + // TODO: tobe implement to get fields + return await Promise.resolve(['a', 'b', 'c'].map(f => `${fieldId}-${f}`)); + }; + return ( + + ); + }, +); export default FilterAction; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionConfiguration.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionConfiguration.tsx index 963c5f79e..dd3977641 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionConfiguration.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionConfiguration.tsx @@ -21,7 +21,7 @@ import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; import useMount from 'app/hooks/useMount'; import { FilterConditionType, - FilterValueOption, + RelationFilterValue, } from 'app/types/ChartConfig'; import ChartDataView from 'app/types/ChartDataView'; import { getDistinctFields } from 'app/utils/fetch'; @@ -33,7 +33,6 @@ import ChartFilterCondition, { ConditionBuilder, } from '../../../../../models/ChartFilterCondition'; import CategoryConditionEditableTable from './CategoryConditionEditableTable'; -// import CategoryConditionEditableTable from './CategoryConditionEditableTableBak'; import CategoryConditionRelationSelector from './CategoryConditionRelationSelector'; const CategoryConditionConfiguration: FC< @@ -72,12 +71,12 @@ const CategoryConditionConfiguration: FC< if (Array.isArray(condition?.value)) { const firstValues = (condition?.value as [])?.filter(n => { - if (IsKeyIn(n as FilterValueOption, 'key')) { - return (n as FilterValueOption).isSelected; + if (IsKeyIn(n as RelationFilterValue, 'key')) { + return (n as RelationFilterValue).isSelected; } return false; }) || []; - values = firstValues?.map((n: FilterValueOption) => n.key); + values = firstValues?.map((n: RelationFilterValue) => n.key); } } return values || []; @@ -85,8 +84,8 @@ const CategoryConditionConfiguration: FC< const [selectedKeys, setSelectedKeys] = useState([]); const [isTree, setIsTree] = useState(isTreeModel(condition?.value)); const [treeOptions, setTreeOptions] = useState([]); - const [listDatas, setListDatas] = useState([]); - const [treeDatas, setTreeDatas] = useState([]); + const [listDatas, setListDatas] = useState([]); + const [treeDatas, setTreeDatas] = useState([]); useMount(() => { if (curTab === FilterConditionType.List) { @@ -102,17 +101,17 @@ const CategoryConditionConfiguration: FC< selectedKeys.indexOf(eventKey) !== -1; const fetchNewDataset = async (viewId, colName) => { - const feildDataset = await getDistinctFields( + const fieldDataset = await getDistinctFields( viewId, colName, undefined, undefined, ); - return feildDataset; + return fieldDataset; }; - const setListSelctedState = ( - list?: FilterValueOption[], + const setListSelectedState = ( + list?: RelationFilterValue[], keys?: string[], ) => { return (list || []).map(c => @@ -121,7 +120,7 @@ const CategoryConditionConfiguration: FC< }; const setTreeCheckableState = ( - treeList?: FilterValueOption[], + treeList?: RelationFilterValue[], keys?: string[], ) => { return (treeList || []).map(c => { @@ -132,7 +131,7 @@ const CategoryConditionConfiguration: FC< }; const handleGeneralListChange = async selectedKeys => { - const items = setListSelctedState(listDatas, selectedKeys); + const items = setListSelectedState(listDatas, selectedKeys); setTargetKeys(selectedKeys); setListDatas(items); @@ -178,27 +177,21 @@ const CategoryConditionConfiguration: FC< // setListDatas(convertToList(dataset?.columns, selectedKeys)); } else { setListDatas(convertToList(dataset?.rows, selectedKeys)); - setTargetKeys([]); - const filter = new ConditionBuilder(condition) - .setOperator(FilterSqlOperator.In) - .setValue([]) - .asGeneral(); - onConditionChange(filter); } }); }; - const convertToList = (collection, selecteKeys) => { + const convertToList = (collection, selectedKeys) => { const items: string[] = (collection || []).flatMap(c => c); const uniqueKeys = Array.from(new Set(items)); return uniqueKeys.map(item => ({ key: item, label: item, - isSelected: selecteKeys.includes(item), + isSelected: selectedKeys.includes(item), })); }; - const convertToTree = (collection, selecteKeys) => { + const convertToTree = (collection, selectedKeys) => { const associateField = treeOptions?.[0]; const labelField = treeOptions?.[1]; @@ -215,25 +208,25 @@ const CategoryConditionConfiguration: FC< if (!associateItem) { return null; } - const assocaiteChildren = collection + const associateChildren = collection .filter(c => c[associateField] === key) .map(c => { const itemKey = c[labelField]; return { key: itemKey, label: itemKey, - isSelected: isChecked(selecteKeys, itemKey), + isSelected: isChecked(selectedKeys, itemKey), }; }); const itemKey = associateItem?.[colName]; return { key: itemKey, label: itemKey, - isSelected: isChecked(selecteKeys, itemKey), - children: assocaiteChildren, + isSelected: isChecked(selectedKeys, itemKey), + children: associateChildren, }; }) - .filter(i => Boolean(i)) as FilterValueOption[]; + .filter(i => Boolean(i)) as RelationFilterValue[]; return treeNodes; }; 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 d2364921a..ef6a7ba5a 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 @@ -19,11 +19,11 @@ import { Button, Space } from 'antd'; import DragSortEditTable from 'app/components/DragSortEditTable'; import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import { FilterValueOption } from 'app/types/ChartConfig'; -import ChartDataView from 'app/types/ChartDataView'; import ChartFilterCondition, { ConditionBuilder, } from 'app/pages/ChartWorkbenchPage/models/ChartFilterCondition'; +import { RelationFilterValue } from 'app/types/ChartConfig'; +import ChartDataView from 'app/types/ChartDataView'; import { getDistinctFields } from 'app/utils/fetch'; import { FilterSqlOperator } from 'globalConstants'; import { FC, memo, useCallback, useEffect, useState } from 'react'; @@ -44,12 +44,11 @@ const CategoryConditionEditableTable: FC< fetchDataByField, }) => { const t = useI18NPrefix(i18nPrefix); - const [rows, setRows] = useState([]); - const [showPopover, setShowPopover] = useState(false); + const [rows, setRows] = useState([]); useEffect(() => { if (Array.isArray(condition?.value)) { - setRows(condition?.value as FilterValueOption[]); + setRows(condition?.value as RelationFilterValue[]); } else { setRows([]); } @@ -78,15 +77,13 @@ const CategoryConditionEditableTable: FC< title: t('tableHeaderAction'), dataIndex: 'action', width: 80, - render: (_, record: FilterValueOption) => ( + render: (_, record: RelationFilterValue) => ( {!record.isSelected && ( - handleRowStateUpdate( - Object.assign(record, { isSelected: true }), - ) + handleRowStateUpdate({ ...record, isSelected: true }) } > {t('setDefault')} @@ -96,9 +93,7 @@ const CategoryConditionEditableTable: FC< - handleRowStateUpdate( - Object.assign(record, { isSelected: false }), - ) + handleRowStateUpdate({ ...record, isSelected: false }) } > {t('setUnDefault')} @@ -118,7 +113,7 @@ const CategoryConditionEditableTable: FC< } return { ...col, - onCell: (record: FilterValueOption) => ({ + onCell: (record: RelationFilterValue) => ({ record, editable: col.editable, dataIndex: col.dataIndex, @@ -144,7 +139,7 @@ const CategoryConditionEditableTable: FC< const handleAdd = () => { const newKey = rows?.length + 1; - const newRow: FilterValueOption = { + const newRow: RelationFilterValue = { key: String(newKey), label: String(newKey), isSelected: false, @@ -153,14 +148,14 @@ const CategoryConditionEditableTable: FC< handleFilterConditionChange(currentRows); }; - const handleRowStateUpdate = (row: FilterValueOption) => { - const oldRowIndex = rows.findIndex(r => r.index === row.index); - rows.splice(oldRowIndex, 1, row); - handleFilterConditionChange(rows); + const handleRowStateUpdate = (row: RelationFilterValue) => { + const newRows = [...rows]; + const targetIndex = newRows.findIndex(r => r.index === row.index); + newRows.splice(targetIndex, 1, row); + handleFilterConditionChange(newRows); }; const handleFetchDataFromField = field => async () => { - setShowPopover(false); if (fetchDataByField) { const dataset = await fetchNewDataset(dataView?.id!, field); const newRows = convertToList(dataset?.rows, []); @@ -181,23 +176,23 @@ const CategoryConditionEditableTable: FC< ); const fetchNewDataset = async (viewId, colName) => { - const feildDataset = await getDistinctFields( + const fieldDataset = await getDistinctFields( viewId, colName, undefined, undefined, ); - return feildDataset; + return fieldDataset; }; - const convertToList = (collection, selecteKeys) => { + 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, key: item, label: item, - isSelected: selecteKeys.includes(item), + isSelected: selectedKeys.includes(item), })); }; @@ -216,10 +211,9 @@ const CategoryConditionEditableTable: FC< dataSource={rows} size="small" bordered - rowKey={(r: FilterValueOption) => `${r.key}-${r.label}`} + rowKey={(r: RelationFilterValue) => `${r.key}-${r.label}`} columns={columnsWithCell} pagination={false} - // onMoveRowEnd={onMoveRowEnd} onRow={(_, index) => ({ index, 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 07cdde075..8fcb7b9e7 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 @@ -18,8 +18,18 @@ import { Tabs } from 'antd'; import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import { FC, memo } from 'react'; -import ChartFilterCondition from '../../../../../models/ChartFilterCondition'; +import { FilterConditionType } from 'app/types/ChartConfig'; +import { formatTime } from 'app/utils/time'; +import { + FILTER_TIME_FORMATTER_IN_QUERY, + RECOMMEND_TIME, +} from 'globalConstants'; +import moment from 'moment'; +import { FC, memo, useState } from 'react'; +import styled from 'styled-components/macro'; +import ChartFilterCondition, { + ConditionBuilder, +} from '../../../../../models/ChartFilterCondition'; import TimeSelector from '../../ChartTimeSelector'; const DateConditionConfiguration: FC< @@ -29,27 +39,65 @@ const DateConditionConfiguration: FC< } & I18NComponentProps > = memo(({ i18nPrefix, condition, onChange: onConditionChange }) => { const t = useI18NPrefix(i18nPrefix); + const [type, setType] = useState(() => + condition?.type === FilterConditionType.RangeTime + ? String(FilterConditionType.RangeTime) + : String(FilterConditionType.RecommendTime), + ); + + const clearFilterWhenTypeChange = (type: string) => { + setType(type); + const conditionType = Number(type); + if (conditionType === FilterConditionType.RecommendTime) { + const filter = new ConditionBuilder(condition) + .setValue(RECOMMEND_TIME.TODAY) + .asRecommendTime(); + onConditionChange?.(filter); + } else if (conditionType === FilterConditionType.RangeTime) { + const filterRow = new ConditionBuilder(condition) + .setValue([ + formatTime(moment(), FILTER_TIME_FORMATTER_IN_QUERY), + formatTime(moment(), FILTER_TIME_FORMATTER_IN_QUERY), + ]) + .asRangeTime(); + onConditionChange?.(filterRow); + } + }; return ( - - - - - - - - - - + + + + + + + + ); }); export default DateConditionConfiguration; + +const StyledDateConditionConfiguration = styled(Tabs)` + width: 100%; + padding: 0 !important; + + .ant-tabs-content-holder { + margin: 10px 0; + } +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterControlPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterControlPanel.tsx index b657782ba..75644c518 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterControlPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterControlPanel.tsx @@ -50,6 +50,7 @@ const FilterControllPanel: FC< dataset?: ChartDataset; dataView?: ChartDataView; dataConfig?: ChartDataSectionConfig; + aggregation?: boolean; onConfigChange: ( config: ChartDataSectionField, needRefresh?: boolean, @@ -63,6 +64,7 @@ const FilterControllPanel: FC< dataView, i18nPrefix, dataConfig, + aggregation, onConfigChange, fetchDataByField, }) => { @@ -75,6 +77,9 @@ const FilterControllPanel: FC< const t = useI18NPrefix(customizeI18NPrefix); const [alias, setAlias] = useState(config.alias); const [aggregate, setAggregate] = useState(() => { + if (Boolean(dataConfig?.disableAggregate) || aggregation === false) { + return AggregateFieldActionType.NONE; + } if (config.aggregate) { return config.aggregate; } else if ( @@ -217,7 +222,7 @@ const FilterControllPanel: FC< > handleNameChange(e.target?.value)} /> - {config.category === ChartDataViewFieldCategory.Field && ( + {config.category === ChartDataViewFieldCategory.Field && aggregation && ( { case FilterConditionType.Value: return [ControllerFacadeTypes.Value]; case FilterConditionType.RangeTime: - case FilterConditionType.RelativeTime: - return [ControllerFacadeTypes.RangeTime]; + return [ControllerFacadeTypes.RangeTimePicker]; + case FilterConditionType.RecommendTime: + return [ControllerFacadeTypes.RangeTimePicker]; case FilterConditionType.Tree: return [ControllerFacadeTypes.Tree]; } @@ -133,6 +131,10 @@ const FilterFacadeConfiguration: FC< !facades.includes(currentFacade as ControllerFacadeTypes) ) { setCurrentFacade(undefined); + handleFacadeChange(undefined); + } else if (!currentFacade) { + setCurrentFacade(facades?.[0]); + handleFacadeChange(facades?.[0]); } }, [condition, category, currentFacade]); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterVisibilityConfiguration.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterVisibilityConfiguration.tsx index 122a3f80c..adf857386 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterVisibilityConfiguration.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterVisibilityConfiguration.tsx @@ -18,10 +18,7 @@ import { Input, Radio, Row, Select, Space } from 'antd'; import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import { - ChartDataSectionField, - FilterVisibility, -} from 'app/types/ChartConfig'; +import { ChartDataSectionField, FilterVisibility } from 'app/types/ChartConfig'; import { ControllerVisibilityTypes } from 'app/types/FilterControlPanel'; import { FilterSqlOperator } from 'globalConstants'; import { FC, memo, useState } from 'react'; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/NumberFormatAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/NumberFormatAction.tsx index 98af535ea..fa413c1df 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/NumberFormatAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/NumberFormatAction.tsx @@ -26,6 +26,7 @@ import { Select, Space, } from 'antd'; +import { FormItemEx } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField, @@ -37,6 +38,7 @@ import { updateBy } from 'app/utils/mutation'; import { NumberUnitKey, NumericUnitDescriptions } from 'globalConstants'; import { FC, useState } from 'react'; import styled from 'styled-components/macro'; +import { SPACE_TIMES } from 'styles/StyleConstants'; const DefaultFormatDetailConfig: IFieldFormatConfig = { type: FieldFormatType.DEFAULT, @@ -71,6 +73,11 @@ const NumberFormatAction: FC<{ config: ChartDataSectionField; onConfigChange: (config: ChartDataSectionField) => void; }> = ({ config, onConfigChange }) => { + const formItemLayout = { + labelAlign: 'right' as any, + labelCol: { span: 8 }, + wrapperCol: { span: 8 }, + }; const t = useI18NPrefix(`viz.palette.data.actions`); const [type, setType] = useState( @@ -112,149 +119,129 @@ const NumberFormatAction: FC<{ return null; } else { return ( - <> - - {t('format.decimalPlace')} - - { - handleFormatDetailChanged( - Object.assign({}, formatDetail, { decimalPlaces }), - ); - }} - /> - - + + + { + handleFormatDetailChanged( + Object.assign({}, formatDetail, { decimalPlaces }), + ); + }} + /> + + {FieldFormatType.CURRENCY === type && ( <> - - {t('format.unit')} - - { - handleFormatDetailChanged( - Object.assign({}, formatDetail, { unitKey }), - ); - }} - > - {Array.from(NumericUnitDescriptions.keys()).map(k => { - const values = NumericUnitDescriptions.get(k); - return ( - - {values?.[1] || ' '} - - ); - })} - - - - - {t('format.currency')} - - { - handleFormatDetailChanged( - Object.assign({}, formatDetail, { currency }), - ); - }} - > - {CURRENCIES.map(c => { - return ( - - {c.code} - - ); - })} - - - + + { + handleFormatDetailChanged( + Object.assign({}, formatDetail, { unitKey }), + ); + }} + > + {Array.from(NumericUnitDescriptions.keys()).map(k => { + const values = NumericUnitDescriptions.get(k); + return ( + + {values?.[1] || ' '} + + ); + })} + + + + { + handleFormatDetailChanged( + Object.assign({}, formatDetail, { currency }), + ); + }} + > + {CURRENCIES.map(c => { + return ( + + {c.code} + + ); + })} + + > )} {FieldFormatType.NUMERIC === type && ( <> - - {t('format.unit')} - - { - handleFormatDetailChanged( - Object.assign({}, formatDetail, { unitKey }), - ); - }} - > - {Array.from(NumericUnitDescriptions.keys()).map(k => { - const values = NumericUnitDescriptions.get(k); - return ( - - {values?.[1] || ' '} - - ); - })} - - - - - {t('format.useSeparator')} - - - handleFormatDetailChanged( - Object.assign({}, formatDetail, { - useThousandSeparator: e.target.checked, - }), - ) - } - /> - - - - {t('format.prefix')} - - - handleFormatDetailChanged( - Object.assign({}, formatDetail, { - prefix: e?.target?.value, - }), - ) - } - /> - - - - {t('format.suffix')} - - - handleFormatDetailChanged( - Object.assign({}, formatDetail, { - suffix: e?.target?.value, - }), - ) - } - /> - - + + { + handleFormatDetailChanged( + Object.assign({}, formatDetail, { unitKey }), + ); + }} + > + {Array.from(NumericUnitDescriptions.keys()).map(k => { + const values = NumericUnitDescriptions.get(k); + return ( + + {values?.[1] || ' '} + + ); + })} + + + + + handleFormatDetailChanged( + Object.assign({}, formatDetail, { + useThousandSeparator: e.target.checked, + }), + ) + } + /> + + + + handleFormatDetailChanged( + Object.assign({}, formatDetail, { + prefix: e?.target?.value, + }), + ) + } + /> + + + + handleFormatDetailChanged( + Object.assign({}, formatDetail, { + suffix: e?.target?.value, + }), + ) + } + /> + > )} - > + ); } }; return ( - + handleFormatTypeChanged(e.target.value)} value={type} @@ -274,16 +261,25 @@ const NumberFormatAction: FC<{ - {renderFieldFormatExtendSetting()} + {renderFieldFormatExtendSetting()} ); }; export default NumberFormatAction; -const StyledNumberFormatAction = styled(Row)``; +const StyledNumberFormatAction = styled(Row)` + .ant-radio-wrapper { + line-height: 32px; + } + + .ant-input-number, + .ant-select, + .ant-input { + width: ${SPACE_TIMES(50)}; + } -const StyledFormatDetailRow = styled(Row)` - align-items: center; - padding: 5px 0; + .ant-space { + width: 100%; + } `; 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 8250566c2..996b3effc 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 @@ -20,15 +20,16 @@ import { CheckOutlined } from '@ant-design/icons'; import { Col, Menu, Radio, Row, Space } from 'antd'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import DraggableList from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/DraggableList'; -import { - ChartDataSectionField, - SortActionType, -} from 'app/types/ChartConfig'; +import { ChartDataSectionField, SortActionType } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; -import { getValueByColumnKey, transfromToObjectArray } from 'app/utils/chartHelper'; +import { + getValueByColumnKey, + transformToObjectArray, +} from 'app/utils/chartHelper'; import { updateBy } from 'app/utils/mutation'; import { FC, useState } from 'react'; import styled from 'styled-components/macro'; +import { isEmpty } from 'utils/object'; const SortAction: FC<{ config: ChartDataSectionField; @@ -38,12 +39,15 @@ const SortAction: FC<{ needRefresh?: boolean, ) => void; mode?: 'menu'; -}> = ({ config, dataset, mode, onConfigChange }) => { - const actionNeedNewRequest = true; + options?; +}> = ({ config, dataset, mode, options, onConfigChange }) => { + const actionNeedNewRequest = isEmpty(options?.backendSort) + ? true + : Boolean(options?.backendSort); const t = useI18NPrefix(`viz.palette.data.actions`); const [direction, setDirection] = useState(config?.sort?.type); const [sortValue, setSortValue] = useState(() => { - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset?.rows, dataset?.columns, ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicAreaChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicAreaChart/config.ts index 88a853b10..7d737abec 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicAreaChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicAreaChart/config.ts @@ -94,6 +94,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -103,16 +123,7 @@ const config: ChartConfig = { showLabelBySwitch: '显示标签2', showLabelByInput: '显示标签3', showLabelWithSelect: '显示标签4', - fontFamily: '字体', - fontSize: '字体大小', - fontColor: '字体颜色', - rotateLabel: '旋转标签', showDataColumns: '选择数据列', - legend: { - label: '图例', - showLabel: '图例-显示标签', - showLabel2: '图例-显示标签2', - }, }, }, { @@ -123,6 +134,7 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + showDataColumns: 'Show Data Columns', }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/BasicBarChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/BasicBarChart.tsx index f6cd20f7d..d48f3afd0 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/BasicBarChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/BasicBarChart.tsx @@ -34,7 +34,7 @@ import { getSeriesTooltips4Rectangular2, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toExponential, @@ -120,7 +120,7 @@ class BasicBarChart extends Chart { .filter(c => c.type === ChartDataSectionType.INFO) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -162,7 +162,7 @@ class BasicBarChart extends Chart { return { tooltip: { trigger: 'item', - formatter: this.getTooltipFormmaterFunc( + formatter: this.getTooltipFormatterFunc( styleConfigs, groupConfigs, aggregateConfigs, @@ -535,6 +535,7 @@ class BasicBarChart extends Chart { return labels.join('\n'); }, }, + labelLayout: { hideOverlap: true }, }; } @@ -579,7 +580,7 @@ class BasicBarChart extends Chart { return `${label}: ${value}`; } - getTooltipFormmaterFunc( + getTooltipFormatterFunc( styleConfigs, groupConfigs, aggregateConfigs, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/config.ts index 0a171ad76..ab639b619 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/config.ts @@ -171,6 +171,7 @@ const config: ChartConfig = { comType: 'select', default: 'top', options: { + // TODO(Stephen): to be extract customize LabelPosition Component items: [ { label: '上', value: 'top' }, { label: '左', value: 'left' }, @@ -502,27 +503,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -597,8 +604,76 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/BasicDoubleYChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/BasicDoubleYChart.tsx index 927fd857a..d9986b0cf 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/BasicDoubleYChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/BasicDoubleYChart.tsx @@ -36,7 +36,7 @@ import { getSplitLine, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; @@ -95,7 +95,7 @@ class BasicDoubleYChart extends Chart { const styleConfigs = config.styles || []; const settingConfigs = config.settings; - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/config.ts index 3f1c90142..262bda483 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/config.ts @@ -387,27 +387,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -475,9 +481,6 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/BasicFunnelChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/BasicFunnelChart.tsx index 764758129..fad73fee6 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/BasicFunnelChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/BasicFunnelChart.tsx @@ -30,11 +30,11 @@ import { getSeriesTooltips4Scatter, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; -import { isEmpty } from 'lodash'; +import isEmpty from 'lodash/isEmpty'; import Config from './config'; class BasicFunnelChart extends Chart { @@ -99,23 +99,37 @@ class BasicFunnelChart extends Chart { .filter(c => c.type === ChartDataSectionType.INFO) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); + const dataList = !groupConfigs.length + ? objDataColumns + : objDataColumns?.sort( + (a, b) => + b?.[getValueByColumnKey(aggregateConfigs[0])] - + a?.[getValueByColumnKey(aggregateConfigs[0])], + ); + const aggregateList = !groupConfigs.length + ? aggregateConfigs?.sort( + (a, b) => + objDataColumns?.[0]?.[getValueByColumnKey(b)] - + objDataColumns?.[0]?.[getValueByColumnKey(a)], + ) + : aggregateConfigs; const series = this.getSeries( styleConfigs, - aggregateConfigs, + aggregateList, groupConfigs, - objDataColumns, + dataList, infoConfigs, ); return { tooltip: this.getFunnelChartTooltip( groupConfigs, - aggregateConfigs, + aggregateList, infoConfigs, ), legend: this.getLegendStyle(styleConfigs), @@ -244,7 +258,7 @@ class BasicFunnelChart extends Chart { return { ...aggConfig, select: selectAll, - value: aggregateConfigs + value: [aggConfig] .concat(infoConfigs) .map(config => dc?.[getValueByColumnKey(config)]), name: getColumnRenderName(aggConfig), @@ -272,6 +286,7 @@ class BasicFunnelChart extends Chart { shadowColor: 'rgba(0, 0, 0, 0.5)', }, label: this.getLabelStyle(styles), + labelLayout: { hideOverlap: true }, data: this.getFunnelSeriesData(datas), }; } @@ -355,10 +370,12 @@ class BasicFunnelChart extends Chart { }`, ] : []; - const aggTooltips = getSeriesTooltips4Scatter( - [params], - aggregateConfigs.concat(infoConfigs), - ); + const aggTooltips = !!groupConfigs?.length + ? getSeriesTooltips4Scatter( + [params], + aggregateConfigs.concat(infoConfigs), + ) + : getSeriesTooltips4Scatter([params], [data].concat(infoConfigs)); tooltips = tooltips.concat(aggTooltips); if (data.conversion) { tooltips.push(`转化率: ${data.conversion}%`); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/config.ts index 5e172089f..97a989b16 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/config.ts @@ -71,8 +71,8 @@ const config: ChartConfig = { { label: 'funnel.align', key: 'align', - comType: 'select', default: 'center', + comType: 'select', options: { items: [ { label: '居中', value: 'center' }, @@ -239,14 +239,20 @@ const config: ChartConfig = { ], settings: [ { - label: 'cache.title', - key: 'cache', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'cache.title', - key: 'panel', - comType: 'cache', + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, @@ -287,8 +293,42 @@ const config: ChartConfig = { color: '颜色', colorize: '配色', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + section: { + legend: 'Legend', + detail: 'Detail', + info: 'Info', + }, + label: { + title: 'Title', + showLabel: 'Show Label', + position: 'Position', + metric: 'Metric', + dimension: 'Dimension', + conversion: 'Conversion', + arrival: 'Arrival', + percentage: 'Percentage', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + funnel: { + title: 'Funnel', + sort: 'Sort', + align: 'Alignment', + gap: 'Gap', + }, + data: { + color: 'Color', + colorize: 'Colorize', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/BasicGaugeChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/BasicGaugeChart.tsx index fc06358c1..53b6312f2 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/BasicGaugeChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/BasicGaugeChart.tsx @@ -23,8 +23,9 @@ import { getColumnRenderName, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; +import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; import Config from './config'; @@ -89,18 +90,32 @@ class BasicGaugeChart extends Chart { const aggregateConfigs = dataConfigs .filter(c => c.type === ChartDataSectionType.AGGREGATE) .flatMap(config => config.rows || []); - const dataColumns = transfromToObjectArray(dataset.rows, dataset.columns); - const series = this.getSeries(styleConfigs, dataColumns, aggregateConfigs); + const dataColumns = transformToObjectArray(dataset.rows, dataset.columns); + const series = this.getSeries( + styleConfigs, + dataColumns, + aggregateConfigs[0], + ); + const [prefix, suffix] = this.getArrStyleValueByGroup( + ['prefix', 'suffix'], + styleConfigs, + 'gauge', + ); return { tooltip: { - formatter: '{b} : {c}%', + formatter: ({ data }) => { + return `${data.name} : ${prefix}${toFormattedValue( + data.value, + aggregateConfigs[0].format, + )}${suffix}`; + }, }, series, }; } - private getSeries(styleConfigs, dataColumns, aggregateConfigs) { - const detail = this.getDetail(styleConfigs); + private getSeries(styleConfigs, dataColumns, aggConfig) { + const detail = this.getDetail(styleConfigs, aggConfig); const title = this.getTitle(styleConfigs); const pointer = this.getPointer(styleConfigs); const axis = this.getAxis(styleConfigs); @@ -113,30 +128,29 @@ class BasicGaugeChart extends Chart { 'pointerColor', ); - return aggregateConfigs.map(aggConfig => { - return { - ...this.getGauge(styleConfigs), - data: dataColumns.map(dc => { - const dataConfig: { name: string; value: string; itemStyle: any } = { - name: getColumnRenderName(aggConfig), - value: dc[getValueByColumnKey(aggConfig)] || 0, - itemStyle: { - color: pointerColor, - }, - }; - if (aggConfig?.color?.start) { - dataConfig.itemStyle.color = aggConfig.color.start; - } - return dataConfig; - }), - pointer, - ...axis, - title, - splitLine, - detail, - progress, - }; - }); + const dataConfig: { name: string; value: string; itemStyle: any } = { + name: getColumnRenderName(aggConfig), + value: dataColumns?.[0]?.[getValueByColumnKey(aggConfig)] || 0, + itemStyle: { + color: pointerColor, + }, + }; + if (aggConfig?.color?.start) { + dataConfig.itemStyle.color = aggConfig.color.start; + } + + return { + ...this.getGauge(styleConfigs), + data: [ + dataConfig, + ], + pointer, + ...axis, + title, + splitLine, + detail, + progress, + }; } private getProgress(styleConfigs) { @@ -257,7 +271,7 @@ class BasicGaugeChart extends Chart { }; } - private getDetail(styleConfigs) { + private getDetail(styleConfigs, aggConfig) { const [show, font, detailOffsetLeft, detailOffsetTop] = this.getArrStyleValueByGroup( ['showData', 'font', 'detailOffsetLeft', 'detailOffsetTop'], @@ -277,7 +291,8 @@ class BasicGaugeChart extends Chart { detailOffsetLeft ? detailOffsetLeft : 0, detailOffsetTop ? detailOffsetTop : 0, ], - formatter: value => `${prefix}${Number(value) || 0}${suffix}`, + formatter: value => + `${prefix}${toFormattedValue(value || 0, aggConfig.format)}${suffix}`, }; } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/config.ts index 8261ac848..c97244579 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/config.ts @@ -21,8 +21,8 @@ import { ChartConfig } from 'app/types/ChartConfig'; const config: ChartConfig = { datas: [ { - label: 'dimension', - key: 'dimension', + label: 'metrics', + key: 'metrics', required: true, type: 'aggregate', limit: 1, @@ -344,7 +344,26 @@ const config: ChartConfig = { ], }, ], - settings: [], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -407,6 +426,67 @@ const config: ChartConfig = { }, }, }, + { + lang: 'en-US', + translation: { + common: { + detailOffsetLeft: 'Offset Left', + detailOffsetTop: 'Offset Top', + distance: 'Distance', + lineStyle: 'Line Style', + splitNumber: 'Split Number', + }, + gauge: { + title: 'Gauge', + max: 'Max', + prefix: 'Prefix', + suffix: 'Suffix', + radius: 'Radius', + startAngle: 'Start Angle', + endAngle: 'End Angle', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + }, + data: { + title: 'Data', + showData: 'Show Data', + }, + pointer: { + title: 'Pointer', + showPointer: 'Show Pointer', + customPointerColor: 'Show Customize Pointer Color', + pointerColor: 'Pointer Color', + pointerLength: 'Pointer Length', + pointerWidth: 'Pointer Width', + lineStyle: 'Line Style', + }, + axis: { + title: 'Axis', + axisLineSize: 'Axis Line Size', + axisLineColor: 'Axis Line Color', + }, + axisTick: { + title: 'Axis Tick', + showAxisTick: 'Show Axis Tick', + }, + axisLabel: { + title: 'Axis Label', + showAxisLabel: 'Show Axis Label', + }, + progress: { + title: 'Progress', + showProgress: 'Show Progress', + roundCap: 'Round Cap', + }, + splitLine: { + title: 'Split Line', + showSplitLine: 'Show Split Line', + splitLineLength: 'Split Line Length', + }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/BasicLineChart.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/BasicLineChart.ts index f1d7a8f2d..c09c099ff 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/BasicLineChart.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/BasicLineChart.ts @@ -39,7 +39,7 @@ import { getSplitLine, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toExponential, @@ -124,7 +124,7 @@ class BasicLineChart extends Chart { .filter(c => c.type === ChartDataSectionType.INFO) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -409,6 +409,7 @@ class BasicLineChart extends Chart { return labels.join('\n'); }, }, + labelLayout: { hideOverlap: true }, }; } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/config.ts index 7c0671543..91ee0e8e6 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/config.ts @@ -424,27 +424,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -510,8 +516,67 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + graph: { + title: 'Graph', + smooth: 'Smooth', + step: 'Step', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Split Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/BasicOutlineMapChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/BasicOutlineMapChart.tsx index cd92c75ad..ba0227a48 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/BasicOutlineMapChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/BasicOutlineMapChart.tsx @@ -26,7 +26,7 @@ import { getSeriesTooltips4Polar2, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { init, registerMap } from 'echarts'; import Config from './config'; @@ -110,7 +110,7 @@ class BasicOutlineMapChart extends Chart { this.registerGeoMap(styleConfigs); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -193,6 +193,7 @@ class BasicOutlineMapChart extends Chart { position, ...font, }, + labelLayout: { hideOverlap: true }, }; } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/config.ts index 932dcacf2..d96c31d8d 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/config.ts @@ -217,6 +217,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -261,6 +281,49 @@ const config: ChartConfig = { background: { title: '背景设置' }, }, }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + metricsAndColor: 'Metrics and Color', + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + map: { + title: 'Map', + level: 'Level', + enableZoom: 'Enabel Zoom', + backgroundColor: 'Background Color', + borderStyle: 'Border Style', + focusArea: 'Focus Area', + areaColor: 'Area Color', + areaEmphasisColor: 'Area Emphasis Color', + }, + background: { title: 'Background' }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/BasicPieChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/BasicPieChart.tsx index 74b40c92f..1fa4cc66d 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/BasicPieChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/BasicPieChart.tsx @@ -21,7 +21,6 @@ import { ChartConfig, ChartDataSectionField, ChartDataSectionType, - ChartStyleSectionConfig, } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; import { @@ -30,9 +29,10 @@ import { getExtraSeriesRowData, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, valueFormatter, } from 'app/utils/chartHelper'; +import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; import Config from './config'; @@ -89,7 +89,7 @@ class BasicPieChart extends Chart { } getOptions(dataset: ChartDataset, config: ChartConfig) { - const dataColumns = transfromToObjectArray(dataset.rows, dataset.columns); + const dataColumns = transformToObjectArray(dataset.rows, dataset.columns); const styleConfigs = config.styles; const dataConfigs = config.datas || []; const groupConfigs = dataConfigs @@ -107,11 +107,12 @@ class BasicPieChart extends Chart { dataColumns, groupConfigs, aggregateConfigs, + infoConfigs, ); return { tooltip: { - formatter: this.getTooltipFormmaterFunc( + formatter: this.getTooltipFormatterFunc( styleConfigs, groupConfigs, aggregateConfigs, @@ -124,18 +125,24 @@ class BasicPieChart extends Chart { }; } - private getSeries(styleConfigs, dataColumns, groupConfigs, aggregateConfigs) { + private getSeries( + styleConfigs, + dataColumns, + groupConfigs, + aggregateConfigs, + infoConfigs, + ) { if (!groupConfigs?.length) { const dc = dataColumns?.[0]; return { ...this.getBarSeiesImpl(styleConfigs), data: aggregateConfigs.map(config => { return { - ...getExtraSeriesRowData({ - [getValueByColumnKey(config)]: dc[getValueByColumnKey(config)], - }), + ...config, name: getColumnRenderName(config), - value: dc[getValueByColumnKey(config)], + value: [config] + .concat(infoConfigs) + .map(config => dc?.[getValueByColumnKey(config)]), itemStyle: this.getDataItemStyle(config, groupConfigs, dc), ...getExtraSeriesRowData(dc), ...getExtraSeriesDataFormat(config?.format), @@ -151,9 +158,11 @@ class BasicPieChart extends Chart { name: getColumnRenderName(config), data: dataColumns.map(dc => { return { - ...getExtraSeriesRowData(dc), + ...config, name: groupedConfigNames.map(config => dc[config]).join('-'), - value: dc[getValueByColumnKey(config)], + value: aggregateConfigs + .concat(infoConfigs) + .map(config => dc?.[getValueByColumnKey(config)]), itemStyle: this.getDataItemStyle(config, groupConfigs, dc), ...getExtraSeriesRowData(dc), ...getExtraSeriesDataFormat(config?.format), @@ -202,6 +211,7 @@ class BasicPieChart extends Chart { sampling: 'average', avoidLabelOverlap: false, label: this.getLabelStyle(styleConfigs), + labelLayout: { hideOverlap: true }, ...this.getSeriesStyle(styleConfigs), ...this.getGrid(styleConfigs), }; @@ -261,7 +271,39 @@ class BasicPieChart extends Chart { const show = getStyleValueByGroup(styles, 'label', 'showLabel'); const position = getStyleValueByGroup(styles, 'label', 'position'); const font = getStyleValueByGroup(styles, 'label', 'font'); - return { show, position, ...font, formatter: '{b}: {d}%' }; + const formatter = this.getLabelFormatter(styles); + return { show, position, ...font, formatter }; + } + + getLabelFormatter(styles) { + const showValue = getStyleValueByGroup(styles, 'label', 'showValue'); + const showPercent = getStyleValueByGroup(styles, 'label', 'showPercent'); + const showName = getStyleValueByGroup(styles, 'label', 'showName'); + return seriesParams => { + if (seriesParams.componentType !== 'series') { + return seriesParams.name; + } + const data = seriesParams?.data || {}; + + //处理 label 旧数据中没有 showValue, showPercent, showName 数据 alpha.3版本之后是 boolean 类型 后续版本稳定之后 可以移除此逻辑 + // TODO migration start + if (showName === null || showPercent === null || showValue === null) { + return `${seriesParams?.name}: ${seriesParams?.percent + '%'}`; + } + // TODO migration end --tl + + return `${showName ? seriesParams?.name : ''}${ + showName && (showValue || showPercent) ? ': ' : '' + }${ + showValue ? toFormattedValue(seriesParams?.value[0], data?.format) : '' + }${ + showPercent && showValue + ? '(' + seriesParams?.percent + '%)' + : showPercent + ? seriesParams?.percent + '%' + : '' + }`; + }; } getSeriesStyle(styles) { @@ -272,16 +314,7 @@ class BasicPieChart extends Chart { return { radius: radiusValue, roseType: this.isRose }; } - getStyleValueByGroup( - styles: ChartStyleSectionConfig[], - groupPath: string, - childPath: string, - ) { - const childPaths = childPath.split('.'); - return this.getStyleValue(styles, [groupPath, ...childPaths]); - } - - getTooltipFormmaterFunc( + getTooltipFormatterFunc( styleConfigs, groupConfigs, aggregateConfigs, @@ -289,31 +322,37 @@ class BasicPieChart extends Chart { dataColumns, ) { return seriesParams => { - let dataRow = dataColumns?.find( - dc => - groupConfigs - .map(config => dc?.[getValueByColumnKey(config)]) - .join('-') === seriesParams?.name, - ); - if (dataColumns?.length === 1) { - dataRow = dataColumns[0]; + if (seriesParams.componentType !== 'series') { + return seriesParams.name; } - - const toolTips = [] - .concat(groupConfigs) - .concat( - aggregateConfigs?.filter( - aggConfig => - getValueByColumnKey(aggConfig) === seriesParams?.name || - getValueByColumnKey(aggConfig) === seriesParams?.seriesName, - ), - ) + const { data, value, percent } = seriesParams; + if (!groupConfigs?.length) { + const tooltip = [data] + .concat(infoConfigs) + .map((config, index) => valueFormatter(config, value?.[index])); + tooltip[0] += '(' + percent + '%)'; + return tooltip.join(''); + } + const infoTotal = infoConfigs.map(info => { + let total = 0; + dataColumns.map(dc => { + total += dc?.[getValueByColumnKey(info)]; + }); + return total; + }); + let tooltip = aggregateConfigs .concat(infoConfigs) - .map(config => - valueFormatter(config, dataRow?.[getValueByColumnKey(config)]), - ); - - return toolTips.join(''); + .map((config, index) => { + let tooltipValue = valueFormatter(config, value?.[index]); + if (!index) { + return (tooltipValue += '(' + percent + '%)'); + } + const percentNum = + (value?.[aggregateConfigs?.length] / infoTotal?.[index - 1]) * + 100 || 0; + return (tooltipValue += '(' + percentNum.toFixed(2) + '%)'); + }); + return tooltip.join(''); }; } } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/config.ts index 24c3e4f53..d30f16138 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/config.ts @@ -87,6 +87,24 @@ const config: ChartConfig = { color: '#495057', }, }, + { + label: 'label.showName', + key: 'showName', + default: true, + comType: 'checkbox', + }, + { + label: 'label.showValue', + key: 'showValue', + default: false, + comType: 'checkbox', + }, + { + label: 'label.showPercent', + key: 'showPercent', + default: true, + comType: 'checkbox', + }, ], }, { @@ -183,30 +201,23 @@ const config: ChartConfig = { }, ], }, - { - label: 'tooltip.title', - key: 'tooltip', - comType: 'group', - rows: [ - { - label: 'tooltip.showPercentage', - key: 'showPercentage', - default: false, - comType: 'checkbox', - }, - ], - }, ], settings: [ { - label: 'cache.title', - key: 'cache', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'cache.title', - key: 'panel', - comType: 'cache', + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, @@ -233,6 +244,9 @@ const config: ChartConfig = { title: '标签', showLabel: '显示标签', position: '位置', + showName: '维度值', + showPercent: '百分比', + showValue: '指标值', }, legend: { title: '图例', @@ -245,15 +259,54 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, tooltip: { title: '提示信息', showPercentage: '增加百分比显示', }, }, }, + { + lang: 'en-US', + translation: { + section: { + legend: 'Legend', + detail: 'Detail', + }, + common: { + showLabel: 'Show Label', + rotate: 'Rotate', + position: 'Position', + }, + pie: { + title: 'Pie', + circle: 'Circle', + roseType: 'Rose', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + showName: 'Show Name', + showPercent: 'Show Percentage', + showValue: 'Show Value', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + reference: { + title: 'Reference', + open: 'Open', + }, + tooltip: { + title: 'Tooltip', + showPercentage: 'Show Percentage', + }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRadarChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRadarChart/config.ts index 7db180c0b..14a329c37 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRadarChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRadarChart/config.ts @@ -505,27 +505,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -600,9 +606,6 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/BasicRichText.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/BasicRichText.tsx new file mode 100644 index 000000000..bdfa09ebe --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/BasicRichText.tsx @@ -0,0 +1,151 @@ +/** + * 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 ReactChart from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactChart'; +import ChartRichTextAdapter from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartRichTextAdapter'; +import { ChartConfig, ChartDataSectionType } from 'app/types/ChartConfig'; +import ChartDataset from 'app/types/ChartDataset'; +import { + getColumnRenderName, + getCustomSortableColumns, + getStyleValueByGroup, + getValueByColumnKey, + transformToObjectArray, +} from 'app/utils/chartHelper'; +import { toFormattedValue } from 'app/utils/number'; +import Config from './config'; + +class BasicRichText extends ReactChart { + _useIFrame = false; + isISOContainer = 'react-rich-text'; + config = Config; + protected isAutoMerge = false; + richTextOptions = { + dataset: {}, + config: {}, + containerId: '', + widgetSpecialConfig: { env: '' }, + }; + + constructor(props?) { + super(ChartRichTextAdapter, { + id: props?.id || 'react-rich-text', + name: props?.name || '富文本', + icon: props?.icon || 'rich-text', + }); + this.meta.requirements = props?.requirements || [ + { + group: [0, 999], + aggregate: [0, 999], + }, + ]; + } + + onMount(options, context): void { + if (options.containerId === undefined || !context.document) { + return; + } + this.richTextOptions = Object.assign(this.richTextOptions, options); + this.adapter?.mounted( + context.document.getElementById(options.containerId), + options, + context, + ); + } + + onUpdated(options, context): void { + this.richTextOptions = Object.assign(this.richTextOptions, options); + if (!this.isMatchRequirement(options.config)) { + this.adapter?.unmount(); + return; + } + + this.adapter?.updated( + this.getOptions(context, options.dataset, options.config), + context, + ); + } + + onResize(opt: any, context): void { + this.onUpdated(this.richTextOptions, context); + } + + getOptions(context, dataset?: ChartDataset, config?: ChartConfig) { + const { containerId, widgetSpecialConfig } = this.richTextOptions; + if (!dataset || !config || !containerId) { + return { dataList: [], id: '', isEditing: !!widgetSpecialConfig?.env }; + } + const dataConfigs = config.datas || []; + const stylesConfigs = config.styles || []; + const groupConfigs = dataConfigs + .filter(c => c.type === ChartDataSectionType.GROUP) + .flatMap(config => config.rows || []); + const aggregateConfigs = dataConfigs + .filter(c => c.type === ChartDataSectionType.AGGREGATE) + .flatMap(config => config.rows || []); + const objDataColumns = transformToObjectArray( + dataset.rows, + dataset.columns, + ); + const dataColumns = getCustomSortableColumns(objDataColumns, dataConfigs); + + const dataList = groupConfigs.concat(aggregateConfigs).map(config => { + return { + id: config.uid, + name: getColumnRenderName(config), + value: this.getDataListValue(config, dataColumns), + }; + }); + const initContent = getStyleValueByGroup( + stylesConfigs, + 'delta', + 'richText', + ); + return { + dataList, + initContent, + id: containerId, + isEditing: !!widgetSpecialConfig?.env, + ...this.getOnChange(), + }; + } + + getDataListValue(config, dataColumns) { + const value = dataColumns.map(dc => + toFormattedValue(dc[getValueByColumnKey(config)], config.format), + )[0]; + return typeof value !== 'string' && value ? value.toString() : value; + } + + getOnChange(): any { + return this._mouseEvents?.reduce((acc, cur) => { + if (cur.name === 'click') { + Object.assign(acc, { + onChange: delta => + cur.callback?.({ + seriesName: 'richText', + value: delta, + }), + }); + } + return acc; + }, {}); + } +} + +export default BasicRichText; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/config.ts new file mode 100644 index 000000000..1e92a9ab3 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/config.ts @@ -0,0 +1,97 @@ +/** + * 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 { ChartConfig } from 'app/types/ChartConfig'; + +const config: ChartConfig = { + datas: [ + { + label: 'dimension', + key: 'dimension', + type: 'group', + }, + { + label: 'metrics', + key: 'metrics', + type: 'aggregate', + actions: { + NUMERIC: ['alias', 'sortable', 'format', 'aggregate'], + STRING: ['alias', 'sortable', 'format', 'aggregate'], + }, + }, + { + label: 'filter', + key: 'filter', + type: 'filter', + allowSameField: true, + }, + ], + styles: [ + { + label: 'delta.title', + hidden: true, + key: 'delta', + comType: 'group', + rows: [ + { + label: 'delta.richText', + key: 'richText', + default: '', + comType: 'input', + }, + ], + }, + ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], + i18ns: [ + { + lang: 'zh-CN', + translation: { + delta: { + title: '富文本', + text: '内容', + }, + }, + }, + { + lang: 'en-US', + translation: {}, + }, + ], +}; + +export default config; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/index.ts new file mode 100644 index 000000000..bbeaeac5d --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/index.ts @@ -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 BasicRichText from './BasicRichText'; + +export default BasicRichText; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/BasicScatterChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/BasicScatterChart.tsx index 58b4524cc..ed2910390 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/BasicScatterChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/BasicScatterChart.tsx @@ -28,7 +28,7 @@ import { getSeriesTooltips4Scatter, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { init } from 'echarts'; import Config from './config'; @@ -84,7 +84,7 @@ class BasicScatterChart extends Chart { } getOptions(dataset: ChartDataset, config: ChartConfig) { - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -233,7 +233,7 @@ class BasicScatterChart extends Chart { 'scatter', 'cycleRatio', ); - const defaultSizeValue = max - min; + const defaultSizeValue = (max - min) / 2; const seriesName = groupConfigs ?.map(gc => getColumnRenderName(gc)) .join('-'); @@ -394,7 +394,10 @@ class BasicScatterChart extends Chart { const show = getStyleValueByGroup(styles, 'label', 'showLabel'); const position = getStyleValueByGroup(styles, 'label', 'position'); const font = getStyleValueByGroup(styles, 'label', 'font'); - return { label: { show, position, ...font, formatter: '{b}' } }; + return { + label: { show, position, ...font, formatter: '{b}' }, + labelLayout: { hideOverlap: true }, + }; } getTooltipFormmaterFunc( diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/config.ts index de0807450..001c4987e 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/config.ts @@ -350,6 +350,7 @@ const config: ChartConfig = { default: 'center', comType: 'select', options: { + // TODO(Stephen): to be extract to axis name location component items: [ { label: '开始', value: 'start' }, { label: '结束', value: 'end' }, @@ -460,27 +461,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -526,11 +533,6 @@ const config: ChartConfig = { color: '颜色', colorize: '配色', }, - graph: { - title: '折线图', - smooth: '平滑', - step: '阶梯', - }, xAxis: { title: 'X轴', }, @@ -546,15 +548,73 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, scatter: { title: '散点图配置', cycleRatio: '气泡大像素比', }, }, }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Split Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference', + open: 'Open', + }, + scatter: { + title: 'Scatter', + cycleRatio: 'Cycle Ratio', + }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/AntdTableWrapper.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/AntdTableWrapper.tsx new file mode 100644 index 000000000..c66164c4b --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/AntdTableWrapper.tsx @@ -0,0 +1,72 @@ +/** + * 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 { Table } from 'antd'; +import { FC, memo } from 'react'; +import styled from 'styled-components/macro'; + +const AntdTableWrapper: FC<{ + dataSource: []; + columns: []; + summaryFn?: (data) => { total: number; summarys: [] }; +}> = memo(({ dataSource, columns, children, summaryFn, ...rest }) => { + const getTableSummaryRow = pageData => { + if (!summaryFn) { + return undefined; + } + const summaryData = summaryFn?.(pageData); + return ( + + + {(summaryData?.summarys || []).map((data, index) => { + return ( + {data} + ); + })} + + + ); + }; + + return ( + + ); +}); + +const StyledTable = styled(Table)` + background: 'transparent'; + height: 100%; + overflow: auto; + + .ant-table-summary { + background: #fafafa; + } + .ant-table-cell-fix-left { + background: #fafafa; + } + .ant-table-cell-fix-right { + background: #fafafa; + } +`; + +export default AntdTableWrapper; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/BasicTableChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/BasicTableChart.tsx index 41cf0b041..86c1029eb 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/BasicTableChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/BasicTableChart.tsx @@ -23,27 +23,41 @@ import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { getColumnRenderName, getCustomSortableColumns, + getUnusedHeaderRows, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toFormattedValue } from 'app/utils/number'; -import { Omit } from 'utils/object'; -import { v4 as uuidv4 } from 'uuid'; -import AntdTableChartAdapter from '../../ChartTools/AntdTableChartAdapter'; +import { isEmptyArray, Omit } from 'utils/object'; +import { uuidv4 } from 'utils/utils'; +import AntdTableWrapper from './AntdTableWrapper'; +import { + getCustomBodyCellStyle, + getCustomBodyRowStyle, +} from './conditionStyle'; import Config from './config'; +import { TableComponentsTd } from './TableComponents'; class BasicTableChart extends ReactChart { + _useIFrame = false; isISOContainer = 'react-table'; config = Config; - protected isAutoMerge = false; - tableOptions = { dataset: {}, config: {} }; + utilCanvas = null; + dataColumnWidths = {}; + tablePadding = 16; + tableCellBorder = 1; + cachedAntTableOptions = {}; + cachedDatartConfig: ChartConfig = {}; + showSummaryRow = false; + rowNumberUniqKey = `@datart@rowNumberKey`; constructor(props?) { - super( - props?.id || 'react-table', - props?.name || '表格', - props?.icon || 'table', - ); + super(AntdTableWrapper, { + id: props?.id || 'react-table', + name: props?.name || '表格', + icon: props?.icon || 'table', + }); + this.meta.requirements = props?.requirements || [ { group: [0, 999], @@ -52,54 +66,51 @@ class BasicTableChart extends ReactChart { ]; } - onMount(options, context): void { - if (options.containerId === undefined || !context.document) { - return; - } - - this.getInstance().init(AntdTableChartAdapter); - this.getInstance().mounted( - context.document.getElementById(options.containerId), - options, - context, - ); - } - onUpdated(options, context): void { - this.tableOptions = options; - if (!this.isMatchRequirement(options.config)) { - this.getInstance()?.unmount(); + this.adapter?.unmount(); return; } - this.getInstance()?.updated( - this.getOptions(context, options.dataset, options.config), + const tableOptions = this.getOptions( context, + options.dataset, + options.config, + options.widgetSpecialConfig, ); + this.cachedAntTableOptions = tableOptions; + this.cachedDatartConfig = options.config; + this.adapter?.updated(tableOptions, context); } - onUnMount(): void { - this.getInstance()?.unmount(); - } - - onResize(opt: any, context): void { - this.onUpdated(this.tableOptions, context); + public onResize(options, context?): void { + this.adapter?.updated( + Object.assign(this.cachedAntTableOptions, { + ...this.getAntdTableStyleOptions( + this.cachedDatartConfig?.styles, + this.cachedDatartConfig?.settings!, + context?.height, + ), + }), + context, + ); } - getTableY() {} - - getOptions(context, dataset?: ChartDataset, config?: ChartConfig) { + getOptions( + context, + dataset?: ChartDataset, + config?: ChartConfig, + widgetSpecialConfig?: any, + ) { if (!dataset || !config) { return { locale: { emptyText: ' ' } }; } - const { clientWidth, clientHeight } = context.document.documentElement; const dataConfigs = config.datas || []; const styleConfigs = config.styles || []; const settingConfigs = config.settings || []; - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -108,41 +119,186 @@ class BasicTableChart extends ReactChart { const mixedSectionConfigRows = dataConfigs .filter(c => c.key === 'mixed') .flatMap(config => config.rows || []); - const groupConfigs = mixedSectionConfigRows.filter( r => r.type === ChartDataViewFieldType.STRING || r.type === ChartDataViewFieldType.DATE, ); - const aggregateConfigs = mixedSectionConfigRows.filter( r => r.type === ChartDataViewFieldType.NUMERIC, ); - - let tablePagination = this.getPagingOptions( + const tablePagination = this.getPagingOptions( settingConfigs, dataset?.pageInfo, ); + this.dataColumnWidths = this.calcuteFieldsMaxWidth( + mixedSectionConfigRows, + dataColumns, + styleConfigs, + context, + ); + + const tableColumns = this.getColumns( + groupConfigs, + aggregateConfigs, + styleConfigs, + dataColumns, + ); + return { rowKey: 'uid', pagination: tablePagination, dataSource: this.generateTableRowUniqId(dataColumns), - columns: this.getColumns( - groupConfigs, - aggregateConfigs, - styleConfigs, + columns: tableColumns, + summaryFn: this.getTableSummaryFn( + settingConfigs, dataColumns, + tableColumns, + aggregateConfigs, ), - components: this.getTableComponents(styleConfigs), + components: this.getTableComponents(styleConfigs, widgetSpecialConfig), ...this.getAntdTableStyleOptions( styleConfigs, - dataset, - clientWidth, - clientHeight, - tablePagination, + settingConfigs, + context?.height, ), + onChange: (pagination, filters, sorter, extra) => { + if (extra?.action === 'sort' || extra?.action === 'paginate') { + this.invokePagingRelatedEvents( + sorter?.field, + sorter?.order, + pagination?.current, + ); + } + }, + }; + } + + getTableSummaryFn( + settingConfigs, + dataColumns, + tableColumns, + aggregateConfigs, + ) { + const aggregateFields = this.getStyleValue(settingConfigs, [ + 'summary', + 'aggregateFields', + ]); + this.showSummaryRow = aggregateFields && aggregateFields.length > 0; + if (!this.showSummaryRow) { + return; + } + + const aggregateFieldConfigs = aggregateConfigs.filter(c => + aggregateFields.includes(c.uid), + ); + + const _flatChildren = node => { + if (Array.isArray(node?.children)) { + return (node.children || []).reduce((acc, cur) => { + return acc.concat(..._flatChildren(cur)); + }, []); + } + return [node]; }; + const flatHeaderColumns = (tableColumns || []).reduce((acc, cur) => { + return acc.concat(..._flatChildren(cur)); + }, []); + + return _ => { + return { + summarys: flatHeaderColumns + .map(c => c.key) + .map(k => { + const currentSummaryField = aggregateFieldConfigs.find( + c => getValueByColumnKey(c) === k, + ); + if (currentSummaryField) { + const total = dataColumns.map( + dc => dc?.[getValueByColumnKey(currentSummaryField)], + ); + return total.reduce((acc, cur) => acc + cur, 0); + } + return null; + }), + }; + }; + } + + calcuteFieldsMaxWidth( + mixedSectionConfigRows, + dataColumns, + styleConfigs, + context, + ) { + const bodyFont = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'font', + ]); + const headerFont = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'font', + ]); + const tableHeaders = this.getStyleValue(styleConfigs, [ + 'header', + 'modal', + 'tableHeaders', + ]); + const enableRowNumber = this.getStyleValue(styleConfigs, [ + 'style', + 'enableRowNumber', + ]); + const maxContentByFields = mixedSectionConfigRows.map(c => { + const header = this.findHeader(c.uid, tableHeaders); + const rowUniqKey = getValueByColumnKey(c); + const datas = dataColumns?.map(dc => { + const text = dc[rowUniqKey]; + let width = this.getTextWidth( + context, + text, + bodyFont?.fontWeight, + bodyFont?.fontSize, + bodyFont?.fontFamily, + ); + const headerWidth = this.getTextWidth( + context, + header?.label || header?.colName, + headerFont?.fontWeight, + headerFont?.fontSize, + headerFont?.fontFamily, + ); + const sorterIconWidth = 12; + return Math.max(width, headerWidth + sorterIconWidth); + }); + + const getRowNumberWidth = maxContent => { + if (!enableRowNumber) { + return 0; + } + + return this.getTextWidth( + context, + maxContent, + bodyFont?.fontWeight, + bodyFont?.fontSize, + bodyFont?.fontFamily, + ); + }; + + return { + [this.rowNumberUniqKey]: + getRowNumberWidth(dataColumns?.length) + + this.tablePadding * 2 + + this.tableCellBorder * 2, + [rowUniqKey]: + Math.max(...datas) + this.tablePadding * 2 + this.tableCellBorder * 2, + }; + }); + + return maxContentByFields.reduce((acc, cur) => { + return Object.assign({}, acc, { ...cur }); + }, {}); } generateTableRowUniqId(dataColumns) { @@ -154,34 +310,68 @@ class BasicTableChart extends ReactChart { }); } - getTableComponents(styleConfigs) { + getTableComponents(styleConfigs, widgetSpecialConfig) { + const linkFields = widgetSpecialConfig?.linkFields; + const jumpField = widgetSpecialConfig?.jumpField; + const tableHeaders = this.getStyleValue(styleConfigs, [ 'header', 'modal', 'tableHeaders', ]); + const headerBgColor = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'bgColor', + ]); + const headerFont = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'font', + ]); + const headerTextAlign = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'align', + ]); + const bodyBgColor = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'bgColor', + ]); + const bodyFont = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'font', + ]); + const bodyTextAlign = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'align', + ]); + const getAllColumnListInfo = this.getValue( + styleConfigs, + ['column', 'modal', 'list'], + 'rows', + ); + let allConditionStyle: any[] = []; + getAllColumnListInfo?.forEach(info => { + const getConditionStyleValue = this.getValue( + info.rows, + ['conditionStyle', 'conditionStylePanel'], + 'value', + ); + if (Array.isArray(getConditionStyleValue)) { + allConditionStyle = [...allConditionStyle, ...getConditionStyleValue]; + } + }); return { header: { cell: props => { const uid = props.uid; - const _findRow = (uid, headers) => { - let header = headers.find(h => h.uid === uid); - if (!!header) { - return header; - } - for (let i = 0; i < headers.length; i++) { - header = _findRow(uid, headers[i].children || []); - if (!!header) { - break; - } - } - return header; + const { style, title, ...rest } = props; + const header = this.findHeader(uid, tableHeaders || []); + const cellCssStyle = { + textAlign: headerTextAlign, + backgroundColor: headerBgColor, + ...headerFont, + fontSize: +headerFont?.fontSize, }; - - const header = _findRow(uid, tableHeaders || []); - const cellCssStyle = {}; - if (header && header.style) { const fontStyle = header.style?.font?.value; Object.assign( @@ -193,38 +383,46 @@ class BasicTableChart extends ReactChart { { ...fontStyle }, ); } - return ; + return ; }, }, body: { cell: props => { + const { style, dataIndex, ...rest } = props; const uid = props.uid; - const backgroundColor = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', + const conditionStyle = this.getStyleValue(getAllColumnListInfo, [ uid, - 'basicStyle', - 'backgroundColor', - ]); - const textAlign = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'basicStyle', - 'align', - ]); - const font = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'basicStyle', - 'font', + 'conditionStyle', + 'conditionStylePanel', ]); + const conditionalCellStyle = getCustomBodyCellStyle( + props, + conditionStyle, + ); + return ( + + ); + }, + row: props => { + const { style, ...rest } = props; + const rowStyle = getCustomBodyRowStyle(props, allConditionStyle); + return ; + }, + wrapper: props => { + const { style, ...rest } = props; + const bodyStyle = { + textAlign: bodyTextAlign, + backgroundColor: bodyBgColor, + ...bodyFont, + fontSize: +bodyFont?.fontSize, + }; return ( - + ); }, }, @@ -232,9 +430,9 @@ class BasicTableChart extends ReactChart { } getColumns(groupConfigs, aggregateConfigs, styleConfigs, dataColumns) { - const enableFixedHeader = this.getStyleValue(styleConfigs, [ + const enableRowNumber = this.getStyleValue(styleConfigs, [ 'style', - 'enableFixedHeader', + 'enableRowNumber', ]); const leftFixedColumns = this.getStyleValue(styleConfigs, [ 'style', @@ -244,71 +442,30 @@ class BasicTableChart extends ReactChart { 'style', 'rightFixedColumns', ]); + const autoMergeFields = this.getStyleValue(styleConfigs, [ + 'style', + 'autoMergeFields', + ]); const tableHeaderStyles = this.getStyleValue(styleConfigs, [ 'header', 'modal', 'tableHeaders', ]); - const _getFixedColumn = name => { - if ( - leftFixedColumns === name || - (leftFixedColumns && leftFixedColumns.includes(name)) - ) { + const _getFixedColumn = uid => { + if (String(leftFixedColumns).includes(uid)) { return 'left'; } - if ( - rightFixedColumns === name || - (rightFixedColumns && rightFixedColumns.includes(name)) - ) { + if (String(rightFixedColumns).includes(uid)) { return 'right'; } return null; }; - const _sortFn = rowKey => (prev, next) => { - return prev[rowKey] > next[rowKey]; - }; - const _getFlatColumns = (groupConfigs, aggregateConfigs, dataColumns) => [...groupConfigs, ...aggregateConfigs].map(c => { const colName = c.colName; - const uid = c.uid; - const enableSort = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'sortAndFilter', - 'enableSort', - ]); - const textAlign = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'basicStyle', - 'align', - ]); - const enableFixedCol = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'basicStyle', - 'enableFixedCol', - ]); - const fixedColWidth = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'basicStyle', - 'enableFixedCol', - 'fixedColWidth', - ]); - - const columnRowSpans = this.isAutoMerge + const columnRowSpans = (autoMergeFields || []).includes(c.uid) ? dataColumns ?.map(dc => dc[getValueByColumnKey(c)]) .reverse() @@ -335,18 +492,16 @@ class BasicTableChart extends ReactChart { .reverse() : []; + const colMaxWidth = + this.dataColumnWidths?.[getValueByColumnKey(c)] || 100; return { - sorter: !!enableSort ? _sortFn(colName) : undefined, + sorter: true, title: getColumnRenderName(c), dataIndex: getValueByColumnKey(c), key: getValueByColumnKey(c), - width: enableFixedHeader - ? enableFixedCol - ? fixedColWidth - : null - : null, - fixed: _getFixedColumn(getValueByColumnKey(c)), - align: textAlign, + colName, + width: colMaxWidth, + fixed: _getFixedColumn(c?.uid), onHeaderCell: record => { return { ...Omit(record, [ @@ -355,28 +510,34 @@ class BasicTableChart extends ReactChart { 'onCell', 'colName', 'render', + 'sorter', ]), uid: c.uid, }; }, onCell: (record, rowIndex) => { + const cellValue = record[getValueByColumnKey(c)]; + return { uid: c.uid, + cellValue, + dataIndex: getValueByColumnKey(c), ...this.registerTableCellEvents( getValueByColumnKey(c), + cellValue, rowIndex, - record[getValueByColumnKey(c)], + record, ), }; }, render: (value, row, rowIndex) => { const formattedValue = toFormattedValue(value, c.format); - if (!this.isAutoMerge) { + if (!(autoMergeFields || []).includes(c.uid)) { return formattedValue; } return { children: formattedValue, - props: { rowSpan: columnRowSpans[rowIndex] }, + props: { rowSpan: columnRowSpans[rowIndex], cellValue: value }, }; }, }; @@ -387,22 +548,49 @@ class BasicTableChart extends ReactChart { aggregateConfigs, tableHeaderStyles, dataColumns, - ) => - tableHeaderStyles - ?.map(style => - this.getHeaderColumnGroup( - style, - _getFlatColumns(groupConfigs, aggregateConfigs, dataColumns), - ), - ) - ?.filter(column => !!column) || []; + ) => { + const flattenedColumns = _getFlatColumns( + groupConfigs, + aggregateConfigs, + dataColumns, + ); + + const groupedHeaderColumns = + tableHeaderStyles + ?.map(style => this.getHeaderColumnGroup(style, flattenedColumns)) + ?.filter(Boolean) || []; + + const unusedHeaderRows = getUnusedHeaderRows( + flattenedColumns, + groupedHeaderColumns, + ); + + return groupedHeaderColumns.concat(unusedHeaderRows); + }; + + const rowNumbers = enableRowNumber + ? [ + { + key: 'id', + title: '', + dataIndex: 'id', + width: this.dataColumnWidths?.[this.rowNumberUniqKey] || 0, + fixed: leftFixedColumns?.length > 0 ? 'left' : null, + } as any, + ] + : []; + return !tableHeaderStyles || tableHeaderStyles.length === 0 - ? _getFlatColumns(groupConfigs, aggregateConfigs, dataColumns) - : _getGroupColumns( - groupConfigs, - aggregateConfigs, - tableHeaderStyles, - dataColumns, + ? rowNumbers.concat( + _getFlatColumns(groupConfigs, aggregateConfigs, dataColumns), + ) + : rowNumbers.concat( + _getGroupColumns( + groupConfigs, + aggregateConfigs, + tableHeaderStyles, + dataColumns, + ), ); } @@ -415,6 +603,7 @@ class BasicTableChart extends ReactChart { } return { uid: tableHeader?.uid, + colName: tableHeader?.colName, title: tableHeader.label, onHeaderCell: record => { return { @@ -425,17 +614,15 @@ class BasicTableChart extends ReactChart { .map(th => { return this.getHeaderColumnGroup(th, columns); }) - .filter(column => !!column), + .filter(Boolean), }; } - getAntdTableStyleOptions( - styleConfigs, - dataset: ChartDataset, - width, - height, - tablePagination, - ) { + getAntdTableStyleOptions(styleConfigs?, settingConfigs?, height?) { + const enablePaging = this.getStyleValue(settingConfigs, [ + 'paging', + 'enablePaging', + ]); const showTableBorder = this.getStyleValue(styleConfigs, [ 'style', 'enableBorder', @@ -444,22 +631,42 @@ class BasicTableChart extends ReactChart { 'style', 'enableFixedHeader', ]); + const tableHeaderStyles = this.getStyleValue(styleConfigs, [ + 'header', + 'modal', + 'tableHeaders', + ]); const tableSize = - this.getStyleValue(styleConfigs, ['data', 'tableSize']) || 'default'; + this.getStyleValue(styleConfigs, ['style', 'tableSize']) || 'default'; const HEADER_HEIGHT = { default: 56, middle: 48, small: 40 }; const PAGINATION_HEIGHT = { default: 64, middle: 56, small: 56 }; + const SUMMRAY_ROW_HEIGHT = { default: 64, middle: 56, small: 56 }; + const _getMaxHeaderHierarchy = (headerStyles: Array<{ children: [] }>) => { + const _maxDeeps = (arr: Array<{ children: [] }> = [], deeps: number) => { + if (!isEmptyArray(arr) && arr?.length > 0) { + return Math.max(...arr.map(a => _maxDeeps(a.children, deeps + 1))); + } + return deeps; + }; + return _maxDeeps(headerStyles, 0) || 1; + }; + const totalWidth = Object.values(this.dataColumnWidths).reduce( + (a, b) => a + b, + 0, + ); return { - scroll: enableFixedHeader - ? { - scrollToFirstRowOnChange: true, - x: 'max-content', - y: - height - - HEADER_HEIGHT[tableSize] - - (tablePagination ? PAGINATION_HEIGHT[tableSize] : 0), - } - : { scrollToFirstRowOnChange: true, x: 'max-content' }, + scroll: Object.assign({ + scrollToFirstRowOnChange: true, + x: !enableFixedHeader ? 'max-content' : totalWidth, + y: !enableFixedHeader + ? undefined + : height - + (this.showSummaryRow ? SUMMRAY_ROW_HEIGHT[tableSize] : 0) - + HEADER_HEIGHT[tableSize] * + _getMaxHeaderHierarchy(tableHeaderStyles) - + (enablePaging ? PAGINATION_HEIGHT[tableSize] : 0), + }), bordered: !!showTableBorder, size: tableSize, }; @@ -476,70 +683,121 @@ class BasicTableChart extends ReactChart { current: pageInfo?.pageNo, pageSize: pageInfo?.pageSize, total: pageInfo?.total, - ...this.registerTablePagingEvents('paging', 0, null), }) : false; } - registerTablePagingEvents(seriesName: string, dataIndex: number, value: any) { - const eventParams = { - componentType: 'series', - seriesType: 'table', - seriesName, // column name/index - dataIndex, // row index - value, // cell value - }; - return this._mouseEvents?.reduce((acc, cur) => { + createrEventParams = params => ({ + type: 'click', + componentType: 'table', + seriesType: undefined, + data: undefined, + dataIndex: undefined, + event: undefined, + name: undefined, + seriesName: undefined, + value: undefined, + ...params, + }); + + invokePagingRelatedEvents(seriesName: string, value: any, pageNo: number) { + const eventParams = this.createrEventParams({ + seriesType: 'paging-sort-filter', + seriesName, + value: { + direction: + value === undefined ? undefined : value === 'ascend' ? 'ASC' : 'DESC', + pageNo, + }, + }); + this._mouseEvents?.forEach(cur => { if (cur.name === 'click') { - Object.assign(acc, { - onChange: (page, pageSize) => - cur.callback?.( - Object.assign({}, eventParams, { value: { page, pageSize } }), - ), - }); + cur.callback?.(eventParams); } - return acc; - }, {}); + }); } - registerTableCellEvents(seriesName: string, dataIndex: number, value: any) { - const eventParams = { - componentType: 'series', - seriesType: 'table', - name: value, + registerTableCellEvents( + seriesName: string, + value: any, + dataIndex: number, + record: any, + ) { + const eventParams = this.createrEventParams({ + seriesType: 'body', + name: seriesName, + data: { + format: undefined, + name: seriesName, + rowData: record, + value: value, + }, seriesName, // column name/index dataIndex, // row index value, // cell value - }; + }); return this._mouseEvents?.reduce((acc, cur) => { + cur.name && (eventParams.type = cur.name); if (cur.name === 'click') { Object.assign(acc, { - onClick: event => cur.callback?.(eventParams), + onClick: event => cur.callback?.({ ...eventParams, event }), }); } if (cur.name === 'dblclick') { Object.assign(acc, { - onDoubleClick: event => cur.callback?.(eventParams), + onDoubleClick: event => cur.callback?.({ ...eventParams, event }), }); } if (cur.name === 'contextmenu') { Object.assign(acc, { - onContextMenu: event => cur.callback?.(eventParams), + onContextMenu: event => cur.callback?.({ ...eventParams, event }), }); } if (cur.name === 'mouseover') { Object.assign(acc, { - onMouseEnter: event => cur.callback?.(eventParams), + onMouseEnter: event => cur.callback?.({ ...eventParams, event }), }); } if (cur.name === 'mouseout') { Object.assign(acc, { - onMouseLeave: event => cur.callback?.(eventParams), + onMouseLeave: event => cur.callback?.({ ...eventParams, event }), }); } return acc; }, {}); } + + getTextWidth = ( + context, + text: string, + fontWeight: string, + fontSize: string, + fontFamily: string, + ): number => { + const canvas = + this.utilCanvas || + (this.utilCanvas = context.document.createElement('canvas')); + const measureLayer = canvas.getContext('2d'); + measureLayer.font = `${fontWeight} ${fontSize}px ${fontFamily}`; + const metrics = measureLayer.measureText(text); + return Math.ceil(metrics.width); + }; + + findHeader = (uid, headers) => { + let header = (headers || []) + .filter(h => !h.isGroup) + .find(h => h.uid === uid); + if (!!header) { + return header; + } + for (let i = 0; i < (headers || []).length; i++) { + header = this.findHeader(uid, headers[i].children || []); + if (!!header) { + break; + } + } + return header; + }; } export default BasicTableChart; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/TableComponents.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/TableComponents.tsx new file mode 100644 index 000000000..a0ca743bd --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/TableComponents.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import styled from 'styled-components/macro'; +import { BLUE } from 'styles/StyleConstants'; + +export const TableComponentsTd = (props: any) => { + return ; +}; + +const Td = styled.td` + ${p => + p.isLinkCell + ? ` + :hover { + color: ${BLUE}; + cursor: pointer; + } + ` + : p.isJumpCell + ? ` + :hover { + color: ${BLUE}; + cursor: pointer; + text-decoration: underline; + } + ` + : null} +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/conditionStyle.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/conditionStyle.ts new file mode 100644 index 000000000..85998e473 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/conditionStyle.ts @@ -0,0 +1,146 @@ +import { ConditionStyleFormValues } from 'app/components/FormGenerator/Customize/ConditionStylePanel'; +import { OperatorTypes } from 'app/components/FormGenerator/Customize/ConditionStylePanel/types'; +import { CSSProperties } from 'react'; + +const isMatchedTheCondition = ( + value: string | number, + operatorType: OperatorTypes, + conditionValues: string | number | (string | number)[], +) => { + let matchTheCondition = false; + + switch (operatorType) { + case OperatorTypes.Equal: + matchTheCondition = value === conditionValues; + break; + case OperatorTypes.NotEqual: + matchTheCondition = value !== conditionValues; + break; + case OperatorTypes.Contain: + matchTheCondition = (value as string).includes(conditionValues as string); + break; + case OperatorTypes.NotContain: + matchTheCondition = !(value as string).includes( + conditionValues as string, + ); + break; + case OperatorTypes.In: + matchTheCondition = (conditionValues as (string | number)[]).includes( + value, + ); + break; + case OperatorTypes.NotIn: + matchTheCondition = !(conditionValues as (string | number)[]).includes( + value, + ); + break; + case OperatorTypes.Between: + const [min, max] = conditionValues as number[]; + matchTheCondition = value >= min && value <= max; + break; + case OperatorTypes.LessThan: + matchTheCondition = value < conditionValues; + break; + case OperatorTypes.GreaterThan: + matchTheCondition = value > conditionValues; + break; + case OperatorTypes.LessThanOrEqual: + matchTheCondition = value <= conditionValues; + break; + case OperatorTypes.GreaterThanOrEqual: + matchTheCondition = value >= conditionValues; + break; + case OperatorTypes.IsNull: + if (typeof value === 'object' && value === null) { + matchTheCondition = true; + } else if (typeof value === 'string' && value === '') { + matchTheCondition = true; + } else if (typeof value === 'undefined') { + matchTheCondition = true; + } else { + matchTheCondition = false; + } + break; + default: + break; + } + return matchTheCondition; +}; + +const getTheSameRange = (list, type) => + list?.filter(({ range }) => range === type); + +const getRowRecord = row => { + if (!row?.length) { + return {}; + } + return row?.[0]?.props?.record || {}; +}; + +const deleteUndefinedProps = props => { + return Object.keys(props).reduce((acc, cur) => { + if (props[cur] !== undefined || props[cur] !== null) { + acc[cur] = props[cur]; + } + return acc; + }, {}); +}; + +export const getCustomBodyCellStyle = ( + props: any, + conditionStyle: ConditionStyleFormValues[], +): CSSProperties => { + const currentConfigs = getTheSameRange(conditionStyle, 'cell'); + if (!currentConfigs?.length) { + return {}; + } + const text = props.cellValue; + let cellStyle: CSSProperties = {}; + + try { + currentConfigs?.forEach( + ({ operator, value, color: { background, textColor: color } }) => { + cellStyle = isMatchedTheCondition(text, operator, value) + ? { backgroundColor: background, color } + : cellStyle; + }, + ); + } catch (error) { + console.error('getCustomBodyCellStyle | error ', error); + } + return deleteUndefinedProps(cellStyle); +}; + +export const getCustomBodyRowStyle = ( + props: any, + conditionStyle: ConditionStyleFormValues[], +): CSSProperties => { + const currentConfigs: ConditionStyleFormValues[] = getTheSameRange( + conditionStyle, + 'row', + ); + if (!currentConfigs?.length) { + return {}; + } + + const rowRecord = getRowRecord(props.children); + let rowStyle: CSSProperties = {}; + + try { + currentConfigs?.forEach( + ({ + operator, + value, + color: { background, textColor }, + target: { name }, + }) => { + rowStyle = isMatchedTheCondition(rowRecord[name], operator, value) + ? { backgroundColor: background, color: textColor } + : rowStyle; + }, + ); + } catch (error) { + console.error('getCustomBodyRowStyle | error ', error); + } + return deleteUndefinedProps(rowStyle); +}; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/config.ts index 175d368ce..e990157fe 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/config.ts @@ -55,7 +55,7 @@ const config: ChartConfig = { ], }, { - label: 'column.title', + label: 'column.conditionStyle', key: 'column', comType: 'group', rows: [ @@ -80,6 +80,7 @@ const config: ChartConfig = { .map(c => ({ key: c.uid, value: c.uid, + type: c.type, label: c.label || c.aggregate ? `${c.aggregate}(${c.colName})` @@ -94,76 +95,18 @@ const config: ChartConfig = { comType: 'group', rows: [ { - label: 'column.sortAndFilter', - key: 'sortAndFilter', - comType: 'group', - options: { expand: true }, - rows: [ - { - label: 'column.enableSort', - key: 'enableSort', - comType: 'checkbox', - }, - ], - }, - { - label: 'column.basicStyle', - key: 'basicStyle', + label: 'column.conditionStyle', + key: 'conditionStyle', comType: 'group', options: { expand: true }, rows: [ { - label: 'column.backgroundColor', - key: 'backgroundColor', - comType: 'fontColor', - }, - { - label: 'column.align', - key: 'align', - default: 'left', - comType: 'select', - options: { - items: [ - { label: '左对齐', value: 'left' }, - { label: '居中对齐', value: 'center' }, - { label: '右对齐', value: 'right' }, - ], - }, - }, - { - label: 'column.enableFixedCol', - key: 'enableFixedCol', - comType: 'switch', - rows: [ - { - label: 'column.fixedColWidth', - key: 'fixedColWidth', - default: 100, - comType: 'inputNumber', - }, - ], - }, - { - label: 'font', - key: 'font', - comType: 'font', - default: { - fontFamily: 'PingFang SC', - fontSize: '12', - fontWeight: 'normal', - fontStyle: 'normal', - color: 'black', - }, + label: 'column.conditionStylePanel', + key: 'conditionStylePanel', + comType: 'conditionStylePanel', }, ], }, - { - label: 'column.conditionStyle', - key: 'conditionStyle', - comType: 'group', - options: { expand: true }, - rows: [], - }, ], }, }, @@ -260,18 +203,6 @@ const config: ChartConfig = { }, ], settings: [ - { - label: 'cache.title', - key: 'cache', - comType: 'group', - rows: [ - { - label: 'cache.title', - key: 'panel', - comType: 'cache', - }, - ], - }, { label: 'paging.title', key: 'paging', @@ -320,18 +251,18 @@ const config: ChartConfig = { lang: 'zh-CN', translation: { header: { - title: '表头样式与分组', - open: '打开表头样式与分组', - styleAndGroup: '表头样式与分组', + title: '表头分组', + open: '打开', + styleAndGroup: '表头分组', }, column: { - title: '表格数据列', open: '打开列设置', list: '字段列表', sortAndFilter: '排序与过滤', enableSort: '开启列排序', basicStyle: '基础样式', conditionStyle: '条件样式', + conditionStylePanel: '条件样式配置器', backgroundColor: '背景颜色', align: '对齐方式', enableFixedCol: '开启固定列宽', @@ -351,9 +282,6 @@ const config: ChartConfig = { autoMerge: '自动合并相同内容', enableRaw: '使用原始数据', }, - cache: { - title: '数据处理', - }, paging: { title: '分页设置', enablePaging: '启用分页', @@ -365,9 +293,9 @@ const config: ChartConfig = { lang: 'en-US', translation: { header: { - title: 'Title', - open: 'Open Table Header and Group', - styleAndGroup: 'Style and Group', + title: 'Table Header Group', + open: 'Open', + styleAndGroup: 'Header Group', }, column: { title: 'Table Data Column', @@ -376,7 +304,8 @@ const config: ChartConfig = { sortAndFilter: 'Sort and Filter', enableSort: 'Enable Sort', basicStyle: 'Baisc Style', - conditionStyle: 'Condition Style', + conditionStyle: 'Column Condition Style', + conditionStylePanel: 'Condition Style Panel', backgroundColor: 'Background Color', align: 'Align', enableFixedCol: 'Enable Fixed Column', @@ -396,9 +325,6 @@ const config: ChartConfig = { autoMerge: 'Auto Merge', enableRaw: 'Enable Raw Data', }, - cache: { - title: 'Data Process', - }, paging: { title: 'Paging', enablePaging: 'Enable Paging', diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ChartJSChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ChartJSChart/config.ts index 11c8d3026..50bdcfea2 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ChartJSChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ChartJSChart/config.ts @@ -98,6 +98,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -115,7 +135,6 @@ const config: ChartConfig = { legend: { label: '图例', showLabel: '图例-显示标签', - showLabel2: '图例-显示标签2', }, }, }, @@ -127,6 +146,15 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + fontFamily: 'Font Family', + fontSize: 'Font Size', + fontColor: 'Font Color', + rotateLabel: 'Rotate Label', + showDataColumns: 'Show Data Column', + legend: { + label: 'Legend', + showLabel: 'Show Legend', + }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterBarChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterBarChart/config.ts index 19657173e..6a7e8e2f3 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterBarChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterBarChart/config.ts @@ -436,27 +436,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -531,8 +537,84 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + paging: { + title: '常规', + pageSize: '总行数', + }, + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', + }, + paging: { + title: 'Paging', + pageSize: 'Page Size', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterColumnChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterColumnChart/config.ts index 19657173e..6a7e8e2f3 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterColumnChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterColumnChart/config.ts @@ -436,27 +436,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -531,8 +537,84 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + paging: { + title: '常规', + pageSize: '总行数', + }, + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', + }, + paging: { + title: 'Paging', + pageSize: 'Page Size', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/D3USMapChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/D3USMapChart/config.ts index a34d17741..3e0e53509 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/D3USMapChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/D3USMapChart/config.ts @@ -86,6 +86,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -97,13 +117,10 @@ const config: ChartConfig = { showLabelWithSelect: '显示标签4', fontFamily: '字体', fontSize: '字体大小', - fontColor: '字体颜色', - rotateLabel: '旋转标签', showDataColumns: '选择数据列', legend: { label: '图例', showLabel: '图例-显示标签', - showLabel2: '图例-显示标签2', }, }, }, @@ -115,6 +132,13 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + fontFamily: 'Font Family', + fontSize: 'Font Size', + showDataColumns: 'Show Data Columns', + legend: { + label: 'Legend', + showLabel: 'Show label', + }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/FenZuTableChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/FenZuTableChart.tsx index 7d5d67b95..0c7fdc7da 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/FenZuTableChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/FenZuTableChart.tsx @@ -20,13 +20,17 @@ import { ChartConfig, ChartDataSectionType } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; import { getCustomSortableColumns, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import BasicTableChart from '../BasicTableChart'; import Config from './config'; +/** + * @deprecated Please use @see PivotSheetChart instead + * @class FenZuTableChart + * @extends {BasicTableChart} + */ class FenZuTableChart extends BasicTableChart { - chart: any = null; config = Config; isAutoMerge = true; @@ -39,16 +43,20 @@ class FenZuTableChart extends BasicTableChart { }); } - getOptions(context, dataset?: ChartDataset, config?: ChartConfig) { + getOptions( + context, + dataset?: ChartDataset, + config?: ChartConfig, + widgetSpecialConfig?: any, + ) { if (!dataset || !config) { return { locale: { emptyText: ' ' } }; } - const { clientWidth, clientHeight } = context.document.documentElement; const dataConfigs = config.datas || []; const styleConfigs = config.styles || []; const settingConfigs = config.settings || []; - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -71,14 +79,14 @@ class FenZuTableChart extends BasicTableChart { styleConfigs, dataColumns, ), - components: this.getTableComponents(styleConfigs), + summaryFn: undefined as any, + components: this.getTableComponents(styleConfigs, widgetSpecialConfig), ...this.getAntdTableStyleOptions( styleConfigs, - dataset, - clientWidth, - clientHeight, - tablePagination, + settingConfigs, + context?.height, ), + onChange: () => {}, }; } } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/config.ts index c16d9d278..7d9575526 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/config.ts @@ -45,7 +45,7 @@ const config: ChartConfig = { ], styles: [ { - label: 'column.title', + label: 'column.conditionStyle', key: 'column', comType: 'group', rows: [ @@ -70,6 +70,7 @@ const config: ChartConfig = { .map(c => ({ key: c.uid, value: c.uid, + type: c.type, label: c.label || c.aggregate ? `${c.aggregate}(${c.colName})` @@ -84,63 +85,18 @@ const config: ChartConfig = { comType: 'group', rows: [ { - label: 'column.basicStyle', - key: 'basicStyle', + label: 'column.conditionStyle', + key: 'conditionStyle', comType: 'group', options: { expand: true }, rows: [ { - label: 'column.backgroundColor', - key: 'backgroundColor', - comType: 'fontColor', - }, - { - label: 'column.align', - key: 'align', - default: 'left', - comType: 'select', - options: { - items: [ - { label: '左对齐', value: 'left' }, - { label: '居中对齐', value: 'center' }, - { label: '右对齐', value: 'right' }, - ], - }, - }, - { - label: 'column.enableFixedCol', - key: 'enableFixedCol', - comType: 'switch', - rows: [ - { - label: 'column.fixedColWidth', - key: 'fixedColWidth', - default: 100, - comType: 'inputNumber', - }, - ], - }, - { - label: 'font', - key: 'font', - comType: 'font', - default: { - fontFamily: 'PingFang SC', - fontSize: '12', - fontWeight: 'normal', - fontStyle: 'normal', - color: 'black', - }, + label: 'column.conditionStylePanel', + key: 'conditionStylePanel', + comType: 'conditionStylePanel', }, ], }, - { - label: 'column.conditionStyle', - key: 'conditionStyle', - comType: 'group', - options: { expand: true }, - rows: [], - }, ], }, }, @@ -238,14 +194,20 @@ const config: ChartConfig = { ], settings: [ { - label: 'cache.title', - key: 'cache', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'cache.title', - key: 'panel', - comType: 'cache', + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, @@ -267,6 +229,7 @@ const config: ChartConfig = { enableSort: '开启列排序', basicStyle: '基础样式', conditionStyle: '条件样式', + conditionStylePanel: '条件样式配置器', backgroundColor: '背景颜色', align: '对齐方式', enableFixedCol: '开启固定列宽', @@ -286,13 +249,9 @@ const config: ChartConfig = { autoMerge: '自动合并相同内容', enableRaw: '使用原始数据', }, - cache: { - title: '数据处理', - }, paging: { - title: '分页设置', - enablePaging: '启用分页', - pageSize: '分页大小', + title: '常规', + pageSize: '总行数', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/MingXiTableChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/MingXiTableChart.tsx index 122abe902..ba0ad3ec9 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/MingXiTableChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/MingXiTableChart.tsx @@ -17,14 +17,15 @@ */ import BasicTableChart from '../BasicTableChart'; +import Config from './config'; class MingXiTableChart extends BasicTableChart { - chart: any = null; + config = Config; constructor() { super({ id: 'mingxi-table', - name: '明细表', + name: '表格', icon: 'mingxibiao', }); } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/config.ts new file mode 100644 index 000000000..9a74a849c --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/config.ts @@ -0,0 +1,464 @@ +/** + * 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 { ChartConfig } from 'app/types/ChartConfig'; + +const config: ChartConfig = { + datas: [ + { + label: 'mixed', + key: 'mixed', + required: true, + type: 'mixed', + }, + { + label: 'filter', + key: 'filter', + type: 'filter', + disableAggregate: true, + }, + ], + styles: [ + { + label: 'header.title', + key: 'header', + comType: 'group', + rows: [ + { + label: 'header.open', + key: 'modal', + comType: 'group', + options: { type: 'modal', modalSize: 'middle' }, + rows: [ + { + label: 'header.styleAndGroup', + key: 'tableHeaders', + comType: 'tableHeader', + }, + ], + }, + ], + }, + { + label: 'column.conditionStyle', + key: 'column', + comType: 'group', + rows: [ + { + label: 'column.open', + key: 'modal', + comType: 'group', + options: { type: 'modal', modalSize: 'middle' }, + rows: [ + { + label: 'column.list', + key: 'list', + comType: 'listTemplate', + rows: [], + options: { + getItems: cols => { + const columns = (cols || []) + .filter(col => + ['aggregate', 'group', 'mixed'].includes(col.type), + ) + .reduce((acc, cur) => acc.concat(cur.rows || []), []) + .map(c => ({ + key: c.uid, + value: c.uid, + type: c.type, + label: + c.label || c.aggregate + ? `${c.aggregate}(${c.colName})` + : c.colName, + })); + return columns; + }, + }, + template: { + label: 'column.listItem', + key: 'listItem', + comType: 'group', + rows: [ + { + label: 'column.conditionStyle', + key: 'conditionStyle', + comType: 'group', + options: { expand: true }, + rows: [ + { + label: 'column.conditionStylePanel', + key: 'conditionStylePanel', + comType: 'conditionStylePanel', + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + label: 'style.title', + key: 'style', + comType: 'group', + rows: [ + { + label: 'style.enableFixedHeader', + key: 'enableFixedHeader', + default: true, + comType: 'checkbox', + }, + { + label: 'style.enableBorder', + key: 'enableBorder', + default: true, + comType: 'checkbox', + }, + { + label: 'style.enableRowNumber', + key: 'enableRowNumber', + default: false, + comType: 'checkbox', + }, + { + label: 'style.leftFixedColumns', + key: 'leftFixedColumns', + comType: 'select', + options: { + mode: 'multiple', + getItems: cols => { + const columns = (cols || []) + .filter(col => + ['aggregate', 'group', 'mixed'].includes(col.type), + ) + .reduce((acc, cur) => acc.concat(cur.rows || []), []) + .map(c => ({ + key: c.uid, + value: c.uid, + label: + c.label || c.aggregate + ? `${c.aggregate}(${c.colName})` + : c.colName, + })); + return columns; + }, + }, + }, + { + label: 'style.rightFixedColumns', + key: 'rightFixedColumns', + comType: 'select', + options: { + mode: 'multiple', + getItems: cols => { + const columns = (cols || []) + .filter(col => + ['aggregate', 'group', 'mixed'].includes(col.type), + ) + .reduce((acc, cur) => acc.concat(cur.rows || []), []) + .map(c => ({ + key: c.uid, + value: c.uid, + label: + c.label || c.aggregate + ? `${c.aggregate}(${c.colName})` + : c.colName, + })); + return columns; + }, + }, + }, + { + label: 'style.autoMergeFields', + key: 'autoMergeFields', + comType: 'select', + options: { + mode: 'multiple', + getItems: cols => { + const columns = (cols || []) + .filter(col => ['mixed'].includes(col.type)) + .reduce((acc, cur) => acc.concat(cur.rows || []), []) + .filter(c => c.type === 'STRING') + .map(c => ({ + key: c.uid, + value: c.uid, + label: + c.label || c.aggregate + ? `${c.aggregate}(${c.colName})` + : c.colName, + })); + return columns; + }, + }, + }, + { + label: 'style.tableSize', + key: 'tableSize', + default: 'small', + comType: 'select', + options: { + items: [ + { label: '默认', value: 'default' }, + { label: '中', value: 'middle' }, + { label: '小', value: 'small' }, + ], + }, + }, + ], + }, + { + label: 'style.tableHeaderStyle', + key: 'tableHeaderStyle', + comType: 'group', + rows: [ + { + label: 'style.bgColor', + key: 'bgColor', + default: '#f8f9fa', + comType: 'fontColor', + }, + { + label: 'style.font', + key: 'font', + comType: 'font', + default: { + fontFamily: 'PingFang SC', + fontSize: 12, + fontWeight: 'bold', + fontStyle: 'normal', + color: '#495057', + }, + }, + { + label: 'style.align', + key: 'align', + default: 'left', + comType: 'select', + options: { + items: [ + { label: '左对齐', value: 'left' }, + { label: '居中对齐', value: 'center' }, + { label: '右对齐', value: 'right' }, + ], + }, + }, + ], + }, + { + label: 'style.tableBodyStyle', + key: 'tableBodyStyle', + comType: 'group', + rows: [ + { + label: 'style.bgColor', + key: 'bgColor', + default: 'rgba(0,0,0,0)', + comType: 'fontColor', + }, + { + label: 'style.font', + key: 'font', + comType: 'font', + default: { + fontFamily: 'PingFang SC', + fontSize: 12, + fontWeight: 'normal', + fontStyle: 'normal', + color: '#495057', + }, + }, + { + label: 'style.align', + key: 'align', + default: 'left', + comType: 'fontAlignment', + }, + ], + }, + ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.enablePaging', + key: 'enablePaging', + default: true, + comType: 'checkbox', + options: { + needRefresh: true, + }, + }, + + { + label: 'paging.pageSize', + key: 'pageSize', + default: 100, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + watcher: { + deps: ['enablePaging'], + action: props => { + return { + disabled: !props.enablePaging, + }; + }, + }, + }, + ], + }, + { + label: 'summary.title', + key: 'summary', + comType: 'group', + rows: [ + { + label: 'summary.aggregateFields', + key: 'aggregateFields', + comType: 'select', + options: { + mode: 'multiple', + getItems: cols => { + const columns = (cols || []) + .filter(col => ['mixed'].includes(col.type)) + .reduce((acc, cur) => acc.concat(cur.rows || []), []) + .filter(c => c.type === 'NUMERIC') + .map(c => ({ + key: c.uid, + value: c.uid, + label: + c.label || c.aggregate + ? `${c.aggregate}(${c.colName})` + : c.colName, + })); + return columns; + }, + }, + }, + ], + }, + ], + i18ns: [ + { + lang: 'zh-CN', + translation: { + header: { + title: '表头分组', + open: '打开', + styleAndGroup: '表头分组', + }, + column: { + open: '打开样式设置', + list: '字段列表', + sortAndFilter: '排序与过滤', + enableSort: '开启列排序', + basicStyle: '基础样式', + conditionStyle: '条件样式', + conditionStylePanel: '条件样式配置器', + backgroundColor: '背景颜色', + align: '对齐方式', + enableFixedCol: '开启固定列宽', + fixedColWidth: '固定列宽度设置', + font: '字体与样式', + }, + style: { + title: '表格样式', + enableFixedHeader: '固定表头', + enableBorder: '显示边框', + enableRowNumber: '启用行号', + leftFixedColumns: '左侧固定列', + rightFixedColumns: '右侧固定列', + autoMergeFields: '自动合并列内容', + tableSize: '表格大小', + tableHeaderStyle: '表头样式', + tableBodyStyle: '表体样式', + bgColor: '背景颜色', + font: '字体', + align: '对齐方式', + }, + summary: { + title: '数据汇总', + aggregateFields: '汇总列', + }, + paging: { + title: '常规', + enablePaging: '启用分页', + pageSize: '分页大小', + }, + }, + }, + { + lang: 'en-US', + translation: { + header: { + title: 'Table Header Group', + open: 'Open', + styleAndGroup: 'Header Group', + }, + column: { + open: 'Open Style Setting', + list: 'Field List', + sortAndFilter: 'Sort and Filter', + enableSort: 'Enable Sort', + basicStyle: 'Baisc Style', + conditionStyle: 'Condition Style', + conditionStylePanel: 'Condition Style Panel', + backgroundColor: 'Background Color', + align: 'Align', + enableFixedCol: 'Enable Fixed Column', + fixedColWidth: 'Fixed Column Width', + font: 'Font and Style', + }, + style: { + title: 'Table Style', + enableFixedHeader: 'Enable Fixed Header', + enableBorder: 'Show Border', + enableRowNumber: 'Enable Row Number', + leftFixedColumns: 'Left Fixed Columns', + rightFixedColumns: 'Right Fixed Columns', + autoMergeFields: 'Auto Merge Column Content', + tableSize: 'Table Size', + tableHeaderStyle: 'Table Header Style', + tableBodyStyle: 'Table Body Style', + bgColor: 'Background Color', + font: 'Font', + align: 'Align', + }, + summary: { + title: 'Summary', + aggregateFields: 'Summary Fields', + }, + paging: { + title: 'Paging', + enablePaging: 'Enable Paging', + pageSize: 'Page Size', + }, + }, + }, + ], +}; + +export default config; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/NormalOutlineMapChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/NormalOutlineMapChart/config.ts index 4b47a11ef..31171589f 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/NormalOutlineMapChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/NormalOutlineMapChart/config.ts @@ -217,6 +217,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -261,6 +281,49 @@ const config: ChartConfig = { background: { title: '背景设置' }, }, }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + metricsAndColor: 'Metrics and Color', + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + map: { + title: 'Map', + level: 'Level', + enableZoom: 'Enabel Zoom', + backgroundColor: 'Background Color', + borderStyle: 'Border Style', + focusArea: 'Focus Area', + areaColor: 'Area Color', + areaEmphasisColor: 'Area Emphasis Color', + }, + background: { title: 'Background' }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackBarChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackBarChart/config.ts index 6bf100a04..92aeadfd9 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackBarChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackBarChart/config.ts @@ -430,27 +430,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -525,8 +531,76 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackColumnChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackColumnChart/config.ts index 6bf100a04..92aeadfd9 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackColumnChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackColumnChart/config.ts @@ -430,27 +430,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -525,8 +531,76 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/AntVS2Wrapper.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/AntVS2Wrapper.tsx new file mode 100644 index 000000000..8f1e22a27 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/AntVS2Wrapper.tsx @@ -0,0 +1,88 @@ +/** + * 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 { S2Theme } from '@antv/s2'; +import { SheetComponent } from '@antv/s2-react'; +import '@antv/s2-react/dist/style.min.css'; +import { FC, memo } from 'react'; +import styled from 'styled-components/macro'; +import { FONT_SIZE_LABEL } from 'styles/StyleConstants'; + +const AntVS2Wrapper: FC<{ + dataCfg; + options; + theme?: S2Theme; +}> = memo(({ dataCfg, options, theme }) => { + const onDataCellHover = ({ event, viewMeta }) => { + viewMeta.spreadsheet.tooltip.show({ + position: { + x: event.clientX, + y: event.clientY, + }, + content: ( + + ), + }); + }; + + return ( + + ); +}); + +const TableDataCellTooltip: FC<{ + datas?: object; + meta?: Array<{ field: string; name: string; formatter }>; +}> = ({ datas, meta }) => { + if (!datas) { + return null; + } + + return ( + + {(meta || []) + .map(m => { + const uniqKey = m?.field; + if (uniqKey in datas) { + return {`${m?.name}: ${m?.formatter(datas[uniqKey])}`}; + } + return null; + }) + .filter(Boolean)} + + ); +}; + +const StyledTableDataCellTooltip = styled.ul` + padding: 4px; + font-size: ${FONT_SIZE_LABEL}; + color: ${p => p.theme.textColorLight}; +`; + +const StyledAntVS2Wrapper = styled(SheetComponent)``; + +export default AntVS2Wrapper; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/PivotSheetChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/PivotSheetChart.tsx new file mode 100644 index 000000000..6963d36b6 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/PivotSheetChart.tsx @@ -0,0 +1,384 @@ +/** + * 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 { + ChartConfig, + ChartDataSectionType, + SortActionType, +} from 'app/types/ChartConfig'; +import ChartDataset from 'app/types/ChartDataset'; +import { + getColumnRenderName, + getCustomSortableColumns, + getValueByColumnKey, + transformToObjectArray, +} from 'app/utils/chartHelper'; +import { isNumber, toFormattedValue } from 'app/utils/number'; +import groupBy from 'lodash/groupBy'; +import ReactChart from '../ReactChart'; +import AntVS2Wrapper from './AntVS2Wrapper'; +import Config from './config'; +class PivotSheetChart extends ReactChart { + static icon = ``; + + _useIFrame = false; + isISOContainer = 'piovt-sheet'; + config = Config; + chart: any = null; + updateOptions: any = {}; + + constructor() { + super(AntVS2Wrapper, { + id: 'piovt-sheet', + name: '透视表', + icon: PivotSheetChart.icon, + }); + this.meta.requirements = [{}]; + } + + onUpdated(options, context): void { + if (!this.isMatchRequirement(options.config)) { + this.adapter?.unmount(); + return; + } + + this.updateOptions = this.getOptions( + context, + options.dataset, + options.config, + ); + this.adapter?.updated(this.updateOptions); + } + + onResize(_, context) { + if (this.updateOptions?.options) { + this.updateOptions.options = Object.assign( + { + ...this.updateOptions.options, + }, + { width: context.width, height: context.height }, + ); + this.adapter?.updated(this.updateOptions); + } + } + + getOptions(context, dataset?: ChartDataset, config?: ChartConfig) { + if (!dataset || !config) { + return {}; + } + + const dataConfigs = config.datas || []; + const styleConfigs = config.styles || []; + const settingConfigs = config.settings || []; + const objDataColumns = transformToObjectArray( + dataset.rows, + dataset.columns, + ); + const dataColumns = getCustomSortableColumns(objDataColumns, dataConfigs); + + const rowSectionConfigRows = dataConfigs + .filter(c => c.type === ChartDataSectionType.GROUP) + .filter(c => c.key === 'row') + .flatMap(config => config.rows || []); + + const columnSectionConfigRows = dataConfigs + .filter(c => c.type === ChartDataSectionType.GROUP) + .filter(c => c.key === 'column') + .flatMap(config => config.rows || []); + + const metricsSectionConfigRows = dataConfigs + .filter(c => c.type === ChartDataSectionType.AGGREGATE) + .flatMap(config => config.rows || []); + + const infoSectionConfigRows = dataConfigs + .filter(c => c.type === ChartDataSectionType.INFO) + .flatMap(config => config.rows || []); + + const enableExpandRow = this.getStyleValue(styleConfigs, [ + 'style', + 'enableExpandRow', + ]); + const enableHoverHighlight = this.getStyleValue(styleConfigs, [ + 'style', + 'enableHoverHighlight', + ]); + const enableSelectedHighlight = this.getStyleValue(styleConfigs, [ + 'style', + 'enableSelectedHighlight', + ]); + const metricNameShowIn = this.getStyleValue(styleConfigs, [ + 'style', + 'metricNameShowIn', + ]); + const enableTotal = this.getStyleValue(settingConfigs, [ + 'rowSummary', + 'enableTotal', + ]); + const totalPosition = this.getStyleValue(settingConfigs, [ + 'rowSummary', + 'totalPosition', + ]); + const enableSubTotal = this.getStyleValue(settingConfigs, [ + 'rowSummary', + 'enableSubTotal', + ]); + const subTotalPosition = this.getStyleValue(settingConfigs, [ + 'rowSummary', + 'subTotalPosition', + ]); + + return { + options: { + hierarchyType: enableExpandRow ? 'tree' : 'grid', + width: context?.width, + height: context?.height, + tooltip: { + showTooltip: true, + }, + interaction: { + hoverHighlight: Boolean(enableHoverHighlight), + selectedCellsSpotlight: Boolean(enableSelectedHighlight), + }, + totals: { + row: { + showGrandTotals: Boolean(enableTotal), + reverseLayout: Boolean(totalPosition), + showSubTotals: Boolean(enableSubTotal), + reverseSubLayout: Boolean(subTotalPosition), + subTotalsDimensions: + rowSectionConfigRows.map(getValueByColumnKey)?.[0], + }, + }, + }, + dataCfg: { + fields: { + rows: rowSectionConfigRows.map(getValueByColumnKey), + columns: columnSectionConfigRows.map(getValueByColumnKey), + values: metricsSectionConfigRows.map(getValueByColumnKey), + valueInCols: !!metricNameShowIn, + }, + meta: rowSectionConfigRows + .concat(columnSectionConfigRows) + .concat(metricsSectionConfigRows) + .concat(infoSectionConfigRows) + .map(config => { + return { + field: getValueByColumnKey(config), + name: getColumnRenderName(config), + formatter: value => toFormattedValue(value, config?.format), + }; + }), + data: dataColumns, + totalData: this.getCalcSummaryValues( + dataColumns, + rowSectionConfigRows, + columnSectionConfigRows, + metricsSectionConfigRows, + enableTotal, + enableSubTotal, + ), + sortParams: this.getTableSorters( + rowSectionConfigRows + .concat(columnSectionConfigRows) + .concat(metricsSectionConfigRows), + ), + }, + theme: { + /* + DATA_CELL = "dataCell", + HEADER_CELL = "headerCell", + ROW_CELL = "rowCell", + COL_CELL = "colCell", + CORNER_CELL = "cornerCell", + MERGED_CELL = "mergedCell" + */ + cornerCell: this.getHeaderStyle(styleConfigs), + colCell: this.getHeaderStyle(styleConfigs), + rowCell: this.getHeaderStyle(styleConfigs), + dataCell: this.getBodyStyle(styleConfigs), + }, + }; + } + + private getTableSorters(sectionConfigRows) { + return sectionConfigRows + .map(config => { + if (!config?.sort?.type || config?.sort?.type === SortActionType.NONE) { + return null; + } + const isASC = config.sort.type === SortActionType.ASC; + return { + sortFieldId: getValueByColumnKey(config), + sortFunc: params => { + const { data } = params; + return data?.sort((a, b) => + isASC ? a?.localeCompare(b) : b?.localeCompare(a), + ); + }, + }; + }) + .filter(Boolean); + } + + private getBodyStyle(styleConfigs) { + const bodyFont = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'font', + ]); + const oddBgColor = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'oddBgColor', + ]); + const evenBgColor = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'evenBgColor', + ]); + const bodyTextAlign = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'align', + ]); + + return { + cell: { + crossBackgroundColor: evenBgColor, + backgroundColor: oddBgColor, + }, + text: { + fill: bodyFont?.color, + fontFamily: bodyFont?.fontFamily, + fontSize: bodyFont?.fontSize, + fontWeight: bodyFont?.fontWeight, + textAlign: bodyTextAlign, + }, + }; + } + + private getHeaderStyle(styleConfigs) { + const headerFont = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'font', + ]); + const headerBgColor = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'bgColor', + ]); + const headerTextAlign = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'align', + ]); + + return { + cell: { + backgroundColor: headerBgColor, + }, + text: { + fill: headerFont?.color, + fontFamily: headerFont?.fontFamily, + fontSize: headerFont?.fontSize, + fontWeight: headerFont?.fontWeight, + textAlign: headerTextAlign, + }, + bolderText: { + fill: headerFont?.color, + fontFamily: headerFont?.fontFamily, + fontSize: headerFont?.fontSize, + fontWeight: headerFont?.fontWeight, + textAlign: headerTextAlign, + }, + }; + } + + private getCalcSummaryValues( + dataColumns, + rowSectionConfigRows, + columnSectionConfigRows, + metricsSectionConfigRows, + enableTotal, + enableSubTotal, + ) { + let summarys: any[] = []; + if (enableTotal) { + if (!columnSectionConfigRows.length) { + const rowTotals = metricsSectionConfigRows.map(c => { + const values = dataColumns + .map(dc => +dc?.[getValueByColumnKey(c)]) + .filter(isNumber); + return { + [getValueByColumnKey(c)]: values?.reduce((a, b) => a + b, 0), + }; + }); + summarys.push(...rowTotals); + } else { + const rowTotals = this.calculateGroupedColumnTotal( + {}, + columnSectionConfigRows.map(getValueByColumnKey), + metricsSectionConfigRows, + dataColumns, + ); + summarys.push(...rowTotals); + } + } + if (enableSubTotal) { + const rowTotals = this.calculateGroupedColumnTotal( + {}, + [rowSectionConfigRows[0]] + .concat(columnSectionConfigRows) + .map(getValueByColumnKey), + metricsSectionConfigRows, + dataColumns, + ); + summarys.push(...rowTotals); + } + + return summarys; + } + + private calculateGroupedColumnTotal( + preObj, + groupKeys, + metrics: any[], + datas, + ) { + const _groupKeys = [...(groupKeys || [])]; + const groupKey = _groupKeys.shift(); + const groupDataSet = groupBy(datas, groupKey); + + return Object.entries(groupDataSet).flatMap(([k, v]) => { + if (_groupKeys.length) { + return this.calculateGroupedColumnTotal( + Object.assign({}, preObj, { [groupKey]: k }), + _groupKeys, + metrics, + v, + ); + } + return metrics.map(metric => { + const values = (v as any[]) + .map(dc => +dc?.[getValueByColumnKey(metric)]) + .filter(isNumber); + return { + ...preObj, + [groupKey]: k, + [getValueByColumnKey(metric)]: values?.reduce((a, b) => a + b, 0), + }; + }); + }); + } +} + +export default PivotSheetChart; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/config.ts new file mode 100644 index 000000000..b28338fec --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/config.ts @@ -0,0 +1,397 @@ +/** + * 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 { ChartConfig } from 'app/types/ChartConfig'; + +const config: ChartConfig = { + datas: [ + { + label: 'datas.column', + key: 'column', + type: 'group', + options: { + sortable: { backendSort: false }, + }, + }, + { + label: 'datas.row', + key: 'row', + type: 'group', + options: { + sortable: { backendSort: false }, + }, + }, + { + label: 'metrics', + key: 'metrics', + type: 'aggregate', + actions: { + NUMERIC: ['aggregate', 'alias', 'format', 'sortable'], + STRING: ['aggregate', 'alias', 'format', 'sortable'], + }, + options: { + sortable: { backendSort: false }, + }, + }, + { + label: 'filter', + key: 'filter', + type: 'filter', + }, + { + label: 'info', + key: 'info', + type: 'info', + }, + ], + styles: [ + // { + // label: 'column.title', + // key: 'column', + // comType: 'group', + // rows: [ + // { + // label: 'column.open', + // key: 'modal', + // comType: 'group', + // options: { type: 'modal', modalSize: 'middle' }, + // rows: [ + // { + // label: 'column.list', + // key: 'list', + // comType: 'listTemplate', + // rows: [], + // options: { + // getItems: cols => { + // const columns = (cols || []) + // .filter(col => + // ['aggregate', 'group', 'mixed'].includes(col.type), + // ) + // .reduce((acc, cur) => acc.concat(cur.rows || []), []) + // .map(c => ({ + // key: c.uid, + // value: c.uid, + // label: + // c.label || c.aggregate + // ? `${c.aggregate}(${c.colName})` + // : c.colName, + // })); + // return columns; + // }, + // }, + // template: { + // label: 'column.listItem', + // key: 'listItem', + // comType: 'group', + // rows: [ + // { + // label: 'column.conditionStyle', + // key: 'conditionStyle', + // comType: 'group', + // options: { expand: true }, + // rows: [], + // }, + // ], + // }, + // }, + // ], + // }, + // ], + // }, + { + label: 'style.title', + key: 'style', + comType: 'group', + rows: [ + { + label: 'style.enableExpandRow', + key: 'enableExpandRow', + default: false, + comType: 'checkbox', + }, + { + label: 'style.enableHoverHighlight', + key: 'enableHoverHighlight', + default: true, + comType: 'checkbox', + }, + { + label: 'style.enableSelectedHighlight', + key: 'enableSelectedHighlight', + default: false, + comType: 'checkbox', + }, + { + label: 'style.metricNameShowIn.label', + key: 'metricNameShowIn', + default: true, + comType: 'radio', + options: { + translateItemLabel: true, + items: [ + { + key: 'inCol', + label: 'style.metricNameShowIn.inCol', + value: true, + }, + { + key: 'inRow', + label: 'style.metricNameShowIn.inRow', + value: false, + }, + ], + }, + }, + ], + }, + { + label: 'style.tableHeaderStyle', + key: 'tableHeaderStyle', + comType: 'group', + rows: [ + { + label: 'style.bgColor', + key: 'bgColor', + comType: 'fontColor', + }, + { + label: 'style.font', + key: 'font', + comType: 'font', + default: { + fontFamily: 'PingFang SC', + fontSize: 12, + fontWeight: 'normal', + color: '#495057', + }, + options: { + fontFamilies: [ + 'Roboto', + 'PingFangSC', + 'BlinkMacSystemFont', + 'Microsoft YaHei', + 'Arial', + 'sans-serif', + ], + }, + }, + { + label: 'style.align', + key: 'align', + default: 'right', + comType: 'fontAlignment', + }, + ], + }, + { + label: 'style.tableBodyStyle', + key: 'tableBodyStyle', + comType: 'group', + rows: [ + { + label: 'style.oddBgColor', + key: 'oddBgColor', + comType: 'fontColor', + }, + { + label: 'style.evenBgColor', + key: 'evenBgColor', + comType: 'fontColor', + }, + { + label: 'style.font', + key: 'font', + comType: 'font', + default: { + fontFamily: 'PingFang SC', + fontSize: 12, + fontWeight: 'normal', + color: '#495057', + }, + options: { + fontFamilies: [ + 'Roboto', + 'PingFangSC', + 'BlinkMacSystemFont', + 'Microsoft YaHei', + 'Arial', + 'sans-serif', + ], + }, + }, + { + label: 'style.align', + key: 'align', + default: 'left', + comType: 'fontAlignment', + }, + ], + }, + ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + { + label: 'summary.rowSummary', + key: 'rowSummary', + comType: 'group', + rows: [ + { + label: 'summary.enableTotal', + key: 'enableTotal', + default: false, + comType: 'checkbox', + }, + { + label: 'summary.totalPosition', + key: 'totalPosition', + default: true, + comType: 'select', + options: { + items: [ + { label: '顶部', value: true }, + { label: '底部', value: false }, + ], + }, + }, + { + label: 'summary.enableSubTotal', + key: 'enableSubTotal', + default: false, + comType: 'checkbox', + }, + { + label: 'summary.subTotalPosition', + key: 'subTotalPosition', + default: true, + comType: 'select', + options: { + items: [ + { label: '顶部', value: true }, + { label: '底部', value: false }, + ], + }, + }, + ], + }, + ], + i18ns: [ + { + lang: 'zh-CN', + translation: { + datas: { + row: '行', + column: '列', + }, + style: { + title: '表格样式', + enableExpandRow: '行表头折叠', + enableHoverHighlight: '启用联动高亮', + enableSelectedHighlight: '启用选中高亮', + enableAdjustRowHeight: '启用调整行高', + enableAdjustColumnWidth: '启用调整列宽', + metricNameShowIn: { + label: '指标名称位置', + inCol: '列表头', + inRow: '行表头', + }, + tableSize: '表格大小', + tableHeaderStyle: '表头样式', + tableBodyStyle: '表体样式', + bgColor: '背景颜色', + evenBgColor: '偶数行背景颜色', + oddBgColor: '奇数行背景颜色', + font: '字体', + align: '对齐方式', + }, + summary: { + title: '数据汇总', + rowSummary: '行总计', + columnSummary: '列总计', + enableTotal: '启用总计', + enableSubTotal: '启用小计', + totalPosition: '总计位置', + subTotalPosition: '小计位置', + aggregateFields: '汇总列', + }, + }, + }, + { + lang: 'en-US', + translation: { + datas: { + row: 'Row', + column: 'Column', + }, + style: { + title: 'Table Style', + enableExpandRow: 'Fold Row', + enableHoverHighlight: 'Enable Hover Highlight', + enableSelectedHighlight: 'Enable Selected Highlight', + enableAdjustRowHeight: 'Enable Adjust Row Height', + enableAdjustColumnWidth: 'Enable Adjust Column Width', + metricNameShowIn: { + label: 'Metric Name Position', + inCol: 'Col Header', + inRow: 'Row Header', + }, + tableSize: 'Table Size', + tableHeaderStyle: 'Table Header Style', + tableBodyStyle: 'Table Body Style', + bgColor: 'Background Color', + evenBgColor: 'Even Row Background Color', + oddBgColor: 'Odd Row Background Color', + font: 'Font', + align: 'Align', + }, + summary: { + title: 'Summary', + rowSummary: 'Row Total', + columnSummary: 'Column Total', + enableTotal: 'Enable Total', + enableSubTotal: 'Enable Sub Total', + totalPosition: 'Total Position', + subTotalPosition: 'Sub Total Position', + aggregateFields: 'Summary Fields', + }, + paging: { + title: 'Paging', + pageSize: 'Page Size', + }, + }, + }, + ], +}; + +export default config; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/icon.svg b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/icon.svg new file mode 100644 index 000000000..3a104222c --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/index.ts new file mode 100644 index 000000000..60675f656 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/index.ts @@ -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 PivotSheetChart from './PivotSheetChart'; + +export default PivotSheetChart; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/ReChartsChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/ReChartsChart.tsx index fac68f71e..0b6fb5f72 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/ReChartsChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/ReChartsChart.tsx @@ -21,10 +21,6 @@ import Config from './config'; import ReChartPie from './ReChartPie'; class ReChartsChart extends ReactChart { - constructor() { - super('rechart-chart', 'React ReChart Chart', 'preview'); - } - isISOContainer = 'react-rechart-chart'; config = Config; dependency = [ @@ -34,20 +30,21 @@ class ReChartsChart extends ReactChart { 'https://unpkg.com/recharts@2.0.8/umd/Recharts.min.js', ]; + constructor() { + super(ReChartPie, { + id: 'rechart-chart', + name: 'React ReChart Chart', + icon: 'preview', + }); + } + onMount(options, context): void { const { Surface, Pie } = context.window.Recharts; - this.getInstance().init(ReChartPie); - this.getInstance().registerImportDependenies({ Surface, Pie }); - this.getInstance().mounted( - context.document.getElementById(options.containerId), - ); + this.adapter.registerImportDependenies({ Surface, Pie }); + this.adapter.mounted(context.document.getElementById(options.containerId)); } onUpdated({ config }: { config: any }): void {} - - onUnMount(): void { - // this.getWrapper().unmount(); - } } export default ReChartsChart; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/config.ts index 11c8d3026..1257e9135 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/config.ts @@ -98,6 +98,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -127,6 +147,15 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + fontFamily: 'Font Family', + fontSize: 'Font Size', + fontColor: 'Font Color', + rotateLabel: 'Rotate Label', + showDataColumns: 'Show Data Columns', + legend: { + label: 'Legend', + showLabel: 'Show Label', + }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactChart.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactChart.ts index 010336ef3..62213bab3 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactChart.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactChart.ts @@ -17,16 +17,41 @@ */ import Chart from '../../../../models/Chart'; -import ReactChartAdapter from '../ChartTools/ReactChartAdapter'; +import ReactLifecycleAdapter from '../ChartTools/ReactLifecycleAdapter'; export default class ReactChart extends Chart { - adapter = new ReactChartAdapter(); + private _adapter; - init(component) { - this.adapter.init(component); + constructor(wrapper, props) { + super( + props?.id || 'react-table', + props?.name || '表格', + props?.icon || 'table', + ); + this._adapter = new ReactLifecycleAdapter(wrapper); } - getInstance() { - return this.adapter; + get adapter() { + if (!this._adapter) { + throw new Error( + 'should be register component by initAdapter before in used', + ); + } + return this._adapter; + } + + public onMount(options, context?): void { + if (options.containerId === undefined || !context.document) { + return; + } + this.adapter?.mounted( + context.document.getElementById(options.containerId), + options, + context, + ); + } + + public onUnMount(options, context?): void { + this.adapter?.unmount(); } } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/ReactVizXYPlotChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/ReactVizXYPlotChart.tsx index f833e6d71..36c018b52 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/ReactVizXYPlotChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/ReactVizXYPlotChart.tsx @@ -21,10 +21,6 @@ import Config from './config'; import ReactXYPlot from './ReactVizXYPlot'; class ReactVizXYPlotChart extends ReactChart { - constructor() { - super('reactviz-xyplot-chart', 'ReactViz XYPlot Chart', 'star'); - } - isISOContainer = 'reactviz-container'; config = Config; dependency = [ @@ -32,30 +28,34 @@ class ReactVizXYPlotChart extends ReactChart { 'https://unpkg.com/react-vis/dist/dist.min.js', ]; + constructor() { + super(ReactXYPlot, { + id: 'reactviz-xyplot-chart', + name: 'ReactViz XYPlot Chart', + icon: 'star', + }); + } + onMount(options, context): void { if (!context.window.reactVis) { return; } const { XYPlot, XAxis, YAxis, HorizontalGridLines, LineSeries } = context.window.reactVis; - this.getInstance().init(ReactXYPlot); - this.getInstance().registerImportDependenies({ + this.adapter.init(ReactXYPlot); + this.adapter.registerImportDependenies({ XYPlot, XAxis, YAxis, HorizontalGridLines, LineSeries, }); - this.getInstance().mounted( - context.document.getElementById(options.containerId), - ); + this.adapter.mounted(context.document.getElementById(options.containerId)); } onUpdated(props): void { - // this.getWrapper().updated(props); + // this.adapter.updated(props); } - - onUnMount(): void {} } export default ReactVizXYPlotChart; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/config.ts index 9571bdb37..0087e925f 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/config.ts @@ -99,6 +99,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -116,7 +136,6 @@ const config: ChartConfig = { legend: { label: '图例', showLabel: '图例-显示标签', - showLabel2: '图例-显示标签2', }, }, }, @@ -128,6 +147,15 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + fontFamily: 'Font Family', + fontSize: 'Font Size', + fontColor: 'Font Color', + rotateLabel: 'Rotate Label', + showDataColumns: 'Show Data Columns', + legend: { + label: 'Legend', + showLabel: 'Show Label', + }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/RephaelPaperChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/RephaelPaperChart/config.ts index 11c8d3026..1257e9135 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/RephaelPaperChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/RephaelPaperChart/config.ts @@ -98,6 +98,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -127,6 +147,15 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + fontFamily: 'Font Family', + fontSize: 'Font Size', + fontColor: 'Font Color', + rotateLabel: 'Rotate Label', + showDataColumns: 'Show Data Columns', + legend: { + label: 'Legend', + showLabel: 'Show Label', + }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScatterOutlineMapChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScatterOutlineMapChart/config.ts index 56bc4bd93..ae8c592b1 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScatterOutlineMapChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScatterOutlineMapChart/config.ts @@ -228,6 +228,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -273,6 +293,49 @@ const config: ChartConfig = { background: { title: '背景设置' }, }, }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + metricsAndColor: 'Metrics and Color', + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + map: { + title: 'Map', + level: 'Level', + enableZoom: 'Enabel Zoom', + backgroundColor: 'Background Color', + borderStyle: 'Border Style', + focusArea: 'Focus Area', + areaColor: 'Area Color', + areaEmphasisColor: 'Area Emphasis Color', + }, + background: { title: 'Background' }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/ScoreChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/ScoreChart.tsx index d0d35d2c4..5b58f69a4 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/ScoreChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/ScoreChart.tsx @@ -22,7 +22,7 @@ import ChartDataset from 'app/types/ChartDataset'; import { getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; @@ -38,6 +38,8 @@ class ScoreChart extends Chart { chart: any = null; config = Config; utilCanvas = null; + scoreChartOptions = { dataset: {}, config: {} }; + boardTypes = ['header', 'body', 'footer']; constructor(props?) { @@ -76,6 +78,7 @@ class ScoreChart extends Chart { this.chart?.clear(); return; } + this.scoreChartOptions = props; const newOptions = this.getOptions(props.dataset, props.config, context); this.chart?.setOption(Object.assign({}, newOptions), true); } @@ -85,6 +88,7 @@ class ScoreChart extends Chart { } onResize(opt: any, context): void { + this.onUpdated(this.scoreChartOptions, context); this.chart?.resize(context); } @@ -95,7 +99,7 @@ class ScoreChart extends Chart { .filter(c => c.type === ChartDataSectionType.AGGREGATE) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -112,7 +116,6 @@ class ScoreChart extends Chart { const { basicFontSize, bodyContentFontSize } = this.computeFontSize( context, - { width: this.chart?.getWidth(), height: this.chart?.getHeight() }, ).apply(null, measureTexts as any); const richStyles = aggConfigValues @@ -295,7 +298,7 @@ class ScoreChart extends Chart { } private computeFontSize = - (context, style) => + context => ( prefixHeader: string, headerText: string, @@ -314,7 +317,7 @@ class ScoreChart extends Chart { const hasContent = prefixContent || contentText || suffixContent; const hasFooter = prefixFooter || footerText || suffixFooter; - const { width, height } = style; + const { width, height } = context; const maxPartSize = 16; const exactWidth = diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/config.ts index 45994c7b4..10dabc4e6 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/config.ts @@ -248,6 +248,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -269,6 +289,26 @@ const config: ChartConfig = { }, }, }, + { + lang: 'en-US', + translation: { + score: { + headerTitle: 'Header', + bodyTitle: 'Body', + footerTitle: 'Footer Title', + show: 'Show', + prefixText: 'Prefix Text', + suffixText: 'Suffix Text', + prefxFont: 'Prefix Font', + suffixFont: 'Suffix Font', + common: 'Common', + isFixedFontSize: 'Enable Fixed Font Size', + headerFontSize: 'Header Font Size', + bodyFontSize: 'Body Font Size', + footerFontSize: 'Footer Font Size', + }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackBarChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackBarChart/config.ts index 6bf100a04..187922acf 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackBarChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackBarChart/config.ts @@ -430,27 +430,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -525,11 +531,79 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, }, }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', + }, + }, + } ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackColumnChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackColumnChart/config.ts index 6bf100a04..92aeadfd9 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackColumnChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackColumnChart/config.ts @@ -430,27 +430,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -525,8 +531,76 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/WaterfallChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/WaterfallChart.tsx index 537bd8e9a..7639e10aa 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/WaterfallChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/WaterfallChart.tsx @@ -24,8 +24,9 @@ import { getCustomSortableColumns, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; +import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; import { UniqArray } from 'utils/object'; import Config from './config'; @@ -89,7 +90,7 @@ class WaterfallChart extends Chart { .filter(c => c.type === ChartDataSectionType.AGGREGATE) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -127,7 +128,7 @@ class WaterfallChart extends Chart { styles, 'bar', ); - const label = this.getLabel(styles); + const label = this.getLabel(styles, aggregateConfigs[0].format); const dataList = dataColumns.map( dc => dc[getValueByColumnKey(aggregateConfigs[0])], @@ -199,7 +200,10 @@ class WaterfallChart extends Chart { if (!index && typeof param[1].value === 'number') { data += param[1].value; } - return `${pa.seriesName}: ${data}`; + return `${pa.seriesName}: ${toFormattedValue( + data, + aggregateConfigs[0].format, + )}`; }); const xAxis = param[0]['axisValue']; if (xAxis === '累计') { @@ -304,7 +308,7 @@ class WaterfallChart extends Chart { }; } - getLabel(styles) { + getLabel(styles, format) { const [show, position, font] = this.getArrStyleValueByGroup( ['showLabel', 'position', 'font'], styles, @@ -314,6 +318,7 @@ class WaterfallChart extends Chart { show, position, ...font, + formatter: ({ value }) => `${toFormattedValue(value, format)}`, }; } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/config.ts index 317c60e10..d817e28bc 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/config.ts @@ -395,27 +395,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -471,8 +477,76 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/WordCloudChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/WordCloudChart.tsx index a8d0b7118..a1ba6e134 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/WordCloudChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/WordCloudChart.tsx @@ -23,7 +23,7 @@ import { getDefaultThemeColor, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { init } from 'echarts'; import 'echarts-wordcloud'; @@ -93,7 +93,7 @@ class WordCloudChart extends Chart { .filter(c => c.type === ChartDataSectionType.AGGREGATE) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/config.ts index 5b4f03fbf..0fcb4e280 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/config.ts @@ -26,6 +26,10 @@ const config: ChartConfig = { required: true, type: 'group', limit: 1, + actions: { + NUMERIC: ['sortable'], + STRING: ['sortable'], + }, }, { label: 'metrics', @@ -33,6 +37,10 @@ const config: ChartConfig = { required: true, type: 'aggregate', limit: 1, + actions: { + NUMERIC: ['sortable', 'aggregate'], + STRING: ['sortable', 'aggregate'], + }, }, { label: 'filter', @@ -209,6 +217,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -223,7 +251,7 @@ const config: ChartConfig = { label: { title: '标签', fontFamily: '字体', - fontWeight: '字号', + fontWeight: '字体粗细', maxFontSize: '字体最大值', minFontSize: '字体最小值', rotationRangeStart: '起始旋转角度', @@ -236,6 +264,32 @@ const config: ChartConfig = { }, }, }, + { + lang: 'en-US', + translation: { + wordCloud: { + title: 'Word Cloud', + shape: 'Shape', + drawOutOfBound: 'Boundary', + width: 'Width', + height: 'Height', + }, + label: { + title: 'Label', + fontFamily: 'Font Family', + fontWeight: 'Font Weight', + maxFontSize: 'Max Font Size', + minFontSize: 'Min Font Size', + rotationRangeStart: 'Start Rotation Range', + rotationRangeEnd: 'End Rotation Range', + rotationStep: 'Rotation Step', + gridSize: 'Grid Size', + focus: 'Focus', + textShadowBlur: 'Text Shadow Blur', + textShadowColor: 'Text Shadow Color', + }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/index.ts index a2a3e2977..4e38e18f6 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/index.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/index.ts @@ -25,6 +25,7 @@ import BasicGaugeChart from './BasicGaugeChart'; import BasicLineChart from './BasicLineChart'; import BasicOutlineMapChart from './BasicOutlineMapChart'; import BasicPieChart from './BasicPieChart'; +import BasicRichText from './BasicRichText'; import BasicScatterChart from './BasicScatterChart'; import BasicTableChart from './BasicTableChart'; import ClusterBarChart from './ClusterBarChart'; @@ -37,6 +38,7 @@ import NormalOutlineMapChart from './NormalOutlineMapChart'; import PercentageStackBarChart from './PercentageStackBarChart'; import PercentageStackColumnChart from './PercentageStackColumnChart'; import PieChart from './PieChart'; +import PivotSheetChart from './PivotSheetChart'; import RoseChart from './RoseChart'; import ScatterOutlineMapChart from './ScatterOutlineMapChart'; import ScoreChart from './ScoreChart'; @@ -76,5 +78,7 @@ const WidgetPlugins = { ScatterOutlineMapChart, WaterfallChart, BasicGaugeChart, + BasicRichText, + PivotSheetChart, }; export default WidgetPlugins; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphPanel.tsx index 48aa0adf8..fc84e3107 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphPanel.tsx @@ -16,7 +16,7 @@ * limitations under the License. */ -import { Popconfirm, Tooltip } from 'antd'; +import { Tooltip } from 'antd'; import { IW } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; @@ -61,6 +61,7 @@ const ChartGraphPanel: FC<{ const handleChartChange = useCallback( chartId => () => { const chart = chartManager.getById(chartId); + if (!!chart) { onChartChange(chart); } @@ -112,7 +113,6 @@ const ChartGraphPanel: FC<{ > - + {renderIcon({ + iconStr: c?.meta?.icon, + isMatchRequirement: !!requirementsStates?.[c?.meta?.id], + isActive: c?.meta?.id === chart?.meta?.id, + })} ); }; - return allCharts.map(c => { - if (c?.meta?.id !== 'mingxi-table') { - return _getChartIcon(c, handleChartChange(c?.meta?.id)); + const renderIcon = ({ + ...args + }: { + iconStr; + isMatchRequirement; + isActive; + }) => { + if (/^; } + if (/svg\+xml;base64/.test(args?.iconStr)) { + return ; + } + return ; + }; - return ( - - {_getChartIcon(c)} - - ); + return allCharts.map(c => { + return _getChartIcon(c, handleChartChange(c?.meta?.id)); }); }; return {renderCharts()}; }); +const SVGFontIconRender = ({ iconStr, isMatchRequirement }) => { + return ( + + ); +}; + +const SVGImageRender = ({ iconStr, isMatchRequirement, isActive }) => { + return ( + + ); +}; + +const Base64ImageRender = ({ iconStr, isMatchRequirement, isActive }) => { + return ( + + ); +}; + export default ChartGraphPanel; const StyledChartGraphPanel = styled.div` @@ -164,10 +200,17 @@ const IconWrapper = styled.span` padding: ${SPACE_TIMES(0.5)}; `; -const StyledChartIcon = styled(IW)<{ isMatchRequirement?: boolean }>` +const StyledInlineSVGIcon = styled.img<{ isMatchRequirement?: boolean }>` + opacity: ${p => (p.isMatchRequirement ? 1 : 0.4)}; +`; + +const StyledSVGFontIcon = styled.i<{ isMatchRequirement?: boolean }>` + opacity: ${p => (p.isMatchRequirement ? 1 : 0.4)}; +`; + +const StyledChartIcon = styled(IW)` cursor: pointer; border-radius: ${BORDER_RADIUS}; - opacity: ${p => (p.isMatchRequirement ? 1 : 0.4)}; &:hover, &.active { 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 9ce29ba80..7e1de891e 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 @@ -19,14 +19,14 @@ import { Table } from 'antd'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import useMount from 'app/hooks/useMount'; -import useResizeObserver from 'app/hooks/useResizeObserver'; import ChartTools from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import { ChartConfig } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; -import { FC, memo, useRef, useState } from 'react'; +import { FC, memo, useState } from 'react'; import styled from 'styled-components/macro'; import { BORDER_RADIUS, SPACE_LG, SPACE_MD } from 'styles/StyleConstants'; +import { Debugger } from 'utils/debugger'; import Chart404Graph from './components/Chart404Graph'; import ChartTypeSelector, { ChartPresentType, @@ -35,105 +35,104 @@ import ChartTypeSelector, { const CHART_TYPE_SELECTOR_HEIGHT_OFFSET = 50; const ChartPresentPanel: FC<{ + containerHeight?: number; + containerWidth?: number; chart?: Chart; dataset?: ChartDataset; chartConfig?: ChartConfig; -}> = memo(({ chart, dataset, chartConfig }) => { - const translate = useI18NPrefix(`viz.palette.present`); - const [chartType, setChartType] = useState(ChartPresentType.GRAPH); - const panelRef = useRef<{ offsetWidth; offsetHeight }>(null); - const [chartDispatcher] = useState(() => - ChartTools.ChartIFrameContainerDispatcher.instance(), - ); - - useMount(undefined, () => { - console.debug('Disposing - Chart Container'); - ChartTools.ChartIFrameContainerDispatcher.dispose(); - }); - - const { ref: graphRef } = useResizeObserver({ - refreshMode: 'debounce', - refreshRate: 10, - }); - - const renderGraph = (containerId, chart?: Chart, chartConfig?, style?) => { - if (!chart?.isMatchRequirement(chartConfig)) { - return ; - } - return ( - !!chart && - chartDispatcher.getContainers( - containerId, - chart, - dataset, - chartConfig!, - style, - ) - ); - }; - - const renderReusableChartContainer = () => { - const style = { - width: panelRef.current?.offsetWidth, - height: - panelRef.current?.offsetHeight - CHART_TYPE_SELECTOR_HEIGHT_OFFSET, // TODO(Stephen): calculate when change chart +}> = memo( + ({ containerHeight, containerWidth, chart, dataset, chartConfig }) => { + const translate = useI18NPrefix(`viz.palette.present`); + const chartDispatcher = + ChartTools.ChartIFrameContainerDispatcher.instance(); + const [chartType, setChartType] = useState(ChartPresentType.GRAPH); + + useMount(undefined, () => { + Debugger.instance.measure(`ChartPresentPanel | Dispose Event`, () => { + ChartTools.ChartIFrameContainerDispatcher.dispose(); + }); + }); + + const renderGraph = (containerId, chart?: Chart, chartConfig?, style?) => { + if (!chart?.isMatchRequirement(chartConfig)) { + return ; + } + return ( + !!chart && + chartDispatcher.getContainers( + containerId, + chart, + dataset, + chartConfig!, + style, + ) + ); }; - const containerId = chart?.isISOContainer - ? (chart?.isISOContainer as string) - : 'container-1'; + const renderReusableChartContainer = () => { + const style = { + width: containerWidth, + height: + (containerHeight || CHART_TYPE_SELECTOR_HEIGHT_OFFSET) - + CHART_TYPE_SELECTOR_HEIGHT_OFFSET, + }; + + const containerId = chart?.isISOContainer + ? (chart?.isISOContainer as string) + : 'container-1'; + + return ( + <> + {ChartPresentType.GRAPH === chartType && ( + + {renderGraph(containerId, chart, chartConfig, style)} + + )} + {ChartPresentType.RAW === chartType && ( + + ({ + key: col.name, + title: col.name, + dataIndex: index, + }))} + bordered + /> + + )} + {ChartPresentType.SQL === chartType && ( + + {dataset?.script} + + )} + > + ); + }; - return ( - <> - {ChartPresentType.GRAPH === chartType && ( - - {renderGraph(containerId, chart, chartConfig, style)} - - )} - {ChartPresentType.RAW === chartType && ( - - ({ - key: col.name, - title: col.name, - dataIndex: index, - }))} - bordered - /> - - )} - {ChartPresentType.SQL === chartType && ( - - {dataset?.script} - - )} - > - ); - }; + const renderChartTypeSelector = () => { + return ( + + ); + }; - const renderChartTypeSelector = () => { return ( - + + {renderChartTypeSelector()} + {renderReusableChartContainer()} + ); - }; - - return ( - - {renderChartTypeSelector()} - {renderReusableChartContainer()} - - ); -}); + }, +); export default ChartPresentPanel; -const StyledChartPresentPanel = styled.div<{ ref }>` +const StyledChartPresentPanel = styled.div` display: flex; flex: 1; flex-direction: column; 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 b1276e398..86d98c7b5 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentWrapper.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentWrapper.tsx @@ -16,43 +16,73 @@ * limitations under the License. */ +import useResizeObserver from 'app/hooks/useResizeObserver'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import { ChartConfig } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; -import { FC } from 'react'; +import { FC, memo, useMemo } from 'react'; import styled from 'styled-components/macro'; import { SPACE_MD } from 'styles/StyleConstants'; import ChartGraphPanel from './ChartGraphPanel'; import ChartPresentPanel from './ChartPresentPanel'; const ChartPresentWrapper: FC<{ + containerHeight?: number; + containerWidth?: number; chart?: Chart; dataset?: ChartDataset; chartConfig?: ChartConfig; onChartChange: (c: Chart) => void; -}> = ({ chart, dataset, chartConfig, onChartChange }) => { - return ( - - - - - ); -}; +}> = memo( + ({ + containerHeight, + containerWidth, + chart, + dataset, + chartConfig, + onChartChange, + }) => { + const { ref: ChartGraphPanelRef } = useResizeObserver({ + refreshMode: 'debounce', + refreshRate: 500, + }); + + const borderWidth = useMemo(() => { + return +SPACE_MD.replace('px', ''); + }, []); + + return ( + + + + + + + ); + }, +); export default ChartPresentWrapper; -const StyledChartPresentWrapper = styled.div` +const StyledChartPresentWrapper = styled.div<{ borderWidth }>` display: flex; flex-direction: column; height: 100%; - padding: ${SPACE_MD} ${SPACE_MD} ${SPACE_MD} 0; + padding: ${p => p.borderWidth}px ${p => p.borderWidth}px + ${p => p.borderWidth}px 0; background-color: ${p => p.theme.bodyBackground}; `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/AntdTableChartAdapter.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/CurrentRangeTime.tsx similarity index 63% rename from frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/AntdTableChartAdapter.tsx rename to frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/CurrentRangeTime.tsx index cc8961daa..ad31a32c6 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/AntdTableChartAdapter.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/CurrentRangeTime.tsx @@ -16,20 +16,20 @@ * limitations under the License. */ -import { Table } from 'antd'; +import { DatePicker } from 'antd'; +import moment from 'moment'; import { FC, memo } from 'react'; +const { RangePicker } = DatePicker; -const AntdTableChartAdapter: FC<{ dataSource: []; columns: [] }> = memo( - ({ dataSource, columns, ...rest }) => { +const CurrentRangeTime: FC<{ times?: [string, string]; disabled?: boolean }> = + memo(({ times, disabled = true }) => { return ( - ); - }, -); + }); -export default AntdTableChartAdapter; +export default CurrentRangeTime; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/ExactTimeSelector.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/ExactTimeSelector.tsx new file mode 100644 index 000000000..49a649d8f --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/ExactTimeSelector.tsx @@ -0,0 +1,50 @@ +/** + * 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 { DatePicker } from 'antd'; +import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; +import { TimeFilterConditionValue } from 'app/types/ChartConfig'; +import { formatTime } from 'app/utils/time'; +import { FILTER_TIME_FORMATTER_IN_QUERY } from 'globalConstants'; +import moment from 'moment'; +import { FC, memo } from 'react'; + +const ExactTimeSelector: FC< + { + time?: TimeFilterConditionValue; + onChange: (time) => void; + } & I18NComponentProps +> = memo(({ time, i18nPrefix, onChange }) => { + const t = useI18NPrefix(i18nPrefix); + + const handleMomentTimeChange = momentTime => { + const timeStr = formatTime(momentTime, FILTER_TIME_FORMATTER_IN_QUERY); + onChange?.(timeStr); + }; + + return ( + + ); +}); + +export default ExactTimeSelector; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/MannualRangeTimeSelector.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/MannualRangeTimeSelector.tsx index 06ef747eb..303ced8ad 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/MannualRangeTimeSelector.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/MannualRangeTimeSelector.tsx @@ -17,60 +17,69 @@ */ import { Row, Space } from 'antd'; -import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import TimeConfigContext from 'app/pages/ChartWorkbenchPage/contexts/TimeConfigContext'; -import { - FilterCondition, - FilterConditionType, -} from 'app/types/ChartConfig'; -import moment from 'moment'; -import { FC, memo, useContext, useState } from 'react'; +import { I18NComponentProps } from 'app/hooks/useI18NPrefix'; +import { FilterCondition, FilterConditionType } from 'app/types/ChartConfig'; +import { formatTime, getTime } from 'app/utils/time'; +import { FILTER_TIME_FORMATTER_IN_QUERY } from 'globalConstants'; +import { FC, memo, useState } from 'react'; import ChartFilterCondition, { ConditionBuilder, } from '../../../../models/ChartFilterCondition'; +import CurrentRangeTime from './CurrentRangeTime'; import ManualSingleTimeSelector from './ManualSingleTimeSelector'; const MannualRangeTimeSelector: FC< { condition?: FilterCondition; - onFilterChange: (filter: ChartFilterCondition) => void; + onConditionChange: (filter: ChartFilterCondition) => void; } & I18NComponentProps -> = memo(({ i18nPrefix, condition, onFilterChange }) => { - const t = useI18NPrefix(i18nPrefix); - const { format } = useContext(TimeConfigContext); - const [timeRange, setTimeRange] = useState(() => { +> = memo(({ i18nPrefix, condition, onConditionChange }) => { + const [rangeTimes, setRangeTimes] = useState(() => { if (condition?.type === FilterConditionType.RangeTime) { - const startTime = moment(condition?.value?.[0]); - const endTime = moment(condition?.value?.[1]); + const startTime = condition?.value?.[0]; + const endTime = condition?.value?.[1]; return [startTime, endTime]; } return []; }); const handleTimeChange = index => time => { - timeRange[index] = time; - setTimeRange(timeRange); + rangeTimes[index] = time; + setRangeTimes(rangeTimes); const filterRow = new ConditionBuilder(condition) - .setValue((timeRange || []).map(d => d.toString())) + .setValue(rangeTimes || []) .asRangeTime(); - onFilterChange && onFilterChange(filterRow); + onConditionChange?.(filterRow); + }; + + const getRangeStringTimes = () => { + return (rangeTimes || []).map(t => { + if (Boolean(t) && typeof t === 'object' && 'unit' in t) { + const time = getTime(+(t.direction + t.amount), t.unit)( + t.unit, + t.isStart, + ); + return formatTime(time, FILTER_TIME_FORMATTER_IN_QUERY); + } + return t; + }); }; return ( - - {/* {`${t('currentTime')} : ${timeRange - ?.map(time => formatTime(time, format)) - ?.join(' - ')}`} */} - + + + void; + time?: TimeFilterConditionValue; + isStart: boolean; + onTimeChange: (time) => void; } & I18NComponentProps -> = memo(({ isStart, i18nPrefix, time, onTimeChange }) => { +> = memo(({ time, isStart, i18nPrefix, onTimeChange }) => { const t = useI18NPrefix(i18nPrefix); - const [type, setType] = useState(RelativeOrExactTime.Exact); + const [type, setType] = useState(() => { + return typeof time === 'string' + ? TimeFilterValueCategory.Exact + : TimeFilterValueCategory.Relative; + }); - const handleTimeChange = time => { - onTimeChange?.(time); + const handleTimeCategoryChange = type => { + setType(type); + if (type === TimeFilterValueCategory.Exact) { + onTimeChange?.(formatTime(moment(), FILTER_TIME_FORMATTER_IN_QUERY)); + } else if (type === TimeFilterValueCategory.Relative) { + onTimeChange?.({ unit: 'd', amount: 1, direction: '-' }); + } else { + onTimeChange?.(null); + } }; const renderTimeSelector = type => { switch (type) { - case RelativeOrExactTime.Exact: + case TimeFilterValueCategory.Exact: return ( - ); - case RelativeOrExactTime.Relative: + case TimeFilterValueCategory.Relative: return ( ); } @@ -62,12 +76,12 @@ const ManualSingleTimeSelector: FC< return ( - setType(value)}> - - {t(RelativeOrExactTime.Exact)} + + + {t(TimeFilterValueCategory.Exact)} - - {t(RelativeOrExactTime.Relative)} + + {t(TimeFilterValueCategory.Relative)} {isStart ? `${t('startTime')} : ` : `${t('endTime')} : `} @@ -79,7 +93,7 @@ const ManualSingleTimeSelector: FC< export default ManualSingleTimeSelector; const StyledManualSingleTimeSelector = styled(Space)` - & .ant-select { + & > .ant-select { width: 80px; } `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RecommendRangeTimeSelector.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RecommendRangeTimeSelector.tsx index 174b60b1e..a5df50a9c 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RecommendRangeTimeSelector.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RecommendRangeTimeSelector.tsx @@ -16,16 +16,16 @@ * limitations under the License. */ -import { Radio, Row, Space } from 'antd'; +import { Radio, Space } from 'antd'; import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import TimeConfigContext from 'app/pages/ChartWorkbenchPage/contexts/TimeConfigContext'; import { FilterCondition } from 'app/types/ChartConfig'; -import { convertRelativeTimeRange } from 'app/utils/time'; +import { recommendTimeRangeConverter } from 'app/utils/time'; import { RECOMMEND_TIME } from 'globalConstants'; -import { FC, memo, useContext, useState } from 'react'; +import { FC, memo, useMemo, useState } from 'react'; import ChartFilterCondition, { ConditionBuilder, } from '../../../../models/ChartFilterCondition'; +import CurrentRangeTime from './CurrentRangeTime'; const RecommendRangeTimeSelector: FC< { @@ -34,60 +34,57 @@ const RecommendRangeTimeSelector: FC< } & I18NComponentProps > = memo(({ i18nPrefix, condition, onConditionChange }) => { const t = useI18NPrefix(i18nPrefix); - const { format } = useContext(TimeConfigContext); - const [timeRange, setTimeRange] = useState([]); - const [relativeTime, setRelativeTime] = useState(); - - const handleChange = relativeTime => { - const timeRange = convertRelativeTimeRange(relativeTime); - setTimeRange(timeRange); + const [recommend, setRecommend] = useState( + condition?.value as string, + ); - setRelativeTime(relativeTime); + const handleChange = recommendTime => { + setRecommend(recommendTime); const filter = new ConditionBuilder(condition) - .setValue(timeRange) - .asRangeTime(); + .setValue(recommendTime) + .asRecommendTime(); onConditionChange?.(filter); }; + const rangeTimes = useMemo(() => { + return recommendTimeRangeConverter(recommend); + }, [recommend]); + return ( - - - {/* {`${t('currentTime')} : ${timeRange - ?.map(t => formatTime(t, format)) - .join(' - ')}`} */} - - handleChange(e.target?.value)} - > - - {t(RECOMMEND_TIME.TODAY)} - - {t(RECOMMEND_TIME.YESTERDAY)} - - - {t(RECOMMEND_TIME.THISWEEK)} - - - - - {t(RECOMMEND_TIME.LAST_7_DAYS)} - - - {t(RECOMMEND_TIME.LAST_30_DAYS)} - - - {t(RECOMMEND_TIME.LAST_90_DAYS)} - - - {t(RECOMMEND_TIME.LAST_1_MONTH)} - - - {t(RECOMMEND_TIME.LAST_1_YEAR)} - - - - + <> + + + handleChange(e.target?.value)} + > + + {[ + RECOMMEND_TIME.TODAY, + RECOMMEND_TIME.YESTERDAY, + RECOMMEND_TIME.THISWEEK, + ].map(time => ( + + {t(time)} + + ))} + + + {[ + RECOMMEND_TIME.LAST_7_DAYS, + RECOMMEND_TIME.LAST_30_DAYS, + RECOMMEND_TIME.LAST_90_DAYS, + RECOMMEND_TIME.LAST_1_MONTH, + RECOMMEND_TIME.LAST_1_YEAR, + ].map(time => ( + + {t(time)} + + ))} + + + + > ); }); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RelativeTimeSelector.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RelativeTimeSelector.tsx index 92c934262..6b2815037 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RelativeTimeSelector.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RelativeTimeSelector.tsx @@ -18,29 +18,37 @@ import { InputNumber, Select, Space } from 'antd'; import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import { getTime } from 'app/utils/time'; +import { TimeFilterConditionValue } from 'app/types/ChartConfig'; import { TIME_DIRECTION, TIME_UNIT_OPTIONS } from 'globalConstants'; import { unitOfTime } from 'moment'; import { FC, memo, useState } from 'react'; import styled from 'styled-components/macro'; + const RelativeTimeSelector: FC< { - isStart?: boolean; + time?: TimeFilterConditionValue; onChange: (time) => void; } & I18NComponentProps -> = memo(({ isStart, i18nPrefix, onChange }) => { +> = memo(({ time, i18nPrefix, onChange }) => { const t = useI18NPrefix(i18nPrefix); - const [amount, setAmount] = useState(0); - const [unit, setUnit] = useState('d'); - const [direction, setDirection] = useState('-'); + const [amount, setAmount] = useState(() => (time as any)?.amount || 1); + const [unit, setUnit] = useState( + () => (time as any)?.unit || 'd', + ); + const [direction, setDirection] = useState( + () => (time as any)?.direction || '-', + ); const handleTimeChange = ( unit: unitOfTime.DurationConstructor, amount: number, direction: string, ) => { - const time = getTime(+(direction + amount), unit)(unit, isStart); - onChange?.(time); + onChange?.({ + unit, + amount, + direction, + }); }; const handleUnitChange = (newUnit: unitOfTime.DurationConstructor) => { @@ -53,36 +61,40 @@ const RelativeTimeSelector: FC< handleTimeChange(unit, newAmount, direction); }; - const handleDirecitonChange = newDirection => { + const handleDirectionChange = newDirection => { setDirection(newDirection); handleTimeChange(unit, amount, newDirection); }; return ( - - - - {TIME_UNIT_OPTIONS.map(item => ( + + + {TIME_DIRECTION.map(item => ( {t(item.name)} ))} - - {TIME_DIRECTION.map(item => ( + {TIME_DIRECTION.filter(d => d.name !== 'current').find( + d => d.value === direction, + ) && ( + + )} + + {TIME_UNIT_OPTIONS.map(item => ( {t(item.name)} ))} - + ); }); export default RelativeTimeSelector; -const StyledReativeTimeSelector = styled(Space)` +const StyledRelativeTimeSelector = styled(Space)` & .ant-select, .ant-input-number { max-width: 80px; 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 a308ceb63..58212ab6a 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 @@ -16,6 +16,7 @@ * limitations under the License. */ +import CurrentRangeTime from './CurrentRangeTime'; import MannualRangeTimeSelector from './MannualRangeTimeSelector'; import ManualSingleTimeSelector from './ManualSingleTimeSelector'; import RecommendRangeTimeSelector from './RecommendRangeTimeSelector'; @@ -24,6 +25,7 @@ const TimeSelector = { MannualRangeTimeSelector, ManualSingleTimeSelector, RecommendRangeTimeSelector, + CurrentRangeTime, }; export default TimeSelector; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainer.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainer.tsx index 3ccc253a6..8bff6b5d0 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainer.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainer.tsx @@ -16,75 +16,101 @@ * limitations under the License. */ +import { + Frame, + FrameContextConsumer, +} from 'app/components/ReactFrameComponent'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import { ChartConfig } from 'app/types/ChartConfig'; -import React from 'react'; -import Frame, { FrameContextConsumer } from 'react-frame-component'; +import { FC, memo } from 'react'; import styled, { StyleSheetManager } from 'styled-components/macro'; import { isEmpty } from 'utils/object'; import ChartLifecycleAdapter from './ChartLifecycleAdapter'; -// eslint-disable-next-line import/no-webpack-loader-syntax -const antdStyles = require('!!css-loader!antd/dist/antd.min.css'); -const ChartIFrameContainer: React.FC<{ +const ChartIFrameContainer: FC<{ dataset: any; chart: Chart; config: ChartConfig; containerId?: string; - style?; -}> = props => { - // Note: manually add table css style in iframe - const isTable = props.chart?.isISOContainer === 'react-table'; - - const transformToSafeCSSProps = style => { - if (isNaN(style?.width) || isEmpty(style?.width)) { - style.width = 0; + width?: any; + height?: any; + isShown?: boolean; + widgetSpecialConfig?: any; +}> = memo(props => { + const transformToSafeCSSProps = (width, height) => { + let newStyle = { width, height }; + if (isNaN(newStyle?.width) || isEmpty(newStyle?.width)) { + newStyle.width = 0; } - if (isNaN(style?.height) || isEmpty(style?.height)) { - style.height = 0; + if (isNaN(newStyle?.height) || isEmpty(newStyle?.height)) { + newStyle.height = 0; } - return style; + return newStyle; + }; + + const render = () => { + if (!props?.chart?._useIFrame) { + return ( + + + + ); + } + + return ( + + + > + } + > + + {frameContext => ( + + + + + + )} + + + ); }; - return ( - - - - > - } - > - - {frameContext => ( - - - - - - )} - - - ); -}; + return render(); +}); export default ChartIFrameContainer; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerDispatcher.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerDispatcher.tsx index 719a689e6..523f7b69e 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerDispatcher.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerDispatcher.tsx @@ -20,7 +20,7 @@ import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import { ChartConfig } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; import { CSSProperties } from 'styled-components'; -import ChartTools from '.'; +import ChartIFrameContainer from './ChartIFrameContainer'; const DEFAULT_CONTAINER_ID = 'frame-container-1'; @@ -29,6 +29,7 @@ class ChartIFrameContainerDispatcher { private currentContainerId = DEFAULT_CONTAINER_ID; private chartContainerMap = new Map(); private chartMetadataMap = new Map(); + private editorEnv = { env: 'workbench' }; public static instance(): ChartIFrameContainerDispatcher { if (!this.dispatcher) { @@ -53,13 +54,15 @@ class ChartIFrameContainerDispatcher { this.switchContainer(containerId, chart, dataset, config); const renders: Function[] = []; this.chartContainerMap.forEach((chartRenderer: Function, key) => { + const isShown = key === this.currentContainerId; renders.push( chartRenderer .call( - null, - this.getVisibilityStyle(key === this.currentContainerId, style), + Object.create(null), + this.getVisibilityStyle(isShown, style), + isShown, ) - .apply(null, this.chartMetadataMap.get(key)), + .apply(Object.create(null), this.chartMetadataMap.get(key)), ); }); return renders; @@ -77,16 +80,20 @@ class ChartIFrameContainerDispatcher { private createNewIfNotExist(containerId: string) { if (!this.chartContainerMap.has(containerId)) { - const newContainer = style => (chart, dataset, config) => { + const newContainer = (style, isShown) => (chart, dataset, config) => { return ( - + + + ); }; this.chartContainerMap.set(containerId, newContainer); @@ -94,8 +101,8 @@ class ChartIFrameContainerDispatcher { this.currentContainerId = containerId; } - private getVisibilityStyle(isShow, style?: CSSProperties) { - return isShow + private getVisibilityStyle(isShown, style?: CSSProperties) { + return isShown ? { ...style, transform: 'none', @@ -105,7 +112,7 @@ class ChartIFrameContainerDispatcher { ...style, transform: 'translate(-9999px, -9999px)', position: 'absolute', - }; /* TODO: visibilty: 'collapse' */ + }; } } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerResourceLoader.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerResourceLoader.ts index 8b595505d..5fb643327 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerResourceLoader.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerResourceLoader.ts @@ -21,7 +21,7 @@ import { loadScript } from '../../../../../../../utils/resource'; class ChartIFrameContainerResourceLoader { private resources: string[] = []; - laodResource(doc, deps?: string[]): Promise { + loadResource(doc, deps?: string[]): Promise { const unloadedDeps = (deps || []).filter(d => !this.resources.includes(d)); return this.loadDependencies(doc, unloadedDeps); } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartLifecycleAdapter.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartLifecycleAdapter.tsx index 7db8a1889..4f353a7d3 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartLifecycleAdapter.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartLifecycleAdapter.tsx @@ -18,15 +18,14 @@ import { LoadingOutlined } from '@ant-design/icons'; import { Spin } from 'antd'; -import useMount from 'app/hooks/useMount'; +import { useFrame } from 'app/components/ReactFrameComponent'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import ChartEventBroker from 'app/pages/ChartWorkbenchPage/models/ChartEventBroker'; import { ChartConfig } from 'app/types/ChartConfig'; import { ChartLifecycle } from 'app/types/ChartLifecycle'; import React, { CSSProperties, useEffect, useRef, useState } from 'react'; -import { useFrame } from 'react-frame-component'; import styled from 'styled-components/macro'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import ChartIFrameContainerResourceLoader from './ChartIFrameContainerResourceLoader'; enum ContainerStatus { @@ -41,8 +40,17 @@ const ChartLifecycleAdapter: React.FC<{ chart: Chart; config: ChartConfig; style: CSSProperties; -}> = ({ dataset, chart, config, style }) => { - const [chartResourceLoader, setChartResourceLoader] = useState( + isShown?: boolean; + widgetSpecialConfig?: any; +}> = ({ + dataset, + chart, + config, + style, + isShown = true, + widgetSpecialConfig, +}) => { + const [chartResourceLoader] = useState( () => new ChartIFrameContainerResourceLoader(), ); const [containerStatus, setContainerStatus] = useState(ContainerStatus.INIT); @@ -50,30 +58,33 @@ const ChartLifecycleAdapter: React.FC<{ const [containerId] = useState(() => uuidv4()); const eventBrokerRef = useRef(); - useMount(() => { - setChartResourceLoader(new ChartIFrameContainerResourceLoader()); - }); - - // when chart change + /** + * Chart Mount Event + * Dependency: 'chart?.meta?.id', 'eventBrokerRef', 'isShown' + */ useEffect(() => { - if (!chart || !document || !window || !config) { - return; - } - if (containerStatus === ContainerStatus.LOADING) { + if ( + !isShown || + !chart || + !document || + !window || + !config || + containerStatus === ContainerStatus.LOADING + ) { return; } setContainerStatus(ContainerStatus.LOADING); (async () => { chartResourceLoader - .laodResource(document, chart?.getDependencies?.()) + .loadResource(document, chart?.getDependencies?.()) .then(_ => { chart.init(config); const newBrokerRef = new ChartEventBroker(); newBrokerRef.register(chart); newBrokerRef.publish( ChartLifecycle.MOUNTED, - { containerId, dataset, config }, + { containerId, dataset, config, widgetSpecialConfig }, { document, window, @@ -92,12 +103,17 @@ const ChartLifecycleAdapter: React.FC<{ return function cleanup() { setContainerStatus(ContainerStatus.INIT); eventBrokerRef?.current?.publish(ChartLifecycle.UNMOUNTED, {}); + eventBrokerRef?.current?.dispose(); }; - }, [chart?.meta?.name, eventBrokerRef]); + }, [chart?.meta?.id, eventBrokerRef, isShown]); - // when chart config or dataset change + /** + * Chart Update Event + * Dependency: 'config', 'dataset', 'widgetSpecialConfig', 'containerStatus', 'document', 'window', 'isShown' + */ useEffect(() => { if ( + !isShown || !document || !window || !config || @@ -111,20 +127,48 @@ const ChartLifecycleAdapter: React.FC<{ { dataset, config, + widgetSpecialConfig, + }, + { + document, + window, + width: style?.width, + height: style?.height, }, - { document, window }, ); - }, [config, dataset, containerStatus, document, window]); + }, [ + config, + dataset, + widgetSpecialConfig, + containerStatus, + document, + window, + isShown, + ]); - // when chart size change + /** + * Chart Resize Event + * Dependency: 'style.width', 'style.height', 'document', 'window', 'isShown' + */ useEffect(() => { - if (!style.width || !style.height) { + if ( + !isShown || + !document || + !window || + !config || + !dataset || + containerStatus !== ContainerStatus.SUCCESS + ) { return; } eventBrokerRef.current?.publish( ChartLifecycle.RESIZE, - {}, + { + dataset, + config, + widgetSpecialConfig, + }, { document, window, @@ -132,7 +176,7 @@ const ChartLifecycleAdapter: React.FC<{ height: style?.height, }, ); - }, [style.width, style.height, document, window]); + }, [style.width, style.height, document, window, isShown]); return ( `${fontSize}px`); +Quill.register(size, true); + +const font = Quill.import('attributors/style/font'); +font.whitelist = FONT_FAMILIES.map(font => font.value); +Quill.register(font, true); + +const MenuItem = Menu.Item; + +const ChartRichTextAdapter: FC<{ + dataList: any[]; + id: string; + isEditing?: boolean; + initContent: string | undefined; + onChange: (delta: string | undefined) => void; +}> = memo(({ dataList, id, isEditing = false, initContent, onChange }) => { + const [containerId, setContainerId] = useState(); + const [quillModules, setQuillModules] = useState(null); + const [visible, setVisible] = useState(false); + const [quillValue, setQuillValue] = useState(''); + const quillRef = useRef(null); + const quillEditRef = useRef(null); + const [translate, setTranslate] = useState(''); + + useEffect(() => { + const value = (initContent && JSON.parse(initContent)) || ''; + setQuillValue(value); + }, [initContent]); + + useEffect(() => { + if (id) { + const newId = `rich-text-${id}`; + setContainerId(newId); + const modules = { + toolbar: { + container: isEditing ? `#${newId}` : null, + handlers: {}, + }, + calcfield: {}, + imageDrop: true, + }; + setQuillModules(modules); + } + }, [id, isEditing]); + + const debouncedDataChange = useMemo( + () => + debounce(value => { + onChange?.(value); + }, 300), + [onChange], + ); + + const quillChange = useCallback(() => { + if (quillEditRef.current && quillEditRef.current?.getEditor()) { + const contents = quillEditRef.current!.getEditor().getContents(); + setQuillValue(contents); + contents && debouncedDataChange(JSON.stringify(contents)); + } + }, [debouncedDataChange]); + + useEffect(() => { + if (typeof quillValue !== 'string') { + const quill = Object.assign({}, quillValue); + const ops = quill.ops?.concat().map(item => { + let insert = item.insert; + if (typeof insert !== 'string') { + if (insert.hasOwnProperty('calcfield')) { + const name = insert.calcfield?.name; + const config = name + ? dataList.find(items => items.name === name) + : null; + insert = config?.value || ''; + } + } + return { ...item, insert }; + }); + setTranslate(ops?.length ? { ...quill, ops } : ''); + } else { + setTranslate(quillValue); + } + }, [quillValue, dataList, setTranslate]); + + useLayoutEffect(() => { + if (quillEditRef.current) { + new QuillMarkdown(quillEditRef.current.getEditor(), MarkdownOptions); + } + }, [quillModules]); + + const reactQuillView = useMemo( + () => + (!isEditing || visible) && ( + + ), + [translate, quillRef, isEditing, visible], + ); + + const selectField = useCallback( + (field: any) => () => { + if (quillEditRef.current) { + const quill = quillEditRef.current.getEditor(); + if (field) { + const text = `[${field.name}]`; + quill.getModule('calcfield').insertItem( + { + denotationChar: '', + id: field.id, + name: field.name, + value: field.value, + text, + }, + true, + ); + setImmediate(() => { + setQuillValue( + quillEditRef.current?.getEditor().getContents() || '', + ); + }); + } + } + }, + [quillEditRef], + ); + + const fieldItems = useMemo(() => { + return dataList?.length ? ( + + {dataList.map(fieldName => ( + + {fieldName.name} + + ))} + + ) : ( + + 暂无可用字段 + + ); + }, [dataList, selectField]); + + const toolbar = useMemo( + () => ( + + + + {FONT_FAMILIES.map(font => ( + + {font.name} + + ))} + + + {FONT_SIZES.map(size => ( + {`${size}px`} + ))} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + [containerId, fieldItems], + ); + + const reactQuillEdit = useMemo( + () => + isEditing && ( + <> + {toolbar} + + + { + setVisible(true); + }} + type="primary" + > + 预览 + + + > + ), + [quillModules, quillValue, isEditing, toolbar, quillChange], + ); + + const ssp = e => { + e.stopPropagation(); + }; + + return ( + + + {quillModules && reactQuillEdit} + {quillModules && !isEditing && reactQuillView} + + { + setVisible(false); + }} + > + {isEditing && reactQuillView} + + + ); +}); +export default ChartRichTextAdapter; + +const QuillBox = styled.div` + width: 100%; + height: 100%; + flex-direction: column; + display: flex; + .react-quill { + flex: 1; + overflow-y: auto; + } + .react-quill-view { + flex: 1; + overflow-y: auto; + } +`; +const TextWrap = styled.div` + width: 100%; + height: 100%; + + & .ql-snow { + border: none; + } + + & .ql-container.ql-snow { + border: none; + } + + & .selectLink { + height: 24px; + width: 28px; + padding: 0 5px; + display: inline-block; + color: black; + + i { + font-size: 16px; + } + } + + & .selectLink:hover { + color: #06c; + } +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactChartAdapter.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactLifecycleAdapter.ts similarity index 79% rename from frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactChartAdapter.ts rename to frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactLifecycleAdapter.ts index f040aa3bd..ec83ee8d8 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactChartAdapter.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactLifecycleAdapter.ts @@ -19,7 +19,7 @@ import React from 'react'; import ReactDom from 'react-dom'; -interface ReactChartAdapterProps { +interface ReactLifecycleAdapterProps { init: (component: React.Component | Function) => void; mounted: (container, options?, context?) => any; updated: (options: any, context?) => any; @@ -27,11 +27,17 @@ interface ReactChartAdapterProps { resize: (opt: any) => void; } -export default class ReactChartAdapter implements ReactChartAdapterProps { +export default class ReactLifecycleAdapter + implements ReactLifecycleAdapterProps +{ private domContainer; private reactComponent; private externalLibs; + constructor(componentWrapper) { + this.reactComponent = componentWrapper; + } + public init(component) { this.reactComponent = component; } @@ -43,14 +49,14 @@ export default class ReactChartAdapter implements ReactChartAdapterProps { public mounted(container, options?, context?) { this.domContainer = container; return ReactDom.render( - React.createElement(this.getComponent(), options), + React.createElement(this.getComponent(), options, context), this.domContainer, ); } public updated(options, context?) { return ReactDom.render( - React.createElement(this.getComponent(), options), + React.createElement(this.getComponent(), options, context), this.domContainer, ); } @@ -59,9 +65,9 @@ export default class ReactChartAdapter implements ReactChartAdapterProps { ReactDom.unmountComponentAtNode(this.domContainer); } - public resize(opt: any) { + public resize(options, context?) { return ReactDom.render( - React.createElement(this.getComponent(), opt), + React.createElement(this.getComponent(), options, context), this.domContainer, ); } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/CalcFieldBlot.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/CalcFieldBlot.ts new file mode 100644 index 000000000..a03dee758 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/CalcFieldBlot.ts @@ -0,0 +1,64 @@ +/** + * 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 Quill from 'quill'; + +const Embed = Quill.import('blots/embed'); +class CalcFieldBlot extends Embed { + static blotName = 'calcfield'; + static tagName = 'span'; + static className = 'calcfield'; + + static create(data) { + const node = super.create(); + node.addEventListener( + 'click', + e => { + const event = new Event('mention-clicked', { + bubbles: true, + cancelable: true, + }); + // @ts-ignore + event.value = data; + // @ts-ignore + event.event = e; + window.dispatchEvent(event); + e.preventDefault(); + }, + false, + ); + const denotationChar = document.createElement('span'); + denotationChar.className = 'ql-calcfield-denotation-char'; + denotationChar.innerHTML = data.denotationChar; + node.appendChild(denotationChar); + node.innerHTML += data.text; + return CalcFieldBlot.setDataValues(node, data); + } + + static setDataValues(element, data) { + const domNode = element; + Object.keys(data).forEach(key => { + domNode.dataset[key] = data[key]; + }); + return domNode; + } + + static value(domNode) { + return domNode.dataset; + } +} +export default CalcFieldBlot; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/index.ts new file mode 100644 index 000000000..5f9bc993c --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/index.ts @@ -0,0 +1,416 @@ +//@ts-nocheck +import Quill from 'quill'; +import CalcFieldBlot from './CalcFieldBlot'; + +Quill.register(CalcFieldBlot); +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError('Cannot call a class as a function'); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ('value' in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; +} + +function _extends() { + _extends = + Object.assign || + function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; + }; + + return _extends.apply(this, arguments); +} + +function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === 'string') return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === 'Object' && o.constructor) n = o.constructor.name; + if (n === 'Map' || n === 'Set') return Array.from(o); + if (n === 'Arguments' || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) + return _arrayLikeToArray(o, minLen); +} + +function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; + + return arr2; +} + +function _createForOfIteratorHelper(o, allowArrayLike) { + var it; + + if (typeof Symbol === 'undefined' || o[Symbol.iterator] == null) { + if ( + Array.isArray(o) || + (it = _unsupportedIterableToArray(o)) || + (allowArrayLike && o && typeof o.length === 'number') + ) { + if (it) o = it; + var i = 0; + + var F = function () {}; + + return { + s: F, + n: function () { + if (i >= o.length) + return { + done: true, + }; + return { + done: false, + value: o[i++], + }; + }, + e: function (e) { + throw e; + }, + f: F, + }; + } + + throw new TypeError( + 'Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); + } + + var normalCompletion = true, + didErr = false, + err; + return { + s: function () { + it = o[Symbol.iterator](); + }, + n: function () { + var step = it.next(); + normalCompletion = step.done; + return step; + }, + e: function (e) { + didErr = true; + err = e; + }, + f: function () { + try { + if (!normalCompletion && it.return != null) it.return(); + } finally { + if (didErr) throw err; + } + }, + }; +} + +var Keys = { + TAB: 9, + ENTER: 13, + ESCAPE: 27, + UP: 38, + DOWN: 40, +}; + +function getFieldCharIndex(text, numberFieldDenotationChars) { + return numberFieldDenotationChars.reduce( + function (prev, numberFieldChar) { + var numberFieldCharIndex = text.lastIndexOf(numberFieldChar); + + if (numberFieldCharIndex > prev.numberFieldCharIndex) { + return { + numberFieldChar: numberFieldChar, + numberFieldCharIndex: numberFieldCharIndex, + }; + } + + return { + numberFieldChar: prev.numberFieldChar, + numberFieldCharIndex: prev.numberFieldCharIndex, + }; + }, + { + numberFieldChar: null, + numberFieldCharIndex: -1, + }, + ); +} + +function hasValidChars(text, allowedChars) { + return allowedChars.test(text); +} + +function hasValidFieldCharIndex(numberFieldCharIndex, text, isolateChar) { + if (numberFieldCharIndex > -1) { + if ( + isolateChar && + !( + numberFieldCharIndex === 0 || + !!text[numberFieldCharIndex - 1].match(/\s/g) + ) + ) { + return false; + } + + return true; + } + + return false; +} + +var CalcField = (function () { + function CalcField(quill, options) { + var _this = this; + + _classCallCheck(this, CalcField); + + this.isOpen = false; + this.itemIndex = 0; + this.numberFieldCharPos = null; + this.cursorPos = null; + this.values = []; + this.suspendMouseEnter = false; //this token is an object that may contains one key "abandoned", set to + //true when the previous source call should be ignored in favor or a + //more recent execution. This token will be null unless a source call + //is in progress. + + this.existingSourceExecutionToken = null; + this.quill = quill; + this.options = { + source: null, + numberFieldDenotationChars: ['@'], + showDenotationChar: true, + allowedChars: /^[a-zA-Z0-9_]*$/, + minChars: 0, + maxChars: 31, + offsetTop: 2, + offsetLeft: 0, + isolateCharacter: false, + fixFieldsToQuill: false, + positioningStrategy: 'normal', + defaultMenuOrientation: 'bottom', + blotName: 'calcfield', + dataAttributes: [ + 'id', + 'value', + 'denotationChar', + 'link', + 'target', + 'disabled', + 'viewId', + 'model', + 'text', + 'agg', + 'size', + 'font-size', + ], + linkTarget: '_blank', + + // Style options + spaceAfterInsert: true, + selectKeys: [Keys.ENTER], + }; + _extends(this.options, options, { + dataAttributes: Array.isArray(options.dataAttributes) + ? this.options.dataAttributes.concat(options.dataAttributes) + : this.options.dataAttributes, + }); //create calcfield container + + quill.on('text-change', this.onTextChange.bind(this)); + quill.on('selection-change', this.onSelectionChange.bind(this)); //Pasting doesn't fire selection-change after the pasted text is + //inserted, so here we manually trigger one + + quill.container.addEventListener('paste', function () { + setTimeout(function () { + var range = quill.getSelection(); + _this.onSelectionChange(range); + }); + }); + + var _iterator = _createForOfIteratorHelper(this.options.selectKeys), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done; ) { + var selectKey = _step.value; + quill.keyboard.addBinding({ + key: selectKey, + }); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + } + + _createClass(CalcField, [ + { + key: 'insertItem', + value: function insertItem(data, programmaticInsert) { + var render = data; + + if (render === null) { + return; + } + + if (!this.options.showDenotationChar) { + render.denotationChar = ''; + } + + var insertAtPos; + + if (!programmaticInsert) { + insertAtPos = this.numberFieldCharPos; + this.quill.deleteText( + this.numberFieldCharPos, + this.cursorPos - this.numberFieldCharPos, + Quill.sources.USER, + ); + } else { + insertAtPos = this.cursorPos; + } + + this.quill.insertEmbed( + insertAtPos, + this.options.blotName, + render, + Quill.sources.USER, + ); + + if (this.options.spaceAfterInsert) { + this.quill.insertText(insertAtPos + 1, ' ', Quill.sources.USER); // setSelection here sets cursor position + + this.quill.setSelection(insertAtPos + 2, Quill.sources.USER); + } else { + this.quill.setSelection(insertAtPos + 1, Quill.sources.USER); + } + }, + }, + { + key: 'getTextBeforeCursor', + value: function getTextBeforeCursor() { + var startPos = Math.max(0, this.cursorPos - this.options.maxChars); + var textBeforeCursorPos = this.quill.getText( + startPos, + this.cursorPos - startPos, + ); + return textBeforeCursorPos; + }, + }, + { + key: 'onSomethingChange', + value: function onSomethingChange() { + var _this5 = this; + + var range = this.quill.getSelection(); + if (range == null) return; + this.cursorPos = range.index; + var textBeforeCursor = this.getTextBeforeCursor(); + + var _getFieldCharIndex = getFieldCharIndex( + textBeforeCursor, + this.options.numberFieldDenotationChars, + ), + numberFieldChar = _getFieldCharIndex.numberFieldChar, + numberFieldCharIndex = _getFieldCharIndex.numberFieldCharIndex; + + if ( + hasValidFieldCharIndex( + numberFieldCharIndex, + textBeforeCursor, + this.options.isolateCharacter, + ) + ) { + var numberFieldCharPos = + this.cursorPos - (textBeforeCursor.length - numberFieldCharIndex); + this.numberFieldCharPos = numberFieldCharPos; + var textAfter = textBeforeCursor.substring( + numberFieldCharIndex + numberFieldChar.length, + ); + + if ( + textAfter.length >= this.options.minChars && + hasValidChars(textAfter, this.getAllowedCharsRegex(numberFieldChar)) + ) { + if (this.existingSourceExecutionToken) { + this.existingSourceExecutionToken.abandoned = true; + } + + var sourceRequestToken = { + abandoned: false, + }; + this.existingSourceExecutionToken = sourceRequestToken; + this.options.source( + textAfter, + function (data, searchTerm) { + if (sourceRequestToken.abandoned) { + return; + } + + _this5.existingSourceExecutionToken = null; + }, + numberFieldChar, + ); + } else { + } + } else { + } + }, + }, + { + key: 'getAllowedCharsRegex', + value: function getAllowedCharsRegex(denotationChar) { + if (this.options.allowedChars instanceof RegExp) { + return this.options.allowedChars; + } else { + return this.options.allowedChars(denotationChar); + } + }, + }, + { + key: 'onTextChange', + value: function onTextChange(delta, oldDelta, source) { + if (source === 'user') { + this.onSomethingChange(); + } + }, + }, + { + key: 'onSelectionChange', + value: function onSelectionChange(range) { + if (range && range.length === 0) { + this.onSomethingChange(); + } + }, + }, + ]); + + return CalcField; +})(); + +Quill.register('modules/calcfield', CalcField); +export default CalcField; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/index.ts index 63bdc3cb4..4d542b67f 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/index.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/index.ts @@ -22,7 +22,7 @@ import ChartIFrameContainerResourceLoader from './ChartIFrameContainerResourceLo import ChartPluginLoader from './ChartPluginLoader'; const ChartTools = { - ChartIFrame: ChartIFrameContainer, + ChartIFrameContainer, ChartPluginLoader, ChartIFrameContainerDispatcher, ChartIFrameContainerResourceLoader, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx index 83f5e6f35..539a9ae13 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx @@ -16,6 +16,7 @@ * limitations under the License. */ +import ChartAggregationContext from 'app/pages/ChartWorkbenchPage/contexts/ChartAggregationContext'; import ChartDatasetContext from 'app/pages/ChartWorkbenchPage/contexts/ChartDatasetContext'; import ChartDataViewContext from 'app/pages/ChartWorkbenchPage/contexts/ChartDataViewContext'; import TimeConfigContext from 'app/pages/ChartWorkbenchPage/contexts/TimeConfigContext'; @@ -38,10 +39,12 @@ const ChartWorkbench: FC<{ dataview?: ChartDataView; chartConfig?: ChartConfig; chart?: Chart; + aggregation?: boolean; header?: { name?: string; onSaveChart?: () => void; onGoBack?: () => void; + onChangeAggregation?: (state: boolean) => void; }; onChartChange: (c: Chart) => void; onChartConfigChange: (type, payload) => void; @@ -52,6 +55,7 @@ const ChartWorkbench: FC<{ dataview, chartConfig, chart, + aggregation, header, onChartChange, onChartConfigChange, @@ -59,34 +63,36 @@ const ChartWorkbench: FC<{ }) => { const language = useSelector(languageSelector); const dateFormat = useSelector(dateFormatSelector); - return ( - - - - - {header && ( - - )} - - - - - - - + + + + + + {header && ( + + )} + + + + + + + + ); }, (prev, next) => diff --git a/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartAggregationContext.ts b/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartAggregationContext.ts new file mode 100644 index 000000000..42f99f573 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartAggregationContext.ts @@ -0,0 +1,27 @@ +/** + * 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 { createContext } from 'react'; + +const ChartAggregationContext = createContext<{ + aggregation: boolean | undefined; +}>({ + aggregation: true, +}); + +export default ChartAggregationContext; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/Chart.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/Chart.ts index 1cd78c42a..d90b8c1bc 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/Chart.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/Chart.ts @@ -38,6 +38,7 @@ class Chart extends DatartChartBase { dependency: string[] = []; isISOContainer: boolean | string = false; + _useIFrame: boolean = true; _state: ChartStatus = 'init'; _stateHistory: ChartStatus[] = []; _hooks?: ChartEventBroker; @@ -55,13 +56,10 @@ class Chart extends DatartChartBase { constructor(id: string, name: string, icon?: string, requirements?: []) { super(); - const fontIcon = `iconfont icon-${ - !icon ? 'fsux_tubiao_zhuzhuangtu1' : icon - }`; this.meta = { id, name, - icon: fontIcon, + icon: icon, requirements, }; this.state = 'init'; @@ -118,7 +116,7 @@ class Chart extends DatartChartBase { return series?.data?.valueColName || series.seriesName; } - private getValue( + protected getValue( configs: ChartStyleSectionConfig[] = [], paths?: string[], targetKey?, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartEventBroker.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartEventBroker.ts index 4d50a05fc..88f272918 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartEventBroker.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartEventBroker.ts @@ -17,6 +17,7 @@ */ import { ChartLifecycle } from 'app/types/ChartLifecycle'; +import { Debugger } from 'utils/debugger'; import Chart from './Chart'; type BrokerContext = { @@ -28,7 +29,6 @@ type BrokerContext = { type HooksEvent = 'mounted' | 'updated' | 'resize' | 'unmount'; -// TODO(Stephen): remove to Chart Tool Folder class ChartEventBroker { private _listeners: Map = new Map(); private _chart?: Chart; @@ -49,6 +49,7 @@ class ChartEventBroker { if (!this._listeners.has(event) || !this._listeners.get(event)) { return; } + this.invokeEvent(event, options, context); } @@ -83,9 +84,15 @@ class ChartEventBroker { } } - private safeInvoke(event, options, context) { + private safeInvoke(event: HooksEvent, options: any, context?: BrokerContext) { try { - this._listeners.get(event)?.call?.(this._chart, options, context); + Debugger.instance.measure( + `ChartEventBroker | ${event} `, + () => { + this._listeners.get(event)?.call?.(this._chart, options, context); + }, + false, + ); } catch (e) { console.error(`ChartEventBroker | ${event} exception ----> `, e); } finally { diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts index 409278085..d62ead2e4 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts @@ -162,8 +162,8 @@ export class ConditionBuilder { return this.condition; } - asRelativeTime(name?, sqlType?) { - this.condition.type = FilterConditionType.RelativeTime; + asRecommendTime(name?, sqlType?) { + this.condition.type = FilterConditionType.RecommendTime; this.condition.operator = FilterSqlOperator.Equal; this.condition.name = name || this.condition.name; this.condition.visualType = sqlType || this.condition.visualType; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartHttpRequest.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartHttpRequest.ts index 717fe4785..08829f946 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartHttpRequest.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartHttpRequest.ts @@ -17,17 +17,23 @@ */ import { ChartDatasetPageInfo } from 'app/types/ChartDataset'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { getStyleValue } from 'app/utils/chartHelper'; -import { formatTime } from 'app/utils/time'; +import { + formatTime, + getTime, + recommendTimeRangeConverter, +} from 'app/utils/time'; import { FILTER_TIME_FORMATTER_IN_QUERY } from 'globalConstants'; -import { IsKeyIn } from 'utils/object'; +import { isEmptyArray, IsKeyIn } from 'utils/object'; import { AggregateFieldActionType, ChartDataSectionConfig, ChartDataSectionField, ChartDataSectionType, ChartStyleSectionConfig, - FilterValueOption, + FilterConditionType, + RelationFilterValue, SortActionType, } from '../../../types/ChartConfig'; import ChartDataView from '../../../types/ChartDataView'; @@ -42,7 +48,11 @@ export type ChartRequest = { functionColumns?: Array<{ alias: string; snippet: string }>; limit?: any; nativeQuery?: boolean; - orders: Array<{ column: string; operator: SortActionType }>; + orders: Array<{ + column: string; + operator: SortActionType; + aggOperator?: AggregateFieldActionType; + }>; pageInfo?: ChartDatasetPageInfo; columns?: string[]; script?: boolean; @@ -87,6 +97,8 @@ export class ChartDataRequestBuilder { pageInfo: ChartDatasetPageInfo; dataView: ChartDataView; script: boolean; + aggregation?: boolean; + private extraSorters: ChartRequest['orders'] = []; constructor( dataView: ChartDataView, @@ -94,20 +106,25 @@ export class ChartDataRequestBuilder { settingConfigs?: ChartStyleSectionConfig[], pageInfo?: ChartDatasetPageInfo, script?: boolean, + aggregation?: boolean, ) { this.dataView = dataView; this.chartDataConfigs = dataConfigs || []; this.charSettingConfigs = settingConfigs || []; this.pageInfo = pageInfo || {}; this.script = script || false; + this.aggregation = aggregation; } - private buildAggregators() { const aggColumns = this.chartDataConfigs.reduce( (acc, cur) => { if (!cur.rows) { return acc; } + + if (this.aggregation === false) { + return acc; + } if ( cur.type === ChartDataSectionType.AGGREGATE || cur.type === ChartDataSectionType.SIZE || @@ -115,6 +132,17 @@ export class ChartDataRequestBuilder { ) { return acc.concat(cur.rows); } + + if ( + cur.type === ChartDataSectionType.MIXED && + cur.rows?.findIndex( + v => v.type === ChartDataViewFieldType.NUMERIC, + ) !== -1 + ) { + return acc.concat( + cur.rows.filter(v => v.type === ChartDataViewFieldType.NUMERIC), + ); + } return acc; }, [], @@ -148,61 +176,80 @@ export class ChartDataRequestBuilder { return true; }) .map(col => col); + return this.normalizeFilters(fields); + } - const _transformToRequest = (fields: ChartDataSectionField[]) => { - const _convertTime = (visualType, value) => { - if (visualType !== 'DATE') { - return value; - } - return formatTime(value, FILTER_TIME_FORMATTER_IN_QUERY); - }; + private normalizeFilters = (fields: ChartDataSectionField[]) => { + const _timeConverter = (visualType, value) => { + if (visualType !== 'DATE') { + return value; + } + if (Boolean(value) && typeof value === 'object' && 'unit' in value) { + const time = getTime(+(value.direction + value.amount), value.unit)( + value.unit, + value.isStart, + ); + return formatTime(time, FILTER_TIME_FORMATTER_IN_QUERY); + } + return formatTime(value, FILTER_TIME_FORMATTER_IN_QUERY); + }; - const convertValues = (field: ChartDataSectionField) => { - if (Array.isArray(field.filter?.condition?.value)) { - return field.filter?.condition?.value - .map(v => { - if (IsKeyIn(v as FilterValueOption, 'key')) { - const listItem = v as FilterValueOption; - if (!listItem.isSelected) { - return undefined; - } - return { - value: listItem.key, - valueType: field.type, - }; + const _transformFieldValues = (field: ChartDataSectionField) => { + const conditionValue = field.filter?.condition?.value; + if (!conditionValue) { + return null; + } + if (Array.isArray(conditionValue)) { + return conditionValue + .map(v => { + if (IsKeyIn(v as RelationFilterValue, 'key')) { + const listItem = v as RelationFilterValue; + if (!listItem.isSelected) { + return undefined; } return { - value: _convertTime(field.filter?.condition?.visualType, v), + value: listItem.key, valueType: field.type, }; - }) - .filter(Boolean) as any[]; - } - const v = field.filter?.condition?.value; - if (!v) { - return null; - } - return [ - { - value: _convertTime(field.filter?.condition?.visualType, v), - valueType: field.type, - }, - ]; - }; - - return fields.map(field => ({ - aggOperator: - field.aggregate === AggregateFieldActionType.NONE - ? null - : field.aggregate, - column: field.colName, - sqlOperator: field.filter?.condition?.operator!, - values: convertValues(field) || [], - })); + } else { + return { + value: _timeConverter(field.filter?.condition?.visualType, v), + valueType: field.type, + }; + } + }) + .filter(Boolean) as any[]; + } + if ( + field?.filter?.condition?.type === FilterConditionType.RecommendTime + ) { + const timeRange = recommendTimeRangeConverter(conditionValue); + return (timeRange || []).map(t => ({ + value: t, + valueType: field.type, + })); + } + return [ + { + value: _timeConverter( + field.filter?.condition?.visualType, + conditionValue, + ), + valueType: field.type, + }, + ]; }; - return _transformToRequest(fields); - } + return fields.map(field => ({ + aggOperator: + field.aggregate === AggregateFieldActionType.NONE + ? null + : field.aggregate, + column: field.colName, + sqlOperator: field.filter?.condition?.operator!, + values: _transformFieldValues(field) || [], + })); + }; private buildOrders() { const sortColumns = this.chartDataConfigs @@ -212,7 +259,8 @@ export class ChartDataRequestBuilder { } if ( cur.type === ChartDataSectionType.GROUP || - cur.type === ChartDataSectionType.AGGREGATE + cur.type === ChartDataSectionType.AGGREGATE || + cur.type === ChartDataSectionType.MIXED ) { return acc.concat(cur.rows); } @@ -224,38 +272,41 @@ export class ChartDataRequestBuilder { [SortActionType.ASC, SortActionType.DESC].includes(col?.sort?.type), ); - return sortColumns.map(aggCol => ({ + const originalSorters = sortColumns.map(aggCol => ({ column: aggCol.colName, operator: aggCol.sort?.type!, - aggOperator: aggCol.aggregate!, + aggOperator: aggCol.aggregate, })); - } - - private buildLimit() { - const settingStyles = this.charSettingConfigs; - return getStyleValue(settingStyles, ['cache', 'panel', 'displayCount']); - } - private buildNativeQuery() { - const settingStyles = this.charSettingConfigs; - return getStyleValue(settingStyles, ['cache', 'panel', 'enableRaw']); + return originalSorters + .reduce((acc, cur) => { + const uniqSorter = sorter => + `${sorter.column}-${ + sorter.aggOperator?.length > 0 ? sorter.aggOperator : '' + }`; + const newSorter = this.extraSorters?.find( + extraSorter => uniqSorter(extraSorter) === uniqSorter(cur), + ); + if (newSorter) { + return acc; + } + return acc.concat([cur]); + }, []) + .concat(this.extraSorters as []) + .filter(sorter => Boolean(sorter?.operator)); } private buildPageInfo() { const settingStyles = this.charSettingConfigs; + const pageSize = getStyleValue(settingStyles, ['paging', 'pageSize']); const enablePaging = getStyleValue(settingStyles, [ 'paging', 'enablePaging', ]); - const pageSize = getStyleValue(settingStyles, ['paging', 'pageSize']); - if (!enablePaging) { - return { - pageSize: Number.MAX_SAFE_INTEGER, - }; - } return { + countTotal: !!enablePaging, pageNo: this.pageInfo?.pageNo, - pageSize: pageSize || 10, + pageSize: pageSize || 1000, }; } @@ -278,9 +329,20 @@ export class ChartDataRequestBuilder { if (!cur.rows) { return acc; } - if (cur.type === ChartDataSectionType.MIXED) { - return acc.concat(cur.rows); + + if (this.aggregation === false) { + if ( + cur.type === ChartDataSectionType.GROUP || + cur.type === ChartDataSectionType.COLOR || + cur.type === ChartDataSectionType.AGGREGATE || + cur.type === ChartDataSectionType.SIZE || + cur.type === ChartDataSectionType.INFO || + cur.type === ChartDataSectionType.MIXED + ) { + return acc.concat(cur.rows); + } } + return acc; }, [], @@ -289,7 +351,14 @@ export class ChartDataRequestBuilder { } private buildViewConfigs() { - return transformToViewConfig(this.dataView?.view?.config); + return transformToViewConfig(this.dataView?.config); + } + + public addExtraSorters(sorters: ChartRequest['orders']) { + if (!isEmptyArray(sorters)) { + this.extraSorters = this.extraSorters.concat(sorters!); + } + return this; } public build(): ChartRequest { @@ -304,10 +373,6 @@ export class ChartDataRequestBuilder { columns: this.buildSelectColumns(), script: this.script, ...this.buildViewConfigs(), - // expired: 0, - // flush: false, - // limit: this.buildLimit(), - // nativeQuery: this.buildNativeQuery(), }; } @@ -317,12 +382,32 @@ export class ChartDataRequestBuilder { if (!cur.rows) { return acc; } + if (this.aggregation === false) { + return acc; + } if ( cur.type === ChartDataSectionType.GROUP || cur.type === ChartDataSectionType.COLOR ) { return acc.concat(cur.rows); } + if ( + cur.type === ChartDataSectionType.MIXED && + !cur.rows?.every( + v => + v.type !== ChartDataViewFieldType.DATE && + v.type !== ChartDataViewFieldType.STRING, + ) + ) { + //zh: 判断数据中是否含有 DATE 和 STRING 类型 en: Determine whether the data contains DATE and STRING types + return acc.concat( + cur.rows.filter( + v => + v.type === ChartDataViewFieldType.DATE || + v.type === ChartDataViewFieldType.STRING, + ), + ); + } return acc; }, [], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts index 6e080c6e4..a56ccf451 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts @@ -19,6 +19,7 @@ import WidgetPlugins from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph'; import ChartTools from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools'; import { getChartPluginPaths } from 'app/utils/fetch'; +import { Debugger } from 'utils/debugger'; import { CloneValueDeep } from 'utils/object'; import Chart from './Chart'; @@ -40,12 +41,13 @@ const { RoseChart, ScoreChart, MingXiTableChart, - FenZuTableChart, NormalOutlineMapChart, WordCloudChart, ScatterOutlineMapChart, BasicGaugeChart, WaterfallChart, + BasicRichText, + PivotSheetChart, } = WidgetPlugins; class ChartManager { @@ -66,7 +68,9 @@ class ChartManager { return; } const pluginsPaths = await getChartPluginPaths(); - return await this._loadCustomizeCharts(pluginsPaths); + Debugger.instance.measure('Plugin Charts | ', async () => { + await this._loadCustomizeCharts(pluginsPaths); + }); } public getAllCharts(): Chart[] { @@ -99,8 +103,8 @@ class ChartManager { private _basicCharts(): Chart[] { return [ - new FenZuTableChart(), new MingXiTableChart(), + new PivotSheetChart(), new ScoreChart(), new ClusterColumnChart(), new ClusterBarChart(), @@ -122,6 +126,7 @@ class ChartManager { new NormalOutlineMapChart(), new ScatterOutlineMapChart(), new BasicGaugeChart(), + new BasicRichText(), ]; } } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts b/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts index 50a68f220..71907a5b3 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts @@ -72,6 +72,7 @@ export type BackendChartConfig = { chartConfig: string; chartGraphId: string; computedFields: ChartDataViewMeta[]; + aggregation: boolean; }; export type WorkbenchState = { @@ -84,6 +85,7 @@ export type WorkbenchState = { shadowChartConfig?: ChartConfig; backendChart?: BackendChart; backendChartId?: string; + aggregation?: boolean; }; const initState: WorkbenchState = { @@ -137,6 +139,11 @@ export const shadowChartConfigSelector = createSelector( wb => wb.shadowChartConfig, ); +export const aggregationSelector = createSelector( + workbenchSelector, + wb => wb.aggregation, +); + // Effects export const initWorkbenchAction = createAsyncThunk( 'workbench/initWorkbenchAction', @@ -245,24 +252,34 @@ export const updateChartConfigAndRefreshDatasetAction = createAsyncThunk( export const refreshDatasetAction = createAsyncThunk( 'workbench/refreshDatasetAction', - async (arg: { pageInfo? }, thunkAPI) => { + async ( + arg: { + pageInfo?; + sorter?: { column: string; operator: string; aggOperator?: string }; + }, + thunkAPI, + ) => { try { const state = thunkAPI.getState() as any; const workbenchState = state.workbench as typeof initState; + if (!workbenchState.currentDataView?.id) { return; } + const builder = new ChartDataRequestBuilder( { ...workbenchState.currentDataView, - view: workbenchState?.backendChart?.view, }, workbenchState.chartConfig?.datas, workbenchState.chartConfig?.settings, arg?.pageInfo, true, + workbenchState.aggregation, ); - const requestParams = builder.build(); + const requestParams = builder + .addExtraSorters(arg?.sorter ? [arg?.sorter as any] : []) + .build(); thunkAPI.dispatch(fetchDataSetAction(requestParams)); } catch (error) { return rejectHandle(error, thunkAPI.rejectWithValue); @@ -270,6 +287,36 @@ export const refreshDatasetAction = createAsyncThunk( }, ); +export const updateRichTextAction = createAsyncThunk( + 'workbench/updateRichTextAction', + async (delta: string | undefined, thunkAPI) => { + try { + const state = thunkAPI.getState() as any; + const workbenchState = state.workbench as typeof initState; + if (!workbenchState.currentDataView?.id) { + return; + } + await thunkAPI.dispatch( + workbenchSlice.actions.updateChartConfig({ + type: 'style', + payload: { + ancestors: [0, 0], + value: { + label: 'delta.richText', + key: 'richText', + default: '', + comType: 'input', + value: delta, + }, + }, + }), + ); + } catch (error) { + return rejectHandle(error, thunkAPI.rejectWithValue); + } + }, +); + export const fetchChartAction = createAsyncThunk( 'workbench/fetchChartAction', async (arg: { chartId?: string; backendChart?: BackendChart }, thunkAPI) => { @@ -291,7 +338,7 @@ export const fetchChartAction = createAsyncThunk( export const updateChartAction = createAsyncThunk( 'workbench/updateChartAction', async ( - arg: { name; viewId; graphId; chartId; index; parentId }, + arg: { name; viewId; graphId; chartId; index; parentId; aggregation }, thunkAPI, ) => { try { @@ -299,6 +346,7 @@ export const updateChartAction = createAsyncThunk( const workbenchState = state.workbench as typeof initState; const stringConfig = JSON.stringify({ + aggregation: arg.aggregation, chartConfig: workbenchState.chartConfig, chartGraphId: arg.graphId, computedFields: workbenchState.currentDataView?.computedFields || [], @@ -399,6 +447,7 @@ const workbenchSlice = createSlice({ return state; } }; + state.chartConfig = chartConfigReducer(state.chartConfig!, { type: action.payload.type, payload: action.payload.payload, @@ -413,6 +462,9 @@ const workbenchSlice = createSlice({ computedFields: action.payload, } as ChartDataView; }, + updateChartAggregation: (state, action: PayloadAction) => { + state.aggregation = action.payload; + }, resetWorkbenchState: (state, action) => { return initState; }, @@ -448,14 +500,22 @@ const workbenchSlice = createSlice({ return; } - const backendChartConfig = + let backendChartConfig = typeof payload.config === 'string' ? JSON.parse(payload.config) : CloneValueDeep(payload.config); + backendChartConfig = backendChartConfig || {}; + + if (backendChartConfig?.aggregation === undefined) { + backendChartConfig.aggregation = true; + } + state.backendChart = { ...payload, config: backendChartConfig, }; + state.aggregation = backendChartConfig.aggregation; + const currentChart = ChartManager.instance().getById( backendChartConfig?.chartGraphId, ); diff --git a/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx b/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx index 9ef8c4ad9..3ed1a7a9c 100644 --- a/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx @@ -17,6 +17,7 @@ */ import { CloudDownloadOutlined, ShareAltOutlined } from '@ant-design/icons'; import { Menu, Popconfirm } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { memo, useContext, useMemo } from 'react'; import { BoardContext } from '../contexts/BoardContext'; export interface BoardOverLayProps { @@ -24,8 +25,10 @@ export interface BoardOverLayProps { onBoardToDownLoad: () => void; onShareDownloadData?: () => void; } + export const BoardOverLay: React.FC = memo( ({ onOpenShareLink, onBoardToDownLoad, onShareDownloadData }) => { + const t = useI18NPrefix(`viz.action`); const { allowShare, allowDownload, renderMode } = useContext(BoardContext); const renderList = useMemo( () => [ @@ -33,10 +36,9 @@ export const BoardOverLay: React.FC = memo( key: 'shareLink', icon: , onClick: onOpenShareLink, - disabled: false, render: allowShare && renderMode === 'read', - content: '分享链接', + content: t('share.shareLink'), }, { key: 'downloadData', @@ -47,9 +49,9 @@ export const BoardOverLay: React.FC = memo( content: ( { if (renderMode === 'read') { onBoardToDownLoad?.(); @@ -58,7 +60,7 @@ export const BoardOverLay: React.FC = memo( } }} > - 下载数据 + {t('share.downloadData')} ), }, @@ -67,6 +69,7 @@ export const BoardOverLay: React.FC = memo( onOpenShareLink, allowShare, renderMode, + t, allowDownload, onBoardToDownLoad, onShareDownloadData, diff --git a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx index 8547b7616..5a3763d59 100644 --- a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx @@ -18,7 +18,7 @@ import { PageInfo } from 'app/pages/MainPage/pages/ViewPage/slice/types'; import { generateShareLinkAsync } from 'app/utils/fetch'; -import { debounce } from 'lodash'; +import debounce from 'lodash/debounce'; import React, { FC, useContext } from 'react'; import { useDispatch } from 'react-redux'; import { @@ -33,15 +33,22 @@ import { resetControllerAction, widgetsQueryAction, } from '../../pages/Board/slice/asyncActions'; -import { getWidgetDataAsync } from '../../pages/Board/slice/thunk'; +import { + getChartWidgetDataAsync, + getControllerOptions, +} from '../../pages/Board/slice/thunk'; import { Widget } from '../../pages/Board/slice/types'; import { editBoardStackActions } from '../../pages/BoardEditor/slice'; import { editWidgetsQueryAction } from '../../pages/BoardEditor/slice/actions/controlActions'; import { - getEditWidgetDataAsync, + getEditChartWidgetDataAsync, + getEditControllerOptions, toUpdateDashboard, } from '../../pages/BoardEditor/slice/thunk'; -import { getNeedRefreshWidgetsByFilter } from '../../utils/widget'; +import { + getCascadeControllers, + getNeedRefreshWidgetsByController, +} from '../../utils/widget'; export const BoardActionProvider: FC<{ id: string }> = ({ id: boardId, @@ -75,25 +82,37 @@ export const BoardActionProvider: FC<{ id: string }> = ({ dispatch(resetControllerAction({ boardId, renderMode })); } }, 500), - refreshWidgetsByFilter: debounce((widget: Widget) => { + refreshWidgetsByController: debounce((widget: Widget) => { + const controllerIds = getCascadeControllers(widget); + controllerIds.forEach(controlWidgetId => { + if (editing) { + dispatch(getEditControllerOptions(controlWidgetId)); + } else { + dispatch( + getControllerOptions({ + boardId, + widgetId: controlWidgetId, + renderMode, + }), + ); + } + }); if (hasQueryControl) { return; } - const widgetIds = getNeedRefreshWidgetsByFilter(widget); const pageInfo: Partial = { pageNo: 1, }; - widgetIds.forEach(widgetId => { + const chartWidgetIds = getNeedRefreshWidgetsByController(widget); + + chartWidgetIds.forEach(widgetId => { if (editing) { dispatch( - getEditWidgetDataAsync({ - widgetId, - option: { pageInfo }, - }), + getEditChartWidgetDataAsync({ widgetId, option: { pageInfo } }), ); } else { dispatch( - getWidgetDataAsync({ + getChartWidgetDataAsync({ boardId, widgetId, renderMode, diff --git a/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx b/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx index c52eb6691..3f9e73972 100644 --- a/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx @@ -27,8 +27,17 @@ import { } from '@ant-design/icons'; import { Button, Dropdown, Space } from 'antd'; import { ShareLinkModal } from 'app/components/VizOperationMenu'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import classnames from 'classnames'; -import React, { FC, memo, useCallback, useContext, useState } from 'react'; +import { TITLE_SUFFIX } from 'globalConstants'; +import React, { + FC, + memo, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; import styled from 'styled-components/macro'; import { FONT_SIZE_ICON_SM, @@ -42,8 +51,6 @@ import { BoardContext } from '../contexts/BoardContext'; import { BoardInfoContext } from '../contexts/BoardInfoContext'; import { BoardOverLay } from './BoardOverLay'; -const TITLE_SUFFIX = ['[已归档]', '[未发布]']; - interface TitleHeaderProps { name?: string; publishLoading?: boolean; @@ -60,6 +67,7 @@ const TitleHeader: FC = memo( publishLoading, onPublish, }) => { + const t = useI18NPrefix(`viz.action`); const [showShareLinkModal, setShowShareLinkModal] = useState(false); const { editing, @@ -84,7 +92,11 @@ const TitleHeader: FC = memo( toggleBoardEditor?.(false); }; - const title = `${name || boardName} ${TITLE_SUFFIX[status] || ''}`; + const title = useMemo(() => { + const base = name || boardName; + const suffix = TITLE_SUFFIX[status] ? `[${t(TITLE_SUFFIX[status])}]` : ''; + return base + suffix; + }, [boardName, name, status, t]); const isArchived = status === 0; return ( @@ -102,7 +114,7 @@ const TitleHeader: FC = memo( icon={} onClick={closeBoardEditor} > - 取消 + {t('common.cancel')} = memo( icon={} onClick={onUpdateBoard} > - 保存 + {t('common.save')} > ) : ( @@ -130,7 +142,7 @@ const TitleHeader: FC = memo( loading={publishLoading} onClick={onPublish} > - {status === 1 ? '发布' : '取消发布'} + {status === 1 ? t('publish') : t('unpublish')} )} {allowManage && !isArchived && renderMode === 'read' && ( @@ -139,7 +151,7 @@ const TitleHeader: FC = memo( icon={} onClick={() => toggleBoardEditor?.(true)} > - 编辑 + {t('edit')} )} { 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 2e99692cc..f898e2a35 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 @@ -23,7 +23,7 @@ import React, { useCallback, useContext, useState } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components/macro'; import { PRIMARY } from 'styles/StyleConstants'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import { BoardContext } from '../../../../contexts/BoardContext'; import { editBoardStackActions } from '../../../../pages/BoardEditor/slice'; import { WidgetAllProvider } from '../../../WidgetProvider/WidgetAllProvider'; diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/Controller/CheckboxGroupController.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/Controller/CheckboxGroupController.tsx new file mode 100644 index 000000000..e8943752b --- /dev/null +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/Controller/CheckboxGroupController.tsx @@ -0,0 +1,68 @@ +/** + * 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 { Checkbox, Form } from 'antd'; +import { CheckboxValueType } from 'antd/lib/checkbox/Group'; +import { ControlOption } from 'app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/types'; +import React, { memo, useCallback } from 'react'; +import styled from 'styled-components/macro'; + +export interface CheckboxGroupControllerProps { + options?: ControlOption[]; + value?: CheckboxValueType[]; + placeholder?: string; + onChange: (values) => void; + label?: React.ReactNode; + name?: string; + required?: boolean; +} + +export const CheckboxGroupControllerForm: React.FC = + memo(({ label, name, required, ...rest }) => { + return ( + + + + ); + }); +export const CheckboxGroupSetter: React.FC = memo( + ({ options, onChange, value }) => { + const renderOptions = useCallback(() => { + return (options || []).map(o => ({ + label: o.label ?? o.value, + value: o.value, + })); + }, [options]); + return ( + + + + ); + }, +); +const Wrapper = styled.div` + display: flex; +`; 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 2dbfdcb03..83e5eadc8 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/ControllerWidgetCore.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/ControllerWidgetCore.tsx @@ -28,10 +28,10 @@ import { ControlOption, } from 'app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/types'; import { getControllerDateValues } from 'app/pages/DashBoardPage/utils'; -import { FilterValueOption } from 'app/types/ChartConfig'; +import { RelationFilterValue } from 'app/types/ChartConfig'; import { ControllerFacadeTypes, - RelativeOrExactTime, + TimeFilterValueCategory, } from 'app/types/FilterControlPanel'; import produce from 'immer'; import React, { @@ -43,6 +43,7 @@ import React, { } from 'react'; import styled from 'styled-components/macro'; import { LabelName } from '../WidgetName/WidgetName'; +import { CheckboxGroupControllerForm } from './Controller/CheckboxGroupController'; import { MultiSelectControllerForm } from './Controller/MultiSelectController'; import { NumberControllerForm } from './Controller/NumberController'; import { RadioGroupControllerForm } from './Controller/RadioGroupController'; @@ -62,7 +63,7 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { const { data: { rows }, } = useContext(WidgetDataContext); - const { widgetUpdate, refreshWidgetsByFilter } = + const { widgetUpdate, refreshWidgetsByController } = useContext(BoardActionContext); const { config, type: facadeType } = useMemo( @@ -103,7 +104,7 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { const dataRows = rows?.flat(2) || []; if (valueOptionType === 'common') { return dataRows.map(ele => { - const item: FilterValueOption = { + const item: RelationFilterValue = { key: ele, label: ele, // children? @@ -137,7 +138,7 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { ).config.controllerValues = _values; }); widgetUpdate(nextWidget); - refreshWidgetsByFilter(nextWidget); + refreshWidgetsByController(nextWidget); }; // const onSqlOperatorAndValues = useCallback( // (sql: FilterSqlOperator, values: any[]) => { @@ -158,11 +159,11 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { const nextFilterDate: ControllerDate = { ...controllerDate!, startTime: { - relativeOrExact: RelativeOrExactTime.Exact, + relativeOrExact: TimeFilterValueCategory.Exact, exactValue: timeValues?.[0], }, endTime: { - relativeOrExact: RelativeOrExactTime.Exact, + relativeOrExact: TimeFilterValueCategory.Exact, exactValue: timeValues?.[1], }, }; @@ -172,9 +173,9 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { ).config.controllerDate = nextFilterDate; }); widgetUpdate(nextWidget); - refreshWidgetsByFilter(nextWidget); + refreshWidgetsByController(nextWidget); }, - [controllerDate, refreshWidgetsByFilter, widget, widgetUpdate], + [controllerDate, refreshWidgetsByController, widget, widgetUpdate], ); const onTimeChange = useCallback( @@ -182,7 +183,7 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { const nextFilterDate: ControllerDate = { ...controllerDate!, startTime: { - relativeOrExact: RelativeOrExactTime.Exact, + relativeOrExact: TimeFilterValueCategory.Exact, exactValue: value, }, }; @@ -192,9 +193,9 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { ).config.controllerDate = nextFilterDate; }); widgetUpdate(nextWidget); - refreshWidgetsByFilter(nextWidget); + refreshWidgetsByController(nextWidget); }, - [controllerDate, refreshWidgetsByFilter, widget, widgetUpdate], + [controllerDate, refreshWidgetsByController, widget, widgetUpdate], ); const control = useMemo(() => { @@ -223,7 +224,16 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { label={leftControlLabel} /> ); - + case ControllerFacadeTypes.CheckboxGroup: + form.setFieldsValue({ value: controllerValues }); + return ( + + ); case ControllerFacadeTypes.Slider: form.setFieldsValue({ value: controllerValues?.[0] }); const step = config.sliderConfig?.step || 1; 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 491c7c4fe..0e9281ed3 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/DataChartWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/DataChartWidget/index.tsx @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import useResizeObserver from 'app/hooks/useResizeObserver'; +import { useCacheWidthHeight } from 'app/hooks/useCacheWidthHeight'; import ChartIFrameContainer from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainer'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import ChartManager from 'app/pages/ChartWorkbenchPage/models/ChartManager'; @@ -33,7 +33,6 @@ import React, { useEffect, useMemo, useRef, - useState, } from 'react'; import styled from 'styled-components/macro'; export interface DataChartWidgetProps {} @@ -43,8 +42,7 @@ export const DataChartWidget: React.FC = memo(() => { const widget = useContext(WidgetContext); const { id: widgetId } = widget; const { widgetChartClick } = useContext(WidgetMethodContext); - const [cacheW, setCacheW] = useState(200); - const [cacheH, setCacheH] = useState(200); + const { ref, cacheW, cacheH } = useCacheWidthHeight(); const widgetRef = useRef(widget); useEffect(() => { widgetRef.current = widget; @@ -87,23 +85,25 @@ export const DataChartWidget: React.FC = memo(() => { } }, [chartClick, dataChart]); - const onResize = useCallback(() => {}, []); - - const { - ref, - width = 200, - height = 200, - } = useResizeObserver({ - refreshMode: 'debounce', - refreshRate: 120, - onResize, - }); - useEffect(() => { - if (width !== 0 && height !== 0) { - setCacheW(width); - setCacheH(height); + const widgetSpecialConfig = useMemo(() => { + let linkFields: string[] = []; + let jumpField: string = ''; + const { jumpConfig, linkageConfig } = widget.config; + if (linkageConfig?.open) { + linkFields = widget?.relations + .filter(re => re.config.type === 'widgetToWidget') + .map(item => item.config.widgetToWidget?.triggerColumn as string); } - }, [width, height]); + if (jumpConfig?.open) { + jumpField = jumpConfig?.field?.jumpFieldName as string; + } + + return { + linkFields, + jumpField, + }; + }, [widget]); + const dataset = useMemo( () => ({ columns: data.columns, @@ -131,28 +131,38 @@ export const DataChartWidget: React.FC = memo(() => { dataset={dataset} chart={chart} config={dataChart.config.chartConfig as ChartConfig} - style={{ width: cacheW, height: cacheH }} + width={cacheW} + height={cacheH} containerId={widgetId} + widgetSpecialConfig={widgetSpecialConfig} /> ); } catch (error) { return has err in {``}; } - }, [cacheH, cacheW, chart, dataChart, dataset, widgetId]); + }, [ + cacheH, + cacheW, + chart, + dataChart, + dataset, + widgetSpecialConfig, + widgetId, + ]); return ( - {chartFrame} + {chartFrame} ); }); +const ChartFrameBox = styled.div` + position: absolute; + height: 100%; + width: 100%; + overflow: hidden; +`; const Wrap = styled.div` display: flex; flex: 1; - width: 100%; - height: 100%; - overflow: hidden; - & div { - max-width: 100%; - max-height: 100%; - } + 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 2be25f9af..481e3952c 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 @@ -79,7 +79,7 @@ const IframeWidget: React.FC<{}> = () => { frameBorder="0" allow="autoplay" style={{ width: '100%', height: '100%' }} - /> + > ); }; 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 6b459466b..19e8592b5 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 @@ -15,29 +15,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +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, - Widget, - WidgetInfo, -} from 'app/pages/DashBoardPage/pages/Board/slice/types'; -import { convertImageUrl } from 'app/pages/DashBoardPage/utils'; -import React, { useMemo } from 'react'; +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'; -export interface ImageWidgetProps { - widgetConfig: Widget; - widgetInfo: WidgetInfo; -} const widgetSize: React.CSSProperties = { width: '100%', height: '100%', }; -const ImageWidget: React.FC = ({ widgetConfig }) => { - const { imageConfig } = widgetConfig.config.content as MediaWidgetContent; - +const ImageWidget: React.FC<{}> = () => { + const widget = useContext(WidgetContext); + const { editing } = useContext(WidgetInfoContext); + const { widgetUpdate } = useContext(BoardActionContext); + const { imageConfig } = widget.config.content as MediaWidgetContent; + const widgetBgImage = widget.config.background.image; const [rect, refDom] = useClientRect(32); - + const t = useI18NPrefix(`viz.board.setting`); const widthBigger = useMemo(() => { return rect.width >= rect.height; }, [rect]); @@ -53,21 +54,41 @@ const ImageWidget: React.FC = ({ widgetConfig }) => { height: 'auto', }; }, [widthBigger]); + const onChange = useCallback( + value => { + const nextWidget = produce(widget, draft => { + draft.config.background.image = value; + }); + widgetUpdate(nextWidget); + }, + [widget, widgetUpdate], + ); const imageSize = useMemo(() => { return imageConfig?.type === 'IMAGE_RATIO' ? imageRatioCss : widgetSize; }, [imageRatioCss, imageConfig?.type]); - - return ( - - {imageConfig?.src && ( - - )} - - ); + const renderImage = useMemo(() => { + return editing ? ( + + ) : widgetBgImage ? null : ( + + ); + }, [editing, onChange, t, widgetBgImage]); + return {renderImage}; }; export default ImageWidget; const Wrap = styled.div` + display: flex; + align-items: center; + justify-content: center; width: 100%; height: 100%; + + .ant-upload-list { + display: none; + } `; diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/Formats.ts b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/Formats.ts index 6ced5aa1b..828929094 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/Formats.ts +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/Formats.ts @@ -31,4 +31,9 @@ export const Formats = [ 'calcfield', 'mention', 'image', + 'size', + 'background', + 'font', + 'align', + 'code-block', ]; diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/VideoWidget/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/VideoWidget/index.tsx index 2d9213bcf..54b3352e0 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/VideoWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/VideoWidget/index.tsx @@ -110,7 +110,7 @@ const VideoWidget: React.FC = () => { src={srcWithParams} frameBorder="0" style={{ width: '100%', height: '100%' }} - /> + > ); } diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/index.tsx index 41a0b92ed..76671c8da 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/index.tsx @@ -33,7 +33,7 @@ export const MediaWidget: React.FC<{}> = memo(() => { case 'richText': return ; case 'image': - return ; + return ; case 'video': return ; case 'iframe': diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetProvider/WidgetMethodProvider.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetProvider/WidgetMethodProvider.tsx index 5327369c5..19d52c387 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetProvider/WidgetMethodProvider.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetProvider/WidgetMethodProvider.tsx @@ -18,7 +18,7 @@ import { ExclamationCircleOutlined } from '@ant-design/icons'; import { Modal } from 'antd'; -import { PageInfo } from 'app/pages/MainPage/pages/ViewPage/slice/types'; +import usePrefixI18N from 'app/hooks/useI18NPrefix'; import { urlSearchTransfer } from 'app/pages/MainPage/pages/VizPage/utils'; import { ChartMouseEventParams } from 'app/types/DatartChartBase'; import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; @@ -26,7 +26,6 @@ import React, { FC, useCallback, useContext } from 'react'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router'; import { BoardContext } from '../../contexts/BoardContext'; -import { WidgetContext } from '../../contexts/WidgetContext'; import { WidgetMethodContext, WidgetMethodContextProps, @@ -34,7 +33,7 @@ import { import { boardActions } from '../../pages/Board/slice'; import { getChartWidgetDataAsync, - getWidgetDataAsync, + getWidgetData, } from '../../pages/Board/slice/thunk'; import { BoardLinkFilter, @@ -42,6 +41,7 @@ import { WidgetContentChartType, WidgetType, } from '../../pages/Board/slice/types'; +import { jumpTypes } from '../../pages/BoardEditor/components/SettingJumpModal/config'; import { editBoardStackActions, editDashBoardInfoActions, @@ -55,7 +55,7 @@ import { import { editWidgetsQueryAction } from '../../pages/BoardEditor/slice/actions/controlActions'; import { getEditChartWidgetDataAsync, - getEditWidgetDataAsync, + getEditWidgetData, } from '../../pages/BoardEditor/slice/thunk'; import { widgetActionType } from '../WidgetToolBar/config'; @@ -64,8 +64,9 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ widgetId, children, }) => { + const t = usePrefixI18N('viz.widget.action'); const { boardId, editing, renderMode, orgId } = useContext(BoardContext); - const widget = useContext(WidgetContext); + const dispatch = useDispatch(); const history = useHistory(); @@ -74,10 +75,9 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ (type: WidgetType, wid: string) => { if (type === 'container') { confirm({ - // TODO i18n - title: '确认删除', + title: t('confirmDel'), icon: , - content: '该组件内的组件也会被删除,确认是否删除?', + content: t('ContainerConfirmDel'), onOk() { dispatch(editBoardStackActions.deleteWidgets([wid])); }, @@ -91,13 +91,12 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ if (type === 'reset') { dispatch(editBoardStackActions.changeBoardHasResetControl(false)); } - dispatch(editBoardStackActions.deleteWidgets([wid])); - if (type === 'controller') { dispatch(editWidgetsQueryAction({ boardId })); } + dispatch(editBoardStackActions.deleteWidgets([wid])); }, - [dispatch, boardId], + [dispatch, t, boardId], ); const onWidgetEdit = useCallback( (widget: Widget, wid: string) => { @@ -162,11 +161,11 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ [dispatch], ); const onWidgetGetData = useCallback( - (boardId: string, widgetId: string) => { + (boardId: string, widget: Widget) => { if (editing) { - dispatch(getEditWidgetDataAsync({ widgetId })); + dispatch(getEditWidgetData({ widget })); } else { - dispatch(getWidgetDataAsync({ boardId, widgetId, renderMode })); + dispatch(getWidgetData({ boardId, widget, renderMode })); } }, [dispatch, editing, renderMode], @@ -246,18 +245,56 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ ); setTimeout(() => { linkRelations.forEach(link => { - onWidgetGetData(boardId, link.targetId); + if (editing) { + dispatch( + getEditChartWidgetDataAsync({ + widgetId: link.targetId, + option: { + pageInfo: { pageNo: 1 }, + }, + }), + ); + } else { + dispatch( + getChartWidgetDataAsync({ + boardId, + widgetId: link.targetId, + renderMode, + option: { + pageInfo: { pageNo: 1 }, + }, + }), + ); + } }); }, 60); }, - [onToggleLinkage, onChangeBoardFilter, onWidgetGetData, boardId], + [ + onToggleLinkage, + onChangeBoardFilter, + editing, + dispatch, + boardId, + renderMode, + ], ); const toLinkingWidgets = useCallback( (widget: Widget, params: ChartMouseEventParams) => { - const linkRelations = widget.relations.filter( - re => re.config.type === 'widgetToWidget', - ); + const { componentType, seriesType, seriesName } = params; + const isTableHandle = componentType === 'table' && seriesType === 'body'; + + const linkRelations = widget.relations.filter(re => { + const { + config: { type, widgetToWidget }, + } = re; + if (type !== 'widgetToWidget') return false; + if (isTableHandle) { + if (widgetToWidget?.triggerColumn === seriesName) return true; + return false; + } + return true; + }); const boardFilters = linkRelations.map(re => { let linkageFieldName: string = @@ -294,20 +331,48 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ onToggleLinkage(true); setTimeout(() => { boardFilters.forEach(f => { - onWidgetGetData(boardId, f.linkerWidgetId); + if (editing) { + dispatch( + getEditChartWidgetDataAsync({ + widgetId: f.linkerWidgetId, + option: { + pageInfo: { pageNo: 1 }, + }, + }), + ); + } else { + dispatch( + getChartWidgetDataAsync({ + boardId, + widgetId: f.linkerWidgetId, + renderMode, + option: { + pageInfo: { pageNo: 1 }, + }, + }), + ); + } }); }, 60); }, - [boardId, dispatch, editing, onToggleLinkage, onWidgetGetData, widgetId], + [boardId, dispatch, editing, onToggleLinkage, renderMode, widgetId], ); const clickJump = useCallback( (values: { widget: Widget; params: ChartMouseEventParams }) => { const { widget, params } = values; const jumpConfig = widget.config?.jumpConfig; + const targetType = jumpConfig?.targetType || jumpTypes[0].value; + const URL = jumpConfig?.URL || ''; + const queryName = jumpConfig?.queryName || ''; const targetId = jumpConfig?.target?.relId; const jumpFieldName: string = jumpConfig?.field?.jumpFieldName || ''; + if ( + params.componentType === 'table' && + jumpFieldName !== params.seriesName + ) + return; - if (typeof jumpConfig?.filter === 'object') { + if (typeof jumpConfig?.filter === 'object' && targetType === 'INTERNAL') { const searchParamsStr = urlSearchTransfer.toUrlString({ [jumpConfig?.filter?.filterId]: (params?.data?.rowData?.[jumpFieldName] as string) || '', @@ -317,22 +382,28 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ `/organizations/${orgId}/vizs/${targetId}?${searchParamsStr}`, ); } + } else if (targetType === 'URL') { + let jumpUrl; + if (URL.indexOf('?') > -1) { + jumpUrl = `${URL}&${queryName}=${params?.data?.rowData?.[jumpFieldName]}`; + } else { + jumpUrl = `${URL}?${queryName}=${params?.data?.rowData?.[jumpFieldName]}`; + } + window.location.href = jumpUrl; } }, [history, orgId], ); const getTableChartData = useCallback( - (options: { widget: Widget; params: any }) => { + (options: { widget: Widget; params: any; sorters?: any[] }) => { const { params } = options; - const pageInfo: Partial = { - pageNo: params.value.page, - }; if (editing) { dispatch( getEditChartWidgetDataAsync({ widgetId, option: { - pageInfo, + pageInfo: params?.pageInfo, + sorters: params?.sorters, }, }), ); @@ -343,7 +414,8 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ widgetId, renderMode, option: { - pageInfo, + pageInfo: params?.pageInfo, + sorters: params?.sorters, }, }), ); @@ -360,7 +432,7 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ case 'info': break; case 'refresh': - onWidgetGetData(boardId, widgetId); + onWidgetGetData(boardId, widget); break; case 'delete': onWidgetDelete(widget.config.type, widgetId); @@ -400,10 +472,22 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ const widgetChartClick = useCallback( (widget: Widget, params: ChartMouseEventParams) => { - // table 分页 - if (params?.seriesType === 'table' && params?.seriesName === 'paging') { - // table 分页逻辑 - getTableChartData({ widget, params }); + if ( + params.componentType === 'table' && + params.seriesType === 'paging-sort-filter' + ) { + getTableChartData({ + widget, + params: { + pageInfo: { pageNo: params?.value?.pageNo }, + sorters: [ + { + column: params?.seriesName!, + operator: (params?.value as any)?.direction, + }, + ], + }, + }); return; } // jump diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/WidgetActionDropdown.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/WidgetActionDropdown.tsx index f58bd4798..e951ff04c 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/WidgetActionDropdown.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/WidgetActionDropdown.tsx @@ -27,15 +27,14 @@ import { SyncOutlined, } from '@ant-design/icons'; import { Button, Dropdown, Menu } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { memo, useCallback, useContext, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { selectDataChartById } from '../..//pages/Board/slice/selector'; import { BoardContext } from '../../contexts/BoardContext'; +import { WidgetChartContext } from '../../contexts/WidgetChartContext'; import { WidgetMethodContext } from '../../contexts/WidgetMethodContext'; import { Widget } from '../../pages/Board/slice/types'; import { getWidgetActionList, - TriggerChartIds, WidgetActionListItem, widgetActionType, } from './config'; @@ -48,93 +47,82 @@ export const WidgetActionDropdown: React.FC = memo( ({ widget }) => { const { editing: boardEditing } = useContext(BoardContext); const { onWidgetAction } = useContext(WidgetMethodContext); - const dataChart = useSelector(state => - selectDataChartById(state, widget?.datachartId), - ); - const IsSupportTrigger = useMemo( - () => TriggerChartIds.includes(dataChart?.config.chartGraphId), - [dataChart], - ); - + const dataChart = useContext(WidgetChartContext)!; + const t = useI18NPrefix(`viz.widget.action`); const menuClick = useCallback( ({ key }) => { onWidgetAction(key, widget); }, [onWidgetAction, widget], ); - const getAllList = () => { + const getAllList = useCallback(() => { const allWidgetActionList: WidgetActionListItem[] = [ { key: 'refresh', - label: '同步数据', + label: t('refresh'), icon: , }, { key: 'fullScreen', - label: '全屏', + label: t('fullScreen'), icon: , }, { key: 'edit', - label: '编辑', + label: t('edit'), icon: , }, { key: 'delete', - label: '删除', + label: t('delete'), icon: , danger: true, }, { key: 'info', - label: '信息', + label: t('info'), icon: , }, { key: 'makeLinkage', - label: '联动设置', + label: t('makeLinkage'), icon: , divider: true, }, { key: 'closeLinkage', - label: '关闭联动', + label: t('closeLinkage'), icon: , danger: true, }, { key: 'makeJump', - label: '跳转设置', + label: t('makeJump'), icon: , divider: true, }, { key: 'closeJump', - label: '关闭跳转', + label: t('closeJump'), icon: , danger: true, }, ]; return allWidgetActionList; - }; + }, [t]); const actionList = useMemo(() => { return ( getWidgetActionList({ allList: getAllList(), widget, boardEditing, + chartGraphId: dataChart?.config.chartGraphId, }) || [] ); - }, [boardEditing, widget]); + }, [boardEditing, dataChart?.config.chartGraphId, getAllList, widget]); const dropdownList = useMemo(() => { const menuItems = actionList.map(item => { - if ( - (item.key === 'makeLinkage' || item.key === 'makeJump') && - !IsSupportTrigger - ) - return null; - return ( {item.divider && } @@ -151,7 +139,7 @@ export const WidgetActionDropdown: React.FC = memo( }); return {menuItems}; - }, [actionList, menuClick, IsSupportTrigger]); + }, [actionList, menuClick]); return ( []; widget: Widget; boardEditing: boolean; + chartGraphId?: string; }) => { - const { widget, allList, boardEditing } = opt; + const { widget, allList, boardEditing, chartGraphId } = opt; const widgetType = widget.config.type; if (boardEditing) { if (widget.config.type === 'chart') { - return getEditChartActionList({ allList, widget }); + return getEditChartActionList({ allList, widget, chartGraphId }); } else { return allList.filter(item => widgetActionMap.edit[widgetType].includes(item.key), @@ -110,26 +113,26 @@ export const getWidgetActionList = (opt: { export const getEditChartActionList = (opt: { allList: WidgetActionListItem[]; widget: Widget; + chartGraphId?: string; }) => { - const { widget, allList } = opt; + const { widget, allList, chartGraphId } = opt; const widgetType = widget.config.type; const curChartItems: widgetActionType[] = widgetActionMap.edit[widgetType].slice(); - // TODO 判断哪些 chart 可以添加跳转 和联动 暂时用true 代替 - let chartCanMakeJump = true; - let chartCanMakeLink = true; - if (chartCanMakeLink) { + const isTrigger = SupportTriggerChartIds.includes(chartGraphId as string); + + if (isTrigger) { + // Linkage curChartItems.push('makeLinkage'); - } - if (widget.config.linkageConfig?.open) { - curChartItems.push('closeLinkage'); - } - if (chartCanMakeJump) { + if (widget.config.linkageConfig?.open) { + curChartItems.push('closeLinkage'); + } + // Jump curChartItems.push('makeJump'); - } - if (widget.config.jumpConfig?.open) { - curChartItems.push('closeJump'); + if (widget.config.jumpConfig?.open) { + curChartItems.push('closeJump'); + } } return allList diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/index.tsx index bde7fb63b..5ad62c426 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/index.tsx @@ -20,11 +20,12 @@ import { ClockCircleOutlined, LinkOutlined, SyncOutlined, + WarningTwoTone, } from '@ant-design/icons'; -import { Space, Tooltip } from 'antd'; +import { Button, Space, Tooltip } from 'antd'; import React, { FC, useContext } from 'react'; import styled from 'styled-components'; -import { PRIMARY } from 'styles/StyleConstants'; +import { ERROR, PRIMARY } from 'styles/StyleConstants'; import { BoardContext } from '../../contexts/BoardContext'; import { WidgetContext } from '../../contexts/WidgetContext'; import { WidgetInfoContext } from '../../contexts/WidgetInfoContext'; @@ -36,7 +37,8 @@ interface WidgetToolBarProps {} const WidgetToolBar: FC = () => { const { boardType, editing: boardEditing } = useContext(BoardContext); - const { loading, inLinking, rendered } = useContext(WidgetInfoContext); + const { loading, inLinking, rendered, errInfo } = + useContext(WidgetInfoContext); const widget = useContext(WidgetContext); const { onClearLinkage } = useContext(WidgetMethodContext); const ssp = e => { @@ -49,7 +51,10 @@ const WidgetToolBar: FC = () => { if (!showTypes.includes(widgetType)) return null; return rendered ? null : ( - + } + type="link" + /> ); }; @@ -57,7 +62,12 @@ const WidgetToolBar: FC = () => { const widgetType = widget.config.type; const showTypes: WidgetType[] = ['chart', 'controller']; if (!showTypes.includes(widgetType)) return null; - return loading ? : null; + return loading ? ( + } + type="link" + /> + ) : null; }; const linkageIcon = () => { if (inLinking) { @@ -72,11 +82,35 @@ const WidgetToolBar: FC = () => { } else { return widget.config?.linkageConfig?.open ? ( - + } + type="link" + /> ) : null; } }; + const renderErrorIcon = (errInfo?: string) => { + if (!errInfo) return null; + const renderTitle = errInfo => { + if (typeof errInfo !== 'string') return 'object'; + return ( + + {errInfo} + + ); + }; + return ( + + } + type="link" + /> + + ); + }; const renderWidgetAction = () => { const widgetType = widget.config.type; const hideTypes: WidgetType[] = ['query', 'reset', 'controller']; @@ -85,9 +119,11 @@ const WidgetToolBar: FC = () => { } return ; }; + return ( - + + {renderErrorIcon(errInfo)} {renderedIcon()} {loadingIcon()} {linkageIcon()} @@ -110,3 +146,12 @@ const StyleWrap = styled.div` visibility: hidden; } `; + +const StyledErrorIcon = styled(Button)` + background: ${p => p.theme.componentBackground}; + + &:hover, + &:focus { + background: ${p => p.theme.componentBackground}; + } +`; diff --git a/frontend/src/app/pages/DashBoardPage/constants.ts b/frontend/src/app/pages/DashBoardPage/constants.ts index 50f094ccd..96aad995c 100644 --- a/frontend/src/app/pages/DashBoardPage/constants.ts +++ b/frontend/src/app/pages/DashBoardPage/constants.ts @@ -22,9 +22,9 @@ import { } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; import { FilterSqlOperator } from 'globalConstants'; +import i18next from 'i18next'; import { PRIMARY, WHITE } from 'styles/StyleConstants'; import { WidgetType } from './pages/Board/slice/types'; - export const RGL_DRAG_HANDLE = 'dashboard-draggableHandle'; export const STORAGE_BOARD_KEY_PREFIX = 'DATART_BOARD_DATA_'; export const STORAGE_IMAGE_KEY_PREFIX = 'DATART_IMAGE_'; @@ -89,6 +89,7 @@ export const CanFullScreenWidgetTypes: WidgetType[] = ['chart', 'media']; export const CONTAINER_TAB = 'containerTab'; // +export const NeedFetchWidgetTypes: WidgetType[] = ['chart', 'controller']; // setting @@ -96,11 +97,11 @@ export const TEXT_ALIGN_ENUM = strEnumType(['left', 'center', 'right']); export type TextAlignType = keyof typeof TEXT_ALIGN_ENUM; export const BORDER_STYLE_ENUM = strEnumType([ + 'none', 'solid', 'dashed', 'dotted', 'double', - 'none', 'hidden', 'ridge', 'groove', @@ -110,16 +111,16 @@ export const BORDER_STYLE_ENUM = strEnumType([ export type BorderStyleType = keyof typeof BORDER_STYLE_ENUM; export const BORDER_STYLE_OPTIONS = [ - { name: '无', value: BORDER_STYLE_ENUM.none }, - { name: '实线', value: BORDER_STYLE_ENUM.solid }, - { name: '虚线', value: BORDER_STYLE_ENUM.dashed }, - { name: '点线', value: BORDER_STYLE_ENUM.dotted }, - { name: '双线', value: BORDER_STYLE_ENUM.double }, - { name: '隐藏', value: BORDER_STYLE_ENUM.hidden }, - { name: '凹槽', value: BORDER_STYLE_ENUM.groove }, - { name: '垄状', value: BORDER_STYLE_ENUM.ridge }, - { name: 'inset', value: BORDER_STYLE_ENUM.inset }, - { name: 'outset', value: BORDER_STYLE_ENUM.outset }, + { value: BORDER_STYLE_ENUM.none }, + { value: BORDER_STYLE_ENUM.solid }, + { value: BORDER_STYLE_ENUM.dashed }, + { value: BORDER_STYLE_ENUM.dotted }, + { value: BORDER_STYLE_ENUM.double }, + { value: BORDER_STYLE_ENUM.hidden }, + { value: BORDER_STYLE_ENUM.groove }, + { value: BORDER_STYLE_ENUM.ridge }, + { value: BORDER_STYLE_ENUM.inset }, + { value: BORDER_STYLE_ENUM.outset }, ]; export const SCALE_MODE_ENUM = strEnumType([ @@ -131,10 +132,10 @@ export const SCALE_MODE_ENUM = strEnumType([ export type ScaleModeType = keyof typeof SCALE_MODE_ENUM; export const SCALE_MODE__OPTIONS = [ - { name: '等比宽度缩放', value: SCALE_MODE_ENUM.scaleWidth }, - { name: '等比高度缩放', value: SCALE_MODE_ENUM.scaleHeight }, - { name: '全屏铺满', value: SCALE_MODE_ENUM.scaleFull }, - { name: '实际尺寸', value: SCALE_MODE_ENUM.noScale }, + { value: SCALE_MODE_ENUM.scaleWidth }, + { value: SCALE_MODE_ENUM.scaleHeight }, + { value: SCALE_MODE_ENUM.scaleFull }, + { value: SCALE_MODE_ENUM.noScale }, ]; export const enum ValueOptionTypes { @@ -155,33 +156,53 @@ export const enum ControllerVisibleTypes { export type ControllerVisibleType = Uncapitalize< keyof typeof ControllerVisibleTypes >; +const tfo = (operator: FilterSqlOperator) => { + const preStr = 'viz.common.enum.filterOperator.'; + return i18next.t(preStr + operator); +}; +const tft = (type: ControllerVisibleTypes) => { + const preStr = 'viz.common.enum.controllerVisibilityTypes.'; + return i18next.t(preStr + type); +}; +const getVisibleOptionItem = (type: ControllerVisibleTypes) => { + return { + name: tft(type), + value: type, + }; +}; +const getOperatorItem = (value: FilterSqlOperator) => { + return { + name: tfo(value), + value: value, + }; +}; export const VISIBILITY_TYPE_OPTION = [ - { name: '显示', value: ControllerVisibleTypes.Show }, - { name: '隐藏', value: ControllerVisibleTypes.Hide }, - { name: '条件', value: ControllerVisibleTypes.Condition }, + getVisibleOptionItem(ControllerVisibleTypes.Show), + getVisibleOptionItem(ControllerVisibleTypes.Hide), + getVisibleOptionItem(ControllerVisibleTypes.Condition), ]; export const ALL_SQL_OPERATOR_OPTIONS = [ - { name: '等于', value: FilterSqlOperator.Equal }, - { name: '不相等', value: FilterSqlOperator.NotEqual }, + getOperatorItem(FilterSqlOperator.Equal), + getOperatorItem(FilterSqlOperator.NotEqual), - { name: '包含', value: FilterSqlOperator.In }, - { name: '不包含', value: FilterSqlOperator.NotIn }, + getOperatorItem(FilterSqlOperator.In), + getOperatorItem(FilterSqlOperator.NotIn), - { name: '为空', value: FilterSqlOperator.Null }, - { name: '不为空', value: FilterSqlOperator.NotNull }, + getOperatorItem(FilterSqlOperator.Null), + getOperatorItem(FilterSqlOperator.NotNull), - { name: '前缀包含', value: FilterSqlOperator.PrefixContain }, - { name: '前缀不包含', value: FilterSqlOperator.NotPrefixContain }, + getOperatorItem(FilterSqlOperator.PrefixContain), + getOperatorItem(FilterSqlOperator.NotPrefixContain), - { name: '后缀包含', value: FilterSqlOperator.SuffixContain }, - { name: '后缀不包含', value: FilterSqlOperator.NotSuffixContain }, + getOperatorItem(FilterSqlOperator.SuffixContain), + getOperatorItem(FilterSqlOperator.NotSuffixContain), - { name: '区间', value: FilterSqlOperator.Between }, + getOperatorItem(FilterSqlOperator.Between), - { name: '大于或等于', value: FilterSqlOperator.GreaterThanOrEqual }, - { name: '小于或等于', value: FilterSqlOperator.LessThanOrEqual }, - { name: '大于', value: FilterSqlOperator.GreaterThan }, - { name: '小于', value: FilterSqlOperator.LessThan }, + getOperatorItem(FilterSqlOperator.GreaterThanOrEqual), + getOperatorItem(FilterSqlOperator.LessThanOrEqual), + getOperatorItem(FilterSqlOperator.GreaterThan), + getOperatorItem(FilterSqlOperator.LessThan), ]; export const SQL_OPERATOR_OPTIONS_TYPES = { @@ -193,6 +214,10 @@ export const SQL_OPERATOR_OPTIONS_TYPES = { FilterSqlOperator.In, FilterSqlOperator.NotIn, ], + [ControllerFacadeTypes.CheckboxGroup]: [ + FilterSqlOperator.In, + FilterSqlOperator.NotIn, + ], [ControllerFacadeTypes.RadioGroup]: [ FilterSqlOperator.Equal, FilterSqlOperator.NotEqual, diff --git a/frontend/src/app/pages/DashBoardPage/contexts/BoardActionContext.ts b/frontend/src/app/pages/DashBoardPage/contexts/BoardActionContext.ts index 6cce6a5f8..ef6e24c62 100644 --- a/frontend/src/app/pages/DashBoardPage/contexts/BoardActionContext.ts +++ b/frontend/src/app/pages/DashBoardPage/contexts/BoardActionContext.ts @@ -20,7 +20,7 @@ import { createContext } from 'react'; import { Widget } from '../pages/Board/slice/types'; export interface BoardActionContextProps { widgetUpdate: (widget: Widget) => void; - refreshWidgetsByFilter: (widget: Widget) => void; + refreshWidgetsByController: (widget: Widget) => void; updateBoard?: (callback: () => void) => void; onGenerateShareLink?: (date, usePwd) => any; onBoardToDownLoad: () => any; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/AutoDashboard/AutoBoardCore.tsx b/frontend/src/app/pages/DashBoardPage/pages/Board/AutoDashboard/AutoBoardCore.tsx index d04df39fe..7dd8454f5 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/AutoDashboard/AutoBoardCore.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/AutoDashboard/AutoBoardCore.tsx @@ -15,6 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Empty } from 'antd'; import { WidgetAllProvider } from 'app/pages/DashBoardPage/components/WidgetProvider/WidgetAllProvider'; import { BREAK_POINTS } from 'app/pages/DashBoardPage/constants'; import { BoardContext } from 'app/pages/DashBoardPage/contexts/BoardContext'; @@ -114,10 +115,10 @@ const AutoBoardCore: React.FC = ({ boardId }) => { const scrollThrottle = useRef(false); const lazyLoad = useCallback(() => { if (!gridWrapRef.current) return; - if (!scrollThrottle.current) { requestAnimationFrame(() => { const waitingItems = layoutInfos.current.filter(item => !item.rendered); + if (waitingItems.length > 0) { const { offsetHeight, scrollTop } = gridWrapRef.current!; waitingItems.forEach(item => { @@ -145,7 +146,14 @@ const AutoBoardCore: React.FC = ({ boardId }) => { lazyLoad(); gridWrapRef.current.removeEventListener('scroll', lazyLoad, false); gridWrapRef.current.addEventListener('scroll', lazyLoad, false); + // issues#339 + window.addEventListener('resize', lazyLoad, false); } + + return () => { + gridWrapRef?.current?.removeEventListener('scroll', lazyLoad, false); + window.removeEventListener('resize', lazyLoad, false); + }; }, [boardLoading, WidgetConfigsLen, lazyLoad]); const onLayoutChange = useCallback((layouts: Layout[]) => { @@ -168,27 +176,32 @@ const AutoBoardCore: React.FC = ({ boardId }) => { return ( - {boardLoading ? loading... : null} - - - - {boardChildren} - + {layoutWidgetConfigs.length ? ( + + + + {boardChildren} + + - + ) : ( + + + + )} ); @@ -222,4 +235,11 @@ const StyledContainer = styled(StyledBackground)` .grid-wrap::-webkit-scrollbar { width: 0 !important; } + + .empty { + display: flex; + flex: 1; + justify-content: center; + align-items: center; + } `; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/FreeDashboard/FreeBoardCore.tsx b/frontend/src/app/pages/DashBoardPage/pages/Board/FreeDashboard/FreeBoardCore.tsx index 25409d63d..fe9ff1c57 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/FreeDashboard/FreeBoardCore.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/FreeDashboard/FreeBoardCore.tsx @@ -15,6 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Empty } from 'antd'; import { WidgetAllProvider } from 'app/pages/DashBoardPage/components/WidgetProvider/WidgetAllProvider'; import { BoardConfigContext } from 'app/pages/DashBoardPage/contexts/BoardConfigContext'; import { BoardContext } from 'app/pages/DashBoardPage/contexts/BoardContext'; @@ -84,7 +85,13 @@ const FreeBoardCore: React.FC = memo( ref={refGridBackground} > - {boardChildren} + {widgetConfigs.length ? ( + boardChildren + ) : ( + + + + )} {showZoomCtrl && ( @@ -115,6 +122,14 @@ const Wrap = styled.div` flex: 1; -ms-overflow-style: none; overflow-y: hidden; + + .empty { + height: 100%; + display: flex; + flex: 1; + justify-content: center; + align-items: center; + } } .grid-background::-webkit-scrollbar { width: 0 !important; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/index.tsx b/frontend/src/app/pages/DashBoardPage/pages/Board/index.tsx index ac0869c7b..d9cf80c1a 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/index.tsx @@ -16,8 +16,7 @@ * limitations under the License. */ -import { LoadingOutlined } from '@ant-design/icons'; -import { message } from 'antd'; +import { message, Spin } from 'antd'; import useResizeObserver from 'app/hooks/useResizeObserver'; import { selectPublishLoading } from 'app/pages/MainPage/pages/VizPage/slice/selectors'; import { publishViz } from 'app/pages/MainPage/pages/VizPage/slice/thunks'; @@ -99,14 +98,19 @@ export const Board: React.FC = memo( }, [boardId, dispatch, fetchData, searchParams]); const [showBoardEditor, setShowBoardEditor] = useState(false); - - const toggleBoardEditor = (bool: boolean) => { - setShowBoardEditor(bool); - }; - const dashboard = useSelector((state: { board: BoardState }) => makeSelectBoardConfigById()(state, boardId), ); + const toggleBoardEditor = useCallback( + (bool: boolean) => { + setShowBoardEditor(bool); + if (!bool) { + dispatch(fetchBoardDetail({ dashboardRelId: dashboard?.id || '' })); + } + }, + [dashboard?.id, dispatch], + ); + const publishLoading = useSelector(selectPublishLoading); const onPublish = useCallback(() => { @@ -166,8 +170,8 @@ export const Board: React.FC = memo( ); } else { return ( - - loading + + ); } @@ -180,6 +184,7 @@ export const Board: React.FC = memo( allowManage, hideTitle, publishLoading, + toggleBoardEditor, onPublish, showZoomCtrl, ]); @@ -230,4 +235,10 @@ const Wrapper = styled.div<{}>` flex-direction: column; min-height: 0; } + .loading { + display: flex; + flex: 1; + justify-content: center; + align-items: center; + } `; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/asyncActions.ts b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/asyncActions.ts index bcd0a621e..09a600ad5 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/asyncActions.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/asyncActions.ts @@ -32,8 +32,11 @@ import { getWidgetInfoMapByServer, getWidgetMapByServer, } from '../../../utils/widget'; -import { PageInfo } from './../../../../MainPage/pages/ViewPage/slice/types'; -import { getChartWidgetDataAsync } from './thunk'; +import { + PageInfo, + View, +} from './../../../../MainPage/pages/ViewPage/slice/types'; +import { getChartWidgetDataAsync, getWidgetData } from './thunk'; import { BoardState, DataChart, ServerDashboard, VizRenderMode } from './types'; export const handleServerBoardAction = @@ -155,16 +158,25 @@ export const resetControllerAction = pageNo: 1, }; - Object.values(widgetMap) - .filter(it => it.config.type === 'chart') - .forEach(it => { - dispatch( - getChartWidgetDataAsync({ - boardId, - widgetId: it.id, - renderMode, - option: { pageInfo }, - }), - ); - }); + Object.values(widgetMap).forEach(widget => { + dispatch( + getWidgetData({ + boardId, + widget: widget, + renderMode, + option: { pageInfo }, + }), + ); + }); + }; + +export const saveToViewMapAction = + (serverView: View) => (dispatch, getState) => { + const boardState = getState() as { board: BoardState }; + const viewMap = boardState.board.viewMap; + let existed = serverView.id in viewMap; + if (!existed) { + const viewViews = getChartDataView([serverView], []); + dispatch(boardActions.setViewMap(viewViews)); + } }; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/index.ts b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/index.ts index 9694e0484..b2621c7a9 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/index.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/index.ts @@ -28,7 +28,7 @@ import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; import { createSlice } from 'utils/@reduxjs/toolkit'; import { PageInfo } from '../../../../MainPage/pages/ViewPage/slice/types'; import { createWidgetInfo } from '../../../utils/widget'; -import { getChartWidgetDataAsync, getWidgetDataAsync } from './thunk'; +import { getChartWidgetDataAsync, getControllerOptions } from './thunk'; import { BoardInfo, BoardState, Widget } from './types'; export const boardInit: BoardState = { @@ -196,6 +196,7 @@ const boardSlice = createSlice({ } state.widgetInfoRecord[boardId][widgetId].inLinking = toggle; }, + addFetchedItem( state, action: PayloadAction<{ boardId: string; widgetId: string }>, @@ -208,6 +209,7 @@ const boardSlice = createSlice({ ); } catch (error) {} }, + setBoardWidthHeight( state, action: PayloadAction<{ boardId: string; wh: [number, number] }>, @@ -236,6 +238,17 @@ const boardSlice = createSlice({ pageNo: 1, }; }, + setWidgetErrInfo( + state, + action: PayloadAction<{ + boardId: string; + widgetId: string; + errInfo?: string; + }>, + ) { + const { boardId, widgetId, errInfo } = action.payload; + state.widgetInfoRecord[boardId][widgetId].errInfo = errInfo; + }, resetControlWidgets( state, action: PayloadAction<{ @@ -254,38 +267,37 @@ const boardSlice = createSlice({ }, }, extraReducers: builder => { - // getWidgetDataAsync - builder.addCase(getWidgetDataAsync.pending, (state, action) => { + builder.addCase(getChartWidgetDataAsync.pending, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = true; } catch (error) {} }); - builder.addCase(getWidgetDataAsync.fulfilled, (state, action) => { + builder.addCase(getChartWidgetDataAsync.fulfilled, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = false; } catch (error) {} }); - builder.addCase(getWidgetDataAsync.rejected, (state, action) => { + builder.addCase(getChartWidgetDataAsync.rejected, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = false; } catch (error) {} }); - builder.addCase(getChartWidgetDataAsync.pending, (state, action) => { + builder.addCase(getControllerOptions.pending, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = true; } catch (error) {} }); - builder.addCase(getChartWidgetDataAsync.fulfilled, (state, action) => { + builder.addCase(getControllerOptions.fulfilled, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = false; } catch (error) {} }); - builder.addCase(getChartWidgetDataAsync.rejected, (state, action) => { + builder.addCase(getControllerOptions.rejected, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = false; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/thunk.ts b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/thunk.ts index 9f3b9094f..8037f9f30 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/thunk.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/thunk.ts @@ -6,14 +6,15 @@ import { VizRenderMode, Widget, } from 'app/pages/DashBoardPage/pages/Board/slice/types'; +import { getControlOptionQueryParams } from 'app/pages/DashBoardPage/utils/widgetToolKit/chart'; import { FilterSearchParams } from 'app/pages/MainPage/pages/VizPage/slice/types'; import { shareActions } from 'app/pages/SharePage/slice'; import { ExecuteToken, ShareVizInfo } from 'app/pages/SharePage/slice/types'; +import ChartDataset from 'app/types/ChartDataset'; import { RootState } from 'types'; import { request } from 'utils/request'; -import { errorHandle } from 'utils/utils'; +import { errorHandle, getErrorMessage } from 'utils/utils'; import { boardActions } from '.'; -import { getDistinctFields } from '../../../../../utils/fetch'; import { getChartWidgetRequestParams } from '../../../utils'; import { handleServerBoardAction } from './asyncActions'; import { selectBoardById, selectBoardWidgetMap } from './selector'; @@ -134,7 +135,7 @@ export const renderedWidgetAsync = createAsyncThunk< dispatch(boardActions.renderedWidgets({ boardId, widgetIds: [widgetId] })); // 2 widget getData dispatch( - getWidgetDataAsync({ boardId: boardId, widgetId: widgetId, renderMode }), + getWidgetData({ boardId: boardId, widget: curWidget, renderMode }), ); if (curWidget.config.type === 'container') { const content = curWidget.config.content as ContainerWidgetContent; @@ -149,7 +150,11 @@ export const renderedWidgetAsync = createAsyncThunk< // 2 widget getData subWidgetIds.forEach(wid => { dispatch( - getWidgetDataAsync({ boardId: boardId, widgetId: wid, renderMode }), + getWidgetData({ + boardId: boardId, + widget: widgetMap[wid], + renderMode, + }), ); }); } @@ -158,54 +163,29 @@ export const renderedWidgetAsync = createAsyncThunk< }, ); -export const getWidgetDataAsync = createAsyncThunk< +export const getWidgetData = createAsyncThunk< null, { boardId: string; - widgetId: string; + widget: Widget; renderMode: VizRenderMode | undefined; option?: getDataOption; }, { state: RootState } >( - 'board/getWidgetDataAsync', - async ({ boardId, widgetId, renderMode, option }, { getState, dispatch }) => { - const boardState = getState() as { board: BoardState }; - const curWidget = boardState.board.widgetRecord?.[boardId]?.[widgetId]; - if (!curWidget) return null; - dispatch(boardActions.renderedWidgets({ boardId, widgetIds: [widgetId] })); - switch (curWidget.config.type) { + 'board/getWidgetData', + ({ widget, renderMode, option }, { getState, dispatch }) => { + const boardId = widget.dashboardId; + dispatch(boardActions.renderedWidgets({ boardId, widgetIds: [widget.id] })); + const widgetId = widget.id; + switch (widget.config.type) { case 'chart': - try { - await dispatch( - getChartWidgetDataAsync({ boardId, widgetId, renderMode, option }), - ); - if (renderMode === 'schedule') { - dispatch( - boardActions.addFetchedItem({ - boardId: curWidget.dashboardId, - widgetId: curWidget.id, - }), - ); - } - } catch (error) { - if (renderMode === 'schedule') { - dispatch( - boardActions.addFetchedItem({ - boardId: curWidget.dashboardId, - widgetId: curWidget.id, - }), - ); - } - } - return null; - case 'media': - return null; - case 'container': + dispatch( + getChartWidgetDataAsync({ boardId, widgetId, renderMode, option }), + ); return null; case 'controller': - await dispatch(getControllerOptions({ widget: curWidget, renderMode })); - + dispatch(getControllerOptions({ boardId, widgetId, renderMode })); return null; default: return null; @@ -252,36 +232,67 @@ export const getChartWidgetDataAsync = createAsyncThunk< return null; } let widgetData; - if (renderMode === 'read') { - const { data } = await request({ - method: 'POST', - url: `data-provider/execute`, - data: requestParams, - }); - widgetData = { ...data, id: widgetId }; - } else { - const executeTokenMap = (getState() as RootState)?.share?.executeTokenMap; - const dataChart = dataChartMap[curWidget.datachartId]; - const viewId = viewMap[dataChart.viewId].id; - const executeToken = executeTokenMap?.[viewId]; - const { data } = await request({ - method: 'POST', - url: `share/execute`, - params: { - executeToken: executeToken?.token, - password: executeToken?.password, - }, - data: requestParams, - }); - widgetData = { ...data, id: widgetId }; - } + try { + if (renderMode === 'read') { + const { data } = await request({ + method: 'POST', + url: `data-provider/execute`, + data: requestParams, + }); + widgetData = { ...data, id: widgetId }; + } else { + const executeTokenMap = (getState() as RootState)?.share + ?.executeTokenMap; + const dataChart = dataChartMap[curWidget.datachartId]; + const viewId = viewMap[dataChart.viewId].id; + const executeToken = executeTokenMap?.[viewId]; + const { data } = await request({ + method: 'POST', + url: `share/execute`, + params: { + executeToken: executeToken?.token, + password: executeToken?.password, + }, + data: requestParams, + }); + widgetData = { ...data, id: widgetId }; + } + dispatch(boardActions.setWidgetData(widgetData as WidgetData)); + dispatch( + boardActions.changePageInfo({ + boardId, + widgetId, + pageInfo: widgetData.pageInfo, + }), + ); + dispatch( + boardActions.setWidgetErrInfo({ + boardId, + widgetId, + errInfo: undefined, + }), + ); + } catch (error) { + dispatch( + boardActions.setWidgetErrInfo({ + boardId, + widgetId, + errInfo: getErrorMessage(error), + }), + ); - dispatch(boardActions.setWidgetData(widgetData as WidgetData)); + dispatch( + boardActions.setWidgetData({ + id: widgetId, + columns: [], + rows: [], + } as WidgetData), + ); + } dispatch( - boardActions.changePageInfo({ + boardActions.addFetchedItem({ boardId, widgetId, - pageInfo: widgetData.pageInfo, }), ); return null; @@ -291,32 +302,91 @@ export const getChartWidgetDataAsync = createAsyncThunk< // 根据 字段获取 Controller 的options export const getControllerOptions = createAsyncThunk< null, - { widget: Widget; renderMode: VizRenderMode | undefined }, + { boardId: string; widgetId: string; renderMode: VizRenderMode | undefined }, { state: RootState } >( 'board/getControllerOptions', - async ({ widget, renderMode }, { getState, dispatch }) => { + async ({ boardId, widgetId, renderMode }, { getState, dispatch }) => { + dispatch( + boardActions.renderedWidgets({ + boardId: boardId, + widgetIds: [widgetId], + }), + ); + const boardState = getState() as { board: BoardState }; + const viewMap = boardState.board.viewMap; + const widgetMapMap = boardState.board.widgetRecord; + const widgetMap = widgetMapMap[boardId]; + const widget = widgetMap[widgetId]; + if (!widget) return null; const content = widget.config.content as ControllerWidgetContent; const config = content.config; + if (!Array.isArray(config.assistViewFields)) return null; + if (config.assistViewFields.length !== 2) return null; + const executeTokenMap = (getState() as RootState)?.share?.executeTokenMap; - if (config.assistViewFields && Array.isArray(config.assistViewFields)) { - // 请求 - const [viewId, viewField] = config.assistViewFields; - const executeToken = executeTokenMap?.[viewId]; - const dataset = await getDistinctFields( - viewId, - viewField, - undefined, - executeToken, + const [viewId, viewField] = config.assistViewFields; + + const executeToken = executeTokenMap?.[viewId]; + + const view = viewMap[viewId]; + if (!view) return null; + const requestParams = getControlOptionQueryParams({ + view, + field: viewField, + curWidget: widget, + widgetMap, + }); + + if (!requestParams) { + return null; + } + let widgetData; + try { + if (executeToken && renderMode !== 'read') { + const { data } = await request({ + method: 'POST', + url: `share/execute`, + params: { + executeToken: executeToken?.token, + password: executeToken?.password, + }, + data: requestParams, + }); + widgetData = { ...data, id: widget.id }; + dispatch(boardActions.setWidgetData(widgetData as WidgetData)); + } else { + const { data } = await request({ + method: 'POST', + url: `data-provider/execute`, + data: requestParams, + }); + widgetData = { ...data, id: widget.id }; + dispatch(boardActions.setWidgetData(widgetData as WidgetData)); + } + dispatch( + boardActions.setWidgetErrInfo({ + boardId, + widgetId, + errInfo: undefined, + }), ); + } catch (error) { dispatch( - boardActions.setWidgetData({ - ...dataset, - id: widget.id, - } as unknown as WidgetData), + boardActions.setWidgetErrInfo({ + boardId, + widgetId, + errInfo: getErrorMessage(error), + }), ); } + dispatch( + boardActions.addFetchedItem({ + boardId, + widgetId, + }), + ); return null; }, ); diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/types.ts b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/types.ts index 4a2e17581..1a7db5d1d 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/types.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/types.ts @@ -27,7 +27,10 @@ import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; import { DeltaStatic } from 'quill'; import { Layout } from 'react-grid-layout'; import { ChartDataSectionField } from '../../../../../types/ChartConfig'; -import { PageInfo } from '../../../../MainPage/pages/ViewPage/slice/types'; +import { + PageInfo, + View, +} from '../../../../MainPage/pages/ViewPage/slice/types'; import { BorderStyleType, LAYOUT_COLS, @@ -74,7 +77,7 @@ export interface SaveDashboard extends Omit { } export interface ServerDashboard extends Omit { config: string; - views: ServerView[]; + views: View[]; datacharts: ServerDatachart[]; widgets: ServerWidget[]; } @@ -161,6 +164,9 @@ export interface JumpConfigField { } export interface JumpConfig { open: boolean; + targetType: string; + URL: string; + queryName: string; field: JumpConfigField; target: JumpConfigTarget; filter: JumpConfigFilter; @@ -183,6 +189,7 @@ export interface WidgetInfo { inLinking: boolean; //是否在触发联动 selected: boolean; pageInfo: Partial; + errInfo?: string; selectItems?: string[]; parameters?: any; } @@ -213,11 +220,11 @@ export interface Relation { } /** * @controlToWidget Controller associated widgets - * @controlToControl Controller associated Controller visible - * @widgetToWidget widget inOther WidgetContainer + * @controlToControl Controller associated Controller visible cascade + * @widgetToWidget widget inOther WidgetContainer linkage * */ export interface RelationConfig { - type: 'controlToWidget' | 'controlToControl' | 'widgetToWidget'; + type: RelationConfigType; controlToWidget?: { widgetRelatedViewIds: string[]; }; @@ -227,6 +234,14 @@ export interface RelationConfig { linkerColumn: string; }; } +export type RelationConfigType = + | 'controlToWidget' // control - ChartFetch will del + | 'controlToChartFetch' // control - ChartFetch + | 'controlToControl' // control - control -visible will del + | 'controlToControlVisible' // control - control -visible + | 'controlToControlCascade' // control - control -Cascade + | 'widgetToWidget' // linkage will del + | 'chartToChartLinkage'; // linkage export interface RelatedView { viewId: string; relatedCategory: ChartDataViewFieldCategory; @@ -390,16 +405,12 @@ export interface DataChart { status: any; } export interface DataChartConfig { + aggregation: boolean | undefined; chartConfig: ChartConfig; chartGraphId: string; computedFields: any[]; } -export interface ServerView extends ChartDataView { - model: string; -} -// TODO - export type ColsType = typeof LAYOUT_COLS; // Dashboard view model @@ -458,4 +469,5 @@ export interface ServerDatachart extends Omit { export interface getDataOption { pageInfo?: Partial; + sorters?: Array<{ column: string; operator?: string; aggOperator?: string }>; } diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddChartBtn.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddChartBtn.tsx index 84fb34802..242c35b21 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddChartBtn.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddChartBtn.tsx @@ -24,15 +24,9 @@ import { } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { selectVizs } from 'app/pages/MainPage/pages/VizPage/slice/selectors'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import { addDataChartWidgets, addWrapChartWidget } from '../../slice/thunk'; import ChartSelectModalModal from '../ChartSelectModal'; import { ChartWidgetDropdown, ToolBtnProps } from './ToolBarItem'; @@ -40,16 +34,12 @@ const AddChartBtn: React.FC = props => { const dispatch = useDispatch(); const { boardId, boardType } = useContext(BoardContext); const orgId = useSelector(selectOrgId); - // const chartOptions = useSelector(selectDataChartList); const chartOptionsMock = useSelector(selectVizs); const chartOptions = useMemo( () => chartOptionsMock.filter(item => item.relType !== 'DASHBOARD'), [chartOptionsMock], ); - useEffect(() => { - // dispatch(getDataCharts(orgId)); - }, [dispatch, orgId]); const [dataChartVisible, setDataChartVisible] = useState(false); const [widgetChartVisible, setWidgetChartVisible] = useState(false); diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddControl/AddControlBtn.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddControl/AddControlBtn.tsx index 98fcf798d..4a32f3387 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddControl/AddControlBtn.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddControl/AddControlBtn.tsx @@ -17,6 +17,7 @@ */ import { ControlOutlined } from '@ant-design/icons'; import { Dropdown, Menu } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { BoardConfigContext } from 'app/pages/DashBoardPage/contexts/BoardConfigContext'; import { WidgetType } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; @@ -28,12 +29,16 @@ import { BoardToolBarContext } from '../context/BoardToolBarContext'; import { WithTipButton } from '../ToolBarItem'; export interface AddControlBtnProps {} export interface ButtonItemType { - name: string; + name?: string; icon: any; type: T; - disabled: boolean; + disabled?: boolean; } export const AddControlBtn: React.FC = () => { + const t = useI18NPrefix(`viz.board.action`); + const tFilterName = useI18NPrefix(`viz.common.enum.controllerFacadeTypes`); + const tType = useI18NPrefix(`viz.board.controlTypes`); + const tWt = useI18NPrefix(`viz.widget.type`); const { boardId, boardType, showLabel } = useContext(BoardToolBarContext); const dispatch = useDispatch(); const { config: boardConfig } = useContext(BoardConfigContext); @@ -49,34 +54,24 @@ export const AddControlBtn: React.FC = () => { }; const conventionalControllers: ButtonItemType[] = [ { - name: '单选下拉菜单', icon: '', type: ControllerFacadeTypes.DropdownList, - disabled: false, }, { - name: '多选下拉菜单', icon: '', type: ControllerFacadeTypes.MultiDropdownList, - disabled: false, }, { - name: '单选按钮', icon: '', type: ControllerFacadeTypes.RadioGroup, - disabled: false, }, - // { - // name: '复选框', - // icon: '', - // type: ControllerFacadeTypes.RadioGroup, - // disabled: false, - // }, { - name: '文本', + icon: '', + type: ControllerFacadeTypes.CheckboxGroup, + }, + { icon: '', type: ControllerFacadeTypes.Text, - disabled: false, }, // { // name: '单选下拉树', @@ -91,38 +86,28 @@ export const AddControlBtn: React.FC = () => { // disabled: false, // }, ]; - const dateControllers = [ + const dateControllers: ButtonItemType[] = [ { - name: '日期范围', icon: '', type: ControllerFacadeTypes.RangeTime, - disabled: false, }, { - name: '日期', icon: '', type: ControllerFacadeTypes.Time, - disabled: false, }, ]; - const numericalControllers = [ + const numericalControllers: ButtonItemType[] = [ { - name: '数值范围', icon: '', type: ControllerFacadeTypes.RangeValue, - disabled: false, }, { - name: '数值', icon: '', type: ControllerFacadeTypes.Value, - disabled: false, }, { - name: '滑块', icon: '', type: ControllerFacadeTypes.Slider, - disabled: false, }, // { // name: '范围滑块', @@ -133,13 +118,11 @@ export const AddControlBtn: React.FC = () => { ]; const buttonControllers: ButtonItemType[] = [ { - name: '查询按钮', icon: '', type: 'query', disabled: !!hasQueryControl, }, { - name: '重置按钮', icon: '', type: 'reset', disabled: !!hasResetControl, @@ -150,31 +133,40 @@ export const AddControlBtn: React.FC = () => { }; const controlerItems = (
+ * 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.common; + +public class DateUtils { + + private static final String[] FMT = {"y", "M", "d", "H", "m", "s", "S"}; + + public static String inferDateFormat(String src) { + int fmtIdx = 0; + boolean findMatch = false; + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < src.length(); i++) { + char chr = src.charAt(i); + if (Character.isDigit(chr)) { + findMatch = true; + stringBuilder.append(FMT[fmtIdx]); + } else { + if (findMatch) { + fmtIdx++; + findMatch = false; + } + stringBuilder.append(chr); + } + if (fmtIdx == FMT.length - 1 && i < src.length() - 1) { + stringBuilder.append(src.substring(i + 1)); + break; + } + } + return stringBuilder.toString(); + } + +} diff --git a/core/src/main/java/datart/core/common/JavascriptUtils.java b/core/src/main/java/datart/core/common/JavascriptUtils.java index 8eb370d56..89e2f6b9c 100644 --- a/core/src/main/java/datart/core/common/JavascriptUtils.java +++ b/core/src/main/java/datart/core/common/JavascriptUtils.java @@ -18,13 +18,14 @@ package datart.core.common; -import datart.core.base.exception.BaseException; import datart.core.base.exception.Exceptions; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; +import javax.script.ScriptException; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -36,7 +37,14 @@ public class JavascriptUtils { engineFactory = new NashornScriptEngineFactory(); } - public static Object invoke(String path, String functionName, Object... args) throws Exception { + public static Object invoke(Invocable invocable, String functionName, Object... args) throws Exception { + if (invocable != null) { + return invocable.invokeFunction(functionName, args); + } + return null; + } + + public static Invocable load(String path) throws IOException, ScriptException { InputStream stream = JavascriptUtils.class.getClassLoader().getResourceAsStream(path); if (stream == null) { Exceptions.notFound(path); @@ -45,10 +53,10 @@ public static Object invoke(String path, String functionName, Object... args) th ScriptEngine engine = engineFactory.getScriptEngine(); engine.eval(reader); if (engine instanceof Invocable) { - Invocable invocable = (Invocable) engine; - return invocable.invokeFunction(functionName, args); + return (Invocable) engine; } return null; } } + } diff --git a/core/src/main/java/datart/core/common/WebUtils.java b/core/src/main/java/datart/core/common/WebUtils.java index 35a9355d1..3ec3bafde 100644 --- a/core/src/main/java/datart/core/common/WebUtils.java +++ b/core/src/main/java/datart/core/common/WebUtils.java @@ -70,19 +70,19 @@ public static T screenShot(String url, OutputType outputType, int imageWi ExpectedCondition ConditionOfHeight = ExpectedConditions.presenceOfElementLocated(By.id("height")); wait.until(ExpectedConditions.and(ConditionOfSign, ConditionOfWidth, ConditionOfHeight)); - int contentWidth = Integer.parseInt(webDriver.findElement(By.id("width")).getAttribute("value")); + Double contentWidth = Double.parseDouble(webDriver.findElement(By.id("width")).getAttribute("value")); - int contentHeight = Integer.parseInt(webDriver.findElement(By.id("height")).getAttribute("value")); + Double contentHeight = Double.parseDouble(webDriver.findElement(By.id("height")).getAttribute("value")); if (imageWidth != contentWidth) { // scale the window - webDriver.manage().window().setSize(new Dimension(imageWidth, contentHeight)); + webDriver.manage().window().setSize(new Dimension(imageWidth, contentHeight.intValue())); Thread.sleep(1000); } // scale the window again - contentWidth = Integer.parseInt(webDriver.findElement(By.id("width")).getAttribute("value")); - contentHeight = Integer.parseInt(webDriver.findElement(By.id("height")).getAttribute("value")); - webDriver.manage().window().setSize(new Dimension(contentWidth, contentHeight)); + contentWidth = Double.parseDouble(webDriver.findElement(By.id("width")).getAttribute("value")); + contentHeight = Double.parseDouble(webDriver.findElement(By.id("height")).getAttribute("value")); + webDriver.manage().window().setSize(new Dimension(contentWidth.intValue(), contentHeight.intValue())); Thread.sleep(1000); TakesScreenshot screenshot = (TakesScreenshot) webDriver; 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 bbfa6fb7a..7de431686 100644 --- a/core/src/main/java/datart/core/data/provider/Column.java +++ b/core/src/main/java/datart/core/data/provider/Column.java @@ -29,6 +29,8 @@ public class Column implements Serializable { private String name; private ValueType type; + + private String fmt; public Column(String name, ValueType type) { this.name = name; diff --git a/core/src/main/java/datart/core/data/provider/DataProvider.java b/core/src/main/java/datart/core/data/provider/DataProvider.java index 0fd475333..4bd59da3e 100644 --- a/core/src/main/java/datart/core/data/provider/DataProvider.java +++ b/core/src/main/java/datart/core/data/provider/DataProvider.java @@ -26,7 +26,6 @@ import java.io.InputStream; import java.sql.SQLException; import java.util.Collections; -import java.util.List; import java.util.Set; public abstract class DataProvider extends AutoCloseBean { @@ -76,6 +75,9 @@ public DataProviderConfigTemplate getConfigTemplate() throws IOException { } } + public abstract String getConfigDisplayName(String name); + + public abstract String getConfigDescription(String name); public abstract Dataframe execute(DataProviderSource config, QueryScript script, ExecuteParam executeParam) throws Exception; @@ -119,4 +121,5 @@ public Set supportedStdFunctions(DataProviderSource source) { public void resetSource(DataProviderSource source) { } + } \ No newline at end of file diff --git a/core/src/main/java/datart/core/data/provider/DataProviderConfigTemplate.java b/core/src/main/java/datart/core/data/provider/DataProviderConfigTemplate.java index 9fd98f7fb..01b60dcdc 100644 --- a/core/src/main/java/datart/core/data/provider/DataProviderConfigTemplate.java +++ b/core/src/main/java/datart/core/data/provider/DataProviderConfigTemplate.java @@ -29,6 +29,8 @@ public class DataProviderConfigTemplate implements Serializable { private String name; + private String displayName; + private List attributes; @Data @@ -52,7 +54,7 @@ public static class Attribute implements Serializable { private List options; - private Object children; + private List children; } diff --git a/core/src/main/java/datart/core/data/provider/QueryScript.java b/core/src/main/java/datart/core/data/provider/QueryScript.java index a99dd530f..4d7ba6ccf 100644 --- a/core/src/main/java/datart/core/data/provider/QueryScript.java +++ b/core/src/main/java/datart/core/data/provider/QueryScript.java @@ -45,11 +45,10 @@ public class QueryScript implements Serializable { private List variables; - private Map schema; + private Map schema; public String toQueryKey() { return 'Q' + DigestUtils.md5Hex(JSON.toJSONString(this)); } - } \ No newline at end of file diff --git a/core/src/main/java/datart/core/data/provider/ScriptVariable.java b/core/src/main/java/datart/core/data/provider/ScriptVariable.java index 115055757..8727aa158 100644 --- a/core/src/main/java/datart/core/data/provider/ScriptVariable.java +++ b/core/src/main/java/datart/core/data/provider/ScriptVariable.java @@ -18,10 +18,12 @@ package datart.core.data.provider; +import datart.core.base.consts.Const; import datart.core.base.consts.ValueType; import datart.core.base.consts.VariableTypeEnum; import lombok.Data; import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.StringUtils; import java.util.Set; @@ -35,6 +37,8 @@ public class ScriptVariable extends TypedValue { private Set values; + private String nameWithQuote; + private boolean expression; @Override @@ -56,4 +60,12 @@ public ScriptVariable(String name, VariableTypeEnum type, ValueType valueType, S this.expression = expression; } + public String getNameWithQuote() { + if (nameWithQuote != null) { + return nameWithQuote; + } + nameWithQuote = StringUtils.prependIfMissing(name, Const.DEFAULT_VARIABLE_QUOTE); + nameWithQuote = StringUtils.appendIfMissing(nameWithQuote, Const.DEFAULT_VARIABLE_QUOTE); + return nameWithQuote; + } } \ No newline at end of file diff --git a/core/src/main/java/datart/core/data/provider/SingleTypedValue.java b/core/src/main/java/datart/core/data/provider/SingleTypedValue.java index 8659e5ac0..5cb9dcfe0 100644 --- a/core/src/main/java/datart/core/data/provider/SingleTypedValue.java +++ b/core/src/main/java/datart/core/data/provider/SingleTypedValue.java @@ -20,8 +20,10 @@ import datart.core.base.consts.ValueType; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor public class SingleTypedValue extends TypedValue { private Object value; diff --git a/core/src/main/java/datart/core/mappers/ext/RelRoleResourceMapperExt.java b/core/src/main/java/datart/core/mappers/ext/RelRoleResourceMapperExt.java index 99f66cea8..38aa9a208 100644 --- a/core/src/main/java/datart/core/mappers/ext/RelRoleResourceMapperExt.java +++ b/core/src/main/java/datart/core/mappers/ext/RelRoleResourceMapperExt.java @@ -63,7 +63,7 @@ public interface RelRoleResourceMapperExt extends RelRoleResourceMapper { @Select({ "" }) - int countUserPermission(String resourceId, String userId); + int countRolePermission(String resourceId, String roleId); @Select({ "", }) int batchInsert(List elements); + + @Delete({ + "DELETE FROM variable where view_id=#{viewId}" + }) + int deleteByView(String viewId); } diff --git a/core/src/main/resources/mybatis-generator/generatorConfig.xml b/core/src/main/resources/mybatis-generator/generatorConfig.xml index fde6c3d08..7069717e1 100644 --- a/core/src/main/resources/mybatis-generator/generatorConfig.xml +++ b/core/src/main/resources/mybatis-generator/generatorConfig.xml @@ -17,8 +17,8 @@ + connectionURL="jdbc:mysql://127.0.0.1:3306/datart" userId="root" + password=""> diff --git a/data-providers/file-data-provider/pom.xml b/data-providers/file-data-provider/pom.xml index e3e1d310e..18ee29208 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-alpha.3 + 1.0.0-beta.0 ../../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 c3fb8b576..8b11daae1 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 @@ -21,10 +21,7 @@ import datart.core.base.consts.ValueType; import datart.core.base.exception.BaseException; import datart.core.base.exception.Exceptions; -import datart.core.common.CSVParse; -import datart.core.common.FileUtils; -import datart.core.common.POIUtils; -import datart.core.common.UUIDGenerator; +import datart.core.common.*; import datart.core.data.provider.Column; import datart.core.data.provider.DataProviderSource; import datart.core.data.provider.Dataframe; @@ -46,6 +43,23 @@ public class FileDataProvider extends DefaultDataProvider { public static final String FILE_PATH = "path"; + private static final String I18N_PREFIX = "config.template.file."; + + @Override + public String getConfigDisplayName(String name) { + return MessageResolver.getMessage(I18N_PREFIX + name); + } + + @Override + public String getConfigDescription(String name) { + String message = MessageResolver.getMessage(I18N_PREFIX + name + ".desc"); + if (message.startsWith(I18N_PREFIX)) { + return null; + } else { + return message; + } + } + @Override public List loadFullDataFromSource(DataProviderSource config) throws Exception { Map properties = config.getProperties(); diff --git a/data-providers/file-data-provider/src/main/resources/file-data-provider.json b/data-providers/file-data-provider/src/main/resources/file-data-provider.json index b468bd49b..33a0530bc 100644 --- a/data-providers/file-data-provider/src/main/resources/file-data-provider.json +++ b/data-providers/file-data-provider/src/main/resources/file-data-provider.json @@ -20,19 +20,11 @@ "required": true, "defaultValue": "", "type": "string", - "description": "文件格式,目前支持excel 和 csv。", "options": [ "XLSX", "CSV" ] }, - { - "name": "path", - "required": true, - "defaultValue": "", - "type": "string", - "description": "文件路径,上传后自动生成" - }, { "name": "columns", "defaultValue": "", @@ -44,15 +36,13 @@ "name": "cacheEnable", "required": false, "defaultValue": true, - "type": "bool", - "description": "是否开启本地缓存。开启后,文件解析结果将被缓存。" + "type": "bool" }, { "name": "cacheTimeout", "required": false, "type": "string", - "defaultValue": "30", - "description": "缓存超时时间(分钟)" + "defaultValue": "30" } ] } \ No newline at end of file diff --git a/data-providers/http-data-provider/pom.xml b/data-providers/http-data-provider/pom.xml index ccbd396f7..5a564262b 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-alpha.3 + 1.0.0-beta.0 ../../pom.xml diff --git a/data-providers/http-data-provider/src/main/java/datart/data/provider/HttpDataProvider.java b/data-providers/http-data-provider/src/main/java/datart/data/provider/HttpDataProvider.java index 22933a477..1e55b0a27 100644 --- a/data-providers/http-data-provider/src/main/java/datart/data/provider/HttpDataProvider.java +++ b/data-providers/http-data-provider/src/main/java/datart/data/provider/HttpDataProvider.java @@ -20,7 +20,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import datart.core.common.MessageResolver; import datart.core.common.UUIDGenerator; +import datart.core.data.provider.DataProviderConfigTemplate; import datart.core.data.provider.DataProviderSource; import datart.core.data.provider.Dataframe; import lombok.extern.slf4j.Slf4j; @@ -29,11 +31,9 @@ import org.springframework.util.CollectionUtils; import java.io.IOException; +import java.io.InputStream; import java.net.URISyntaxException; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; @Slf4j public class HttpDataProvider extends DefaultDataProvider { @@ -64,6 +64,8 @@ public class HttpDataProvider extends DefaultDataProvider { private static final String CONTENT_TYPE = "contentType"; + private static final String I18N_PREFIX = "config.template.http."; + private final static ObjectMapper MAPPER; static { @@ -102,6 +104,21 @@ public String getConfigFile() { return "http-data-provider.json"; } + @Override + public String getConfigDisplayName(String name) { + return MessageResolver.getMessage(I18N_PREFIX + name); + } + + @Override + public String getConfigDescription(String name) { + String message = MessageResolver.getMessage(I18N_PREFIX + name + ".desc"); + if (message.startsWith(I18N_PREFIX)) { + return null; + } else { + return message; + } + } + private HttpRequestParam convert2RequestParam(Map schema) throws ClassNotFoundException { HttpRequestParam httpRequestParam = new HttpRequestParam(); diff --git a/data-providers/http-data-provider/src/main/resources/http-data-provider.json b/data-providers/http-data-provider/src/main/resources/http-data-provider.json index a589800a5..65f65bd24 100644 --- a/data-providers/http-data-provider/src/main/resources/http-data-provider.json +++ b/data-providers/http-data-provider/src/main/resources/http-data-provider.json @@ -29,7 +29,6 @@ { "name": "property", "defaultValue": "", - "description": "Http返回结果中,JSON数组的属性名称。嵌套结构用 . 隔开。如 data.list", "type": "string" }, { @@ -50,13 +49,11 @@ { "name": "timeout", "defaultValue": 0, - "description": "请求超时时间", "type": "number" }, { "name": "responseParser", "defaultValue": "", - "description": "请求结果解析器,自定义解析器时,指定解析器的全类名", "type": "string" }, { @@ -82,15 +79,13 @@ "name": "cacheEnable", "required": false, "defaultValue": true, - "type": "bool", - "description": "是否开启本地缓存。开启后,HTTP请求结果将会缓存到服务端。" + "type": "bool" }, { "name": "cacheTimeout", "required": false, "type": "string", - "defaultValue": "5", - "description": "缓存超时时间(分钟)" + "defaultValue": "5" } ] } \ No newline at end of file diff --git a/data-providers/jdbc-data-provider/pom.xml b/data-providers/jdbc-data-provider/pom.xml index 79328b2b5..8e5bdd357 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-alpha.3 + 1.0.0-beta.0 ../../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 e0f83fd54..239141e99 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 @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy; import datart.core.base.exception.Exceptions; import datart.core.common.FileUtils; +import datart.core.common.MessageResolver; import datart.core.data.provider.*; import datart.data.provider.base.DataProviderException; import datart.data.provider.jdbc.JdbcDriverInfo; @@ -48,6 +49,8 @@ public class JdbcDataProvider extends DataProvider { public static final String DRIVER_CLASS = "driverClass"; + private static final String I18N_PREFIX = "config.template.jdbc."; + /** * 获取连接时最大等待时间(毫秒) */ @@ -157,6 +160,7 @@ public boolean validateFunction(DataProviderSource source, String snippet) { public DataProviderConfigTemplate getConfigTemplate() throws IOException { DataProviderConfigTemplate configTemplate = super.getConfigTemplate(); for (DataProviderConfigTemplate.Attribute attribute : configTemplate.getAttributes()) { + attribute.setDisplayName(MessageResolver.getMessage("config.template.jdbc." + attribute.getName())); if (attribute.getName().equals("dbType")) { List jdbcDriverInfos = ProviderFactory.loadDriverInfoFromResource(); List dbInfos = jdbcDriverInfos.stream().map(info -> { @@ -172,6 +176,21 @@ public DataProviderConfigTemplate getConfigTemplate() throws IOException { return configTemplate; } + @Override + public String getConfigDisplayName(String name) { + return MessageResolver.getMessage(I18N_PREFIX + name); + } + + @Override + public String getConfigDescription(String name) { + String message = MessageResolver.getMessage(I18N_PREFIX + name + ".desc"); + if (message.startsWith(I18N_PREFIX)) { + return null; + } else { + return message; + } + } + @Override public void close() throws IOException { 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 00c200fc9..791b17d7e 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 @@ -46,7 +46,6 @@ public void destroy(DataSource dataSource) { private Properties configDataSource(JdbcProperties properties) { Properties pro = new Properties(); - //connect params pro.setProperty(DruidDataSourceFactory.PROP_DRIVERCLASSNAME, properties.getDriverClass()); pro.setProperty(DruidDataSourceFactory.PROP_URL, properties.getUrl()); 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 fe76b5a76..61340240f 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 @@ -199,7 +199,7 @@ protected Dataframe execute(String selectSql, PageInfo pageInfo) throws SQLExcep public Dataframe execute(QueryScript script, ExecuteParam executeParam) throws Exception { //If server aggregation is enabled, query the full data before performing server aggregation if (executeParam.isServerAggregate()) { - return executeLocally(script, executeParam); + return executeInLocal(script, executeParam); } else { return executeOnSource(script, executeParam); } @@ -240,7 +240,6 @@ public SqlDialect getSqlDialect() { if (sqlDialect != null) { return sqlDialect; } - if (StringUtils.isNotBlank(driverInfo.getSqlDialect())) { try { Class> clz = Class.forName(driverInfo.getSqlDialect()); @@ -305,7 +304,7 @@ protected List getColumns(ResultSet rs) throws SQLException { /** * 本地执行,从数据源拉取全量数据,在本地执行聚合操作 */ - protected Dataframe executeLocally(QueryScript script, ExecuteParam executeParam) throws Exception { + protected Dataframe executeInLocal(QueryScript script, ExecuteParam executeParam) throws Exception { SqlScriptRender render = new SqlScriptRender(script , executeParam , getSqlDialect() 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 7d5bdce45..6595cea12 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 @@ -1,28 +1,19 @@ { "type": "JDBC", "name": "jdbc-data-provider", - "syntax": { - "name": "SQL", - "keywords": [ - "SELECT", - "FROM", - "JOIN" - ] - }, "attributes": [ { + "code": "dbType", "name": "dbType", "type": "string", "required": true, - "defaultValue": "", - "description": "database type" + "defaultValue": "" }, { "name": "url", "type": "string", "required": true, - "defaultValue": "", - "description": "connect url" + "defaultValue": "" }, { "name": "user", @@ -45,15 +36,13 @@ "name": "serverAggregate", "type": "bool", "required": false, - "defaultValue": false, - "description": "是否开启服务端聚合" + "defaultValue": false }, { "name": "properties", "type": "object", "required": false, - "defaultValue": "", - "description": "Druid连接池其它配置参数" + "defaultValue": "" } ] } \ No newline at end of file diff --git a/data-providers/pom.xml b/data-providers/pom.xml index 70030b755..950981ee4 100644 --- a/data-providers/pom.xml +++ b/data-providers/pom.xml @@ -5,7 +5,7 @@ datart-parent datart - 1.0.0-alpha.3 + 1.0.0-beta.0 4.0.0 diff --git a/data-providers/src/main/java/codegen/Parser.jj b/data-providers/src/main/java/codegen/Parser.jj index c9090f9a5..99043f31a 100644 --- a/data-providers/src/main/java/codegen/Parser.jj +++ b/data-providers/src/main/java/codegen/Parser.jj @@ -1847,9 +1847,9 @@ SqlLiteral JoinType() : | { joinType = JoinType.INNER; } | - [ ] { joinType = JoinType.LEFT; } + [ || ] { joinType = JoinType.LEFT; } | - [ ] { joinType = JoinType.RIGHT; } + [ || ] { joinType = JoinType.RIGHT; } | [ ] { joinType = JoinType.FULL; } | @@ -2071,6 +2071,10 @@ SqlNode TableRef2(boolean lateral) : } { ( + // datart: function as table + LOOKAHEAD(256,TableFunctionCall(getPos())) + tableRef = TableFunctionCall(getPos()) + | LOOKAHEAD(2) tableRef = TableRefWithHintsOpt() [ @@ -2133,6 +2137,7 @@ SqlNode TableRef2(boolean lateral) : } | tableRef = ExtendedTableRef() + ) [ tableRef = Pivot(tableRef) 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 cdcc488bb..5c6083ae5 100644 --- a/data-providers/src/main/java/datart/data/provider/DefaultDataProvider.java +++ b/data-providers/src/main/java/datart/data/provider/DefaultDataProvider.java @@ -20,17 +20,20 @@ import datart.core.base.PageInfo; import datart.core.base.consts.ValueType; import datart.core.base.exception.Exceptions; +import datart.core.common.DateUtils; import datart.core.data.provider.*; -import datart.data.provider.base.DataProviderException; import datart.data.provider.calcite.SqlParserUtils; import datart.data.provider.local.LocalDB; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateParser; +import org.apache.commons.lang3.time.FastDateFormat; import org.springframework.util.CollectionUtils; import java.io.IOException; import java.sql.SQLException; +import java.text.ParseException; import java.util.*; import java.util.stream.Collectors; @@ -195,9 +198,9 @@ protected List> parseValues(List> values, List return values; } if (values.get(0).size() != columns.size()) { - Exceptions.msg( "message.provider.default.schema", values.get(0).size() + ":" + columns.size()); + Exceptions.msg("message.provider.default.schema", values.get(0).size() + ":" + columns.size()); } - values.parallelStream().forEach(vals -> { + values.stream().forEach(vals -> { for (int i = 0; i < vals.size(); i++) { Object val = vals.get(i); if (val == null) { @@ -216,10 +219,28 @@ protected List> parseValues(List> values, List val = null; } else if (NumberUtils.isDigits(val.toString())) { val = Long.parseLong(val.toString()); - } else { + } else if (NumberUtils.isNumber(val.toString())) { val = Double.parseDouble(val.toString()); + } else { + val = null; } break; + case DATE: + String fmt = columns.get(i).getFmt(); + if (StringUtils.isBlank(fmt)) { + fmt = DateUtils.inferDateFormat(val.toString()); + columns.get(i).setFmt(fmt); + } + if (StringUtils.isNotBlank(fmt)) { + DateParser parser = FastDateFormat.getInstance(fmt); + try { + val = parser.parse(val.toString()); + } catch (ParseException e) { + val = null; + } + } else { + val = null; + } default: } vals.set(i, val); diff --git a/data-providers/src/main/java/datart/data/provider/ProviderManager.java b/data-providers/src/main/java/datart/data/provider/ProviderManager.java index 1d49e8768..c8ee2673c 100644 --- a/data-providers/src/main/java/datart/data/provider/ProviderManager.java +++ b/data-providers/src/main/java/datart/data/provider/ProviderManager.java @@ -19,6 +19,7 @@ package datart.data.provider; import datart.core.base.exception.Exceptions; +import datart.core.common.MessageResolver; import datart.core.data.provider.*; import datart.data.provider.optimize.DataProviderExecuteOptimizer; import lombok.extern.slf4j.Slf4j; @@ -53,7 +54,21 @@ public List getSupportedDataProviders() { @Override public DataProviderConfigTemplate getSourceConfigTemplate(String type) throws IOException { - return getDataProviderService(type).getConfigTemplate(); + DataProvider providerService = getDataProviderService(type); + DataProviderConfigTemplate configTemplate = providerService.getConfigTemplate(); + if (!CollectionUtils.isEmpty(configTemplate.getAttributes())) { + for (DataProviderConfigTemplate.Attribute attribute : configTemplate.getAttributes()) { + attribute.setDisplayName(providerService.getConfigDisplayName(attribute.getName())); + attribute.setDescription(providerService.getConfigDescription(attribute.getName())); + if (!CollectionUtils.isEmpty(attribute.getChildren())) { + for (DataProviderConfigTemplate.Attribute child : attribute.getChildren()) { + child.setDisplayName(providerService.getConfigDisplayName(child.getName())); + child.setDescription(providerService.getConfigDescription(child.getName())); + } + } + } + } + return configTemplate; } @Override @@ -120,36 +135,28 @@ public void updateSource(DataProviderSource source) { providerService.resetSource(source); } - private void excludeColumns(Dataframe data, Set columns) { + private void excludeColumns(Dataframe data, Set include) { if (data == null || CollectionUtils.isEmpty(data.getColumns()) - || columns == null - || columns.size() == 0 - || columns.contains("*")) { + || include == null + || include.size() == 0 + || include.contains("*")) { return; } List excludeIndex = new LinkedList<>(); for (int i = 0; i < data.getColumns().size(); i++) { Column column = data.getColumns().get(i); - if (!columns.contains(column.getName())) { + if (!include.contains(column.getName())) { excludeIndex.add(i); - data.getColumns().remove(column); } } if (excludeIndex.size() > 0) { - List> rows = data.getRows().parallelStream().map(row -> { - List r = new LinkedList<>(); - for (int i = 0; i < row.size(); i++) { - if (excludeIndex.size() > 0 && i == excludeIndex.get(0)) { - excludeIndex.remove(0); - } else { - r.add(row.get(i)); - } + data.getRows().parallelStream().forEach(row -> { + for (Integer index : excludeIndex) { + row.set(index, null); } - return r; - }).collect(Collectors.toList()); - data.setRows(rows); + }); } } 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 e37c0d231..aae4682e0 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 @@ -18,11 +18,12 @@ package datart.data.provider.calcite; -import org.apache.calcite.sql.SqlCall; -import org.apache.calcite.sql.SqlFunction; -import org.apache.calcite.sql.SqlOperator; +import org.apache.calcite.sql.*; import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.apache.calcite.sql.util.SqlBasicVisitor; +import org.apache.calcite.sql.validate.SqlNameMatchers; + +import java.util.LinkedList; public class SqlFunctionRegisterVisitor extends SqlBasicVisitor { @@ -30,13 +31,20 @@ public class SqlFunctionRegisterVisitor extends SqlBasicVisitor { public Object visit(SqlCall call) { SqlOperator operator = call.getOperator(); if (operator instanceof SqlFunction) { - registerIfNotExists(operator); + registerIfNotExists((SqlFunction) operator); } return operator.acceptCall(this, call); } - private void registerIfNotExists(SqlOperator sqlFunction) { - SqlStdOperatorTable.instance().register(sqlFunction); + private void registerIfNotExists(SqlFunction sqlFunction) { + SqlStdOperatorTable opTab = SqlStdOperatorTable.instance(); + LinkedList list = new LinkedList<>(); + opTab.lookupOperatorOverloads(sqlFunction.getSqlIdentifier(), null, SqlSyntax.FUNCTION, list, + SqlNameMatchers.withCaseSensitive(sqlFunction.getSqlIdentifier().isComponentQuoted(0))); + if (list.size() > 0) { + return; + } + opTab.register(sqlFunction); } } 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 0118b8c40..dc21af313 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 @@ -38,9 +38,7 @@ public static boolean validateQuery(SqlNode sqlCall) { return true; } - if (sqlCall instanceof SqlDdl || sqlCall instanceof SqlDelete || sqlCall instanceof SqlUpdate) { - Exceptions.tr(DataProviderException.class, "message.sql.op.forbidden", sqlCall.getKind() + ":" + sqlCall); - } + Exceptions.tr(DataProviderException.class, "message.sql.op.forbidden", sqlCall.getKind() + ":" + sqlCall); return false; } diff --git a/data-providers/src/main/java/datart/data/provider/calcite/SqlVariableVisitor.java b/data-providers/src/main/java/datart/data/provider/calcite/SqlVariableVisitor.java index c55cc8a93..d9f3e5d29 100644 --- a/data-providers/src/main/java/datart/data/provider/calcite/SqlVariableVisitor.java +++ b/data-providers/src/main/java/datart/data/provider/calcite/SqlVariableVisitor.java @@ -107,7 +107,6 @@ private VariablePlaceholder createVariablePlaceholder(SqlCall sqlCall, String va return new TrueVariablePlaceholder(originalSqlFragment); } - variable.setName(variableName); if (VariableTypeEnum.PERMISSION.equals(variable.getType())) { return new PermissionVariablePlaceholder(variable, sqlDialect, sqlCall, originalSqlFragment); } else { diff --git a/data-providers/src/main/java/datart/data/provider/calcite/dialect/ImpalaSqlDialectSupport.java b/data-providers/src/main/java/datart/data/provider/calcite/dialect/ImpalaSqlDialectSupport.java index b9d8a735e..7ebf4d4df 100644 --- a/data-providers/src/main/java/datart/data/provider/calcite/dialect/ImpalaSqlDialectSupport.java +++ b/data-providers/src/main/java/datart/data/provider/calcite/dialect/ImpalaSqlDialectSupport.java @@ -19,6 +19,7 @@ package datart.data.provider.calcite.dialect; import datart.data.provider.jdbc.JdbcDriverInfo; +import org.apache.calcite.sql.SqlAbstractDateTimeLiteral; import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.SqlWriter; @@ -32,4 +33,9 @@ public ImpalaSqlDialectSupport(JdbcDriverInfo driverInfo) { public void unparseOffsetFetch(SqlWriter writer, SqlNode offset, SqlNode fetch) { super.unparseFetchUsingLimit(writer, offset, fetch); } + + @Override + public void unparseDateTimeLiteral(SqlWriter writer, SqlAbstractDateTimeLiteral literal, int leftPrec, int rightPrec) { + writer.literal("'" + literal.toFormattedString() + "'"); + } } 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 ff0ca8a88..49309352d 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,7 +29,6 @@ import datart.data.provider.calcite.SqlValidateUtils; import datart.data.provider.calcite.SqlParserUtils; import datart.data.provider.calcite.SqlVariableVisitor; -import datart.data.provider.calcite.parser.impl.SqlParserImpl; import datart.data.provider.freemarker.FreemarkerContext; import datart.data.provider.local.LocalDB; import datart.data.provider.script.ReplacementPair; @@ -37,12 +36,9 @@ import datart.data.provider.script.VariablePlaceholder; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; -import org.apache.calcite.config.Lex; import org.apache.calcite.sql.SqlDialect; import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.parser.SqlParseException; -import org.apache.calcite.sql.parser.SqlParser; -import org.apache.calcite.sql.validate.SqlConformanceEnum; import org.apache.commons.lang3.CharUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.util.CollectionUtils; @@ -85,7 +81,7 @@ public String replaceVariables(String selectSql) { Map variableMap = queryScript.getVariables() .stream() - .collect(Collectors.toMap(v -> getVariablePattern(v.getName()), variable -> variable)); + .collect(Collectors.toMap(ScriptVariable::getNameWithQuote, variable -> variable)); String srcSql = selectSql; SqlNode sqlNode = null; try { @@ -128,15 +124,14 @@ public String render(boolean withExecuteParam, boolean withPage, boolean onlySel if (size != 1) { Exceptions.tr(DataProviderException.class, "message.provider.variable.expression.size", size + ":" + variable.getValues()); } - script = script.replace(getVariablePattern(variable.getName()), Iterables.get(variable.getValues(), 0)); + script = script.replace(variable.getNameWithQuote(), Iterables.get(variable.getValues(), 0)); } } - // find select sql final String selectSql0 = findSelectSql(script); if (StringUtils.isEmpty(selectSql0)) { - Exceptions.tr(DataProviderException.class,"message.no.valid.sql"); + Exceptions.tr(DataProviderException.class, "message.no.valid.sql"); } String selectSql = cleanupSql(selectSql0); @@ -177,14 +172,6 @@ private String findSelectSql(String script) { return selectSql; } - private SqlParser sqlParser() { - SqlParser.Config config = SqlParser.config() - .withLex(Lex.MYSQL) - .withParserFactory(SqlParserImpl.FACTORY) - .withConformance(SqlConformanceEnum.LENIENT); - return SqlParser.create("", config); - } - private SqlNode parseSql(String sql) throws SqlParseException { return SqlParserUtils.createParser(sql, sqlDialect).parseQuery(); } 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 549c77fe9..13e6840d4 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 @@ -17,7 +17,6 @@ */ package datart.data.provider.local; -import com.google.common.collect.Lists; import datart.core.base.PageInfo; import datart.core.base.consts.Const; import datart.core.base.exception.Exceptions; @@ -34,24 +33,22 @@ import org.apache.calcite.sql.SqlDialect; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateFormatUtils; +import org.h2.jdbc.JdbcSQLNonTransientException; import org.h2.tools.DeleteDbFiles; import org.h2.tools.SimpleResultSet; import java.sql.*; -import java.sql.Date; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; @Slf4j public class LocalDB { - private static final String MEM_URL = "jdbc:h2:mem:/LOG=0;DATABASE_TO_UPPER=false;CACHE_SIZE=65536;LOCK_MODE=0;UNDO_LOG=0"; + 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; @@ -61,12 +58,8 @@ public class LocalDB { private static final String SELECT_START_SQL = "SELECT * FROM `%s` "; - private static final String INSERT_SQL = "INSERT INTO `%s` VALUES %s"; - private static final String CREATE_TEMP_TABLE = "CREATE TABLE IF NOT EXISTS `%s` AS (SELECT * FROM FUNCTION_TABLE('%s'))"; - private static final int MAX_INSERT_BATCH = 5_000; - private static final String CACHE_EXPIRE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS `cache_expire` ( `source_id` VARCHAR(128),`expire_time` DATETIME )"; private static final String SET_EXPIRE_SQL = "INSERT INTO `cache_expire` VALUES( '%s', PARSEDATETIME('%s','%s')) "; @@ -146,7 +139,11 @@ private static void registerDataAsTable(Dataframe dataframe, Connection connecti TEMP_RS_CACHE.put(dataframe.getId(), dataframe); // register temporary table String sql = String.format(CREATE_TEMP_TABLE, dataframe.getName(), dataframe.getId()); - connection.prepareStatement(sql).execute(); + try { + connection.prepareStatement(sql).execute(); + } catch (JdbcSQLNonTransientException e) { + //忽略重复创建表导致的异常 + } } /** @@ -185,6 +182,7 @@ public static Dataframe executeLocalQuery(QueryScript queryScript, ExecuteParam queryScript = new QueryScript(); queryScript.setScript(String.format(SELECT_START_SQL, srcData.get(0).getName())); queryScript.setVariables(Collections.emptyList()); + queryScript.setSourceId(srcData.get(0).getName()); } return persistent ? executeInLocalDB(queryScript, executeParam, srcData, expire) : executeInMemDB(queryScript, executeParam, srcData); } @@ -296,29 +294,26 @@ private static Dataframe execute(Connection connection, QueryScript queryScript, } - private static void createTable(String tableName, List columns, Connection connection) throws SQLException { - String sql = tableCreateSQL(tableName, columns); - connection.createStatement().execute(sql); - } - - private static void insertTableData(Dataframe dataframe, Connection connection) throws SQLException { - if (dataframe == null) { - return; - } -// DeleteDbFiles.execute(); - createTable(dataframe.getName(), dataframe.getColumns(), connection); - - List values = createInsertValues(dataframe.getRows(), dataframe.getColumns()); - - List> partition = Lists.partition(values, MAX_INSERT_BATCH); - for (List vals : partition) { - String insertSql = String.format(INSERT_SQL, dataframe.getName(), String.join(",", vals)); - connection.createStatement().execute(insertSql); - } - } +// private static void createTable(String tableName, List columns, Connection connection) throws SQLException { +// String sql = tableCreateSQL(tableName, columns); +// connection.createStatement().execute(sql); +// } + +// private static void insertTableData(Dataframe dataframe, Connection connection) throws SQLException { +// if (dataframe == null) { +// return; +// } +// createTable(dataframe.getName(), dataframe.getColumns(), connection); +// List values = createInsertValues(dataframe.getRows(), dataframe.getColumns()); +// List> partition = Lists.partition(values, MAX_INSERT_BATCH); +// for (List vals : partition) { +// String insertSql = String.format(INSERT_SQL, dataframe.getName(), String.join(",", vals)); +// connection.createStatement().execute(insertSql); +// } +// } private static Connection getConnection(boolean persistent, String database) throws SQLException { - String url = persistent ? getDatabaseUrl(database) : MEM_URL; + String url = persistent ? getDatabaseUrl(database) : MEM_URL + "DB" +database + H2_PARAM; return DriverManager.getConnection(url); } @@ -331,43 +326,43 @@ private static String tableCreateSQL(String name, List columns) { return String.format(TABLE_CREATE_SQL_TEMPLATE, name, sj); } - private static List createInsertValues(List> data, List columns) { - return data.parallelStream().map(row -> { - StringJoiner stringJoiner = new StringJoiner(",", "(", ")"); - for (int i = 0; i < row.size(); i++) { - Object val = row.get(i); - if (val == null || StringUtils.isBlank(val.toString())) { - stringJoiner.add(null); - continue; - } - Column column = columns.get(i); - switch (column.getType()) { - case NUMERIC: - stringJoiner.add(val.toString()); - break; - case DATE: - String valStr; - if (val instanceof Timestamp) { - valStr = DateFormatUtils.format((Timestamp) val, Const.DEFAULT_DATE_FORMAT); - } else if (val instanceof Date) { - valStr = DateFormatUtils.format((Date) val, Const.DEFAULT_DATE_FORMAT); - } else if (val instanceof LocalDateTime) { - valStr = ((LocalDateTime) val).format(DateTimeFormatter.ofPattern(Const.DEFAULT_DATE_FORMAT)); - } else { - valStr = null; - } - if (valStr != null) { - valStr = "PARSEDATETIME('" + valStr + "','" + Const.DEFAULT_DATE_FORMAT + "')"; - } - stringJoiner.add(valStr); - break; - default: - stringJoiner.add("'" + StringEscapeUtils.escapeSql(val.toString()) + "'"); - } - } - return stringJoiner.toString(); - }).collect(Collectors.toList()); - } +// private static List createInsertValues(List> data, List columns) { +// return data.parallelStream().map(row -> { +// StringJoiner stringJoiner = new StringJoiner(",", "(", ")"); +// for (int i = 0; i < row.size(); i++) { +// Object val = row.get(i); +// if (val == null || StringUtils.isBlank(val.toString())) { +// stringJoiner.add(null); +// continue; +// } +// Column column = columns.get(i); +// switch (column.getType()) { +// case NUMERIC: +// stringJoiner.add(val.toString()); +// break; +// case DATE: +// String valStr; +// if (val instanceof Timestamp) { +// valStr = DateFormatUtils.format((Timestamp) val, Const.DEFAULT_DATE_FORMAT); +// } else if (val instanceof Date) { +// valStr = DateFormatUtils.format((Date) val, Const.DEFAULT_DATE_FORMAT); +// } else if (val instanceof LocalDateTime) { +// valStr = ((LocalDateTime) val).format(DateTimeFormatter.ofPattern(Const.DEFAULT_DATE_FORMAT)); +// } else { +// valStr = null; +// } +// if (valStr != null) { +// valStr = "PARSEDATETIME('" + valStr + "','" + Const.DEFAULT_DATE_FORMAT + "')"; +// } +// stringJoiner.add(valStr); +// break; +// default: +// stringJoiner.add("'" + StringEscapeUtils.escapeSql(val.toString()) + "'"); +// } +// } +// return stringJoiner.toString(); +// }).collect(Collectors.toList()); +// } private static String getDatabaseUrl(String database) { if (database == null) { @@ -375,7 +370,7 @@ private static String getDatabaseUrl(String database) { } else { database = toDatabase(database); } - return fileUrl = String.format("jdbc:h2:file:%s/%s;LOG=0;DATABASE_TO_UPPER=false;CACHE_SIZE=65536;LOCK_MODE=0;UNDO_LOG=0", getDbFileBasePath(), database); + return fileUrl = 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/optimize/DataProviderExecuteOptimizer.java b/data-providers/src/main/java/datart/data/provider/optimize/DataProviderExecuteOptimizer.java index a77163580..a5991ebac 100644 --- a/data-providers/src/main/java/datart/data/provider/optimize/DataProviderExecuteOptimizer.java +++ b/data-providers/src/main/java/datart/data/provider/optimize/DataProviderExecuteOptimizer.java @@ -54,20 +54,24 @@ public Dataframe runOptimize(String queryKey, DataProviderSource source, QuerySc } } - public Dataframe getFromCache(String queryKey) { - Cache cache = CacheFactory.getCache(); - if (cache != null) { - return cache.get(queryKey); - } else { - return null; + try { + Cache cache = CacheFactory.getCache(); + if (cache != null) { + return cache.get(queryKey); + } + } catch (Exception e) { } + return null; } public void setCache(String queryKey, Dataframe dataframe, int cacheExpires) { - Cache cache = CacheFactory.getCache(); - if (cache != null) { - cache.put(queryKey, dataframe, cacheExpires); + try { + Cache cache = CacheFactory.getCache(); + if (cache != null) { + cache.put(queryKey, dataframe, cacheExpires); + } + } catch (Exception e) { } } 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 52a38590f..dc1b0ff36 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 @@ -52,17 +52,4 @@ public ScriptRender(QueryScript queryScript, ExecuteParam executeParam, String v this.variableQuote = variableQuote; } - protected String getVariablePattern(String variableName) { - variableName = StringUtils.prependIfMissing(variableName, variableQuote); - variableName = StringUtils.appendIfMissing(variableName, variableQuote); - return variableName; - } - -// private String variableValueString(ScriptVariable variable) { -// if (variable == null || CollectionUtils.isEmpty(variable.getValues())) { -// return ""; -// } else { -// return String.join(",", variable.getValues()); -// } -// } } \ 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 8e34a983a..c0fe4b7e8 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 @@ -141,14 +141,16 @@ protected void replaceVariable(SqlCall sqlCall) { } if (sqlNode instanceof SqlCall) { replaceVariable((SqlCall) sqlNode); + } else if (sqlNode instanceof SqlLiteral) { + // pass } else if (sqlNode instanceof SqlIdentifier) { - if (sqlNode.toString().equals(variable.getName())) { + if (sqlNode.toString().equals(variable.getNameWithQuote())) { sqlCall.setOperand(i, SqlNodeUtils.toSingleSqlLiteral(variable, sqlNode.getParserPosition())); } } else if (sqlNode instanceof SqlNodeList) { SqlNodeList nodeList = (SqlNodeList) sqlNode; List otherNodes = Arrays.stream(nodeList.toArray()) - .filter(node -> !node.toString().equals(variable.getName())) + .filter(node -> !node.toString().equals(variable.getNameWithQuote())) .collect(Collectors.toList()); if (otherNodes.size() == nodeList.size()) { diff --git a/docker-compose.yml.example b/docker-compose.yml.example deleted file mode 100644 index 52693f538..000000000 --- a/docker-compose.yml.example +++ /dev/null @@ -1,15 +0,0 @@ -version: '3' -services: - datart: - image: java:8 - hostname: datart - container_name: datart - restart: always - volumes: - - "{datart application root path}:/datart" - entrypoint: [ "sh","/datart/bin/datart-server.sh" ] - environment: - - TZ=Asia/Shanghai - logging: - options: - max-size: "1g" \ No newline at end of file diff --git a/frontend/.babelrc b/frontend/.babelrc new file mode 100644 index 000000000..50144f1f2 --- /dev/null +++ b/frontend/.babelrc @@ -0,0 +1,23 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + // 避免转换成 CommonJS + "modules": false, + // 使用 loose 模式,避免产生副作用 + "loose": true + } + ] + ], + "plugins": [ + "@babel/plugin-external-helpers", + [ + // 开启 babel 各依赖联动,由此插件负责自动导入 helper 辅助函数,从而形成沙箱 polyfill + "@babel/plugin-transform-runtime", + { + "useESModules": true // 关闭 esm 转化,交由 rollup 处理,同上防止冲突 + } + ] + ] + } \ No newline at end of file diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 1b2d2016b..420ad9f53 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -10,6 +10,21 @@ module.exports = { plugins: ['prettier'], rules: { 'prettier/prettier': ['error', prettierOptions], + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: 'lodash', + message: 'suggest import xxx from `lodash/xxx`', + }, + { + name: 'uuid', + message: 'suggest import xxx from `uuid/dist/xxx`', + }, + ], + }, + ], }, parserOptions: { ecmaVersion: 2018, diff --git a/frontend/.gitignore b/frontend/.gitignore index 978b23d43..ca2ab7ebe 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -31,3 +31,5 @@ yarn-error.log* # vscode .vscode + +/public/task diff --git a/frontend/craco.config.js b/frontend/craco.config.js index c75f1ce8e..6b1b071fd 100644 --- a/frontend/craco.config.js +++ b/frontend/craco.config.js @@ -3,6 +3,7 @@ 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'); const { when, whenDev, @@ -56,7 +57,11 @@ module.exports = { ], webpack: { alias: {}, - plugins: [new WebpackBar(), new MonacoWebpackPlugin({ languages: [''] })], + plugins: [ + new WebpackBar(), + new MonacoWebpackPlugin({ languages: [''] }), + // new BundleAnalyzerPlugin(), + ], configure: (webpackConfig, { env, paths }) => { // paths.appPath='public' // paths.appBuild = 'dist'; // 配合输出打包修改文件目录 diff --git a/frontend/package.json b/frontend/package.json index f837e24cb..cd134a53c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,9 +14,12 @@ "author": "", "license": "Apache-2.0", "scripts": { - "analyze": "craco build && source-map-explorer 'build/static/js/*.js'", + "bootstrap": "npm install --legacy-peer-deps", "start": "craco start", "build": "cross-env GENERATE_SOURCEMAP=false craco build", + "build:task": "rollup -c", + "build:all": "npm run build:task && npm run build", + "build:analyze": "craco build && source-map-explorer 'build/static/js/*.js'", "test": "craco test", "test:coverage": "npm run test -- --watchAll=false --coverage", "checkTs": "tsc --noEmit", @@ -84,6 +87,8 @@ "dependencies": { "@ant-design/icons": "^4.5.0", "@ant-design/pro-table": "2.60.1", + "@antv/s2": "^1.3.0", + "@antv/s2-react": "^1.3.0", "@dinero.js/currencies": "^2.0.0-alpha.8", "@reduxjs/toolkit": "^1.5.0", "@types/react-color": "^3.0.5", @@ -114,7 +119,6 @@ "react-dnd-html5-backend": "^14.0.0", "react-dom": "^17.0.1", "react-draggable": "^4.4.3", - "react-frame-component": "^5.1.0", "react-grid-layout": "^1.2.4", "react-helmet-async": "^1.0.7", "react-hotkeys-hook": "^3.4.0", @@ -125,6 +129,7 @@ "react-resizable": "^1.11.1", "react-resize-detector": "^6.7.6", "react-router-dom": "^5.2.0", + "react-window": "^1.8.6", "redux-undo": "^1.0.1", "reveal.js": "^4.1.0", "split.js": "^1.6.4", @@ -132,9 +137,17 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@babel/core": "^7.15.8", + "@babel/preset-env": "^7.15.8", "@commitlint/cli": "^12.0.1", "@commitlint/config-conventional": "^12.0.1", "@craco/craco": "^6.1.1", + "@rollup/plugin-babel": "^5.3.0", + "@rollup/plugin-commonjs": "^21.0.1", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^13.0.6", + "@rollup/plugin-replace": "^2.4.2", + "@rollup/plugin-typescript": "^8.3.0", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.8.0", @@ -170,6 +183,8 @@ "prettier-plugin-organize-imports": "^2.3.3", "react-scripts": "4.0.3", "react-test-renderer": "^17.0.1", + "rollup": "^2.62.0", + "rollup-plugin-cleanup": "^3.2.1", "serve": "^11.3.2", "source-map-explorer": "^2.5.2", "styled-components": "^5.3.0", diff --git a/frontend/public/custom-chart-plugins/demo-custom-line-chart.js b/frontend/public/custom-chart-plugins/demo-custom-line-chart.js index 16b9bbace..65c4f76dd 100644 --- a/frontend/public/custom-chart-plugins/demo-custom-line-chart.js +++ b/frontend/public/custom-chart-plugins/demo-custom-line-chart.js @@ -17,6 +17,8 @@ */ function DemoCustomLineChart({ dHelper }) { + const svgIcon = ``; + return { config: { datas: [ @@ -408,7 +410,26 @@ function DemoCustomLineChart({ dHelper }) { ], }, ], - settings: [], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -470,9 +491,6 @@ function DemoCustomLineChart({ dHelper }) { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, }, }, ], @@ -482,7 +500,7 @@ function DemoCustomLineChart({ dHelper }) { meta: { id: 'demo-custom-line-chart', name: '[DEMO]用户自定义折线图', - icon: 'chart', + icon: svgIcon, requirements: [ { group: 1, @@ -530,7 +548,7 @@ function DemoCustomLineChart({ dHelper }) { .filter(c => c.type === 'aggregate') .flatMap(config => config.rows || []); - const objDataColumns = dHelper.transfromToObjectArray( + const objDataColumns = dHelper.transformToObjectArray( dataset.rows, dataset.columns, ); diff --git a/frontend/public/custom-chart-plugins/demo-d3js-scatter-chart.js b/frontend/public/custom-chart-plugins/demo-d3js-scatter-chart.js index 650ad9466..a68407762 100644 --- a/frontend/public/custom-chart-plugins/demo-d3js-scatter-chart.js +++ b/frontend/public/custom-chart-plugins/demo-d3js-scatter-chart.js @@ -47,6 +47,26 @@ function D3JSScatterChart({ dHelper }) { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -173,7 +193,7 @@ function D3JSScatterChart({ dHelper }) { .flatMap(config => config.rows || []); // 数据转换,根据Datart提供了Helper转换工具 - const objDataColumns = dHelper.transfromToObjectArray( + const objDataColumns = dHelper.transformToObjectArray( dataset.rows, dataset.columns, ); diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js new file mode 100644 index 000000000..30e83916e --- /dev/null +++ b/frontend/rollup.config.js @@ -0,0 +1,41 @@ +/* eslint-disable import/no-anonymous-default-export */ +import { babel } from '@rollup/plugin-babel'; +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import replace from '@rollup/plugin-replace'; +import typescript from '@rollup/plugin-typescript'; +import path from 'path'; +import cleanup from 'rollup-plugin-cleanup'; +export default { + input: 'src/task.ts', // 打包入口 + output: { + // 打包出口 + name: 'getQueryData', // namespace + file: path.resolve(__dirname, 'public/task/index.js'), // 最终打包出来的文件路径和文件名 + format: 'umd', // umd/amd/cjs/iife + }, + plugins: [ + json(), + nodeResolve({ + extensions: ['.js', '.ts'], + }), + // 解析TypeScript + typescript({ + tsconfig: path.resolve(__dirname, 'tsconfig.json'), + }), + // 将 CommonJS 转换成 ES2015 模块供 Rollup 处理 + commonjs(), + // es6--> es5 + babel({ + babelHelpers: 'runtime', + exclude: 'node_modules/**', + presets: [['@babel/preset-env', { modules: false }]], + comments: false, + }), + cleanup(), + replace({ + 'console.log': '//console.log', + }), + ], +}; diff --git a/frontend/src/app/LoginAuthRoute.tsx b/frontend/src/app/LoginAuthRoute.tsx index 8214d0df5..3dcaf77f1 100644 --- a/frontend/src/app/LoginAuthRoute.tsx +++ b/frontend/src/app/LoginAuthRoute.tsx @@ -1,3 +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 { AuthorizedRoute } from 'app/components'; import { getToken } from 'utils/auth'; import { LazyMainPage } from './pages/MainPage/Loadable'; diff --git a/frontend/src/app/assets/fonts/iconfont.css b/frontend/src/app/assets/fonts/iconfont.css index c5b40a831..a19e73c73 100644 --- a/frontend/src/app/assets/fonts/iconfont.css +++ b/frontend/src/app/assets/fonts/iconfont.css @@ -1,8 +1,8 @@ @font-face { - font-family: 'iconfont'; /* Project id 2869064 */ - src: url('iconfont.woff2?t=1637912668357') format('woff2'), - url('iconfont.woff?t=1637912668357') format('woff'), - url('iconfont.ttf?t=1637912668357') format('truetype'); + font-family: 'iconfont'; + src: url('iconfont.woff2?t=1639456509648') format('woff2'), + url('iconfont.woff?t=1639456509648') format('woff'), + url('iconfont.ttf?t=1639456509648') format('truetype'); } .iconfont { @@ -14,6 +14,10 @@ -moz-osx-font-smoothing: grayscale; } +.icon-rich-text:before { + content: '\e7a4'; +} + .icon-graph-circular:before { content: '\e7d0'; } diff --git a/frontend/src/app/assets/fonts/iconfont.ttf b/frontend/src/app/assets/fonts/iconfont.ttf index c15a98170..abe9580e1 100644 Binary files a/frontend/src/app/assets/fonts/iconfont.ttf and b/frontend/src/app/assets/fonts/iconfont.ttf differ diff --git a/frontend/src/app/assets/fonts/iconfont.woff b/frontend/src/app/assets/fonts/iconfont.woff index 41636bee4..39a91360e 100644 Binary files a/frontend/src/app/assets/fonts/iconfont.woff and b/frontend/src/app/assets/fonts/iconfont.woff differ diff --git a/frontend/src/app/assets/fonts/iconfont.woff2 b/frontend/src/app/assets/fonts/iconfont.woff2 index 6bbb94360..6d5446701 100644 Binary files a/frontend/src/app/assets/fonts/iconfont.woff2 and b/frontend/src/app/assets/fonts/iconfont.woff2 differ diff --git a/frontend/src/app/assets/theme/colorsConfig.ts b/frontend/src/app/assets/theme/colorsConfig.ts new file mode 100644 index 000000000..2868f6efd --- /dev/null +++ b/frontend/src/app/assets/theme/colorsConfig.ts @@ -0,0 +1,476 @@ +/** + * 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 { BLACK, WHITE } from 'styles/StyleConstants'; +export const defaultPalette = [ + '#FAFAFA', + '#9E9E9E', + '#E3F2FD', + '#FFF3E0', + '#FFEBEE', + '#E0F2F1', + '#E8F5E9', + '#FFF8E1', + '#EDE7F6', + '#FCE4EC', + '#EFEBE9', + '#F5F5F5', + '#757575', + '#BBDEFB', + '#FFE0B2', + '#FFCDD2', + '#B2DFDB', + '#C8E6C9', + '#FFECB3', + '#D1C4E9', + '#F8BBD0', + '#D7CCC8', + '#EEEEEE', + '#616161', + '#64B5F6', + '#FFB74D', + '#E57373', + '#64FFDA', + '#81C784', + '#FFD740', + '#9575CD', + '#FF4081', + '#A1887F', + '#E0E0E0', + '#424242', + '#1976D2', + '#F57C00', + '#D32F2F', + '#1DE9B6', + '#388E3C', + '#FFC400', + '#512DA8', + '#F50057', + '#5D4037', + '#BDBDBD', + '#212121', + '#0D47A1', + '#E65100', + '#B71C1C', + '#00BFA5', + '#1B5E20', + '#FFAB00', + '#311B92', + '#C51162', + '#3E2723', +]; +export const colorThemes = [ + { + id: 'default', + colors: [ + '#448aff', + '#ffab40', + '#ff5252', + '#a7ffeb', + '#4caf50', + '#ffecb3', + '#7c4dff', + '#f8bbd0', + '#795548', + '#f5f5f5', + ], + en: { + title: 'Default', + }, + zh: { + title: '默认', + }, + }, + { + id: 'default20', + colors: [ + '#448aff', + '#e3f2fd', + '#ffab40', + '#fff3e0', + '#4caf50', + '#b9f6ca', + '#ffd740', + '#ffecb3', + '#009688', + '#a7ffeb', + '#ff5252', + '#ffcdd2', + '#9e9e9e', + '#f5f5f5', + '#FF4081', + '#f8bbd0', + '#7c4dff', + '#b388ff', + '#795548', + '#d7ccc8', + ], + en: { + title: 'Default 20', + }, + zh: { + title: '默认20色', + }, + }, + { + id: 'spectrum', + colors: [ + '#16b4bb', + '#3f28c9', + '#e17315', + '#cf167d', + '#7d6ff9', + '#40e15c', + '#2068e7', + '#5a20a2', + '#d8b509', + '#bf5b0e', + '#217d59', + '#8ced43', + ], + en: { + title: 'Spectrum', + }, + zh: { + title: 'Spectrum', + }, + }, + { + id: 'retrometro', + colors: [ + '#b33dc6', + '#27aeef', + '#87bc45', + '#bdcf32', + '#ede15b', + '#edbf33', + '#ef9b20', + '#f46a9b', + '#ea5545', + ], + en: { + title: 'Retro Metro', + }, + zh: { + title: 'Retro Metro', + }, + }, + { + id: 'dutchfield', + colors: [ + '#e60049', + '#0bb4ff', + '#50e991', + '#e6d800', + '#9b19f5', + '#ffa300', + '#dc0ab4', + '#b3d4ff', + '#00bfa0', + ], + en: { + title: 'Dutch Field', + }, + zh: { + title: 'Dutch Field', + }, + }, + { + id: 'rivernights', + colors: [ + '#b30000', + '#7c1158', + '#4421af', + '#1a53ff', + '#0d88e6', + '#00b7c7', + '#5ad45a', + '#8be04e', + '#ebdc78', + ], + en: { + title: 'River Nights', + }, + zh: { + title: 'River Nights', + }, + }, + { + id: 'springpastels', + colors: [ + '#fd7f6f', + '#7eb0d5', + '#b2e061', + '#bd7ebe', + '#ffb55a', + '#ffee65', + '#beb9db', + '#fdcce5', + '#8bd3c7', + ], + en: { + title: 'Spring Pastels', + }, + zh: { + title: 'Spring Pastels', + }, + }, + { + id: 'echarts', + colors: [ + '#5470c6', + '#91cc75', + '#fac858', + '#ee6666', + '#73c0de', + '#3ba272', + '#fc8452', + '#9a60b4', + '#ea7ccc', + ], + en: { + title: 'Echarts', + }, + zh: { + title: 'Echarts', + }, + }, + { + id: 'vintage', + colors: [ + '#d87c7c', + '#919e8b', + '#d7ab82', + '#6e7074', + '#61a0a8', + '#efa18d', + '#787464', + '#cc7e63', + '#724e58', + '#4b565b', + ], + en: { + title: 'Vintage', + }, + zh: { + title: '怀旧', + }, + }, + { + id: 'dark', + colors: [ + '#dd6b66', + '#759aa0', + '#e69d87', + '#8dc1a9', + '#ea7e53', + '#eedd78', + '#73a373', + '#73b9bc', + '#7289ab', + '#91ca8c', + '#f49f42', + ], + en: { + title: 'Dark', + }, + zh: { + title: '暗色', + }, + }, + { + id: 'westeros', + colors: ['#516b91', '#59c4e6', '#edafda', '#93b7e3', '#a5e7f0', '#cbb0e3'], + en: { + title: 'Westeros', + }, + zh: { + title: 'Westeros', + }, + }, + { + id: 'essos', + colors: ['#893448', '#d95850', '#eb8146', '#ffb248', '#f2d643', '#ebdba4'], + en: { + title: 'Essos', + }, + zh: { + title: 'Essos', + }, + }, + { + id: 'wonderland', + colors: ['#4ea397', '#22c3aa', '#7bd9a5', '#d0648a', '#f58db2', '#f2b3c9'], + en: { + title: 'Wonderland', + }, + zh: { + title: 'Wonderland', + }, + }, + { + id: 'walden', + colors: ['#3fb1e3', '#6be6c1', '#626c91', '#a0a7e6', '#c4ebad', '#96dee8'], + en: { + title: 'Walden', + }, + zh: { + title: 'Walden', + }, + }, + { + id: 'chalk', + colors: [ + '#fc97af', + '#87f7cf', + '#f7f494', + '#72ccff', + '#f7c5a0', + '#d4a4eb', + '#d2f5a6', + '#76f2f2', + ], + en: { + title: 'Chalk', + }, + zh: { + title: '粉笔', + }, + }, + { + id: 'infographic', + colors: [ + '#c1232b', + '#27727b', + '#fcce10', + '#e87c25', + '#b5c334', + '#fe8463', + '#9bca63', + '#fad860', + '#f3a43b', + '#60c0dd', + '#d7504b', + '#c6e579', + '#f4e001', + '#f0805a', + '#26c0c0', + ], + en: { + title: 'Infographic', + }, + zh: { + title: '信息图', + }, + }, + { + id: 'macarons', + colors: [ + '#2ec7c9', + '#b6a2de', + '#5ab1ef', + '#ffb980', + '#d87a80', + '#8d98b3', + '#e5cf0d', + '#97b552', + '#95706d', + '#dc69aa', + '#07a2a4', + '#9a7fd1', + '#588dd5', + '#f5994e', + '#c05050', + '#59678c', + '#c9ab00', + '#7eb00a', + '#6f5553', + '#c14089', + ], + en: { + title: 'Macarons', + }, + zh: { + title: '马卡龙', + }, + }, + { + id: 'roma', + colors: [ + '#e01f54', + '#001852', + '#f5e8c8', + '#b8d2c7', + '#c6b38e', + '#a4d8c2', + '#f3d999', + '#d3758f', + '#dcc392', + '#2e4783', + '#82b6e9', + '#ff6347', + '#a092f1', + '#0a915d', + '#eaf889', + '#6699ff', + '#ff6666', + '#3cb371', + '#d5b158', + '#38b6b6', + ], + en: { + title: 'Roma', + }, + zh: { + title: '罗马', + }, + }, + { + id: 'shine', + colors: [ + '#c12e34', + '#e6b600', + '#0098d9', + '#2b821d', + '#005eaa', + '#339ca8', + '#cda819', + '#32a487', + ], + en: { + title: 'Shine', + }, + zh: { + title: '阳光', + }, + }, + { + id: 'purplepassion', + colors: ['#9b8bba', '#e098c7', '#8fd3e8', '#71669e', '#cc70af', '#7cb4cc'], + en: { + title: 'Purple Passion', + }, + zh: { + title: '热情', + }, + }, +]; +export const defaultThemes = [ + WHITE, + BLACK, + ...colorThemes[0].colors.slice(0, colorThemes[0].colors.length - 1), +]; diff --git a/frontend/src/app/components/ChartEditor.tsx b/frontend/src/app/components/ChartEditor.tsx index fadb372e8..c836f26ec 100644 --- a/frontend/src/app/components/ChartEditor.tsx +++ b/frontend/src/app/components/ChartEditor.tsx @@ -20,6 +20,7 @@ import { ExclamationCircleOutlined } from '@ant-design/icons'; import { Modal } from 'antd'; import useMount from 'app/hooks/useMount'; import workbenchSlice, { + aggregationSelector, BackendChart, backendChartSelector, ChartConfigReducerActionType, @@ -31,6 +32,7 @@ import workbenchSlice, { shadowChartConfigSelector, updateChartAction, updateChartConfigAndRefreshDatasetAction, + updateRichTextAction, useWorkbenchSlice, } from 'app/pages/ChartWorkbenchPage/slice/workbenchSlice'; import { transferChartConfigs } from 'app/utils/internalChartHelper'; @@ -82,12 +84,17 @@ export const ChartEditor: React.FC = ({ const chartConfig = useSelector(chartConfigSelector); const shadowChartConfig = useSelector(shadowChartConfigSelector); const backendChart = useSelector(backendChartSelector); + const aggregation = useSelector(aggregationSelector); const [chart, setChart] = useState(); useMount( () => { - const currentChart = ChartManager.instance().getDefaultChart(); - handleChartChange(currentChart); + if (!dataChartId && !originChart) { + // Note: add default chart if new to editor + const currentChart = ChartManager.instance().getDefaultChart(); + handleChartChange(currentChart); + } + if (container === 'dataChart') { dispatch( initWorkbenchAction({ @@ -104,6 +111,9 @@ export const ChartEditor: React.FC = ({ backendChart: originChart as BackendChart, }), ); + if (!originChart) { + dispatch(actions.updateChartAggregation(true)); + } } else { // chartType === 'dataChart' dispatch( @@ -129,7 +139,7 @@ export const ChartEditor: React.FC = ({ setChart(currentChart); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [backendChart]); + }, [backendChart?.config?.chartGraphId]); const handleChartChange = (c: Chart) => { registerChartEvents(c); @@ -166,15 +176,19 @@ export const ChartEditor: React.FC = ({ const currentChart = ChartManager.instance().getDefaultChart(); registerChartEvents(currentChart); setChart(currentChart); - let clonedState = CloneValueDeep(currentChart.config); + let targetChartConfig = CloneValueDeep(currentChart.config); + const finalChartConfig = transferChartConfigs( + targetChartConfig, + targetChartConfig, + ); + + dispatch(workbenchSlice.actions.updateShadowChartConfig({})); dispatch( workbenchSlice.actions.updateChartConfig({ type: ChartConfigReducerActionType.INIT, payload: { - init: { - ...clonedState, - }, + init: finalChartConfig, }, }), ); @@ -185,11 +199,12 @@ export const ChartEditor: React.FC = ({ chartConfig: chartConfig!, chartGraphId: chart?.meta.id!, computedFields: dataview?.computedFields || [], + aggregation, }; const dataChart: DataChart = { id: dataChartId, - name: backendChart?.name || 'widget_chart', + name: backendChart?.name || '', viewId: dataview?.id || '', orgId: orgId, config: dataChartConfig, @@ -206,6 +221,7 @@ export const ChartEditor: React.FC = ({ dataview, onSaveInWidget, orgId, + aggregation, ]); const saveChart = useCallback(async () => { @@ -218,6 +234,7 @@ export const ChartEditor: React.FC = ({ chartId: dataChartId, index: 0, parentId: 0, + aggregation: aggregation, }), ); onSaveInDataChart?.(orgId, dataChartId); @@ -238,6 +255,7 @@ export const ChartEditor: React.FC = ({ chartId: dataChartId, index: 0, parentId: 0, + aggregation, }), ); saveToWidget(); @@ -259,6 +277,7 @@ export const ChartEditor: React.FC = ({ orgId, chartType, saveToWidget, + aggregation, ]); const registerChartEvents = chart => { @@ -266,25 +285,55 @@ export const ChartEditor: React.FC = ({ { name: 'click', callback: param => { - if (param.seriesName === 'paging') { - const page = param.value?.page; - dispatch(refreshDatasetAction({ pageInfo: { pageNo: page } })); + if ( + param.componentType === 'table' && + param.seriesType === 'paging-sort-filter' + ) { + dispatch( + refreshDatasetAction({ + sorter: { + column: param?.seriesName!, + operator: param?.value?.direction, + }, + pageInfo: { + pageNo: param?.value?.pageNo, + }, + }), + ); + return; + } + if (param.seriesName === 'richText') { + dispatch(updateRichTextAction(param.value)); return; } - }, - }, - { - name: 'dblclick', - callback: param => { - console.log( - '//TODO: to be remove | mouse db click event ----> ', - param, - ); }, }, ]); }; + const handleAggregationState = state => { + const currentChart = ChartManager.instance().getById(chart?.meta?.id); + let targetChartConfig = CloneValueDeep(currentChart?.config); + registerChartEvents(currentChart); + setChart(currentChart); + + const finalChartConfig = transferChartConfigs( + targetChartConfig, + targetChartConfig, + ); + + dispatch(actions.updateChartAggregation(state)); + dispatch(workbenchSlice.actions.updateShadowChartConfig({})); + dispatch( + workbenchSlice.actions.updateChartConfig({ + type: ChartConfigReducerActionType.INIT, + payload: { + init: finalChartConfig, + }, + }), + ); + }; + return ( = ({ onGoBack: () => { onClose?.(); }, + onChangeAggregation: handleAggregationState, }} + aggregation={aggregation} chart={chart} dataset={dataset} dataview={dataview} diff --git a/frontend/src/app/components/ColorPicker/ChromeColorPicker.tsx b/frontend/src/app/components/ColorPicker/ChromeColorPicker.tsx new file mode 100644 index 000000000..d5d48fdae --- /dev/null +++ b/frontend/src/app/components/ColorPicker/ChromeColorPicker.tsx @@ -0,0 +1,88 @@ +/** + * 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 { Button } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import React, { useState } from 'react'; +import { ChromePicker, ColorResult } from 'react-color'; +import styled from 'styled-components/macro'; +import { SPACE_TIMES } from 'styles/StyleConstants'; +import { colorSelectionPropTypes } from './slice/types'; + +const toChangeValue = (data: ColorResult) => { + const { r, g, b, a } = data.rgb; + return `rgba(${r}, ${g}, ${b}, ${a})`; +}; + +/** + * 单色选择组件 + * @param onChange + * @param color + * @returns 返回一个新的颜色值 + */ +function ChromeColorPicker({ color, onChange }: colorSelectionPropTypes) { + const [selectColor, setSelectColor] = useState(color); + const t = useI18NPrefix('components.colorPicker'); + + return ( + + { + let colorRgb = toChangeValue(color); + setSelectColor(colorRgb); + }} + /> + + { + onChange?.(false); + }} + > + {t('cancel')} + + { + onChange?.(selectColor); + }} + > + {t('ok')} + + + + ); +} + +export default ChromeColorPicker; + +const ChromeColorWrap = styled.div` + .chrome-picker { + box-shadow: none !important; + } +`; + +const BtnWrap = styled.div` + text-align: right; + margin-top: ${SPACE_TIMES(2.5)}; + > button:first-child { + margin-right: ${SPACE_TIMES(2.5)}; + } +`; diff --git a/frontend/src/app/components/ColorPicker/ColorPickerPopover.tsx b/frontend/src/app/components/ColorPicker/ColorPickerPopover.tsx new file mode 100644 index 000000000..2ab262b93 --- /dev/null +++ b/frontend/src/app/components/ColorPicker/ColorPickerPopover.tsx @@ -0,0 +1,54 @@ +import { Popover, PopoverProps } from 'antd'; +import { FC, useCallback, useMemo, useState } from 'react'; +import { SketchPickerProps } from 'react-color'; +import { ColorPicker } from './ColorTag'; +import SingleColorSelection from './SingleColorSelection'; + +interface ColorPickerPopoverProps { + popoverProps?: PopoverProps; + defaultValue?: string; + onSubmit?: (color) => void; + onChange?: (color) => void; + colorPickerClass?: string; + colors?: SketchPickerProps['presetColors']; +} +export const ColorPickerPopover: FC = ({ + children, + defaultValue, + popoverProps, + onSubmit, + onChange, + colorPickerClass, +}) => { + const [visible, setVisible] = useState(false); + const [color] = useState(defaultValue); + + const onCancel = useCallback(() => { + setVisible(false); + }, []); + const onColorChange = useCallback( + color => { + onSubmit?.(color); + onChange?.(color); + onCancel(); + }, + [onSubmit, onCancel, onChange], + ); + const _popoverProps = useMemo(() => { + return typeof popoverProps === 'object' ? popoverProps : {}; + }, [popoverProps]); + return ( + } + trigger="click" + placement="right" + > + {children || ( + + )} + + ); +}; diff --git a/frontend/src/app/components/ReactColorPicker/ColorTag.tsx b/frontend/src/app/components/ColorPicker/ColorTag.tsx similarity index 100% rename from frontend/src/app/components/ReactColorPicker/ColorTag.tsx rename to frontend/src/app/components/ColorPicker/ColorTag.tsx diff --git a/frontend/src/app/components/ColorPicker/SingleColorSelection.tsx b/frontend/src/app/components/ColorPicker/SingleColorSelection.tsx new file mode 100644 index 000000000..09614e3ba --- /dev/null +++ b/frontend/src/app/components/ColorPicker/SingleColorSelection.tsx @@ -0,0 +1,162 @@ +/** + * 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 { Popover } from 'antd'; +import { defaultPalette, defaultThemes } from 'app/assets/theme/colorsConfig'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import React, { useState } from 'react'; +import styled from 'styled-components/macro'; +import { + BORDER_RADIUS, + FONT_SIZE_BODY, + G40, + G80, + SPACE_TIMES, + WHITE, +} from 'styles/StyleConstants'; +import ChromeColorPicker from './ChromeColorPicker'; +import { colorSelectionPropTypes } from './slice/types'; + +/** + * 单色选择组件 + * @param onChange + * @param color + * @returns 返回一个新的颜色值 + */ +function SingleColorSelection({ color, onChange }: colorSelectionPropTypes) { + const [moreStatus, setMoreStatus] = useState(false); + const [selectColor, setSelectColor] = useState(color); + const t = useI18NPrefix('components.colorPicker'); + + //更多颜色里的回调函数 + const moreCallBackFn = value => { + if (value) { + setSelectColor(value); + onChange?.(value); + } + setMoreStatus(false); + }; + const selectColorFn = (color: string) => { + setSelectColor(color); + onChange?.(color); + }; + return ( + + + {defaultThemes.map((color, i) => { + return ( + { + selectColorFn(color); + }} + color={color} + key={i} + className={selectColor === color ? 'active' : ''} + > + ); + })} + + + {defaultPalette.map((color, i) => { + return ( + { + selectColorFn(color); + }} + color={color} + key={i} + className={selectColor === color ? 'active' : ''} + > + ); + })} + + } + > + { + setMoreStatus(true); + }} + > + {t('more')} + + + + ); +} + +export default SingleColorSelection; + +const ColorWrap = styled.div` + background-color: ${WHITE}; + width: 426px; + min-width: 426px; + // max-width: 426px; +`; + +const ThemeColorWrap = styled.div` + border-bottom: 1px solid ${G40}; + padding-bottom: ${SPACE_TIMES(1.5)}; + margin: ${SPACE_TIMES(2.5)} 0; +`; + +const ColorBlock = styled.span<{ color: string }>` + display: inline-block; + min-width: ${SPACE_TIMES(6)}; + min-height: ${SPACE_TIMES(6)}; + background-color: ${p => p.color}; + border-radius: ${BORDER_RADIUS}; + cursor: pointer; + transition: all 0.2s; + margin-right: ${SPACE_TIMES(4)}; + border: 1px solid ${G40}; + &:last-child { + margin-right: 0px; + } + &:hover { + opacity: 0.7; + } + &.active { + border: 1px solid ${p => p.theme.primary}; + } +`; + +const ColorPalette = styled.div` + border-bottom: 1px solid ${G40}; + padding-bottom: ${SPACE_TIMES(1.5)}; + > span:nth-child(11n) { + margin-right: 0px; + } +`; + +const MoreColor = styled.div` + text-align: center; + cursor: pointer; + margin-top: ${SPACE_TIMES(2.5)}; + font-size: ${FONT_SIZE_BODY}; + color: ${G80}; + &:hover { + color: ${p => p.theme.primary}; + } +`; diff --git a/frontend/src/app/components/ColorPicker/ThemeColorSelection.tsx b/frontend/src/app/components/ColorPicker/ThemeColorSelection.tsx new file mode 100644 index 000000000..9885c2266 --- /dev/null +++ b/frontend/src/app/components/ColorPicker/ThemeColorSelection.tsx @@ -0,0 +1,121 @@ +/** + * 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 { List, Popover } from 'antd'; +import { colorThemes } from 'app/assets/theme/colorsConfig'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components/macro'; +import { FONT_SIZE_BODY, G10, SPACE_TIMES } from 'styles/StyleConstants'; +import { themeColorPropTypes } from './slice/types'; + +/** + * @param callbackFn 回调函数返回一个颜色数组 + * @param children 点击弹出按钮的文字 支持文字和html类型 + */ +function ThemeColorSelection({ children, callbackFn }: themeColorPropTypes) { + const [switchStatus, setSwitchStatus] = useState(false); + const [colors] = useState(colorThemes); + const { i18n } = useTranslation(); + + return ( + + ( + { + callbackFn(item.colors); + setSwitchStatus(false); + }} + > + {item[i18n.language].title} + + {item.colors.map((v, i) => { + return ; + })} + + + )} + /> + + } + > + { + setSwitchStatus(!switchStatus); + }} + > + {children} + + + ); +} + +export default ThemeColorSelection; + +const ChooseTheme = styled.div` + display: inline-block; + width: 100%; + text-align: right; + margin-bottom: ${SPACE_TIMES(1)}; +`; +const ChooseThemeSpan = styled.div` + cursor: pointer; + font-size: ${FONT_SIZE_BODY}; + display: inline-block; + width: max-content; + &:hover { + color: ${p => p.theme.primary}; + } +`; +const ColorWrapAlert = styled.div` + width: 350px; + max-height: 300px; + overflow-y: auto; + .ant-list-item { + display: flex; + flex-direction: column; + align-items: flex-start; + cursor: pointer; + padding: ${SPACE_TIMES(2.5)}; + &:hover { + background-color: ${G10}; + } + } +`; +const ColorTitle = styled.span``; +const ColorBlockWrap = styled.div` + display: flex; + flex-wrap: wrap; + margin-top: ${SPACE_TIMES(1)}; +`; +const ColorBlock = styled.span<{ color: string }>` + display: inline-block; + min-width: ${SPACE_TIMES(6)}; + min-height: ${SPACE_TIMES(6)}; + background-color: ${p => p.color}; +`; diff --git a/frontend/src/app/components/ColorPicker/index.tsx b/frontend/src/app/components/ColorPicker/index.tsx new file mode 100644 index 000000000..e98c9046a --- /dev/null +++ b/frontend/src/app/components/ColorPicker/index.tsx @@ -0,0 +1,28 @@ +/** + * 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 { ColorPickerPopover } from './ColorPickerPopover'; +import { ColorTag } from './ColorTag'; +import SingleColorSelection from './SingleColorSelection'; +import ThemeColorSelection from './ThemeColorSelection'; + +export { + SingleColorSelection, + ThemeColorSelection, + ColorTag, + ColorPickerPopover, +}; diff --git a/frontend/src/app/components/ColorPicker/slice/types.ts b/frontend/src/app/components/ColorPicker/slice/types.ts new file mode 100644 index 000000000..f240fca44 --- /dev/null +++ b/frontend/src/app/components/ColorPicker/slice/types.ts @@ -0,0 +1,10 @@ +import { ReactNode } from 'react'; + +export interface colorSelectionPropTypes { + color?: string; + onChange?: (color) => void; +} +export interface themeColorPropTypes { + children: ReactNode; + callbackFn: (Array) => void; +} diff --git a/frontend/src/app/components/Configuration.tsx b/frontend/src/app/components/Configuration.tsx index e21bff043..4a816a40a 100644 --- a/frontend/src/app/components/Configuration.tsx +++ b/frontend/src/app/components/Configuration.tsx @@ -1,7 +1,7 @@ import { EditableProTable, ProColumns } from '@ant-design/pro-table'; import { useCallback, useMemo, useState } from 'react'; import { css } from 'styled-components/macro'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; const tableStyle = css` .ant-card-body { diff --git a/frontend/src/app/components/DragSortEditTable.tsx b/frontend/src/app/components/DragSortEditTable.tsx index dfbc3ea86..e8696c7a8 100644 --- a/frontend/src/app/components/DragSortEditTable.tsx +++ b/frontend/src/app/components/DragSortEditTable.tsx @@ -18,7 +18,7 @@ import { Form, Input, Table, TableProps } from 'antd'; import { FormInstance } from 'antd/lib/form'; -import { FilterValueOption } from 'app/types/ChartConfig'; +import { RelationFilterValue } from 'app/types/ChartConfig'; import { createContext, useCallback, @@ -36,9 +36,9 @@ interface EditableCellProps { title: React.ReactNode; editable: boolean; children: React.ReactNode; - dataIndex: keyof FilterValueOption; - record: FilterValueOption; - handleSave: (record: FilterValueOption) => void; + dataIndex: keyof RelationFilterValue; + record: RelationFilterValue; + handleSave: (record: RelationFilterValue) => void; } interface EditableRowProps { diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicColorSelector.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicColorSelector.tsx index a0c3824d5..501ec5bf0 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicColorSelector.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicColorSelector.tsx @@ -17,7 +17,7 @@ */ import { Col, Row } from 'antd'; -import { ColorPickerPopover } from 'app/components/ReactColorPicker'; +import { ColorPickerPopover } from 'app/components/ColorPicker'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; import { FC, memo } from 'react'; import styled from 'styled-components/macro'; @@ -46,7 +46,7 @@ const BasicColorSelector: FC> = memo( ({ ancestors, translate: t = title => title, data: row, onChange }) => { const { comType, options, ...rest } = row; - const hanldePickerSelect = value => { + const handlePickerSelect = value => { onChange?.(ancestors, value); }; @@ -63,7 +63,7 @@ const BasicColorSelector: FC> = memo( {...options} colors={COLORS} defaultValue={getColor()} - onSubmit={hanldePickerSelect} + onSubmit={handlePickerSelect} > @@ -85,4 +85,5 @@ const StyledColor = styled.div` height: 24px; background-color: ${props => props.color}; border: ${props => (props.color === 'transparent' ? '1px solid red' : '0px')}; + cursor: pointer; `; diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicFont.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicFont.tsx index 42c796639..8de5a8055 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicFont.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicFont.tsx @@ -17,7 +17,7 @@ */ import { Select } from 'antd'; -import { ColorPickerPopover } from 'app/components/ReactColorPicker'; +import { ColorPickerPopover } from 'app/components/ColorPicker'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; import { updateByKey } from 'app/utils/mutation'; import { @@ -36,7 +36,7 @@ const BasicFont: FC> = memo( ({ ancestors, translate: t = title => title, data, onChange }) => { const { comType, options, ...rest } = data; - const hanldePickerSelect = value => { + const handlePickerSelect = value => { handleSettingChange('color')(value); }; @@ -50,19 +50,22 @@ const BasicFont: FC> = memo( - {FONT_FAMILIES.map(o => ( - - {o.name} + {(options?.fontFamilies || FONT_FAMILIES).map(o => ( + + {typeof o === 'string' ? o : o.name} ))} @@ -76,7 +79,7 @@ const BasicFont: FC> = memo( @@ -87,7 +90,7 @@ const BasicFont: FC> = memo( ))} @@ -102,7 +105,7 @@ const BasicFont: FC> = memo( {...rest} {...options} defaultValue={data.value?.color} - onSubmit={hanldePickerSelect} + onSubmit={handlePickerSelect} /> diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicFontFamilySelector.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicFontFamilySelector.tsx index 93f8aa196..68ffccaf0 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicFontFamilySelector.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicFontFamilySelector.tsx @@ -36,7 +36,7 @@ const BasicFontFamilySelector: FC> = dropdownMatchSelectWidth {...rest} {...options} - placeholder={t('pleaseSelect')} + placeholder={t('select')} onChange={value => onChange?.(ancestors, value)} > {FONT_FAMILIES.map(o => ( diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicFontSizeSelector.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicFontSizeSelector.tsx index af2f01278..36994b514 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicFontSizeSelector.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicFontSizeSelector.tsx @@ -36,7 +36,7 @@ const BasicFontSizeSelector: FC> = dropdownMatchSelectWidth {...rest} {...options} - placeholder={t('pleaseSelect')} + placeholder={t('select')} onChange={value => onChange?.(ancestors, value)} > {FONT_SIZES.map(o => ( diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicInput.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicInput.tsx index d44287f24..a5039586f 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicInput.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicInput.tsx @@ -18,23 +18,38 @@ import { Input } from 'antd'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; -import { FC, memo } from 'react'; +import debounce from 'lodash/debounce'; +import { FC, memo, useMemo, useState } from 'react'; import styled from 'styled-components/macro'; import { ItemLayoutProps } from '../types'; import { itemLayoutComparer } from '../utils'; import { BW } from './components/BasicWrapper'; const BasicInput: FC> = memo( - ({ ancestors, translate: t = title => title, data: row, onChange }) => { - const { comType, options, ...rest } = row; - - const handleChangeByEvent = e => { - onChange?.(ancestors, e.target?.value); - }; + ({ ancestors, translate: t = title => title, data, onChange }) => { + const [cache, setCache] = useState(data); + const { comType, options, ...rest } = cache; + const debouncedDataChange = useMemo( + () => + debounce(value => { + onChange?.(ancestors, value, options?.needRefresh); + }, 500), + [ancestors, onChange, options?.needRefresh], + ); return ( - - + + { + const newCache = Object.assign({}, cache, { + value: value.target?.value, + }); + setCache(newCache); + debouncedDataChange(newCache.value); + }} + /> ); }, diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicInputNumber.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicInputNumber.tsx index 70a590005..60ee831b3 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicInputNumber.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicInputNumber.tsx @@ -18,7 +18,8 @@ import { InputNumber } from 'antd'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; -import { FC, memo } from 'react'; +import debounce from 'lodash/debounce'; +import { FC, memo, useMemo, useState } from 'react'; import styled from 'styled-components/macro'; import { BORDER_RADIUS } from 'styles/StyleConstants'; import { ItemLayoutProps } from '../types'; @@ -26,16 +27,29 @@ import { itemLayoutComparer } from '../utils'; import { BW } from './components/BasicWrapper'; const BasicInputNumber: FC> = memo( - ({ ancestors, translate: t = title => title, data: row, onChange }) => { - const { comType, options, ...rest } = row; + ({ ancestors, translate: t = title => title, data, onChange }) => { + const [cache, setCache] = useState(data); + const { comType, options, ...rest } = cache; + + const debouncedDataChange = useMemo( + () => + debounce(value => { + onChange?.(ancestors, value, options?.needRefresh); + }, 500), + [ancestors, onChange, options?.needRefresh], + ); return ( - + onChange?.(ancestors, value)} - defaultValue={rest?.default} + onChange={value => { + const newCache = Object.assign({}, cache, { value }); + setCache(newCache); + debouncedDataChange(newCache.value); + }} + defaultValue={cache?.default} /> ); diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicLine.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicLine.tsx index 6b37edd90..8540bf3f4 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicLine.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicLine.tsx @@ -17,7 +17,7 @@ */ import { Select } from 'antd'; -import { ColorPickerPopover } from 'app/components/ReactColorPicker'; +import { ColorPickerPopover } from 'app/components/ColorPicker'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; import { updateByKey } from 'app/utils/mutation'; import { CHART_LINE_STYLES, CHART_LINE_WIDTH } from 'globalConstants'; @@ -47,7 +47,7 @@ const BasicLine: FC> = memo( > = memo( ))} > = memo( + ({ ancestors, translate: t = title => title, data: row, onChange }) => { + const { value, comType, options, ...rest } = row; + const items = options?.items || []; + const needTranslate = !!options?.translateItemLabel; + + const handleValueChange = e => { + const newValue = e.target.value; + onChange?.(ancestors, newValue, options?.needRefresh); + }; + + return ( + + + {items?.map(o => { + return ( + + {needTranslate ? t(o.label) : o?.label} + + ); + })} + + + ); + }, + itemLayoutComparer, +); + +export default BasicRadio; + +const StyledBasicRadio = styled(BW)``; diff --git a/frontend/src/app/components/FormGenerator/Basic/BaiscSelector.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicSelector.tsx similarity index 88% rename from frontend/src/app/components/FormGenerator/Basic/BaiscSelector.tsx rename to frontend/src/app/components/FormGenerator/Basic/BasicSelector.tsx index d3aae288a..5c510a352 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BaiscSelector.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicSelector.tsx @@ -26,7 +26,7 @@ import { ItemLayoutProps } from '../types'; import { itemLayoutComparer } from '../utils'; import { BW } from './components/BasicWrapper'; -const BaiscSelector: FC> = memo( +const BasicSelector: FC> = memo( ({ ancestors, translate: t = title => title, @@ -36,6 +36,7 @@ const BaiscSelector: FC> = memo( }) => { const { comType, options, ...rest } = row; const hideLabel = !!options?.hideLabel; + const needTranslate = !!options?.translateItemLabel; const handleSelectorValueChange = value => { onChange?.(ancestors, value, options?.needRefresh); @@ -51,7 +52,10 @@ const BaiscSelector: FC> = memo( try { results = typeof row?.options?.getItems === 'function' - ? row?.options?.getItems.call(null, getDataConfigs()) || [] + ? row?.options?.getItems.call( + Object.create(null), + getDataConfigs(), + ) || [] : row?.options?.items || []; } catch (error) { console.error( @@ -70,7 +74,7 @@ const BaiscSelector: FC> = memo( {...rest} {...options} defaultValue={rest.default} - placeholder={t('pleaseSelect')} + placeholder={t('select')} onChange={handleSelectorValueChange} > {safeInvokeAction()?.map((o, index) => { @@ -79,7 +83,7 @@ const BaiscSelector: FC> = memo( const value = isEmpty(o['value']) ? o : o.value; return ( - {label} + {needTranslate ? t(label) : label} ); })} @@ -90,7 +94,7 @@ const BaiscSelector: FC> = memo( itemLayoutComparer, ); -export default BaiscSelector; +export default BasicSelector; const Wrapper = styled(BW)` .ant-select { diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicSwitch.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicSwitch.tsx index 3725fb893..1e9b9c2bb 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicSwitch.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicSwitch.tsx @@ -52,7 +52,7 @@ const BasicSwitch: FC> = memo( onChange?.(ancestors, newRow, needRefresh); }; - const hanldeSwitchChange = value => { + const handleSwitchChange = value => { const newRow = updateByKey(row, 'value', value); onChange?.(ancestors, newRow); }; @@ -74,7 +74,7 @@ const BasicSwitch: FC> = memo( {...rest} {...options} checked={row.value} - onChange={hanldeSwitchChange} + onChange={handleSwitchChange} /> diff --git a/frontend/src/app/components/FormGenerator/Basic/BasicUnControlledTabPanel.tsx b/frontend/src/app/components/FormGenerator/Basic/BasicUnControlledTabPanel.tsx index e5ed5366d..40ad11f91 100644 --- a/frontend/src/app/components/FormGenerator/Basic/BasicUnControlledTabPanel.tsx +++ b/frontend/src/app/components/FormGenerator/Basic/BasicUnControlledTabPanel.tsx @@ -33,7 +33,7 @@ import { isEmpty, resetValue, } from 'utils/object'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import GroupLayout from '../Layout/GroupLayout'; import { GroupLayoutMode, ItemLayoutProps } from '../types'; import { itemLayoutComparer } from '../utils'; diff --git a/frontend/src/app/components/FormGenerator/Basic/index.ts b/frontend/src/app/components/FormGenerator/Basic/index.ts index d7a7fd385..9e11f3ee8 100644 --- a/frontend/src/app/components/FormGenerator/Basic/index.ts +++ b/frontend/src/app/components/FormGenerator/Basic/index.ts @@ -15,8 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export { default as BaiscSelector } from './BaiscSelector'; export { default as BasicCheckbox } from './BasicCheckbox'; export { default as BasicColorSelector } from './BasicColorSelector'; export { default as BasicFont } from './BasicFont'; @@ -27,6 +25,8 @@ export { default as BasicInputNumber } from './BasicInputNumber'; export { default as BasicInputPercentage } from './BasicInputPercentage'; export { default as BasicLine } from './BasicLine'; export { default as BasicMarginWidth } from './BasicMarginWidth'; +export { default as BasicRadio } from './BasicRadio'; +export { default as BasicSelector } from './BasicSelector'; export { default as BasicSlider } from './BasicSlider'; export { default as BasicSwitch } from './BasicSwitch'; export { default as BasicText } from './BasicText'; diff --git a/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/ConditionalStylePanel.tsx b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/ConditionalStylePanel.tsx new file mode 100644 index 000000000..9d33faf5c --- /dev/null +++ b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/ConditionalStylePanel.tsx @@ -0,0 +1,186 @@ +/** + * 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 { Button, Col, Popconfirm, Row, Space, Table, Tag } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; +import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; +import { FC, memo, useState } from 'react'; +import styled from 'styled-components/macro'; +import { CloneValueDeep } from 'utils/object'; +import { uuidv4 } from 'utils/utils'; +import { ItemLayoutProps } from '../../types'; +import { itemLayoutComparer } from '../../utils'; +import AddModal from './add'; +import { ConditionStyleFormValues } from './types'; + +const ConditionStylePanel: FC> = memo( + ({ + ancestors, + translate: t = title => title, + data, + onChange, + dataConfigs, + context, + }) => { + const [myData] = useState(() => CloneValueDeep(data)); + const [visible, setVisible] = useState(false); + const [dataSource, setDataSource] = useState( + myData.value || [], + ); + + const [currentItem, setCurrentItem] = useState( + {} as ConditionStyleFormValues, + ); + const onEditItem = (values: ConditionStyleFormValues) => { + setCurrentItem(CloneValueDeep(values)); + openConditionStyle(); + }; + const onRemoveItem = (values: ConditionStyleFormValues) => { + const result: ConditionStyleFormValues[] = dataSource.filter( + item => item.uid !== values.uid, + ); + + setDataSource(result); + onChange?.(ancestors, { + ...myData, + value: result, + }); + }; + + const tableColumnsSettings: ColumnsType = [ + { + title: t('conditionStyleTable.header.range.title'), + dataIndex: 'range', + width: 100, + render: (_, { range }) => ( + {t(`conditionStyleTable.header.range.${range}`)} + ), + }, + { + title: t('conditionStyleTable.header.operator'), + dataIndex: 'operator', + }, + { + title: t('conditionStyleTable.header.value'), + dataIndex: 'value', + render: (_, { value }) => <>{JSON.stringify(value)}>, + }, + { + title: t('conditionStyleTable.header.color.title'), + dataIndex: 'value', + render: (_, { color }) => ( + <> + + {t('conditionStyleTable.header.color.background')} + + + {t('conditionStyleTable.header.color.text')} + + > + ), + }, + { + title: t('conditionStyleTable.header.action'), + dataIndex: 'action', + width: 140, + render: (_, record) => { + return [ + onEditItem(record)}> + {t('conditionStyleTable.btn.edit')} + , + onRemoveItem(record)} + > + + {t('conditionStyleTable.btn.remove')} + + , + ]; + }, + }, + ]; + + const openConditionStyle = () => { + setVisible(true); + }; + const closeConditionStyleModal = () => { + setVisible(false); + setCurrentItem({} as ConditionStyleFormValues); + }; + const submitConditionStyleModal = (values: ConditionStyleFormValues) => { + let result: ConditionStyleFormValues[] = []; + + if (values.uid) { + result = dataSource.map(item => { + if (item.uid === values.uid) { + return values; + } + return item; + }); + } else { + result = [...dataSource, { ...values, uid: uuidv4() }]; + } + + setDataSource(result); + closeConditionStyleModal(); + onChange?.(ancestors, { + ...myData, + value: result, + }); + }; + + return ( + + + {t('conditionStyleTable.btn.add')} + + + + + bordered={true} + size="small" + pagination={false} + rowKey={record => record.uid!} + columns={tableColumnsSettings} + dataSource={dataSource} + /> + + + + + ); + }, + itemLayoutComparer, +); + +const StyledConditionStylePanel = styled(Space)` + width: 100%; + margin-top: 10px; +`; + +export default ConditionStylePanel; diff --git a/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/add.tsx b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/add.tsx new file mode 100644 index 000000000..f9394bde6 --- /dev/null +++ b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/add.tsx @@ -0,0 +1,294 @@ +import { Col, Form, Input, InputNumber, Modal, Radio, 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 { + ConditionOperatorTypes, + ConditionStyleFormValues, + ConditionStyleRange, + OperatorTypes, + OperatorTypesLocale, +} from './types'; + +interface AddProps { + context?: any; + translate?: (title: string, options?: any) => string; + visible: boolean; + values: ConditionStyleFormValues; + onOk: (values: ConditionStyleFormValues) => void; + onCancel: () => void; +} + +export default function Add({ + translate: t = title => title, + values, + visible, + onOk, + onCancel, + context: { label, type }, +}: AddProps) { + const [colors] = useState([ + { + name: 'background', + label: t('conditionStyleTable.header.color.background'), + value: undefined, + }, + { + name: 'textColor', + label: t('conditionStyleTable.header.color.text'), + value: undefined, + }, + ]); + const [operatorSelect, setOperatorSelect] = useState< + { label: string; value: string }[] + >([]); + const [operatorValue, setOperatorValue] = useState( + OperatorTypes.Equal, + ); + const [form] = Form.useForm(); + + useEffect(() => { + if (type) { + setOperatorSelect( + ConditionOperatorTypes[type]?.map(item => ({ + label: `${OperatorTypesLocale[item]} [${item}]`, + value: item, + })), + ); + } else { + setOperatorSelect([]); + } + }, [type]); + + useEffect(() => { + // !重置form + if (visible) { + const result: Partial = + Object.keys(values).length === 0 + ? { + range: ConditionStyleRange.Cell, + operator: OperatorTypes.Equal, + } + : values; + + form.setFieldsValue(result); + setOperatorValue(result.operator ?? OperatorTypes.Equal); + } + }, [form, visible, values, label]); + + const modalOk = () => { + form.validateFields().then(values => { + onOk({ + ...values, + target: { + name: label, + 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 ( + {t('conditionStyleTable.modal.notFoundContent')}> + } + /> + ); + case OperatorTypes.Between: + return ; + default: + return DefaultNode; + } + }; + + return ( + + + + + + + + + + {t('conditionStyleTable.header.range.cell')} + + + {t('conditionStyleTable.header.range.row')} + + + + + + + + + {operatorValue !== OperatorTypes.IsNull ? ( + + {renderValueNode()} + + ) : null} + + + + {colors.map(({ label, value, name }) => ( + + + + ))} + + + + + ); +} + +const ColorSelector = memo( + ({ + label, + value, + onChange, + }: { + label: string; + value?: string; + onChange?: (value: any) => void; + }) => { + return ( + <> + {label} + + + + + + > + ); + }, +); + +const InputNumberScope = memo( + ({ + value, + onChange, + }: { + value?: [number, number]; + onChange?: (value: any) => void; + }) => { + const [[min, max], setState] = useState< + [number | undefined, number | undefined] + >([undefined, undefined]); + const [index, setIndex] = useState(0); + + useEffect(() => { + setIndex(index + 1); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (Array.isArray(value)) { + setState([Number(value[0]), Number(value[1])]); + } else { + setState([value, undefined]); + } + }, [value]); + + const inputNumberScopeChange = state => { + setState(state); + const result = state.filter(num => typeof num === 'number'); + if (result.length === 2) { + onChange?.(state); + } else { + onChange?.(undefined); + } + }; + + const minChange = (value: number) => inputNumberScopeChange([value, max]); + const maxChange = (value: number) => inputNumberScopeChange([min, value]); + + return ( + + + + + - + + + + + ); + }, +); + +const StyledColor = styled.div` + width: 16px; + height: 16px; + background-color: ${props => props.color}; + position: relative; + cursor: pointer; + ::after { + position: absolute; + top: -7px; + left: -7px; + display: inline-block; + width: 30px; + height: 30px; + border-radius: 5px; + border: 1px solid #d9d9d9; + content: ''; + } +`; diff --git a/frontend/src/locales/moment.ts b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/index.tsx similarity index 79% rename from frontend/src/locales/moment.ts rename to frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/index.tsx index bbd0a4353..f071c67bc 100644 --- a/frontend/src/locales/moment.ts +++ b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/index.tsx @@ -16,10 +16,7 @@ * limitations under the License. */ -// TODO: add more language here -import moment from 'moment'; -import 'moment/locale/zh-cn'; +import ConditionStylePanel from './ConditionalStylePanel'; -export function setLocale(locale) { - moment.locale(locale); -} +export * from './types'; +export default ConditionStylePanel; diff --git a/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/types.ts b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/types.ts new file mode 100644 index 000000000..7bf87dc10 --- /dev/null +++ b/frontend/src/app/components/FormGenerator/Customize/ConditionStylePanel/types.ts @@ -0,0 +1,74 @@ +import { ColumnTypes } from 'app/pages/MainPage/pages/ViewPage/constants'; + +export interface ConditionStyleFormValues { + uid: string; + target: { name: string; type: any }; + range: ConditionStyleRange; + operator: OperatorTypes; + value: string; + color: { background: string; textColor: string }; +} + +export enum ConditionStyleRange { + Cell = 'cell', + Row = 'row', +} + +export enum OperatorTypes { + Equal = '=', + NotEqual = '!=', + Contain = 'like', + NotContain = 'not like', + Between = 'between', + In = 'in', + NotIn = 'not in', + LessThan = '<', + GreaterThan = '>', + LessThanOrEqual = '<=', + GreaterThanOrEqual = '>=', + IsNull = 'is null', +} + +export const OperatorTypesLocale = { + [OperatorTypes.Equal]: '等于', + [OperatorTypes.NotEqual]: '不等于', + [OperatorTypes.Contain]: '包含', + [OperatorTypes.NotContain]: '不包含', + [OperatorTypes.In]: '在……范围内', + [OperatorTypes.NotIn]: '不在……范围内', + [OperatorTypes.Between]: '在……之间', + [OperatorTypes.LessThan]: '小于', + [OperatorTypes.GreaterThan]: '大于', + [OperatorTypes.LessThanOrEqual]: '小于等于', + [OperatorTypes.GreaterThanOrEqual]: '大于等于', + [OperatorTypes.IsNull]: '空值', +}; + +export const ConditionOperatorTypes = { + [ColumnTypes.String]: [ + OperatorTypes.Equal, + OperatorTypes.NotEqual, + OperatorTypes.Contain, + OperatorTypes.NotContain, + OperatorTypes.In, + OperatorTypes.NotIn, + OperatorTypes.IsNull, + ], + [ColumnTypes.Number]: [ + OperatorTypes.Equal, + OperatorTypes.NotEqual, + OperatorTypes.Between, + OperatorTypes.LessThan, + OperatorTypes.GreaterThan, + OperatorTypes.LessThanOrEqual, + OperatorTypes.GreaterThanOrEqual, + OperatorTypes.IsNull, + ], + [ColumnTypes.Date]: [ + OperatorTypes.Equal, + OperatorTypes.NotEqual, + OperatorTypes.In, + OperatorTypes.NotIn, + OperatorTypes.IsNull, + ], +}; diff --git a/frontend/src/app/components/FormGenerator/Customize/DataCachePanel.tsx b/frontend/src/app/components/FormGenerator/Customize/DataCachePanel.tsx deleted file mode 100644 index f6cb5f3f1..000000000 --- a/frontend/src/app/components/FormGenerator/Customize/DataCachePanel.tsx +++ /dev/null @@ -1,87 +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. - */ - -import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; -import { updateByKey } from 'app/utils/mutation'; -import { FC, memo, useEffect } from 'react'; -import { CloneValueDeep, mergeDefaultToValue } from 'utils/object'; -import { GroupLayout } from '../Layout'; -import { GroupLayoutMode, ItemLayoutProps } from '../types'; -import { itemLayoutComparer } from '../utils'; - -const defaultRows = [ - { - label: 'displayCount', - key: 'displayCount', - default: 10, - comType: 'inputNumber', - }, - { - label: 'autoLoad', - key: 'autoLoad', - default: true, - comType: 'switch', - }, - { - label: 'enableRaw', - key: 'enableRaw', - default: false, - comType: 'switch', - }, -]; - -const DataCachePanel: FC> = memo( - ({ - ancestors, - translate: t = title => title, - data: row, - dataConfigs, - onChange, - }) => { - useEffect(() => { - if (!row.rows || row.rows.length === 0) { - onChange?.( - ancestors, - updateByKey( - row, - 'rows', - mergeDefaultToValue(CloneValueDeep(defaultRows)), - ), - ); - } - }, [row]); - - const handleOnChange = (ancestors, value) => { - onChange?.(ancestors, value, true); - }; - - return ( - - ); - }, - itemLayoutComparer, -); - -export default DataCachePanel; diff --git a/frontend/src/app/components/FormGenerator/Customize/FontAlignment.tsx b/frontend/src/app/components/FormGenerator/Customize/FontAlignment.tsx new file mode 100644 index 000000000..ec503e9f7 --- /dev/null +++ b/frontend/src/app/components/FormGenerator/Customize/FontAlignment.tsx @@ -0,0 +1,76 @@ +/** + * 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 { ChartStyleSectionConfig } from 'app/types/ChartConfig'; +import { FC, memo } from 'react'; +import { ItemLayout } from '../Layout'; +import { ItemLayoutProps } from '../types'; +import { itemLayoutComparer } from '../utils'; + +const template = { + label: '@global@.viz.common.enum.fontAlignment.alignment', + key: 'align', + default: 'left', + comType: 'select', + options: { + translateItemLabel: true, + items: [ + { + label: '@global@.viz.common.enum.fontAlignment.left', + value: 'left', + }, + { + label: '@global@.viz.common.enum.fontAlignment.center', + value: 'center', + }, + { + label: '@global@.viz.common.enum.fontAlignment.right', + value: 'right', + }, + ], + }, +}; + +const FontAlignment: FC> = memo( + ({ + ancestors, + translate: t = title => title, + data, + dataConfigs, + onChange, + }) => { + const props = { + ancestors, + data: Object.assign({}, data, { + label: data?.label || template.label, + key: data?.key || template.key, + default: data?.default || template.default, + options: data?.options || template.options, + comType: 'select', + }), + translate: t, + onChange, + dataConfigs, + }; + + return ; + }, + itemLayoutComparer, +); + +export default FontAlignment; diff --git a/frontend/src/app/components/FormGenerator/Customize/ListTemplatePanel.tsx b/frontend/src/app/components/FormGenerator/Customize/ListTemplatePanel.tsx index b96b72c96..451b90fb4 100644 --- a/frontend/src/app/components/FormGenerator/Customize/ListTemplatePanel.tsx +++ b/frontend/src/app/components/FormGenerator/Customize/ListTemplatePanel.tsx @@ -136,6 +136,7 @@ const ListTemplatePanel: FC> = memo( data={r} translate={t} onChange={handleChildComponentUpdate(r.key)} + context={currentSelectedItem} /> ); }; @@ -146,7 +147,7 @@ const ListTemplatePanel: FC> = memo( diff --git a/frontend/src/app/components/FormGenerator/Customize/UnControlledTableHeaderPanel.tsx b/frontend/src/app/components/FormGenerator/Customize/UnControlledTableHeaderPanel.tsx index 303474368..d2a3d7cc2 100644 --- a/frontend/src/app/components/FormGenerator/Customize/UnControlledTableHeaderPanel.tsx +++ b/frontend/src/app/components/FormGenerator/Customize/UnControlledTableHeaderPanel.tsx @@ -22,22 +22,22 @@ import { CheckOutlined, DeleteOutlined, EditOutlined, + RedoOutlined, } from '@ant-design/icons'; import { Button, Col, Input, Row, Space, Table } from 'antd'; import { + ChartDataSectionConfig, ChartDataSectionType, ChartStyleSectionConfig, } from 'app/types/ChartConfig'; import { - diffHeaderRows, - flattenHeaderRowsWithoutGroupRow, getColumnRenderName, + getUnusedHeaderRows, } from 'app/utils/chartHelper'; import { DATARTSEPERATOR } from 'globalConstants'; import { FC, memo, useState } from 'react'; import styled from 'styled-components'; import { CloneValueDeep } from 'utils/object'; -import { BaiscSelector, BasicColorSelector, BasicFont } from '../Basic'; import { ItemLayoutProps } from '../types'; import { itemLayoutComparer } from '../utils'; @@ -57,6 +57,18 @@ interface RowValue { children?: RowValue[]; } +const getFlattenHeaders = (dataConfigs: ChartDataSectionConfig[] = []) => { + const newDataConfigs = CloneValueDeep(dataConfigs); + return newDataConfigs + .filter( + c => + ChartDataSectionType.AGGREGATE === c.type || + ChartDataSectionType.GROUP === c.type || + ChartDataSectionType.MIXED === c.type, + ) + .flatMap(config => config.rows || []); +}; + const UnControlledTableHeaderPanel: FC< ItemLayoutProps > = memo( @@ -70,43 +82,13 @@ const UnControlledTableHeaderPanel: FC< const [selectedRowUids, setSelectedRowUids] = useState([]); const [myData, setMyData] = useState(() => CloneValueDeep(data)); const [tableDataSource, setTableDataSource] = useState(() => { - const currentHeaderRows = (CloneValueDeep(dataConfigs) || []) - .filter( - c => - ChartDataSectionType.AGGREGATE === c.type || - ChartDataSectionType.GROUP === c.type || - ChartDataSectionType.MIXED === c.type, - ) - .flatMap(config => config.rows || []); - - const oldGroupedHeaderRows: RowValue[] = myData?.value || []; - const oldFlattenedHeaderRows: RowValue[] = oldGroupedHeaderRows.flatMap( - row => flattenHeaderRowsWithoutGroupRow(row), - ); - const isChanged = diffHeaderRows( - oldFlattenedHeaderRows, + const originalFlattenHeaderRows = getFlattenHeaders(dataConfigs); + const currentHeaderRows: RowValue[] = myData?.value || []; + const unusedHeaderRows = getUnusedHeaderRows( + originalFlattenHeaderRows || [], currentHeaderRows, ); - if (!isChanged) { - oldFlattenedHeaderRows.forEach(oldRow => { - const current = currentHeaderRows?.find(v => v.uid === oldRow.uid); - Object.assign(oldRow, current); - }); - return oldGroupedHeaderRows; - } - - return (CloneValueDeep(dataConfigs) || []) - .filter( - c => - ChartDataSectionType.AGGREGATE === c.type || - ChartDataSectionType.GROUP === c.type || - ChartDataSectionType.MIXED === c.type, - ) - .flatMap(config => config.rows || []) - .map(r => { - const previous = oldFlattenedHeaderRows?.find(v => v.uid === r.uid); - return { ...previous, ...r }; - }); + return currentHeaderRows.concat(unusedHeaderRows); }); const mergeRowToGroup = () => { @@ -120,7 +102,6 @@ const UnControlledTableHeaderPanel: FC< mergeSameLineageAncesterRows(lineageRowUids); const ancestorsRows = makeSameLinageRows(noDuplicateLineageRows); const newDataSource = groupTreeNode(ancestorsRows, tableDataSource); - handleConfigChange([...newDataSource]); }; @@ -175,7 +156,7 @@ const UnControlledTableHeaderPanel: FC< }; const groupTreeNode = (rowAncestors, collection) => { - if (rowAncestors && rowAncestors.length <= 1) { + if (rowAncestors && rowAncestors.length < 1) { return collection; } @@ -201,7 +182,7 @@ const UnControlledTableHeaderPanel: FC< const groupRow = { uid: groupRowUid, colName: groupRowUid, - label: 'Please input header name', + label: t('table.header.newName'), isGroup: true, children: selectedRows, }; @@ -239,6 +220,11 @@ const UnControlledTableHeaderPanel: FC< }); }; + const handleRollback = () => { + const originalFlattenHeaders = getFlattenHeaders(dataConfigs); + handleConfigChange?.(originalFlattenHeaders); + }; + const handleTableRowChange = rowUid => style => prop => (_, value) => { const brotherRows = findRowBrothers(rowUid, tableDataSource); const row = brotherRows.find(r => r.uid === rowUid); @@ -295,110 +281,22 @@ const UnControlledTableHeaderPanel: FC< const { label, isGroup, uid } = record; return isGroup ? ( <> + handleDeleteGroupRow(uid)} + /> handleTableRowChange(uid)(undefined)('label')([], value) } /> - handleDeleteGroupRow(uid)} /> > ) : ( getColumnRenderName(record) ); }, }, - { - title: t('table.header.backgroundColor'), - dataIndex: 'backgroundColor', - key: 'backgroundColor', - width: 100, - render: (_, record) => { - const { style, uid } = record; - const row = { - label: 'column.backgroundColor', - key: 'backgroundColor', - comType: 'fontColor', - value: style?.backgroundColor, - options: { - hideLabel: true, - }, - }; - return ( - - ); - }, - }, - { - title: t('table.header.font'), - dataIndex: 'font', - key: 'font', - width: 500, - render: (_, record) => { - const { style, uid } = record; - const row = { - label: 'column.font', - key: 'font', - comType: 'font', - value: style?.font?.value, - options: { - hideLabel: true, - }, - default: { - fontFamily: 'PingFang SC', - fontSize: '12', - fontWeight: 'normal', - fontStyle: 'normal', - color: 'black', - }, - }; - return ( - - ); - }, - }, - { - title: t('table.header.align.title'), - dataIndex: 'align', - key: 'align', - width: 150, - render: (_, record) => { - const { style, uid } = record; - const row = { - label: 'column.align', - key: 'align', - comType: 'select', - default: 'left', - value: style?.align, - options: { - hideLabel: true, - items: [ - { label: t('table.header.align.left'), value: 'left' }, - { label: t('table.header.align.center'), value: 'center' }, - { label: t('table.header.align.right'), value: 'right' }, - ], - }, - }; - return ( - - ); - }, - }, ]; const rowSelection = { @@ -411,39 +309,43 @@ const UnControlledTableHeaderPanel: FC< return ( - - - {t('table.header.merge')} - - + + + {t('table.header.merge')} + + } + onClick={handleRowMoveUp} + > + {t('table.header.moveUp')} + + } + onClick={handleRowMoveDown} + > + {t('table.header.moveDown')} + + + + - - } - onClick={handleRowMoveUp} - > - {t('table.header.moveUp')} - - } - onClick={handleRowMoveDown} - > - {t('table.header.moveDown')} - - + } onClick={handleRollback}> + {t('table.header.reset')} + {label} } onClick={() => setIsEditing(true)} > @@ -495,9 +398,13 @@ const EditableLabel: FC<{ ); }; - return render(); + return {render()}; }); +const StyledEditableLabel = styled.div` + display: inline-block; +`; + const StyledUnControlledTableHeaderPanel = styled(Space)` width: 100%; margin-top: 10px; diff --git a/frontend/src/app/components/FormGenerator/Customize/index.ts b/frontend/src/app/components/FormGenerator/Customize/index.ts index 5d7fdb711..bcdb6f591 100644 --- a/frontend/src/app/components/FormGenerator/Customize/index.ts +++ b/frontend/src/app/components/FormGenerator/Customize/index.ts @@ -16,7 +16,8 @@ * limitations under the License. */ -export { default as DataCachePanel } from './DataCachePanel'; +export { default as ConditionStylePanel } from './ConditionStylePanel'; export { default as DataReferencePanel } from './DataReferencePanel'; +export { default as FontAlignment } from './FontAlignment'; export { default as ListTemplatePanel } from './ListTemplatePanel'; export { default as UnControlledTableHeaderPanel } from './UnControlledTableHeaderPanel'; diff --git a/frontend/src/app/components/FormGenerator/Layout/CollectionLayout.tsx b/frontend/src/app/components/FormGenerator/Layout/CollectionLayout.tsx index 587d63bf8..48abeb565 100644 --- a/frontend/src/app/components/FormGenerator/Layout/CollectionLayout.tsx +++ b/frontend/src/app/components/FormGenerator/Layout/CollectionLayout.tsx @@ -41,34 +41,46 @@ import { groupLayoutComparer } from '../utils'; import ItemLayout from './ItemLayout'; const CollectionLayout: FC> = - memo(({ ancestors, translate, data, dataConfigs, flatten, onChange }) => { - const getDependencyValue = useCallback((watcher, children) => { - if (watcher?.deps) { - // Note: only support depend on one property for now. - const dependencyKey = watcher?.deps?.[0]; - return children?.find(r => r.key === dependencyKey)?.value; - } - }, []); + memo( + ({ + ancestors, + translate, + data, + dataConfigs, + flatten, + onChange, + context, + }) => { + const getDependencyValue = useCallback((watcher, children) => { + if (watcher?.deps) { + // Note: only support depend on one property for now. + const dependencyKey = watcher?.deps?.[0]; + return children?.find(r => r.key === dependencyKey)?.value; + } + }, []); - return ( - - {data?.rows - ?.filter(r => Boolean(!r.hide)) - .map((r, index) => ( - - ))} - - ); - }, groupLayoutComparer); + return ( + + {data?.rows + ?.filter(r => Boolean(!r.hide)) + .map((r, index) => ( + + ))} + + ); + }, + groupLayoutComparer, + ); export default CollectionLayout; diff --git a/frontend/src/app/components/FormGenerator/Layout/GroupLayout.tsx b/frontend/src/app/components/FormGenerator/Layout/GroupLayout.tsx index f3242cb91..a25dea80f 100644 --- a/frontend/src/app/components/FormGenerator/Layout/GroupLayout.tsx +++ b/frontend/src/app/components/FormGenerator/Layout/GroupLayout.tsx @@ -17,7 +17,7 @@ */ import { Button, Collapse } from 'antd'; -import useStateModal from 'app/hooks/useStateModal'; +import useStateModal, { StateModalSize } from 'app/hooks/useStateModal'; import { ChartStyleSectionConfig } from 'app/types/ChartConfig'; import { FC, memo, useState } from 'react'; import styled from 'styled-components/macro'; @@ -41,10 +41,13 @@ const GroupLayout: FC> = memo( dataConfigs, flatten, onChange, + context, }) => { const [openStateModal, contextHolder] = useStateModal({}); const [type] = useState(data?.options?.type || 'default'); - const [modalSize] = useState(data?.options?.modalSize || ''); + const [modalSize] = useState( + data?.options?.modalSize || StateModalSize.SMALL, + ); const [expand] = useState(!!data?.options?.expand); const handleConfrimModalDialogOrDataUpdate = ( @@ -112,6 +115,7 @@ const GroupLayout: FC> = memo( dataConfigs={dataConfigs} flatten={flatten} onChange={onChangeEvent} + context={context} /> ); }; diff --git a/frontend/src/app/components/FormGenerator/Layout/ItemLayout.tsx b/frontend/src/app/components/FormGenerator/Layout/ItemLayout.tsx index 309c5e9a1..1d980fb29 100644 --- a/frontend/src/app/components/FormGenerator/Layout/ItemLayout.tsx +++ b/frontend/src/app/components/FormGenerator/Layout/ItemLayout.tsx @@ -32,7 +32,6 @@ import { } from 'utils/object'; import { GroupLayout } from '.'; import { - BaiscSelector, BasicCheckbox, BasicColorSelector, BasicFont, @@ -43,14 +42,17 @@ import { BasicInputPercentage, BasicLine, BasicMarginWidth, + BasicRadio, + BasicSelector, BasicSlider, BasicSwitch, BasicText, BasicUnControlledTabPanel, } from '../Basic'; import { - DataCachePanel, + ConditionStylePanel, DataReferencePanel, + FontAlignment, ListTemplatePanel, UnControlledTableHeaderPanel, } from '../Customize'; @@ -68,6 +70,7 @@ const ItemLayout: FC> = memo( onChange, dataConfigs, flatten, + context, }) => { useEffect(() => { const key = data?.watcher?.deps?.[0] as string; @@ -96,6 +99,7 @@ const ItemLayout: FC> = memo( }); onChange?.(ancestors, newData); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [dependency]); const handleDataChange = ( @@ -116,6 +120,7 @@ const ItemLayout: FC> = memo( translate, onChange: handleDataChange, dataConfigs, + context, }; switch (data.comType) { @@ -126,7 +131,7 @@ const ItemLayout: FC> = memo( case ChartStyleSectionComponentType.INPUT: return ; case ChartStyleSectionComponentType.SELECT: - return ; + return ; case ChartStyleSectionComponentType.TABS: return ; case ChartStyleSectionComponentType.FONT: @@ -149,16 +154,20 @@ const ItemLayout: FC> = memo( return ; case ChartStyleSectionComponentType.LINE: return ; - case ChartStyleSectionComponentType.CACHE: - return ; case ChartStyleSectionComponentType.REFERENCE: return ; case ChartStyleSectionComponentType.TABLEHEADER: return ; + case ChartStyleSectionComponentType.CONDITIONSTYLE: + return ; case ChartStyleSectionComponentType.GROUP: return ; case ChartStyleSectionComponentType.TEXT: return ; + case ChartStyleSectionComponentType.RADIO: + return ; + case ChartStyleSectionComponentType.FontAlignment: + return ; default: return {`no matched component comType of ${data.comType}`}; } diff --git a/frontend/src/app/components/FormGenerator/types.ts b/frontend/src/app/components/FormGenerator/types.ts index 539ad3a3f..cddf38daa 100644 --- a/frontend/src/app/components/FormGenerator/types.ts +++ b/frontend/src/app/components/FormGenerator/types.ts @@ -20,6 +20,7 @@ export interface ItemLayoutProps { ) => void; dataConfigs?: ChartDataSectionConfig[]; flatten?: boolean; + context?: any; } export interface FormGeneratorLayoutProps extends ItemLayoutProps { diff --git a/frontend/src/app/components/From/FormItemEx.tsx b/frontend/src/app/components/From/FormItemEx.tsx index ab326cdb9..2672671b2 100644 --- a/frontend/src/app/components/From/FormItemEx.tsx +++ b/frontend/src/app/components/From/FormItemEx.tsx @@ -38,4 +38,8 @@ const StyledFromItemEx = styled(Form.Item)` .ant-form-item-explain { padding-left: 10px; } + + .ant-form-item-control-input { + width: 100%; + } `; diff --git a/frontend/src/app/components/ListItem.tsx b/frontend/src/app/components/ListItem.tsx index cf79298a9..40d21fd9d 100644 --- a/frontend/src/app/components/ListItem.tsx +++ b/frontend/src/app/components/ListItem.tsx @@ -47,11 +47,6 @@ const StyledItem = styled(Item)` color: ${p => p.theme.textColorSnd}; text-overflow: ellipsis; white-space: nowrap; - - > span { - margin-right: ${SPACE_XS}; - color: ${p => p.theme.textColorDisabled}; - } } &.with-avatar { diff --git a/frontend/src/app/components/ListTitle/index.tsx b/frontend/src/app/components/ListTitle/index.tsx index ed9c30825..8e097f5bd 100644 --- a/frontend/src/app/components/ListTitle/index.tsx +++ b/frontend/src/app/components/ListTitle/index.tsx @@ -1,6 +1,7 @@ import { LeftOutlined, MoreOutlined, SearchOutlined } from '@ant-design/icons'; import { Input, Menu, Space, Tooltip } from 'antd'; import { MenuListItem, Popup, ToolbarButton } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { ReactElement, useCallback, useState } from 'react'; import styled from 'styled-components/macro'; import { @@ -60,7 +61,7 @@ export function ListTitle({ onNext, }: ListTitleProps) { const [searchbarVisible, setSearchbarVisible] = useState(false); - + const t = useI18NPrefix('components.listTitle'); const toggleSearchbar = useCallback(() => { setSearchbarVisible(!searchbarVisible); }, [searchbarVisible]); @@ -88,7 +89,7 @@ export function ListTitle({ {subTitle && {subTitle}} {search && ( - + } @@ -129,7 +130,7 @@ export function ListTitle({ } - placeholder="搜索名称关键字" + placeholder={t('searchValue')} bordered={false} onChange={onSearch} /> @@ -157,17 +158,23 @@ const Title = styled.div` h3 { flex: 1; + overflow: hidden; padding: ${SPACE_MD} 0; font-size: ${FONT_SIZE_TITLE}; font-weight: ${FONT_WEIGHT_MEDIUM}; + text-overflow: ellipsis; + white-space: nowrap; } h5 { flex: 1; + overflow: hidden; padding: ${SPACE_XS} 0; font-size: ${FONT_SIZE_SUBTITLE}; font-weight: ${FONT_WEIGHT_MEDIUM}; color: ${p => p.theme.textColorLight}; + text-overflow: ellipsis; + white-space: nowrap; } .back { diff --git a/frontend/src/app/components/ModalForm.tsx b/frontend/src/app/components/ModalForm.tsx index 6f8b193f7..49bea9a8a 100644 --- a/frontend/src/app/components/ModalForm.tsx +++ b/frontend/src/app/components/ModalForm.tsx @@ -1,5 +1,6 @@ import { Form, FormProps, Modal, ModalProps } from 'antd'; -import { CommonFormTypes, COMMON_FORM_TITLE_PREFIX } from 'globalConstants'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { CommonFormTypes } from 'globalConstants'; import { forwardRef, ReactNode, useCallback, useImperativeHandle } from 'react'; export interface ModalFormProps extends ModalProps { @@ -15,6 +16,7 @@ export const ModalForm = forwardRef( ref, ) => { const [form] = Form.useForm(); + const tg = useI18NPrefix('global'); useImperativeHandle(ref, () => form); const onOk = useCallback(() => { @@ -29,7 +31,7 @@ export const ModalForm = forwardRef( return ( diff --git a/frontend/src/app/components/Popup/MenuListItem.tsx b/frontend/src/app/components/Popup/MenuListItem.tsx index a943f91d2..62c518892 100644 --- a/frontend/src/app/components/Popup/MenuListItem.tsx +++ b/frontend/src/app/components/Popup/MenuListItem.tsx @@ -1,5 +1,5 @@ import { Menu, MenuItemProps } from 'antd'; -import React, { cloneElement, ReactElement } from 'react'; +import React, { cloneElement, ReactElement, ReactNode } from 'react'; import styled, { css } from 'styled-components/macro'; import { LINE_HEIGHT_HEADING, SPACE, SPACE_XS } from 'styles/StyleConstants'; import { mergeClassNames } from 'utils/utils'; @@ -10,36 +10,68 @@ const WrapperStyle = css` &.selected { background-color: ${p => p.theme.emphasisBackground}; } + + .ant-dropdown-menu-submenu-title { + line-height: ${LINE_HEIGHT_HEADING}; + } `; interface MenuListItemProps extends Omit { prefix?: ReactElement; suffix?: ReactElement; + sub?: boolean; } export function MenuListItem({ prefix, suffix, + sub, ...menuProps }: MenuListItemProps) { - return ( + return sub ? ( + + {menuProps.title} + + } + > + {menuProps.children} + + ) : ( - - {prefix && - cloneElement(prefix, { - className: mergeClassNames(prefix.props.className, 'prefix'), - })} + {menuProps.children} - {suffix && - cloneElement(suffix, { - className: mergeClassNames(suffix.props.className, 'suffix'), - })} ); } -const ListItem = styled.div` +interface ListItemProps { + prefix?: ReactElement; + suffix?: ReactElement; + children?: ReactNode; +} + +function ListItem({ prefix, suffix, children }: ListItemProps) { + return ( + + {prefix && + cloneElement(prefix, { + className: mergeClassNames(prefix.props.className, 'prefix'), + })} + {children} + {suffix && + cloneElement(suffix, { + className: mergeClassNames(suffix.props.className, 'suffix'), + })} + + ); +} + +const StyledListItem = styled.div` display: flex; align-items: center; diff --git a/frontend/src/app/components/ReactColorPicker/ColorPanel.tsx b/frontend/src/app/components/ReactColorPicker/ColorPanel.tsx deleted file mode 100644 index e69f9ce0a..000000000 --- a/frontend/src/app/components/ReactColorPicker/ColorPanel.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { FC, useCallback } from 'react'; -import { ColorResult, SketchPicker, SketchPickerProps } from 'react-color'; -import styled from 'styled-components/macro'; -type ValueType = string | undefined; -export interface ColorPanelProps { - value?: ValueType; - onChange?: (value: ValueType, colorResult?: ColorResult) => void; - colors?: SketchPickerProps['presetColors']; -} -const toChangeValue = (data: ColorResult) => { - const { r, g, b, a } = data.rgb; - return `rgba(${r}, ${g}, ${b}, ${a})`; -}; -export const ReactColorPicker: FC = ({ value, onChange }) => { - const onChangeComplete = useCallback( - v => { - const rgbaValue = toChangeValue(v); - onChange?.(rgbaValue, v); - }, - [onChange], - ); - - return ( - - ); -}; - -const SketchPickerPanel = styled(SketchPicker)` - width: 260px !important; - padding: 0 !important; - border-radius: 0 !important; - box-shadow: none !important; -`; - -export type { ColorResult } from 'react-color'; diff --git a/frontend/src/app/components/ReactColorPicker/ColorPickerPopover.tsx b/frontend/src/app/components/ReactColorPicker/ColorPickerPopover.tsx deleted file mode 100644 index cced4054c..000000000 --- a/frontend/src/app/components/ReactColorPicker/ColorPickerPopover.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Button, Popover, PopoverProps, Row } from 'antd'; -import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components/macro'; -import { SPACE_MD } from 'styles/StyleConstants'; -import { ColorPanelProps, ColorResult, ReactColorPicker } from './ColorPanel'; -import { ColorPicker } from './ColorTag'; - -interface ColorPickerPopoverProps extends ColorPanelProps { - popoverProps?: PopoverProps; - defaultValue?: string; - onSubmit?: ColorPanelProps['onChange']; - colorPickerClass?: string; -} -export const ColorPickerPopover: FC> = - ({ children, defaultValue, popoverProps, onSubmit, colorPickerClass }) => { - const [visible, setVisible] = useState(false); - const [color, setColor] = useState(defaultValue); - const [colorResult, setColorResult] = useState(); - - useEffect(() => { - if (visible) { - setColor(defaultValue); - } - }, [visible, defaultValue]); - const onCancel = useCallback(() => { - setVisible(false); - }, []); - const onSure = useCallback(() => { - onSubmit?.(color, colorResult); - onCancel(); - }, [onSubmit, color, colorResult, onCancel]); - const onColorChange = useCallback((color, result) => { - setColor(color); - setColorResult(result); - }, []); - const _popoverProps = useMemo(() => { - return typeof popoverProps === 'object' ? popoverProps : {}; - }, [popoverProps]); - return ( - - - - - 取消 - - - 确定 - - - - } - trigger="click" - placement="right" - > - {children || ( - - )} - - ); - }; - -const ContentWrapper = styled.div` - .ant-btn-primary { - margin-left: ${SPACE_MD}; - } -`; diff --git a/frontend/src/app/components/ReactColorPicker/index.tsx b/frontend/src/app/components/ReactColorPicker/index.tsx deleted file mode 100644 index 489d25afb..000000000 --- a/frontend/src/app/components/ReactColorPicker/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { ReactColorPicker } from './ColorPanel'; -import { ColorPickerPopover } from './ColorPickerPopover'; -import { ColorTag } from './ColorTag'; -export { ReactColorPicker, ColorPickerPopover, ColorTag }; diff --git a/frontend/src/app/components/ReactFrameComponent/Content.jsx b/frontend/src/app/components/ReactFrameComponent/Content.jsx new file mode 100644 index 000000000..47b78362d --- /dev/null +++ b/frontend/src/app/components/ReactFrameComponent/Content.jsx @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types'; +import { Children, Component } from 'react'; + +export default class Content extends Component { + static propTypes = { + children: PropTypes.element.isRequired, + contentDidMount: PropTypes.func.isRequired, + contentDidUpdate: PropTypes.func.isRequired, + }; + + componentDidMount() { + this.props.contentDidMount(); + } + + componentDidUpdate() { + this.props.contentDidUpdate(); + } + + render() { + return Children.only(this.props.children); + } +} diff --git a/frontend/src/app/components/ReactFrameComponent/Context.jsx b/frontend/src/app/components/ReactFrameComponent/Context.jsx new file mode 100644 index 000000000..80c4cf24d --- /dev/null +++ b/frontend/src/app/components/ReactFrameComponent/Context.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +let doc; +let win; +if (typeof document !== 'undefined') { + doc = document; +} +if (typeof window !== 'undefined') { + win = window; +} + +export const FrameContext = React.createContext({ document: doc, window: win }); + +export const useFrame = () => React.useContext(FrameContext); + +export const { + Provider: FrameContextProvider, + Consumer: FrameContextConsumer, +} = FrameContext; diff --git a/frontend/src/app/components/ReactFrameComponent/Frame.jsx b/frontend/src/app/components/ReactFrameComponent/Frame.jsx new file mode 100644 index 000000000..4e412c5c2 --- /dev/null +++ b/frontend/src/app/components/ReactFrameComponent/Frame.jsx @@ -0,0 +1,134 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Content from './Content'; +import { FrameContextProvider } from './Context'; + +export class Frame extends Component { + // React warns when you render directly into the body since browser extensions + // also inject into the body and can mess up React. For this reason + // initialContent is expected to have a div inside of the body + // element that we render react into. + static propTypes = { + style: PropTypes.object, + head: PropTypes.node, + mountTarget: PropTypes.string, + contentDidMount: PropTypes.func, + contentDidUpdate: PropTypes.func, + children: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.arrayOf(PropTypes.element), + ]), + }; + + static defaultProps = { + style: {}, + head: null, + children: undefined, + mountTarget: undefined, + contentDidMount: () => {}, + contentDidUpdate: () => {}, + }; + + constructor(props, context) { + super(props, context); + this._isMounted = false; + this.nodeRef = React.createRef(); + this.state = { iframeLoaded: false }; + } + + componentDidMount() { + this._isMounted = true; + const doc = this.getDoc(); + if (doc && doc.readyState === 'complete') { + this.forceUpdate(); + } else { + this.nodeRef.current.addEventListener('load', this.handleLoad); + } + } + + componentWillUnmount() { + this._isMounted = false; + this.nodeRef.current.removeEventListener('load', this.handleLoad); + } + + getDoc() { + return this.nodeRef.current ? this.nodeRef.current.contentDocument : null; // eslint-disable-line + } + + getMountTarget() { + const doc = this.getDoc(); + if (this.props.mountTarget) { + return doc.querySelector(this.props.mountTarget); + } + return doc.body; + } + + setRef = node => { + this.nodeRef.current = node; + if (!this.nodeRef.current?.contentWindow) { + return; + } + }; + + handleLoad = () => { + this.setState({ iframeLoaded: true }); + }; + + renderFrameContents() { + if (!this._isMounted) { + return null; + } + const doc = this.getDoc(); + const contentDidMount = this.props.contentDidMount; + const contentDidUpdate = this.props.contentDidUpdate; + + const win = doc.defaultView || doc.parentView; + const contents = ( + + + {this.props.children} + + + ); + + const mountTarget = this.getMountTarget(); + const res = [ + ReactDOM.createPortal(this.props.head, this.getDoc().head), + ReactDOM.createPortal(contents, mountTarget), + ]; + + return res; + } + + render() { + const props = { + ...this.props, + children: undefined, // The iframe isn't ready so we drop children from props here. #12, #17 + }; + + delete props.head; + /** + * Because ios wechat browser issue which not support srcDoc props + * we remove this props and also to avoid performance issue with + * document.write function, but PR is still welcome! + */ + delete props.srcDoc; + delete props.mountTarget; + delete props.contentDidMount; + delete props.contentDidUpdate; + return ( + + {this.renderFrameContents()} + + ); + } +} diff --git a/frontend/src/app/components/ReactFrameComponent/index.js b/frontend/src/app/components/ReactFrameComponent/index.js new file mode 100644 index 000000000..47f08f142 --- /dev/null +++ b/frontend/src/app/components/ReactFrameComponent/index.js @@ -0,0 +1,2 @@ +export { FrameContext, FrameContextConsumer, useFrame } from './Context.jsx'; +export { Frame } from './Frame'; diff --git a/frontend/src/app/components/VirtualTable.tsx b/frontend/src/app/components/VirtualTable.tsx new file mode 100644 index 000000000..691192c2d --- /dev/null +++ b/frontend/src/app/components/VirtualTable.tsx @@ -0,0 +1,164 @@ +/** + * 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 { Empty, Table, TableProps } from 'antd'; +import classNames from 'classnames'; +import React, { + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { VariableSizeGrid as Grid } from 'react-window'; +import { SPACE_TIMES } from 'styles/StyleConstants'; + +interface VirtualTableProps extends TableProps { + width: number; + scroll: { x: number; y: number }; + columns: any; +} + +/** + * Table组件中使用了虚拟滚动条 渲染的速度变快 基于(react-windows) + * 使用方法:import { VirtualTable } from 'app/components/VirtualTable'; + * + */ +export const VirtualTable = memo((props: VirtualTableProps) => { + const { columns, scroll, width: boxWidth, dataSource } = props; + const widthColumns = columns.map(v => v.width); + const gridRef: any = useRef(); + const isFull = useRef(false); + const widthColumnCount = widthColumns.filter(width => !width).length; + const [connectObject] = useState(() => { + const obj = {}; + Object.defineProperty(obj, 'scrollLeft', { + get: () => null, + set: scrollLeft => { + if (gridRef.current) { + gridRef.current.scrollTo({ + scrollLeft, + }); + } + }, + }); + return obj; + }); + isFull.current = boxWidth > scroll.x; + + if (isFull.current === true) { + widthColumns.forEach((v, i) => { + return (widthColumns[i] = + widthColumns[i] + (boxWidth - scroll.x) / widthColumns.length); + }); + } + + const mergedColumns = useMemo(() => { + return columns.map((column, i) => { + return { + ...column, + width: column.width + ? widthColumns[i] + : Math.floor(boxWidth / widthColumnCount), + }; + }); + }, [boxWidth, columns, widthColumnCount, widthColumns]); + + const resetVirtualGrid = useCallback(() => { + gridRef.current?.resetAfterIndices({ + columnIndex: 0, + shouldForceUpdate: true, + }); + }, [gridRef]); + + useEffect(() => resetVirtualGrid, [boxWidth, dataSource, resetVirtualGrid]); + + const renderVirtualList = useCallback( + (rawData, { scrollbarSize, ref, onScroll }) => { + ref.current = connectObject; + const totalHeight = rawData.length * 39; + + if (!dataSource?.length) { + //如果数据为空 If the data is empty + return ; + } + + return ( + { + const { width } = mergedColumns[index]; + return totalHeight > scroll.y && index === mergedColumns.length - 1 + ? width - scrollbarSize - 16 + : width; + }} + height={scroll.y} + rowCount={rawData.length} + rowHeight={() => 39} + width={boxWidth} + onScroll={({ scrollLeft }) => { + onScroll({ + scrollLeft, + }); + }} + > + {({ rowIndex, columnIndex, style }) => { + style = { + padding: `${SPACE_TIMES(2)}`, + textAlign: mergedColumns[columnIndex].align, + ...style, + borderBottom: '1px solid #f0f0f0', + }; + return ( + + {rawData[rowIndex][mergedColumns[columnIndex].dataIndex]} + + ); + }} + + ); + }, + [mergedColumns, boxWidth, connectObject, dataSource, scroll], + ); + + return ( + + ); +}); diff --git a/frontend/src/app/components/VizHeader/VizHeader.tsx b/frontend/src/app/components/VizHeader/VizHeader.tsx index cf7d870b1..aa84fcfcd 100644 --- a/frontend/src/app/components/VizHeader/VizHeader.tsx +++ b/frontend/src/app/components/VizHeader/VizHeader.tsx @@ -29,12 +29,11 @@ import { VizOperationMenu, } from 'app/components/VizOperationMenu'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; -import { FC, memo, useState } from 'react'; +import { TITLE_SUFFIX } from 'globalConstants'; +import { FC, memo, useMemo, useState } from 'react'; import styled from 'styled-components/macro'; import { DetailPageHeader } from '../DetailPageHeader'; -const TITLE_SUFFIX = ['[已归档]', '[未发布]']; - const VizHeader: FC<{ chartName?: string; status?: number; @@ -63,7 +62,7 @@ const VizHeader: FC<{ allowShare, allowManage, }) => { - const t = useI18NPrefix(`viz.chartPreview`); + const t = useI18NPrefix(`viz.action`); const [showShareLinkModal, setShowShareLinkModal] = useState(false); const handleCloseShareLinkModal = () => { @@ -85,7 +84,13 @@ const VizHeader: FC<{ ); }; - const title = `${chartName || ''} ${TITLE_SUFFIX[Number(status)] || ''}`; + const title = useMemo(() => { + const base = chartName || ''; + const suffix = TITLE_SUFFIX[Number(status)] + ? `[${t(TITLE_SUFFIX[Number(status)])}]` + : ''; + return base + suffix; + }, [chartName, status, t]); const isArchived = Number(status) === 0; return ( diff --git a/frontend/src/app/components/VizOperationMenu/VizOperationMenu.tsx b/frontend/src/app/components/VizOperationMenu/VizOperationMenu.tsx index 0ce61ed8d..184710121 100644 --- a/frontend/src/app/components/VizOperationMenu/VizOperationMenu.tsx +++ b/frontend/src/app/components/VizOperationMenu/VizOperationMenu.tsx @@ -34,7 +34,7 @@ const VizOperationMenu: FC<{ allowDownload, allowShare, }) => { - const t = useI18NPrefix(`viz.chartPreview`); + const t = useI18NPrefix(`viz.action`); const moreActionMenu = () => { const menus: any[] = []; diff --git a/frontend/src/app/components/VizOperationMenu/components/ShareLinkModal.tsx b/frontend/src/app/components/VizOperationMenu/components/ShareLinkModal.tsx index d307da90b..622545ca9 100644 --- a/frontend/src/app/components/VizOperationMenu/components/ShareLinkModal.tsx +++ b/frontend/src/app/components/VizOperationMenu/components/ShareLinkModal.tsx @@ -35,7 +35,7 @@ const ShareLinkModal: FC<{ onOk?; onCancel?; }> = memo(({ visibility, onGenerateShareLink, onOk, onCancel }) => { - const t = useI18NPrefix(`viz.chartPreview`); + const t = useI18NPrefix(`viz.action`); const [expireDate, setExpireDate] = useState(); const [enablePassword, setEnablePassword] = useState(false); const [shareLink, setShareLink] = useState<{ @@ -44,7 +44,7 @@ const ShareLinkModal: FC<{ usePassword?: boolean; }>(); - const hanldeCopyToClipboard = value => { + const handleCopyToClipboard = value => { const ta = document.createElement('textarea'); ta.innerText = value; document.body.appendChild(ta); @@ -63,7 +63,7 @@ const ShareLinkModal: FC<{ }`; }; - const hanldeGenerateShareLink = async (expireDate, enablePassword) => { + const handleGenerateShareLink = async (expireDate, enablePassword) => { const result = await onGenerateShareLink?.(expireDate, enablePassword); setShareLink(result); }; @@ -106,7 +106,7 @@ const ShareLinkModal: FC<{ htmlType="button" disabled={!expireDate} onClick={() => - hanldeGenerateShareLink?.(expireDate, enablePassword) + handleGenerateShareLink?.(expireDate, enablePassword) } > {t('share.generateLink')} @@ -119,7 +119,7 @@ const ShareLinkModal: FC<{ addonAfter={ - hanldeCopyToClipboard(getFullShareLinkPath(shareLink)) + handleCopyToClipboard(getFullShareLinkPath(shareLink)) } /> } @@ -132,7 +132,7 @@ const ShareLinkModal: FC<{ value={shareLink?.password} addonAfter={ hanldeCopyToClipboard(shareLink?.password)} + onClick={() => handleCopyToClipboard(shareLink?.password)} /> } /> diff --git a/frontend/src/app/constants.ts b/frontend/src/app/constants.ts deleted file mode 100644 index 6e94ac58a..000000000 --- a/frontend/src/app/constants.ts +++ /dev/null @@ -1,39 +0,0 @@ -const REGS = { - password: /^[0-9a-zA-Z][0-9a-zA-Z_]{5,19}$/, -}; -export const RULES = { - password: [ - { - required: true, - message: '密码不能为空', - }, - { - validator(_, value) { - if (value && !REGS.password.test(value)) { - return Promise.reject(new Error('由6-20位字母、数字、下划线组成')); - } - return Promise.resolve(); - }, - }, - ], - getConfirmRule: (filed = 'newPassword') => { - return [ - { required: true, message: '密码不能为空' }, - ({ getFieldValue }) => { - return { - validator(_, value) { - if (value && !REGS.password.test(value)) { - return Promise.reject( - new Error('由6-20位字母、数字、下划线组成'), - ); - } - if (value && getFieldValue(filed) !== value) { - return Promise.reject(new Error('两次输入的密码不一致')); - } - return Promise.resolve(); - }, - }; - }, - ]; - }, -}; diff --git a/frontend/src/app/hooks/useCacheWidthHeight.ts b/frontend/src/app/hooks/useCacheWidthHeight.ts new file mode 100644 index 000000000..f4a55bd93 --- /dev/null +++ b/frontend/src/app/hooks/useCacheWidthHeight.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 { useEffect, useState } from 'react'; +import useResizeObserver from './useResizeObserver'; + +export const useCacheWidthHeight = ( + initWidth: number = 400, + initHeight: number = 300, +) => { + const [cacheW, setCacheW] = useState(initWidth); + const [cacheH, setCacheH] = useState(initHeight); + const { + ref, + width = initWidth, + height = initHeight, + } = useResizeObserver({ + refreshMode: 'debounce', + refreshRate: 500, + }); + useEffect(() => { + if (width !== 0 && height !== 0) { + setCacheW(width); + setCacheH(height); + } + }, [width, height]); + return { + ref, + cacheW, + cacheH, + }; +}; diff --git a/frontend/src/app/hooks/useDebouncedSearch.ts b/frontend/src/app/hooks/useDebouncedSearch.ts index 6d887cedf..dd380a6e3 100644 --- a/frontend/src/app/hooks/useDebouncedSearch.ts +++ b/frontend/src/app/hooks/useDebouncedSearch.ts @@ -25,15 +25,15 @@ export function useDebouncedSearch( dataSource: T[] | undefined, filterFunc: (keywords: string, data: T) => boolean, wait: number = DEFAULT_DEBOUNCE_WAIT, + filterLeaf: boolean = false, ) { const [keywords, setKeywords] = useState(''); - const filteredData = useMemo( () => dataSource && keywords.trim() - ? filterListOrTree(dataSource, keywords, filterFunc) + ? filterListOrTree(dataSource, keywords, filterFunc, filterLeaf) : dataSource, - [dataSource, keywords, filterFunc], + [dataSource, keywords, filterFunc, filterLeaf], ); const debouncedSearch = useMemo(() => { diff --git a/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts b/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts index ae4dc42b0..192cbcbd7 100644 --- a/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts +++ b/frontend/src/app/hooks/useFetchFilterDataByCondtion.ts @@ -19,7 +19,7 @@ import { FilterCondition, FilterConditionType, - FilterValueOption, + RelationFilterValue, } from 'app/types/ChartConfig'; import { BackendChart } from 'app/pages/ChartWorkbenchPage/slice/workbenchSlice'; import { getDistinctFields } from 'app/utils/fetch'; @@ -28,7 +28,7 @@ import useMount from './useMount'; export const useFetchFilterDataByCondtion = ( viewId?: string, condition?: FilterCondition, - onFinish?: (datas: FilterValueOption[]) => void, + onFinish?: (datas: RelationFilterValue[]) => void, view?: BackendChart['view'], ) => { useMount(() => { diff --git a/frontend/src/app/hooks/useFieldActionModal.tsx b/frontend/src/app/hooks/useFieldActionModal.tsx index 3949f3c05..f85bc9316 100644 --- a/frontend/src/app/hooks/useFieldActionModal.tsx +++ b/frontend/src/app/hooks/useFieldActionModal.tsx @@ -39,6 +39,7 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { dataView?: ChartDataView, dataConfig?: ChartDataSectionConfig, onChange?, + aggregation?: boolean, ) => { if (!config) { return null; @@ -50,8 +51,9 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { dataView, dataConfig, onConfigChange: onChange, + aggregation, + i18nPrefix, }; - switch (actionType) { case ChartDataSectionFieldActionType.Sortable: return ; @@ -91,13 +93,14 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { dataset?: ChartDataset, dataView?: ChartDataView, modalSize?: string, + aggregation?: boolean, ) => { const currentConfig = dataConfig.rows?.find(c => c.uid === columnUid); - let _modalSize = StateModalSize.Middle; + let _modalSize = StateModalSize.MIDDLE; if (actionType === ChartDataSectionFieldActionType.Colorize) { - _modalSize = StateModalSize.Small; + _modalSize = StateModalSize.XSMALL; } else if (actionType === ChartDataSectionFieldActionType.ColorizeSingle) { - _modalSize = StateModalSize.Small; + _modalSize = StateModalSize.XSMALL; } return (show as Function)({ title: t(actionType), @@ -110,6 +113,7 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { dataView, dataConfig, onChange, + aggregation, ), onOk: handleOk(onConfigChange, columnUid), maskClosable: true, diff --git a/frontend/src/app/hooks/useI18NPrefix.ts b/frontend/src/app/hooks/useI18NPrefix.ts index b359fb339..7dfc73dbd 100644 --- a/frontend/src/app/hooks/useI18NPrefix.ts +++ b/frontend/src/app/hooks/useI18NPrefix.ts @@ -17,6 +17,8 @@ */ import ChartI18NContext from 'app/pages/ChartWorkbenchPage/contexts/Chart18NContext'; +import { DATART_TRANSLATE_HOLDER } from 'globalConstants'; +import i18n from 'i18next'; import get from 'lodash/get'; import { useCallback, useContext } from 'react'; import { useTranslation } from 'react-i18next'; @@ -25,6 +27,10 @@ export interface I18NComponentProps { i18nPrefix?: string; } +export function prefixI18N(key) { + return i18n.t(key); +} + function usePrefixI18N(prefix?: string) { const { t, i18n } = useTranslation(); const { i18NConfigs: vizI18NConfigs } = useContext(ChartI18NContext); @@ -41,10 +47,14 @@ function usePrefixI18N(prefix?: string) { if (contextTranslation) { return contextTranslation; } + if (key.includes(DATART_TRANSLATE_HOLDER)) { + const newKey = key.replace(`${DATART_TRANSLATE_HOLDER}.`, ''); + return t.call(Object.create(null), `${newKey}`, options) as string; + } if (disablePrefix) { - return t.call(null, `${key}`, options) as string; + return t.call(Object.create(null), `${key}`, options) as string; } - return t.call(null, `${prefix}.${key}`, options) as string; + return t.call(Object.create(null), `${prefix}.${key}`, options) as string; }, [i18n.language, prefix, t, vizI18NConfigs], ); diff --git a/frontend/src/app/hooks/useMount.ts b/frontend/src/app/hooks/useMount.ts index 12dabc7b7..2e1be0b4d 100644 --- a/frontend/src/app/hooks/useMount.ts +++ b/frontend/src/app/hooks/useMount.ts @@ -24,6 +24,7 @@ const useMount = (fn?: () => void, dispose?: () => void) => { return () => { dispose?.(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); }; diff --git a/frontend/src/app/hooks/useSearchAndExpand.ts b/frontend/src/app/hooks/useSearchAndExpand.ts index a86e3cc84..0c63abef7 100644 --- a/frontend/src/app/hooks/useSearchAndExpand.ts +++ b/frontend/src/app/hooks/useSearchAndExpand.ts @@ -26,6 +26,7 @@ export function useSearchAndExpand( dataSource: T[] | undefined, filterFunc: (keywords: string, data: T) => boolean, wait: number = DEFAULT_DEBOUNCE_WAIT, + filterLeaf: boolean = false, ) { const [expandedRowKeys, setExpandedRowKeys] = useState([]); @@ -33,6 +34,7 @@ export function useSearchAndExpand( dataSource, filterFunc, wait, + filterLeaf, ); const filteredExpandedRowKeys = useMemo( diff --git a/frontend/src/app/hooks/useStateModal.tsx b/frontend/src/app/hooks/useStateModal.tsx index b0a20833a..8b8e293bf 100644 --- a/frontend/src/app/hooks/useStateModal.tsx +++ b/frontend/src/app/hooks/useStateModal.tsx @@ -18,33 +18,26 @@ import { Form, Modal } from 'antd'; import { useRef } from 'react'; -import useI18NPrefix from './useI18NPrefix'; export interface IStateModalContentProps { onChange: (o: any) => void; } export enum StateModalSize { - Small = 600, - Middle = 1000, - Large = 1600, - XLarge = 2000, + XSMALL = 520, + SMALL = 600, + MIDDLE = 1000, + LARGE = 1600, + XLARGE = 2000, } const defaultBodyStyle: React.CSSProperties = { maxHeight: 1000, - overflowY: 'scroll', + overflowY: 'auto', overflowX: 'auto', }; -function useStateModal({ - i18nPrefix, - initState, -}: { - i18nPrefix?: string; - initState?: any; -}) { - const t = useI18NPrefix(i18nPrefix); +function useStateModal({ initState }: { initState?: any }) { const [form] = Form.useForm(); const [modal, contextHolder] = Modal.useModal(); const okCallbackRef = useRef(); @@ -64,19 +57,19 @@ function useStateModal({ Object.keys(stateRef.current || {}).length > 0 ? stateRef.current : []; - okCallbackRef.current?.call(null, ...spreadParmas); + okCallbackRef.current?.call(Object.create(null), ...spreadParmas); } catch (e) { console.error('useStateModal | exception message ---> ', e); } return closeFn; }) .catch(info => { - return Promise.reject(); + return Promise.reject(info); }); }; const handleClickCancelButton = () => { - cancelCallbackRef.current?.call(null, null); + cancelCallbackRef.current?.call(Object.create(null), null); }; const FormWrapper = content => { @@ -87,13 +80,26 @@ function useStateModal({ ); }; + const getModalSize = (size?: string | number | StateModalSize): number => { + if (!size) { + return StateModalSize.MIDDLE; + } + if (!isNaN(+size)) { + return +size; + } + if (typeof size === 'string' && StateModalSize[size.toUpperCase()]) { + return StateModalSize[size.toUpperCase()]; + } + return StateModalSize.MIDDLE; + }; + const showModal = (props: { title: string; content: ( cacheOnChangeValue: typeof handleSaveCacheValue, ) => React.ReactElement; bodyStyle?: React.CSSProperties; - modalSize?: StateModalSize; + modalSize?: string | number | StateModalSize; onOk?: typeof handleClickOKButton; onCancel?: typeof handleClickCancelButton; }) => { @@ -106,9 +112,11 @@ function useStateModal({ return modal.confirm({ title: props.title, - width: props.modalSize || StateModalSize.Small, + width: getModalSize(props?.modalSize), bodyStyle: props.bodyStyle || defaultBodyStyle, - content: FormWrapper(props?.content?.call(null, handleSaveCacheValue)), + content: FormWrapper( + props?.content?.call(Object.create(null), handleSaveCacheValue), + ), onOk: handleClickOKButton, onCancel: handleClickCancelButton, maskClosable: true, diff --git a/frontend/src/app/index.tsx b/frontend/src/app/index.tsx index 39744cf16..07a06e458 100644 --- a/frontend/src/app/index.tsx +++ b/frontend/src/app/index.tsx @@ -1,15 +1,26 @@ /** + * Datart * - * App + * Copyright 2021 * - * This component is the skeleton around the actual pages, and should only - * contain code that should be seen on all pages. (e.g. navigation bar) + * 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 { message } from 'antd'; +import { ConfigProvider, message } from 'antd'; import echartsDefaultTheme from 'app/assets/theme/echarts_default_theme.json'; import { registerTheme } from 'echarts'; import { StorageKeys } from 'globalConstants'; +import { antdLocales } from 'locales/i18n'; import { useEffect, useLayoutEffect } from 'react'; import { Helmet } from 'react-helmet-async'; import { useTranslation } from 'react-i18next'; @@ -17,6 +28,7 @@ import { useDispatch } from 'react-redux'; import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { GlobalStyle, OverriddenStyle } from 'styles/globalStyles'; import { getToken } from 'utils/auth'; +import useI18NPrefix from './hooks/useI18NPrefix'; import { LoginAuthRoute } from './LoginAuthRoute'; import { LazyActivePage } from './pages/ActivePage/Loadable'; import { LazyAuthorizationPage } from './pages/AuthorizationPage/Loadable'; @@ -32,6 +44,7 @@ export function App() { const dispatch = useDispatch(); const { i18n } = useTranslation(); const logged = !!getToken(); + const t = useI18NPrefix('global'); useAppSlice(); useLayoutEffect(() => { @@ -39,35 +52,40 @@ export function App() { dispatch(setLoggedInUser()); } else { if (localStorage.getItem(StorageKeys.LoggedInUser)) { - message.warning('会话过期,请重新登录'); + message.warning(t('tokenExpired')); } dispatch(logout()); } - }, [dispatch, logged]); + }, [dispatch, t, logged]); useEffect(() => { dispatch(getSystemInfo()); }, [dispatch]); return ( - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + ); } diff --git a/frontend/src/app/migration/alpha3.ts b/frontend/src/app/migration/alpha3.ts index e58203885..d8b809c68 100644 --- a/frontend/src/app/migration/alpha3.ts +++ b/frontend/src/app/migration/alpha3.ts @@ -17,7 +17,7 @@ */ import { ChartConfig } from 'app/types/ChartConfig'; -import { isUndefined } from 'lodash'; +import isUndefined from 'lodash/isUndefined'; export const hasWrongDimensionName = (config?: ChartConfig) => { if (!config) { diff --git a/frontend/src/app/pages/ActivePage/Loadable.tsx b/frontend/src/app/pages/ActivePage/Loadable.tsx index 205f3aa05..af9b98fc0 100644 --- a/frontend/src/app/pages/ActivePage/Loadable.tsx +++ b/frontend/src/app/pages/ActivePage/Loadable.tsx @@ -15,9 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** - * Asynchronously loads the component for NotFoundPage - */ import { defaultLazyLoad } from 'utils/loadable'; diff --git a/frontend/src/app/pages/ActivePage/index.tsx b/frontend/src/app/pages/ActivePage/index.tsx index 951d204ab..4e38411cc 100644 --- a/frontend/src/app/pages/ActivePage/index.tsx +++ b/frontend/src/app/pages/ActivePage/index.tsx @@ -1,4 +1,5 @@ import { EmptyFiller } from 'app/components/EmptyFiller'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { getUserInfoByToken } from 'app/slice/thunks'; import { useCallback, useEffect } from 'react'; import { useDispatch } from 'react-redux'; @@ -9,6 +10,8 @@ import { activeAccount } from './service'; export const ActivePage = () => { const history = useHistory(); const dispatch = useDispatch(); + const t = useI18NPrefix('active'); + const onActiveUser = useCallback( token => { activeAccount(token).then(loginToken => { @@ -37,7 +40,7 @@ export const ActivePage = () => { }, []); return ( - + ); }; diff --git a/frontend/src/app/pages/AuthorizationPage/Loadable.tsx b/frontend/src/app/pages/AuthorizationPage/Loadable.tsx index 252622fab..aeb3fe23a 100644 --- a/frontend/src/app/pages/AuthorizationPage/Loadable.tsx +++ b/frontend/src/app/pages/AuthorizationPage/Loadable.tsx @@ -15,9 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** - * Asynchronously loads the component for NotFoundPage - */ import { defaultLazyLoad } from 'utils/loadable'; diff --git a/frontend/src/app/pages/AuthorizationPage/index.tsx b/frontend/src/app/pages/AuthorizationPage/index.tsx index 47f3632fd..e0a6ee723 100644 --- a/frontend/src/app/pages/AuthorizationPage/index.tsx +++ b/frontend/src/app/pages/AuthorizationPage/index.tsx @@ -1,4 +1,5 @@ import { EmptyFiller } from 'app/components/EmptyFiller'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { getUserInfoByToken } from 'app/slice/thunks'; import { useCallback, useEffect } from 'react'; import { useDispatch } from 'react-redux'; @@ -10,9 +11,12 @@ export const AuthorizationPage = () => { const history = useHistory(); const paramsMatch = useRouteMatch<{ token: string }>(); const token = paramsMatch.params.token; + const t = useI18NPrefix('authorization'); + const toApp = useCallback(() => { history.replace('/'); }, [history]); + useEffect(() => { if (token) { dispatch(getUserInfoByToken({ token, resolve: toApp })); @@ -20,7 +24,7 @@ export const AuthorizationPage = () => { }, [token, dispatch, toApp]); return ( - + ); }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx index 8c8d669d6..683a77e4a 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/ChartHeaderPanel.tsx @@ -16,10 +16,10 @@ * limitations under the License. */ -import { LeftOutlined } from '@ant-design/icons'; -import { Button, Space } from 'antd'; +import { LeftOutlined, MoreOutlined } from '@ant-design/icons'; +import { Button, Dropdown, Space } from 'antd'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; -import { FC, memo } from 'react'; +import { FC, memo, useCallback, useContext } from 'react'; import styled from 'styled-components/macro'; import { FONT_SIZE_ICON_SM, @@ -30,13 +30,28 @@ import { SPACE_TIMES, SPACE_XS, } from 'styles/StyleConstants'; +import ChartAggregationContext from '../../contexts/ChartAggregationContext'; +import AggregationOperationMenu from './components/AggregationOperationMenu'; const ChartHeaderPanel: FC<{ chartName?: string; onSaveChart?: () => void; onGoBack?: () => void; -}> = memo(({ chartName, onSaveChart, onGoBack }) => { + onChangeAggregation?: (state: boolean) => void; +}> = memo(({ chartName, onSaveChart, onGoBack, onChangeAggregation }) => { const t = useI18NPrefix(`viz.workbench.header`); + const { aggregation } = useContext(ChartAggregationContext); + + const getOverlays = useCallback(() => { + return ( + { + onChangeAggregation?.(e); + }} + > + ); + }, [aggregation, onChangeAggregation]); return ( @@ -47,29 +62,12 @@ const ChartHeaderPanel: FC<{ )} {chartName} - {/* - {t('lang.zh')} - {t('lang.en')} - - - - {t('format.local')} - - - {t('format.date')} - - {t('format.ll')} - {t('format.lll')} - */} {t('save')} + + } /> + ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/components/AggregationOperationMenu.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/components/AggregationOperationMenu.tsx new file mode 100644 index 000000000..69a125cf0 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartHeaderPanel/components/AggregationOperationMenu.tsx @@ -0,0 +1,51 @@ +/** + * 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 { Menu, Modal, Switch } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { FC, memo, useMemo } from 'react'; + +const AggregationOperationMenu: FC<{ + defaultValue?: boolean; + onChangeAggregation: (value: boolean) => void; +}> = memo(({ defaultValue = true, onChangeAggregation }) => { + const checkedValue = useMemo(() => defaultValue, [defaultValue]); + const t = useI18NPrefix(`viz.workbench.header`); + + const onChange = value => { + Modal.confirm({ + icon: <>>, + content: t('aggregationSwitchTip'), + okText: checkedValue ? t('close') : t('open'), + // cancelText: t('close'), + onOk() { + onChangeAggregation(value); + }, + }); + }; + return ( + + + {t('aggregationSwitch')}{' '} + + + + ); +}); + +export default AggregationOperationMenu; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx index 6e80f6ecc..1ccf37347 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/ChartOperationPanel.tsx @@ -73,6 +73,12 @@ const ChartOperationPanel: FC<{ if (component === LayoutComponentType.PRESENT) { return ( = memo( ({ dataConfigs, onChange }) => { const translate = useI18NPrefix(`viz.palette.data`); + const { aggregation } = useContext(ChartAggregationContext); const getSectionComponent = (config, index) => { const props = { - key: index, + key: config?.key || index, ancestors: [index], config, translate, + aggregation, onConfigChanged: (ancestors, config, needRefresh?: boolean) => { onChange?.(ancestors, config, needRefresh); }, }; + switch (props.config?.type) { case ChartDataSectionType.GROUP: return ; @@ -67,7 +71,11 @@ const ChartDataConfigPanel: FC<{ } }; - return {(dataConfigs || []).map(getSectionComponent)}; + return ( + + {(dataConfigs || []).map(getSectionComponent)} + + ); }, (prev, next) => { return prev.dataConfigs === next.dataConfigs; @@ -76,7 +84,7 @@ const ChartDataConfigPanel: FC<{ export default ChartDataConfigPanel; -const Wrapper = styled.div` +const StyledChartDataConfigPanel = styled.div` display: flex; flex: 1; flex-direction: column; 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 929719da4..bc34e52cb 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 @@ -37,25 +37,26 @@ const ChartStyleConfigPanel: FC<{ }> = memo( ({ configs, dataConfigs, onChange }) => { const t = useI18NPrefix(`viz.palette.style`); - return ( - {configs?.map((c, index) => ( - - - - ))} + {configs + ?.filter(c => !Boolean(c.hidden)) + .map((c, index) => ( + + + + ))} ); }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/AggregateTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/AggregateTypeSection.tsx index 232e8b911..63c31782e 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/AggregateTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/AggregateTypeSection.tsx @@ -17,15 +17,15 @@ */ import { ChartDataSectionFieldActionType } from 'app/types/ChartConfig'; -import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; -import { dataConfigSectionComparer } from './utils'; +import { dataConfigSectionComparer, handleDefaultConfig } from './utils'; const AggregateTypeSection: FC = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( { allowSameField: true, }, @@ -49,6 +49,10 @@ const AggregateTypeSection: FC = memo( }, config, ); + + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } return ; }, dataConfigSectionComparer, 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 14d994f8d..bc098615d 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 @@ -26,18 +26,18 @@ import { dataConfigSectionComparer } from './utils'; const BaseDataConfigSection: FC = memo( ({ modalSize, config, extra, translate = title => title, ...rest }) => { return ( - - + + {translate(config.label)} {extra?.()} - + - + ); }, dataConfigSectionComparer, @@ -45,10 +45,10 @@ const BaseDataConfigSection: FC = memo( export default BaseDataConfigSection; -const Container = styled.div` +const StyledBaseDataConfigSection = styled.div` padding: ${SPACE} 0; `; -const Title = styled.div` +const StyledBaseDataConfigSectionTitle = styled.div` color: ${p => p.theme.textColor}; `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/ColorTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/ColorTypeSection.tsx index f31b8e56b..4111ac83a 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/ColorTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/ColorTypeSection.tsx @@ -21,11 +21,11 @@ import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; -import { dataConfigSectionComparer } from './utils'; +import { dataConfigSectionComparer, handleDefaultConfig } from './utils'; const ColorTypeSection: FC = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( {}, { actions: { @@ -37,7 +37,9 @@ const ColorTypeSection: FC = memo( }, config, ); - + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } return ; }, dataConfigSectionComparer, 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 053e0e5ac..384162e79 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 @@ -24,21 +24,28 @@ import { ChartDataSectionConfig, ChartDataSectionFieldActionType, } from 'app/types/ChartConfig'; -import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo, useState } from 'react'; import { CloneValueDeep } from 'utils/object'; import BaseDataConfigSection from './BaseDataConfigSection'; import { dataConfigSectionComparer } from './utils'; const FilterTypeSection: FC = memo( - ({ ancestors, config, translate = title => title, onConfigChanged }) => { + ({ + ancestors, + config, + translate = title => title, + onConfigChanged, + aggregation, + }) => { const [currentConfig, setCurrentConfig] = useState(config); const [originalConfig, setOriginalConfig] = useState(config); const [enableExtraAction] = useState(false); const extendedConfig = Object.assign( { allowSameField: true, + disableAggregate: false, }, { actions: { @@ -96,7 +103,7 @@ const FilterTypeSection: FC = memo( return ( = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( {}, { actions: { @@ -45,6 +45,9 @@ const GroupTypeSection: FC = memo( }, config, ); + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } return ; }, dataConfigSectionComparer, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/InfoTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/InfoTypeSection.tsx index b810e6941..fa4e9ab7c 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/InfoTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/InfoTypeSection.tsx @@ -17,15 +17,15 @@ */ import { ChartDataSectionFieldActionType } from 'app/types/ChartConfig'; -import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; -import { dataConfigSectionComparer } from './utils'; +import { dataConfigSectionComparer, handleDefaultConfig } from './utils'; const InfoTypeSection: FC = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( { allowSameField: true, }, @@ -33,13 +33,18 @@ const InfoTypeSection: FC = memo( actions: { [ChartDataViewFieldType.NUMERIC]: [ ChartDataSectionFieldActionType.Aggregate, - ChartDataSectionFieldActionType.Alias, ChartDataSectionFieldActionType.Format, + ChartDataSectionFieldActionType.Alias, ], }, }, config, ); + + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } + return ; }, dataConfigSectionComparer, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/MixedTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/MixedTypeSection.tsx index 992b24715..4663053e9 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/MixedTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/MixedTypeSection.tsx @@ -17,32 +17,41 @@ */ import { ChartDataSectionFieldActionType } from 'app/types/ChartConfig'; -import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; -import { dataConfigSectionComparer } from './utils'; +import { dataConfigSectionComparer, handleDefaultConfig } from './utils'; const MixedTypeSection: FC = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( {}, { actions: { [ChartDataViewFieldType.NUMERIC]: [ + ChartDataSectionFieldActionType.Aggregate, ChartDataSectionFieldActionType.Alias, ChartDataSectionFieldActionType.Format, + ChartDataSectionFieldActionType.Sortable, ], [ChartDataViewFieldType.STRING]: [ ChartDataSectionFieldActionType.Alias, + ChartDataSectionFieldActionType.Sortable, ], [ChartDataViewFieldType.DATE]: [ ChartDataSectionFieldActionType.Alias, + ChartDataSectionFieldActionType.Sortable, ], }, }, config, ); + + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } + return ; }, dataConfigSectionComparer, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SizeTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SizeTypeSection.tsx index ff871bf01..80945cd9e 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SizeTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SizeTypeSection.tsx @@ -21,11 +21,11 @@ import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; -import { dataConfigSectionComparer } from './utils'; +import { dataConfigSectionComparer, handleDefaultConfig } from './utils'; const SizeTypeSection: FC = memo( - ({ config, ...rest }) => { - const defaultConfig = Object.assign( + ({ config, aggregation, ...rest }) => { + let defaultConfig = Object.assign( {}, { actions: { @@ -38,6 +38,11 @@ const SizeTypeSection: FC = memo( }, config, ); + + if (aggregation === false) { + defaultConfig = handleDefaultConfig(defaultConfig, config.type); + } + return ; }, dataConfigSectionComparer, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SortTypeSection.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SortTypeSection.tsx index 82142e8e6..d9f9d9482 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SortTypeSection.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDataConfigSection/SortTypeSection.tsx @@ -17,8 +17,8 @@ */ import { ChartDataSectionFieldActionType } from 'app/types/ChartConfig'; -import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { FC, memo } from 'react'; import BaseDataConfigSection from './BaseDataConfigSection'; import { dataConfigSectionComparer } from './utils'; 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 427f0a938..c9e660438 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 @@ -16,17 +16,46 @@ * limitations under the License. */ +import { ChartDataSectionType } from 'app/types/ChartConfig'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; - +import produce from 'immer'; export function dataConfigSectionComparer( prevProps: ChartDataConfigSectionProps, nextProps: ChartDataConfigSectionProps, ) { if ( prevProps.translate !== nextProps.translate || - prevProps.config !== nextProps.config + prevProps.config !== nextProps.config || + prevProps.aggregation !== nextProps.aggregation ) { return false; } return true; } + +export function handleDefaultConfig(defaultConfig, configType): any { + const nextConfig = produce(defaultConfig, draft => { + let _actions = {}; + + draft.rows?.forEach((row, i) => { + draft.rows[i].aggregate = undefined; + }); + + if (configType === ChartDataSectionType.AGGREGATE) { + delete draft.actions.STRING; + } + + if (configType === ChartDataSectionType.GROUP) { + delete draft.actions.NUMERIC; + } + + for (let key in draft.actions) { + _actions[key] = draft.actions[key].filter( + v => v !== 'aggregate' && v !== 'aggregateLimit', + ); + } + + draft.actions = _actions; + }); + return nextConfig; +} 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 c74823cfe..94b2f5d92 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 @@ -144,7 +144,7 @@ const ChartDataViewPanel: FC<{ const handleAddOrEditComputedField = field => { (showModal as Function)({ title: t('createComputedFields'), - modalSize: StateModalSize.Middle, + modalSize: StateModalSize.MIDDLE, content: onChange => ( { const fieldConfig = config.rows?.find(c => c.uid === uid)!; + const options = config?.options?.[actionName]; if (actionName === ChartDataSectionFieldActionType.Sortable) { return ( { handleFieldConfigChanged(uid, config, needRefresh); }} + options={options} mode="menu" /> ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragLayer.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragLayer.tsx new file mode 100644 index 000000000..3a35ef37b --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragLayer.tsx @@ -0,0 +1,81 @@ +/** + * 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 React from 'react'; +import { DragLayer } from 'react-dnd'; +import styled from 'styled-components'; +import ChartDragPreview from './ChartDragPreview'; + +const collect = monitor => { + return { + item: monitor.getItem(), + itemType: monitor.getItemType(), + currentOffset: monitor.getSourceClientOffset(), + isDragging: monitor.isDragging(), + }; +}; + +const getItemStyles = currentOffset => { + if (!currentOffset) { + return { + display: 'none', + }; + } + const { x, y } = currentOffset; + return { + transform: `translate(${x}px, ${y}px)`, + }; +}; + +function CardDragLayer(props) { + const { item, itemType, currentOffset, isDragging } = props; + + /** + * zh: 如果不是正在拖动或者拖动的数据项不是一个数组则不执行 + * en: If it is not being dragged or the data item being dragged is not an array, do not execute + */ + if (!isDragging || !Array.isArray(item)) { + return null; + } + + const renderItem = (type, item) => { + switch (type) { + case 'dataset_column': + return ( + + + + ); + default: + return null; + } + }; + + return {renderItem(itemType, item)}; +} +export default DragLayer(collect)(CardDragLayer); + +const LayerStyles = styled.div` + position: fixed; + pointer-events: none; + z-index: 100; + left: 0; + top: 0; + right: 0; + bottom: 0; +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragPreview.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragPreview.tsx new file mode 100644 index 000000000..fc7fd2e86 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDragPreview.tsx @@ -0,0 +1,54 @@ +/** + * 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 React from 'react'; +import styled from 'styled-components/macro'; +import ChartDraggableSourceContainer from './ChartDraggableSourceContainer'; + +const DragPreview = ({ dataItem }) => { + return ( + + {dataItem?.slice(0, 3).map((v, i) => ( + + + + ))} + + ); +}; + +export default DragPreview; + +const Preview = styled.div` + border: 1px solid #fff; + background: #f2f2f2; + width: 256px; + position: absolute; + transform-origin: bottom left; + -webkit-backface-visibility: hidden; +`; 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 f6f9efb53..dc224b288 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 @@ -54,7 +54,11 @@ interface ChartDraggableElementProps { config: ChartDataSectionField; connectDragSource: ConnectDragSource; connectDropTarget: ConnectDropTarget; - moveCard: (dragIndex: number, hoverIndex: number) => void; + moveCard: ( + dragIndex: number, + hoverIndex: number, + config?: ChartDataSectionField, + ) => void; onDelete: () => void; } @@ -65,7 +69,7 @@ interface ChartDraggableElementInstance { const ChartDraggableElement = forwardRef< HTMLDivElement, ChartDraggableElementProps ->(function Card( +>(function ChartDraggableElement( { content, isDragging, @@ -104,7 +108,7 @@ const ChartDraggableElement = forwardRef< }); export default DropTarget( - CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN, + [CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN], { hover( props: ChartDraggableElementProps, @@ -120,7 +124,9 @@ export default DropTarget( return null; } - const dragIndex = monitor.getItem().index; + const dragItem = monitor.getItem(); + + const dragIndex = dragItem.index; const hoverIndex = props.index; // Don't replace items with themselves @@ -205,7 +211,7 @@ const StyledChartDraggableElement = styled.div<{ background: ${p => p.type === ChartDataViewFieldType.NUMERIC ? p.theme.success : p.theme.info}; border-radius: ${BORDER_RADIUS}; - opacity: ${p => (p.isDragging ? 0 : 1)}; + opacity: ${p => (p.isDragging ? 0.2 : 1)}; `; const Content = styled.div` 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 fc16fcf0b..b4cb0176c 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 @@ -50,8 +50,10 @@ import { export const ChartDraggableSourceContainer: FC< { - onDeleteComputedField: (fieldName) => void; - onEditComputedField: (fieldName) => void; + onDeleteComputedField?: (fieldName) => void; + onEditComputedField?: (fieldName) => void; + onSelectionChange?: (dataItemId, cmdKeyActive, shiftKeyActive) => void; + onClearCheckedList?: () => void; } & ChartDataViewMeta > = memo(function ChartDraggableSourceContainer({ id, @@ -61,23 +63,45 @@ export const ChartDraggableSourceContainer: FC< expression, onDeleteComputedField, onEditComputedField, + onSelectionChange, + selectedItems, + isActive, + onClearCheckedList, }) { const t = useI18NPrefix(`viz.workbench.dataview`); const [, drag] = useDrag( () => ({ type: CHART_DRAG_ELEMENT_TYPE.DATASET_COLUMN, canDrag: true, - item: { colName, type, category }, + item: selectedItems?.length + ? selectedItems.map(item => ({ + colName: item.id, + type: item.type, + category: item.category, + })) + : { colName, type, category }, + collect: monitor => ({ + isDragging: monitor.isDragging(), + }), + end: onClearCheckedList, }), - [], + [selectedItems], ); + const styleClasses: Array = useMemo(() => { + let styleArr: Array = []; + if (isActive) { + styleArr.push('container-active'); + } + return styleArr; + }, [isActive]); + const renderContent = useMemo(() => { const _handleMenuClick = (e, fieldName) => { if (e.key === 'delete') { - onDeleteComputedField(fieldName); + onDeleteComputedField?.(fieldName); } else { - onEditComputedField(fieldName); + onEditComputedField?.(fieldName); } }; @@ -151,7 +175,17 @@ export const ChartDraggableSourceContainer: FC< ); }, [type, colName, onDeleteComputedField, onEditComputedField, category, t]); - return {renderContent}; + return ( + { + onSelectionChange?.(colName, e.metaKey || e.ctrlKey, e.shiftKey); + }} + ref={drag} + className={styleClasses.join(' ')} + > + {renderContent} + + ); }); export default ChartDraggableSourceContainer; @@ -165,7 +199,9 @@ const Container = styled.div` font-weight: ${FONT_WEIGHT_MEDIUM}; color: ${p => p.theme.textColorSnd}; cursor: pointer; - + &.container-active { + background-color: #f8f9fa; + } > p { flex: 1; } 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 4ffc389aa..481d0b9b5 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 @@ -16,13 +16,13 @@ * limitations under the License. */ -import { Checkbox, List } from 'antd'; +import { List } from 'antd'; import { ChartDataViewMeta } from 'app/types/ChartDataView'; -import { CHART_DRAG_ELEMENT_TYPE } from 'globalConstants'; import { FC, memo, useState } from 'react'; -import { DragSourceMonitor, useDrag } from 'react-dnd'; import styled from 'styled-components/macro'; +import { stopPPG } from 'utils/utils'; import { ChartDraggableSourceContainer } from './ChartDraggableSourceContainer'; +import ChartDragLayer from './ChartDragLayer'; export const ChartDraggableSourceGroupContainer: FC<{ meta?: ChartDataViewMeta[]; @@ -33,79 +33,73 @@ export const ChartDraggableSourceGroupContainer: FC<{ onDeleteComputedField, onEditComputedField, }) { - const [indeterminate, setIndeterminate] = useState(false); - const [checkedList, setCheckedList] = useState([]); - const [isCheckAll, setIsCheckAll] = useState(false); + const [selectedItems, setSelectedItems] = useState([]); + const [selectedItemsIds, setselectedItemsIds] = useState>([]); + const [activeItemId, setActiveItemId] = useState(''); - const [, drag] = useDrag( - () => ({ - type: CHART_DRAG_ELEMENT_TYPE.DATASET_GROUP_COLUMNS, - canDrag: true, - item: checkedList.map(item => ({ - colName: item.id, - type: item.type, - category: item.category, - })), - collect: (monitor: DragSourceMonitor) => ({ - isDragging: monitor.isDragging(), - }), - }), - [checkedList], - ); + const onDataItemSelectionChange = ( + dataItemId: string, + cmdKeyActive: boolean, + shiftKeyActive: boolean, + ) => { + let interimSelectedItemsIds: Array = []; + let interimActiveItemId = ''; + const dataViewMeta = meta?.slice() || []; + const previousSelectedItemsIds: Array = selectedItemsIds.slice(); + const previousActiveItemId = activeItemId; - const handleListItemChecked = item => checked => { - if ( - !!checked && - !checkedList.find(checkedItem => checkedItem.id === item.id) - ) { - const updatedList = checkedList.concat([item]); - setCheckedList(updatedList); - setIsCheckAll(meta?.length === updatedList.length); - setIndeterminate( - !!updatedList.length && (meta || []).length > updatedList.length, - ); - } else if (!checked) { - const updatedList = checkedList.filter( - checkedItem => checkedItem.id !== item.id, - ); - setCheckedList(updatedList); - setIsCheckAll(meta?.length === updatedList.length); - setIndeterminate( - !!updatedList.length && (meta || []).length > updatedList.length, + if (cmdKeyActive) { + if ( + previousSelectedItemsIds.indexOf(dataItemId) > -1 && + dataItemId !== previousActiveItemId + ) { + interimSelectedItemsIds = previousSelectedItemsIds.filter( + id => id !== dataItemId, + ); + } else { + interimSelectedItemsIds = [...previousSelectedItemsIds, dataItemId]; + } + } else if (shiftKeyActive && dataItemId !== previousActiveItemId) { + const activeCardIndex: any = dataViewMeta.findIndex( + c => c.id === previousActiveItemId, ); + const cardIndex = dataViewMeta.findIndex(c => c.id === dataItemId); + const lowerIndex = Math.min(activeCardIndex, cardIndex); + const upperIndex = Math.max(activeCardIndex, cardIndex); + interimSelectedItemsIds = dataViewMeta + .slice(lowerIndex, upperIndex + 1) + .map(c => c.id); + } else { + interimSelectedItemsIds = [dataItemId]; + interimActiveItemId = dataItemId; } + + const selectedCards = dataViewMeta.filter(c => + interimSelectedItemsIds.includes(c.id), + ); + + setselectedItemsIds(interimSelectedItemsIds); + setActiveItemId(interimActiveItemId); + setSelectedItems(selectedCards); }; - const handleCheckAll = checked => { - setCheckedList(checked ? meta || [] : []); - setIndeterminate(false); - setIsCheckAll(checked); + const onClearCheckedList = () => { + if (selectedItems?.length > 0) { + setselectedItemsIds([]); + setActiveItemId(''); + setSelectedItems([]); + } }; return ( - + + {/* 拖动层组件 */} + item.id} - // header={ - // handleCheckAll(e.target?.checked)} - // > - // Allow MultiDraggable (Drag Me) - // - // } renderItem={item => ( - - {(isCheckAll || indeterminate) && ( - checkedItem.id === item.id) - } - onChange={e => handleListItemChecked(item)(e.target?.checked)} - /> - )} + )} 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 55d448af1..c3762f498 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 @@ -54,9 +54,14 @@ import { SPACE_SM, } from 'styles/StyleConstants'; import { ValueOf } from 'types'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; +import ChartAggregationContext from '../../../../contexts/ChartAggregationContext'; import ChartDataConfigSectionActionMenu from './ChartDataConfigSectionActionMenu'; -import VizDraggableItem from './ChartDraggableElement'; +import ChartDraggableElement from './ChartDraggableElement'; + +type DragItem = { + index?: number; +}; export const ChartDraggableTargetContainer: FC = memo(function ChartDraggableTargetContainer({ @@ -72,68 +77,88 @@ export const ChartDraggableTargetContainer: FC = const [showModal, contextHolder] = useFieldActionModal({ i18nPrefix: 'viz.palette.data.enum.actionType', }); + const { aggregation } = useContext(ChartAggregationContext); const [{ isOver, canDrop }, drop] = useDrop( () => ({ accept: [ - CHART_DRAG_ELEMENT_TYPE.DATASET_GROUP_COLUMNS, CHART_DRAG_ELEMENT_TYPE.DATASET_COLUMN, CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN, ], - drop(item: ChartDataSectionField, monitor) { - let items = [item]; + drop(item: ChartDataSectionField & DragItem, monitor) { + let items = Array.isArray(item) ? item : [item]; + let needDelete = true; if ( - monitor.getItemType() === - CHART_DRAG_ELEMENT_TYPE.DATASET_GROUP_COLUMNS + monitor.getItemType() === CHART_DRAG_ELEMENT_TYPE.DATASET_COLUMN ) { - items = item as any; - } - if ( - monitor.getItemType() === CHART_DRAG_ELEMENT_TYPE.DATASET_COLUMN || - monitor.getItemType() === CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN - ) { - let currentColumns: ChartDataSectionField[] = ( + const currentColumns: ChartDataSectionField[] = ( currentConfig.rows || [] ).concat( - items.map(i => ({ + items.map(val => ({ uid: uuidv4(), - colName: i.colName, - category: i.category, - type: i.type, - aggregate: getDefaultAggregate(item), + colName: val.colName, + category: val.category, + type: val.type, + aggregate: getDefaultAggregate(val), })), ); - const newCurrentConfig = updateByKey( - currentConfig, - 'rows', - currentColumns, + updateCurrentConfigColumns(currentConfig, currentColumns, true); + } else if ( + monitor.getItemType() === CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN + ) { + const originItemIndex = (currentConfig.rows || []).findIndex( + r => r.uid === item.uid, ); - setCurrentConfig(newCurrentConfig); - onConfigChanged?.(ancestors, newCurrentConfig, true); + if (originItemIndex > -1) { + needDelete = false; + const currentColumns = updateBy( + currentConfig?.rows || [], + draft => { + draft.splice(originItemIndex, 1); + return draft.splice(item?.index!, 0, item); + }, + ); + updateCurrentConfigColumns(currentConfig, currentColumns); + } else { + const currentColumns = updateBy( + currentConfig?.rows || [], + draft => { + return draft.splice(item?.index!, 0, item); + }, + ); + updateCurrentConfigColumns(currentConfig, currentColumns); + } } - return { delete: true }; + return { delete: needDelete }; }, canDrop: (item: ChartDataSectionField, monitor) => { + let items = Array.isArray(item) ? item : [item]; + if ( typeof currentConfig.actions === 'object' && - !(item.type in currentConfig.actions) + !items.every(val => val.type in (currentConfig.actions || {})) ) { + //zh: 判断现在拖动的数据项是否可以拖动到当前容器中 en: Determine whether the currently dragged data item can be dragged into the current container return false; } + // if ( + // typeof currentConfig.actions === 'object' && + // !(item.type in currentConfig.actions) + // ) { + // return false; + // } + if (currentConfig.allowSameField) { return true; } - let items = [item]; if ( - monitor.getItemType() === - CHART_DRAG_ELEMENT_TYPE.DATASET_GROUP_COLUMNS + monitor.getItemType() === CHART_DRAG_ELEMENT_TYPE.DATA_CONFIG_COLUMN ) { - items = item as any; + return true; } - const exists = currentConfig.rows?.map(col => col.colName); return items.every(i => !exists?.includes(i.colName)); }, @@ -149,47 +174,77 @@ export const ChartDraggableTargetContainer: FC = setCurrentConfig(config); }, [config]); + const updateCurrentConfigColumns = ( + currentConfig, + newColumns, + refreshDataset = false, + ) => { + const newCurrentConfig = updateByKey(currentConfig, 'rows', newColumns); + setCurrentConfig(newCurrentConfig); + onConfigChanged?.(ancestors, newCurrentConfig, refreshDataset); + }; + const getDefaultAggregate = item => { if ( currentConfig?.type === ChartDataSectionType.AGGREGATE || currentConfig?.type === ChartDataSectionType.SIZE || - currentConfig?.type === ChartDataSectionType.INFO + currentConfig?.type === ChartDataSectionType.INFO || + currentConfig?.type === ChartDataSectionType.MIXED ) { + if (currentConfig.disableAggregate) { + return; + } if ( - item.category !== + item.category === (ChartDataViewFieldCategory.AggregateComputedField as string) ) { - let aggType: string = ''; - if (currentConfig?.actions instanceof Array) { - currentConfig?.actions?.find( - type => - type === ChartDataSectionFieldActionType.Aggregate || - type === ChartDataSectionFieldActionType.AggregateLimit, - ); - } else if (currentConfig?.actions instanceof Object) { - aggType = currentConfig?.actions?.[item?.type]?.find( - type => - type === ChartDataSectionFieldActionType.Aggregate || - type === ChartDataSectionFieldActionType.AggregateLimit, - ); - } - if (aggType) { - return AggregateFieldSubAggregateType?.[aggType]?.[0]; - } + return; + } + + let aggType: string = ''; + if (currentConfig?.actions instanceof Array) { + currentConfig?.actions?.find( + type => + type === ChartDataSectionFieldActionType.Aggregate || + type === ChartDataSectionFieldActionType.AggregateLimit, + ); + } else if (currentConfig?.actions instanceof Object) { + aggType = currentConfig?.actions?.[item?.type]?.find( + type => + type === ChartDataSectionFieldActionType.Aggregate || + type === ChartDataSectionFieldActionType.AggregateLimit, + ); + } + if (aggType) { + return AggregateFieldSubAggregateType?.[aggType]?.[0]; } } }; const onDraggableItemMove = (dragIndex: number, hoverIndex: number) => { const draggedItem = currentConfig.rows?.[dragIndex]; - - if (draggedItem && !currentConfig?.rows?.length) { + if (draggedItem) { const newCurrentConfig = updateBy(currentConfig, draft => { const columns = draft.rows || []; columns.splice(dragIndex, 1); columns.splice(hoverIndex, 0, draggedItem); }); setCurrentConfig(newCurrentConfig); + } else { + // const placeholder = { + // uid: CHARTCONFIG_FIELD_PLACEHOLDER_UID, + // colName: 'Placeholder', + // category: 'field', + // type: 'STRING', + // } as any; + // const newCurrentConfig = updateBy(currentConfig, draft => { + // const columns = draft.rows || []; + // if (dragIndex) { + // columns.splice(dragIndex, 1); + // } + // columns.splice(hoverIndex, 0, placeholder); + // }); + // setCurrentConfig(newCurrentConfig); } }; @@ -221,7 +276,7 @@ export const ChartDraggableTargetContainer: FC = return currentConfig.rows?.map((columnConfig, index) => { return ( - = {currentConfig?.actions && ( )} - {getColumnRenderName(columnConfig)} + + {aggregation + ? getColumnRenderName(columnConfig) + : columnConfig.colName} + {enableActionsIcons(columnConfig)} @@ -254,7 +313,7 @@ export const ChartDraggableTargetContainer: FC = }} moveCard={onDraggableItemMove} onDelete={handleOnDeleteItem(columnConfig.uid)} - > + > ); }); }; @@ -287,6 +346,7 @@ export const ChartDraggableTargetContainer: FC = dataset, dataView, modalSize, + aggregation, ); }; 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 23e4683d4..525c2b103 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 @@ -16,9 +16,14 @@ * limitations under the License. */ -import { Col, Row } from 'antd'; +import { Col, Popover, Row } from 'antd'; import Theme from 'app/assets/theme/echarts_default_theme.json'; -import { ColorTag, ReactColorPicker } from 'app/components/ReactColorPicker'; +import { + ColorTag, + SingleColorSelection, + ThemeColorSelection, +} from 'app/components/ColorPicker'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; import { updateBy } from 'app/utils/mutation'; @@ -33,39 +38,16 @@ const AggregationColorizeAction: FC<{ config: ChartDataSectionField, needRefresh?: boolean, ) => void; -}> = memo(({ config, dataset, onConfigChange }) => { + i18nPrefix?: string; +}> = memo(({ config, dataset, onConfigChange, i18nPrefix }) => { const actionNeedNewRequest = true; - const [themeColors] = useState(Theme.color); - const [colors, setColors] = useState(() => { - const colorizedColumnName = config.colName; - const colorizeIndex = - dataset?.columns?.findIndex(r => r.name === colorizedColumnName) || 0; - const colorizedGroupValues = Array.from( - new Set(dataset?.rows?.map(r => r[colorizeIndex])), - ); - const originalColors = config.color?.colors || []; - const themeColorTotalCount = themeColors.length; - return colorizedGroupValues.filter(Boolean).map((k, index) => { - return { - key: k!, - value: - originalColors.find(pc => pc.key === k)?.value || - themeColors[index % themeColorTotalCount], - }; - }); - }); + const [themeColors, setThemeColors] = useState(Theme.color); + const [colors, setColors] = useState<{ key: string; value: string }[]>( + setColorFn(config, dataset, themeColors), + ); const [selectColor, setSelectColor] = useState(colors[0]); - - // useMount(() => { - // if (!config?.color) { - // onConfigChange( - // updateBy(config, draft => { - // draft.color = { colors: colors as any }; - // }), - // actionNeedNewRequest, - // ); - // } - // }); + const [selColorBoxStatus, setSelColorBoxStatus] = useState(false); + const t = useI18NPrefix(i18nPrefix); const handleColorChange = value => { if (selectColor) { @@ -79,7 +61,6 @@ const AggregationColorizeAction: FC<{ setColors(newColors); setSelectColor(currentSelectColor); - onConfigChange( updateBy(config, draft => { draft.color = { colors: newColors }; @@ -87,13 +68,21 @@ const AggregationColorizeAction: FC<{ actionNeedNewRequest, ); } + + setSelColorBoxStatus(false); }; const renderGroupColors = () => { return ( <> {colors.map(c => ( - setSelectColor(c)}> + { + setSelColorBoxStatus(true); + setSelectColor(c); + }} + > {' '} {c.key} @@ -104,24 +93,76 @@ const AggregationColorizeAction: FC<{ ); }; + const selectThemeColorFn = colorArr => { + let selectColor1 = setColorFn(config, dataset, colorArr, true); + + setThemeColors(colorArr); + setColors(selectColor1); + onConfigChange( + updateBy(config, draft => { + draft.color = { colors: selectColor1 }; + }), + actionNeedNewRequest, + ); + }; return ( - - - {renderGroupColors()} - - - - - + <> + + {t('chooseTheme')} + + + + {renderGroupColors()} + + + } + > + + > ); }); export default AggregationColorizeAction; +function setColorFn( + config, + dataset, + themeColors, + need = false, +): { key: string; value: string }[] { + const colorizedColumnName = config.colName; + const colorizeIndex = + dataset?.columns?.findIndex(r => r.name === colorizedColumnName) || 0; + const colorizedGroupValues = Array.from( + new Set(dataset?.rows?.map(r => String(r[colorizeIndex]))), + ); + const originalColors = config.color?.colors || []; + const themeColorTotalCount = themeColors.length; + + return colorizedGroupValues + .filter(Boolean) + .map((k, index): { key: string; value: string } => { + return { + key: k! as string, + value: need + ? themeColors[index % themeColorTotalCount] + : originalColors.find(pc => pc.key === k)?.value || + themeColors[index % themeColorTotalCount], + }; + }); +} + const StyledUL = styled.ul` padding-inline-start: 0; max-height: 300px; @@ -132,7 +173,7 @@ const StyledUL = styled.ul` flex-wrap: nowrap; align-items: center; justify-content: flex-start; - padding: 0 ${SPACE_MD}; + padding-right: ${SPACE_MD}; cursor: pointer; .text-span { margin-left: ${SPACE_XS}; @@ -140,5 +181,8 @@ const StyledUL = styled.ul` text-overflow: ellipsis; white-space: nowrap; } + &:hover { + background-color: rgba(27, 154, 238, 0.05); + } } `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AliasAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AliasAction.tsx index 50e385d57..8a087f6d7 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AliasAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/AliasAction.tsx @@ -16,16 +16,24 @@ * limitations under the License. */ -import { Input } from 'antd'; +import { Input, Space } from 'antd'; +import { FormItemEx } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField } from 'app/types/ChartConfig'; +import { getColumnRenderOriginName } from 'app/utils/chartHelper'; import { updateBy } from 'app/utils/mutation'; import { FC, useState } from 'react'; +import styled from 'styled-components/macro'; const AliasAction: FC<{ config: ChartDataSectionField; onConfigChange: (config: ChartDataSectionField) => void; }> = ({ config, onConfigChange }) => { + const formItemLayout = { + labelAlign: 'right' as any, + labelCol: { span: 8 }, + wrapperCol: { span: 8 }, + }; const t = useI18NPrefix(`viz.palette.data.actions`); const [aliasName, setAliasName] = useState(config?.alias?.name); const [nameDesc, setNameDesc] = useState(config?.alias?.desc); @@ -40,23 +48,32 @@ const AliasAction: FC<{ }; return ( - - {t('alias.name')} - { - onChange(value, nameDesc); - }} - /> - {t('alias.description')} - { - onChange(aliasName, value); - }} - /> - + + + {getColumnRenderOriginName(config)} + + + { + onChange(value, nameDesc); + }} + /> + + + { + onChange(aliasName, value); + }} + /> + + ); }; export default AliasAction; + +const StyledAliasAction = styled(Space)` + width: 100%; +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeRangeAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeRangeAction.tsx index 86daff594..404f85e9e 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeRangeAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeRangeAction.tsx @@ -17,8 +17,9 @@ */ import { Checkbox, Col, Row } from 'antd'; +import { ColorPickerPopover } from 'app/components/ColorPicker'; +import { ColorPicker } from 'app/components/ColorPicker/ColorTag'; import { FormItemEx } from 'app/components/From'; -import { ReactColorPicker } from 'app/components/ReactColorPicker'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; @@ -54,7 +55,7 @@ const ColorizeRangeAction: FC<{ onConfigChange?.(newConfig, actionNeedNewRequest); }; - const hanldeEnableColorChecked = checked => { + const handleEnableColorChecked = checked => { if (Boolean(checked)) { handleColorRangeChange('#7567bd', '#7567bd'); } else { @@ -67,7 +68,7 @@ const ColorizeRangeAction: FC<{ hanldeEnableColorChecked(e.target?.checked)} + onChange={e => handleEnableColorChecked(e.target?.checked)} > {t('color.enable')} @@ -79,13 +80,15 @@ const ColorizeRangeAction: FC<{ name="StartColor" rules={[{ required: true }]} initialValue={colorRange?.start} + className="form-item-ex" > - { handleColorRangeChange(v, colorRange?.end); }} - /> + > + + @@ -96,13 +99,15 @@ const ColorizeRangeAction: FC<{ name="EndColor" rules={[{ required: true }]} initialValue={colorRange?.end} + className="form-item-ex" > - { handleColorRangeChange(colorRange?.start, v); }} - /> + > + + @@ -114,4 +119,13 @@ export default ColorizeRangeAction; const StyledColorizeRangeAction = styled(Row)` justify-content: center; + .ColorPicker { + border: 1px solid ${p => p.theme.borderColorBase}; + } + .form-item-ex { + width: 100%; + .ant-form-item-control-input { + width: auto; + } + } `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeSingleAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeSingleAction.tsx index 7cc9f8316..479dd19e7 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeSingleAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/ColorizeSingleAction.tsx @@ -17,7 +17,7 @@ */ import { Checkbox, Col, Row } from 'antd'; -import { ReactColorPicker } from 'app/components/ReactColorPicker'; +import { SingleColorSelection } from 'app/components/ColorPicker'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; @@ -55,7 +55,7 @@ const ColorizeSingleAction: FC<{ onConfigChange?.(newConfig, actionNeedNewRequest); }; - const hanldeEnableColorChecked = checked => { + const handleEnableColorChecked = checked => { if (Boolean(checked)) { handleColorChange('#7567bd'); } else { @@ -65,19 +65,19 @@ const ColorizeSingleAction: FC<{ return ( - + hanldeEnableColorChecked(e.target?.checked)} + onChange={e => handleEnableColorChecked(e.target?.checked)} > {t('color.enable')} - handleColorChange(v)} + 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 37483db53..eeb83212e 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 @@ -30,25 +30,29 @@ const FilterAction: FC<{ dataset?: ChartDataset; dataView?: ChartDataView; dataConfig?: ChartDataSectionConfig; + aggregation?: boolean; onConfigChange: ( config: ChartDataSectionField, needRefresh?: boolean, ) => void; -}> = memo(({ config, dataset, dataView, dataConfig, onConfigChange }) => { - const handleFetchDataFromField = async fieldId => { - // TODO: tobe implement to get fields - return await Promise.resolve(['a', 'b', 'c'].map(f => `${fieldId}-${f}`)); - }; - return ( - - ); -}); +}> = memo( + ({ config, dataset, dataView, dataConfig, onConfigChange, aggregation }) => { + const handleFetchDataFromField = async fieldId => { + // TODO: tobe implement to get fields + return await Promise.resolve(['a', 'b', 'c'].map(f => `${fieldId}-${f}`)); + }; + return ( + + ); + }, +); export default FilterAction; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionConfiguration.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionConfiguration.tsx index 963c5f79e..dd3977641 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionConfiguration.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/CategoryConditionConfiguration.tsx @@ -21,7 +21,7 @@ import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; import useMount from 'app/hooks/useMount'; import { FilterConditionType, - FilterValueOption, + RelationFilterValue, } from 'app/types/ChartConfig'; import ChartDataView from 'app/types/ChartDataView'; import { getDistinctFields } from 'app/utils/fetch'; @@ -33,7 +33,6 @@ import ChartFilterCondition, { ConditionBuilder, } from '../../../../../models/ChartFilterCondition'; import CategoryConditionEditableTable from './CategoryConditionEditableTable'; -// import CategoryConditionEditableTable from './CategoryConditionEditableTableBak'; import CategoryConditionRelationSelector from './CategoryConditionRelationSelector'; const CategoryConditionConfiguration: FC< @@ -72,12 +71,12 @@ const CategoryConditionConfiguration: FC< if (Array.isArray(condition?.value)) { const firstValues = (condition?.value as [])?.filter(n => { - if (IsKeyIn(n as FilterValueOption, 'key')) { - return (n as FilterValueOption).isSelected; + if (IsKeyIn(n as RelationFilterValue, 'key')) { + return (n as RelationFilterValue).isSelected; } return false; }) || []; - values = firstValues?.map((n: FilterValueOption) => n.key); + values = firstValues?.map((n: RelationFilterValue) => n.key); } } return values || []; @@ -85,8 +84,8 @@ const CategoryConditionConfiguration: FC< const [selectedKeys, setSelectedKeys] = useState([]); const [isTree, setIsTree] = useState(isTreeModel(condition?.value)); const [treeOptions, setTreeOptions] = useState([]); - const [listDatas, setListDatas] = useState([]); - const [treeDatas, setTreeDatas] = useState([]); + const [listDatas, setListDatas] = useState([]); + const [treeDatas, setTreeDatas] = useState([]); useMount(() => { if (curTab === FilterConditionType.List) { @@ -102,17 +101,17 @@ const CategoryConditionConfiguration: FC< selectedKeys.indexOf(eventKey) !== -1; const fetchNewDataset = async (viewId, colName) => { - const feildDataset = await getDistinctFields( + const fieldDataset = await getDistinctFields( viewId, colName, undefined, undefined, ); - return feildDataset; + return fieldDataset; }; - const setListSelctedState = ( - list?: FilterValueOption[], + const setListSelectedState = ( + list?: RelationFilterValue[], keys?: string[], ) => { return (list || []).map(c => @@ -121,7 +120,7 @@ const CategoryConditionConfiguration: FC< }; const setTreeCheckableState = ( - treeList?: FilterValueOption[], + treeList?: RelationFilterValue[], keys?: string[], ) => { return (treeList || []).map(c => { @@ -132,7 +131,7 @@ const CategoryConditionConfiguration: FC< }; const handleGeneralListChange = async selectedKeys => { - const items = setListSelctedState(listDatas, selectedKeys); + const items = setListSelectedState(listDatas, selectedKeys); setTargetKeys(selectedKeys); setListDatas(items); @@ -178,27 +177,21 @@ const CategoryConditionConfiguration: FC< // setListDatas(convertToList(dataset?.columns, selectedKeys)); } else { setListDatas(convertToList(dataset?.rows, selectedKeys)); - setTargetKeys([]); - const filter = new ConditionBuilder(condition) - .setOperator(FilterSqlOperator.In) - .setValue([]) - .asGeneral(); - onConditionChange(filter); } }); }; - const convertToList = (collection, selecteKeys) => { + const convertToList = (collection, selectedKeys) => { const items: string[] = (collection || []).flatMap(c => c); const uniqueKeys = Array.from(new Set(items)); return uniqueKeys.map(item => ({ key: item, label: item, - isSelected: selecteKeys.includes(item), + isSelected: selectedKeys.includes(item), })); }; - const convertToTree = (collection, selecteKeys) => { + const convertToTree = (collection, selectedKeys) => { const associateField = treeOptions?.[0]; const labelField = treeOptions?.[1]; @@ -215,25 +208,25 @@ const CategoryConditionConfiguration: FC< if (!associateItem) { return null; } - const assocaiteChildren = collection + const associateChildren = collection .filter(c => c[associateField] === key) .map(c => { const itemKey = c[labelField]; return { key: itemKey, label: itemKey, - isSelected: isChecked(selecteKeys, itemKey), + isSelected: isChecked(selectedKeys, itemKey), }; }); const itemKey = associateItem?.[colName]; return { key: itemKey, label: itemKey, - isSelected: isChecked(selecteKeys, itemKey), - children: assocaiteChildren, + isSelected: isChecked(selectedKeys, itemKey), + children: associateChildren, }; }) - .filter(i => Boolean(i)) as FilterValueOption[]; + .filter(i => Boolean(i)) as RelationFilterValue[]; return treeNodes; }; 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 d2364921a..ef6a7ba5a 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 @@ -19,11 +19,11 @@ import { Button, Space } from 'antd'; import DragSortEditTable from 'app/components/DragSortEditTable'; import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import { FilterValueOption } from 'app/types/ChartConfig'; -import ChartDataView from 'app/types/ChartDataView'; import ChartFilterCondition, { ConditionBuilder, } from 'app/pages/ChartWorkbenchPage/models/ChartFilterCondition'; +import { RelationFilterValue } from 'app/types/ChartConfig'; +import ChartDataView from 'app/types/ChartDataView'; import { getDistinctFields } from 'app/utils/fetch'; import { FilterSqlOperator } from 'globalConstants'; import { FC, memo, useCallback, useEffect, useState } from 'react'; @@ -44,12 +44,11 @@ const CategoryConditionEditableTable: FC< fetchDataByField, }) => { const t = useI18NPrefix(i18nPrefix); - const [rows, setRows] = useState([]); - const [showPopover, setShowPopover] = useState(false); + const [rows, setRows] = useState([]); useEffect(() => { if (Array.isArray(condition?.value)) { - setRows(condition?.value as FilterValueOption[]); + setRows(condition?.value as RelationFilterValue[]); } else { setRows([]); } @@ -78,15 +77,13 @@ const CategoryConditionEditableTable: FC< title: t('tableHeaderAction'), dataIndex: 'action', width: 80, - render: (_, record: FilterValueOption) => ( + render: (_, record: RelationFilterValue) => ( {!record.isSelected && ( - handleRowStateUpdate( - Object.assign(record, { isSelected: true }), - ) + handleRowStateUpdate({ ...record, isSelected: true }) } > {t('setDefault')} @@ -96,9 +93,7 @@ const CategoryConditionEditableTable: FC< - handleRowStateUpdate( - Object.assign(record, { isSelected: false }), - ) + handleRowStateUpdate({ ...record, isSelected: false }) } > {t('setUnDefault')} @@ -118,7 +113,7 @@ const CategoryConditionEditableTable: FC< } return { ...col, - onCell: (record: FilterValueOption) => ({ + onCell: (record: RelationFilterValue) => ({ record, editable: col.editable, dataIndex: col.dataIndex, @@ -144,7 +139,7 @@ const CategoryConditionEditableTable: FC< const handleAdd = () => { const newKey = rows?.length + 1; - const newRow: FilterValueOption = { + const newRow: RelationFilterValue = { key: String(newKey), label: String(newKey), isSelected: false, @@ -153,14 +148,14 @@ const CategoryConditionEditableTable: FC< handleFilterConditionChange(currentRows); }; - const handleRowStateUpdate = (row: FilterValueOption) => { - const oldRowIndex = rows.findIndex(r => r.index === row.index); - rows.splice(oldRowIndex, 1, row); - handleFilterConditionChange(rows); + const handleRowStateUpdate = (row: RelationFilterValue) => { + const newRows = [...rows]; + const targetIndex = newRows.findIndex(r => r.index === row.index); + newRows.splice(targetIndex, 1, row); + handleFilterConditionChange(newRows); }; const handleFetchDataFromField = field => async () => { - setShowPopover(false); if (fetchDataByField) { const dataset = await fetchNewDataset(dataView?.id!, field); const newRows = convertToList(dataset?.rows, []); @@ -181,23 +176,23 @@ const CategoryConditionEditableTable: FC< ); const fetchNewDataset = async (viewId, colName) => { - const feildDataset = await getDistinctFields( + const fieldDataset = await getDistinctFields( viewId, colName, undefined, undefined, ); - return feildDataset; + return fieldDataset; }; - const convertToList = (collection, selecteKeys) => { + 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, key: item, label: item, - isSelected: selecteKeys.includes(item), + isSelected: selectedKeys.includes(item), })); }; @@ -216,10 +211,9 @@ const CategoryConditionEditableTable: FC< dataSource={rows} size="small" bordered - rowKey={(r: FilterValueOption) => `${r.key}-${r.label}`} + rowKey={(r: RelationFilterValue) => `${r.key}-${r.label}`} columns={columnsWithCell} pagination={false} - // onMoveRowEnd={onMoveRowEnd} onRow={(_, index) => ({ index, 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 07cdde075..8fcb7b9e7 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 @@ -18,8 +18,18 @@ import { Tabs } from 'antd'; import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import { FC, memo } from 'react'; -import ChartFilterCondition from '../../../../../models/ChartFilterCondition'; +import { FilterConditionType } from 'app/types/ChartConfig'; +import { formatTime } from 'app/utils/time'; +import { + FILTER_TIME_FORMATTER_IN_QUERY, + RECOMMEND_TIME, +} from 'globalConstants'; +import moment from 'moment'; +import { FC, memo, useState } from 'react'; +import styled from 'styled-components/macro'; +import ChartFilterCondition, { + ConditionBuilder, +} from '../../../../../models/ChartFilterCondition'; import TimeSelector from '../../ChartTimeSelector'; const DateConditionConfiguration: FC< @@ -29,27 +39,65 @@ const DateConditionConfiguration: FC< } & I18NComponentProps > = memo(({ i18nPrefix, condition, onChange: onConditionChange }) => { const t = useI18NPrefix(i18nPrefix); + const [type, setType] = useState(() => + condition?.type === FilterConditionType.RangeTime + ? String(FilterConditionType.RangeTime) + : String(FilterConditionType.RecommendTime), + ); + + const clearFilterWhenTypeChange = (type: string) => { + setType(type); + const conditionType = Number(type); + if (conditionType === FilterConditionType.RecommendTime) { + const filter = new ConditionBuilder(condition) + .setValue(RECOMMEND_TIME.TODAY) + .asRecommendTime(); + onConditionChange?.(filter); + } else if (conditionType === FilterConditionType.RangeTime) { + const filterRow = new ConditionBuilder(condition) + .setValue([ + formatTime(moment(), FILTER_TIME_FORMATTER_IN_QUERY), + formatTime(moment(), FILTER_TIME_FORMATTER_IN_QUERY), + ]) + .asRangeTime(); + onConditionChange?.(filterRow); + } + }; return ( - - - - - - - - - - + + + + + + + + ); }); export default DateConditionConfiguration; + +const StyledDateConditionConfiguration = styled(Tabs)` + width: 100%; + padding: 0 !important; + + .ant-tabs-content-holder { + margin: 10px 0; + } +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterControlPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterControlPanel.tsx index b657782ba..75644c518 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterControlPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterControlPanel.tsx @@ -50,6 +50,7 @@ const FilterControllPanel: FC< dataset?: ChartDataset; dataView?: ChartDataView; dataConfig?: ChartDataSectionConfig; + aggregation?: boolean; onConfigChange: ( config: ChartDataSectionField, needRefresh?: boolean, @@ -63,6 +64,7 @@ const FilterControllPanel: FC< dataView, i18nPrefix, dataConfig, + aggregation, onConfigChange, fetchDataByField, }) => { @@ -75,6 +77,9 @@ const FilterControllPanel: FC< const t = useI18NPrefix(customizeI18NPrefix); const [alias, setAlias] = useState(config.alias); const [aggregate, setAggregate] = useState(() => { + if (Boolean(dataConfig?.disableAggregate) || aggregation === false) { + return AggregateFieldActionType.NONE; + } if (config.aggregate) { return config.aggregate; } else if ( @@ -217,7 +222,7 @@ const FilterControllPanel: FC< > handleNameChange(e.target?.value)} /> - {config.category === ChartDataViewFieldCategory.Field && ( + {config.category === ChartDataViewFieldCategory.Field && aggregation && ( { case FilterConditionType.Value: return [ControllerFacadeTypes.Value]; case FilterConditionType.RangeTime: - case FilterConditionType.RelativeTime: - return [ControllerFacadeTypes.RangeTime]; + return [ControllerFacadeTypes.RangeTimePicker]; + case FilterConditionType.RecommendTime: + return [ControllerFacadeTypes.RangeTimePicker]; case FilterConditionType.Tree: return [ControllerFacadeTypes.Tree]; } @@ -133,6 +131,10 @@ const FilterFacadeConfiguration: FC< !facades.includes(currentFacade as ControllerFacadeTypes) ) { setCurrentFacade(undefined); + handleFacadeChange(undefined); + } else if (!currentFacade) { + setCurrentFacade(facades?.[0]); + handleFacadeChange(facades?.[0]); } }, [condition, category, currentFacade]); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterVisibilityConfiguration.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterVisibilityConfiguration.tsx index 122a3f80c..adf857386 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterVisibilityConfiguration.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/FilterControlPanel/FilterVisibilityConfiguration.tsx @@ -18,10 +18,7 @@ import { Input, Radio, Row, Select, Space } from 'antd'; import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import { - ChartDataSectionField, - FilterVisibility, -} from 'app/types/ChartConfig'; +import { ChartDataSectionField, FilterVisibility } from 'app/types/ChartConfig'; import { ControllerVisibilityTypes } from 'app/types/FilterControlPanel'; import { FilterSqlOperator } from 'globalConstants'; import { FC, memo, useState } from 'react'; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/NumberFormatAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/NumberFormatAction.tsx index 98af535ea..fa413c1df 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/NumberFormatAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/NumberFormatAction.tsx @@ -26,6 +26,7 @@ import { Select, Space, } from 'antd'; +import { FormItemEx } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField, @@ -37,6 +38,7 @@ import { updateBy } from 'app/utils/mutation'; import { NumberUnitKey, NumericUnitDescriptions } from 'globalConstants'; import { FC, useState } from 'react'; import styled from 'styled-components/macro'; +import { SPACE_TIMES } from 'styles/StyleConstants'; const DefaultFormatDetailConfig: IFieldFormatConfig = { type: FieldFormatType.DEFAULT, @@ -71,6 +73,11 @@ const NumberFormatAction: FC<{ config: ChartDataSectionField; onConfigChange: (config: ChartDataSectionField) => void; }> = ({ config, onConfigChange }) => { + const formItemLayout = { + labelAlign: 'right' as any, + labelCol: { span: 8 }, + wrapperCol: { span: 8 }, + }; const t = useI18NPrefix(`viz.palette.data.actions`); const [type, setType] = useState( @@ -112,149 +119,129 @@ const NumberFormatAction: FC<{ return null; } else { return ( - <> - - {t('format.decimalPlace')} - - { - handleFormatDetailChanged( - Object.assign({}, formatDetail, { decimalPlaces }), - ); - }} - /> - - + + + { + handleFormatDetailChanged( + Object.assign({}, formatDetail, { decimalPlaces }), + ); + }} + /> + + {FieldFormatType.CURRENCY === type && ( <> - - {t('format.unit')} - - { - handleFormatDetailChanged( - Object.assign({}, formatDetail, { unitKey }), - ); - }} - > - {Array.from(NumericUnitDescriptions.keys()).map(k => { - const values = NumericUnitDescriptions.get(k); - return ( - - {values?.[1] || ' '} - - ); - })} - - - - - {t('format.currency')} - - { - handleFormatDetailChanged( - Object.assign({}, formatDetail, { currency }), - ); - }} - > - {CURRENCIES.map(c => { - return ( - - {c.code} - - ); - })} - - - + + { + handleFormatDetailChanged( + Object.assign({}, formatDetail, { unitKey }), + ); + }} + > + {Array.from(NumericUnitDescriptions.keys()).map(k => { + const values = NumericUnitDescriptions.get(k); + return ( + + {values?.[1] || ' '} + + ); + })} + + + + { + handleFormatDetailChanged( + Object.assign({}, formatDetail, { currency }), + ); + }} + > + {CURRENCIES.map(c => { + return ( + + {c.code} + + ); + })} + + > )} {FieldFormatType.NUMERIC === type && ( <> - - {t('format.unit')} - - { - handleFormatDetailChanged( - Object.assign({}, formatDetail, { unitKey }), - ); - }} - > - {Array.from(NumericUnitDescriptions.keys()).map(k => { - const values = NumericUnitDescriptions.get(k); - return ( - - {values?.[1] || ' '} - - ); - })} - - - - - {t('format.useSeparator')} - - - handleFormatDetailChanged( - Object.assign({}, formatDetail, { - useThousandSeparator: e.target.checked, - }), - ) - } - /> - - - - {t('format.prefix')} - - - handleFormatDetailChanged( - Object.assign({}, formatDetail, { - prefix: e?.target?.value, - }), - ) - } - /> - - - - {t('format.suffix')} - - - handleFormatDetailChanged( - Object.assign({}, formatDetail, { - suffix: e?.target?.value, - }), - ) - } - /> - - + + { + handleFormatDetailChanged( + Object.assign({}, formatDetail, { unitKey }), + ); + }} + > + {Array.from(NumericUnitDescriptions.keys()).map(k => { + const values = NumericUnitDescriptions.get(k); + return ( + + {values?.[1] || ' '} + + ); + })} + + + + + handleFormatDetailChanged( + Object.assign({}, formatDetail, { + useThousandSeparator: e.target.checked, + }), + ) + } + /> + + + + handleFormatDetailChanged( + Object.assign({}, formatDetail, { + prefix: e?.target?.value, + }), + ) + } + /> + + + + handleFormatDetailChanged( + Object.assign({}, formatDetail, { + suffix: e?.target?.value, + }), + ) + } + /> + > )} - > + ); } }; return ( - + handleFormatTypeChanged(e.target.value)} value={type} @@ -274,16 +261,25 @@ const NumberFormatAction: FC<{ - {renderFieldFormatExtendSetting()} + {renderFieldFormatExtendSetting()} ); }; export default NumberFormatAction; -const StyledNumberFormatAction = styled(Row)``; +const StyledNumberFormatAction = styled(Row)` + .ant-radio-wrapper { + line-height: 32px; + } + + .ant-input-number, + .ant-select, + .ant-input { + width: ${SPACE_TIMES(50)}; + } -const StyledFormatDetailRow = styled(Row)` - align-items: center; - padding: 5px 0; + .ant-space { + width: 100%; + } `; 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 8250566c2..996b3effc 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 @@ -20,15 +20,16 @@ import { CheckOutlined } from '@ant-design/icons'; import { Col, Menu, Radio, Row, Space } from 'antd'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import DraggableList from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/DraggableList'; -import { - ChartDataSectionField, - SortActionType, -} from 'app/types/ChartConfig'; +import { ChartDataSectionField, SortActionType } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; -import { getValueByColumnKey, transfromToObjectArray } from 'app/utils/chartHelper'; +import { + getValueByColumnKey, + transformToObjectArray, +} from 'app/utils/chartHelper'; import { updateBy } from 'app/utils/mutation'; import { FC, useState } from 'react'; import styled from 'styled-components/macro'; +import { isEmpty } from 'utils/object'; const SortAction: FC<{ config: ChartDataSectionField; @@ -38,12 +39,15 @@ const SortAction: FC<{ needRefresh?: boolean, ) => void; mode?: 'menu'; -}> = ({ config, dataset, mode, onConfigChange }) => { - const actionNeedNewRequest = true; + options?; +}> = ({ config, dataset, mode, options, onConfigChange }) => { + const actionNeedNewRequest = isEmpty(options?.backendSort) + ? true + : Boolean(options?.backendSort); const t = useI18NPrefix(`viz.palette.data.actions`); const [direction, setDirection] = useState(config?.sort?.type); const [sortValue, setSortValue] = useState(() => { - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset?.rows, dataset?.columns, ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicAreaChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicAreaChart/config.ts index 88a853b10..7d737abec 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicAreaChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicAreaChart/config.ts @@ -94,6 +94,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -103,16 +123,7 @@ const config: ChartConfig = { showLabelBySwitch: '显示标签2', showLabelByInput: '显示标签3', showLabelWithSelect: '显示标签4', - fontFamily: '字体', - fontSize: '字体大小', - fontColor: '字体颜色', - rotateLabel: '旋转标签', showDataColumns: '选择数据列', - legend: { - label: '图例', - showLabel: '图例-显示标签', - showLabel2: '图例-显示标签2', - }, }, }, { @@ -123,6 +134,7 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + showDataColumns: 'Show Data Columns', }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/BasicBarChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/BasicBarChart.tsx index f6cd20f7d..d48f3afd0 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/BasicBarChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/BasicBarChart.tsx @@ -34,7 +34,7 @@ import { getSeriesTooltips4Rectangular2, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toExponential, @@ -120,7 +120,7 @@ class BasicBarChart extends Chart { .filter(c => c.type === ChartDataSectionType.INFO) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -162,7 +162,7 @@ class BasicBarChart extends Chart { return { tooltip: { trigger: 'item', - formatter: this.getTooltipFormmaterFunc( + formatter: this.getTooltipFormatterFunc( styleConfigs, groupConfigs, aggregateConfigs, @@ -535,6 +535,7 @@ class BasicBarChart extends Chart { return labels.join('\n'); }, }, + labelLayout: { hideOverlap: true }, }; } @@ -579,7 +580,7 @@ class BasicBarChart extends Chart { return `${label}: ${value}`; } - getTooltipFormmaterFunc( + getTooltipFormatterFunc( styleConfigs, groupConfigs, aggregateConfigs, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/config.ts index 0a171ad76..ab639b619 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicBarChart/config.ts @@ -171,6 +171,7 @@ const config: ChartConfig = { comType: 'select', default: 'top', options: { + // TODO(Stephen): to be extract customize LabelPosition Component items: [ { label: '上', value: 'top' }, { label: '左', value: 'left' }, @@ -502,27 +503,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -597,8 +604,76 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/BasicDoubleYChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/BasicDoubleYChart.tsx index 927fd857a..d9986b0cf 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/BasicDoubleYChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/BasicDoubleYChart.tsx @@ -36,7 +36,7 @@ import { getSplitLine, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; @@ -95,7 +95,7 @@ class BasicDoubleYChart extends Chart { const styleConfigs = config.styles || []; const settingConfigs = config.settings; - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/config.ts index 3f1c90142..262bda483 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicDoubleYChart/config.ts @@ -387,27 +387,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -475,9 +481,6 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/BasicFunnelChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/BasicFunnelChart.tsx index 764758129..fad73fee6 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/BasicFunnelChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/BasicFunnelChart.tsx @@ -30,11 +30,11 @@ import { getSeriesTooltips4Scatter, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; -import { isEmpty } from 'lodash'; +import isEmpty from 'lodash/isEmpty'; import Config from './config'; class BasicFunnelChart extends Chart { @@ -99,23 +99,37 @@ class BasicFunnelChart extends Chart { .filter(c => c.type === ChartDataSectionType.INFO) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); + const dataList = !groupConfigs.length + ? objDataColumns + : objDataColumns?.sort( + (a, b) => + b?.[getValueByColumnKey(aggregateConfigs[0])] - + a?.[getValueByColumnKey(aggregateConfigs[0])], + ); + const aggregateList = !groupConfigs.length + ? aggregateConfigs?.sort( + (a, b) => + objDataColumns?.[0]?.[getValueByColumnKey(b)] - + objDataColumns?.[0]?.[getValueByColumnKey(a)], + ) + : aggregateConfigs; const series = this.getSeries( styleConfigs, - aggregateConfigs, + aggregateList, groupConfigs, - objDataColumns, + dataList, infoConfigs, ); return { tooltip: this.getFunnelChartTooltip( groupConfigs, - aggregateConfigs, + aggregateList, infoConfigs, ), legend: this.getLegendStyle(styleConfigs), @@ -244,7 +258,7 @@ class BasicFunnelChart extends Chart { return { ...aggConfig, select: selectAll, - value: aggregateConfigs + value: [aggConfig] .concat(infoConfigs) .map(config => dc?.[getValueByColumnKey(config)]), name: getColumnRenderName(aggConfig), @@ -272,6 +286,7 @@ class BasicFunnelChart extends Chart { shadowColor: 'rgba(0, 0, 0, 0.5)', }, label: this.getLabelStyle(styles), + labelLayout: { hideOverlap: true }, data: this.getFunnelSeriesData(datas), }; } @@ -355,10 +370,12 @@ class BasicFunnelChart extends Chart { }`, ] : []; - const aggTooltips = getSeriesTooltips4Scatter( - [params], - aggregateConfigs.concat(infoConfigs), - ); + const aggTooltips = !!groupConfigs?.length + ? getSeriesTooltips4Scatter( + [params], + aggregateConfigs.concat(infoConfigs), + ) + : getSeriesTooltips4Scatter([params], [data].concat(infoConfigs)); tooltips = tooltips.concat(aggTooltips); if (data.conversion) { tooltips.push(`转化率: ${data.conversion}%`); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/config.ts index 5e172089f..97a989b16 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicFunnelChart/config.ts @@ -71,8 +71,8 @@ const config: ChartConfig = { { label: 'funnel.align', key: 'align', - comType: 'select', default: 'center', + comType: 'select', options: { items: [ { label: '居中', value: 'center' }, @@ -239,14 +239,20 @@ const config: ChartConfig = { ], settings: [ { - label: 'cache.title', - key: 'cache', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'cache.title', - key: 'panel', - comType: 'cache', + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, @@ -287,8 +293,42 @@ const config: ChartConfig = { color: '颜色', colorize: '配色', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + section: { + legend: 'Legend', + detail: 'Detail', + info: 'Info', + }, + label: { + title: 'Title', + showLabel: 'Show Label', + position: 'Position', + metric: 'Metric', + dimension: 'Dimension', + conversion: 'Conversion', + arrival: 'Arrival', + percentage: 'Percentage', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + funnel: { + title: 'Funnel', + sort: 'Sort', + align: 'Alignment', + gap: 'Gap', + }, + data: { + color: 'Color', + colorize: 'Colorize', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/BasicGaugeChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/BasicGaugeChart.tsx index fc06358c1..53b6312f2 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/BasicGaugeChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/BasicGaugeChart.tsx @@ -23,8 +23,9 @@ import { getColumnRenderName, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; +import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; import Config from './config'; @@ -89,18 +90,32 @@ class BasicGaugeChart extends Chart { const aggregateConfigs = dataConfigs .filter(c => c.type === ChartDataSectionType.AGGREGATE) .flatMap(config => config.rows || []); - const dataColumns = transfromToObjectArray(dataset.rows, dataset.columns); - const series = this.getSeries(styleConfigs, dataColumns, aggregateConfigs); + const dataColumns = transformToObjectArray(dataset.rows, dataset.columns); + const series = this.getSeries( + styleConfigs, + dataColumns, + aggregateConfigs[0], + ); + const [prefix, suffix] = this.getArrStyleValueByGroup( + ['prefix', 'suffix'], + styleConfigs, + 'gauge', + ); return { tooltip: { - formatter: '{b} : {c}%', + formatter: ({ data }) => { + return `${data.name} : ${prefix}${toFormattedValue( + data.value, + aggregateConfigs[0].format, + )}${suffix}`; + }, }, series, }; } - private getSeries(styleConfigs, dataColumns, aggregateConfigs) { - const detail = this.getDetail(styleConfigs); + private getSeries(styleConfigs, dataColumns, aggConfig) { + const detail = this.getDetail(styleConfigs, aggConfig); const title = this.getTitle(styleConfigs); const pointer = this.getPointer(styleConfigs); const axis = this.getAxis(styleConfigs); @@ -113,30 +128,29 @@ class BasicGaugeChart extends Chart { 'pointerColor', ); - return aggregateConfigs.map(aggConfig => { - return { - ...this.getGauge(styleConfigs), - data: dataColumns.map(dc => { - const dataConfig: { name: string; value: string; itemStyle: any } = { - name: getColumnRenderName(aggConfig), - value: dc[getValueByColumnKey(aggConfig)] || 0, - itemStyle: { - color: pointerColor, - }, - }; - if (aggConfig?.color?.start) { - dataConfig.itemStyle.color = aggConfig.color.start; - } - return dataConfig; - }), - pointer, - ...axis, - title, - splitLine, - detail, - progress, - }; - }); + const dataConfig: { name: string; value: string; itemStyle: any } = { + name: getColumnRenderName(aggConfig), + value: dataColumns?.[0]?.[getValueByColumnKey(aggConfig)] || 0, + itemStyle: { + color: pointerColor, + }, + }; + if (aggConfig?.color?.start) { + dataConfig.itemStyle.color = aggConfig.color.start; + } + + return { + ...this.getGauge(styleConfigs), + data: [ + dataConfig, + ], + pointer, + ...axis, + title, + splitLine, + detail, + progress, + }; } private getProgress(styleConfigs) { @@ -257,7 +271,7 @@ class BasicGaugeChart extends Chart { }; } - private getDetail(styleConfigs) { + private getDetail(styleConfigs, aggConfig) { const [show, font, detailOffsetLeft, detailOffsetTop] = this.getArrStyleValueByGroup( ['showData', 'font', 'detailOffsetLeft', 'detailOffsetTop'], @@ -277,7 +291,8 @@ class BasicGaugeChart extends Chart { detailOffsetLeft ? detailOffsetLeft : 0, detailOffsetTop ? detailOffsetTop : 0, ], - formatter: value => `${prefix}${Number(value) || 0}${suffix}`, + formatter: value => + `${prefix}${toFormattedValue(value || 0, aggConfig.format)}${suffix}`, }; } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/config.ts index 8261ac848..c97244579 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicGaugeChart/config.ts @@ -21,8 +21,8 @@ import { ChartConfig } from 'app/types/ChartConfig'; const config: ChartConfig = { datas: [ { - label: 'dimension', - key: 'dimension', + label: 'metrics', + key: 'metrics', required: true, type: 'aggregate', limit: 1, @@ -344,7 +344,26 @@ const config: ChartConfig = { ], }, ], - settings: [], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -407,6 +426,67 @@ const config: ChartConfig = { }, }, }, + { + lang: 'en-US', + translation: { + common: { + detailOffsetLeft: 'Offset Left', + detailOffsetTop: 'Offset Top', + distance: 'Distance', + lineStyle: 'Line Style', + splitNumber: 'Split Number', + }, + gauge: { + title: 'Gauge', + max: 'Max', + prefix: 'Prefix', + suffix: 'Suffix', + radius: 'Radius', + startAngle: 'Start Angle', + endAngle: 'End Angle', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + }, + data: { + title: 'Data', + showData: 'Show Data', + }, + pointer: { + title: 'Pointer', + showPointer: 'Show Pointer', + customPointerColor: 'Show Customize Pointer Color', + pointerColor: 'Pointer Color', + pointerLength: 'Pointer Length', + pointerWidth: 'Pointer Width', + lineStyle: 'Line Style', + }, + axis: { + title: 'Axis', + axisLineSize: 'Axis Line Size', + axisLineColor: 'Axis Line Color', + }, + axisTick: { + title: 'Axis Tick', + showAxisTick: 'Show Axis Tick', + }, + axisLabel: { + title: 'Axis Label', + showAxisLabel: 'Show Axis Label', + }, + progress: { + title: 'Progress', + showProgress: 'Show Progress', + roundCap: 'Round Cap', + }, + splitLine: { + title: 'Split Line', + showSplitLine: 'Show Split Line', + splitLineLength: 'Split Line Length', + }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/BasicLineChart.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/BasicLineChart.ts index f1d7a8f2d..c09c099ff 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/BasicLineChart.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/BasicLineChart.ts @@ -39,7 +39,7 @@ import { getSplitLine, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toExponential, @@ -124,7 +124,7 @@ class BasicLineChart extends Chart { .filter(c => c.type === ChartDataSectionType.INFO) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -409,6 +409,7 @@ class BasicLineChart extends Chart { return labels.join('\n'); }, }, + labelLayout: { hideOverlap: true }, }; } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/config.ts index 7c0671543..91ee0e8e6 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicLineChart/config.ts @@ -424,27 +424,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -510,8 +516,67 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + graph: { + title: 'Graph', + smooth: 'Smooth', + step: 'Step', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Split Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/BasicOutlineMapChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/BasicOutlineMapChart.tsx index cd92c75ad..ba0227a48 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/BasicOutlineMapChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/BasicOutlineMapChart.tsx @@ -26,7 +26,7 @@ import { getSeriesTooltips4Polar2, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { init, registerMap } from 'echarts'; import Config from './config'; @@ -110,7 +110,7 @@ class BasicOutlineMapChart extends Chart { this.registerGeoMap(styleConfigs); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -193,6 +193,7 @@ class BasicOutlineMapChart extends Chart { position, ...font, }, + labelLayout: { hideOverlap: true }, }; } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/config.ts index 932dcacf2..d96c31d8d 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicOutlineMapChart/config.ts @@ -217,6 +217,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -261,6 +281,49 @@ const config: ChartConfig = { background: { title: '背景设置' }, }, }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + metricsAndColor: 'Metrics and Color', + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + map: { + title: 'Map', + level: 'Level', + enableZoom: 'Enabel Zoom', + backgroundColor: 'Background Color', + borderStyle: 'Border Style', + focusArea: 'Focus Area', + areaColor: 'Area Color', + areaEmphasisColor: 'Area Emphasis Color', + }, + background: { title: 'Background' }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/BasicPieChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/BasicPieChart.tsx index 74b40c92f..1fa4cc66d 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/BasicPieChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/BasicPieChart.tsx @@ -21,7 +21,6 @@ import { ChartConfig, ChartDataSectionField, ChartDataSectionType, - ChartStyleSectionConfig, } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; import { @@ -30,9 +29,10 @@ import { getExtraSeriesRowData, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, valueFormatter, } from 'app/utils/chartHelper'; +import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; import Config from './config'; @@ -89,7 +89,7 @@ class BasicPieChart extends Chart { } getOptions(dataset: ChartDataset, config: ChartConfig) { - const dataColumns = transfromToObjectArray(dataset.rows, dataset.columns); + const dataColumns = transformToObjectArray(dataset.rows, dataset.columns); const styleConfigs = config.styles; const dataConfigs = config.datas || []; const groupConfigs = dataConfigs @@ -107,11 +107,12 @@ class BasicPieChart extends Chart { dataColumns, groupConfigs, aggregateConfigs, + infoConfigs, ); return { tooltip: { - formatter: this.getTooltipFormmaterFunc( + formatter: this.getTooltipFormatterFunc( styleConfigs, groupConfigs, aggregateConfigs, @@ -124,18 +125,24 @@ class BasicPieChart extends Chart { }; } - private getSeries(styleConfigs, dataColumns, groupConfigs, aggregateConfigs) { + private getSeries( + styleConfigs, + dataColumns, + groupConfigs, + aggregateConfigs, + infoConfigs, + ) { if (!groupConfigs?.length) { const dc = dataColumns?.[0]; return { ...this.getBarSeiesImpl(styleConfigs), data: aggregateConfigs.map(config => { return { - ...getExtraSeriesRowData({ - [getValueByColumnKey(config)]: dc[getValueByColumnKey(config)], - }), + ...config, name: getColumnRenderName(config), - value: dc[getValueByColumnKey(config)], + value: [config] + .concat(infoConfigs) + .map(config => dc?.[getValueByColumnKey(config)]), itemStyle: this.getDataItemStyle(config, groupConfigs, dc), ...getExtraSeriesRowData(dc), ...getExtraSeriesDataFormat(config?.format), @@ -151,9 +158,11 @@ class BasicPieChart extends Chart { name: getColumnRenderName(config), data: dataColumns.map(dc => { return { - ...getExtraSeriesRowData(dc), + ...config, name: groupedConfigNames.map(config => dc[config]).join('-'), - value: dc[getValueByColumnKey(config)], + value: aggregateConfigs + .concat(infoConfigs) + .map(config => dc?.[getValueByColumnKey(config)]), itemStyle: this.getDataItemStyle(config, groupConfigs, dc), ...getExtraSeriesRowData(dc), ...getExtraSeriesDataFormat(config?.format), @@ -202,6 +211,7 @@ class BasicPieChart extends Chart { sampling: 'average', avoidLabelOverlap: false, label: this.getLabelStyle(styleConfigs), + labelLayout: { hideOverlap: true }, ...this.getSeriesStyle(styleConfigs), ...this.getGrid(styleConfigs), }; @@ -261,7 +271,39 @@ class BasicPieChart extends Chart { const show = getStyleValueByGroup(styles, 'label', 'showLabel'); const position = getStyleValueByGroup(styles, 'label', 'position'); const font = getStyleValueByGroup(styles, 'label', 'font'); - return { show, position, ...font, formatter: '{b}: {d}%' }; + const formatter = this.getLabelFormatter(styles); + return { show, position, ...font, formatter }; + } + + getLabelFormatter(styles) { + const showValue = getStyleValueByGroup(styles, 'label', 'showValue'); + const showPercent = getStyleValueByGroup(styles, 'label', 'showPercent'); + const showName = getStyleValueByGroup(styles, 'label', 'showName'); + return seriesParams => { + if (seriesParams.componentType !== 'series') { + return seriesParams.name; + } + const data = seriesParams?.data || {}; + + //处理 label 旧数据中没有 showValue, showPercent, showName 数据 alpha.3版本之后是 boolean 类型 后续版本稳定之后 可以移除此逻辑 + // TODO migration start + if (showName === null || showPercent === null || showValue === null) { + return `${seriesParams?.name}: ${seriesParams?.percent + '%'}`; + } + // TODO migration end --tl + + return `${showName ? seriesParams?.name : ''}${ + showName && (showValue || showPercent) ? ': ' : '' + }${ + showValue ? toFormattedValue(seriesParams?.value[0], data?.format) : '' + }${ + showPercent && showValue + ? '(' + seriesParams?.percent + '%)' + : showPercent + ? seriesParams?.percent + '%' + : '' + }`; + }; } getSeriesStyle(styles) { @@ -272,16 +314,7 @@ class BasicPieChart extends Chart { return { radius: radiusValue, roseType: this.isRose }; } - getStyleValueByGroup( - styles: ChartStyleSectionConfig[], - groupPath: string, - childPath: string, - ) { - const childPaths = childPath.split('.'); - return this.getStyleValue(styles, [groupPath, ...childPaths]); - } - - getTooltipFormmaterFunc( + getTooltipFormatterFunc( styleConfigs, groupConfigs, aggregateConfigs, @@ -289,31 +322,37 @@ class BasicPieChart extends Chart { dataColumns, ) { return seriesParams => { - let dataRow = dataColumns?.find( - dc => - groupConfigs - .map(config => dc?.[getValueByColumnKey(config)]) - .join('-') === seriesParams?.name, - ); - if (dataColumns?.length === 1) { - dataRow = dataColumns[0]; + if (seriesParams.componentType !== 'series') { + return seriesParams.name; } - - const toolTips = [] - .concat(groupConfigs) - .concat( - aggregateConfigs?.filter( - aggConfig => - getValueByColumnKey(aggConfig) === seriesParams?.name || - getValueByColumnKey(aggConfig) === seriesParams?.seriesName, - ), - ) + const { data, value, percent } = seriesParams; + if (!groupConfigs?.length) { + const tooltip = [data] + .concat(infoConfigs) + .map((config, index) => valueFormatter(config, value?.[index])); + tooltip[0] += '(' + percent + '%)'; + return tooltip.join(''); + } + const infoTotal = infoConfigs.map(info => { + let total = 0; + dataColumns.map(dc => { + total += dc?.[getValueByColumnKey(info)]; + }); + return total; + }); + let tooltip = aggregateConfigs .concat(infoConfigs) - .map(config => - valueFormatter(config, dataRow?.[getValueByColumnKey(config)]), - ); - - return toolTips.join(''); + .map((config, index) => { + let tooltipValue = valueFormatter(config, value?.[index]); + if (!index) { + return (tooltipValue += '(' + percent + '%)'); + } + const percentNum = + (value?.[aggregateConfigs?.length] / infoTotal?.[index - 1]) * + 100 || 0; + return (tooltipValue += '(' + percentNum.toFixed(2) + '%)'); + }); + return tooltip.join(''); }; } } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/config.ts index 24c3e4f53..d30f16138 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicPieChart/config.ts @@ -87,6 +87,24 @@ const config: ChartConfig = { color: '#495057', }, }, + { + label: 'label.showName', + key: 'showName', + default: true, + comType: 'checkbox', + }, + { + label: 'label.showValue', + key: 'showValue', + default: false, + comType: 'checkbox', + }, + { + label: 'label.showPercent', + key: 'showPercent', + default: true, + comType: 'checkbox', + }, ], }, { @@ -183,30 +201,23 @@ const config: ChartConfig = { }, ], }, - { - label: 'tooltip.title', - key: 'tooltip', - comType: 'group', - rows: [ - { - label: 'tooltip.showPercentage', - key: 'showPercentage', - default: false, - comType: 'checkbox', - }, - ], - }, ], settings: [ { - label: 'cache.title', - key: 'cache', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'cache.title', - key: 'panel', - comType: 'cache', + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, @@ -233,6 +244,9 @@ const config: ChartConfig = { title: '标签', showLabel: '显示标签', position: '位置', + showName: '维度值', + showPercent: '百分比', + showValue: '指标值', }, legend: { title: '图例', @@ -245,15 +259,54 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, tooltip: { title: '提示信息', showPercentage: '增加百分比显示', }, }, }, + { + lang: 'en-US', + translation: { + section: { + legend: 'Legend', + detail: 'Detail', + }, + common: { + showLabel: 'Show Label', + rotate: 'Rotate', + position: 'Position', + }, + pie: { + title: 'Pie', + circle: 'Circle', + roseType: 'Rose', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + showName: 'Show Name', + showPercent: 'Show Percentage', + showValue: 'Show Value', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + reference: { + title: 'Reference', + open: 'Open', + }, + tooltip: { + title: 'Tooltip', + showPercentage: 'Show Percentage', + }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRadarChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRadarChart/config.ts index 7db180c0b..14a329c37 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRadarChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRadarChart/config.ts @@ -505,27 +505,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -600,9 +606,6 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/BasicRichText.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/BasicRichText.tsx new file mode 100644 index 000000000..bdfa09ebe --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/BasicRichText.tsx @@ -0,0 +1,151 @@ +/** + * 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 ReactChart from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactChart'; +import ChartRichTextAdapter from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartRichTextAdapter'; +import { ChartConfig, ChartDataSectionType } from 'app/types/ChartConfig'; +import ChartDataset from 'app/types/ChartDataset'; +import { + getColumnRenderName, + getCustomSortableColumns, + getStyleValueByGroup, + getValueByColumnKey, + transformToObjectArray, +} from 'app/utils/chartHelper'; +import { toFormattedValue } from 'app/utils/number'; +import Config from './config'; + +class BasicRichText extends ReactChart { + _useIFrame = false; + isISOContainer = 'react-rich-text'; + config = Config; + protected isAutoMerge = false; + richTextOptions = { + dataset: {}, + config: {}, + containerId: '', + widgetSpecialConfig: { env: '' }, + }; + + constructor(props?) { + super(ChartRichTextAdapter, { + id: props?.id || 'react-rich-text', + name: props?.name || '富文本', + icon: props?.icon || 'rich-text', + }); + this.meta.requirements = props?.requirements || [ + { + group: [0, 999], + aggregate: [0, 999], + }, + ]; + } + + onMount(options, context): void { + if (options.containerId === undefined || !context.document) { + return; + } + this.richTextOptions = Object.assign(this.richTextOptions, options); + this.adapter?.mounted( + context.document.getElementById(options.containerId), + options, + context, + ); + } + + onUpdated(options, context): void { + this.richTextOptions = Object.assign(this.richTextOptions, options); + if (!this.isMatchRequirement(options.config)) { + this.adapter?.unmount(); + return; + } + + this.adapter?.updated( + this.getOptions(context, options.dataset, options.config), + context, + ); + } + + onResize(opt: any, context): void { + this.onUpdated(this.richTextOptions, context); + } + + getOptions(context, dataset?: ChartDataset, config?: ChartConfig) { + const { containerId, widgetSpecialConfig } = this.richTextOptions; + if (!dataset || !config || !containerId) { + return { dataList: [], id: '', isEditing: !!widgetSpecialConfig?.env }; + } + const dataConfigs = config.datas || []; + const stylesConfigs = config.styles || []; + const groupConfigs = dataConfigs + .filter(c => c.type === ChartDataSectionType.GROUP) + .flatMap(config => config.rows || []); + const aggregateConfigs = dataConfigs + .filter(c => c.type === ChartDataSectionType.AGGREGATE) + .flatMap(config => config.rows || []); + const objDataColumns = transformToObjectArray( + dataset.rows, + dataset.columns, + ); + const dataColumns = getCustomSortableColumns(objDataColumns, dataConfigs); + + const dataList = groupConfigs.concat(aggregateConfigs).map(config => { + return { + id: config.uid, + name: getColumnRenderName(config), + value: this.getDataListValue(config, dataColumns), + }; + }); + const initContent = getStyleValueByGroup( + stylesConfigs, + 'delta', + 'richText', + ); + return { + dataList, + initContent, + id: containerId, + isEditing: !!widgetSpecialConfig?.env, + ...this.getOnChange(), + }; + } + + getDataListValue(config, dataColumns) { + const value = dataColumns.map(dc => + toFormattedValue(dc[getValueByColumnKey(config)], config.format), + )[0]; + return typeof value !== 'string' && value ? value.toString() : value; + } + + getOnChange(): any { + return this._mouseEvents?.reduce((acc, cur) => { + if (cur.name === 'click') { + Object.assign(acc, { + onChange: delta => + cur.callback?.({ + seriesName: 'richText', + value: delta, + }), + }); + } + return acc; + }, {}); + } +} + +export default BasicRichText; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/config.ts new file mode 100644 index 000000000..1e92a9ab3 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/config.ts @@ -0,0 +1,97 @@ +/** + * 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 { ChartConfig } from 'app/types/ChartConfig'; + +const config: ChartConfig = { + datas: [ + { + label: 'dimension', + key: 'dimension', + type: 'group', + }, + { + label: 'metrics', + key: 'metrics', + type: 'aggregate', + actions: { + NUMERIC: ['alias', 'sortable', 'format', 'aggregate'], + STRING: ['alias', 'sortable', 'format', 'aggregate'], + }, + }, + { + label: 'filter', + key: 'filter', + type: 'filter', + allowSameField: true, + }, + ], + styles: [ + { + label: 'delta.title', + hidden: true, + key: 'delta', + comType: 'group', + rows: [ + { + label: 'delta.richText', + key: 'richText', + default: '', + comType: 'input', + }, + ], + }, + ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], + i18ns: [ + { + lang: 'zh-CN', + translation: { + delta: { + title: '富文本', + text: '内容', + }, + }, + }, + { + lang: 'en-US', + translation: {}, + }, + ], +}; + +export default config; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/index.ts new file mode 100644 index 000000000..bbeaeac5d --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicRichText/index.ts @@ -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 BasicRichText from './BasicRichText'; + +export default BasicRichText; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/BasicScatterChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/BasicScatterChart.tsx index 58b4524cc..ed2910390 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/BasicScatterChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/BasicScatterChart.tsx @@ -28,7 +28,7 @@ import { getSeriesTooltips4Scatter, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { init } from 'echarts'; import Config from './config'; @@ -84,7 +84,7 @@ class BasicScatterChart extends Chart { } getOptions(dataset: ChartDataset, config: ChartConfig) { - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -233,7 +233,7 @@ class BasicScatterChart extends Chart { 'scatter', 'cycleRatio', ); - const defaultSizeValue = max - min; + const defaultSizeValue = (max - min) / 2; const seriesName = groupConfigs ?.map(gc => getColumnRenderName(gc)) .join('-'); @@ -394,7 +394,10 @@ class BasicScatterChart extends Chart { const show = getStyleValueByGroup(styles, 'label', 'showLabel'); const position = getStyleValueByGroup(styles, 'label', 'position'); const font = getStyleValueByGroup(styles, 'label', 'font'); - return { label: { show, position, ...font, formatter: '{b}' } }; + return { + label: { show, position, ...font, formatter: '{b}' }, + labelLayout: { hideOverlap: true }, + }; } getTooltipFormmaterFunc( diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/config.ts index de0807450..001c4987e 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicScatterChart/config.ts @@ -350,6 +350,7 @@ const config: ChartConfig = { default: 'center', comType: 'select', options: { + // TODO(Stephen): to be extract to axis name location component items: [ { label: '开始', value: 'start' }, { label: '结束', value: 'end' }, @@ -460,27 +461,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -526,11 +533,6 @@ const config: ChartConfig = { color: '颜色', colorize: '配色', }, - graph: { - title: '折线图', - smooth: '平滑', - step: '阶梯', - }, xAxis: { title: 'X轴', }, @@ -546,15 +548,73 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, scatter: { title: '散点图配置', cycleRatio: '气泡大像素比', }, }, }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Split Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference', + open: 'Open', + }, + scatter: { + title: 'Scatter', + cycleRatio: 'Cycle Ratio', + }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/AntdTableWrapper.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/AntdTableWrapper.tsx new file mode 100644 index 000000000..c66164c4b --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/AntdTableWrapper.tsx @@ -0,0 +1,72 @@ +/** + * 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 { Table } from 'antd'; +import { FC, memo } from 'react'; +import styled from 'styled-components/macro'; + +const AntdTableWrapper: FC<{ + dataSource: []; + columns: []; + summaryFn?: (data) => { total: number; summarys: [] }; +}> = memo(({ dataSource, columns, children, summaryFn, ...rest }) => { + const getTableSummaryRow = pageData => { + if (!summaryFn) { + return undefined; + } + const summaryData = summaryFn?.(pageData); + return ( + + + {(summaryData?.summarys || []).map((data, index) => { + return ( + {data} + ); + })} + + + ); + }; + + return ( + + ); +}); + +const StyledTable = styled(Table)` + background: 'transparent'; + height: 100%; + overflow: auto; + + .ant-table-summary { + background: #fafafa; + } + .ant-table-cell-fix-left { + background: #fafafa; + } + .ant-table-cell-fix-right { + background: #fafafa; + } +`; + +export default AntdTableWrapper; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/BasicTableChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/BasicTableChart.tsx index 41cf0b041..86c1029eb 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/BasicTableChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/BasicTableChart.tsx @@ -23,27 +23,41 @@ import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { getColumnRenderName, getCustomSortableColumns, + getUnusedHeaderRows, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toFormattedValue } from 'app/utils/number'; -import { Omit } from 'utils/object'; -import { v4 as uuidv4 } from 'uuid'; -import AntdTableChartAdapter from '../../ChartTools/AntdTableChartAdapter'; +import { isEmptyArray, Omit } from 'utils/object'; +import { uuidv4 } from 'utils/utils'; +import AntdTableWrapper from './AntdTableWrapper'; +import { + getCustomBodyCellStyle, + getCustomBodyRowStyle, +} from './conditionStyle'; import Config from './config'; +import { TableComponentsTd } from './TableComponents'; class BasicTableChart extends ReactChart { + _useIFrame = false; isISOContainer = 'react-table'; config = Config; - protected isAutoMerge = false; - tableOptions = { dataset: {}, config: {} }; + utilCanvas = null; + dataColumnWidths = {}; + tablePadding = 16; + tableCellBorder = 1; + cachedAntTableOptions = {}; + cachedDatartConfig: ChartConfig = {}; + showSummaryRow = false; + rowNumberUniqKey = `@datart@rowNumberKey`; constructor(props?) { - super( - props?.id || 'react-table', - props?.name || '表格', - props?.icon || 'table', - ); + super(AntdTableWrapper, { + id: props?.id || 'react-table', + name: props?.name || '表格', + icon: props?.icon || 'table', + }); + this.meta.requirements = props?.requirements || [ { group: [0, 999], @@ -52,54 +66,51 @@ class BasicTableChart extends ReactChart { ]; } - onMount(options, context): void { - if (options.containerId === undefined || !context.document) { - return; - } - - this.getInstance().init(AntdTableChartAdapter); - this.getInstance().mounted( - context.document.getElementById(options.containerId), - options, - context, - ); - } - onUpdated(options, context): void { - this.tableOptions = options; - if (!this.isMatchRequirement(options.config)) { - this.getInstance()?.unmount(); + this.adapter?.unmount(); return; } - this.getInstance()?.updated( - this.getOptions(context, options.dataset, options.config), + const tableOptions = this.getOptions( context, + options.dataset, + options.config, + options.widgetSpecialConfig, ); + this.cachedAntTableOptions = tableOptions; + this.cachedDatartConfig = options.config; + this.adapter?.updated(tableOptions, context); } - onUnMount(): void { - this.getInstance()?.unmount(); - } - - onResize(opt: any, context): void { - this.onUpdated(this.tableOptions, context); + public onResize(options, context?): void { + this.adapter?.updated( + Object.assign(this.cachedAntTableOptions, { + ...this.getAntdTableStyleOptions( + this.cachedDatartConfig?.styles, + this.cachedDatartConfig?.settings!, + context?.height, + ), + }), + context, + ); } - getTableY() {} - - getOptions(context, dataset?: ChartDataset, config?: ChartConfig) { + getOptions( + context, + dataset?: ChartDataset, + config?: ChartConfig, + widgetSpecialConfig?: any, + ) { if (!dataset || !config) { return { locale: { emptyText: ' ' } }; } - const { clientWidth, clientHeight } = context.document.documentElement; const dataConfigs = config.datas || []; const styleConfigs = config.styles || []; const settingConfigs = config.settings || []; - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -108,41 +119,186 @@ class BasicTableChart extends ReactChart { const mixedSectionConfigRows = dataConfigs .filter(c => c.key === 'mixed') .flatMap(config => config.rows || []); - const groupConfigs = mixedSectionConfigRows.filter( r => r.type === ChartDataViewFieldType.STRING || r.type === ChartDataViewFieldType.DATE, ); - const aggregateConfigs = mixedSectionConfigRows.filter( r => r.type === ChartDataViewFieldType.NUMERIC, ); - - let tablePagination = this.getPagingOptions( + const tablePagination = this.getPagingOptions( settingConfigs, dataset?.pageInfo, ); + this.dataColumnWidths = this.calcuteFieldsMaxWidth( + mixedSectionConfigRows, + dataColumns, + styleConfigs, + context, + ); + + const tableColumns = this.getColumns( + groupConfigs, + aggregateConfigs, + styleConfigs, + dataColumns, + ); + return { rowKey: 'uid', pagination: tablePagination, dataSource: this.generateTableRowUniqId(dataColumns), - columns: this.getColumns( - groupConfigs, - aggregateConfigs, - styleConfigs, + columns: tableColumns, + summaryFn: this.getTableSummaryFn( + settingConfigs, dataColumns, + tableColumns, + aggregateConfigs, ), - components: this.getTableComponents(styleConfigs), + components: this.getTableComponents(styleConfigs, widgetSpecialConfig), ...this.getAntdTableStyleOptions( styleConfigs, - dataset, - clientWidth, - clientHeight, - tablePagination, + settingConfigs, + context?.height, ), + onChange: (pagination, filters, sorter, extra) => { + if (extra?.action === 'sort' || extra?.action === 'paginate') { + this.invokePagingRelatedEvents( + sorter?.field, + sorter?.order, + pagination?.current, + ); + } + }, + }; + } + + getTableSummaryFn( + settingConfigs, + dataColumns, + tableColumns, + aggregateConfigs, + ) { + const aggregateFields = this.getStyleValue(settingConfigs, [ + 'summary', + 'aggregateFields', + ]); + this.showSummaryRow = aggregateFields && aggregateFields.length > 0; + if (!this.showSummaryRow) { + return; + } + + const aggregateFieldConfigs = aggregateConfigs.filter(c => + aggregateFields.includes(c.uid), + ); + + const _flatChildren = node => { + if (Array.isArray(node?.children)) { + return (node.children || []).reduce((acc, cur) => { + return acc.concat(..._flatChildren(cur)); + }, []); + } + return [node]; }; + const flatHeaderColumns = (tableColumns || []).reduce((acc, cur) => { + return acc.concat(..._flatChildren(cur)); + }, []); + + return _ => { + return { + summarys: flatHeaderColumns + .map(c => c.key) + .map(k => { + const currentSummaryField = aggregateFieldConfigs.find( + c => getValueByColumnKey(c) === k, + ); + if (currentSummaryField) { + const total = dataColumns.map( + dc => dc?.[getValueByColumnKey(currentSummaryField)], + ); + return total.reduce((acc, cur) => acc + cur, 0); + } + return null; + }), + }; + }; + } + + calcuteFieldsMaxWidth( + mixedSectionConfigRows, + dataColumns, + styleConfigs, + context, + ) { + const bodyFont = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'font', + ]); + const headerFont = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'font', + ]); + const tableHeaders = this.getStyleValue(styleConfigs, [ + 'header', + 'modal', + 'tableHeaders', + ]); + const enableRowNumber = this.getStyleValue(styleConfigs, [ + 'style', + 'enableRowNumber', + ]); + const maxContentByFields = mixedSectionConfigRows.map(c => { + const header = this.findHeader(c.uid, tableHeaders); + const rowUniqKey = getValueByColumnKey(c); + const datas = dataColumns?.map(dc => { + const text = dc[rowUniqKey]; + let width = this.getTextWidth( + context, + text, + bodyFont?.fontWeight, + bodyFont?.fontSize, + bodyFont?.fontFamily, + ); + const headerWidth = this.getTextWidth( + context, + header?.label || header?.colName, + headerFont?.fontWeight, + headerFont?.fontSize, + headerFont?.fontFamily, + ); + const sorterIconWidth = 12; + return Math.max(width, headerWidth + sorterIconWidth); + }); + + const getRowNumberWidth = maxContent => { + if (!enableRowNumber) { + return 0; + } + + return this.getTextWidth( + context, + maxContent, + bodyFont?.fontWeight, + bodyFont?.fontSize, + bodyFont?.fontFamily, + ); + }; + + return { + [this.rowNumberUniqKey]: + getRowNumberWidth(dataColumns?.length) + + this.tablePadding * 2 + + this.tableCellBorder * 2, + [rowUniqKey]: + Math.max(...datas) + this.tablePadding * 2 + this.tableCellBorder * 2, + }; + }); + + return maxContentByFields.reduce((acc, cur) => { + return Object.assign({}, acc, { ...cur }); + }, {}); } generateTableRowUniqId(dataColumns) { @@ -154,34 +310,68 @@ class BasicTableChart extends ReactChart { }); } - getTableComponents(styleConfigs) { + getTableComponents(styleConfigs, widgetSpecialConfig) { + const linkFields = widgetSpecialConfig?.linkFields; + const jumpField = widgetSpecialConfig?.jumpField; + const tableHeaders = this.getStyleValue(styleConfigs, [ 'header', 'modal', 'tableHeaders', ]); + const headerBgColor = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'bgColor', + ]); + const headerFont = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'font', + ]); + const headerTextAlign = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'align', + ]); + const bodyBgColor = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'bgColor', + ]); + const bodyFont = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'font', + ]); + const bodyTextAlign = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'align', + ]); + const getAllColumnListInfo = this.getValue( + styleConfigs, + ['column', 'modal', 'list'], + 'rows', + ); + let allConditionStyle: any[] = []; + getAllColumnListInfo?.forEach(info => { + const getConditionStyleValue = this.getValue( + info.rows, + ['conditionStyle', 'conditionStylePanel'], + 'value', + ); + if (Array.isArray(getConditionStyleValue)) { + allConditionStyle = [...allConditionStyle, ...getConditionStyleValue]; + } + }); return { header: { cell: props => { const uid = props.uid; - const _findRow = (uid, headers) => { - let header = headers.find(h => h.uid === uid); - if (!!header) { - return header; - } - for (let i = 0; i < headers.length; i++) { - header = _findRow(uid, headers[i].children || []); - if (!!header) { - break; - } - } - return header; + const { style, title, ...rest } = props; + const header = this.findHeader(uid, tableHeaders || []); + const cellCssStyle = { + textAlign: headerTextAlign, + backgroundColor: headerBgColor, + ...headerFont, + fontSize: +headerFont?.fontSize, }; - - const header = _findRow(uid, tableHeaders || []); - const cellCssStyle = {}; - if (header && header.style) { const fontStyle = header.style?.font?.value; Object.assign( @@ -193,38 +383,46 @@ class BasicTableChart extends ReactChart { { ...fontStyle }, ); } - return ; + return ; }, }, body: { cell: props => { + const { style, dataIndex, ...rest } = props; const uid = props.uid; - const backgroundColor = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', + const conditionStyle = this.getStyleValue(getAllColumnListInfo, [ uid, - 'basicStyle', - 'backgroundColor', - ]); - const textAlign = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'basicStyle', - 'align', - ]); - const font = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'basicStyle', - 'font', + 'conditionStyle', + 'conditionStylePanel', ]); + const conditionalCellStyle = getCustomBodyCellStyle( + props, + conditionStyle, + ); + return ( + + ); + }, + row: props => { + const { style, ...rest } = props; + const rowStyle = getCustomBodyRowStyle(props, allConditionStyle); + return ; + }, + wrapper: props => { + const { style, ...rest } = props; + const bodyStyle = { + textAlign: bodyTextAlign, + backgroundColor: bodyBgColor, + ...bodyFont, + fontSize: +bodyFont?.fontSize, + }; return ( - + ); }, }, @@ -232,9 +430,9 @@ class BasicTableChart extends ReactChart { } getColumns(groupConfigs, aggregateConfigs, styleConfigs, dataColumns) { - const enableFixedHeader = this.getStyleValue(styleConfigs, [ + const enableRowNumber = this.getStyleValue(styleConfigs, [ 'style', - 'enableFixedHeader', + 'enableRowNumber', ]); const leftFixedColumns = this.getStyleValue(styleConfigs, [ 'style', @@ -244,71 +442,30 @@ class BasicTableChart extends ReactChart { 'style', 'rightFixedColumns', ]); + const autoMergeFields = this.getStyleValue(styleConfigs, [ + 'style', + 'autoMergeFields', + ]); const tableHeaderStyles = this.getStyleValue(styleConfigs, [ 'header', 'modal', 'tableHeaders', ]); - const _getFixedColumn = name => { - if ( - leftFixedColumns === name || - (leftFixedColumns && leftFixedColumns.includes(name)) - ) { + const _getFixedColumn = uid => { + if (String(leftFixedColumns).includes(uid)) { return 'left'; } - if ( - rightFixedColumns === name || - (rightFixedColumns && rightFixedColumns.includes(name)) - ) { + if (String(rightFixedColumns).includes(uid)) { return 'right'; } return null; }; - const _sortFn = rowKey => (prev, next) => { - return prev[rowKey] > next[rowKey]; - }; - const _getFlatColumns = (groupConfigs, aggregateConfigs, dataColumns) => [...groupConfigs, ...aggregateConfigs].map(c => { const colName = c.colName; - const uid = c.uid; - const enableSort = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'sortAndFilter', - 'enableSort', - ]); - const textAlign = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'basicStyle', - 'align', - ]); - const enableFixedCol = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'basicStyle', - 'enableFixedCol', - ]); - const fixedColWidth = this.getStyleValue(styleConfigs, [ - 'column', - 'modal', - 'list', - uid, - 'basicStyle', - 'enableFixedCol', - 'fixedColWidth', - ]); - - const columnRowSpans = this.isAutoMerge + const columnRowSpans = (autoMergeFields || []).includes(c.uid) ? dataColumns ?.map(dc => dc[getValueByColumnKey(c)]) .reverse() @@ -335,18 +492,16 @@ class BasicTableChart extends ReactChart { .reverse() : []; + const colMaxWidth = + this.dataColumnWidths?.[getValueByColumnKey(c)] || 100; return { - sorter: !!enableSort ? _sortFn(colName) : undefined, + sorter: true, title: getColumnRenderName(c), dataIndex: getValueByColumnKey(c), key: getValueByColumnKey(c), - width: enableFixedHeader - ? enableFixedCol - ? fixedColWidth - : null - : null, - fixed: _getFixedColumn(getValueByColumnKey(c)), - align: textAlign, + colName, + width: colMaxWidth, + fixed: _getFixedColumn(c?.uid), onHeaderCell: record => { return { ...Omit(record, [ @@ -355,28 +510,34 @@ class BasicTableChart extends ReactChart { 'onCell', 'colName', 'render', + 'sorter', ]), uid: c.uid, }; }, onCell: (record, rowIndex) => { + const cellValue = record[getValueByColumnKey(c)]; + return { uid: c.uid, + cellValue, + dataIndex: getValueByColumnKey(c), ...this.registerTableCellEvents( getValueByColumnKey(c), + cellValue, rowIndex, - record[getValueByColumnKey(c)], + record, ), }; }, render: (value, row, rowIndex) => { const formattedValue = toFormattedValue(value, c.format); - if (!this.isAutoMerge) { + if (!(autoMergeFields || []).includes(c.uid)) { return formattedValue; } return { children: formattedValue, - props: { rowSpan: columnRowSpans[rowIndex] }, + props: { rowSpan: columnRowSpans[rowIndex], cellValue: value }, }; }, }; @@ -387,22 +548,49 @@ class BasicTableChart extends ReactChart { aggregateConfigs, tableHeaderStyles, dataColumns, - ) => - tableHeaderStyles - ?.map(style => - this.getHeaderColumnGroup( - style, - _getFlatColumns(groupConfigs, aggregateConfigs, dataColumns), - ), - ) - ?.filter(column => !!column) || []; + ) => { + const flattenedColumns = _getFlatColumns( + groupConfigs, + aggregateConfigs, + dataColumns, + ); + + const groupedHeaderColumns = + tableHeaderStyles + ?.map(style => this.getHeaderColumnGroup(style, flattenedColumns)) + ?.filter(Boolean) || []; + + const unusedHeaderRows = getUnusedHeaderRows( + flattenedColumns, + groupedHeaderColumns, + ); + + return groupedHeaderColumns.concat(unusedHeaderRows); + }; + + const rowNumbers = enableRowNumber + ? [ + { + key: 'id', + title: '', + dataIndex: 'id', + width: this.dataColumnWidths?.[this.rowNumberUniqKey] || 0, + fixed: leftFixedColumns?.length > 0 ? 'left' : null, + } as any, + ] + : []; + return !tableHeaderStyles || tableHeaderStyles.length === 0 - ? _getFlatColumns(groupConfigs, aggregateConfigs, dataColumns) - : _getGroupColumns( - groupConfigs, - aggregateConfigs, - tableHeaderStyles, - dataColumns, + ? rowNumbers.concat( + _getFlatColumns(groupConfigs, aggregateConfigs, dataColumns), + ) + : rowNumbers.concat( + _getGroupColumns( + groupConfigs, + aggregateConfigs, + tableHeaderStyles, + dataColumns, + ), ); } @@ -415,6 +603,7 @@ class BasicTableChart extends ReactChart { } return { uid: tableHeader?.uid, + colName: tableHeader?.colName, title: tableHeader.label, onHeaderCell: record => { return { @@ -425,17 +614,15 @@ class BasicTableChart extends ReactChart { .map(th => { return this.getHeaderColumnGroup(th, columns); }) - .filter(column => !!column), + .filter(Boolean), }; } - getAntdTableStyleOptions( - styleConfigs, - dataset: ChartDataset, - width, - height, - tablePagination, - ) { + getAntdTableStyleOptions(styleConfigs?, settingConfigs?, height?) { + const enablePaging = this.getStyleValue(settingConfigs, [ + 'paging', + 'enablePaging', + ]); const showTableBorder = this.getStyleValue(styleConfigs, [ 'style', 'enableBorder', @@ -444,22 +631,42 @@ class BasicTableChart extends ReactChart { 'style', 'enableFixedHeader', ]); + const tableHeaderStyles = this.getStyleValue(styleConfigs, [ + 'header', + 'modal', + 'tableHeaders', + ]); const tableSize = - this.getStyleValue(styleConfigs, ['data', 'tableSize']) || 'default'; + this.getStyleValue(styleConfigs, ['style', 'tableSize']) || 'default'; const HEADER_HEIGHT = { default: 56, middle: 48, small: 40 }; const PAGINATION_HEIGHT = { default: 64, middle: 56, small: 56 }; + const SUMMRAY_ROW_HEIGHT = { default: 64, middle: 56, small: 56 }; + const _getMaxHeaderHierarchy = (headerStyles: Array<{ children: [] }>) => { + const _maxDeeps = (arr: Array<{ children: [] }> = [], deeps: number) => { + if (!isEmptyArray(arr) && arr?.length > 0) { + return Math.max(...arr.map(a => _maxDeeps(a.children, deeps + 1))); + } + return deeps; + }; + return _maxDeeps(headerStyles, 0) || 1; + }; + const totalWidth = Object.values(this.dataColumnWidths).reduce( + (a, b) => a + b, + 0, + ); return { - scroll: enableFixedHeader - ? { - scrollToFirstRowOnChange: true, - x: 'max-content', - y: - height - - HEADER_HEIGHT[tableSize] - - (tablePagination ? PAGINATION_HEIGHT[tableSize] : 0), - } - : { scrollToFirstRowOnChange: true, x: 'max-content' }, + scroll: Object.assign({ + scrollToFirstRowOnChange: true, + x: !enableFixedHeader ? 'max-content' : totalWidth, + y: !enableFixedHeader + ? undefined + : height - + (this.showSummaryRow ? SUMMRAY_ROW_HEIGHT[tableSize] : 0) - + HEADER_HEIGHT[tableSize] * + _getMaxHeaderHierarchy(tableHeaderStyles) - + (enablePaging ? PAGINATION_HEIGHT[tableSize] : 0), + }), bordered: !!showTableBorder, size: tableSize, }; @@ -476,70 +683,121 @@ class BasicTableChart extends ReactChart { current: pageInfo?.pageNo, pageSize: pageInfo?.pageSize, total: pageInfo?.total, - ...this.registerTablePagingEvents('paging', 0, null), }) : false; } - registerTablePagingEvents(seriesName: string, dataIndex: number, value: any) { - const eventParams = { - componentType: 'series', - seriesType: 'table', - seriesName, // column name/index - dataIndex, // row index - value, // cell value - }; - return this._mouseEvents?.reduce((acc, cur) => { + createrEventParams = params => ({ + type: 'click', + componentType: 'table', + seriesType: undefined, + data: undefined, + dataIndex: undefined, + event: undefined, + name: undefined, + seriesName: undefined, + value: undefined, + ...params, + }); + + invokePagingRelatedEvents(seriesName: string, value: any, pageNo: number) { + const eventParams = this.createrEventParams({ + seriesType: 'paging-sort-filter', + seriesName, + value: { + direction: + value === undefined ? undefined : value === 'ascend' ? 'ASC' : 'DESC', + pageNo, + }, + }); + this._mouseEvents?.forEach(cur => { if (cur.name === 'click') { - Object.assign(acc, { - onChange: (page, pageSize) => - cur.callback?.( - Object.assign({}, eventParams, { value: { page, pageSize } }), - ), - }); + cur.callback?.(eventParams); } - return acc; - }, {}); + }); } - registerTableCellEvents(seriesName: string, dataIndex: number, value: any) { - const eventParams = { - componentType: 'series', - seriesType: 'table', - name: value, + registerTableCellEvents( + seriesName: string, + value: any, + dataIndex: number, + record: any, + ) { + const eventParams = this.createrEventParams({ + seriesType: 'body', + name: seriesName, + data: { + format: undefined, + name: seriesName, + rowData: record, + value: value, + }, seriesName, // column name/index dataIndex, // row index value, // cell value - }; + }); return this._mouseEvents?.reduce((acc, cur) => { + cur.name && (eventParams.type = cur.name); if (cur.name === 'click') { Object.assign(acc, { - onClick: event => cur.callback?.(eventParams), + onClick: event => cur.callback?.({ ...eventParams, event }), }); } if (cur.name === 'dblclick') { Object.assign(acc, { - onDoubleClick: event => cur.callback?.(eventParams), + onDoubleClick: event => cur.callback?.({ ...eventParams, event }), }); } if (cur.name === 'contextmenu') { Object.assign(acc, { - onContextMenu: event => cur.callback?.(eventParams), + onContextMenu: event => cur.callback?.({ ...eventParams, event }), }); } if (cur.name === 'mouseover') { Object.assign(acc, { - onMouseEnter: event => cur.callback?.(eventParams), + onMouseEnter: event => cur.callback?.({ ...eventParams, event }), }); } if (cur.name === 'mouseout') { Object.assign(acc, { - onMouseLeave: event => cur.callback?.(eventParams), + onMouseLeave: event => cur.callback?.({ ...eventParams, event }), }); } return acc; }, {}); } + + getTextWidth = ( + context, + text: string, + fontWeight: string, + fontSize: string, + fontFamily: string, + ): number => { + const canvas = + this.utilCanvas || + (this.utilCanvas = context.document.createElement('canvas')); + const measureLayer = canvas.getContext('2d'); + measureLayer.font = `${fontWeight} ${fontSize}px ${fontFamily}`; + const metrics = measureLayer.measureText(text); + return Math.ceil(metrics.width); + }; + + findHeader = (uid, headers) => { + let header = (headers || []) + .filter(h => !h.isGroup) + .find(h => h.uid === uid); + if (!!header) { + return header; + } + for (let i = 0; i < (headers || []).length; i++) { + header = this.findHeader(uid, headers[i].children || []); + if (!!header) { + break; + } + } + return header; + }; } export default BasicTableChart; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/TableComponents.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/TableComponents.tsx new file mode 100644 index 000000000..a0ca743bd --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/TableComponents.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import styled from 'styled-components/macro'; +import { BLUE } from 'styles/StyleConstants'; + +export const TableComponentsTd = (props: any) => { + return ; +}; + +const Td = styled.td` + ${p => + p.isLinkCell + ? ` + :hover { + color: ${BLUE}; + cursor: pointer; + } + ` + : p.isJumpCell + ? ` + :hover { + color: ${BLUE}; + cursor: pointer; + text-decoration: underline; + } + ` + : null} +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/conditionStyle.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/conditionStyle.ts new file mode 100644 index 000000000..85998e473 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/conditionStyle.ts @@ -0,0 +1,146 @@ +import { ConditionStyleFormValues } from 'app/components/FormGenerator/Customize/ConditionStylePanel'; +import { OperatorTypes } from 'app/components/FormGenerator/Customize/ConditionStylePanel/types'; +import { CSSProperties } from 'react'; + +const isMatchedTheCondition = ( + value: string | number, + operatorType: OperatorTypes, + conditionValues: string | number | (string | number)[], +) => { + let matchTheCondition = false; + + switch (operatorType) { + case OperatorTypes.Equal: + matchTheCondition = value === conditionValues; + break; + case OperatorTypes.NotEqual: + matchTheCondition = value !== conditionValues; + break; + case OperatorTypes.Contain: + matchTheCondition = (value as string).includes(conditionValues as string); + break; + case OperatorTypes.NotContain: + matchTheCondition = !(value as string).includes( + conditionValues as string, + ); + break; + case OperatorTypes.In: + matchTheCondition = (conditionValues as (string | number)[]).includes( + value, + ); + break; + case OperatorTypes.NotIn: + matchTheCondition = !(conditionValues as (string | number)[]).includes( + value, + ); + break; + case OperatorTypes.Between: + const [min, max] = conditionValues as number[]; + matchTheCondition = value >= min && value <= max; + break; + case OperatorTypes.LessThan: + matchTheCondition = value < conditionValues; + break; + case OperatorTypes.GreaterThan: + matchTheCondition = value > conditionValues; + break; + case OperatorTypes.LessThanOrEqual: + matchTheCondition = value <= conditionValues; + break; + case OperatorTypes.GreaterThanOrEqual: + matchTheCondition = value >= conditionValues; + break; + case OperatorTypes.IsNull: + if (typeof value === 'object' && value === null) { + matchTheCondition = true; + } else if (typeof value === 'string' && value === '') { + matchTheCondition = true; + } else if (typeof value === 'undefined') { + matchTheCondition = true; + } else { + matchTheCondition = false; + } + break; + default: + break; + } + return matchTheCondition; +}; + +const getTheSameRange = (list, type) => + list?.filter(({ range }) => range === type); + +const getRowRecord = row => { + if (!row?.length) { + return {}; + } + return row?.[0]?.props?.record || {}; +}; + +const deleteUndefinedProps = props => { + return Object.keys(props).reduce((acc, cur) => { + if (props[cur] !== undefined || props[cur] !== null) { + acc[cur] = props[cur]; + } + return acc; + }, {}); +}; + +export const getCustomBodyCellStyle = ( + props: any, + conditionStyle: ConditionStyleFormValues[], +): CSSProperties => { + const currentConfigs = getTheSameRange(conditionStyle, 'cell'); + if (!currentConfigs?.length) { + return {}; + } + const text = props.cellValue; + let cellStyle: CSSProperties = {}; + + try { + currentConfigs?.forEach( + ({ operator, value, color: { background, textColor: color } }) => { + cellStyle = isMatchedTheCondition(text, operator, value) + ? { backgroundColor: background, color } + : cellStyle; + }, + ); + } catch (error) { + console.error('getCustomBodyCellStyle | error ', error); + } + return deleteUndefinedProps(cellStyle); +}; + +export const getCustomBodyRowStyle = ( + props: any, + conditionStyle: ConditionStyleFormValues[], +): CSSProperties => { + const currentConfigs: ConditionStyleFormValues[] = getTheSameRange( + conditionStyle, + 'row', + ); + if (!currentConfigs?.length) { + return {}; + } + + const rowRecord = getRowRecord(props.children); + let rowStyle: CSSProperties = {}; + + try { + currentConfigs?.forEach( + ({ + operator, + value, + color: { background, textColor }, + target: { name }, + }) => { + rowStyle = isMatchedTheCondition(rowRecord[name], operator, value) + ? { backgroundColor: background, color: textColor } + : rowStyle; + }, + ); + } catch (error) { + console.error('getCustomBodyRowStyle | error ', error); + } + return deleteUndefinedProps(rowStyle); +}; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/config.ts index 175d368ce..e990157fe 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/BasicTableChart/config.ts @@ -55,7 +55,7 @@ const config: ChartConfig = { ], }, { - label: 'column.title', + label: 'column.conditionStyle', key: 'column', comType: 'group', rows: [ @@ -80,6 +80,7 @@ const config: ChartConfig = { .map(c => ({ key: c.uid, value: c.uid, + type: c.type, label: c.label || c.aggregate ? `${c.aggregate}(${c.colName})` @@ -94,76 +95,18 @@ const config: ChartConfig = { comType: 'group', rows: [ { - label: 'column.sortAndFilter', - key: 'sortAndFilter', - comType: 'group', - options: { expand: true }, - rows: [ - { - label: 'column.enableSort', - key: 'enableSort', - comType: 'checkbox', - }, - ], - }, - { - label: 'column.basicStyle', - key: 'basicStyle', + label: 'column.conditionStyle', + key: 'conditionStyle', comType: 'group', options: { expand: true }, rows: [ { - label: 'column.backgroundColor', - key: 'backgroundColor', - comType: 'fontColor', - }, - { - label: 'column.align', - key: 'align', - default: 'left', - comType: 'select', - options: { - items: [ - { label: '左对齐', value: 'left' }, - { label: '居中对齐', value: 'center' }, - { label: '右对齐', value: 'right' }, - ], - }, - }, - { - label: 'column.enableFixedCol', - key: 'enableFixedCol', - comType: 'switch', - rows: [ - { - label: 'column.fixedColWidth', - key: 'fixedColWidth', - default: 100, - comType: 'inputNumber', - }, - ], - }, - { - label: 'font', - key: 'font', - comType: 'font', - default: { - fontFamily: 'PingFang SC', - fontSize: '12', - fontWeight: 'normal', - fontStyle: 'normal', - color: 'black', - }, + label: 'column.conditionStylePanel', + key: 'conditionStylePanel', + comType: 'conditionStylePanel', }, ], }, - { - label: 'column.conditionStyle', - key: 'conditionStyle', - comType: 'group', - options: { expand: true }, - rows: [], - }, ], }, }, @@ -260,18 +203,6 @@ const config: ChartConfig = { }, ], settings: [ - { - label: 'cache.title', - key: 'cache', - comType: 'group', - rows: [ - { - label: 'cache.title', - key: 'panel', - comType: 'cache', - }, - ], - }, { label: 'paging.title', key: 'paging', @@ -320,18 +251,18 @@ const config: ChartConfig = { lang: 'zh-CN', translation: { header: { - title: '表头样式与分组', - open: '打开表头样式与分组', - styleAndGroup: '表头样式与分组', + title: '表头分组', + open: '打开', + styleAndGroup: '表头分组', }, column: { - title: '表格数据列', open: '打开列设置', list: '字段列表', sortAndFilter: '排序与过滤', enableSort: '开启列排序', basicStyle: '基础样式', conditionStyle: '条件样式', + conditionStylePanel: '条件样式配置器', backgroundColor: '背景颜色', align: '对齐方式', enableFixedCol: '开启固定列宽', @@ -351,9 +282,6 @@ const config: ChartConfig = { autoMerge: '自动合并相同内容', enableRaw: '使用原始数据', }, - cache: { - title: '数据处理', - }, paging: { title: '分页设置', enablePaging: '启用分页', @@ -365,9 +293,9 @@ const config: ChartConfig = { lang: 'en-US', translation: { header: { - title: 'Title', - open: 'Open Table Header and Group', - styleAndGroup: 'Style and Group', + title: 'Table Header Group', + open: 'Open', + styleAndGroup: 'Header Group', }, column: { title: 'Table Data Column', @@ -376,7 +304,8 @@ const config: ChartConfig = { sortAndFilter: 'Sort and Filter', enableSort: 'Enable Sort', basicStyle: 'Baisc Style', - conditionStyle: 'Condition Style', + conditionStyle: 'Column Condition Style', + conditionStylePanel: 'Condition Style Panel', backgroundColor: 'Background Color', align: 'Align', enableFixedCol: 'Enable Fixed Column', @@ -396,9 +325,6 @@ const config: ChartConfig = { autoMerge: 'Auto Merge', enableRaw: 'Enable Raw Data', }, - cache: { - title: 'Data Process', - }, paging: { title: 'Paging', enablePaging: 'Enable Paging', diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ChartJSChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ChartJSChart/config.ts index 11c8d3026..50bdcfea2 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ChartJSChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ChartJSChart/config.ts @@ -98,6 +98,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -115,7 +135,6 @@ const config: ChartConfig = { legend: { label: '图例', showLabel: '图例-显示标签', - showLabel2: '图例-显示标签2', }, }, }, @@ -127,6 +146,15 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + fontFamily: 'Font Family', + fontSize: 'Font Size', + fontColor: 'Font Color', + rotateLabel: 'Rotate Label', + showDataColumns: 'Show Data Column', + legend: { + label: 'Legend', + showLabel: 'Show Legend', + }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterBarChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterBarChart/config.ts index 19657173e..6a7e8e2f3 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterBarChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterBarChart/config.ts @@ -436,27 +436,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -531,8 +537,84 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + paging: { + title: '常规', + pageSize: '总行数', + }, + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', + }, + paging: { + title: 'Paging', + pageSize: 'Page Size', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterColumnChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterColumnChart/config.ts index 19657173e..6a7e8e2f3 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterColumnChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ClusterColumnChart/config.ts @@ -436,27 +436,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -531,8 +537,84 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + paging: { + title: '常规', + pageSize: '总行数', + }, + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', + }, + paging: { + title: 'Paging', + pageSize: 'Page Size', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/D3USMapChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/D3USMapChart/config.ts index a34d17741..3e0e53509 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/D3USMapChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/D3USMapChart/config.ts @@ -86,6 +86,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -97,13 +117,10 @@ const config: ChartConfig = { showLabelWithSelect: '显示标签4', fontFamily: '字体', fontSize: '字体大小', - fontColor: '字体颜色', - rotateLabel: '旋转标签', showDataColumns: '选择数据列', legend: { label: '图例', showLabel: '图例-显示标签', - showLabel2: '图例-显示标签2', }, }, }, @@ -115,6 +132,13 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + fontFamily: 'Font Family', + fontSize: 'Font Size', + showDataColumns: 'Show Data Columns', + legend: { + label: 'Legend', + showLabel: 'Show label', + }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/FenZuTableChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/FenZuTableChart.tsx index 7d5d67b95..0c7fdc7da 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/FenZuTableChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/FenZuTableChart.tsx @@ -20,13 +20,17 @@ import { ChartConfig, ChartDataSectionType } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; import { getCustomSortableColumns, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import BasicTableChart from '../BasicTableChart'; import Config from './config'; +/** + * @deprecated Please use @see PivotSheetChart instead + * @class FenZuTableChart + * @extends {BasicTableChart} + */ class FenZuTableChart extends BasicTableChart { - chart: any = null; config = Config; isAutoMerge = true; @@ -39,16 +43,20 @@ class FenZuTableChart extends BasicTableChart { }); } - getOptions(context, dataset?: ChartDataset, config?: ChartConfig) { + getOptions( + context, + dataset?: ChartDataset, + config?: ChartConfig, + widgetSpecialConfig?: any, + ) { if (!dataset || !config) { return { locale: { emptyText: ' ' } }; } - const { clientWidth, clientHeight } = context.document.documentElement; const dataConfigs = config.datas || []; const styleConfigs = config.styles || []; const settingConfigs = config.settings || []; - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -71,14 +79,14 @@ class FenZuTableChart extends BasicTableChart { styleConfigs, dataColumns, ), - components: this.getTableComponents(styleConfigs), + summaryFn: undefined as any, + components: this.getTableComponents(styleConfigs, widgetSpecialConfig), ...this.getAntdTableStyleOptions( styleConfigs, - dataset, - clientWidth, - clientHeight, - tablePagination, + settingConfigs, + context?.height, ), + onChange: () => {}, }; } } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/config.ts index c16d9d278..7d9575526 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/FenZuTableChart/config.ts @@ -45,7 +45,7 @@ const config: ChartConfig = { ], styles: [ { - label: 'column.title', + label: 'column.conditionStyle', key: 'column', comType: 'group', rows: [ @@ -70,6 +70,7 @@ const config: ChartConfig = { .map(c => ({ key: c.uid, value: c.uid, + type: c.type, label: c.label || c.aggregate ? `${c.aggregate}(${c.colName})` @@ -84,63 +85,18 @@ const config: ChartConfig = { comType: 'group', rows: [ { - label: 'column.basicStyle', - key: 'basicStyle', + label: 'column.conditionStyle', + key: 'conditionStyle', comType: 'group', options: { expand: true }, rows: [ { - label: 'column.backgroundColor', - key: 'backgroundColor', - comType: 'fontColor', - }, - { - label: 'column.align', - key: 'align', - default: 'left', - comType: 'select', - options: { - items: [ - { label: '左对齐', value: 'left' }, - { label: '居中对齐', value: 'center' }, - { label: '右对齐', value: 'right' }, - ], - }, - }, - { - label: 'column.enableFixedCol', - key: 'enableFixedCol', - comType: 'switch', - rows: [ - { - label: 'column.fixedColWidth', - key: 'fixedColWidth', - default: 100, - comType: 'inputNumber', - }, - ], - }, - { - label: 'font', - key: 'font', - comType: 'font', - default: { - fontFamily: 'PingFang SC', - fontSize: '12', - fontWeight: 'normal', - fontStyle: 'normal', - color: 'black', - }, + label: 'column.conditionStylePanel', + key: 'conditionStylePanel', + comType: 'conditionStylePanel', }, ], }, - { - label: 'column.conditionStyle', - key: 'conditionStyle', - comType: 'group', - options: { expand: true }, - rows: [], - }, ], }, }, @@ -238,14 +194,20 @@ const config: ChartConfig = { ], settings: [ { - label: 'cache.title', - key: 'cache', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'cache.title', - key: 'panel', - comType: 'cache', + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, @@ -267,6 +229,7 @@ const config: ChartConfig = { enableSort: '开启列排序', basicStyle: '基础样式', conditionStyle: '条件样式', + conditionStylePanel: '条件样式配置器', backgroundColor: '背景颜色', align: '对齐方式', enableFixedCol: '开启固定列宽', @@ -286,13 +249,9 @@ const config: ChartConfig = { autoMerge: '自动合并相同内容', enableRaw: '使用原始数据', }, - cache: { - title: '数据处理', - }, paging: { - title: '分页设置', - enablePaging: '启用分页', - pageSize: '分页大小', + title: '常规', + pageSize: '总行数', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/MingXiTableChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/MingXiTableChart.tsx index 122abe902..ba0ad3ec9 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/MingXiTableChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/MingXiTableChart.tsx @@ -17,14 +17,15 @@ */ import BasicTableChart from '../BasicTableChart'; +import Config from './config'; class MingXiTableChart extends BasicTableChart { - chart: any = null; + config = Config; constructor() { super({ id: 'mingxi-table', - name: '明细表', + name: '表格', icon: 'mingxibiao', }); } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/config.ts new file mode 100644 index 000000000..9a74a849c --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/MingXiTableChart/config.ts @@ -0,0 +1,464 @@ +/** + * 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 { ChartConfig } from 'app/types/ChartConfig'; + +const config: ChartConfig = { + datas: [ + { + label: 'mixed', + key: 'mixed', + required: true, + type: 'mixed', + }, + { + label: 'filter', + key: 'filter', + type: 'filter', + disableAggregate: true, + }, + ], + styles: [ + { + label: 'header.title', + key: 'header', + comType: 'group', + rows: [ + { + label: 'header.open', + key: 'modal', + comType: 'group', + options: { type: 'modal', modalSize: 'middle' }, + rows: [ + { + label: 'header.styleAndGroup', + key: 'tableHeaders', + comType: 'tableHeader', + }, + ], + }, + ], + }, + { + label: 'column.conditionStyle', + key: 'column', + comType: 'group', + rows: [ + { + label: 'column.open', + key: 'modal', + comType: 'group', + options: { type: 'modal', modalSize: 'middle' }, + rows: [ + { + label: 'column.list', + key: 'list', + comType: 'listTemplate', + rows: [], + options: { + getItems: cols => { + const columns = (cols || []) + .filter(col => + ['aggregate', 'group', 'mixed'].includes(col.type), + ) + .reduce((acc, cur) => acc.concat(cur.rows || []), []) + .map(c => ({ + key: c.uid, + value: c.uid, + type: c.type, + label: + c.label || c.aggregate + ? `${c.aggregate}(${c.colName})` + : c.colName, + })); + return columns; + }, + }, + template: { + label: 'column.listItem', + key: 'listItem', + comType: 'group', + rows: [ + { + label: 'column.conditionStyle', + key: 'conditionStyle', + comType: 'group', + options: { expand: true }, + rows: [ + { + label: 'column.conditionStylePanel', + key: 'conditionStylePanel', + comType: 'conditionStylePanel', + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + label: 'style.title', + key: 'style', + comType: 'group', + rows: [ + { + label: 'style.enableFixedHeader', + key: 'enableFixedHeader', + default: true, + comType: 'checkbox', + }, + { + label: 'style.enableBorder', + key: 'enableBorder', + default: true, + comType: 'checkbox', + }, + { + label: 'style.enableRowNumber', + key: 'enableRowNumber', + default: false, + comType: 'checkbox', + }, + { + label: 'style.leftFixedColumns', + key: 'leftFixedColumns', + comType: 'select', + options: { + mode: 'multiple', + getItems: cols => { + const columns = (cols || []) + .filter(col => + ['aggregate', 'group', 'mixed'].includes(col.type), + ) + .reduce((acc, cur) => acc.concat(cur.rows || []), []) + .map(c => ({ + key: c.uid, + value: c.uid, + label: + c.label || c.aggregate + ? `${c.aggregate}(${c.colName})` + : c.colName, + })); + return columns; + }, + }, + }, + { + label: 'style.rightFixedColumns', + key: 'rightFixedColumns', + comType: 'select', + options: { + mode: 'multiple', + getItems: cols => { + const columns = (cols || []) + .filter(col => + ['aggregate', 'group', 'mixed'].includes(col.type), + ) + .reduce((acc, cur) => acc.concat(cur.rows || []), []) + .map(c => ({ + key: c.uid, + value: c.uid, + label: + c.label || c.aggregate + ? `${c.aggregate}(${c.colName})` + : c.colName, + })); + return columns; + }, + }, + }, + { + label: 'style.autoMergeFields', + key: 'autoMergeFields', + comType: 'select', + options: { + mode: 'multiple', + getItems: cols => { + const columns = (cols || []) + .filter(col => ['mixed'].includes(col.type)) + .reduce((acc, cur) => acc.concat(cur.rows || []), []) + .filter(c => c.type === 'STRING') + .map(c => ({ + key: c.uid, + value: c.uid, + label: + c.label || c.aggregate + ? `${c.aggregate}(${c.colName})` + : c.colName, + })); + return columns; + }, + }, + }, + { + label: 'style.tableSize', + key: 'tableSize', + default: 'small', + comType: 'select', + options: { + items: [ + { label: '默认', value: 'default' }, + { label: '中', value: 'middle' }, + { label: '小', value: 'small' }, + ], + }, + }, + ], + }, + { + label: 'style.tableHeaderStyle', + key: 'tableHeaderStyle', + comType: 'group', + rows: [ + { + label: 'style.bgColor', + key: 'bgColor', + default: '#f8f9fa', + comType: 'fontColor', + }, + { + label: 'style.font', + key: 'font', + comType: 'font', + default: { + fontFamily: 'PingFang SC', + fontSize: 12, + fontWeight: 'bold', + fontStyle: 'normal', + color: '#495057', + }, + }, + { + label: 'style.align', + key: 'align', + default: 'left', + comType: 'select', + options: { + items: [ + { label: '左对齐', value: 'left' }, + { label: '居中对齐', value: 'center' }, + { label: '右对齐', value: 'right' }, + ], + }, + }, + ], + }, + { + label: 'style.tableBodyStyle', + key: 'tableBodyStyle', + comType: 'group', + rows: [ + { + label: 'style.bgColor', + key: 'bgColor', + default: 'rgba(0,0,0,0)', + comType: 'fontColor', + }, + { + label: 'style.font', + key: 'font', + comType: 'font', + default: { + fontFamily: 'PingFang SC', + fontSize: 12, + fontWeight: 'normal', + fontStyle: 'normal', + color: '#495057', + }, + }, + { + label: 'style.align', + key: 'align', + default: 'left', + comType: 'fontAlignment', + }, + ], + }, + ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.enablePaging', + key: 'enablePaging', + default: true, + comType: 'checkbox', + options: { + needRefresh: true, + }, + }, + + { + label: 'paging.pageSize', + key: 'pageSize', + default: 100, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + watcher: { + deps: ['enablePaging'], + action: props => { + return { + disabled: !props.enablePaging, + }; + }, + }, + }, + ], + }, + { + label: 'summary.title', + key: 'summary', + comType: 'group', + rows: [ + { + label: 'summary.aggregateFields', + key: 'aggregateFields', + comType: 'select', + options: { + mode: 'multiple', + getItems: cols => { + const columns = (cols || []) + .filter(col => ['mixed'].includes(col.type)) + .reduce((acc, cur) => acc.concat(cur.rows || []), []) + .filter(c => c.type === 'NUMERIC') + .map(c => ({ + key: c.uid, + value: c.uid, + label: + c.label || c.aggregate + ? `${c.aggregate}(${c.colName})` + : c.colName, + })); + return columns; + }, + }, + }, + ], + }, + ], + i18ns: [ + { + lang: 'zh-CN', + translation: { + header: { + title: '表头分组', + open: '打开', + styleAndGroup: '表头分组', + }, + column: { + open: '打开样式设置', + list: '字段列表', + sortAndFilter: '排序与过滤', + enableSort: '开启列排序', + basicStyle: '基础样式', + conditionStyle: '条件样式', + conditionStylePanel: '条件样式配置器', + backgroundColor: '背景颜色', + align: '对齐方式', + enableFixedCol: '开启固定列宽', + fixedColWidth: '固定列宽度设置', + font: '字体与样式', + }, + style: { + title: '表格样式', + enableFixedHeader: '固定表头', + enableBorder: '显示边框', + enableRowNumber: '启用行号', + leftFixedColumns: '左侧固定列', + rightFixedColumns: '右侧固定列', + autoMergeFields: '自动合并列内容', + tableSize: '表格大小', + tableHeaderStyle: '表头样式', + tableBodyStyle: '表体样式', + bgColor: '背景颜色', + font: '字体', + align: '对齐方式', + }, + summary: { + title: '数据汇总', + aggregateFields: '汇总列', + }, + paging: { + title: '常规', + enablePaging: '启用分页', + pageSize: '分页大小', + }, + }, + }, + { + lang: 'en-US', + translation: { + header: { + title: 'Table Header Group', + open: 'Open', + styleAndGroup: 'Header Group', + }, + column: { + open: 'Open Style Setting', + list: 'Field List', + sortAndFilter: 'Sort and Filter', + enableSort: 'Enable Sort', + basicStyle: 'Baisc Style', + conditionStyle: 'Condition Style', + conditionStylePanel: 'Condition Style Panel', + backgroundColor: 'Background Color', + align: 'Align', + enableFixedCol: 'Enable Fixed Column', + fixedColWidth: 'Fixed Column Width', + font: 'Font and Style', + }, + style: { + title: 'Table Style', + enableFixedHeader: 'Enable Fixed Header', + enableBorder: 'Show Border', + enableRowNumber: 'Enable Row Number', + leftFixedColumns: 'Left Fixed Columns', + rightFixedColumns: 'Right Fixed Columns', + autoMergeFields: 'Auto Merge Column Content', + tableSize: 'Table Size', + tableHeaderStyle: 'Table Header Style', + tableBodyStyle: 'Table Body Style', + bgColor: 'Background Color', + font: 'Font', + align: 'Align', + }, + summary: { + title: 'Summary', + aggregateFields: 'Summary Fields', + }, + paging: { + title: 'Paging', + enablePaging: 'Enable Paging', + pageSize: 'Page Size', + }, + }, + }, + ], +}; + +export default config; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/NormalOutlineMapChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/NormalOutlineMapChart/config.ts index 4b47a11ef..31171589f 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/NormalOutlineMapChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/NormalOutlineMapChart/config.ts @@ -217,6 +217,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -261,6 +281,49 @@ const config: ChartConfig = { background: { title: '背景设置' }, }, }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + metricsAndColor: 'Metrics and Color', + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + map: { + title: 'Map', + level: 'Level', + enableZoom: 'Enabel Zoom', + backgroundColor: 'Background Color', + borderStyle: 'Border Style', + focusArea: 'Focus Area', + areaColor: 'Area Color', + areaEmphasisColor: 'Area Emphasis Color', + }, + background: { title: 'Background' }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackBarChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackBarChart/config.ts index 6bf100a04..92aeadfd9 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackBarChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackBarChart/config.ts @@ -430,27 +430,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -525,8 +531,76 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackColumnChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackColumnChart/config.ts index 6bf100a04..92aeadfd9 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackColumnChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PercentageStackColumnChart/config.ts @@ -430,27 +430,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -525,8 +531,76 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/AntVS2Wrapper.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/AntVS2Wrapper.tsx new file mode 100644 index 000000000..8f1e22a27 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/AntVS2Wrapper.tsx @@ -0,0 +1,88 @@ +/** + * 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 { S2Theme } from '@antv/s2'; +import { SheetComponent } from '@antv/s2-react'; +import '@antv/s2-react/dist/style.min.css'; +import { FC, memo } from 'react'; +import styled from 'styled-components/macro'; +import { FONT_SIZE_LABEL } from 'styles/StyleConstants'; + +const AntVS2Wrapper: FC<{ + dataCfg; + options; + theme?: S2Theme; +}> = memo(({ dataCfg, options, theme }) => { + const onDataCellHover = ({ event, viewMeta }) => { + viewMeta.spreadsheet.tooltip.show({ + position: { + x: event.clientX, + y: event.clientY, + }, + content: ( + + ), + }); + }; + + return ( + + ); +}); + +const TableDataCellTooltip: FC<{ + datas?: object; + meta?: Array<{ field: string; name: string; formatter }>; +}> = ({ datas, meta }) => { + if (!datas) { + return null; + } + + return ( + + {(meta || []) + .map(m => { + const uniqKey = m?.field; + if (uniqKey in datas) { + return {`${m?.name}: ${m?.formatter(datas[uniqKey])}`}; + } + return null; + }) + .filter(Boolean)} + + ); +}; + +const StyledTableDataCellTooltip = styled.ul` + padding: 4px; + font-size: ${FONT_SIZE_LABEL}; + color: ${p => p.theme.textColorLight}; +`; + +const StyledAntVS2Wrapper = styled(SheetComponent)``; + +export default AntVS2Wrapper; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/PivotSheetChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/PivotSheetChart.tsx new file mode 100644 index 000000000..6963d36b6 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/PivotSheetChart.tsx @@ -0,0 +1,384 @@ +/** + * 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 { + ChartConfig, + ChartDataSectionType, + SortActionType, +} from 'app/types/ChartConfig'; +import ChartDataset from 'app/types/ChartDataset'; +import { + getColumnRenderName, + getCustomSortableColumns, + getValueByColumnKey, + transformToObjectArray, +} from 'app/utils/chartHelper'; +import { isNumber, toFormattedValue } from 'app/utils/number'; +import groupBy from 'lodash/groupBy'; +import ReactChart from '../ReactChart'; +import AntVS2Wrapper from './AntVS2Wrapper'; +import Config from './config'; +class PivotSheetChart extends ReactChart { + static icon = ``; + + _useIFrame = false; + isISOContainer = 'piovt-sheet'; + config = Config; + chart: any = null; + updateOptions: any = {}; + + constructor() { + super(AntVS2Wrapper, { + id: 'piovt-sheet', + name: '透视表', + icon: PivotSheetChart.icon, + }); + this.meta.requirements = [{}]; + } + + onUpdated(options, context): void { + if (!this.isMatchRequirement(options.config)) { + this.adapter?.unmount(); + return; + } + + this.updateOptions = this.getOptions( + context, + options.dataset, + options.config, + ); + this.adapter?.updated(this.updateOptions); + } + + onResize(_, context) { + if (this.updateOptions?.options) { + this.updateOptions.options = Object.assign( + { + ...this.updateOptions.options, + }, + { width: context.width, height: context.height }, + ); + this.adapter?.updated(this.updateOptions); + } + } + + getOptions(context, dataset?: ChartDataset, config?: ChartConfig) { + if (!dataset || !config) { + return {}; + } + + const dataConfigs = config.datas || []; + const styleConfigs = config.styles || []; + const settingConfigs = config.settings || []; + const objDataColumns = transformToObjectArray( + dataset.rows, + dataset.columns, + ); + const dataColumns = getCustomSortableColumns(objDataColumns, dataConfigs); + + const rowSectionConfigRows = dataConfigs + .filter(c => c.type === ChartDataSectionType.GROUP) + .filter(c => c.key === 'row') + .flatMap(config => config.rows || []); + + const columnSectionConfigRows = dataConfigs + .filter(c => c.type === ChartDataSectionType.GROUP) + .filter(c => c.key === 'column') + .flatMap(config => config.rows || []); + + const metricsSectionConfigRows = dataConfigs + .filter(c => c.type === ChartDataSectionType.AGGREGATE) + .flatMap(config => config.rows || []); + + const infoSectionConfigRows = dataConfigs + .filter(c => c.type === ChartDataSectionType.INFO) + .flatMap(config => config.rows || []); + + const enableExpandRow = this.getStyleValue(styleConfigs, [ + 'style', + 'enableExpandRow', + ]); + const enableHoverHighlight = this.getStyleValue(styleConfigs, [ + 'style', + 'enableHoverHighlight', + ]); + const enableSelectedHighlight = this.getStyleValue(styleConfigs, [ + 'style', + 'enableSelectedHighlight', + ]); + const metricNameShowIn = this.getStyleValue(styleConfigs, [ + 'style', + 'metricNameShowIn', + ]); + const enableTotal = this.getStyleValue(settingConfigs, [ + 'rowSummary', + 'enableTotal', + ]); + const totalPosition = this.getStyleValue(settingConfigs, [ + 'rowSummary', + 'totalPosition', + ]); + const enableSubTotal = this.getStyleValue(settingConfigs, [ + 'rowSummary', + 'enableSubTotal', + ]); + const subTotalPosition = this.getStyleValue(settingConfigs, [ + 'rowSummary', + 'subTotalPosition', + ]); + + return { + options: { + hierarchyType: enableExpandRow ? 'tree' : 'grid', + width: context?.width, + height: context?.height, + tooltip: { + showTooltip: true, + }, + interaction: { + hoverHighlight: Boolean(enableHoverHighlight), + selectedCellsSpotlight: Boolean(enableSelectedHighlight), + }, + totals: { + row: { + showGrandTotals: Boolean(enableTotal), + reverseLayout: Boolean(totalPosition), + showSubTotals: Boolean(enableSubTotal), + reverseSubLayout: Boolean(subTotalPosition), + subTotalsDimensions: + rowSectionConfigRows.map(getValueByColumnKey)?.[0], + }, + }, + }, + dataCfg: { + fields: { + rows: rowSectionConfigRows.map(getValueByColumnKey), + columns: columnSectionConfigRows.map(getValueByColumnKey), + values: metricsSectionConfigRows.map(getValueByColumnKey), + valueInCols: !!metricNameShowIn, + }, + meta: rowSectionConfigRows + .concat(columnSectionConfigRows) + .concat(metricsSectionConfigRows) + .concat(infoSectionConfigRows) + .map(config => { + return { + field: getValueByColumnKey(config), + name: getColumnRenderName(config), + formatter: value => toFormattedValue(value, config?.format), + }; + }), + data: dataColumns, + totalData: this.getCalcSummaryValues( + dataColumns, + rowSectionConfigRows, + columnSectionConfigRows, + metricsSectionConfigRows, + enableTotal, + enableSubTotal, + ), + sortParams: this.getTableSorters( + rowSectionConfigRows + .concat(columnSectionConfigRows) + .concat(metricsSectionConfigRows), + ), + }, + theme: { + /* + DATA_CELL = "dataCell", + HEADER_CELL = "headerCell", + ROW_CELL = "rowCell", + COL_CELL = "colCell", + CORNER_CELL = "cornerCell", + MERGED_CELL = "mergedCell" + */ + cornerCell: this.getHeaderStyle(styleConfigs), + colCell: this.getHeaderStyle(styleConfigs), + rowCell: this.getHeaderStyle(styleConfigs), + dataCell: this.getBodyStyle(styleConfigs), + }, + }; + } + + private getTableSorters(sectionConfigRows) { + return sectionConfigRows + .map(config => { + if (!config?.sort?.type || config?.sort?.type === SortActionType.NONE) { + return null; + } + const isASC = config.sort.type === SortActionType.ASC; + return { + sortFieldId: getValueByColumnKey(config), + sortFunc: params => { + const { data } = params; + return data?.sort((a, b) => + isASC ? a?.localeCompare(b) : b?.localeCompare(a), + ); + }, + }; + }) + .filter(Boolean); + } + + private getBodyStyle(styleConfigs) { + const bodyFont = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'font', + ]); + const oddBgColor = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'oddBgColor', + ]); + const evenBgColor = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'evenBgColor', + ]); + const bodyTextAlign = this.getStyleValue(styleConfigs, [ + 'tableBodyStyle', + 'align', + ]); + + return { + cell: { + crossBackgroundColor: evenBgColor, + backgroundColor: oddBgColor, + }, + text: { + fill: bodyFont?.color, + fontFamily: bodyFont?.fontFamily, + fontSize: bodyFont?.fontSize, + fontWeight: bodyFont?.fontWeight, + textAlign: bodyTextAlign, + }, + }; + } + + private getHeaderStyle(styleConfigs) { + const headerFont = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'font', + ]); + const headerBgColor = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'bgColor', + ]); + const headerTextAlign = this.getStyleValue(styleConfigs, [ + 'tableHeaderStyle', + 'align', + ]); + + return { + cell: { + backgroundColor: headerBgColor, + }, + text: { + fill: headerFont?.color, + fontFamily: headerFont?.fontFamily, + fontSize: headerFont?.fontSize, + fontWeight: headerFont?.fontWeight, + textAlign: headerTextAlign, + }, + bolderText: { + fill: headerFont?.color, + fontFamily: headerFont?.fontFamily, + fontSize: headerFont?.fontSize, + fontWeight: headerFont?.fontWeight, + textAlign: headerTextAlign, + }, + }; + } + + private getCalcSummaryValues( + dataColumns, + rowSectionConfigRows, + columnSectionConfigRows, + metricsSectionConfigRows, + enableTotal, + enableSubTotal, + ) { + let summarys: any[] = []; + if (enableTotal) { + if (!columnSectionConfigRows.length) { + const rowTotals = metricsSectionConfigRows.map(c => { + const values = dataColumns + .map(dc => +dc?.[getValueByColumnKey(c)]) + .filter(isNumber); + return { + [getValueByColumnKey(c)]: values?.reduce((a, b) => a + b, 0), + }; + }); + summarys.push(...rowTotals); + } else { + const rowTotals = this.calculateGroupedColumnTotal( + {}, + columnSectionConfigRows.map(getValueByColumnKey), + metricsSectionConfigRows, + dataColumns, + ); + summarys.push(...rowTotals); + } + } + if (enableSubTotal) { + const rowTotals = this.calculateGroupedColumnTotal( + {}, + [rowSectionConfigRows[0]] + .concat(columnSectionConfigRows) + .map(getValueByColumnKey), + metricsSectionConfigRows, + dataColumns, + ); + summarys.push(...rowTotals); + } + + return summarys; + } + + private calculateGroupedColumnTotal( + preObj, + groupKeys, + metrics: any[], + datas, + ) { + const _groupKeys = [...(groupKeys || [])]; + const groupKey = _groupKeys.shift(); + const groupDataSet = groupBy(datas, groupKey); + + return Object.entries(groupDataSet).flatMap(([k, v]) => { + if (_groupKeys.length) { + return this.calculateGroupedColumnTotal( + Object.assign({}, preObj, { [groupKey]: k }), + _groupKeys, + metrics, + v, + ); + } + return metrics.map(metric => { + const values = (v as any[]) + .map(dc => +dc?.[getValueByColumnKey(metric)]) + .filter(isNumber); + return { + ...preObj, + [groupKey]: k, + [getValueByColumnKey(metric)]: values?.reduce((a, b) => a + b, 0), + }; + }); + }); + } +} + +export default PivotSheetChart; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/config.ts new file mode 100644 index 000000000..b28338fec --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/config.ts @@ -0,0 +1,397 @@ +/** + * 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 { ChartConfig } from 'app/types/ChartConfig'; + +const config: ChartConfig = { + datas: [ + { + label: 'datas.column', + key: 'column', + type: 'group', + options: { + sortable: { backendSort: false }, + }, + }, + { + label: 'datas.row', + key: 'row', + type: 'group', + options: { + sortable: { backendSort: false }, + }, + }, + { + label: 'metrics', + key: 'metrics', + type: 'aggregate', + actions: { + NUMERIC: ['aggregate', 'alias', 'format', 'sortable'], + STRING: ['aggregate', 'alias', 'format', 'sortable'], + }, + options: { + sortable: { backendSort: false }, + }, + }, + { + label: 'filter', + key: 'filter', + type: 'filter', + }, + { + label: 'info', + key: 'info', + type: 'info', + }, + ], + styles: [ + // { + // label: 'column.title', + // key: 'column', + // comType: 'group', + // rows: [ + // { + // label: 'column.open', + // key: 'modal', + // comType: 'group', + // options: { type: 'modal', modalSize: 'middle' }, + // rows: [ + // { + // label: 'column.list', + // key: 'list', + // comType: 'listTemplate', + // rows: [], + // options: { + // getItems: cols => { + // const columns = (cols || []) + // .filter(col => + // ['aggregate', 'group', 'mixed'].includes(col.type), + // ) + // .reduce((acc, cur) => acc.concat(cur.rows || []), []) + // .map(c => ({ + // key: c.uid, + // value: c.uid, + // label: + // c.label || c.aggregate + // ? `${c.aggregate}(${c.colName})` + // : c.colName, + // })); + // return columns; + // }, + // }, + // template: { + // label: 'column.listItem', + // key: 'listItem', + // comType: 'group', + // rows: [ + // { + // label: 'column.conditionStyle', + // key: 'conditionStyle', + // comType: 'group', + // options: { expand: true }, + // rows: [], + // }, + // ], + // }, + // }, + // ], + // }, + // ], + // }, + { + label: 'style.title', + key: 'style', + comType: 'group', + rows: [ + { + label: 'style.enableExpandRow', + key: 'enableExpandRow', + default: false, + comType: 'checkbox', + }, + { + label: 'style.enableHoverHighlight', + key: 'enableHoverHighlight', + default: true, + comType: 'checkbox', + }, + { + label: 'style.enableSelectedHighlight', + key: 'enableSelectedHighlight', + default: false, + comType: 'checkbox', + }, + { + label: 'style.metricNameShowIn.label', + key: 'metricNameShowIn', + default: true, + comType: 'radio', + options: { + translateItemLabel: true, + items: [ + { + key: 'inCol', + label: 'style.metricNameShowIn.inCol', + value: true, + }, + { + key: 'inRow', + label: 'style.metricNameShowIn.inRow', + value: false, + }, + ], + }, + }, + ], + }, + { + label: 'style.tableHeaderStyle', + key: 'tableHeaderStyle', + comType: 'group', + rows: [ + { + label: 'style.bgColor', + key: 'bgColor', + comType: 'fontColor', + }, + { + label: 'style.font', + key: 'font', + comType: 'font', + default: { + fontFamily: 'PingFang SC', + fontSize: 12, + fontWeight: 'normal', + color: '#495057', + }, + options: { + fontFamilies: [ + 'Roboto', + 'PingFangSC', + 'BlinkMacSystemFont', + 'Microsoft YaHei', + 'Arial', + 'sans-serif', + ], + }, + }, + { + label: 'style.align', + key: 'align', + default: 'right', + comType: 'fontAlignment', + }, + ], + }, + { + label: 'style.tableBodyStyle', + key: 'tableBodyStyle', + comType: 'group', + rows: [ + { + label: 'style.oddBgColor', + key: 'oddBgColor', + comType: 'fontColor', + }, + { + label: 'style.evenBgColor', + key: 'evenBgColor', + comType: 'fontColor', + }, + { + label: 'style.font', + key: 'font', + comType: 'font', + default: { + fontFamily: 'PingFang SC', + fontSize: 12, + fontWeight: 'normal', + color: '#495057', + }, + options: { + fontFamilies: [ + 'Roboto', + 'PingFangSC', + 'BlinkMacSystemFont', + 'Microsoft YaHei', + 'Arial', + 'sans-serif', + ], + }, + }, + { + label: 'style.align', + key: 'align', + default: 'left', + comType: 'fontAlignment', + }, + ], + }, + ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + { + label: 'summary.rowSummary', + key: 'rowSummary', + comType: 'group', + rows: [ + { + label: 'summary.enableTotal', + key: 'enableTotal', + default: false, + comType: 'checkbox', + }, + { + label: 'summary.totalPosition', + key: 'totalPosition', + default: true, + comType: 'select', + options: { + items: [ + { label: '顶部', value: true }, + { label: '底部', value: false }, + ], + }, + }, + { + label: 'summary.enableSubTotal', + key: 'enableSubTotal', + default: false, + comType: 'checkbox', + }, + { + label: 'summary.subTotalPosition', + key: 'subTotalPosition', + default: true, + comType: 'select', + options: { + items: [ + { label: '顶部', value: true }, + { label: '底部', value: false }, + ], + }, + }, + ], + }, + ], + i18ns: [ + { + lang: 'zh-CN', + translation: { + datas: { + row: '行', + column: '列', + }, + style: { + title: '表格样式', + enableExpandRow: '行表头折叠', + enableHoverHighlight: '启用联动高亮', + enableSelectedHighlight: '启用选中高亮', + enableAdjustRowHeight: '启用调整行高', + enableAdjustColumnWidth: '启用调整列宽', + metricNameShowIn: { + label: '指标名称位置', + inCol: '列表头', + inRow: '行表头', + }, + tableSize: '表格大小', + tableHeaderStyle: '表头样式', + tableBodyStyle: '表体样式', + bgColor: '背景颜色', + evenBgColor: '偶数行背景颜色', + oddBgColor: '奇数行背景颜色', + font: '字体', + align: '对齐方式', + }, + summary: { + title: '数据汇总', + rowSummary: '行总计', + columnSummary: '列总计', + enableTotal: '启用总计', + enableSubTotal: '启用小计', + totalPosition: '总计位置', + subTotalPosition: '小计位置', + aggregateFields: '汇总列', + }, + }, + }, + { + lang: 'en-US', + translation: { + datas: { + row: 'Row', + column: 'Column', + }, + style: { + title: 'Table Style', + enableExpandRow: 'Fold Row', + enableHoverHighlight: 'Enable Hover Highlight', + enableSelectedHighlight: 'Enable Selected Highlight', + enableAdjustRowHeight: 'Enable Adjust Row Height', + enableAdjustColumnWidth: 'Enable Adjust Column Width', + metricNameShowIn: { + label: 'Metric Name Position', + inCol: 'Col Header', + inRow: 'Row Header', + }, + tableSize: 'Table Size', + tableHeaderStyle: 'Table Header Style', + tableBodyStyle: 'Table Body Style', + bgColor: 'Background Color', + evenBgColor: 'Even Row Background Color', + oddBgColor: 'Odd Row Background Color', + font: 'Font', + align: 'Align', + }, + summary: { + title: 'Summary', + rowSummary: 'Row Total', + columnSummary: 'Column Total', + enableTotal: 'Enable Total', + enableSubTotal: 'Enable Sub Total', + totalPosition: 'Total Position', + subTotalPosition: 'Sub Total Position', + aggregateFields: 'Summary Fields', + }, + paging: { + title: 'Paging', + pageSize: 'Page Size', + }, + }, + }, + ], +}; + +export default config; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/icon.svg b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/icon.svg new file mode 100644 index 000000000..3a104222c --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/index.ts new file mode 100644 index 000000000..60675f656 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/PivotSheetChart/index.ts @@ -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 PivotSheetChart from './PivotSheetChart'; + +export default PivotSheetChart; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/ReChartsChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/ReChartsChart.tsx index fac68f71e..0b6fb5f72 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/ReChartsChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/ReChartsChart.tsx @@ -21,10 +21,6 @@ import Config from './config'; import ReChartPie from './ReChartPie'; class ReChartsChart extends ReactChart { - constructor() { - super('rechart-chart', 'React ReChart Chart', 'preview'); - } - isISOContainer = 'react-rechart-chart'; config = Config; dependency = [ @@ -34,20 +30,21 @@ class ReChartsChart extends ReactChart { 'https://unpkg.com/recharts@2.0.8/umd/Recharts.min.js', ]; + constructor() { + super(ReChartPie, { + id: 'rechart-chart', + name: 'React ReChart Chart', + icon: 'preview', + }); + } + onMount(options, context): void { const { Surface, Pie } = context.window.Recharts; - this.getInstance().init(ReChartPie); - this.getInstance().registerImportDependenies({ Surface, Pie }); - this.getInstance().mounted( - context.document.getElementById(options.containerId), - ); + this.adapter.registerImportDependenies({ Surface, Pie }); + this.adapter.mounted(context.document.getElementById(options.containerId)); } onUpdated({ config }: { config: any }): void {} - - onUnMount(): void { - // this.getWrapper().unmount(); - } } export default ReChartsChart; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/config.ts index 11c8d3026..1257e9135 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReChartsChart/config.ts @@ -98,6 +98,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -127,6 +147,15 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + fontFamily: 'Font Family', + fontSize: 'Font Size', + fontColor: 'Font Color', + rotateLabel: 'Rotate Label', + showDataColumns: 'Show Data Columns', + legend: { + label: 'Legend', + showLabel: 'Show Label', + }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactChart.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactChart.ts index 010336ef3..62213bab3 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactChart.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactChart.ts @@ -17,16 +17,41 @@ */ import Chart from '../../../../models/Chart'; -import ReactChartAdapter from '../ChartTools/ReactChartAdapter'; +import ReactLifecycleAdapter from '../ChartTools/ReactLifecycleAdapter'; export default class ReactChart extends Chart { - adapter = new ReactChartAdapter(); + private _adapter; - init(component) { - this.adapter.init(component); + constructor(wrapper, props) { + super( + props?.id || 'react-table', + props?.name || '表格', + props?.icon || 'table', + ); + this._adapter = new ReactLifecycleAdapter(wrapper); } - getInstance() { - return this.adapter; + get adapter() { + if (!this._adapter) { + throw new Error( + 'should be register component by initAdapter before in used', + ); + } + return this._adapter; + } + + public onMount(options, context?): void { + if (options.containerId === undefined || !context.document) { + return; + } + this.adapter?.mounted( + context.document.getElementById(options.containerId), + options, + context, + ); + } + + public onUnMount(options, context?): void { + this.adapter?.unmount(); } } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/ReactVizXYPlotChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/ReactVizXYPlotChart.tsx index f833e6d71..36c018b52 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/ReactVizXYPlotChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/ReactVizXYPlotChart.tsx @@ -21,10 +21,6 @@ import Config from './config'; import ReactXYPlot from './ReactVizXYPlot'; class ReactVizXYPlotChart extends ReactChart { - constructor() { - super('reactviz-xyplot-chart', 'ReactViz XYPlot Chart', 'star'); - } - isISOContainer = 'reactviz-container'; config = Config; dependency = [ @@ -32,30 +28,34 @@ class ReactVizXYPlotChart extends ReactChart { 'https://unpkg.com/react-vis/dist/dist.min.js', ]; + constructor() { + super(ReactXYPlot, { + id: 'reactviz-xyplot-chart', + name: 'ReactViz XYPlot Chart', + icon: 'star', + }); + } + onMount(options, context): void { if (!context.window.reactVis) { return; } const { XYPlot, XAxis, YAxis, HorizontalGridLines, LineSeries } = context.window.reactVis; - this.getInstance().init(ReactXYPlot); - this.getInstance().registerImportDependenies({ + this.adapter.init(ReactXYPlot); + this.adapter.registerImportDependenies({ XYPlot, XAxis, YAxis, HorizontalGridLines, LineSeries, }); - this.getInstance().mounted( - context.document.getElementById(options.containerId), - ); + this.adapter.mounted(context.document.getElementById(options.containerId)); } onUpdated(props): void { - // this.getWrapper().updated(props); + // this.adapter.updated(props); } - - onUnMount(): void {} } export default ReactVizXYPlotChart; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/config.ts index 9571bdb37..0087e925f 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ReactVizXYPlotChart/config.ts @@ -99,6 +99,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -116,7 +136,6 @@ const config: ChartConfig = { legend: { label: '图例', showLabel: '图例-显示标签', - showLabel2: '图例-显示标签2', }, }, }, @@ -128,6 +147,15 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + fontFamily: 'Font Family', + fontSize: 'Font Size', + fontColor: 'Font Color', + rotateLabel: 'Rotate Label', + showDataColumns: 'Show Data Columns', + legend: { + label: 'Legend', + showLabel: 'Show Label', + }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/RephaelPaperChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/RephaelPaperChart/config.ts index 11c8d3026..1257e9135 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/RephaelPaperChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/RephaelPaperChart/config.ts @@ -98,6 +98,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -127,6 +147,15 @@ const config: ChartConfig = { showLabelBySwitch: 'Show Lable Switch', showLabelWithInput: 'Show Label Input', showLabelWithSelect: 'Show Label Select', + fontFamily: 'Font Family', + fontSize: 'Font Size', + fontColor: 'Font Color', + rotateLabel: 'Rotate Label', + showDataColumns: 'Show Data Columns', + legend: { + label: 'Legend', + showLabel: 'Show Label', + }, }, }, ], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScatterOutlineMapChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScatterOutlineMapChart/config.ts index 56bc4bd93..ae8c592b1 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScatterOutlineMapChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScatterOutlineMapChart/config.ts @@ -228,6 +228,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -273,6 +293,49 @@ const config: ChartConfig = { background: { title: '背景设置' }, }, }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + metricsAndColor: 'Metrics and Color', + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + map: { + title: 'Map', + level: 'Level', + enableZoom: 'Enabel Zoom', + backgroundColor: 'Background Color', + borderStyle: 'Border Style', + focusArea: 'Focus Area', + areaColor: 'Area Color', + areaEmphasisColor: 'Area Emphasis Color', + }, + background: { title: 'Background' }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/ScoreChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/ScoreChart.tsx index d0d35d2c4..5b58f69a4 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/ScoreChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/ScoreChart.tsx @@ -22,7 +22,7 @@ import ChartDataset from 'app/types/ChartDataset'; import { getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; @@ -38,6 +38,8 @@ class ScoreChart extends Chart { chart: any = null; config = Config; utilCanvas = null; + scoreChartOptions = { dataset: {}, config: {} }; + boardTypes = ['header', 'body', 'footer']; constructor(props?) { @@ -76,6 +78,7 @@ class ScoreChart extends Chart { this.chart?.clear(); return; } + this.scoreChartOptions = props; const newOptions = this.getOptions(props.dataset, props.config, context); this.chart?.setOption(Object.assign({}, newOptions), true); } @@ -85,6 +88,7 @@ class ScoreChart extends Chart { } onResize(opt: any, context): void { + this.onUpdated(this.scoreChartOptions, context); this.chart?.resize(context); } @@ -95,7 +99,7 @@ class ScoreChart extends Chart { .filter(c => c.type === ChartDataSectionType.AGGREGATE) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -112,7 +116,6 @@ class ScoreChart extends Chart { const { basicFontSize, bodyContentFontSize } = this.computeFontSize( context, - { width: this.chart?.getWidth(), height: this.chart?.getHeight() }, ).apply(null, measureTexts as any); const richStyles = aggConfigValues @@ -295,7 +298,7 @@ class ScoreChart extends Chart { } private computeFontSize = - (context, style) => + context => ( prefixHeader: string, headerText: string, @@ -314,7 +317,7 @@ class ScoreChart extends Chart { const hasContent = prefixContent || contentText || suffixContent; const hasFooter = prefixFooter || footerText || suffixFooter; - const { width, height } = style; + const { width, height } = context; const maxPartSize = 16; const exactWidth = diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/config.ts index 45994c7b4..10dabc4e6 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/ScoreChart/config.ts @@ -248,6 +248,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -269,6 +289,26 @@ const config: ChartConfig = { }, }, }, + { + lang: 'en-US', + translation: { + score: { + headerTitle: 'Header', + bodyTitle: 'Body', + footerTitle: 'Footer Title', + show: 'Show', + prefixText: 'Prefix Text', + suffixText: 'Suffix Text', + prefxFont: 'Prefix Font', + suffixFont: 'Suffix Font', + common: 'Common', + isFixedFontSize: 'Enable Fixed Font Size', + headerFontSize: 'Header Font Size', + bodyFontSize: 'Body Font Size', + footerFontSize: 'Footer Font Size', + }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackBarChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackBarChart/config.ts index 6bf100a04..187922acf 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackBarChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackBarChart/config.ts @@ -430,27 +430,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -525,11 +531,79 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', - }, }, }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', + }, + }, + } ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackColumnChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackColumnChart/config.ts index 6bf100a04..92aeadfd9 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackColumnChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/StackColumnChart/config.ts @@ -430,27 +430,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -525,8 +531,76 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/WaterfallChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/WaterfallChart.tsx index 537bd8e9a..7639e10aa 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/WaterfallChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/WaterfallChart.tsx @@ -24,8 +24,9 @@ import { getCustomSortableColumns, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; +import { toFormattedValue } from 'app/utils/number'; import { init } from 'echarts'; import { UniqArray } from 'utils/object'; import Config from './config'; @@ -89,7 +90,7 @@ class WaterfallChart extends Chart { .filter(c => c.type === ChartDataSectionType.AGGREGATE) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); @@ -127,7 +128,7 @@ class WaterfallChart extends Chart { styles, 'bar', ); - const label = this.getLabel(styles); + const label = this.getLabel(styles, aggregateConfigs[0].format); const dataList = dataColumns.map( dc => dc[getValueByColumnKey(aggregateConfigs[0])], @@ -199,7 +200,10 @@ class WaterfallChart extends Chart { if (!index && typeof param[1].value === 'number') { data += param[1].value; } - return `${pa.seriesName}: ${data}`; + return `${pa.seriesName}: ${toFormattedValue( + data, + aggregateConfigs[0].format, + )}`; }); const xAxis = param[0]['axisValue']; if (xAxis === '累计') { @@ -304,7 +308,7 @@ class WaterfallChart extends Chart { }; } - getLabel(styles) { + getLabel(styles, format) { const [show, position, font] = this.getArrStyleValueByGroup( ['showLabel', 'position', 'font'], styles, @@ -314,6 +318,7 @@ class WaterfallChart extends Chart { show, position, ...font, + formatter: ({ value }) => `${toFormattedValue(value, format)}`, }; } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/config.ts index 317c60e10..d817e28bc 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WaterfallChart/config.ts @@ -395,27 +395,33 @@ const config: ChartConfig = { ], settings: [ { - label: 'reference.title', - key: 'reference', + label: 'paging.title', + key: 'paging', comType: 'group', rows: [ { - label: 'reference.open', - key: 'panel', - comType: 'reference', - options: { type: 'modal' }, + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, }, ], }, { - label: 'cache.title', - key: 'cache', + label: 'reference.title', + key: 'reference', comType: 'group', rows: [ { - label: 'cache.title', + label: 'reference.open', key: 'panel', - comType: 'cache', + comType: 'reference', + options: { type: 'modal' }, }, ], }, @@ -471,8 +477,76 @@ const config: ChartConfig = { title: '参考线', open: '点击参考线配置', }, - cache: { - title: '数据处理', + }, + }, + { + lang: 'en-US', + translation: { + common: { + showAxis: 'Show Axis', + inverseAxis: 'Inverse Axis', + lineStyle: 'Line Style', + borderStyle: 'Border Style', + borderType: 'Border Type', + borderWidth: 'Border Width', + borderColor: 'Border Color', + backgroundColor: 'Background Color', + showLabel: 'Show Label', + unitFont: 'Unit Font', + rotate: 'Rotate', + position: 'Position', + showInterval: 'Show Interval', + interval: 'Interval', + showTitleAndUnit: 'Show Title and Unit', + nameLocation: 'Name Location', + nameRotate: 'Name Rotate', + nameGap: 'Name Gap', + min: 'Min', + max: 'Max', + }, + label: { + title: 'Label', + showLabel: 'Show Label', + position: 'Position', + }, + legend: { + title: 'Legend', + showLegend: 'Show Legend', + type: 'Type', + selectAll: 'Select All', + position: 'Position', + }, + data: { + color: 'Color', + colorize: 'Colorize', + }, + stack: { + title: 'Stack', + enable: 'Enable', + percentage: 'Percentage', + enableTotal: 'Enable Total', + }, + bar: { + title: 'Bar Chart', + enable: 'Enable Horizon', + radius: 'Bar Radius', + width: 'Bar Width', + gap: 'Bar Gap', + }, + xAxis: { + title: 'X Axis', + }, + yAxis: { + title: 'Y Axis', + }, + splitLine: { + title: 'Splite Line', + showHorizonLine: 'Show Horizontal Line', + showVerticalLine: 'Show Vertical Line', + }, + reference: { + title: 'Reference Line', + open: 'Open', }, }, }, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/WordCloudChart.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/WordCloudChart.tsx index a8d0b7118..a1ba6e134 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/WordCloudChart.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/WordCloudChart.tsx @@ -23,7 +23,7 @@ import { getDefaultThemeColor, getStyleValueByGroup, getValueByColumnKey, - transfromToObjectArray, + transformToObjectArray, } from 'app/utils/chartHelper'; import { init } from 'echarts'; import 'echarts-wordcloud'; @@ -93,7 +93,7 @@ class WordCloudChart extends Chart { .filter(c => c.type === ChartDataSectionType.AGGREGATE) .flatMap(config => config.rows || []); - const objDataColumns = transfromToObjectArray( + const objDataColumns = transformToObjectArray( dataset.rows, dataset.columns, ); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/config.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/config.ts index 5b4f03fbf..0fcb4e280 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/config.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/WordCloudChart/config.ts @@ -26,6 +26,10 @@ const config: ChartConfig = { required: true, type: 'group', limit: 1, + actions: { + NUMERIC: ['sortable'], + STRING: ['sortable'], + }, }, { label: 'metrics', @@ -33,6 +37,10 @@ const config: ChartConfig = { required: true, type: 'aggregate', limit: 1, + actions: { + NUMERIC: ['sortable', 'aggregate'], + STRING: ['sortable', 'aggregate'], + }, }, { label: 'filter', @@ -209,6 +217,26 @@ const config: ChartConfig = { ], }, ], + settings: [ + { + label: 'paging.title', + key: 'paging', + comType: 'group', + rows: [ + { + label: 'paging.pageSize', + key: 'pageSize', + default: 1000, + comType: 'inputNumber', + options: { + needRefresh: true, + step: 1, + min: 0, + }, + }, + ], + }, + ], i18ns: [ { lang: 'zh-CN', @@ -223,7 +251,7 @@ const config: ChartConfig = { label: { title: '标签', fontFamily: '字体', - fontWeight: '字号', + fontWeight: '字体粗细', maxFontSize: '字体最大值', minFontSize: '字体最小值', rotationRangeStart: '起始旋转角度', @@ -236,6 +264,32 @@ const config: ChartConfig = { }, }, }, + { + lang: 'en-US', + translation: { + wordCloud: { + title: 'Word Cloud', + shape: 'Shape', + drawOutOfBound: 'Boundary', + width: 'Width', + height: 'Height', + }, + label: { + title: 'Label', + fontFamily: 'Font Family', + fontWeight: 'Font Weight', + maxFontSize: 'Max Font Size', + minFontSize: 'Min Font Size', + rotationRangeStart: 'Start Rotation Range', + rotationRangeEnd: 'End Rotation Range', + rotationStep: 'Rotation Step', + gridSize: 'Grid Size', + focus: 'Focus', + textShadowBlur: 'Text Shadow Blur', + textShadowColor: 'Text Shadow Color', + }, + }, + }, ], }; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/index.ts index a2a3e2977..4e38e18f6 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/index.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph/index.ts @@ -25,6 +25,7 @@ import BasicGaugeChart from './BasicGaugeChart'; import BasicLineChart from './BasicLineChart'; import BasicOutlineMapChart from './BasicOutlineMapChart'; import BasicPieChart from './BasicPieChart'; +import BasicRichText from './BasicRichText'; import BasicScatterChart from './BasicScatterChart'; import BasicTableChart from './BasicTableChart'; import ClusterBarChart from './ClusterBarChart'; @@ -37,6 +38,7 @@ import NormalOutlineMapChart from './NormalOutlineMapChart'; import PercentageStackBarChart from './PercentageStackBarChart'; import PercentageStackColumnChart from './PercentageStackColumnChart'; import PieChart from './PieChart'; +import PivotSheetChart from './PivotSheetChart'; import RoseChart from './RoseChart'; import ScatterOutlineMapChart from './ScatterOutlineMapChart'; import ScoreChart from './ScoreChart'; @@ -76,5 +78,7 @@ const WidgetPlugins = { ScatterOutlineMapChart, WaterfallChart, BasicGaugeChart, + BasicRichText, + PivotSheetChart, }; export default WidgetPlugins; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphPanel.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphPanel.tsx index 48aa0adf8..fc84e3107 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphPanel.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraphPanel.tsx @@ -16,7 +16,7 @@ * limitations under the License. */ -import { Popconfirm, Tooltip } from 'antd'; +import { Tooltip } from 'antd'; import { IW } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; @@ -61,6 +61,7 @@ const ChartGraphPanel: FC<{ const handleChartChange = useCallback( chartId => () => { const chart = chartManager.getById(chartId); + if (!!chart) { onChartChange(chart); } @@ -112,7 +113,6 @@ const ChartGraphPanel: FC<{ > - + {renderIcon({ + iconStr: c?.meta?.icon, + isMatchRequirement: !!requirementsStates?.[c?.meta?.id], + isActive: c?.meta?.id === chart?.meta?.id, + })} ); }; - return allCharts.map(c => { - if (c?.meta?.id !== 'mingxi-table') { - return _getChartIcon(c, handleChartChange(c?.meta?.id)); + const renderIcon = ({ + ...args + }: { + iconStr; + isMatchRequirement; + isActive; + }) => { + if (/^; } + if (/svg\+xml;base64/.test(args?.iconStr)) { + return ; + } + return ; + }; - return ( - - {_getChartIcon(c)} - - ); + return allCharts.map(c => { + return _getChartIcon(c, handleChartChange(c?.meta?.id)); }); }; return {renderCharts()}; }); +const SVGFontIconRender = ({ iconStr, isMatchRequirement }) => { + return ( + + ); +}; + +const SVGImageRender = ({ iconStr, isMatchRequirement, isActive }) => { + return ( + + ); +}; + +const Base64ImageRender = ({ iconStr, isMatchRequirement, isActive }) => { + return ( + + ); +}; + export default ChartGraphPanel; const StyledChartGraphPanel = styled.div` @@ -164,10 +200,17 @@ const IconWrapper = styled.span` padding: ${SPACE_TIMES(0.5)}; `; -const StyledChartIcon = styled(IW)<{ isMatchRequirement?: boolean }>` +const StyledInlineSVGIcon = styled.img<{ isMatchRequirement?: boolean }>` + opacity: ${p => (p.isMatchRequirement ? 1 : 0.4)}; +`; + +const StyledSVGFontIcon = styled.i<{ isMatchRequirement?: boolean }>` + opacity: ${p => (p.isMatchRequirement ? 1 : 0.4)}; +`; + +const StyledChartIcon = styled(IW)` cursor: pointer; border-radius: ${BORDER_RADIUS}; - opacity: ${p => (p.isMatchRequirement ? 1 : 0.4)}; &:hover, &.active { 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 9ce29ba80..7e1de891e 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 @@ -19,14 +19,14 @@ import { Table } from 'antd'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import useMount from 'app/hooks/useMount'; -import useResizeObserver from 'app/hooks/useResizeObserver'; import ChartTools from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import { ChartConfig } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; -import { FC, memo, useRef, useState } from 'react'; +import { FC, memo, useState } from 'react'; import styled from 'styled-components/macro'; import { BORDER_RADIUS, SPACE_LG, SPACE_MD } from 'styles/StyleConstants'; +import { Debugger } from 'utils/debugger'; import Chart404Graph from './components/Chart404Graph'; import ChartTypeSelector, { ChartPresentType, @@ -35,105 +35,104 @@ import ChartTypeSelector, { const CHART_TYPE_SELECTOR_HEIGHT_OFFSET = 50; const ChartPresentPanel: FC<{ + containerHeight?: number; + containerWidth?: number; chart?: Chart; dataset?: ChartDataset; chartConfig?: ChartConfig; -}> = memo(({ chart, dataset, chartConfig }) => { - const translate = useI18NPrefix(`viz.palette.present`); - const [chartType, setChartType] = useState(ChartPresentType.GRAPH); - const panelRef = useRef<{ offsetWidth; offsetHeight }>(null); - const [chartDispatcher] = useState(() => - ChartTools.ChartIFrameContainerDispatcher.instance(), - ); - - useMount(undefined, () => { - console.debug('Disposing - Chart Container'); - ChartTools.ChartIFrameContainerDispatcher.dispose(); - }); - - const { ref: graphRef } = useResizeObserver({ - refreshMode: 'debounce', - refreshRate: 10, - }); - - const renderGraph = (containerId, chart?: Chart, chartConfig?, style?) => { - if (!chart?.isMatchRequirement(chartConfig)) { - return ; - } - return ( - !!chart && - chartDispatcher.getContainers( - containerId, - chart, - dataset, - chartConfig!, - style, - ) - ); - }; - - const renderReusableChartContainer = () => { - const style = { - width: panelRef.current?.offsetWidth, - height: - panelRef.current?.offsetHeight - CHART_TYPE_SELECTOR_HEIGHT_OFFSET, // TODO(Stephen): calculate when change chart +}> = memo( + ({ containerHeight, containerWidth, chart, dataset, chartConfig }) => { + const translate = useI18NPrefix(`viz.palette.present`); + const chartDispatcher = + ChartTools.ChartIFrameContainerDispatcher.instance(); + const [chartType, setChartType] = useState(ChartPresentType.GRAPH); + + useMount(undefined, () => { + Debugger.instance.measure(`ChartPresentPanel | Dispose Event`, () => { + ChartTools.ChartIFrameContainerDispatcher.dispose(); + }); + }); + + const renderGraph = (containerId, chart?: Chart, chartConfig?, style?) => { + if (!chart?.isMatchRequirement(chartConfig)) { + return ; + } + return ( + !!chart && + chartDispatcher.getContainers( + containerId, + chart, + dataset, + chartConfig!, + style, + ) + ); }; - const containerId = chart?.isISOContainer - ? (chart?.isISOContainer as string) - : 'container-1'; + const renderReusableChartContainer = () => { + const style = { + width: containerWidth, + height: + (containerHeight || CHART_TYPE_SELECTOR_HEIGHT_OFFSET) - + CHART_TYPE_SELECTOR_HEIGHT_OFFSET, + }; + + const containerId = chart?.isISOContainer + ? (chart?.isISOContainer as string) + : 'container-1'; + + return ( + <> + {ChartPresentType.GRAPH === chartType && ( + + {renderGraph(containerId, chart, chartConfig, style)} + + )} + {ChartPresentType.RAW === chartType && ( + + ({ + key: col.name, + title: col.name, + dataIndex: index, + }))} + bordered + /> + + )} + {ChartPresentType.SQL === chartType && ( + + {dataset?.script} + + )} + > + ); + }; - return ( - <> - {ChartPresentType.GRAPH === chartType && ( - - {renderGraph(containerId, chart, chartConfig, style)} - - )} - {ChartPresentType.RAW === chartType && ( - - ({ - key: col.name, - title: col.name, - dataIndex: index, - }))} - bordered - /> - - )} - {ChartPresentType.SQL === chartType && ( - - {dataset?.script} - - )} - > - ); - }; + const renderChartTypeSelector = () => { + return ( + + ); + }; - const renderChartTypeSelector = () => { return ( - + + {renderChartTypeSelector()} + {renderReusableChartContainer()} + ); - }; - - return ( - - {renderChartTypeSelector()} - {renderReusableChartContainer()} - - ); -}); + }, +); export default ChartPresentPanel; -const StyledChartPresentPanel = styled.div<{ ref }>` +const StyledChartPresentPanel = styled.div` display: flex; flex: 1; flex-direction: column; 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 b1276e398..86d98c7b5 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentWrapper.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartPresentWrapper.tsx @@ -16,43 +16,73 @@ * limitations under the License. */ +import useResizeObserver from 'app/hooks/useResizeObserver'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import { ChartConfig } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; -import { FC } from 'react'; +import { FC, memo, useMemo } from 'react'; import styled from 'styled-components/macro'; import { SPACE_MD } from 'styles/StyleConstants'; import ChartGraphPanel from './ChartGraphPanel'; import ChartPresentPanel from './ChartPresentPanel'; const ChartPresentWrapper: FC<{ + containerHeight?: number; + containerWidth?: number; chart?: Chart; dataset?: ChartDataset; chartConfig?: ChartConfig; onChartChange: (c: Chart) => void; -}> = ({ chart, dataset, chartConfig, onChartChange }) => { - return ( - - - - - ); -}; +}> = memo( + ({ + containerHeight, + containerWidth, + chart, + dataset, + chartConfig, + onChartChange, + }) => { + const { ref: ChartGraphPanelRef } = useResizeObserver({ + refreshMode: 'debounce', + refreshRate: 500, + }); + + const borderWidth = useMemo(() => { + return +SPACE_MD.replace('px', ''); + }, []); + + return ( + + + + + + + ); + }, +); export default ChartPresentWrapper; -const StyledChartPresentWrapper = styled.div` +const StyledChartPresentWrapper = styled.div<{ borderWidth }>` display: flex; flex-direction: column; height: 100%; - padding: ${SPACE_MD} ${SPACE_MD} ${SPACE_MD} 0; + padding: ${p => p.borderWidth}px ${p => p.borderWidth}px + ${p => p.borderWidth}px 0; background-color: ${p => p.theme.bodyBackground}; `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/AntdTableChartAdapter.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/CurrentRangeTime.tsx similarity index 63% rename from frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/AntdTableChartAdapter.tsx rename to frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/CurrentRangeTime.tsx index cc8961daa..ad31a32c6 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/AntdTableChartAdapter.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/CurrentRangeTime.tsx @@ -16,20 +16,20 @@ * limitations under the License. */ -import { Table } from 'antd'; +import { DatePicker } from 'antd'; +import moment from 'moment'; import { FC, memo } from 'react'; +const { RangePicker } = DatePicker; -const AntdTableChartAdapter: FC<{ dataSource: []; columns: [] }> = memo( - ({ dataSource, columns, ...rest }) => { +const CurrentRangeTime: FC<{ times?: [string, string]; disabled?: boolean }> = + memo(({ times, disabled = true }) => { return ( - ); - }, -); + }); -export default AntdTableChartAdapter; +export default CurrentRangeTime; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/ExactTimeSelector.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/ExactTimeSelector.tsx new file mode 100644 index 000000000..49a649d8f --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/ExactTimeSelector.tsx @@ -0,0 +1,50 @@ +/** + * 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 { DatePicker } from 'antd'; +import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; +import { TimeFilterConditionValue } from 'app/types/ChartConfig'; +import { formatTime } from 'app/utils/time'; +import { FILTER_TIME_FORMATTER_IN_QUERY } from 'globalConstants'; +import moment from 'moment'; +import { FC, memo } from 'react'; + +const ExactTimeSelector: FC< + { + time?: TimeFilterConditionValue; + onChange: (time) => void; + } & I18NComponentProps +> = memo(({ time, i18nPrefix, onChange }) => { + const t = useI18NPrefix(i18nPrefix); + + const handleMomentTimeChange = momentTime => { + const timeStr = formatTime(momentTime, FILTER_TIME_FORMATTER_IN_QUERY); + onChange?.(timeStr); + }; + + return ( + + ); +}); + +export default ExactTimeSelector; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/MannualRangeTimeSelector.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/MannualRangeTimeSelector.tsx index 06ef747eb..303ced8ad 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/MannualRangeTimeSelector.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/MannualRangeTimeSelector.tsx @@ -17,60 +17,69 @@ */ import { Row, Space } from 'antd'; -import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import TimeConfigContext from 'app/pages/ChartWorkbenchPage/contexts/TimeConfigContext'; -import { - FilterCondition, - FilterConditionType, -} from 'app/types/ChartConfig'; -import moment from 'moment'; -import { FC, memo, useContext, useState } from 'react'; +import { I18NComponentProps } from 'app/hooks/useI18NPrefix'; +import { FilterCondition, FilterConditionType } from 'app/types/ChartConfig'; +import { formatTime, getTime } from 'app/utils/time'; +import { FILTER_TIME_FORMATTER_IN_QUERY } from 'globalConstants'; +import { FC, memo, useState } from 'react'; import ChartFilterCondition, { ConditionBuilder, } from '../../../../models/ChartFilterCondition'; +import CurrentRangeTime from './CurrentRangeTime'; import ManualSingleTimeSelector from './ManualSingleTimeSelector'; const MannualRangeTimeSelector: FC< { condition?: FilterCondition; - onFilterChange: (filter: ChartFilterCondition) => void; + onConditionChange: (filter: ChartFilterCondition) => void; } & I18NComponentProps -> = memo(({ i18nPrefix, condition, onFilterChange }) => { - const t = useI18NPrefix(i18nPrefix); - const { format } = useContext(TimeConfigContext); - const [timeRange, setTimeRange] = useState(() => { +> = memo(({ i18nPrefix, condition, onConditionChange }) => { + const [rangeTimes, setRangeTimes] = useState(() => { if (condition?.type === FilterConditionType.RangeTime) { - const startTime = moment(condition?.value?.[0]); - const endTime = moment(condition?.value?.[1]); + const startTime = condition?.value?.[0]; + const endTime = condition?.value?.[1]; return [startTime, endTime]; } return []; }); const handleTimeChange = index => time => { - timeRange[index] = time; - setTimeRange(timeRange); + rangeTimes[index] = time; + setRangeTimes(rangeTimes); const filterRow = new ConditionBuilder(condition) - .setValue((timeRange || []).map(d => d.toString())) + .setValue(rangeTimes || []) .asRangeTime(); - onFilterChange && onFilterChange(filterRow); + onConditionChange?.(filterRow); + }; + + const getRangeStringTimes = () => { + return (rangeTimes || []).map(t => { + if (Boolean(t) && typeof t === 'object' && 'unit' in t) { + const time = getTime(+(t.direction + t.amount), t.unit)( + t.unit, + t.isStart, + ); + return formatTime(time, FILTER_TIME_FORMATTER_IN_QUERY); + } + return t; + }); }; return ( - - {/* {`${t('currentTime')} : ${timeRange - ?.map(time => formatTime(time, format)) - ?.join(' - ')}`} */} - + + + void; + time?: TimeFilterConditionValue; + isStart: boolean; + onTimeChange: (time) => void; } & I18NComponentProps -> = memo(({ isStart, i18nPrefix, time, onTimeChange }) => { +> = memo(({ time, isStart, i18nPrefix, onTimeChange }) => { const t = useI18NPrefix(i18nPrefix); - const [type, setType] = useState(RelativeOrExactTime.Exact); + const [type, setType] = useState(() => { + return typeof time === 'string' + ? TimeFilterValueCategory.Exact + : TimeFilterValueCategory.Relative; + }); - const handleTimeChange = time => { - onTimeChange?.(time); + const handleTimeCategoryChange = type => { + setType(type); + if (type === TimeFilterValueCategory.Exact) { + onTimeChange?.(formatTime(moment(), FILTER_TIME_FORMATTER_IN_QUERY)); + } else if (type === TimeFilterValueCategory.Relative) { + onTimeChange?.({ unit: 'd', amount: 1, direction: '-' }); + } else { + onTimeChange?.(null); + } }; const renderTimeSelector = type => { switch (type) { - case RelativeOrExactTime.Exact: + case TimeFilterValueCategory.Exact: return ( - ); - case RelativeOrExactTime.Relative: + case TimeFilterValueCategory.Relative: return ( ); } @@ -62,12 +76,12 @@ const ManualSingleTimeSelector: FC< return ( - setType(value)}> - - {t(RelativeOrExactTime.Exact)} + + + {t(TimeFilterValueCategory.Exact)} - - {t(RelativeOrExactTime.Relative)} + + {t(TimeFilterValueCategory.Relative)} {isStart ? `${t('startTime')} : ` : `${t('endTime')} : `} @@ -79,7 +93,7 @@ const ManualSingleTimeSelector: FC< export default ManualSingleTimeSelector; const StyledManualSingleTimeSelector = styled(Space)` - & .ant-select { + & > .ant-select { width: 80px; } `; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RecommendRangeTimeSelector.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RecommendRangeTimeSelector.tsx index 174b60b1e..a5df50a9c 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RecommendRangeTimeSelector.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RecommendRangeTimeSelector.tsx @@ -16,16 +16,16 @@ * limitations under the License. */ -import { Radio, Row, Space } from 'antd'; +import { Radio, Space } from 'antd'; import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import TimeConfigContext from 'app/pages/ChartWorkbenchPage/contexts/TimeConfigContext'; import { FilterCondition } from 'app/types/ChartConfig'; -import { convertRelativeTimeRange } from 'app/utils/time'; +import { recommendTimeRangeConverter } from 'app/utils/time'; import { RECOMMEND_TIME } from 'globalConstants'; -import { FC, memo, useContext, useState } from 'react'; +import { FC, memo, useMemo, useState } from 'react'; import ChartFilterCondition, { ConditionBuilder, } from '../../../../models/ChartFilterCondition'; +import CurrentRangeTime from './CurrentRangeTime'; const RecommendRangeTimeSelector: FC< { @@ -34,60 +34,57 @@ const RecommendRangeTimeSelector: FC< } & I18NComponentProps > = memo(({ i18nPrefix, condition, onConditionChange }) => { const t = useI18NPrefix(i18nPrefix); - const { format } = useContext(TimeConfigContext); - const [timeRange, setTimeRange] = useState([]); - const [relativeTime, setRelativeTime] = useState(); - - const handleChange = relativeTime => { - const timeRange = convertRelativeTimeRange(relativeTime); - setTimeRange(timeRange); + const [recommend, setRecommend] = useState( + condition?.value as string, + ); - setRelativeTime(relativeTime); + const handleChange = recommendTime => { + setRecommend(recommendTime); const filter = new ConditionBuilder(condition) - .setValue(timeRange) - .asRangeTime(); + .setValue(recommendTime) + .asRecommendTime(); onConditionChange?.(filter); }; + const rangeTimes = useMemo(() => { + return recommendTimeRangeConverter(recommend); + }, [recommend]); + return ( - - - {/* {`${t('currentTime')} : ${timeRange - ?.map(t => formatTime(t, format)) - .join(' - ')}`} */} - - handleChange(e.target?.value)} - > - - {t(RECOMMEND_TIME.TODAY)} - - {t(RECOMMEND_TIME.YESTERDAY)} - - - {t(RECOMMEND_TIME.THISWEEK)} - - - - - {t(RECOMMEND_TIME.LAST_7_DAYS)} - - - {t(RECOMMEND_TIME.LAST_30_DAYS)} - - - {t(RECOMMEND_TIME.LAST_90_DAYS)} - - - {t(RECOMMEND_TIME.LAST_1_MONTH)} - - - {t(RECOMMEND_TIME.LAST_1_YEAR)} - - - - + <> + + + handleChange(e.target?.value)} + > + + {[ + RECOMMEND_TIME.TODAY, + RECOMMEND_TIME.YESTERDAY, + RECOMMEND_TIME.THISWEEK, + ].map(time => ( + + {t(time)} + + ))} + + + {[ + RECOMMEND_TIME.LAST_7_DAYS, + RECOMMEND_TIME.LAST_30_DAYS, + RECOMMEND_TIME.LAST_90_DAYS, + RECOMMEND_TIME.LAST_1_MONTH, + RECOMMEND_TIME.LAST_1_YEAR, + ].map(time => ( + + {t(time)} + + ))} + + + + > ); }); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RelativeTimeSelector.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RelativeTimeSelector.tsx index 92c934262..6b2815037 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RelativeTimeSelector.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector/RelativeTimeSelector.tsx @@ -18,29 +18,37 @@ import { InputNumber, Select, Space } from 'antd'; import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; -import { getTime } from 'app/utils/time'; +import { TimeFilterConditionValue } from 'app/types/ChartConfig'; import { TIME_DIRECTION, TIME_UNIT_OPTIONS } from 'globalConstants'; import { unitOfTime } from 'moment'; import { FC, memo, useState } from 'react'; import styled from 'styled-components/macro'; + const RelativeTimeSelector: FC< { - isStart?: boolean; + time?: TimeFilterConditionValue; onChange: (time) => void; } & I18NComponentProps -> = memo(({ isStart, i18nPrefix, onChange }) => { +> = memo(({ time, i18nPrefix, onChange }) => { const t = useI18NPrefix(i18nPrefix); - const [amount, setAmount] = useState(0); - const [unit, setUnit] = useState('d'); - const [direction, setDirection] = useState('-'); + const [amount, setAmount] = useState(() => (time as any)?.amount || 1); + const [unit, setUnit] = useState( + () => (time as any)?.unit || 'd', + ); + const [direction, setDirection] = useState( + () => (time as any)?.direction || '-', + ); const handleTimeChange = ( unit: unitOfTime.DurationConstructor, amount: number, direction: string, ) => { - const time = getTime(+(direction + amount), unit)(unit, isStart); - onChange?.(time); + onChange?.({ + unit, + amount, + direction, + }); }; const handleUnitChange = (newUnit: unitOfTime.DurationConstructor) => { @@ -53,36 +61,40 @@ const RelativeTimeSelector: FC< handleTimeChange(unit, newAmount, direction); }; - const handleDirecitonChange = newDirection => { + const handleDirectionChange = newDirection => { setDirection(newDirection); handleTimeChange(unit, amount, newDirection); }; return ( - - - - {TIME_UNIT_OPTIONS.map(item => ( + + + {TIME_DIRECTION.map(item => ( {t(item.name)} ))} - - {TIME_DIRECTION.map(item => ( + {TIME_DIRECTION.filter(d => d.name !== 'current').find( + d => d.value === direction, + ) && ( + + )} + + {TIME_UNIT_OPTIONS.map(item => ( {t(item.name)} ))} - + ); }); export default RelativeTimeSelector; -const StyledReativeTimeSelector = styled(Space)` +const StyledRelativeTimeSelector = styled(Space)` & .ant-select, .ant-input-number { max-width: 80px; 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 a308ceb63..58212ab6a 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 @@ -16,6 +16,7 @@ * limitations under the License. */ +import CurrentRangeTime from './CurrentRangeTime'; import MannualRangeTimeSelector from './MannualRangeTimeSelector'; import ManualSingleTimeSelector from './ManualSingleTimeSelector'; import RecommendRangeTimeSelector from './RecommendRangeTimeSelector'; @@ -24,6 +25,7 @@ const TimeSelector = { MannualRangeTimeSelector, ManualSingleTimeSelector, RecommendRangeTimeSelector, + CurrentRangeTime, }; export default TimeSelector; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainer.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainer.tsx index 3ccc253a6..8bff6b5d0 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainer.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainer.tsx @@ -16,75 +16,101 @@ * limitations under the License. */ +import { + Frame, + FrameContextConsumer, +} from 'app/components/ReactFrameComponent'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import { ChartConfig } from 'app/types/ChartConfig'; -import React from 'react'; -import Frame, { FrameContextConsumer } from 'react-frame-component'; +import { FC, memo } from 'react'; import styled, { StyleSheetManager } from 'styled-components/macro'; import { isEmpty } from 'utils/object'; import ChartLifecycleAdapter from './ChartLifecycleAdapter'; -// eslint-disable-next-line import/no-webpack-loader-syntax -const antdStyles = require('!!css-loader!antd/dist/antd.min.css'); -const ChartIFrameContainer: React.FC<{ +const ChartIFrameContainer: FC<{ dataset: any; chart: Chart; config: ChartConfig; containerId?: string; - style?; -}> = props => { - // Note: manually add table css style in iframe - const isTable = props.chart?.isISOContainer === 'react-table'; - - const transformToSafeCSSProps = style => { - if (isNaN(style?.width) || isEmpty(style?.width)) { - style.width = 0; + width?: any; + height?: any; + isShown?: boolean; + widgetSpecialConfig?: any; +}> = memo(props => { + const transformToSafeCSSProps = (width, height) => { + let newStyle = { width, height }; + if (isNaN(newStyle?.width) || isEmpty(newStyle?.width)) { + newStyle.width = 0; } - if (isNaN(style?.height) || isEmpty(style?.height)) { - style.height = 0; + if (isNaN(newStyle?.height) || isEmpty(newStyle?.height)) { + newStyle.height = 0; } - return style; + return newStyle; + }; + + const render = () => { + if (!props?.chart?._useIFrame) { + return ( + + + + ); + } + + return ( + + + > + } + > + + {frameContext => ( + + + + + + )} + + + ); }; - return ( - - - - > - } - > - - {frameContext => ( - - - - - - )} - - - ); -}; + return render(); +}); export default ChartIFrameContainer; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerDispatcher.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerDispatcher.tsx index 719a689e6..523f7b69e 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerDispatcher.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerDispatcher.tsx @@ -20,7 +20,7 @@ import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import { ChartConfig } from 'app/types/ChartConfig'; import ChartDataset from 'app/types/ChartDataset'; import { CSSProperties } from 'styled-components'; -import ChartTools from '.'; +import ChartIFrameContainer from './ChartIFrameContainer'; const DEFAULT_CONTAINER_ID = 'frame-container-1'; @@ -29,6 +29,7 @@ class ChartIFrameContainerDispatcher { private currentContainerId = DEFAULT_CONTAINER_ID; private chartContainerMap = new Map(); private chartMetadataMap = new Map(); + private editorEnv = { env: 'workbench' }; public static instance(): ChartIFrameContainerDispatcher { if (!this.dispatcher) { @@ -53,13 +54,15 @@ class ChartIFrameContainerDispatcher { this.switchContainer(containerId, chart, dataset, config); const renders: Function[] = []; this.chartContainerMap.forEach((chartRenderer: Function, key) => { + const isShown = key === this.currentContainerId; renders.push( chartRenderer .call( - null, - this.getVisibilityStyle(key === this.currentContainerId, style), + Object.create(null), + this.getVisibilityStyle(isShown, style), + isShown, ) - .apply(null, this.chartMetadataMap.get(key)), + .apply(Object.create(null), this.chartMetadataMap.get(key)), ); }); return renders; @@ -77,16 +80,20 @@ class ChartIFrameContainerDispatcher { private createNewIfNotExist(containerId: string) { if (!this.chartContainerMap.has(containerId)) { - const newContainer = style => (chart, dataset, config) => { + const newContainer = (style, isShown) => (chart, dataset, config) => { return ( - + + + ); }; this.chartContainerMap.set(containerId, newContainer); @@ -94,8 +101,8 @@ class ChartIFrameContainerDispatcher { this.currentContainerId = containerId; } - private getVisibilityStyle(isShow, style?: CSSProperties) { - return isShow + private getVisibilityStyle(isShown, style?: CSSProperties) { + return isShown ? { ...style, transform: 'none', @@ -105,7 +112,7 @@ class ChartIFrameContainerDispatcher { ...style, transform: 'translate(-9999px, -9999px)', position: 'absolute', - }; /* TODO: visibilty: 'collapse' */ + }; } } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerResourceLoader.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerResourceLoader.ts index 8b595505d..5fb643327 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerResourceLoader.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainerResourceLoader.ts @@ -21,7 +21,7 @@ import { loadScript } from '../../../../../../../utils/resource'; class ChartIFrameContainerResourceLoader { private resources: string[] = []; - laodResource(doc, deps?: string[]): Promise { + loadResource(doc, deps?: string[]): Promise { const unloadedDeps = (deps || []).filter(d => !this.resources.includes(d)); return this.loadDependencies(doc, unloadedDeps); } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartLifecycleAdapter.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartLifecycleAdapter.tsx index 7db8a1889..4f353a7d3 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartLifecycleAdapter.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartLifecycleAdapter.tsx @@ -18,15 +18,14 @@ import { LoadingOutlined } from '@ant-design/icons'; import { Spin } from 'antd'; -import useMount from 'app/hooks/useMount'; +import { useFrame } from 'app/components/ReactFrameComponent'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import ChartEventBroker from 'app/pages/ChartWorkbenchPage/models/ChartEventBroker'; import { ChartConfig } from 'app/types/ChartConfig'; import { ChartLifecycle } from 'app/types/ChartLifecycle'; import React, { CSSProperties, useEffect, useRef, useState } from 'react'; -import { useFrame } from 'react-frame-component'; import styled from 'styled-components/macro'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import ChartIFrameContainerResourceLoader from './ChartIFrameContainerResourceLoader'; enum ContainerStatus { @@ -41,8 +40,17 @@ const ChartLifecycleAdapter: React.FC<{ chart: Chart; config: ChartConfig; style: CSSProperties; -}> = ({ dataset, chart, config, style }) => { - const [chartResourceLoader, setChartResourceLoader] = useState( + isShown?: boolean; + widgetSpecialConfig?: any; +}> = ({ + dataset, + chart, + config, + style, + isShown = true, + widgetSpecialConfig, +}) => { + const [chartResourceLoader] = useState( () => new ChartIFrameContainerResourceLoader(), ); const [containerStatus, setContainerStatus] = useState(ContainerStatus.INIT); @@ -50,30 +58,33 @@ const ChartLifecycleAdapter: React.FC<{ const [containerId] = useState(() => uuidv4()); const eventBrokerRef = useRef(); - useMount(() => { - setChartResourceLoader(new ChartIFrameContainerResourceLoader()); - }); - - // when chart change + /** + * Chart Mount Event + * Dependency: 'chart?.meta?.id', 'eventBrokerRef', 'isShown' + */ useEffect(() => { - if (!chart || !document || !window || !config) { - return; - } - if (containerStatus === ContainerStatus.LOADING) { + if ( + !isShown || + !chart || + !document || + !window || + !config || + containerStatus === ContainerStatus.LOADING + ) { return; } setContainerStatus(ContainerStatus.LOADING); (async () => { chartResourceLoader - .laodResource(document, chart?.getDependencies?.()) + .loadResource(document, chart?.getDependencies?.()) .then(_ => { chart.init(config); const newBrokerRef = new ChartEventBroker(); newBrokerRef.register(chart); newBrokerRef.publish( ChartLifecycle.MOUNTED, - { containerId, dataset, config }, + { containerId, dataset, config, widgetSpecialConfig }, { document, window, @@ -92,12 +103,17 @@ const ChartLifecycleAdapter: React.FC<{ return function cleanup() { setContainerStatus(ContainerStatus.INIT); eventBrokerRef?.current?.publish(ChartLifecycle.UNMOUNTED, {}); + eventBrokerRef?.current?.dispose(); }; - }, [chart?.meta?.name, eventBrokerRef]); + }, [chart?.meta?.id, eventBrokerRef, isShown]); - // when chart config or dataset change + /** + * Chart Update Event + * Dependency: 'config', 'dataset', 'widgetSpecialConfig', 'containerStatus', 'document', 'window', 'isShown' + */ useEffect(() => { if ( + !isShown || !document || !window || !config || @@ -111,20 +127,48 @@ const ChartLifecycleAdapter: React.FC<{ { dataset, config, + widgetSpecialConfig, + }, + { + document, + window, + width: style?.width, + height: style?.height, }, - { document, window }, ); - }, [config, dataset, containerStatus, document, window]); + }, [ + config, + dataset, + widgetSpecialConfig, + containerStatus, + document, + window, + isShown, + ]); - // when chart size change + /** + * Chart Resize Event + * Dependency: 'style.width', 'style.height', 'document', 'window', 'isShown' + */ useEffect(() => { - if (!style.width || !style.height) { + if ( + !isShown || + !document || + !window || + !config || + !dataset || + containerStatus !== ContainerStatus.SUCCESS + ) { return; } eventBrokerRef.current?.publish( ChartLifecycle.RESIZE, - {}, + { + dataset, + config, + widgetSpecialConfig, + }, { document, window, @@ -132,7 +176,7 @@ const ChartLifecycleAdapter: React.FC<{ height: style?.height, }, ); - }, [style.width, style.height, document, window]); + }, [style.width, style.height, document, window, isShown]); return ( `${fontSize}px`); +Quill.register(size, true); + +const font = Quill.import('attributors/style/font'); +font.whitelist = FONT_FAMILIES.map(font => font.value); +Quill.register(font, true); + +const MenuItem = Menu.Item; + +const ChartRichTextAdapter: FC<{ + dataList: any[]; + id: string; + isEditing?: boolean; + initContent: string | undefined; + onChange: (delta: string | undefined) => void; +}> = memo(({ dataList, id, isEditing = false, initContent, onChange }) => { + const [containerId, setContainerId] = useState(); + const [quillModules, setQuillModules] = useState(null); + const [visible, setVisible] = useState(false); + const [quillValue, setQuillValue] = useState(''); + const quillRef = useRef(null); + const quillEditRef = useRef(null); + const [translate, setTranslate] = useState(''); + + useEffect(() => { + const value = (initContent && JSON.parse(initContent)) || ''; + setQuillValue(value); + }, [initContent]); + + useEffect(() => { + if (id) { + const newId = `rich-text-${id}`; + setContainerId(newId); + const modules = { + toolbar: { + container: isEditing ? `#${newId}` : null, + handlers: {}, + }, + calcfield: {}, + imageDrop: true, + }; + setQuillModules(modules); + } + }, [id, isEditing]); + + const debouncedDataChange = useMemo( + () => + debounce(value => { + onChange?.(value); + }, 300), + [onChange], + ); + + const quillChange = useCallback(() => { + if (quillEditRef.current && quillEditRef.current?.getEditor()) { + const contents = quillEditRef.current!.getEditor().getContents(); + setQuillValue(contents); + contents && debouncedDataChange(JSON.stringify(contents)); + } + }, [debouncedDataChange]); + + useEffect(() => { + if (typeof quillValue !== 'string') { + const quill = Object.assign({}, quillValue); + const ops = quill.ops?.concat().map(item => { + let insert = item.insert; + if (typeof insert !== 'string') { + if (insert.hasOwnProperty('calcfield')) { + const name = insert.calcfield?.name; + const config = name + ? dataList.find(items => items.name === name) + : null; + insert = config?.value || ''; + } + } + return { ...item, insert }; + }); + setTranslate(ops?.length ? { ...quill, ops } : ''); + } else { + setTranslate(quillValue); + } + }, [quillValue, dataList, setTranslate]); + + useLayoutEffect(() => { + if (quillEditRef.current) { + new QuillMarkdown(quillEditRef.current.getEditor(), MarkdownOptions); + } + }, [quillModules]); + + const reactQuillView = useMemo( + () => + (!isEditing || visible) && ( + + ), + [translate, quillRef, isEditing, visible], + ); + + const selectField = useCallback( + (field: any) => () => { + if (quillEditRef.current) { + const quill = quillEditRef.current.getEditor(); + if (field) { + const text = `[${field.name}]`; + quill.getModule('calcfield').insertItem( + { + denotationChar: '', + id: field.id, + name: field.name, + value: field.value, + text, + }, + true, + ); + setImmediate(() => { + setQuillValue( + quillEditRef.current?.getEditor().getContents() || '', + ); + }); + } + } + }, + [quillEditRef], + ); + + const fieldItems = useMemo(() => { + return dataList?.length ? ( + + {dataList.map(fieldName => ( + + {fieldName.name} + + ))} + + ) : ( + + 暂无可用字段 + + ); + }, [dataList, selectField]); + + const toolbar = useMemo( + () => ( + + + + {FONT_FAMILIES.map(font => ( + + {font.name} + + ))} + + + {FONT_SIZES.map(size => ( + {`${size}px`} + ))} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + [containerId, fieldItems], + ); + + const reactQuillEdit = useMemo( + () => + isEditing && ( + <> + {toolbar} + + + { + setVisible(true); + }} + type="primary" + > + 预览 + + + > + ), + [quillModules, quillValue, isEditing, toolbar, quillChange], + ); + + const ssp = e => { + e.stopPropagation(); + }; + + return ( + + + {quillModules && reactQuillEdit} + {quillModules && !isEditing && reactQuillView} + + { + setVisible(false); + }} + > + {isEditing && reactQuillView} + + + ); +}); +export default ChartRichTextAdapter; + +const QuillBox = styled.div` + width: 100%; + height: 100%; + flex-direction: column; + display: flex; + .react-quill { + flex: 1; + overflow-y: auto; + } + .react-quill-view { + flex: 1; + overflow-y: auto; + } +`; +const TextWrap = styled.div` + width: 100%; + height: 100%; + + & .ql-snow { + border: none; + } + + & .ql-container.ql-snow { + border: none; + } + + & .selectLink { + height: 24px; + width: 28px; + padding: 0 5px; + display: inline-block; + color: black; + + i { + font-size: 16px; + } + } + + & .selectLink:hover { + color: #06c; + } +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactChartAdapter.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactLifecycleAdapter.ts similarity index 79% rename from frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactChartAdapter.ts rename to frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactLifecycleAdapter.ts index f040aa3bd..ec83ee8d8 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactChartAdapter.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ReactLifecycleAdapter.ts @@ -19,7 +19,7 @@ import React from 'react'; import ReactDom from 'react-dom'; -interface ReactChartAdapterProps { +interface ReactLifecycleAdapterProps { init: (component: React.Component | Function) => void; mounted: (container, options?, context?) => any; updated: (options: any, context?) => any; @@ -27,11 +27,17 @@ interface ReactChartAdapterProps { resize: (opt: any) => void; } -export default class ReactChartAdapter implements ReactChartAdapterProps { +export default class ReactLifecycleAdapter + implements ReactLifecycleAdapterProps +{ private domContainer; private reactComponent; private externalLibs; + constructor(componentWrapper) { + this.reactComponent = componentWrapper; + } + public init(component) { this.reactComponent = component; } @@ -43,14 +49,14 @@ export default class ReactChartAdapter implements ReactChartAdapterProps { public mounted(container, options?, context?) { this.domContainer = container; return ReactDom.render( - React.createElement(this.getComponent(), options), + React.createElement(this.getComponent(), options, context), this.domContainer, ); } public updated(options, context?) { return ReactDom.render( - React.createElement(this.getComponent(), options), + React.createElement(this.getComponent(), options, context), this.domContainer, ); } @@ -59,9 +65,9 @@ export default class ReactChartAdapter implements ReactChartAdapterProps { ReactDom.unmountComponentAtNode(this.domContainer); } - public resize(opt: any) { + public resize(options, context?) { return ReactDom.render( - React.createElement(this.getComponent(), opt), + React.createElement(this.getComponent(), options, context), this.domContainer, ); } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/CalcFieldBlot.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/CalcFieldBlot.ts new file mode 100644 index 000000000..a03dee758 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/CalcFieldBlot.ts @@ -0,0 +1,64 @@ +/** + * 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 Quill from 'quill'; + +const Embed = Quill.import('blots/embed'); +class CalcFieldBlot extends Embed { + static blotName = 'calcfield'; + static tagName = 'span'; + static className = 'calcfield'; + + static create(data) { + const node = super.create(); + node.addEventListener( + 'click', + e => { + const event = new Event('mention-clicked', { + bubbles: true, + cancelable: true, + }); + // @ts-ignore + event.value = data; + // @ts-ignore + event.event = e; + window.dispatchEvent(event); + e.preventDefault(); + }, + false, + ); + const denotationChar = document.createElement('span'); + denotationChar.className = 'ql-calcfield-denotation-char'; + denotationChar.innerHTML = data.denotationChar; + node.appendChild(denotationChar); + node.innerHTML += data.text; + return CalcFieldBlot.setDataValues(node, data); + } + + static setDataValues(element, data) { + const domNode = element; + Object.keys(data).forEach(key => { + domNode.dataset[key] = data[key]; + }); + return domNode; + } + + static value(domNode) { + return domNode.dataset; + } +} +export default CalcFieldBlot; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/index.ts new file mode 100644 index 000000000..5f9bc993c --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/RichTextPluginLoader/index.ts @@ -0,0 +1,416 @@ +//@ts-nocheck +import Quill from 'quill'; +import CalcFieldBlot from './CalcFieldBlot'; + +Quill.register(CalcFieldBlot); +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError('Cannot call a class as a function'); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ('value' in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; +} + +function _extends() { + _extends = + Object.assign || + function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; + }; + + return _extends.apply(this, arguments); +} + +function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === 'string') return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === 'Object' && o.constructor) n = o.constructor.name; + if (n === 'Map' || n === 'Set') return Array.from(o); + if (n === 'Arguments' || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) + return _arrayLikeToArray(o, minLen); +} + +function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; + + return arr2; +} + +function _createForOfIteratorHelper(o, allowArrayLike) { + var it; + + if (typeof Symbol === 'undefined' || o[Symbol.iterator] == null) { + if ( + Array.isArray(o) || + (it = _unsupportedIterableToArray(o)) || + (allowArrayLike && o && typeof o.length === 'number') + ) { + if (it) o = it; + var i = 0; + + var F = function () {}; + + return { + s: F, + n: function () { + if (i >= o.length) + return { + done: true, + }; + return { + done: false, + value: o[i++], + }; + }, + e: function (e) { + throw e; + }, + f: F, + }; + } + + throw new TypeError( + 'Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); + } + + var normalCompletion = true, + didErr = false, + err; + return { + s: function () { + it = o[Symbol.iterator](); + }, + n: function () { + var step = it.next(); + normalCompletion = step.done; + return step; + }, + e: function (e) { + didErr = true; + err = e; + }, + f: function () { + try { + if (!normalCompletion && it.return != null) it.return(); + } finally { + if (didErr) throw err; + } + }, + }; +} + +var Keys = { + TAB: 9, + ENTER: 13, + ESCAPE: 27, + UP: 38, + DOWN: 40, +}; + +function getFieldCharIndex(text, numberFieldDenotationChars) { + return numberFieldDenotationChars.reduce( + function (prev, numberFieldChar) { + var numberFieldCharIndex = text.lastIndexOf(numberFieldChar); + + if (numberFieldCharIndex > prev.numberFieldCharIndex) { + return { + numberFieldChar: numberFieldChar, + numberFieldCharIndex: numberFieldCharIndex, + }; + } + + return { + numberFieldChar: prev.numberFieldChar, + numberFieldCharIndex: prev.numberFieldCharIndex, + }; + }, + { + numberFieldChar: null, + numberFieldCharIndex: -1, + }, + ); +} + +function hasValidChars(text, allowedChars) { + return allowedChars.test(text); +} + +function hasValidFieldCharIndex(numberFieldCharIndex, text, isolateChar) { + if (numberFieldCharIndex > -1) { + if ( + isolateChar && + !( + numberFieldCharIndex === 0 || + !!text[numberFieldCharIndex - 1].match(/\s/g) + ) + ) { + return false; + } + + return true; + } + + return false; +} + +var CalcField = (function () { + function CalcField(quill, options) { + var _this = this; + + _classCallCheck(this, CalcField); + + this.isOpen = false; + this.itemIndex = 0; + this.numberFieldCharPos = null; + this.cursorPos = null; + this.values = []; + this.suspendMouseEnter = false; //this token is an object that may contains one key "abandoned", set to + //true when the previous source call should be ignored in favor or a + //more recent execution. This token will be null unless a source call + //is in progress. + + this.existingSourceExecutionToken = null; + this.quill = quill; + this.options = { + source: null, + numberFieldDenotationChars: ['@'], + showDenotationChar: true, + allowedChars: /^[a-zA-Z0-9_]*$/, + minChars: 0, + maxChars: 31, + offsetTop: 2, + offsetLeft: 0, + isolateCharacter: false, + fixFieldsToQuill: false, + positioningStrategy: 'normal', + defaultMenuOrientation: 'bottom', + blotName: 'calcfield', + dataAttributes: [ + 'id', + 'value', + 'denotationChar', + 'link', + 'target', + 'disabled', + 'viewId', + 'model', + 'text', + 'agg', + 'size', + 'font-size', + ], + linkTarget: '_blank', + + // Style options + spaceAfterInsert: true, + selectKeys: [Keys.ENTER], + }; + _extends(this.options, options, { + dataAttributes: Array.isArray(options.dataAttributes) + ? this.options.dataAttributes.concat(options.dataAttributes) + : this.options.dataAttributes, + }); //create calcfield container + + quill.on('text-change', this.onTextChange.bind(this)); + quill.on('selection-change', this.onSelectionChange.bind(this)); //Pasting doesn't fire selection-change after the pasted text is + //inserted, so here we manually trigger one + + quill.container.addEventListener('paste', function () { + setTimeout(function () { + var range = quill.getSelection(); + _this.onSelectionChange(range); + }); + }); + + var _iterator = _createForOfIteratorHelper(this.options.selectKeys), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done; ) { + var selectKey = _step.value; + quill.keyboard.addBinding({ + key: selectKey, + }); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + } + + _createClass(CalcField, [ + { + key: 'insertItem', + value: function insertItem(data, programmaticInsert) { + var render = data; + + if (render === null) { + return; + } + + if (!this.options.showDenotationChar) { + render.denotationChar = ''; + } + + var insertAtPos; + + if (!programmaticInsert) { + insertAtPos = this.numberFieldCharPos; + this.quill.deleteText( + this.numberFieldCharPos, + this.cursorPos - this.numberFieldCharPos, + Quill.sources.USER, + ); + } else { + insertAtPos = this.cursorPos; + } + + this.quill.insertEmbed( + insertAtPos, + this.options.blotName, + render, + Quill.sources.USER, + ); + + if (this.options.spaceAfterInsert) { + this.quill.insertText(insertAtPos + 1, ' ', Quill.sources.USER); // setSelection here sets cursor position + + this.quill.setSelection(insertAtPos + 2, Quill.sources.USER); + } else { + this.quill.setSelection(insertAtPos + 1, Quill.sources.USER); + } + }, + }, + { + key: 'getTextBeforeCursor', + value: function getTextBeforeCursor() { + var startPos = Math.max(0, this.cursorPos - this.options.maxChars); + var textBeforeCursorPos = this.quill.getText( + startPos, + this.cursorPos - startPos, + ); + return textBeforeCursorPos; + }, + }, + { + key: 'onSomethingChange', + value: function onSomethingChange() { + var _this5 = this; + + var range = this.quill.getSelection(); + if (range == null) return; + this.cursorPos = range.index; + var textBeforeCursor = this.getTextBeforeCursor(); + + var _getFieldCharIndex = getFieldCharIndex( + textBeforeCursor, + this.options.numberFieldDenotationChars, + ), + numberFieldChar = _getFieldCharIndex.numberFieldChar, + numberFieldCharIndex = _getFieldCharIndex.numberFieldCharIndex; + + if ( + hasValidFieldCharIndex( + numberFieldCharIndex, + textBeforeCursor, + this.options.isolateCharacter, + ) + ) { + var numberFieldCharPos = + this.cursorPos - (textBeforeCursor.length - numberFieldCharIndex); + this.numberFieldCharPos = numberFieldCharPos; + var textAfter = textBeforeCursor.substring( + numberFieldCharIndex + numberFieldChar.length, + ); + + if ( + textAfter.length >= this.options.minChars && + hasValidChars(textAfter, this.getAllowedCharsRegex(numberFieldChar)) + ) { + if (this.existingSourceExecutionToken) { + this.existingSourceExecutionToken.abandoned = true; + } + + var sourceRequestToken = { + abandoned: false, + }; + this.existingSourceExecutionToken = sourceRequestToken; + this.options.source( + textAfter, + function (data, searchTerm) { + if (sourceRequestToken.abandoned) { + return; + } + + _this5.existingSourceExecutionToken = null; + }, + numberFieldChar, + ); + } else { + } + } else { + } + }, + }, + { + key: 'getAllowedCharsRegex', + value: function getAllowedCharsRegex(denotationChar) { + if (this.options.allowedChars instanceof RegExp) { + return this.options.allowedChars; + } else { + return this.options.allowedChars(denotationChar); + } + }, + }, + { + key: 'onTextChange', + value: function onTextChange(delta, oldDelta, source) { + if (source === 'user') { + this.onSomethingChange(); + } + }, + }, + { + key: 'onSelectionChange', + value: function onSelectionChange(range) { + if (range && range.length === 0) { + this.onSomethingChange(); + } + }, + }, + ]); + + return CalcField; +})(); + +Quill.register('modules/calcfield', CalcField); +export default CalcField; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/index.ts index 63bdc3cb4..4d542b67f 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/index.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/index.ts @@ -22,7 +22,7 @@ import ChartIFrameContainerResourceLoader from './ChartIFrameContainerResourceLo import ChartPluginLoader from './ChartPluginLoader'; const ChartTools = { - ChartIFrame: ChartIFrameContainer, + ChartIFrameContainer, ChartPluginLoader, ChartIFrameContainerDispatcher, ChartIFrameContainerResourceLoader, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx index 83f5e6f35..539a9ae13 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartWorkbench/ChartWorkbench.tsx @@ -16,6 +16,7 @@ * limitations under the License. */ +import ChartAggregationContext from 'app/pages/ChartWorkbenchPage/contexts/ChartAggregationContext'; import ChartDatasetContext from 'app/pages/ChartWorkbenchPage/contexts/ChartDatasetContext'; import ChartDataViewContext from 'app/pages/ChartWorkbenchPage/contexts/ChartDataViewContext'; import TimeConfigContext from 'app/pages/ChartWorkbenchPage/contexts/TimeConfigContext'; @@ -38,10 +39,12 @@ const ChartWorkbench: FC<{ dataview?: ChartDataView; chartConfig?: ChartConfig; chart?: Chart; + aggregation?: boolean; header?: { name?: string; onSaveChart?: () => void; onGoBack?: () => void; + onChangeAggregation?: (state: boolean) => void; }; onChartChange: (c: Chart) => void; onChartConfigChange: (type, payload) => void; @@ -52,6 +55,7 @@ const ChartWorkbench: FC<{ dataview, chartConfig, chart, + aggregation, header, onChartChange, onChartConfigChange, @@ -59,34 +63,36 @@ const ChartWorkbench: FC<{ }) => { const language = useSelector(languageSelector); const dateFormat = useSelector(dateFormatSelector); - return ( - - - - - {header && ( - - )} - - - - - - - + + + + + + {header && ( + + )} + + + + + + + + ); }, (prev, next) => diff --git a/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartAggregationContext.ts b/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartAggregationContext.ts new file mode 100644 index 000000000..42f99f573 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/contexts/ChartAggregationContext.ts @@ -0,0 +1,27 @@ +/** + * 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 { createContext } from 'react'; + +const ChartAggregationContext = createContext<{ + aggregation: boolean | undefined; +}>({ + aggregation: true, +}); + +export default ChartAggregationContext; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/Chart.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/Chart.ts index 1cd78c42a..d90b8c1bc 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/Chart.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/Chart.ts @@ -38,6 +38,7 @@ class Chart extends DatartChartBase { dependency: string[] = []; isISOContainer: boolean | string = false; + _useIFrame: boolean = true; _state: ChartStatus = 'init'; _stateHistory: ChartStatus[] = []; _hooks?: ChartEventBroker; @@ -55,13 +56,10 @@ class Chart extends DatartChartBase { constructor(id: string, name: string, icon?: string, requirements?: []) { super(); - const fontIcon = `iconfont icon-${ - !icon ? 'fsux_tubiao_zhuzhuangtu1' : icon - }`; this.meta = { id, name, - icon: fontIcon, + icon: icon, requirements, }; this.state = 'init'; @@ -118,7 +116,7 @@ class Chart extends DatartChartBase { return series?.data?.valueColName || series.seriesName; } - private getValue( + protected getValue( configs: ChartStyleSectionConfig[] = [], paths?: string[], targetKey?, diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartEventBroker.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartEventBroker.ts index 4d50a05fc..88f272918 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartEventBroker.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartEventBroker.ts @@ -17,6 +17,7 @@ */ import { ChartLifecycle } from 'app/types/ChartLifecycle'; +import { Debugger } from 'utils/debugger'; import Chart from './Chart'; type BrokerContext = { @@ -28,7 +29,6 @@ type BrokerContext = { type HooksEvent = 'mounted' | 'updated' | 'resize' | 'unmount'; -// TODO(Stephen): remove to Chart Tool Folder class ChartEventBroker { private _listeners: Map = new Map(); private _chart?: Chart; @@ -49,6 +49,7 @@ class ChartEventBroker { if (!this._listeners.has(event) || !this._listeners.get(event)) { return; } + this.invokeEvent(event, options, context); } @@ -83,9 +84,15 @@ class ChartEventBroker { } } - private safeInvoke(event, options, context) { + private safeInvoke(event: HooksEvent, options: any, context?: BrokerContext) { try { - this._listeners.get(event)?.call?.(this._chart, options, context); + Debugger.instance.measure( + `ChartEventBroker | ${event} `, + () => { + this._listeners.get(event)?.call?.(this._chart, options, context); + }, + false, + ); } catch (e) { console.error(`ChartEventBroker | ${event} exception ----> `, e); } finally { diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts index 409278085..d62ead2e4 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartFilterCondition.ts @@ -162,8 +162,8 @@ export class ConditionBuilder { return this.condition; } - asRelativeTime(name?, sqlType?) { - this.condition.type = FilterConditionType.RelativeTime; + asRecommendTime(name?, sqlType?) { + this.condition.type = FilterConditionType.RecommendTime; this.condition.operator = FilterSqlOperator.Equal; this.condition.name = name || this.condition.name; this.condition.visualType = sqlType || this.condition.visualType; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartHttpRequest.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartHttpRequest.ts index 717fe4785..08829f946 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartHttpRequest.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartHttpRequest.ts @@ -17,17 +17,23 @@ */ import { ChartDatasetPageInfo } from 'app/types/ChartDataset'; +import { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { getStyleValue } from 'app/utils/chartHelper'; -import { formatTime } from 'app/utils/time'; +import { + formatTime, + getTime, + recommendTimeRangeConverter, +} from 'app/utils/time'; import { FILTER_TIME_FORMATTER_IN_QUERY } from 'globalConstants'; -import { IsKeyIn } from 'utils/object'; +import { isEmptyArray, IsKeyIn } from 'utils/object'; import { AggregateFieldActionType, ChartDataSectionConfig, ChartDataSectionField, ChartDataSectionType, ChartStyleSectionConfig, - FilterValueOption, + FilterConditionType, + RelationFilterValue, SortActionType, } from '../../../types/ChartConfig'; import ChartDataView from '../../../types/ChartDataView'; @@ -42,7 +48,11 @@ export type ChartRequest = { functionColumns?: Array<{ alias: string; snippet: string }>; limit?: any; nativeQuery?: boolean; - orders: Array<{ column: string; operator: SortActionType }>; + orders: Array<{ + column: string; + operator: SortActionType; + aggOperator?: AggregateFieldActionType; + }>; pageInfo?: ChartDatasetPageInfo; columns?: string[]; script?: boolean; @@ -87,6 +97,8 @@ export class ChartDataRequestBuilder { pageInfo: ChartDatasetPageInfo; dataView: ChartDataView; script: boolean; + aggregation?: boolean; + private extraSorters: ChartRequest['orders'] = []; constructor( dataView: ChartDataView, @@ -94,20 +106,25 @@ export class ChartDataRequestBuilder { settingConfigs?: ChartStyleSectionConfig[], pageInfo?: ChartDatasetPageInfo, script?: boolean, + aggregation?: boolean, ) { this.dataView = dataView; this.chartDataConfigs = dataConfigs || []; this.charSettingConfigs = settingConfigs || []; this.pageInfo = pageInfo || {}; this.script = script || false; + this.aggregation = aggregation; } - private buildAggregators() { const aggColumns = this.chartDataConfigs.reduce( (acc, cur) => { if (!cur.rows) { return acc; } + + if (this.aggregation === false) { + return acc; + } if ( cur.type === ChartDataSectionType.AGGREGATE || cur.type === ChartDataSectionType.SIZE || @@ -115,6 +132,17 @@ export class ChartDataRequestBuilder { ) { return acc.concat(cur.rows); } + + if ( + cur.type === ChartDataSectionType.MIXED && + cur.rows?.findIndex( + v => v.type === ChartDataViewFieldType.NUMERIC, + ) !== -1 + ) { + return acc.concat( + cur.rows.filter(v => v.type === ChartDataViewFieldType.NUMERIC), + ); + } return acc; }, [], @@ -148,61 +176,80 @@ export class ChartDataRequestBuilder { return true; }) .map(col => col); + return this.normalizeFilters(fields); + } - const _transformToRequest = (fields: ChartDataSectionField[]) => { - const _convertTime = (visualType, value) => { - if (visualType !== 'DATE') { - return value; - } - return formatTime(value, FILTER_TIME_FORMATTER_IN_QUERY); - }; + private normalizeFilters = (fields: ChartDataSectionField[]) => { + const _timeConverter = (visualType, value) => { + if (visualType !== 'DATE') { + return value; + } + if (Boolean(value) && typeof value === 'object' && 'unit' in value) { + const time = getTime(+(value.direction + value.amount), value.unit)( + value.unit, + value.isStart, + ); + return formatTime(time, FILTER_TIME_FORMATTER_IN_QUERY); + } + return formatTime(value, FILTER_TIME_FORMATTER_IN_QUERY); + }; - const convertValues = (field: ChartDataSectionField) => { - if (Array.isArray(field.filter?.condition?.value)) { - return field.filter?.condition?.value - .map(v => { - if (IsKeyIn(v as FilterValueOption, 'key')) { - const listItem = v as FilterValueOption; - if (!listItem.isSelected) { - return undefined; - } - return { - value: listItem.key, - valueType: field.type, - }; + const _transformFieldValues = (field: ChartDataSectionField) => { + const conditionValue = field.filter?.condition?.value; + if (!conditionValue) { + return null; + } + if (Array.isArray(conditionValue)) { + return conditionValue + .map(v => { + if (IsKeyIn(v as RelationFilterValue, 'key')) { + const listItem = v as RelationFilterValue; + if (!listItem.isSelected) { + return undefined; } return { - value: _convertTime(field.filter?.condition?.visualType, v), + value: listItem.key, valueType: field.type, }; - }) - .filter(Boolean) as any[]; - } - const v = field.filter?.condition?.value; - if (!v) { - return null; - } - return [ - { - value: _convertTime(field.filter?.condition?.visualType, v), - valueType: field.type, - }, - ]; - }; - - return fields.map(field => ({ - aggOperator: - field.aggregate === AggregateFieldActionType.NONE - ? null - : field.aggregate, - column: field.colName, - sqlOperator: field.filter?.condition?.operator!, - values: convertValues(field) || [], - })); + } else { + return { + value: _timeConverter(field.filter?.condition?.visualType, v), + valueType: field.type, + }; + } + }) + .filter(Boolean) as any[]; + } + if ( + field?.filter?.condition?.type === FilterConditionType.RecommendTime + ) { + const timeRange = recommendTimeRangeConverter(conditionValue); + return (timeRange || []).map(t => ({ + value: t, + valueType: field.type, + })); + } + return [ + { + value: _timeConverter( + field.filter?.condition?.visualType, + conditionValue, + ), + valueType: field.type, + }, + ]; }; - return _transformToRequest(fields); - } + return fields.map(field => ({ + aggOperator: + field.aggregate === AggregateFieldActionType.NONE + ? null + : field.aggregate, + column: field.colName, + sqlOperator: field.filter?.condition?.operator!, + values: _transformFieldValues(field) || [], + })); + }; private buildOrders() { const sortColumns = this.chartDataConfigs @@ -212,7 +259,8 @@ export class ChartDataRequestBuilder { } if ( cur.type === ChartDataSectionType.GROUP || - cur.type === ChartDataSectionType.AGGREGATE + cur.type === ChartDataSectionType.AGGREGATE || + cur.type === ChartDataSectionType.MIXED ) { return acc.concat(cur.rows); } @@ -224,38 +272,41 @@ export class ChartDataRequestBuilder { [SortActionType.ASC, SortActionType.DESC].includes(col?.sort?.type), ); - return sortColumns.map(aggCol => ({ + const originalSorters = sortColumns.map(aggCol => ({ column: aggCol.colName, operator: aggCol.sort?.type!, - aggOperator: aggCol.aggregate!, + aggOperator: aggCol.aggregate, })); - } - - private buildLimit() { - const settingStyles = this.charSettingConfigs; - return getStyleValue(settingStyles, ['cache', 'panel', 'displayCount']); - } - private buildNativeQuery() { - const settingStyles = this.charSettingConfigs; - return getStyleValue(settingStyles, ['cache', 'panel', 'enableRaw']); + return originalSorters + .reduce((acc, cur) => { + const uniqSorter = sorter => + `${sorter.column}-${ + sorter.aggOperator?.length > 0 ? sorter.aggOperator : '' + }`; + const newSorter = this.extraSorters?.find( + extraSorter => uniqSorter(extraSorter) === uniqSorter(cur), + ); + if (newSorter) { + return acc; + } + return acc.concat([cur]); + }, []) + .concat(this.extraSorters as []) + .filter(sorter => Boolean(sorter?.operator)); } private buildPageInfo() { const settingStyles = this.charSettingConfigs; + const pageSize = getStyleValue(settingStyles, ['paging', 'pageSize']); const enablePaging = getStyleValue(settingStyles, [ 'paging', 'enablePaging', ]); - const pageSize = getStyleValue(settingStyles, ['paging', 'pageSize']); - if (!enablePaging) { - return { - pageSize: Number.MAX_SAFE_INTEGER, - }; - } return { + countTotal: !!enablePaging, pageNo: this.pageInfo?.pageNo, - pageSize: pageSize || 10, + pageSize: pageSize || 1000, }; } @@ -278,9 +329,20 @@ export class ChartDataRequestBuilder { if (!cur.rows) { return acc; } - if (cur.type === ChartDataSectionType.MIXED) { - return acc.concat(cur.rows); + + if (this.aggregation === false) { + if ( + cur.type === ChartDataSectionType.GROUP || + cur.type === ChartDataSectionType.COLOR || + cur.type === ChartDataSectionType.AGGREGATE || + cur.type === ChartDataSectionType.SIZE || + cur.type === ChartDataSectionType.INFO || + cur.type === ChartDataSectionType.MIXED + ) { + return acc.concat(cur.rows); + } } + return acc; }, [], @@ -289,7 +351,14 @@ export class ChartDataRequestBuilder { } private buildViewConfigs() { - return transformToViewConfig(this.dataView?.view?.config); + return transformToViewConfig(this.dataView?.config); + } + + public addExtraSorters(sorters: ChartRequest['orders']) { + if (!isEmptyArray(sorters)) { + this.extraSorters = this.extraSorters.concat(sorters!); + } + return this; } public build(): ChartRequest { @@ -304,10 +373,6 @@ export class ChartDataRequestBuilder { columns: this.buildSelectColumns(), script: this.script, ...this.buildViewConfigs(), - // expired: 0, - // flush: false, - // limit: this.buildLimit(), - // nativeQuery: this.buildNativeQuery(), }; } @@ -317,12 +382,32 @@ export class ChartDataRequestBuilder { if (!cur.rows) { return acc; } + if (this.aggregation === false) { + return acc; + } if ( cur.type === ChartDataSectionType.GROUP || cur.type === ChartDataSectionType.COLOR ) { return acc.concat(cur.rows); } + if ( + cur.type === ChartDataSectionType.MIXED && + !cur.rows?.every( + v => + v.type !== ChartDataViewFieldType.DATE && + v.type !== ChartDataViewFieldType.STRING, + ) + ) { + //zh: 判断数据中是否含有 DATE 和 STRING 类型 en: Determine whether the data contains DATE and STRING types + return acc.concat( + cur.rows.filter( + v => + v.type === ChartDataViewFieldType.DATE || + v.type === ChartDataViewFieldType.STRING, + ), + ); + } return acc; }, [], diff --git a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts index 6e080c6e4..a56ccf451 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/models/ChartManager.ts @@ -19,6 +19,7 @@ import WidgetPlugins from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartGraph'; import ChartTools from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools'; import { getChartPluginPaths } from 'app/utils/fetch'; +import { Debugger } from 'utils/debugger'; import { CloneValueDeep } from 'utils/object'; import Chart from './Chart'; @@ -40,12 +41,13 @@ const { RoseChart, ScoreChart, MingXiTableChart, - FenZuTableChart, NormalOutlineMapChart, WordCloudChart, ScatterOutlineMapChart, BasicGaugeChart, WaterfallChart, + BasicRichText, + PivotSheetChart, } = WidgetPlugins; class ChartManager { @@ -66,7 +68,9 @@ class ChartManager { return; } const pluginsPaths = await getChartPluginPaths(); - return await this._loadCustomizeCharts(pluginsPaths); + Debugger.instance.measure('Plugin Charts | ', async () => { + await this._loadCustomizeCharts(pluginsPaths); + }); } public getAllCharts(): Chart[] { @@ -99,8 +103,8 @@ class ChartManager { private _basicCharts(): Chart[] { return [ - new FenZuTableChart(), new MingXiTableChart(), + new PivotSheetChart(), new ScoreChart(), new ClusterColumnChart(), new ClusterBarChart(), @@ -122,6 +126,7 @@ class ChartManager { new NormalOutlineMapChart(), new ScatterOutlineMapChart(), new BasicGaugeChart(), + new BasicRichText(), ]; } } diff --git a/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts b/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts index 50a68f220..71907a5b3 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/slice/workbenchSlice.ts @@ -72,6 +72,7 @@ export type BackendChartConfig = { chartConfig: string; chartGraphId: string; computedFields: ChartDataViewMeta[]; + aggregation: boolean; }; export type WorkbenchState = { @@ -84,6 +85,7 @@ export type WorkbenchState = { shadowChartConfig?: ChartConfig; backendChart?: BackendChart; backendChartId?: string; + aggregation?: boolean; }; const initState: WorkbenchState = { @@ -137,6 +139,11 @@ export const shadowChartConfigSelector = createSelector( wb => wb.shadowChartConfig, ); +export const aggregationSelector = createSelector( + workbenchSelector, + wb => wb.aggregation, +); + // Effects export const initWorkbenchAction = createAsyncThunk( 'workbench/initWorkbenchAction', @@ -245,24 +252,34 @@ export const updateChartConfigAndRefreshDatasetAction = createAsyncThunk( export const refreshDatasetAction = createAsyncThunk( 'workbench/refreshDatasetAction', - async (arg: { pageInfo? }, thunkAPI) => { + async ( + arg: { + pageInfo?; + sorter?: { column: string; operator: string; aggOperator?: string }; + }, + thunkAPI, + ) => { try { const state = thunkAPI.getState() as any; const workbenchState = state.workbench as typeof initState; + if (!workbenchState.currentDataView?.id) { return; } + const builder = new ChartDataRequestBuilder( { ...workbenchState.currentDataView, - view: workbenchState?.backendChart?.view, }, workbenchState.chartConfig?.datas, workbenchState.chartConfig?.settings, arg?.pageInfo, true, + workbenchState.aggregation, ); - const requestParams = builder.build(); + const requestParams = builder + .addExtraSorters(arg?.sorter ? [arg?.sorter as any] : []) + .build(); thunkAPI.dispatch(fetchDataSetAction(requestParams)); } catch (error) { return rejectHandle(error, thunkAPI.rejectWithValue); @@ -270,6 +287,36 @@ export const refreshDatasetAction = createAsyncThunk( }, ); +export const updateRichTextAction = createAsyncThunk( + 'workbench/updateRichTextAction', + async (delta: string | undefined, thunkAPI) => { + try { + const state = thunkAPI.getState() as any; + const workbenchState = state.workbench as typeof initState; + if (!workbenchState.currentDataView?.id) { + return; + } + await thunkAPI.dispatch( + workbenchSlice.actions.updateChartConfig({ + type: 'style', + payload: { + ancestors: [0, 0], + value: { + label: 'delta.richText', + key: 'richText', + default: '', + comType: 'input', + value: delta, + }, + }, + }), + ); + } catch (error) { + return rejectHandle(error, thunkAPI.rejectWithValue); + } + }, +); + export const fetchChartAction = createAsyncThunk( 'workbench/fetchChartAction', async (arg: { chartId?: string; backendChart?: BackendChart }, thunkAPI) => { @@ -291,7 +338,7 @@ export const fetchChartAction = createAsyncThunk( export const updateChartAction = createAsyncThunk( 'workbench/updateChartAction', async ( - arg: { name; viewId; graphId; chartId; index; parentId }, + arg: { name; viewId; graphId; chartId; index; parentId; aggregation }, thunkAPI, ) => { try { @@ -299,6 +346,7 @@ export const updateChartAction = createAsyncThunk( const workbenchState = state.workbench as typeof initState; const stringConfig = JSON.stringify({ + aggregation: arg.aggregation, chartConfig: workbenchState.chartConfig, chartGraphId: arg.graphId, computedFields: workbenchState.currentDataView?.computedFields || [], @@ -399,6 +447,7 @@ const workbenchSlice = createSlice({ return state; } }; + state.chartConfig = chartConfigReducer(state.chartConfig!, { type: action.payload.type, payload: action.payload.payload, @@ -413,6 +462,9 @@ const workbenchSlice = createSlice({ computedFields: action.payload, } as ChartDataView; }, + updateChartAggregation: (state, action: PayloadAction) => { + state.aggregation = action.payload; + }, resetWorkbenchState: (state, action) => { return initState; }, @@ -448,14 +500,22 @@ const workbenchSlice = createSlice({ return; } - const backendChartConfig = + let backendChartConfig = typeof payload.config === 'string' ? JSON.parse(payload.config) : CloneValueDeep(payload.config); + backendChartConfig = backendChartConfig || {}; + + if (backendChartConfig?.aggregation === undefined) { + backendChartConfig.aggregation = true; + } + state.backendChart = { ...payload, config: backendChartConfig, }; + state.aggregation = backendChartConfig.aggregation; + const currentChart = ChartManager.instance().getById( backendChartConfig?.chartGraphId, ); diff --git a/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx b/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx index 9ef8c4ad9..3ed1a7a9c 100644 --- a/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/BoardOverLay.tsx @@ -17,6 +17,7 @@ */ import { CloudDownloadOutlined, ShareAltOutlined } from '@ant-design/icons'; import { Menu, Popconfirm } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { memo, useContext, useMemo } from 'react'; import { BoardContext } from '../contexts/BoardContext'; export interface BoardOverLayProps { @@ -24,8 +25,10 @@ export interface BoardOverLayProps { onBoardToDownLoad: () => void; onShareDownloadData?: () => void; } + export const BoardOverLay: React.FC = memo( ({ onOpenShareLink, onBoardToDownLoad, onShareDownloadData }) => { + const t = useI18NPrefix(`viz.action`); const { allowShare, allowDownload, renderMode } = useContext(BoardContext); const renderList = useMemo( () => [ @@ -33,10 +36,9 @@ export const BoardOverLay: React.FC = memo( key: 'shareLink', icon: , onClick: onOpenShareLink, - disabled: false, render: allowShare && renderMode === 'read', - content: '分享链接', + content: t('share.shareLink'), }, { key: 'downloadData', @@ -47,9 +49,9 @@ export const BoardOverLay: React.FC = memo( content: ( { if (renderMode === 'read') { onBoardToDownLoad?.(); @@ -58,7 +60,7 @@ export const BoardOverLay: React.FC = memo( } }} > - 下载数据 + {t('share.downloadData')} ), }, @@ -67,6 +69,7 @@ export const BoardOverLay: React.FC = memo( onOpenShareLink, allowShare, renderMode, + t, allowDownload, onBoardToDownLoad, onShareDownloadData, diff --git a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx index 8547b7616..5a3763d59 100644 --- a/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/BoardProvider/BoardActionProvider.tsx @@ -18,7 +18,7 @@ import { PageInfo } from 'app/pages/MainPage/pages/ViewPage/slice/types'; import { generateShareLinkAsync } from 'app/utils/fetch'; -import { debounce } from 'lodash'; +import debounce from 'lodash/debounce'; import React, { FC, useContext } from 'react'; import { useDispatch } from 'react-redux'; import { @@ -33,15 +33,22 @@ import { resetControllerAction, widgetsQueryAction, } from '../../pages/Board/slice/asyncActions'; -import { getWidgetDataAsync } from '../../pages/Board/slice/thunk'; +import { + getChartWidgetDataAsync, + getControllerOptions, +} from '../../pages/Board/slice/thunk'; import { Widget } from '../../pages/Board/slice/types'; import { editBoardStackActions } from '../../pages/BoardEditor/slice'; import { editWidgetsQueryAction } from '../../pages/BoardEditor/slice/actions/controlActions'; import { - getEditWidgetDataAsync, + getEditChartWidgetDataAsync, + getEditControllerOptions, toUpdateDashboard, } from '../../pages/BoardEditor/slice/thunk'; -import { getNeedRefreshWidgetsByFilter } from '../../utils/widget'; +import { + getCascadeControllers, + getNeedRefreshWidgetsByController, +} from '../../utils/widget'; export const BoardActionProvider: FC<{ id: string }> = ({ id: boardId, @@ -75,25 +82,37 @@ export const BoardActionProvider: FC<{ id: string }> = ({ dispatch(resetControllerAction({ boardId, renderMode })); } }, 500), - refreshWidgetsByFilter: debounce((widget: Widget) => { + refreshWidgetsByController: debounce((widget: Widget) => { + const controllerIds = getCascadeControllers(widget); + controllerIds.forEach(controlWidgetId => { + if (editing) { + dispatch(getEditControllerOptions(controlWidgetId)); + } else { + dispatch( + getControllerOptions({ + boardId, + widgetId: controlWidgetId, + renderMode, + }), + ); + } + }); if (hasQueryControl) { return; } - const widgetIds = getNeedRefreshWidgetsByFilter(widget); const pageInfo: Partial = { pageNo: 1, }; - widgetIds.forEach(widgetId => { + const chartWidgetIds = getNeedRefreshWidgetsByController(widget); + + chartWidgetIds.forEach(widgetId => { if (editing) { dispatch( - getEditWidgetDataAsync({ - widgetId, - option: { pageInfo }, - }), + getEditChartWidgetDataAsync({ widgetId, option: { pageInfo } }), ); } else { dispatch( - getWidgetDataAsync({ + getChartWidgetDataAsync({ boardId, widgetId, renderMode, diff --git a/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx b/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx index c52eb6691..3f9e73972 100644 --- a/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/TitleHeader.tsx @@ -27,8 +27,17 @@ import { } from '@ant-design/icons'; import { Button, Dropdown, Space } from 'antd'; import { ShareLinkModal } from 'app/components/VizOperationMenu'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import classnames from 'classnames'; -import React, { FC, memo, useCallback, useContext, useState } from 'react'; +import { TITLE_SUFFIX } from 'globalConstants'; +import React, { + FC, + memo, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; import styled from 'styled-components/macro'; import { FONT_SIZE_ICON_SM, @@ -42,8 +51,6 @@ import { BoardContext } from '../contexts/BoardContext'; import { BoardInfoContext } from '../contexts/BoardInfoContext'; import { BoardOverLay } from './BoardOverLay'; -const TITLE_SUFFIX = ['[已归档]', '[未发布]']; - interface TitleHeaderProps { name?: string; publishLoading?: boolean; @@ -60,6 +67,7 @@ const TitleHeader: FC = memo( publishLoading, onPublish, }) => { + const t = useI18NPrefix(`viz.action`); const [showShareLinkModal, setShowShareLinkModal] = useState(false); const { editing, @@ -84,7 +92,11 @@ const TitleHeader: FC = memo( toggleBoardEditor?.(false); }; - const title = `${name || boardName} ${TITLE_SUFFIX[status] || ''}`; + const title = useMemo(() => { + const base = name || boardName; + const suffix = TITLE_SUFFIX[status] ? `[${t(TITLE_SUFFIX[status])}]` : ''; + return base + suffix; + }, [boardName, name, status, t]); const isArchived = status === 0; return ( @@ -102,7 +114,7 @@ const TitleHeader: FC = memo( icon={} onClick={closeBoardEditor} > - 取消 + {t('common.cancel')} = memo( icon={} onClick={onUpdateBoard} > - 保存 + {t('common.save')} > ) : ( @@ -130,7 +142,7 @@ const TitleHeader: FC = memo( loading={publishLoading} onClick={onPublish} > - {status === 1 ? '发布' : '取消发布'} + {status === 1 ? t('publish') : t('unpublish')} )} {allowManage && !isArchived && renderMode === 'read' && ( @@ -139,7 +151,7 @@ const TitleHeader: FC = memo( icon={} onClick={() => toggleBoardEditor?.(true)} > - 编辑 + {t('edit')} )} { 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 2e99692cc..f898e2a35 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 @@ -23,7 +23,7 @@ import React, { useCallback, useContext, useState } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components/macro'; import { PRIMARY } from 'styles/StyleConstants'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import { BoardContext } from '../../../../contexts/BoardContext'; import { editBoardStackActions } from '../../../../pages/BoardEditor/slice'; import { WidgetAllProvider } from '../../../WidgetProvider/WidgetAllProvider'; diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/Controller/CheckboxGroupController.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/Controller/CheckboxGroupController.tsx new file mode 100644 index 000000000..e8943752b --- /dev/null +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/Controller/CheckboxGroupController.tsx @@ -0,0 +1,68 @@ +/** + * 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 { Checkbox, Form } from 'antd'; +import { CheckboxValueType } from 'antd/lib/checkbox/Group'; +import { ControlOption } from 'app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/types'; +import React, { memo, useCallback } from 'react'; +import styled from 'styled-components/macro'; + +export interface CheckboxGroupControllerProps { + options?: ControlOption[]; + value?: CheckboxValueType[]; + placeholder?: string; + onChange: (values) => void; + label?: React.ReactNode; + name?: string; + required?: boolean; +} + +export const CheckboxGroupControllerForm: React.FC = + memo(({ label, name, required, ...rest }) => { + return ( + + + + ); + }); +export const CheckboxGroupSetter: React.FC = memo( + ({ options, onChange, value }) => { + const renderOptions = useCallback(() => { + return (options || []).map(o => ({ + label: o.label ?? o.value, + value: o.value, + })); + }, [options]); + return ( + + + + ); + }, +); +const Wrapper = styled.div` + display: flex; +`; 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 2dbfdcb03..83e5eadc8 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/ControllerWidgetCore.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/ControllerWIdget/ControllerWidgetCore.tsx @@ -28,10 +28,10 @@ import { ControlOption, } from 'app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/types'; import { getControllerDateValues } from 'app/pages/DashBoardPage/utils'; -import { FilterValueOption } from 'app/types/ChartConfig'; +import { RelationFilterValue } from 'app/types/ChartConfig'; import { ControllerFacadeTypes, - RelativeOrExactTime, + TimeFilterValueCategory, } from 'app/types/FilterControlPanel'; import produce from 'immer'; import React, { @@ -43,6 +43,7 @@ import React, { } from 'react'; import styled from 'styled-components/macro'; import { LabelName } from '../WidgetName/WidgetName'; +import { CheckboxGroupControllerForm } from './Controller/CheckboxGroupController'; import { MultiSelectControllerForm } from './Controller/MultiSelectController'; import { NumberControllerForm } from './Controller/NumberController'; import { RadioGroupControllerForm } from './Controller/RadioGroupController'; @@ -62,7 +63,7 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { const { data: { rows }, } = useContext(WidgetDataContext); - const { widgetUpdate, refreshWidgetsByFilter } = + const { widgetUpdate, refreshWidgetsByController } = useContext(BoardActionContext); const { config, type: facadeType } = useMemo( @@ -103,7 +104,7 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { const dataRows = rows?.flat(2) || []; if (valueOptionType === 'common') { return dataRows.map(ele => { - const item: FilterValueOption = { + const item: RelationFilterValue = { key: ele, label: ele, // children? @@ -137,7 +138,7 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { ).config.controllerValues = _values; }); widgetUpdate(nextWidget); - refreshWidgetsByFilter(nextWidget); + refreshWidgetsByController(nextWidget); }; // const onSqlOperatorAndValues = useCallback( // (sql: FilterSqlOperator, values: any[]) => { @@ -158,11 +159,11 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { const nextFilterDate: ControllerDate = { ...controllerDate!, startTime: { - relativeOrExact: RelativeOrExactTime.Exact, + relativeOrExact: TimeFilterValueCategory.Exact, exactValue: timeValues?.[0], }, endTime: { - relativeOrExact: RelativeOrExactTime.Exact, + relativeOrExact: TimeFilterValueCategory.Exact, exactValue: timeValues?.[1], }, }; @@ -172,9 +173,9 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { ).config.controllerDate = nextFilterDate; }); widgetUpdate(nextWidget); - refreshWidgetsByFilter(nextWidget); + refreshWidgetsByController(nextWidget); }, - [controllerDate, refreshWidgetsByFilter, widget, widgetUpdate], + [controllerDate, refreshWidgetsByController, widget, widgetUpdate], ); const onTimeChange = useCallback( @@ -182,7 +183,7 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { const nextFilterDate: ControllerDate = { ...controllerDate!, startTime: { - relativeOrExact: RelativeOrExactTime.Exact, + relativeOrExact: TimeFilterValueCategory.Exact, exactValue: value, }, }; @@ -192,9 +193,9 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { ).config.controllerDate = nextFilterDate; }); widgetUpdate(nextWidget); - refreshWidgetsByFilter(nextWidget); + refreshWidgetsByController(nextWidget); }, - [controllerDate, refreshWidgetsByFilter, widget, widgetUpdate], + [controllerDate, refreshWidgetsByController, widget, widgetUpdate], ); const control = useMemo(() => { @@ -223,7 +224,16 @@ export const ControllerWidgetCore: React.FC<{}> = memo(() => { label={leftControlLabel} /> ); - + case ControllerFacadeTypes.CheckboxGroup: + form.setFieldsValue({ value: controllerValues }); + return ( + + ); case ControllerFacadeTypes.Slider: form.setFieldsValue({ value: controllerValues?.[0] }); const step = config.sliderConfig?.step || 1; 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 491c7c4fe..0e9281ed3 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/DataChartWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/DataChartWidget/index.tsx @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import useResizeObserver from 'app/hooks/useResizeObserver'; +import { useCacheWidthHeight } from 'app/hooks/useCacheWidthHeight'; import ChartIFrameContainer from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTools/ChartIFrameContainer'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import ChartManager from 'app/pages/ChartWorkbenchPage/models/ChartManager'; @@ -33,7 +33,6 @@ import React, { useEffect, useMemo, useRef, - useState, } from 'react'; import styled from 'styled-components/macro'; export interface DataChartWidgetProps {} @@ -43,8 +42,7 @@ export const DataChartWidget: React.FC = memo(() => { const widget = useContext(WidgetContext); const { id: widgetId } = widget; const { widgetChartClick } = useContext(WidgetMethodContext); - const [cacheW, setCacheW] = useState(200); - const [cacheH, setCacheH] = useState(200); + const { ref, cacheW, cacheH } = useCacheWidthHeight(); const widgetRef = useRef(widget); useEffect(() => { widgetRef.current = widget; @@ -87,23 +85,25 @@ export const DataChartWidget: React.FC = memo(() => { } }, [chartClick, dataChart]); - const onResize = useCallback(() => {}, []); - - const { - ref, - width = 200, - height = 200, - } = useResizeObserver({ - refreshMode: 'debounce', - refreshRate: 120, - onResize, - }); - useEffect(() => { - if (width !== 0 && height !== 0) { - setCacheW(width); - setCacheH(height); + const widgetSpecialConfig = useMemo(() => { + let linkFields: string[] = []; + let jumpField: string = ''; + const { jumpConfig, linkageConfig } = widget.config; + if (linkageConfig?.open) { + linkFields = widget?.relations + .filter(re => re.config.type === 'widgetToWidget') + .map(item => item.config.widgetToWidget?.triggerColumn as string); } - }, [width, height]); + if (jumpConfig?.open) { + jumpField = jumpConfig?.field?.jumpFieldName as string; + } + + return { + linkFields, + jumpField, + }; + }, [widget]); + const dataset = useMemo( () => ({ columns: data.columns, @@ -131,28 +131,38 @@ export const DataChartWidget: React.FC = memo(() => { dataset={dataset} chart={chart} config={dataChart.config.chartConfig as ChartConfig} - style={{ width: cacheW, height: cacheH }} + width={cacheW} + height={cacheH} containerId={widgetId} + widgetSpecialConfig={widgetSpecialConfig} /> ); } catch (error) { return has err in {``}; } - }, [cacheH, cacheW, chart, dataChart, dataset, widgetId]); + }, [ + cacheH, + cacheW, + chart, + dataChart, + dataset, + widgetSpecialConfig, + widgetId, + ]); return ( - {chartFrame} + {chartFrame} ); }); +const ChartFrameBox = styled.div` + position: absolute; + height: 100%; + width: 100%; + overflow: hidden; +`; const Wrap = styled.div` display: flex; flex: 1; - width: 100%; - height: 100%; - overflow: hidden; - & div { - max-width: 100%; - max-height: 100%; - } + 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 2be25f9af..481e3952c 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 @@ -79,7 +79,7 @@ const IframeWidget: React.FC<{}> = () => { frameBorder="0" allow="autoplay" style={{ width: '100%', height: '100%' }} - /> + > ); }; 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 6b459466b..19e8592b5 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 @@ -15,29 +15,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +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, - Widget, - WidgetInfo, -} from 'app/pages/DashBoardPage/pages/Board/slice/types'; -import { convertImageUrl } from 'app/pages/DashBoardPage/utils'; -import React, { useMemo } from 'react'; +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'; -export interface ImageWidgetProps { - widgetConfig: Widget; - widgetInfo: WidgetInfo; -} const widgetSize: React.CSSProperties = { width: '100%', height: '100%', }; -const ImageWidget: React.FC = ({ widgetConfig }) => { - const { imageConfig } = widgetConfig.config.content as MediaWidgetContent; - +const ImageWidget: React.FC<{}> = () => { + const widget = useContext(WidgetContext); + const { editing } = useContext(WidgetInfoContext); + const { widgetUpdate } = useContext(BoardActionContext); + const { imageConfig } = widget.config.content as MediaWidgetContent; + const widgetBgImage = widget.config.background.image; const [rect, refDom] = useClientRect(32); - + const t = useI18NPrefix(`viz.board.setting`); const widthBigger = useMemo(() => { return rect.width >= rect.height; }, [rect]); @@ -53,21 +54,41 @@ const ImageWidget: React.FC = ({ widgetConfig }) => { height: 'auto', }; }, [widthBigger]); + const onChange = useCallback( + value => { + const nextWidget = produce(widget, draft => { + draft.config.background.image = value; + }); + widgetUpdate(nextWidget); + }, + [widget, widgetUpdate], + ); const imageSize = useMemo(() => { return imageConfig?.type === 'IMAGE_RATIO' ? imageRatioCss : widgetSize; }, [imageRatioCss, imageConfig?.type]); - - return ( - - {imageConfig?.src && ( - - )} - - ); + const renderImage = useMemo(() => { + return editing ? ( + + ) : widgetBgImage ? null : ( + + ); + }, [editing, onChange, t, widgetBgImage]); + return {renderImage}; }; export default ImageWidget; const Wrap = styled.div` + display: flex; + align-items: center; + justify-content: center; width: 100%; height: 100%; + + .ant-upload-list { + display: none; + } `; diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/Formats.ts b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/Formats.ts index 6ced5aa1b..828929094 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/Formats.ts +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/RichTextWidget/Formats.ts @@ -31,4 +31,9 @@ export const Formats = [ 'calcfield', 'mention', 'image', + 'size', + 'background', + 'font', + 'align', + 'code-block', ]; diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/VideoWidget/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/VideoWidget/index.tsx index 2d9213bcf..54b3352e0 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/VideoWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/VideoWidget/index.tsx @@ -110,7 +110,7 @@ const VideoWidget: React.FC = () => { src={srcWithParams} frameBorder="0" style={{ width: '100%', height: '100%' }} - /> + > ); } diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/index.tsx index 41a0b92ed..76671c8da 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetCore/MediaWidget/index.tsx @@ -33,7 +33,7 @@ export const MediaWidget: React.FC<{}> = memo(() => { case 'richText': return ; case 'image': - return ; + return ; case 'video': return ; case 'iframe': diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetProvider/WidgetMethodProvider.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetProvider/WidgetMethodProvider.tsx index 5327369c5..19d52c387 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetProvider/WidgetMethodProvider.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetProvider/WidgetMethodProvider.tsx @@ -18,7 +18,7 @@ import { ExclamationCircleOutlined } from '@ant-design/icons'; import { Modal } from 'antd'; -import { PageInfo } from 'app/pages/MainPage/pages/ViewPage/slice/types'; +import usePrefixI18N from 'app/hooks/useI18NPrefix'; import { urlSearchTransfer } from 'app/pages/MainPage/pages/VizPage/utils'; import { ChartMouseEventParams } from 'app/types/DatartChartBase'; import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; @@ -26,7 +26,6 @@ import React, { FC, useCallback, useContext } from 'react'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router'; import { BoardContext } from '../../contexts/BoardContext'; -import { WidgetContext } from '../../contexts/WidgetContext'; import { WidgetMethodContext, WidgetMethodContextProps, @@ -34,7 +33,7 @@ import { import { boardActions } from '../../pages/Board/slice'; import { getChartWidgetDataAsync, - getWidgetDataAsync, + getWidgetData, } from '../../pages/Board/slice/thunk'; import { BoardLinkFilter, @@ -42,6 +41,7 @@ import { WidgetContentChartType, WidgetType, } from '../../pages/Board/slice/types'; +import { jumpTypes } from '../../pages/BoardEditor/components/SettingJumpModal/config'; import { editBoardStackActions, editDashBoardInfoActions, @@ -55,7 +55,7 @@ import { import { editWidgetsQueryAction } from '../../pages/BoardEditor/slice/actions/controlActions'; import { getEditChartWidgetDataAsync, - getEditWidgetDataAsync, + getEditWidgetData, } from '../../pages/BoardEditor/slice/thunk'; import { widgetActionType } from '../WidgetToolBar/config'; @@ -64,8 +64,9 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ widgetId, children, }) => { + const t = usePrefixI18N('viz.widget.action'); const { boardId, editing, renderMode, orgId } = useContext(BoardContext); - const widget = useContext(WidgetContext); + const dispatch = useDispatch(); const history = useHistory(); @@ -74,10 +75,9 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ (type: WidgetType, wid: string) => { if (type === 'container') { confirm({ - // TODO i18n - title: '确认删除', + title: t('confirmDel'), icon: , - content: '该组件内的组件也会被删除,确认是否删除?', + content: t('ContainerConfirmDel'), onOk() { dispatch(editBoardStackActions.deleteWidgets([wid])); }, @@ -91,13 +91,12 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ if (type === 'reset') { dispatch(editBoardStackActions.changeBoardHasResetControl(false)); } - dispatch(editBoardStackActions.deleteWidgets([wid])); - if (type === 'controller') { dispatch(editWidgetsQueryAction({ boardId })); } + dispatch(editBoardStackActions.deleteWidgets([wid])); }, - [dispatch, boardId], + [dispatch, t, boardId], ); const onWidgetEdit = useCallback( (widget: Widget, wid: string) => { @@ -162,11 +161,11 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ [dispatch], ); const onWidgetGetData = useCallback( - (boardId: string, widgetId: string) => { + (boardId: string, widget: Widget) => { if (editing) { - dispatch(getEditWidgetDataAsync({ widgetId })); + dispatch(getEditWidgetData({ widget })); } else { - dispatch(getWidgetDataAsync({ boardId, widgetId, renderMode })); + dispatch(getWidgetData({ boardId, widget, renderMode })); } }, [dispatch, editing, renderMode], @@ -246,18 +245,56 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ ); setTimeout(() => { linkRelations.forEach(link => { - onWidgetGetData(boardId, link.targetId); + if (editing) { + dispatch( + getEditChartWidgetDataAsync({ + widgetId: link.targetId, + option: { + pageInfo: { pageNo: 1 }, + }, + }), + ); + } else { + dispatch( + getChartWidgetDataAsync({ + boardId, + widgetId: link.targetId, + renderMode, + option: { + pageInfo: { pageNo: 1 }, + }, + }), + ); + } }); }, 60); }, - [onToggleLinkage, onChangeBoardFilter, onWidgetGetData, boardId], + [ + onToggleLinkage, + onChangeBoardFilter, + editing, + dispatch, + boardId, + renderMode, + ], ); const toLinkingWidgets = useCallback( (widget: Widget, params: ChartMouseEventParams) => { - const linkRelations = widget.relations.filter( - re => re.config.type === 'widgetToWidget', - ); + const { componentType, seriesType, seriesName } = params; + const isTableHandle = componentType === 'table' && seriesType === 'body'; + + const linkRelations = widget.relations.filter(re => { + const { + config: { type, widgetToWidget }, + } = re; + if (type !== 'widgetToWidget') return false; + if (isTableHandle) { + if (widgetToWidget?.triggerColumn === seriesName) return true; + return false; + } + return true; + }); const boardFilters = linkRelations.map(re => { let linkageFieldName: string = @@ -294,20 +331,48 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ onToggleLinkage(true); setTimeout(() => { boardFilters.forEach(f => { - onWidgetGetData(boardId, f.linkerWidgetId); + if (editing) { + dispatch( + getEditChartWidgetDataAsync({ + widgetId: f.linkerWidgetId, + option: { + pageInfo: { pageNo: 1 }, + }, + }), + ); + } else { + dispatch( + getChartWidgetDataAsync({ + boardId, + widgetId: f.linkerWidgetId, + renderMode, + option: { + pageInfo: { pageNo: 1 }, + }, + }), + ); + } }); }, 60); }, - [boardId, dispatch, editing, onToggleLinkage, onWidgetGetData, widgetId], + [boardId, dispatch, editing, onToggleLinkage, renderMode, widgetId], ); const clickJump = useCallback( (values: { widget: Widget; params: ChartMouseEventParams }) => { const { widget, params } = values; const jumpConfig = widget.config?.jumpConfig; + const targetType = jumpConfig?.targetType || jumpTypes[0].value; + const URL = jumpConfig?.URL || ''; + const queryName = jumpConfig?.queryName || ''; const targetId = jumpConfig?.target?.relId; const jumpFieldName: string = jumpConfig?.field?.jumpFieldName || ''; + if ( + params.componentType === 'table' && + jumpFieldName !== params.seriesName + ) + return; - if (typeof jumpConfig?.filter === 'object') { + if (typeof jumpConfig?.filter === 'object' && targetType === 'INTERNAL') { const searchParamsStr = urlSearchTransfer.toUrlString({ [jumpConfig?.filter?.filterId]: (params?.data?.rowData?.[jumpFieldName] as string) || '', @@ -317,22 +382,28 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ `/organizations/${orgId}/vizs/${targetId}?${searchParamsStr}`, ); } + } else if (targetType === 'URL') { + let jumpUrl; + if (URL.indexOf('?') > -1) { + jumpUrl = `${URL}&${queryName}=${params?.data?.rowData?.[jumpFieldName]}`; + } else { + jumpUrl = `${URL}?${queryName}=${params?.data?.rowData?.[jumpFieldName]}`; + } + window.location.href = jumpUrl; } }, [history, orgId], ); const getTableChartData = useCallback( - (options: { widget: Widget; params: any }) => { + (options: { widget: Widget; params: any; sorters?: any[] }) => { const { params } = options; - const pageInfo: Partial = { - pageNo: params.value.page, - }; if (editing) { dispatch( getEditChartWidgetDataAsync({ widgetId, option: { - pageInfo, + pageInfo: params?.pageInfo, + sorters: params?.sorters, }, }), ); @@ -343,7 +414,8 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ widgetId, renderMode, option: { - pageInfo, + pageInfo: params?.pageInfo, + sorters: params?.sorters, }, }), ); @@ -360,7 +432,7 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ case 'info': break; case 'refresh': - onWidgetGetData(boardId, widgetId); + onWidgetGetData(boardId, widget); break; case 'delete': onWidgetDelete(widget.config.type, widgetId); @@ -400,10 +472,22 @@ export const WidgetMethodProvider: FC<{ widgetId: string }> = ({ const widgetChartClick = useCallback( (widget: Widget, params: ChartMouseEventParams) => { - // table 分页 - if (params?.seriesType === 'table' && params?.seriesName === 'paging') { - // table 分页逻辑 - getTableChartData({ widget, params }); + if ( + params.componentType === 'table' && + params.seriesType === 'paging-sort-filter' + ) { + getTableChartData({ + widget, + params: { + pageInfo: { pageNo: params?.value?.pageNo }, + sorters: [ + { + column: params?.seriesName!, + operator: (params?.value as any)?.direction, + }, + ], + }, + }); return; } // jump diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/WidgetActionDropdown.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/WidgetActionDropdown.tsx index f58bd4798..e951ff04c 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/WidgetActionDropdown.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/WidgetActionDropdown.tsx @@ -27,15 +27,14 @@ import { SyncOutlined, } from '@ant-design/icons'; import { Button, Dropdown, Menu } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { memo, useCallback, useContext, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { selectDataChartById } from '../..//pages/Board/slice/selector'; import { BoardContext } from '../../contexts/BoardContext'; +import { WidgetChartContext } from '../../contexts/WidgetChartContext'; import { WidgetMethodContext } from '../../contexts/WidgetMethodContext'; import { Widget } from '../../pages/Board/slice/types'; import { getWidgetActionList, - TriggerChartIds, WidgetActionListItem, widgetActionType, } from './config'; @@ -48,93 +47,82 @@ export const WidgetActionDropdown: React.FC = memo( ({ widget }) => { const { editing: boardEditing } = useContext(BoardContext); const { onWidgetAction } = useContext(WidgetMethodContext); - const dataChart = useSelector(state => - selectDataChartById(state, widget?.datachartId), - ); - const IsSupportTrigger = useMemo( - () => TriggerChartIds.includes(dataChart?.config.chartGraphId), - [dataChart], - ); - + const dataChart = useContext(WidgetChartContext)!; + const t = useI18NPrefix(`viz.widget.action`); const menuClick = useCallback( ({ key }) => { onWidgetAction(key, widget); }, [onWidgetAction, widget], ); - const getAllList = () => { + const getAllList = useCallback(() => { const allWidgetActionList: WidgetActionListItem[] = [ { key: 'refresh', - label: '同步数据', + label: t('refresh'), icon: , }, { key: 'fullScreen', - label: '全屏', + label: t('fullScreen'), icon: , }, { key: 'edit', - label: '编辑', + label: t('edit'), icon: , }, { key: 'delete', - label: '删除', + label: t('delete'), icon: , danger: true, }, { key: 'info', - label: '信息', + label: t('info'), icon: , }, { key: 'makeLinkage', - label: '联动设置', + label: t('makeLinkage'), icon: , divider: true, }, { key: 'closeLinkage', - label: '关闭联动', + label: t('closeLinkage'), icon: , danger: true, }, { key: 'makeJump', - label: '跳转设置', + label: t('makeJump'), icon: , divider: true, }, { key: 'closeJump', - label: '关闭跳转', + label: t('closeJump'), icon: , danger: true, }, ]; return allWidgetActionList; - }; + }, [t]); const actionList = useMemo(() => { return ( getWidgetActionList({ allList: getAllList(), widget, boardEditing, + chartGraphId: dataChart?.config.chartGraphId, }) || [] ); - }, [boardEditing, widget]); + }, [boardEditing, dataChart?.config.chartGraphId, getAllList, widget]); const dropdownList = useMemo(() => { const menuItems = actionList.map(item => { - if ( - (item.key === 'makeLinkage' || item.key === 'makeJump') && - !IsSupportTrigger - ) - return null; - return ( {item.divider && } @@ -151,7 +139,7 @@ export const WidgetActionDropdown: React.FC = memo( }); return {menuItems}; - }, [actionList, menuClick, IsSupportTrigger]); + }, [actionList, menuClick]); return ( []; widget: Widget; boardEditing: boolean; + chartGraphId?: string; }) => { - const { widget, allList, boardEditing } = opt; + const { widget, allList, boardEditing, chartGraphId } = opt; const widgetType = widget.config.type; if (boardEditing) { if (widget.config.type === 'chart') { - return getEditChartActionList({ allList, widget }); + return getEditChartActionList({ allList, widget, chartGraphId }); } else { return allList.filter(item => widgetActionMap.edit[widgetType].includes(item.key), @@ -110,26 +113,26 @@ export const getWidgetActionList = (opt: { export const getEditChartActionList = (opt: { allList: WidgetActionListItem[]; widget: Widget; + chartGraphId?: string; }) => { - const { widget, allList } = opt; + const { widget, allList, chartGraphId } = opt; const widgetType = widget.config.type; const curChartItems: widgetActionType[] = widgetActionMap.edit[widgetType].slice(); - // TODO 判断哪些 chart 可以添加跳转 和联动 暂时用true 代替 - let chartCanMakeJump = true; - let chartCanMakeLink = true; - if (chartCanMakeLink) { + const isTrigger = SupportTriggerChartIds.includes(chartGraphId as string); + + if (isTrigger) { + // Linkage curChartItems.push('makeLinkage'); - } - if (widget.config.linkageConfig?.open) { - curChartItems.push('closeLinkage'); - } - if (chartCanMakeJump) { + if (widget.config.linkageConfig?.open) { + curChartItems.push('closeLinkage'); + } + // Jump curChartItems.push('makeJump'); - } - if (widget.config.jumpConfig?.open) { - curChartItems.push('closeJump'); + if (widget.config.jumpConfig?.open) { + curChartItems.push('closeJump'); + } } return allList diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/index.tsx b/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/index.tsx index bde7fb63b..5ad62c426 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetToolBar/index.tsx @@ -20,11 +20,12 @@ import { ClockCircleOutlined, LinkOutlined, SyncOutlined, + WarningTwoTone, } from '@ant-design/icons'; -import { Space, Tooltip } from 'antd'; +import { Button, Space, Tooltip } from 'antd'; import React, { FC, useContext } from 'react'; import styled from 'styled-components'; -import { PRIMARY } from 'styles/StyleConstants'; +import { ERROR, PRIMARY } from 'styles/StyleConstants'; import { BoardContext } from '../../contexts/BoardContext'; import { WidgetContext } from '../../contexts/WidgetContext'; import { WidgetInfoContext } from '../../contexts/WidgetInfoContext'; @@ -36,7 +37,8 @@ interface WidgetToolBarProps {} const WidgetToolBar: FC = () => { const { boardType, editing: boardEditing } = useContext(BoardContext); - const { loading, inLinking, rendered } = useContext(WidgetInfoContext); + const { loading, inLinking, rendered, errInfo } = + useContext(WidgetInfoContext); const widget = useContext(WidgetContext); const { onClearLinkage } = useContext(WidgetMethodContext); const ssp = e => { @@ -49,7 +51,10 @@ const WidgetToolBar: FC = () => { if (!showTypes.includes(widgetType)) return null; return rendered ? null : ( - + } + type="link" + /> ); }; @@ -57,7 +62,12 @@ const WidgetToolBar: FC = () => { const widgetType = widget.config.type; const showTypes: WidgetType[] = ['chart', 'controller']; if (!showTypes.includes(widgetType)) return null; - return loading ? : null; + return loading ? ( + } + type="link" + /> + ) : null; }; const linkageIcon = () => { if (inLinking) { @@ -72,11 +82,35 @@ const WidgetToolBar: FC = () => { } else { return widget.config?.linkageConfig?.open ? ( - + } + type="link" + /> ) : null; } }; + const renderErrorIcon = (errInfo?: string) => { + if (!errInfo) return null; + const renderTitle = errInfo => { + if (typeof errInfo !== 'string') return 'object'; + return ( + + {errInfo} + + ); + }; + return ( + + } + type="link" + /> + + ); + }; const renderWidgetAction = () => { const widgetType = widget.config.type; const hideTypes: WidgetType[] = ['query', 'reset', 'controller']; @@ -85,9 +119,11 @@ const WidgetToolBar: FC = () => { } return ; }; + return ( - + + {renderErrorIcon(errInfo)} {renderedIcon()} {loadingIcon()} {linkageIcon()} @@ -110,3 +146,12 @@ const StyleWrap = styled.div` visibility: hidden; } `; + +const StyledErrorIcon = styled(Button)` + background: ${p => p.theme.componentBackground}; + + &:hover, + &:focus { + background: ${p => p.theme.componentBackground}; + } +`; diff --git a/frontend/src/app/pages/DashBoardPage/constants.ts b/frontend/src/app/pages/DashBoardPage/constants.ts index 50f094ccd..96aad995c 100644 --- a/frontend/src/app/pages/DashBoardPage/constants.ts +++ b/frontend/src/app/pages/DashBoardPage/constants.ts @@ -22,9 +22,9 @@ import { } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; import { FilterSqlOperator } from 'globalConstants'; +import i18next from 'i18next'; import { PRIMARY, WHITE } from 'styles/StyleConstants'; import { WidgetType } from './pages/Board/slice/types'; - export const RGL_DRAG_HANDLE = 'dashboard-draggableHandle'; export const STORAGE_BOARD_KEY_PREFIX = 'DATART_BOARD_DATA_'; export const STORAGE_IMAGE_KEY_PREFIX = 'DATART_IMAGE_'; @@ -89,6 +89,7 @@ export const CanFullScreenWidgetTypes: WidgetType[] = ['chart', 'media']; export const CONTAINER_TAB = 'containerTab'; // +export const NeedFetchWidgetTypes: WidgetType[] = ['chart', 'controller']; // setting @@ -96,11 +97,11 @@ export const TEXT_ALIGN_ENUM = strEnumType(['left', 'center', 'right']); export type TextAlignType = keyof typeof TEXT_ALIGN_ENUM; export const BORDER_STYLE_ENUM = strEnumType([ + 'none', 'solid', 'dashed', 'dotted', 'double', - 'none', 'hidden', 'ridge', 'groove', @@ -110,16 +111,16 @@ export const BORDER_STYLE_ENUM = strEnumType([ export type BorderStyleType = keyof typeof BORDER_STYLE_ENUM; export const BORDER_STYLE_OPTIONS = [ - { name: '无', value: BORDER_STYLE_ENUM.none }, - { name: '实线', value: BORDER_STYLE_ENUM.solid }, - { name: '虚线', value: BORDER_STYLE_ENUM.dashed }, - { name: '点线', value: BORDER_STYLE_ENUM.dotted }, - { name: '双线', value: BORDER_STYLE_ENUM.double }, - { name: '隐藏', value: BORDER_STYLE_ENUM.hidden }, - { name: '凹槽', value: BORDER_STYLE_ENUM.groove }, - { name: '垄状', value: BORDER_STYLE_ENUM.ridge }, - { name: 'inset', value: BORDER_STYLE_ENUM.inset }, - { name: 'outset', value: BORDER_STYLE_ENUM.outset }, + { value: BORDER_STYLE_ENUM.none }, + { value: BORDER_STYLE_ENUM.solid }, + { value: BORDER_STYLE_ENUM.dashed }, + { value: BORDER_STYLE_ENUM.dotted }, + { value: BORDER_STYLE_ENUM.double }, + { value: BORDER_STYLE_ENUM.hidden }, + { value: BORDER_STYLE_ENUM.groove }, + { value: BORDER_STYLE_ENUM.ridge }, + { value: BORDER_STYLE_ENUM.inset }, + { value: BORDER_STYLE_ENUM.outset }, ]; export const SCALE_MODE_ENUM = strEnumType([ @@ -131,10 +132,10 @@ export const SCALE_MODE_ENUM = strEnumType([ export type ScaleModeType = keyof typeof SCALE_MODE_ENUM; export const SCALE_MODE__OPTIONS = [ - { name: '等比宽度缩放', value: SCALE_MODE_ENUM.scaleWidth }, - { name: '等比高度缩放', value: SCALE_MODE_ENUM.scaleHeight }, - { name: '全屏铺满', value: SCALE_MODE_ENUM.scaleFull }, - { name: '实际尺寸', value: SCALE_MODE_ENUM.noScale }, + { value: SCALE_MODE_ENUM.scaleWidth }, + { value: SCALE_MODE_ENUM.scaleHeight }, + { value: SCALE_MODE_ENUM.scaleFull }, + { value: SCALE_MODE_ENUM.noScale }, ]; export const enum ValueOptionTypes { @@ -155,33 +156,53 @@ export const enum ControllerVisibleTypes { export type ControllerVisibleType = Uncapitalize< keyof typeof ControllerVisibleTypes >; +const tfo = (operator: FilterSqlOperator) => { + const preStr = 'viz.common.enum.filterOperator.'; + return i18next.t(preStr + operator); +}; +const tft = (type: ControllerVisibleTypes) => { + const preStr = 'viz.common.enum.controllerVisibilityTypes.'; + return i18next.t(preStr + type); +}; +const getVisibleOptionItem = (type: ControllerVisibleTypes) => { + return { + name: tft(type), + value: type, + }; +}; +const getOperatorItem = (value: FilterSqlOperator) => { + return { + name: tfo(value), + value: value, + }; +}; export const VISIBILITY_TYPE_OPTION = [ - { name: '显示', value: ControllerVisibleTypes.Show }, - { name: '隐藏', value: ControllerVisibleTypes.Hide }, - { name: '条件', value: ControllerVisibleTypes.Condition }, + getVisibleOptionItem(ControllerVisibleTypes.Show), + getVisibleOptionItem(ControllerVisibleTypes.Hide), + getVisibleOptionItem(ControllerVisibleTypes.Condition), ]; export const ALL_SQL_OPERATOR_OPTIONS = [ - { name: '等于', value: FilterSqlOperator.Equal }, - { name: '不相等', value: FilterSqlOperator.NotEqual }, + getOperatorItem(FilterSqlOperator.Equal), + getOperatorItem(FilterSqlOperator.NotEqual), - { name: '包含', value: FilterSqlOperator.In }, - { name: '不包含', value: FilterSqlOperator.NotIn }, + getOperatorItem(FilterSqlOperator.In), + getOperatorItem(FilterSqlOperator.NotIn), - { name: '为空', value: FilterSqlOperator.Null }, - { name: '不为空', value: FilterSqlOperator.NotNull }, + getOperatorItem(FilterSqlOperator.Null), + getOperatorItem(FilterSqlOperator.NotNull), - { name: '前缀包含', value: FilterSqlOperator.PrefixContain }, - { name: '前缀不包含', value: FilterSqlOperator.NotPrefixContain }, + getOperatorItem(FilterSqlOperator.PrefixContain), + getOperatorItem(FilterSqlOperator.NotPrefixContain), - { name: '后缀包含', value: FilterSqlOperator.SuffixContain }, - { name: '后缀不包含', value: FilterSqlOperator.NotSuffixContain }, + getOperatorItem(FilterSqlOperator.SuffixContain), + getOperatorItem(FilterSqlOperator.NotSuffixContain), - { name: '区间', value: FilterSqlOperator.Between }, + getOperatorItem(FilterSqlOperator.Between), - { name: '大于或等于', value: FilterSqlOperator.GreaterThanOrEqual }, - { name: '小于或等于', value: FilterSqlOperator.LessThanOrEqual }, - { name: '大于', value: FilterSqlOperator.GreaterThan }, - { name: '小于', value: FilterSqlOperator.LessThan }, + getOperatorItem(FilterSqlOperator.GreaterThanOrEqual), + getOperatorItem(FilterSqlOperator.LessThanOrEqual), + getOperatorItem(FilterSqlOperator.GreaterThan), + getOperatorItem(FilterSqlOperator.LessThan), ]; export const SQL_OPERATOR_OPTIONS_TYPES = { @@ -193,6 +214,10 @@ export const SQL_OPERATOR_OPTIONS_TYPES = { FilterSqlOperator.In, FilterSqlOperator.NotIn, ], + [ControllerFacadeTypes.CheckboxGroup]: [ + FilterSqlOperator.In, + FilterSqlOperator.NotIn, + ], [ControllerFacadeTypes.RadioGroup]: [ FilterSqlOperator.Equal, FilterSqlOperator.NotEqual, diff --git a/frontend/src/app/pages/DashBoardPage/contexts/BoardActionContext.ts b/frontend/src/app/pages/DashBoardPage/contexts/BoardActionContext.ts index 6cce6a5f8..ef6e24c62 100644 --- a/frontend/src/app/pages/DashBoardPage/contexts/BoardActionContext.ts +++ b/frontend/src/app/pages/DashBoardPage/contexts/BoardActionContext.ts @@ -20,7 +20,7 @@ import { createContext } from 'react'; import { Widget } from '../pages/Board/slice/types'; export interface BoardActionContextProps { widgetUpdate: (widget: Widget) => void; - refreshWidgetsByFilter: (widget: Widget) => void; + refreshWidgetsByController: (widget: Widget) => void; updateBoard?: (callback: () => void) => void; onGenerateShareLink?: (date, usePwd) => any; onBoardToDownLoad: () => any; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/AutoDashboard/AutoBoardCore.tsx b/frontend/src/app/pages/DashBoardPage/pages/Board/AutoDashboard/AutoBoardCore.tsx index d04df39fe..7dd8454f5 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/AutoDashboard/AutoBoardCore.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/AutoDashboard/AutoBoardCore.tsx @@ -15,6 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Empty } from 'antd'; import { WidgetAllProvider } from 'app/pages/DashBoardPage/components/WidgetProvider/WidgetAllProvider'; import { BREAK_POINTS } from 'app/pages/DashBoardPage/constants'; import { BoardContext } from 'app/pages/DashBoardPage/contexts/BoardContext'; @@ -114,10 +115,10 @@ const AutoBoardCore: React.FC = ({ boardId }) => { const scrollThrottle = useRef(false); const lazyLoad = useCallback(() => { if (!gridWrapRef.current) return; - if (!scrollThrottle.current) { requestAnimationFrame(() => { const waitingItems = layoutInfos.current.filter(item => !item.rendered); + if (waitingItems.length > 0) { const { offsetHeight, scrollTop } = gridWrapRef.current!; waitingItems.forEach(item => { @@ -145,7 +146,14 @@ const AutoBoardCore: React.FC = ({ boardId }) => { lazyLoad(); gridWrapRef.current.removeEventListener('scroll', lazyLoad, false); gridWrapRef.current.addEventListener('scroll', lazyLoad, false); + // issues#339 + window.addEventListener('resize', lazyLoad, false); } + + return () => { + gridWrapRef?.current?.removeEventListener('scroll', lazyLoad, false); + window.removeEventListener('resize', lazyLoad, false); + }; }, [boardLoading, WidgetConfigsLen, lazyLoad]); const onLayoutChange = useCallback((layouts: Layout[]) => { @@ -168,27 +176,32 @@ const AutoBoardCore: React.FC = ({ boardId }) => { return ( - {boardLoading ? loading... : null} - - - - {boardChildren} - + {layoutWidgetConfigs.length ? ( + + + + {boardChildren} + + - + ) : ( + + + + )} ); @@ -222,4 +235,11 @@ const StyledContainer = styled(StyledBackground)` .grid-wrap::-webkit-scrollbar { width: 0 !important; } + + .empty { + display: flex; + flex: 1; + justify-content: center; + align-items: center; + } `; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/FreeDashboard/FreeBoardCore.tsx b/frontend/src/app/pages/DashBoardPage/pages/Board/FreeDashboard/FreeBoardCore.tsx index 25409d63d..fe9ff1c57 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/FreeDashboard/FreeBoardCore.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/FreeDashboard/FreeBoardCore.tsx @@ -15,6 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Empty } from 'antd'; import { WidgetAllProvider } from 'app/pages/DashBoardPage/components/WidgetProvider/WidgetAllProvider'; import { BoardConfigContext } from 'app/pages/DashBoardPage/contexts/BoardConfigContext'; import { BoardContext } from 'app/pages/DashBoardPage/contexts/BoardContext'; @@ -84,7 +85,13 @@ const FreeBoardCore: React.FC = memo( ref={refGridBackground} > - {boardChildren} + {widgetConfigs.length ? ( + boardChildren + ) : ( + + + + )} {showZoomCtrl && ( @@ -115,6 +122,14 @@ const Wrap = styled.div` flex: 1; -ms-overflow-style: none; overflow-y: hidden; + + .empty { + height: 100%; + display: flex; + flex: 1; + justify-content: center; + align-items: center; + } } .grid-background::-webkit-scrollbar { width: 0 !important; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/index.tsx b/frontend/src/app/pages/DashBoardPage/pages/Board/index.tsx index ac0869c7b..d9cf80c1a 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/index.tsx @@ -16,8 +16,7 @@ * limitations under the License. */ -import { LoadingOutlined } from '@ant-design/icons'; -import { message } from 'antd'; +import { message, Spin } from 'antd'; import useResizeObserver from 'app/hooks/useResizeObserver'; import { selectPublishLoading } from 'app/pages/MainPage/pages/VizPage/slice/selectors'; import { publishViz } from 'app/pages/MainPage/pages/VizPage/slice/thunks'; @@ -99,14 +98,19 @@ export const Board: React.FC = memo( }, [boardId, dispatch, fetchData, searchParams]); const [showBoardEditor, setShowBoardEditor] = useState(false); - - const toggleBoardEditor = (bool: boolean) => { - setShowBoardEditor(bool); - }; - const dashboard = useSelector((state: { board: BoardState }) => makeSelectBoardConfigById()(state, boardId), ); + const toggleBoardEditor = useCallback( + (bool: boolean) => { + setShowBoardEditor(bool); + if (!bool) { + dispatch(fetchBoardDetail({ dashboardRelId: dashboard?.id || '' })); + } + }, + [dashboard?.id, dispatch], + ); + const publishLoading = useSelector(selectPublishLoading); const onPublish = useCallback(() => { @@ -166,8 +170,8 @@ export const Board: React.FC = memo( ); } else { return ( - - loading + + ); } @@ -180,6 +184,7 @@ export const Board: React.FC = memo( allowManage, hideTitle, publishLoading, + toggleBoardEditor, onPublish, showZoomCtrl, ]); @@ -230,4 +235,10 @@ const Wrapper = styled.div<{}>` flex-direction: column; min-height: 0; } + .loading { + display: flex; + flex: 1; + justify-content: center; + align-items: center; + } `; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/asyncActions.ts b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/asyncActions.ts index bcd0a621e..09a600ad5 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/asyncActions.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/asyncActions.ts @@ -32,8 +32,11 @@ import { getWidgetInfoMapByServer, getWidgetMapByServer, } from '../../../utils/widget'; -import { PageInfo } from './../../../../MainPage/pages/ViewPage/slice/types'; -import { getChartWidgetDataAsync } from './thunk'; +import { + PageInfo, + View, +} from './../../../../MainPage/pages/ViewPage/slice/types'; +import { getChartWidgetDataAsync, getWidgetData } from './thunk'; import { BoardState, DataChart, ServerDashboard, VizRenderMode } from './types'; export const handleServerBoardAction = @@ -155,16 +158,25 @@ export const resetControllerAction = pageNo: 1, }; - Object.values(widgetMap) - .filter(it => it.config.type === 'chart') - .forEach(it => { - dispatch( - getChartWidgetDataAsync({ - boardId, - widgetId: it.id, - renderMode, - option: { pageInfo }, - }), - ); - }); + Object.values(widgetMap).forEach(widget => { + dispatch( + getWidgetData({ + boardId, + widget: widget, + renderMode, + option: { pageInfo }, + }), + ); + }); + }; + +export const saveToViewMapAction = + (serverView: View) => (dispatch, getState) => { + const boardState = getState() as { board: BoardState }; + const viewMap = boardState.board.viewMap; + let existed = serverView.id in viewMap; + if (!existed) { + const viewViews = getChartDataView([serverView], []); + dispatch(boardActions.setViewMap(viewViews)); + } }; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/index.ts b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/index.ts index 9694e0484..b2621c7a9 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/index.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/index.ts @@ -28,7 +28,7 @@ import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; import { createSlice } from 'utils/@reduxjs/toolkit'; import { PageInfo } from '../../../../MainPage/pages/ViewPage/slice/types'; import { createWidgetInfo } from '../../../utils/widget'; -import { getChartWidgetDataAsync, getWidgetDataAsync } from './thunk'; +import { getChartWidgetDataAsync, getControllerOptions } from './thunk'; import { BoardInfo, BoardState, Widget } from './types'; export const boardInit: BoardState = { @@ -196,6 +196,7 @@ const boardSlice = createSlice({ } state.widgetInfoRecord[boardId][widgetId].inLinking = toggle; }, + addFetchedItem( state, action: PayloadAction<{ boardId: string; widgetId: string }>, @@ -208,6 +209,7 @@ const boardSlice = createSlice({ ); } catch (error) {} }, + setBoardWidthHeight( state, action: PayloadAction<{ boardId: string; wh: [number, number] }>, @@ -236,6 +238,17 @@ const boardSlice = createSlice({ pageNo: 1, }; }, + setWidgetErrInfo( + state, + action: PayloadAction<{ + boardId: string; + widgetId: string; + errInfo?: string; + }>, + ) { + const { boardId, widgetId, errInfo } = action.payload; + state.widgetInfoRecord[boardId][widgetId].errInfo = errInfo; + }, resetControlWidgets( state, action: PayloadAction<{ @@ -254,38 +267,37 @@ const boardSlice = createSlice({ }, }, extraReducers: builder => { - // getWidgetDataAsync - builder.addCase(getWidgetDataAsync.pending, (state, action) => { + builder.addCase(getChartWidgetDataAsync.pending, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = true; } catch (error) {} }); - builder.addCase(getWidgetDataAsync.fulfilled, (state, action) => { + builder.addCase(getChartWidgetDataAsync.fulfilled, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = false; } catch (error) {} }); - builder.addCase(getWidgetDataAsync.rejected, (state, action) => { + builder.addCase(getChartWidgetDataAsync.rejected, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = false; } catch (error) {} }); - builder.addCase(getChartWidgetDataAsync.pending, (state, action) => { + builder.addCase(getControllerOptions.pending, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = true; } catch (error) {} }); - builder.addCase(getChartWidgetDataAsync.fulfilled, (state, action) => { + builder.addCase(getControllerOptions.fulfilled, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = false; } catch (error) {} }); - builder.addCase(getChartWidgetDataAsync.rejected, (state, action) => { + builder.addCase(getControllerOptions.rejected, (state, action) => { const { boardId, widgetId } = action.meta.arg; try { state.widgetInfoRecord[boardId][widgetId].loading = false; diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/thunk.ts b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/thunk.ts index 9f3b9094f..8037f9f30 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/thunk.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/thunk.ts @@ -6,14 +6,15 @@ import { VizRenderMode, Widget, } from 'app/pages/DashBoardPage/pages/Board/slice/types'; +import { getControlOptionQueryParams } from 'app/pages/DashBoardPage/utils/widgetToolKit/chart'; import { FilterSearchParams } from 'app/pages/MainPage/pages/VizPage/slice/types'; import { shareActions } from 'app/pages/SharePage/slice'; import { ExecuteToken, ShareVizInfo } from 'app/pages/SharePage/slice/types'; +import ChartDataset from 'app/types/ChartDataset'; import { RootState } from 'types'; import { request } from 'utils/request'; -import { errorHandle } from 'utils/utils'; +import { errorHandle, getErrorMessage } from 'utils/utils'; import { boardActions } from '.'; -import { getDistinctFields } from '../../../../../utils/fetch'; import { getChartWidgetRequestParams } from '../../../utils'; import { handleServerBoardAction } from './asyncActions'; import { selectBoardById, selectBoardWidgetMap } from './selector'; @@ -134,7 +135,7 @@ export const renderedWidgetAsync = createAsyncThunk< dispatch(boardActions.renderedWidgets({ boardId, widgetIds: [widgetId] })); // 2 widget getData dispatch( - getWidgetDataAsync({ boardId: boardId, widgetId: widgetId, renderMode }), + getWidgetData({ boardId: boardId, widget: curWidget, renderMode }), ); if (curWidget.config.type === 'container') { const content = curWidget.config.content as ContainerWidgetContent; @@ -149,7 +150,11 @@ export const renderedWidgetAsync = createAsyncThunk< // 2 widget getData subWidgetIds.forEach(wid => { dispatch( - getWidgetDataAsync({ boardId: boardId, widgetId: wid, renderMode }), + getWidgetData({ + boardId: boardId, + widget: widgetMap[wid], + renderMode, + }), ); }); } @@ -158,54 +163,29 @@ export const renderedWidgetAsync = createAsyncThunk< }, ); -export const getWidgetDataAsync = createAsyncThunk< +export const getWidgetData = createAsyncThunk< null, { boardId: string; - widgetId: string; + widget: Widget; renderMode: VizRenderMode | undefined; option?: getDataOption; }, { state: RootState } >( - 'board/getWidgetDataAsync', - async ({ boardId, widgetId, renderMode, option }, { getState, dispatch }) => { - const boardState = getState() as { board: BoardState }; - const curWidget = boardState.board.widgetRecord?.[boardId]?.[widgetId]; - if (!curWidget) return null; - dispatch(boardActions.renderedWidgets({ boardId, widgetIds: [widgetId] })); - switch (curWidget.config.type) { + 'board/getWidgetData', + ({ widget, renderMode, option }, { getState, dispatch }) => { + const boardId = widget.dashboardId; + dispatch(boardActions.renderedWidgets({ boardId, widgetIds: [widget.id] })); + const widgetId = widget.id; + switch (widget.config.type) { case 'chart': - try { - await dispatch( - getChartWidgetDataAsync({ boardId, widgetId, renderMode, option }), - ); - if (renderMode === 'schedule') { - dispatch( - boardActions.addFetchedItem({ - boardId: curWidget.dashboardId, - widgetId: curWidget.id, - }), - ); - } - } catch (error) { - if (renderMode === 'schedule') { - dispatch( - boardActions.addFetchedItem({ - boardId: curWidget.dashboardId, - widgetId: curWidget.id, - }), - ); - } - } - return null; - case 'media': - return null; - case 'container': + dispatch( + getChartWidgetDataAsync({ boardId, widgetId, renderMode, option }), + ); return null; case 'controller': - await dispatch(getControllerOptions({ widget: curWidget, renderMode })); - + dispatch(getControllerOptions({ boardId, widgetId, renderMode })); return null; default: return null; @@ -252,36 +232,67 @@ export const getChartWidgetDataAsync = createAsyncThunk< return null; } let widgetData; - if (renderMode === 'read') { - const { data } = await request({ - method: 'POST', - url: `data-provider/execute`, - data: requestParams, - }); - widgetData = { ...data, id: widgetId }; - } else { - const executeTokenMap = (getState() as RootState)?.share?.executeTokenMap; - const dataChart = dataChartMap[curWidget.datachartId]; - const viewId = viewMap[dataChart.viewId].id; - const executeToken = executeTokenMap?.[viewId]; - const { data } = await request({ - method: 'POST', - url: `share/execute`, - params: { - executeToken: executeToken?.token, - password: executeToken?.password, - }, - data: requestParams, - }); - widgetData = { ...data, id: widgetId }; - } + try { + if (renderMode === 'read') { + const { data } = await request({ + method: 'POST', + url: `data-provider/execute`, + data: requestParams, + }); + widgetData = { ...data, id: widgetId }; + } else { + const executeTokenMap = (getState() as RootState)?.share + ?.executeTokenMap; + const dataChart = dataChartMap[curWidget.datachartId]; + const viewId = viewMap[dataChart.viewId].id; + const executeToken = executeTokenMap?.[viewId]; + const { data } = await request({ + method: 'POST', + url: `share/execute`, + params: { + executeToken: executeToken?.token, + password: executeToken?.password, + }, + data: requestParams, + }); + widgetData = { ...data, id: widgetId }; + } + dispatch(boardActions.setWidgetData(widgetData as WidgetData)); + dispatch( + boardActions.changePageInfo({ + boardId, + widgetId, + pageInfo: widgetData.pageInfo, + }), + ); + dispatch( + boardActions.setWidgetErrInfo({ + boardId, + widgetId, + errInfo: undefined, + }), + ); + } catch (error) { + dispatch( + boardActions.setWidgetErrInfo({ + boardId, + widgetId, + errInfo: getErrorMessage(error), + }), + ); - dispatch(boardActions.setWidgetData(widgetData as WidgetData)); + dispatch( + boardActions.setWidgetData({ + id: widgetId, + columns: [], + rows: [], + } as WidgetData), + ); + } dispatch( - boardActions.changePageInfo({ + boardActions.addFetchedItem({ boardId, widgetId, - pageInfo: widgetData.pageInfo, }), ); return null; @@ -291,32 +302,91 @@ export const getChartWidgetDataAsync = createAsyncThunk< // 根据 字段获取 Controller 的options export const getControllerOptions = createAsyncThunk< null, - { widget: Widget; renderMode: VizRenderMode | undefined }, + { boardId: string; widgetId: string; renderMode: VizRenderMode | undefined }, { state: RootState } >( 'board/getControllerOptions', - async ({ widget, renderMode }, { getState, dispatch }) => { + async ({ boardId, widgetId, renderMode }, { getState, dispatch }) => { + dispatch( + boardActions.renderedWidgets({ + boardId: boardId, + widgetIds: [widgetId], + }), + ); + const boardState = getState() as { board: BoardState }; + const viewMap = boardState.board.viewMap; + const widgetMapMap = boardState.board.widgetRecord; + const widgetMap = widgetMapMap[boardId]; + const widget = widgetMap[widgetId]; + if (!widget) return null; const content = widget.config.content as ControllerWidgetContent; const config = content.config; + if (!Array.isArray(config.assistViewFields)) return null; + if (config.assistViewFields.length !== 2) return null; + const executeTokenMap = (getState() as RootState)?.share?.executeTokenMap; - if (config.assistViewFields && Array.isArray(config.assistViewFields)) { - // 请求 - const [viewId, viewField] = config.assistViewFields; - const executeToken = executeTokenMap?.[viewId]; - const dataset = await getDistinctFields( - viewId, - viewField, - undefined, - executeToken, + const [viewId, viewField] = config.assistViewFields; + + const executeToken = executeTokenMap?.[viewId]; + + const view = viewMap[viewId]; + if (!view) return null; + const requestParams = getControlOptionQueryParams({ + view, + field: viewField, + curWidget: widget, + widgetMap, + }); + + if (!requestParams) { + return null; + } + let widgetData; + try { + if (executeToken && renderMode !== 'read') { + const { data } = await request({ + method: 'POST', + url: `share/execute`, + params: { + executeToken: executeToken?.token, + password: executeToken?.password, + }, + data: requestParams, + }); + widgetData = { ...data, id: widget.id }; + dispatch(boardActions.setWidgetData(widgetData as WidgetData)); + } else { + const { data } = await request({ + method: 'POST', + url: `data-provider/execute`, + data: requestParams, + }); + widgetData = { ...data, id: widget.id }; + dispatch(boardActions.setWidgetData(widgetData as WidgetData)); + } + dispatch( + boardActions.setWidgetErrInfo({ + boardId, + widgetId, + errInfo: undefined, + }), ); + } catch (error) { dispatch( - boardActions.setWidgetData({ - ...dataset, - id: widget.id, - } as unknown as WidgetData), + boardActions.setWidgetErrInfo({ + boardId, + widgetId, + errInfo: getErrorMessage(error), + }), ); } + dispatch( + boardActions.addFetchedItem({ + boardId, + widgetId, + }), + ); return null; }, ); diff --git a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/types.ts b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/types.ts index 4a2e17581..1a7db5d1d 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/Board/slice/types.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/Board/slice/types.ts @@ -27,7 +27,10 @@ import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; import { DeltaStatic } from 'quill'; import { Layout } from 'react-grid-layout'; import { ChartDataSectionField } from '../../../../../types/ChartConfig'; -import { PageInfo } from '../../../../MainPage/pages/ViewPage/slice/types'; +import { + PageInfo, + View, +} from '../../../../MainPage/pages/ViewPage/slice/types'; import { BorderStyleType, LAYOUT_COLS, @@ -74,7 +77,7 @@ export interface SaveDashboard extends Omit { } export interface ServerDashboard extends Omit { config: string; - views: ServerView[]; + views: View[]; datacharts: ServerDatachart[]; widgets: ServerWidget[]; } @@ -161,6 +164,9 @@ export interface JumpConfigField { } export interface JumpConfig { open: boolean; + targetType: string; + URL: string; + queryName: string; field: JumpConfigField; target: JumpConfigTarget; filter: JumpConfigFilter; @@ -183,6 +189,7 @@ export interface WidgetInfo { inLinking: boolean; //是否在触发联动 selected: boolean; pageInfo: Partial; + errInfo?: string; selectItems?: string[]; parameters?: any; } @@ -213,11 +220,11 @@ export interface Relation { } /** * @controlToWidget Controller associated widgets - * @controlToControl Controller associated Controller visible - * @widgetToWidget widget inOther WidgetContainer + * @controlToControl Controller associated Controller visible cascade + * @widgetToWidget widget inOther WidgetContainer linkage * */ export interface RelationConfig { - type: 'controlToWidget' | 'controlToControl' | 'widgetToWidget'; + type: RelationConfigType; controlToWidget?: { widgetRelatedViewIds: string[]; }; @@ -227,6 +234,14 @@ export interface RelationConfig { linkerColumn: string; }; } +export type RelationConfigType = + | 'controlToWidget' // control - ChartFetch will del + | 'controlToChartFetch' // control - ChartFetch + | 'controlToControl' // control - control -visible will del + | 'controlToControlVisible' // control - control -visible + | 'controlToControlCascade' // control - control -Cascade + | 'widgetToWidget' // linkage will del + | 'chartToChartLinkage'; // linkage export interface RelatedView { viewId: string; relatedCategory: ChartDataViewFieldCategory; @@ -390,16 +405,12 @@ export interface DataChart { status: any; } export interface DataChartConfig { + aggregation: boolean | undefined; chartConfig: ChartConfig; chartGraphId: string; computedFields: any[]; } -export interface ServerView extends ChartDataView { - model: string; -} -// TODO - export type ColsType = typeof LAYOUT_COLS; // Dashboard view model @@ -458,4 +469,5 @@ export interface ServerDatachart extends Omit { export interface getDataOption { pageInfo?: Partial; + sorters?: Array<{ column: string; operator?: string; aggOperator?: string }>; } diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddChartBtn.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddChartBtn.tsx index 84fb34802..242c35b21 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddChartBtn.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddChartBtn.tsx @@ -24,15 +24,9 @@ import { } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { selectVizs } from 'app/pages/MainPage/pages/VizPage/slice/selectors'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import { addDataChartWidgets, addWrapChartWidget } from '../../slice/thunk'; import ChartSelectModalModal from '../ChartSelectModal'; import { ChartWidgetDropdown, ToolBtnProps } from './ToolBarItem'; @@ -40,16 +34,12 @@ const AddChartBtn: React.FC = props => { const dispatch = useDispatch(); const { boardId, boardType } = useContext(BoardContext); const orgId = useSelector(selectOrgId); - // const chartOptions = useSelector(selectDataChartList); const chartOptionsMock = useSelector(selectVizs); const chartOptions = useMemo( () => chartOptionsMock.filter(item => item.relType !== 'DASHBOARD'), [chartOptionsMock], ); - useEffect(() => { - // dispatch(getDataCharts(orgId)); - }, [dispatch, orgId]); const [dataChartVisible, setDataChartVisible] = useState(false); const [widgetChartVisible, setWidgetChartVisible] = useState(false); diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddControl/AddControlBtn.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddControl/AddControlBtn.tsx index 98fcf798d..4a32f3387 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddControl/AddControlBtn.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/BoardToolBar/AddControl/AddControlBtn.tsx @@ -17,6 +17,7 @@ */ import { ControlOutlined } from '@ant-design/icons'; import { Dropdown, Menu } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { BoardConfigContext } from 'app/pages/DashBoardPage/contexts/BoardConfigContext'; import { WidgetType } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; @@ -28,12 +29,16 @@ import { BoardToolBarContext } from '../context/BoardToolBarContext'; import { WithTipButton } from '../ToolBarItem'; export interface AddControlBtnProps {} export interface ButtonItemType { - name: string; + name?: string; icon: any; type: T; - disabled: boolean; + disabled?: boolean; } export const AddControlBtn: React.FC = () => { + const t = useI18NPrefix(`viz.board.action`); + const tFilterName = useI18NPrefix(`viz.common.enum.controllerFacadeTypes`); + const tType = useI18NPrefix(`viz.board.controlTypes`); + const tWt = useI18NPrefix(`viz.widget.type`); const { boardId, boardType, showLabel } = useContext(BoardToolBarContext); const dispatch = useDispatch(); const { config: boardConfig } = useContext(BoardConfigContext); @@ -49,34 +54,24 @@ export const AddControlBtn: React.FC = () => { }; const conventionalControllers: ButtonItemType[] = [ { - name: '单选下拉菜单', icon: '', type: ControllerFacadeTypes.DropdownList, - disabled: false, }, { - name: '多选下拉菜单', icon: '', type: ControllerFacadeTypes.MultiDropdownList, - disabled: false, }, { - name: '单选按钮', icon: '', type: ControllerFacadeTypes.RadioGroup, - disabled: false, }, - // { - // name: '复选框', - // icon: '', - // type: ControllerFacadeTypes.RadioGroup, - // disabled: false, - // }, { - name: '文本', + icon: '', + type: ControllerFacadeTypes.CheckboxGroup, + }, + { icon: '', type: ControllerFacadeTypes.Text, - disabled: false, }, // { // name: '单选下拉树', @@ -91,38 +86,28 @@ export const AddControlBtn: React.FC = () => { // disabled: false, // }, ]; - const dateControllers = [ + const dateControllers: ButtonItemType[] = [ { - name: '日期范围', icon: '', type: ControllerFacadeTypes.RangeTime, - disabled: false, }, { - name: '日期', icon: '', type: ControllerFacadeTypes.Time, - disabled: false, }, ]; - const numericalControllers = [ + const numericalControllers: ButtonItemType[] = [ { - name: '数值范围', icon: '', type: ControllerFacadeTypes.RangeValue, - disabled: false, }, { - name: '数值', icon: '', type: ControllerFacadeTypes.Value, - disabled: false, }, { - name: '滑块', icon: '', type: ControllerFacadeTypes.Slider, - disabled: false, }, // { // name: '范围滑块', @@ -133,13 +118,11 @@ export const AddControlBtn: React.FC = () => { ]; const buttonControllers: ButtonItemType[] = [ { - name: '查询按钮', icon: '', type: 'query', disabled: !!hasQueryControl, }, { - name: '重置按钮', icon: '', type: 'reset', disabled: !!hasResetControl, @@ -150,31 +133,40 @@ export const AddControlBtn: React.FC = () => { }; const controlerItems = (
{dataset?.script}