This is an automated email from the ASF dual-hosted git repository.
yuzelin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-paimon-webui.git
The following commit(s) were added to refs/heads/main by this push:
new cf48e27 [UI] Optimized style (#31)
cf48e27 is described below
commit cf48e277c24a9b6356612e81a7b35772543f4bcb
Author: s7monk <[email protected]>
AuthorDate: Wed Aug 23 10:38:56 2023 +0800
[UI] Optimized style (#31)
---
.../apache/paimon/web/api/table/TableManager.java | 8 +-
.../apache/paimon/web/api/table/TableMetadata.java | 21 +-
.../web/server/controller/CatalogController.java | 13 +
.../web/server/controller/DatabaseController.java | 132 ++++++++
.../web/server/controller/TableController.java | 255 +++++++++++++++
.../paimon/web/server/data/model/DatabaseInfo.java | 38 +++
.../paimon/web/server/data/model/TableColumn.java | 42 +++
.../paimon/web/server/data/model/TableInfo.java | 49 +++
.../web/server/data/result/enums/Status.java | 8 +
.../paimon/web/server/mapper/DatabaseMapper.java | 33 ++
.../paimon/web/server/service/DatabaseService.java | 45 +++
.../server/service/impl/DatabaseServiceImpl.java | 47 +++
.../paimon/web/server/util/CatalogUtils.java | 44 +++
.../web/server/util/DataTypeConvertUtils.java | 91 ++++++
paimon-web-server/src/main/resources/db/ddl-h2.sql | 13 +-
paimon-web-server/src/main/resources/db/dml-h2.sql | 10 +-
.../src/main/resources/i18n/messages.properties | 4 +
.../main/resources/i18n/messages_en_US.properties | 4 +
.../src/main/resources/mapper/DatabaseMapper.xml | 38 +++
paimon-web-ui/src/api/api.ts | 40 ++-
paimon-web-ui/src/api/data.d.ts | 7 +
paimon-web-ui/src/api/endpoints.ts | 8 +
.../components/{ => Btn}/ChangeI18nBtn/index.tsx | 0
.../src/components/Btn/GithubLogoButton/index.tsx | 49 +++
.../src/components/Btn/ThemeSwitcherBtn/index.tsx | 51 +++
.../CatalogIconMoreDropdown/index.tsx} | 43 ++-
.../DatabaseIconMoreDropdown/index.tsx} | 43 ++-
.../TableIconMoreDropdown/index.tsx | 41 +++
paimon-web-ui/src/locales/en/translation.json | 36 ++-
paimon-web-ui/src/locales/zh-CN/translation.json | 52 ++-
.../Header/header.module.less} | 14 +-
paimon-web-ui/src/pages/Layout/Header/index.tsx | 52 ++-
.../CatalogModalForm/CatalogForm/index.tsx | 33 +-
.../components/CatalogModalForm/index.tsx | 13 +-
.../CatalogTree/catalog-tree.module.less | 62 +++-
.../LeftContent/components/CatalogTree/index.tsx | 355 +++++++++++++++++++--
.../DatabaseModalForm/DatabaseForm/index.tsx | 74 +++++
.../index.tsx | 24 +-
.../components/TableModalForm/TableForm/index.tsx | 244 ++++++++++++++
.../{CatalogModalForm => TableModalForm}/index.tsx | 33 +-
.../Metadata/components/LeftContent/index.tsx | 4 +
paimon-web-ui/src/router/index.tsx | 6 +-
paimon-web-ui/src/store/databaseStore.ts | 66 ++++
paimon-web-ui/src/store/tableStore.ts | 79 +++++
.../Database/data.d.ts} | 8 +-
paimon-web-ui/src/types/Public/data.d.ts | 3 -
.../{api/endpoints.ts => types/Table/data.d.ts} | 27 +-
47 files changed, 2173 insertions(+), 189 deletions(-)
diff --git
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/TableManager.java
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/TableManager.java
index d09baa4..3e18828 100644
---
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/TableManager.java
+++
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/TableManager.java
@@ -81,14 +81,14 @@ public class TableManager {
Schema.Builder schemaBuilder =
Schema.newBuilder()
- .partitionKeys(
- tableMetadata.primaryKeys() == null
- ? ImmutableList.of()
- :
ImmutableList.copyOf(tableMetadata.primaryKeys()))
.partitionKeys(
tableMetadata.partitionKeys() == null
? ImmutableList.of()
:
ImmutableList.copyOf(tableMetadata.partitionKeys()))
+ .primaryKey(
+ tableMetadata.primaryKeys() == null
+ ? ImmutableList.of()
+ :
ImmutableList.copyOf(tableMetadata.primaryKeys()))
.comment(tableMetadata.comment() == null ? "" :
tableMetadata.comment())
.options(handleOptions(tableMetadata.options()));
diff --git
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/TableMetadata.java
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/TableMetadata.java
index 90ea33d..89d02f4 100644
---
a/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/TableMetadata.java
+++
b/paimon-web-api/src/main/java/org/apache/paimon/web/api/table/TableMetadata.java
@@ -144,6 +144,23 @@ public class TableMetadata {
.collect(Collectors.toSet());
}
+ @Override
+ public String toString() {
+ return "TableMetadata{"
+ + "columns="
+ + columns
+ + ", partitionKeys="
+ + partitionKeys
+ + ", primaryKeys="
+ + primaryKeys
+ + ", options="
+ + options
+ + ", comment='"
+ + comment
+ + '\''
+ + '}';
+ }
+
public static TableMetadata.Builder builder() {
return new Builder();
}
@@ -156,7 +173,7 @@ public class TableMetadata {
private List<String> primaryKeys = new ArrayList<>();
- private Map<String, String> options = new HashMap<>();
+ @Nullable private Map<String, String> options = new HashMap<>();
@Nullable private String comment;
@@ -171,7 +188,7 @@ public class TableMetadata {
}
public Builder primaryKeys(List<String> primaryKeys) {
- this.partitionKeys = new ArrayList<>(primaryKeys);
+ this.primaryKeys = new ArrayList<>(primaryKeys);
return this;
}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/CatalogController.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/CatalogController.java
index ef954b4..cdc0bf1 100644
---
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/CatalogController.java
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/CatalogController.java
@@ -26,7 +26,9 @@ import org.apache.paimon.web.server.service.CatalogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -97,4 +99,15 @@ public class CatalogController {
List<CatalogInfo> catalogs = catalogService.list();
return R.succeed(catalogs);
}
+
+ /**
+ * Removes a catalog by its ID.
+ *
+ * @param catalogId The ID of the catalog to be removed.
+ * @return A response indicating the success or failure of the removal
operation.
+ */
+ @DeleteMapping("/{catalogId}")
+ public R<Void> remove(@PathVariable Integer catalogId) {
+ return catalogService.removeById(catalogId) ? R.succeed() : R.failed();
+ }
}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/DatabaseController.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/DatabaseController.java
new file mode 100644
index 0000000..83d0584
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/DatabaseController.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.controller;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.web.api.database.DatabaseManager;
+import org.apache.paimon.web.server.data.model.CatalogInfo;
+import org.apache.paimon.web.server.data.model.DatabaseInfo;
+import org.apache.paimon.web.server.data.result.R;
+import org.apache.paimon.web.server.data.result.enums.Status;
+import org.apache.paimon.web.server.service.CatalogService;
+import org.apache.paimon.web.server.util.CatalogUtils;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Database api controller. */
+@Slf4j
+@RestController
+@RequestMapping("/api/database")
+public class DatabaseController {
+
+ @Autowired private CatalogService catalogService;
+
+ /**
+ * Creates a new database based on the provided DatabaseInfo.
+ *
+ * @param databaseInfo The DatabaseInfo object containing the details of
the new database.
+ * @return R<Void/> indicating the result of the operation.
+ */
+ @PostMapping("/createDatabase")
+ public R<Void> createDatabase(@RequestBody DatabaseInfo databaseInfo) {
+ try {
+ CatalogInfo catalogInfo = getCatalogInfo(databaseInfo);
+ Catalog catalog = CatalogUtils.getCatalog(catalogInfo);
+ if (DatabaseManager.databaseExists(catalog,
databaseInfo.getDatabaseName())) {
+ return R.failed(Status.DATABASE_NAME_IS_EXIST,
databaseInfo.getDatabaseName());
+ }
+ DatabaseManager.createDatabase(catalog,
databaseInfo.getDatabaseName());
+ return R.succeed();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return R.failed(Status.DATABASE_CREATE_ERROR);
+ }
+ }
+
+ /**
+ * /** Get all database information.
+ *
+ * @return The list of all databases.
+ */
+ @GetMapping("/getAllDatabases")
+ public R<List<DatabaseInfo>> getAllDatabases() {
+ List<DatabaseInfo> databaseInfoList = new ArrayList<>();
+ List<CatalogInfo> catalogInfoList = catalogService.list();
+ if (catalogInfoList.size() > 0) {
+ catalogInfoList.forEach(
+ item -> {
+ Catalog catalog = CatalogUtils.getCatalog(item);
+ List<String> list =
DatabaseManager.listDatabase(catalog);
+ list.forEach(
+ databaseName -> {
+ DatabaseInfo info =
+ DatabaseInfo.builder()
+ .databaseName(databaseName)
+ .catalogId(item.getId())
+ .description("")
+ .build();
+ databaseInfoList.add(info);
+ });
+ });
+ }
+ return R.succeed(databaseInfoList);
+ }
+
+ /**
+ * Retrieves the associated CatalogInfo object based on the given
DatabaseInfo object.
+ *
+ * @param databaseInfo The DatabaseInfo object for which to retrieve the
associated CatalogInfo.
+ * @return The associated CatalogInfo object, or null if it doesn't exist.
+ */
+ private CatalogInfo getCatalogInfo(DatabaseInfo databaseInfo) {
+ LambdaQueryWrapper<CatalogInfo> queryWrapper = new
LambdaQueryWrapper<>();
+ queryWrapper.eq(CatalogInfo::getId, databaseInfo.getCatalogId());
+ return catalogService.getOne(queryWrapper);
+ }
+
+ /**
+ * Removes a database by its name.
+ *
+ * @param databaseInfo The information of the database to be removed.
+ * @return A response indicating the success or failure of the removal
operation.
+ * @throws RuntimeException if the database is not found or it is not
empty.
+ */
+ @DeleteMapping("/delete")
+ public R<Void> remove(@RequestBody DatabaseInfo databaseInfo) {
+ try {
+ CatalogInfo catalogInfo = getCatalogInfo(databaseInfo);
+ Catalog catalog = CatalogUtils.getCatalog(catalogInfo);
+ DatabaseManager.dropDatabase(catalog,
databaseInfo.getDatabaseName());
+ return R.succeed();
+ } catch (Catalog.DatabaseNotEmptyException |
Catalog.DatabaseNotExistException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/TableController.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/TableController.java
new file mode 100644
index 0000000..33eaa1c
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/TableController.java
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.controller;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.web.api.database.DatabaseManager;
+import org.apache.paimon.web.api.table.ColumnMetadata;
+import org.apache.paimon.web.api.table.TableManager;
+import org.apache.paimon.web.api.table.TableMetadata;
+import org.apache.paimon.web.server.data.model.CatalogInfo;
+import org.apache.paimon.web.server.data.model.TableColumn;
+import org.apache.paimon.web.server.data.model.TableInfo;
+import org.apache.paimon.web.server.data.result.R;
+import org.apache.paimon.web.server.data.result.enums.Status;
+import org.apache.paimon.web.server.service.CatalogService;
+import org.apache.paimon.web.server.util.CatalogUtils;
+import org.apache.paimon.web.server.util.DataTypeConvertUtils;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** Table api controller. */
+@Slf4j
+@RestController
+@RequestMapping("/api/table")
+public class TableController {
+
+ @Autowired private CatalogService catalogService;
+
+ /**
+ * Creates a table in the database based on the provided TableInfo.
+ *
+ * @param tableInfo The TableInfo object containing information about the
table.
+ * @return R<Void/> indicating the success or failure of the operation.
+ */
+ @PostMapping("/createTable")
+ public R<Void> createTable(@RequestBody TableInfo tableInfo) {
+ try {
+ Catalog catalog =
CatalogUtils.getCatalog(getCatalogInfo(tableInfo.getCatalogName()));
+ List<String> partitionKeys = tableInfo.getPartitionKey();
+ Map<String, String> tableOptions = tableInfo.getTableOptions();
+ TableMetadata tableMetadata =
+ TableMetadata.builder()
+ .columns(buildColumns(tableInfo))
+ .partitionKeys(partitionKeys)
+ .primaryKeys(buildPrimaryKeys(tableInfo))
+ .options(tableOptions)
+ .comment(tableInfo.getDescription())
+ .build();
+ if (TableManager.tableExists(
+ catalog, tableInfo.getDatabaseName(),
tableInfo.getTableName())) {
+ return R.failed(Status.TABLE_NAME_IS_EXIST,
tableInfo.getTableName());
+ }
+ TableManager.createTable(
+ catalog, tableInfo.getDatabaseName(),
tableInfo.getTableName(), tableMetadata);
+ return R.succeed();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return R.failed(Status.TABLE_CREATE_ERROR);
+ }
+ }
+
+ /**
+ * Handler method for the "/getAllTables" endpoint. Retrieves information
about all tables and
+ * returns a response containing the table details.
+ *
+ * @return Response object containing a list of {@link TableInfo}
representing the tables.
+ */
+ @GetMapping("/getAllTables")
+ public R<List<TableInfo>> getAllTables() {
+ List<TableInfo> tableInfoList = new ArrayList<>();
+ List<CatalogInfo> catalogInfoList = catalogService.list();
+ if (catalogInfoList.size() > 0) {
+ catalogInfoList.forEach(
+ item -> {
+ Catalog catalog = CatalogUtils.getCatalog(item);
+ List<String> databaseList =
DatabaseManager.listDatabase(catalog);
+ if (databaseList.size() > 0) {
+ databaseList.forEach(
+ db -> {
+ try {
+ List<String> tables =
+
TableManager.listTables(catalog, db);
+ if (tables.size() > 0) {
+ tables.forEach(
+ t -> {
+ try {
+ Table table =
+
TableManager.getTable(
+
catalog, db, t);
+ if (table !=
null) {
+
List<String> primaryKeys =
+
table.primaryKeys();
+
List<DataField> fields =
+
table.rowType()
+
.getFields();
+
List<TableColumn> tableColumns =
+
new ArrayList<>();
+ if
(fields.size() > 0) {
+
fields.forEach(
+
field -> {
+
TableColumn
+
.TableColumnBuilder
+
builder =
+
TableColumn
+
.builder()
+
.field(
+
field
+
.name())
+
.dataType(
+
DataTypeConvertUtils
+
.fromPaimonType(
+
field
+
.type()))
+
.comment(
+
field
+
.description());
+
if (primaryKeys
+
.size()
+
> 0
+
&& primaryKeys
+
.contains(
+
field
+
.name())) {
+
builder
+
.isPK(
+
true);
+
}
+
tableColumns
+
.add(
+
builder
+
.build());
+
});
+ }
+ TableInfo
tableInfo =
+
TableInfo.builder()
+
.catalogName(
+
item
+
.getCatalogName())
+
.databaseName(
+
db)
+
.tableName(
+
table
+
.name())
+
.partitionKey(
+
table
+
.partitionKeys())
+
.tableOptions(
+
table
+
.options())
+
.tableColumns(
+
tableColumns)
+
.build();
+
tableInfoList.add(tableInfo);
+ }
+ } catch (
+
Catalog.TableNotExistException
+ e)
{
+ throw new
RuntimeException(e);
+ }
+ });
+ }
+ } catch
(Catalog.DatabaseNotExistException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+ });
+ }
+ return R.succeed(tableInfoList);
+ }
+
+ /**
+ * Builds a list of primary keys for the given table.
+ *
+ * @param tableInfo The TableInfo object representing the table.
+ * @return A list of primary keys as strings.
+ */
+ private List<String> buildPrimaryKeys(TableInfo tableInfo) {
+ List<String> primaryKeys = new ArrayList<>();
+ List<TableColumn> tableColumns = tableInfo.getTableColumns();
+ if (tableColumns != null && tableColumns.size() > 0) {
+ tableColumns.forEach(
+ item -> {
+ if (item.isPK()) {
+ primaryKeys.add(item.getField());
+ }
+ });
+ }
+ return primaryKeys;
+ }
+
+ /**
+ * Builds a list of ColumnMetadata objects for the given table.
+ *
+ * @param tableInfo The TableInfo object representing the table.
+ * @return A list of ColumnMetadata objects.
+ */
+ private List<ColumnMetadata> buildColumns(TableInfo tableInfo) {
+ List<ColumnMetadata> columns = new ArrayList<>();
+ List<TableColumn> tableColumns = tableInfo.getTableColumns();
+ if (tableColumns != null && tableColumns.size() > 0) {
+ tableColumns.forEach(
+ item -> {
+ ColumnMetadata columnMetadata =
+ new ColumnMetadata(
+ item.getField(),
+
DataTypeConvertUtils.convert(item.getDataType()),
+ item.getComment() != null ?
item.getComment() : null);
+ columns.add(columnMetadata);
+ });
+ }
+ return columns;
+ }
+
+ /**
+ * Retrieves the associated CatalogInfo object based on the given catalog
name.
+ *
+ * @param catalogName The name of the catalog for which to retrieve the
associated CatalogInfo.
+ * @return The associated CatalogInfo object, or null if it doesn't exist.
+ */
+ private CatalogInfo getCatalogInfo(String catalogName) {
+ LambdaQueryWrapper<CatalogInfo> queryWrapper = new
LambdaQueryWrapper<>();
+ queryWrapper.eq(CatalogInfo::getCatalogName, catalogName);
+ return catalogService.getOne(queryWrapper);
+ }
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/DatabaseInfo.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/DatabaseInfo.java
new file mode 100644
index 0000000..0f93b78
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/DatabaseInfo.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.data.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/** Database table model. */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DatabaseInfo {
+
+ private String databaseName;
+
+ private Integer catalogId;
+
+ private String description;
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/TableColumn.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/TableColumn.java
new file mode 100644
index 0000000..0593dea
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/TableColumn.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.data.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/** TableColumn model. */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TableColumn {
+
+ private String field;
+
+ private String dataType;
+
+ private String comment;
+
+ private boolean isPK;
+
+ private String defaultValue;
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/TableInfo.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/TableInfo.java
new file mode 100644
index 0000000..7420063
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/TableInfo.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.data.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+
+/** Table model. */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TableInfo {
+
+ private String catalogName;
+
+ private String databaseName;
+
+ private String tableName;
+
+ private String description;
+
+ private List<TableColumn> tableColumns;
+
+ private List<String> partitionKey;
+
+ private Map<String, String> tableOptions;
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java
index 9f1209f..c6e5788 100644
---
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java
@@ -56,6 +56,14 @@ public enum Status {
/** ------------catalog-----------------. */
CATALOG_NAME_IS_EXIST(10301, "catalog.name.exist"),
CATALOG_CREATE_ERROR(10302, "catalog.create.error"),
+
+ /** ------------database-----------------. */
+ DATABASE_NAME_IS_EXIST(10401, "database.name.exist"),
+ DATABASE_CREATE_ERROR(10402, "database.create.error"),
+
+ /** ------------table-----------------. */
+ TABLE_NAME_IS_EXIST(10501, "table.name.exist"),
+ TABLE_CREATE_ERROR(10502, "table.create.error"),
;
private final int code;
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/mapper/DatabaseMapper.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/mapper/DatabaseMapper.java
new file mode 100644
index 0000000..cf07ce4
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/mapper/DatabaseMapper.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.mapper;
+
+import org.apache.paimon.web.server.data.model.DatabaseInfo;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/** Database table mapper. */
+@Mapper
+public interface DatabaseMapper extends BaseMapper<DatabaseInfo> {
+
+ List<DatabaseInfo> selectByCatalogId(Integer catalogId);
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/DatabaseService.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/DatabaseService.java
new file mode 100644
index 0000000..8b38e42
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/DatabaseService.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.service;
+
+import org.apache.paimon.web.server.data.model.DatabaseInfo;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import java.util.List;
+
+/** Database Service. */
+public interface DatabaseService extends IService<DatabaseInfo> {
+
+ /**
+ * Verify if the database name is unique.
+ *
+ * @param databaseInfo database info
+ * @return result
+ */
+ boolean checkCatalogNameUnique(DatabaseInfo databaseInfo);
+
+ /**
+ * Retrieve a list of databases by catalog ID.
+ *
+ * @param catalogId the ID of the catalog
+ * @return a list of DatabaseInfo objects
+ */
+ List<DatabaseInfo> selectByCatalogId(Integer catalogId);
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/DatabaseServiceImpl.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/DatabaseServiceImpl.java
new file mode 100644
index 0000000..6d25fe0
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/DatabaseServiceImpl.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.service.impl;
+
+import org.apache.paimon.web.server.data.model.DatabaseInfo;
+import org.apache.paimon.web.server.mapper.DatabaseMapper;
+import org.apache.paimon.web.server.service.DatabaseService;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/** DatabaseServiceImpl. */
+@Service
+public class DatabaseServiceImpl extends ServiceImpl<DatabaseMapper,
DatabaseInfo>
+ implements DatabaseService {
+
+ @Autowired private DatabaseMapper databaseMapper;
+
+ @Override
+ public boolean checkCatalogNameUnique(DatabaseInfo databaseInfo) {
+ return false;
+ }
+
+ @Override
+ public List<DatabaseInfo> selectByCatalogId(Integer catalogId) {
+ return databaseMapper.selectByCatalogId(catalogId);
+ }
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/CatalogUtils.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/CatalogUtils.java
new file mode 100644
index 0000000..c19668e
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/CatalogUtils.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.util;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.web.api.catalog.CatalogCreator;
+import org.apache.paimon.web.server.data.model.CatalogInfo;
+
+/** catalog util. */
+public class CatalogUtils {
+
+ /**
+ * Get a Catalog based on the provided CatalogInfo.
+ *
+ * @param catalogInfo The CatalogInfo object containing the catalog
details.
+ * @return The created Catalog object.
+ */
+ public static Catalog getCatalog(CatalogInfo catalogInfo) {
+ if ("filesystem".equals(catalogInfo.getCatalogType())) {
+ return
CatalogCreator.createFilesystemCatalog(catalogInfo.getWarehouse());
+ } else {
+ return CatalogCreator.createHiveCatalog(
+ catalogInfo.getWarehouse(),
+ catalogInfo.getHiveUri(),
+ catalogInfo.getHiveConfDir());
+ }
+ }
+}
diff --git
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/DataTypeConvertUtils.java
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/DataTypeConvertUtils.java
new file mode 100644
index 0000000..aa4f9f9
--- /dev/null
+++
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/util/DataTypeConvertUtils.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.util;
+
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypes;
+
+/** data type convert util. */
+public class DataTypeConvertUtils {
+
+ public static DataType convert(String type) {
+ switch (type) {
+ case "INT":
+ return DataTypes.INT();
+ case "TINYINT":
+ return DataTypes.TINYINT();
+ case "SMALLINT":
+ return DataTypes.SMALLINT();
+ case "BIGINT":
+ return DataTypes.BIGINT();
+ case "STRING":
+ return DataTypes.STRING();
+ case "DOUBLE":
+ return DataTypes.DOUBLE();
+ case "BOOLEAN":
+ return DataTypes.BOOLEAN();
+ case "DATE":
+ return DataTypes.DATE();
+ case "TIME":
+ return DataTypes.TIME();
+ case "TIMESTAMP":
+ return DataTypes.TIMESTAMP();
+ case "BYTES":
+ return DataTypes.BYTES();
+ case "FLOAT":
+ return DataTypes.FLOAT();
+ case "DECIMAL":
+ return DataTypes.DECIMAL(38, 0);
+ default:
+ throw new RuntimeException("Invalid type: " + type);
+ }
+ }
+
+ public static String fromPaimonType(DataType dataType) {
+ if (dataType.equals(DataTypes.INT())) {
+ return "INT";
+ } else if (dataType.equals(DataTypes.TINYINT())) {
+ return "TINYINT";
+ } else if (dataType.equals(DataTypes.SMALLINT())) {
+ return "SMALLINT";
+ } else if (dataType.equals(DataTypes.BIGINT())) {
+ return "BIGINT";
+ } else if (dataType.equals(DataTypes.STRING())) {
+ return "STRING";
+ } else if (dataType.equals(DataTypes.DOUBLE())) {
+ return "DOUBLE";
+ } else if (dataType.equals(DataTypes.BOOLEAN())) {
+ return "BOOLEAN";
+ } else if (dataType.equals(DataTypes.DATE())) {
+ return "DATE";
+ } else if (dataType.equals(DataTypes.TIME())) {
+ return "TIME";
+ } else if (dataType.equals(DataTypes.TIMESTAMP())) {
+ return "TIMESTAMP";
+ } else if (dataType.equals(DataTypes.BYTES())) {
+ return "BYTES";
+ } else if (dataType.equals(DataTypes.FLOAT())) {
+ return "FLOAT";
+ } else if (dataType.equals(DataTypes.DECIMAL(38, 0))) {
+ return "DECIMAL";
+ } else {
+ return "UNKNOWN";
+ }
+ }
+}
diff --git a/paimon-web-server/src/main/resources/db/ddl-h2.sql
b/paimon-web-server/src/main/resources/db/ddl-h2.sql
index fc6936f..ab24591 100644
--- a/paimon-web-server/src/main/resources/db/ddl-h2.sql
+++ b/paimon-web-server/src/main/resources/db/ddl-h2.sql
@@ -114,4 +114,15 @@ CREATE TABLE if not exists `catalog`
`is_delete` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'is delete',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create
time',
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update
time'
-) engine = innodb;
\ No newline at end of file
+) engine = innodb;
+
+CREATE TABLE if not exists `databases`
+(
+ `id` int(11) not null auto_increment primary key comment 'id',
+ `database_name` varchar(50) not null comment 'database name',
+ `catalog_id` int(11) not null comment 'catalog id',
+ `description` varchar(200) comment 'description',
+ `is_delete` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'is delete',
+ `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create
time',
+ `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update
time'
+) engine = innodb;
diff --git a/paimon-web-server/src/main/resources/db/dml-h2.sql
b/paimon-web-server/src/main/resources/db/dml-h2.sql
index 6b0cfa6..77d10ae 100644
--- a/paimon-web-server/src/main/resources/db/dml-h2.sql
+++ b/paimon-web-server/src/main/resources/db/dml-h2.sql
@@ -61,7 +61,13 @@ values (1, 1),
(1, 1003),
(1, 1004);
-insert into `catalog` (catalog_type, catalog_name, warehouse)
+/*insert into `catalog` (catalog_type, catalog_name, warehouse)
values ('filesystem', 'paimon', 'file:///D:/path/'),
('filesystem', 'fts', 'file:///D:/path/'),
- ('filesystem', 'streaming_warehouse', 'file:///D:/path/')
+ ('filesystem', 'streaming_warehouse', 'file:///D:/path/');
+
+insert into `databases` (database_name, catalog_id, description)
+values ('ods', 1, 'description'),
+ ('ods', 2, 'description'),
+ ('ods', 3, 'description')*/
+
diff --git a/paimon-web-server/src/main/resources/i18n/messages.properties
b/paimon-web-server/src/main/resources/i18n/messages.properties
index 7c16a94..86a4bc8 100644
--- a/paimon-web-server/src/main/resources/i18n/messages.properties
+++ b/paimon-web-server/src/main/resources/i18n/messages.properties
@@ -35,3 +35,7 @@ menu.name.exist=This menu name is exist:{0}
menu.path.invalid=This menu path is invalid:{0}
catalog.name.exist=This catalog name {0} is exist
catalog.create.error=An exception is returned when calling the Paimon API to
create a Catalog.
+database.name.exist=This database name {0} is exist.
+database.create.error=An exception is returned when calling the Paimon API to
create a Database.
+table.name.exist=This table name {0} is exist.
+table.create.error=An exception is returned when calling the Paimon API to
create a Table.
diff --git
a/paimon-web-server/src/main/resources/i18n/messages_en_US.properties
b/paimon-web-server/src/main/resources/i18n/messages_en_US.properties
index f48cf18..ca74509 100644
--- a/paimon-web-server/src/main/resources/i18n/messages_en_US.properties
+++ b/paimon-web-server/src/main/resources/i18n/messages_en_US.properties
@@ -33,3 +33,7 @@ role.key.exist=This role key {0} is exist
menu.in.used=This menu is in used
menu.name.exist=This menu name is exist:{0}
menu.path.invalid=This menu path is invalid:{0}
+catalog.name.exist=This catalog name {0} is exist
+catalog.create.error=An exception is returned when calling the Paimon API to
create a Catalog.
+database.name.exist=This database name {0} is exist.
+database.create.error=An exception is returned when calling the Paimon API to
create a Database.
diff --git a/paimon-web-server/src/main/resources/mapper/DatabaseMapper.xml
b/paimon-web-server/src/main/resources/mapper/DatabaseMapper.xml
new file mode 100644
index 0000000..8f1aa28
--- /dev/null
+++ b/paimon-web-server/src/main/resources/mapper/DatabaseMapper.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<!DOCTYPE mapper
+ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+ "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.apache.paimon.web.server.mapper.DatabaseMapper">
+
+ <resultMap type="org.apache.paimon.web.server.data.model.DatabaseInfo"
id="BaseResultMap">
+ <id property="id" column="id" />
+ <result property="databaseName" column="database_name" />
+ <result property="catalogId" column="catalog_id" />
+ </resultMap>
+
+ <select id="selectByCatalogId" parameterType="java.lang.Integer"
resultMap="BaseResultMap">
+ SELECT *
+ FROM database_info
+ WHERE catalog_id = #{catalogId}
+ </select>
+
+</mapper>
\ No newline at end of file
diff --git a/paimon-web-ui/src/api/api.ts b/paimon-web-ui/src/api/api.ts
index 679728c..1d80977 100644
--- a/paimon-web-ui/src/api/api.ts
+++ b/paimon-web-ui/src/api/api.ts
@@ -19,6 +19,8 @@ import http from '@api/http'
import {API_ENDPOINTS} from '@api/endpoints';
import Result = API.Result;
import {CatalogItemList} from "@src/types/Catalog/data";
+import {DatabaseItem} from "@src/types/Database/data";
+import {TableItem} from "@src/types/Table/data";
export const createFileSystemCatalog = async (catalogProp: Prop.CatalogProp)
=> {
try {
@@ -44,10 +46,46 @@ export const getAllCatalogs = async () => {
}
}
+export const createDatabase = async (databaseProp: Prop.DatabaseProp) => {
+ try {
+ return await http.httpPost<Result<any>,
Prop.DatabaseProp>(API_ENDPOINTS.CREATE_DATABASE, databaseProp);
+ } catch (error) {
+ console.error('Failed to create database:', error);
+ }
+};
+
+export const getAllDatabases = async () => {
+ try {
+ return await http.httpGet<Result<DatabaseItem[]>,
null>(API_ENDPOINTS.GET_ALL_DATABASES)
+ } catch (error: any) {
+ console.error('Failed to get database:', error);
+ }
+}
+
+export const createTable = async (databaseProp: TableItem) => {
+ try {
+ return await http.httpPost<Result<any>,
TableItem>(API_ENDPOINTS.CREATE_TABLE, databaseProp);
+ } catch (error) {
+ console.error('Failed to create table:', error);
+ }
+};
+
+export const getAllTables = async () => {
+ try {
+ return await http.httpGet<Result<TableItem[]>,
null>(API_ENDPOINTS.GET_ALL_TABLES)
+ } catch (error: any) {
+ console.error('Failed to get tables:', error);
+ }
+}
+
const Api = {
createFileSystemCatalog,
createHiveCatalog,
- getAllCatalogs
+ getAllCatalogs,
+ createDatabase,
+ getAllDatabases,
+ createTable,
+ getAllTables,
}
export default Api;
diff --git a/paimon-web-ui/src/api/data.d.ts b/paimon-web-ui/src/api/data.d.ts
index 2381a78..886c716 100644
--- a/paimon-web-ui/src/api/data.d.ts
+++ b/paimon-web-ui/src/api/data.d.ts
@@ -42,4 +42,11 @@ declare namespace Prop {
hiveConfDir: string,
isDelete: boolean
}
+
+ // Database
+ export interface DatabaseProp {
+ databaseName: string,
+ catalogId: number,
+ description: string
+ }
}
\ No newline at end of file
diff --git a/paimon-web-ui/src/api/endpoints.ts
b/paimon-web-ui/src/api/endpoints.ts
index 1567581..8cfbfce 100644
--- a/paimon-web-ui/src/api/endpoints.ts
+++ b/paimon-web-ui/src/api/endpoints.ts
@@ -26,4 +26,12 @@ export const API_ENDPOINTS = {
CREATE_HIVE_CATALOG: '/catalog/createHiveCatalog',
GET_ALL_CATALOGS: '/catalog/getAllCatalogs',
+ // database
+ CREATE_DATABASE: '/database/createDatabase',
+ GET_ALL_DATABASES: '/database/getAllDatabases',
+
+ // table
+ CREATE_TABLE: '/table/createTable',
+ GET_ALL_TABLES: '/table/getAllTables',
+
};
\ No newline at end of file
diff --git a/paimon-web-ui/src/components/ChangeI18nBtn/index.tsx
b/paimon-web-ui/src/components/Btn/ChangeI18nBtn/index.tsx
similarity index 100%
rename from paimon-web-ui/src/components/ChangeI18nBtn/index.tsx
rename to paimon-web-ui/src/components/Btn/ChangeI18nBtn/index.tsx
diff --git a/paimon-web-ui/src/components/Btn/GithubLogoButton/index.tsx
b/paimon-web-ui/src/components/Btn/GithubLogoButton/index.tsx
new file mode 100644
index 0000000..2b34ab2
--- /dev/null
+++ b/paimon-web-ui/src/components/Btn/GithubLogoButton/index.tsx
@@ -0,0 +1,49 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License. */
+
+import {Button, Tooltip} from "@douyinfe/semi-ui";
+import {IconGithubLogo} from "@douyinfe/semi-icons";
+import i18n from 'i18next';
+import {useMemo} from "react";
+import { useTranslation } from 'react-i18next';
+
+const GithubLogoButton = () => {
+ const { t } = useTranslation();
+ const handleClick = () => {
+ window.open('https://github.com/apache/incubator-paimon-webui',
'_blank');
+ }
+
+ const tooltipContent = useMemo(() => {
+ return t('component.btn-github-icon');
+ }, [i18n.language]);
+
+ return (
+ <Tooltip content={tooltipContent} position='bottom'>
+ <Button
+ theme="borderless"
+ icon={<IconGithubLogo size="extra-large"/>}
+ style={{
+ color: 'var(--semi-color-text-2)',
+ marginRight: '12px',
+ }}
+ onClick={handleClick}
+ />
+ </Tooltip>
+ )
+}
+
+export default GithubLogoButton;
\ No newline at end of file
diff --git a/paimon-web-ui/src/components/Btn/ThemeSwitcherBtn/index.tsx
b/paimon-web-ui/src/components/Btn/ThemeSwitcherBtn/index.tsx
new file mode 100644
index 0000000..a298b6e
--- /dev/null
+++ b/paimon-web-ui/src/components/Btn/ThemeSwitcherBtn/index.tsx
@@ -0,0 +1,51 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License. */
+
+import { useMemo } from "react";
+import {Button, Tooltip} from "@douyinfe/semi-ui";
+import {IconMoon, IconSun} from "@douyinfe/semi-icons";
+import useThemeSwitcher from '@utils/mode.ts';
+import i18n from 'i18next';
+import { useTranslation } from 'react-i18next';
+
+const ThemeSwitcherBtn = () => {
+ const { t } = useTranslation();
+ const {dark, switchMode } = useThemeSwitcher();
+
+ const [tooltipContent, icon] = useMemo(() => {
+ const tooltipContent = dark ? t('component.btn-switch-to-light-mode')
: t('component.btn-switch-to-dark-mode');
+ const icon = dark ? <IconSun size={"extra-large"}/> : <IconMoon
size={"extra-large"}/>;
+ return [tooltipContent, icon];
+ }, [dark, i18n.language]);
+
+ return (
+ <Tooltip content={tooltipContent} position='bottom'>
+ <Button
+ theme="borderless"
+ icon={icon}
+ style={{
+ color: 'var(--semi-color-text-2)',
+ marginRight: '12px',
+ }}
+ aria-label={tooltipContent}
+ onClick={switchMode}
+ />
+ </Tooltip>
+ )
+}
+
+export default ThemeSwitcherBtn;
\ No newline at end of file
diff --git a/paimon-web-ui/src/api/data.d.ts
b/paimon-web-ui/src/components/IconMoreDropdown/CatalogIconMoreDropdown/index.tsx
similarity index 53%
copy from paimon-web-ui/src/api/data.d.ts
copy to
paimon-web-ui/src/components/IconMoreDropdown/CatalogIconMoreDropdown/index.tsx
index 2381a78..bf3c504 100644
--- a/paimon-web-ui/src/api/data.d.ts
+++
b/paimon-web-ui/src/components/IconMoreDropdown/CatalogIconMoreDropdown/index.tsx
@@ -15,31 +15,26 @@ KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License. */
-declare namespace API {
- export interface Result<T> {
- code: number;
- msg: string;
- data: T;
- }
+import { IconMore, IconDelete } from "@douyinfe/semi-icons";
+import { Dropdown } from '@douyinfe/semi-ui';
+import { useTranslation } from 'react-i18next';
- export interface PageData<T> {
- records: T[];
- page: number;
- size: number;
- total: number;
- }
+const CatalogIconMoreDropdown = () => {
+ const { t } = useTranslation();
- export type PageResult<T> = Result<PageData<T>>
+ return(
+ <Dropdown
+ trigger={'click'}
+ position={'bottomLeft'}
+ render={
+ <Dropdown.Menu>
+ <Dropdown.Item><IconDelete/>
{t('component.icon-more-dropdown-delete')}</Dropdown.Item>
+ </Dropdown.Menu>
+ }
+ >
+ <IconMore onClick={(e) => e.stopPropagation()}/>
+ </Dropdown>
+ );
}
-declare namespace Prop {
- // Catalog
- export interface CatalogProp {
- catalogName: string,
- catalogType: string,
- warehouse: string,
- hiveUri: string,
- hiveConfDir: string,
- isDelete: boolean
- }
-}
\ No newline at end of file
+export default CatalogIconMoreDropdown;
\ No newline at end of file
diff --git a/paimon-web-ui/src/api/data.d.ts
b/paimon-web-ui/src/components/IconMoreDropdown/DatabaseIconMoreDropdown/index.tsx
similarity index 53%
copy from paimon-web-ui/src/api/data.d.ts
copy to
paimon-web-ui/src/components/IconMoreDropdown/DatabaseIconMoreDropdown/index.tsx
index 2381a78..bf3c504 100644
--- a/paimon-web-ui/src/api/data.d.ts
+++
b/paimon-web-ui/src/components/IconMoreDropdown/DatabaseIconMoreDropdown/index.tsx
@@ -15,31 +15,26 @@ KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License. */
-declare namespace API {
- export interface Result<T> {
- code: number;
- msg: string;
- data: T;
- }
+import { IconMore, IconDelete } from "@douyinfe/semi-icons";
+import { Dropdown } from '@douyinfe/semi-ui';
+import { useTranslation } from 'react-i18next';
- export interface PageData<T> {
- records: T[];
- page: number;
- size: number;
- total: number;
- }
+const CatalogIconMoreDropdown = () => {
+ const { t } = useTranslation();
- export type PageResult<T> = Result<PageData<T>>
+ return(
+ <Dropdown
+ trigger={'click'}
+ position={'bottomLeft'}
+ render={
+ <Dropdown.Menu>
+ <Dropdown.Item><IconDelete/>
{t('component.icon-more-dropdown-delete')}</Dropdown.Item>
+ </Dropdown.Menu>
+ }
+ >
+ <IconMore onClick={(e) => e.stopPropagation()}/>
+ </Dropdown>
+ );
}
-declare namespace Prop {
- // Catalog
- export interface CatalogProp {
- catalogName: string,
- catalogType: string,
- warehouse: string,
- hiveUri: string,
- hiveConfDir: string,
- isDelete: boolean
- }
-}
\ No newline at end of file
+export default CatalogIconMoreDropdown;
\ No newline at end of file
diff --git
a/paimon-web-ui/src/components/IconMoreDropdown/TableIconMoreDropdown/index.tsx
b/paimon-web-ui/src/components/IconMoreDropdown/TableIconMoreDropdown/index.tsx
new file mode 100644
index 0000000..81b504f
--- /dev/null
+++
b/paimon-web-ui/src/components/IconMoreDropdown/TableIconMoreDropdown/index.tsx
@@ -0,0 +1,41 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License. */
+
+import { IconMore, IconDelete, IconEdit } from "@douyinfe/semi-icons";
+import { Dropdown } from '@douyinfe/semi-ui';
+import { useTranslation } from 'react-i18next';
+
+const TableIconMoreDropdown = () => {
+ const { t } = useTranslation();
+
+ return(
+ <Dropdown
+ trigger={'click'}
+ position={'bottomLeft'}
+ render={
+ <Dropdown.Menu>
+ <Dropdown.Item><IconEdit/>
{t('component.icon-more-dropdown-rename')}</Dropdown.Item>
+ <Dropdown.Item><IconDelete/>
{t('component.icon-more-dropdown-delete')}</Dropdown.Item>
+ </Dropdown.Menu>
+ }
+ >
+ <IconMore onClick={(e) => e.stopPropagation()}/>
+ </Dropdown>
+ );
+}
+
+export default TableIconMoreDropdown;
\ No newline at end of file
diff --git a/paimon-web-ui/src/locales/en/translation.json
b/paimon-web-ui/src/locales/en/translation.json
index e4277d5..1ac2217 100644
--- a/paimon-web-ui/src/locales/en/translation.json
+++ b/paimon-web-ui/src/locales/en/translation.json
@@ -15,6 +15,40 @@
"catalog": "Catalog",
"tableInfo": "TableInfo",
"details": "Details",
- "files": "Files"
+ "files": "Files",
+ "add-database": "Add Database",
+ "add-table": "Add Table",
+ "create-database": "Create Database",
+ "create-table": "Create Table",
+ "submit": "Submit",
+ "cancel": "Cancel",
+ "message": "This item is required.",
+ "database-name": "Database Name",
+ "table-name": "Table Name",
+ "description": "Description",
+ "text-area-description": "Please Enter",
+ "create-catalog": "Create Catalog",
+ "create-database-success": "Database created successfully!",
+ "create-database-failed": "Failed to create database:",
+ "create-table-success": "Table created successfully!",
+ "create-table-failed": "Failed to create table:",
+ "form-group-basic-information": "Basic Information",
+ "form-group-add-column-ordinary": "Common Column",
+ "form-group-add-column-partition": "Partition Column",
+ "add-column-field": "Field",
+ "add-column-default-value": "Default Value",
+ "add-column-type": "Type",
+ "add-column-primary-key": "Primary Key",
+ "add-column-comment": "Comment",
+ "form-group-add-config": "Add Options",
+ "add-config-key": "Key",
+ "add-config-value": "Value"
+ },
+ "component": {
+ "icon-more-dropdown-rename": "Rename",
+ "icon-more-dropdown-delete": "Delete",
+ "btn-switch-to-light-mode": "Switch to Light Mode",
+ "btn-switch-to-dark-mode": "Switch to Dark Mode",
+ "btn-github-icon": "Visit GitHub repository"
}
}
diff --git a/paimon-web-ui/src/locales/zh-CN/translation.json
b/paimon-web-ui/src/locales/zh-CN/translation.json
index a144468..8d5ecc3 100644
--- a/paimon-web-ui/src/locales/zh-CN/translation.json
+++ b/paimon-web-ui/src/locales/zh-CN/translation.json
@@ -1,20 +1,54 @@
{
"common": {
- "filter": "过滤"
+ "filter": "搜索"
},
"header": {
- "playground": "运行信息",
- "metadata": "元数据",
- "cdcingestion": "CDC集成",
- "system": "系统"
+ "playground": "查询控制台",
+ "metadata": "元数据管理",
+ "cdcingestion": "CDC 集成",
+ "system": "系统管理"
},
"playground": {
"selectCatalog": "选择Catalog"
},
"metadata": {
- "catalog": "元数据",
- "tableInfo": "表信息",
- "details": "详细",
- "files": "文件"
+ "catalog": "Catalog目录",
+ "tableInfo": "基本信息",
+ "details": "详细信息",
+ "files": "数据文件",
+ "add-database": "添加数据库",
+ "add-table": "添加数据表",
+ "create-database": "创建 Database",
+ "create-table": "创建 Table",
+ "submit": "确定",
+ "cancel": "取消",
+ "message": "该项为必填项!",
+ "database-name": "名称",
+ "table-name": "名称",
+ "description": "描述",
+ "text-area-description": "请输入",
+ "create-catalog": "创建Catalog",
+ "create-database-success": "创建 Database 成功!",
+ "create-database-failed": "创建 Database 失败:",
+ "create-table-success": "创建 Table 成功!",
+ "create-table-failed": "创建 Table 失败:",
+ "form-group-basic-information": "基本信息",
+ "form-group-add-column-ordinary": "普通列",
+ "form-group-add-column-partition": "分区列",
+ "add-column-field": "字段",
+ "add-column-default-value": "默认值",
+ "add-column-type": "类型",
+ "add-column-primary-key": "主键",
+ "add-column-comment": "备注",
+ "form-group-add-config": "添加选项",
+ "add-config-key": "键",
+ "add-config-value": "值"
+ },
+ "component": {
+ "icon-more-dropdown-rename": "重命名",
+ "icon-more-dropdown-delete": "删除",
+ "btn-switch-to-light-mode": "切换到亮色模式",
+ "btn-switch-to-dark-mode": "切换到暗色模式",
+ "btn-github-icon": "访问 GitHub 仓库"
}
}
diff --git
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/catalog-tree.module.less
b/paimon-web-ui/src/pages/Layout/Header/header.module.less
similarity index 80%
copy from
paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/catalog-tree.module.less
copy to paimon-web-ui/src/pages/Layout/Header/header.module.less
index 4f3f847..c3449e3 100644
---
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/catalog-tree.module.less
+++ b/paimon-web-ui/src/pages/Layout/Header/header.module.less
@@ -15,8 +15,14 @@ KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License. */
-.catalog-tree-input {
- //background-color: var(--semi-color-bg-0);
- width: 347px;
- margin-left: -12px;
+.selected-nav-item {
+ color: rgba(var(--semi-blue-5), 1) !important;
+}
+
+.header {
+ width: 96px;
+ height: 36px;
+ font-size: 36px;
+ margin-left: -45px;
+ margin-right: -20px;
}
\ No newline at end of file
diff --git a/paimon-web-ui/src/pages/Layout/Header/index.tsx
b/paimon-web-ui/src/pages/Layout/Header/index.tsx
index 05b5440..8b9aceb 100644
--- a/paimon-web-ui/src/pages/Layout/Header/index.tsx
+++ b/paimon-web-ui/src/pages/Layout/Header/index.tsx
@@ -15,26 +15,33 @@ KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License. */
-import {Avatar, Button, Layout, Nav} from '@douyinfe/semi-ui';
-import { IconMoon, IconGithubLogo, IconSun } from '@douyinfe/semi-icons';
-import useThemeSwitcher from '@src/utils/mode'
-import paimonLogo from '@src/assets/logo/favicon_blue.svg'
-import paimonWhiteLogo from '@src/assets/logo/favicon_white.svg'
-import {useNavigate} from "react-router";
+import {Avatar, Layout, Nav} from '@douyinfe/semi-ui';
+import paimonLogo from '@src/assets/logo/favicon_blue.svg';
+import {useLocation, useNavigate} from "react-router";
import {useMemo, useState} from "react";
import menuList from "@config/menu.tsx";
-import ChangeI18nBtn from "@components/ChangeI18nBtn";
+import ChangeI18nBtn from "@components/Btn/ChangeI18nBtn";
+import ThemeSwitcherBtn from "@components/Btn/ThemeSwitcherBtn";
import {useTranslation} from "react-i18next";
import {Link} from "react-router-dom";
+import GithubLogoButton from "@components/Btn/GithubLogoButton";
+import styles from "./header.module.less";
const { Header } = Layout
const HeaderRoot = ()=> {
- const {dark, switchMode } = useThemeSwitcher();
const navigate = useNavigate()
const [openKeys, setOpenKeys] = useState<string[]>([])
- const [selectedKeys, setSelectedKeys] = useState<string[]>([])
+ const location = useLocation();
+
+ const initialKey = useMemo(() => {
+ const path = location.pathname;
+ const matchedItem = menuList.find(item => item.path === path);
+ return matchedItem ? matchedItem.name : menuList[0]?.name;
+ }, [location, menuList]);
+
+ const [selectedKeys, setSelectedKeys] = useState<string[]>([initialKey]);
const navList = useMemo(() => {
return menuList.map((e) => {
@@ -67,7 +74,7 @@ const HeaderRoot = ()=> {
<div>
<Nav
header={{
- logo: <img src={dark ? paimonWhiteLogo : paimonLogo}
alt="Apache Paimon"
+ logo: <img src={ paimonLogo } alt="Apache Paimon"
style={{ width: '96px', height: '36px',
fontSize: 36, marginLeft: '-45px', marginRight: '-20px'}}/>,
text: 'Apache Paimon'
}}
@@ -80,23 +87,8 @@ const HeaderRoot = ()=> {
// items={navList}
footer={
<div>
- <Button
- theme="borderless"
- icon={!dark ? <IconMoon size={"extra-large"}/>
: <IconSun size={"extra-large"}/>}
- style={{
- color: 'var(--semi-color-text-2)',
- marginRight: '12px',
- }}
- onClick={switchMode}
- />
- <Button
- theme="borderless"
- icon={<IconGithubLogo size="extra-large"/>}
- style={{
- color: 'var(--semi-color-text-2)',
- marginRight: '12px',
- }}
- />
+ <ThemeSwitcherBtn />
+ <GithubLogoButton />
<ChangeI18nBtn />
<Avatar
color="orange"
@@ -115,7 +107,11 @@ const HeaderRoot = ()=> {
style={{ textDecoration: "none" }}
to={nav.path}
>
- <Nav.Item itemKey={nav.name} text={t('header.'
+ nav.name)} />
+ <Nav.Item
+ itemKey={nav.name}
+ text={t('header.' + nav.name)}
+ className={selectedKeys.includes(nav.name)
? styles['selected-nav-item'] : ''}
+ />
</Link>
)
})
diff --git
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/CatalogForm/index.tsx
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/CatalogForm/index.tsx
index 55cd745..cb0723e 100644
---
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/CatalogForm/index.tsx
+++
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/CatalogForm/index.tsx
@@ -16,10 +16,11 @@ specific language governing permissions and limitations
under the License. */
import { Form } from '@douyinfe/semi-ui';
+import {useTranslation} from "react-i18next";
// @ts-ignore
const CatalogForm = ({ getFormApi }) => {
- let message = 'This item is required.';
+ const { t } = useTranslation();
return(
<>
<Form
@@ -37,18 +38,20 @@ const CatalogForm = ({ getFormApi }) => {
label='Catalog Name'
trigger='blur'
rules={[
- { required: true, message },
+ { required: true, message:
t('metadata.message') },
]}
- style={{ width: "100%" }}/>
+ style={{ width: "100%" }}
+ showClear/>
<Form.Select
field="catalogType"
label='Catalog Type'
placeholder={"please select catalog"}
rules={[
- { required: true, message },
+ { required: true, message:
t('metadata.message') },
]}
- style={{ width: "100%" }}>
+ style={{ width: "100%" }}
+ showClear>
<Form.Select.Option
value="filesystem">FileSystem</Form.Select.Option>
<Form.Select.Option
value="hive">Hive</Form.Select.Option>
</Form.Select>
@@ -59,9 +62,10 @@ const CatalogForm = ({ getFormApi }) => {
label='Warehouse'
trigger='blur'
rules={[
- { required: true, message },
+ { required: true, message:
t('metadata.message') },
]}
- style={{ width: "100%" }}/>
+ style={{ width: "100%" }}
+ showClear/>
)
:
<>
@@ -70,27 +74,30 @@ const CatalogForm = ({ getFormApi }) => {
label='Warehouse'
trigger='blur'
rules={[
- { required: true, message },
+ { required: true, message:
t('metadata.message') },
]}
- style={{ width: "100%" }}/>
+ style={{ width: "100%" }}
+ showClear/>
<Form.Input
field="uri"
label='Hive Uri'
trigger='blur'
rules={[
- { required: true, message },
+ { required: true, message:
t('metadata.message') },
]}
- style={{ width: "100%" }}/>
+ style={{ width: "100%" }}
+ showClear/>
<Form.Input
field="hiveConfDir"
label='Hive Conf Dir'
trigger='blur'
rules={[
- { required: true, message },
+ { required: true, message:
t('metadata.message') },
]}
- style={{ width: "100%" }}/>
+ style={{ width: "100%" }}
+ showClear/>
</>
}
</>
diff --git
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/index.tsx
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/index.tsx
index 05780b1..11c546d 100644
---
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/index.tsx
+++
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/index.tsx
@@ -18,6 +18,8 @@ under the License. */
import React, {useState} from "react";
import { Modal } from '@douyinfe/semi-ui';
import CatalogForm from
"@pages/Metadata/components/LeftContent/components/CatalogModalForm/CatalogForm";
+import {useTranslation} from "react-i18next";
+import {auto} from "@popperjs/core";
type CatalogModalFormProps = {
visible: boolean;
@@ -27,6 +29,7 @@ type CatalogModalFormProps = {
const CatalogModalForm: React.FC<CatalogModalFormProps> = ({ visible ,
onClose, onOk }) => {
+ const { t } = useTranslation();
const [formApi, setFormApi] = useState(null);
const getFormApi = (api: any) => {
@@ -40,14 +43,14 @@ const CatalogModalForm: React.FC<CatalogModalFormProps> =
({ visible , onClose,
return(
<Modal
- title="Create Catalog"
+ title={t('metadata.create-catalog')}
visible = {visible}
onCancel= {onClose}
maskClosable={false}
- okText={'Submit'}
- cancelText={'Cancel'}
- width={'750px'}
- height={'550px'}
+ okText={t('metadata.submit')}
+ cancelText={t('metadata.cancel')}
+ width={'690px'}
+ height={auto}
onOk={() => handleOkClick()}
>
<CatalogForm getFormApi={getFormApi}/>
diff --git
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/catalog-tree.module.less
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/catalog-tree.module.less
index 4f3f847..d9be919 100644
---
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/catalog-tree.module.less
+++
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/catalog-tree.module.less
@@ -19,4 +19,64 @@ under the License. */
//background-color: var(--semi-color-bg-0);
width: 347px;
margin-left: -12px;
-}
\ No newline at end of file
+}
+
+.liClass {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.iconPlus {
+ position: absolute;
+ right: 40px;
+ padding: 2px;
+ transition: all 0.2s ease-in-out; /* Add transition effect */
+ border: none;
+ color: #000;
+ font-size: 12px; /* Small size for the "+" icon in the initial state */
+ visibility: hidden; /* Hide the "+" icon in the initial state */
+ border-radius: 50%; /* Add border radius */
+ background: transparent; /* Transparent background in the initial state */
+}
+
+.liClass:hover .iconPlus{
+ visibility: visible; /* Show the "+" icon when hovering over the entire node
*/
+ border: none;
+ color: #000;
+ font-size: 12px;
+ background: transparent;
+}
+
+.liClass .iconPlus:hover{
+ border: 1px solid #d3d3d3; /* Gray border when hovering over the "+" icon */
+ color: #d3d3d3; /* Gray color when hovering over the "+" icon */
+ font-size: 16px; /* Increase the size of the "+" icon when hovering */
+ background: #fff; /* White background when hovering over the "+" icon */
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); /* Add shadow effect */
+}
+
+.iconMore {
+ display: flex;
+ align-items: center;
+ transition: all 0.2s ease-in-out;
+ visibility: hidden;
+ margin-right: 12px;
+ color: #000;
+}
+
+.liClass:hover .iconMore {
+ visibility: visible;
+}
+
+.semi-tree-node-selected {
+ color: rgba(var(--semi-blue-5), 1) !important;
+}
+
+.icon {
+ margin-right: 8px;
+ color: var(--semi-color-text-2);
+}
+
diff --git
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/index.tsx
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/index.tsx
index 61746f1..ae8322f 100644
---
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/index.tsx
+++
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/index.tsx
@@ -16,26 +16,108 @@ specific language governing permissions and limitations
under the License. */
import {Input, Tree} from '@douyinfe/semi-ui';
-import { IconSearch, IconFile } from "@douyinfe/semi-icons";
+import { IconSearch, IconFile, IconPlus, IconFolder, IconFolderOpen } from
"@douyinfe/semi-icons";
+import {Tooltip} from "@douyinfe/semi-ui";
import {useEffect, useState} from "react";
-import styles from "./catalog-tree.module.less"
import { useTranslation } from 'react-i18next';
+import { useDatabaseStore } from "@src/store/databaseStore.ts";
import { useCatalogStore } from "@src/store/catalogStore.ts";
+import {useTableStore} from "@src/store/tableStore.ts";
+import DatabaseModalForm from
"@pages/Metadata/components/LeftContent/components/DatabaseModalForm";
+import TableModalForm from
"@pages/Metadata/components/LeftContent/components/TableModalForm";
+import {TableItem, TableColumn} from "@src/types/Table/data";
+import CatalogIconMoreDropdown from
"@components/IconMoreDropdown/CatalogIconMoreDropdown";
+import DatabaseIconMoreDropdown from
"@components/IconMoreDropdown/DatabaseIconMoreDropdown";
+import TableIconMoreDropdown from
"@components/IconMoreDropdown/TableIconMoreDropdown";
+import styles from "./catalog-tree.module.less"
+
+type TreeDataItem = {
+ label: string;
+ value: string;
+ key: string;
+ type: "catalog" | "database" | "table";
+ catalogId: number;
+ parentId?: number;
+ children?: TreeDataItem[];
+};
const CatalogTree = () => {
const { t } = useTranslation()
+ const [hoveredNode, setHoveredNode] = useState(null);
+ const [showModal, setShowModal] = useState(false);
+ const [createType, setCreateType] =
+ useState<"catalog" | "database" | "table" | null>(null);
- type TreeDataItem = {
- label: string;
- value: string;
- key: string;
- children?: TreeDataItem[];
- };
+ // Database
+ const createDatabase = useDatabaseStore(state => state.createDatabase);
+ const fetchDatabases= useDatabaseStore(state => state.fetchDatabases);
+ const databaseItemList = useDatabaseStore(state => state.databaseItemList);
+ // Catalog
const [treeData, setTreeData] = useState<TreeDataItem[]>([]);
const fetchCatalogData = useCatalogStore(state => state.fetchCatalogData);
const catalogItemList = useCatalogStore(state => state.catalogItemList);
+ // Table
+ const { inputs, configs, setInputs, setConfigs} = useTableStore();
+ const createTable = useTableStore(state => state.createTable);
+ const fetchTables= useTableStore(state => state.fetchTables);
+ const tableItemList = useTableStore(state => state.tableItemList);
+
+ const [selectedCatalogName, setSelectedCatalogName] = useState<string |
null>(null);
+ const [selectedDatabaseName, setSelectedDatabaseName] = useState<string |
null>(null);
+
+ const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
+ const [selectedKey, setSelectedKey] = useState(null);
+
+ useEffect(() => {
+ // Fetch the catalog data when the component mounts
+ fetchTables();
+ }, [fetchTables]);
+
+ useEffect(() => {
+ // Fetch the catalog data when the component mounts
+ fetchDatabases();
+ }, [fetchDatabases]);
+
+ const handleMouseEnter = (key: any) => {
+ setHoveredNode(key);
+ };
+
+ const handleMouseLeave = () => {
+ setHoveredNode(null);
+ };
+
+ const handleOpenModal = (type: "catalog" | "database" | "table", name:
string, parentId?: number) => {
+ if (type === "catalog" || type === "database") {
+ if (type === "catalog") {
+ setSelectedCatalogName(name);
+ } else if (type === "database") {
+ const databaseItem =
+ databaseItemList.find(item => item.databaseName === name
&& item.catalogId === parentId);
+ if (databaseItem) {
+ const catalogItem = catalogItemList.find(item => item.id
=== databaseItem.catalogId);
+ if (catalogItem) {
+ setSelectedCatalogName(catalogItem.catalogName);
+ }
+ }
+ setSelectedDatabaseName(name);
+ }
+ setCreateType(type);
+ setShowModal(true);
+ }
+ };
+
+ const handleCloseDatabaseModal = () => {
+ setShowModal(false);
+ };
+
+ const handleCloseTableModal = () => {
+ setInputs([{}]);
+ setConfigs([]);
+ setShowModal(false);
+ };
+
useEffect(() => {
// Fetch the catalog data when the component mounts
fetchCatalogData();
@@ -46,42 +128,259 @@ const CatalogTree = () => {
const transformedData = catalogItemList.map(item => ({
label: item.catalogName,
value: item.catalogName,
+ type: "catalog" as "catalog",
+ catalogId: item.id,
key: item.id.toString(),
+ children: databaseItemList
+ .filter(dbItem => dbItem.catalogId === item.id)
+ .map(dbItem => {
+ const databaseKey = item.id.toString() + "-" +
dbItem.databaseName;
+ const tableChildren = tableItemList
+ .filter(tableItem => tableItem.catalogName ===
item.catalogName && tableItem.databaseName === dbItem.databaseName)
+ .map(tableItem => ({
+ label: tableItem.tableName,
+ value: tableItem.tableName,
+ type: "table" as "table",
+ catalogId: item.id,
+ key: databaseKey + "-" + tableItem.tableName
+ }));
+ return {
+ label: dbItem.databaseName,
+ value: dbItem.databaseName,
+ type: "database" as "database",
+ catalogId: item.id,
+ parentId: item.id,
+ key: databaseKey,
+ children: tableChildren
+ };
+ }),
}));
setTreeData(transformedData);
- }, [catalogItemList]);
+ }, [catalogItemList, databaseItemList, tableItemList]);
+
+ const handleCreateDatabaseOk = (formApi: any) => {
+ return new Promise<void>((resolve, reject) => {
+ formApi
+ .validate()
+ .then((values: any) => {
+ const formData = values;
+ const databaseProp: Prop.DatabaseProp = {
+ databaseName: formData.databaseName,
+ catalogId: formData.catalogId,
+ description: formData.description
+ };
+ createDatabase(databaseProp)
+ .then(() => {
+ fetchDatabases();
+ resolve();
+ })
+ .catch((error: any) => {
+ console.log(error);
+ reject(error);
+ });
+ })
+ .catch((errors: any) => {
+ console.log(errors);
+ reject(errors);
+ });
+ });
+ }
+
+ const handleCreateTableOk = (formApi: any) => {
+ return new Promise<void>((resolve, reject) => {
+ formApi
+ .validate()
+ .then(() => {
+ const values = formApi.getValues();
+ const catalogName = formApi.catalogName;
+ const databaseName = formApi.databaseName;
+
+ let tableColumns: TableColumn[] = [];
+ let tableOptions: Map<string, string> =new Map<string,
string>();
+ if (inputs.length > 0) {
+ inputs.map((_, index) => {
+ const field = values[`field${index}`];
+ const dataType = values[`type${index}`];
+ const comment = values[`comment${index}`];
+ const primaryKey = values[`primaryKey${index}`];
+ const defaultValue =
values[`defaultValue${index}`];
+ if (field !== undefined && dataType !== undefined)
{
+ const tableColumn: TableColumn = {
+ field: field,
+ dataType: dataType,
+ comment: comment === undefined ? null :
comment,
+ isPK: primaryKey !== undefined,
+ defaultValue: defaultValue === undefined ?
null : defaultValue,
+ }
+ tableColumns.push(tableColumn);
+ }
+ });
+ }
+
+ if (configs.length > 0) {
+ configs.map((_, index) => {
+ const key = values[`configKey${index}`];
+ const value = values[`configValue${index}`];
+ tableOptions.set(key, value);
+ });
+ }
+
+ const tableProp: TableItem = {
+ catalogName: catalogName,
+ databaseName: databaseName,
+ tableName: values.tableName,
+ description: values.description === undefined ? null :
values.description,
+ tableColumns: tableColumns,
+ partitionKey: values.partitionKey === undefined ? [] :
values.partitionKey,
+ tableOptions: tableOptions,
+ }
+ createTable(tableProp)
+ .then(() => {
+ fetchTables();
+ setInputs([{}]);
+ setConfigs([]);
+ resolve();
+ })
+ .catch((error: any) => {
+ console.log(error);
+ setInputs([{}]);
+ setConfigs([]);
+ reject(error);
+ });
+ console.log(tableProp)
+ })
+ .catch((errors: any) => {
+ console.log(errors);
+ setInputs([{}]);
+ setConfigs([]);
+ reject(errors);
+ });
+ });
+ }
+
+ const findNodeByKey = (key: string, nodes?: TreeDataItem[]): TreeDataItem
| null => {
+ if (!nodes) {
+ return null;
+ }
+
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].key === key) {
+ return nodes[i];
+ } else if (nodes[i].children) {
+ const foundNode = findNodeByKey(key, nodes[i].children);
+ if (foundNode) {
+ return foundNode;
+ }
+ }
+ }
+ return null;
+ };
+
+ const onExpand = (_: string[], info: any) => {
+ const key = info.node.key;
+ const expanded = info.expanded;
+
+ if (expanded) {
+ setExpandedKeys(prevKeys => [...prevKeys, key]);
+ } else {
+ const nodeToCollapse = findNodeByKey(key, treeData);
+ let keysToCollapse = [key];
+ if (nodeToCollapse && nodeToCollapse.children) {
+ keysToCollapse = [...keysToCollapse,
...nodeToCollapse.children.map(child => child.key)];
+ }
+ setExpandedKeys(prevKeys => prevKeys.filter(k =>
!keysToCollapse.includes(k)));
+ }
+ };
const renderLabel = (x: any) => {
- const className = x.className;
- const onExpand = x.onExpand;
- const onClick = x.onClick;
- const data = x.data;
- const expandIcon = x.expandIcon;
- const { label } = data;
+ const { className, onExpand, onClick, data, expandIcon } = x;
+ const { label, key, type } = data;
const isLeaf = !(data.children && data.children.length);
+ const hasChildren = data.children && data.children.length > 0;
+ const isExpanded = expandedKeys.includes(key);
+ const isSelected = selectedKey === key;
+
+ let icon;
+ if (type === 'table') {
+ icon = <IconFile size={"small"} className={styles.icon}/>;
+ } else if (hasChildren) {
+ icon = isExpanded ? <IconFolderOpen size={"small"}
className={styles.icon}/>
+ : <IconFolder size={"small"} className={styles.icon}/>
+ } else {
+ icon = <IconFolder size={"small"} className={styles.icon}/>;
+ }
+
+ let iconMore;
+ if (type === 'catalog') {
+ iconMore = <CatalogIconMoreDropdown/>;
+ } else if (type === 'database') {
+ iconMore = <DatabaseIconMoreDropdown/>;
+ } else {
+ iconMore = <TableIconMoreDropdown/>;
+ }
+
return (
<li
- className={className}
+ className={`${className} ${styles.liClass} ${isSelected ?
styles['semi-tree-node-selected'] : ''}`}
role="treeitem"
- onClick={isLeaf ? onClick : onExpand}
+ onClick={(e) => {
+ if (isLeaf) {
+ onClick(e);
+ } else {
+ onExpand(e);
+ }
+ setSelectedKey(key);
+ }}
+ onMouseEnter={() => handleMouseEnter(key)}
+ onMouseLeave={handleMouseLeave}
+ tabIndex={0}
>
- {isLeaf ? <IconFile style={{marginRight: "8px", color:
"var(--semi-color-text-2)"}}/> : expandIcon}
- <span>{label}</span>
+ {expandIcon}
+ {icon}
+ <div style={{ flex: 1 }}>{label}</div>
+ { key === hoveredNode && (type === "catalog" || type ===
"database") && (
+ <Tooltip content={type === "catalog" ?
t('metadata.add-database') : t('metadata.add-table')}>
+ <IconPlus
+ className={styles.iconPlus}
+ onClick={(e) => {
+ e.stopPropagation();
+ handleOpenModal(type, label, data.parentId)
+ }}
+ />
+ </Tooltip>
+ )}
+ <div className={styles.iconMore}>{iconMore}</div>
</li>
);
};
return(
- <Tree
- filterTreeNode
- treeData={treeData}
- searchPlaceholder={t('common.filter')}
- searchRender={({ prefix, ...restProps }) => (
- <Input suffix={<IconSearch
className={styles['catalog-tree-input-icon']}/>} {...restProps}
className={styles['catalog-tree-input']}></Input>
+ <>
+ <Tree
+ filterTreeNode
+ treeData={treeData}
+ expandedKeys={expandedKeys}
+ onExpand={onExpand}
+ selectedKey={selectedKey}
+ searchPlaceholder={t('common.filter')}
+ searchRender={({ prefix, ...restProps }) => (
+ <Input suffix={<IconSearch
className={styles['catalog-tree-input-icon']}/>} {...restProps}
className={styles['catalog-tree-input']}></Input>
+ )}
+ renderFullLabel={renderLabel}
+ />
+ {showModal && createType === "catalog" && (
+ <DatabaseModalForm visible={showModal}
onClose={handleCloseDatabaseModal} onOk={handleCreateDatabaseOk}/>
+ )}
+ {showModal && createType === "database" && (
+ <TableModalForm
+ visible={showModal}
+ onClose={handleCloseTableModal}
+ onOk={handleCreateTableOk}
+ catalogName={selectedCatalogName}
+ databaseName={selectedDatabaseName}/>
)}
- renderFullLabel={renderLabel}
- />
- )
+ </>
+ );
}
export default CatalogTree;
diff --git
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/DatabaseModalForm/DatabaseForm/index.tsx
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/DatabaseModalForm/DatabaseForm/index.tsx
new file mode 100644
index 0000000..f1f200a
--- /dev/null
+++
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/DatabaseModalForm/DatabaseForm/index.tsx
@@ -0,0 +1,74 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License. */
+
+import { Form } from '@douyinfe/semi-ui';
+import {useTranslation} from "react-i18next";
+import {useCatalogStore} from "@src/store/catalogStore.ts";
+
+// @ts-ignore
+const DatabaseForm = ({ getFormApi }) => {
+ const { t } = useTranslation();
+ const catalogItemList = useCatalogStore(state => state.catalogItemList);
+
+ return(
+ <>
+ <Form
+ getFormApi={getFormApi}
+ >
+ {
+ ({}) => (
+ <>
+ <Form.Input
+ field="databaseName"
+ label={t('metadata.database-name')}
+ trigger='blur'
+ rules={[
+ { required: true, message:
t('metadata.message') },
+ ]}
+ style={{ width: "100%" }}
+ showClear/>
+
+ <Form.Select
+ field="catalogId"
+ label='Catalog'
+ rules={[
+ { required: true, message:
t('metadata.message') },
+ ]}
+ style={{ width: "100%" }}
+ showClear>
+ {catalogItemList.map(item => (
+ <Form.Select.Option key={item.id}
value={item.id}>
+ {item.catalogName}
+ </Form.Select.Option>
+ ))}
+ </Form.Select>
+
+ <Form.TextArea
+ field="description"
+ label={t('metadata.description')}
+ style={{ width: "100%" }}
+
placeholder={t('metadata.text-area-description')}
+ showClear/>
+ </>
+ )
+ }
+ </Form>
+ </>
+ );
+}
+
+export default DatabaseForm;
diff --git
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/index.tsx
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/DatabaseModalForm/index.tsx
similarity index 69%
copy from
paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/index.tsx
copy to
paimon-web-ui/src/pages/Metadata/components/LeftContent/components/DatabaseModalForm/index.tsx
index 05780b1..d5722a3 100644
---
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/index.tsx
+++
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/DatabaseModalForm/index.tsx
@@ -17,17 +17,18 @@ under the License. */
import React, {useState} from "react";
import { Modal } from '@douyinfe/semi-ui';
-import CatalogForm from
"@pages/Metadata/components/LeftContent/components/CatalogModalForm/CatalogForm";
+import DatabaseForm from
"@pages/Metadata/components/LeftContent/components/DatabaseModalForm/DatabaseForm";
+import {useTranslation} from "react-i18next";
-type CatalogModalFormProps = {
+type DatabaseModalFormProps = {
visible: boolean;
onClose: () => void;
onOk: (formApi: any) => void;
};
-
-const CatalogModalForm: React.FC<CatalogModalFormProps> = ({ visible ,
onClose, onOk }) => {
+const DatabaseModalForm: React.FC<DatabaseModalFormProps> = ({ visible ,
onClose, onOk }) => {
const [formApi, setFormApi] = useState(null);
+ const { t } = useTranslation();
const getFormApi = (api: any) => {
setFormApi(api);
@@ -40,19 +41,20 @@ const CatalogModalForm: React.FC<CatalogModalFormProps> =
({ visible , onClose,
return(
<Modal
- title="Create Catalog"
+ title = {t('metadata.create-database')}
visible = {visible}
onCancel= {onClose}
maskClosable={false}
- okText={'Submit'}
- cancelText={'Cancel'}
- width={'750px'}
- height={'550px'}
+ okText={t('metadata.submit')}
+ cancelText={t('metadata.cancel')}
+ width={'650px'}
+ height={'490px'}
onOk={() => handleOkClick()}
>
- <CatalogForm getFormApi={getFormApi}/>
+ <DatabaseForm getFormApi={getFormApi}/>
</Modal>
);
}
-export default CatalogModalForm;
\ No newline at end of file
+export default DatabaseModalForm;
+
diff --git
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/TableModalForm/TableForm/index.tsx
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/TableModalForm/TableForm/index.tsx
new file mode 100644
index 0000000..3c7ef8e
--- /dev/null
+++
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/TableModalForm/TableForm/index.tsx
@@ -0,0 +1,244 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License. */
+
+import {Button, Form} from '@douyinfe/semi-ui';
+import {useTranslation} from "react-i18next";
+import {IconPlus, IconMinus} from "@douyinfe/semi-icons";
+import {useRef, useState} from "react";
+import {useTableStore} from "@src/store/tableStore.ts";
+
+// @ts-ignore
+const TableForm = ({ getFormApi }) => {
+ const formApiRef = useRef<any>(null);
+ const { t } = useTranslation();
+ const { inputs, configs, setInputs, setConfigs } = useTableStore();
+ const [fieldOptions, setFieldOptions] = useState<string[]>([]);
+
+ const handleAddInput = () => {
+ const newInputs = inputs.concat({});
+ setInputs(newInputs);
+ }
+
+ const handleRemoveInput = (index: any) => {
+ if (formApiRef.current) {
+ const newInputs = [...inputs];
+ formApiRef.current.setValue(`field${index}`, null);
+ formApiRef.current.setValue(`comment${index}`, null);
+ formApiRef.current.setValue(`type${index}`, null);
+ newInputs.splice(index, 1);
+ setInputs(newInputs);
+ const newFieldOptions = newInputs.map((_, index) =>
formApiRef.current.getValue(`field${index}`));
+ setFieldOptions(newFieldOptions);
+ }
+ }
+
+ const updateFieldOptions = (index: any) => {
+ if (formApiRef.current) {
+ const fieldValue = formApiRef.current.getValue(`field${index}`);
+ if (fieldValue !== undefined) {
+ const newFieldOptions = [...fieldOptions];
+ newFieldOptions[index] = fieldValue;
+ setFieldOptions(newFieldOptions);
+ }
+ }
+ };
+
+ const handleAddConfig = () => {
+ setConfigs(configs.concat({}));
+ }
+
+ const handleRemoveConfig = (index: any) => {
+ if (formApiRef.current) {
+ const newConfigs = [...configs];
+ formApiRef.current.setValue(`configKey${index}`, null);
+ formApiRef.current.setValue(`configValue${index}`, null);
+ newConfigs.splice(index, 1);
+ setConfigs(newConfigs);
+ }
+ }
+
+ const options = [
+ { value: 'INT', label: 'INT' },
+ { value: 'TINYINT', label: 'TINYINT' },
+ { value: 'SMALLINT', label: 'SMALLINT' },
+ { value: 'BIGINT', label: 'BIGINT' },
+ { value: 'STRING', label: 'STRING' },
+ { value: 'DOUBLE', label: 'DOUBLE' },
+ { value: 'BOOLEAN', label: 'BOOLEAN' },
+ { value: 'DATE', label: 'DATE' },
+ { value: 'TIME', label: 'TIME' },
+ { value: 'TIMESTAMP', label: 'TIMESTAMP' },
+ { value: 'BYTES', label: 'BYTES' },
+ { value: 'FLOAT', label: 'FLOAT' },
+ { value: 'DECIMAL', label: 'DECIMAL' },
+ ];
+
+ return(
+ <>
+ <Form
+ getFormApi={api => {
+ formApiRef.current = api;
+ if (getFormApi) {
+ getFormApi(api);
+ }
+ }}
+ >
+ {
+ ({}) => (
+ <>
+ <Form.Section
text={t('metadata.form-group-basic-information')}>
+ <Form.Input
+ field="tableName"
+ label={t('metadata.table-name')}
+ trigger='blur'
+ rules={[
+ { required: true, message:
t('metadata.message') },
+ ]}
+ style={{ width: "100%" }}
+ showClear/>
+ <Form.TextArea
+ field="description"
+ label={t('metadata.description')}
+ style={{ width: "100%" }}
+
placeholder={t('metadata.text-area-description')}
+ autosize rows={2}
+ showClear/>
+ </Form.Section>
+
+ <Form.Section
+ text={
+ <div
+ style={{ display: 'flex',
justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer',
+ color: inputs.length > 0 ? 'black'
: 'lightgray',}}
+ onClick={handleAddInput}
+ >
+
<span>{t('metadata.form-group-add-column-ordinary')}</span>
+ <IconPlus />
+ </div>
+ }
+ >
+ {inputs.map((_, index) => (
+ <div style={{ display: 'flex', alignItems:
'center', marginBottom: '10px'}} key={index}>
+ <Form.Input
+ noLabel={true}
+ field={`field${index}`}
+
placeholder={t('metadata.add-column-field')}
+ style={{ width: "160px" }}
+ onBlur={() =>
updateFieldOptions(index)}
+ showClear />
+ <Form.Select
+ noLabel={true}
+ field={`type${index}`}
+
placeholder={t('metadata.add-column-type')}
+ style={{ width: "130px",
marginLeft: '10px' }}
+ allowCreate={true}
+ filter={true}
+ showClear>
+ {options.map((option, i) => (
+ <Form.Select.Option key={i}
value={option.value}>
+ {option.label}
+ </Form.Select.Option>
+ ))}
+ </Form.Select>
+ <Form.Input
+ noLabel={true}
+ field={`comment${index}`}
+
placeholder={t('metadata.add-column-comment')}
+ style={{ width: "160px",
marginLeft: '10px' }}
+ showClear />
+ <Form.Checkbox
+ field={`primaryKey${index}`}
+ noLabel={true}
+ style={{marginLeft: '10px'}}>
+
{t('metadata.add-column-primary-key')}
+ </Form.Checkbox>
+ <Form.Input
+ noLabel={true}
+ field={`defaultValue${index}`}
+
placeholder={t('metadata.add-column-default-value')}
+ style={{ width: "126px",
marginLeft: '10px'}}
+ showClear />
+ <Button
+ onClick={() =>
handleRemoveInput(index)}
+ icon={<IconMinus />}
+ style={{ marginLeft: '10px',
borderRadius: '50%' }}
+ />
+ </div>
+ ))}
+ </Form.Section>
+
+ <Form.Section
text={t('metadata.form-group-add-column-partition')}>
+ <Form.Select
+ noLabel={true}
+ field="partitionKey"
+ style={{ width: "100%" }}
+ filter={true}
+ showClear
+ multiple
+ >
+ {fieldOptions.map((field, index) => (
+ <Form.Select.Option key={index}
value={field}>{field}</Form.Select.Option>
+ ))}
+ </Form.Select>
+ </Form.Section>
+
+ <Form.Section
+ text={
+ <div
+ style={{
+ display: 'flex', justifyContent:
'space-between', alignItems: 'center',
+ cursor: 'pointer',
+ color: configs.length > 0 ?
'black' : 'lightgray',
+ }}
+ onClick={handleAddConfig}
+ >
+
<span>{t('metadata.form-group-add-config')}</span>
+ <IconPlus />
+ </div>
+ }
+ >
+ {configs.map((_, index) => (
+ <div style={{ display: 'flex', alignItems:
'center', marginBottom: '10px' }} key={index}>
+ <Form.Input
+ noLabel={true}
+ field={`configKey${index}`}
+
placeholder={t('metadata.add-config-key')}
+ style={{ width: "329px"}}
+ showClear />
+ <Form.Input
+ noLabel={true}
+ field={`configValue${index}`}
+
placeholder={t('metadata.add-config-value')}
+ style={{ width: "329px",
marginLeft: '10px' }}
+ showClear />
+ <Button
+ onClick={() =>
handleRemoveConfig(index)}
+ icon={<IconMinus />}
+ style={{ marginLeft: '10px',
borderRadius: '50%'}}
+ />
+ </div>
+ ))}
+ </Form.Section>
+ </>
+ )
+ }
+ </Form>
+ </>
+ );
+}
+
+export default TableForm;
\ No newline at end of file
diff --git
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/index.tsx
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/TableModalForm/index.tsx
similarity index 61%
copy from
paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/index.tsx
copy to
paimon-web-ui/src/pages/Metadata/components/LeftContent/components/TableModalForm/index.tsx
index 05780b1..53938c5 100644
---
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogModalForm/index.tsx
+++
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/TableModalForm/index.tsx
@@ -17,20 +17,29 @@ under the License. */
import React, {useState} from "react";
import { Modal } from '@douyinfe/semi-ui';
-import CatalogForm from
"@pages/Metadata/components/LeftContent/components/CatalogModalForm/CatalogForm";
+import {useTranslation} from "react-i18next";
+import {auto} from "@popperjs/core";
+import TableForm from
"@pages/Metadata/components/LeftContent/components/TableModalForm/TableForm";
-type CatalogModalFormProps = {
+type TableModalFormProps = {
visible: boolean;
onClose: () => void;
onOk: (formApi: any) => void;
+ catalogName: string | null;
+ databaseName: string | null;
};
-
-const CatalogModalForm: React.FC<CatalogModalFormProps> = ({ visible ,
onClose, onOk }) => {
+const TableModalForm: React.FC<TableModalFormProps> = ({ visible , onClose,
onOk, catalogName, databaseName }) => {
const [formApi, setFormApi] = useState(null);
+ const { t } = useTranslation();
const getFormApi = (api: any) => {
- setFormApi(api);
+ const formData = {
+ ... api,
+ catalogName,
+ databaseName,
+ }
+ setFormApi(formData);
};
const handleOkClick = async () => {
@@ -40,19 +49,19 @@ const CatalogModalForm: React.FC<CatalogModalFormProps> =
({ visible , onClose,
return(
<Modal
- title="Create Catalog"
+ title = {t('metadata.create-table')}
visible = {visible}
onCancel= {onClose}
maskClosable={false}
- okText={'Submit'}
- cancelText={'Cancel'}
- width={'750px'}
- height={'550px'}
+ okText={t('metadata.submit')}
+ cancelText={t('metadata.cancel')}
+ width={'760px'}
+ height={auto}
onOk={() => handleOkClick()}
>
- <CatalogForm getFormApi={getFormApi}/>
+ <TableForm getFormApi={getFormApi}/>
</Modal>
);
}
-export default CatalogModalForm;
\ No newline at end of file
+export default TableModalForm;
\ No newline at end of file
diff --git a/paimon-web-ui/src/pages/Metadata/components/LeftContent/index.tsx
b/paimon-web-ui/src/pages/Metadata/components/LeftContent/index.tsx
index 1bada6f..c7979da 100644
--- a/paimon-web-ui/src/pages/Metadata/components/LeftContent/index.tsx
+++ b/paimon-web-ui/src/pages/Metadata/components/LeftContent/index.tsx
@@ -22,6 +22,7 @@ import CatalogModalForm from
"@pages/Metadata/components/LeftContent/components/
import {useCatalogStore} from "@src/store/catalogStore.ts";
import styles from "./left-content.module.less";
import { useTranslation } from 'react-i18next';
+import {useDatabaseStore} from "@src/store/databaseStore.ts";
const MetadataSidebar = () => {
@@ -30,6 +31,7 @@ const MetadataSidebar = () => {
const createFilesystemCatalog = useCatalogStore(state =>
state.createFileSystemCatalog);
const createHiveCatalog = useCatalogStore(state =>
state.createHiveCatalog);
const fetchCatalogData = useCatalogStore(state => state.fetchCatalogData);
+ const fetchAllDatabases = useDatabaseStore(state => state.fetchDatabases);
const handleOpenModal = () => {
setShowModal(true);
@@ -60,12 +62,14 @@ const MetadataSidebar = () => {
createFilesystemCatalog(catalogProp)
.then(() => {
fetchCatalogData();
+ fetchAllDatabases();
resolve();
})
} else {
createHiveCatalog(catalogProp)
.then(() => {
fetchCatalogData();
+ fetchAllDatabases();
resolve();
})
}
diff --git a/paimon-web-ui/src/router/index.tsx
b/paimon-web-ui/src/router/index.tsx
index 3a9af47..8285513 100644
--- a/paimon-web-ui/src/router/index.tsx
+++ b/paimon-web-ui/src/router/index.tsx
@@ -16,7 +16,7 @@ specific language governing permissions and limitations
under the License. */
/*import { lazy } from 'react'*/
-import { RouteObject } from 'react-router';
+import {Navigate, RouteObject} from 'react-router';
import { useRoutes } from 'react-router-dom';
import LayoutPage from '@src/pages/Layout';
import PlaygroundPage from '@src/pages/Playground';
@@ -36,6 +36,10 @@ const routeList: RouteObject[] = [
path: '/layout',
element: <LayoutPage/>,
children: [
+ {
+ path: '/',
+ element: <Navigate to="playground" />
+ },
{
path: 'playground',
element: <PlaygroundPage/>
diff --git a/paimon-web-ui/src/store/databaseStore.ts
b/paimon-web-ui/src/store/databaseStore.ts
new file mode 100644
index 0000000..b597b89
--- /dev/null
+++ b/paimon-web-ui/src/store/databaseStore.ts
@@ -0,0 +1,66 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License. */
+
+import {create} from 'zustand';
+import Api from "@api/api.ts";
+import {Toast} from "@douyinfe/semi-ui";
+import {DatabaseItem} from "@src/types/Database/data";
+import i18n from 'i18next';
+
+type Store = {
+ databaseItemList: DatabaseItem[];
+ createDatabase: (databaseProp: Prop.DatabaseProp) => Promise<void>;
+ fetchDatabases: () => Promise<void>;
+};
+
+export const useDatabaseStore = create<Store>((set) => ({
+ databaseItemList: [],
+ createDatabase: async (databaseProp) => {
+ try {
+ const response = await Api.createDatabase(databaseProp);
+ if (!response) {
+ throw new Error('No response from createDatabase');
+ }
+ if (response.code === 200) {
+ Toast.success(i18n.t('metadata.create-database-success'));
+ } else {
+ console.error('Failed to create database:', response.msg);
+ Toast.error(i18n.t('metadata.create-database-failed') +
response.msg);
+ }
+ } catch (error) {
+ console.error('Failed to create database:', error);
+ Toast.error(i18n.t('metadata.create-database-failed') + error);
+ }
+ },
+ fetchDatabases: async () => {
+ try {
+ const result = await Api.getAllDatabases();
+ if (result && result.data) {
+ const newDatabaseItemList = result.data.map((item) => {
+ return {
+ databaseName: item.databaseName,
+ catalogId: item.catalogId,
+ description: item.description,
+ };
+ });
+ set((state) => ({ ...state, databaseItemList:
newDatabaseItemList }));
+ }
+ } catch (error) {
+ console.error('Failed to get databases:', error);
+ }
+ },
+}));
\ No newline at end of file
diff --git a/paimon-web-ui/src/store/tableStore.ts
b/paimon-web-ui/src/store/tableStore.ts
new file mode 100644
index 0000000..1c07932
--- /dev/null
+++ b/paimon-web-ui/src/store/tableStore.ts
@@ -0,0 +1,79 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License. */
+
+import {create} from 'zustand';
+import Api from "@api/api.ts";
+import {Toast} from "@douyinfe/semi-ui";
+import i18n from 'i18next';
+import {TableItem} from "@src/types/Table/data";
+
+type Store = {
+ inputs: Array<{}>;
+ configs: Array<{}>;
+ tableItemList: TableItem[];
+ setInputs: (newInputs: Array<{}>) => void;
+ setConfigs: (newConfigs: Array<{}>) => void;
+ createTable: (tableProp: TableItem) => Promise<void>;
+ fetchTables: () => Promise<void>;
+};
+
+export const useTableStore = create<Store>((set) => ({
+ inputs: [{}],
+ configs: [],
+ tableItemList: [],
+ setInputs: (newInputs) => set(() => ({ inputs: newInputs })),
+ setConfigs: (newConfigs) => set(() => ({ configs: newConfigs })),
+ createTable: async (tableProp) => {
+ try {
+ const response = await Api.createTable(tableProp);
+ if (!response) {
+ throw new Error('No response from createTable');
+ }
+ if (response.code === 200) {
+ Toast.success(i18n.t('metadata.create-table-success'));
+ } else {
+ console.error('Failed to create table:', response.msg);
+ Toast.error(i18n.t('metadata.create-table-failed') +
response.msg);
+ }
+ } catch (error) {
+ console.error('Failed to create table:', error);
+ Toast.error(i18n.t('metadata.create-table-failed') + error);
+ }
+ },
+ fetchTables: async () => {
+ try {
+ const result = await Api.getAllTables();
+ if (result && result.data) {
+ const newTableItemList = result.data.map((item) => {
+ return {
+ catalogName: item.catalogName,
+ databaseName: item.databaseName,
+ tableName: item.tableName,
+ description: item.description,
+ tableColumns: item.tableColumns,
+ partitionKey: item.partitionKey,
+ tableOptions: item.tableOptions,
+ };
+ });
+ set((state) => ({ ...state, tableItemList: newTableItemList
}));
+ }
+ } catch (error) {
+ console.error('Failed to get tables:', error);
+ }
+ },
+}));
+
diff --git
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/catalog-tree.module.less
b/paimon-web-ui/src/types/Database/data.d.ts
similarity index 87%
copy from
paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/catalog-tree.module.less
copy to paimon-web-ui/src/types/Database/data.d.ts
index 4f3f847..792be79 100644
---
a/paimon-web-ui/src/pages/Metadata/components/LeftContent/components/CatalogTree/catalog-tree.module.less
+++ b/paimon-web-ui/src/types/Database/data.d.ts
@@ -15,8 +15,8 @@ KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License. */
-.catalog-tree-input {
- //background-color: var(--semi-color-bg-0);
- width: 347px;
- margin-left: -12px;
+export type DatabaseItem = {
+ databaseName: string,
+ catalogId: number,
+ description: string,
}
\ No newline at end of file
diff --git a/paimon-web-ui/src/types/Public/data.d.ts
b/paimon-web-ui/src/types/Public/data.d.ts
index accfa53..cb0a630 100644
--- a/paimon-web-ui/src/types/Public/data.d.ts
+++ b/paimon-web-ui/src/types/Public/data.d.ts
@@ -33,9 +33,6 @@ export interface PageData<T> {
// with page
export type PageResult<T> = Result<PageData<T>>
-
-
-
export interface BaseBeanColumns extends ExcludeNameAndEnableColumns {
name: string,
enabled: boolean,
diff --git a/paimon-web-ui/src/api/endpoints.ts
b/paimon-web-ui/src/types/Table/data.d.ts
similarity index 66%
copy from paimon-web-ui/src/api/endpoints.ts
copy to paimon-web-ui/src/types/Table/data.d.ts
index 1567581..80c7896 100644
--- a/paimon-web-ui/src/api/endpoints.ts
+++ b/paimon-web-ui/src/types/Table/data.d.ts
@@ -15,15 +15,20 @@ KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License. */
-export const API_ENDPOINTS = {
+export type TableColumn = {
+ field: string,
+ dataType: string,
+ comment: string | null,
+ isPK: boolean,
+ defaultValue: string | null,
+}
- // auth && login
- GET_LDAP_ENABLE: '/ldap/enable',
- LOGIN: '/login',
-
- // catalog
- CREATE_FILE_SYSTEM_CATALOG: '/catalog/createFilesystemCatalog',
- CREATE_HIVE_CATALOG: '/catalog/createHiveCatalog',
- GET_ALL_CATALOGS: '/catalog/getAllCatalogs',
-
-};
\ No newline at end of file
+export type TableItem = {
+ catalogName: string,
+ databaseName: string,
+ tableName: string,
+ description: string | null,
+ tableColumns: TableColumn[],
+ partitionKey: [],
+ tableOptions: Map<string, string>,
+}
\ No newline at end of file