From 0241b0deaaf001b14210595fb699aee581547ce6 Mon Sep 17 00:00:00 2001 From: Geunee Date: Wed, 3 Jul 2024 21:26:23 +0900 Subject: [PATCH] Feature/query editor draft (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Cassandra Query Editor 초안 * system ui draft page --- cadio-core/build.gradle | 2 +- .../domain/cluster/ClusterQueryCommander.java | 91 ++++++++++++++ .../cluster/ClusterQueryCommanderResult.java | 32 +++++ .../web/route/cluster/ClusterQueryApi.java | 65 ++++++++++ .../pages/cluster/components/query-home.js | 1 + .../cluster/components/query/query-editor.js | 116 +++++++++++++++++- 6 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 cadio-core/src/main/java/kr/hakdang/cadio/core/domain/cluster/ClusterQueryCommander.java create mode 100644 cadio-core/src/main/java/kr/hakdang/cadio/core/domain/cluster/ClusterQueryCommanderResult.java create mode 100644 cadio-web/src/main/java/kr/hakdang/cadio/web/route/cluster/ClusterQueryApi.java diff --git a/cadio-core/build.gradle b/cadio-core/build.gradle index b1ce7c4f..a2ff0d8d 100644 --- a/cadio-core/build.gradle +++ b/cadio-core/build.gradle @@ -12,7 +12,7 @@ dependencies { // implementation("com.datastax.oss:java-driver-mapper-runtime:${datastaxJavaDriverVersion}") // CommonsLang3 - api("org.apache.commons:commons-lang3") + 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") } diff --git a/cadio-core/src/main/java/kr/hakdang/cadio/core/domain/cluster/ClusterQueryCommander.java b/cadio-core/src/main/java/kr/hakdang/cadio/core/domain/cluster/ClusterQueryCommander.java new file mode 100644 index 00000000..e839b6ca --- /dev/null +++ b/cadio-core/src/main/java/kr/hakdang/cadio/core/domain/cluster/ClusterQueryCommander.java @@ -0,0 +1,91 @@ +package kr.hakdang.cadio.core.domain.cluster; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.QueryTrace; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.TraceEvent; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.protocol.internal.util.Bytes; +import io.micrometer.common.util.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * ClusterQueryCommander + * + * @author akageun + * @since 2024-07-03 + */ +@Slf4j +@Service +public class ClusterQueryCommander { + + public ClusterQueryCommanderResult execute(CqlSession session, String query, String nextTokenParam) { + SimpleStatement statement = SimpleStatement.builder(query) + .setPageSize(2) // 10 per pages + .setTimeout(Duration.ofSeconds(3)) // 3s timeout + .setPagingState(StringUtils.isNotBlank(nextTokenParam) ? Bytes.fromHexString(nextTokenParam) : null) + .setTracing(false) + .build(); + //.setConsistencyLevel(ConsistencyLevel.ONE); + + ResultSet resultSet = session.execute(statement); + + ColumnDefinitions definitions = resultSet.getColumnDefinitions(); + + //log.info("+ Page 1 has {} items", resultSet.getAvailableWithoutFetching()); + Iterator page1Iter = resultSet.iterator(); + + List> rows = new ArrayList<>(); + while (0 < resultSet.getAvailableWithoutFetching()) { + rows.add(convertMap(definitions, page1Iter.next())); + } + + ByteBuffer pagingStateAsBytes = resultSet.getExecutionInfo().getPagingState(); + + List columnNames = new ArrayList<>(); + for (ColumnDefinition definition : definitions) { + columnNames.add(definition.getName().asCql(true)); + } + + QueryTrace queryTrace = resultSet.getExecutionInfo().getQueryTrace(); + log.info("query Trace : {}", queryTrace.getTracingId()); + for (TraceEvent event : queryTrace.getEvents()) { + log.info("event : {}", event); + } + + return ClusterQueryCommanderResult.builder() + .wasApplied(resultSet.wasApplied()) + .columnNames(columnNames) + .rows(rows) + .nextToken(Bytes.toHexString(pagingStateAsBytes)) + .build(); + } + + private Map convertMap(ColumnDefinitions definitions, Row row) { + Map result = new HashMap<>(); + + for (int i = 0; i < definitions.size(); i++) { + ColumnDefinition definition = definitions.get(i); + String name = definition.getName().asCql(true); + TypeCodec codec = row.codecRegistry().codecFor(definition.getType()); + Object value = codec.decode(row.getBytesUnsafe(i), row.protocolVersion()); + + result.put(name, value); + } + + return result; + } +} diff --git a/cadio-core/src/main/java/kr/hakdang/cadio/core/domain/cluster/ClusterQueryCommanderResult.java b/cadio-core/src/main/java/kr/hakdang/cadio/core/domain/cluster/ClusterQueryCommanderResult.java new file mode 100644 index 00000000..e5553173 --- /dev/null +++ b/cadio-core/src/main/java/kr/hakdang/cadio/core/domain/cluster/ClusterQueryCommanderResult.java @@ -0,0 +1,32 @@ +package kr.hakdang.cadio.core.domain.cluster; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * ClusterQueryCommanderResult + * + * @author akageun + * @since 2024-07-03 + */ +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ClusterQueryCommanderResult { + private boolean wasApplied; + private List columnNames; + private List> rows; + private String nextToken; + + @Builder + public ClusterQueryCommanderResult(boolean wasApplied, List columnNames, List> rows, String nextToken) { + this.wasApplied = wasApplied; + this.columnNames = columnNames; + this.rows = rows; + this.nextToken = nextToken; + } +} diff --git a/cadio-web/src/main/java/kr/hakdang/cadio/web/route/cluster/ClusterQueryApi.java b/cadio-web/src/main/java/kr/hakdang/cadio/web/route/cluster/ClusterQueryApi.java new file mode 100644 index 00000000..32e45a73 --- /dev/null +++ b/cadio-web/src/main/java/kr/hakdang/cadio/web/route/cluster/ClusterQueryApi.java @@ -0,0 +1,65 @@ +package kr.hakdang.cadio.web.route.cluster; + +import com.datastax.oss.driver.api.core.CqlSession; +import kr.hakdang.cadio.core.domain.cluster.ClusterQueryCommander; +import kr.hakdang.cadio.core.domain.cluster.ClusterQueryCommanderResult; +import kr.hakdang.cadio.web.common.dto.response.ApiResponse; +import kr.hakdang.cadio.web.route.BaseSample; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +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.HashMap; +import java.util.Map; + +import static java.util.Collections.emptyMap; + +/** + * ClusterQueryApi + * + * @author akageun + * @since 2024-07-03 + */ +@Slf4j +@RestController +@RequestMapping("/api/cassandra/cluster") +public class ClusterQueryApi extends BaseSample { + + @Autowired + private ClusterQueryCommander clusterQueryCommander; + + @PostMapping("/query") + public ApiResponse> clusterQueryCommand( + @RequestBody ClusterQueryRequest request + ) { + + Map map = new HashMap<>(); + try (CqlSession session = makeSession()) { //TODO : interface 작업할 때 facade layer 로 변경 예정 + ClusterQueryCommanderResult result1 = clusterQueryCommander.execute(session, request.getQuery(), request.getNextToken()); + + map.put("wasApplied", result1.isWasApplied()); + map.put("nextToken", result1.getNextToken()); + map.put("rows", result1.getRows()); + map.put("columnNames", result1.getColumnNames()); + } catch (Exception e) { + log.error("error : {}", e.getMessage(), e); + throw e; + } + + + return ApiResponse.ok(map); + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class ClusterQueryRequest { + private String query; + private String nextToken; + } +} diff --git a/cadio-web/src/main/webapp/src/pages/cluster/components/query-home.js b/cadio-web/src/main/webapp/src/pages/cluster/components/query-home.js index 4a978e82..35fbca7c 100644 --- a/cadio-web/src/main/webapp/src/pages/cluster/components/query-home.js +++ b/cadio-web/src/main/webapp/src/pages/cluster/components/query-home.js @@ -20,6 +20,7 @@ const QueryHome = () => { + ) } diff --git a/cadio-web/src/main/webapp/src/pages/cluster/components/query/query-editor.js b/cadio-web/src/main/webapp/src/pages/cluster/components/query/query-editor.js index d055f599..0e49ebd2 100644 --- a/cadio-web/src/main/webapp/src/pages/cluster/components/query/query-editor.js +++ b/cadio-web/src/main/webapp/src/pages/cluster/components/query/query-editor.js @@ -1,12 +1,126 @@ +import {Link} from "react-router-dom"; +import {useState} from "react"; +import axios from "axios"; + const QueryEditor = () => { + const [queryParam, setQueryParam] = useState( + { + query: "SELECT * FROM testdb.test_table_1;", + nextToken: "", + } + ); + + const [queryResult, setQueryResult] = useState({ + wasApplied: null, + rows: [], + columnNames: [], + }) + + const queryExecute = () => { + setQueryResult({ + wasApplied: null, + rows: [], + columnNames: [], + }) + + axios({ + method: "POST", + url: "/api/cassandra/cluster/query", + data: { + query: queryParam.query, + nextToken: queryParam.nextToken, + }, + }).then((response) => { + console.log("response : ", response); + setQueryResult({ + wasApplied: response.data.result.wasApplied, + rows: response.data.result.rows, + columnNames: response.data.result.columnNames, + }) + }).catch((error) => { + + }).finally(() => { + }) + } + return ( <> +
+
+ +
+ {/*
*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/*
*/} + {/*
*/} + {/* */} + {/* */} + {/* */} + {/*
*/} +
+
+ style={{"height": "300px"}} + value={queryParam.query || ''} + rows={10} + onChange={evt => setQueryParam(t => { + return {...t, query: evt.target.value} + })} + >
+ +

Result

+ { + queryResult.wasApplied && <> +

wasApplied : {queryResult.wasApplied}

+ +
+ + + + { + queryResult.columnNames.map((info, infoIndex) => { + return ( + + ) + }) + } + + + + { + queryResult.rows.map((row, rowIndex) => { + return ( + + { + queryResult.columnNames.map((info, infoIndex) => { + return ( + + ) + }) + } + + ) + }) + } + + +
{info}
{row[info]}
+
+ + } + + ) }