Skip to content

Commit

Permalink
Feature/query editor draft (#14)
Browse files Browse the repository at this point in the history
* Cassandra Query Editor 초안

* system ui draft page
  • Loading branch information
akageun committed Jul 3, 2024
1 parent 4d215f4 commit 0241b0d
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 2 deletions.
2 changes: 1 addition & 1 deletion cadio-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Row> page1Iter = resultSet.iterator();

List<Map<String, Object>> rows = new ArrayList<>();
while (0 < resultSet.getAvailableWithoutFetching()) {
rows.add(convertMap(definitions, page1Iter.next()));
}

ByteBuffer pagingStateAsBytes = resultSet.getExecutionInfo().getPagingState();

List<String> 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<String, Object> convertMap(ColumnDefinitions definitions, Row row) {
Map<String, Object> result = new HashMap<>();

for (int i = 0; i < definitions.size(); i++) {
ColumnDefinition definition = definitions.get(i);
String name = definition.getName().asCql(true);
TypeCodec<Object> codec = row.codecRegistry().codecFor(definition.getType());
Object value = codec.decode(row.getBytesUnsafe(i), row.protocolVersion());

result.put(name, value);
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> columnNames;
private List<Map<String, Object>> rows;
private String nextToken;

@Builder
public ClusterQueryCommanderResult(boolean wasApplied, List<String> columnNames, List<Map<String, Object>> rows, String nextToken) {
this.wasApplied = wasApplied;
this.columnNames = columnNames;
this.rows = rows;
this.nextToken = nextToken;
}
}
Original file line number Diff line number Diff line change
@@ -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<Map<String, Object>> clusterQueryCommand(
@RequestBody ClusterQueryRequest request
) {

Map<String, Object> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const QueryHome = () => {
</div>

<QueryEditor/>

</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className="btn-toolbar pb-1" role="toolbar" aria-label="Toolbar with button groups">
<div className="btn-group btn-group-sm me-2 " role="group" aria-label="Third group">
<button type="button" className="btn btm-sm btn-danger" onClick={queryExecute}>
<i className="bi bi-play-fill"></i> Execute
</button>
</div>
{/*<div className="btn-group btn-group-sm me-2" role="group" aria-label="First group">*/}
{/* <button type="button" className="btn btm-sm btn-primary">*/}
{/* <i className="bi bi-play-fill"></i>*/}
{/* </button>*/}
{/* <button type="button" className="btn btm-sm btn-primary">2</button>*/}
{/* <button type="button" className="btn btm-sm btn-primary">3</button>*/}
{/* <button type="button" className="btn btm-sm btn-primary">4</button>*/}
{/*</div>*/}
{/*<div className="btn-group btn-group-sm" role="group" aria-label="Second group">*/}
{/* <button type="button" className="btn btm-sm btn-secondary">5</button>*/}
{/* <button type="button" className="btn btm-sm btn-secondary">6</button>*/}
{/* <button type="button" className="btn btm-sm btn-secondary">7</button>*/}
{/*</div>*/}
</div>

<div className="form-floating">
<textarea className="form-control" placeholder="Query" id="queryEditor"
style={{"height": "300px"}}></textarea>
style={{"height": "300px"}}
value={queryParam.query || ''}
rows={10}
onChange={evt => setQueryParam(t => {
return {...t, query: evt.target.value}
})}
></textarea>
<label htmlFor="queryEditor">Query</label>
</div>

<h4 className={"h4 mt-3"}>Result</h4>
{
queryResult.wasApplied && <>
<p>wasApplied : {queryResult.wasApplied}</p>

<div className="table-responsive small">
<table className="table table-striped table-hover table-sm">
<thead>
<tr>
{
queryResult.columnNames.map((info, infoIndex) => {
return (
<th key={`resultHeader${infoIndex}`} scope="col">{info}</th>
)
})
}
</tr>
</thead>
<tbody>
{
queryResult.rows.map((row, rowIndex) => {
return (
<tr key={`resultBody${rowIndex}`}>
{
queryResult.columnNames.map((info, infoIndex) => {
return (
<td key={`resultItem${infoIndex}`}>{row[info]}</td>
)
})
}
</tr>
)
})
}

</tbody>
</table>
</div>
</>
}


</>
)
}
Expand Down

0 comments on commit 0241b0d

Please sign in to comment.