Skip to content

Commit

Permalink
improvement(graphs): add show graph button for each column in results
Browse files Browse the repository at this point in the history
When viewing results tables users now can view graph for selected column
to see values from the past/future.

closes: scylladb/qa-tasks#1822
  • Loading branch information
soyacz committed Jan 8, 2025
1 parent 23a9638 commit 7a76cf9
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 101 deletions.
3 changes: 2 additions & 1 deletion argus/backend/controller/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ def test_results():
test_id = request.args.get("testId")
start_date_str = request.args.get("startDate")
end_date_str = request.args.get("endDate")
table_names = request.args.getlist("tableNames[]")

if not test_id:
raise Exception("No testId provided")
Expand All @@ -407,7 +408,7 @@ def test_results():
exists = service.is_results_exist(test_id=UUID(test_id))
return Response(status=200 if exists else 404)

graphs, ticks, releases_filters = service.get_test_graphs(test_id=UUID(test_id), start_date=start_date, end_date=end_date)
graphs, ticks, releases_filters = service.get_test_graphs(test_id=UUID(test_id), start_date=start_date, end_date=end_date, table_names=table_names)

return {
"status": "ok",
Expand Down
6 changes: 5 additions & 1 deletion argus/backend/service/results_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,9 +500,13 @@ def get_run_results(self, test_id: UUID, run_id: UUID, key_metrics: list[str] |

return [{entry['table_name']: entry['table_data']} for entry in table_entries]

def get_test_graphs(self, test_id: UUID, start_date: datetime | None = None, end_date: datetime | None = None):
def get_test_graphs(self, test_id: UUID, start_date: datetime | None = None, end_date: datetime | None = None, table_names: list[str] | None = None):
runs_details = self._get_runs_details(test_id)
tables_meta = self._get_tables_metadata(test_id=test_id)

if table_names:
tables_meta = [table for table in tables_meta if table.name in table_names]

graphs = []
releases_filters = set()
for table in tables_meta:
Expand Down
253 changes: 208 additions & 45 deletions frontend/TestRun/Components/ResultTable.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
<script>
import { faMarkdown } from "@fortawesome/free-brands-svg-icons";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { faInfoCircle, faChartLine } from "@fortawesome/free-solid-svg-icons";
import { ResultCellStatusStyleMap } from "../../Common/TestStatus";
import Cell from "./Cell.svelte";
import Fa from "svelte-fa";
import { sendMessage } from "../../Stores/AlertStore";
import ResultsGraph from "../ResultsGraph.svelte";
import queryString from "query-string";
import { onMount, onDestroy } from "svelte";
import GraphFilters from "./GraphFilters.svelte";
import ModalWindow from "../../Common/ModalWindow.svelte";
export let table_name;
export let result;
export let selectedScreenshot;
export let test_id;
let showDescription = false;
let showGraphModal = false;
let graph = null;
let ticks = {};
let releasesFilters = {};
let selectedColumnName = "";
let dateRange = 6;
let startDate = "";
let endDate = "";
let showCustomInputs = false;
let redraw = 0;
const tableStyleToColor = {
"table-success": "green",
Expand All @@ -19,6 +35,11 @@
"table-secondary": "gray",
};
const dispatch_run_click = (e) => {
const runId = e.detail.runId;
window.open(`/tests/scylla-cluster-tests/${runId}`, "_blank", "popup");
};
function styleToColor(classList) {
const classes = classList.split(" ");
for (let className of classes) {
Expand All @@ -37,32 +58,34 @@
const rows = Array.from(table.querySelectorAll("tr"));
rows.forEach((row, rowIndex) => {
const cells = Array.from(row.querySelectorAll("th, td"));
const markdownRow = cells.map(cell => {
let cellText = cell.innerText.trim();
let link = "";
const markdownRow = cells
.map((cell) => {
let cellText = cell.innerText.trim();
let link = "";
const linkElement = cell.querySelector("a");
const buttonElement = cell.querySelector("button");
const linkElement = cell.querySelector("a");
const buttonElement = cell.querySelector("button");
if (linkElement) {
link = linkElement.href;
} else if (buttonElement) {
link = buttonElement.getAttribute("data-link");
}
if (linkElement) {
link = linkElement.href;
} else if (buttonElement) {
link = buttonElement.getAttribute("data-link");
}
cellText = cellText.replace(/#/g, "#&#8203;");
cellText = cellText.replace(/#/g, "#&#8203;");
if (link) {
cellText = `[${cellText}](${link})`;
}
if (link) {
cellText = `[${cellText}](${link})`;
}
const color = styleToColor(cell.className);
if (color) {
cellText = `$$\{\\color{${color}}${cellText}}$$`;
}
const color = styleToColor(cell.className);
if (color) {
cellText = `$$\{\\color{${color}}${cellText}}$$`;
}
return cellText;
}).join(" | ");
return cellText;
})
.join(" | ");
markdown += `| ${markdownRow} |\n`;
if (rowIndex === 0) {
Expand All @@ -83,14 +106,86 @@
function toggleDescription() {
showDescription = !showDescription;
}
async function fetchGraphData() {
try {
const params = queryString.stringify({
testId: test_id,
startDate,
endDate,
"tableNames[]": [table_name],
});
let res = await fetch(`/api/v1/test-results?${params}`);
if (res.status !== 200) {
throw new Error(`HTTP Error ${res.status} trying to fetch test results`);
}
let results = await res.json();
if (results.status !== "ok") {
throw new Error(`API Error: ${results.message}`);
}
const response = results["response"];
if (!response.graphs || response.graphs.length === 0) {
throw new Error("No graph data available for this table");
}
graph = response["graphs"].find((graph) => graph.options.plugins.title.text.endsWith(selectedColumnName));
ticks = response["ticks"];
releasesFilters = Object.fromEntries(response["releases_filters"].map((key) => [key, true]));
} catch (error) {
console.log("Error:", error);
sendMessage("error", `Failed to load graph data: ${error.message}`);
}
}
async function openGraphModal(columnName = "") {
selectedColumnName = columnName;
showGraphModal = true;
await fetchGraphData();
}
function closeGraphModal() {
showGraphModal = false;
graph = null;
selectedColumnName = "";
}
async function handleDateChange(event) {
startDate = event.detail.startDate;
endDate = event.detail.endDate;
if (showGraphModal) {
await fetchGraphData();
redraw++;
}
}
async function handleReleaseChange(event) {
releasesFilters = event.detail.releasesFilters;
redraw++;
}
function handleKeydown(event) {
if (event.key === "Escape" && showGraphModal) {
closeGraphModal();
}
}
onMount(() => {
window.addEventListener("keydown", handleKeydown);
});
onDestroy(() => {
window.removeEventListener("keydown", handleKeydown);
});
</script>

<li class="result-item {result.table_status}">
<div class="result-content">
<div class="table-header">
<h5>{table_name}</h5>
<button class="btn btn-outline-secondary btn-sm p-1" on:click={toggleDescription}>
<Fa icon={faInfoCircle} size="sm"/>
<button class="btn btn-link p-0 ms-1" on:click={toggleDescription}>
<Fa icon={faInfoCircle} size="sm" />
</button>
</div>
{#if showDescription}
Expand All @@ -100,37 +195,90 @@
<div class="table-container">
<table class="table table-sm table-bordered">
<thead class="thead-dark">
<tr>
<th>
<button class="btn btn-outline-success btn-sm p-1" on:click={copyResultTableAsMarkdown}>
<Fa icon={faMarkdown} size="sm"/>
</button>
</th>
{#each result.columns as col}
<th>{col.name} <span class="unit">{col.unit ? `[${col.unit}]` : ''}</span></th>
{/each}
</tr>
</thead>
<tbody>
{#each result.rows as row}
<tr>
<td>{row}</td>
<th>
<button class="btn btn-link p-1" on:click={copyResultTableAsMarkdown}>
<Fa icon={faMarkdown} size="sm" />
</button>
</th>
{#each result.columns as col}
{#key result.table_data[row][col.name]}
<td class="{ResultCellStatusStyleMap[result.table_data[row][col.name]?.status || 'NULL']}">
<Cell cell={result.table_data[row][col.name]} bind:selectedScreenshot/>
</td>
{/key}
<th>
<div class="column-header">
<span
>{col.name}
<span class="unit">{col.unit ? `[${col.unit}]` : ""}</span></span
>
{#if col.unit}
<button
class="btn btn-link p-0 ms-2"
on:click={() => openGraphModal(col.name)}
>
<Fa icon={faChartLine} size="sm" />
</button>
{/if}
</div>
</th>
{/each}
</tr>
{/each}
</thead>
<tbody>
{#each result.rows as row}
<tr>
<td>{row}</td>
{#each result.columns as col}
{#key result.table_data[row][col.name]}
<td
class={ResultCellStatusStyleMap[
result.table_data[row][col.name]?.status || "NULL"
]}
>
<Cell cell={result.table_data[row][col.name]} bind:selectedScreenshot />
</td>
{/key}
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
</li>

{#if showGraphModal}
<ModalWindow widthClass="w-75" on:modalClose={closeGraphModal}>
<div slot="title">
{table_name}
{selectedColumnName ? `- ${selectedColumnName}` : ""}
</div>
<div slot="body" style="min-height: 800px;">
<GraphFilters
bind:dateRange
bind:startDate
bind:endDate
bind:showCustomInputs
bind:releasesFilters
on:dateChange={handleDateChange}
on:releaseChange={handleReleaseChange}
/>
{#if graph}
{#key redraw}
<ResultsGraph
{graph}
{ticks}
{test_id}
width={1400}
height={700}
{releasesFilters}
responsive={true}
on:runClick={dispatch_run_click}
/>
{/key}
{/if}
</div>
</ModalWindow>
{/if}

<style>
.result-item {
margin-bottom: 1rem;
Expand Down Expand Up @@ -194,7 +342,8 @@
font-size: 0.85rem;
}
th, td {
th,
td {
text-align: center;
padding: 0.25rem !important;
}
Expand All @@ -208,5 +357,19 @@
:global(.btn-sm) {
font-size: 0.75rem;
}
</style>
.column-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.column-header button {
opacity: 0.6;
}
.column-header button:hover {
opacity: 1;
}
</style>
3 changes: 2 additions & 1 deletion frontend/TestRun/ResultsGraph.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
export let test_id = "";
export let width = 500;
export let height = 300;
export let responsive = false;
export let releasesFilters = {};
let chart;
let showModal = false;
Expand Down Expand Up @@ -118,7 +119,7 @@
const tickValues = calculateTickValues(xValues, width, ticksGapPx);
graph.options.animation = false;
graph.options.responsive = false;
graph.options.responsive = responsive;
graph.options.lazy = true;
graph.options.plugins.tooltip = {
position: "above",
Expand Down
Loading

0 comments on commit 7a76cf9

Please sign in to comment.