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); + }} + /> + + + + + + ); +} + +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( @@ -87,7 +90,7 @@ const BasicFont: FC> = memo( ))} + + { + 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( ; + break; + } + + switch (operatorValue) { + case OperatorTypes.In: + case OperatorTypes.NotIn: + return ( + + + + + + + {t('conditionStyleTable.header.range.cell')} + + + {t('conditionStyleTable.header.range.row')} + + + + + + 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 ( - - - + + + + + + + - - - - + {label} @@ -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 ( + + ); + } +} 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')} - */} + +
- {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')} - - - - - - {t('format.currency')} - - - - + + + + + + )} {FieldFormatType.NUMERIC === type && ( <> - - {t('format.unit')} - - - - - - {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, { + 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
; + }, + 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
; + 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
+
; +}; + +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 ( + svg icon + ); +}; + 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 ( - + + {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_DIRECTION.map(item => ( {t(item.name)} ))} - + {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( + () => ( +
    + + + + +
    )); @@ -163,7 +165,7 @@ export const RelatedViewForm: React.FC = memo( return ( -

    关联字段/变量

    +

    {t('title')}

    = memo(

    {getViewName(index)}

    -
    +
    = memo( - 字段 + {t('field')} - 变量 + {t('variable')} @@ -232,9 +234,7 @@ export const RelatedViewForm: React.FC = memo( - {!fields.length && ( - - )} + {!fields.length && } ); }} diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/RelatedWidgets.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/RelatedWidgets.tsx index 1312c0ce4..b30d7344a 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/RelatedWidgets.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/RelatedWidgets.tsx @@ -16,6 +16,7 @@ * limitations under the License. */ import { Table } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { Widget } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import React, { memo, useEffect, useMemo, useState } from 'react'; @@ -31,6 +32,7 @@ export interface RelatedWidgetsProps { export const RelatedWidgets: React.FC = memo( ({ widgets, relatedWidgets, onChange }) => { + const tw = useI18NPrefix(`viz.widget`); const [selectedWidgetIds, setSelectedWidgetIds] = useState([]); const rowSelection = keys => { @@ -49,15 +51,19 @@ export const RelatedWidgets: React.FC = memo( const columns = useMemo( () => [ { - title: '', + title: tw('widgetName'), render: (w: Widget) => {w.config.name}, }, + { + title: tw('widgetType'), + render: (w: Widget) => {w.config.type}, + }, ], - [], + [tw], ); return ( <> -

    关联组件

    +

    {tw('associatedWidget')}

    record.id} rowSelection={{ diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/constants.ts b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/constants.ts index bb41d50b3..05457e5a8 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/constants.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/constants.ts @@ -39,14 +39,21 @@ export const DateControllerTypes = [ ControllerFacadeTypes.Time, ]; -export const NeedLoadOptionTypes = [ - ControllerFacadeTypes.DropdownList, - ControllerFacadeTypes.MultiDropdownList, - ControllerFacadeTypes.RadioGroup, - ControllerFacadeTypes.Tree, -]; export const RangeControlTypes = [ ControllerFacadeTypes.RangeTime, ControllerFacadeTypes.RangeSlider, ControllerFacadeTypes.RangeValue, ]; +export const StrControlTypes = [ + ControllerFacadeTypes.DropdownList, + ControllerFacadeTypes.MultiDropdownList, + ControllerFacadeTypes.RadioGroup, + ControllerFacadeTypes.Text, +]; + +export const HasOptionsControlTypes = [ + ControllerFacadeTypes.DropdownList, + ControllerFacadeTypes.MultiDropdownList, + ControllerFacadeTypes.RadioGroup, + ControllerFacadeTypes.CheckboxGroup, +]; diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/index.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/index.tsx index 370523724..951e8c3cb 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/index.tsx @@ -25,14 +25,12 @@ import { selectViewMap } from 'app/pages/DashBoardPage/pages/Board/slice/selecto import { ControllerWidgetContent, RelatedView, - Relation, } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { convertToWidgetMap, - createControllerWidget, - getCanLinkControlWidgets, getOtherStringControlWidgets, } from 'app/pages/DashBoardPage/utils/widget'; +import { widgetToolKit } from 'app/pages/DashBoardPage/utils/widgetToolKit/widgetToolKit'; import { ChartDataViewFieldCategory, ChartDataViewFieldType, @@ -49,7 +47,6 @@ import React, { import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { SPACE_XS } from 'styles/StyleConstants'; -import { v4 as uuidv4 } from 'uuid'; import { editBoardStackActions, editDashBoardInfoActions } from '../../slice'; import { selectControllerPanel, @@ -57,37 +54,42 @@ import { } from '../../slice/selectors'; import { addWidgetsToEditBoard, - getEditControllerOptionAsync, + getEditControllerOptions, } from '../../slice/thunk'; import { WidgetControlForm } from './ControllerConfig'; import { RelatedViewForm } from './RelatedViewForm'; import { RelatedWidgetItem, RelatedWidgets } from './RelatedWidgets'; -import { ControllerConfig } from './types'; import { getInitWidgetController, postControlConfig, - preformatWidgetFilter, + preformatControlConfig, } from './utils'; -const FilterWidgetPanel: React.FC = memo(props => { +const ControllerWidgetPanel: React.FC = memo(props => { const dispatch = useDispatch(); const t = useI18NPrefix('viz.common.enum.controllerFacadeTypes'); + const tGMT = useI18NPrefix(`global.modal.title`); const { type, widgetId, controllerType } = useSelector(selectControllerPanel); const { boardId, boardType, queryVariables } = useContext(BoardContext); - const { refreshWidgetsByFilter } = useContext(BoardActionContext); + const { refreshWidgetsByController: refreshWidgetsByFilter } = + useContext(BoardActionContext); const allWidgets = useSelector(selectSortAllWidgets); const widgets = useMemo( - () => getCanLinkControlWidgets(allWidgets), - [allWidgets], + () => + widgetToolKit.controller.tool + .getCanLinkControlWidgets(allWidgets) + .filter(t => t.id !== widgetId), + [allWidgets, widgetId], ); - const otherStrFilterWidgets = useMemo( + const otherStrTypeController = useMemo( () => getOtherStringControlWidgets(allWidgets, widgetId), [allWidgets, widgetId], ); + const widgetMap = useMemo(() => convertToWidgetMap(allWidgets), [allWidgets]); const viewMap = useSelector(selectViewMap); - + // const [relatedWidgets, setRelatedWidgets] = useState([]); const [visible, setVisible] = useState(false); @@ -147,7 +149,7 @@ const FilterWidgetPanel: React.FC = memo(props => { useEffect(() => { if (!curFilterWidget || !curFilterWidget?.relations) { form.setFieldsValue({ - config: preformatWidgetFilter( + config: preformatControlConfig( getInitWidgetController(controllerType), controllerType!, ), @@ -177,7 +179,7 @@ const FilterWidgetPanel: React.FC = memo(props => { form.setFieldsValue({ ...confContent, relatedViews: preRelatedViews, - config: preformatWidgetFilter(config, controllerType!), + config: preformatControlConfig(config, controllerType!), }); }, [ curFilterWidget, @@ -194,44 +196,13 @@ const FilterWidgetPanel: React.FC = memo(props => { setVisible(false); const { relatedViews, config, name } = values; if (type === 'add') { - const sourceId = uuidv4(); - const controlToWidgetRelations: Relation[] = relatedWidgets - .filter(relatedWidgetItem => { - return widgetMap[relatedWidgetItem.widgetId]; - }) - .map(relatedWidgetItem => { - const widget = widgetMap[relatedWidgetItem.widgetId]; - const relation: Relation = { - sourceId, - targetId: widget.id, - config: { - type: 'controlToWidget', - controlToWidget: { - widgetRelatedViewIds: widget.viewIds, - }, - }, - id: uuidv4(), - }; - return relation; - }); - let newRelations = [...controlToWidgetRelations]; - const ControllerVisibility = (config as ControllerConfig).visibility; - if (ControllerVisibility) { - const { visibilityType, condition } = ControllerVisibility; - if (visibilityType === 'condition' && condition) { - const controlToControlRelation: Relation = { - sourceId, - targetId: condition.dependentControllerId, - config: { - type: 'controlToControl', - }, - id: uuidv4(), - }; - newRelations = newRelations.concat([controlToControlRelation]); - } - } - - const widget = createControllerWidget({ + let newRelations = widgetToolKit.controller.tool.makeControlRelations({ + sourceId: undefined, + relatedWidgets: relatedWidgets, + widgetMap, + config: config, + }); + const widget = widgetToolKit.controller.create({ boardId, boardType, name, @@ -239,48 +210,20 @@ const FilterWidgetPanel: React.FC = memo(props => { controllerType: controllerType!, views: relatedViews, config: postControlConfig(config, controllerType!), - hasVariable: false, + viewIds: + widgetToolKit.controller.tool.getViewIdsInControlConfig(config), }); dispatch(addWidgetsToEditBoard([widget])); - dispatch(getEditControllerOptionAsync(widget)); + dispatch(getEditControllerOptions(widget.id)); refreshWidgetsByFilter(widget); } else if (type === 'edit') { - const sourceId = curFilterWidget.id; + let newRelations = widgetToolKit.controller.tool.makeControlRelations({ + sourceId: curFilterWidget.id, + relatedWidgets: relatedWidgets, + widgetMap, + config: config, + }); - const controlToWidgetRelations: Relation[] = relatedWidgets - .filter(relatedWidgetItem => { - return widgetMap[relatedWidgetItem.widgetId]; - }) - .map(relatedWidgetItem => { - const widget = widgetMap[relatedWidgetItem.widgetId]; - return { - sourceId, - targetId: widget.id, - config: { - type: 'controlToWidget', - controlToWidget: { - widgetRelatedViewIds: widget.viewIds, - }, - }, - id: uuidv4(), - }; - }); - let newRelations = [...controlToWidgetRelations]; - const controllerVisible = (config as ControllerConfig).visibility; - if (controllerVisible) { - const { visibilityType, condition } = controllerVisible; - if (visibilityType === 'condition' && condition) { - const controlToControlRelation: Relation = { - sourceId, - targetId: condition.dependentControllerId, - config: { - type: 'controlToControl', - }, - id: uuidv4(), - }; - newRelations = newRelations.concat([controlToControlRelation]); - } - } const nextContent: ControllerWidgetContent = { ...(curFilterWidget.config.content as ControllerWidgetContent), name, @@ -292,9 +235,11 @@ const FilterWidgetPanel: React.FC = memo(props => { draft.relations = newRelations; draft.config.name = name; draft.config.content = nextContent; + draft.viewIds = + widgetToolKit.controller.tool.getViewIdsInControlConfig(config); }); dispatch(editBoardStackActions.updateWidget(newWidget)); - dispatch(getEditControllerOptionAsync(newWidget)); + dispatch(getEditControllerOptions(newWidget.id)); refreshWidgetsByFilter(newWidget); } }, @@ -342,7 +287,7 @@ const FilterWidgetPanel: React.FC = memo(props => { }; return ( { {visible && ( { ); }); -export default FilterWidgetPanel; +export default ControllerWidgetPanel; const Container = styled(Split)` display: flex; flex: 1; diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/types.ts b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/types.ts index 2de621b17..c5d19a306 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/types.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/types.ts @@ -3,10 +3,11 @@ import { ControllerVisibleType, ValueOptionType, } from 'app/pages/DashBoardPage/constants'; -import { FilterValueOption } from 'app/types/ChartConfig'; +import { RelationFilterValue } from 'app/types/ChartConfig'; import { ChartDataViewFieldType } from 'app/types/ChartDataView'; -import { RelativeOrExactTime } from 'app/types/FilterControlPanel'; +import { TimeFilterValueCategory } from 'app/types/FilterControlPanel'; import { FilterSqlOperator } from 'globalConstants'; +import i18next from 'i18next'; import { Moment, unitOfTime } from 'moment'; import { VariableValueTypes } from '../../../../../MainPage/pages/VariablePage/constants'; @@ -34,7 +35,7 @@ export interface ControllerConfig { valueOptionType: ValueOptionType; // visibility: ControllerVisibility; sqlOperator: FilterSqlOperator; - valueOptions: FilterValueOption[]; + valueOptions: RelationFilterValue[]; controllerValues: any[]; required: boolean; // 是否允许空值 canChangeSqlOperator?: boolean; // 是否显示 sqlOperator 切换 @@ -70,17 +71,27 @@ export const enum PickerTypes { } export type PickerType = Uncapitalize; +const td = (value: PickerTypes) => { + const preStr = 'viz.date.'; + return i18next.t(preStr + value); +}; +const getPickerTypeItem = (value: PickerTypes) => { + return { + name: td(value), + value: value, + }; +}; export const PickerTypeOptions = [ - { name: '日期', value: PickerTypes.Date }, - { name: '日期时间', value: PickerTypes.DateTime }, - { name: '年', value: PickerTypes.Year }, - { name: '月', value: PickerTypes.Month }, - { name: '季度', value: PickerTypes.Quarter }, - { name: '周', value: PickerTypes.Week }, + getPickerTypeItem(PickerTypes.Date), + getPickerTypeItem(PickerTypes.DateTime), + getPickerTypeItem(PickerTypes.Year), + getPickerTypeItem(PickerTypes.Quarter), + getPickerTypeItem(PickerTypes.Month), + getPickerTypeItem(PickerTypes.Week), ]; export interface ControllerDateType { - relativeOrExact: RelativeOrExactTime; + relativeOrExact: TimeFilterValueCategory; relativeValue?: RelativeDate; exactValue?: Moment | string | null; } @@ -88,5 +99,5 @@ export interface ControllerDateType { export interface RelativeDate { amount: number; unit: unitOfTime.DurationConstructor; - direction: '-' | '+'; + direction: '-' | '+' | '+0'; } diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/utils.ts b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/utils.ts index a82ffda93..0523b2924 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/utils.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/utils.ts @@ -31,7 +31,7 @@ import { import { ControllerFacadeTypes, ControllerFacadeTypes as Opt, - RelativeOrExactTime, + TimeFilterValueCategory, } from 'app/types/FilterControlPanel'; import moment, { Moment } from 'moment'; import { FilterSqlOperator } from '../../../../../../../globalConstants'; @@ -82,7 +82,7 @@ export const getDateFacadeOptions = (category: ChartDataViewFieldCategory) => { } }; // 展示前处理 -export const preformatWidgetFilter = ( +export const preformatControlConfig = ( preConfig: ControllerConfig, controllerType: ControllerFacadeTypes, ) => { @@ -92,7 +92,26 @@ export const preformatWidgetFilter = ( } return config; }; +// 设置后处理 +export const postControlConfig = ( + config: ControllerConfig, + controllerType: ControllerFacadeTypes, +) => { + if (config.valueOptions.length > 0) { + config.controllerValues = config.valueOptions + .filter(ele => ele.isSelected) + .map(ele => ele.key); + } + if (DateControllerTypes.includes(controllerType)) { + config = formatControlDateToStr(config); + } + if (!Array.isArray(config.controllerValues)) { + config.controllerValues = [config.controllerValues]; + } + + return config; +}; export const formatControlDateToMoment = (config: ControllerConfig) => { if (config.controllerDate) { const filterDate = config.controllerDate; @@ -134,26 +153,6 @@ export const formatControlDateToStr = (config: ControllerConfig) => { } return config; }; -// 设置后处理 -export const postControlConfig = ( - config: ControllerConfig, - controllerType: ControllerFacadeTypes, -) => { - if (config.valueOptions.length > 0) { - config.controllerValues = config.valueOptions - .filter(ele => ele.isSelected) - .map(ele => ele.key); - } - - if (DateControllerTypes.includes(controllerType)) { - config = formatControlDateToStr(config); - } - if (!Array.isArray(config.controllerValues)) { - config.controllerValues = [config.controllerValues]; - } - - return config; -}; export const getInitWidgetController = ( type: ControllerFacadeTypes = ControllerFacadeTypes.DropdownList, @@ -171,6 +170,8 @@ export const getInitWidgetController = ( return getRangeSliderControllerConfig(); case ControllerFacadeTypes.RadioGroup: return getRadioGroupControllerConfig(); + case ControllerFacadeTypes.CheckboxGroup: + return getCheckboxGroupControllerConfig(); case ControllerFacadeTypes.Slider: return getSliderControllerConfig(); case ControllerFacadeTypes.DropdownList: @@ -201,7 +202,7 @@ export const getTimeControllerConfig = () => { config.controllerDate = { pickerType: 'date', startTime: { - relativeOrExact: RelativeOrExactTime.Exact, + relativeOrExact: TimeFilterValueCategory.Exact, exactValue: null, }, }; @@ -213,11 +214,11 @@ export const getRangeTimeControllerConfig = () => { config.controllerDate = { pickerType: 'date', startTime: { - relativeOrExact: RelativeOrExactTime.Exact, + relativeOrExact: TimeFilterValueCategory.Exact, exactValue: null, }, endTime: { - relativeOrExact: RelativeOrExactTime.Exact, + relativeOrExact: TimeFilterValueCategory.Exact, exactValue: null, }, }; @@ -228,6 +229,11 @@ export const getMultiDropdownListControllerConfig = () => { config.sqlOperator = FilterSqlOperator.In; return config; }; +export const getCheckboxGroupControllerConfig = () => { + const config = getInitControllerConfig(); + config.sqlOperator = FilterSqlOperator.In; + return config; +}; export const getRadioGroupControllerConfig = () => { const config = getInitControllerConfig(); config.sqlOperator = FilterSqlOperator.Equal; @@ -296,21 +302,18 @@ export const formatDateByPickType = ( switch (pickerType) { case 'dateTime': - case 'quarter': + return momentTime.format(formatTemp); case 'date': - return momentTime.set({ h: 0, m: 0, s: 0 }).format(formatTemp); + return momentTime.startOf('day').format(formatTemp); case 'week': let year = String(momentTime.year()); let week = String(momentTime.week() - 1); - var date = moment(year).add(week, 'weeks').startOf('week'); - var value = date.format(formatTemp); - return value; + return moment(year).add(week, 'weeks').startOf('week').format(formatTemp); + case 'quarter': case 'month': - return momentTime.set({ date: 1, h: 0, m: 0, s: 0 }).format(formatTemp); + return momentTime.startOf('month').format(formatTemp); case 'year': - return momentTime - .set({ month: 0, date: 1, h: 0, m: 0, s: 0 }) - .format(formatTemp); + return momentTime.startOf('year').format(formatTemp); default: return null; } diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/LinkagePanel/LinkageFields.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/LinkagePanel/LinkageFields.tsx index fee856c22..4d7824455 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/LinkagePanel/LinkageFields.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/LinkagePanel/LinkageFields.tsx @@ -17,12 +17,13 @@ */ import { LinkOutlined } from '@ant-design/icons'; import { Divider, Empty, Form, FormInstance, Select } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { Widget } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { ChartDataSectionField } from 'app/types/ChartConfig'; import ChartDataView, { ChartDataViewFieldType } from 'app/types/ChartDataView'; import React, { memo, useCallback } from 'react'; import styled from 'styled-components/macro'; -import { PRIMARY } from 'styles/StyleConstants'; +import { G20, PRIMARY } from 'styles/StyleConstants'; const { Option } = Select; export interface ViewLinkageItem { sameView?: boolean; @@ -41,7 +42,7 @@ export interface LinkageFieldsProps { } export const LinkageFields: React.FC = memo( ({ form, viewMap, curWidget, chartGroupColumns }) => { - // const dataChart + const t = useI18NPrefix(`viz.linkage`); const renderOptions = useCallback( (index: number, key: 'triggerViewId' | 'linkerViewId') => { const viewLinkages: ViewLinkageItem[] = @@ -58,7 +59,7 @@ export const LinkageFields: React.FC = memo( >
    {item.colName} - {item.type} + {item.type}
    )); @@ -73,7 +74,7 @@ export const LinkageFields: React.FC = memo( style={{ display: 'flex', justifyContent: 'space-between' }} > {item.id} - {item.type} + {item.type} )); @@ -102,9 +103,11 @@ export const LinkageFields: React.FC = memo( ); return ( - 关联字段 + {t('associatedFields')} -
    数据源 : {viewMap[curWidget?.viewIds?.[0]]?.name}
    +
    + {t('dataSource')} : {viewMap[curWidget?.viewIds?.[0]]?.name} +
    {(fields, _, { errors }) => { return ( @@ -121,13 +124,13 @@ export const LinkageFields: React.FC = memo( name={[field.name, 'triggerColumn']} fieldKey={[field.fieldKey, 'id']} rules={[ - { required: true, message: '请选择 触发字段' }, + { required: true, message: t('selectTriggers') }, ]} > {renderOptions(index, 'linkerViewId')} @@ -171,9 +174,7 @@ export const LinkageFields: React.FC = memo( - {!fields.length && ( - - )} + {!fields.length && } ); }} diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/LinkagePanel/index.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/LinkagePanel/index.tsx index 45b9474ff..047dd7621 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/LinkagePanel/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/LinkagePanel/index.tsx @@ -17,6 +17,7 @@ */ import { Form, Modal } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectDataChartById, selectViewMap, @@ -26,10 +27,8 @@ import { Relation, } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { getChartGroupColumns } from 'app/pages/DashBoardPage/utils'; -import { - convertToWidgetMap, - getCanLinkControlWidgets, -} from 'app/pages/DashBoardPage/utils/widget'; +import { convertToWidgetMap } from 'app/pages/DashBoardPage/utils/widget'; +import { getCanLinkageWidgets } from 'app/pages/DashBoardPage/utils/widgetToolKit/chart'; import produce from 'immer'; import React, { memo, @@ -50,21 +49,20 @@ import { LinkageWidgets } from './linkageWidgets'; export interface LinkagePanelProps {} export const LinkagePanel: React.FC = memo(() => { + const t = useI18NPrefix(`viz.linkage`); const [form] = Form.useForm(); const dispatch = useDispatch(); const { type, widgetId } = useSelector(selectLinkagePanel); const allWidgets = useSelector(selectSortAllWidgets); const viewMap = useSelector(selectViewMap); const widgets = useMemo( - () => getCanLinkControlWidgets(allWidgets).filter(w => w.id !== widgetId), + () => getCanLinkageWidgets(allWidgets).filter(w => w.id !== widgetId), [allWidgets, widgetId], ); - - // selectDataChartById const widgetMap = useMemo(() => convertToWidgetMap(allWidgets), [allWidgets]); const [visible, setVisible] = useState(false); - // const [sameViewWidgetIds, setSameViewWidgetIds] = useState([]); + const sameViewWidgetIds = useRef([]); const linkagesRef = useRef([]); useEffect(() => { @@ -83,7 +81,6 @@ export const LinkagePanel: React.FC = memo(() => { }; const onSubmit = useCallback(() => { - // handle onFinish form.submit(); }, [form]); const afterClose = useCallback(() => { @@ -225,8 +222,7 @@ export const LinkagePanel: React.FC = memo(() => { }, [curWidget, form, setColNames, widgetMap]); return ( = memo( ({ widgets, onChange, curWidget }) => { const [selectedWidgetIds, setSelectedWidgetIds] = useState([]); - + const t = useI18NPrefix(`viz.linkage`); useEffect(() => { if (!curWidget) { return; @@ -55,7 +56,7 @@ export const LinkageWidgets: React.FC = memo( ); return ( <> - 关联组件 + {t('associatedWidgets')} = ({ onChange, ...restProps }) => { + const t = useI18NPrefix(`viz.jump`); const _treeData = useMemo(() => { return disabledTreeData(treeData || [], filterBoardId); }, [treeData, filterBoardId]); @@ -52,7 +54,7 @@ export const TargetTreeSelect: FC = ({ }, [value]); return ( = ({ children, ...restProps }) => { + const t = useI18NPrefix(`viz.jump`); + const tv = useI18NPrefix(`global.validation`); const [form] = Form.useForm(); const dispatch = useDispatch(); const selectVizTree = useMemo(makeSelectVizTree, []); @@ -88,8 +93,11 @@ export const SettingJumpModal: FC = ({ useEffect(() => { const _jumpConfig = curJumpWidget?.config?.jumpConfig; setVisible(jumpVisible); + setTargetType(_jumpConfig?.targetType || jumpTypes[0].value); if (jumpVisible && _jumpConfig) { - onGetController(curJumpWidget?.config?.jumpConfig?.target); + if (curJumpWidget?.config?.jumpConfig?.targetType === 'INTERNAL') { + onGetController(curJumpWidget?.config?.jumpConfig?.target); + } form.setFieldsValue(_jumpConfig); } }, [jumpVisible, curJumpWidget, form, onGetController]); @@ -99,14 +107,11 @@ export const SettingJumpModal: FC = ({ const dataChart = useSelector((state: { board: BoardState }) => selectDataChartById(state, curJumpWidget?.datachartId), ); - const chartGroupColumns = useMemo(() => { - if (!dataChart) { - return []; - } - const builder = getChartDataRequestBuilder(dataChart); - let groupColumns = builder.buildGroupColumns(); - return groupColumns; - }, [dataChart]); + const [targetType, setTargetType] = useState(jumpTypes[0].value); + const chartGroupColumns = useMemo( + () => getChartGroupColumns(dataChart), + [dataChart], + ); const onTargetChange = useCallback( value => { form.setFieldsValue({ filter: undefined }); @@ -114,6 +119,9 @@ export const SettingJumpModal: FC = ({ }, [form, onGetController], ); + const onTargetTypeChange = useCallback(value => { + setTargetType(value); + }, []); const handleClose = useCallback(() => { dispatch( editDashBoardInfoActions.changeJumpPanel({ @@ -139,10 +147,9 @@ export const SettingJumpModal: FC = ({ }, [dispatch, curJumpWidget, handleClose, chartGroupColumns], ); - return ( = ({ >
    - + + {targetType === 'INTERNAL' && ( + + + + )} + + {targetType === 'URL' && ( + + + + )} + {targetType === 'URL' && ( + + + + )} {chartGroupColumns?.length > 1 && ( = ({ > )} - - - - + {targetType === 'INTERNAL' && ( + + + + )}
    ); diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/BoardSetting.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/BoardSetting.tsx index b46022ca6..6d968080b 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/BoardSetting.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/BoardSetting.tsx @@ -16,12 +16,13 @@ * limitations under the License. */ import { Collapse, Form } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { BoardConfigContext } from 'app/pages/DashBoardPage/contexts/BoardConfigContext'; import { BoardContext } from 'app/pages/DashBoardPage/contexts/BoardContext'; import { DashboardConfig } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { getRGBAColor } from 'app/pages/DashBoardPage/utils'; import produce from 'immer'; -import { throttle } from 'lodash'; +import throttle from 'lodash/throttle'; import React, { FC, memo, @@ -40,6 +41,7 @@ import { Group, SettingPanel } from './SettingPanel'; const { Panel } = Collapse; export const BoardSetting: FC = memo(() => { + const t = useI18NPrefix(`viz.board.setting`); const dispatch = useDispatch(); const { boardType } = useContext(BoardContext); const { config } = useContext(BoardConfigContext); @@ -48,7 +50,7 @@ export const BoardSetting: FC = memo(() => { useEffect(() => { cacheValue.current = { backgroundColor: config.background.color, - backgroundImage: [config.background.image], + backgroundImage: config.background.image, scaleMode: config.scaleMode, boardWidth: config.width, boardHeight: config.height, @@ -57,7 +59,7 @@ export const BoardSetting: FC = memo(() => { paddingW: config.containerPadding[0], paddingH: config.containerPadding[1], rowHeight: config.rowHeight, - initialQuery: config.initialQuery=== false ? false : true, // TODO migration 如果initialQuery的值为undefined默认为true 兼容旧的仪表盘没有initialQuery参数的问题 + initialQuery: config.initialQuery === false ? false : true, // TODO migration 如果initialQuery的值为undefined默认为true 兼容旧的仪表盘没有initialQuery参数的问题 }; form.setFieldsValue({ ...cacheValue.current }); }, [config, form]); @@ -65,9 +67,10 @@ export const BoardSetting: FC = memo(() => { const onUpdate = useCallback( (newValues, config: DashboardConfig) => { const value = { ...cacheValue.current, ...newValues }; + const nextConf = produce(config, draft => { draft.background.color = getRGBAColor(value.backgroundColor); - draft.background.image = value.backgroundImage[0]; + draft.background.image = value.backgroundImage; draft.scaleMode = value.scaleMode; draft.width = value.boardWidth; draft.height = value.boardHeight; @@ -76,16 +79,13 @@ export const BoardSetting: FC = memo(() => { draft.containerPadding[0] = value.paddingW; draft.containerPadding[1] = value.paddingH; draft.rowHeight = value.rowHeight; - draft.initialQuery= value.initialQuery; + draft.initialQuery = value.initialQuery; }); dispatch(editBoardStackActions.updateBoardConfig(nextConf)); }, [dispatch], ); - const onForceUpdate = useCallback(() => { - const values = form.getFieldsValue(); - onUpdate(values, config); - }, [config, form, onUpdate]); + const throttledUpdate = useRef( throttle((allValue, config) => onUpdate(allValue, config), 1000), ); @@ -97,7 +97,7 @@ export const BoardSetting: FC = memo(() => { ); return ( - +
    { > {boardType === 'auto' && ( <> - + - - - - - + + + + + )} {boardType === 'free' && ( <> - + - - + + - + )} - + - + - + - + diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/AutoUpdateSet.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/AutoUpdateSet.tsx index 98e5a3c78..273ed4f44 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/AutoUpdateSet.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/AutoUpdateSet.tsx @@ -16,16 +16,18 @@ * limitations under the License. */ import { Checkbox, Form } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { FC } from 'react'; import NumberSet from './BasicSet/NumberSet'; export const AutoUpdateSet: FC = () => { + const t = useI18NPrefix(`viz.board.setting`); return ( <> - 定时同步数据 + {t('openAutoUpdate')} - + ); diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BackgroundSet.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BackgroundSet.tsx index 274d22757..3f8cac562 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BackgroundSet.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BackgroundSet.tsx @@ -15,26 +15,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Form, FormInstance } from 'antd'; +import { Form } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { BackgroundConfig } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import React, { FC, memo } from 'react'; import ColorSet from './BasicSet/ColorSet'; -import ImageUpload from './BasicSet/ImageUpload'; +import { ImageUpload } from './BasicSet/ImageUpload'; export const BackgroundSet: FC<{ - form: FormInstance; - onForceUpdate: () => void; background: BackgroundConfig; -}> = memo(({ form, background, onForceUpdate }) => { +}> = memo(({ background }) => { + const t = useI18NPrefix(`viz.board.setting`); return ( <> - + ); diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ColorSet.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ColorSet.tsx index 18d3c062d..91e434c88 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ColorSet.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ColorSet.tsx @@ -17,7 +17,7 @@ */ import { BgColorsOutlined } from '@ant-design/icons'; import { Form, Popover } from 'antd'; -import { ReactColorPicker } from 'app/components/ReactColorPicker'; +import { SingleColorSelection } from 'app/components/ColorPicker'; import { NamePath } from 'rc-field-form/lib/interface'; import React, { FC, memo } from 'react'; import styled from 'styled-components/macro'; @@ -27,7 +27,7 @@ export const ColorSet: FC<{ }> = memo(({ filedValue, filedName }) => { const widgetContent = ( - + ); return ( @@ -44,6 +44,7 @@ export const ColorSet: FC<{ export default ColorSet; const StyledWrap = styled.div` display: inline-block; + cursor: pointer; `; const StyledColorIcon = styled.span<{ color: string }>` font-size: 1.4em; diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ImageUpload.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ImageUpload.tsx index a5cd0dfac..0b66ada65 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ImageUpload.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ImageUpload.tsx @@ -16,7 +16,7 @@ * limitations under the License. */ import { DeleteOutlined } from '@ant-design/icons'; -import { Form, FormInstance, Upload } from 'antd'; +import { Form, Upload } from 'antd'; import { BoardContext } from 'app/pages/DashBoardPage/contexts/BoardContext'; import { convertImageUrl } from 'app/pages/DashBoardPage/utils'; import React, { useCallback, useContext } from 'react'; @@ -25,49 +25,47 @@ import styled from 'styled-components/macro'; import { uploadBoardImage } from '../../../../slice/thunk'; export interface ImageUploadProps { - form: FormInstance; filedName: string; - onForceUpdate: () => void; - filedValue: string; + value: string; + label: string; + placeholder: string; } -const ImageUpload: React.FC = ({ - form, +export const ImageUpload: React.FC = ({ filedName, - filedValue, - onForceUpdate, + value, + label, + placeholder, }) => { + return ( + + + + + + ); +}; +export const UploadDragger: React.FC<{ + value: string; + onChange?: any; + placeholder: string; +}> = ({ value, onChange, placeholder }) => { const dispatch = useDispatch(); const { boardId } = useContext(BoardContext); - const onChange = useCallback( - value => { - form.setFieldsValue({ - [filedName]: [value], - }); - onForceUpdate(); - }, - [filedName, form, onForceUpdate], - ); const beforeUpload = useCallback( async info => { - // const reader = new FileReader(); - // reader.readAsDataURL(info); - // reader.onload = () => { - // const urlIndex = `${STORAGE_IMAGE_KEY_PREFIX}${info.name || info.uid}`; - // if (reader.result) { - // localStorage.setItem(urlIndex, reader.result as string); - // onChange(urlIndex); - // } - // }; const formData = new FormData(); formData.append('file', info); - dispatch( + await dispatch( uploadBoardImage({ boardId, formData: formData, resolve: onChange }), ); return false; }, [boardId, dispatch, onChange], ); + const getImageError = useCallback(() => { + onChange(''); + }, [onChange]); const delImageUrl = useCallback( e => { e.stopPropagation(); @@ -75,56 +73,30 @@ const ImageUpload: React.FC = ({ }, [onChange], ); - const normFile = (e: any) => { - if (Array.isArray(e)) { - return e; - } - return e && e.fileList; - }; - const getImageError = useCallback(() => { - onChange(''); - }, [onChange]); return ( - - - - {filedValue ? ( -
    - - -
    - ) : ( - 点击上传 - )} -
    -
    -
    + + {value ? ( +
    + + +
    + ) : ( + {placeholder} + )} +
    ); }; - -export default ImageUpload; - -const Wrapper = styled.div` - .ant-upload-list { - display: none; - } - +const StyleUpload = styled(Upload.Dragger)` .imageUpload { display: block; } @@ -155,6 +127,11 @@ const Wrapper = styled.div` height: auto; } `; +const Wrapper = styled.div` + .ant-upload-list { + display: none; + } +`; const Placeholder = styled.p` color: ${p => p.theme.textColorLight}; diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BorderSet.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BorderSet.tsx index 40be61a61..a23f51325 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BorderSet.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BorderSet.tsx @@ -16,6 +16,7 @@ * limitations under the License. */ import { Form, InputNumber, Select } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { BORDER_STYLE_OPTIONS } from 'app/pages/DashBoardPage/constants'; import { BorderConfig } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import React, { FC, memo } from 'react'; @@ -23,26 +24,28 @@ import ColorSet from './BasicSet/ColorSet'; export const BorderSet: FC<{ border: BorderConfig; }> = memo(({ border }) => { + const t = useI18NPrefix(`viz.board.setting`); + const tLine = useI18NPrefix(`viz.lineOptions`); return ( <> {/* 边框颜色: */} - + - + - + - + diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/InitialQuerySet.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/InitialQuerySet.tsx index 8999b796c..5e0bf33b0 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/InitialQuerySet.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/InitialQuerySet.tsx @@ -15,20 +15,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { Form, Checkbox } from 'antd'; - import { NamePath } from 'rc-field-form/lib/interface'; - import React, { FC, memo } from 'react'; - export const InitialQuerySet: FC<{ - name: NamePath; - }> = memo(({ name}) => { - return ( - <> - - 初始化自动查询 - - - ); - }); - - export default InitialQuerySet; - \ No newline at end of file +import { Checkbox, Form } from 'antd'; +import { NamePath } from 'rc-field-form/lib/interface'; +import React, { FC, memo } from 'react'; +export const InitialQuerySet: FC<{ + name: NamePath; + label: string; +}> = memo(({ name, label }) => { + return ( + <> + + {label} + + + ); +}); + +export default InitialQuerySet; diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/NameSet.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/NameSet.tsx index 95ad41723..56c0052d0 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/NameSet.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/NameSet.tsx @@ -15,8 +15,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Checkbox, Form, FormInstance, Input } from 'antd'; +import { Checkbox, Form, Input } from 'antd'; import BasicFont from 'app/components/FormGenerator/Basic/BasicFont'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { WIDGET_TITLE_ALIGN_OPTIONS } from 'app/pages/DashBoardPage/constants'; import { WidgetNameConfig } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { fontDefault } from 'app/pages/DashBoardPage/utils/widget'; @@ -33,9 +34,8 @@ const FONT_DATA = { export const NameSet: FC<{ config: WidgetNameConfig; - form: FormInstance; - onForceUpdate: () => void; -}> = memo(({ config, onForceUpdate, form }) => { +}> = memo(({ config }) => { + const t = useI18NPrefix(`viz.board.setting`); const fontData = useMemo(() => { const data = { ...FONT_DATA, @@ -44,20 +44,20 @@ export const NameSet: FC<{ return data; }, [config]); - const normfontData = (ancestors, data) => { + const normFontData = (ancestors, data) => { const nameConfig = { ...config, ...data.value }; return nameConfig; }; return ( <> - + - 显示标题 + {t('showTitle')} - + = memo(() => { + const t = useI18NPrefix(`viz.board.setting`); return ( <> - + - + - + - + diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/ScaleModeSet.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/ScaleModeSet.tsx index 58d1b7ecd..55911da55 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/ScaleModeSet.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/ScaleModeSet.tsx @@ -16,21 +16,23 @@ * limitations under the License. */ import { Form, Select } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ScaleModeType, SCALE_MODE__OPTIONS, } from 'app/pages/DashBoardPage/constants'; import React, { FC, memo } from 'react'; -import styled from 'styled-components/macro'; export const ScaleModeSet: FC<{ scaleMode: ScaleModeType; }> = memo(({ scaleMode }) => { + const t = useI18NPrefix(`viz.board.setting`); + const tScale = useI18NPrefix(`viz.scaleMode`); return ( - + @@ -39,4 +41,3 @@ export const ScaleModeSet: FC<{ }); export default ScaleModeSet; -const StyledWrap = styled.div``; diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/WidgetSetting.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/WidgetSetting.tsx index 0197f5642..b9bedf5e4 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/WidgetSetting.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/WidgetSetting.tsx @@ -16,12 +16,13 @@ * limitations under the License. */ import { Collapse, Form } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { BoardContext } from 'app/pages/DashBoardPage/contexts/BoardContext'; import { WidgetContext } from 'app/pages/DashBoardPage/contexts/WidgetContext'; import { Widget } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { getRGBAColor } from 'app/pages/DashBoardPage/utils'; import produce from 'immer'; -import { throttle } from 'lodash'; +import throttle from 'lodash/throttle'; import React, { FC, memo, @@ -43,6 +44,7 @@ import { WidgetNameList } from './WidgetList/WidgetNameList'; const { Panel } = Collapse; export const WidgetSetting: FC = memo(() => { + const t = useI18NPrefix(`viz.board.setting`); const dispatch = useDispatch(); const { boardType } = useContext(BoardContext); const widget = useContext(WidgetContext); @@ -54,7 +56,7 @@ export const WidgetSetting: FC = memo(() => { name: config.name, nameConfig: config.nameConfig, backgroundColor: config.background.color, - backgroundImage: [config.background.image], + backgroundImage: config.background.image, border: config.border, rect: config.rect, autoUpdate: config.autoUpdate || false, @@ -69,14 +71,17 @@ export const WidgetSetting: FC = memo(() => { const value = { ...cacheValue.current, ...newValues }; value.border.color = getRGBAColor(value.border.color); - value.nameConfig = {...value.nameConfig,color:getRGBAColor(value.nameConfig.color)}; + value.nameConfig = { + ...value.nameConfig, + color: getRGBAColor(value.nameConfig.color), + }; // value.nameConfig.color = getRGBAColor(value.nameConfig.color); const nextConf = produce(widget.config, draft => { draft.name = value.name; draft.nameConfig = value.nameConfig; draft.background.color = getRGBAColor(value.backgroundColor); - draft.background.image = value.backgroundImage[0]; + draft.background.image = value.backgroundImage; draft.border = value.border; draft.rect = value.rect; draft.padding = value.padding; @@ -102,13 +107,9 @@ export const WidgetSetting: FC = memo(() => { }, [throttledUpdate, widget], ); - const onForceUpdate = useCallback(() => { - const values = form.getFieldsValue(); - onUpdate(values, widget); - }, [form, onUpdate, widget]); return ( - + { className="datart-config-panel" ghost > - + - + {boardType === 'free' && ( <> - + - - + + - + - - + + )} - + - + - + - + - + - + diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/index.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/index.tsx index 17bbc0385..07d5efc05 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/index.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/index.tsx @@ -25,12 +25,15 @@ import { BoardProvider } from '../../components/BoardProvider/BoardProvider'; import TitleHeader from '../../components/TitleHeader'; import { DataChart, WidgetContentChartType } from '../Board/slice/types'; import AutoEditor from './AutoEditor/index'; -import FilterWidgetPanel from './components/ControllerWidgetPanel'; +import ControllerWidgetPanel from './components/ControllerWidgetPanel'; import { LinkagePanel } from './components/LinkagePanel'; import { SettingJumpModal } from './components/SettingJumpModal'; import FreeEditor from './FreeEditor/index'; import { editDashBoardInfoActions } from './slice'; -import { editWrapChartWidget } from './slice/actions/actions'; +import { + addVariablesToBoard, + editHasChartWidget, +} from './slice/actions/actions'; import { selectBoardChartEditorProps, selectEditBoard, @@ -65,15 +68,10 @@ export const BoardEditor: React.FC<{ const onSaveToWidget = useCallback( (chartType: WidgetContentChartType, dataChart: DataChart, view) => { - if (chartType === 'widgetChart') { - const widgetId = boardChartEditorProps?.widgetId!; - dispatch(editWrapChartWidget({ widgetId, dataChart, view })); - onCloseChartEditor(); - } else { - const widgetId = boardChartEditorProps?.widgetId!; - dispatch(editWrapChartWidget({ widgetId, dataChart, view })); - onCloseChartEditor(); - } + const widgetId = boardChartEditorProps?.widgetId!; + dispatch(editHasChartWidget({ widgetId, dataChart, view })); + onCloseChartEditor(); + dispatch(addVariablesToBoard(view.variables)); }, [boardChartEditorProps?.widgetId, dispatch, onCloseChartEditor], ); @@ -98,7 +96,7 @@ export const BoardEditor: React.FC<{ {boardType === 'auto' && } {boardType === 'free' && } - + {boardChartEditorProps && ( diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/actions/actions.ts b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/actions/actions.ts index 2357cebc9..2d5911a80 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/actions/actions.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/actions/actions.ts @@ -32,18 +32,19 @@ import { createInitWidgetConfig, createWidget, } from 'app/pages/DashBoardPage/utils/widget'; +import { Variable } from 'app/pages/MainPage/pages/VariablePage/slice/types'; import ChartDataView, { ChartDataViewFieldType } from 'app/types/ChartDataView'; import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; +import i18next from 'i18next'; import produce from 'immer'; import { RootState } from 'types'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import { editBoardStackActions, editDashBoardInfoActions } from '..'; import { BoardType } from '../../../Board/slice/types'; import { ControllerConfig } from '../../components/ControllerWidgetPanel/types'; -import { addWidgetsToEditBoard, getEditWidgetDataAsync } from '../thunk'; +import { addWidgetsToEditBoard, getEditChartWidgetDataAsync } from '../thunk'; import { HistoryEditBoard } from '../types'; import { editWidgetsQueryAction } from './controlActions'; - const { confirm } = Modal; export const clearEditBoardState = (boardId: string) => async (dispatch, getState) => { @@ -90,11 +91,10 @@ export const deleteWidgetsAction = () => (dispatch, getState) => { }); if (childWidgetIds.length > 0) { + const perStr = 'viz.widget.action.'; confirm({ - // TODO i18n - title: '注意', - content: - '您要删除的组件中 有Container 组件,删除会将容器内的组件一起删除', + title: i18next.t(perStr + 'confirmDel'), + content: i18next.t(perStr + 'confirmDel1'), onOk() { dispatch(editBoardStackActions.deleteWidgets(selectedIds)); }, @@ -141,7 +141,6 @@ export const updateWidgetControllerAction = controllerFacadeType: ControllerFacadeTypes; views: RelatedView[]; config: ControllerConfig; - hasVariable?: boolean; }) => async (dispatch, getState) => { const { @@ -220,7 +219,7 @@ export const editChartInWidgetAction = }; dispatch(editDashBoardInfoActions.changeChartEditorProps(editorProps)); }; -export const editWrapChartWidget = +export const editHasChartWidget = (props: { widgetId: string; dataChart: DataChart; view: ChartDataView }) => async (dispatch, getState) => { const { dataChart, view, widgetId } = props; @@ -235,7 +234,7 @@ export const editWrapChartWidget = const viewViews = [view]; dispatch(boardActions.setDataChartMap(dataCharts)); dispatch(boardActions.setViewMap(viewViews)); - dispatch(getEditWidgetDataAsync({ widgetId })); + dispatch(getEditChartWidgetDataAsync({ widgetId: curWidget.id })); }; export const closeJumpAction = (widget: Widget) => (dispatch, getState) => { @@ -261,3 +260,17 @@ export const closeLinkageAction = (widget: Widget) => (dispatch, getState) => { }), ); }; + +export const addVariablesToBoard = + (variables: Variable[]) => (dispatch, getState) => { + if (!variables?.length) return; + const addedViewId = variables[0].viewId; + if (!addedViewId) return; + + const editBoard = getState().editBoard as HistoryEditBoard; + const queryVariables = editBoard.stack.present.dashBoard.queryVariables; + const hasAddedViewId = queryVariables.find(v => v.viewId === addedViewId); + if (hasAddedViewId) return; + let newVariables = queryVariables.concat(variables); + dispatch(editBoardStackActions.updateQueryVariables(newVariables)); + }; diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/actions/controlActions.ts b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/actions/controlActions.ts index bb87a970b..10c5b3887 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/actions/controlActions.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/actions/controlActions.ts @@ -19,15 +19,15 @@ import { BoardType, WidgetType, } from 'app/pages/DashBoardPage/pages/Board/slice/types'; -import { createControlBtn } from 'app/pages/DashBoardPage/utils/widget'; +import { widgetToolKit } from 'app/pages/DashBoardPage/utils/widgetToolKit/widgetToolKit'; import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; import { editBoardStackActions, editDashBoardInfoActions } from '..'; import { PageInfo } from './../../../../../MainPage/pages/ViewPage/slice/types'; -import { addWidgetsToEditBoard, getEditWidgetDataAsync } from './../thunk'; +import { addWidgetsToEditBoard, getEditChartWidgetDataAsync } from './../thunk'; import { HistoryEditBoard } from './../types'; export type BtnActionParams = { - type: ControllerFacadeTypes | WidgetType; + type: WidgetType; boardId: string; boardType: BoardType; }; @@ -35,7 +35,7 @@ export const addControllerAction = (opt: BtnActionParams) => async (dispatch, getState) => { switch (opt.type as WidgetType) { case 'query': - const queryWidget = createControlBtn({ + const queryWidget = widgetToolKit.query.create({ boardId: opt.boardId, boardType: opt.boardType, type: opt.type as any, @@ -44,7 +44,7 @@ export const addControllerAction = dispatch(editBoardStackActions.changeBoardHasQueryControl(true)); break; case 'reset': - const resetWidget = createControlBtn({ + const resetWidget = widgetToolKit.query.create({ boardId: opt.boardId, boardType: opt.boardType, type: opt.type as any, @@ -81,7 +81,7 @@ export const editWidgetsQueryAction = .filter(it => it.config.type === 'chart') .forEach(it => { dispatch( - getEditWidgetDataAsync({ + getEditChartWidgetDataAsync({ widgetId: it.id, option: { pageInfo }, }), diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/childSlice/stackSlice.ts b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/childSlice/stackSlice.ts index 892193439..299c11c1d 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/childSlice/stackSlice.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/childSlice/stackSlice.ts @@ -26,6 +26,8 @@ import { Widget, WidgetConf, } from 'app/pages/DashBoardPage/pages/Board/slice/types'; +import { getDefaultWidgetName } from 'app/pages/DashBoardPage/utils'; +import { Variable } from 'app/pages/MainPage/pages/VariablePage/slice/types'; import produce from 'immer'; import { Layout } from 'react-grid-layout'; import { createSlice } from 'utils/@reduxjs/toolkit'; @@ -56,6 +58,10 @@ export const editBoardStackSlice = createSlice({ updateBoardConfig(state, action: PayloadAction) { state.dashBoard.config = action.payload; }, + updateQueryVariables(state, action: PayloadAction) { + const variables = action.payload; + state.dashBoard.queryVariables = variables; + }, changeBoardHasQueryControl(state, action: PayloadAction) { state.dashBoard.config.hasQueryControl = action.payload; }, @@ -70,6 +76,8 @@ export const editBoardStackSlice = createSlice({ maxWidgetIndex++; const widget = produce(ele, draft => { draft.config.index = maxWidgetIndex; + draft.config.name = + ele.config.name || getDefaultWidgetName(ele, maxWidgetIndex); }); state.widgetRecord[widget.id] = widget; }); diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/index.ts b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/index.ts index f0a385817..7529a0fe0 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/index.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/index.ts @@ -22,7 +22,7 @@ import { editBoardStackSlice } from './childSlice/stackSlice'; import { getEditBoardDetail, getEditChartWidgetDataAsync, - getEditWidgetDataAsync, + getEditControllerOptions, toUpdateDashboard, } from './thunk'; @@ -235,30 +235,41 @@ const widgetInfoRecordSlice = createSlice({ const { widgetId, pageInfo } = action.payload; state[widgetId].pageInfo = pageInfo || { pageNo: 1 }; }, + setWidgetErrInfo( + state, + action: PayloadAction<{ + boardId?: string; + widgetId: string; + errInfo?: string; + }>, + ) { + const { widgetId, errInfo } = action.payload; + state[widgetId].errInfo = errInfo; + }, }, extraReducers: builder => { - builder.addCase(getEditWidgetDataAsync.pending, (state, action) => { + builder.addCase(getEditChartWidgetDataAsync.pending, (state, action) => { const { widgetId } = action.meta.arg; state[widgetId].loading = true; }); - builder.addCase(getEditWidgetDataAsync.fulfilled, (state, action) => { + builder.addCase(getEditChartWidgetDataAsync.fulfilled, (state, action) => { const { widgetId } = action.meta.arg; state[widgetId].loading = false; }); - builder.addCase(getEditWidgetDataAsync.rejected, (state, action) => { + builder.addCase(getEditChartWidgetDataAsync.rejected, (state, action) => { const { widgetId } = action.meta.arg; state[widgetId].loading = false; }); - builder.addCase(getEditChartWidgetDataAsync.pending, (state, action) => { - const { widgetId } = action.meta.arg; + builder.addCase(getEditControllerOptions.pending, (state, action) => { + const widgetId = action.meta.arg; state[widgetId].loading = true; }); - builder.addCase(getEditChartWidgetDataAsync.fulfilled, (state, action) => { - const { widgetId } = action.meta.arg; + builder.addCase(getEditControllerOptions.fulfilled, (state, action) => { + const widgetId = action.meta.arg; state[widgetId].loading = false; }); - builder.addCase(getEditChartWidgetDataAsync.rejected, (state, action) => { - const { widgetId } = action.meta.arg; + builder.addCase(getEditControllerOptions.rejected, (state, action) => { + const widgetId = action.meta.arg; state[widgetId].loading = false; }); }, diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/thunk.ts b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/thunk.ts index c843c6b5f..514296da9 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/thunk.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/thunk.ts @@ -9,7 +9,6 @@ import { getDataOption, SaveDashboard, ServerDatachart, - ServerView, Widget, WidgetData, WidgetInfo, @@ -24,19 +23,22 @@ import { } from 'app/pages/DashBoardPage/utils/board'; import { convertWrapChartWidget, - createDataChartWidget, createToSaveWidgetGroup, createWidgetInfo, createWidgetInfoMap, getWidgetInfoMapByServer, + getWidgetMapByServer, } from 'app/pages/DashBoardPage/utils/widget'; +import { getControlOptionQueryParams } from 'app/pages/DashBoardPage/utils/widgetToolKit/chart'; +import { widgetToolKit } from 'app/pages/DashBoardPage/utils/widgetToolKit/widgetToolKit'; +import { Variable } from 'app/pages/MainPage/pages/VariablePage/slice/types'; +import { View } from 'app/pages/MainPage/pages/ViewPage/slice/types'; import ChartDataView from 'app/types/ChartDataView'; import { ActionCreators } from 'redux-undo'; import { RootState } from 'types'; import { CloneValueDeep } from 'utils/object'; import { request } from 'utils/request'; -import { errorHandle } from 'utils/utils'; -import { v4 as uuidv4 } from 'uuid'; +import { errorHandle, uuidv4 } from 'utils/utils'; import { editBoardStackActions, editDashBoardInfoActions, @@ -44,12 +46,9 @@ import { editWidgetInfoActions, } from '.'; import { BoardInfo, BoardType, ServerDashboard } from '../../Board/slice/types'; -import { getDistinctFields } from './../../../../../utils/fetch'; import { getDataChartMap } from './../../../utils/board'; -import { - getWidgetMapByServer, - updateWidgetsRect, -} from './../../../utils/widget'; +import { updateWidgetsRect } from './../../../utils/widget'; +import { addVariablesToBoard } from './actions/actions'; import { boardInfoState, editBoardStackState, @@ -162,32 +161,27 @@ export const toUpdateDashboard = createAsyncThunk< getState() as { editBoard: EditBoardState }, ); const boardState = getState() as unknown as { board: BoardState }; - // const dataChart = boardState.board.dataChartMap[curWidget.datachartId]; - // const chartDataView = boardState.board.viewMap[dataChart.viewId]; - const { dataChartMap, viewMap } = boardState.board; - const widgets = convertWrapChartWidget({ - widgetMap: widgetRecord, - dataChartMap, - viewMap, - }); - const group = createToSaveWidgetGroup(widgets, boardInfo.widgetIds); - const updateData: SaveDashboard = { - ...dashBoard, - config: JSON.stringify(dashBoard.config), - widgetToCreate: group.widgetToCreate, - widgetToUpdate: group.widgetToUpdate, - widgetToDelete: group.widgetToDelete, - }; try { - const { data } = await request({ + const { dataChartMap, viewMap } = boardState.board; + const widgets = convertWrapChartWidget({ + widgetMap: widgetRecord, + dataChartMap, + viewMap, + }); + const group = createToSaveWidgetGroup(widgets, boardInfo.widgetIds); + const updateData: SaveDashboard = { + ...dashBoard, + config: JSON.stringify(dashBoard.config), + widgetToCreate: group.widgetToCreate, + widgetToUpdate: group.widgetToUpdate, + widgetToDelete: group.widgetToDelete, + }; + + await request({ url: `viz/dashboards/${dashBoard.id}`, method: 'put', data: updateData, }); - // TODO - // 清空历史栈 - // 更新当前编辑面板的旧数据 widget Id 还都是本地的不对,应该更新成服务端id - // callback(); dispatch(ActionCreators.clearHistory()); // 更新view界面数据 @@ -195,11 +189,8 @@ export const toUpdateDashboard = createAsyncThunk< dispatch(fetchEditBoardDetail(dashBoard.id)); // 关闭编辑 界面 - - // TODO } catch (error) { errorHandle(error); - throw error; } }, ); @@ -238,10 +229,11 @@ export const addDataChartWidgets = createAsyncThunk< 'editBoard/addDataChartWidgets', async ({ boardId, chartIds, boardType }, { getState, dispatch }) => { const { - data: { datacharts, views }, + data: { datacharts, views, viewVariables }, } = await request<{ datacharts: ServerDatachart[]; - views: ServerView[]; + views: View[]; + viewVariables: Record; }>({ url: `viz/datacharts?datachartIds=${chartIds.join()}`, method: 'get', @@ -251,8 +243,9 @@ export const addDataChartWidgets = createAsyncThunk< const viewViews = getChartDataView(views, dataCharts); dispatch(boardActions.setDataChartMap(dataCharts)); dispatch(boardActions.setViewMap(viewViews)); + const widgets = chartIds.map(dcId => { - let widget = createDataChartWidget({ + let widget = widgetToolKit.chart.create({ dashboardId: boardId, boardType: boardType, dataChartId: dcId, @@ -263,6 +256,10 @@ export const addDataChartWidgets = createAsyncThunk< return widget; }); dispatch(addWidgetsToEditBoard(widgets)); + + Object.values(viewVariables).forEach(variables => { + dispatch(addVariablesToBoard(variables)); + }); return null; }, ); @@ -288,7 +285,7 @@ export const addWrapChartWidget = createAsyncThunk< const viewViews = [view]; dispatch(boardActions.setDataChartMap(dataCharts)); dispatch(boardActions.setViewMap(viewViews)); - let widget = createDataChartWidget({ + let widget = widgetToolKit.chart.create({ dashboardId: boardId, boardType: boardType, dataChartId: chartId, @@ -297,6 +294,7 @@ export const addWrapChartWidget = createAsyncThunk< subType: 'widgetChart', }); dispatch(addWidgetsToEditBoard([widget])); + dispatch(addVariablesToBoard(view.variables)); return null; }, ); @@ -308,14 +306,14 @@ export const renderedEditWidgetAsync = createAsyncThunk< >( 'editBoard/renderedEditWidgetAsync', async ({ boardId, widgetId }, { getState, dispatch, rejectWithValue }) => { - const { widgetRecord } = editBoardStackState( + const { widgetRecord: WidgetMap } = editBoardStackState( getState() as unknown as { editBoard: HistoryEditBoard; }, ); - const curWidget = widgetRecord[widgetId]; + const curWidget = WidgetMap[widgetId]; if (!curWidget) return null; - // 1 widget render + dispatch(editWidgetInfoActions.renderedWidgets([widgetId])); if (curWidget.config.type === 'container') { @@ -328,121 +326,12 @@ export const renderedEditWidgetAsync = createAsyncThunk< dispatch(editWidgetInfoActions.renderedWidgets(subWidgetIds)); // 2 widget getData subWidgetIds.forEach(wid => { - dispatch(getEditWidgetDataAsync({ widgetId: wid })); + dispatch(getEditWidgetData({ widget: WidgetMap[wid] })); }); return null; } // 2 widget getData - dispatch(getEditWidgetDataAsync({ widgetId })); - return null; - }, -); -export const getEditWidgetDataAsync = createAsyncThunk< - null, - { widgetId: string; option?: getDataOption }, - { state: RootState } ->( - 'editBoard/getEditWidgetDataAsync', - async ({ widgetId, option }, { getState, dispatch }) => { - dispatch(editWidgetInfoActions.renderedWidgets([widgetId])); - const rootState = getState() as RootState; - const stackEditBoard = rootState.editBoard as unknown as HistoryEditBoard; - const { widgetRecord: widgetMap } = stackEditBoard.stack.present; - - const curWidget = widgetMap[widgetId]; - if (!curWidget) return null; - - switch (curWidget.config.type) { - case 'chart': - await dispatch(getEditChartWidgetDataAsync({ widgetId, option })); - return null; - case 'controller': - await dispatch(getEditControllerOptionAsync(curWidget)); - return null; - case 'media': - case 'container': - default: - return null; - } - }, -); -export const getEditControllerOptionAsync = createAsyncThunk< - null, - Widget, - { state: RootState } ->('editBoard/getControllerOptions', async (widget, { getState, dispatch }) => { - const content = widget.config.content as ControllerWidgetContent; - const config = content.config; - if (config.assistViewFields && Array.isArray(config.assistViewFields)) { - // 请求 - const [viewId, viewField] = config.assistViewFields; - const dataset = await getDistinctFields( - viewId, - viewField, - undefined, - undefined, - ); - dispatch( - editWidgetDataActions.setWidgetData({ - ...dataset, - id: widget.id, - } as unknown as WidgetData), - ); - } - return null; -}); - -export const getEditChartWidgetDataAsync = createAsyncThunk< - null, - { - widgetId: string; - option?: getDataOption; - }, - { state: RootState } ->( - 'editBoard/getEditChartWidgetDataAsync', - async ({ widgetId, option }, { getState, dispatch, rejectWithValue }) => { - const rootState = getState() as RootState; - dispatch(editWidgetInfoActions.renderedWidgets([widgetId])); - const stackEditBoard = rootState.editBoard as unknown as HistoryEditBoard; - const { widgetRecord: widgetMap } = stackEditBoard.stack.present; - const editBoard = rootState.editBoard; - const boardInfo = editBoard?.boardInfo as BoardInfo; - const boardState = rootState.board as BoardState; - const widgetInfo = editBoard?.widgetInfoRecord[widgetId]; - const viewMap = boardState.viewMap; - const curWidget = widgetMap[widgetId]; - - if (!curWidget) return null; - const dataChartMap = boardState.dataChartMap; - const boardLinkFilters = boardInfo.linkFilter; - - let requestParams = getChartWidgetRequestParams({ - widgetId, - widgetMap, - viewMap, - option, - widgetInfo, - dataChartMap, - boardLinkFilters, - }); - if (!requestParams) { - return null; - } - let widgetData; - const { data } = await request({ - method: 'POST', - url: `data-provider/execute`, - data: requestParams, - }); - widgetData = { ...data, id: widgetId }; - dispatch(editWidgetDataActions.setWidgetData(widgetData as WidgetData)); - dispatch( - editWidgetInfoActions.changePageInfo({ - widgetId, - pageInfo: data.pageInfo, - }), - ); + dispatch(getEditWidgetData({ widget: curWidget })); return null; }, ); @@ -480,6 +369,7 @@ export const copyWidgetByIds = createAsyncThunk< return null; }, ); + // 粘贴 export const pasteWidgets = createAsyncThunk( 'editBoard/pasteWidgets', @@ -528,6 +418,8 @@ export const pasteWidgets = createAsyncThunk( return null; }, ); + +// export const uploadBoardImage = createAsyncThunk< null, { boardId: string; formData: FormData; resolve: (url: string) => void } @@ -548,3 +440,149 @@ export const uploadBoardImage = createAsyncThunk< } }, ); + +export const getEditWidgetData = createAsyncThunk< + null, + { widget: Widget; option?: getDataOption }, + { state: RootState } +>( + 'editBoard/getEditWidgetData', + ({ widget, option }, { getState, dispatch }) => { + dispatch(editWidgetInfoActions.renderedWidgets([widget.id])); + if (widget.config.type === 'chart') { + dispatch(getEditChartWidgetDataAsync({ widgetId: widget.id, option })); + } + if (widget.config.type === 'controller') { + dispatch(getEditControllerOptions(widget.id)); + } + return null; + }, +); + +export const getEditChartWidgetDataAsync = createAsyncThunk< + null, + { + widgetId: string; + option?: getDataOption; + }, + { state: RootState } +>( + 'editBoard/getEditChartWidgetDataAsync', + async ({ widgetId, option }, { getState, dispatch, rejectWithValue }) => { + const rootState = getState() as RootState; + dispatch(editWidgetInfoActions.renderedWidgets([widgetId])); + const stackEditBoard = rootState.editBoard as unknown as HistoryEditBoard; + const { widgetRecord: widgetMap } = stackEditBoard.stack.present; + const editBoard = rootState.editBoard; + const boardInfo = editBoard?.boardInfo as BoardInfo; + const boardState = rootState.board as BoardState; + const widgetInfo = editBoard?.widgetInfoRecord[widgetId]; + const viewMap = boardState.viewMap; + const curWidget = widgetMap[widgetId]; + + if (!curWidget) return null; + const dataChartMap = boardState.dataChartMap; + const boardLinkFilters = boardInfo.linkFilter; + + let requestParams = getChartWidgetRequestParams({ + widgetId, + widgetMap, + viewMap, + option, + widgetInfo, + dataChartMap, + boardLinkFilters, + }); + if (!requestParams) { + return null; + } + let widgetData; + try { + const { data } = await request({ + method: 'POST', + url: `data-provider/execute`, + data: requestParams, + }); + widgetData = { ...data, id: widgetId }; + dispatch(editWidgetDataActions.setWidgetData(widgetData as WidgetData)); + dispatch( + editWidgetInfoActions.changePageInfo({ + widgetId, + pageInfo: data.pageInfo, + }), + ); + dispatch( + editWidgetInfoActions.setWidgetErrInfo({ + widgetId, + errInfo: undefined, + }), + ); + } catch (error) { + dispatch( + editWidgetInfoActions.setWidgetErrInfo({ + widgetId, + errInfo: (error as any)?.message as any, + }), + ); + dispatch( + editWidgetDataActions.setWidgetData({ + id: widgetId, + columns: [], + rows: [], + } as WidgetData), + ); + } + return null; + }, +); + +export const getEditControllerOptions = createAsyncThunk< + null, + string, + { state: RootState } +>( + 'editBoard/getEditControllerOptions', + async (widgetId, { getState, dispatch }) => { + dispatch(editWidgetInfoActions.renderedWidgets([widgetId])); + const rootState = getState() as RootState; + + const stackEditBoard = rootState.editBoard as unknown as HistoryEditBoard; + const { widgetRecord: widgetMap } = stackEditBoard.stack.present; + 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 boardState = rootState.board as BoardState; + const viewMap = boardState.viewMap; + const [viewId, viewField] = config.assistViewFields; + const view = viewMap[viewId]; + if (!view) return null; + const requestParams = getControlOptionQueryParams({ + view, + field: viewField, + curWidget: widget, + widgetMap, + }); + + if (!requestParams) { + return null; + } + let widgetData; + try { + const { data } = await request({ + method: 'POST', + url: `data-provider/execute`, + data: requestParams, + }); + widgetData = { ...data, id: widget.id }; + dispatch(editWidgetDataActions.setWidgetData(widgetData as WidgetData)); + } catch (error) { + errorHandle(error); + } + + return null; + }, +); diff --git a/frontend/src/app/pages/DashBoardPage/utils/board.ts b/frontend/src/app/pages/DashBoardPage/utils/board.ts index f332c936d..dc76da961 100644 --- a/frontend/src/app/pages/DashBoardPage/utils/board.ts +++ b/frontend/src/app/pages/DashBoardPage/utils/board.ts @@ -24,16 +24,16 @@ import { DataChart, ServerDashboard, ServerDatachart, - ServerView, Widget, } from 'app/pages/DashBoardPage/pages/Board/slice/types'; +import { View } from 'app/pages/MainPage/pages/ViewPage/slice/types'; import { ChartDataView } from 'app/types/ChartDataView'; -// import { dataChartServerModel } from 'app/pages/MainPage/pages/VizPage/slice/types'; import { transformMeta } from 'app/utils/chartHelper'; import { AutoBoardWidgetBackgroundDefault, BackgroundDefault, LAYOUT_COLS, + NeedFetchWidgetTypes, } from '../constants'; export const getDashBoardByResBoard = (data: ServerDashboard): Dashboard => { @@ -87,7 +87,11 @@ export const getScheduleBoardInfo = ( let newBoardInfo: BoardInfo = { ...boardInfo }; const needFetchItems = Object.values(widgetMap) .filter(widget => { - if (widget.viewIds.length && widget.viewIds.length > 0) { + if ( + widget.viewIds && + widget.viewIds.length > 0 && + NeedFetchWidgetTypes.includes(widget.config.type) + ) { return true; } return false; @@ -98,6 +102,7 @@ export const getScheduleBoardInfo = ( return newBoardInfo; }; + export const getInitBoardInfo = (obj: { id: string; widgetIds?: string[]; @@ -179,22 +184,17 @@ export const getDataChartMap = (dataCharts: DataChart[]) => { }, {} as Record); }; -export const getChartDataView = ( - views: ServerView[], - dataCharts: DataChart[], -) => { +export const getChartDataView = (views: View[], dataCharts: DataChart[]) => { const viewViews: ChartDataView[] = []; views.forEach(view => { const dataChart = dataCharts.find(dc => dc.viewId === view.id); - if (dataChart) { - let viewView = { - ...view, - meta: transformMeta(view.model), - model: '', - computedFields: dataChart.config.computedFields || [], - }; - viewViews.push(viewView); - } + let viewView = { + ...view, + meta: transformMeta(view.model), + model: '', + computedFields: dataChart?.config.computedFields || [], + }; + viewViews.push(viewView); }); return viewViews; }; diff --git a/frontend/src/app/pages/DashBoardPage/utils/index.ts b/frontend/src/app/pages/DashBoardPage/utils/index.ts index 0caab3afc..646583fb8 100644 --- a/frontend/src/app/pages/DashBoardPage/utils/index.ts +++ b/frontend/src/app/pages/DashBoardPage/utils/index.ts @@ -14,12 +14,12 @@ import ChartDataView, { } from 'app/types/ChartDataView'; import { ControllerFacadeTypes, - RelativeOrExactTime, + TimeFilterValueCategory, } from 'app/types/FilterControlPanel'; import { getTime } from 'app/utils/time'; import { FilterSqlOperator } from 'globalConstants'; +import i18next from 'i18next'; import moment from 'moment'; -import { errorHandle } from 'utils/utils'; import { STORAGE_IMAGE_KEY_PREFIX } from '../constants'; import { BoardLinkFilter, @@ -36,7 +36,6 @@ import { import { ChartRequestFilter } from './../../ChartWorkbenchPage/models/ChartHttpRequest'; import { PickerType } from './../pages/BoardEditor/components/ControllerWidgetPanel/types'; import { getLinkedColumn } from './widget'; - export const convertImageUrl = (urlKey: string = ''): string => { if (urlKey.startsWith(STORAGE_IMAGE_KEY_PREFIX)) { return localStorage.getItem(urlKey) || ''; @@ -52,10 +51,10 @@ export const convertImageUrl = (urlKey: string = ''): string => { * 将当前前端渲染环境 id 替换掉原有的id ,原来的和当前的相等不受影响 */ export const adaptBoardImageUrl = (url: string = '', curBoardId: string) => { - // // url=resources/image/dashboard/3062ff86cdcb47b3bba75565b3f2991d/2e1cac3a-600c-4636-b858-cbcb07f4a3b3 - const spliter = '/image/dashboard/'; - if (url.includes(spliter)) { - const originalBoardId = url.split(spliter)[1].split('/')[0]; + // // url=resources/image/dashboard/boardIdXXXXXXX/fileIDxxxxxxxxx + const splitter = '/image/dashboard/'; + if (url.includes(splitter)) { + const originalBoardId = url.split(splitter)[1].split('/')[0]; url.replace(originalBoardId, curBoardId); return url; } @@ -84,6 +83,9 @@ export const getChartDataRequestBuilder = (dataChart: DataChart) => { } as any, dataChart?.config?.chartConfig?.datas, dataChart?.config?.chartConfig?.settings, + {}, + false, + dataChart?.config?.aggregation, ); return builder; }; @@ -98,7 +100,6 @@ export const getChartGroupColumns = (dataChart: DataChart) => { const chartDataConfigs = dataChart?.config?.chartConfig?.datas; if (!chartDataConfigs) return [] as ChartDataSectionField[]; const groupTypes = [ChartDataSectionType.GROUP, ChartDataSectionType.COLOR]; - // ChartDataSectionType.MIXED ?? const groupColumns = chartDataConfigs.reduce( (acc, cur) => { if (!cur.rows) { @@ -107,6 +108,11 @@ export const getChartGroupColumns = (dataChart: DataChart) => { if (groupTypes.includes(cur.type as any)) { return acc.concat(cur.rows); } + if (cur.type === ChartDataSectionType.MIXED) { + return acc.concat( + cur.rows.filter(({ type }) => type === ChartDataViewFieldType.STRING), + ); + } return acc; }, [], @@ -119,22 +125,16 @@ export const getTneWidgetFiltersAndParams = (obj: { widgetMap: Record; params: Record | undefined; }) => { + // TODO chart 本身携带了变量,board没有相关配置的时候要拿到 chart本身的 变量值 Params const { chartWidget, widgetMap, params: chartParams } = obj; - const filterWidgets = Object.values(widgetMap).filter( + const controllerWidgets = Object.values(widgetMap).filter( widget => widget.config.type === 'controller', ); let filterParams: ChartRequestFilter[] = []; let variableParams: Record = {}; - // TODO chartParams 实现后添加 --xld - // if (chartParams) { - // Object.keys(chartParams).forEach(key => { - // variableParams[key] = chartParams[key]; - // }); - // } - - filterWidgets.forEach(filterWidget => { + controllerWidgets.forEach(filterWidget => { const hasRelation = filterWidget.relations.find( re => re.targetId === chartWidget.id, ); @@ -167,14 +167,14 @@ export const getTneWidgetFiltersAndParams = (obj: { let key1 = String(relatedViewItem.fieldValue?.[0]); let key2 = String(relatedViewItem.fieldValue?.[1]); - // TODO need confirm 叠加还是替换? --xld + // variableParams[key1] = [curValues?.[0]]; variableParams[key2] = [curValues?.[1]]; } else { const key = String(relatedViewItem.fieldValue); - // TODO need confirm 叠加还是替换? --xld - variableParams[key] = [curValues?.[0]]; + //单个变量的取值逻辑 不限制为1个 + variableParams[key] = curValues; } } // 关联字段 逻辑 @@ -279,7 +279,7 @@ export const getControllerDateValues = (obj: { }) => { const { endTime, startTime, pickerType } = obj.filterDate; let timeValues: [string, string] = ['', '']; - if (startTime.relativeOrExact === RelativeOrExactTime.Exact) { + if (startTime.relativeOrExact === TimeFilterValueCategory.Exact) { timeValues[0] = startTime.exactValue as string; } else { const { amount, unit, direction } = startTime.relativeValue!; @@ -287,7 +287,7 @@ export const getControllerDateValues = (obj: { timeValues[0] = time.format(DEFAULT_VALUE_DATE_FORMAT); } if (endTime) { - if (endTime.relativeOrExact === RelativeOrExactTime.Exact) { + if (endTime.relativeOrExact === TimeFilterValueCategory.Exact) { timeValues[1] = endTime.exactValue as string; if (obj.execute) { timeValues[1] = adjustRangeDataEndValue( @@ -393,16 +393,20 @@ export const getChartWidgetRequestParams = (obj: { if (!curWidget.datachartId) return null; const dataChart = dataChartMap[curWidget.datachartId]; if (!dataChart) { - errorHandle(`can\`t find Chart ${curWidget.datachartId}`); + // errorHandle(`can\`t find Chart ${curWidget.datachartId}`); return null; } const chartDataView = viewMap[dataChart?.viewId]; + if (!chartDataView) { - errorHandle(`can\`t find View ${dataChart?.viewId}`); + // errorHandle(`can\`t find View ${dataChart?.viewId}`); return null; } + const builder = getChartDataRequestBuilder(dataChart); - let requestParams = builder.build(); + let requestParams = builder + .addExtraSorters((option?.sorters as any) || []) + .build(); const viewConfig = transformToViewConfig(chartDataView?.config); requestParams = { ...requestParams, ...viewConfig }; @@ -472,3 +476,25 @@ export const getDistinctFiltersByColumn = (filter: ChartRequestFilter[]) => { return Object.values(filterMap); }; + +export const getDefaultWidgetName = (widget: Widget, index: number) => { + const widgetType = widget.config.type; + const subWidgetType = widget.config.content.type; + const typeTitle = i18next.t(`viz.widget.type.${widgetType}`); + const subTypeTitle = i18next.t(`viz.widget.type.${subWidgetType}`); + switch (widgetType) { + case 'chart': + return `${subTypeTitle}_${index}`; + case 'container': + return `${subTypeTitle}_${index}`; + case 'controller': + return `${subTypeTitle}_${index}`; + case 'media': + return `${subTypeTitle}_${index}`; + case 'query': + case 'reset': + return `${typeTitle}`; + default: + return `xxx${index}`; + } +}; diff --git a/frontend/src/app/pages/DashBoardPage/utils/widget.ts b/frontend/src/app/pages/DashBoardPage/utils/widget.ts index 266d25edc..2401eee61 100644 --- a/frontend/src/app/pages/DashBoardPage/utils/widget.ts +++ b/frontend/src/app/pages/DashBoardPage/utils/widget.ts @@ -15,7 +15,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { WidgetType } from 'app/pages/DashBoardPage/pages/Board/slice/types'; +import { + ContainerItem, + WidgetType, +} from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { FilterSearchParamsWithMatch } from 'app/pages/MainPage/pages/VizPage/slice/types'; import ChartDataView from 'app/types/ChartDataView'; import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; @@ -24,7 +27,7 @@ import produce from 'immer'; import { DeltaStatic } from 'quill'; import { CSSProperties } from 'react'; import { FONT_FAMILY, G90, WHITE } from 'styles/StyleConstants'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import { convertImageUrl, fillPx } from '.'; import { AutoBoardWidgetBackgroundDefault, @@ -38,7 +41,6 @@ import { BoardType, BorderConfig, ChartWidgetContent, - ContainerItem, ContainerWidgetContent, ContainerWidgetType, ControllerWidgetContent, @@ -58,35 +60,54 @@ import { WidgetInfo, WidgetPadding, } from '../pages/Board/slice/types'; +import { StrControlTypes } from '../pages/BoardEditor/components/ControllerWidgetPanel/constants'; import { ControllerConfig } from '../pages/BoardEditor/components/ControllerWidgetPanel/types'; import { BtnActionParams } from '../pages/BoardEditor/slice/actions/controlActions'; export const VALUE_SPLITTER = '###'; -export const createDataChartWidget = (opt: { - dashboardId: string; +export const createControllerWidget = (opt: { + boardId: string; boardType: BoardType; - dataChartId: string; - dataChartConfig: DataChart; - viewId: string; - subType: WidgetContentChartType; + relations: Relation[]; + name?: string; + controllerType: ControllerFacadeTypes; + views: RelatedView[]; + config: ControllerConfig; + viewIds: string[]; }) => { - const content = createChartWidgetContent(opt.subType); + const { + boardId, + boardType, + views, + config, + controllerType, + relations, + name = 'newController', + } = opt; + const content: ControllerWidgetContent = { + type: controllerType, + relatedViews: views, + name: name, + config: config, + }; + const widgetConf = createInitWidgetConfig({ - type: 'chart', + name: name, + type: 'controller', content: content, - boardType: opt.boardType, - name: opt.dataChartConfig.name, + boardType: boardType, }); + + const widgetId = relations[0]?.sourceId || uuidv4(); const widget: Widget = createWidget({ - dashboardId: opt.dashboardId, - datachartId: opt.dataChartId, - viewIds: opt.viewId ? [opt.viewId] : [], + id: widgetId, + dashboardId: boardId, config: widgetConf, + relations, }); return widget; }; - export const createMediaWidget = (opt: { dashboardId: string; boardType: BoardType; @@ -104,7 +125,6 @@ export const createMediaWidget = (opt: { }); return widget; }; - export const createContainerWidget = (opt: { dashboardId: string; boardType: BoardType; @@ -122,11 +142,10 @@ export const createContainerWidget = (opt: { }); return widget; }; - export const createControlBtn = (opt: BtnActionParams) => { const content = { type: opt.type }; const widgetConf = createInitWidgetConfig({ - name: opt.type === 'query' ? '查询' : '重置', + name: '', type: opt.type as WidgetType, content: content, boardType: opt.boardType, @@ -137,7 +156,6 @@ export const createControlBtn = (opt: BtnActionParams) => { }); return widget; }; - export const createInitWidgetConfig = (opt: { type: WidgetType; content: WidgetContent; @@ -150,7 +168,7 @@ export const createInitWidgetConfig = (opt: { return { type: opt.type, index: opt.index || 0, - name: opt.name || `${opt.type}_${opt.content.type}`, + name: opt.name || '', linkageConfig: { open: false, chartGroupColumns: [], @@ -178,15 +196,6 @@ export const createInitWidgetConfig = (opt: { padding: createWidgetPadding(opt.type), }; }; - -export const fontDefault = { - fontFamily: FONT_FAMILY, - fontSize: '14', - fontWeight: 'normal', - fontStyle: 'normal', - color: G90, -}; - export const createWidget = (option: { dashboardId: string; config: WidgetConf; @@ -207,6 +216,14 @@ export const createWidget = (option: { }; return widget; }; +export const fontDefault = { + fontFamily: FONT_FAMILY, + fontSize: '14', + fontWeight: 'normal', + fontStyle: 'normal', + color: G90, +}; + export const createWidgetInfo = (id: string): WidgetInfo => { const widgetInfo: WidgetInfo = { id: id, @@ -379,180 +396,6 @@ export const createMediaContent = (type: MediaWidgetType) => { return content; }; -export const createControllerWidget = (params: { - boardId: string; - boardType: BoardType; - relations: Relation[]; - name?: string; - controllerType: ControllerFacadeTypes; - views: RelatedView[]; - config: ControllerConfig; - hasVariable: boolean; -}) => { - const { - boardId, - boardType, - views, - config, - controllerType, - relations, - name = 'newController', - } = params; - const content: ControllerWidgetContent = { - type: controllerType, - relatedViews: views, - name: name, - config: config, - }; - - const widgetConf = createInitWidgetConfig({ - name: name, - type: 'controller', - content: content, - boardType: boardType, - }); - - const widgetId = relations[0]?.sourceId || uuidv4(); - const widget: Widget = createWidget({ - id: widgetId, - dashboardId: boardId, - config: widgetConf, - relations, - }); - return widget; -}; - -// TODO chart widget -export const getWidgetMapByServer = ( - widgets: ServerWidget[], - dataCharts: DataChart[], - filterSearchParamsMap?: FilterSearchParamsWithMatch, -) => { - const filterSearchParams = filterSearchParamsMap?.params, - isMatchByName = filterSearchParamsMap?.isMatchByName; - const dataChartMap = dataCharts.reduce((acc, cur) => { - acc[cur.id] = cur; - return acc; - }, {} as Record); - const widgetMap = widgets.reduce((acc, cur) => { - const viewIds = cur.datachartId - ? [dataChartMap[cur.datachartId].viewId] - : cur.viewIds; - try { - let widget: Widget = { - ...cur, - config: JSON.parse(cur.config), - relations: convertWidgetRelationsToObj(cur.relations), - viewIds, - }; - // TODO migration about font 5 --xld - widget.config.nameConfig = { - ...fontDefault, - ...widget.config.nameConfig, - }; - // TODO migration about filter --xld - if ((widget.config.type as any) !== 'filter') { - acc[cur.id] = widget; - } - return acc; - } catch (error) { - return acc; - } - }, {} as Record); - - const wrappedDataCharts: DataChart[] = []; - const controllerWidgets: Widget[] = []; - Object.values(widgetMap).forEach(widget => { - // 处理 widget包含关系 - if (widget.parentId) { - const parentWidgetId = widget.parentId; - const childTabId = widget.config.tabId as string; - const curItem = ( - widgetMap[parentWidgetId].config.content as ContainerWidgetContent - ).itemMap[childTabId]; - if (curItem) { - curItem.childWidgetId = widget.id; - curItem.name = widget.config.name; - } else { - let newItem: ContainerItem = { - tabId: childTabId, - name: widget.config.name, - childWidgetId: widget.id, - }; - ( - widgetMap[parentWidgetId].config.content as ContainerWidgetContent - ).itemMap[childTabId] = newItem; - } - } - - // 处理 controller config visibility依赖关系 id, url参数修改filter - if (widget.config.type === 'controller') { - const content = widget.config.content as ControllerWidgetContent; - // 根据 url参数修改filter 默认值 - if (filterSearchParams) { - const paramsKey = Object.keys(filterSearchParams); - const macthKey = isMatchByName ? widget.config.name : widget.id; - if (paramsKey.includes(macthKey)) { - const _value = isMatchByName - ? filterSearchParams[widget.config.name] - : filterSearchParams[widget.id]; - switch (content?.type) { - case ControllerFacadeTypes.RangeTime: - if ( - content.config.controllerDate && - content.config.controllerDate?.startTime && - content.config.controllerDate?.endTime - ) { - content.config.controllerDate.startTime.exactValue = - _value?.[0]; - content.config.controllerDate.endTime.exactValue = _value?.[0]; - } - break; - default: - content.config.controllerValues = _value || []; - break; - } - } - } - // 适配filter 的可见性 - const { visibilityType: visibility, condition } = - content.config.visibility; - const { relations } = widget; - if (visibility === 'condition' && condition) { - const dependentFilterId = relations - .filter(re => re.config.type === 'controlToControl') - .map(re => re.targetId)?.[0]; - if (dependentFilterId) { - condition.dependentControllerId = dependentFilterId; - } - } - - //处理 assistViewField - if (typeof content?.config?.assistViewFields === 'string') { - content.config.assistViewFields = ( - content.config.assistViewFields as string - ).split(VALUE_SPLITTER); - } - // use for reset button - controllerWidgets.push(widget); - } - - // 处理 自有 chart widget - - if (widget.config.content.type === 'widgetChart') { - let content = widget.config.content as ChartWidgetContent; - widget.datachartId = content.dataChart?.id || ''; - wrappedDataCharts.push(content.dataChart!); - delete content.dataChart; - } - }); - - return { - widgetMap, - wrappedDataCharts, - controllerWidgets, - }; -}; export const getWidgetInfoMapByServer = (widgetMap: Record) => { const widgetInfoMap = {}; Object.values(widgetMap).forEach(item => { @@ -738,17 +581,11 @@ export const getOtherStringControlWidgets = ( return false; } const content = ele.config.content as ControllerWidgetContent; - const strControlTypes = [ - ControllerFacadeTypes.DropdownList, - ControllerFacadeTypes.MultiDropdownList, - ControllerFacadeTypes.RadioGroup, - ]; - return strControlTypes.includes(content.type); + return StrControlTypes.includes(content.type); }); if (!widgetId) { return allFilterWidgets; } else { - // 自己不能关联自己 把自己排除 return allFilterWidgets.filter(ele => ele.id !== widgetId); } }; @@ -805,9 +642,6 @@ export const getNoHiddenControllers = (widgets: Widget[]) => { } const content = dependWidget.config.content as ControllerWidgetContent; const dependWidgetValue = content.config.controllerValues?.[0]; - // if (!dependWidgetValue) { - // return false; - // } if (relation === FilterSqlOperator.Equal) { return targetValue === dependWidgetValue; } @@ -823,14 +657,20 @@ export const getNoHiddenControllers = (widgets: Widget[]) => { return noHiddenControlWidgets; }; -export const getNeedRefreshWidgetsByFilter = (filterWidget: Widget) => { - const relations = filterWidget.relations; +export const getNeedRefreshWidgetsByController = (controller: Widget) => { + const relations = controller.relations; const widgetIds = relations .filter(ele => ele.config.type === 'controlToWidget') .map(ele => ele.targetId); return widgetIds; }; - +export const getCascadeControllers = (controller: Widget) => { + const relations = controller.relations; + const ids = relations + .filter(ele => ele.config.type === 'controlToControlCascade') + .map(ele => ele.targetId); + return ids; +}; // getWidgetStyle start export const getWidgetStyle = (boardType: BoardType, widget: Widget) => { return boardType === 'auto' @@ -930,20 +770,6 @@ export const getWidgetSomeStyle = (opt: { // get some css end // Controller -export const getCanLinkControlWidgets = (widgets: Widget[]) => { - const CanLinkControllerWidgetTypes: WidgetType[] = ['chart']; - - const canLinkWidgets = widgets.filter(widget => { - if (widget.viewIds.length === 0) { - return false; - } - if (CanLinkControllerWidgetTypes.includes(widget.config.type)) { - return true; - } - return false; - }); - return canLinkWidgets; -}; export const getLinkedColumn = ( targetWidgetId: string, @@ -958,3 +784,143 @@ export const getLinkedColumn = ( '' ); }; + +// TODO chart widget +export const getWidgetMapByServer = ( + widgets: ServerWidget[], + dataCharts: DataChart[], + filterSearchParamsMap?: FilterSearchParamsWithMatch, +) => { + const filterSearchParams = filterSearchParamsMap?.params, + isMatchByName = filterSearchParamsMap?.isMatchByName; + const dataChartMap = dataCharts.reduce((acc, cur) => { + acc[cur.id] = cur; + return acc; + }, {} as Record); + const widgetMap = widgets.reduce((acc, cur) => { + const viewIds = cur.datachartId + ? [dataChartMap[cur.datachartId].viewId] + : cur.viewIds; + try { + let widget: Widget = { + ...cur, + config: JSON.parse(cur.config), + relations: convertWidgetRelationsToObj(cur.relations), + viewIds, + }; + // TODO migration about font 5 --xld + widget.config.nameConfig = { + ...fontDefault, + ...widget.config.nameConfig, + }; + // TODO migration about filter --xld + if ((widget.config.type as any) !== 'filter') { + acc[cur.id] = widget; + } + return acc; + } catch (error) { + return acc; + } + }, {} as Record); + + const wrappedDataCharts: DataChart[] = []; + const controllerWidgets: Widget[] = []; // use for reset button + const widgetList = Object.values(widgetMap); + + // 处理 widget包含关系 containerWidget 被包含的 widget.parentId 不为空 + widgetList + .filter(w => w.parentId) + .forEach(widget => { + const parentWidgetId = widget.parentId!; + const childTabId = widget.config.tabId as string; + const curItem = ( + widgetMap[parentWidgetId].config.content as ContainerWidgetContent + ).itemMap[childTabId]; + if (curItem) { + curItem.childWidgetId = widget.id; + curItem.name = widget.config.name; + } else { + let newItem: ContainerItem = { + tabId: childTabId, + name: widget.config.name, + childWidgetId: widget.id, + }; + ( + widgetMap[parentWidgetId].config.content as ContainerWidgetContent + ).itemMap[childTabId] = newItem; + } + }); + + // 处理 controller config visibility依赖关系 id, url参数修改filter + widgetList + .filter(w => w.config.type === 'controller') + .forEach(widget => { + const content = widget.config.content as ControllerWidgetContent; + // 根据 url参数修改filter 默认值 + if (filterSearchParams) { + const paramsKey = Object.keys(filterSearchParams); + const matchKey = isMatchByName ? widget.config.name : widget.id; + if (paramsKey.includes(matchKey)) { + const _value = isMatchByName + ? filterSearchParams[widget.config.name] + : filterSearchParams[widget.id]; + switch (content?.type) { + case ControllerFacadeTypes.RangeTime: + if ( + content.config.controllerDate && + content.config.controllerDate?.startTime && + content.config.controllerDate?.endTime + ) { + content.config.controllerDate.startTime.exactValue = + _value?.[0]; + content.config.controllerDate.endTime.exactValue = _value?.[0]; + } + break; + default: + content.config.controllerValues = _value || []; + break; + } + } + } + + // 通过widget.relation 那里面的 targetId确定 关联controllerWidget 的真实ID + const { visibilityType: visibility, condition } = + content.config.visibility; + const { relations } = widget; + if (visibility === 'condition' && condition) { + const dependentFilterId = relations + .filter(re => re.config.type === 'controlToControl') + .map(re => re.targetId)?.[0]; + if (dependentFilterId) { + condition.dependentControllerId = dependentFilterId; + } + } + + //处理 assistViewFields 旧数据 assistViewFields 是 string 类型 alpha.3版本之后 使用数组存储的 后续版本稳定之后 可以移除此逻辑 + // TODO migration << + if (typeof content?.config?.assistViewFields === 'string') { + content.config.assistViewFields = ( + content.config.assistViewFields as string + ).split(VALUE_SPLITTER); + } + // TODO migration >> --xld + + controllerWidgets.push(widget); + }); + + // 处理 自有 chart widgetControl + widgetList + .filter(w => w.config.content.type === 'widgetChart') + .forEach(widget => { + let content = widget.config.content as ChartWidgetContent; + widget.datachartId = content.dataChart?.id || ''; + wrappedDataCharts.push(content.dataChart!); + delete content.dataChart; + }); + + return { + widgetMap, + wrappedDataCharts, + controllerWidgets, + }; +}; diff --git a/frontend/src/app/pages/DashBoardPage/utils/widgetToolKit/chart/index.ts b/frontend/src/app/pages/DashBoardPage/utils/widgetToolKit/chart/index.ts new file mode 100644 index 000000000..1a5e62941 --- /dev/null +++ b/frontend/src/app/pages/DashBoardPage/utils/widgetToolKit/chart/index.ts @@ -0,0 +1,111 @@ +/** + * 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 { + BoardType, + DataChart, + Widget, + WidgetContentChartType, + WidgetType, +} from 'app/pages/DashBoardPage/pages/Board/slice/types'; +import ChartDataView from 'app/types/ChartDataView'; +import { getTneWidgetFiltersAndParams } from '../..'; +import { + createChartWidgetContent, + createInitWidgetConfig, + createWidget, +} from '../../widget'; +import ChartRequest, { + transformToViewConfig, +} from './../../../../ChartWorkbenchPage/models/ChartHttpRequest'; + +export const createDataChartWidget = (opt: { + dashboardId: string; + boardType: BoardType; + dataChartId: string; + dataChartConfig: DataChart; + viewId: string; + subType: WidgetContentChartType; +}): Widget => { + const content = createChartWidgetContent(opt.subType); + const widgetConf = createInitWidgetConfig({ + type: 'chart', + content: content, + boardType: opt.boardType, + name: opt.dataChartConfig.name, + }); + const widget: Widget = createWidget({ + dashboardId: opt.dashboardId, + datachartId: opt.dataChartId, + viewIds: opt.viewId ? [opt.viewId] : [], + config: widgetConf, + }); + return widget; +}; +export const getCanLinkageWidgets = (widgets: Widget[]) => { + const CanLinkageTypes: WidgetType[] = ['chart']; + const canLinkWidgets = widgets.filter(widget => { + if (!CanLinkageTypes.includes(widget.config.type)) { + return false; + } + if (widget.viewIds.length === 0) { + return false; + } + return true; + }); + return canLinkWidgets; +}; + +export const getControlOptionQueryParams = (obj: { + view: ChartDataView; + field: string; + curWidget: Widget; + widgetMap: Record; +}) => { + const viewConfigs = transformToViewConfig(obj.view?.config); + const { filterParams, variableParams } = getTneWidgetFiltersAndParams({ + chartWidget: obj.curWidget, + widgetMap: obj.widgetMap, + params: undefined, + }); + const requestParams: ChartRequest = { + aggregators: [], + filters: filterParams, + groups: [], + columns: [obj.field], + pageInfo: { + pageNo: 1, + pageSize: 99999999, + total: 99999999, + }, + orders: [], + keywords: ['DISTINCT'], + viewId: obj.view.id, + ...viewConfigs, + }; + if (variableParams) { + requestParams.params = variableParams; + } + return requestParams; +}; + +export const chartWidgetToolKit = { + create: createDataChartWidget, + tool: { + getCanLinkageWidgets, + }, +}; diff --git a/frontend/src/app/pages/DashBoardPage/utils/widgetToolKit/controller/index.ts b/frontend/src/app/pages/DashBoardPage/utils/widgetToolKit/controller/index.ts new file mode 100644 index 000000000..466d6b13f --- /dev/null +++ b/frontend/src/app/pages/DashBoardPage/utils/widgetToolKit/controller/index.ts @@ -0,0 +1,178 @@ +/** + * 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 { + BoardType, + ControllerWidgetContent, + RelatedView, + Relation, + RelationConfigType, + Widget, + WidgetType, +} from 'app/pages/DashBoardPage/pages/Board/slice/types'; +import { RelatedWidgetItem } from 'app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/RelatedWidgets'; +import { ControllerConfig } from 'app/pages/DashBoardPage/pages/BoardEditor/components/ControllerWidgetPanel/types'; +import { ControllerFacadeTypes } from 'app/types/FilterControlPanel'; +import { uuidv4 } from 'utils/utils'; +import { createInitWidgetConfig, createWidget } from '../../widget'; +export const createControllerWidget = (opt: { + boardId: string; + boardType: BoardType; + relations: Relation[]; + name?: string; + controllerType: ControllerFacadeTypes; + views: RelatedView[]; + config: ControllerConfig; + viewIds: string[]; +}) => { + const { + boardId, + boardType, + views, + config, + controllerType, + relations, + name = 'newController', + viewIds, + } = opt; + const content: ControllerWidgetContent = { + type: controllerType, + relatedViews: views, + name: name, + config: config, + }; + + const widgetConf = createInitWidgetConfig({ + name: name, + type: 'controller', + content: content, + boardType: boardType, + }); + + const widgetId = relations[0]?.sourceId || uuidv4(); + const widget: Widget = createWidget({ + id: widgetId, + dashboardId: boardId, + config: widgetConf, + relations, + viewIds, + }); + return widget; +}; + +export const getViewIdsInControlConfig = ( + controllerConfig: ControllerConfig, +) => { + if (!controllerConfig.assistViewFields) return []; + if (controllerConfig.assistViewFields?.[0]) { + return [controllerConfig.assistViewFields[0]]; + } else { + return []; + } +}; +export const getCanLinkControlWidgets = (widgets: Widget[]) => { + const CanLinkControllerWidgetTypes: WidgetType[] = ['chart', 'controller']; + + const canLinkWidgets = widgets.filter(widget => { + if (!CanLinkControllerWidgetTypes.includes(widget.config.type)) { + return false; + } + if (widget.viewIds.length === 0) { + return false; + } + return true; + }); + return canLinkWidgets; +}; + +const makeControlRelations = (obj: { + sourceId: string | undefined; + relatedWidgets: RelatedWidgetItem[]; + widgetMap: Record; + config: ControllerConfig; +}) => { + const sourceId = obj.sourceId || uuidv4(); + const { relatedWidgets, widgetMap, config } = obj; + const trimRelatedWidgets = relatedWidgets.filter(relatedWidgetItem => { + return widgetMap[relatedWidgetItem.widgetId]; + }); + let chartWidgets: Widget[] = []; + let controllerWidgets: Widget[] = []; + trimRelatedWidgets.forEach(relatedWidgetItem => { + let widget = widgetMap[relatedWidgetItem.widgetId]; + if (!widget) return false; + if (widget.config.type === 'chart') { + chartWidgets.push(widget); + } + if (widget.config.type === 'controller') { + controllerWidgets.push(widget); + } + }); + const controlToChartRelations: Relation[] = chartWidgets.map(widget => { + const relationType: RelationConfigType = 'controlToWidget'; + return { + sourceId, + targetId: widget.id, + config: { + type: relationType, + controlToWidget: { + widgetRelatedViewIds: widget.viewIds, + }, + }, + id: uuidv4(), + }; + }); + const controlToCascadeRelations: Relation[] = controllerWidgets.map( + widget => { + const relationType: RelationConfigType = 'controlToControlCascade'; + return { + sourceId, + targetId: widget.id, + config: { + type: relationType, + }, + id: uuidv4(), + }; + }, + ); + let newRelations = [...controlToChartRelations, ...controlToCascadeRelations]; + const controllerVisible = (config as ControllerConfig).visibility; + if (controllerVisible) { + const { visibilityType, condition } = controllerVisible; + if (visibilityType === 'condition' && condition) { + const controlToControlRelation: Relation = { + sourceId, + targetId: condition.dependentControllerId, + config: { + type: 'controlToControl', + }, + id: uuidv4(), + }; + newRelations = newRelations.concat([controlToControlRelation]); + } + } + return newRelations; +}; + +export const controllerWidgetToolKit = { + create: createControllerWidget, + tool: { + getViewIdsInControlConfig, + getCanLinkControlWidgets, + makeControlRelations, + }, +}; diff --git a/frontend/src/app/pages/DashBoardPage/utils/widgetToolKit/widgetToolKit.ts b/frontend/src/app/pages/DashBoardPage/utils/widgetToolKit/widgetToolKit.ts new file mode 100644 index 000000000..ab3a4f215 --- /dev/null +++ b/frontend/src/app/pages/DashBoardPage/utils/widgetToolKit/widgetToolKit.ts @@ -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 { createContainerWidget, createControlBtn } from '../widget'; +import { + BoardType, + ContainerWidgetType, + MediaWidgetType, + Widget, + WidgetType, +} from './../../pages/Board/slice/types'; +import { createMediaWidget } from './../widget'; +import { chartWidgetToolKit } from './chart/index'; +import { controllerWidgetToolKit } from './controller'; +export interface CreateParamsType { + boardType: BoardType; + [key: string]: any; +} +export interface WidgetToolKit { + create: (option) => Widget; +} +export const widgetToolKit = { + chart: chartWidgetToolKit, + controller: controllerWidgetToolKit, + container: { + create: (opt: { + dashboardId: string; + boardType: BoardType; + type: ContainerWidgetType; + }) => { + return createContainerWidget(opt); + }, + }, + + media: { + create: (opt: { + dashboardId: string; + boardType: BoardType; + type: MediaWidgetType; + }) => { + return createMediaWidget(opt); + }, + }, + query: { + create: (opt: { + type: WidgetType; + boardId: string; + boardType: BoardType; + }) => { + return createControlBtn(opt); + }, + }, + reset: { + create: (opt: { + type: WidgetType; + boardId: string; + boardType: BoardType; + }) => { + return createControlBtn(opt); + }, + }, +}; diff --git a/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/CheckCodeForm.tsx b/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/CheckCodeForm.tsx index 7b8a6c57b..9873adc40 100644 --- a/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/CheckCodeForm.tsx +++ b/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/CheckCodeForm.tsx @@ -1,8 +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 { Button, Form, Input, Radio } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { FC, useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { SPACE_LG } from 'styles/StyleConstants'; -import { FindWays, FIND_WAY_OPTIONS } from '../constants'; +import { FindWays } from '../constants'; import { captchaforResetPassword } from '../service'; import { CaptchaParams } from '../types'; @@ -15,9 +34,13 @@ export const CheckCodeForm: FC = ({ onNextStep }) => { const [token, setToken] = useState(); const [ticket, setTicket] = useState(''); const [submitLoading, setSubmitLoading] = useState(false); + const t = useI18NPrefix('forgotPassword'); + const tg = useI18NPrefix('global'); + const initialValues = useMemo(() => { return { type: FindWays.Email }; }, []); + const onFinish = useCallback((values: CaptchaParams) => { setSubmitLoading(true); captchaforResetPassword(values) @@ -28,9 +51,8 @@ export const CheckCodeForm: FC = ({ onNextStep }) => { .finally(() => { setSubmitLoading(false); }); - // setToken('token-----------'); - // setTicket(values?.principal); }, []); + const isEmail = useMemo(() => { return type === FindWays.Email; }, [type]); @@ -45,6 +67,16 @@ export const CheckCodeForm: FC = ({ onNextStep }) => { }, [form], ); + + const typeOptions = useMemo( + () => + Object.values(FindWays).map(w => ({ + label: t(w.toLowerCase()), + value: w, + })), + [t], + ); + const ticketFormItem = useMemo(() => { return isEmail ? ( = ({ onNextStep }) => { rules={[ { required: true, - message: '请输入邮箱', + message: `${t('email')}${tg('validation.required')}`, }, { type: 'email', - message: '邮箱格式不正确', + message: t('emailInvalid'), }, ]} > - + ) : ( = ({ onNextStep }) => { rules={[ { required: true, - message: '请输入用户名', + message: `${t('username')}${tg('validation.required')}`, }, ]} > - + ); - }, [isEmail]); + }, [isEmail, t, tg]); + const goNext = useCallback(() => { onNextStep(token as string); }, [onNextStep, token]); @@ -84,31 +117,29 @@ export const CheckCodeForm: FC = ({ onNextStep }) => { () => token && token.length ? ( - 一封确认信已经发到 + {t('desc1')} {type === FindWays.UserName ? ( <> {ticket} - 所关联的邮箱 + {t('desc2')} ) : ( {ticket} )} - ,请前往该邮箱获取验证码,然后点击下一步重置密码。 + {t('desc3')} ) : ( <> ), - [ticket, type, token], + [ticket, type, token, t], ); + return ( - + @@ -118,7 +149,7 @@ export const CheckCodeForm: FC = ({ onNextStep }) => { <> {tips} - 下一步 + {t('nextStep')} ) : ( @@ -128,7 +159,7 @@ export const CheckCodeForm: FC = ({ onNextStep }) => { htmlType="submit" size="large" > - 确定 + {t('send')} )} diff --git a/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/ResetPasswordForm.tsx b/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/ResetPasswordForm.tsx index 60d1b98d2..4f99578d2 100644 --- a/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/ResetPasswordForm.tsx +++ b/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/ResetPasswordForm.tsx @@ -1,8 +1,31 @@ +/** + * 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, Form, Input, message } from 'antd'; -import { RULES } from 'app/constants'; -import { FC, useCallback, useMemo, useState } from 'react'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { FC, useCallback, useState } from 'react'; import { useHistory } from 'react-router'; +import { + getConfirmPasswordValidator, + getPasswordValidator, +} from 'utils/validators'; import { resetPassword } from '../service'; + interface ResetPasswordFormProps { token: string; } @@ -10,17 +33,9 @@ export const ResetPasswordForm: FC = ({ token }) => { const [form] = Form.useForm(); const history = useHistory(); const [submiting, setSubmiting] = useState(false); + const t = useI18NPrefix('forgotPassword'); + const tg = useI18NPrefix('global'); - const checkPasswordConfirm = useCallback( - (_, value, callBack) => { - if (value && value !== form.getFieldValue('newPassword')) { - return Promise.reject('两次输入的密码不一致'); - } else { - return Promise.resolve(); - } - }, - [form], - ); const onFinish = useCallback( values => { setSubmiting(true); @@ -32,7 +47,7 @@ export const ResetPasswordForm: FC = ({ token }) => { resetPassword(params) .then(res => { if (res) { - message.success('重置密码成功'); + message.success(t('resetSuccess')); history.replace('/login'); } }) @@ -40,27 +55,52 @@ export const ResetPasswordForm: FC = ({ token }) => { setSubmiting(false); }); }, - [token, history], + [token, history, t], ); - const confirmPasswordRule = useMemo(() => { - return RULES.getConfirmRule('newPassword'); - }, []); + return ( - - + + - - + + - + ); diff --git a/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/index.tsx b/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/index.tsx index d4a33cef7..55f52079d 100644 --- a/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/index.tsx +++ b/frontend/src/app/pages/ForgetPasswordPage/ForgetPasswordForm/index.tsx @@ -1,13 +1,34 @@ +/** + * 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 { AuthForm } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { useCallback, useState } from 'react'; import { Link } from 'react-router-dom'; import styled from 'styled-components/macro'; import { LINE_HEIGHT_ICON_LG } from 'styles/StyleConstants'; import { CheckCodeForm } from './CheckCodeForm'; import { ResetPasswordForm } from './ResetPasswordForm'; + export function ForgetPasswordForm() { const [isCheckForm, setIsCheckForm] = useState(true); const [token, setToken] = useState(''); + const t = useI18NPrefix('forgotPassword'); const onNextStep = useCallback((token: string) => { setIsCheckForm(false); @@ -20,7 +41,7 @@ export function ForgetPasswordForm() { ) : ( )} - 返回登录页 + {t('return')} ); } diff --git a/frontend/src/app/pages/ForgetPasswordPage/Loadable.tsx b/frontend/src/app/pages/ForgetPasswordPage/Loadable.tsx index 5198635b9..ba1e2bdab 100644 --- a/frontend/src/app/pages/ForgetPasswordPage/Loadable.tsx +++ b/frontend/src/app/pages/ForgetPasswordPage/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/ForgetPasswordPage/constants.ts b/frontend/src/app/pages/ForgetPasswordPage/constants.ts index fe8a05be8..ef2308863 100644 --- a/frontend/src/app/pages/ForgetPasswordPage/constants.ts +++ b/frontend/src/app/pages/ForgetPasswordPage/constants.ts @@ -1,8 +1,22 @@ +/** + * 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. + */ + export enum FindWays { Email = 'EMAIL', UserName = 'USERNAME', } -export const FIND_WAY_OPTIONS = [ - { label: '邮箱', value: FindWays.Email }, - { label: '用户名', value: FindWays.UserName }, -]; diff --git a/frontend/src/app/pages/ForgetPasswordPage/index.tsx b/frontend/src/app/pages/ForgetPasswordPage/index.tsx index 4871b0e11..3c1f34869 100644 --- a/frontend/src/app/pages/ForgetPasswordPage/index.tsx +++ b/frontend/src/app/pages/ForgetPasswordPage/index.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 { Brand } from 'app/components/Brand'; import React from 'react'; import styled from 'styled-components/macro'; diff --git a/frontend/src/app/pages/ForgetPasswordPage/service.ts b/frontend/src/app/pages/ForgetPasswordPage/service.ts index cd4fab933..6f548e23a 100644 --- a/frontend/src/app/pages/ForgetPasswordPage/service.ts +++ b/frontend/src/app/pages/ForgetPasswordPage/service.ts @@ -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 { request } from 'utils/request'; import { errorHandle } from 'utils/utils'; import { CaptchaParams, ResetPasswordParams } from './types'; diff --git a/frontend/src/app/pages/ForgetPasswordPage/types.ts b/frontend/src/app/pages/ForgetPasswordPage/types.ts index f46d39f34..70947351b 100644 --- a/frontend/src/app/pages/ForgetPasswordPage/types.ts +++ b/frontend/src/app/pages/ForgetPasswordPage/types.ts @@ -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 { FindWays } from './constants'; export interface CaptchaParams { diff --git a/frontend/src/app/pages/LoginPage/Loadable.tsx b/frontend/src/app/pages/LoginPage/Loadable.tsx index 0c71ae061..c026aa0ce 100644 --- a/frontend/src/app/pages/LoginPage/Loadable.tsx +++ b/frontend/src/app/pages/LoginPage/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/LoginPage/LoginForm.tsx b/frontend/src/app/pages/LoginPage/LoginForm.tsx index 64735311a..b0a389cc2 100644 --- a/frontend/src/app/pages/LoginPage/LoginForm.tsx +++ b/frontend/src/app/pages/LoginPage/LoginForm.tsx @@ -1,5 +1,24 @@ +/** + * 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, Form, Input } from 'antd'; import { AuthForm } from 'app/components'; +import usePrefixI18N from 'app/hooks/useI18NPrefix'; import { selectLoggedInUser, selectLoginLoading } from 'app/slice/selectors'; import { login } from 'app/slice/thunks'; import React, { useCallback, useState } from 'react'; @@ -21,6 +40,8 @@ export function LoginForm() { const loggedInUser = useSelector(selectLoggedInUser); const [form] = Form.useForm(); const logged = !!getToken(); + const t = usePrefixI18N('login'); + const tg = usePrefixI18N('global'); const toApp = useCallback(() => { history.replace('/'); @@ -48,13 +69,13 @@ export function LoginForm() { {logged && !switchUser ? ( <> -

    账号已登录

    +

    {t('alreadyLoggedIn')}

    {loggedInUser?.username}

    - 点击进入系统 + {t('enter')}
    ) : ( @@ -64,22 +85,22 @@ export function LoginForm() { rules={[ { required: true, - message: '用户名或邮箱不能为空', + message: `${t('username')}${tg('validation.required')}`, }, ]} > - +
    - + {() => ( @@ -96,13 +117,13 @@ export function LoginForm() { } block > - 登录 + {t('login')} )} - 忘记密码 - 注册账号 + {t('forgotPassword')} + {t('register')} )} diff --git a/frontend/src/app/pages/LoginPage/index.tsx b/frontend/src/app/pages/LoginPage/index.tsx index 09030c5bf..ce5eccb4f 100644 --- a/frontend/src/app/pages/LoginPage/index.tsx +++ b/frontend/src/app/pages/LoginPage/index.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 { Brand } from 'app/components/Brand'; import { Version } from 'app/components/Version'; import { selectVersion } from 'app/slice/selectors'; diff --git a/frontend/src/app/pages/MainPage/Access.tsx b/frontend/src/app/pages/MainPage/Access.tsx index 260b23f0f..794fd08eb 100644 --- a/frontend/src/app/pages/MainPage/Access.tsx +++ b/frontend/src/app/pages/MainPage/Access.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 { Authorized as AuthorizedComponent } from 'app/components'; import { ReactElement } from 'react'; import { useSelector } from 'react-redux'; @@ -31,13 +49,7 @@ export function Access({ return null; } - const isAuthorized = isOwner - ? true - : type === 'module' - ? permissionMap[module]['*'] >= level - : id - ? permissionMap[module][id] >= level - : false; + const isAuthorized = calcAc(isOwner, permissionMap, module, level, id, type); return ( diff --git a/frontend/src/app/pages/MainPage/AccessRoute.tsx b/frontend/src/app/pages/MainPage/AccessRoute.tsx index af565d95b..bbba6c030 100644 --- a/frontend/src/app/pages/MainPage/AccessRoute.tsx +++ b/frontend/src/app/pages/MainPage/AccessRoute.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 React from 'react'; import { Redirect } from 'react-router-dom'; import { Access, AccessProps } from './Access'; diff --git a/frontend/src/app/pages/MainPage/Background.tsx b/frontend/src/app/pages/MainPage/Background.tsx index 59216906c..a5006dcf7 100644 --- a/frontend/src/app/pages/MainPage/Background.tsx +++ b/frontend/src/app/pages/MainPage/Background.tsx @@ -1,8 +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 { AppstoreAddOutlined, ReloadOutlined, SettingOutlined, } from '@ant-design/icons'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import styled, { keyframes } from 'styled-components/macro'; @@ -26,6 +45,7 @@ export function Background() { const organizations = useSelector(selectOrganizations); const userSettingLoading = useSelector(selectUserSettingLoading); const error = useSelector(selectInitializationError); + const t = useI18NPrefix('main.background'); const showForm = useCallback(() => { setFormVisible(true); @@ -42,14 +62,14 @@ export function Background() { content = ( -

    应用配置加载中…

    +

    {t('loading')}

    ); } else if (error) { content = ( -

    初始化错误,请刷新页面重试

    +

    {t('initError')}

    ); } else if ( @@ -61,7 +81,7 @@ export function Background() { <> -

    未加入任何组织,点击创建

    +

    {t('createOrg')}

    diff --git a/frontend/src/app/pages/MainPage/Loadable.tsx b/frontend/src/app/pages/MainPage/Loadable.tsx index 7276ef68f..6b5e7ce8d 100644 --- a/frontend/src/app/pages/MainPage/Loadable.tsx +++ b/frontend/src/app/pages/MainPage/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/MainPage/Navbar/DownloadListPopup/DownloadList.tsx b/frontend/src/app/pages/MainPage/Navbar/DownloadListPopup/DownloadList.tsx index 802a2f26f..34a37adcf 100644 --- a/frontend/src/app/pages/MainPage/Navbar/DownloadListPopup/DownloadList.tsx +++ b/frontend/src/app/pages/MainPage/Navbar/DownloadListPopup/DownloadList.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 { List, Tag } from 'antd'; import { ListItem } from 'app/components'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; @@ -16,16 +34,11 @@ import { } from 'styles/StyleConstants'; import { DownloadTask, DownloadTaskState } from '../../slice/types'; import { DownloadListProps } from './types'; -const DOWNLOAD_STATUS_TAGS = { - [DownloadTaskState.CREATE]: '处理中', - [DownloadTaskState.DOWNLOADED]: '已下载', - [DownloadTaskState.FINISH]: '完成', - [DownloadTaskState.FAILED]: '失败', -}; + const DOWNLOAD_STATUS_COLORS = { - [DownloadTaskState.CREATE]: INFO, + [DownloadTaskState.CREATED]: INFO, [DownloadTaskState.DOWNLOADED]: G50, - [DownloadTaskState.FINISH]: SUCCESS, + [DownloadTaskState.DONE]: SUCCESS, [DownloadTaskState.FAILED]: ERROR, }; @@ -38,19 +51,20 @@ const DownloadFileItem: FC = ({ ...restProps }) => { const { name, status } = restProps; + const t = useI18NPrefix('main.nav.download.status'); const { color, tagName, titleClasses } = useMemo(() => { const titleClasses = ['download-file-name']; if (status === DownloadTaskState.DOWNLOADED) { titleClasses.push('downloaded'); - } else if (status === DownloadTaskState.FINISH) { + } else if (status === DownloadTaskState.DONE) { titleClasses.push('finished'); } return { color: DOWNLOAD_STATUS_COLORS[status], - tagName: DOWNLOAD_STATUS_TAGS[status], + tagName: t(DownloadTaskState[status].toLowerCase()), titleClasses: titleClasses.join(' '), }; - }, [status]); + }, [status, t]); return ( onDownloadFile(restProps)}> @@ -147,7 +161,6 @@ const DownloadFileItemWrapper = styled.div` } } .ant-tag { - width: 56px; margin: 0; text-align: center; } diff --git a/frontend/src/app/pages/MainPage/Navbar/DownloadListPopup/index.tsx b/frontend/src/app/pages/MainPage/Navbar/DownloadListPopup/index.tsx index ab4bc49cb..f9461e268 100644 --- a/frontend/src/app/pages/MainPage/Navbar/DownloadListPopup/index.tsx +++ b/frontend/src/app/pages/MainPage/Navbar/DownloadListPopup/index.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 { CloudDownloadOutlined } from '@ant-design/icons'; import { Badge, Tooltip, TooltipProps } from 'antd'; import { Popup } from 'app/components'; @@ -26,7 +44,7 @@ export const DownloadListPopup: FC = ({ const t = useI18NPrefix('main.nav'); const downloadableNum = useMemo(() => { - return (tasks || []).filter(v => v.status === DownloadTaskState.FINISH) + return (tasks || []).filter(v => v.status === DownloadTaskState.DONE) .length; }, [tasks]); diff --git a/frontend/src/app/pages/MainPage/Navbar/DownloadListPopup/types.ts b/frontend/src/app/pages/MainPage/Navbar/DownloadListPopup/types.ts index 40f285ee9..f31c44716 100644 --- a/frontend/src/app/pages/MainPage/Navbar/DownloadListPopup/types.ts +++ b/frontend/src/app/pages/MainPage/Navbar/DownloadListPopup/types.ts @@ -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 { DownloadTask } from '../../slice/types'; type OnDownloadFileType = (item: T) => void; export interface DownloadListProps { diff --git a/frontend/src/app/pages/MainPage/Navbar/ModifyPassword.tsx b/frontend/src/app/pages/MainPage/Navbar/ModifyPassword.tsx index e8a964b43..378d24a1b 100644 --- a/frontend/src/app/pages/MainPage/Navbar/ModifyPassword.tsx +++ b/frontend/src/app/pages/MainPage/Navbar/ModifyPassword.tsx @@ -1,13 +1,35 @@ +/** + * 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, Form, Input, message, Modal } from 'antd'; -import { RULES } from 'app/constants'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectLoggedInUser, selectModifyPasswordLoading, } from 'app/slice/selectors'; import { modifyAccountPassword } from 'app/slice/thunks'; import { ModifyUserPassword } from 'app/slice/types'; -import { FC, useCallback, useEffect, useMemo } from 'react'; +import { FC, useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { + getConfirmPasswordValidator, + getPasswordValidator, +} from 'utils/validators'; const FormItem = Form.Item; interface ModifyPasswordProps { visible: boolean; @@ -21,6 +43,8 @@ export const ModifyPassword: FC = ({ const loggedInUser = useSelector(selectLoggedInUser); const loading = useSelector(selectModifyPasswordLoading); const [form] = Form.useForm(); + const t = useI18NPrefix('main.nav.account.changePassword'); + const tg = useI18NPrefix('global'); const reset = useCallback(() => { form.resetFields(); @@ -38,22 +62,18 @@ export const ModifyPassword: FC = ({ modifyAccountPassword({ params, resolve: () => { - message.success('修改成功'); + message.success(tg('operation.updateSuccess')); onCancel(); }, }), ); }, - [dispatch, onCancel], + [dispatch, onCancel, tg], ); - const confirmRule = useMemo(() => { - return RULES.getConfirmRule('newPassword'); - }, []); - return ( = ({ wrapperCol={{ span: 12 }} onFinish={formSubmit} > - + - + diff --git a/frontend/src/app/pages/MainPage/Navbar/OrganizationList.tsx b/frontend/src/app/pages/MainPage/Navbar/OrganizationList.tsx index c97123192..872f85dbc 100644 --- a/frontend/src/app/pages/MainPage/Navbar/OrganizationList.tsx +++ b/frontend/src/app/pages/MainPage/Navbar/OrganizationList.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 { CheckOutlined, LoadingOutlined, @@ -5,6 +23,7 @@ import { } from '@ant-design/icons'; import { Menu } from 'antd'; import { Avatar, MenuListItem, ToolbarButton } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectOrganizationListLoading, selectOrganizations, @@ -33,6 +52,7 @@ export function OrganizationList() { const organizations = useSelector(selectOrganizations); const orgId = useSelector(selectOrgId); const listLoading = useSelector(selectOrganizationListLoading); + const t = useI18NPrefix('main.nav.organization'); const showForm = useCallback(() => { setFormVisible(true); @@ -95,7 +115,7 @@ export function OrganizationList() { return ( - <h2>组织列表</h2> + <h2>{t('title')}</h2> <ToolbarButton size="small" icon={<PlusOutlined />} diff --git a/frontend/src/app/pages/MainPage/Navbar/Profile.tsx b/frontend/src/app/pages/MainPage/Navbar/Profile.tsx index abe854928..d2199598d 100644 --- a/frontend/src/app/pages/MainPage/Navbar/Profile.tsx +++ b/frontend/src/app/pages/MainPage/Navbar/Profile.tsx @@ -1,5 +1,24 @@ +/** + * 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, Form, Input, message, Modal, ModalProps, Upload } from 'antd'; import { Avatar } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectLoggedInUser, selectSaveProfileLoading, @@ -21,6 +40,8 @@ export function Profile({ visible, onCancel }: ModalProps) { const loading = useSelector(selectSaveProfileLoading); const [saveDisabled, setSaveDisabled] = useState(true); const [form] = Form.useForm(); + const t = useI18NPrefix('main.nav.account.profile'); + const tg = useI18NPrefix('global'); const reset = useCallback(() => { form.resetFields(); @@ -69,18 +90,18 @@ export function Profile({ visible, onCancel }: ModalProps) { email: loggedInUser!.email, }, resolve: () => { - message.success('修改成功'); + message.success(tg('operation.updateSuccess')); onCancel && onCancel(null as any); }, }), ); }, - [dispatch, loggedInUser, onCancel], + [dispatch, loggedInUser, onCancel, tg], ); return ( <Modal - title="账号设置" + title={t('title')} footer={false} visible={visible} onCancel={onCancel} @@ -103,7 +124,7 @@ export function Profile({ visible, onCancel }: ModalProps) { onChange={avatarChange} > <Button type="link" loading={avatarLoading}> - 点击上传 + {t('clickUpload')} </Button> </Upload> </AvatarUpload> @@ -115,12 +136,12 @@ export function Profile({ visible, onCancel }: ModalProps) { onValuesChange={formChange} onFinish={formSubmit} > - <FormItem label="用户名">{loggedInUser?.username}</FormItem> - <FormItem label="邮箱">{loggedInUser?.email}</FormItem> - <FormItem label="姓名" name="name"> - <Input placeholder="" /> + <FormItem label={t('username')}>{loggedInUser?.username}</FormItem> + <FormItem label={t('email')}>{loggedInUser?.email}</FormItem> + <FormItem label={t('name')} name="name"> + <Input /> </FormItem> - <FormItem label="部门" name="department"> + <FormItem label={t('department')} name="department"> <Input /> </FormItem> <Form.Item wrapperCol={{ offset: 7, span: 12 }}> @@ -131,7 +152,7 @@ export function Profile({ visible, onCancel }: ModalProps) { disabled={saveDisabled} block > - 保存 + {tg('button.save')} </Button> </Form.Item> </Form> diff --git a/frontend/src/app/pages/MainPage/Navbar/index.tsx b/frontend/src/app/pages/MainPage/Navbar/index.tsx index dcda81448..0cb392db4 100644 --- a/frontend/src/app/pages/MainPage/Navbar/index.tsx +++ b/frontend/src/app/pages/MainPage/Navbar/index.tsx @@ -1,8 +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 { BankFilled, ExportOutlined, FormOutlined, FunctionOutlined, + GlobalOutlined, ProfileOutlined, SafetyCertificateFilled, SettingFilled, @@ -12,6 +31,7 @@ import { import { List, Menu, Tooltip } from 'antd'; import logo from 'app/assets/images/logo.svg'; import { Avatar, MenuListItem, Popup } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectCurrentOrganization, selectDownloadPolling, @@ -23,7 +43,9 @@ import { selectLoggedInUser } from 'app/slice/selectors'; import { logout } from 'app/slice/thunks'; import { downloadFile } from 'app/utils/fetch'; import { BASE_RESOURCE_URL } from 'globalConstants'; +import { changeLang } from 'locales/i18n'; import React, { cloneElement, useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { NavLink, useHistory, useRouteMatch } from 'react-router-dom'; import styled from 'styled-components/macro'; @@ -63,7 +85,8 @@ export function Navbar() { const matchModules = useRouteMatch<{ moduleName: string }>( '/organizations/:orgId/:moduleName', ); - + const { i18n } = useTranslation(); + const t = useI18NPrefix('main'); const brandClick = useCallback(() => { history.push('/'); }, [history]); @@ -88,49 +111,49 @@ export function Navbar() { () => [ { name: 'variables', - title: '公共变量设置', + title: t('subNavs.variables.title'), icon: <FunctionOutlined />, module: ResourceTypes.Manager, }, { name: 'orgSettings', - title: '组织设置', + title: t('subNavs.orgSettings.title'), icon: <SettingOutlined />, module: ResourceTypes.Manager, }, ], - [], + [t], ); const navs = useMemo( () => [ { name: 'vizs', - title: '可视化', + title: t('nav.vizs'), icon: <i className="iconfont icon-xietongzhihuidaping" />, module: ResourceTypes.Viz, }, { name: 'views', - title: '数据视图', + title: t('nav.views'), icon: <i className="iconfont icon-24gf-table" />, module: ResourceTypes.View, }, { name: 'sources', - title: '数据源', + title: t('nav.sources'), icon: <i className="iconfont icon-shujukupeizhi" />, module: ResourceTypes.Source, }, { name: 'schedules', - title: '定时任务', + title: t('nav.schedules'), icon: <i className="iconfont icon-fasongyoujian" />, module: ResourceTypes.Schedule, }, { name: 'members', - title: '成员与角色', + title: t('nav.members'), icon: <i className="iconfont icon-users1" />, isActive: (_, location) => !!location.pathname.match( @@ -140,13 +163,13 @@ export function Navbar() { }, { name: 'permissions', - title: '权限', + title: t('nav.permissions'), icon: <SafetyCertificateFilled />, module: ResourceTypes.Manager, }, { name: 'toSub', - title: '设置', + title: t('nav.settings'), icon: <SettingFilled />, isActive: (_, location) => { const reg = new RegExp( @@ -159,7 +182,7 @@ export function Navbar() { module: ResourceTypes.Manager, }, ], - [subNavs], + [subNavs, t], ); const showSubNav = useMemo( @@ -183,6 +206,10 @@ export function Navbar() { case 'password': setModifyPasswordVisible(true); break; + case 'zh': + case 'en': + changeLang(key); + break; default: break; } @@ -246,11 +273,13 @@ export function Navbar() { onVisibleChange={organizationListVisibleChange} > <li> - <Avatar - src={`${BASE_RESOURCE_URL}${currentOrganization?.avatar}`} - > - <BankFilled /> - </Avatar> + <Tooltip title={t('nav.organization.title')} placement="right"> + <Avatar + src={`${BASE_RESOURCE_URL}${currentOrganization?.avatar}`} + > + <BankFilled /> + </Avatar> + </Tooltip> </li> </Popup> <Popup @@ -260,23 +289,33 @@ export function Navbar() { selectable={false} onClick={userMenuSelect} > + <MenuListItem + key="language" + prefix={<GlobalOutlined className="icon" />} + title={<p>{t('nav.account.switchLanguage.title')}</p>} + sub + > + <MenuListItem key="zh">中文</MenuListItem> + <MenuListItem key="en">English</MenuListItem> + </MenuListItem> + <Menu.Divider /> <MenuListItem key="profile" prefix={<ProfileOutlined className="icon" />} > - <p>账号设置</p> + <p>{t('nav.account.profile.title')}</p> </MenuListItem> <MenuListItem key="password" prefix={<FormOutlined className="icon" />} > - <p>修改密码</p> + <p>{t('nav.account.changePassword.title')}</p> </MenuListItem> <MenuListItem key="logout" prefix={<ExportOutlined className="icon" />} > - <p>退出登录</p> + <p>{t('nav.account.logout.title')}</p> </MenuListItem> </Menu> } diff --git a/frontend/src/app/pages/MainPage/Navbar/service.ts b/frontend/src/app/pages/MainPage/Navbar/service.ts index 9ad7f663c..dd2e26a50 100644 --- a/frontend/src/app/pages/MainPage/Navbar/service.ts +++ b/frontend/src/app/pages/MainPage/Navbar/service.ts @@ -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 { request } from 'utils/request'; import { errorHandle } from 'utils/utils'; import { DownloadTask, DownloadTaskState } from '../slice/types'; @@ -9,7 +27,7 @@ export const loadTasks = async () => { method: 'GET', }); const isNeedStopPolling = !(data || []).some( - v => v.status === DownloadTaskState.CREATE, + v => v.status === DownloadTaskState.CREATED, ); return { isNeedStopPolling, diff --git a/frontend/src/app/pages/MainPage/OrganizationForm.tsx b/frontend/src/app/pages/MainPage/OrganizationForm.tsx index fd834a615..5bfdd9e26 100644 --- a/frontend/src/app/pages/MainPage/OrganizationForm.tsx +++ b/frontend/src/app/pages/MainPage/OrganizationForm.tsx @@ -1,4 +1,23 @@ +/** + * 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 { Form, Input, Modal, ModalProps } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import debounce from 'debounce-promise'; import { DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; import React, { useCallback } from 'react'; @@ -18,6 +37,8 @@ export function OrganizationForm({ visible, onCancel }: OrganizationFormProps) { const history = useHistory(); const loading = useSelector(selectSaveOrganizationLoading); const [form] = Form.useForm(); + const t = useI18NPrefix('main.nav.organization'); + const tg = useI18NPrefix('global'); const formSubmit = useCallback( values => { @@ -44,9 +65,9 @@ export function OrganizationForm({ visible, onCancel }: OrganizationFormProps) { return ( <Modal - title="创建组织" + title={t('create')} visible={visible} - okText="保存并进入" + okText={t('save')} confirmLoading={loading} onOk={save} onCancel={onCancel} @@ -54,16 +75,19 @@ export function OrganizationForm({ visible, onCancel }: OrganizationFormProps) { > <Form form={form} - labelCol={{ span: 4 }} + labelCol={{ span: 6 }} labelAlign="left" - wrapperCol={{ span: 18 }} + wrapperCol={{ span: 16 }} onFinish={formSubmit} > <FormItem name="name" - label="名称" + label={t('name')} rules={[ - { required: true, message: '名称不能为空' }, + { + required: true, + message: `${t('name')}${tg('validation.required')}`, + }, { validator: debounce((_, value) => { return request({ @@ -72,7 +96,7 @@ export function OrganizationForm({ visible, onCancel }: OrganizationFormProps) { params: { name: value }, }).then( () => Promise.resolve(), - () => Promise.reject(new Error('名称重复')), + err => Promise.reject(new Error(err.response.data.message)), ); }, DEFAULT_DEBOUNCE_WAIT), }, @@ -80,7 +104,7 @@ export function OrganizationForm({ visible, onCancel }: OrganizationFormProps) { > <Input /> </FormItem> - <FormItem name="description" label="描述"> + <FormItem name="description" label={t('desc')}> <Input.TextArea autoSize={{ minRows: 4, maxRows: 8 }} /> </FormItem> </Form> diff --git a/frontend/src/app/pages/MainPage/constants.ts b/frontend/src/app/pages/MainPage/constants.ts index 2d4b385c2..100bc5632 100644 --- a/frontend/src/app/pages/MainPage/constants.ts +++ b/frontend/src/app/pages/MainPage/constants.ts @@ -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. + */ + export enum UserSettingTypes { LastVisitedOrganization = 'LAST_VISITED_ORGANIZATION', } diff --git a/frontend/src/app/pages/MainPage/index.tsx b/frontend/src/app/pages/MainPage/index.tsx index 5ac02e53b..767f654bf 100644 --- a/frontend/src/app/pages/MainPage/index.tsx +++ b/frontend/src/app/pages/MainPage/index.tsx @@ -1,14 +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 { useAppSlice } from 'app/slice'; import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { - Redirect, - Route, - Switch, - useHistory, - useRouteMatch, -} from 'react-router'; +import { Redirect, Route, Switch, useRouteMatch } from 'react-router'; import styled from 'styled-components/macro'; +import ChartManager from '../ChartWorkbenchPage/models/ChartManager'; import { NotFoundPage } from '../NotFoundPage'; import { AccessRoute } from './AccessRoute'; import { Background } from './Background'; @@ -40,15 +53,16 @@ export function MainPage() { const { actions: vizActions } = useVizSlice(); const { actions: viewActions } = useViewSlice(); const dispatch = useDispatch(); - const history = useHistory(); const organizationMatch = useRouteMatch<MainPageRouteParams>( '/organizations/:orgId', ); - const { isExact } = useRouteMatch(); const orgId = useSelector(selectOrgId); // loaded first time useEffect(() => { + ChartManager.instance() + .load() + .catch(err => console.error('Fail to load customize charts with ', err)); dispatch(getUserSettings(organizationMatch?.params.orgId)); dispatch(getDataProviders()); return () => { @@ -64,18 +78,15 @@ export function MainPage() { } }, [dispatch, vizActions, viewActions, orgId]); - useEffect(() => { - if (isExact && orgId) { - history.push(`/organizations/${orgId}`); - } - }, [isExact, orgId, history]); - return ( <AppContainer> <Background /> <Navbar /> {orgId && ( <Switch> + <Route path="/" exact> + <Redirect to={`/organizations/${orgId}`} /> + </Route> <Route path="/confirminvite" component={ConfirmInvitePage} /> <Route path="/organizations/:orgId" exact> <Redirect diff --git a/frontend/src/app/pages/MainPage/pages/ConfirmInvitePage/index.tsx b/frontend/src/app/pages/MainPage/pages/ConfirmInvitePage/index.tsx index fc62e472d..6f771c929 100644 --- a/frontend/src/app/pages/MainPage/pages/ConfirmInvitePage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/ConfirmInvitePage/index.tsx @@ -1,5 +1,6 @@ import { message } from 'antd'; import { EmptyFiller } from 'app/components/EmptyFiller'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { useCallback, useEffect } from 'react'; import { useHistory } from 'react-router'; import styled from 'styled-components'; @@ -7,14 +8,16 @@ import { confirmInvite } from './service'; export const ConfirmInvitePage = () => { const history = useHistory(); + const t = useI18NPrefix('confirmInvite'); + const onConfirm = useCallback( token => { confirmInvite(token).then(() => { - message.success('成功加入组织'); + message.success(t('join')); history.replace('/'); }); }, - [history], + [history, t], ); useEffect(() => { const search = window.location.search, @@ -27,7 +30,7 @@ export const ConfirmInvitePage = () => { }, []); return ( <Wrapper> - <EmptyFiller loading title="确认邀请中" /> + <EmptyFiller loading title={t('confirming')} /> </Wrapper> ); }; diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/InviteForm.tsx b/frontend/src/app/pages/MainPage/pages/MemberPage/InviteForm.tsx index 91255b5d7..c76fc6140 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/InviteForm.tsx +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/InviteForm.tsx @@ -1,5 +1,24 @@ +/** + * 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, FormInstance, Select } from 'antd'; import { ModalForm, ModalFormProps } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { User } from 'app/slice/types'; import { DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; import debounce from 'lodash/debounce'; @@ -16,6 +35,7 @@ export const InviteForm = memo( ({ formProps, afterClose, ...modalProps }: ModalFormProps) => { const [options, setOptions] = useState<ValueType[]>([]); const formRef = useRef<FormInstance>(); + const t = useI18NPrefix('member.form'); const debouncedSearchUser = useMemo(() => { const searchUser = async (val: string) => { @@ -53,13 +73,13 @@ export const InviteForm = memo( <Form.Item name="emails"> <Select<ValueType> mode="tags" - placeholder="请搜索或粘贴被邀请成员邮箱" + placeholder={t('search')} options={options} onSearch={debouncedSearchUser} /> </Form.Item> <Form.Item name="sendMail" valuePropName="checked" initialValue={true}> - <Checkbox>需要被邀请成员邮件确认</Checkbox> + <Checkbox>{t('needConfirm')}</Checkbox> </Form.Item> </ModalForm> ); diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/MemberList.tsx b/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/MemberList.tsx index 9eef5a16a..b22269718 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/MemberList.tsx +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/MemberList.tsx @@ -1,7 +1,26 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { LoadingOutlined, UserAddOutlined } from '@ant-design/icons'; import { List, Modal } from 'antd'; import { Avatar, ListItem, ListTitle } from 'app/components'; import { useDebouncedSearch } from 'app/hooks/useDebouncedSearch'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { memo, ReactElement, @@ -35,6 +54,8 @@ export const MemberList = memo(() => { '/organizations/:orgId/members/:memberId', ); const memberId = matchRoleDetail?.params.memberId; + const t = useI18NPrefix('member.sidebar'); + const { filteredData, debouncedSearch } = useDebouncedSearch( list, (keywords, d) => { @@ -70,7 +91,7 @@ export const MemberList = memo(() => { if (values.sendMail) { if (success.length > 0) { - title.push('邀请邮件已成功发送'); + title.push(t('inviteSuccess')); } } else { if (success.length > 0) { @@ -79,7 +100,7 @@ export const MemberList = memo(() => { } if (fail.length > 0) { - title.push('请检查以下无效邮件地址'); + title.push(t('invalidEmail')); fail.forEach(e => { content.push(<p>{e}</p>); }); @@ -97,7 +118,7 @@ export const MemberList = memo(() => { }), ); }, - [dispatch, orgId], + [dispatch, orgId, t], ); const toDetail = useCallback( @@ -110,16 +131,16 @@ export const MemberList = memo(() => { const titleProps = useMemo( () => ({ key: 'list', - subTitle: '成员列表', + subTitle: t('memberTitle'), search: true, add: { - items: [{ key: 'invite', text: '邀请成员' }], + items: [{ key: 'invite', text: t('inviteMember') }], icon: <UserAddOutlined />, callback: showInviteForm, }, onSearch: debouncedSearch, }), - [showInviteForm, debouncedSearch], + [showInviteForm, debouncedSearch, t], ); return ( <Wrapper> @@ -148,7 +169,7 @@ export const MemberList = memo(() => { /> </ListWrapper> <InviteForm - title="邀请成员" + title={t('inviteMember')} visible={inviteFormVisible} confirmLoading={inviteLoading} onSave={invite} diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/RoleList.tsx b/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/RoleList.tsx index f55f95b0b..5d7ae277a 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/RoleList.tsx +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/RoleList.tsx @@ -1,7 +1,26 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { LoadingOutlined, PlusOutlined } from '@ant-design/icons'; import { List } from 'antd'; import { ListItem, ListTitle } from 'app/components'; import { useDebouncedSearch } from 'app/hooks/useDebouncedSearch'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { memo, useCallback, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory, useRouteMatch } from 'react-router-dom'; @@ -20,6 +39,8 @@ export const RoleList = memo(() => { '/organizations/:orgId/roles/:roleId', ); const roleId = matchRoleDetail?.params.roleId; + const t = useI18NPrefix('member.sidebar'); + const { filteredData, debouncedSearch } = useDebouncedSearch( list, (keywords, d) => d.name.toLowerCase().includes(keywords.toLowerCase()), @@ -43,16 +64,16 @@ export const RoleList = memo(() => { const titleProps = useMemo( () => ({ key: 'list', - subTitle: '角色列表', + subTitle: t('roleTitle'), search: true, add: { - items: [{ key: 'add', text: '新建角色' }], + items: [{ key: 'add', text: t('addRole') }], icon: <PlusOutlined />, callback: toAdd, }, onSearch: debouncedSearch, }), - [toAdd, debouncedSearch], + [toAdd, debouncedSearch, t], ); return ( diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/index.tsx b/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/index.tsx index 8d97507ca..9d78b3483 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/Sidebar/index.tsx @@ -1,5 +1,24 @@ +/** + * 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 { TeamOutlined, UserOutlined } from '@ant-design/icons'; import { ListSwitch } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; @@ -14,6 +33,7 @@ export const Sidebar = memo(() => { const history = useHistory(); const orgId = useSelector(selectOrgId); const { url } = useRouteMatch(); + const t = useI18NPrefix('member.sidebar'); useEffect(() => { const urlArr = url.split('/'); @@ -22,14 +42,14 @@ export const Sidebar = memo(() => { const titles = useMemo( () => [ - { key: 'members', icon: <UserOutlined />, text: '成员' }, + { key: 'members', icon: <UserOutlined />, text: t('member') }, { key: 'roles', icon: <TeamOutlined />, - text: '角色', + text: t('role'), }, ], - [], + [t], ); const switchSelect = useCallback( diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/index.tsx b/frontend/src/app/pages/MainPage/pages/MemberPage/index.tsx index 558e753b7..b08f9427d 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/index.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 React from 'react'; import { Route, Switch, useRouteMatch } from 'react-router-dom'; import styled from 'styled-components/macro'; diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/pages/MemberDetailPage.tsx b/frontend/src/app/pages/MainPage/pages/MemberPage/pages/MemberDetailPage.tsx index fc2047c96..46a70612b 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/pages/MemberDetailPage.tsx +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/pages/MemberDetailPage.tsx @@ -1,6 +1,25 @@ +/** + * 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 { LoadingOutlined } from '@ant-design/icons'; import { Button, Card, Form, message, Popconfirm, Select } from 'antd'; import { DetailPageHeader } from 'app/components/DetailPageHeader'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory, useRouteMatch } from 'react-router-dom'; @@ -43,6 +62,8 @@ export function MemberDetailPage() { params: { memberId }, } = useRouteMatch<{ memberId: string }>(); const [form] = Form.useForm(); + const t = useI18NPrefix('member.memberDetail'); + const tg = useI18NPrefix('global'); const resetForm = useCallback(() => { form.resetFields(); @@ -89,11 +110,11 @@ export function MemberDetailPage() { orgId: orgId, roles: roleSelectValues.map(id => roles.find(r => r.id === id)!), resolve: () => { - message.success('修改成功'); + message.success(tg('operation.updateSuccess')); }, }), ); - }, [dispatch, orgId, roleSelectValues, roles]); + }, [dispatch, orgId, roleSelectValues, roles, tg]); const remove = useCallback(() => { dispatch( @@ -101,12 +122,12 @@ export function MemberDetailPage() { id: editingMember!.info.id, orgId: orgId, resolve: () => { - message.success('移除成功'); + message.success(t('removeSuccess')); history.replace(`/organizations/${orgId}/members`); }, }), ); - }, [dispatch, history, orgId, editingMember]); + }, [dispatch, history, orgId, editingMember, t]); const grantOrgOwner = useCallback( (grant: boolean) => () => { @@ -115,23 +136,23 @@ export function MemberDetailPage() { userId: editingMember.info.id, orgId, resolve: () => { - message.success(`${grant ? '设置' : '撤销'}成功`); + message.success(grant ? t('grantSuccess') : t('revokeSuccess')); }, }; dispatch(grant ? grantOwner(params) : revokeOwner(params)); } }, - [dispatch, orgId, editingMember], + [dispatch, orgId, editingMember, t], ); return ( <Wrapper> <DetailPageHeader - title="成员详情" + title={t('title')} actions={ <> <Button type="primary" loading={saveMemberLoading} onClick={save}> - 保存 + {tg('button.save')} </Button> {editingMember?.info.orgOwner ? ( <Button @@ -139,16 +160,16 @@ export function MemberDetailPage() { onClick={grantOrgOwner(false)} danger > - 撤销拥有者 + {t('revokeOwner')} </Button> ) : ( <Button loading={grantLoading} onClick={grantOrgOwner(true)}> - 设为组织拥有者 + {t('grantOwner')} </Button> )} - <Popconfirm title="确定移除该成员?" onConfirm={remove}> - <Button danger>移除成员</Button> + <Popconfirm title={t('removeConfirm')} onConfirm={remove}> + <Button danger>{t('remove')}</Button> </Popconfirm> </> } @@ -162,17 +183,21 @@ export function MemberDetailPage() { labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} > - <Form.Item label="用户名">{editingMember?.info.username}</Form.Item> - <Form.Item label="邮箱">{editingMember?.info.email}</Form.Item> - <Form.Item label="用户姓名"> + <Form.Item label={t('username')}> + {editingMember?.info.username} + </Form.Item> + <Form.Item label={t('email')}> + {editingMember?.info.email} + </Form.Item> + <Form.Item label={t('name')}> {editingMember?.info.name || '-'} </Form.Item> - <Form.Item label="角色列表"> + <Form.Item label={t('roles')}> {getMemberRolesLoading ? ( <LoadingOutlined /> ) : ( <Select - placeholder="为用户指定角色" + placeholder={t('assignRole')} mode="multiple" loading={roleListLoading} onDropdownVisibleChange={roleListVisibleChange} diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/MemberForm.tsx b/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/MemberForm.tsx index 70022c945..0763e67b2 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/MemberForm.tsx +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/MemberForm.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 { Form, FormInstance, ModalProps, Transfer } from 'antd'; import { LoadingMask, ModalForm } from 'app/components'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/MemberTable.tsx b/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/MemberTable.tsx index 89131e8d3..6c213bdf8 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/MemberTable.tsx +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/MemberTable.tsx @@ -1,9 +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 { DeleteOutlined, PlusOutlined, SearchOutlined, } from '@ant-design/icons'; import { Button, Col, Input, Row, Table } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { User } from 'app/slice/types'; import { DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; import debounce from 'lodash/debounce'; @@ -22,6 +41,9 @@ export const MemberTable = memo( ({ loading, dataSource, onAdd, onChange }: MemberTableProps) => { const [keywords, setKeywords] = useState(''); const [selectedRowKeys, setSelectedRowKeys] = useState<Key[]>([]); + const t = useI18NPrefix('member.roleDetail'); + const tg = useI18NPrefix('global'); + const filteredSource = useMemo( () => dataSource.filter( @@ -55,19 +77,19 @@ export const MemberTable = memo( const columns = useMemo( () => [ - { dataIndex: 'username', title: '用户名' }, - { dataIndex: 'email', title: '邮箱' }, - { dataIndex: 'name', title: '姓名' }, + { dataIndex: 'username', title: t('username') }, + { dataIndex: 'email', title: t('email') }, + { dataIndex: 'name', title: t('name') }, { - title: '操作', + title: tg('title.action'), width: 80, align: 'center' as const, render: (_, record) => ( - <Action onClick={removeMember(record.id)}>移除</Action> + <Action onClick={removeMember(record.id)}>{t('remove')}</Action> ), }, ], - [removeMember], + [removeMember, t, tg], ); return ( @@ -80,7 +102,7 @@ export const MemberTable = memo( className="btn" onClick={onAdd} > - 添加成员 + {t('addMember')} </Button> </Col> <Col span={14}> @@ -91,13 +113,13 @@ export const MemberTable = memo( className="btn" onClick={removeSelectedMember} > - 批量删除 + {t('deleteAll')} </Button> )} </Col> <Col span={6}> <Input - placeholder="搜索成员关键字" + placeholder={t('searchMember')} prefix={<SearchOutlined className="icon" />} bordered={false} onChange={debouncedSearch} diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/index.tsx b/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/index.tsx index 959e4c3db..15f20f9d6 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/pages/RoleDetailPage/index.tsx @@ -1,12 +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 { Button, Card, Form, Input, message, Popconfirm } from 'antd'; import { DetailPageHeader } from 'app/components/DetailPageHeader'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { User } from 'app/slice/types'; import debounce from 'debounce-promise'; -import { - CommonFormTypes, - COMMON_FORM_TITLE_PREFIX, - DEFAULT_DEBOUNCE_WAIT, -} from 'globalConstants'; +import { CommonFormTypes, DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory, useRouteMatch } from 'react-router-dom'; @@ -45,6 +60,8 @@ export function RoleDetailPage() { const editingRole = useSelector(selectEditingRole); const getRoleMembersLoading = useSelector(selectGetRoleMembersLoading); const saveRoleLoading = useSelector(selectSaveRoleLoading); + const t = useI18NPrefix('member.roleDetail'); + const tg = useI18NPrefix('global'); const { params } = useRouteMatch<{ roleId: string }>(); const { roleId } = params; const [form] = Form.useForm<Pick<Role, 'name' | 'description'>>(); @@ -96,7 +113,7 @@ export function RoleDetailPage() { addRole({ role: { ...values, avatar: '', orgId }, resolve: () => { - message.success('新建成功'); + message.success(t('createSuccess')); resetForm(); }, }), @@ -108,7 +125,7 @@ export function RoleDetailPage() { role: { ...values, orgId }, members: memberTableDataSource, resolve: () => { - message.success('修改成功'); + message.success(tg('operation.updateSuccess')); }, }), ); @@ -117,7 +134,7 @@ export function RoleDetailPage() { break; } }, - [dispatch, orgId, formType, memberTableDataSource, resetForm], + [dispatch, orgId, formType, memberTableDataSource, resetForm, t, tg], ); const del = useCallback(() => { @@ -125,17 +142,17 @@ export function RoleDetailPage() { deleteRole({ id: editingRole!.info.id, resolve: () => { - message.success('删除成功'); + message.success(tg('operation.deleteSuccess')); history.replace(`/organizations/${orgId}/roles`); }, }), ); - }, [dispatch, history, orgId, editingRole]); + }, [dispatch, history, orgId, editingRole, tg]); return ( <Wrapper> <DetailPageHeader - title={`${COMMON_FORM_TITLE_PREFIX[formType]}角色`} + title={`${tg(`modal.title.${formType}`)}${t('role')}`} actions={ <> <Button @@ -143,11 +160,11 @@ export function RoleDetailPage() { loading={saveRoleLoading} onClick={form.submit} > - 保存 + {tg('button.save')} </Button> {formType === CommonFormTypes.Edit && ( - <Popconfirm title="确认删除?" onConfirm={del}> - <Button danger>删除角色</Button> + <Popconfirm title={tg('operation.deleteConfirm')} onConfirm={del}> + <Button danger>{`${tg('button.delete')}${t('role')}`}</Button> </Popconfirm> )} </> @@ -165,10 +182,13 @@ export function RoleDetailPage() { > <Form.Item name="name" - label="名称" + label={t('roleName')} validateFirst rules={[ - { required: true, message: '名称不能为空' }, + { + required: true, + message: `${t('roleName')}${tg('validation.required')}`, + }, { validator: debounce((_, value) => { if (value === editingRole?.info.name) { @@ -180,7 +200,8 @@ export function RoleDetailPage() { params: { name: value, orgId }, }).then( () => Promise.resolve(), - () => Promise.reject(new Error('名称重复')), + err => + Promise.reject(new Error(err.response.data.message)), ); }, DEFAULT_DEBOUNCE_WAIT), }, @@ -188,11 +209,11 @@ export function RoleDetailPage() { > <Input /> </Form.Item> - <Form.Item name="description" label="描述"> + <Form.Item name="description" label={t('description')}> <Input.TextArea /> </Form.Item> {formType === CommonFormTypes.Edit && ( - <Form.Item label="关联成员" wrapperCol={{ span: 17 }}> + <Form.Item label={t('relatedMember')} wrapperCol={{ span: 17 }}> <MemberTable loading={getRoleMembersLoading} dataSource={memberTableDataSource} @@ -204,7 +225,7 @@ export function RoleDetailPage() { </Form> </Card> <MemberForm - title="添加成员" + title={t('addMember')} visible={memberFormVisible} width={992} onCancel={hideMemberForm} diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/slice/index.ts b/frontend/src/app/pages/MainPage/pages/MemberPage/slice/index.ts index 4757dbfeb..b6f55eb45 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/slice/index.ts +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/slice/index.ts @@ -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 { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; import { diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/slice/selectors.ts b/frontend/src/app/pages/MainPage/pages/MemberPage/slice/selectors.ts index 2dbc8e23f..9abe4d2d3 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/slice/selectors.ts +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/slice/selectors.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'types'; import { initialState } from '.'; diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/slice/thunks.ts b/frontend/src/app/pages/MainPage/pages/MemberPage/slice/thunks.ts index 578315c79..ce684775f 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/slice/thunks.ts +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/slice/thunks.ts @@ -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 { createAsyncThunk } from '@reduxjs/toolkit'; import { User } from 'app/slice/types'; import omit from 'lodash/omit'; diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/slice/types.ts b/frontend/src/app/pages/MainPage/pages/MemberPage/slice/types.ts index 663bebf5a..3087f6f40 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/slice/types.ts +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/slice/types.ts @@ -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 { User } from 'app/slice/types'; export interface MemberState { diff --git a/frontend/src/app/pages/MainPage/pages/MemberPage/types.ts b/frontend/src/app/pages/MainPage/pages/MemberPage/types.ts index 7c7f6cb47..24fb71a99 100644 --- a/frontend/src/app/pages/MainPage/pages/MemberPage/types.ts +++ b/frontend/src/app/pages/MainPage/pages/MemberPage/types.ts @@ -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 { MainPageRouteParams } from '../../types'; export interface MemberPageRouteParams extends MainPageRouteParams { diff --git a/frontend/src/app/pages/MainPage/pages/OrgSettingPage/DeleteConfirm.tsx b/frontend/src/app/pages/MainPage/pages/OrgSettingPage/DeleteConfirm.tsx index 23b4defe7..9283eb66c 100644 --- a/frontend/src/app/pages/MainPage/pages/OrgSettingPage/DeleteConfirm.tsx +++ b/frontend/src/app/pages/MainPage/pages/OrgSettingPage/DeleteConfirm.tsx @@ -1,4 +1,5 @@ import { Button, Form, Input, message, Modal, ModalProps } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectCurrentOrganization, selectDeleteOrganizationLoading, @@ -15,6 +16,8 @@ export const DeleteConfirm = (props: ModalProps) => { const currentOrganization = useSelector(selectCurrentOrganization); const loading = useSelector(selectDeleteOrganizationLoading); const confirmDisabled = inputValue !== currentOrganization?.name; + const t = useI18NPrefix('orgSetting'); + const tg = useI18NPrefix('global'); const inputChange = useCallback(e => { setInputValue(e.target.value); @@ -24,17 +27,17 @@ export const DeleteConfirm = (props: ModalProps) => { dispatch( deleteOrganization(() => { history.replace('/'); - message.success('删除成功'); + message.success(tg('operation.deleteSuccess')); }), ); - }, [dispatch, history]); + }, [dispatch, history, tg]); return ( <Modal {...props} footer={[ <Button key="cancel" onClick={props.onCancel}> - 取消 + {t('cancel')} </Button>, <Button key="confirm" @@ -43,13 +46,13 @@ export const DeleteConfirm = (props: ModalProps) => { onClick={deleteOrg} danger > - 确认删除 + {t('delete')} </Button>, ]} > <Form.Item> <Input - placeholder="输入组织名称确认删除" + placeholder={t('enterOrgName')} value={inputValue} onChange={inputChange} /> diff --git a/frontend/src/app/pages/MainPage/pages/OrgSettingPage/index.tsx b/frontend/src/app/pages/MainPage/pages/OrgSettingPage/index.tsx index a9a67b326..ebc9ec798 100644 --- a/frontend/src/app/pages/MainPage/pages/OrgSettingPage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/OrgSettingPage/index.tsx @@ -1,5 +1,6 @@ import { Button, Card, Form, Input, message, Upload } from 'antd'; import { Avatar } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import debounce from 'debounce-promise'; import { BASE_API_URL, @@ -34,6 +35,8 @@ export function OrgSettingPage() { const currentOrganization = useSelector(selectCurrentOrganization); const saveOrganizationLoading = useSelector(selectSaveOrganizationLoading); const [form] = Form.useForm(); + const t = useI18NPrefix('orgSetting'); + const tg = useI18NPrefix('global'); useEffect(() => { if (currentOrganization) { @@ -62,12 +65,12 @@ export function OrgSettingPage() { editOrganization({ organization: { id: currentOrganization?.id, ...values }, resolve: () => { - message.success('修改成功'); + message.success(tg('operation.updateSuccess')); }, }), ); }, - [dispatch, currentOrganization], + [dispatch, currentOrganization, tg], ); const showDeleteConfirm = useCallback(() => { @@ -80,7 +83,7 @@ export function OrgSettingPage() { return ( <Wrapper> - <Card title="基本信息"> + <Card title={t('info')}> <Form name="source_form_" form={form} @@ -89,7 +92,7 @@ export function OrgSettingPage() { wrapperCol={{ span: 16 }} onFinish={save} > - <Form.Item label="头像" className="avatar"> + <Form.Item label={t('avatar')} className="avatar"> <Avatar size={SPACE_UNIT * 20} src={`${BASE_RESOURCE_URL}${currentOrganization?.avatar}`} @@ -108,15 +111,18 @@ export function OrgSettingPage() { onChange={avatarChange} > <Button type="link" loading={avatarLoading}> - 点击上传 + {t('clickToUpload')} </Button> </Upload> </Form.Item> <Form.Item name="name" - label="名称" + label={t('name')} rules={[ - { required: true, message: '名称不能为空' }, + { + required: true, + message: `${t('name')}${tg('validation.required')}`, + }, { validator: debounce((_, value) => { if (value === currentOrganization?.name) { @@ -128,7 +134,7 @@ export function OrgSettingPage() { params: { name: value }, }).then( () => Promise.resolve(), - () => Promise.reject(new Error('名称重复')), + err => Promise.reject(new Error(err.response.data.message)), ); }, DEFAULT_DEBOUNCE_WAIT), }, @@ -136,7 +142,7 @@ export function OrgSettingPage() { > <Input /> </Form.Item> - <Form.Item name="description" label="描述"> + <Form.Item name="description" label={t('description')}> <Input.TextArea autoSize={{ minRows: 4, maxRows: 8 }} /> </Form.Item> <Form.Item label=" " colon={false}> @@ -145,21 +151,19 @@ export function OrgSettingPage() { htmlType="submit" loading={saveOrganizationLoading} > - 保存 + {tg('button.save')} </Button> </Form.Item> </Form> </Card> - <Card title="删除组织"> - <h4 className="notice"> - 删除组织时,会将组织内所有资源、成员、角色和其他配置信息一并永久删除,请谨慎操作。 - </h4> + <Card title={t('deleteOrg')}> + <h4 className="notice">{t('deleteOrgDesc')}</h4> <Button danger onClick={showDeleteConfirm}> - 删除组织 + {t('deleteOrg')} </Button> <DeleteConfirm width={480} - title="删除组织" + title={t('deleteOrg')} visible={deleteConfirmVisible} onCancel={hideDeleteConfirm} /> diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/IndependentPermissionSetting.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/IndependentPermissionSetting.tsx index 41e32a0a8..555a80de1 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/IndependentPermissionSetting.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/IndependentPermissionSetting.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 { Form, Radio, RadioChangeEvent } from 'antd'; import { memo } from 'react'; import { PermissionLevels } from '../../constants'; diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/PermissionTable.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/PermissionTable.tsx index 105a668bf..8a34de680 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/PermissionTable.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/PermissionTable.tsx @@ -1,5 +1,24 @@ +/** + * 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 { SearchOutlined } from '@ant-design/icons'; import { Col, Input, Row, Table, TableColumnProps } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import useResizeObserver from 'app/hooks/useResizeObserver'; import { useSearchAndExpand } from 'app/hooks/useSearchAndExpand'; import { memo, useEffect, useMemo } from 'react'; @@ -47,6 +66,7 @@ export const PermissionTable = memo( onPrivilegeChange, }: PermissionTableProps) => { const { height, ref } = useResizeObserver(); + const t = useI18NPrefix('permission'); const treeData = useMemo(() => { if (dataSource && privileges) { @@ -92,10 +112,10 @@ export const PermissionTable = memo( const columns: TableColumnProps<DataSourceTreeNode>[] = [ { dataIndex: 'name', - title: '资源名称', + title: t('resourceName'), }, { - title: '权限详情', + title: t('privileges'), align: 'center' as const, width: getPrivilegeSettingWidth( viewpoint, @@ -114,14 +134,14 @@ export const PermissionTable = memo( }, ]; return columns; - }, [viewpoint, viewpointType, dataSourceType, privilegeChange]); + }, [viewpoint, viewpointType, dataSourceType, privilegeChange, t]); return ( <> <Toolbar> <Col span={12}> <Input - placeholder="搜索资源名称关键字" + placeholder={t('searchResources')} prefix={<SearchOutlined className="icon" />} bordered={false} onChange={debouncedSearch} diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/PrivilegeSetting.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/PrivilegeSetting.tsx index 5e4a4def7..07a8d8421 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/PrivilegeSetting.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/PrivilegeSetting.tsx @@ -1,9 +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 { Checkbox, Space } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { memo, useCallback, useEffect, useState } from 'react'; import { PermissionLevels, ResourceTypes, - RESOURCE_TYPE_PERMISSION_LABEL, RESOURCE_TYPE_PERMISSION_MAPPING, SubjectTypes, Viewpoints, @@ -36,6 +54,7 @@ export const PrivilegeSetting = memo( const [values, setValues] = useState<PermissionLevels[]>( getDefaultPermissionArray(), ); + const t = useI18NPrefix('permission'); const resourceType = getPrivilegeSettingType( viewpoint, @@ -68,7 +87,7 @@ export const PrivilegeSetting = memo( checked={level === values[index]} onChange={privilegeChange(index, level)} > - {RESOURCE_TYPE_PERMISSION_LABEL[resourceType!][index]} + {t(`privilegeLabel.${resourceType!.toLowerCase()}.${index}`)} </Checkbox> ))} </Space> diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/VizPermissionForm.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/VizPermissionForm.tsx index d1a4e414f..a73d9688c 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/VizPermissionForm.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/VizPermissionForm.tsx @@ -1,13 +1,30 @@ +/** + * 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 { Form, Radio } from 'antd'; import { LoadingMask } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import classnames from 'classnames'; import { memo, useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { listToTree } from 'utils/utils'; import { - CREATE_PERMISSION_VALUES, - MODULE_PERMISSION_VALUES, PermissionLevels, ResourceTypes, SubjectTypes, @@ -69,6 +86,7 @@ export const VizPermissionForm = memo( const privileges = useSelector(state => selectPrivileges(state, { viewpoint, dataSourceType }), ); + const t = useI18NPrefix('permission'); const vizTreeData = useMemo(() => { if (folders && storyboards && privileges) { @@ -267,25 +285,68 @@ export const VizPermissionForm = memo( ], ); + const modulePermissionValues = useMemo( + () => [ + { + text: t( + `modulePermissionLabel.${ + PermissionLevels[PermissionLevels.Disable] + }`, + ), + value: PermissionLevels.Disable, + }, + { + text: t( + `modulePermissionLabel.${ + PermissionLevels[PermissionLevels.Enable] + }`, + ), + value: PermissionLevels.Enable, + }, + ], + [t], + ); + const createPermissionValues = useMemo( + () => [ + { + text: t( + `createPermissionLabel.${ + PermissionLevels[PermissionLevels.Disable] + }`, + ), + value: PermissionLevels.Disable, + }, + { + text: t( + `createPermissionLabel.${ + PermissionLevels[PermissionLevels.Create] + }`, + ), + value: PermissionLevels.Create, + }, + ], + [t], + ); + return ( <Wrapper className={classnames({ selected })}> <LoadingMask loading={permissionLoading}> <FormContent labelAlign="left" - labelCol={{ span: 3 }} - wrapperCol={{ span: 19 }} + labelCol={{ span: 4 }} + wrapperCol={{ span: 18 }} > <IndependentPermissionSetting enabled={moduleEnabled} - label="功能权限" - extra="开启功能权限之后,用户才能在 Datart 界面上使用该功能" - values={MODULE_PERMISSION_VALUES} + label={t('modulePermission')} + extra={t('modulePermissionDesc')} + values={modulePermissionValues} onChange={independentPermissionChange('*')} /> - <Form.Item label="资源明细"> + <Form.Item label={t('resourceDetail')}> <Radio.Group value={vizType} onChange={vizTypeChange}> - <Radio value="folder">目录</Radio> - <Radio value="persentation">演示</Radio> + <Radio value="folder">{t('folder')}</Radio> + <Radio value="persentation">{t('presentation')}</Radio> </Radio.Group> </Form.Item> <Form.Item @@ -309,8 +370,8 @@ export const VizPermissionForm = memo( {vizType === 'persentation' && ( <IndependentPermissionSetting enabled={storyboardCreateEnabled} - label="新增故事板" - values={CREATE_PERMISSION_VALUES} + label={t('createStoryboard')} + values={createPermissionValues} onChange={independentPermissionChange( VizResourceSubTypes.Storyboard, )} diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/index.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/index.tsx index 94c1555c4..9aef40b25 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/index.tsx @@ -1,15 +1,31 @@ +/** + * 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 { Form } from 'antd'; import { LoadingMask } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import classnames from 'classnames'; import { memo, useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { - CREATE_PERMISSION_VALUES, - MODULE_PERMISSION_VALUES, PermissionLevels, ResourceTypes, - RESOURCE_TYPE_LABEL, SubjectTypes, Viewpoints, } from '../../constants'; @@ -62,6 +78,7 @@ export const PermissionForm = memo( const privileges = useSelector(state => selectPrivileges(state, { viewpoint, dataSourceType }), ); + const t = useI18NPrefix('permission'); const { moduleEnabled, createEnabled } = useMemo(() => { let moduleEnabled = PermissionLevels.Disable; @@ -229,20 +246,63 @@ export const PermissionForm = memo( ], ); + const modulePermissionValues = useMemo( + () => [ + { + text: t( + `modulePermissionLabel.${ + PermissionLevels[PermissionLevels.Disable] + }`, + ), + value: PermissionLevels.Disable, + }, + { + text: t( + `modulePermissionLabel.${ + PermissionLevels[PermissionLevels.Enable] + }`, + ), + value: PermissionLevels.Enable, + }, + ], + [t], + ); + const createPermissionValues = useMemo( + () => [ + { + text: t( + `createPermissionLabel.${ + PermissionLevels[PermissionLevels.Disable] + }`, + ), + value: PermissionLevels.Disable, + }, + { + text: t( + `createPermissionLabel.${ + PermissionLevels[PermissionLevels.Create] + }`, + ), + value: PermissionLevels.Create, + }, + ], + [t], + ); + return ( <Wrapper className={classnames({ selected })}> <LoadingMask loading={permissionLoading}> <FormContent labelAlign="left" - labelCol={{ span: 3 }} - wrapperCol={{ span: 19 }} + labelCol={{ span: 4 }} + wrapperCol={{ span: 18 }} > {viewpoint === Viewpoints.Subject && ( <IndependentPermissionSetting enabled={moduleEnabled} - label="功能权限" - extra="开启功能权限之后,用户才能在 Datart 界面上使用该功能" - values={MODULE_PERMISSION_VALUES} + label={t('modulePermission')} + extra={t('modulePermissionDesc')} + values={modulePermissionValues} onChange={independentPermissionChange('*')} /> )} @@ -252,12 +312,14 @@ export const PermissionForm = memo( ) && ( <IndependentPermissionSetting enabled={createEnabled} - label={`新增${RESOURCE_TYPE_LABEL[dataSourceType]}`} - values={CREATE_PERMISSION_VALUES} + label={`${t('add')}${t( + `module.${dataSourceType.toLowerCase()}`, + )}`} + values={createPermissionValues} onChange={independentPermissionChange(dataSourceType)} /> )} - <Form.Item label="资源明细"> + <Form.Item label={t('resourceDetail')}> <PermissionTable viewpoint={viewpoint} viewpointType={viewpointType} diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/utils.ts b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/utils.ts index 71bc59f7e..63560a19a 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/utils.ts +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/PermissionForm/utils.ts @@ -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 { fastDeleteArrayElement, getDiffParams } from 'utils/utils'; import { PermissionLevels, diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/ResourcesPermissionSetting.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/ResourcesPermissionSetting.tsx index ba3fe9562..d038246a2 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/ResourcesPermissionSetting.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/ResourcesPermissionSetting.tsx @@ -1,12 +1,26 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { Card } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { - ResourceTypes, - RESOURCE_TYPE_LABEL, - SubjectTypes, - Viewpoints, -} from '../constants'; +import { ResourceTypes, SubjectTypes, Viewpoints } from '../constants'; import { selectFolderListLoading, selectFolders, @@ -56,6 +70,7 @@ export const ResourcesPermissionSetting = memo( const permissionLoading = useSelector(state => selectPermissionLoading(state, { viewpoint }), ); + const t = useI18NPrefix('permission'); useEffect(() => { if (viewpointType && viewpointId) { @@ -119,9 +134,9 @@ export const ResourcesPermissionSetting = memo( () => tabSource.map(({ type }) => ({ key: type, - tab: RESOURCE_TYPE_LABEL[type], + tab: t(`module.${type.toLowerCase()}`), })), - [tabSource], + [tabSource, t], ); return ( diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/SubjectsPermissionSetting.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/SubjectsPermissionSetting.tsx index a52b25e30..0169143af 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/SubjectsPermissionSetting.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/SubjectsPermissionSetting.tsx @@ -1,4 +1,23 @@ +/** + * 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 { Card } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { ResourceTypes, SubjectTypes, Viewpoints } from '../constants'; @@ -38,6 +57,7 @@ export const SubjectPermissionSetting = memo( const permissionLoading = useSelector(state => selectPermissionLoading(state, { viewpoint }), ); + const t = useI18NPrefix('permission'); useEffect(() => { if (viewpointType && viewpointId) { @@ -68,18 +88,18 @@ export const SubjectPermissionSetting = memo( () => [ { type: SubjectTypes.Role, - label: '角色', + label: t('role'), dataSource: roles, loading: roleListLoading, }, { type: SubjectTypes.UserRole, - label: '成员', + label: t('member'), dataSource: members, loading: memberListLoading, }, ], - [roles, members, roleListLoading, memberListLoading], + [roles, members, roleListLoading, memberListLoading, t], ); const tabList = useMemo( diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/index.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/index.tsx index 399898e1d..0b1fd2a7a 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Main/index.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 { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { memo } from 'react'; import { useSelector } from 'react-redux'; diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/FlexCollapse.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/FlexCollapse.tsx index 17c9f1b4e..6b5f2ab07 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/FlexCollapse.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/FlexCollapse.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 { CaretRightFilled } from '@ant-design/icons'; import classnames from 'classnames'; import { diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/ResourcePanels.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/ResourcePanels.tsx index 202294f46..aed075134 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/ResourcePanels.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/ResourcePanels.tsx @@ -1,10 +1,29 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { Col, Radio, Row } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import classNames from 'classnames'; import { memo, useCallback, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { SPACE_MD, SPACE_XS } from 'styles/StyleConstants'; -import { ResourceTypes, RESOURCE_TYPE_LABEL } from '../constants'; +import { ResourceTypes } from '../constants'; import { selectFolderListLoading, selectFolders, @@ -35,6 +54,7 @@ export const ResourcePanels = memo( const viewListLoading = useSelector(selectViewListLoading); const sourceListLoading = useSelector(selectSourceListLoading); const scheduleListLoading = useSelector(selectScheduleListLoading); + const t = useI18NPrefix('permission'); const resourcePanels = useMemo( () => [ @@ -79,7 +99,7 @@ export const ResourcePanels = memo( <Panel key={resourceType} id={resourceType} - title={RESOURCE_TYPE_LABEL[resourceType]} + title={t(`module.${resourceType.toLowerCase()}`)} onChange={onToggle} > {resourceType === ResourceTypes.Viz ? ( @@ -87,8 +107,8 @@ export const ResourcePanels = memo( <VizTypeSwitch key="switch"> <Col> <Radio.Group value={vizType} onChange={vizTypeChange}> - <Radio value="folder">目录</Radio> - <Radio value="presentation">演示</Radio> + <Radio value="folder">{t('folder')}</Radio> + <Radio value="presentation">{t('presentation')}</Radio> </Radio.Group> </Col> </VizTypeSwitch> diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/ResourceTree.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/ResourceTree.tsx index e5bffa2d7..070952b51 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/ResourceTree.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/ResourceTree.tsx @@ -1,4 +1,23 @@ +/** + * 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 { Tree, TreeTitle } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { useSearchAndExpand } from 'app/hooks/useSearchAndExpand'; import { memo, useCallback, useEffect, useMemo } from 'react'; import { listToTree } from 'utils/utils'; @@ -14,6 +33,8 @@ interface ResourceTreeProps { export const ResourceTree = memo( ({ loading, dataSource, onSelect }: ResourceTreeProps) => { + const t = useI18NPrefix('permission'); + const treeData = useMemo( () => listToTree(dataSource, null, []) as DataSourceTreeNode[], [dataSource], @@ -55,7 +76,7 @@ export const ResourceTree = memo( return ( <> <Searchbar - placeholder="搜索资源名称关键字" + placeholder={t('searchResources')} onSearch={debouncedSearch} /> <Tree diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/Searchbar.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/Searchbar.tsx index 78a493619..a606e17fd 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/Searchbar.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/Searchbar.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 { SearchOutlined } from '@ant-design/icons'; import { Col, Input, Row } from 'antd'; import styled from 'styled-components/macro'; diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/SubjectList.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/SubjectList.tsx index 30ceadabf..5bf66823f 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/SubjectList.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/SubjectList.tsx @@ -1,7 +1,26 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { LoadingOutlined } from '@ant-design/icons'; import { List } from 'antd'; import { ListItem } from 'app/components'; import { useDebouncedSearch } from 'app/hooks/useDebouncedSearch'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { memo } from 'react'; import styled from 'styled-components/macro'; import { SubjectTypes } from '../constants'; @@ -25,13 +44,15 @@ export const SubjectList = memo( loading, onToDetail, }: SubjectListProps) => { + const t = useI18NPrefix('permission'); + const { filteredData, debouncedSearch } = useDebouncedSearch( dataSource, (keywords, d) => d.name.includes(keywords), ); return ( <> - <Searchbar placeholder="搜索关键字" onSearch={debouncedSearch} /> + <Searchbar placeholder={t('search')} onSearch={debouncedSearch} /> <List loading={{ spinning: loading, diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/SubjectPanels.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/SubjectPanels.tsx index 4f52657d5..df0d55274 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/SubjectPanels.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/SubjectPanels.tsx @@ -1,3 +1,22 @@ +/** + * 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 useI18NPrefix from 'app/hooks/useI18NPrefix'; import { memo, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { SubjectTypes } from '../constants'; @@ -18,23 +37,24 @@ export const SubjectPanels = memo( const members = useSelector(selectMembers); const roleListLoading = useSelector(selectRoleListLoading); const memberListLoading = useSelector(selectMemberListLoading); + const t = useI18NPrefix('permission'); const subjectPanels = useMemo( () => [ { type: SubjectTypes.Role, - label: '角色', + label: t('role'), dataSource: roles, loading: roleListLoading, }, { type: SubjectTypes.UserRole, - label: '成员', + label: t('member'), dataSource: members, loading: memberListLoading, }, ], - [roles, members, roleListLoading, memberListLoading], + [roles, members, roleListLoading, memberListLoading, t], ); return ( diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/index.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/index.tsx index 3acb9cfc5..ed63239ec 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/index.tsx @@ -1,17 +1,31 @@ +/** + * 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 { FileProtectOutlined, TeamOutlined } from '@ant-design/icons'; import { ListSwitch } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { memo, useCallback, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router'; import styled from 'styled-components/macro'; import { SPACE_XS } from 'styles/StyleConstants'; -import { - ResourceTypes, - SubjectTypes, - Viewpoints, - VIEWPOINT_LABEL, -} from '../constants'; +import { ResourceTypes, SubjectTypes, Viewpoints } from '../constants'; import { getDataSource } from '../slice/thunks'; import { ResourcePanels } from './ResourcePanels'; import { SubjectPanels } from './SubjectPanels'; @@ -27,6 +41,7 @@ export const Sidebar = memo( const dispatch = useDispatch(); const history = useHistory(); const orgId = useSelector(selectOrgId); + const t = useI18NPrefix('permission'); useEffect(() => { if (viewpointType) { @@ -64,15 +79,15 @@ export const Sidebar = memo( { key: Viewpoints.Subject, icon: <TeamOutlined />, - text: VIEWPOINT_LABEL[Viewpoints.Subject], + text: t(`viewpoint.${Viewpoints.Subject}`), }, { key: Viewpoints.Resource, icon: <FileProtectOutlined />, - text: VIEWPOINT_LABEL[Viewpoints.Resource], + text: t(`viewpoint.${Viewpoints.Resource}`), }, ], - [], + [t], ); return ( diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/types.ts b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/types.ts index 8172015b6..d3ea0acc9 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/types.ts +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/Sidebar/types.ts @@ -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 { ResourceTypes, SubjectTypes } from '../constants'; export interface PanelsProps { diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/constants.ts b/frontend/src/app/pages/MainPage/pages/PermissionPage/constants.ts index eded26659..058026a72 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/constants.ts +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/constants.ts @@ -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. + */ + export enum Viewpoints { Subject = 'subject', Resource = 'resource', @@ -37,18 +55,6 @@ export enum PermissionLevels { Create = (1 << 7) | Manage, } -export const VIEWPOINT_LABEL = { - [Viewpoints.Subject]: '常规视图', - [Viewpoints.Resource]: '资源视图', -}; - -export const RESOURCE_TYPE_LABEL = { - [ResourceTypes.Viz]: '可视化', - [ResourceTypes.View]: '数据视图', - [ResourceTypes.Source]: '数据源', - [ResourceTypes.Schedule]: '定时任务', -}; - export const RESOURCE_TYPE_PERMISSION_MAPPING = { [ResourceTypes.Viz]: [ PermissionLevels.Read, @@ -60,19 +66,3 @@ export const RESOURCE_TYPE_PERMISSION_MAPPING = { [ResourceTypes.Source]: [PermissionLevels.Read, PermissionLevels.Create], [ResourceTypes.Schedule]: [PermissionLevels.Create], }; - -export const RESOURCE_TYPE_PERMISSION_LABEL = { - [ResourceTypes.Viz]: ['查看', '下载', '分享', '管理'], - [ResourceTypes.View]: ['使用', '管理'], - [ResourceTypes.Source]: ['使用', '管理'], - [ResourceTypes.Schedule]: ['管理'], -}; - -export const MODULE_PERMISSION_VALUES = [ - { text: '禁用', value: PermissionLevels.Disable }, - { text: '启用', value: PermissionLevels.Enable }, -]; -export const CREATE_PERMISSION_VALUES = [ - { text: '禁用', value: PermissionLevels.Disable }, - { text: '启用', value: PermissionLevels.Create }, -]; diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/index.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/index.tsx index b45d91965..d49b92881 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/index.tsx @@ -1,4 +1,22 @@ +/** + * 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 { EmptyFiller, Split } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { useSplitSizes } from 'app/hooks/useSplitSizes'; import { useCallback } from 'react'; import { Route, useRouteMatch } from 'react-router-dom'; @@ -27,6 +45,7 @@ export function PermissionPage() { type: ResourceTypes | SubjectTypes; id: string; }>('/organizations/:orgId/permissions/:viewpoint/:type/:id'); + const t = useI18NPrefix('permission'); const { sizes, setSizes } = useSplitSizes({ limitedSide: 0, range: [256, 768], @@ -60,9 +79,11 @@ export function PermissionPage() { /> ) : ( <EmptyFiller - title={`请在左侧列表选择${ - viewpoint === Viewpoints.Resource ? '资源项' : '角色或用户' - }`} + title={`${t('empty1')}${ + viewpoint === Viewpoints.Resource + ? t('emptyResource') + : t('emptySubject') + }${t('empty2')}`} /> )} </Container> diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/index.tsx b/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/index.tsx index 213c37b01..bd75d5504 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/index.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 { createSlice } from '@reduxjs/toolkit'; import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; import { getMembers, getRoles } from '../../MemberPage/slice/thunks'; diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/selectors.ts b/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/selectors.ts index 890367128..8738a5208 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/selectors.ts +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/selectors.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'types'; import { initialState } from '.'; diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/thunks.ts b/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/thunks.ts index e9e1f6b93..0124c3424 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/thunks.ts +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/thunks.ts @@ -1,3 +1,20 @@ +/** + * 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 { createAsyncThunk } from '@reduxjs/toolkit'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { RootState } from 'types'; diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/types.ts b/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/types.ts index 9287dffef..f4cd28fae 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/types.ts +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/slice/types.ts @@ -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 { TreeDataNode } from 'antd'; import { PermissionLevels, diff --git a/frontend/src/app/pages/MainPage/pages/PermissionPage/utils.ts b/frontend/src/app/pages/MainPage/pages/PermissionPage/utils.ts index e70793e02..d3ca0331f 100644 --- a/frontend/src/app/pages/MainPage/pages/PermissionPage/utils.ts +++ b/frontend/src/app/pages/MainPage/pages/PermissionPage/utils.ts @@ -1,3 +1,22 @@ +/** + * 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 i18n from 'i18next'; import { PermissionLevels, ResourceTypes, @@ -21,7 +40,7 @@ export function generateRootNode( ): DataSourceViewModel { return { id: type === ResourceTypes.Viz ? (vizId as string) : (type as string), - name: '所有资源', + name: i18n.t('permission.allResources'), type, parentId: null, index: null, diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/BasicBaseForm/ExecuteFormItem.tsx b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/BasicBaseForm/ExecuteFormItem.tsx index f690cfc00..fbc53db6d 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/BasicBaseForm/ExecuteFormItem.tsx +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/BasicBaseForm/ExecuteFormItem.tsx @@ -1,13 +1,14 @@ import { Checkbox, Form, Input, Select, Space } from 'antd'; +import useI18NPrefix, { prefixI18N } from 'app/hooks/useI18NPrefix'; import { FC, useMemo } from 'react'; import { TimeModes } from '../../constants'; const timeModeOptions = [ - { label: '分钟', value: TimeModes.Minute }, - { label: '小时', value: TimeModes.Hour }, - { label: '天', value: TimeModes.Day }, - { label: '月', value: TimeModes.Month }, - { label: '年', value: TimeModes.Year }, + { label: prefixI18N('global.time.minute'), value: TimeModes.Minute }, + { label: prefixI18N('global.time.hour'), value: TimeModes.Hour }, + { label: prefixI18N('global.time.day'), value: TimeModes.Day }, + { label: prefixI18N('global.time.month'), value: TimeModes.Month }, + { label: prefixI18N('global.time.year'), value: TimeModes.Year }, ]; const minutePeriodOptions = Array.from({ length: 50 }, (_, i) => { const n = i + 10; @@ -15,32 +16,32 @@ const minutePeriodOptions = Array.from({ length: 50 }, (_, i) => { }); const minuteOptions = Array.from({ length: 60 }, (_, i) => { - const n = `${`0${i}`.slice(-2)} 分`; + const n = `${`0${i}`.slice(-2)} ${prefixI18N('global.time.m')}`; return { label: n, value: i }; }); const hourOptions = Array.from({ length: 24 }, (_, i) => { - const n = `${`0${i}`.slice(-2)} 时`; + const n = `${`0${i}`.slice(-2)} ${prefixI18N('global.time.h')}`; return { label: n, value: i }; }); const dayOptions = Array.from({ length: 31 }, (_, i) => { - const n = `${`0${i + 1}`.slice(-2)} 日`; + const n = `${`0${i + 1}`.slice(-2)} ${prefixI18N('global.time.d')}`; return { label: n, value: i + 1 }; }); const monthOptions = Array.from({ length: 12 }, (_, i) => { - const n = `${`0${i + 1}`.slice(-2)} 月`; + const n = `${`0${i + 1}`.slice(-2)} ${prefixI18N('global.time.month')}`; return { label: n, value: i + 1 }; }); const weekOptions = [ - { label: '星期天', value: 1 }, - { label: '星期一', value: 2 }, - { label: '星期二', value: 3 }, - { label: '星期三', value: 4 }, - { label: '星期四', value: 5 }, - { label: '星期五', value: 6 }, - { label: '星期六', value: 7 }, + { label: prefixI18N('global.time.sun'), value: 1 }, + { label: prefixI18N('global.time.mon'), value: 2 }, + { label: prefixI18N('global.time.tues'), value: 3 }, + { label: prefixI18N('global.time.wednes'), value: 4 }, + { label: prefixI18N('global.time.thurs'), value: 5 }, + { label: prefixI18N('global.time.fri'), value: 6 }, + { label: prefixI18N('global.time.satur'), value: 7 }, ]; export interface ExecuteFormItemProps { @@ -55,6 +56,10 @@ export const ExecuteFormItem: FC<ExecuteFormItemProps> = ({ periodInput: isInput, onPeriodInputChange, }) => { + const t = useI18NPrefix( + 'main.pages.schedulePage.sidebar.editorPage.basicBaseForm.executeFormItem', + ); + const modeSelect = useMemo(() => { return ( <Form.Item name="periodUnit"> @@ -92,7 +97,7 @@ export const ExecuteFormItem: FC<ExecuteFormItemProps> = ({ case TimeModes.Minute: return ( <> - 每 + {t('per')} <Form.Item name="minute"> <Select options={minutePeriodOptions} style={{ width: 80 }} /> </Form.Item> @@ -102,20 +107,22 @@ export const ExecuteFormItem: FC<ExecuteFormItemProps> = ({ case TimeModes.Hour: return ( <> - 每 {modeSelect} 的{minuteSelect} + {t('per')} {modeSelect} {t('of')} + {minuteSelect} </> ); case TimeModes.Day: return ( <> - 每 {modeSelect} 的{hourSelect} + {t('per')} {modeSelect} {t('of')} + {hourSelect} {minuteSelect} </> ); case TimeModes.Week: return ( <> - 每 {modeSelect} 的{' '} + {t('per')} {modeSelect} {t('of')}{' '} <Form.Item name="weekDay"> <Select options={weekOptions} style={{ width: 80 }} /> </Form.Item> @@ -124,7 +131,7 @@ export const ExecuteFormItem: FC<ExecuteFormItemProps> = ({ case TimeModes.Month: return ( <> - 每 {modeSelect} 的 {daySelect} + {t('per')} {modeSelect} {t('of')} {daySelect} {hourSelect} {minuteSelect} </> @@ -132,7 +139,8 @@ export const ExecuteFormItem: FC<ExecuteFormItemProps> = ({ case TimeModes.Year: return ( <> - 每 {modeSelect}的 + {t('per')} {modeSelect} + {t('of')} <Form.Item name="month"> <Select options={monthOptions} style={{ width: 80 }} /> </Form.Item> @@ -144,23 +152,26 @@ export const ExecuteFormItem: FC<ExecuteFormItemProps> = ({ default: return; } - }, [timeMode, modeSelect, daySelect, minuteSelect, hourSelect]); + }, [timeMode, modeSelect, daySelect, minuteSelect, hourSelect, t]); return ( - <Form.Item label="执行时间设置" required> + <Form.Item label={t('executionTimeSetting')} required> <Space align="baseline"> {isInput ? ( <Form.Item name="cronExpression" - rules={[{ required: true, message: '表达式为必填项' }]} + rules={[{ required: true, message: t('expressionIsRequired') }]} > - <Input placeholder="请输入cron表达式" style={{ width: 300 }} /> + <Input + placeholder={t('pleaseEnterCronExpression')} + style={{ width: 300 }} + /> </Form.Item> ) : ( timeContent )} <Form.Item name="setCronExpressionManually" valuePropName="checked"> <Checkbox onChange={e => onPeriodInputChange(e.target.checked)}> - 手动输入 + {t('manualInput')} </Checkbox> </Form.Item> </Space> diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/BasicBaseForm/index.tsx b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/BasicBaseForm/index.tsx index b18078301..b915b67af 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/BasicBaseForm/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/BasicBaseForm/index.tsx @@ -1,8 +1,10 @@ import { DatePicker, Form, Input, Radio } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { FC, useCallback } from 'react'; import { JobTypes, JOB_TYPES_OPTIONS } from '../../constants'; import { checkScheduleName } from '../../services'; import { ExecuteFormItem, ExecuteFormItemProps } from './ExecuteFormItem'; + const { RangePicker } = DatePicker; interface BasicBaseFormProps extends ExecuteFormItemProps { @@ -19,6 +21,9 @@ export const BasicBaseForm: FC<BasicBaseFormProps> = ({ children, ...restProps }) => { + const t = useI18NPrefix( + 'main.pages.schedulePage.sidebar.editorPage.basicBaseForm.index', + ); const checkNameUnique = useCallback( async (_, name) => { if (!isAdd && initialName === name) { @@ -28,32 +33,32 @@ export const BasicBaseForm: FC<BasicBaseFormProps> = ({ return Promise.resolve(); } else { const res = await checkScheduleName(orgId, name); - return res ? Promise.resolve() : Promise.reject('名称已存在'); + return res ? Promise.resolve() : Promise.reject(t('nameAlreadyExists')); } }, - [orgId, isAdd, initialName], + [orgId, isAdd, initialName, t], ); return ( <> <Form.Item - label="名称" + label={t('name')} hasFeedback name="name" validateTrigger={'onBlur'} rules={[ - { required: true, message: '名称为必填项' }, + { required: true, message: t('nameRequired') }, { validator: checkNameUnique }, ]} > <Input autoComplete="new-name" /> </Form.Item> - <Form.Item label="类型" name="jobType"> + <Form.Item label={t('type')} name="jobType"> <Radio.Group options={JOB_TYPES_OPTIONS} onChange={e => onJobTypeChange(e.target.value)} /> </Form.Item> - <Form.Item label="有效时间范围" name={'dateRange'}> + <Form.Item label={t('effectiveTimeRange')} name={'dateRange'}> <RangePicker allowClear showTime format="YYYY-MM-DD HH:mm:ss" /> </Form.Item> <ExecuteFormItem {...restProps} /> diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/CommonRichText.tsx b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/CommonRichText.tsx index 8235350bd..a534f891b 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/CommonRichText.tsx +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/CommonRichText.tsx @@ -1,10 +1,10 @@ +import { prefixI18N } from 'app/hooks/useI18NPrefix'; import Quill from 'quill'; import { ImageDrop } from 'quill-image-drop-module'; import { FC } from 'react'; import ReactQuill from 'react-quill'; import 'react-quill/dist/quill.snow.css'; import styled from 'styled-components/macro'; - Quill.register('modules/imageDrop', ImageDrop); export const Formats = [ @@ -31,7 +31,9 @@ interface CommonRichTextProps { } export const CommonRichText: FC<CommonRichTextProps> = ({ children, - placeholder = '请输入', + placeholder = prefixI18N( + 'main.pages.schedulePage.sidebar.editorPage.emailSettingForm.commonRichText.pleaseEnter', + ), ...restProps }) => { return ( diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/MailTagFormItem.tsx b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/MailTagFormItem.tsx index 12b9d1e56..f0cca98b2 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/MailTagFormItem.tsx +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/MailTagFormItem.tsx @@ -1,5 +1,6 @@ import { SearchOutlined, UserOutlined } from '@ant-design/icons'; import { AutoComplete, Avatar, Input, Space, Tag } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; import debounce from 'lodash/debounce'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; @@ -25,6 +26,9 @@ export const MailTagFormItem: FC<MailTagFormItemProps> = ({ }) => { const [dataSource, setDataSource] = useState<IUserInfo[]>([]); const [keyword, setKeyword] = useState(''); + const t = useI18NPrefix( + 'main.pages.schedulePage.sidebar.editorPage.emailSettingForm.mailTagFormItem', + ); const emails = useMemo(() => { return value ? value.split(';').filter(v => !!v) : []; @@ -117,10 +121,7 @@ export const MailTagFormItem: FC<MailTagFormItemProps> = ({ onSelect={onSelectOrRemoveEmail} onBlur={() => onSearch('')} > - <Input - suffix={<SearchOutlined />} - placeholder="输入邮箱或姓名关键字查找..." - /> + <Input suffix={<SearchOutlined />} placeholder={t('placeholder')} /> </AutoComplete> </> ); diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/index.tsx b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/index.tsx index 06a29c222..d8c208fa4 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/EmailSettingForm/index.tsx @@ -1,5 +1,6 @@ import { DownCircleOutlined, UpCircleOutlined } from '@ant-design/icons'; import { Checkbox, Col, Form, Input, InputNumber, Row } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { FC, useMemo, useState } from 'react'; import { FileTypes, FILE_TYPE_OPTIONS } from '../../constants'; import { CommonRichText } from './CommonRichText'; @@ -14,7 +15,9 @@ export const EmailSettingForm: FC<EmailSettingFormProps> = ({ onFileTypeChange, }) => { const [showBcc, setShowBcc] = useState(false); - + const t = useI18NPrefix( + 'main.pages.schedulePage.sidebar.editorPage.emailSettingForm.index', + ); const hasImgeWidth = useMemo(() => { return fileType && fileType?.length > 0 ? fileType?.includes(FileTypes.Image) @@ -23,21 +26,21 @@ export const EmailSettingForm: FC<EmailSettingFormProps> = ({ const ccLabel = useMemo(() => { return ( <> - 抄送{' '} + {t('CC') + ' '} <span onClick={() => setShowBcc(!showBcc)}> {' '} {showBcc ? <UpCircleOutlined /> : <DownCircleOutlined />} </span> </> ); - }, [showBcc]); + }, [showBcc,t]); return ( <> <Form.Item - label="主题" + label={t('theme')} name="subject" - rules={[{ required: true, message: '主题为必填项' }]} + rules={[{ required: true, message: t('subjectIsRequired') }]} > <Input /> </Form.Item> @@ -45,7 +48,7 @@ export const EmailSettingForm: FC<EmailSettingFormProps> = ({ <Col span={15}> <Form.Item labelCol={{ span: 8 }} - label="文件类型" + label={t('fileType')} name="type" rules={[{ required: true }]} > @@ -59,22 +62,22 @@ export const EmailSettingForm: FC<EmailSettingFormProps> = ({ {hasImgeWidth ? ( <div className="image_width_form_item_wrapper"> <Form.Item - label="图片宽度" + label={t('picWidth')} labelCol={{ span: 10 }} name="imageWidth" rules={[{ required: true }]} > <InputNumber min={100} /> </Form.Item> - <span className="image_width_unit">像素</span> + <span className="image_width_unit">{t('px')}</span> </div> ) : null} </Col> </Row> <Form.Item - label="收件人" + label={t('recipient')} name="to" - rules={[{ required: true, message: '收件人为必填项' }]} + rules={[{ required: true, message: t('recipientIsRequired') }]} > <MailTagFormItem /> </Form.Item> @@ -82,11 +85,11 @@ export const EmailSettingForm: FC<EmailSettingFormProps> = ({ <MailTagFormItem /> </Form.Item> {showBcc ? ( - <Form.Item label="密送" name="bcc"> + <Form.Item label={t('bcc')} name="bcc"> <MailTagFormItem /> </Form.Item> ) : null} - <Form.Item label="邮件内容" validateFirst name="textContent"> + <Form.Item label={t('contentOfEmail')} validateFirst name="textContent"> <CommonRichText placeholder="This email comes from cron job on the datart." /> </Form.Item> </> diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/ScheduleErrorLog/index.tsx b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/ScheduleErrorLog/index.tsx index 19799f9ac..8aef600de 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/ScheduleErrorLog/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/ScheduleErrorLog/index.tsx @@ -1,4 +1,5 @@ import { Card, Table, TableColumnsType } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { FC, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; @@ -20,6 +21,9 @@ export const ScheduleErrorLog: FC<ScheduleErrorLogProps> = ({ scheduleId }) => { const logs = useSelector(selectScheduleLogs), loading = useSelector(selectScheduleLogsLoading); const { actions } = useScheduleSlice(); + const t = useI18NPrefix( + 'main.pages.schedulePage.sidebar.editorPage.scheduleErrorLog.index', + ); useEffect(() => { if (scheduleId) { dispatch(getScheduleErrorLogs({ scheduleId, count: 100 })); @@ -31,10 +35,10 @@ export const ScheduleErrorLog: FC<ScheduleErrorLogProps> = ({ scheduleId }) => { }, [scheduleId, dispatch]); const columns: TableColumnsType<ErrorLog> = useMemo(() => { return [ - { title: '开始时间', dataIndex: 'start', key: 'start' }, - { title: '结束时间', dataIndex: 'end', key: 'end' }, + { title: t('startTime'), dataIndex: 'start', key: 'start' }, + { title: t('endTime'), dataIndex: 'end', key: 'end' }, { - title: '日志阶段', + title: t('logPhase'), dataIndex: 'status', key: 'status', render(status: LogStatus) { @@ -42,19 +46,19 @@ export const ScheduleErrorLog: FC<ScheduleErrorLogProps> = ({ scheduleId }) => { }, }, { - title: '执行信息', + title: t('executionInformation'), dataIndex: 'message', key: 'message', render(text, record) { const isSuccess = record?.status === LogStatus.S15; - return isSuccess ? '成功' : text; + return isSuccess ? t('success') : text; }, }, ]; }, []); if (logs?.length > 0) { return ( - <FormCard title="日志"> + <FormCard title={t('log')}> <FormWrapper> <Table rowKey="id" diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/WeChartSetttingForm.tsx b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/WeChartSetttingForm.tsx index f5590d739..933d960f2 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/WeChartSetttingForm.tsx +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/WeChartSetttingForm.tsx @@ -1,14 +1,22 @@ import { Checkbox, Col, Form, Input, InputNumber, Row } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { FC } from 'react'; import { WECHART_FILE_TYPE_OPTIONS } from '../constants'; + interface WeChartSetttingFormProps {} export const WeChartSetttingForm: FC<WeChartSetttingFormProps> = ({}) => { + const t = useI18NPrefix( + 'main.pages.schedulePage.sidebar.editorPage.weChartSetttingForm', + ); + return ( <> <Form.Item - label="机器人webhook地址" + label={t('RobotWebhookAddress')} name="webHookUrl" - rules={[{ required: true, message: '机器人webhook地址为必填项' }]} + rules={[ + { required: true, message: t('RobotWebhookAddressIsRequired') }, + ]} > <Input /> </Form.Item> @@ -16,7 +24,7 @@ export const WeChartSetttingForm: FC<WeChartSetttingFormProps> = ({}) => { <Col span={12}> <Form.Item labelCol={{ span: 10 }} - label="文件类型" + label={t('fileType')} name="type" rules={[{ required: true }]} > @@ -26,14 +34,14 @@ export const WeChartSetttingForm: FC<WeChartSetttingFormProps> = ({}) => { <Col span={12}> <div className="image_width_form_item_wrapper"> <Form.Item - label="图片宽度" + label={t('picWidth')} labelCol={{ span: 10 }} name="imageWidth" rules={[{ required: true }]} > <InputNumber min={100} /> </Form.Item> - <span className="image_width_unit">像素</span> + <span className="image_width_unit">{t('px')}</span> </div> </Col> </Row> diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/index.tsx b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/index.tsx index ee1d00618..ffb453d52 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/EditorPage/index.tsx @@ -9,6 +9,7 @@ import { Tooltip, } from 'antd'; import { DetailPageHeader } from 'app/components/DetailPageHeader'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { getFolders } from 'app/pages/MainPage/pages/VizPage/slice/thunks'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -64,6 +65,7 @@ export const EditorPage: FC = () => { const deleteLoding = useSelector(selectDeleteLoading); const { toDetails } = useToScheduleDetails(); const isArchived = editingSchedule?.status === 0; + const t = useI18NPrefix('main.pages.schedulePage.sidebar.editorPage.index'); const { actions } = useScheduleSlice(); const { scheduleId, orgId } = params; @@ -79,7 +81,7 @@ export const EditorPage: FC = () => { const onFinish = useCallback(() => { form.validateFields().then((values: FormValues) => { if (!(values?.folderContent && values?.folderContent?.length > 0)) { - message.error('请勾选发送内容'); + message.error(t('tickToSendContent')); return; } const params = toScheduleSubmitParams(values, orgId); @@ -88,7 +90,7 @@ export const EditorPage: FC = () => { addSchedule({ params, resolve: (id: string) => { - message.success('新增成功'); + message.success(t('addSuccess')); toDetails(orgId, id); refreshScheduleList(); }, @@ -100,7 +102,7 @@ export const EditorPage: FC = () => { scheduleId: editingSchedule?.id as string, params: { ...params, id: editingSchedule?.id as string }, resolve: () => { - message.success('保存成功'); + message.success(t('saveSuccess')); refreshScheduleList(); }, }), @@ -115,6 +117,7 @@ export const EditorPage: FC = () => { editingSchedule, refreshScheduleList, toDetails, + t, ]); const onResetForm = useCallback(() => { @@ -190,12 +193,12 @@ export const EditorPage: FC = () => { unarchiveSchedule({ id: editingSchedule!.id, resolve: () => { - message.success('还原成功'); + message.success(t('restoredSuccess')); toDetails(orgId); }, }), ); - }, [dispatch, toDetails, orgId, editingSchedule]); + }, [dispatch, toDetails, orgId, editingSchedule, t]); const del = useCallback( archive => () => { @@ -204,13 +207,15 @@ export const EditorPage: FC = () => { id: editingSchedule!.id, archive, resolve: () => { - message.success(`成功${archive ? '移至回收站' : '删除'}`); + message.success( + `${t('success')}${archive ? t('moveToTrash') : t('delete')}`, + ); toDetails(orgId); }, }), ); }, - [dispatch, toDetails, orgId, editingSchedule], + [dispatch, toDetails, orgId, editingSchedule, t], ); useEffect(() => { @@ -234,16 +239,16 @@ export const EditorPage: FC = () => { <Container ref={setContainer}> <Affix offsetTop={0} target={() => container}> <DetailPageHeader - title={isAdd ? '新建定时任务' : editingSchedule?.name} + title={isAdd ? t('newTimedTask') : editingSchedule?.name} actions={ isArchived ? ( <> - <Popconfirm title="确定还原?" onConfirm={unarchive}> - <Button loading={unarchiveLoading}>还原</Button> + <Popconfirm title={t('sureToRestore')} onConfirm={unarchive}> + <Button loading={unarchiveLoading}>{t('restore')}</Button> </Popconfirm> - <Popconfirm title="确定删除?" onConfirm={del(false)}> + <Popconfirm title={t('sureToDelete')} onConfirm={del(false)}> <Button loading={deleteLoding} danger> - 删除 + {t('delete')} </Button> </Popconfirm> </> @@ -251,7 +256,7 @@ export const EditorPage: FC = () => { <> <Tooltip placement="bottom" - title={active ? '停止后允许修改' : ''} + title={active ? t('allowModificationAfterStopping') : ''} > <Button loading={saveLoding} @@ -259,17 +264,20 @@ export const EditorPage: FC = () => { onClick={form.submit} disabled={active} > - 保存 + {t('save')} </Button> </Tooltip> {!isAdd && ( <Tooltip placement="bottom" - title={active ? '停止后允许移至回收站' : ''} + title={active ? t('allowMoveAfterStopping') : ''} > - <Popconfirm title="确定移至回收站?" onConfirm={del(true)}> + <Popconfirm + title={t('sureMoveRecycleBin')} + onConfirm={del(true)} + > <Button loading={deleteLoding} disabled={active} danger> - 移至回收站 + {t('basicSettings')} </Button> </Popconfirm> </Tooltip> @@ -293,7 +301,7 @@ export const EditorPage: FC = () => { {!isAdd && editingSchedule?.id ? ( <ScheduleErrorLog scheduleId={editingSchedule?.id} /> ) : null} - <FormCard title="基本设置"> + <FormCard title={t('basicSettings')}> <FormWrapper> <BasicBaseForm isAdd={isAdd} @@ -309,7 +317,7 @@ export const EditorPage: FC = () => { </FormCard> {jobType === JobTypes.Email ? ( - <FormCard title="邮件设置"> + <FormCard title={t('emailSetting')}> <FormWrapper> <EmailSettingForm fileType={fileType} @@ -318,13 +326,13 @@ export const EditorPage: FC = () => { </FormWrapper> </FormCard> ) : ( - <FormCard title="企业微信设置"> + <FormCard title={t('enterpriseWeChatSettings')}> <FormWrapper> <WeChartSetttingForm /> </FormWrapper> </FormCard> )} - <FormCard title="发送内容设置"> + <FormCard title={t('sendContentSettings')}> <FormWrapper> <SendContentForm /> </FormWrapper> diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/Sidebar/ScheduleList.tsx b/frontend/src/app/pages/MainPage/pages/SchedulePage/Sidebar/ScheduleList.tsx index d8eb241f6..45981a991 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/Sidebar/ScheduleList.tsx +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/Sidebar/ScheduleList.tsx @@ -6,6 +6,7 @@ import { } from '@ant-design/icons'; import { ButtonProps, List, message, Popconfirm, Space, Tooltip } from 'antd'; import { IW, ListItem, ToolbarButton } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import classnames from 'classnames'; import { FC, memo, ReactNode, useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -55,6 +56,8 @@ const Operations: FC<OperationsProps> = ({ const [executeLoading, setExecuteLoading] = useState(false); const { actions } = useScheduleSlice(); const dispatch = useDispatch(); + const t = useI18NPrefix('main.pages.schedulePage.sidebar.scheduleList'); + const updateScheduleActive = useCallback( (active: boolean) => { dispatch(actions.setEditingScheduleActive(active)); @@ -66,7 +69,7 @@ const Operations: FC<OperationsProps> = ({ startSchedule(scheduleId) .then(res => { if (!!res) { - message.success('启动成功'); + message.success(t('successStarted')); onUpdateScheduleList(); if (editingId === scheduleId) { updateScheduleActive(true); @@ -76,13 +79,13 @@ const Operations: FC<OperationsProps> = ({ .finally(() => { setStartLoading(false); }); - }, [scheduleId, editingId, onUpdateScheduleList, updateScheduleActive]); + }, [scheduleId, editingId, onUpdateScheduleList, updateScheduleActive, t]); const handleStopSchedule = useCallback(() => { setStopLoading(true); stopSchedule(scheduleId) .then(res => { if (!!res) { - message.success('停止成功'); + message.success(t('successStop')); onUpdateScheduleList(); if (editingId === scheduleId) { updateScheduleActive(false); @@ -92,26 +95,26 @@ const Operations: FC<OperationsProps> = ({ .finally(() => { setStopLoading(false); }); - }, [scheduleId, onUpdateScheduleList, editingId, updateScheduleActive]); + }, [scheduleId, onUpdateScheduleList, editingId, updateScheduleActive, t]); const handleExecuteSchedule = useCallback(() => { setExecuteLoading(true); executeSchedule(scheduleId) .then(res => { if (!!res) { - message.success('立即执行成功'); + message.success(t('successImmediately')); onUpdateScheduleList(); } }) .finally(() => { setExecuteLoading(false); }); - }, [scheduleId, onUpdateScheduleList]); + }, [scheduleId, onUpdateScheduleList, t]); return ( <Space onClick={stopPPG}> {!active ? ( <OperationButton - tooltipTitle="启动" + tooltipTitle={t('start')} loading={startLoading} icon={<CaretRightOutlined />} onClick={handleStartSchedule} @@ -119,7 +122,7 @@ const Operations: FC<OperationsProps> = ({ ) : null} {active ? ( <OperationButton - tooltipTitle="停止" + tooltipTitle={t('stop')} loading={stopLoading} icon={<PauseOutlined />} onClick={handleStopSchedule} @@ -127,13 +130,13 @@ const Operations: FC<OperationsProps> = ({ ) : null} <Popconfirm - title="确定立即执行?" + title={t('executeItNow')} okButtonProps={{ loading: executeLoading }} onConfirm={handleExecuteSchedule} > <OperationButton loading={executeLoading} - tooltipTitle="立即执行" + tooltipTitle={t('executeImmediately')} icon={ <SendOutlined rotate={-45} diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/Sidebar/index.tsx b/frontend/src/app/pages/MainPage/pages/SchedulePage/Sidebar/index.tsx index c69185165..227e244e9 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/Sidebar/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/Sidebar/index.tsx @@ -1,6 +1,7 @@ import { DeleteOutlined } from '@ant-design/icons'; import { ListNav, ListPane, ListTitle } from 'app/components'; import { useDebouncedSearch } from 'app/hooks/useDebouncedSearch'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { useAccess } from 'app/pages/MainPage/Access'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { memo, useCallback, useMemo } from 'react'; @@ -23,7 +24,7 @@ export const Sidebar = memo(() => { const list = useSelector(selectSchedules); const archived = useSelector(selectArchived); const allowCreate = useAccess(allowCreateSchedule()); - + const t = useI18NPrefix('main.pages.schedulePage.sidebar.index'); const { filteredData: scheduleList, debouncedSearch: listSearch } = useDebouncedSearch(list, (keywords, d) => d.name.toLowerCase().includes(keywords.toLowerCase()), @@ -50,12 +51,12 @@ export const Sidebar = memo(() => { () => [ { key: 'list', - title: '定时任务列表', + title: t('scheduledTaskList'), search: true, onSearch: listSearch, ...allowCreate({ add: { - items: [{ key: 'add', text: '新建定时任务' }], + items: [{ key: 'add', text: t('newTimedTask') }], callback: toAdd, }, }), @@ -63,7 +64,7 @@ export const Sidebar = memo(() => { items: [ { key: 'recycle', - text: '回收站', + text: t('recycle'), prefix: <DeleteOutlined className="icon" />, }, ], @@ -72,13 +73,13 @@ export const Sidebar = memo(() => { }, { key: 'recycle', - title: '回收站', + title: t('recycle'), back: true, search: true, onSearch: archivedSearch, }, ], - [toAdd, moreMenuClick, listSearch, archivedSearch, allowCreate], + [toAdd, moreMenuClick, listSearch, archivedSearch, allowCreate, t], ); return ( diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/constants.ts b/frontend/src/app/pages/MainPage/pages/SchedulePage/constants.ts index a62e2cddb..f2f41d921 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/constants.ts +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/constants.ts @@ -1,4 +1,6 @@ +import { prefixI18N } from 'app/hooks/useI18NPrefix'; import { FormValues } from './types'; +const Prefix = 'main.pages.schedulePage.constants.'; export enum JobTypes { Email = 'EMAIL', WeChart = 'WECHART', @@ -9,15 +11,15 @@ export enum FileTypes { Image = 'IMAGE', } export const JOB_TYPES_OPTIONS = [ - { label: '邮箱', value: JobTypes.Email }, - { label: '微信', value: JobTypes.WeChart }, + { label: prefixI18N(Prefix + 'email'), value: JobTypes.Email }, + { label: prefixI18N(Prefix + 'weChat'), value: JobTypes.WeChart }, ]; export const FILE_TYPE_OPTIONS = [ - // { label: 'Excel', value: FileTypes.Excel }, - { label: '图片', value: FileTypes.Image }, + { label: 'Excel', value: FileTypes.Excel }, + { label: prefixI18N(Prefix + 'picture'), value: FileTypes.Image }, ]; export const WECHART_FILE_TYPE_OPTIONS = [ - { label: '图片', value: FileTypes.Image }, + { label: prefixI18N(Prefix + 'picture'), value: FileTypes.Image }, ]; export enum TimeModes { @@ -79,12 +81,12 @@ export enum LogStatus { } export const LOG_STATUS_TEXT = { - [LogStatus.S1]: '任务执行', - [LogStatus.S3]: '配置解析', - [LogStatus.S7]: '数据获取', - [LogStatus.S15]: '发送', + [LogStatus.S1]: prefixI18N(Prefix + 'taskExecution'), + [LogStatus.S3]: prefixI18N(Prefix + 'configurationAnalysis'), + [LogStatus.S7]: prefixI18N(Prefix + 'getData'), + [LogStatus.S15]: prefixI18N(Prefix + 'send'), }; export const JOB_TYPE_TEXT = { - [JobTypes.Email]: '邮箱', - [JobTypes.WeChart]: '微信', + [JobTypes.Email]: prefixI18N(Prefix + 'email'), + [JobTypes.WeChart]: prefixI18N(Prefix + 'weChat'), }; diff --git a/frontend/src/app/pages/MainPage/pages/SchedulePage/utils.ts b/frontend/src/app/pages/MainPage/pages/SchedulePage/utils.ts index 0e9183e23..46b6db614 100644 --- a/frontend/src/app/pages/MainPage/pages/SchedulePage/utils.ts +++ b/frontend/src/app/pages/MainPage/pages/SchedulePage/utils.ts @@ -143,6 +143,7 @@ export const toEchoFormValues = ({ cc: configObj?.cc || '', bcc: configObj?.bcc || '', type: configObj?.attachments || [], + webHookUrl: configObj?.webHookUrl || '', demoContent, folderContent, setCronExpressionManually, diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/Recycle.tsx b/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/Recycle.tsx index bb66d2646..355affc69 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/Recycle.tsx +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/Recycle.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 { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import classnames from 'classnames'; import { memo, useCallback, useEffect } from 'react'; diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/SourceList.tsx b/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/SourceList.tsx index a7436acc7..60ee0596c 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/SourceList.tsx +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/SourceList.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 { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import classnames from 'classnames'; import React, { memo, useCallback, useEffect } from 'react'; diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/index.tsx b/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/index.tsx index 0adba6bed..b1aa8b3ff 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/Sidebar/index.tsx @@ -1,6 +1,25 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { DeleteOutlined } from '@ant-design/icons'; import { ListNav, ListPane, ListTitle } from 'app/components'; import { useDebouncedSearch } from 'app/hooks/useDebouncedSearch'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { useAccess } from 'app/pages/MainPage/Access'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { memo, useCallback, useMemo } from 'react'; @@ -33,6 +52,7 @@ export const Sidebar = memo(() => { '/organizations/:orgId/sources/:sourceId', ); const sourceId = matchSourceDetail?.params.sourceId; + const t = useI18NPrefix('source.sidebar'); const allowCreate = useAccess(allowCreateSource()); const { filteredData: sourceList, debouncedSearch: listSearch } = @@ -60,17 +80,17 @@ export const Sidebar = memo(() => { () => [ { key: 'list', - title: '数据源列表', + title: t('title'), search: true, onSearch: listSearch, ...allowCreate({ - add: { items: [{ key: 'add', text: '新建数据源' }], callback: toAdd }, + add: { items: [{ key: 'add', text: t('add') }], callback: toAdd }, }), more: { items: [ { key: 'recycle', - text: '回收站', + text: t('recycle'), prefix: <DeleteOutlined className="icon" />, }, ], @@ -79,13 +99,13 @@ export const Sidebar = memo(() => { }, { key: 'recycle', - title: '回收站', + title: t('recycle'), back: true, search: true, onSearch: archivedSearch, }, ], - [toAdd, moreMenuClick, listSearch, archivedSearch, allowCreate], + [toAdd, moreMenuClick, listSearch, archivedSearch, allowCreate, t], ); return ( diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/ArrayConfig.tsx b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/ArrayConfig.tsx index a450c1a4e..893758dfa 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/ArrayConfig.tsx +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/ArrayConfig.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 { PlusOutlined } from '@ant-design/icons'; import { Button, @@ -7,6 +25,7 @@ import { Table, TableColumnProps, } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ModalForm } from 'app/components'; import { Model, @@ -54,6 +73,8 @@ export function ArrayConfig({ const [editingRowKey, setEditingRowKey] = useState(''); const [schemaDataSource, setSchemaDataSource] = useState<object[]>([]); const formRef = useRef<FormInstance<SourceFormModel>>(); + const t = useI18NPrefix('source'); + const tg = useI18NPrefix('global'); const showForm = useCallback(() => { setFormVisible(true); @@ -174,39 +195,39 @@ export function ArrayConfig({ () => [ { title: attr.key, dataIndex: attr.key }, { - title: '操作', + title: tg('title.action'), align: 'center', width: 120, render: (_, record) => ( <Space> <ActionButton - key="view" + key="edit" type="link" onClick={editConfig(record[attr.key!])} > - 查看 + {tg('button.edit')} </ActionButton> {allowManage && ( <Popconfirm key="del" - title="确认删除?" + title={tg('operation.deleteConfirm')} onConfirm={delConfig(record[attr.key!])} > - <ActionButton type="link">删除</ActionButton> + <ActionButton type="link">{tg('button.delete')}</ActionButton> </Popconfirm> )} </Space> ), }, ], - [attr, editConfig, delConfig, allowManage], + [attr, editConfig, delConfig, allowManage, tg], ); return ( <Wrapper> {allowManage && !disabled && ( <AddButton type="link" icon={<PlusOutlined />} onClick={showForm}> - 新增配置 + {t('form.addConfig')} </AddButton> )} <Table @@ -218,7 +239,7 @@ export function ArrayConfig({ bordered /> <ModalForm - title={`${attr.name}配置编辑`} + title={t('form.editConfig')} visible={formVisible} width={SPACE_TIMES(240)} formProps={{ diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/FileUpload.tsx b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/FileUpload.tsx index 692da232a..6ec49ba02 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/FileUpload.tsx +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/FileUpload.tsx @@ -1,5 +1,24 @@ +/** + * 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 { UploadOutlined } from '@ant-design/icons'; import { Button, Form, FormInstance, Input, Upload } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { BASE_API_URL } from 'globalConstants'; import { useCallback, useState } from 'react'; import { APIResponse } from 'types'; @@ -19,6 +38,7 @@ export function FileUpload({ onTest, }: FileUploadProps) { const [uploadFileLoading, setUploadFileLoading] = useState(false); + const t = useI18NPrefix('source'); const normFile = useCallback(e => { if (Array.isArray(e)) { @@ -50,7 +70,7 @@ export function FileUpload({ return ( <> <Form.Item - label="文件" + label={t('form.file')} valuePropName="fileList" getValueFromEvent={normFile} > @@ -66,7 +86,7 @@ export function FileUpload({ icon={<UploadOutlined />} loading={uploadFileLoading || loading} > - 选择文件 + {t('form.selectFile')} </Button> </Upload> </Form.Item> diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/Properties.tsx b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/Properties.tsx index 3b91d29c8..1f35c7308 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/Properties.tsx +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/Properties.tsx @@ -1,10 +1,29 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { ProColumnType } from '@ant-design/pro-table'; import { Button } from 'antd'; import { Configuration } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { LINE_HEIGHT_BODY } from 'styles/StyleConstants'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; interface Property { key: string; @@ -28,6 +47,9 @@ export function Properties({ allowManage, onChange, }: PropertiesProps) { + const t = useI18NPrefix('source'); + const tg = useI18NPrefix('global'); + const tableDataSource = useMemo( () => valueProp @@ -57,7 +79,7 @@ export function Properties({ dataIndex: 'key', formItemProps: (_, { rowIndex }) => ({ rules: [ - { required: true, message: 'Key不能为空' }, + { required: true, message: `Key${tg('validation.required')}` }, { validator: (_, val) => { if (!valueProp) { @@ -70,9 +92,8 @@ export function Properties({ ) { return Promise.resolve(); } - return Promise.reject(new Error('Key重复')); + return Promise.reject(new Error(t('form.duplicateKey'))); }, - message: 'Key不能重复', }, ], }), @@ -82,7 +103,7 @@ export function Properties({ dataIndex: 'value', }, { - title: '操作', + title: tg('title.action'), valueType: 'option', width: 160, render: (_, record, __, action) => [ @@ -94,7 +115,7 @@ export function Properties({ action?.startEditable?.(record.id); }} > - 编辑 + {tg('button.edit')} </ActionButton>, <ActionButton key="delete" @@ -112,17 +133,17 @@ export function Properties({ ); }} > - 删除 + {tg('button.delete')} </ActionButton>, ], }, ], - [disabled, allowManage, valueProp, tableDataSource, onChange], + [disabled, allowManage, valueProp, tableDataSource, onChange, t, tg], ); return ( <Configuration columns={columns} - creatorButtonText="新增配置项" + creatorButtonText={t('form.addProperty')} value={tableDataSource} disabled={disabled || !allowManage} onChange={change} diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/SchemaComponent.tsx b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/SchemaComponent.tsx index 555a5730b..9e167ac55 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/SchemaComponent.tsx +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/SchemaComponent.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 { Empty } from 'antd'; import { SchemaTable } from 'app/pages/MainPage/pages/ViewPage/components/SchemaTable'; import { Schema } from 'app/pages/MainPage/pages/ViewPage/slice/types'; @@ -31,6 +49,7 @@ export function SchemaComponent({ value, dataSource, onChange }: SchemaProps) { return value ? ( <SchemaTable + width={600} height={400} model={model} dataSource={dataSource} diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/index.tsx b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/index.tsx index 47fab41f2..24bb898b9 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/ConfigComponent/index.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 { Button, Form, @@ -8,6 +26,7 @@ import { Select, Switch, } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { QueryResult } from 'app/pages/MainPage/pages/ViewPage/slice/types'; import { DataProviderAttribute } from 'app/pages/MainPage/slice/types'; import { Rule } from 'rc-field-form/lib/interface'; @@ -52,6 +71,8 @@ export function ConfigComponent({ const { name, description, required, defaultValue, type, options } = attr; let component: ReactElement | null = null; let extraFormItemProps: Partial<FormItemProps> = {}; + const t = useI18NPrefix('source'); + const tg = useI18NPrefix('global'); switch (name) { case 'url': @@ -64,7 +85,7 @@ export function ConfigComponent({ loading={testLoading} onClick={onTest} > - 测试连接 + {t('form.test')} </Button> } disabled={disabled} @@ -170,7 +191,10 @@ export function ConfigComponent({ let rules: Rule[] = []; if (required) { - rules.push({ required: true, message: `${name}不能为空` }); + rules.push({ + required: true, + message: `${name}${tg('validation.required')}`, + }); } if (subFormRowKey === name) { @@ -179,7 +203,7 @@ export function ConfigComponent({ const valid = subFormRowKeyValidator && subFormRowKeyValidator(value); return valid ? Promise.resolve() - : Promise.reject(new Error('名称重复')); + : Promise.reject(new Error(t('form.duplicateName'))); }, }); } diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/index.tsx b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/index.tsx index b5af995ea..ad6a0ae22 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/index.tsx @@ -1,22 +1,29 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { LoadingOutlined } from '@ant-design/icons'; -import { - Button, - Card, - Form, - Input, - message, - Popconfirm, - Select, - Space, -} from 'antd'; +import { Button, Card, Form, Input, message, Popconfirm, Select } from 'antd'; +import { Authorized, EmptyFiller } from 'app/components'; import { DetailPageHeader } from 'app/components/DetailPageHeader'; -import { Access, useAccess } from 'app/pages/MainPage/Access'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import { useAccess } from 'app/pages/MainPage/Access'; import debounce from 'debounce-promise'; -import { - CommonFormTypes, - COMMON_FORM_TITLE_PREFIX, - DEFAULT_DEBOUNCE_WAIT, -} from 'globalConstants'; +import { CommonFormTypes, DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory, useRouteMatch } from 'react-router-dom'; @@ -52,7 +59,7 @@ import { unarchiveSource, } from '../slice/thunks'; import { Source, SourceFormModel } from '../slice/types'; -import { allowManageSource } from '../utils'; +import { allowCreateSource, allowManageSource } from '../utils'; import { ConfigComponent } from './ConfigComponent'; export function SourceDetailPage() { @@ -75,8 +82,13 @@ export function SourceDetailPage() { const { params } = useRouteMatch<{ sourceId: string }>(); const { sourceId } = params; const [form] = Form.useForm<SourceFormModel>(); + const t = useI18NPrefix('source'); + const tg = useI18NPrefix('global'); const isArchived = editingSource?.status === 0; - const allowManage = useAccess(allowManageSource(editingSource?.id)); + const allowCreate = + useAccess(allowCreateSource())(true) && sourceId === 'add'; + const allowManage = + useAccess(allowManageSource(sourceId))(true) && sourceId !== 'add'; const config = useMemo( () => dataProviders[providerType]?.config, @@ -106,11 +118,11 @@ export function SourceDetailPage() { setProviderType(type); form.setFieldsValue({ name, type, config: JSON.parse(config) }); } catch (error) { - message.error('配置解析错误'); + message.error(tg('operation.parseError')); throw error; } } - }, [form, editingSource]); + }, [form, editingSource, tg]); useEffect(() => { if ( @@ -172,14 +184,14 @@ export function SourceDetailPage() { method: 'POST', data: { name, type, properties: config }, }); - message.success('测试成功'); + message.success(t('testSuccess')); } catch (error) { errorHandle(error); throw error; } finally { setTestLoading(false); } - }, [form, dataProviders]); + }, [form, dataProviders, t]); const subFormTest = useCallback( async (config, callback) => { @@ -228,7 +240,7 @@ export function SourceDetailPage() { addSource({ source: { ...rest, orgId, config: configStr }, resolve: id => { - message.success('新建成功'); + message.success(t('createSuccess')); history.push(`/organizations/${orgId}/sources/${id}`); }, }), @@ -244,7 +256,7 @@ export function SourceDetailPage() { config: configStr, }, resolve: () => { - message.success('修改成功'); + message.success(tg('operation.updateSuccess')); }, }), ); @@ -253,7 +265,7 @@ export function SourceDetailPage() { break; } }, - [dispatch, history, orgId, editingSource, formType], + [dispatch, history, orgId, editingSource, formType, t, tg], ); const del = useCallback( @@ -263,13 +275,17 @@ export function SourceDetailPage() { id: editingSource!.id, archive, resolve: () => { - message.success(`成功${archive ? '移至回收站' : '删除'}`); + message.success( + archive + ? tg('operation.archiveSuccess') + : tg('operation.deleteSuccess'), + ); history.replace(`/organizations/${orgId}/sources`); }, }), ); }, - [dispatch, history, orgId, editingSource], + [dispatch, history, orgId, editingSource, tg], ); const unarchive = useCallback(() => { @@ -277,130 +293,153 @@ export function SourceDetailPage() { unarchiveSource({ id: editingSource!.id, resolve: () => { - message.success('还原成功'); + message.success(tg('operation.restoreSuccess')); history.replace(`/organizations/${orgId}/sources`); }, }), ); - }, [dispatch, history, orgId, editingSource]); + }, [dispatch, history, orgId, editingSource, tg]); const titleLabelPrefix = useMemo( - () => (isArchived ? '已归档' : COMMON_FORM_TITLE_PREFIX[formType]), - [isArchived, formType], + () => (isArchived ? t('archived') : tg(`modal.title.${formType}`)), + [isArchived, formType, t, tg], ); return ( - <Wrapper> - <DetailPageHeader - title={`${titleLabelPrefix}数据源`} - actions={ - <Access {...allowManageSource(editingSource?.id)}> - {!isArchived ? ( - <Space> + <Authorized + authority={allowCreate || allowManage} + denied={<EmptyFiller title={t('noPermission')} />} + > + <Wrapper> + <DetailPageHeader + title={`${titleLabelPrefix}${t('source')}`} + actions={ + !isArchived ? ( + <> <Button type="primary" loading={saveSourceLoading} onClick={form.submit} > - 保存 + {tg('button.save')} </Button> {formType === CommonFormTypes.Edit && ( - <Popconfirm title="确定移至回收站?" onConfirm={del(true)}> + <Popconfirm + title={tg('operation.archiveConfirm')} + onConfirm={del(true)} + > <Button loading={deleteSourceLoading} danger> - 移至回收站 + {tg('button.archive')} </Button> </Popconfirm> )} - </Space> + </> ) : ( - <Space> - <Popconfirm title="确定还原?" onConfirm={unarchive}> - <Button loading={unarchiveSourceLoading}>还原</Button> + <> + <Popconfirm + title={tg('operation.restoreConfirm')} + onConfirm={unarchive} + > + <Button loading={unarchiveSourceLoading}> + {tg('button.restore')} + </Button> </Popconfirm> - <Popconfirm title="确认删除?" onConfirm={del(false)}> + <Popconfirm + title={tg('operation.deleteConfirm')} + onConfirm={del(false)} + > <Button loading={deleteSourceLoading} danger> - 删除 + {tg('button.delete')} </Button> </Popconfirm> - </Space> - )} - </Access> - } - /> - <Content> - <Card bordered={false}> - <Form - name="source_form_" - className="detailForm" - form={form} - labelAlign="left" - labelCol={{ offset: 1, span: 5 }} - wrapperCol={{ span: 8 }} - onFinish={formSubmit} - > - <Form.Item - name="name" - label="名称" - validateFirst - rules={[ - { required: true, message: '名称不能为空' }, - { - validator: debounce((_, value) => { - if (value === editingSource?.name) { - return Promise.resolve(); - } - return request({ - url: `/sources/check/name`, - method: 'POST', - params: { name: value, orgId }, - }).then( - () => Promise.resolve(), - () => Promise.reject(new Error('名称重复')), - ); - }, DEFAULT_DEBOUNCE_WAIT), - }, - ]} + </> + ) + } + /> + <Content> + <Card bordered={false}> + <Form + name="source_form_" + className="detailForm" + form={form} + labelAlign="left" + labelCol={{ offset: 1, span: 5 }} + wrapperCol={{ span: 8 }} + onFinish={formSubmit} > - <Input disabled={isArchived} /> - </Form.Item> - <Form.Item - name="type" - label="类型" - rules={[{ required: true, message: '类型为必选项' }]} - > - <Select - loading={dataProviderListLoading} - disabled={isArchived} - onChange={dataProviderChange} + <Form.Item + name="name" + label={t('form.name')} + validateFirst + rules={[ + { + required: true, + message: `${t('form.name')}${tg('validation.required')}`, + }, + { + validator: debounce((_, value) => { + if (value === editingSource?.name) { + return Promise.resolve(); + } + return request({ + url: `/sources/check/name`, + method: 'POST', + params: { name: value, orgId }, + }).then( + () => Promise.resolve(), + err => + Promise.reject(new Error(err.response.data.message)), + ); + }, DEFAULT_DEBOUNCE_WAIT), + }, + ]} > - {Object.keys(dataProviders).map(key => ( - <Select.Option key={key} value={key}> - {key} - </Select.Option> - ))} - </Select> - </Form.Item> - {dataProviderConfigTemplateLoading && <LoadingOutlined />} - - {(providerType !== 'FILE' || formType === CommonFormTypes.Edit) && - config?.attributes.map(attr => ( - <ConfigComponent - key={`${providerType}_${attr.name}`} - attr={attr} - form={form} - sourceId={editingSource?.id} - testLoading={testLoading} + <Input disabled={isArchived} /> + </Form.Item> + <Form.Item + name="type" + label={t('type')} + rules={[ + { + required: true, + message: `${t('type')}${tg('validation.required')}`, + }, + ]} + > + <Select + loading={dataProviderListLoading} disabled={isArchived} - allowManage={allowManage(true)} - onTest={test} - onSubFormTest={subFormTest} - onDbTypeChange={dbTypeChange} - /> - ))} - </Form> - </Card> - </Content> - </Wrapper> + onChange={dataProviderChange} + > + {Object.keys(dataProviders).map(key => ( + <Select.Option key={key} value={key}> + {key} + </Select.Option> + ))} + </Select> + </Form.Item> + {dataProviderConfigTemplateLoading && <LoadingOutlined />} + + {(providerType !== 'FILE' || formType === CommonFormTypes.Edit) && + config?.attributes.map(attr => ( + <ConfigComponent + key={`${providerType}_${attr.name}`} + attr={attr} + form={form} + sourceId={editingSource?.id} + testLoading={testLoading} + disabled={isArchived} + allowManage={allowManage} + onTest={test} + onSubFormTest={subFormTest} + onDbTypeChange={dbTypeChange} + /> + ))} + </Form> + </Card> + </Content> + </Wrapper> + </Authorized> ); } diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/types.ts b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/types.ts index 579dbd636..9696829e2 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/types.ts +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/SourceDetailPage/types.ts @@ -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 { Schema } from 'app/pages/MainPage/pages/ViewPage/slice/types'; export interface HttpSourceConfig { diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/index.tsx b/frontend/src/app/pages/MainPage/pages/SourcePage/index.tsx index 0844572c4..627549721 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/index.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 React from 'react'; import { Route } from 'react-router-dom'; import styled from 'styled-components/macro'; diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/index.ts b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/index.ts index 56591ed6d..385515a3a 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/index.ts +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/index.ts @@ -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 { createSlice } from '@reduxjs/toolkit'; import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; import { diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/selectors.ts b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/selectors.ts index 151053372..04effa454 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/selectors.ts +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/selectors.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'types'; import { initialState } from '.'; diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/thunks.ts b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/thunks.ts index f58c83693..51f591dc6 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/thunks.ts +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/thunks.ts @@ -1,4 +1,25 @@ +/** + * 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 { createAsyncThunk } from '@reduxjs/toolkit'; +import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; +import { getLoggedInUserPermissions } from 'app/pages/MainPage/slice/thunks'; +import { RootState } from 'types'; import { request } from 'utils/request'; import { errorHandle } from 'utils/utils'; import { @@ -56,23 +77,29 @@ export const getSource = createAsyncThunk<Source, string>( }, ); -export const addSource = createAsyncThunk<Source, AddSourceParams>( - 'source/addSource', - async ({ source, resolve }) => { - try { - const { data } = await request<Source>({ - url: '/sources', - method: 'POST', - data: source, - }); - resolve(data.id); - return data; - } catch (error) { - errorHandle(error); - throw error; - } - }, -); +export const addSource = createAsyncThunk< + Source, + AddSourceParams, + { state: RootState } +>('source/addSource', async ({ source, resolve }, { getState, dispatch }) => { + try { + const { data } = await request<Source>({ + url: '/sources', + method: 'POST', + data: source, + }); + + // FIXME 拥有Read权限等级的扁平结构资源新增后需要更新权限字典;后续如改造为目录结构则删除该逻辑 + const orgId = selectOrgId(getState()); + await dispatch(getLoggedInUserPermissions(orgId)); + + resolve(data.id); + return data; + } catch (error) { + errorHandle(error); + throw error; + } +}); export const editSource = createAsyncThunk<Source, EditSourceParams>( 'source/editSource', diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/types.ts b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/types.ts index 250efd7b1..d43f4a481 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/slice/types.ts +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/slice/types.ts @@ -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. + */ + export interface SourceState { sources: Source[]; archived: Source[]; diff --git a/frontend/src/app/pages/MainPage/pages/SourcePage/utils.ts b/frontend/src/app/pages/MainPage/pages/SourcePage/utils.ts index 550d04ae7..d2bcae9a5 100644 --- a/frontend/src/app/pages/MainPage/pages/SourcePage/utils.ts +++ b/frontend/src/app/pages/MainPage/pages/SourcePage/utils.ts @@ -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 { PermissionLevels, ResourceTypes } from '../PermissionPage/constants'; export function allowCreateSource() { diff --git a/frontend/src/app/pages/MainPage/pages/VariablePage/DefaultValue.tsx b/frontend/src/app/pages/MainPage/pages/VariablePage/DefaultValue.tsx index 53e286430..58ed14c29 100644 --- a/frontend/src/app/pages/MainPage/pages/VariablePage/DefaultValue.tsx +++ b/frontend/src/app/pages/MainPage/pages/VariablePage/DefaultValue.tsx @@ -1,5 +1,24 @@ +/** + * 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 { CheckOutlined } from '@ant-design/icons'; import { Button, DatePicker, Input, InputNumber, Space, Tag } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import moment from 'moment'; import { memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components/macro'; @@ -17,6 +36,7 @@ interface DefaultValueProps { export const DefaultValue = memo( ({ type, expression, disabled, value = [], onChange }: DefaultValueProps) => { const [inputValue, setInputValue] = useState<any>(void 0); + const t = useI18NPrefix('variable'); useEffect(() => { setInputValue(void 0); @@ -85,7 +105,7 @@ export const DefaultValue = memo( case VariableValueTypes.Number: conditionalInputComponent = ( <InputNumber - placeholder="输入默认值后回车添加" + placeholder={t('enterToAdd')} value={inputValue} className="input" disabled={!!disabled} @@ -109,7 +129,7 @@ export const DefaultValue = memo( default: conditionalInputComponent = ( <Input - placeholder="输入默认值后回车添加" + placeholder={t('enterToAdd')} value={inputValue} className="input" disabled={!!disabled} @@ -124,7 +144,7 @@ export const DefaultValue = memo( <Wrapper direction="vertical" size={0}> {expression || type === VariableValueTypes.Expression ? ( <Input.TextArea - placeholder="请输入表达式" + placeholder={t('enterExpression')} autoSize={{ minRows: 4, maxRows: 8 }} value={value ? value[0] : void 0} disabled={!!disabled} diff --git a/frontend/src/app/pages/MainPage/pages/VariablePage/SubjectForm/RowPermissionTable.tsx b/frontend/src/app/pages/MainPage/pages/VariablePage/SubjectForm/RowPermissionTable.tsx index b08b9260e..db24667eb 100644 --- a/frontend/src/app/pages/MainPage/pages/VariablePage/SubjectForm/RowPermissionTable.tsx +++ b/frontend/src/app/pages/MainPage/pages/VariablePage/SubjectForm/RowPermissionTable.tsx @@ -1,5 +1,24 @@ +/** + * 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, Table, TableColumnProps } from 'antd'; import { LoadingMask } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import produce from 'immer'; import { Key, memo, useCallback, useMemo } from 'react'; import styled from 'styled-components/macro'; @@ -33,6 +52,8 @@ export const RowPermissionTable = memo( onSelectedRowKeyChange, onRowPermissionSubjectChange, }: SubjectFormProps) => { + const t = useI18NPrefix('variable'); + const checkUseDefaultValue = useCallback( id => e => { onRowPermissionSubjectChange( @@ -59,9 +80,9 @@ export const RowPermissionTable = memo( const columns: TableColumnProps<RowPermissionSubject>[] = useMemo( () => [ - { dataIndex: 'name', title: '名称' }, + { dataIndex: 'name', title: t('name') }, { - title: '使用变量默认值', + title: t('useDefaultValue'), width: SPACE_TIMES(32), render: (_, record) => { return ( @@ -74,7 +95,7 @@ export const RowPermissionTable = memo( }, }, { - title: '值', + title: t('value'), width: SPACE_TIMES(72), render: (_, record) => editingVariable && ( @@ -90,7 +111,7 @@ export const RowPermissionTable = memo( ), }, ], - [selectedRowKeys, editingVariable, checkUseDefaultValue, valueChange], + [selectedRowKeys, editingVariable, checkUseDefaultValue, valueChange, t], ); const rowClassName = useCallback( diff --git a/frontend/src/app/pages/MainPage/pages/VariablePage/SubjectForm/index.tsx b/frontend/src/app/pages/MainPage/pages/VariablePage/SubjectForm/index.tsx index f6b07cee8..d79a8f07d 100644 --- a/frontend/src/app/pages/MainPage/pages/VariablePage/SubjectForm/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/VariablePage/SubjectForm/index.tsx @@ -1,9 +1,29 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { Modal, ModalProps, Tabs } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import moment from 'moment'; import { Key, memo, useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { SPACE_XS } from 'styles/StyleConstants'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import { selectMemberListLoading, selectMembers, @@ -11,7 +31,7 @@ import { selectRoles, } from '../../MemberPage/slice/selectors'; import { SubjectTypes } from '../../PermissionPage/constants'; -import { VariableScopes } from '../constants'; +import { VariableScopes, VariableValueTypes } from '../constants'; import { RowPermission, RowPermissionSubject, Variable } from '../slice/types'; import { RowPermissionTable } from './RowPermissionTable'; @@ -47,6 +67,7 @@ export const SubjectForm = memo( const members = useSelector(selectMembers); const roleListLoading = useSelector(selectRoleListLoading); const memberListLoading = useSelector(selectMemberListLoading); + const t = useI18NPrefix('variable'); useEffect(() => { if (editingVariable && rowPermissions && roles) { @@ -64,7 +85,11 @@ export const SubjectForm = memo( name, type: SubjectTypes.Role, useDefaultValue: permission ? permission.useDefaultValue : true, - value: permission ? permission.value : void 0, + value: permission?.value + ? editingVariable.valueType === VariableValueTypes.Date + ? permission.value.map(str => moment(str)) + : permission.value + : void 0, }); if (permission) { selectedRowKeys.push(id); @@ -92,7 +117,11 @@ export const SubjectForm = memo( email, type: SubjectTypes.User, useDefaultValue: permission ? permission.useDefaultValue : true, - value: permission ? permission.value : void 0, + value: permission?.value + ? editingVariable.valueType === VariableValueTypes.Date + ? permission.value.map(str => moment(str)) + : permission.value + : void 0, }); if (permission) { selectedRowKeys.push(id); @@ -158,16 +187,17 @@ export const SubjectForm = memo( scope === VariableScopes.Public ? ( <> <StyledTabs defaultActiveKey={tab} onChange={setTab}> - <Tabs.TabPane key="role" tab="关联角色" /> - <Tabs.TabPane key="member" tab="关联成员" /> + <Tabs.TabPane key="role" tab={t('relatedRole')} /> + <Tabs.TabPane key="member" tab={t('relatedMember')} /> </StyledTabs> </> ) : ( - '关联角色' + t('relatedRole') ) } onOk={save} afterClose={onAfterClose} + destroyOnClose > <RowPermissionTable type="role" diff --git a/frontend/src/app/pages/MainPage/pages/VariablePage/VariableForm.tsx b/frontend/src/app/pages/MainPage/pages/VariablePage/VariableForm.tsx index faba51f73..c92f82e14 100644 --- a/frontend/src/app/pages/MainPage/pages/VariablePage/VariableForm.tsx +++ b/frontend/src/app/pages/MainPage/pages/VariablePage/VariableForm.tsx @@ -1,19 +1,33 @@ +/** + * 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, FormInstance, Input, Radio } from 'antd'; import { ModalForm, ModalFormProps } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import debounce from 'debounce-promise'; import { DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import moment from 'moment'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { SPACE_XS } from 'styles/StyleConstants'; import { request } from 'utils/request'; import { errorHandle } from 'utils/utils'; import { VariableHierarchy } from '../ViewPage/slice/types'; -import { - VariableScopes, - VariableTypes, - VariableValueTypes, - VARIABLE_TYPE_LABEL, - VARIABLE_VALUE_TYPE_LABEL, -} from './constants'; +import { VariableScopes, VariableTypes, VariableValueTypes } from './constants'; import { DefaultValue } from './DefaultValue'; import { Variable } from './slice/types'; import { VariableFormModel } from './types'; @@ -46,17 +60,25 @@ export const VariableForm = memo( ); const [expression, setExpression] = useState(false); const formRef = useRef<FormInstance<VariableFormModel>>(); + const t = useI18NPrefix('variable'); + const tg = useI18NPrefix('global'); useEffect(() => { if (visible && editingVariable) { - const { defaultValue, ...rest } = editingVariable; try { - setType(rest.type); - setValueType(rest.valueType); - setExpression(rest.expression || false); + const { type, valueType, expression } = editingVariable; + let defaultValue = editingVariable.defaultValue + ? JSON.parse(editingVariable.defaultValue) + : []; + if (valueType === VariableValueTypes.Date && !expression) { + defaultValue = defaultValue.map(str => moment(str)); + } + setType(type); + setValueType(valueType); + setExpression(expression || false); formRef.current?.setFieldsValue({ - ...rest, - defaultValue: defaultValue ? JSON.parse(defaultValue) : [], + ...editingVariable, + defaultValue, }); } catch (error) { errorHandle(error); @@ -90,89 +112,106 @@ export const VariableForm = memo( formRef.current?.setFieldsValue({ defaultValue: [] }); }, []); + const nameValidator = useMemo( + () => + scope === VariableScopes.Private + ? (_, value) => { + if (value === editingVariable?.name) { + return Promise.resolve(); + } + if (variables?.find(({ name }) => name === value)) { + return Promise.reject(new Error(t('duplicateName'))); + } else { + return Promise.resolve(); + } + } + : debounce((_, value) => { + if (!value || value === editingVariable?.name) { + return Promise.resolve(); + } + return request({ + url: `/variables/check/name`, + method: 'POST', + params: { name: value, orgId }, + }).then( + () => Promise.resolve(), + err => Promise.reject(new Error(err.response.data.message)), + ); + }, DEFAULT_DEBOUNCE_WAIT), + [scope, editingVariable?.name, variables, orgId, t], + ); + return ( <ModalForm {...modalProps} visible={visible} formProps={{ labelAlign: 'left', - labelCol: { offset: 1, span: 5 }, + labelCol: { offset: 1, span: 6 }, wrapperCol: { span: 16 }, className: '', }} afterClose={onAfterClose} ref={formRef} + destroyOnClose > <Form.Item name="name" - label="名称" + label={t('name')} validateFirst rules={[ - { required: true, message: '名称不能为空' }, { - validator: debounce((_, value) => { - if (!value || value === editingVariable?.name) { - return Promise.resolve(); - } - return request({ - url: `/variables/check/name`, - method: 'POST', - params: { name: value, orgId }, - }).then( - () => Promise.resolve(), - () => Promise.reject(new Error('名称重复')), - ); - }, DEFAULT_DEBOUNCE_WAIT), + required: true, + message: `${t('name')}${tg('validation.required')}`, }, { - validator: (_, value) => { - if (value === editingVariable?.name) { - return Promise.resolve(); - } - if (variables?.find(({ name }) => name === value)) { - return Promise.reject(new Error('名称重复')); - } else { - return Promise.resolve(); - } - }, + validator: nameValidator, }, ]} > <Input /> </Form.Item> - <Form.Item name="label" label="标题"> + <Form.Item name="label" label={t('label')}> <Input /> </Form.Item> - <Form.Item name="type" label="类型" initialValue={type}> + <Form.Item name="type" label={t('type')} initialValue={type}> <Radio.Group onChange={typeChange}> {Object.values(VariableTypes).map(value => ( <Radio.Button key={value} value={value}> - {VARIABLE_TYPE_LABEL[value]} + {t(`variableType.${value.toLowerCase()}`)} </Radio.Button> ))} </Radio.Group> </Form.Item> - <Form.Item name="valueType" label="值类型" initialValue={valueType}> + <Form.Item + name="valueType" + label={t('valueType')} + initialValue={valueType} + > <Radio.Group onChange={valueTypeChange}> {Object.values(VariableValueTypes).map(value => ( <Radio.Button key={value} value={value}> - {VARIABLE_VALUE_TYPE_LABEL[value]} + {t(`variableValueType.${value.toLowerCase()}`)} </Radio.Button> ))} </Radio.Group> </Form.Item> {scope === VariableScopes.Public && type === VariableTypes.Permission && ( - <Form.Item name="permission" label="编辑权限" initialValue={0}> + <Form.Item + name="permission" + label={t('permission.label')} + initialValue={0} + > <Radio.Group> - <Radio.Button value={0}>不可见</Radio.Button> - <Radio.Button value={1}>只读</Radio.Button> - <Radio.Button value={2}>可编辑</Radio.Button> + <Radio.Button value={0}>{t('permission.hidden')}</Radio.Button> + <Radio.Button value={1}>{t('permission.readonly')}</Radio.Button> + <Radio.Button value={2}>{t('permission.editable')}</Radio.Button> </Radio.Group> </Form.Item> )} <Form.Item name="defaultValue" - label="默认值" + label={t('defaultValue')} css={` margin-bottom: ${SPACE_XS}; `} @@ -187,9 +226,7 @@ export const VariableForm = memo( valuePropName="checked" initialValue={expression} > - <Checkbox onChange={expressionChange}> - 使用表达式作为默认值 - </Checkbox> + <Checkbox onChange={expressionChange}>{t('expression')}</Checkbox> </Form.Item> )} </ModalForm> diff --git a/frontend/src/app/pages/MainPage/pages/VariablePage/constants.ts b/frontend/src/app/pages/MainPage/pages/VariablePage/constants.ts index c7b308fd8..6c6e1540e 100644 --- a/frontend/src/app/pages/MainPage/pages/VariablePage/constants.ts +++ b/frontend/src/app/pages/MainPage/pages/VariablePage/constants.ts @@ -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. + */ + export enum VariableTypes { Query = 'QUERY', Permission = 'PERMISSION', @@ -14,16 +32,4 @@ export enum VariableValueTypes { Expression = 'FRAGMENT', } -export const VARIABLE_TYPE_LABEL = { - [VariableTypes.Query]: '查询变量', - [VariableTypes.Permission]: '权限变量', -}; - -export const VARIABLE_VALUE_TYPE_LABEL = { - [VariableValueTypes.String]: '字符', - [VariableValueTypes.Number]: '数值', - [VariableValueTypes.Date]: '日期', - [VariableValueTypes.Expression]: '表达式', -}; - export const DEFAULT_VALUE_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'; diff --git a/frontend/src/app/pages/MainPage/pages/VariablePage/index.tsx b/frontend/src/app/pages/MainPage/pages/VariablePage/index.tsx index 9a259cf87..a3938b5fd 100644 --- a/frontend/src/app/pages/MainPage/pages/VariablePage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/VariablePage/index.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 { DeleteOutlined, EditOutlined, @@ -15,6 +33,7 @@ import { Tag, Tooltip, } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { CommonFormTypes } from 'globalConstants'; import { Moment } from 'moment'; import { Key, useCallback, useEffect, useMemo, useState } from 'react'; @@ -40,8 +59,6 @@ import { VariableScopes, VariableTypes, VariableValueTypes, - VARIABLE_TYPE_LABEL, - VARIABLE_VALUE_TYPE_LABEL, } from './constants'; import { useVariableSlice } from './slice'; import { @@ -88,6 +105,8 @@ export function VariablePage() { const saveLoading = useSelector(selectSaveVariableLoading); const deleteVariablesLoading = useSelector(selectDeleteVariablesLoading); const orgId = useSelector(selectOrgId); + const t = useI18NPrefix('variable'); + const tg = useI18NPrefix('global'); useEffect(() => { dispatch(getVariables(orgId)); @@ -154,12 +173,12 @@ export function VariablePage() { deleteVariable({ ids: [id], resolve: () => { - message.success('删除成功'); + message.success(tg('operation.deleteSuccess')); }, }), ); }, - [dispatch], + [dispatch, tg], ); const delSelectedVariables = useCallback(() => { @@ -167,17 +186,17 @@ export function VariablePage() { deleteVariable({ ids: selectedRowKeys as string[], resolve: () => { - message.success('删除成功'); + message.success(tg('operation.deleteSuccess')); setSelectedRowKeys([]); }, }), ); - }, [dispatch, selectedRowKeys]); + }, [dispatch, selectedRowKeys, tg]); const save = useCallback( (values: VariableFormModel) => { let defaultValue: any = values.defaultValue; - if (values.valueType === VariableValueTypes.Date) { + if (values.valueType === VariableValueTypes.Date && !values.expression) { defaultValue = values.defaultValue.map(d => (d as Moment).format(DEFAULT_VALUE_DATE_FORMAT), ); @@ -207,21 +226,30 @@ export function VariablePage() { variable: { ...editingVariable!, ...values, defaultValue }, resolve: () => { hideForm(); - message.success('修改成功'); + message.success(tg('operation.updateSuccess')); }, }), ); } }, - [dispatch, formType, orgId, editingVariable, hideForm], + [dispatch, formType, orgId, editingVariable, hideForm, tg], ); const saveRelations = useCallback( async (changedRowPermissions: RowPermission[]) => { + let changedRowPermissionsRaw = changedRowPermissions.map(cr => ({ + ...cr, + value: + cr.value && + (editingVariable?.valueType === VariableValueTypes.Date + ? cr.value.map(m => (m as Moment).format(DEFAULT_VALUE_DATE_FORMAT)) + : cr.value), + })); + if (rowPermissions) { const { created, updated, deleted } = getDiffParams( [...rowPermissions], - changedRowPermissions, + changedRowPermissionsRaw, (oe, ce) => oe.subjectId === ce.subjectId && oe.variableId === ce.variableId, (oe, ce) => @@ -246,7 +274,7 @@ export function VariablePage() { relToDelete: deleted.map(({ id }) => id), }, }); - message.success('修改成功'); + message.success(tg('operation.updateSuccess')); setSubjectFormVisible(false); } catch (error) { errorHandle(error); @@ -254,40 +282,43 @@ export function VariablePage() { } finally { setUpdateRowPermissionLoading(false); } + } else { + setSubjectFormVisible(false); } } }, - [rowPermissions], + [rowPermissions, editingVariable, tg], ); const columns: TableColumnProps<VariableViewModel>[] = useMemo( () => [ - { dataIndex: 'name', title: '名称' }, - { dataIndex: 'label', title: '标签' }, + { dataIndex: 'name', title: t('name') }, + { dataIndex: 'label', title: t('label') }, { dataIndex: 'type', - title: '类型', + title: t('type'), render: (_, record) => ( <Tag color={record.type === VariableTypes.Permission ? WARNING : INFO} > - {VARIABLE_TYPE_LABEL[record.type]} + {t(`variableType.${record.type.toLowerCase()}`)} </Tag> ), }, { dataIndex: 'valueType', - title: '值类型', - render: (_, record) => VARIABLE_VALUE_TYPE_LABEL[record.valueType], + title: t('valueType'), + render: (_, record) => + t(`variableValueType.${record.valueType.toLowerCase()}`), }, { - title: '操作', + title: tg('title.action'), align: 'center', width: 140, render: (_, record) => ( <Actions> {record.type === VariableTypes.Permission && ( - <Tooltip title="关联角色或用户"> + <Tooltip title={t('related')}> <Button type="link" icon={<TeamOutlined />} @@ -295,15 +326,18 @@ export function VariablePage() { /> </Tooltip> )} - <Tooltip title="编辑"> + <Tooltip title={tg('button.edit')}> <Button type="link" icon={<EditOutlined />} onClick={showEditForm(record.id)} /> </Tooltip> - <Tooltip title="删除"> - <Popconfirm title="确认删除?" onConfirm={del(record.id)}> + <Tooltip title={tg('button.delete')}> + <Popconfirm + title={tg('operation.deleteConfirm')} + onConfirm={del(record.id)} + > <Button type="link" icon={<DeleteOutlined />} /> </Popconfirm> </Tooltip> @@ -311,7 +345,7 @@ export function VariablePage() { ), }, ], - [del, showEditForm, showSubjectForm], + [del, showEditForm, showSubjectForm, t, tg], ); const pagination = useMemo( @@ -323,18 +357,18 @@ export function VariablePage() { <Wrapper> <Card> <TableHeader> - <h3>公共变量列表</h3> + <h3>{t('title')}</h3> <Toolbar> {selectedRowKeys.length > 0 && ( <Popconfirm - title="确认删除全部?" + title={t('deleteAllConfirm')} onConfirm={delSelectedVariables} > <Button icon={<DeleteOutlined />} loading={deleteVariablesLoading} > - 批量删除 + {t('deleteAll')} </Button> </Popconfirm> )} @@ -343,7 +377,7 @@ export function VariablePage() { type="primary" onClick={showAddForm} > - 新建 + {tg('button.create')} </Button> </Toolbar> </TableHeader> @@ -361,7 +395,7 @@ export function VariablePage() { orgId={orgId} editingVariable={editingVariable} visible={formVisible} - title="公共变量" + title={t('public')} type={formType} confirmLoading={saveLoading} onSave={save} diff --git a/frontend/src/app/pages/MainPage/pages/VariablePage/slice/index.tsx b/frontend/src/app/pages/MainPage/pages/VariablePage/slice/index.tsx index 45f3662a2..26ece2fde 100644 --- a/frontend/src/app/pages/MainPage/pages/VariablePage/slice/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/VariablePage/slice/index.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 { createSlice } from '@reduxjs/toolkit'; import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; import { diff --git a/frontend/src/app/pages/MainPage/pages/VariablePage/slice/selectors.ts b/frontend/src/app/pages/MainPage/pages/VariablePage/slice/selectors.ts index d9295fd79..ad87d3a2a 100644 --- a/frontend/src/app/pages/MainPage/pages/VariablePage/slice/selectors.ts +++ b/frontend/src/app/pages/MainPage/pages/VariablePage/slice/selectors.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'types'; import { initialState } from '.'; diff --git a/frontend/src/app/pages/MainPage/pages/VariablePage/slice/thunks.ts b/frontend/src/app/pages/MainPage/pages/VariablePage/slice/thunks.ts index b9ea2f7fd..aa5c2567a 100644 --- a/frontend/src/app/pages/MainPage/pages/VariablePage/slice/thunks.ts +++ b/frontend/src/app/pages/MainPage/pages/VariablePage/slice/thunks.ts @@ -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 { createAsyncThunk } from '@reduxjs/toolkit'; import { request } from 'utils/request'; import { errorHandle } from 'utils/utils'; diff --git a/frontend/src/app/pages/MainPage/pages/VariablePage/slice/types.ts b/frontend/src/app/pages/MainPage/pages/VariablePage/slice/types.ts index d73ba0410..fdd76cb91 100644 --- a/frontend/src/app/pages/MainPage/pages/VariablePage/slice/types.ts +++ b/frontend/src/app/pages/MainPage/pages/VariablePage/slice/types.ts @@ -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 { SubjectTypes } from '../../PermissionPage/constants'; import { VariableTypes, VariableValueTypes } from '../constants'; diff --git a/frontend/src/app/pages/MainPage/pages/VariablePage/types.ts b/frontend/src/app/pages/MainPage/pages/VariablePage/types.ts index bac759928..5ba6cca65 100644 --- a/frontend/src/app/pages/MainPage/pages/VariablePage/types.ts +++ b/frontend/src/app/pages/MainPage/pages/VariablePage/types.ts @@ -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 { Variable } from './slice/types'; export interface VariableFormModel diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Container.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Container.tsx index adb87b04d..96584eb77 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Container.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Container.tsx @@ -1,4 +1,23 @@ +/** + * 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 { Split } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { useSplitSizes } from 'app/hooks/useSplitSizes'; import React, { useCallback, useContext } from 'react'; import styled from 'styled-components/macro'; @@ -9,6 +28,8 @@ import { Sidebar } from './Sidebar'; export function Container() { const { editorInstance } = useContext(EditorContext); + const t = useI18NPrefix('view.saveForm'); + const tg = useI18NPrefix('global'); const editorResize = useCallback(() => { editorInstance?.layout(); @@ -39,13 +60,13 @@ export function Container() { <Sidebar /> <Main /> <SaveForm - title="数据视图" + title={t('title')} formProps={{ labelAlign: 'left', - labelCol: { offset: 1, span: 6 }, - wrapperCol: { span: 15 }, + labelCol: { offset: 1, span: 8 }, + wrapperCol: { span: 13 }, }} - okText="保存" + okText={tg('button.save')} /> </StyledContainer> ); diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/EditorContext.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/EditorContext.ts index d04ca4afb..97371a08b 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/EditorContext.ts +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/EditorContext.ts @@ -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 { createContext, MutableRefObject, diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/SQLEditor.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/SQLEditor.tsx index 05f9edcb8..2089dca5c 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/SQLEditor.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/SQLEditor.tsx @@ -1,3 +1,22 @@ +/** + * 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 useI18NPrefix from 'app/hooks/useI18NPrefix'; import classnames from 'classnames'; import { CommonFormTypes } from 'globalConstants'; import debounce from 'lodash/debounce'; @@ -59,6 +78,7 @@ export const SQLEditor = memo(() => { ) as ViewStatus; const theme = useSelector(selectThemeKey); const viewsData = useSelector(selectViews); + const t = useI18NPrefix('view.editor'); const run = useCallback(() => { const fragment = editorInstance @@ -83,7 +103,7 @@ export const SQLEditor = memo(() => { showSaveForm({ type: CommonFormTypes.Edit, visible: true, - parentIdLabel: '目录', + parentIdLabel: t('folder'), onSave: (values, onClose) => { let index = getInsertedNodeIndex(values, viewsData); @@ -101,7 +121,7 @@ export const SQLEditor = memo(() => { save(); } } - }, [dispatch, actions, stage, status, id, save, showSaveForm, viewsData]); + }, [dispatch, actions, stage, status, id, save, showSaveForm, viewsData, t]); const editorWillMount = useCallback( editor => { @@ -110,6 +130,7 @@ export const SQLEditor = memo(() => { dispatch( getEditorProvideCompletionItems({ resolve: getItems => { + editorCompletionItemProviderRef?.current?.dispose(); const providerRef = editor.languages.registerCompletionItemProvider( 'sql', { @@ -143,12 +164,12 @@ export const SQLEditor = memo(() => { }); editor.onDidAttemptReadOnlyEdit(() => { (messageContribution as any).showMessage( - '回收站中不可编辑', + t('readonlyTip'), editor.getPosition(), ); }); }, - [setEditor, dispatch, actions], + [setEditor, dispatch, actions, t], ); useEffect(() => { diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/Toolbar.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/Toolbar.tsx index 8afdde047..98f2bc8dd 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/Toolbar.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/Toolbar.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 { AlignCenterOutlined, CaretRightOutlined, @@ -9,6 +27,7 @@ import { import { Divider, Dropdown, Menu, Select, Space, Tooltip } from 'antd'; import { ToolbarButton } from 'app/components'; import { Chronograph } from 'app/components/Chronograph'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { CommonFormTypes } from 'globalConstants'; import React, { memo, useCallback, useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -86,6 +105,7 @@ export const Toolbar = memo(({ allowManage }: ToolbarProps) => { selectCurrentEditingViewAttr(state, { name: 'index' }), ) as number; const viewsData = useSelector(selectViews); + const t = useI18NPrefix('view.editor'); const isArchived = status === ViewStatus.Archived; @@ -106,7 +126,7 @@ export const Toolbar = memo(({ allowManage }: ToolbarProps) => { parentId, config, }, - parentIdLabel: '目录', + parentIdLabel: t('folder'), onSave: (values, onClose) => { let index = ViewIndex; @@ -133,6 +153,7 @@ export const Toolbar = memo(({ allowManage }: ToolbarProps) => { config, viewsData, ViewIndex, + t, ]); const sourceChange = useCallback( @@ -155,7 +176,7 @@ export const Toolbar = memo(({ allowManage }: ToolbarProps) => { <Space split={<Divider type="vertical" className="divider" />}> {allowManage && ( <Select - placeholder="请选择数据源" + placeholder={t('source')} value={sourceId} bordered={false} disabled={isArchived} @@ -174,9 +195,9 @@ export const Toolbar = memo(({ allowManage }: ToolbarProps) => { title={ <TipTitle title={[ - `${fragment ? '执行片段' : '执行'}`, - 'Win: [Ctrl + Enter]', - 'Mac: [Command + Enter]', + `${fragment ? t('runSelection') : t('run')}`, + t('runWinTip'), + t('runMacTip'), ]} /> } @@ -194,7 +215,7 @@ export const Toolbar = memo(({ allowManage }: ToolbarProps) => { onClick={onRun} /> </Tooltip> - <Tooltip title="美化" placement="bottom"> + <Tooltip title={t('beautify')} placement="bottom"> <ToolbarButton icon={<AlignCenterOutlined />} disabled={isArchived} @@ -234,7 +255,7 @@ export const Toolbar = memo(({ allowManage }: ToolbarProps) => { <Tooltip title={ <TipTitle - title={['保存', 'Win: [Ctrl + S]', 'Mac: [Command + S]']} + title={[t('save'), t('saveWinTip'), t('saveMacTip')]} /> } placement="bottom" @@ -247,7 +268,7 @@ export const Toolbar = memo(({ allowManage }: ToolbarProps) => { /> </Tooltip> {!isNewView(id) && ( - <Tooltip title="详情设置" placement="bottom"> + <Tooltip title={t('info')} placement="bottom"> <ToolbarButton icon={<SettingFilled />} disabled={isArchived} @@ -256,13 +277,13 @@ export const Toolbar = memo(({ allowManage }: ToolbarProps) => { /> </Tooltip> )} - <Tooltip title="另存为" placement="bottom"> + <Tooltip title={t('saveAs')} placement="bottom"> <ToolbarButton icon={<CopyFilled />} disabled={stage !== ViewViewModelStages.Saveable} /> </Tooltip> - {/* <Tooltip title="保存片段" placement="bottom"> + {/* <Tooltip title={t('saveFragment')} placement="bottom"> <ToolbarButton icon={<SnippetsFilled />} /> </Tooltip> */} </Space> diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/index.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/index.tsx index bb95bbbdb..9014e7ee3 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Editor/index.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 React, { memo } from 'react'; import styled from 'styled-components/macro'; import { SQLEditor } from './SQLEditor'; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Error.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Error.tsx index 7131bb993..1d303814a 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Error.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Error.tsx @@ -1,4 +1,23 @@ +/** + * 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 { InfoCircleOutlined } from '@ant-design/icons'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { memo } from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components/macro'; @@ -15,12 +34,13 @@ export const Error = memo(() => { const error = useSelector(state => selectCurrentEditingViewAttr(state, { name: 'error' }), ) as string; + const t = useI18NPrefix('view'); return ( <Wrapper> <h3> <InfoCircleOutlined className="icon" /> - 执行错误 + {t('errorTitle')} </h3> <p>{error}</p> </Wrapper> diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Results.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Results.tsx index 33522ec9a..855cbb2f9 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Results.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/Results.tsx @@ -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 { CaretRightOutlined, EyeInvisibleOutlined, @@ -5,12 +23,13 @@ import { } from '@ant-design/icons'; import { Tooltip } from 'antd'; import { Popup, ToolbarButton, Tree } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import classnames from 'classnames'; import { memo, useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { FONT_FAMILY, FONT_SIZE_BASE } from 'styles/StyleConstants'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import { selectRoles } from '../../../MemberPage/slice/selectors'; import { SubjectTypes } from '../../../PermissionPage/constants'; import { SchemaTable } from '../../components/SchemaTable'; @@ -28,9 +47,10 @@ const ROW_KEY = 'DATART_ROW_KEY'; interface ResultsProps { height?: number; + width?: number; } -export const Results = memo(({ height = 0 }: ResultsProps) => { +export const Results = memo(({ height = 0, width = 0 }: ResultsProps) => { const { actions } = useViewSlice(); const dispatch = useDispatch(); const viewId = useSelector(state => @@ -49,6 +69,7 @@ export const Results = memo(({ height = 0 }: ResultsProps) => { selectCurrentEditingViewAttr(state, { name: 'previewResults' }), ) as ViewViewModel['previewResults']; const roles = useSelector(selectRoles); + const t = useI18NPrefix('view'); const dataSource = useMemo( () => previewResults.map(o => ({ ...o, [ROW_KEY]: uuidv4() })), @@ -174,7 +195,7 @@ export const Results = memo(({ height = 0 }: ResultsProps) => { /> } > - <Tooltip title="列权限"> + <Tooltip title={t('columnPermission.title')}> <ToolbarButton size="small" iconSize={FONT_SIZE_BASE} @@ -194,11 +215,14 @@ export const Results = memo(({ height = 0 }: ResultsProps) => { </Popup>, ]; }, - [columnPermissions, roleDropdownData, checkRoleColumnPermission], + [columnPermissions, roleDropdownData, checkRoleColumnPermission, t], ); const pagination = useMemo( - () => ({ pageSize: 100, pageSizeOptions: ['100', '200', '500', '1000'] }), + () => ({ + defaultPageSize: 100, + pageSizeOptions: ['100', '200', '500', '1000'], + }), [], ); @@ -206,6 +230,7 @@ export const Results = memo(({ height = 0 }: ResultsProps) => { <TableWrapper> <SchemaTable height={height ? height - 96 : 0} + width={width} model={model} dataSource={dataSource} pagination={pagination} @@ -217,7 +242,9 @@ export const Results = memo(({ height = 0 }: ResultsProps) => { ) : ( <InitialDesc> <p> - 请点击 <CaretRightOutlined /> 按钮执行,运行结果将在此处展示 + {t('resultEmpty1')} + <CaretRightOutlined /> + {t('resultEmpty2')} </p> </InitialDesc> ); diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/index.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/index.tsx index d7bdb802d..d66f17038 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Outputs/index.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 { Spin } from 'antd'; import useResizeObserver from 'app/hooks/useResizeObserver'; import { transparentize } from 'polished'; @@ -17,11 +35,14 @@ export const Outputs = memo(() => { selectCurrentEditingViewAttr(state, { name: 'stage' }), ) as ViewViewModelStages; - const { height, ref } = useResizeObserver(); + const { width, height, ref } = useResizeObserver({ + refreshMode: 'debounce', + refreshRate: 200, + }); return ( <Wrapper ref={ref}> - <Results height={height} /> + <Results width={width} height={height} /> {error && <Error />} {stage === ViewViewModelStages.Running && ( <LoadingMask> diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/ColumnPermissions.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/ColumnPermissions.tsx index 0c7ae6348..fba3584cb 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/ColumnPermissions.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/ColumnPermissions.tsx @@ -1,7 +1,26 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { LoadingOutlined, SearchOutlined } from '@ant-design/icons'; import { Button, Col, Input, List, Row } from 'antd'; import { ListItem, ListTitle, Popup, Tree } from 'app/components'; import { useDebouncedSearch } from 'app/hooks/useDebouncedSearch'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import classnames from 'classnames'; import { memo, useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -12,7 +31,7 @@ import { SPACE_XS, WARNING, } from 'styles/StyleConstants'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import { selectRoles } from '../../../MemberPage/slice/selectors'; import { SubjectTypes } from '../../../PermissionPage/constants'; import { ViewStatus, ViewViewModelStages } from '../../constants'; @@ -39,6 +58,7 @@ export const ColumnPermissions = memo(() => { selectCurrentEditingViewAttr(state, { name: 'columnPermissions' }), ) as ColumnPermission[]; const roles = useSelector(selectRoles); + const t = useI18NPrefix('view.columnPermission'); const { filteredData, debouncedSearch } = useDebouncedSearch( roles, @@ -135,9 +155,9 @@ export const ColumnPermissions = memo(() => { > {permission ? checkedKeys.length > 0 - ? '部分字段' - : '不可见' - : '全部字段'} + ? t('partial') + : t('none') + : t('all')} </Button> </Popup>, ]} @@ -146,17 +166,17 @@ export const ColumnPermissions = memo(() => { </ListItem> ); }, - [columnDropdownData, columnPermissions, checkColumnPermission, status], + [columnDropdownData, columnPermissions, checkColumnPermission, status, t], ); return ( <Container> - <ListTitle title="列权限" /> + <ListTitle title={t('title')} /> <Searchbar> <Col span={24}> <Input prefix={<SearchOutlined className="icon" />} - placeholder="搜索角色关键字" + placeholder={t('search')} className="input" bordered={false} onChange={debouncedSearch} diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Resource.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Resource.tsx index 69b1e1351..9e0adc902 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Resource.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Resource.tsx @@ -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 { CalendarOutlined, DatabaseOutlined, @@ -8,9 +26,11 @@ import { } from '@ant-design/icons'; import { Col, Input, Row } from 'antd'; import { ListTitle, Tree } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { useSearchAndExpand } from 'app/hooks/useSearchAndExpand'; import { selectDataProviderDatabaseListLoading } from 'app/pages/MainPage/slice/selectors'; import { getDataProviderDatabases } from 'app/pages/MainPage/slice/thunks'; +import { DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; import { memo, useCallback, useContext, useEffect } from 'react'; import { monaco } from 'react-monaco-editor'; import { useDispatch, useSelector } from 'react-redux'; @@ -42,12 +62,15 @@ export const Resource = memo(() => { const databaseListLoading = useSelector( selectDataProviderDatabaseListLoading, ); + const t = useI18NPrefix('view.resource'); - const { filteredData, expandedRowKeys, onExpand, debouncedSearch } = - useSearchAndExpand(databases, (keywords, data) => - (data.title as string).includes(keywords), + const { filteredData, onExpand, debouncedSearch, expandedRowKeys } = + useSearchAndExpand( + databases, + (keywords, data) => (data.title as string).includes(keywords), + DEFAULT_DEBOUNCE_WAIT, + true, ); - useEffect(() => { if (sourceId && !databases) { dispatch(getDataProviderDatabases(sourceId)); @@ -98,7 +121,13 @@ export const Resource = memo(() => { actions.addTables({ sourceId, databaseName: database, - tables: data, + tables: data.sort((a, b) => + a.toLowerCase() < b.toLowerCase() + ? -1 + : a.toLowerCase() > b.toLowerCase() + ? 1 + : 0, + ), }), ); resolve(); @@ -134,12 +163,12 @@ export const Resource = memo(() => { return ( <Container> - <ListTitle title="数据源信息" /> + <ListTitle title={t('title')} /> <Searchbar> <Col span={24}> <Input prefix={<SearchOutlined className="icon" />} - placeholder="搜索数据库 / 表 / 字段关键字" + placeholder={t('search')} className="input" bordered={false} onChange={debouncedSearch} @@ -154,7 +183,7 @@ export const Resource = memo(() => { loading={databaseListLoading} icon={renderIcon} selectable={false} - expandedKeys={expandedRowKeys} + defaultExpandedKeys={expandedRowKeys} onExpand={onExpand} /> </TreeWrapper> diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Variables.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Variables.tsx index 7b07a43f6..98ec187de 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Variables.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/Variables.tsx @@ -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 { DeleteOutlined, EditOutlined, @@ -8,10 +26,13 @@ import { } from '@ant-design/icons'; import { Button, List, Popconfirm } from 'antd'; import { ListItem, ListTitle } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { getRoles } from 'app/pages/MainPage/pages/MemberPage/slice/thunks'; import { + DEFAULT_VALUE_DATE_FORMAT, VariableScopes, VariableTypes, + VariableValueTypes, } from 'app/pages/MainPage/pages/VariablePage/constants'; import { RowPermission, @@ -21,7 +42,9 @@ import { SubjectForm } from 'app/pages/MainPage/pages/VariablePage/SubjectForm'; import { VariableFormModel } from 'app/pages/MainPage/pages/VariablePage/types'; import { VariableForm } from 'app/pages/MainPage/pages/VariablePage/VariableForm'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; +import classnames from 'classnames'; import { CommonFormTypes } from 'globalConstants'; +import { Moment } from 'moment'; import { memo, ReactElement, @@ -34,9 +57,8 @@ import { import { monaco } from 'react-monaco-editor'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; -import { SPACE_MD, SPACE_TIMES } from 'styles/StyleConstants'; -import { errorHandle } from 'utils/utils'; -import { v4 as uuidv4 } from 'uuid'; +import { SPACE_MD, SPACE_TIMES, SPACE_XS } from 'styles/StyleConstants'; +import { errorHandle, uuidv4 } from 'utils/utils'; import { selectVariables } from '../../../VariablePage/slice/selectors'; import { getVariables } from '../../../VariablePage/slice/thunks'; import { ViewViewModelStages } from '../../constants'; @@ -68,6 +90,8 @@ export const Variables = memo(() => { ) as string; const orgId = useSelector(selectOrgId); const publicVariables = useSelector(selectVariables); + const t = useI18NPrefix('view.variable'); + const tg = useI18NPrefix('global'); useEffect(() => { if (editorCompletionItemProviderRef) { @@ -154,35 +178,45 @@ export const Variables = memo(() => { const save = useCallback( (values: VariableFormModel) => { + let defaultValue: any = values.defaultValue; + if (values.valueType === VariableValueTypes.Date && !values.expression) { + defaultValue = values.defaultValue.map(d => + (d as Moment).format(DEFAULT_VALUE_DATE_FORMAT), + ); + } + try { - const defaultValue = JSON.stringify(values.defaultValue); - if (formType === CommonFormTypes.Add) { - dispatch( - actions.changeCurrentEditingView({ - variables: variables.concat({ - ...values, - id: uuidv4(), - defaultValue, - relVariableSubjects: [], - }), - }), - ); - } else { - dispatch( - actions.changeCurrentEditingView({ - variables: variables.map(v => - v.id === editingVariable!.id - ? { ...editingVariable!, ...values, defaultValue } - : v, - ), - }), - ); + if (defaultValue !== void 0 && defaultValue !== null) { + defaultValue = JSON.stringify(defaultValue); } - setFormVisible(false); } catch (error) { errorHandle(error); throw error; } + + if (formType === CommonFormTypes.Add) { + dispatch( + actions.changeCurrentEditingView({ + variables: variables.concat({ + ...values, + id: uuidv4(), + defaultValue, + relVariableSubjects: [], + }), + }), + ); + } else { + dispatch( + actions.changeCurrentEditingView({ + variables: variables.map(v => + v.id === editingVariable!.id + ? { ...editingVariable!, ...values, defaultValue } + : v, + ), + }), + ); + } + setFormVisible(false); }, [dispatch, actions, formType, editingVariable, variables], ); @@ -203,7 +237,14 @@ export const Variables = memo(() => { try { const changedRowPermissionsRaw = changedRowPermissions.map(cr => ({ ...cr, - value: JSON.stringify(cr.value), + value: JSON.stringify( + cr.value && + (editingVariable?.valueType === VariableValueTypes.Date + ? cr.value.map(d => + (d as Moment).format(DEFAULT_VALUE_DATE_FORMAT), + ) + : cr.value), + ), })); if ( !comparePermissionChange( @@ -236,25 +277,33 @@ export const Variables = memo(() => { [dispatch, actions, editingVariable, variables], ); - const renderTitleText = useCallback(item => { - return ( - <> - {item.relVariableSubjects ? '' : <span>[公共]</span>} - {item.name} - </> - ); - }, []); + const renderTitleText = useCallback( + item => { + const isPrivate = !!item.relVariableSubjects; + const isDuplicate = isPrivate + ? publicVariables.some(v => v.name === item.name) + : variables.some(v => v.name === item.name); + return ( + <ListItemTitle className={classnames({ duplicate: isDuplicate })}> + {!isPrivate && <span className="prefix">{t('prefix')}</span>} + {item.name} + {isDuplicate && <span className="suffix">{t('suffix')}</span>} + </ListItemTitle> + ); + }, + [variables, publicVariables, t], + ); const titleProps = useMemo( () => ({ - title: '变量配置', + title: t('title'), search: true, add: { - items: [{ key: 'variable', text: '新建变量' }], + items: [{ key: 'variable', text: t('add') }], callback: showAddForm, }, }), - [showAddForm], + [showAddForm, t], ); return ( @@ -281,7 +330,7 @@ export const Variables = memo(() => { />, <Popconfirm key="del" - title="确认删除?" + title={tg('operation.deleteConfirm')} placement="bottom" onConfirm={del(item.id)} > @@ -325,8 +374,9 @@ export const Variables = memo(() => { scope={VariableScopes.Private} orgId={orgId} editingVariable={editingVariable} + variables={variables} visible={formVisible} - title="变量" + title={t('formTitle')} type={formType} onSave={save} onCancel={hideForm} @@ -371,3 +421,19 @@ const ListWrapper = styled.div` color: ${p => p.theme.warning}; } `; + +const ListItemTitle = styled.div` + &.duplicate { + color: ${p => p.theme.highlight}; + } + + .prefix { + margin-right: ${SPACE_XS}; + color: ${p => p.theme.textColorDisabled}; + } + + .suffix { + margin-left: ${SPACE_XS}; + color: ${p => p.theme.highlight}; + } +`; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/VerticalTabs.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/VerticalTabs.tsx index d62afba56..4cebd1c2b 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/VerticalTabs.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/VerticalTabs.tsx @@ -1,12 +1,33 @@ +/** + * 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 classnames from 'classnames'; import { cloneElement, memo, ReactElement, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import styled from 'styled-components/macro'; import { FONT_SIZE_TITLE, FONT_WEIGHT_MEDIUM, + FONT_WEIGHT_REGULAR, SPACE, SPACE_XS, } from 'styles/StyleConstants'; +import { getTextWidth } from 'utils/utils'; interface TabProps { name: string; @@ -21,6 +42,7 @@ interface VerticalTabsProps { export const VerticalTabs = memo(({ tabs, onSelect }: VerticalTabsProps) => { const [selectedTab, setSelectedTab] = useState(''); + const { i18n } = useTranslation(); const selectTab = useCallback( name => () => { @@ -33,18 +55,34 @@ export const VerticalTabs = memo(({ tabs, onSelect }: VerticalTabsProps) => { return ( <Wrapper> - {tabs.map(({ name, title, icon }) => ( - <Tab - key={name} - className={classnames({ selected: selectedTab === name })} - onClick={selectTab(name)} - > - {icon && <Word className="icon">{cloneElement(icon)}</Word>} - {title.split('').map((s, index) => ( - <Word key={index}>{s}</Word> - ))} - </Tab> - ))} + {tabs.map(({ name, title, icon }) => { + const rotate = ['en'].includes(i18n.language) ? '90deg' : '0'; + return ( + <Tab + key={name} + className={classnames({ selected: selectedTab === name })} + onClick={selectTab(name)} + > + {icon && ( + <Word rotate={rotate} className="icon"> + {cloneElement(icon)} + </Word> + )} + {title.split('').map((s, index) => { + const wordHeight = getTextWidth( + s, + String(FONT_WEIGHT_REGULAR), + FONT_SIZE_TITLE, + ); + return ( + <Word key={index} rotate={rotate} height={wordHeight}> + {s} + </Word> + ); + })} + </Tab> + ); + })} </Wrapper> ); }); @@ -79,12 +117,14 @@ const Tab = styled.li` } `; -const Word = styled.span` +const Word = styled.span<{ rotate: string; height?: number }>` display: block; width: ${FONT_SIZE_TITLE}; - height: ${FONT_SIZE_TITLE}; - line-height: ${FONT_SIZE_TITLE}; - /* transform: rotate(90deg); */ + height: ${p => (p.height ? `${p.height}px` : FONT_SIZE_TITLE)}; + line-height: ${p => (p.height ? `${p.height}px` : FONT_SIZE_TITLE)}; + font-size: ${FONT_SIZE_TITLE}; + text-align: center; + transform: ${p => `rotate(${p.rotate})`}; &.icon { margin-bottom: ${SPACE}; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/index.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/index.tsx index e8984cf02..3a84c12a7 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Properties/index.tsx @@ -1,9 +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 { DatabaseOutlined, FunctionOutlined, SafetyCertificateOutlined, } from '@ant-design/icons'; import { PaneWrapper } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { memo, useCallback, @@ -26,6 +45,7 @@ interface PropertiesProps { export const Properties = memo(({ allowManage }: PropertiesProps) => { const [selectedTab, setSelectedTab] = useState(''); const { editorInstance } = useContext(EditorContext); + const t = useI18NPrefix('view.properties'); useEffect(() => { editorInstance?.layout(); @@ -33,15 +53,15 @@ export const Properties = memo(({ allowManage }: PropertiesProps) => { const tabTitle = useMemo( () => [ - { name: 'resource', title: '数据源信息', icon: <DatabaseOutlined /> }, - { name: 'variable', title: '变量配置', icon: <FunctionOutlined /> }, + { name: 'reference', title: t('reference'), icon: <DatabaseOutlined /> }, + { name: 'variable', title: t('variable'), icon: <FunctionOutlined /> }, { name: 'columnPermissions', - title: '列权限', + title: t('columnPermissions'), icon: <SafetyCertificateOutlined />, }, ], - [], + [t], ); const tabSelect = useCallback(tab => { @@ -53,7 +73,7 @@ export const Properties = memo(({ allowManage }: PropertiesProps) => { <PaneWrapper selected={selectedTab === 'variable'}> <Variables /> </PaneWrapper> - <PaneWrapper selected={selectedTab === 'resource'}> + <PaneWrapper selected={selectedTab === 'reference'}> <Resource /> </PaneWrapper> <PaneWrapper selected={selectedTab === 'columnPermissions'}> diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Tabs.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Tabs.tsx index 26c8839d9..097a0dcd5 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Tabs.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Tabs.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 { CloseOutlined, InfoCircleOutlined, @@ -5,6 +23,7 @@ import { } from '@ant-design/icons'; import { Button, Space } from 'antd'; import { Confirm, TabPane, Tabs as TabsComponent } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import React, { memo, useCallback, useContext, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -37,6 +56,7 @@ export const Tabs = memo(() => { const id = useSelector(state => selectCurrentEditingViewAttr(state, { name: 'id' }), ) as string; + const t = useI18NPrefix('view.tabs'); const redirect = useCallback( currentEditingViewKey => { @@ -116,7 +136,7 @@ export const Tabs = memo(() => { </TabsComponent> <Confirm visible={confirmVisible} - title={`${operatingView?.name} 中有未执行的修改,是否执行?`} + title={t('warning')} icon={ <InfoCircleOutlined css={` @@ -126,10 +146,10 @@ export const Tabs = memo(() => { } footer={ <Space> - <Button onClick={removeTab}>放弃</Button> - <Button onClick={hideConfirm}>取消</Button> + <Button onClick={removeTab}>{t('discard')}</Button> + <Button onClick={hideConfirm}>{t('cancel')}</Button> <Button onClick={runTab} type="primary"> - 执行 + {t('execute')} </Button> </Space> } diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Workbench.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Workbench.tsx index 50a2877b4..f4c17d6eb 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Workbench.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/Workbench.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 { Split } from 'app/components'; import { useCascadeAccess } from 'app/pages/MainPage/Access'; import React, { diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/index.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/index.tsx index 8dffb846e..2b9de72d6 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Main/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Main/index.tsx @@ -1,4 +1,23 @@ +/** + * 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 { EmptyFiller } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { memo, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useRouteMatch } from 'react-router-dom'; @@ -23,6 +42,7 @@ export const Main = memo(() => { } = useRouteMatch<{ viewId: string }>(); const orgId = useSelector(selectOrgId); const editingViews = useSelector(selectEditingViews); + const t = useI18NPrefix('view'); useEffect(() => { dispatch(getSources(orgId)); @@ -42,7 +62,7 @@ export const Main = memo(() => { <Workbench /> </> ) : ( - <EmptyFiller title="请在左侧列表选择数据视图" /> + <EmptyFiller title={t('empty')} /> )} </Container> ); diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/SaveForm.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/SaveForm.tsx index a4043b392..497432e73 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/SaveForm.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/SaveForm.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 { DoubleRightOutlined } from '@ant-design/icons'; import { Button, @@ -10,6 +28,7 @@ import { TreeSelect, } from 'antd'; import { ModalForm, ModalFormProps } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import debounce from 'debounce-promise'; import { DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; import { @@ -31,11 +50,7 @@ import { selectPermissionMap, } from '../../slice/selectors'; import { PermissionLevels, ResourceTypes } from '../PermissionPage/constants'; -import { - ConcurrencyControlModes, - CONCURRENCY_CONTROL_MODE_LABEL, - ViewViewModelStages, -} from './constants'; +import { ConcurrencyControlModes, ViewViewModelStages } from './constants'; import { SaveFormContext } from './SaveFormContext'; import { makeSelectViewFolderTree, @@ -81,6 +96,8 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { const currentEditingView = useSelector(selectCurrentEditingView); const orgId = useSelector(selectOrgId); const formRef = useRef<FormInstance>(); + const t = useI18NPrefix('view.saveForm'); + const tg = useI18NPrefix('global'); useEffect(() => { if (initialValues) { @@ -124,9 +141,12 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { > <Form.Item name="name" - label="名称" + label={t('name')} rules={[ - { required: true, message: '名称不能为空' }, + { + required: true, + message: `${t('name')}${tg('validation.required')}`, + }, { validator: debounce((_, value) => { if (!value || initialValues?.name === value) { @@ -139,7 +159,7 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { params: { name: value, orgId, parentId: parentId || null }, }).then( () => Promise.resolve(), - () => Promise.reject(new Error('名称重复')), + err => Promise.reject(new Error(err.response.data.message)), ); }, DEFAULT_DEBOUNCE_WAIT), }, @@ -149,9 +169,12 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { </Form.Item> <Form.Item name="parentId" label={parentIdLabel}> <TreeSelect - placeholder="根目录" + placeholder={t('root')} treeData={folderTree || []} allowClear + onChange={() => { + formRef.current?.validateFields(); + }} /> </Form.Item> {!simple && ( @@ -161,12 +184,12 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { icon={<DoubleRightOutlined rotate={advancedVisible ? -90 : 90} />} onClick={toggleAdvanced} > - 高级配置 + {t('advanced')} </AdvancedToggle> <AdvancedWrapper show={advancedVisible}> <Form.Item name={['config', 'concurrencyControl']} - label="并发控制" + label={t('concurrencyControl')} valuePropName="checked" initialValue={concurrencyControl} > @@ -174,20 +197,20 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { </Form.Item> <Form.Item name={['config', 'concurrencyControlMode']} - label="模式" + label={t('concurrencyControlMode')} initialValue={ConcurrencyControlModes.DirtyRead} > <Radio.Group disabled={!concurrencyControl}> {Object.values(ConcurrencyControlModes).map(value => ( <Radio key={value} value={value}> - {CONCURRENCY_CONTROL_MODE_LABEL[value]} + {t(value.toLowerCase())} </Radio> ))} </Radio.Group> </Form.Item> <Form.Item name={['config', 'cache']} - label="缓存" + label={t('cache')} valuePropName="checked" initialValue={cache} > @@ -195,7 +218,7 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { </Form.Item> <Form.Item name={['config', 'cacheExpires']} - label="失效时间" + label={t('cacheExpires')} initialValue={0} > <InputNumber disabled={!cache} /> diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/SaveFormContext.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/SaveFormContext.ts index fd80f0b49..fd4de0dff 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/SaveFormContext.ts +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/SaveFormContext.ts @@ -1,3 +1,22 @@ +/** + * 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 useI18NPrefix from 'app/hooks/useI18NPrefix'; import { CommonFormTypes } from 'globalConstants'; import { createContext, useCallback, useMemo, useState } from 'react'; @@ -36,13 +55,14 @@ const saveFormContextValue: SaveFormContextValue = { export const SaveFormContext = createContext(saveFormContextValue); export const useSaveFormContext = (): SaveFormContextValue => { + const t = useI18NPrefix('view.saveForm'); const [type, setType] = useState(CommonFormTypes.Add); const [visible, setVisible] = useState(false); const [simple, setSimple] = useState<boolean | undefined>(false); const [initialValues, setInitialValues] = useState< undefined | SaveFormModel >(); - const [parentIdLabel, setParentIdLabel] = useState('目录'); + const [parentIdLabel, setParentIdLabel] = useState(t('folder')); const [onSave, setOnSave] = useState(() => () => {}); const [onAfterClose, setOnAfterClose] = useState(() => () => {}); diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/FolderTree.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/FolderTree.tsx index 47837d414..2a7b75acd 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/FolderTree.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/FolderTree.tsx @@ -1,12 +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 { DeleteOutlined, EditOutlined, MoreOutlined } from '@ant-design/icons'; import { Menu, message, Popconfirm, TreeDataNode } from 'antd'; import { MenuListItem, Popup, Tree, TreeTitle } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { CascadeAccess } from 'app/pages/MainPage/Access'; -import { - selectIsOrgOwner, - selectOrgId, - selectPermissionMap, -} from 'app/pages/MainPage/slice/selectors'; +import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { CommonFormTypes } from 'globalConstants'; import React, { memo, useCallback, useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -18,7 +33,6 @@ import { ResourceTypes, } from '../../PermissionPage/constants'; import { SaveFormContext } from '../SaveFormContext'; -import { useViewSlice } from '../slice'; import { selectCurrentEditingViewKey, selectViewListLoading, @@ -42,10 +56,9 @@ export const FolderTree = memo(({ treeData }: FolderTreeProps) => { const loading = useSelector(selectViewListLoading); const currentEditingViewKey = useSelector(selectCurrentEditingViewKey); const orgId = useSelector(selectOrgId); - const isOwner = useSelector(selectIsOrgOwner); - const permissionMap = useSelector(selectPermissionMap); - const { actions } = useViewSlice(); const viewsData = useSelector(selectViews); + const t = useI18NPrefix('view.form'); + const tg = useI18NPrefix('global'); useEffect(() => { dispatch(getViews(orgId)); @@ -71,12 +84,16 @@ export const FolderTree = memo(({ treeData }: FolderTreeProps) => { archive: !isFolder, resolve: () => { dispatch(removeEditingView({ id, resolve: redirect })); - message.success(`成功${isFolder ? '删除' : '移至回收站'}`); + message.success( + isFolder + ? tg('operation.deleteSuccess') + : tg('operation.archiveSuccess'), + ); }, }), ); }, - [dispatch, redirect], + [dispatch, redirect, tg], ); const moreMenuClick = useCallback( @@ -94,7 +111,7 @@ export const FolderTree = memo(({ treeData }: FolderTreeProps) => { name, parentId, }, - parentIdLabel: '目录', + parentIdLabel: t('folder'), onSave: (values, onClose) => { if (isParentIdEqual(parentId, values.parentId)) { index = getInsertedNodeIndex(values, viewsData); @@ -120,7 +137,7 @@ export const FolderTree = memo(({ treeData }: FolderTreeProps) => { break; } }, - [dispatch, showSaveForm, viewsData], + [dispatch, showSaveForm, viewsData, t], ); const renderTreeTitle = useCallback( @@ -146,17 +163,23 @@ export const FolderTree = memo(({ treeData }: FolderTreeProps) => { key="info" prefix={<EditOutlined className="icon" />} > - 基本信息 + {tg('button.info')} </MenuListItem> <MenuListItem key="delete" prefix={<DeleteOutlined className="icon" />} > <Popconfirm - title={`确定${node.isFolder ? '删除' : '移至回收站'}?`} + title={ + node.isFolder + ? tg('operation.deleteConfirm') + : tg('operation.archiveConfirm') + } onConfirm={archive(node.id, node.isFolder)} > - {node.isFolder ? '删除' : '移至回收站'} + {node.isFolder + ? tg('button.delete') + : tg('button.archive')} </Popconfirm> </MenuListItem> </Menu> @@ -170,7 +193,7 @@ export const FolderTree = memo(({ treeData }: FolderTreeProps) => { </TreeTitle> ); }, - [archive, moreMenuClick], + [archive, moreMenuClick, tg], ); const treeSelect = useCallback( diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/Recycle.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/Recycle.tsx index b7b16b1dc..0c0ab9a3a 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/Recycle.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/Recycle.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 { DeleteOutlined, MoreOutlined, @@ -5,6 +23,7 @@ import { } from '@ant-design/icons'; import { Menu, message, Popconfirm, TreeDataNode } from 'antd'; import { MenuListItem, Popup, Tree, TreeTitle } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { getCascadeAccess } from 'app/pages/MainPage/Access'; import { selectIsOrgOwner, @@ -47,6 +66,8 @@ export const Recycle = memo(({ list }: RecycleProps) => { const views = useSelector(selectViews); const isOwner = useSelector(selectIsOrgOwner); const permissionMap = useSelector(selectPermissionMap); + const t = useI18NPrefix('view.form'); + const tg = useI18NPrefix('global'); useEffect(() => { dispatch(getArchivedViews(orgId)); @@ -72,12 +93,12 @@ export const Recycle = memo(({ list }: RecycleProps) => { archive: false, resolve: () => { dispatch(removeEditingView({ id, resolve: redirect })); - message.success('删除成功'); + message.success(tg('operation.deleteSuccess')); }, }), ); }, - [dispatch, redirect], + [dispatch, redirect, tg], ); const moreMenuClick = useCallback( @@ -91,7 +112,7 @@ export const Recycle = memo(({ list }: RecycleProps) => { visible: true, simple: true, initialValues: { name, parentId: null }, - parentIdLabel: '目录', + parentIdLabel: t('folder'), onSave: (values, onClose) => { let index = getInsertedNodeIndex(values, views); @@ -100,7 +121,7 @@ export const Recycle = memo(({ list }: RecycleProps) => { view: { ...values, id, index }, resolve: () => { dispatch(removeEditingView({ id, resolve: redirect })); - message.success('还原成功'); + message.success(tg('operation.restoreSuccess')); onClose(); }, }), @@ -112,7 +133,7 @@ export const Recycle = memo(({ list }: RecycleProps) => { break; } }, - [dispatch, showSaveForm, redirect, views], + [dispatch, showSaveForm, redirect, views, t, tg], ); const renderTreeTitle = useCallback( @@ -148,14 +169,17 @@ export const Recycle = memo(({ list }: RecycleProps) => { key="reset" prefix={<ReloadOutlined className="icon" />} > - 还原 + {tg('button.restore')} </MenuListItem> <MenuListItem key="delelte" prefix={<DeleteOutlined className="icon" />} > - <Popconfirm title="确认删除?" onConfirm={del(key)}> - 删除 + <Popconfirm + title={tg('operation.deleteConfirm')} + onConfirm={del(key)} + > + {tg('button.delete')} </Popconfirm> </MenuListItem> </Menu> @@ -169,7 +193,7 @@ export const Recycle = memo(({ list }: RecycleProps) => { </TreeTitle> ); }, - [moreMenuClick, del, views, isOwner, permissionMap], + [moreMenuClick, del, views, isOwner, permissionMap, tg], ); const treeSelect = useCallback( diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/index.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/index.tsx index 3a55924a6..e5d6afa90 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/Sidebar/index.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 { CodeFilled, DeleteOutlined, @@ -8,6 +26,7 @@ import { } from '@ant-design/icons'; import { ListNav, ListPane, ListTitle } from 'app/components'; import { useDebouncedSearch } from 'app/hooks/useDebouncedSearch'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { CommonFormTypes } from 'globalConstants'; import React, { memo, useCallback, useContext, useMemo } from 'react'; @@ -15,8 +34,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import styled from 'styled-components/macro'; import { SPACE_XS } from 'styles/StyleConstants'; -import { getInsertedNodeIndex } from 'utils/utils'; -import { v4 as uuidv4 } from 'uuid'; +import { getInsertedNodeIndex, uuidv4 } from 'utils/utils'; import { UNPERSISTED_ID_PREFIX } from '../constants'; import { SaveFormContext } from '../SaveFormContext'; import { @@ -36,6 +54,7 @@ export const Sidebar = memo(() => { const orgId = useSelector(selectOrgId); const selectViewTree = useMemo(makeSelectViewTree, []); const viewsData = useSelector(selectViews); + const t = useI18NPrefix('view.sidebar'); const getIcon = useCallback( ({ isFolder }: ViewSimpleViewModel) => @@ -90,7 +109,7 @@ export const Sidebar = memo(() => { type: CommonFormTypes.Add, visible: true, simple: true, - parentIdLabel: '所属目录', + parentIdLabel: t('parent'), onSave: (values, onClose) => { let index = getInsertedNodeIndex(values, viewsData); @@ -111,19 +130,19 @@ export const Sidebar = memo(() => { break; } }, - [dispatch, history, orgId, showSaveForm, viewsData], + [dispatch, history, orgId, showSaveForm, viewsData, t], ); const titles = useMemo( () => [ { key: 'list', - title: '数据视图列表', + title: t('title'), search: true, add: { items: [ - { key: 'view', text: '新建数据视图' }, - { key: 'folder', text: '新建目录' }, + { key: 'view', text: t('addView') }, + { key: 'folder', text: t('addFolder') }, ], callback: add, }, @@ -131,7 +150,7 @@ export const Sidebar = memo(() => { items: [ { key: 'recycle', - text: '回收站', + text: t('recycle'), prefix: <DeleteOutlined className="icon" />, }, ], @@ -147,13 +166,13 @@ export const Sidebar = memo(() => { }, { key: 'recycle', - title: '回收站', + title: t('recycle'), back: true, search: true, onSearch: listSearch, }, ], - [add, treeSearch, listSearch], + [add, treeSearch, listSearch, t], ); return ( diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/components/SchemaTable.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/components/SchemaTable.tsx index 03453c64f..ddea2ecef 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/components/SchemaTable.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/components/SchemaTable.tsx @@ -1,17 +1,30 @@ +/** + * 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 { CalendarOutlined, FieldStringOutlined, NumberOutlined, } from '@ant-design/icons'; -import { - Dropdown, - Menu, - Table, - TableColumnType, - TableProps, - Tooltip, -} from 'antd'; +import { Dropdown, Menu, TableColumnType, TableProps, Tooltip } from 'antd'; import { ToolbarButton } from 'app/components'; +import { VirtualTable } from 'app/components/VirtualTable'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { memo, ReactElement, useMemo } from 'react'; import styled from 'styled-components/macro'; import { @@ -20,20 +33,15 @@ import { SPACE_XS, WARNING, } from 'styles/StyleConstants'; -import { v4 as uuidv4 } from 'uuid'; -import { - ColumnCategories, - ColumnTypes, - COLUMN_CATEGORY_LABEL, - COLUMN_TYPE_LABEL, -} from '../constants'; +import { uuidv4 } from 'utils/utils'; +import { ColumnCategories, ColumnTypes } from '../constants'; import { Column, Model } from '../slice/types'; import { getColumnWidthMap } from '../utils'; - const ROW_KEY = 'DATART_ROW_KEY'; interface SchemaTableProps extends TableProps<object> { height: number; + width: number; model: Model; dataSource?: object[]; hasCategory?: boolean; @@ -50,6 +58,7 @@ interface SchemaTableProps extends TableProps<object> { export const SchemaTable = memo( ({ height, + width: propsWidth, model, dataSource, hasCategory, @@ -65,6 +74,8 @@ export const SchemaTable = memo( () => getColumnWidthMap(model, dataSource || []), [model, dataSource], ); + const t = useI18NPrefix('view.schemaTable'); + const tg = useI18NPrefix('global'); const { columns, @@ -106,19 +117,21 @@ export const SchemaTable = memo( onClick={onSchemaTypeChange(name, column)} > {Object.values(ColumnTypes).map(t => ( - <Menu.Item key={t}>{COLUMN_TYPE_LABEL[t]}</Menu.Item> + <Menu.Item key={t}> + {tg(`columnType.${t.toLowerCase()}`)} + </Menu.Item> ))} {hasCategory && ( <> <Menu.Divider /> <Menu.SubMenu key="categories" - title="分类" + title={t('category')} popupClassName="datart-schema-table-header-menu" > {Object.values(ColumnCategories).map(t => ( <Menu.Item key={`category-${t}`}> - {COLUMN_CATEGORY_LABEL[t]} + {tg(`columnCategory.${t.toLowerCase()}`)} </Menu.Item> ))} </Menu.SubMenu> @@ -127,7 +140,7 @@ export const SchemaTable = memo( </Menu> } > - <Tooltip title={`类型${hasCategory ? '与分类' : ''}`}> + <Tooltip title={hasCategory ? t('typeAndCategory') : t('type')}> <ToolbarButton size="small" iconSize={FONT_SIZE_BASE} @@ -157,20 +170,20 @@ export const SchemaTable = memo( hasCategory, getExtraHeaderActions, onSchemaTypeChange, + t, + tg, ]); - return ( - <> - <Table - {...tableProps} - rowKey={ROW_KEY} - size="small" - components={{ header: { cell: TableHeader } }} - dataSource={dataSourceWithKey} - columns={columns} - scroll={{ x: tableWidth, y: height }} - /> - </> + <VirtualTable + {...tableProps} + rowKey={ROW_KEY} + size="small" + components={{ header: { cell: TableHeader } }} + dataSource={dataSourceWithKey} + columns={columns} + scroll={{ x: tableWidth, y: height }} + width={propsWidth} + /> ); }, ); diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/constants.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/constants.ts index 7288b488c..42a5df94b 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/constants.ts +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/constants.ts @@ -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. + */ + export enum ColumnTypes { String = 'STRING', Number = 'NUMERIC', @@ -38,22 +56,3 @@ export const UNPERSISTED_ID_PREFIX = 'GENERATED-'; export const DEFAULT_PREVIEW_SIZE = 1000; export const PREVIEW_SIZE_LIST = [100, 1000, 10000, 100000]; export const MAX_RESULT_TABLE_COLUMN_WIDTH = 480; - -export const COLUMN_TYPE_LABEL = { - [ColumnTypes.String]: '字符', - [ColumnTypes.Number]: '数值', - [ColumnTypes.Date]: '日期', -}; - -export const COLUMN_CATEGORY_LABEL = { - [ColumnCategories.Uncategorized]: '未分类', - [ColumnCategories.Country]: '国家', - [ColumnCategories.ProvinceOrState]: '省份', - [ColumnCategories.City]: '城市', - [ColumnCategories.County]: '区县', -}; - -export const CONCURRENCY_CONTROL_MODE_LABEL = { - [ConcurrencyControlModes.DirtyRead]: '延迟更新', - [ConcurrencyControlModes.FastFailOver]: '快速失败', -}; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/index.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/index.tsx index 6e579721b..8254e9527 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/index.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 { useSourceSlice } from '../SourcePage/slice'; import { Container } from './Container'; import { EditorContext, useEditorContext } from './EditorContext'; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/index.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/index.ts index 022536739..0a0f43bab 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/index.ts +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/index.ts @@ -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 { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { getDataProviderDatabases } from 'app/pages/MainPage/slice/thunks'; import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/selectors.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/selectors.ts index 951172f31..319d7255e 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/selectors.ts +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/selectors.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'types'; import { listToTree } from 'utils/utils'; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/thunks.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/thunks.ts index 17755f4b8..72fed6624 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/thunks.ts +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/thunks.ts @@ -1,6 +1,25 @@ +/** + * 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 { createAsyncThunk } from '@reduxjs/toolkit'; import sqlReservedWords from 'app/assets/javascripts/sqlReservedWords'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; +import i18n from 'i18next'; import { monaco } from 'react-monaco-editor'; import { RootState } from 'types'; import { request } from 'utils/request'; @@ -93,7 +112,7 @@ export const getViewDetail = createAsyncThunk< const viewSimple = views?.find(v => v.id === viewId); const tempViewModel = generateEditingView({ id: viewId, - name: viewSimple?.name || '加载中...', + name: viewSimple?.name || i18n.t('view.loading'), stage: ViewViewModelStages.Loading, }); dispatch(viewActions.addEditingView(tempViewModel)); @@ -141,7 +160,7 @@ export const runSql = createAsyncThunk< const { script, sourceId, size, fragment, variables } = currentEditingView; if (!sourceId) { - return rejectWithValue('请选择数据源'); + return rejectWithValue(i18n.t('view.selectSource')); } if (!script.trim()) { diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/types.ts b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/types.ts index 65b6397ca..31ad1e9e1 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/slice/types.ts +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/slice/types.ts @@ -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 { TreeDataNode, TreeNodeProps } from 'antd'; import { ReactElement } from 'react'; import { SubjectTypes } from '../../PermissionPage/constants'; diff --git a/frontend/src/app/pages/MainPage/pages/ViewPage/utils.tsx b/frontend/src/app/pages/MainPage/pages/ViewPage/utils.tsx index 9b92e5143..a896d8c7a 100644 --- a/frontend/src/app/pages/MainPage/pages/ViewPage/utils.tsx +++ b/frontend/src/app/pages/MainPage/pages/ViewPage/utils.tsx @@ -1,9 +1,26 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { FONT_WEIGHT_MEDIUM, SPACE_UNIT } from 'styles/StyleConstants'; import { getDiffParams, getTextWidth } from 'utils/utils'; import { ColumnCategories, DEFAULT_PREVIEW_SIZE, - MAX_RESULT_TABLE_COLUMN_WIDTH, UNPERSISTED_ID_PREFIX, ViewViewModelStages, } from './constants'; @@ -111,7 +128,7 @@ export function getColumnWidthMap( map[name] = dataSource.reduce((width, o) => { // column width return Math.min( - MAX_RESULT_TABLE_COLUMN_WIDTH, + // MAX_RESULT_TABLE_COLUMN_WIDTH, Math.max( width, map[name], diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/ChartPreviewBoard.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/ChartPreview.tsx similarity index 83% rename from frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/ChartPreviewBoard.tsx rename to frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/ChartPreview.tsx index 588a4d78d..c3bc22451 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/ChartPreviewBoard.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/ChartPreview.tsx @@ -19,21 +19,14 @@ import { message } from 'antd'; import ChartEditor from 'app/components/ChartEditor'; import { VizHeader } from 'app/components/VizHeader'; -import useResizeObserver from 'app/hooks/useResizeObserver'; +import { useCacheWidthHeight } from 'app/hooks/useCacheWidthHeight'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import Chart from 'app/pages/ChartWorkbenchPage/models/Chart'; import { ChartDataRequestBuilder } from 'app/pages/ChartWorkbenchPage/models/ChartHttpRequest'; import ChartManager from 'app/pages/ChartWorkbenchPage/models/ChartManager'; import { useMainSlice } from 'app/pages/MainPage/slice'; -// import { makeDownloadDataTask } from 'app/pages/MainPage/slice/thunks'; import { generateShareLinkAsync, makeDownloadDataTask } from 'app/utils/fetch'; -import { - CSSProperties, - FC, - memo, - useCallback, - useEffect, - useState, -} from 'react'; +import { FC, memo, useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components/macro'; import { BORDER_RADIUS, SPACE_LG } from 'styles/StyleConstants'; @@ -53,7 +46,6 @@ import ControllerPanel from './components/ControllerPanel'; const ChartPreviewBoard: FC<{ backendChartId: string; orgId: string; - style?: CSSProperties; filterSearchUrl?: string; allowDownload?: boolean; allowShare?: boolean; @@ -62,21 +54,13 @@ const ChartPreviewBoard: FC<{ ({ backendChartId, orgId, - style = { width: 300, height: 300 }, filterSearchUrl, allowDownload, allowShare, allowManage, }) => { useVizSlice(); - const { - ref, - width = style?.width, - height = style?.height, - } = useResizeObserver<HTMLDivElement>({ - refreshMode: 'throttle', - refreshRate: 500, - }); + const { ref, cacheW, cacheH } = useCacheWidthHeight(800, 600); const { actions } = useMainSlice(); const chartManager = ChartManager.instance(); const dispatch = useDispatch(); @@ -86,6 +70,7 @@ const ChartPreviewBoard: FC<{ const [chartPreview, setChartPreview] = useState<ChartPreview>(); const [chart, setChart] = useState<Chart>(); const [editChartVisible, setEditChartVisible] = useState<boolean>(false); + const t = useI18NPrefix('viz.main'); useEffect(() => { const filterSearchParams = filterSearchUrl @@ -135,29 +120,30 @@ const ChartPreviewBoard: FC<{ { name: 'click', callback: param => { - if (param.seriesName === 'paging') { - if (!chartPreview) { - return; - } - const page = param.value?.page; + if ( + param.componentType === 'table' && + param.seriesType === 'paging-sort-filter' + ) { dispatch( fetchDataSetByPreviewChartAction({ chartPreview: chartPreview!, - pageInfo: { pageNo: page }, + sorter: { + column: param?.seriesName!, + operator: param?.value?.direction, + }, + pageInfo: { + pageNo: param?.value?.pageNo, + }, }), ); return; } }, }, - { - name: 'dblclick', - callback: param => {}, - }, ]); }; - const hanldeGotoWorkbenchPage = () => { + const handleGotoWorkbenchPage = () => { setEditChartVisible(true); }; const onSaveInDataChart = useCallback( @@ -197,6 +183,7 @@ const ChartPreviewBoard: FC<{ if (!chartPreview) { return; } + const builder = new ChartDataRequestBuilder( { id: chartPreview?.backendChart?.viewId, @@ -205,6 +192,9 @@ const ChartPreviewBoard: FC<{ } as any, chartPreview?.chartConfig?.datas, chartPreview?.chartConfig?.settings, + {}, + false, + chartPreview?.backendChart?.config?.aggregation, ); dispatch( makeDownloadDataTask({ @@ -226,15 +216,15 @@ const ChartPreviewBoard: FC<{ publish: chartPreview.backendChart.status === 1 ? true : false, resolve: () => { message.success( - `${ - chartPreview.backendChart?.status === 2 ? '取消' : '' - }发布成功`, + chartPreview.backendChart?.status === 2 + ? t('unpublishSuccess') + : t('publishSuccess'), ); }, }), ); } - }, [dispatch, chartPreview?.backendChart]); + }, [dispatch, chartPreview?.backendChart, t]); return ( <StyledChartPreviewBoard> @@ -242,7 +232,7 @@ const ChartPreviewBoard: FC<{ chartName={chartPreview?.backendChart?.name} status={chartPreview?.backendChart?.status} publishLoading={publishLoading} - onGotoEdit={hanldeGotoWorkbenchPage} + onGotoEdit={handleGotoWorkbenchPage} onPublish={handlePublish} onGenerateShareLink={handleGenerateShareLink} onDownloadData={handleCreateDownloadDataTask} @@ -251,20 +241,23 @@ const ChartPreviewBoard: FC<{ allowManage={allowManage} /> <PreviewBlock> - <ControllerPanel - viewId={chartPreview?.backendChart?.viewId} - view={chartPreview?.backendChart?.view} - chartConfig={chartPreview?.chartConfig} - onChange={handleFilterChange} - /> + <div> + <ControllerPanel + viewId={chartPreview?.backendChart?.viewId} + view={chartPreview?.backendChart?.view} + chartConfig={chartPreview?.chartConfig} + onChange={handleFilterChange} + /> + </div> <ChartWrapper ref={ref}> - <ChartTools.ChartIFrame + <ChartTools.ChartIFrameContainer key={backendChartId} containerId={backendChartId} dataset={chartPreview?.dataset} chart={chart!} config={chartPreview?.chartConfig!} - style={{ width: width, height: height as number }} + width={cacheW} + height={cacheH} /> </ChartWrapper> </PreviewBlock> @@ -284,23 +277,24 @@ const ChartPreviewBoard: FC<{ ); export default ChartPreviewBoard; - const StyledChartPreviewBoard = styled.div` display: flex; flex: 1; flex-flow: column; + height: 100%; iframe { flex-grow: 1000; } `; - const PreviewBlock = styled.div` display: flex; flex: 1; flex-direction: column; + height: 100%; padding: ${SPACE_LG}; box-shadow: ${p => p.theme.shadowBlock}; + overflow: hidden; `; const ChartWrapper = styled.div` diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/ControllerPanel.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/ControllerPanel.tsx index 3737deeda..7203aec68 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/ControllerPanel.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/ControllerPanel.tsx @@ -35,6 +35,7 @@ import debounce from 'lodash/debounce'; import { FC, memo, useEffect, useState } from 'react'; import styled from 'styled-components/macro'; import { FONT_SIZE_LABEL, SPACE, SPACE_MD } from 'styles/StyleConstants'; +import { isEmptyArray } from 'utils/object'; import Filters, { PresentControllerFilterProps } from './components'; const AUTO_CONTROL_PANEL_COLS = { @@ -128,8 +129,8 @@ const ControllerPanel: FC<{ return <Filters.DropdownListFilter {...props} />; case ControllerFacadeTypes.MultiDropdownList: return <Filters.MultiDropdownListFilter {...props} />; - case ControllerFacadeTypes.RangeTime: - return <Filters.RangTimeFilter {...props} />; + case ControllerFacadeTypes.RangeTimePicker: + return <Filters.RangeTimePickerFilter {...props} />; case ControllerFacadeTypes.RangeValue: return <Filters.RangValueFilter {...props} />; case ControllerFacadeTypes.Text: @@ -149,28 +150,36 @@ const ControllerPanel: FC<{ } }; - return filters && filters.length ? ( - <Wrapper layout="vertical"> - <ControllerBlock> - {filters - ?.filter(config => checkFilterVisibility(config.filter)) - .map((config, index) => { - return ( - <Col - key={config.uid} - {...(config.filter?.width === 'auto' - ? { ...AUTO_CONTROL_PANEL_COLS } - : { span: config.filter?.width })} - > - <Form.Item label={getColumnRenderName(config)}> - {renderComponentByFacade(index, config)} - </Form.Item> - </Col> - ); - })} - </ControllerBlock> - </Wrapper> - ) : null; + const renderControls = () => { + const validFilters = filters + ?.filter(config => checkFilterVisibility(config.filter)) + .map((config, index) => { + return ( + <Col + key={config.uid} + {...(config.filter?.width === 'auto' + ? { ...AUTO_CONTROL_PANEL_COLS } + : { span: config.filter?.width })} + > + <Form.Item label={getColumnRenderName(config)}> + {renderComponentByFacade(index, config)} + </Form.Item> + </Col> + ); + }); + + if (isEmptyArray(validFilters)) { + return null; + } + + return ( + <Wrapper layout="vertical"> + <ControllerBlock>{validFilters}</ControllerBlock> + </Wrapper> + ); + }; + + return renderControls(); }); export default ControllerPanel; diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/DropdownListFilter.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/DropdownListFilter.tsx index bd3816403..22eaf4300 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/DropdownListFilter.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/DropdownListFilter.tsx @@ -18,7 +18,7 @@ import { Select } from 'antd'; import useFetchFilterDataByCondtion from 'app/hooks/useFetchFilterDataByCondtion'; -import { FilterValueOption } from 'app/types/ChartConfig'; +import { RelationFilterValue } from 'app/types/ChartConfig'; import { updateBy } from 'app/utils/mutation'; import { FC, memo, useState } from 'react'; import styled from 'styled-components/macro'; @@ -27,14 +27,14 @@ import { PresentControllerFilterProps } from '.'; const DropdownListFilter: FC<PresentControllerFilterProps> = memo( ({ viewId, view, condition, onConditionChange }) => { - const [originalNodes, setOriginalNodes] = useState<FilterValueOption[]>( - condition?.value as FilterValueOption[], + const [originalNodes, setOriginalNodes] = useState<RelationFilterValue[]>( + condition?.value as RelationFilterValue[], ); const [selectedNode, setSelectedNode] = useState<string>(() => { if (Array.isArray(condition?.value)) { const firstValue = (condition?.value as [])?.find(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; }); diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/MultiDropdownListFilter.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/MultiDropdownListFilter.tsx index 700679438..77094b054 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/MultiDropdownListFilter.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/MultiDropdownListFilter.tsx @@ -18,7 +18,7 @@ import { TreeSelect } from 'antd'; import useFetchFilterDataByCondtion from 'app/hooks/useFetchFilterDataByCondtion'; -import { FilterValueOption } from 'app/types/ChartConfig'; +import { RelationFilterValue } from 'app/types/ChartConfig'; import { updateBy } from 'app/utils/mutation'; import { FC, memo, useMemo, useState } from 'react'; import styled from 'styled-components/macro'; @@ -27,8 +27,8 @@ import { PresentControllerFilterProps } from '.'; const MultiDropdownListFilter: FC<PresentControllerFilterProps> = memo( ({ viewId, view, condition, onConditionChange }) => { - const [originalNodes, setOriginalNodes] = useState<FilterValueOption[]>( - condition?.value as FilterValueOption[], + const [originalNodes, setOriginalNodes] = useState<RelationFilterValue[]>( + condition?.value as RelationFilterValue[], ); useFetchFilterDataByCondtion(viewId, condition, setOriginalNodes, view); @@ -45,12 +45,12 @@ const MultiDropdownListFilter: FC<PresentControllerFilterProps> = memo( if (typeof item === 'object') { 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; }) || []; - return firstValues?.map((n: FilterValueOption) => n.key); + return firstValues?.map((n: RelationFilterValue) => n.key); } else { return condition?.value || []; } @@ -64,10 +64,10 @@ const MultiDropdownListFilter: FC<PresentControllerFilterProps> = memo( multiple treeCheckable treeDefaultExpandAll - value={selectedNodes} + value={selectedNodes as any} onChange={handleSelectedChange} > - {originalNodes?.map((n: FilterValueOption) => { + {originalNodes?.map((n: RelationFilterValue) => { return ( <TreeSelect.TreeNode key={n.key} value={n.key} title={n.label} /> ); diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RadioGroupFilter.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RadioGroupFilter.tsx index 758f9b04f..b6c6d5a01 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RadioGroupFilter.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RadioGroupFilter.tsx @@ -18,7 +18,7 @@ import { Radio } from 'antd'; import useFetchFilterDataByCondtion from 'app/hooks/useFetchFilterDataByCondtion'; -import { FilterValueOption } from 'app/types/ChartConfig'; +import { RelationFilterValue } from 'app/types/ChartConfig'; import { ControllerRadioFacadeTypes } from 'app/types/FilterControlPanel'; import { updateBy } from 'app/utils/mutation'; import { FC, memo, useState } from 'react'; @@ -27,14 +27,14 @@ import { PresentControllerFilterProps } from '.'; const RadioGroupFilter: FC<PresentControllerFilterProps> = memo( ({ viewId, view, condition, options, onConditionChange }) => { - const [originalNodes, setOriginalNodes] = useState<FilterValueOption[]>( - condition?.value as FilterValueOption[], + const [originalNodes, setOriginalNodes] = useState<RelationFilterValue[]>( + condition?.value as RelationFilterValue[], ); const [selectedNode, setSelectedNode] = useState<string>(() => { if (Array.isArray(condition?.value)) { const firstValue = (condition?.value as [])?.find(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; }); @@ -58,7 +58,7 @@ const RadioGroupFilter: FC<PresentControllerFilterProps> = memo( }; const renderChildrenByRadioType = type => { - const _getProps = (n: FilterValueOption) => ({ + const _getProps = (n: RelationFilterValue) => ({ key: n.key, value: n.key, checked: n.key === selectedNode, diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangTimeFilter.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangTimeFilter.tsx deleted file mode 100644 index 1e2924211..000000000 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangTimeFilter.tsx +++ /dev/null @@ -1,52 +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 { DatePicker } from 'antd'; -import { updateBy } from 'app/utils/mutation'; -import moment from 'moment'; -import { FC, memo, useState } from 'react'; -import { PresentControllerFilterProps } from '.'; - -const RangTimeFilter: FC<PresentControllerFilterProps> = memo( - ({ condition, onConditionChange }) => { - const [timeRange, setTimeRange] = useState<string[]>(() => { - if (Array.isArray(condition?.value)) { - return condition?.value as string[]; - } - return []; - }); - - const handleDateChange = (times: any) => { - const newCondition = updateBy(condition!, draft => { - draft.value = (times || []).map(d => d.toString()); - }); - onConditionChange(newCondition); - setTimeRange(newCondition.value as string[]); - }; - - return ( - <DatePicker.RangePicker - showTime - value={[moment(timeRange?.[0]), moment(timeRange?.[1])]} - onChange={handleDateChange} - /> - ); - }, -); - -export default RangTimeFilter; diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangeTimeFilter.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangeTimeFilter.tsx new file mode 100644 index 000000000..128655ed0 --- /dev/null +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangeTimeFilter.tsx @@ -0,0 +1,69 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Space } from 'antd'; +import TimeSelector from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector'; +import { ConditionBuilder } from 'app/pages/ChartWorkbenchPage/models/ChartFilterCondition'; +import { FilterConditionType } from 'app/types/ChartConfig'; +import { FC, memo, useState } from 'react'; +import { PresentControllerFilterProps } from '.'; + +const RangeTimeFilter: FC<PresentControllerFilterProps> = memo( + ({ condition, onConditionChange }) => { + const i18NPrefix = 'viz.common.filter.date'; + const [rangeTimes, setRangeTimes] = useState(() => { + if (condition?.type === FilterConditionType.RangeTime) { + const startTime = condition?.value?.[0]; + const endTime = condition?.value?.[1]; + return [startTime, endTime]; + } + return []; + }); + + const handleTimeChange = index => time => { + rangeTimes[index] = time; + setRangeTimes(rangeTimes); + + const filterRow = new ConditionBuilder(condition) + .setValue(rangeTimes || []) + .asRangeTime(); + onConditionChange?.(filterRow); + }; + + return ( + <div> + <Space direction="vertical" size={12}> + <TimeSelector.ManualSingleTimeSelector + i18nPrefix={i18NPrefix} + time={rangeTimes?.[0] as any} + isStart={true} + onTimeChange={handleTimeChange(0)} + /> + <TimeSelector.ManualSingleTimeSelector + i18nPrefix={i18NPrefix} + time={rangeTimes?.[1] as any} + isStart={false} + onTimeChange={handleTimeChange(1)} + /> + </Space> + </div> + ); + }, +); + +export default RangeTimeFilter; diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangeTimePickerFilter.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangeTimePickerFilter.tsx new file mode 100644 index 000000000..cdd53284a --- /dev/null +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RangeTimePickerFilter.tsx @@ -0,0 +1,75 @@ +/** + * 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 { ConditionBuilder } from 'app/pages/ChartWorkbenchPage/models/ChartFilterCondition'; +import { FilterConditionType } from 'app/types/ChartConfig'; +import { + formatTime, + getTime, + recommendTimeRangeConverter, +} from 'app/utils/time'; +import { FILTER_TIME_FORMATTER_IN_QUERY } from 'globalConstants'; +import moment from 'moment'; +import { FC, memo, useMemo } from 'react'; +import { PresentControllerFilterProps } from '.'; +const { RangePicker } = DatePicker; + +const toMoment = t => { + if (!t) { + return moment(); + } + if (Boolean(t) && typeof t === 'object' && 'unit' in t) { + const time = getTime(+(t.direction + t.amount), t.unit)(t.unit, t.isStart); + return moment(formatTime(time, FILTER_TIME_FORMATTER_IN_QUERY)); + } + return moment(t); +}; + +const RangeTimePickerFilter: FC<PresentControllerFilterProps> = memo( + ({ condition, onConditionChange }) => { + const handleTimeChange = (moments, dateStrings) => { + const filterRow = new ConditionBuilder(condition) + .setValue(dateStrings) + .asRangeTime(); + onConditionChange?.(filterRow); + }; + + const rangeTimes = useMemo(() => { + if (condition?.type === FilterConditionType.RangeTime) { + const startTime = toMoment(condition?.value?.[0]); + const endTime = toMoment(condition?.value?.[1]); + return [startTime, endTime]; + } + if (condition?.type === FilterConditionType.RecommendTime) { + return recommendTimeRangeConverter(condition?.value)?.map(toMoment); + } + return [moment(), moment()]; + }, [condition?.type, condition?.value]); + + return ( + <RangePicker + showTime + value={rangeTimes as any} + onChange={handleTimeChange} + /> + ); + }, +); + +export default RangeTimePickerFilter; diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RecommendTimeFilter.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RecommendTimeFilter.tsx new file mode 100644 index 000000000..ceb649748 --- /dev/null +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/RecommendTimeFilter.tsx @@ -0,0 +1,36 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import TimeSelector from 'app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartTimeSelector'; +import { FC, memo } from 'react'; +import { PresentControllerFilterProps } from '.'; + +const RecommendTimeFilter: FC<PresentControllerFilterProps> = memo( + ({ condition, onConditionChange }) => { + const i18NPrefix = 'viz.common.filter.date'; + return ( + <TimeSelector.RecommendRangeTimeSelector + i18nPrefix={i18NPrefix} + condition={condition} + onConditionChange={onConditionChange} + /> + ); + }, +); + +export default RecommendTimeFilter; diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/index.ts b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/index.ts index 2c5566cd5..3c1d24bb2 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/index.ts +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/ControllerPanel/components/index.ts @@ -21,8 +21,10 @@ import { FilterCondition } from 'app/types/ChartConfig'; import DropdownListFilter from './DropdownListFilter'; import MultiDropdownListFilter from './MultiDropdownListFilter'; import RadioGroupFilter from './RadioGroupFilter'; -import RangTimeFilter from './RangTimeFilter'; +import RangeTimeFilter from './RangeTimeFilter'; +import RangeTimePickerFilter from './RangeTimePickerFilter'; import RangValueFilter from './RangValueFilter'; +import RecommendTimeFilter from './RecommendTimeFilter'; import SliderFilter from './SliderFilter'; import TextFilter from './TextFilter'; import TimeFilter from './TimeFilter'; @@ -41,7 +43,9 @@ const Filters = { DropdownListFilter, MultiDropdownListFilter, RadioGroupFilter, - RangTimeFilter, + RangeTimeFilter, + RangeTimePickerFilter, + RecommendTimeFilter, RangValueFilter, SliderFilter, TextFilter, diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/PreviewHeader.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/PreviewHeader.tsx index 0b6f9cb2a..91477b899 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/PreviewHeader.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/components/PreviewHeader.tsx @@ -63,7 +63,7 @@ const PreviewHeader: FC<{ onGotoEdit, onGenerateShareLink, }) => { - const t = useI18NPrefix(`viz.chartPreview`); + const t = useI18NPrefix(`viz.action`); const [expireDate, setExpireDate] = useState<string>(); const [enablePassword, setEnablePassword] = useState(false); const [showShareLinkModal, setShowShareLinkModal] = useState(false); @@ -85,7 +85,7 @@ const PreviewHeader: FC<{ return <Menu>{menus}</Menu>; }; - const hanldeCopyToClipboard = value => { + const handleCopyToClipboard = value => { const ta = document.createElement('textarea'); ta.innerText = value; document.body.appendChild(ta); @@ -164,7 +164,7 @@ const PreviewHeader: FC<{ addonAfter={ <CopyOutlined onClick={() => - hanldeCopyToClipboard(getFullShareLinkPath(shareLink)) + handleCopyToClipboard(getFullShareLinkPath(shareLink)) } /> } @@ -177,7 +177,7 @@ const PreviewHeader: FC<{ addonAfter={ <CopyOutlined onClick={() => - hanldeCopyToClipboard(shareLink?.password) + handleCopyToClipboard(shareLink?.password) } /> } diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/index.ts b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/index.ts index 91ab61921..e4b9db2ac 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/index.ts +++ b/frontend/src/app/pages/MainPage/pages/VizPage/ChartPreview/index.ts @@ -16,4 +16,4 @@ * limitations under the License. */ -export { default as ChartPreviewBoard } from './ChartPreviewBoard'; +export { default as ChartPreview } from './ChartPreview'; diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/Main/VizContainer.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/Main/VizContainer.tsx index 9019d7a1c..e0e9b81cc 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/Main/VizContainer.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/Main/VizContainer.tsx @@ -10,7 +10,7 @@ import { ResourceTypes, VizResourceSubTypes, } from '../../PermissionPage/constants'; -import { ChartPreviewBoard } from '../ChartPreview'; +import { ChartPreview } from '../ChartPreview'; import { FolderViewModel, VizTab } from '../slice/types'; interface VizContainerProps { @@ -70,7 +70,7 @@ export const VizContainer = memo( break; case 'DATACHART': content = ( - <ChartPreviewBoard + <ChartPreview key={id} backendChartId={id} orgId={orgId} diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/Main/index.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/Main/index.tsx index 4d599bddb..feab2ad31 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/Main/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/Main/index.tsx @@ -6,6 +6,7 @@ import { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'; import styled from 'styled-components/macro'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { STICKY_LEVEL } from 'styles/StyleConstants'; import { useVizSlice } from '../slice'; import { @@ -39,6 +40,7 @@ export function Main() { const selectedTab = useSelector(selectSelectedTab); const orgId = useSelector(selectOrgId); const playingStoryId = useSelector(selectPlayingStoryId); + const t = useI18NPrefix('viz.main'); useEffect(() => { if (vizId) { @@ -173,7 +175,7 @@ export function Main() { selectedId={selectedTab?.id} /> ))} - {!tabs.length && <EmptyFiller title="请在左侧列表选择可视化" />} + {!tabs.length && <EmptyFiller title={t('empty')} />} {playingStoryId && <StoryPlayer storyId={playingStoryId} />} </Wrapper> diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/SaveForm.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/SaveForm.tsx index 35e2e5ad1..872d47324 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/SaveForm.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/SaveForm.tsx @@ -1,5 +1,6 @@ import { Form, FormInstance, Input, Radio, TreeSelect } from 'antd'; import { ModalForm, ModalFormProps } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { BoardTypeMap } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import debounce from 'debounce-promise'; import { CommonFormTypes, DEFAULT_DEBOUNCE_WAIT } from 'globalConstants'; @@ -21,13 +22,6 @@ import { selectSaveStoryboardLoading, } from './slice/selectors'; -const VIZ_TYPE_TITLES = { - DATACHART: '数据图表', - DASHBOARD: '仪表板', - FOLDER: '目录', - STORYBOARD: '故事板', -}; - type SaveFormProps = Omit<ModalFormProps, 'type' | 'visible' | 'onSave'>; export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { @@ -47,6 +41,8 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { const isOwner = useSelector(selectIsOrgOwner); const permissionMap = useSelector(selectPermissionMap); const formRef = useRef<FormInstance>(); + const t = useI18NPrefix('viz.saveForm'); + const tg = useI18NPrefix('global'); const getDisabled = useCallback( (_, path: string[]) => @@ -85,7 +81,7 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { <ModalForm formProps={formProps} {...modalProps} - title={VIZ_TYPE_TITLES[vizType]} + title={t(`vizType.${vizType.toLowerCase()}`)} type={type} visible={visible} confirmLoading={saveFolderLoading || saveStoryboardLoading} @@ -99,9 +95,12 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { </IdField> <Form.Item name="name" - label="名称" + label={t('name')} rules={[ - { required: true, message: '名称不能为空' }, + { + required: true, + message: `${t('name')}${tg('validation.required')}`, + }, { validator: debounce((_, value) => { if (!value || initialValues?.name === value) { @@ -119,7 +118,7 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { }, }).then( () => Promise.resolve(), - () => Promise.reject(new Error('名称重复')), + err => Promise.reject(new Error(err.response.data.message)), ); }, DEFAULT_DEBOUNCE_WAIT), }, @@ -128,21 +127,32 @@ export function SaveForm({ formProps, ...modalProps }: SaveFormProps) { <Input /> </Form.Item> {vizType === 'DATACHART' && ( - <Form.Item name="description" label="描述"> + <Form.Item name="description" label={t('description')}> <Input.TextArea /> </Form.Item> )} {vizType === 'DASHBOARD' && type === CommonFormTypes.Add && ( - <Form.Item name="boardType" label="布局类型"> + <Form.Item name="boardType" label={t('boardType.label')}> <Radio.Group> - <Radio.Button value={BoardTypeMap.auto}>自动</Radio.Button> - <Radio.Button value={BoardTypeMap.free}>自由</Radio.Button> + <Radio.Button value={BoardTypeMap.auto}> + {t('boardType.auto')} + </Radio.Button> + <Radio.Button value={BoardTypeMap.free}> + {t('boardType.free')} + </Radio.Button> </Radio.Group> </Form.Item> )} {vizType !== 'STORYBOARD' && ( - <Form.Item name="parentId" label="所属目录"> - <TreeSelect placeholder="根目录" treeData={treeData} allowClear /> + <Form.Item name="parentId" label={t('parent')}> + <TreeSelect + placeholder={t('root')} + treeData={treeData} + allowClear + onChange={() => { + formRef.current?.validateFields(); + }} + /> </Form.Item> )} </ModalForm> diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/FolderTree.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/FolderTree.tsx index bee1ef7e0..b92112812 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/FolderTree.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/FolderTree.tsx @@ -1,6 +1,7 @@ import { DeleteOutlined, EditOutlined, MoreOutlined } from '@ant-design/icons'; import { Menu, message, Popconfirm } from 'antd'; import { MenuListItem, Popup, Tree, TreeTitle } from 'app/components'; +import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; import { CascadeAccess } from 'app/pages/MainPage/Access'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { LocalTreeDataNode } from 'app/pages/MainPage/slice/types'; @@ -22,18 +23,24 @@ import { getFolders, removeTab, } from '../../slice/thunks'; -interface FolderTreeProps { + +interface FolderTreeProps extends I18NComponentProps { selectedId?: string; treeData?: LocalTreeDataNode[]; } -export function FolderTree({ selectedId, treeData }: FolderTreeProps) { +export function FolderTree({ + selectedId, + treeData, + i18nPrefix, +}: FolderTreeProps) { const dispatch = useDispatch(); const history = useHistory(); const orgId = useSelector(selectOrgId); const loading = useSelector(selectVizListLoading); const vizsData = useSelector(selectVizs); const { showSaveForm } = useContext(SaveFormContext); + const tg = useI18NPrefix('global'); useEffect(() => { dispatch(getFolders(orgId)); @@ -64,12 +71,12 @@ export function FolderTree({ selectedId, treeData }: FolderTreeProps) { () => { let id = folderId; let archive = false; - let msg = '成功删除'; + let msg = tg('operation.deleteSuccess'); if (['DASHBOARD', 'DATACHART'].includes(relType)) { id = relId; archive = true; - msg = '成功移至回收站'; + msg = tg('operation.archiveSuccess'); } dispatch( deleteViz({ @@ -82,7 +89,7 @@ export function FolderTree({ selectedId, treeData }: FolderTreeProps) { }), ); }, - [dispatch, redirect], + [dispatch, redirect, tg], ); const moreMenuClick = useCallback( @@ -147,19 +154,23 @@ export function FolderTree({ selectedId, treeData }: FolderTreeProps) { key="info" prefix={<EditOutlined className="icon" />} > - 基本信息 + {tg('button.info')} </MenuListItem> <MenuListItem key="delete" prefix={<DeleteOutlined className="icon" />} > <Popconfirm - title={`确定${ - node.relType === 'FOLDER' ? '删除' : '移至回收站' + title={`${ + node.relType === 'FOLDER' + ? tg('operation.deleteConfirm') + : tg('operation.archiveConfirm') }?`} onConfirm={archiveViz(node)} > - {node.relType === 'FOLDER' ? '删除' : '移至回收站'} + {node.relType === 'FOLDER' + ? tg('button.delete') + : tg('button.archive')} </Popconfirm> </MenuListItem> </Menu> @@ -173,7 +184,7 @@ export function FolderTree({ selectedId, treeData }: FolderTreeProps) { </TreeTitle> ); }, - [moreMenuClick, archiveViz], + [moreMenuClick, archiveViz, tg], ); const onDrop = info => { diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/index.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/index.tsx index 5a016f570..ff0211562 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Folders/index.tsx @@ -7,6 +7,7 @@ import { } from '@ant-design/icons'; import { ListNav, ListPane, ListTitle } from 'app/components'; import { useDebouncedSearch } from 'app/hooks/useDebouncedSearch'; +import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; import { BoardTypeMap } from 'app/pages/DashBoardPage/pages/Board/slice/types'; import { getInitBoardConfig } from 'app/pages/DashBoardPage/utils/board'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; @@ -33,161 +34,176 @@ import { import { FolderViewModel, VizType } from '../../slice/types'; import { Recycle } from '../Recycle'; import { FolderTree } from './FolderTree'; -interface FoldersProps { + +interface FoldersProps extends I18NComponentProps { selectedId?: string; className?: string; } -export const Folders = memo(({ selectedId, className }: FoldersProps) => { - const dispatch = useDispatch(); - const orgId = useSelector(selectOrgId); - const { showSaveForm } = useContext(SaveFormContext); - const selectVizTree = useMemo(makeSelectVizTree, []); - const vizsData = useSelector(selectVizs); - const getInitValues = useCallback((relType: VizType) => { - if (relType === 'DASHBOARD') { - return { - name: '', - boardType: BoardTypeMap.auto, - } as SaveFormModel; - } - return undefined; - }, []); +export const Folders = memo( + ({ selectedId, className, i18nPrefix }: FoldersProps) => { + const dispatch = useDispatch(); + const orgId = useSelector(selectOrgId); + const { showSaveForm } = useContext(SaveFormContext); + const selectVizTree = useMemo(makeSelectVizTree, []); + const vizsData = useSelector(selectVizs); + const t = useI18NPrefix(i18nPrefix); + const getInitValues = useCallback((relType: VizType) => { + if (relType === 'DASHBOARD') { + return { + name: '', + boardType: BoardTypeMap.auto, + } as SaveFormModel; + } + return undefined; + }, []); - const updateValue = useCallback((relType: VizType, values: SaveFormModel) => { - const dataValues = values; - if (relType === 'DASHBOARD') { - dataValues.config = JSON.stringify( - getInitBoardConfig(values.boardType || BoardTypeMap.auto), - ); - } - return dataValues; - }, []); + const updateValue = useCallback( + (relType: VizType, values: SaveFormModel) => { + const dataValues = values; + if (relType === 'DASHBOARD') { + dataValues.config = JSON.stringify( + getInitBoardConfig(values.boardType || BoardTypeMap.auto), + ); + } + return dataValues; + }, + [], + ); - const getIcon = useCallback(({ relType }: FolderViewModel) => { - switch (relType) { - case 'DASHBOARD': - return <FundFilled />; - case 'DATACHART': - return <BarChartOutlined />; - default: - return p => (p.expanded ? <FolderOpenFilled /> : <FolderFilled />); - } - }, []); - const getDisabled = useCallback( - ({ deleteLoading }: FolderViewModel) => deleteLoading, - [], - ); + const getIcon = useCallback(({ relType }: FolderViewModel) => { + switch (relType) { + case 'DASHBOARD': + return <FundFilled />; + case 'DATACHART': + return <BarChartOutlined />; + default: + return p => (p.expanded ? <FolderOpenFilled /> : <FolderFilled />); + } + }, []); + const getDisabled = useCallback( + ({ deleteLoading }: FolderViewModel) => deleteLoading, + [], + ); - const treeData = useSelector(state => - selectVizTree(state, { getIcon, getDisabled }), - ); - const { filteredData: filteredTreeData, debouncedSearch: treeSearch } = - useDebouncedSearch(treeData, (keywords, d) => - d.title.toLowerCase().includes(keywords.toLowerCase()), + const treeData = useSelector(state => + selectVizTree(state, { getIcon, getDisabled }), ); - const archivedDatacharts = useSelector(selectArchivedDatacharts); - const archivedDashboards = useSelector(selectArchivedDashboards); - const archivedDatachartloading = useSelector(selectArchivedDatachartLoading); - const archivedDashboardloading = useSelector(selectArchivedDashboardLoading); - const { filteredData: filteredListData, debouncedSearch: listSearch } = - useDebouncedSearch( - archivedDatacharts.concat(archivedDashboards), - (keywords, d) => d.name.toLowerCase().includes(keywords.toLowerCase()), + const { filteredData: filteredTreeData, debouncedSearch: treeSearch } = + useDebouncedSearch(treeData, (keywords, d) => + d.title.toLowerCase().includes(keywords.toLowerCase()), + ); + const archivedDatacharts = useSelector(selectArchivedDatacharts); + const archivedDashboards = useSelector(selectArchivedDashboards); + const archivedDatachartloading = useSelector( + selectArchivedDatachartLoading, + ); + const archivedDashboardloading = useSelector( + selectArchivedDashboardLoading, ); + const { filteredData: filteredListData, debouncedSearch: listSearch } = + useDebouncedSearch( + archivedDatacharts.concat(archivedDashboards), + (keywords, d) => d.name.toLowerCase().includes(keywords.toLowerCase()), + ); - const recycleInit = useCallback(() => { - dispatch(getArchivedDatacharts(orgId)); - dispatch(getArchivedDashboards(orgId)); - }, [dispatch, orgId]); + const recycleInit = useCallback(() => { + dispatch(getArchivedDatacharts(orgId)); + dispatch(getArchivedDashboards(orgId)); + }, [dispatch, orgId]); - const add = useCallback( - ({ key }) => { - showSaveForm({ - vizType: key, - type: CommonFormTypes.Add, - visible: true, - initialValues: getInitValues(key), - onSave: (values, onClose) => { - const dataValues = updateValue(key, values); + const add = useCallback( + ({ key }) => { + showSaveForm({ + vizType: key, + type: CommonFormTypes.Add, + visible: true, + initialValues: getInitValues(key), + onSave: (values, onClose) => { + const dataValues = updateValue(key, values); - let index = getInsertedNodeIndex(values, vizsData); + let index = getInsertedNodeIndex(values, vizsData); - dispatch( - addViz({ - viz: { ...dataValues, orgId: orgId, index: index }, - type: key, - resolve: onClose, - }), - ); - }, - }); - }, - [showSaveForm, getInitValues, updateValue, dispatch, orgId, vizsData], - ); + dispatch( + addViz({ + viz: { ...dataValues, orgId: orgId, index: index }, + type: key, + resolve: onClose, + }), + ); + }, + }); + }, + [showSaveForm, getInitValues, updateValue, dispatch, orgId, vizsData], + ); - const titles = useMemo( - () => [ - { - subTitle: '仪表板 & 数据图表', - add: { - items: [ - { key: 'DASHBOARD', text: '新建仪表板' }, - { key: 'DATACHART', text: '新建数据图表' }, - { key: 'FOLDER', text: '新建目录' }, - ], - callback: add, - }, - more: { - items: [ - { - key: 'recycle', - text: '回收站', - prefix: <DeleteOutlined className="icon" />, + const titles = useMemo( + () => [ + { + subTitle: t('folders.folderTitle'), + add: { + items: [ + { key: 'DASHBOARD', text: t('folders.dashboard') }, + { key: 'DATACHART', text: t('folders.dataChart') }, + { key: 'FOLDER', text: t('folders.folder') }, + ], + callback: add, + }, + more: { + items: [ + { + key: 'recycle', + text: t('folders.recycle'), + prefix: <DeleteOutlined className="icon" />, + }, + ], + callback: (key, _, onNext) => { + switch (key) { + case 'recycle': + onNext(); + break; + } }, - ], - callback: (key, _, onNext) => { - switch (key) { - case 'recycle': - onNext(); - break; - } }, + search: true, + onSearch: treeSearch, }, - search: true, - onSearch: treeSearch, - }, - { - key: 'recycle', - subTitle: '回收站', - back: true, - search: true, - onSearch: listSearch, - }, - ], - [add, treeSearch, listSearch], - ); + { + key: 'recycle', + subTitle: t('folders.recycle'), + back: true, + search: true, + onSearch: listSearch, + }, + ], + [add, treeSearch, listSearch, t], + ); - return ( - <Wrapper className={className} defaultActiveKey="list"> - <ListPane key="list"> - <ListTitle {...titles[0]} /> - <FolderTree treeData={filteredTreeData} selectedId={selectedId} /> - </ListPane> - <ListPane key="recycle"> - <ListTitle {...titles[1]} /> - <Recycle - type="viz" - orgId={orgId} - list={filteredListData} - listLoading={archivedDashboardloading || archivedDatachartloading} - selectedId={selectedId} - onInit={recycleInit} - /> - </ListPane> - </Wrapper> - ); -}); + return ( + <Wrapper className={className} defaultActiveKey="list"> + <ListPane key="list"> + <ListTitle {...titles[0]} /> + <FolderTree + treeData={filteredTreeData} + selectedId={selectedId} + i18nPrefix={i18nPrefix} + /> + </ListPane> + <ListPane key="recycle"> + <ListTitle {...titles[1]} /> + <Recycle + type="viz" + orgId={orgId} + list={filteredListData} + listLoading={archivedDashboardloading || archivedDatachartloading} + selectedId={selectedId} + onInit={recycleInit} + /> + </ListPane> + </Wrapper> + ); + }, +); const Wrapper = styled(ListNav)` display: flex; diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Recycle.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Recycle.tsx index 1f91717dd..4f8674a38 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Recycle.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Recycle.tsx @@ -6,6 +6,7 @@ import { } from '@ant-design/icons'; import { Button, List, Menu, message, Popconfirm } from 'antd'; import { ListItem, MenuListItem, Popup } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { calcAc, getCascadeAccess } from 'app/pages/MainPage/Access'; import { selectIsOrgOwner, @@ -45,6 +46,7 @@ export const Recycle = memo( const vizs = useSelector(selectVizs); const isOwner = useSelector(selectIsOrgOwner); const permissionMap = useSelector(selectPermissionMap); + const tg = useI18NPrefix('global'); useEffect(() => { onInit(); @@ -69,13 +71,13 @@ export const Recycle = memo( params: { id, archive: false }, type, resolve: () => { - message.success('删除成功'); + message.success(tg('operation.deleteSuccess')); dispatch(removeTab({ id, resolve: redirect })); }, }), ); }, - [dispatch, redirect], + [dispatch, redirect, tg], ); const moreMenuClick = useCallback( @@ -102,7 +104,7 @@ export const Recycle = memo( index, }, resolve: () => { - message.success('还原成功'); + message.success(tg('operation.restoreSuccess')); dispatch(removeTab({ id, resolve: redirect })); onClose(); }, @@ -115,7 +117,7 @@ export const Recycle = memo( break; } }, - [dispatch, showSaveForm, redirect, vizs], + [dispatch, showSaveForm, redirect, vizs, tg], ); const toDetail = useCallback( @@ -180,17 +182,17 @@ export const Recycle = memo( key="reset" prefix={<ReloadOutlined className="icon" />} > - 还原 + {tg('button.restore')} </MenuListItem> <MenuListItem key="delelte" prefix={<DeleteOutlined className="icon" />} > <Popconfirm - title="确认删除?" + title={tg('operation.deleteConfirm')} onConfirm={del(id, vizType)} > - 删除 + {tg('button.delete')} </Popconfirm> </MenuListItem> </Menu> diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Storyboards/List.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Storyboards/List.tsx index 6e6055649..7f5061b1d 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Storyboards/List.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Storyboards/List.tsx @@ -6,6 +6,7 @@ import { } from '@ant-design/icons'; import { Button, List as ListComponent, Menu, message, Popconfirm } from 'antd'; import { ListItem, MenuListItem, Popup } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { Access } from 'app/pages/MainPage/Access'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import classNames from 'classnames'; @@ -37,6 +38,7 @@ export const List = memo(({ list, selectedId }: StoryboardListProps) => { const listLoading = useSelector(selectStoryboardListLoading); const orgId = useSelector(selectOrgId); const { showSaveForm } = useContext(SaveFormContext); + const tg = useI18NPrefix('global'); useEffect(() => { dispatch(getStoryboards(orgId)); @@ -67,13 +69,13 @@ export const List = memo(({ list, selectedId }: StoryboardListProps) => { params: { id, archive: true }, type: 'STORYBOARD', resolve: () => { - message.success('成功移至回收站'); + message.success(tg('operation.archiveSuccess')); dispatch(removeTab({ id, resolve: redirect })); }, }), ); }, - [dispatch, redirect], + [dispatch, redirect, tg], ); const moreMenuClick = useCallback( @@ -128,17 +130,17 @@ export const List = memo(({ list, selectedId }: StoryboardListProps) => { key="info" prefix={<EditOutlined className="icon" />} > - 基本信息 + {tg('button.info')} </MenuListItem> <MenuListItem key="delete" prefix={<DeleteOutlined className="icon" />} > <Popconfirm - title={`确定移至回收站?`} + title={tg('operation.archiveConfirm')} onConfirm={archiveStoryboard(s.id)} > - 移至回收站 + {tg('button.archive')} </Popconfirm> </MenuListItem> </Menu> diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Storyboards/index.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Storyboards/index.tsx index ce0818aef..2dce5efe5 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Storyboards/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/Storyboards/index.tsx @@ -1,6 +1,7 @@ import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; import { ListNav, ListPane, ListTitle } from 'app/components'; import { useDebouncedSearch } from 'app/hooks/useDebouncedSearch'; +import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; import { useAccess } from 'app/pages/MainPage/Access'; import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; import { CommonFormTypes } from 'globalConstants'; @@ -19,110 +20,115 @@ import { allowCreateStoryboard } from '../../utils'; import { Recycle } from '../Recycle'; import { List } from './List'; -interface FoldersProps { +interface FoldersProps extends I18NComponentProps { selectedId?: string; className?: string; } -export const Storyboards = memo(({ selectedId, className }: FoldersProps) => { - const dispatch = useDispatch(); - const orgId = useSelector(selectOrgId); - const { showSaveForm } = useContext(SaveFormContext); - const list = useSelector(selectStoryboards); - const allowCreate = useAccess(allowCreateStoryboard()); +export const Storyboards = memo( + ({ selectedId, className, i18nPrefix }: FoldersProps) => { + const dispatch = useDispatch(); + const orgId = useSelector(selectOrgId); + const { showSaveForm } = useContext(SaveFormContext); + const list = useSelector(selectStoryboards); + const allowCreate = useAccess(allowCreateStoryboard()); + const t = useI18NPrefix(i18nPrefix); - const { filteredData: filteredListData, debouncedSearch: listSearch } = - useDebouncedSearch(list, (keywords, d) => - d.name.toLowerCase().includes(keywords.toLowerCase()), - ); - const archived = useSelector(selectArchivedStoryboards); - const archivedListLoading = useSelector(selectArchivedStoryboardLoading); - const { filteredData: filteredRecycleData, debouncedSearch: recycleSearch } = - useDebouncedSearch(archived, (keywords, d) => + const { filteredData: filteredListData, debouncedSearch: listSearch } = + useDebouncedSearch(list, (keywords, d) => + d.name.toLowerCase().includes(keywords.toLowerCase()), + ); + const archived = useSelector(selectArchivedStoryboards); + const archivedListLoading = useSelector(selectArchivedStoryboardLoading); + const { + filteredData: filteredRecycleData, + debouncedSearch: recycleSearch, + } = useDebouncedSearch(archived, (keywords, d) => d.name.toLowerCase().includes(keywords.toLowerCase()), ); - const recycleInit = useCallback(() => { - dispatch(getArchivedStoryboards(orgId)); - }, [dispatch, orgId]); + const recycleInit = useCallback(() => { + dispatch(getArchivedStoryboards(orgId)); + }, [dispatch, orgId]); - const add = useCallback(() => { - showSaveForm({ - vizType: 'STORYBOARD', - type: CommonFormTypes.Add, - visible: true, - onSave: (values, onClose) => { - dispatch( - addStoryboard({ - storyboard: { name: values.name, orgId }, - resolve: onClose, - }), - ); - }, - }); - }, [showSaveForm, dispatch, orgId]); + const add = useCallback(() => { + showSaveForm({ + vizType: 'STORYBOARD', + type: CommonFormTypes.Add, + visible: true, + onSave: (values, onClose) => { + dispatch( + addStoryboard({ + storyboard: { name: values.name, orgId }, + resolve: onClose, + }), + ); + }, + }); + }, [showSaveForm, dispatch, orgId]); - const titles = useMemo( - () => [ - { - subTitle: '故事板列表', - search: true, - ...allowCreate({ - add: { - items: [{ key: 'STORYBOARD', text: '新建故事板' }], - icon: <PlusOutlined />, - callback: add, - }, - }), - more: { - items: [ - { - key: 'recycle', - text: '回收站', - prefix: <DeleteOutlined className="icon" />, + const titles = useMemo( + () => [ + { + subTitle: t('storyboards.title'), + search: true, + ...allowCreate({ + add: { + items: [{ key: 'STORYBOARD', text: t('storyboards.add') }], + icon: <PlusOutlined />, + callback: add, + }, + }), + more: { + items: [ + { + key: 'recycle', + text: t('storyboards.recycle'), + prefix: <DeleteOutlined className="icon" />, + }, + ], + callback: (key, _, onNext) => { + switch (key) { + case 'recycle': + onNext(); + break; + } }, - ], - callback: (key, _, onNext) => { - switch (key) { - case 'recycle': - onNext(); - break; - } }, + onSearch: listSearch, + }, + { + key: 'recycle', + subTitle: t('storyboards.recycle'), + back: true, + search: true, + onSearch: recycleSearch, }, - onSearch: listSearch, - }, - { - key: 'recycle', - subTitle: '回收站', - back: true, - search: true, - onSearch: recycleSearch, - }, - ], - [add, allowCreate, listSearch, recycleSearch], - ); + ], + [add, allowCreate, listSearch, recycleSearch, t], + ); - return ( - <Wrapper className={className} defaultActiveKey="list"> - <ListPane key="list"> - <ListTitle {...titles[0]} /> - <List list={filteredListData} selectedId={selectedId} /> - </ListPane> - <ListPane key="recycle"> - <ListTitle {...titles[1]} /> - <Recycle - type="storyboard" - orgId={orgId} - list={filteredRecycleData} - listLoading={archivedListLoading} - selectedId={selectedId} - onInit={recycleInit} - /> - </ListPane> - </Wrapper> - ); -}); + return ( + <Wrapper className={className} defaultActiveKey="list"> + <ListPane key="list"> + <ListTitle {...titles[0]} /> + <List list={filteredListData} selectedId={selectedId} /> + </ListPane> + <ListPane key="recycle"> + <ListTitle {...titles[1]} /> + <Recycle + type="storyboard" + orgId={orgId} + list={filteredRecycleData} + listLoading={archivedListLoading} + selectedId={selectedId} + onInit={recycleInit} + /> + </ListPane> + </Wrapper> + ); + }, +); const Wrapper = styled(ListNav)` display: flex; diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/index.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/index.tsx index 3cfc6e66b..ba6479600 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/Sidebar/index.tsx @@ -3,6 +3,7 @@ import { FundProjectionScreenOutlined, } from '@ant-design/icons'; import { ListSwitch } from 'app/components'; +import useI18NPrefix, { I18NComponentProps } from 'app/hooks/useI18NPrefix'; import classnames from 'classnames'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; @@ -14,7 +15,7 @@ import { Folder } from '../slice/types'; import { Folders } from './Folders'; import { Storyboards } from './Storyboards'; -export const Sidebar = memo(() => { +export const Sidebar = memo(({ i18nPrefix }: I18NComponentProps) => { const [selectedKey, setSelectedKey] = useState('folder'); const vizs = useSelector(selectVizs); const storyboards = useSelector(selectStoryboards); @@ -22,6 +23,7 @@ export const Sidebar = memo(() => { '/organizations/:orgId/vizs/:vizId', ); const vizId = matchDetail?.params.vizId; + const t = useI18NPrefix(i18nPrefix); const selectedFolderId = useMemo(() => { if (vizId && vizs) { const viz = vizs.find(({ relId }) => relId === vizId); @@ -42,11 +44,11 @@ export const Sidebar = memo(() => { const listTitles = useMemo( () => [ - { key: 'folder', icon: <FolderAddFilled />, text: '目录' }, + { key: 'folder', icon: <FolderAddFilled />, text: t('folder') }, { key: 'presentation', icon: <FundProjectionScreenOutlined />, - text: '演示', + text: t('presentation'), }, ], [], @@ -65,11 +67,13 @@ export const Sidebar = memo(() => { /> <Folders selectedId={selectedFolderId} + i18nPrefix={i18nPrefix} className={classnames({ hidden: selectedKey !== 'folder' })} /> <Storyboards selectedId={vizId} className={classnames({ hidden: selectedKey !== 'presentation' })} + i18nPrefix={i18nPrefix} /> </Wrapper> ); diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/index.tsx b/frontend/src/app/pages/MainPage/pages/VizPage/index.tsx index 8ae35611a..79c4f798d 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/index.tsx +++ b/frontend/src/app/pages/MainPage/pages/VizPage/index.tsx @@ -1,12 +1,13 @@ import { Split } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { useSplitSizes } from 'app/hooks/useSplitSizes'; import { useBoardSlice } from 'app/pages/DashBoardPage/pages/Board/slice'; import { useEditBoardSlice } from 'app/pages/DashBoardPage/pages/BoardEditor/slice'; import { useStoryBoardSlice } from 'app/pages/StoryBoardPage/slice'; +import { dispatchResize } from 'app/utils/dispatchResize'; import React, { useCallback } from 'react'; import { useRouteMatch } from 'react-router-dom'; import styled from 'styled-components/macro'; -import { dispatchResize } from 'utils/utils'; import { Main } from './Main'; import { SaveForm } from './SaveForm'; import { SaveFormContext, useSaveFormContext } from './SaveFormContext'; @@ -24,6 +25,7 @@ export function VizPage() { limitedSide: 0, range: [256, 768], }); + const tg = useI18NPrefix('global'); const siderDragEnd = useCallback( sizes => { @@ -43,7 +45,7 @@ export function VizPage() { onDragEnd={siderDragEnd} className="datart-split" > - <Sidebar /> + <Sidebar i18nPrefix={'viz.sidebar'} /> <Main /> <SaveForm width={400} @@ -52,7 +54,7 @@ export function VizPage() { labelCol: { offset: 1, span: 6 }, wrapperCol: { span: 15 }, }} - okText="保存" + okText={tg('button.save')} /> </Container> </SaveFormContext.Provider> diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/slice/index.ts b/frontend/src/app/pages/MainPage/pages/VizPage/slice/index.ts index 2acfd822e..81cba38c6 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/slice/index.ts +++ b/frontend/src/app/pages/MainPage/pages/VizPage/slice/index.ts @@ -3,8 +3,7 @@ import { ChartDataSectionType } from 'app/types/ChartConfig'; import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; import { isMySliceAction } from 'utils/@reduxjs/toolkit'; import { CloneValueDeep } from 'utils/object'; -import { reduxActionErrorHandler } from 'utils/utils'; -import { v4 as uuidv4 } from 'uuid'; +import { reduxActionErrorHandler, uuidv4 } from 'utils/utils'; import { addStoryboard, addViz, diff --git a/frontend/src/app/pages/MainPage/pages/VizPage/slice/thunks.ts b/frontend/src/app/pages/MainPage/pages/VizPage/slice/thunks.ts index b6ec9cb1f..4d23c2f21 100644 --- a/frontend/src/app/pages/MainPage/pages/VizPage/slice/thunks.ts +++ b/frontend/src/app/pages/MainPage/pages/VizPage/slice/thunks.ts @@ -5,6 +5,8 @@ import { Dashboard, DataChart, } from 'app/pages/DashBoardPage/pages/Board/slice/types'; +import { selectOrgId } from 'app/pages/MainPage/slice/selectors'; +import { getLoggedInUserPermissions } from 'app/pages/MainPage/slice/thunks'; import { StoryBoard } from 'app/pages/StoryBoardPage/slice/types'; import { RootState } from 'types'; import { request } from 'utils/request'; @@ -102,15 +104,24 @@ export const getArchivedStoryboards = createAsyncThunk<StoryBoard[], string>( }, ); -export const addStoryboard = createAsyncThunk<Storyboard, AddStoryboardParams>( +export const addStoryboard = createAsyncThunk< + Storyboard, + AddStoryboardParams, + { state: RootState } +>( 'viz/addStoryboard', - async ({ storyboard, resolve }) => { + async ({ storyboard, resolve }, { getState, dispatch }) => { try { const { data } = await request<Storyboard>({ url: `/viz/storyboards`, method: 'POST', data: storyboard, }); + + // FIXME 拥有Read权限等级的扁平结构资源新增后需要更新权限字典;后续如改造为目录结构则删除该逻辑 + const orgId = selectOrgId(getState()); + await dispatch(getLoggedInUserPermissions(orgId)); + resolve(); return data; } catch (error) { @@ -301,7 +312,14 @@ export const fetchVizChartAction = createAsyncThunk( export const fetchDataSetByPreviewChartAction = createAsyncThunk( 'viz/fetchDataSetByPreviewChartAction', - async (arg: { chartPreview?: ChartPreview; pageInfo? }, thunkAPI) => { + async ( + arg: { + chartPreview?: ChartPreview; + pageInfo?; + sorter?: { column: string; operator: string; aggOperator?: string }; + }, + thunkAPI, + ) => { const builder = new ChartDataRequestBuilder( { id: arg.chartPreview?.backendChart?.viewId, @@ -312,11 +330,15 @@ export const fetchDataSetByPreviewChartAction = createAsyncThunk( arg.chartPreview?.chartConfig?.datas, arg.chartPreview?.chartConfig?.settings, arg.pageInfo, + false, + arg.chartPreview?.backendChart?.config?.aggregation, ); const response = await request({ method: 'POST', url: `data-provider/execute`, - data: builder.build(), + data: builder + .addExtraSorters(arg?.sorter ? [arg?.sorter as any] : []) + .build(), }); return { backendChartId: arg.chartPreview?.backendChartId, diff --git a/frontend/src/app/pages/MainPage/slice/index.ts b/frontend/src/app/pages/MainPage/slice/index.ts index 669f625eb..08108c091 100644 --- a/frontend/src/app/pages/MainPage/slice/index.ts +++ b/frontend/src/app/pages/MainPage/slice/index.ts @@ -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 { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; import { ResourceTypes } from '../pages/PermissionPage/constants'; @@ -77,7 +95,7 @@ const slice = createSlice({ ) { let newDownloadStatus = payload?.newStatus; const _isNotPendingDownload = status => - status !== DownloadTaskState.CREATE; + status !== DownloadTaskState.CREATED; if (!newDownloadStatus) { const originTasks = state.downloadManagement?.tasks || []; const newTasks = payload?.newTasks || []; diff --git a/frontend/src/app/pages/MainPage/slice/selectors.ts b/frontend/src/app/pages/MainPage/slice/selectors.ts index 4cc6cf15e..cef15d82d 100644 --- a/frontend/src/app/pages/MainPage/slice/selectors.ts +++ b/frontend/src/app/pages/MainPage/slice/selectors.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'types'; import { initialState } from '.'; diff --git a/frontend/src/app/pages/MainPage/slice/thunks.ts b/frontend/src/app/pages/MainPage/slice/thunks.ts index 09904eb2d..40817f7be 100644 --- a/frontend/src/app/pages/MainPage/slice/thunks.ts +++ b/frontend/src/app/pages/MainPage/slice/thunks.ts @@ -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 { createAsyncThunk } from '@reduxjs/toolkit'; import { selectLoggedInUser } from 'app/slice/selectors'; import { RootState } from 'types'; @@ -259,7 +277,7 @@ export const fetchDownloadTasks = createAsyncThunk( }); dispatch(mainActions.setDownloadManagement({ newTasks: data })); const isNeedClear = !(data || []).some( - v => v.status === DownloadTaskState.CREATE, + v => v.status === DownloadTaskState.CREATED, ); payload?.resolve?.(isNeedClear); } catch (error) { diff --git a/frontend/src/app/pages/MainPage/slice/types.ts b/frontend/src/app/pages/MainPage/slice/types.ts index e1081c62f..baa7c7172 100644 --- a/frontend/src/app/pages/MainPage/slice/types.ts +++ b/frontend/src/app/pages/MainPage/slice/types.ts @@ -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 { TreeDataNode } from 'antd'; import { UserSettingTypes } from '../constants'; import { PermissionLevels } from '../pages/PermissionPage/constants'; @@ -125,8 +143,8 @@ export enum DownloadManagementStatus { } export enum DownloadTaskState { - CREATE = 0, - FINISH = 1, + CREATED = 0, + DONE = 1, DOWNLOADED = 2, FAILED = -1, } diff --git a/frontend/src/app/pages/MainPage/slice/utils.ts b/frontend/src/app/pages/MainPage/slice/utils.ts index fd94625c4..ab25b9f1c 100644 --- a/frontend/src/app/pages/MainPage/slice/utils.ts +++ b/frontend/src/app/pages/MainPage/slice/utils.ts @@ -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 { request } from 'utils/request'; import { UserSettingTypes } from '../constants'; import { UserSetting } from './types'; diff --git a/frontend/src/app/pages/NotFoundPage/index.tsx b/frontend/src/app/pages/NotFoundPage/index.tsx index f6c87bfb6..0b0dcaa18 100644 --- a/frontend/src/app/pages/NotFoundPage/index.tsx +++ b/frontend/src/app/pages/NotFoundPage/index.tsx @@ -1,20 +1,22 @@ import { SearchOutlined } from '@ant-design/icons'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { Helmet } from 'react-helmet-async'; import styled from 'styled-components/macro'; import { SPACE_TIMES } from 'styles/StyleConstants'; export function NotFoundPage() { + const t = useI18NPrefix('notfound'); return ( <Wrapper> <Helmet> - <title>404 Page Not Found - + {`404 ${t('title')}`} +

    404

    -

    没有这个页面

    +

    {t('title')}

    ); } diff --git a/frontend/src/app/pages/RegisterPage/Loadable.tsx b/frontend/src/app/pages/RegisterPage/Loadable.tsx index eda1906f8..66bffe11e 100644 --- a/frontend/src/app/pages/RegisterPage/Loadable.tsx +++ b/frontend/src/app/pages/RegisterPage/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/RegisterPage/RegisterForm.tsx b/frontend/src/app/pages/RegisterPage/RegisterForm.tsx index 8c0f460a6..aded7c21c 100644 --- a/frontend/src/app/pages/RegisterPage/RegisterForm.tsx +++ b/frontend/src/app/pages/RegisterPage/RegisterForm.tsx @@ -1,5 +1,24 @@ +/** + * 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, Form, Input, message } from 'antd'; import { AuthForm } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { selectRegisterLoading } from 'app/slice/selectors'; import { register } from 'app/slice/thunks'; import React, { FC, useCallback } from 'react'; @@ -7,6 +26,8 @@ import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import styled from 'styled-components/macro'; import { LINE_HEIGHT_ICON_LG } from 'styles/StyleConstants'; +import { getPasswordValidator } from 'utils/validators'; + interface RegisterFormProps { onRegisterSuccess: (email: string) => void; } @@ -15,6 +36,8 @@ export const RegisterForm: FC = ({ onRegisterSuccess }) => { const history = useHistory(); const loading = useSelector(selectRegisterLoading); const [form] = Form.useForm(); + const t = useI18NPrefix('register'); + const tg = useI18NPrefix('global'); const onRegister = useCallback( values => { @@ -22,14 +45,14 @@ export const RegisterForm: FC = ({ onRegisterSuccess }) => { register({ data: values, resolve: () => { - message.success('注册成功'); + message.success(t('registrationSuccess')); form.resetFields(); onRegisterSuccess(values.email); }, }), ); }, - [dispatch, form, onRegisterSuccess], + [dispatch, form, onRegisterSuccess, t], ); const toLogin = useCallback(() => { @@ -44,44 +67,36 @@ export const RegisterForm: FC = ({ onRegisterSuccess }) => { rules={[ { required: true, - message: '用户名不能为空', + message: `${t('username')}${tg('validation.required')}`, }, ]} > - +
    - + = 6 && value.trim().length <= 20) - ) { - return Promise.resolve(); - } - return Promise.reject(new Error('密码长度为6-20位')); - }, + validator: getPasswordValidator(tg('validation.invalidPassword')), }, ]} > - + {() => ( @@ -98,12 +113,13 @@ export const RegisterForm: FC = ({ onRegisterSuccess }) => { } block > - 注册 + {t('register')} )} - 已有账号,点击登录 + {t('desc1')} + {t('login')} diff --git a/frontend/src/app/pages/RegisterPage/SendEmailTips.tsx b/frontend/src/app/pages/RegisterPage/SendEmailTips.tsx index 027662d9c..1286c743d 100644 --- a/frontend/src/app/pages/RegisterPage/SendEmailTips.tsx +++ b/frontend/src/app/pages/RegisterPage/SendEmailTips.tsx @@ -1,7 +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 { LeftCircleOutlined } from '@ant-design/icons'; import { Button } from 'antd'; import { AuthForm } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { FC, useCallback } from 'react'; +import styled from 'styled-components/macro'; +import { SPACE_XS } from 'styles/StyleConstants'; interface SendEmailTipsProps { email: string; @@ -15,6 +36,7 @@ export const SendEmailTips: FC = ({ onBack, onSendEmailAgain, }) => { + const t = useI18NPrefix('register'); const toEmailWebsite = useCallback(() => { if (email) { const suffix = email.split('@')[1]; @@ -25,28 +47,39 @@ export const SendEmailTips: FC = ({ return ( -

    请查收电子邮件

    -

    - 我们向 {email} 发送了一封电子邮件,请 - - 前往 - - 电子邮件中确认。 -

    -

    - 没收到? - + {t('tipDesc3')} + + + {t('tipDesc4')} + -

    +
    ); }; + +const Content = styled.p` + margin: ${SPACE_XS} 0; +`; diff --git a/frontend/src/app/pages/RegisterPage/index.tsx b/frontend/src/app/pages/RegisterPage/index.tsx index 3a393f823..0f2e99443 100644 --- a/frontend/src/app/pages/RegisterPage/index.tsx +++ b/frontend/src/app/pages/RegisterPage/index.tsx @@ -1,5 +1,24 @@ +/** + * 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 { message } from 'antd'; import { Brand } from 'app/components/Brand'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import React, { useCallback, useState } from 'react'; import styled from 'styled-components/macro'; import { RegisterForm } from './RegisterForm'; @@ -10,6 +29,8 @@ export function RegisterPage() { const [isRegister, setIsRegister] = useState(true); const [email, setEmail] = useState(''); const [sendEmailLoading, setSendEmailLoading] = useState(false); + const t = useI18NPrefix('register'); + const onRegisterSuccess = useCallback((email: string) => { setEmail(email); setIsRegister(false); @@ -22,12 +43,13 @@ export function RegisterPage() { sendEmail(email) .then(() => { setSendEmailLoading(false); - message.success('发送成功'); + message.success(t('sendSuccess')); }) .catch(() => { setSendEmailLoading(false); }); - }, [email]); + }, [email, t]); + return ( diff --git a/frontend/src/app/pages/RegisterPage/service.ts b/frontend/src/app/pages/RegisterPage/service.ts index fa72488d7..225843e1b 100644 --- a/frontend/src/app/pages/RegisterPage/service.ts +++ b/frontend/src/app/pages/RegisterPage/service.ts @@ -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 { request } from 'utils/request'; import { errorHandle } from 'utils/utils'; export const sendEmail = async usernameOrEmail => { diff --git a/frontend/src/app/pages/SharePage/BoardForShare.tsx b/frontend/src/app/pages/SharePage/BoardForShare.tsx index 1cbbeb85d..dbfb04e14 100644 --- a/frontend/src/app/pages/SharePage/BoardForShare.tsx +++ b/frontend/src/app/pages/SharePage/BoardForShare.tsx @@ -38,7 +38,7 @@ import { OnLoadTasksType } from '../MainPage/Navbar/DownloadListPopup'; import { DownloadTask } from '../MainPage/slice/types'; import { DownloadTaskContainer } from './DownloadTaskContainer'; import { HeadlessBrowserIdentifier } from './HeadlessBrowserIdentifier'; - +const TitleHeight = 60; export interface ShareBoardProps { dashboard: Dashboard; renderMode: VizRenderMode; @@ -75,7 +75,21 @@ export const BoardForShare: React.FC = memo( }, [hasFetchItems, needFetchItems]); // for sever Browser - + const { taskW, taskH } = useMemo(() => { + const taskWH = { + taskW: boardWidthHeight[0] || 0, + taskH: boardWidthHeight[1] || 0, + }; + if (dashboard) { + if (dashboard?.config?.type === 'free') { + const { width, height } = dashboard.config; + const ratio = width / (height || 1) || 1; + const targetHeight = taskWH.taskW / ratio; + taskWH.taskH = targetHeight; + } + } + return taskWH; + }, [boardWidthHeight, dashboard]); const boardDownLoadAction = useCallback( (params: { boardId: string }) => async dispatch => { const { boardId } = params; @@ -135,8 +149,8 @@ export const BoardForShare: React.FC = memo( {viewBoard} ); diff --git a/frontend/src/app/pages/SharePage/ChartPreviewBoardForShare.tsx b/frontend/src/app/pages/SharePage/ChartForShare.tsx similarity index 70% rename from frontend/src/app/pages/SharePage/ChartPreviewBoardForShare.tsx rename to frontend/src/app/pages/SharePage/ChartForShare.tsx index fc2d5b065..404d54ff0 100644 --- a/frontend/src/app/pages/SharePage/ChartPreviewBoardForShare.tsx +++ b/frontend/src/app/pages/SharePage/ChartForShare.tsx @@ -41,7 +41,8 @@ import { updateFilterAndFetchDatasetForShare, } from './slice/thunks'; -const ChartPreviewBoardForShare: FC<{ +const TitleHeight = 100; +const ChartForShare: FC<{ style?: CSSProperties; chartPreview?: ChartPreview; filterSearchParams?: FilterSearchParams; @@ -57,29 +58,65 @@ const ChartPreviewBoardForShare: FC<{ }) => { const dispatch = useDispatch(); const [chart] = useState(() => { - return ChartManager.instance().getById( + const currentChart = ChartManager.instance().getById( chartPreview?.backendChart?.config?.chartGraphId, ); + return currentChart; }); const { ref, width = style?.width, height = style?.height, } = useResizeObserver({ - refreshMode: 'throttle', + refreshMode: 'debounce', refreshRate: 500, }); + const { ref: controlRef, height: controlH = 0 } = + useResizeObserver({ + refreshMode: 'debounce', + refreshRate: 500, + }); const headlessBrowserRenderSign = useSelector( selectHeadlessBrowserRenderSign, ); - useMount(() => { if (!chartPreview) { return; } - dispatch(fetchShareDataSetByPreviewChartAction(chartPreview)); + dispatch( + fetchShareDataSetByPreviewChartAction({ preview: chartPreview }), + ); + registerChartEvents(chart); }); + const registerChartEvents = chart => { + chart?.registerMouseEvents([ + { + name: 'click', + callback: param => { + if ( + param.componentType === 'table' && + param.seriesType === 'paging-sort-filter' + ) { + dispatch( + fetchShareDataSetByPreviewChartAction({ + preview: chartPreview!, + sorter: { + column: param?.seriesName!, + operator: param?.value?.direction, + }, + pageInfo: { + pageNo: param?.value?.pageNo, + }, + }), + ); + return; + } + }, + }, + ]); + }; + const handleFilterChange = (type, payload) => { dispatch( updateFilterAndFetchDatasetForShare({ @@ -99,6 +136,9 @@ const ChartPreviewBoardForShare: FC<{ } as any, chartPreview?.chartConfig?.datas, chartPreview?.chartConfig?.settings, + {}, + false, + chartPreview?.backendChart?.config?.aggregation, ); const downloadParams = [builder.build()]; const fileName = chartPreview?.backendChart?.name || 'chart'; @@ -113,32 +153,36 @@ const ChartPreviewBoardForShare: FC<{ allowShare allowDownload /> - +
    + +
    +
    -
    ); }, ); -export default ChartPreviewBoardForShare; +export default ChartForShare; const StyledChartPreviewBoard = styled.div` display: flex; diff --git a/frontend/src/app/pages/SharePage/Loadable.tsx b/frontend/src/app/pages/SharePage/Loadable.tsx index 193a7a6c0..c8bafda8f 100644 --- a/frontend/src/app/pages/SharePage/Loadable.tsx +++ b/frontend/src/app/pages/SharePage/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/SharePage/PasswordModal.tsx b/frontend/src/app/pages/SharePage/PasswordModal.tsx index 197d86d29..ef6df8ed4 100644 --- a/frontend/src/app/pages/SharePage/PasswordModal.tsx +++ b/frontend/src/app/pages/SharePage/PasswordModal.tsx @@ -53,7 +53,7 @@ const PasswordModal: FC<{ name={INPUT_PASSWORD_KEY} rules={[{ required: true }]} > - +
    diff --git a/frontend/src/app/pages/SharePage/SharePage.tsx b/frontend/src/app/pages/SharePage/SharePage.tsx index 234a46985..efa57e5fc 100644 --- a/frontend/src/app/pages/SharePage/SharePage.tsx +++ b/frontend/src/app/pages/SharePage/SharePage.tsx @@ -24,8 +24,9 @@ import { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import persistence from 'utils/persistence'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import ChartRequest from '../ChartWorkbenchPage/models/ChartHttpRequest'; +import ChartManager from '../ChartWorkbenchPage/models/ChartManager'; import { useBoardSlice } from '../DashBoardPage/pages/Board/slice'; import { selectShareBoard } from '../DashBoardPage/pages/Board/slice/selector'; import { VizRenderMode } from '../DashBoardPage/pages/Board/slice/types'; @@ -35,7 +36,7 @@ import { urlSearchTransfer } from '../MainPage/pages/VizPage/utils'; import { useStoryBoardSlice } from '../StoryBoardPage/slice'; import { selectShareStoryBoard } from '../StoryBoardPage/slice/selectors'; import BoardForShare from './BoardForShare'; -import ChartPreviewBoardForShare from './ChartPreviewBoardForShare'; +import ChartForShare from './ChartForShare'; import { DownloadTaskContainer } from './DownloadTaskContainer'; import PasswordModal from './PasswordModal'; import { downloadShareDataChartFile } from './sercive'; @@ -85,6 +86,10 @@ export function SharePage() { }, [search]); useMount(() => { + ChartManager.instance() + .load() + .catch(err => console.error('Fail to load customize charts with ', err)); + if (Boolean(usePassword)) { const previousPassword = persistence.session.get(shareToken); if (previousPassword) { @@ -186,7 +191,7 @@ export function SharePage() { onLoadTasks={onLoadShareTask} onDownloadFile={onDownloadFile} > - diff --git a/frontend/src/app/pages/SharePage/StoryPlayerForShare/StoryPlayerForShare.tsx b/frontend/src/app/pages/SharePage/StoryPlayerForShare/StoryPlayerForShare.tsx index 0d3b7ee6d..11a72a408 100644 --- a/frontend/src/app/pages/SharePage/StoryPlayerForShare/StoryPlayerForShare.tsx +++ b/frontend/src/app/pages/SharePage/StoryPlayerForShare/StoryPlayerForShare.tsx @@ -33,7 +33,7 @@ import Reveal from 'reveal.js'; import 'reveal.js/dist/reveal.css'; import RevealZoom from 'reveal.js/plugin/zoom/plugin'; import styled from 'styled-components/macro'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import { storyActions } from '../../StoryBoardPage/slice'; import { makeSelectStoryPagesById } from '../../StoryBoardPage/slice/selectors'; import { getPageContentDetail } from '../../StoryBoardPage/slice/thunks'; diff --git a/frontend/src/app/pages/SharePage/slice/thunks.ts b/frontend/src/app/pages/SharePage/slice/thunks.ts index eb293b623..5480eccc9 100644 --- a/frontend/src/app/pages/SharePage/slice/thunks.ts +++ b/frontend/src/app/pages/SharePage/slice/thunks.ts @@ -123,17 +123,27 @@ export const fetchShareVizInfo = createAsyncThunk( export const fetchShareDataSetByPreviewChartAction = createAsyncThunk( 'share/fetchDataSetByPreviewChartAction', - async (chartPreview: ChartPreview, thunkAPI) => { + async ( + args: { + preview: ChartPreview; + pageInfo?: any; + sorter?: { column: string; operator: string; aggOperator?: string }; + }, + thunkAPI, + ) => { const state = thunkAPI.getState() as RootState; const shareState = state.share; const builder = new ChartDataRequestBuilder( { - id: chartPreview?.backendChart?.viewId, + id: args.preview?.backendChart?.viewId, computedFields: - chartPreview?.backendChart?.config?.computedFields || [], + args.preview?.backendChart?.config?.computedFields || [], } as any, - chartPreview?.chartConfig?.datas, - chartPreview?.chartConfig?.settings, + args.preview?.chartConfig?.datas, + args.preview?.chartConfig?.settings, + args.pageInfo, + false, + args.preview?.backendChart?.config?.aggregation, ); const response = await request({ method: 'POST', @@ -142,7 +152,9 @@ export const fetchShareDataSetByPreviewChartAction = createAsyncThunk( executeToken: shareState?.executeToken, password: shareState?.sharePassword, }, - data: builder.build(), + data: builder + .addExtraSorters(args?.sorter ? [args?.sorter as any] : []) + .build(), }); return response.data; }, @@ -163,7 +175,9 @@ export const updateFilterAndFetchDatasetForShare = createAsyncThunk( const state = thunkAPI.getState() as RootState; const shareState = state.share; await thunkAPI.dispatch( - fetchShareDataSetByPreviewChartAction(shareState?.chartPreview!), + fetchShareDataSetByPreviewChartAction({ + preview: shareState?.chartPreview!, + }), ); return { backendChartId: arg.backendChartId, diff --git a/frontend/src/app/pages/StoryBoardPage/Editor/StoryPageSetting.tsx b/frontend/src/app/pages/StoryBoardPage/Editor/StoryPageSetting.tsx index e5be49762..ce2da55cf 100644 --- a/frontend/src/app/pages/StoryBoardPage/Editor/StoryPageSetting.tsx +++ b/frontend/src/app/pages/StoryBoardPage/Editor/StoryPageSetting.tsx @@ -16,6 +16,7 @@ * limitations under the License. */ import { Form, Select } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import produce from 'immer'; import React, { memo, @@ -39,6 +40,7 @@ import { updateStoryPage } from '../slice/thunks'; import { StoryBoardState, TransitionEffect } from '../slice/types'; export interface StoryPageSettingProps {} export const StoryPageSetting: React.FC = memo(() => { + const t = useI18NPrefix(`viz.board.setting`); const { stroyBoardId: storyId } = useContext(StoryContext); const dispatch = useDispatch(); const selectedPageIds = useSelector( @@ -89,7 +91,7 @@ export const StoryPageSetting: React.FC = memo(() => { onValuesChange={onValuesChange} > <> - + - + - + - {/* - - - - - - - - - - - } - title="切换效果" - > - - */} ); }); diff --git a/frontend/src/app/pages/StoryBoardPage/Editor/StorySetting.tsx b/frontend/src/app/pages/StoryBoardPage/Editor/StorySetting.tsx index 8043af15f..91c87923f 100644 --- a/frontend/src/app/pages/StoryBoardPage/Editor/StorySetting.tsx +++ b/frontend/src/app/pages/StoryBoardPage/Editor/StorySetting.tsx @@ -16,6 +16,7 @@ * limitations under the License. */ import { Checkbox, Form, InputNumber } from 'antd'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import produce from 'immer'; import React, { memo, useCallback, useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -27,12 +28,12 @@ import { updateStory } from '../slice/thunks'; import { StoryBoardState } from '../slice/types'; export interface StorySettingProps {} export const StorySetting: React.FC = memo(() => { + const t = useI18NPrefix(`viz.board.setting`); const dispatch = useDispatch(); const { stroyBoardId: storyId } = useContext(StoryContext); const storyBoard = useSelector((state: { storyBoard: StoryBoardState }) => makeSelectStoryBoardById(state, storyId), ); - // TODO add isLoop option -xieLiuDuo const autoPlay = storyBoard?.config?.autoPlay; const [form] = Form.useForm(); useEffect(() => { @@ -61,10 +62,10 @@ export const StorySetting: React.FC = memo(() => { form={form} layout="inline" > - + - + diff --git a/frontend/src/app/pages/StoryBoardPage/Editor/index.tsx b/frontend/src/app/pages/StoryBoardPage/Editor/index.tsx index 4c2689412..e61651a68 100644 --- a/frontend/src/app/pages/StoryBoardPage/Editor/index.tsx +++ b/frontend/src/app/pages/StoryBoardPage/Editor/index.tsx @@ -17,8 +17,10 @@ */ import { Layout, Modal } from 'antd'; import { Split } from 'app/components'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { useSplitSizes } from 'app/hooks/useSplitSizes'; import { StoryContext } from 'app/pages/StoryBoardPage/contexts/StoryContext'; +import { dispatchResize } from 'app/utils/dispatchResize'; import React, { memo, useCallback, @@ -35,8 +37,7 @@ import 'reveal.js/dist/reveal.css'; import RevealZoom from 'reveal.js/plugin/zoom/plugin'; import styled from 'styled-components/macro'; import { SPACE_MD } from 'styles/StyleConstants'; -import { dispatchResize } from 'utils/utils'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import PageThumbnailList from '../components/PageThumbnailList'; import StoryPageItem from '../components/StoryPageItem'; import { storyActions } from '../slice'; @@ -53,6 +54,7 @@ export const StoryEditor: React.FC<{ storyId: string; onCloseEditor?: () => void; }> = memo(({ storyId, onCloseEditor }) => { + const t = useI18NPrefix(`viz.board.setting`); const domId = useMemo(() => uuidv4(), []); const revealRef = useRef(); const dispatch = useDispatch(); @@ -181,19 +183,15 @@ export const StoryEditor: React.FC<{ const onDeletePages = useCallback( (pageIds: string[]) => { Modal.confirm({ - title: - pageIds.length > 1 - ? '确认删除所有选中的故事页?' - : '确认删除此故事页?', + title: pageIds.length > 1 ? t('delPagesTip') : t('delPageTip'), onOk: () => { - // onDelete(selectedIds); pageIds.forEach(pageId => { dispatch(deleteStoryPage({ storyId, pageId })); }); }, }); }, - [dispatch, storyId], + [dispatch, storyId, t], ); return ( diff --git a/frontend/src/app/pages/StoryBoardPage/Player/index.tsx b/frontend/src/app/pages/StoryBoardPage/Player/index.tsx index dabe74004..134776d7d 100644 --- a/frontend/src/app/pages/StoryBoardPage/Player/index.tsx +++ b/frontend/src/app/pages/StoryBoardPage/Player/index.tsx @@ -33,7 +33,7 @@ import Reveal from 'reveal.js'; import 'reveal.js/dist/reveal.css'; import RevealZoom from 'reveal.js/plugin/zoom/plugin'; import styled from 'styled-components/macro'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv4 } from 'utils/utils'; import StoryPageItem from '../components/StoryPageItem'; import { storyActions } from '../slice'; import { diff --git a/frontend/src/app/pages/StoryBoardPage/Preview/Preview.tsx b/frontend/src/app/pages/StoryBoardPage/Preview/Preview.tsx index 5decfabe9..ca8f5c493 100644 --- a/frontend/src/app/pages/StoryBoardPage/Preview/Preview.tsx +++ b/frontend/src/app/pages/StoryBoardPage/Preview/Preview.tsx @@ -17,11 +17,13 @@ */ import { Layout, message } from 'antd'; import { Split } from 'app/components'; +import usePrefixI18N from 'app/hooks/useI18NPrefix'; import { useSplitSizes } from 'app/hooks/useSplitSizes'; import { vizActions } from 'app/pages/MainPage/pages/VizPage/slice'; import { selectPublishLoading } from 'app/pages/MainPage/pages/VizPage/slice/selectors'; import { publishViz } from 'app/pages/MainPage/pages/VizPage/slice/thunks'; import { StoryContext } from 'app/pages/StoryBoardPage/contexts/StoryContext'; +import { dispatchResize } from 'app/utils/dispatchResize'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; @@ -29,7 +31,6 @@ import { useDispatch, useSelector } from 'react-redux'; import 'reveal.js/dist/reveal.css'; import styled from 'styled-components/macro'; import { SPACE_MD } from 'styles/StyleConstants'; -import { dispatchResize } from 'utils/utils'; import PageThumbnailList from '../components/PageThumbnailList'; import StoryHeader from '../components/StoryHeader'; import StoryPageItem from '../components/StoryPageItem'; @@ -50,9 +51,8 @@ export const StoryPagePreview: React.FC<{ allowManage?: boolean; }> = memo(({ storyId, allowShare, allowManage }) => { const dispatch = useDispatch(); - + const t = usePrefixI18N('viz.action'); const [currentPageIndex, setCurrentPageIndex] = useState(0); - // const [storyEditing, setStoryEditing] = useState(false); const [editorVisible, setEditorVisible] = useState(false); const storyBoard = useSelector((state: { storyBoard: StoryBoardState }) => @@ -82,8 +82,9 @@ export const StoryPagePreview: React.FC<{ setEditorVisible(c => !c); }, []); const playStory = useCallback(() => { + message.info(t('playTip')); dispatch(vizActions.changePlayingStoryId(storyId || '')); - }, [dispatch, storyId]); + }, [dispatch, storyId, t]); useEffect(() => { if (sortedPages.length === 0) { diff --git a/frontend/src/app/pages/StoryBoardPage/components/StoryHeader.tsx b/frontend/src/app/pages/StoryBoardPage/components/StoryHeader.tsx index cf57eb592..1f0791356 100644 --- a/frontend/src/app/pages/StoryBoardPage/components/StoryHeader.tsx +++ b/frontend/src/app/pages/StoryBoardPage/components/StoryHeader.tsx @@ -24,13 +24,19 @@ import { import { Button, Dropdown } from 'antd'; import { DetailPageHeader } from 'app/components/DetailPageHeader'; import { ShareLinkModal } from 'app/components/VizOperationMenu'; +import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { generateShareLinkAsync } from 'app/utils/fetch'; -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 { StoryContext } from '../contexts/StoryContext'; import { StoryOverLay } from './StoryOverLay'; -// import { useSelector } from 'react-redux'; - -const TITLE_SUFFIX = ['[已归档]', '[未发布]']; interface StoryHeaderProps { name?: string; @@ -55,7 +61,14 @@ export const StoryHeader: FC = memo( allowShare, allowManage, }) => { - const title = `${name || ''} ${TITLE_SUFFIX[Number(status)] || ''}`; + const t = useI18NPrefix(`viz.action`); + const title = useMemo(() => { + const base = name || ''; + const suffix = TITLE_SUFFIX[Number(status)] + ? `[${t(TITLE_SUFFIX[Number(status)])}]` + : ''; + return base + suffix; + }, [name, status, t]); const isArchived = Number(status) === 0; const [showShareLinkModal, setShowShareLinkModal] = useState(false); const { stroyBoardId } = useContext(StoryContext); @@ -94,16 +107,16 @@ export const StoryHeader: FC = memo( loading={publishLoading} onClick={onPublish} > - {status === 1 ? '发布' : '取消发布'} + {status === 1 ? t('publish') : t('unpublish')} )} {allowManage && !isArchived && ( )} void; @@ -26,6 +27,7 @@ export interface BoardOverLayProps { } export const StoryOverLay: React.FC = memo( ({ onOpenShareLink, allowShare }) => { + const t = useI18NPrefix(`viz.action.share`); const renderList = useMemo( () => [ { @@ -34,10 +36,10 @@ export const StoryOverLay: React.FC = memo( onClick: onOpenShareLink, disabled: false, render: allowShare, - content: '分享链接', + content: t('shareLink'), }, ], - [onOpenShareLink, allowShare], + [onOpenShareLink, allowShare, t], ); const actionItems = useMemo( () => diff --git a/frontend/src/app/share.tsx b/frontend/src/app/share.tsx index 998261ff6..3994aa508 100644 --- a/frontend/src/app/share.tsx +++ b/frontend/src/app/share.tsx @@ -16,28 +16,34 @@ * limitations under the License. */ +import { ConfigProvider } from 'antd'; import echartsDefaultTheme from 'app/assets/theme/echarts_default_theme.json'; import { registerTheme } from 'echarts'; +import { antdLocales } from 'locales/i18n'; import { Helmet } from 'react-helmet-async'; import { useTranslation } from 'react-i18next'; import { BrowserRouter } from 'react-router-dom'; import { GlobalStyle, OverriddenStyle } from 'styles/globalStyles'; import { LazySharePage } from './pages/SharePage/Loadable'; + registerTheme('default', echartsDefaultTheme); + export function Share() { const { i18n } = useTranslation(); return ( - - - - - - - - + + + + + + + + + + ); } diff --git a/frontend/src/app/slice/index.ts b/frontend/src/app/slice/index.ts index 7a9c7e62d..afb850a50 100644 --- a/frontend/src/app/slice/index.ts +++ b/frontend/src/app/slice/index.ts @@ -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 { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { useInjectReducer } from 'utils/@reduxjs/injectReducer'; import { diff --git a/frontend/src/app/slice/selectors.ts b/frontend/src/app/slice/selectors.ts index ceb569a70..523febb24 100644 --- a/frontend/src/app/slice/selectors.ts +++ b/frontend/src/app/slice/selectors.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'types'; import { initialState } from '.'; diff --git a/frontend/src/app/slice/thunks.ts b/frontend/src/app/slice/thunks.ts index 8f0c2e954..7cfd30842 100644 --- a/frontend/src/app/slice/thunks.ts +++ b/frontend/src/app/slice/thunks.ts @@ -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 { createAsyncThunk } from '@reduxjs/toolkit'; import { StorageKeys } from 'globalConstants'; import { removeToken, setToken, setTokenExpiration } from 'utils/auth'; diff --git a/frontend/src/app/slice/types.ts b/frontend/src/app/slice/types.ts index 8751c3c17..7810f5583 100644 --- a/frontend/src/app/slice/types.ts +++ b/frontend/src/app/slice/types.ts @@ -1,3 +1,20 @@ +/** + * 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. + */ export interface AppState { loggedInUser: null | User; systemInfo: null | SystemInfo; diff --git a/frontend/src/app/types/ChartConfig.ts b/frontend/src/app/types/ChartConfig.ts index 9e86ca901..5691454fc 100644 --- a/frontend/src/app/types/ChartConfig.ts +++ b/frontend/src/app/types/ChartConfig.ts @@ -20,7 +20,11 @@ import { ControllerFacadeTypes, ControllerVisibilityTypes, } from 'app/types/FilterControlPanel'; -import { FilterSqlOperator, NumberUnitKey } from 'globalConstants'; +import { + FilterSqlOperator, + NumberUnitKey, + RECOMMEND_TIME, +} from 'globalConstants'; import { ValueOf } from 'types'; import { ChartDataViewFieldCategory, @@ -86,7 +90,8 @@ export type FilterCondition = { | number | [number, number] | string[] - | Array; + | Array + | TimeFilterConditionValue; visualType: string; operator?: | string @@ -95,12 +100,22 @@ export type FilterCondition = { children?: FilterCondition[]; }; -export type FilterValueOption = { +export type TimeFilterConditionValue = + | string + | string[] + | Lowercase + | Array<{ + unit; + amount; + direction?: string; + }>; + +export type RelationFilterValue = { key: string; label: string; index?: number; isSelected?: boolean; - children?: FilterValueOption[]; + children?: RelationFilterValue[]; }; export const FilterRelationType = { @@ -118,7 +133,7 @@ export enum FilterConditionType { RangeValue = 1 << 4, Value = 1 << 5, RangeTime = 1 << 6, - RelativeTime = 1 << 7, + RecommendTime = 1 << 7, Time = 1 << 8, Tree = 1 << 9, @@ -129,7 +144,7 @@ export enum FilterConditionType { RangeValue | Value | RangeTime | - RelativeTime | + RecommendTime | Time | Tree, Relation = 1 << 50, @@ -179,7 +194,6 @@ export const ChartStyleSectionComponentType = { INPUTPERCENTAGE: 'inputPercentage', SLIDER: 'slider', GROUP: 'group', - CACHE: 'cache', REFERENCE: 'reference', TABS: 'tabs', LISTTEMPLATE: 'listTemplate', @@ -187,6 +201,11 @@ export const ChartStyleSectionComponentType = { LINE: 'line', MARGIN_WIDTH: 'marginWidth', TEXT: 'text', + CONDITIONSTYLE: 'conditionStylePanel', + RADIO: 'radio', + + // Customize Component + FontAlignment: 'fontAlignment', }; type ChartConfigBase = { @@ -262,6 +281,12 @@ export type ChartDataSectionConfig = ChartConfigBase & { rows?: ChartDataSectionField[]; actions?: Array> | object; limit?: null | number | string | number[] | string[]; + disableAggregate?: boolean; + options?: { + [key in ValueOf]: { + backendSort?: boolean; + }; + }; // Question: keep field's filter relation for filter arrangement feature fieldRelation?: FilterCondition; @@ -295,6 +320,7 @@ export type ChartStyleSectionRow = { watcher?: ChartStyleSectionRowWatcher; template?: ChartStyleSectionRow; comType: ValueOf; + hidden?: boolean; }; export type ChartStyleSectionRowOption = { @@ -303,13 +329,20 @@ export type ChartStyleSectionRowOption = { step?: number | string; type?: string; editable?: boolean; - modalSize?: string; + modalSize?: string | number; expand?: boolean; items?: Array | string[] | number[]; hideLabel?: boolean; style?: React.CSSProperties; getItems?: (cols) => Array; needRefresh?: boolean; + fontFamilies?: string[]; + + /** + * Suppport Components: @see BasicRadio, @see BasicSelector and etc + * Default is false for now, will be change in futrue version + */ + translateItemLabel?: boolean; }; export type ChartStyleSelectorItem = { @@ -333,4 +366,5 @@ export type ChartConfig = { styles?: ChartStyleSectionConfig[]; settings?: ChartStyleSectionConfig[]; i18ns?: ChartI18NSectionConfig[]; + env?: string; }; diff --git a/frontend/src/app/types/ChartDataConfigSection.ts b/frontend/src/app/types/ChartDataConfigSection.ts index fe54f5a83..bec5a2047 100644 --- a/frontend/src/app/types/ChartDataConfigSection.ts +++ b/frontend/src/app/types/ChartDataConfigSection.ts @@ -28,6 +28,7 @@ export interface ChartDataConfigSectionProps { category?: Lowercase; extra?: () => ReactNode; translate?: (title: string) => string; + aggregation?: boolean; onConfigChanged: ( ancestors: number[], config: ChartDataSectionConfig, diff --git a/frontend/src/app/types/ChartDataView.ts b/frontend/src/app/types/ChartDataView.ts index a73a74dc9..0bb605a07 100644 --- a/frontend/src/app/types/ChartDataView.ts +++ b/frontend/src/app/types/ChartDataView.ts @@ -34,6 +34,8 @@ export enum ChartDataViewFieldCategory { export type ChartDataViewMeta = { id: string; name: string; + isActive?: boolean; + selectedItems?: Array; primaryKey?: boolean; category?: Uncapitalize; type?: ChartDataViewFieldType; @@ -43,7 +45,6 @@ export type ChartDataViewMeta = { export type ChartDataView = View & { meta?: ChartDataViewMeta[]; computedFields?: ChartDataViewMeta[]; - view?: { config?: string }; }; export default ChartDataView; diff --git a/frontend/src/app/types/DatartChartBase.ts b/frontend/src/app/types/DatartChartBase.ts index 48631f13e..75cdc4c12 100644 --- a/frontend/src/app/types/DatartChartBase.ts +++ b/frontend/src/app/types/DatartChartBase.ts @@ -113,7 +113,7 @@ export interface ChartMouseEventParams { // 其他大部分图表中只有一种 data,dataType 无意义。 dataType?: string; // 传入的数据值 - value?: number | string | []; + value?: number | string | [] | object | any; // 数据图形的颜色。当 componentType 为 'series' 时有意义。 color?: string; } diff --git a/frontend/src/app/types/FilterControlPanel.ts b/frontend/src/app/types/FilterControlPanel.ts index d9a6818dc..0dea30b8c 100644 --- a/frontend/src/app/types/FilterControlPanel.ts +++ b/frontend/src/app/types/FilterControlPanel.ts @@ -19,8 +19,11 @@ export enum ControllerFacadeTypes { DropdownList = 'dropdownList', RadioGroup = 'radioGroup', + CheckboxGroup = 'checkboxGroup', MultiDropdownList = 'multiDropdownList', RangeTime = 'rangeTime', + RangeTimePicker = 'rangeTimePicker', + RecommendTime = 'recommendTime', RangeValue = 'rangeValue', Text = 'text', Tree = 'tree', @@ -35,7 +38,7 @@ export enum ControllerRadioFacadeTypes { Button = 'button', } -export enum RelativeOrExactTime { +export enum TimeFilterValueCategory { Relative = 'relative', Exact = 'exact', } diff --git a/frontend/src/app/utils/chartHelper.ts b/frontend/src/app/utils/chartHelper.ts index d6a2c270e..015880f74 100644 --- a/frontend/src/app/utils/chartHelper.ts +++ b/frontend/src/app/utils/chartHelper.ts @@ -18,6 +18,7 @@ import echartsDefaultTheme from 'app/assets/theme/echarts_default_theme.json'; import { + AggregateFieldActionType, ChartConfig, ChartDataSectionConfig, ChartDataSectionField, @@ -400,23 +401,47 @@ export function getNameTextStyle(fontFamily, fontSize, color) { }; } -export function transfromToObjectArray( +export function transformToObjectArray( columns?: string[][], metas?: ChartDatasetMeta[], ) { if (!columns || !metas) { return []; } - return columns.map(col => { - let objCol = {}; - for (let i = 0; i < metas.length; i++) { + + const result: any[] = Array.apply(null, Array(columns.length)); + for (let j = 0, outterLength = result.length; j < outterLength; j++) { + let objCol = { + id: j + 1, + }; + for (let i = 0, innerLength = metas.length; i < innerLength; i++) { const key = metas?.[i]?.name; if (!!key) { - objCol[key] = col[i]; + objCol[key] = columns[j][i]; } } - return objCol; - }); + result[j] = objCol; + } + return result; +} + +// TODO delete this function #migration +/** + * @deprecated please use new method transformToObjectArray instead + * @see transformToObjectArray + * @export + * @param {string[][]} [columns] + * @param {ChartDatasetMeta[]} [metas] + * @return {*} + */ +export function transfromToObjectArray( + columns?: string[][], + metas?: ChartDatasetMeta[], +) { + console.warn( + 'This method `transfromToObjectArray` will be deprecated and can be replaced by `transformToObjectArray`', + ); + return transformToObjectArray(columns, metas); } export function getValueByColumnKey(col?: { aggregate?; colName: string }) { @@ -429,12 +454,12 @@ export function getValueByColumnKey(col?: { aggregate?; colName: string }) { return `${col.aggregate}(${col.colName})`; } -export function getColumnRenderName(c?: ChartDataSectionField) { +export function getColumnRenderOriginName(c?: ChartDataSectionField) { if (!c) { - return 'unkonwn name'; + return '[unknown]'; } - if (c.alias?.name) { - return c.alias.name; + if (c.aggregate === AggregateFieldActionType.NONE) { + return c.colName; } if (c.aggregate) { return `${c.aggregate}(${c.colName})`; @@ -442,6 +467,37 @@ export function getColumnRenderName(c?: ChartDataSectionField) { return c.colName; } +export function getColumnRenderName(c?: ChartDataSectionField) { + if (!c) { + return '[unknown]'; + } + if (c.alias?.name) { + return c.alias.name; + } + return getColumnRenderOriginName(c); +} + +export function getUnusedHeaderRows( + allRows: Array<{ + colName?: string; + }>, + originalRows: Array<{ + colName?: string; + isGroup?: boolean; + children?: any[]; + }>, +): any[] { + const oldFlattenedColNames = originalRows + .flatMap(row => flattenHeaderRowsWithoutGroupRow(row)) + .map(r => r.colName); + return (allRows || []).reduce((acc, cur) => { + if (!oldFlattenedColNames.includes(cur.colName)) { + acc.push(cur); + } + return acc; + }, []); +} + export function diffHeaderRows( oldRows: Array<{ colName: string }>, newRows: Array<{ colName: string }>, @@ -660,31 +716,6 @@ export function getSeriesTooltips4Rectangular( return []; } -export function getSeriesTooltips4Polar( - params, - groupConfigs, - aggConfigs, - dataColumns, -) { - if (!aggConfigs?.length) { - return []; - } - if (!groupConfigs?.length) { - return aggConfigs.map(config => - valueFormatter(config, dataColumns?.[0]?.[getValueByColumnKey(config)]), - ); - } - if (groupConfigs?.[0]) { - const rowKeyFn = dc => - groupConfigs?.map(config => dc[config?.colName]).join('-'); - const dataRow = dataColumns.find(dc => rowKeyFn(dc) === params?.name); - return aggConfigs.map(config => - valueFormatter(config, dataRow?.[getValueByColumnKey(config)]), - ); - } - return []; -} - export function valueFormatter(config?: ChartDataSectionField, value?: any) { return `${getColumnRenderName(config)}: ${toFormattedValue( value, @@ -698,13 +729,18 @@ export function getScatterSymbolSizeFn( min, cycleRatio?: number, ) { + min = Math.min(0, min); const scaleRatio = cycleRatio || 1; const defaultScatterPointPixelSize = 10; const distance = max - min === 0 ? 100 : max - min; return function (val) { - return ( - (val?.[valueIndex] / distance) * scaleRatio * defaultScatterPointPixelSize + return Math.max( + 3, + ((val?.[valueIndex] - min) / distance) * + scaleRatio * + defaultScatterPointPixelSize * + 2, ); }; } diff --git a/frontend/src/app/utils/dispatchResize.ts b/frontend/src/app/utils/dispatchResize.ts new file mode 100644 index 000000000..5bc484d67 --- /dev/null +++ b/frontend/src/app/utils/dispatchResize.ts @@ -0,0 +1,26 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const ResizeEvent = new Event('resize', { + bubbles: false, + cancelable: true, +}); + +export const dispatchResize = () => { + window.dispatchEvent(ResizeEvent); +}; diff --git a/frontend/src/app/utils/fetch.ts b/frontend/src/app/utils/fetch.ts index ee92a4d9a..f3ebdc6b5 100644 --- a/frontend/src/app/utils/fetch.ts +++ b/frontend/src/app/utils/fetch.ts @@ -154,7 +154,7 @@ export async function checkComputedFieldAsync(sourceId, expression) { return !!response?.data; } -export async function fetchFieldFuncitonsAsync(sourceId) { +export async function fetchFieldFunctionsAsync(sourceId) { const response = await request({ method: 'POST', url: `data-provider/function/support/${sourceId}`, @@ -230,7 +230,7 @@ export async function loadShareTask(params) { params, }); const isNeedStopPolling = !(data || []).some( - v => v.status === DownloadTaskState.CREATE, + v => v.status === DownloadTaskState.CREATED, ); return { isNeedStopPolling, diff --git a/frontend/src/app/utils/history.ts b/frontend/src/app/utils/history.ts index ee3abb793..1cc814309 100644 --- a/frontend/src/app/utils/history.ts +++ b/frontend/src/app/utils/history.ts @@ -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 { createBrowserHistory } from 'history'; const history = createBrowserHistory(); export default history; diff --git a/frontend/src/app/utils/internalChartHelper.ts b/frontend/src/app/utils/internalChartHelper.ts index f74985abf..555a1b4fa 100644 --- a/frontend/src/app/utils/internalChartHelper.ts +++ b/frontend/src/app/utils/internalChartHelper.ts @@ -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 { ChartConfig, ChartDataSectionType } from 'app/types/ChartConfig'; import { curry, pipe } from 'utils/object'; import { diff --git a/frontend/src/app/utils/mutation.ts b/frontend/src/app/utils/mutation.ts index 36db26d08..1f941a5c4 100644 --- a/frontend/src/app/utils/mutation.ts +++ b/frontend/src/app/utils/mutation.ts @@ -58,7 +58,6 @@ export function updateCollectionByAction( ) { const value = action.value; const keys = [...action.ancestors]; - const nextState = produce(base, draft => { const index = keys.shift() as number; if (index !== undefined) { diff --git a/frontend/src/app/utils/number.ts b/frontend/src/app/utils/number.ts index 320468bec..1a6be61de 100644 --- a/frontend/src/app/utils/number.ts +++ b/frontend/src/app/utils/number.ts @@ -19,6 +19,7 @@ import { FieldFormatType, IFieldFormatConfig } from 'app/types/ChartConfig'; import { dinero } from 'dinero.js'; import { NumberUnitKey, NumericUnitDescriptions } from 'globalConstants'; +import isFinite from 'lodash/isFinite'; import moment from 'moment'; import { isEmpty, pipe } from 'utils/object'; import { getCurrency, intlFormat } from './currency'; @@ -73,7 +74,7 @@ export function toFormattedValue( format?: IFieldFormatConfig, ) { if (value === null || value === undefined) { - return value; + return '-'; } if (!format || format.type === FieldFormatType.DEFAULT) { @@ -140,6 +141,10 @@ export function toFormattedValue( return formattedValue; } +export function isNumber(value: any) { + return !isEmpty(value) && !isNaN(value) && isFinite(value) && value !== ''; +} + function unitFormater( value: any, config?: diff --git a/frontend/src/app/utils/time.ts b/frontend/src/app/utils/time.ts index c7fce1bed..93713cc25 100644 --- a/frontend/src/app/utils/time.ts +++ b/frontend/src/app/utils/time.ts @@ -20,10 +20,13 @@ import { DEFAULT_VALUE_DATE_FORMAT } from 'app/pages/MainPage/pages/VariablePage import { RECOMMEND_TIME } from 'globalConstants'; import moment, { Moment, unitOfTime } from 'moment'; -export function getTimeRange(amount?, unit?): (unitTime) => [string, string] { +export function getTimeRange( + amount?: [number, number], + unit?, +): (unitTime) => [string, string] { return unitOfTime => { - const startTime = moment().add(amount, unit).startOf(unitOfTime); - const endTime = moment().add(amount, unit).endOf(unitOfTime); + const startTime = moment().add(amount?.[0], unit).startOf(unitOfTime); + const endTime = moment().add(amount?.[1], unit).endOf(unitOfTime); return [ startTime.format(DEFAULT_VALUE_DATE_FORMAT), endTime.format(DEFAULT_VALUE_DATE_FORMAT), @@ -39,7 +42,7 @@ export function getTime( if (!!isStart) { return moment().add(amount, unit).startOf(unitOfTime); } - return moment().add(amount, unit).endOf(unitOfTime); + return moment().add(amount, unit).add(1, unit).startOf(unitOfTime); }; } @@ -47,25 +50,25 @@ export function formatTime(time: string | Moment, format): string { return moment(time).format(format); } -export function convertRelativeTimeRange(relativeTimeRange) { +export function recommendTimeRangeConverter(relativeTimeRange) { let timeRange = getTimeRange()('d'); switch (relativeTimeRange) { case RECOMMEND_TIME.TODAY: break; case RECOMMEND_TIME.YESTERDAY: - timeRange = getTimeRange(-1, 'd')('d'); + timeRange = getTimeRange([-1, 0], 'd')('d'); break; case RECOMMEND_TIME.THISWEEK: timeRange = getTimeRange()('w'); break; case RECOMMEND_TIME.LAST_7_DAYS: - timeRange = getTimeRange(-7, 'd')('d'); + timeRange = getTimeRange([-7, 0], 'd')('d'); break; case RECOMMEND_TIME.LAST_30_DAYS: - timeRange = getTimeRange(-30, 'd')('d'); + timeRange = getTimeRange([-30, 0], 'd')('d'); break; case RECOMMEND_TIME.LAST_90_DAYS: - timeRange = getTimeRange(-90, 'd')('d'); + timeRange = getTimeRange([-90, 0], 'd')('d'); break; case RECOMMEND_TIME.LAST_1_MONTH: timeRange = getTimeRange()('M'); diff --git a/frontend/src/globalConstants.ts b/frontend/src/globalConstants.ts index fc01383bc..8a64509d3 100644 --- a/frontend/src/globalConstants.ts +++ b/frontend/src/globalConstants.ts @@ -19,11 +19,14 @@ import { FONT_FAMILY } from 'styles/StyleConstants'; export const DATARTSEPERATOR = '@datart@'; +export const CHARTCONFIG_FIELD_PLACEHOLDER_UID = '@placeholder@'; +export const DATART_TRANSLATE_HOLDER = '@global@'; export enum StorageKeys { AuthorizationToken = 'AUTHORIZATION_TOKEN', LoggedInUser = 'LOGGED_IN_USER', ShareClientId = 'SHARE_CLIENT_ID', + Locale = 'LOCALE', } export const BASE_API_URL = '/api/v1'; export const BASE_RESOURCE_URL = '/'; @@ -35,10 +38,7 @@ export enum CommonFormTypes { Edit = 'edit', } -export const COMMON_FORM_TITLE_PREFIX = { - [CommonFormTypes.Add]: '新建', - [CommonFormTypes.Edit]: '编辑', -}; +export const TITLE_SUFFIX = ['archived', 'unpublished']; export const DEFAULT_DEBOUNCE_WAIT = 300; @@ -90,7 +90,6 @@ export const CHART_LINE_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; export const CHART_DRAG_ELEMENT_TYPE = { DATA_CONFIG_COLUMN: 'data_config_column', DATASET_COLUMN: 'dataset_column', - DATASET_GROUP_COLUMNS: 'dataset_group_columns', }; export const TIME_UNIT_OPTIONS = [ @@ -101,9 +100,11 @@ export const TIME_UNIT_OPTIONS = [ { name: 'weeks', value: 'w' }, { name: 'months', value: 'M' }, { name: 'years', value: 'y' }, + { name: 'quarters', value: 'Q' }, ]; export const TIME_DIRECTION = [ { name: 'ago', value: '-' }, + { name: 'current', value: '+0' }, { name: 'fromNow', value: '+' }, ]; @@ -143,11 +144,6 @@ export enum FilterSqlOperator { GreaterThanOrEqual = 'GTE', } -export const ResizeEvent = new Event('resize', { - bubbles: false, - cancelable: true, -}); - export const FILTER_TIME_FORMATTER_IN_QUERY = 'yyyy-MM-DD HH:mm:ss'; export const CONTROLLER_WIDTH_OPTIONS = [ @@ -163,7 +159,7 @@ export const CONTROLLER_WIDTH_OPTIONS = [ export enum NumberUnitKey { None = 'none', - // Engllish Unit + // English Unit Thousand = 'thousand', Million = 'million', Billion = 'billion', diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 6827d7695..0eca9bae8 100755 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,9 +1,7 @@ -import { ConfigProvider } from 'antd'; import 'antd/dist/antd.less'; -import zh_CN from 'antd/lib/locale/zh_CN'; import { App } from 'app'; import 'app/assets/fonts/iconfont.css'; -import ChartManager from 'app/pages/ChartWorkbenchPage/models/ChartManager'; +import 'core-js/features/string/replace-all'; import React from 'react'; import 'react-app-polyfill/ie11'; import 'react-app-polyfill/stable'; @@ -13,47 +11,40 @@ import { HelmetProvider } from 'react-helmet-async'; import { Provider } from 'react-redux'; import { configureAppStore } from 'redux/configureStore'; import { ThemeProvider } from 'styles/theme/ThemeProvider'; +import { Debugger } from 'utils/debugger'; import './locales/i18n'; -export const store = configureAppStore(); const MOUNT_NODE = document.getElementById('root') as HTMLElement; -/** - * hot-key [control,shift,command,c] - */ - -const MainApp = ; +const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; +const InspectorWrapper = IS_DEVELOPMENT ? Inspector : React.Fragment; -const InspectorWrapper = - process.env.NODE_ENV === 'development' ? Inspector : React.Fragment; -ChartManager.instance() - .load() - .catch(err => console.error('Fail to load customize charts with ', err)) - .finally(() => { - ReactDOM.render( - - - - - - {MainApp} - - - - - , - MOUNT_NODE, - ); +Debugger.instance.setEnable(IS_DEVELOPMENT); +export const store = configureAppStore(); - // Hot reloadable translation json files - if (module.hot) { - module.hot.accept(['./locales/i18n'], () => { - // No need to render the App again because i18next works with the hooks - }); - } +ReactDOM.render( + + + + + + + + + + + , + MOUNT_NODE, +); - if (process.env.NODE_ENV === 'production') { - if (typeof (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') { - (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = () => void 0; - } - } +// Hot reloadable translation json files +if (module.hot) { + module.hot.accept(['./locales/i18n'], () => { + // No need to render the App again because i18next works with the hooks }); +} + +if (!IS_DEVELOPMENT) { + if (typeof (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') { + (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = () => void 0; + } +} diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index 11e0f33eb..64970519e 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -1,8 +1,283 @@ { + "global": { + "tokenExpired": "Token expired, please log in", + "title": { + "action": "Action" + }, + "button": { + "create": "Create", + "save": "Save", + "edit": "Edit", + "archive": "Archive", + "restore": "Restore", + "delete": "Delete", + "info": "Info", + "open": "Open", + "close": "Close" + }, + "operation": { + "archiveConfirm": "Archive confirm", + "restoreConfirm": "Restore confirm", + "deleteConfirm": "Delete confirm", + "updateSuccess": "Update successful", + "deleteSuccess": "Delete successful", + "archiveSuccess": "Archive successful", + "restoreSuccess": "Restore successful", + "parseError": "Configuration parse error" + }, + "validation": { + "required": " is required", + "invalidPassword": "Password must be 6-20 characters", + "passwordNotMatch": "Password and confirm password does not match" + }, + "time": { + "minute": "Minute", + "hour": "Hours", + "day": "Day", + "month": "Month", + "year": "Years", + "m": "Minute", + "h": "Hours", + "d": "Day", + "sun": "Sunday", + "mon": "Monday", + "tues": "Tuesday", + "wednes": "Wednesday", + "thurs": "Thursday", + "fri": "Friday", + "satur": "Saturday" + }, + "modal": { + "title": { + "add": "Create ", + "edit": "Edit " + } + }, + "columnType": { + "string": "String", + "numeric": "Number", + "date": "Date" + }, + "columnCategory": { + "uncategorized": "Uncategorized", + "country": "Country", + "provinceorstate": "ProvinceOrState", + "city": "City", + "county": "County" + } + }, + "login": { + "username": "Username / Email", + "password": "Password", + "login": "Log in", + "register": "Sign up for an account", + "forgotPassword": "Can't log in?", + "alreadyLoggedIn": "Your account is already logged in", + "enter": "Click to enter", + "switch": "Switch accounts" + }, + "register": { + "sendSuccess": "Email sent successfully", + "username": "Username", + "email": "Email", + "password": "Password", + "register": "Register", + "desc1": "Already have an account? ", + "login": "Log In", + "tipTitle": "Check your email", + "tipDesc1": "We have sent you email to ", + "tipDesc2": ". Please", + "toMailbox": "click the link ", + "tipDesc3": "to confirm and activate your account.", + "tipDesc4": "Didn't receive?", + "resend": "Click to resend email", + "back": "Go back", + "registrationSuccess": "Registration Success" + }, + "forgotPassword": { + "return": "Return to log in", + "email": "Email", + "enterEmail": "Enter email", + "emailInvalid": "Invalid email address", + "username": "Username", + "enterUsername": "Enter username", + "nextStep": "Next Step", + "send": "Send verification code", + "desc1": "A confirmation email has been sent to ", + "desc2": "'s mailbox", + "desc3": ", please go to the mailbox to get the verification code, and then click next to reset the password.", + "password": "Password", + "enterNewPassword": "Enter new password", + "confirmNewPassword": "Confirm new password", + "verifyCode": "Verification code", + "reset": "Reset password", + "resetSuccess": "Your password has been successfully reset" + }, "main": { "nav": { + "vizs": "Vizs", + "views": "Views", + "sources": "Sources", + "schedules": "Schedules", + "members": "Members", + "permissions": "Permissions", + "settings": "Settings", "download": { - "title": "Download List" + "title": "Download List", + "status": { + "created": "Processing", + "downloaded": "Downloaded", + "done": "Done", + "failed": "Failed" + } + }, + "organization": { + "title": "Organizations", + "create": "Create organization", + "name": "Name", + "desc": "Description", + "save": "Save and enter" + }, + "account": { + "profile": { + "title": "Profile", + "clickUpload": "Click to upload", + "username": "Username", + "email": "Email", + "name": "Name", + "department": "Department" + }, + "changePassword": { + "title": "Password", + "oldPassword": "Password", + "newPassword": "New password", + "confirmPassword": "Confirm password" + }, + "switchLanguage": { + "title": "language" + }, + "logout": { + "title": "Logout" + } + } + }, + "subNavs": { + "variables": { + "title": "Public variables" + }, + "orgSettings": { + "title": "Organization Settings" + } + }, + "background": { + "loading": "Loading...", + "initError": "Initialization error, please refresh the page and try again", + "createOrg": "You have not joined any organization, click to create" + }, + "pages": { + "schedulePage": { + "constants": { + "email": "Email", + "weChat": "WeChat", + "picture": "Picture", + "taskExecution": "Task execution", + "configurationAnalysis": "Configure the parsing", + "getData": "Data collection", + "send": "Send" + }, + "sidebar": { + "index": { + "scheduledTaskList": "Scheduled task list", + "newTimedTask": "Create scheduled task", + "recycle": "Recycle" + }, + "scheduleList": { + "successStarted": "Successfully started", + "successStop": "Stopped successfully", + "successImmediately": "Executed successfully immediately", + "start": "Start", + "stop": "Stop", + "executeItNow": "Are you sure to execute it now?", + "executeImmediately": "Execute immediately" + }, + "editorPage": { + "index": { + "tickToSendContent": "Please tick to send content", + "addSuccess": "Added successfully", + "saveSuccess": "Saved successfully", + "restoredSuccess": "Restored successfully", + "success": "Success", + "moveToTrash": "Move to recycle bin", + "delete": "Delete", + "newTimedTask": "New timed task", + "sureToRestore?": "Are you sure to restore?", + "restore": "Reduction", + "sureToDelete": "Confirm delete?", + "allowModificationAfterStopping": "Allow modification after stopping", + "save": "Save", + "allowMoveAfterStopping": "Allow moving to the recycle bin after stopping", + "sureMoveRecycleBin": "Are you sure to move to the recycle bin?", + "basicSettings": "Basic settings", + "emailSetting": "Email settings", + "enterpriseWeChatSettings": "Enterprise WeChat Settings", + "sendContentSettings": "Send content settings" + }, + "weChartSetttingForm": { + "RobotWebhookAddress": "Robot webhook address", + "RobotWebhookAddressIsRequired": "Robot webhook address is required", + "fileType": "File type", + "picWidth": "Width", + "px": "px" + }, + "scheduleErrorLog": { + "index": { + "startTime": "Start", + "endTime": "End", + "logPhase": "Log phase", + "executionInformation": "Execution information", + "success": "Success", + "log": "Log" + } + }, + "emailSettingForm": { + "commonRichText": { + "pleaseEnter": "Please enter" + }, + "index": { + "CC": "CC", + "theme": "Theme", + "subjectIsRequired": "Theme is required", + "fileType": "File type", + "picWidth": "Width", + "px": "px", + "recipient": "Recipient", + "recipientIsRequired": "Recipient is required", + "bcc": "BCC", + "contentOfEmail": "Content of email" + }, + "mailTagFormItem": { + "placeholder": "Enter email address or name keyword search..." + } + }, + "basicBaseForm": { + "index": { + "nameAlreadyExists": "Name already exists", + "name": "Name", + "nameRequired": "Name is required", + "type": "Type", + "effectiveTimeRange": "Effective time range" + }, + "executeFormItem": { + "per": "Each", + "of": "", + "executionTimeSetting": "Execution time setting", + "expressionIsRequired": "Expression is required", + "pleaseEnterCronExpression": "Please enter cron expression", + "manualInput": "Manual input" + } + } + } + } } } }, @@ -40,8 +315,11 @@ "controllerFacadeTypes": { "dropdownList": "Dropdown List", "radioGroup": "Radio Group", - "multiDropdownList": "Multipile Dropdown List", + "checkboxGroup": "Checkbox Group", + "multiDropdownList": "multiple Dropdown List", "rangeTime": "Range Time", + "rangeTimePicker": "Range Time", + "recommendTime": "Recommend Time", "rangeValue": "Range Value", "text": "Text", "tree": "Tree", @@ -53,6 +331,12 @@ "hide": "Hide", "show": "Show", "condition": "Condition" + }, + "fontAlignment": { + "alignment": "Alignment", + "left": "Left", + "center": "Center", + "right": "right" } }, "filter": { @@ -77,7 +361,7 @@ "button": "Button", "date": { "recommend": "Recommend", - "manual": "Mannual", + "manual": "Manual", "today": "Today", "yesterday": "Yesterday", "this_week": "This Week", @@ -94,9 +378,11 @@ "weeks": "Weeks", "months": "Months", "years": "Years", + "quarters": "Quarter", "ago": "Ago", + "current": "Current", "fromNow": "From Now", - "pleaseSelect": "Please Select", + "select": "Select", "currentTime": "Current Time", "startTime": "Start Time", "endTime": "end Time", @@ -129,7 +415,7 @@ } }, "management": { - "title": "widgets manangement", + "title": "widgets management", "subTitle": "widgets list", "table": { "header": { @@ -141,21 +427,24 @@ } }, "workbench": { + "goBack": "Go back", "header": { "goBack": "Back", "run": "Run", "save": "Save", - "saveToDashboard": "Save to Dashboard", + "saveToDashboard": "Save to dashboard", "login": "Login", + "aggregationSwitch": "Aggregation", + "aggregationSwitchTip": "Switching aggregation will clear all configurations. Are you sure to turn it on / off?", + "open": "Open", + "close": "Close", "lang": { - "zh": "ZH", - "en": "EN" + "zh": "zh", + "en": "en" }, "format": { "local": "local", - "date": "date", - "ll": "LL", - "lll": "LLL" + "date": "date" } }, "dataview": { @@ -174,14 +463,16 @@ "title": { "content": "Data", "design": "Style", - "setting": "Setting" + "setting": "Analysis" }, "style": { "font": "Font", - "pleaseSelect": "Please select ...", + "select": "Select ...", "table": { "header": { + "newName": "Enter a new header name", "merge": "Merge", + "reset": "Reset", "moveUp": "moveUp", "moveDown": "moveDown", "columnName": "Column Name", @@ -195,6 +486,33 @@ } } }, + "conditionStyleTable": { + "btn": { + "add": "Add", + "edit": "Edit", + "remove": "Remove", + "confirm": "Confirm" + }, + "modal": { + "title": "Condition Style", + "notFoundContent": "Multiple values can be added (enter the content and press enter to complete the addition)" + }, + "header": { + "operator": "Operator", + "value": "Value", + "color": { + "title": "Color", + "background": "Background", + "text": "Text" + }, + "range": { + "title": "Applied range", + "cell": "Cell", + "row": "Row" + }, + "action": "Action" + } + }, "visualMap": { "title": "Visual Map", "show": "Show Visual Map", @@ -217,15 +535,15 @@ "data": { "dimension": "Dimension", "metrics": "Metric", - "mixed": "Mixed", + "mixed": "Columns", "filter": "Filter", "colorize": "Color", "colorRange": "Range Color", "info": "Info", "sort": "Sort", "size": "Size", - "drop": "Please drag some data fields here", - "dropCount": "Please drag {{count}} fields here at least", + "drop": "Drag some data fields here", + "dropCount": "Drag {{count}} fields here at least", "actions": { "sort": { "title": "Sort", @@ -252,7 +570,8 @@ "alias": { "title": "Field Setting", "name": "Alias Name", - "description": "Name Description" + "description": "Name Description", + "fieldName": "Field Name" }, "filter": { "title": "Filter" @@ -265,8 +584,8 @@ }, "axis": { "y": { - "left": "left axis demision", - "right": "right axis demision" + "left": "left axis dimension", + "right": "right axis dimension" } }, "enum": { @@ -281,12 +600,13 @@ "colorize": "colorize", "colorRange": "colorize", "colorSingle": "colorize", - "size": "size" + "size": "size", + "chooseTheme": "Choose theme" } } }, "setting": { - "pleaseSelect": "Please select ...", + "select": "Select ...", "displayCount": "Display Count", "enableCache": "Enable Cache", "cacheExpire": "Cache Expire Time(sec)", @@ -313,6 +633,10 @@ "opacity": "Opacity", "backgroundColor": "Background Color", "borderStyle": "Border Style" + }, + "paging": { + "title": "Common", + "pageSize": "Limit" } }, "present": { @@ -331,36 +655,520 @@ "cancel": "Cancel" } }, - "chartPreview": { + "action": { "common": { "confirm": "Please Confirm", "ok": "OK", - "cancel": "Cancel" + "cancel": "Cancel", + "save": "Save" }, "edit": "Edit", "run": "Run", "publish": "Publish", "unpublish": "Unpublish", + "published": "Published", + "unpublished": "Unpublished", + "archived": "Archived", + "play": "Play", + "playTip": "Press Esc to quit playing", "share": { "link": "Link", "password": "Password", "expireDate": "Expire Date", - "enablePassword": "Enable Password", + "enablePassword": "Password", "generateLink": "Generate Link", "shareLink": "Share Link", "downloadData": "Download Data" } + }, + "board": { + "action": { + "dataChart": "DataChart", + "addDataChartFormList": "Add DataChart Form List", + "createDataChartInBoard": "Create DataChart In Board", + "media": "Media", + "image": "Image", + "richText": "Rich Text", + "timer": "Timer", + "iFrame": "iFrame", + "video": "Video", + "container": "Container", + "tab": "Tab", + "controller": "Controller", + "toTop": "move To Top", + "toBottom": "move To Bottom", + "undo": "Undo", + "redo": "Redo", + "delete": "Delete", + "copy": "Copy", + "paste": "Paste" + }, + "controlTypes": { + "common": "Common", + "date": "Date", + "numeric": "Numeric", + "button": "Button" + }, + "setting": { + "setting": "Setting", + "board": "Board", + "widget": "Widget", + "widgetList": "Widget List", + "title": "Title", + "align": "Align", + "showTitle": "Show Title", + "position": "Position", + "xAxis": "xAxis", + "yAxis": "yAxis", + "size": "Size", + "px": "px", + "width": "Width", + "height": "Height", + "background": "Background", + "color": "Color", + "image": "Image", + "uploadTip": "click to Upload", + "padding": "padding", + "paddingTop": "padding Top", + "paddingRight": "padding Right", + "paddingBottom": "padding Bottom", + "paddingLeft": "padding Left", + "border": "Border", + "style": "Style", + "radius": "Radius", + "autoUpdate": "Auto Refresh", + "openAutoUpdate": "open Refresh", + "frequency": "frequency (s)", + "baseProperty": "Base Property", + "marginTB": "Margin-TB", + "marginLR": "Margin-LR", + "paddingTB": "Margin-TB", + "paddingLR": "Margin-LR", + "rowHeight": "rowHeight", + "scaleMode": "scale Mode", + "queryMode": "query Mode", + "openInitQuery": "open Init Query", + "cutIn": "Cut In", + "cutOut": "Cut Out", + "speed": "Speed", + "autoPlay": "Auto Play", + "duration": "Duration (s) ", + "delPagesTip": "Confirm to delete all selected story pages?", + "delPageTip": "Confirm to delete this story page?" + } + }, + "widget": { + "widget": "Widget", + "associatedWidget": "Associated Widgets", + "widgetName": "Widget Name", + "widgetType": "Widget Type", + "type": { + "chart": "Chart", + "widgetChart": "privateChart", + "dataChart": "publicChart", + "media": "Media", + "container": "Container", + "controller": "Controller", + "query": "Query", + "reset": "Reset", + "image": "Image", + "richText": "Rich Text", + "timer": "Timer", + "iFrame": "iFrame", + "video": "Video", + "tab": "Tab" + }, + "action": { + "refresh": "Refresh", + "fullScreen": "FullScreen", + "edit": "Edit", + "delete": "Delete", + "confirmDel": "Confirm Delete", + "ContainerConfirmDel": "The components within this component will also be deleted, confirm whether to delete them or not?", + "info": "Info", + "makeLinkage": "Set Linkage", + "closeLinkage": "Close Linkage", + "makeJump": "Set Jump", + "closeJump": "Close Jump" + } + }, + "linkage": { + "title": "Linkage settings", + "dataSource": "Data source", + "associatedWidgets": "Associated Widgets", + "associatedFields": "Associated Fields", + "selectTriggers": "Select the trigger field", + "selectLinker": "Select linkage field" + }, + "jump": { + "title": "Jump Settings", + "mode": "Jumping mode", + "target": "Target", + "INTERNAL": "Dashboard/DataChart", + "URL": "URL", + "parameters": "Parameters", + "controller": "Associated Controllers", + "associatedFields": "Associated Fields" + }, + "associate": { + "title": "Please associate fields/variables", + "field": "Field", + "variable": "Variable", + "noValueErr": "Please associate fields or variables", + "valueErr": "Please select a field or two variables" + }, + "sidebar": { + "folder": "Folder", + "presentation": "Presentation", + "folders": { + "folderTitle": "Dashboards & Datacharts", + "dashboard": "Create dashboard", + "dataChart": "Create datachart", + "folder": "Create folder", + "recycle": "Recycle" + }, + "storyboards": { + "title": "Storyboards", + "add": "Create storyboard", + "recycle": "Recycle" + } + }, + "lineOptions": { + "none": "none", + "solid": "solid", + "dashed": "dashed", + "dotted": "dotted", + "double": "double", + "hidden": "hidden", + "ridge": "ridge", + "groove": "groove", + "inset": "inset", + "outset": "outset" + }, + "scaleMode": { + "scaleWidth": "scaleWidth", + "scaleHeight": "scaleHeight", + "scaleFull": "scaleFull", + "noScale": "noScale" + }, + "control": { + "title": "Control Title", + "valueConfig": "Value Config", + "selectViewField": "Select ViewField", + "selectDefaultValue": "Select DefaultValue", + "defaultValue": "Default Value", + "visibility": "Visibility", + "sqlOperator": "Sql Operator", + "dateType": "Date Type", + "step": "Step", + "showMark": "Show Mark", + "common": "General", + "custom": "Customize" + }, + "date": { + "year": "Year", + "quarter": "Quarter", + "month": "Month", + "week": "Week", + "date": "Date", + "dateTime": "Date Time" + }, + "saveForm": { + "name": "Name", + "description": "Description", + "boardType": { + "label": "Board type", + "auto": "Auto", + "free": "Free" + }, + "parent": "Parent", + "root": "Root", + "vizType": { + "datachart": "Datachart", + "dashboard": "Dashboard", + "folder": "Folder", + "storyboard": "Storyboard" + } + }, + "main": { + "publishSuccess": "Publish success", + "unpublishSuccess": "Unpublish success", + "empty": "Select vizs in the sidebar" } }, + "view": { + "loading": "Loading...", + "selectSource": "You must select a source", + "empty": "Select views in the sidebar", + "resultEmpty1": "Click ", + "resultEmpty2": " button to execute, the results will be displayed here", + "errorTitle": "Error occurred", + "tabs": { + "discard": "Discard", + "cancel": "Cancel", + "execute": "Execute", + "warning": "There are unexecuted changes, still execute?" + }, + "editor": { + "folder": "Folder", + "source": "Select source", + "run": "Run", + "runSelection": "Run selection", + "runWinTip": "Win: [Ctrl + Enter]", + "runMacTip": "Mac: [Command + Enter]", + "beautify": "Beautify", + "save": "Save", + "saveWinTip": "Win: [Ctrl + S]", + "saveMacTip": "Mac: [Command + S]", + "info": "Info", + "saveAs": "Save as", + "saveFragment": "Save Fragment", + "readonlyTip": "Not editable in recycle" + }, + "properties": { + "reference": "Source reference", + "variable": "Variables", + "columnPermissions": "Column Permissions" + }, + "resource": { + "title": "Source info", + "search": "Search database / table / column" + }, + "variable": { + "title": "Variables", + "formTitle": "variable", + "add": "Create variable", + "prefix": "[Public]", + "suffix": "duplicate" + }, + "columnPermission": { + "title": "Column permissions", + "search": "Search role keywords", + "partial": "Partial", + "none": "None", + "all": "All" + }, + "sidebar": { + "title": "Views", + "addView": "Create view", + "addFolder": "Create folder", + "parent": "Parent", + "recycle": "Recycle" + }, + "saveForm": { + "title": "View", + "name": "Name", + "folder": "Directory", + "root": "Root", + "advanced": "Advanced", + "concurrencyControl": "Concurrency control", + "concurrencyControlMode": "Mode", + "dirtyread": "Dirty read", + "fastfailover": "Fast failover", + "cache": "Cache", + "cacheExpires": "Expires" + }, + "schemaTable": { + "category": "Category", + "type": "Type", + "typeAndCategory": "Type & Category" + } + }, + "source": { + "source": "Source", + "testSuccess": "Test connection succeeded", + "createSuccess": "Source created successfully", + "archived": "Archived ", + "noPermission": "You do not have permission to access this page", + "sidebar": { + "title": "Sources", + "add": "Create source", + "recycle": "Recycle" + }, + "form": { + "name": "Name", + "type": "Type", + "test": "Test connection", + "file": "File", + "selectFile": "Select file", + "addProperty": "Add property", + "editProperty": "Edit property", + "addConfig": "Add configuration", + "editConfig": "Edit configuration", + "duplicate": "Duplicate key", + "duplicateName": "Duplicate Name" + } + }, + "member": { + "memberDetail": { + "title": "Member detail", + "grantOwner": "Granted to organization owner", + "revokeOwner": "Revoke owner", + "remove": "Remove", + "removeConfirm": "Remove confirm", + "removeSuccess": "Remove successful", + "grantSuccess": "Grant successful", + "revokeSuccess": "Revoke successful", + "username": "Username", + "email": "Email", + "name": "Name", + "roles": "Roles", + "assignRole": "Assign roles" + }, + "roleDetail": { + "role": "Role", + "createSuccess": "Role created successfully", + "roleName": "Name", + "description": "Description", + "relatedMember": "Related member", + "addMember": "Add member", + "deleteAll": "Delete all", + "searchMember": "Search member", + "username": "Username", + "email": "Email", + "name": "Name", + "remove": "Remove" + }, + "form": { + "search": "Search or paste the emails of invited members", + "needConfirm": "Need email confirmation" + }, + "sidebar": { + "member": "Members", + "role": "Roles", + "memberTitle": "Members", + "inviteMember": "Invite member", + "inviteSuccess": "The invitation email has been sent successfully", + "invalidEmail": "Invalid email addresses", + "roleTitle": "Roles", + "addRole": "Create role" + } + }, + "permission": { + "empty1": "Select ", + "emptyResource": "resources", + "emptySubject": "roles or members", + "empty2": " in the sidebar", + "allResources": "All resources", + "folder": "Folder", + "presentation": "Presentation", + "search": "Search keywords", + "searchResources": "Search resource keywords", + "member": "Member", + "role": "Role", + "modulePermission": "Module Permission", + "modulePermissionDesc": "You can only access the module after enabling the module permission", + "resourceDetail": "Resource detail", + "createStoryboard": "Create storyboard", + "resourceName": "Resource name", + "privileges": "Privileges", + "add": "Create ", + "viewpoint": { + "subject": "Subject view", + "resource": "Resource view" + }, + "module": { + "source": "Source", + "view": "View", + "viz": "Viz", + "schedule": "Schedule" + }, + "modulePermissionLabel": { + "Enable": "Enable", + "Disable": "Disable" + }, + "createPermissionLabel": { + "Create": "Enable", + "Disable": "Disable" + }, + "privilegeLabel": { + "viz": ["View", "Download", "Share", "Manage"], + "view": ["Access", "Manage"], + "source": ["Access", "Manage"], + "schedule": ["Manage"] + } + }, + "variable": { + "title": "Public variables", + "public": "Public variables", + "name": "Name", + "label": "Label", + "type": "Type", + "valueType": "Value type", + "permission": { + "label": "Permission", + "hidden": "Hidden", + "readonly": "Readonly", + "editable": "Editable" + }, + "defaultValue": "Default value", + "expression": "Use expression default value", + "duplicateName": "Duplicate Name", + "related": "Related roles & members", + "deleteAllConfirm": "Delete confirm", + "deleteAll": "Delete all", + "enterToAdd": "Enter to add", + "enterExpression": "Enter expression", + "relatedRole": "Related roles", + "relatedMember": "Related members", + "useDefaultValue": "Use variable default value", + "value": "Value", + "variableType": { + "query": "Query", + "permission": "Permission" + }, + "variableValueType": { + "string": "String", + "numeric": "Number", + "date": "Date", + "fragment": "Expression" + } + }, + "orgSetting": { + "info": "Info", + "avatar": "Avatar", + "clickToUpload": "Click to upload", + "name": "Name", + "description": "Description", + "deleteOrg": "Delete Organization", + "deleteOrgDesc": "When deleting an organization, all resources, members, roles, and other configurations belonging to the organization will be permanently deleted. Please operate with caution.", + "cancel": "Cancel", + "delete": "Delete", + "enterOrgName": "Enter organization name to delete" + }, + "confirmInvite": { + "join": "Join successful", + "confirming": "Confirming" + }, + "active": { + "activating": "Activating" + }, + "authorization": { + "processing": "Processing" + }, + "notfound": { + "title": "Page not found" + }, "share": { "common": { - "confirm": "Please Confirm", + "confirm": "Confirm", "ok": "OK", "cancel": "Cancel" }, "modal": { "password": "Password", - "pleaseInputPassword": "please input password" + "enterPassword": "Enter password" + } + }, + "components": { + "colorPicker": { + "more": "More", + "ok": "OK", + "cancel": "Cancel" + }, + "listTitle": { + "search": "Search", + "searchValue": "Search keywords" } } } diff --git a/frontend/src/locales/i18n.ts b/frontend/src/locales/i18n.ts index db341b868..b0fc6c0ef 100644 --- a/frontend/src/locales/i18n.ts +++ b/frontend/src/locales/i18n.ts @@ -1,6 +1,12 @@ +import antd_en_US from 'antd/lib/locale/en_US'; +import antd_zh_CN from 'antd/lib/locale/zh_CN'; +import { StorageKeys } from 'globalConstants'; import i18next from 'i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; +import moment from 'moment'; +import 'moment/locale/zh-cn'; import { initReactI18next } from 'react-i18next'; +import { instance as requestInstance } from 'utils/request'; import en from './en/translation.json'; import { convertLanguageJsonToObject } from './translations'; import zh from './zh/translation.json'; @@ -19,8 +25,15 @@ convertLanguageJsonToObject(en); export const changeLang = lang => { i18next.changeLanguage(lang); + requestInstance.defaults.headers['Accept-Language'] = + lang === 'zh' ? 'zh-CN' : 'en-US'; // FIXME locale + localStorage.setItem(StorageKeys.Locale, lang); + moment.locale(lang === 'zh' ? 'zh-cn' : 'en-us'); // FIXME locale }; +const initialLocale = getInitialLocale(); +moment.locale(initialLocale); + export const i18n = i18next // pass the i18n instance to react-i18next. .use(initReactI18next) @@ -30,7 +43,7 @@ export const i18n = i18next // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ - lng: 'zh', + lng: initialLocale, resources: translationsJson, fallbackLng: 'en', debug: @@ -40,3 +53,21 @@ export const i18n = i18next escapeValue: false, // not needed for react as it escapes by default }, }); + +export const antdLocales = { + en: antd_en_US, + zh: antd_zh_CN, +}; + +function getInitialLocale() { + const storedLocale = localStorage.getItem(StorageKeys.Locale); + if (!storedLocale) { + const browserLocale = ['zh', 'zh-CN'].includes(navigator.language) // FIXME locale + ? 'zh' + : 'en'; + localStorage.setItem(StorageKeys.Locale, browserLocale); + return browserLocale; + } else { + return storedLocale; + } +} diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json index 132224682..62283878a 100644 --- a/frontend/src/locales/zh/translation.json +++ b/frontend/src/locales/zh/translation.json @@ -1,8 +1,283 @@ { + "global": { + "tokenExpired": "会话过期,请重新登录", + "title": { + "action": "操作" + }, + "button": { + "create": "新建", + "save": "保存", + "edit": "编辑", + "archive": "移至回收站", + "restore": "还原", + "delete": "删除", + "info": "基本信息", + "open": "开启", + "close": "关闭" + }, + "operation": { + "archiveConfirm": "确定移至回收站?", + "restoreConfirm": "确定还原?", + "deleteConfirm": "确定删除?", + "updateSuccess": "修改成功", + "deleteSuccess": "删除成功", + "archiveSuccess": "成功移至回收站", + "restoreSuccess": "还原成功", + "parseError": "配置解析错误" + }, + "validation": { + "required": "不能为空", + "invalidPassword": "密码长度为6-20位", + "passwordNotMatch": "两次输入的密码不一致" + }, + "time": { + "minute": "分钟", + "hour": "小时", + "day": "天", + "month": "月", + "year": "年", + "m": "分", + "h": "时", + "d": "日", + "sun": "星期天", + "mon": "星期一", + "tues": "星期二", + "wednes": "星期三", + "thurs": "星期四", + "fri": "星期五", + "satur": "星期六" + }, + "modal": { + "title": { + "add": "新建", + "edit": "编辑" + } + }, + "columnType": { + "string": "字符", + "numeric": "数值", + "date": "日期" + }, + "columnCategory": { + "uncategorized": "未分类", + "country": "国家", + "provinceorstate": "省份", + "city": "城市", + "county": "区县" + } + }, + "login": { + "username": "用户名 / 邮箱", + "password": "密码", + "login": "登录", + "register": "注册新账号", + "forgotPassword": "忘记密码?", + "alreadyLoggedIn": "账号已登录", + "enter": "点击进入", + "switch": "切换账号" + }, + "register": { + "sendSuccess": "发送成功", + "username": "用户名", + "email": "邮箱", + "password": "密码", + "register": "注册", + "desc1": "已有账号?", + "login": "点击登录", + "tipTitle": "请查收电子邮件", + "tipDesc1": "我们向 ", + "tipDesc2": " 发送了一封电子邮件,请", + "toMailbox": "前往邮箱", + "tipDesc3": "确认与激活账号。", + "tipDesc4": "没有收到?", + "resend": "重新发送电子邮件", + "back": "返回上一步", + "registrationSuccess": "注册成功" + }, + "forgotPassword": { + "return": "返回登录", + "email": "邮箱", + "enterEmail": "输入邮箱", + "emailInvalid": "邮箱格式不正确", + "username": "用户名", + "enterUsername": "输入用户名", + "nextStep": "下一步", + "send": "发送验证码", + "desc1": "一封确认信已经发到 ", + "desc2": " 所关联的邮箱", + "desc3": ",请前往该邮箱获取验证码,然后点击下一步重置密码。", + "password": "密码", + "enterNewPassword": "输入新密码", + "confirmNewPassword": "确认新密码", + "verifyCode": "验证码", + "reset": "重置密码", + "resetSuccess": "重置密码成功" + }, "main": { "nav": { + "vizs": "可视化", + "views": "数据视图", + "sources": "数据源", + "schedules": "定时任务", + "members": "成员与角色", + "permissions": "权限", + "settings": "设置", "download": { - "title": "下载列表" + "title": "下载列表", + "status": { + "created": "处理中", + "downloaded": "已下载", + "done": "已完成", + "failed": "已失败" + } + }, + "organization": { + "title": "组织列表", + "create": "新建组织", + "name": "名称", + "desc": "描述", + "save": "保存并进入" + }, + "account": { + "profile": { + "title": "账号设置", + "clickUpload": "点击上传", + "username": "用户名", + "email": "邮箱", + "name": "姓名", + "department": "部门" + }, + "changePassword": { + "title": "修改密码", + "oldPassword": "密码", + "newPassword": "新密码", + "confirmPassword": "确定新密码" + }, + "switchLanguage": { + "title": "切换语言" + }, + "logout": { + "title": "退出登录" + } + } + }, + "subNavs": { + "variables": { + "title": "公共变量设置" + }, + "orgSettings": { + "title": "组织设置" + } + }, + "background": { + "loading": "应用配置加载中…", + "initError": "初始化错误,请刷新页面重试", + "createOrg": "未加入任何组织,点击创建" + }, + "pages": { + "schedulePage": { + "constants": { + "email": "邮箱", + "weChat": "微信", + "picture": "图片", + "taskExecution": "任务执行", + "configurationAnalysis": "配置解析", + "getData": "数据获取", + "send": "发送" + }, + "sidebar": { + "index": { + "scheduledTaskList": "定时任务列表", + "newTimedTask": "新建定时任务", + "recycle": "回收站" + }, + "scheduleList": { + "successStarted": "启动成功", + "successStop": "停止成功", + "successImmediately": "立即执行成功", + "start": "启动", + "stop": "停止", + "executeItNow": "确定立即执行?", + "executeImmediately": "立即执行" + }, + "editorPage": { + "index": { + "tickToSendContent": "请勾选发送内容", + "addSuccess": "新增成功", + "saveSuccess": "保存成功", + "restoredSuccess": "还原成功", + "success": "成功", + "moveToTrash": "移至回收站", + "delete": "删除", + "newTimedTask": "新建定时任务", + "sureToRestore?": "确定还原?", + "restore": "还原", + "sureToDelete": "确定删除?", + "allowModificationAfterStopping": "停止后允许修改", + "save": "保存", + "allowMoveAfterStopping": "停止后允许移至回收站", + "sureMoveRecycleBin": "确定移至回收站?", + "basicSettings": "基本设置", + "emailSetting": "邮件设置", + "enterpriseWeChatSettings": "企业微信设置", + "sendContentSettings": "发送内容设置" + }, + "weChartSetttingForm": { + "RobotWebhookAddress": "机器人webhook地址", + "RobotWebhookAddressIsRequired": "机器人webhook地址为必填项", + "fileType": "文件类型", + "picWidth": "图片宽度", + "px": "像素" + }, + "scheduleErrorLog": { + "index": { + "startTime": "开始时间", + "endTime": "结束时间", + "logPhase": "日志阶段", + "executionInformation": "执行信息", + "success": "成功", + "log": "日志" + } + }, + "emailSettingForm": { + "commonRichText": { + "pleaseEnter": "请输入" + }, + "index": { + "CC": "抄送", + "theme": "主题", + "subjectIsRequired": "主题为必填项", + "fileType": "文件类型", + "picWidth": "图片宽度", + "px": "像素", + "recipient": "收件人", + "recipientIsRequired": "收件人为必填项", + "bcc": "密送", + "contentOfEmail": "邮件内容" + }, + "mailTagFormItem": { + "placeholder": "输入邮箱或姓名关键字查找..." + } + }, + "basicBaseForm": { + "index": { + "nameAlreadyExists": "名称已存在", + "name": "名称", + "nameRequired": "名称为必填项", + "type": "类型", + "effectiveTimeRange": "有效时间范围" + }, + "executeFormItem": { + "per": "每", + "of": "的", + "executionTimeSetting": "执行时间设置", + "expressionIsRequired": "表达式为必填项", + "pleaseEnterCronExpression": "请输入cron表达式", + "manualInput": "手动输入" + } + } + } + } } } }, @@ -40,8 +315,11 @@ "controllerFacadeTypes": { "dropdownList": "下拉列表", "radioGroup": "单选按钮", + "checkboxGroup": "多选框", "multiDropdownList": "多选下拉列表", "rangeTime": "日期范围", + "rangeTimePicker": "日期范围", + "recommendTime": "推荐时间", "rangeValue": "数值范围", "text": "文本", "tree": "下拉树列表", @@ -53,6 +331,12 @@ "hide": "隐藏", "show": "显示", "condition": "条件" + }, + "fontAlignment": { + "alignment": "对齐方式", + "left": "左对齐", + "center": "居中", + "right": "右对齐" } }, "filter": { @@ -94,9 +378,11 @@ "weeks": "星期", "months": "月", "years": "年", - "ago": "以前", - "fromNow": "以后", - "pleaseSelect": "请选择", + "quarters": "季度", + "ago": "前", + "current": "当前", + "fromNow": "后", + "select": "请选择", "currentTime": "当前时间", "startTime": "开始时间", "endTime": "结束时间", @@ -148,20 +434,22 @@ "save": "保存", "saveToDashboard": "保存到仪表盘", "login": "登陆", + "aggregationSwitch": "数据聚合", + "aggregationSwitchTip": "切换数据聚合会清空所有配置,确认开启/关闭?", + "open": "开启", + "close": "关闭", "lang": { "zh": "中文", "en": "英文" }, "format": { "local": "本地", - "date": "日期", - "ll": "LL", - "lll": "LLL" + "date": "日期" } }, "dataview": { - "createComputedFields": "创建计算字段", - "createVariableFields": "创建查询变量", + "createComputedFields": "新建计算字段", + "createVariableFields": "新建查询变量", "fieldName": "名称", "type": "类型", "field": "字段", @@ -175,14 +463,16 @@ "title": { "content": "数据", "design": "样式", - "setting": "配置" + "setting": "分析" }, "style": { "font": "字体", - "pleaseSelect": "请选择 ...", + "select": "请选择 ...", "table": { "header": { + "newName": "请输入新名称", "merge": "合并", + "reset": "还原初始值", "moveUp": "上移", "moveDown": "下移", "columnName": "表头名称", @@ -196,6 +486,33 @@ } } }, + "conditionStyleTable": { + "btn": { + "add": "新增", + "edit": "编辑", + "remove": "删除", + "confirm": "请确认是否删除" + }, + "modal": { + "title": "条件格式", + "notFoundContent": "可添加多个值(输入内容后按下回车键完成添加)" + }, + "header": { + "operator": "关系", + "value": "值", + "color": { + "title": "颜色", + "background": "背景", + "text": "文字" + }, + "range": { + "title": "应用范围", + "cell": "单元格", + "row": "整行" + }, + "action": "操作" + } + }, "visualMap": { "title": "视觉映射", "show": "显示视觉映射", @@ -218,9 +535,10 @@ "data": { "dimension": "维度", "metrics": "指标", - "mixed": "混合", + "mixed": "字段", "filter": "筛选", "colorize": "颜色", + "colorRange": "Range Color", "info": "信息", "sort": "排序", "size": "大小", @@ -252,7 +570,8 @@ "alias": { "title": "字段设置", "name": "字段别名", - "description": "名称描述" + "description": "名称描述", + "fieldName": "字段名称" }, "filter": { "title": "条件筛选" @@ -281,12 +600,13 @@ "colorize": "着色", "colorRange": "着色", "colorSingle": "着色", - "size": "大小" + "size": "大小", + "chooseTheme": "选择主题" } } }, "setting": { - "pleaseSelect": "请选择 ...", + "select": "请选择 ...", "displayCount": "显示数量", "enableCache": "启用缓存", "cacheExpire": "缓存时长(秒)", @@ -313,6 +633,10 @@ "opacity": "透明度", "backgroundColor": "背景颜色", "borderStyle": "边框样式" + }, + "paging": { + "title": "常规", + "pageSize": "总行数" } }, "present": { @@ -331,16 +655,22 @@ "cancel": "取消" } }, - "chartPreview": { + "action": { "common": { "confirm": "请确认", "ok": "确认", - "cancel": "取消" + "cancel": "取消", + "save": "保存" }, "edit": "编辑", "run": "运行", "publish": "发布", "unpublish": "取消发布", + "published": "已发布", + "unpublished": "未发布", + "archived": "已归档", + "play": "播放", + "playTip": "按 Esc 退出播放", "share": { "link": "链接", "password": "密码", @@ -350,8 +680,488 @@ "shareLink": "分享链接", "downloadData": "下载数据" } + }, + "board": { + "action": { + "dataChart": "数据图表", + "addDataChartFormList": "添加已有数据图表", + "createDataChartInBoard": "新建数据图表", + "media": "媒体组件", + "image": "图片", + "richText": "富文本", + "timer": "计时器", + "iFrame": "iFrame", + "video": "视频", + "container": "容器", + "tab": "标签页", + "controller": "控制器", + "toTop": "图层置顶", + "toBottom": "图层置底", + "undo": "撤销", + "redo": "重做", + "delete": "删除", + "copy": "复制", + "paste": "粘贴" + }, + "controlTypes": { + "common": "常规", + "date": "日期", + "numeric": "数值", + "button": "按钮" + }, + "setting": { + "setting": "设置", + "board": "面板", + "widget": "组件", + "widgetList": "组件列表", + "title": "标题", + "align": "对齐", + "showTitle": "显示标题", + "position": "位置", + "xAxis": "x轴", + "yAxis": "y轴", + "size": "尺寸", + "px": "像素", + "width": "宽", + "height": "高", + "background": "背景", + "color": "颜色", + "image": "图片", + "uploadTip": "点击上传", + "padding": "内边距", + "paddingTop": "上边距", + "paddingRight": "右边距", + "paddingBottom": "下边距", + "paddingLeft": "左边距", + "border": "边框", + "style": "样式", + "radius": "圆角", + "autoUpdate": "自动刷新数据", + "openAutoUpdate": "开启", + "frequency": "定时同步频率(秒)", + "baseProperty": "基本属性", + "marginTB": "上下外边距", + "marginLR": "左右外边距", + "paddingTB": "上下内边距", + "paddingLR": "左右内边距", + "rowHeight": "行高", + "scaleMode": "缩放模式", + "queryMode": "查询模式", + "openInitQuery": "开启初始化查询", + "cutIn": "切入", + "cutOut": "切出", + "speed": "速度", + "autoPlay": "自动播放", + "duration": "停留时间(秒)", + "delPagesTip": "确认删除所有选中的故事页", + "delPageTip": "确认删除此故事页?" + } + }, + "widget": { + "widget": "组件", + "associatedWidget": "关联组件", + "widgetName": "组件名称", + "widgetType": "组件类型", + "type": { + "chart": "数据图表", + "widgetChart": "私有图表", + "dataChart": "公共图表", + "media": "媒体", + "container": "容器", + "controller": "控制器", + "query": "查询", + "reset": "重置", + "image": "图片", + "richText": "富文本", + "timer": "时间器", + "iFrame": "iFrame", + "video": "视频", + "tab": "Tab" + }, + "action": { + "refresh": "同步数据", + "fullScreen": "全屏", + "edit": "编辑", + "delete": "删除", + "confirmDel": "确认删除", + "ContainerConfirmDel": "该组件内的组件也会被删除,确认是否删除?", + "info": "基本信息", + "makeLinkage": "联动设置", + "closeLinkage": "关闭联动", + "makeJump": "跳转设置", + "closeJump": "关闭跳转" + } + }, + "linkage": { + "title": "联动设置", + "dataSource": "Data source", + "associatedWidgets": "关联组件", + "associatedFields": "关联字段", + "selectTriggers": "选择触发字段", + "selectLinker": "选择联动字段" + }, + "jump": { + "title": "跳转设置", + "mode": "跳转方式", + "target": "跳转目标", + "INTERNAL": "仪表盘 / 数据图表", + "URL": "URL", + "parameters": "Parameters", + "controller": "关联控制器", + "associatedFields": "关联字段" + }, + "associate": { + "title": "关联字段/变量", + "field": "字段", + "variable": "变量", + "noValueErr": "请关联字段 或 变量", + "valueErr": "请选择字段 或 两个变量" + }, + "sidebar": { + "folder": "目录", + "presentation": "演示", + "folders": { + "folderTitle": "仪表板 & 数据图表", + "dashboard": "新建仪表板", + "dataChart": "新建数据图表", + "folder": "新建目录", + "recycle": "回收站" + }, + "storyboards": { + "title": "故事板列表", + "add": "新建故事板", + "recycle": "回收站" + } + }, + "lineOptions": { + "none": "无", + "solid": "实线", + "dashed": "虚线", + "dotted": "点线", + "double": "双线", + "hidden": "隐藏", + "ridge": "垄状", + "groove": "凹槽", + "inset": "内凹", + "outset": "外凸" + }, + "scaleMode": { + "scaleWidth": "等比宽度缩放", + "scaleHeight": "等比高度缩放", + "scaleFull": "全屏铺满", + "noScale": "实际尺寸" + }, + "control": { + "title": "控制器标题", + "valueConfig": "取值配置", + "selectViewField": "选择数据源字段", + "selectDefaultValue": "选择默认值", + "defaultValue": "默认值", + "visibility": "是否显示", + "sqlOperator": "对应关系", + "dateType": "日期类型", + "step": "步长", + "showMark": "显示标签", + "common": "常规", + "custom": "自定义" + }, + "date": { + "year": "年", + "quarter": "季度", + "month": "月", + "week": "周", + "date": "日期", + "dateTime": "日期时间" + }, + "saveForm": { + "name": "名称", + "description": "描述", + "boardType": { + "label": "布局类型", + "auto": "自动", + "free": "自由" + }, + "parent": "所属目录", + "root": "根目录", + "vizType": { + "datachart": "数据图表", + "dashboard": "仪表板", + "folder": "目录", + "storyboard": "故事板" + } + }, + "main": { + "publishSuccess": "发布成功", + "unpublishSuccess": "取消发布成功", + "empty": "请在左侧列表选择可视化" } }, + "view": { + "loading": "加载中...", + "selectSource": "请选择数据源", + "empty": "请在左侧列表选择数据视图", + "resultEmpty1": "请点击 ", + "resultEmpty2": " 按钮执行,运行结果将在此处展示", + "errorTitle": "执行错误", + "tabs": { + "discard": "放弃", + "cancel": "取消", + "execute": "执行", + "warning": "有未执行的修改,是否执行?" + }, + "editor": { + "folder": "目录", + "source": "请选择数据源", + "run": "执行", + "runSelection": "执行片段", + "runWinTip": "Win: [Ctrl + Enter]", + "runMacTip": "Mac: [Command + Enter]", + "beautify": "美化", + "save": "保存", + "saveWinTip": "Win: [Ctrl + S]", + "saveMacTip": "Mac: [Command + S]", + "info": "详情设置", + "saveAs": "另存为", + "saveFragment": "保存片段", + "readonlyTip": "回收站中不可编辑" + }, + "properties": { + "reference": "数据源信息", + "variable": "变量配置", + "columnPermissions": "列权限" + }, + "resource": { + "title": "数据源信息", + "search": "搜索数据库 / 表 / 字段关键字" + }, + "variable": { + "title": "变量配置", + "formTitle": "变量", + "add": "新建变量", + "prefix": "[公共]", + "suffix": "重复" + }, + "columnPermission": { + "title": "列权限", + "search": "搜索角色关键字", + "partial": "部分字段", + "none": "不可见", + "all": "全部字段" + }, + "sidebar": { + "title": "数据视图列表", + "addView": "新建数据视图", + "addFolder": "新建目录", + "parent": "所属目录", + "recycle": "回收站" + }, + "saveForm": { + "title": "数据视图", + "name": "名称", + "folder": "目录", + "root": "根目录", + "advanced": "高级配置", + "concurrencyControl": "并发控制", + "concurrencyControlMode": "模式", + "dirtyread": "延迟更新", + "fastfailover": "快速失败", + "cache": "缓存", + "cacheExpires": "失效时间" + }, + "schemaTable": { + "category": "分类", + "type": "类型", + "typeAndCategory": "类型与分类" + } + }, + "source": { + "source": "数据源", + "testSuccess": "测试连接成功", + "createSuccess": "新建数据源成功", + "archived": "已归档", + "noPermission": "您没有权限访问该页面", + "sidebar": { + "title": "数据源列表", + "add": "新建数据源", + "recycle": "回收站" + }, + "form": { + "name": "名称", + "type": "类型", + "test": "测试连接", + "file": "文件", + "selectFile": "选择文件", + "addProperty": "新增配置项", + "editProperty": "编辑配置项", + "addConfig": "新增配置", + "editConfig": "编辑配置", + "duplicateKey": "Key不能重复", + "duplicateName": "名称重复" + } + }, + "member": { + "memberDetail": { + "title": "成员详情", + "grantOwner": "设为组织拥有者", + "revokeOwner": "撤销拥有者", + "remove": "移除成员", + "removeConfirm": "确定移除该成员?", + "removeSuccess": "移除成功", + "grantSuccess": "设置成功", + "revokeSuccess": "撤销成功", + "username": "用户名", + "email": "邮箱", + "name": "用户姓名", + "roles": "角色列表", + "assignRole": "为用户指定角色" + }, + "roleDetail": { + "role": "角色", + "createSuccess": "新建角色成功", + "roleName": "名称", + "description": "描述", + "relatedMember": "关联成员", + "addMember": "添加成员", + "deleteAll": "批量删除", + "searchMember": "搜索成员关键字", + "username": "用户名", + "email": "邮箱", + "name": "用户姓名", + "remove": "移除" + }, + "form": { + "search": "请搜索或粘贴被邀请成员邮箱", + "needConfirm": "需要被邀请成员邮件确认" + }, + "sidebar": { + "member": "成员", + "role": "角色", + "memberTitle": "成员列表", + "inviteMember": "邀请成员", + "inviteSuccess": "邀请邮件已成功发送", + "invalidEmail": "请检查以下无效邮件地址", + "roleTitle": "角色列表", + "addRole": "新建角色" + } + }, + "permission": { + "empty1": "请在左侧列表选择", + "emptyResource": "资源项", + "emptySubject": "角色或用户", + "empty2": "", + "allResources": "所有资源", + "folder": "目录", + "presentation": "演示", + "search": "搜索关键字", + "searchResources": "搜索资源名称关键字", + "member": "成员", + "role": "角色", + "modulePermission": "功能权限", + "modulePermissionDesc": "开启功能权限之后,用户才能在 Datart 界面上使用该功能", + "resourceDetail": "资源明细", + "createStoryboard": "新增故事板", + "resourceName": "资源名称", + "privileges": "权限详情", + "add": "新建", + "viewpoint": { + "subject": "常规视图", + "resource": "资源视图" + }, + "module": { + "source": "数据源", + "view": "数据视图", + "viz": "可视化", + "schedule": "定时任务" + }, + "modulePermissionLabel": { + "Enable": "启用", + "Disable": "禁用" + }, + "createPermissionLabel": { + "Create": "启用", + "Disable": "禁用" + }, + "privilegeLabel": { + "viz": { + "0": "查看", + "1": "下载", + "2": "分享", + "3": "管理" + }, + "view": { + "0": "使用", + "1": "管理" + }, + "source": { + "0": "使用", + "1": "管理" + }, + "schedule": { + "0": "管理" + } + } + }, + "variable": { + "title": "公共变量列表", + "public": "公共变量", + "name": "名称", + "label": "标签", + "type": "类型", + "valueType": "值类型", + "permission": { + "label": "编辑权限", + "hidden": "不可见", + "readonly": "只读", + "editable": "可编辑" + }, + "defaultValue": "默认值", + "expression": "使用表达式作为默认值", + "duplicateName": "名称重复", + "related": "关联角色或成员", + "deleteAllConfirm": "确认删除全部?", + "deleteAll": "批量删除", + "enterToAdd": "输入默认值后回车添加", + "enterExpression": "请输入表达式", + "relatedRole": "关联角色", + "relatedMember": "关联成员", + "useDefaultValue": "使用变量默认值", + "value": "值", + "variableType": { + "query": "查询变量", + "permission": "权限变量" + }, + "variableValueType": { + "string": "字符", + "numeric": "数值", + "date": "日期", + "fragment": "表达式" + } + }, + "orgSetting": { + "info": "基本信息", + "avatar": "头像", + "clickToUpload": "点击上传", + "name": "名称", + "description": "描述", + "deleteOrg": "删除组织", + "deleteOrgDesc": "删除组织时,会将组织内所有资源、成员、角色和其他配置信息一并永久删除,请谨慎操作。", + "cancel": "取消", + "delete": "确定删除", + "enterOrgName": "输入组织名称确认删除" + }, + "confirmInvite": { + "join": "成功加入组织", + "confirming": "确认邀请中" + }, + "active": { + "activating": "激活中" + }, + "authorization": { + "processing": "处理中" + }, + "notfound": { + "title": "没有这个页面" + }, "share": { "common": { "confirm": "请确认", @@ -360,7 +1170,18 @@ }, "modal": { "password": "密码", - "pleaseInputPassword": "请输入密码" + "enterPassword": "请输入密码" + } + }, + "components": { + "colorPicker": { + "more": "更多", + "ok": "确认", + "cancel": "取消" + }, + "listTitle": { + "search": "搜索", + "searchValue": "搜索名称关键字" } } } diff --git a/frontend/src/share.tsx b/frontend/src/share.tsx index 1437d609c..507048cca 100644 --- a/frontend/src/share.tsx +++ b/frontend/src/share.tsx @@ -1,8 +1,5 @@ -import { ConfigProvider } from 'antd'; import 'antd/dist/antd.less'; -import zh_CN from 'antd/lib/locale/zh_CN'; import 'app/assets/fonts/iconfont.css'; -import ChartManager from 'app/pages/ChartWorkbenchPage/models/ChartManager'; import { Share } from 'app/share'; import React from 'react'; import 'react-app-polyfill/ie11'; @@ -21,39 +18,33 @@ const MOUNT_NODE = document.getElementById('root') as HTMLElement; * hot-key [control,shift,command,c] */ -const MainApp = ; - const InspectorWrapper = process.env.NODE_ENV === 'development' ? Inspector : React.Fragment; -ChartManager.instance() - .load() - .catch(err => console.error('Fail to load customize charts with ', err)) - .finally(() => { - ReactDOM.render( - - - - - - {MainApp} - - - - - , - MOUNT_NODE, - ); - // Hot reloadable translation json files - if (module.hot) { - module.hot.accept(['./locales/i18n'], () => { - // No need to render the App again because i18next works with the hooks - }); - } +ReactDOM.render( + + + + + + + + + + + , + MOUNT_NODE, +); - if (process.env.NODE_ENV === 'production') { - if (typeof (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') { - (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = () => void 0; - } - } +// Hot reloadable translation json files +if (module.hot) { + module.hot.accept(['./locales/i18n'], () => { + // No need to render the App again because i18next works with the hooks }); +} + +if (process.env.NODE_ENV === 'production') { + if (typeof (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') { + (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = () => void 0; + } +} diff --git a/frontend/src/styles/globalStyles.ts b/frontend/src/styles/globalStyles.ts index a94fae420..6fa174706 100644 --- a/frontend/src/styles/globalStyles.ts +++ b/frontend/src/styles/globalStyles.ts @@ -187,4 +187,9 @@ export const OverriddenStyle = createGlobalStyle` .datart-data-section-dropdown { z-index: ${MODAL_LEVEL - 1}; } + .aggregation-colorpopover{ + .ant-popover-arrow{ + display:none; + } + } `; diff --git a/frontend/src/task.ts b/frontend/src/task.ts index 8c44e6896..26828d1d5 100644 --- a/frontend/src/task.ts +++ b/frontend/src/task.ts @@ -1,9 +1,108 @@ /** + + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable prettier/prettier */ + +import 'react-app-polyfill/stable'; +import { ChartDataRequestBuilder } from 'app/pages/ChartWorkbenchPage/models/ChartHttpRequest'; +import { + BackendChart, + BackendChartConfig, +} from 'app/pages/ChartWorkbenchPage/slice/workbenchSlice'; +import { + DataChart, + ServerDashboard, +} from 'app/pages/DashBoardPage/pages/Board/slice/types'; +import { getBoardChartRequests } from 'app/pages/DashBoardPage/utils'; +import { + getChartDataView, + getDashBoardByResBoard, + getDataChartsByServer, +} from 'app/pages/DashBoardPage/utils/board'; +import { getWidgetMapByServer } from 'app/pages/DashBoardPage/utils/widget'; +import { ChartConfig } from 'app/types/ChartConfig'; +// import 'react-app-polyfill/stable'; +// import 'core-js/stable/map'; +// need polyfill [Object.values,Array.prototype.find,new Map] +/** + * @param '' - * @description 'server 定时任务 调用' - */ -const runTask = (params: any) => { - const a = 3; - return a; + * @description 'server task 定时任务 调用' + */ +const getBoardQueryData = (dataStr: string) => { + var data = JSON.parse(dataStr) as ServerDashboard; + + // const renderMode: VizRenderMode = 'schedule'; + const dashboard = getDashBoardByResBoard(data); + const { datacharts, views: serverViews, widgets: serverWidgets } = data; + + const dataCharts: DataChart[] = getDataChartsByServer(datacharts); + const { widgetMap, wrappedDataCharts } = getWidgetMapByServer( + serverWidgets, + dataCharts, + ); + + const allDataCharts: DataChart[] = dataCharts.concat(wrappedDataCharts); + const viewViews = getChartDataView(serverViews, allDataCharts); + + const viewMap = viewViews.reduce((obj, view) => { + obj[view.id] = view; + return obj; + }, {}); + + const dataChartMap = allDataCharts.reduce((obj, dataChart) => { + obj[dataChart.id] = dataChart; + return obj; + }, {}); + let downloadParams = getBoardChartRequests({ + widgetMap, + viewMap, + dataChartMap, + }); + let fileName = dashboard.name; + return JSON.stringify({ downloadParams, fileName }); +}; +const getChartQueryData = (dataStr: string) => { + // see handleCreateDownloadDataTask + const data: BackendChart = JSON.parse(dataStr); + const dataConfig: BackendChartConfig = JSON.parse(data.config as any); + const chartConfig: ChartConfig = dataConfig.chartConfig as ChartConfig; + const builder = new ChartDataRequestBuilder( + { + id: data.viewId, + computedFields: dataConfig.computedFields || [], + } as any, + chartConfig?.datas, + chartConfig?.settings, + {}, + false, + dataConfig?.aggregation, + ); + let downloadParams = [builder.build()]; + let fileName = data?.name || 'chart'; + return JSON.stringify({ downloadParams, fileName }); +}; +const getQueryData = (type: 'chart' | 'board', dataStr: string) => { + if (type === 'board') { + return getBoardQueryData(dataStr); + } else { + return getChartQueryData(dataStr); + } }; -export default runTask; +export default getQueryData; diff --git a/frontend/src/utils/debugger.ts b/frontend/src/utils/debugger.ts new file mode 100644 index 000000000..08f29421b --- /dev/null +++ b/frontend/src/utils/debugger.ts @@ -0,0 +1,44 @@ +/** + * 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. + */ + +export class Debugger { + private static _instance: Debugger; + private _enableDebug = false; + + public static get instance() { + if (!Debugger._instance) { + Debugger._instance = new Debugger(); + } + return Debugger._instance; + } + + public setEnable(enable?: boolean) { + this._enableDebug = !!enable; + } + + public measure(info: string, fn: VoidFunction, forceEnable: boolean = true) { + if (!this._enableDebug || !forceEnable) { + return fn(); + } + + const start = performance.now(); + fn(); + const end = performance.now(); + console.info(`Performance - ${info} - `, `${end - start} ms`); + } +} diff --git a/frontend/src/utils/object.ts b/frontend/src/utils/object.ts index 118af1a29..e87241f32 100644 --- a/frontend/src/utils/object.ts +++ b/frontend/src/utils/object.ts @@ -38,9 +38,9 @@ export function curry(fn) { const collector = (...args) => { _args = _args.concat(args || []); if (_args.length < fn.length) { - return collector.bind(null); + return collector.bind(Object.create(null)); } - return fn.apply(null, _args); + return fn.apply(Object.create(null), _args); }; return collector; } @@ -54,10 +54,10 @@ export function cond(...predicates) { if ( isPairArray(predicates[i]) && typeof predicates[i]?.[0] === 'function' && - predicates[i][0].call(null, value) + predicates[i][0].call(Object.create(null), value) ) { if (typeof predicates[i]?.[1] === 'function') { - return predicates[i][1].call(null, value); + return predicates[i][1].call(Object.create(null), value); } return predicates[i][1]; } diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 99810fda5..c4fb24749 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -21,7 +21,7 @@ import { BASE_API_URL } from 'globalConstants'; import { APIResponse } from 'types'; import { getToken, setToken } from './auth'; -const instance = axios.create({ +export const instance = axios.create({ baseURL: BASE_API_URL, validateStatus(status) { return status < 400; diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index d531b227e..c8f6ad7d5 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -10,6 +10,7 @@ import { import { APIResponse } from 'types'; import { SaveFormModel } from '../app/pages/MainPage/pages/VizPage/SaveFormContext'; import { removeToken } from './auth'; +export { default as uuidv4 } from 'uuid/dist/esm-browser/v4'; export function errorHandle(error) { if (error?.response) { @@ -32,7 +33,20 @@ export function errorHandle(error) { } return error; } - +export function getErrorMessage(error) { + if (error?.response) { + const { response } = error as AxiosError; + switch (response?.status) { + case 401: + message.error({ key: '401', content: '未登录或会话过期,请重新登录' }); + removeToken(); + return '401'; + default: + return response?.data.message || error.message; + } + } + return error?.message; +} export function reduxActionErrorHandler(errorAction) { if (errorAction?.payload) { message.error(errorAction?.payload); @@ -179,7 +193,11 @@ export const onDropTreeFn = ({ info, treeData, callback }) => { index = dropArr[dropArr.length - 1].index + 1; } else { //中间 - index = (dropArr[dropIndex].index + dropArr[dropIndex + 1].index) / 2; + if (!dropArr[dropIndex].index && !dropArr[dropIndex + 1].index) { + index = dropArr[dropArr.length - 1].index + 1; + } else { + index = (dropArr[dropIndex].index + dropArr[dropIndex + 1].index) / 2; + } } let { id } = dragObj, parentId = !info.dropToGap @@ -191,12 +209,12 @@ export const onDropTreeFn = ({ info, treeData, callback }) => { export const getInsertedNodeIndex = ( AddData: Omit & { config?: object | string }, - treeData: any, + viewData: any, ) => { let index: number = 0; /* eslint-disable */ - if (treeData?.length) { - let IndexArr = treeData + if (viewData?.length) { + let IndexArr = viewData .filter((v: any) => v.parentId == AddData.parentId) .map(v => Number(v.index) || 0); index = IndexArr?.length ? Math.max(...IndexArr) + 1 : 0; @@ -223,12 +241,22 @@ export function filterListOrTree( dataSource: T[], keywords: string, filterFunc: (keywords: string, data: T) => boolean, + filterLeaf?: boolean, // 是否展示所有叶子节点 ) { return keywords ? dataSource.reduce((filtered, d) => { const isMatch = filterFunc(keywords, d); - const isChildrenMatch = - d.children && filterListOrTree(d.children, keywords, filterFunc); + let isChildrenMatch: T[] | undefined; + if (filterLeaf && d.children?.every(c => (c as any).isLeaf)) { + isChildrenMatch = + isMatch || d.children.some(c => filterFunc(keywords, c)) + ? d.children + : void 0; + } else { + isChildrenMatch = + d.children && + filterListOrTree(d.children, keywords, filterFunc, filterLeaf); + } if (isMatch || (isChildrenMatch && isChildrenMatch.length > 0)) { filtered.push({ ...d, children: isChildrenMatch }); } @@ -321,11 +349,3 @@ export function fastDeleteArrayElement(arr: any[], index: number) { arr.pop(); } -export const ResizeEvent = new Event('resize', { - bubbles: false, - cancelable: true, -}); - -export const dispatchResize = () => { - window.dispatchEvent(ResizeEvent); -}; diff --git a/frontend/src/utils/validators.ts b/frontend/src/utils/validators.ts new file mode 100644 index 000000000..611d72b7c --- /dev/null +++ b/frontend/src/utils/validators.ts @@ -0,0 +1,46 @@ +/** + * 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. + */ + +export function getPasswordValidator(errorMessage: string) { + return function (_, value: string) { + if (value && (value.trim().length < 6 || value.trim().length > 20)) { + return Promise.reject(new Error(errorMessage)); + } + return Promise.resolve(); + }; +} + +export function getConfirmPasswordValidator( + field: string, + errorMessage: string, + confirmErrorMessage: string, +) { + return function ({ getFieldValue }) { + return { + validator(_, value: string) { + if (value && (value.trim().length < 6 || value.trim().length > 20)) { + return Promise.reject(new Error(errorMessage)); + } + if (value && getFieldValue(field) !== value) { + return Promise.reject(new Error(confirmErrorMessage)); + } + return Promise.resolve(); + }, + }; + }; +} diff --git a/pom.xml b/pom.xml index 0288f8071..80540bca5 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ datart datart-parent pom - 1.0.0-alpha.3 + 1.0.0-beta.0 @@ -74,6 +74,13 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + true + + org.codehaus.mojo versions-maven-plugin diff --git a/security/pom.xml b/security/pom.xml index b25195f13..d2acd004c 100644 --- a/security/pom.xml +++ b/security/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/security/src/main/java/datart/security/base/Permission.java b/security/src/main/java/datart/security/base/Permission.java index 2c38705de..8d23169d9 100644 --- a/security/src/main/java/datart/security/base/Permission.java +++ b/security/src/main/java/datart/security/base/Permission.java @@ -11,10 +11,12 @@ @Builder @AllArgsConstructor @NoArgsConstructor -public class Permission{ +public class Permission { private String orgId; + private String roleId; + private String resourceType; private String resourceId; @@ -31,12 +33,13 @@ public String toString() { '}'; } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Permission that = (Permission) o; - return permission == that.permission && orgId.equals(that.orgId) && resourceType.equals(that.resourceType) && resourceId.equals(that.resourceId); + return permission == that.permission && orgId.equals(that.orgId) && roleId.equals(that.roleId) && resourceType.equals(that.resourceType) && resourceId.equals(that.resourceId); } @Override diff --git a/security/src/main/java/datart/security/manager/DatartSecurityManager.java b/security/src/main/java/datart/security/manager/DatartSecurityManager.java index 669b263bf..a68d9038e 100644 --- a/security/src/main/java/datart/security/manager/DatartSecurityManager.java +++ b/security/src/main/java/datart/security/manager/DatartSecurityManager.java @@ -17,7 +17,9 @@ public interface DatartSecurityManager { boolean isAuthenticated(); - void requirePermissions(Permission... permission) throws PermissionDeniedException; + void requireAllPermissions(Permission... permission) throws PermissionDeniedException; + + void requireAnyPermission(Permission... permissions) throws PermissionDeniedException; void requireOrgOwner(String orgId) throws PermissionDeniedException; diff --git a/security/src/main/java/datart/security/manager/shiro/DatartRealm.java b/security/src/main/java/datart/security/manager/shiro/DatartRealm.java index ab2fb266f..d61f5565b 100644 --- a/security/src/main/java/datart/security/manager/shiro/DatartRealm.java +++ b/security/src/main/java/datart/security/manager/shiro/DatartRealm.java @@ -86,7 +86,7 @@ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal List relRoleResources = rrrMapper.listByOrgAndUser(permissionDataCache.getCurrentOrg(), userId); for (RelRoleResource rrr : relRoleResources) { authorizationInfo.addStringPermissions(ShiroSecurityManager - .toShiroPermissionString(rrr.getOrgId(), rrr.getResourceType(), rrr.getResourceId(), rrr.getPermission())); + .toShiroPermissionString(rrr.getOrgId(), rrr.getRoleId(), rrr.getResourceType(), rrr.getResourceId(), rrr.getPermission())); } permissionDataCache.setAuthorizationInfo(authorizationInfo); diff --git a/security/src/main/java/datart/security/manager/shiro/ShiroSecurityManager.java b/security/src/main/java/datart/security/manager/shiro/ShiroSecurityManager.java index bbdc19fa6..9eccf6076 100644 --- a/security/src/main/java/datart/security/manager/shiro/ShiroSecurityManager.java +++ b/security/src/main/java/datart/security/manager/shiro/ShiroSecurityManager.java @@ -41,6 +41,7 @@ import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; +import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.StringJoiner; @@ -119,7 +120,7 @@ public boolean isAuthenticated() { } @Override - public void requirePermissions(Permission... permissions) throws PermissionDeniedException { + public void requireAllPermissions(Permission... permissions) throws PermissionDeniedException { for (Permission permission : permissions) { Boolean permitted = permissionDataCache.getCachedPermission(permission); if (permitted != null) { @@ -130,6 +131,7 @@ public void requirePermissions(Permission... permissions) throws PermissionDenie } } Set permissionString = toShiroPermissionString(permission.getOrgId() + , permission.getRoleId() , permission.getResourceType() , permission.getResourceId() , permission.getPermission()); @@ -147,6 +149,43 @@ public void requirePermissions(Permission... permissions) throws PermissionDenie } } + @Override + public void requireAnyPermission(Permission... permissions) throws PermissionDeniedException { + boolean anyMatch = Arrays.stream(permissions).anyMatch(permission -> { + if (permission == null) { + return false; + } + Boolean permitted = permissionDataCache.getCachedPermission(permission); + if (permitted != null) { + if (!permitted) { + Exceptions.e(new AuthorizationException()); + } else { + return true; + } + } + Set permissionString = toShiroPermissionString(permission.getOrgId() + , permission.getRoleId() + , permission.getResourceType() + , permission.getResourceId() + , permission.getPermission()); + try { + permissionDataCache.setCurrentOrg(permission.getOrgId()); + SecurityUtils.getSubject().checkPermissions(permissionString.toArray(new String[0])); + permissionDataCache.setPermissionCache(permission, true); + return true; + } catch (AuthorizationException e) { + log.warn("User permission denied. User-{} Permission-{}" + , getCurrentUser() != null ? getCurrentUser().getUsername() : "none" + , permission); + permissionDataCache.setPermissionCache(permission, false); + return false; + } + }); + if (!anyMatch) { + Exceptions.tr(PermissionDeniedException.class, "message.security.permission-denied"); + } + } + @Override public void requireOrgOwner(String orgId) throws PermissionDeniedException { try { @@ -177,6 +216,7 @@ public boolean hasPermission(Permission... permissions) { } Set strings = toShiroPermissionString(permission.getOrgId() + , permission.getRoleId() , permission.getResourceType() , permission.getResourceId() , permission.getPermission()); @@ -185,7 +225,7 @@ public boolean hasPermission(Permission... permissions) { SecurityUtils.getSubject().checkPermissions(strings.toArray(new String[0])); permissionDataCache.setPermissionCache(permission, true); } catch (AuthorizationException e) { - log.warn("User permission denied. User-{} Permission-{}" + log.debug("User permission denied. User-{} Permission-{}" , getCurrentUser() != null ? getCurrentUser().getUsername() : "none" , permission); permissionDataCache.setPermissionCache(permission, false); @@ -217,12 +257,13 @@ public static String toShiroRoleString(String roleType, String orgId) { return roleType + "." + orgId; } - public static Set toShiroPermissionString(String orgId, String resourceType, String resourceId, int permission) { + public static Set toShiroPermissionString(String orgId, String roleId, String resourceType, String resourceId, int permission) { Set shiroPermissionStrings = new HashSet<>(); Set permissions = expand2StringPermissions(permission); for (String p : permissions) { StringJoiner stringJoiner = new StringJoiner(":"); stringJoiner.add(orgId) + .add(roleId != null ? roleId : "*") .add(resourceType) .add(p) .add(resourceId); @@ -241,7 +282,7 @@ public static String toShiroPermissionString(String orgId, String resourceType, return stringJoiner.toString(); } - private static Set expand2StringPermissions(int permission) { + public static Set expand2StringPermissions(int permission) { Set permissions = new HashSet<>(); if (permission == Const.DISABLE) { permissions.add("DISABLE"); diff --git a/security/src/main/java/datart/security/util/PermissionHelper.java b/security/src/main/java/datart/security/util/PermissionHelper.java index d0b23f9e3..d14fd04ba 100644 --- a/security/src/main/java/datart/security/util/PermissionHelper.java +++ b/security/src/main/java/datart/security/util/PermissionHelper.java @@ -24,27 +24,30 @@ public class PermissionHelper { - public static Permission vizPermission(String orgId, String vizId, int permission) { + public static Permission vizPermission(String orgId,String roleId, String vizId, int permission) { return Permission.builder() .orgId(orgId) + .roleId(roleId) .resourceType(ResourceType.VIZ.name()) .resourceId(vizId) .permission(permission) .build(); } - public static Permission sourcePermission(String orgId, String sourceId, int permission) { + public static Permission sourcePermission(String orgId,String roleId, String sourceId, int permission) { return Permission.builder() .orgId(orgId) + .roleId(roleId) .resourceType(ResourceType.SOURCE.name()) .resourceId(sourceId) .permission(permission) .build(); } - public static Permission viewPermission(String orgId, String viewId, int permission) { + public static Permission viewPermission(String orgId,String roleId, String viewId, int permission) { return Permission.builder() .orgId(orgId) + .roleId(roleId) .resourceType(ResourceType.VIEW.name()) .resourceId(viewId) .permission(permission) @@ -53,8 +56,9 @@ public static Permission viewPermission(String orgId, String viewId, int permiss public static Permission rolePermission(String orgId, int permission) { return Permission.builder() - .resourceType(ResourceType.ROLE.name()) .orgId(orgId) + .roleId("*") + .resourceType(ResourceType.ROLE.name()) .resourceId("*") .permission(permission) .build(); @@ -63,15 +67,17 @@ public static Permission rolePermission(String orgId, int permission) { public static Permission userPermission(String orgId, int permission) { return Permission.builder() .orgId(orgId) + .roleId("*") .resourceType(ResourceType.USER.name()) .resourceId("*") .permission(permission) .build(); } - public static Permission schedulePermission(String orgId, String scheduleId, int permission) { + public static Permission schedulePermission(String orgId,String roleId, String scheduleId, int permission) { return Permission.builder() .orgId(orgId) + .roleId(roleId) .resourceType(ResourceType.SCHEDULE.name()) .resourceId(scheduleId) .permission(permission) diff --git a/server/pom.xml b/server/pom.xml index 1b29cef09..45ade66cd 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -5,7 +5,7 @@ datart-parent datart - 1.0.0-alpha.3 + 1.0.0-beta.0 4.0.0 @@ -25,6 +25,16 @@ org.springframework.boot spring-boot-starter-logging + + + log4j-api + org.apache.logging.log4j + + + log4j-to-slf4j + org.apache.logging.log4j + + org.springframework.boot @@ -89,6 +99,10 @@ guava com.google.guava + + log4j + log4j + @@ -240,7 +254,8 @@ npm - install + run + bootstrap --registry=https://registry.npm.taobao.org ${project.parent.basedir}/frontend @@ -257,7 +272,7 @@ npm run - build + build:all ${project.parent.basedir}/frontend @@ -286,6 +301,26 @@ + + + com.coderplus.maven.plugins + copy-rename-maven-plugin + 1.0.1 + + + rename-file + compile + + rename + + + ${project.parent.basedir}/frontend/build/task/index.js + ${project.basedir}/src/main/resources/javascript/parser.js + + + + + diff --git a/server/src/main/java/datart/server/base/dto/DatachartDetailList.java b/server/src/main/java/datart/server/base/dto/DatachartDetailList.java index c744804f7..8472d9e0e 100644 --- a/server/src/main/java/datart/server/base/dto/DatachartDetailList.java +++ b/server/src/main/java/datart/server/base/dto/DatachartDetailList.java @@ -1,10 +1,12 @@ package datart.server.base.dto; import datart.core.entity.Datachart; +import datart.core.entity.Variable; import datart.core.entity.View; import lombok.Data; import java.util.List; +import java.util.Map; @Data public class DatachartDetailList { @@ -13,4 +15,8 @@ public class DatachartDetailList { private List views; + private Map> viewVariables; + + private List orgVariables; + } diff --git a/server/src/main/java/datart/server/config/WebMvcConfig.java b/server/src/main/java/datart/server/config/WebMvcConfig.java index c349ce558..2530fdf36 100644 --- a/server/src/main/java/datart/server/config/WebMvcConfig.java +++ b/server/src/main/java/datart/server/config/WebMvcConfig.java @@ -31,8 +31,6 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; - import java.util.List; @Configuration @@ -51,7 +49,7 @@ public WebMvcConfig(LoginInterceptor loginInterceptor) { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor).addPathPatterns(getPathPrefix() + "/**"); //i18n locale interceptor - registry.addInterceptor(new LocaleChangeInterceptor()); +// registry.addInterceptor(new LocaleChangeInterceptor()); registry.addInterceptor(new BasicValidRequestInterceptor()).addPathPatterns("/**"); } diff --git a/server/src/main/java/datart/server/service/RoleService.java b/server/src/main/java/datart/server/service/RoleService.java index 20f321168..63baf5392 100644 --- a/server/src/main/java/datart/server/service/RoleService.java +++ b/server/src/main/java/datart/server/service/RoleService.java @@ -23,6 +23,8 @@ public interface RoleService extends BaseCRUDService { Role createPerUserRole(String orgId, String userId); + List listUserRoles(String orgId,String userId); + List listRoleUsers(String roleId); boolean grantPermission(List permissionInfo); diff --git a/server/src/main/java/datart/server/service/ScheduleJob.java b/server/src/main/java/datart/server/service/ScheduleJob.java index bca4e87dd..466aa0885 100644 --- a/server/src/main/java/datart/server/service/ScheduleJob.java +++ b/server/src/main/java/datart/server/service/ScheduleJob.java @@ -3,8 +3,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import datart.core.base.PageInfo; import datart.core.base.consts.AttachmentType; import datart.core.base.consts.FileOwner; +import datart.core.base.exception.Exceptions; import datart.core.common.*; import datart.core.data.provider.Dataframe; import datart.core.entity.Folder; @@ -17,7 +19,10 @@ import datart.security.base.PasswordToken; import datart.security.base.ResourceType; import datart.security.manager.DatartSecurityManager; +import datart.server.base.dto.DashboardDetail; +import datart.server.base.dto.DatachartDetail; import datart.server.base.dto.ScheduleJobConfig; +import datart.server.base.params.DownloadCreateParam; import datart.server.base.params.ShareCreateParam; import datart.server.base.params.ShareToken; import datart.server.base.params.ViewExecuteParam; @@ -29,6 +34,8 @@ import org.quartz.JobExecutionContext; import org.springframework.util.CollectionUtils; +import javax.script.Invocable; +import javax.script.ScriptException; import java.io.Closeable; import java.io.File; import java.io.IOException; @@ -45,6 +52,8 @@ public abstract class ScheduleJob implements Job, Closeable { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + public Invocable parser; + static { OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } @@ -61,6 +70,8 @@ public abstract class ScheduleJob implements Job, Closeable { protected final List attachments = new LinkedList<>(); + protected final VizService vizService; + public ScheduleJob() { scheduleLogMapper = Application.getBean(ScheduleLogMapperExt.class); @@ -69,6 +80,8 @@ public ScheduleJob() { securityManager = Application.getBean(DatartSecurityManager.class); + vizService = Application.getBean(VizService.class); + } @Override @@ -115,10 +128,16 @@ public void doGetData() throws Exception { for (ScheduleJobConfig.VizContent vizContent : config.getVizContents()) { Folder folder = folderService.retrieve(vizContent.getVizId()); - + DownloadCreateParam downloadCreateParam; + if (ResourceType.DATACHART.name().equals(folder.getRelType())) { + DatachartDetail datachart = vizService.getDatachart(folder.getRelId()); + downloadCreateParam = parseExecuteParam("chart", OBJECT_MAPPER.writeValueAsString(datachart)); + } else { + DashboardDetail dashboard = vizService.getDashboard(folder.getRelId()); + downloadCreateParam = parseExecuteParam("board", OBJECT_MAPPER.writeValueAsString(dashboard)); + } if (config.getAttachments().contains(AttachmentType.EXCEL)) { - ViewExecuteParam viewExecuteParam = parseExecuteParam(); - downloadExcel(viewExecuteParam); + downloadExcel(downloadCreateParam); } if (config.getAttachments().contains(AttachmentType.IMAGE)) { @@ -148,11 +167,17 @@ private void insertLog(Date start, Date end, String scheduleId, int status, Stri scheduleLogMapper.insert(scheduleLog); } - private void downloadExcel(ViewExecuteParam viewExecuteParam) throws Exception { + private void downloadExcel(DownloadCreateParam downloadParams) throws Exception { DataProviderService dataProviderService = Application.getBean(DataProviderService.class); - Dataframe dataframe = dataProviderService.execute(viewExecuteParam); Workbook workbook = POIUtils.createEmpty(); - POIUtils.withSheet(workbook, "sheet0", dataframe); + for (int i = 0; i < downloadParams.getDownloadParams().size(); i++) { + ViewExecuteParam viewExecuteParam = downloadParams.getDownloadParams().get(i); + viewExecuteParam.setPageInfo(PageInfo.builder().pageNo(1) + .pageSize(Integer.MAX_VALUE).build()); + String vizName = viewExecuteParam.getVizName(); + Dataframe dataframe = dataProviderService.execute(downloadParams.getDownloadParams().get(i)); + POIUtils.withSheet(workbook, StringUtils.isEmpty(vizName) ? "Sheet" + i : vizName, dataframe); + } File tempFile = File.createTempFile(UUIDGenerator.generate(), ".xlsx"); POIUtils.save(workbook, tempFile.getPath(), true); attachments.add(tempFile); @@ -173,21 +198,34 @@ private void downloadImage(ResourceType vizType, String vizId, int imageWidth) t String path = FileUtils.concatPath(Application.getFileBasePath(), FileOwner.SCHEDULE.getPath(), schedule.getId()); - File file = WebUtils.screenShot2File(url, path,imageWidth); - -// ImageUtils.resize(file.getPath(), imageWidth * 1.0, null); + File file = WebUtils.screenShot2File(url, path, imageWidth); attachments.add(file); } - private ViewExecuteParam parseExecuteParam() { - return new ViewExecuteParam(); + private DownloadCreateParam parseExecuteParam(String type, String json) throws ScriptException, NoSuchMethodException, JsonProcessingException { + Invocable parser = getParser(); + if (parser == null) { + Exceptions.msg("param parser load error"); + } + Object result = parser.invokeFunction("getQueryData", type, json); + return OBJECT_MAPPER.readValue(result.toString(), DownloadCreateParam.class); + } + + private synchronized Invocable getParser() { + if (parser == null) { + try { + parser = JavascriptUtils.load("javascript/parser.js"); + } catch (Exception e) { + Exceptions.e(e); + } + } + return parser; } @Override public void close() throws IOException { - try { securityManager.logoutCurrent(); } catch (Exception e) { diff --git a/server/src/main/java/datart/server/service/VariableService.java b/server/src/main/java/datart/server/service/VariableService.java index 272cb114c..43a5ab9ad 100644 --- a/server/src/main/java/datart/server/service/VariableService.java +++ b/server/src/main/java/datart/server/service/VariableService.java @@ -50,6 +50,8 @@ public interface VariableService extends BaseCRUDService relIds); + boolean delViewVariables(String viewId); + } diff --git a/server/src/main/java/datart/server/service/VizCRUDService.java b/server/src/main/java/datart/server/service/VizCRUDService.java index ae6dce4f8..15552db3c 100644 --- a/server/src/main/java/datart/server/service/VizCRUDService.java +++ b/server/src/main/java/datart/server/service/VizCRUDService.java @@ -27,7 +27,8 @@ default E create(BaseCreateParam createParam) { requirePermission(instance, Const.CREATE); - checkUnique(instance); +// checkUnique(instance); + E e = BaseCRUDService.super.create(vizCreateParam); getRoleService().grantPermission(vizCreateParam.getPermissions()); diff --git a/server/src/main/java/datart/server/service/impl/DashboardServiceImpl.java b/server/src/main/java/datart/server/service/impl/DashboardServiceImpl.java index 71743c946..91e5acf99 100644 --- a/server/src/main/java/datart/server/service/impl/DashboardServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/DashboardServiceImpl.java @@ -100,7 +100,7 @@ public DashboardServiceImpl(DashboardMapperExt dashboardMapper, public List listDashboard(String orgId) { List dashboards = dashboardMapper.listByOrgId(orgId); return dashboards.stream().filter(dashboard -> securityManager - .hasPermission(PermissionHelper.vizPermission(dashboard.getOrgId(), dashboard.getId(), Const.READ))) + .hasPermission(PermissionHelper.vizPermission(dashboard.getOrgId(), "*", dashboard.getId(), Const.READ))) .map(DashboardBaseInfo::new) .collect(Collectors.toList()); } @@ -191,7 +191,7 @@ public DashboardDetail getDashboardDetail(String dashboardId) { } dashboardDetail.setQueryVariables(variables); // download permission - dashboardDetail.setDownload(securityManager.hasPermission(PermissionHelper.vizPermission(dashboard.getOrgId(), dashboardId, Const.DOWNLOAD))); + dashboardDetail.setDownload(securityManager.hasPermission(PermissionHelper.vizPermission(dashboard.getOrgId(), "*", dashboardId, Const.DOWNLOAD))); return dashboardDetail; } @@ -211,8 +211,7 @@ public void deleteStaticFiles(Dashboard dashboard) { public void requirePermission(Dashboard dashboard, int permission) { Folder folder = folderMapper.selectByRelTypeAndId(ResourceType.DASHBOARD.name(), dashboard.getId()); if (folder == null) { - securityManager.requirePermissions(PermissionHelper.vizPermission(dashboard.getOrgId(), - ResourceType.FOLDER.name(), permission)); + //创建时,不进行权限校验 } else { folderService.requirePermission(folder, permission); } @@ -221,7 +220,7 @@ public void requirePermission(Dashboard dashboard, int permission) { public Folder createWithFolder(BaseCreateParam createParam) { DashboardCreateParam param = (DashboardCreateParam) createParam; if (!CollectionUtils.isEmpty(folderMapper.checkVizName(param.getOrgId(), param.getParentId(), param.getName()))) { - Exceptions.tr(ParamException.class,"error.param.exists.name"); + Exceptions.tr(ParamException.class, "error.param.exists.name"); } Dashboard dashboard = DashboardService.super.create(createParam); @@ -234,6 +233,9 @@ public Folder createWithFolder(BaseCreateParam createParam) { folder.setId(UUIDGenerator.generate()); folder.setRelType(ResourceType.DASHBOARD.name()); folder.setRelId(dashboard.getId()); + + folderService.requirePermission(folder, Const.CREATE); + folderMapper.insert(folder); return folder; } diff --git a/server/src/main/java/datart/server/service/impl/DataProviderServiceImpl.java b/server/src/main/java/datart/server/service/impl/DataProviderServiceImpl.java index d5cd4c940..6b1250114 100644 --- a/server/src/main/java/datart/server/service/impl/DataProviderServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/DataProviderServiceImpl.java @@ -57,13 +57,13 @@ public class DataProviderServiceImpl extends BaseService implements DataProviderService { // build in variables - private static final String VARIABLE_NAME = "DATART.USER.NAME"; + private static final String VARIABLE_NAME = "DATART_USER_NAME"; - private static final String VARIABLE_USERNAME = "DATART.USER.USERNAME"; + private static final String VARIABLE_USERNAME = "DATART_USER_USERNAME"; - private static final String VARIABLE_EMAIL = "DATART.USER.EMAIL"; + private static final String VARIABLE_EMAIL = "DATART_USER_EMAIL"; - private static final String VARIABLE_ID = "DATART.USER.ID"; + private static final String VARIABLE_ID = "DATART_USER_ID"; private static final String SERVER_AGGREGATE = "serverAggregate"; diff --git a/server/src/main/java/datart/server/service/impl/DatachartServiceImpl.java b/server/src/main/java/datart/server/service/impl/DatachartServiceImpl.java index 1b4f03bc2..65bbae93c 100644 --- a/server/src/main/java/datart/server/service/impl/DatachartServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/DatachartServiceImpl.java @@ -105,8 +105,7 @@ public void deletePermissions(Datachart datachart) { public void requirePermission(Datachart datachart, int permission) { Folder folder = folderMapper.selectByRelTypeAndId(ResourceType.DATACHART.name(), datachart.getId()); if (folder == null) { - securityManager.requirePermissions(PermissionHelper.vizPermission(datachart.getOrgId(), - ResourceType.FOLDER.name(), permission)); + // 在创建时,不进行权限校验 } else { folderService.requirePermission(folder, permission); } @@ -117,7 +116,7 @@ public DatachartDetail getDatachartDetail(String datachartId) { DatachartDetail datachartDetail = new DatachartDetail(); Datachart datachart = retrieve(datachartId); //folder index - Folder folder = folderMapper.selectByRelTypeAndId(ResourceType.DASHBOARD.name(), datachartId); + Folder folder = folderMapper.selectByRelTypeAndId(ResourceType.DATACHART.name(), datachartId); if (folder != null) { datachartDetail.setParentId(folder.getParentId()); datachartDetail.setIndex(folder.getIndex()); @@ -130,7 +129,7 @@ public DatachartDetail getDatachartDetail(String datachartId) { // download permission datachartDetail.setDownload(securityManager - .hasPermission(PermissionHelper.vizPermission(datachart.getOrgId(), datachartId, Const.DOWNLOAD))); + .hasPermission(PermissionHelper.vizPermission(datachart.getOrgId(), "*", datachartId, Const.DOWNLOAD))); return datachartDetail; } @@ -140,16 +139,19 @@ public Folder createWithFolder(BaseCreateParam createParam) { // check unique DatachartCreateParam param = (DatachartCreateParam) createParam; if (!CollectionUtils.isEmpty(folderMapper.checkVizName(param.getOrgId(), param.getParentId(), param.getName()))) { - Exceptions.tr(ParamException.class,"error.param.exists.name"); + Exceptions.tr(ParamException.class, "error.param.exists.name"); } Datachart datachart = DatachartService.super.create(createParam); // create folder Folder folder = new Folder(); - BeanUtils.copyProperties(createParam, folder); folder.setId(UUIDGenerator.generate()); + BeanUtils.copyProperties(createParam, folder); folder.setRelType(ResourceType.DATACHART.name()); folder.setRelId(datachart.getId()); + + folderService.requirePermission(folder, Const.CREATE); folderMapper.insert(folder); + return folder; } diff --git a/server/src/main/java/datart/server/service/impl/DownloadServiceImpl.java b/server/src/main/java/datart/server/service/impl/DownloadServiceImpl.java index 123023bfa..442b65492 100644 --- a/server/src/main/java/datart/server/service/impl/DownloadServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/DownloadServiceImpl.java @@ -17,6 +17,7 @@ */ package datart.server.service.impl; +import datart.core.base.PageInfo; import datart.core.base.consts.Const; import datart.core.base.consts.FileOwner; import datart.core.base.exception.Exceptions; @@ -62,7 +63,6 @@ public DownloadServiceImpl(DownloadMapperExt downloadMapper, DataProviderService @Override public void requirePermission(Download entity, int permission) { - } @Override @@ -99,7 +99,8 @@ public Download submitDownloadTask(DownloadCreateParam downloadParams, String cl try { Workbook workbook = POIUtils.createEmpty(); for (int i = 0; i < downloadParams.getDownloadParams().size(); i++) { - ViewExecuteParam viewExecuteParam = downloadParams.getDownloadParams().get(0); + ViewExecuteParam viewExecuteParam = downloadParams.getDownloadParams().get(i); + viewExecuteParam.setPageInfo(PageInfo.builder().pageNo(1).pageSize(Integer.MAX_VALUE).build()); String vizName = viewExecuteParam.getVizName(); Dataframe dataframe = dataProviderService.execute(downloadParams.getDownloadParams().get(i)); POIUtils.withSheet(workbook, StringUtils.isEmpty(vizName) ? "Sheet" + i : vizName, dataframe); @@ -144,4 +145,5 @@ public Download downloadFile(String downloadId) { downloadMapper.updateByPrimaryKey(download); return download; } + } diff --git a/server/src/main/java/datart/server/service/impl/FolderServiceImpl.java b/server/src/main/java/datart/server/service/impl/FolderServiceImpl.java index 265873f66..a5cefa907 100644 --- a/server/src/main/java/datart/server/service/impl/FolderServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/FolderServiceImpl.java @@ -22,10 +22,13 @@ import datart.core.common.UUIDGenerator; import datart.core.entity.*; import datart.core.mappers.ext.*; +import datart.security.base.PermissionInfo; import datart.security.base.ResourceType; +import datart.security.base.SubjectType; +import datart.security.exception.PermissionDeniedException; +import datart.security.manager.shiro.ShiroSecurityManager; import datart.security.util.PermissionHelper; import datart.core.base.exception.NotAllowedException; -import datart.core.base.exception.NotFoundException; import datart.core.base.exception.ParamException; import datart.server.base.params.*; import datart.server.service.BaseService; @@ -36,10 +39,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Service @@ -73,16 +73,25 @@ public FolderServiceImpl(FolderMapperExt folderMapper, @Override public void requirePermission(Folder folder, int permission) { - if (folder.getId() == null || rrrMapper.countUserPermission(folder.getId(), getCurrentUser().getId()) == 0) { + List roles = roleService.listUserRoles(folder.getOrgId(), getCurrentUser().getId()); + boolean hasPermission = roles.stream().anyMatch(role -> hasPermission(role, folder, permission)); + if (!hasPermission) { + Exceptions.tr(PermissionDeniedException.class, "message.security.permission-denied", + folder.getRelType() + ":" + folder.getName() + ":" + ShiroSecurityManager.expand2StringPermissions(permission)); + } + } + + private boolean hasPermission(Role role, Folder folder, int permission) { + if (folder.getId() == null || rrrMapper.countRolePermission(folder.getId(), role.getId()) == 0) { Folder parent = folderMapper.selectByPrimaryKey(folder.getParentId()); if (parent == null) { - securityManager.requirePermissions(PermissionHelper.vizPermission(folder.getOrgId(), + return securityManager.hasPermission(PermissionHelper.vizPermission(folder.getOrgId(), role.getId(), ResourceType.FOLDER.name(), permission)); } else { - requirePermission(parent, permission); + return hasPermission(role, parent, permission); } } else { - securityManager.requirePermissions(PermissionHelper.vizPermission(folder.getOrgId(), folder.getId(), permission)); + return securityManager.hasPermission(PermissionHelper.vizPermission(folder.getOrgId(), role.getId(), folder.getId(), permission)); } } @@ -229,11 +238,12 @@ public Folder create(BaseCreateParam createParam) { folder.setCreateTime(new Date()); folder.setId(UUIDGenerator.generate()); folder.setRelType(ResourceType.FOLDER.name()); - requirePermission(folder, Const.MANAGE); + requirePermission(folder, Const.CREATE); // insert permissions if (!CollectionUtils.isEmpty(folderCreate.getPermissions())) { roleService.grantPermission(folderCreate.getPermissions()); } + grantDefaultPermission(folder); folderMapper.insert(folder); return folder; } @@ -255,4 +265,20 @@ private boolean hasReadPermission(Folder folder) { } return false; } + + @Override + @Transactional + public void grantDefaultPermission(Folder folder) { + if (securityManager.isOrgOwner(folder.getOrgId())) { + return; + } + PermissionInfo permissionInfo = new PermissionInfo(); + permissionInfo.setOrgId(folder.getOrgId()); + permissionInfo.setSubjectType(SubjectType.USER); + permissionInfo.setSubjectId(getCurrentUser().getId()); + permissionInfo.setResourceType(ResourceType.FOLDER); + permissionInfo.setResourceId(folder.getId()); + permissionInfo.setPermission(Const.CREATE); + roleService.grantPermission(Collections.singletonList(permissionInfo)); + } } diff --git a/server/src/main/java/datart/server/service/impl/MailServiceImpl.java b/server/src/main/java/datart/server/service/impl/MailServiceImpl.java index b64845965..591d443f8 100644 --- a/server/src/main/java/datart/server/service/impl/MailServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/MailServiceImpl.java @@ -34,12 +34,15 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; +import org.thymeleaf.spring5.messageresolver.SpringMessageResolver; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; @@ -91,7 +94,10 @@ public class MailServiceImpl extends BaseService implements MailService { @Value("${spring.mail.senderName:Datart}") private String senderName; - public MailServiceImpl(TemplateEngine templateEngine) { + public MailServiceImpl(TemplateEngine templateEngine,MessageSource messageSource) { + SpringMessageResolver springMessageResolver = new SpringMessageResolver(); + springMessageResolver.setMessageSource(messageSource); + templateEngine.setMessageResolver(springMessageResolver); this.templateEngine = templateEngine; } @@ -140,7 +146,7 @@ public void sendVerifyCode(User user) throws UnsupportedEncodingException, Messa } private MimeMessage createVerifyCodeMimeMessage(User user) throws UnsupportedEncodingException, MessagingException { - Context context = new Context(); + Context context = new Context(LocaleContextHolder.getLocale()); context.setVariable(VERIFY_CODE, user.getPassword()); context.setVariable(MESSAGE, getMessages("message.user.reset.password.mail.message", user.getUsername(), SecurityUtils.VERIFY_CODE_TIMEOUT_MIN)); String mailContent = templateEngine.process(FIND_PASSWORD_TEMPLATE, context); @@ -155,7 +161,7 @@ private MimeMessage createInviteMimeMessage(User user, Organization org) throws inviteToken.setInviter(getCurrentUser().getUsername()); String tokenString = JwtUtils.toJwtString(inviteToken); - Context context = new Context(); + Context context = new Context(LocaleContextHolder.getLocale()); context.setVariable(TOKEN_KEY, tokenString); context.setVariable(USERNAME_KEY, user.getUsername()); context.setVariable(INVITER, inviteToken.getInviter()); @@ -174,7 +180,7 @@ private MimeMessage createActiveMimeMessage(User user) throws MessagingException passwordToken.setCreateTime(System.currentTimeMillis()); String tokenString = JwtUtils.toJwtString(passwordToken); - Context context = new Context(); + Context context = new Context(LocaleContextHolder.getLocale()); context.setVariable(USERNAME_KEY, user.getUsername()); context.setVariable(TOKEN_KEY, tokenString); String activeUrl = Application.getWebRootURL() + "/active"; diff --git a/server/src/main/java/datart/server/service/impl/OrgServiceImpl.java b/server/src/main/java/datart/server/service/impl/OrgServiceImpl.java index af4aba945..506583fbc 100644 --- a/server/src/main/java/datart/server/service/impl/OrgServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/OrgServiceImpl.java @@ -180,7 +180,7 @@ public boolean updateAvatar(String orgId, String path) { @Override public List listOrgMembers(String orgId) { - securityManager.requirePermissions(PermissionHelper.userPermission(orgId, Const.READ)); + securityManager.requireAllPermissions(PermissionHelper.userPermission(orgId, Const.READ)); List users = organizationMapper.listOrgMembers(orgId); if (users == null || users.size() == 0) { return Collections.emptyList(); @@ -286,7 +286,7 @@ public boolean removeUser(String orgId, String userId) { @Override public List listUserRoles(String orgId, String userId) { - securityManager.requirePermissions(PermissionHelper.rolePermission(orgId, Const.READ)); + securityManager.requireAllPermissions(PermissionHelper.rolePermission(orgId, Const.READ)); List roles = roleMapper.listUserGeneralRoles(orgId, userId); if (roles == null || roles.size() == 0) { return Collections.emptyList(); @@ -299,7 +299,7 @@ public void requirePermission(Organization entity, int permission) { if ((Const.CREATE | permission) == Const.CREATE) { return; } - securityManager.requirePermissions(PermissionHelper.rolePermission(entity.getId(), permission)); + securityManager.requireAllPermissions(PermissionHelper.rolePermission(entity.getId(), permission)); } @Override diff --git a/server/src/main/java/datart/server/service/impl/RedisCacheImpl.java b/server/src/main/java/datart/server/service/impl/RedisCacheImpl.java index a6cdec5c3..885495018 100644 --- a/server/src/main/java/datart/server/service/impl/RedisCacheImpl.java +++ b/server/src/main/java/datart/server/service/impl/RedisCacheImpl.java @@ -1,6 +1,7 @@ package datart.server.service.impl; import datart.core.common.Cache; +import org.springframework.beans.factory.annotation.Required; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @@ -16,7 +17,6 @@ public RedisCacheImpl(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } - @Override public void put(String key, Object object) { redisTemplate.opsForValue().set(key, object); @@ -24,7 +24,7 @@ public void put(String key, Object object) { @Override public void put(String key, Object object, int ttl) { - redisTemplate.opsForValue().set(key, object, ttl, TimeUnit.MILLISECONDS); + redisTemplate.opsForValue().set(key, object, ttl, TimeUnit.SECONDS); } @Override diff --git a/server/src/main/java/datart/server/service/impl/RoleServiceImpl.java b/server/src/main/java/datart/server/service/impl/RoleServiceImpl.java index fd498d90c..77418b6fa 100644 --- a/server/src/main/java/datart/server/service/impl/RoleServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/RoleServiceImpl.java @@ -100,7 +100,7 @@ public boolean delete(String id) { @Transactional public boolean updateUsersForRole(String roleId, Set userIds) { Role role = retrieve(roleId); - securityManager.requirePermissions(PermissionHelper.rolePermission(role.getOrgId(), Const.READ | Const.MANAGE), + securityManager.requireAllPermissions(PermissionHelper.rolePermission(role.getOrgId(), Const.READ | Const.MANAGE), PermissionHelper.userPermission(role.getOrgId(), Const.READ | Const.MANAGE)); List users = roleMapper.listRoleUsers(roleId); List userToDelete = new ArrayList<>(); @@ -126,7 +126,7 @@ public boolean updateRolesForUser(String userId, String orgId, Set roleI requireExists(userId, User.class); - securityManager.requirePermissions( + securityManager.requireAllPermissions( PermissionHelper.userPermission(orgId, Const.READ | Const.MANAGE)); List roles = roleMapper.selectUserAllRoles(userId); @@ -203,10 +203,15 @@ public Role createPerUserRole(String orgId, String userId) { return role; } + @Override + public List listUserRoles(String orgId, String userId) { + return roleMapper.selectUserRoles(orgId, userId); + } + @Override public List listRoleUsers(String roleId) { Role role = retrieve(roleId); - securityManager.requirePermissions(PermissionHelper.rolePermission(role.getOrgId(), Const.READ) + securityManager.requireAllPermissions(PermissionHelper.rolePermission(role.getOrgId(), Const.READ) , PermissionHelper.userPermission(role.getOrgId(), Const.READ)); List users = roleMapper.listRoleUsers(roleId); return users.stream().map(UserBaseInfo::new).collect(Collectors.toList()); @@ -585,6 +590,6 @@ private RelSubjectColumns convert2RelRoleView(ViewPermission permission) { @Override public void requirePermission(Role entity, int permission) { - securityManager.requirePermissions(PermissionHelper.rolePermission(entity.getOrgId(), permission)); + securityManager.requireAllPermissions(PermissionHelper.rolePermission(entity.getOrgId(), permission)); } } diff --git a/server/src/main/java/datart/server/service/impl/ScheduleServiceImpl.java b/server/src/main/java/datart/server/service/impl/ScheduleServiceImpl.java index 2a81fee5f..bf758f896 100644 --- a/server/src/main/java/datart/server/service/impl/ScheduleServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/ScheduleServiceImpl.java @@ -22,13 +22,17 @@ import datart.core.base.exception.BaseException; import datart.core.base.exception.Exceptions; import datart.core.common.UUIDGenerator; +import datart.core.entity.Role; import datart.core.entity.Schedule; import datart.core.entity.ScheduleLog; +import datart.core.mappers.ext.RelRoleResourceMapperExt; import datart.core.mappers.ext.ScheduleLogMapperExt; import datart.core.mappers.ext.ScheduleMapperExt; import datart.security.base.PermissionInfo; import datart.security.base.ResourceType; import datart.security.base.SubjectType; +import datart.security.exception.PermissionDeniedException; +import datart.security.manager.shiro.ShiroSecurityManager; import datart.security.util.PermissionHelper; import datart.server.base.dto.ScheduleBaseInfo; import datart.server.base.params.BaseCreateParam; @@ -62,24 +66,38 @@ public class ScheduleServiceImpl extends BaseService implements ScheduleService private final Scheduler scheduler; + private final RelRoleResourceMapperExt rrrMapper; + public ScheduleServiceImpl(ScheduleMapperExt scheduleMapper, RoleService roleService, ScheduleLogMapperExt scheduleLogMapper, - Scheduler scheduler) { + Scheduler scheduler, RelRoleResourceMapperExt rrrMapper) { this.scheduleMapper = scheduleMapper; this.roleService = roleService; this.scheduleLogMapper = scheduleLogMapper; this.scheduler = scheduler; + this.rrrMapper = rrrMapper; } @Override public void requirePermission(Schedule schedule, int permission) { - - if ((permission & Const.CREATE) == Const.CREATE) { - securityManager.requirePermissions(PermissionHelper.schedulePermission(schedule.getOrgId(), ResourceType.SCHEDULE.name(), permission)); + if (securityManager.isOrgOwner(schedule.getOrgId())) { return; } - securityManager.requirePermissions(PermissionHelper.schedulePermission(schedule.getOrgId(), schedule.getId(), permission)); + List roles = roleService.listUserRoles(schedule.getOrgId(), getCurrentUser().getId()); + boolean hasPermission = roles.stream().anyMatch(role -> hasPermission(role, schedule, permission)); + if (!hasPermission) { + Exceptions.tr(PermissionDeniedException.class, "message.security.permission-denied", + ResourceType.SCHEDULE + ":" + schedule.getName() + ":" + ShiroSecurityManager.expand2StringPermissions(permission)); + } + } + + private boolean hasPermission(Role role, Schedule schedule, int permission) { + if (schedule.getId() == null || (permission & Const.CREATE) == permission) { + return securityManager.hasPermission(PermissionHelper.schedulePermission(schedule.getOrgId(), role.getId(), ResourceType.SCHEDULE.name(), permission)); + } else { + return securityManager.hasPermission(PermissionHelper.schedulePermission(schedule.getOrgId(), role.getId(), schedule.getId(), permission)); + } } @Override @@ -161,7 +179,7 @@ public boolean execute(String scheduleId) { public boolean start(String scheduleId) throws SchedulerException { Schedule schedule = retrieve(scheduleId); if (schedule.getActive() && scheduler.checkExists(JobKey.jobKey(schedule.getName(), schedule.getOrgId()))) { - Exceptions.tr(BaseException.class,"message.task.running"); + Exceptions.tr(BaseException.class, "message.task.running"); } Date now = new Date(); if (schedule.getStartDate() != null && now.before(new Date())) { diff --git a/server/src/main/java/datart/server/service/impl/ShareServiceImpl.java b/server/src/main/java/datart/server/service/impl/ShareServiceImpl.java index 14b6639d4..add094f60 100644 --- a/server/src/main/java/datart/server/service/impl/ShareServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/ShareServiceImpl.java @@ -59,16 +59,12 @@ public class ShareServiceImpl extends BaseService implements ShareService { private final DownloadService downloadService; - private final UserMapperExt userMapper; - public ShareServiceImpl(DataProviderService dataProviderService, VizService vizService, - DownloadService downloadService, - UserMapperExt userMapper) { + DownloadService downloadService) { this.dataProviderService = dataProviderService; this.vizService = vizService; this.downloadService = downloadService; - this.userMapper = userMapper; } @Override @@ -220,7 +216,7 @@ private void validateExecutePermission(ShareToken token, ViewExecuteParam execut private void validateVizPermission(ShareToken token, ResourceType vizType, String vizId) { Share share = validateBase(token); if (!share.getVizType().equals(vizType) || !share.getVizId().equals(vizId)) { - Exceptions.tr(PermissionDeniedException.class, "message.permission.denied", "viz"); + Exceptions.tr(PermissionDeniedException.class, "message.security.permission-denied", "viz"); } } diff --git a/server/src/main/java/datart/server/service/impl/SourceServiceImpl.java b/server/src/main/java/datart/server/service/impl/SourceServiceImpl.java index c7224507b..cb4734472 100644 --- a/server/src/main/java/datart/server/service/impl/SourceServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/SourceServiceImpl.java @@ -25,12 +25,15 @@ import datart.core.base.exception.Exceptions; import datart.core.data.provider.DataProviderConfigTemplate; import datart.core.data.provider.DataProviderSource; +import datart.core.entity.Role; import datart.core.entity.Source; +import datart.core.mappers.ext.RelRoleResourceMapperExt; import datart.core.mappers.ext.SourceMapperExt; import datart.security.base.PermissionInfo; import datart.security.base.ResourceType; import datart.security.base.SubjectType; import datart.security.exception.PermissionDeniedException; +import datart.security.manager.shiro.ShiroSecurityManager; import datart.security.util.AESUtil; import datart.security.util.PermissionHelper; import datart.server.base.params.BaseCreateParam; @@ -61,12 +64,15 @@ public class SourceServiceImpl extends BaseService implements SourceService { private final RoleService roleService; + private final RelRoleResourceMapperExt rrrMapper; + public SourceServiceImpl(SourceMapperExt sourceMapper, DataProviderService dataProviderService, - RoleService roleService) { + RoleService roleService, RelRoleResourceMapperExt rrrMapper) { this.sourceMapper = sourceMapper; this.dataProviderService = dataProviderService; this.roleService = roleService; + this.rrrMapper = rrrMapper; } @Override @@ -85,12 +91,23 @@ public List listSources(String orgId, boolean active) throws PermissionD @Override public void requirePermission(Source source, int permission) { - if ((permission & Const.CREATE) == Const.CREATE) { - securityManager.requirePermissions(PermissionHelper.sourcePermission(source.getOrgId(), - ResourceType.SOURCE.name(), permission)); + if (securityManager.isOrgOwner(source.getOrgId())) { return; } - securityManager.requirePermissions(PermissionHelper.sourcePermission(source.getOrgId(), source.getId(), permission)); + List roles = roleService.listUserRoles(source.getOrgId(), getCurrentUser().getId()); + boolean hasPermission = roles.stream().anyMatch(role -> hasPermission(role, source, permission)); + if (!hasPermission) { + Exceptions.tr(PermissionDeniedException.class, "message.security.permission-denied", + ResourceType.SOURCE + ":" + source.getName() + ":" + ShiroSecurityManager.expand2StringPermissions(permission)); + } + } + + private boolean hasPermission(Role role, Source source, int permission) { + if (source.getId() == null || (permission & Const.CREATE) == permission) { + return securityManager.hasPermission(PermissionHelper.sourcePermission(source.getOrgId(), role.getId(), ResourceType.SOURCE.name(), permission)); + } else { + return securityManager.hasPermission(PermissionHelper.sourcePermission(source.getOrgId(), role.getId(), source.getId(), permission)); + } } @Override @@ -142,7 +159,7 @@ public void grantDefaultPermission(Source source) { permissionInfo.setSubjectId(getCurrentUser().getId()); permissionInfo.setResourceType(ResourceType.SOURCE); permissionInfo.setResourceId(source.getId()); - permissionInfo.setPermission(Const.MANAGE); + permissionInfo.setPermission(Const.CREATE); roleService.grantPermission(Collections.singletonList(permissionInfo)); } diff --git a/server/src/main/java/datart/server/service/impl/StoryboardServiceImpl.java b/server/src/main/java/datart/server/service/impl/StoryboardServiceImpl.java index 3f9fc1fe1..6c8f8eee9 100644 --- a/server/src/main/java/datart/server/service/impl/StoryboardServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/StoryboardServiceImpl.java @@ -18,12 +18,17 @@ package datart.server.service.impl; import datart.core.base.consts.Const; +import datart.core.base.exception.Exceptions; import datart.core.entity.BaseEntity; +import datart.core.entity.Role; import datart.core.entity.Storyboard; +import datart.core.mappers.ext.RelRoleResourceMapperExt; import datart.core.mappers.ext.StoryboardMapperExt; import datart.security.base.PermissionInfo; import datart.security.base.ResourceType; import datart.security.base.SubjectType; +import datart.security.exception.PermissionDeniedException; +import datart.security.manager.shiro.ShiroSecurityManager; import datart.security.util.PermissionHelper; import datart.server.base.dto.StoryboardDetail; import datart.server.base.params.BaseCreateParam; @@ -48,12 +53,15 @@ public class StoryboardServiceImpl extends BaseService implements StoryboardServ private final StorypageService storypageService; + private final RelRoleResourceMapperExt rrrMapper; + public StoryboardServiceImpl(RoleService roleService, StoryboardMapperExt storyboardMapper, - StorypageService storypageService) { + StorypageService storypageService, RelRoleResourceMapperExt rrrMapper) { this.roleService = roleService; this.storyboardMapper = storyboardMapper; this.storypageService = storypageService; + this.rrrMapper = rrrMapper; } @Override @@ -84,7 +92,7 @@ public StoryboardDetail getStoryboard(String storyboardId) { storyboardDetail.setStorypages(storypageService.listByStoryboard(storyboardId)); // download permission storyboardDetail.setDownload(securityManager - .hasPermission(PermissionHelper.vizPermission(storyboard.getOrgId(), storyboardId, Const.DOWNLOAD))); + .hasPermission(PermissionHelper.vizPermission(storyboard.getOrgId(), "*", storyboardId, Const.DOWNLOAD))); return storyboardDetail; } @@ -100,12 +108,23 @@ public boolean checkUnique(BaseEntity entity) { @Override public void requirePermission(Storyboard storyboard, int permission) { - if ((permission & Const.CREATE) == Const.CREATE) { - securityManager.requirePermissions(PermissionHelper.vizPermission(storyboard.getOrgId(), - ResourceType.STORYBOARD.name(), permission)); + if (securityManager.isOrgOwner(storyboard.getOrgId())) { return; } - securityManager.requirePermissions(PermissionHelper.vizPermission(storyboard.getOrgId(), storyboard.getId(), permission)); + List roles = roleService.listUserRoles(storyboard.getOrgId(), getCurrentUser().getId()); + boolean hasPermission = roles.stream().anyMatch(role -> hasPermission(role, storyboard, permission)); + if (!hasPermission) { + Exceptions.tr(PermissionDeniedException.class, "message.security.permission-denied", + ResourceType.STORYBOARD + ":" + storyboard.getName() + ":" + ShiroSecurityManager.expand2StringPermissions(permission)); + } + } + + private boolean hasPermission(Role role, Storyboard storyboard, int permission) { + if (storyboard.getId() == null || (permission & Const.CREATE) == permission) { + return securityManager.hasPermission(PermissionHelper.vizPermission(storyboard.getOrgId(), role.getId(), ResourceType.STORYBOARD.name(), permission)); + } else { + return securityManager.hasPermission(PermissionHelper.vizPermission(storyboard.getOrgId(), role.getId(), storyboard.getId(), permission)); + } } @Override diff --git a/server/src/main/java/datart/server/service/impl/StorypageServiceImpl.java b/server/src/main/java/datart/server/service/impl/StorypageServiceImpl.java index be1b15d3d..d7dbca751 100644 --- a/server/src/main/java/datart/server/service/impl/StorypageServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/StorypageServiceImpl.java @@ -40,7 +40,7 @@ public StorypageServiceImpl(StorypageMapperExt spMapper) { @Override public void requirePermission(Storypage entity, int permission) { Storyboard sb = retrieve(entity.getStoryboardId(), Storyboard.class); - securityManager.requirePermissions(PermissionHelper.vizPermission(sb.getOrgId(), sb.getId(), permission)); + securityManager.requireAllPermissions(PermissionHelper.vizPermission(sb.getOrgId(), "*", sb.getId(), permission)); } @Override diff --git a/server/src/main/java/datart/server/service/impl/VariableServiceImpl.java b/server/src/main/java/datart/server/service/impl/VariableServiceImpl.java index e75bb717a..7b3a1e804 100644 --- a/server/src/main/java/datart/server/service/impl/VariableServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/VariableServiceImpl.java @@ -256,6 +256,11 @@ public boolean deleteRel(Set relIds) { return rvsMapper.deleteByPrimaryKeys(RelVariableSubject.class, relIds) > 0; } + @Override + public boolean delViewVariables(String viewId) { + return variableMapper.deleteByView(viewId) >= 0; + } + @Override @Transactional public boolean update(BaseUpdateParam updateParam) { diff --git a/server/src/main/java/datart/server/service/impl/ViewServiceImpl.java b/server/src/main/java/datart/server/service/impl/ViewServiceImpl.java index bcfa8163f..23c075a15 100644 --- a/server/src/main/java/datart/server/service/impl/ViewServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/ViewServiceImpl.java @@ -19,15 +19,16 @@ package datart.server.service.impl; import datart.core.base.consts.Const; +import datart.core.base.exception.Exceptions; import datart.core.common.UUIDGenerator; -import datart.core.entity.BaseEntity; -import datart.core.entity.Datachart; -import datart.core.entity.RelSubjectColumns; -import datart.core.entity.View; +import datart.core.entity.*; import datart.core.mappers.ext.RelRoleResourceMapperExt; import datart.core.mappers.ext.RelSubjectColumnsMapperExt; +import datart.core.mappers.ext.RelVariableSubjectMapperExt; import datart.core.mappers.ext.ViewMapperExt; import datart.security.base.ResourceType; +import datart.security.exception.PermissionDeniedException; +import datart.security.manager.shiro.ShiroSecurityManager; import datart.security.util.PermissionHelper; import datart.server.base.dto.ViewDetailDTO; import datart.server.base.params.*; @@ -55,16 +56,19 @@ public class ViewServiceImpl extends BaseService implements ViewService { private final VariableService variableService; + private final RelVariableSubjectMapperExt rvsMapper; + public ViewServiceImpl(ViewMapperExt viewMapper, RelSubjectColumnsMapperExt rscMapper, RelRoleResourceMapperExt rrrMapper, RoleService roleService, - VariableService variableService) { + VariableService variableService, RelVariableSubjectMapperExt rvsMapper) { this.viewMapper = viewMapper; this.rscMapper = rscMapper; this.rrrMapper = rrrMapper; this.roleService = roleService; this.variableService = variableService; + this.rvsMapper = rvsMapper; } @Override @@ -93,12 +97,7 @@ public View create(BaseCreateParam createParam) { rscMapper.batchInsert(columnPermission); } - ViewDetailDTO viewDetailDTO = new ViewDetailDTO(view); - viewDetailDTO.setRelVariableSubjects(Collections.emptyList()); - viewDetailDTO.setRelSubjectColumns(Collections.emptyList()); - viewDetailDTO.setVariables(Collections.emptyList()); - - return viewDetailDTO; + return getViewDetail(view.getId()); } @Override @@ -169,7 +168,7 @@ public boolean unarchive(String id, String newName, String parentId, double inde check.setName(newName); checkUnique(check); } - + // update status view.setName(newName); view.setParentId(parentId); @@ -179,6 +178,17 @@ public boolean unarchive(String id, String newName, String parentId, double inde } + @Override + @Transactional + public void deleteReference(View view) { + List variables = variableService.listByView(view.getId()); + if (variables.size() > 0) { + rvsMapper.deleteByVariables(variables.stream().map(Variable::getId).collect(Collectors.toSet())); + } + rscMapper.deleteByView(view.getId()); + variableService.delViewVariables(view.getId()); + } + @Override public boolean updateBase(ViewBaseUpdateParam updateParam) { View view = retrieve(updateParam.getId()); @@ -251,22 +261,29 @@ public boolean update(BaseUpdateParam updateParam) { return ViewService.super.update(updateParam); } - @Override public void requirePermission(View view, int permission) { - if (securityManager.isOrgOwner(view.getOrgId())) { return; } - if (view.getId() == null || rrrMapper.countUserPermission(view.getId(), getCurrentUser().getId()) == 0) { + List roles = roleService.listUserRoles(view.getOrgId(), getCurrentUser().getId()); + boolean hasPermission = roles.stream().anyMatch(role -> hasPermission(role, view, permission)); + if (!hasPermission) { + Exceptions.tr(PermissionDeniedException.class, "message.security.permission-denied", + ResourceType.VIEW + ":" + view.getName() + ":" + ShiroSecurityManager.expand2StringPermissions(permission)); + } + } + + private boolean hasPermission(Role role, View view, int permission) { + if (view.getId() == null || rrrMapper.countRolePermission(view.getId(), role.getId()) == 0) { View parent = viewMapper.selectByPrimaryKey(view.getParentId()); if (parent == null) { - securityManager.requirePermissions(PermissionHelper.viewPermission(view.getOrgId(), ResourceType.VIEW.name(), permission)); + return securityManager.hasPermission(PermissionHelper.viewPermission(view.getOrgId(), role.getId(), ResourceType.VIEW.name(), permission)); } else { - requirePermission(parent, permission); + return hasPermission(role, parent, permission); } } else { - securityManager.requirePermissions(PermissionHelper.viewPermission(view.getOrgId(), view.getId(), permission)); + return securityManager.hasPermission(PermissionHelper.viewPermission(view.getOrgId(), role.getId(), view.getId(), permission)); } } @@ -278,7 +295,10 @@ public boolean safeDelete(String viewId) { // check charts reference Datachart datachart = new Datachart(); datachart.setViewId(viewId); - return viewMapper.checkUnique(datachart); + //check widget reference + RelWidgetElement relWidgetElement = new RelWidgetElement(); + relWidgetElement.setRelId(viewId); + return viewMapper.checkUnique(datachart) && viewMapper.checkUnique(relWidgetElement); } } diff --git a/server/src/main/java/datart/server/service/impl/VizServiceImpl.java b/server/src/main/java/datart/server/service/impl/VizServiceImpl.java index d710bdf1f..a80c49b47 100644 --- a/server/src/main/java/datart/server/service/impl/VizServiceImpl.java +++ b/server/src/main/java/datart/server/service/impl/VizServiceImpl.java @@ -18,6 +18,7 @@ package datart.server.service.impl; import datart.core.base.consts.Const; +import datart.core.base.consts.VariableTypeEnum; import datart.core.base.exception.Exceptions; import datart.core.common.UUIDGenerator; import datart.core.entity.*; @@ -31,9 +32,11 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; @Slf4j @Service @@ -51,18 +54,21 @@ public class VizServiceImpl extends BaseService implements VizService { private final ViewService viewService; + private final VariableService variableService; + public VizServiceImpl(DatachartService datachartService, DashboardService dashboardService, StoryboardService storyboardService, StorypageService storypageService, FolderService folderService, - ViewService viewService) { + ViewService viewService, VariableService variableService) { this.datachartService = datachartService; this.dashboardService = dashboardService; this.storyboardService = storyboardService; this.storypageService = storypageService; this.folderService = folderService; this.viewService = viewService; + this.variableService = variableService; } @Override @@ -171,6 +177,7 @@ public DatachartDetailList getDatacharts(Set datachartIds) { DatachartDetailList datachartDetailList = new DatachartDetailList(); datachartDetailList.setDatacharts(new LinkedList<>()); datachartDetailList.setViews(new LinkedList<>()); + datachartDetailList.setViewVariables(new HashMap<>()); if (CollectionUtils.isEmpty(datachartIds)) { return datachartDetailList; } @@ -183,9 +190,17 @@ public DatachartDetailList getDatacharts(Set datachartIds) { for (Datachart datachart : datachartDetailList.getDatacharts()) { try { datachartDetailList.getViews().add(viewService.retrieve(datachart.getViewId())); + if (!datachartDetailList.getViewVariables().containsKey(datachart.getViewId())) { + List variables = variableService.listByView(datachart.getViewId()); + datachartDetailList.getViewVariables().put(datachart.getViewId(), variables); + } } catch (Exception ignored) { } } + List orgVariables = variableService.listOrgVariables(datachartDetailList.getDatacharts().get(0).getOrgId()); + orgVariables = orgVariables.stream().filter(v -> v.getType().equals(VariableTypeEnum.QUERY.name())) + .collect(Collectors.toList()); + datachartDetailList.setOrgVariables(orgVariables); return datachartDetailList; } @@ -314,7 +329,10 @@ public boolean unarchiveViz(String vizId, ResourceType vizType, String newName, Storyboard storyboard = storyboardService.retrieve(vizId); storyboardService.requirePermission(storyboard, Const.MANAGE); // check name - folderService.checkUnique(vizType, storyboard.getOrgId(), parentId, newName); + Storyboard check = new Storyboard(); + check.setOrgId(storyboard.getOrgId()); + check.setName(newName); + storyboardService.checkUnique(check); storyboard.setName(newName); storyboard.setStatus(Const.DATA_STATUS_ACTIVE); return 1 == storyboardService.getDefaultMapper().updateByPrimaryKey(storyboard); diff --git a/server/src/main/java/datart/server/service/impl/WeChartJob.java b/server/src/main/java/datart/server/service/impl/WeChartJob.java index 1385482bc..da738e480 100644 --- a/server/src/main/java/datart/server/service/impl/WeChartJob.java +++ b/server/src/main/java/datart/server/service/impl/WeChartJob.java @@ -32,6 +32,9 @@ @Slf4j public class WeChartJob extends ScheduleJob { + public WeChartJob() { + } + @Override public void doSend() throws Exception { diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index a4653ed1e..4ad0f42e2 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -2,11 +2,6 @@ spring: application: name: datart-server - datasource: - druid: - mysql: - usePingMethod: false - main: banner-mode: off diff --git a/server/src/main/resources/assembly/assembly.xml b/server/src/main/resources/assembly/assembly.xml index 02b88c751..39b8648b9 100644 --- a/server/src/main/resources/assembly/assembly.xml +++ b/server/src/main/resources/assembly/assembly.xml @@ -23,7 +23,7 @@ ./ Dockerfile - docker-compose.yml.example + diff --git a/server/src/main/resources/i18n/datart_i18n.properties b/server/src/main/resources/i18n/datart_i18n.properties index 1d82df779..303135efe 100644 --- a/server/src/main/resources/i18n/datart_i18n.properties +++ b/server/src/main/resources/i18n/datart_i18n.properties @@ -6,7 +6,7 @@ login.not-login=用户未登录,请先登录 login.success=登录成功 message.org.member.delete-creator=组织创建者不能被删除 message.org.member.delete-self=不能删除自己 -message.security.permission-denied=权限不足 +message.security.permission-denied=权限不足: {0} message.user.active.fail=用户激活失败!用户({0})已激活。 message.user.active.mail.active=激活 message.user.active.mail.greeting=您好 @@ -62,7 +62,6 @@ message.file.notfound=文件 {0} 未找到 message.sql.op.forbidden=不允许的SQL操作 {0} message.share.unsupported=不支持的分享类型 {0} message.provider.execute.permission.denied=执行权限不足 -message.permission.denied={0} 权限不足 message.share.expired=分享已过期 message.share.pwd=分享密码验证失败 message.unsupported.format=不支持的文件类型 {0} @@ -83,6 +82,36 @@ base.exists={0} 已存在 base.resource= base.resource.name=名称 base.not.found={0} 未找到 +config.template.jdbc.dbType=数据库类型 +config.template.jdbc.url=连接地址 +config.template.jdbc.user=用户 +config.template.jdbc.password=密码 +config.template.jdbc.driverClass=驱动类 +config.template.jdbc.serverAggregate=开启服务端聚合 +config.template.jdbc.properties=连接池参数 +config.template.http.url=地址 +config.template.http.schemas=表 +config.template.http.tableName=表名 +config.template.http.method=请求方式 +config.template.http.property=解析字段 +config.template.http.columns=列 +config.template.http.username=用户名 +config.template.http.password=密码 +config.template.http.timeout=超时时间 +config.template.http.responseParser=结果解析器 +config.template.http.headers=请求头 +config.template.http.queryParam=路径参数 +config.template.http.body=请求体 +config.template.http.contentType=contentType +config.template.http.cacheEnable=启用缓存 +config.template.http.cacheTimeout=缓存时间(分钟) +config.template.http.property.desc=Http返回结果中,JSON数组的属性名称。嵌套结构用 `.` 隔开。如 data.list +config.template.file.schemas=表 +config.template.file.tableName=表名 +config.template.file.format=文件格式 +config.template.file.columns=列 +config.template.file.cacheEnable=是否开启缓存 +config.template.file.cacheTimeout=缓存超时 diff --git a/server/src/main/resources/i18n/datart_i18n_en_US.properties b/server/src/main/resources/i18n/datart_i18n_en.properties similarity index 76% rename from server/src/main/resources/i18n/datart_i18n_en_US.properties rename to server/src/main/resources/i18n/datart_i18n_en.properties index 0e945cd07..6d941f5a2 100644 --- a/server/src/main/resources/i18n/datart_i18n_en_US.properties +++ b/server/src/main/resources/i18n/datart_i18n_en.properties @@ -6,7 +6,7 @@ login.not-login=User is not logged in, please log in first login.success=Login Success message.org.member.delete-creator=The organization creator cannot be deleted message.org.member.delete-self=You can't delete yourself -message.security.permission-denied=Permission denied +message.security.permission-denied=Permission denied: {0} message.user.active.fail=User activation Failed . The user ({0}) already actived message.user.active.mail.active=Active message.user.active.mail.greeting=Hello @@ -62,7 +62,6 @@ message.sql.op.forbidden=SQL operations not allowed {0} message.file.notfound=file {0} not found message.share.unsupported=Unsupported share type {0} message.provider.execute.permission.denied=execute permission denied -message.permission.denied={0} permission denied message.share.expired=share has expired message.share.pwd=Incorrect access password message.unsupported.format=Unknown file format : {0} @@ -82,4 +81,34 @@ resource.organization.owner=At least one organization owner must exist! base.resource.name=name base.exists={0} already exists base.not.found={0} not found +config.template.jdbc.dbType=database type +config.template.jdbc.url=connection url +config.template.jdbc.user=user +config.template.jdbc.password=password +config.template.jdbc.driverClass=driver class +config.template.jdbc.serverAggregate=serverAggregate +config.template.jdbc.properties=properties +config.template.http.url=url +config.template.http.schemas=schemas +config.template.http.tableName=tableName +config.template.http.method=method +config.template.http.property=property +config.template.http.columns=columns +config.template.http.username=username +config.template.http.password=password +config.template.http.timeout=timeout +config.template.http.responseParser=responseParser +config.template.http.headers=headers +config.template.http.queryParam=queryParam +config.template.http.body=body +config.template.http.contentType=contentType +config.template.http.cacheEnable=cacheEnable +config.template.http.cacheTimeout=cacheTimeout +config.template.http.property.desc=The property name of the JSON array in the result. Nested structures are separated by `.` . Such as the data.list +config.template.file.schemas=schemas +config.template.file.tableName=tableName +config.template.file.format=format +config.template.file.columns=columns +config.template.file.cacheEnable=cacheEnable +config.template.file.cacheTimeout=cacheTimeout diff --git a/server/src/main/resources/i18n/datart_i18n_zh_CN.properties b/server/src/main/resources/i18n/datart_i18n_zh.properties similarity index 75% rename from server/src/main/resources/i18n/datart_i18n_zh_CN.properties rename to server/src/main/resources/i18n/datart_i18n_zh.properties index 5c23bfa02..a4c1ed770 100644 --- a/server/src/main/resources/i18n/datart_i18n_zh_CN.properties +++ b/server/src/main/resources/i18n/datart_i18n_zh.properties @@ -6,7 +6,7 @@ login.not-login=用户未登录,请先登录 login.success=登录成功 message.org.member.delete-creator=组织创建者不能被删除 message.org.member.delete-self=不能删除自己 -message.security.permission-denied=权限不足 +message.security.permission-denied=权限不足: {0} message.user.active.fail=用户激活失败!用户({0})已激活。 message.user.active.mail.active=激活 message.user.active.mail.greeting=您好 @@ -62,7 +62,6 @@ message.sql.op.forbidden=不允许的SQL操作 {0} message.file.notfound=文件 {0} 未找到 message.share.unsupported=不支持的分享类型 {0} message.provider.execute.permission.denied=执行权限不足 -message.permission.denied={0} 权限不足 message.share.expired=分享已过期 message.share.pwd=分享密码验证失败 message.unsupported.format=不支持的文件类型 {0} @@ -82,4 +81,34 @@ resource.organization.owner=组织必须有至少一个管理员 base.resource.name=名称 base.not.found={0} 未找到 base.exists={0} 已存在 +config.template.jdbc.dbType=数据库类型 +config.template.jdbc.url=连接地址 +config.template.jdbc.user=用户 +config.template.jdbc.password=密码 +config.template.jdbc.driverClass=驱动类 +config.template.jdbc.serverAggregate=开启服务端聚合 +config.template.jdbc.properties=连接池参数 +config.template.http.url=地址 +config.template.http.schemas=表 +config.template.http.tableName=表名 +config.template.http.method=请求方式 +config.template.http.property=解析字段 +config.template.http.columns=列 +config.template.http.username=用户名 +config.template.http.password=密码 +config.template.http.timeout=超时时间 +config.template.http.responseParser=结果解析器 +config.template.http.headers=请求头 +config.template.http.queryParam=路径参数 +config.template.http.body=请求体 +config.template.http.contentType=contentType +config.template.http.cacheEnable=启用缓存 +config.template.http.cacheTimeout=缓存时间(分钟) +config.template.http.property.desc=Http返回结果中,JSON数组的属性名称。嵌套结构用 `.` 隔开。如 data.list +config.template.file.schemas=表 +config.template.file.tableName=表名 +config.template.file.format=文件格式 +config.template.file.columns=列 +config.template.file.cacheEnable=是否开启缓存 +config.template.file.cacheTimeout=缓存超时 diff --git a/server/src/main/resources/migrations/source.1.0.0-alpha.2.sql b/server/src/main/resources/migrations/source.1.0.0-alpha.2.sql deleted file mode 100644 index 5a20f4f43..000000000 --- a/server/src/main/resources/migrations/source.1.0.0-alpha.2.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `source` -DROP INDEX `prj_name`; \ No newline at end of file