From eff96693d78ee6eb4361351a753ae4f7dbbd185f Mon Sep 17 00:00:00 2001 From: akageun <13219787+akageun@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:20:34 +0900 Subject: [PATCH] 0.2.0 (#111) * Simplify build gradle version (#108) * Simplify build gradle version * Simplify build gradle version - readme * ColumnList Sorting (#110) - js -> java * Apply Table Import (#116) * Table import * Query Editor Column List (#112) * Query Editor Column List - apply sorting(with partition, clustering key) * modify js file --- README.md | 8 +- cassdio-core/build.gradle | 3 +- .../cassdio/common/utils/CsvHelper.java | 10 ++ .../keyspace/table/ClusterCsvProvider.java | 69 +++++++++ .../table/ClusterTableCsvProvider.java | 36 +++++ .../table/ClusterTableRowCommander.java | 31 ++++ .../cluster/keyspace/table/TableDTO.java | 30 ++++ .../column/ClusterTableColumnCommander.java | 40 ++++- .../cluster/keyspace/table/column/Column.java | 49 ------- .../table/column/ColumnClusteringOrder.java | 30 ---- cassdio-web/build.gradle | 2 + .../keyspace/table/ClusterTableApi.java | 26 +--- .../keyspace/table/ClusterTableRequest.java | 28 ++++ .../keyspace/table/ClusterTableRowApi.java | 128 ++++++++++++++++ .../route/cluster/query/ClusterQueryApi.java | 25 +++- .../cluster/modal/table-detail-modal.js | 7 +- .../cluster/modal/table-import-modal.js | 137 +++++++++++++++++- .../cluster/modal/table-row-detail-modal.js | 4 +- .../src/components/cluster/query-editor.js | 3 +- .../src/components/cluster/query-result.js | 28 ++-- .../layout/cassdio-default-layout.js | 12 +- .../src/main/webapp/src/hooks/useTable.js | 7 +- .../src/pages/cluster/cluster-query-page.js | 4 +- .../src/pages/cluster/cluster-table-page.js | 50 ++++--- .../src/remotes/clusterTableRowImportApi.js | 18 +++ .../src/main/webapp/src/utils/cassdioUtils.js | 85 ----------- 26 files changed, 614 insertions(+), 256 deletions(-) create mode 100644 cassdio-core/src/main/java/kr/hakdang/cassdio/common/utils/CsvHelper.java create mode 100644 cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterCsvProvider.java create mode 100644 cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterTableCsvProvider.java delete mode 100644 cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/Column.java delete mode 100644 cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/ColumnClusteringOrder.java create mode 100644 cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableRequest.java create mode 100644 cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableRowApi.java create mode 100644 cassdio-web/src/main/webapp/src/remotes/clusterTableRowImportApi.js diff --git a/README.md b/README.md index feb7b22a..12d70708 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Cassdio is centralized web management system for managing [Apache Cassandra](https://cassandra.apache.org/_/index.html)! -Cassdio provide powerful tools to efficiently manage and monitor Apache Cassandra databases. You can monitor the +Cassdio provides powerful tools to efficiently manage and monitor Apache Cassandra databases. You can monitor the real-time status of your database clusters and perform various tasks intuitively through a user-friendly interface. This management system helps simplify system operations and enhance stability. It's an essential tool for managing @@ -45,9 +45,9 @@ Apache Cassandra databases, offering excellent functionality and user convenienc ### Installation ``` -wget https://github.com/hakdang/cassdio/releases/download/v0.1.0/cassdio-0.1.0.jar +https://github.com/hakdang/cassdio/releases/latest/download/cassdio.jar -java -jar ./cassdio-0.1.0.jar +java -jar ./cassdio.jar ``` ### Browser @@ -72,5 +72,5 @@ java -jar ./cassdio-0.1.0.jar ## License -Cassdio is Open Source software released under +Cassdio is open-source software released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). diff --git a/cassdio-core/build.gradle b/cassdio-core/build.gradle index d74798f6..1880952f 100644 --- a/cassdio-core/build.gradle +++ b/cassdio-core/build.gradle @@ -12,11 +12,12 @@ dependencies { // Json implementation("org.springframework.boot:spring-boot-starter-json") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1") - + implementation("org.apache.commons:commons-csv:1.9.0") // CommonsLang3 api("org.apache.commons:commons-lang3:3.13.0") api("com.google.guava:guava:33.0.0-jre") api("org.apache.commons:commons-collections4:4.4") + api("org.apache.commons:commons-csv:1.9.0") // Cache implementation("org.springframework.boot:spring-boot-starter-cache") diff --git a/cassdio-core/src/main/java/kr/hakdang/cassdio/common/utils/CsvHelper.java b/cassdio-core/src/main/java/kr/hakdang/cassdio/common/utils/CsvHelper.java new file mode 100644 index 00000000..6348a6f8 --- /dev/null +++ b/cassdio-core/src/main/java/kr/hakdang/cassdio/common/utils/CsvHelper.java @@ -0,0 +1,10 @@ +package kr.hakdang.cassdio.common.utils; + +/** + * CsvHelper + * + * @author akageun + * @since 2024-08-19 + */ +public class CsvHelper { +} diff --git a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterCsvProvider.java b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterCsvProvider.java new file mode 100644 index 00000000..a33513b6 --- /dev/null +++ b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterCsvProvider.java @@ -0,0 +1,69 @@ +package kr.hakdang.cassdio.core.domain.cluster.keyspace.table; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.CSVRecord; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * ClusterTableCsvProvider + * + * @author akageun + * @since 2024-08-08 + */ +@Slf4j +@Service +public class ClusterCsvProvider { + + public void importerCsvSampleDownload(Writer writer, List headerList) { + CSVFormat csvFormat = CSVFormat.DEFAULT.builder() + .setHeader(headerList.toArray(String[]::new)) + .build(); + + try (final CSVPrinter printer = new CSVPrinter(writer, csvFormat)) { + log.info("create complete importer csv sample"); + + printer.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + + public List> importCsvReader(Reader reader, List columnList) throws IOException { + CSVFormat csvFormat = CSVFormat.DEFAULT.builder() + .setHeader(columnList.toArray(String[]::new)) + .setSkipHeaderRecord(true) + .setTrim(true) + .build(); + + Iterable records = csvFormat.parse(reader); + //Validation 방식에 대해 고민 필요 + + List> values = new ArrayList<>(); + + for (CSVRecord record : records) { + + Map map = new HashMap<>(); + + for (String column : columnList) { + map.put(column, StringUtils.defaultIfBlank(record.get(column), "")); + } + + values.add(map); + } + + return values; + } + +} diff --git a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterTableCsvProvider.java b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterTableCsvProvider.java new file mode 100644 index 00000000..7b66fd4b --- /dev/null +++ b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterTableCsvProvider.java @@ -0,0 +1,36 @@ +package kr.hakdang.cassdio.core.domain.cluster.keyspace.table; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.Writer; +import java.util.List; + +/** + * ClusterTableCsvProvider + * + * @author akageun + * @since 2024-08-08 + */ +@Slf4j +@Service +public class ClusterTableCsvProvider { + + public void importerCsvSampleDownload(Writer writer, List sortedColumnList) { + CSVFormat csvFormat = CSVFormat.DEFAULT.builder() + .setHeader(sortedColumnList.toArray(String[]::new)) + .build(); + + try (final CSVPrinter printer = new CSVPrinter(writer, csvFormat)) { + //printer.printRecord(author, title); + log.info("create complete importer csv sample"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + +} diff --git a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterTableRowCommander.java b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterTableRowCommander.java index f719e816..d2e996f2 100644 --- a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterTableRowCommander.java +++ b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/ClusterTableRowCommander.java @@ -1,18 +1,27 @@ package kr.hakdang.cassdio.core.domain.cluster.keyspace.table; import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.DefaultBatchType; import com.datastax.oss.driver.api.core.cql.ResultSet; import com.datastax.oss.driver.api.core.cql.SimpleStatement; import com.datastax.oss.driver.api.querybuilder.QueryBuilder; +import com.datastax.oss.driver.api.querybuilder.insert.InsertInto; +import com.datastax.oss.driver.api.querybuilder.insert.JsonInsert; import com.datastax.oss.protocol.internal.util.Bytes; +import com.google.common.collect.Lists; +import kr.hakdang.cassdio.common.utils.Jsons; import kr.hakdang.cassdio.core.domain.cluster.BaseClusterCommander; import kr.hakdang.cassdio.core.domain.cluster.CqlSessionSelectResults; import kr.hakdang.cassdio.core.domain.cluster.keyspace.CassdioColumnDefinition; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import java.time.Duration; +import java.util.List; +import java.util.Map; /** * ClusterTableRowCommander @@ -42,4 +51,26 @@ public CqlSessionSelectResults rowSelect(String clusterId, TableDTO.ClusterTable resultSet.getExecutionInfo().getPagingState() ); } + + public void rowInserts(TableDTO.ClusterTableRowImportArgs args, List> values) { + if (CollectionUtils.isEmpty(values)) { + return; + } + + CqlSession session = cqlSessionFactory.get(args.getClusterId()); + + for (List> list : Lists.partition(values, args.getPerCommitSize())) { + BatchStatement batchStatement = BatchStatement.newInstance(args.getBatchType()); + + for (Map map : list) { + batchStatement = batchStatement.add( + QueryBuilder.insertInto(args.getKeyspace(), args.getTable()) + .json(Jsons.toJson(map)) + .build() + ); + } + + session.execute(batchStatement); + } + } } diff --git a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/TableDTO.java b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/TableDTO.java index 9e1fd985..60684792 100644 --- a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/TableDTO.java +++ b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/TableDTO.java @@ -1,11 +1,14 @@ package kr.hakdang.cassdio.core.domain.cluster.keyspace.table; +import com.datastax.oss.driver.api.core.cql.BatchType; +import com.datastax.oss.driver.api.core.cql.DefaultBatchType; import io.micrometer.common.util.StringUtils; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; +import org.apache.commons.lang3.EnumUtils; /** * ClusterTableArgs @@ -99,4 +102,31 @@ public ClusterTableRowArgs(String keyspace, String table, int pageSize, int time } } + @ToString + @Getter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class ClusterTableRowImportArgs { + private String clusterId; + private String keyspace; + private String table; + + private BatchType batchType; + private int perCommitSize = 50; + + @Builder + public ClusterTableRowImportArgs( + String clusterId, + String keyspace, + String table, + String batchTypeCode, + int perCommitSize + ) { + this.clusterId = clusterId; + this.keyspace = keyspace; + this.table = table; + this.batchType = EnumUtils.getEnum(DefaultBatchType.class, batchTypeCode); + this.perCommitSize = perCommitSize; + } + } + } diff --git a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/ClusterTableColumnCommander.java b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/ClusterTableColumnCommander.java index 86f3dab2..6d51beac 100644 --- a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/ClusterTableColumnCommander.java +++ b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/ClusterTableColumnCommander.java @@ -17,7 +17,11 @@ import org.springframework.stereotype.Service; import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; import static java.util.Collections.emptyList; @@ -39,28 +43,50 @@ public CqlSessionSelectResults columnList(String clusterId, String keyspace, Str public CqlSessionSelectResults columnList(String clusterId, String keyspace, String table, List columnList) { CqlSession session = cqlSessionFactory.get(clusterId); - SimpleStatement statement; - Select select = getColumnTable(session, keyspace) .all() .whereColumn(CassandraSystemTablesColumn.TABLES_KEYSPACE_NAME.getColumnName()).isEqualTo(bindMarker()) .whereColumn(CassandraSystemTablesColumn.TABLES_TABLE_NAME.getColumnName()).isEqualTo(bindMarker()); -// if (CollectionUtils.isNotEmpty(columnList)) { -// select.whereColumn("column_name").in(columnList.stream().map(info -> bindMarker()).toList()); -// } + List arr = new ArrayList<>(); + arr.add(keyspace); + arr.add(table); + + if (CollectionUtils.isNotEmpty(columnList)) { + select = select.whereColumn("column_name").in(columnList.stream() + .map(info -> bindMarker()) + .collect(Collectors.toSet())); - statement = select.build(keyspace, table) + arr.addAll(columnList); + } + + SimpleStatement statement = select.build(arr.toArray()) .setTimeout(Duration.ofSeconds(3)); ResultSet resultSet = session.execute(statement); + List> rows = convertRows(session, resultSet) + .stream() + .peek(row -> row.put("sortValue", makeSortValue(row))) + .sorted(Comparator.comparing(row -> String.valueOf(row.get("sortValue")))) + .toList(); + return CqlSessionSelectResults.of( - convertRows(session, resultSet), + rows, CassdioColumnDefinition.makes(resultSet.getColumnDefinitions()) ); } + public List columnSortedList(String clusterId, String keyspace, String table) { + CqlSessionSelectResults results = columnList(clusterId, keyspace, table); + return results.getRows().stream().map(row -> String.valueOf(row.get("column_name"))).collect(Collectors.toList()); + } + + private String makeSortValue(Map row) { + ColumnKind columnKind = ColumnKind.findByCode(String.valueOf(row.get("kind"))); + return String.format("%s-%s", columnKind.getOrder(), row.get("position")); + } + private SelectFrom getColumnTable(CqlSession session, String keyspace) { if (ClusterUtils.isVirtualKeyspace(session.getContext(), keyspace)) { return QueryBuilder diff --git a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/Column.java b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/Column.java deleted file mode 100644 index 4c442ad8..00000000 --- a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/Column.java +++ /dev/null @@ -1,49 +0,0 @@ -package kr.hakdang.cassdio.core.domain.cluster.keyspace.table.column; - -import com.datastax.oss.driver.api.core.cql.Row; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; - -/** - * Column - * - * @author seungh0 - * @since 2024-07-01 - */ -@ToString -@Getter -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class Column { - - private String tableName; - private String name; - private ColumnKind kind; - private ColumnClusteringOrder clusteringOrder; - private int position; - private String dataType; - - @Builder - private Column(String tableName, String name, ColumnKind kind, ColumnClusteringOrder clusteringOrder, int position, String dataType) { - this.tableName = tableName; - this.name = name; - this.kind = kind; - this.clusteringOrder = clusteringOrder; - this.position = position; - this.dataType = dataType; - } - - public static Column from(Row row) { - return Column.builder() - .tableName(row.getString("table_name")) - .name(row.getString("column_name")) - .dataType(row.getString("type")) - .kind(ColumnKind.findByCode(row.getString("kind"))) - .clusteringOrder(ColumnClusteringOrder.findByCode(row.getString("clustering_order"))) - .position(row.getInt("position")) - .build(); - } - -} diff --git a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/ColumnClusteringOrder.java b/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/ColumnClusteringOrder.java deleted file mode 100644 index 74b13a50..00000000 --- a/cassdio-core/src/main/java/kr/hakdang/cassdio/core/domain/cluster/keyspace/table/column/ColumnClusteringOrder.java +++ /dev/null @@ -1,30 +0,0 @@ -package kr.hakdang.cassdio.core.domain.cluster.keyspace.table.column; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; - -/** - * ColumnClusteringOrder - * - * @author seungh0 - * @since 2024-07-01 - */ -@Slf4j -public enum ColumnClusteringOrder { - - ASC, - DESC, - NONE, - UNKNOWN, - ; - - public static ColumnClusteringOrder findByCode(String code) { - try { - return ColumnClusteringOrder.valueOf(StringUtils.upperCase(code)); - } catch (Exception exception) { - log.error("Unexpected ColumnClusteringOrder: {}", code); - return UNKNOWN; - } - } - -} diff --git a/cassdio-web/build.gradle b/cassdio-web/build.gradle index 6b8d857e..2e478982 100644 --- a/cassdio-web/build.gradle +++ b/cassdio-web/build.gradle @@ -17,10 +17,12 @@ dependencies { bootJar { enabled = true archiveBaseName.set("cassdio") + archiveVersion.set(""); } jar { enabled = true archiveBaseName.set("cassdio") + archiveVersion.set(""); } apply from: "${project.rootDir}/cassdio-web/frontend.build.gradle" diff --git a/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableApi.java b/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableApi.java index 372362c6..8c24f1c7 100644 --- a/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableApi.java +++ b/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableApi.java @@ -3,6 +3,7 @@ import jakarta.validation.Valid; import kr.hakdang.cassdio.core.domain.cluster.CqlSessionSelectResult; import kr.hakdang.cassdio.core.domain.cluster.CqlSessionSelectResults; +import kr.hakdang.cassdio.core.domain.cluster.keyspace.table.ClusterCsvProvider; import kr.hakdang.cassdio.core.domain.cluster.keyspace.table.ClusterTableCommander; import kr.hakdang.cassdio.core.domain.cluster.keyspace.table.ClusterTableGetCommander; import kr.hakdang.cassdio.core.domain.cluster.keyspace.table.ClusterTableListCommander; @@ -13,7 +14,6 @@ import kr.hakdang.cassdio.web.common.dto.response.ApiResponse; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -33,20 +33,19 @@ public class ClusterTableApi { private final ClusterTableCommander clusterTableCommander; private final ClusterTableListCommander clusterTableListCommander; private final ClusterTableGetCommander clusterTableGetCommander; - private final ClusterTableRowCommander clusterTableRowCommander; + private final ClusterTableColumnCommander clusterTableColumnCommander; + public ClusterTableApi( ClusterTableCommander clusterTableCommander, ClusterTableListCommander clusterTableListCommander, ClusterTableGetCommander clusterTableGetCommander, - ClusterTableRowCommander clusterTableRowCommander, ClusterTableColumnCommander clusterTableColumnCommander ) { this.clusterTableCommander = clusterTableCommander; this.clusterTableListCommander = clusterTableListCommander; this.clusterTableGetCommander = clusterTableGetCommander; - this.clusterTableRowCommander = clusterTableRowCommander; this.clusterTableColumnCommander = clusterTableColumnCommander; } @@ -106,25 +105,6 @@ public ApiResponse> getTableColumn( return ApiResponse.ok(responseMap); } - @GetMapping("/table/{table}/row") - public ApiResponse> tableRow( - @PathVariable String clusterId, - @PathVariable String keyspace, - @PathVariable String table, - @ModelAttribute ClusterTableRowRequest request - ) { - Map responseMap = new HashMap<>(); - CqlSessionSelectResults result1 = clusterTableRowCommander.rowSelect(clusterId, request.makeArgs(keyspace, table)); - - responseMap.put("nextCursor", result1.getNextCursor()); - responseMap.put("rows", result1.getRows()); - responseMap.put("rowHeader", result1.getRowHeader()); - - responseMap.put("columnList", clusterTableColumnCommander.columnList(clusterId, keyspace, table)); - - return ApiResponse.ok(responseMap); - } - //권한 추가해서 ADMIN 만 동작할 수 있도록 해야함. @DeleteMapping("/table/{table}/drop") public ApiResponse tableDrop( diff --git a/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableRequest.java b/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableRequest.java new file mode 100644 index 00000000..262ef7c8 --- /dev/null +++ b/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableRequest.java @@ -0,0 +1,28 @@ +package kr.hakdang.cassdio.web.route.cluster.keyspace.table; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * ClusterTableRequest + * + * @author akageun + * @since 2024-08-19 + */ +public class ClusterTableRequest { + + @Getter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class TableRowImportRequest { + private int perCommitSize; + private String batchTypeCode; + + @Builder + public TableRowImportRequest(int perCommitSize, String batchTypeCode) { + this.perCommitSize = perCommitSize; + this.batchTypeCode = batchTypeCode; + } + } +} diff --git a/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableRowApi.java b/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableRowApi.java new file mode 100644 index 00000000..be5901b3 --- /dev/null +++ b/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/keyspace/table/ClusterTableRowApi.java @@ -0,0 +1,128 @@ +package kr.hakdang.cassdio.web.route.cluster.keyspace.table; + +import jakarta.servlet.http.HttpServletResponse; +import kr.hakdang.cassdio.core.domain.cluster.CqlSessionSelectResults; +import kr.hakdang.cassdio.core.domain.cluster.keyspace.table.ClusterCsvProvider; +import kr.hakdang.cassdio.core.domain.cluster.keyspace.table.ClusterTableRowCommander; +import kr.hakdang.cassdio.core.domain.cluster.keyspace.table.TableDTO; +import kr.hakdang.cassdio.core.domain.cluster.keyspace.table.column.ClusterTableColumnCommander; +import kr.hakdang.cassdio.web.common.dto.response.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.emptyMap; + +/** + * ClusterTableRowApi + * + * @author akageun + * @since 2024-08-19 + */ +@Slf4j +@RestController +@RequestMapping("/api/cassandra/cluster/{clusterId}/keyspace/{keyspace}") +public class ClusterTableRowApi { + + private final ClusterTableRowCommander clusterTableRowCommander; + private final ClusterCsvProvider clusterCsvProvider; + private final ClusterTableColumnCommander clusterTableColumnCommander; + + public ClusterTableRowApi( + ClusterTableRowCommander clusterTableRowCommander, + ClusterCsvProvider clusterCsvProvider, + ClusterTableColumnCommander clusterTableColumnCommander + ) { + this.clusterTableRowCommander = clusterTableRowCommander; + this.clusterCsvProvider = clusterCsvProvider; + this.clusterTableColumnCommander = clusterTableColumnCommander; + } + + @GetMapping("/table/{table}/row") + public ApiResponse> tableRow( + @PathVariable String clusterId, + @PathVariable String keyspace, + @PathVariable String table, + @ModelAttribute ClusterTableRowRequest request + ) { + Map responseMap = new HashMap<>(); + CqlSessionSelectResults result1 = clusterTableRowCommander.rowSelect(clusterId, request.makeArgs(keyspace, table)); + + responseMap.put("nextCursor", result1.getNextCursor()); + responseMap.put("rows", result1.getRows()); + responseMap.put("rowHeader", result1.getRowHeader()); + + responseMap.put("columnList", clusterTableColumnCommander.columnList(clusterId, keyspace, table)); + + return ApiResponse.ok(responseMap); + } + + @PostMapping("/table/{table}/row/import/sample") + public ApiResponse> importerSampleDownload( + HttpServletResponse response, + @PathVariable String clusterId, + @PathVariable String keyspace, + @PathVariable String table + ) throws IOException { + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType("text/csv; charset=UTF-8"); + String exportFileName = "sample-" + LocalDateTime.now() + ".csv"; //TODO : Formatting + + response.setHeader("Content-disposition", "attachment;filename=" + exportFileName); + + Map responseMap = new HashMap<>(); + + List columnList = clusterTableColumnCommander.columnSortedList(clusterId, keyspace, table); + + clusterCsvProvider.importerCsvSampleDownload(response.getWriter(), columnList); + + return ApiResponse.ok(responseMap); + } + + @PostMapping("/table/{table}/row/import") + public ApiResponse> importUpload( + @PathVariable String clusterId, + @PathVariable String keyspace, + @PathVariable String table, + @RequestParam("file") MultipartFile file, + ClusterTableRequest.TableRowImportRequest request + ) throws IOException { + + try (Reader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) { + List columnList = clusterTableColumnCommander.columnSortedList(clusterId, keyspace, table); + + List> values = clusterCsvProvider.importCsvReader( + reader, columnList + ); + + clusterTableRowCommander.rowInserts( + TableDTO.ClusterTableRowImportArgs.builder() + .clusterId(clusterId) + .keyspace(keyspace) + .table(table) + .batchTypeCode(request.getBatchTypeCode()) + .perCommitSize(request.getPerCommitSize()) + .build() + , values); + } + + + return ApiResponse.ok(emptyMap()); + } +} diff --git a/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/query/ClusterQueryApi.java b/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/query/ClusterQueryApi.java index 888379d4..59abacc7 100644 --- a/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/query/ClusterQueryApi.java +++ b/cassdio-web/src/main/java/kr/hakdang/cassdio/web/route/cluster/query/ClusterQueryApi.java @@ -1,5 +1,8 @@ package kr.hakdang.cassdio.web.route.cluster.query; +import kr.hakdang.cassdio.core.domain.cluster.CqlSessionSelectResults; +import kr.hakdang.cassdio.core.domain.cluster.keyspace.CassdioColumnDefinition; +import kr.hakdang.cassdio.core.domain.cluster.keyspace.table.column.ClusterTableColumnCommander; import kr.hakdang.cassdio.core.domain.cluster.query.ClusterQueryCommander; import kr.hakdang.cassdio.core.domain.cluster.query.QueryDTO; import kr.hakdang.cassdio.web.common.dto.response.ApiResponse; @@ -11,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -24,11 +28,14 @@ @RequestMapping("/api/cassandra/cluster") public class ClusterQueryApi { private final ClusterQueryCommander clusterQueryCommander; + private final ClusterTableColumnCommander clusterTableColumnCommander; public ClusterQueryApi( - ClusterQueryCommander clusterQueryCommander + ClusterQueryCommander clusterQueryCommander, + ClusterTableColumnCommander clusterTableColumnCommander ) { this.clusterQueryCommander = clusterQueryCommander; + this.clusterTableColumnCommander = clusterTableColumnCommander; } @PostMapping(value = {"/{clusterId}/query", "/{clusterId}/keyspace/{keyspace}/query"}) @@ -44,10 +51,26 @@ public ApiResponse> clusterQueryCommand( request.makeArgs(keyspace) ); + CassdioColumnDefinition columnDefinition = result.getRowHeader().getFirst(); + + List columnListForParam = result.getRowHeader() + .stream() + .map(CassdioColumnDefinition::getColumnName) + .toList(); + + CqlSessionSelectResults columnListResult = clusterTableColumnCommander.columnList( + clusterId, + columnDefinition.getKeyspace(), + columnDefinition.getTable(), + columnListForParam + ); + responseMap.put("wasApplied", result.isWasApplied()); responseMap.put("nextCursor", result.getNextCursor()); responseMap.put("rows", result.getRows()); responseMap.put("rowHeader", result.getRowHeader()); + responseMap.put("columnList", columnListResult); + if (request.isTrace()) { responseMap.put("queryTrace", result.getQueryTrace()); } diff --git a/cassdio-web/src/main/webapp/src/components/cluster/modal/table-detail-modal.js b/cassdio-web/src/main/webapp/src/components/cluster/modal/table-detail-modal.js index ab7ba6c1..b46fb630 100644 --- a/cassdio-web/src/main/webapp/src/components/cluster/modal/table-detail-modal.js +++ b/cassdio-web/src/main/webapp/src/components/cluster/modal/table-detail-modal.js @@ -3,7 +3,6 @@ import {Modal} from "react-bootstrap"; import TableDetailModalInfo from "./detail/table-detail-modal-info"; import TableDetailModalDescribe from "./detail/table-detail-modal-describe"; import TableDetailModalColumnList from "./detail/table-detail-modal-column-list"; -import {CassdioUtils} from "utils/cassdioUtils"; import clusterTableDetailApi from "remotes/clusterTableDetailApi"; const TableDetailModal = ({show, handleClose, clusterId, keyspaceName, tableName}) => { @@ -36,14 +35,10 @@ const TableDetailModal = ({show, handleClose, clusterId, keyspaceName, tableName return; } - const sortedColumnList = CassdioUtils.columnListSorting( - data.result.columnList - ); - setTableInfo({ detail: data.result.detail, describe: data.result.describe, - columnList: sortedColumnList, + columnList: data.result.columnList, }) }) diff --git a/cassdio-web/src/main/webapp/src/components/cluster/modal/table-import-modal.js b/cassdio-web/src/main/webapp/src/components/cluster/modal/table-import-modal.js index 18948aab..a81388a2 100644 --- a/cassdio-web/src/main/webapp/src/components/cluster/modal/table-import-modal.js +++ b/cassdio-web/src/main/webapp/src/components/cluster/modal/table-import-modal.js @@ -1,10 +1,71 @@ -import {useEffect} from "react"; +import {useEffect, useState} from "react"; import {Modal} from "react-bootstrap"; +import axios from "axios"; +import {toast} from "react-toastify"; +import clusterTableRowImportApi from "../../../remotes/clusterTableRowImportApi"; -const TableImportModal = (props) => { +const TableImportModal = ({show, handleClose, clusterId, keyspaceName, tableName}) => { - const show = props.show; - const handleClose = props.handleClose; + const [importOptions, setImportOptions] = useState({ + perCommitSize: 50, + batchTypeCode: 'LOGGED', + }); + + const importSampleDownload = async () => { + const config = { + method: "POST", + url: `/api/cassandra/cluster/${clusterId}/keyspace/${keyspaceName}/table/${tableName}/row/import/sample`, + responseType: "blob", + }; + const response = await axios(config); + const name = response.headers["content-disposition"] + .split("filename=")[1] + .replace(/"/g, ""); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", name); + link.style.cssText = "display:none"; + document.body.appendChild(link); + link.click(); + link.remove(); + } + + const [files, setFiles] = useState([]); + + const handleFilesChange = (e) => { + setFiles(Array.from(e.target.files)); + } + + const importFileUpload = (evt) => { + evt.preventDefault(); + + if (!files || files.length <= 0) { + toast.warn('File Empty'); + return; + } + + const formData = new FormData(); + + formData.append('file', files[0]) + formData.append(`perCommitSize`, importOptions.perCommitSize) + formData.append(`batchTypeCode`, importOptions.batchTypeCode) + + + clusterTableRowImportApi({ + clusterId, keyspaceName, tableName, formData + }).then((data) => { + if (!data.ok) { + return; + } + + toast.info(`complete`); + + }).finally(() => { + + }); + + } useEffect(() => { //show component @@ -17,14 +78,76 @@ const TableImportModal = (props) => { return ( <> - + - Data Importer + + Data Importer + + + + - Import + <> +
+
+ {/**/} + +
+
+ +
+
+ Option +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ diff --git a/cassdio-web/src/main/webapp/src/components/cluster/modal/table-row-detail-modal.js b/cassdio-web/src/main/webapp/src/components/cluster/modal/table-row-detail-modal.js index 2a983164..83534934 100644 --- a/cassdio-web/src/main/webapp/src/components/cluster/modal/table-row-detail-modal.js +++ b/cassdio-web/src/main/webapp/src/components/cluster/modal/table-row-detail-modal.js @@ -2,7 +2,7 @@ import {useEffect} from "react"; import {Modal, OverlayTrigger, Tooltip} from "react-bootstrap"; import {toast} from "react-toastify"; -const TableRowDetailModal = ({show, handleClose, rowDetailView, convertedRowHeader}) => { +const TableRowDetailModal = ({show, handleClose, rowDetailView, columnList}) => { const handleCopyClipBoard = async (data) => { try { @@ -32,7 +32,7 @@ const TableRowDetailModal = ({show, handleClose, rowDetailView, convertedRowHead { - convertedRowHeader && convertedRowHeader.map((info, infoIndex) => { + columnList.rows && columnList.rows.map((info, infoIndex) => { return ( { - result.rowHeader.map((info, infoIndex) => { + result.columnList.rows && result.columnList.rows.map((info, infoIndex) => { return ( - ) }) } + { result.rows.length <= 0 ? <> - @@ -87,11 +95,11 @@ const QueryResult = ({queryExecute, result, query, nextCursor, setShowQueryTrace return ( { - result.rowHeader.map((info, infoIndex) => { + result.columnList.rows.map((info, infoIndex) => { return ( ) }) diff --git a/cassdio-web/src/main/webapp/src/components/layout/cassdio-default-layout.js b/cassdio-web/src/main/webapp/src/components/layout/cassdio-default-layout.js index 80aa0187..25acc610 100644 --- a/cassdio-web/src/main/webapp/src/components/layout/cassdio-default-layout.js +++ b/cassdio-web/src/main/webapp/src/components/layout/cassdio-default-layout.js @@ -15,11 +15,13 @@ const CassdioDefaultLayout = ({}) => { useEffect(() => { - dispatch({ - type: "SET_BOOTSTRAP", - consistencyLevels: bootstrap.consistencyLevels, - defaultConsistencyLevel: bootstrap.defaultConsistencyLevel, - }); + if (bootstrap) { + dispatch({ + type: "SET_BOOTSTRAP", + consistencyLevels: bootstrap.consistencyLevels, + defaultConsistencyLevel: bootstrap.defaultConsistencyLevel, + }); + } return () => { }; diff --git a/cassdio-web/src/main/webapp/src/hooks/useTable.js b/cassdio-web/src/main/webapp/src/hooks/useTable.js index 4529757b..7d6df6f6 100644 --- a/cassdio-web/src/main/webapp/src/hooks/useTable.js +++ b/cassdio-web/src/main/webapp/src/hooks/useTable.js @@ -1,7 +1,6 @@ import {toast} from "react-toastify"; import {useNavigate, useParams} from "react-router-dom"; import {useState} from "react"; -import {CassdioUtils} from "utils/cassdioUtils"; import clusterTableTruncateApi from "../remotes/clusterTableTruncateApi"; import clusterTableDropApi from "../remotes/clusterTableDropApi"; import clusterTableRowApi from "../remotes/clusterTableRowApi"; @@ -59,9 +58,11 @@ export default function useTable() { rows: [], rowHeader: [], columnList: [], - convertedRowHeader: [], }; + const doInitQueryResult = () => { + setQueryResult(initQueryResult); + } const [queryResult, setQueryResult] = useState(initQueryResult) const [nextCursor, setNextCursor] = useState('') @@ -88,7 +89,6 @@ export default function useTable() { rows: [...queryResult.rows, ...data.result.rows], rowHeader: data.result.rowHeader, columnList: data.result.columnList, - convertedRowHeader: CassdioUtils.convertRowHeader(data.result.columnList, data.result.rowHeader), }) }).finally(() => { if (!setLoading) { @@ -103,6 +103,7 @@ export default function useTable() { doTableTruncate, doTableDrop, doGetTableRows, + doInitQueryResult, queryLoading, queryResult, nextCursor diff --git a/cassdio-web/src/main/webapp/src/pages/cluster/cluster-query-page.js b/cassdio-web/src/main/webapp/src/pages/cluster/cluster-query-page.js index 2bf86a7f..eedb52dd 100644 --- a/cassdio-web/src/main/webapp/src/pages/cluster/cluster-query-page.js +++ b/cassdio-web/src/main/webapp/src/pages/cluster/cluster-query-page.js @@ -1,4 +1,4 @@ -import {Link, useParams} from "react-router-dom"; +import {useParams} from "react-router-dom"; import React, {useEffect, useState} from "react"; import {toast} from "react-toastify"; import QueryEditor from "components/cluster/query-editor"; @@ -35,6 +35,7 @@ const ClusterQueryPage = () => { wasApplied: null, rows: [], rowHeader: [], + columnList: {}, queryTrace: {}, }; @@ -79,6 +80,7 @@ const ClusterQueryPage = () => { wasApplied: data.result.wasApplied, rows: rows, rowHeader: data.result.rowHeader, + columnList : data.result.columnList, queryTrace: data.result.queryTrace, }) }).finally(() => { diff --git a/cassdio-web/src/main/webapp/src/pages/cluster/cluster-table-page.js b/cassdio-web/src/main/webapp/src/pages/cluster/cluster-table-page.js index 97e54525..6e0515fc 100644 --- a/cassdio-web/src/main/webapp/src/pages/cluster/cluster-table-page.js +++ b/cassdio-web/src/main/webapp/src/pages/cluster/cluster-table-page.js @@ -18,6 +18,7 @@ const ClusterTablePage = () => { doTableTruncate, doTableDrop, doGetTableRows, + doInitQueryResult, queryLoading, queryResult, nextCursor, @@ -41,6 +42,7 @@ const ClusterTablePage = () => { return () => { //hide component setTableName(''); + doInitQueryResult(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [tableName]); @@ -73,21 +75,21 @@ const ClusterTablePage = () => { Detail - {/*
*/} - {/* */} - {/* */} - {/*
*/} +
+ + +
{/* MAP, Set 등 다 구현하려면 어려움 추후 대응 필요해보임*/} {/**/}
-
{ - queryResult.convertedRowHeader.map((info, infoIndex) => { + queryResult.columnList.rows && queryResult.columnList.rows.map((info, infoIndex) => { return ( - @@ -179,7 +181,7 @@ const ClusterTablePage = () => { { - queryResult.convertedRowHeader.map((info, infoIndex) => { + queryResult.columnList.rows && queryResult.columnList.rows.map((info, infoIndex) => { return (
{ + @@ -200,7 +201,7 @@ const QueryEditor = ({queryOptions, setQueryOptions, queryExecute}) => {
{ setQueryOptions(t => { diff --git a/cassdio-web/src/main/webapp/src/components/cluster/query-result.js b/cassdio-web/src/main/webapp/src/components/cluster/query-result.js index 8bde7652..d695cd6c 100644 --- a/cassdio-web/src/main/webapp/src/components/cluster/query-result.js +++ b/cassdio-web/src/main/webapp/src/components/cluster/query-result.js @@ -54,31 +54,39 @@ const QueryResult = ({queryExecute, result, query, nextCursor, setShowQueryTrace
- - {info.columnName}
- ({info.type}) + {info.column_name} ({info.type}) }> - {info.columnName} + + { + info.kind === 'partition_key' && + + } + { + info.kind === 'clustering' && + + } + {info.column_name} +
+ No Data
- +
# @@ -150,7 +152,7 @@ const ClusterTablePage = () => { { queryResult.rows.length <= 0 ? <>
+ No Data
@@ -221,7 +223,7 @@ const ClusterTablePage = () => { show={showDetail} clusterId={routeParams.clusterId} keyspaceName={routeParams.keyspaceName} - tableName={tableName} + tableName={routeParams.tableName} handleClose={() => setShowDetail(false)} /> } @@ -229,6 +231,9 @@ const ClusterTablePage = () => { { showExport && setShowExport(false)} /> } @@ -236,6 +241,9 @@ const ClusterTablePage = () => { { showImport && setShowImport(false)} /> } @@ -243,7 +251,7 @@ const ClusterTablePage = () => { showRowDetail && rowDetailView && setShowRowDetail(false)} /> } diff --git a/cassdio-web/src/main/webapp/src/remotes/clusterTableRowImportApi.js b/cassdio-web/src/main/webapp/src/remotes/clusterTableRowImportApi.js new file mode 100644 index 00000000..085c97c4 --- /dev/null +++ b/cassdio-web/src/main/webapp/src/remotes/clusterTableRowImportApi.js @@ -0,0 +1,18 @@ +import AxiosUtils from "utils/axiosUtils"; + +export default async function clusterTableRowImportApi( + {clusterId, keyspaceName, tableName, formData} +) { + try { + const response = await AxiosUtils.axiosInstance({ + method: 'post', + url: `/api/cassandra/cluster/${clusterId}/keyspace/${keyspaceName}/table/${tableName}/row/import`, + data: formData, + }) + + return await response.data; + } catch (error) { + return await error.response.data; + } +} + diff --git a/cassdio-web/src/main/webapp/src/utils/cassdioUtils.js b/cassdio-web/src/main/webapp/src/utils/cassdioUtils.js index 5ca02294..a214e0fe 100644 --- a/cassdio-web/src/main/webapp/src/utils/cassdioUtils.js +++ b/cassdio-web/src/main/webapp/src/utils/cassdioUtils.js @@ -10,89 +10,4 @@ export const CassdioUtils = { return data; }, - - convertRowHeader: (columnList, rowHeader) => { - const convertedMap = CassdioUtils.columnListSortValueMap(columnList); - - const result = [] - for (const header of rowHeader) { - const key = CassdioUtils.makeRowKey(header.keyspace, header.table, header.columnName) - - const convertedColumnInfo = convertedMap[key]; - result.push(convertedColumnInfo) - - } - - return result; - }, - - columnListSortValueMap: (columnList) => { - if (!columnList || columnList.rows.length <= 0) { - return []; - } - - const rows = columnList.rows; - - const kindSort = { - 'partition_key': 0, - 'clustering': 1, - 'regular': 2, - 'unknown': 3, - } - - const temp = {} - - for (const row of rows) { - - temp[CassdioUtils.makeRowKey(row.keyspace_name, row.table_name, row.column_name)] = { - ...row, - sortValue: `${kindSort[row.kind]}-${row.position}` - } - } - - return temp; - }, - - makeRowKey: (keyspace, table, column) => { - return `${keyspace}.${table}.${column}` - }, - - columnListSorting: (columnList) => { - if (!columnList || columnList.rows.length <= 0) { - return []; - } - - const rows = columnList.rows; - - const kindSort = { - 'partition_key': 0, - 'clustering': 1, - 'regular': 2, - 'unknown': 3, - } - - const temp = [] - - for (const row of rows) { - temp.push({ - ...row, - sortValue: `${kindSort[row.kind]}-${row.position}` - }) - } - - temp.sort(function (a, b) { - if (a.sortValue < b.sortValue) { - return -1; - } - if (a.sortValue > b.sortValue) { - return 1; - } - return 0; - }) - - return { - ...columnList, - rows: temp - }; - } };