Skip to content

Commit

Permalink
feature(view): graphs widget
Browse files Browse the repository at this point in the history
Introducing Graphs Widget which displays view's tests Graphs Views
(added previously in test's graphs window).

closes: scylladb/qa-tasks#1844
  • Loading branch information
soyacz committed Jan 23, 2025
1 parent 681f2e6 commit fee8a8a
Show file tree
Hide file tree
Showing 4 changed files with 334 additions and 0 deletions.
2 changes: 2 additions & 0 deletions argus/backend/controller/view_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
)
from argus.backend.controller.views_widgets.highlights import bp as highlights_bp
from argus.backend.controller.views_widgets.summary import bp as summary_bp
from argus.backend.controller.views_widgets.graphs import bp as graphs_bp
from argus.backend.error_handlers import handle_api_exception
from argus.backend.models.web import User
from argus.backend.service.stats import ViewStatsCollector
Expand All @@ -18,6 +19,7 @@
LOGGER = logging.getLogger(__name__)
bp.register_blueprint(highlights_bp)
bp.register_blueprint(summary_bp)
bp.register_blueprint(graphs_bp)
bp.register_error_handler(Exception, handle_api_exception)


Expand Down
66 changes: 66 additions & 0 deletions argus/backend/controller/views_widgets/graphs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from uuid import UUID
from datetime import datetime, timezone

from flask import Blueprint, request

from argus.backend.models.web import ArgusUserView, ArgusTest
from argus.backend.service.results_service import ResultsService
from argus.backend.service.user import api_login_required
bp = Blueprint("graphs", __name__, url_prefix="/widgets")

@bp.route("/graphs/graph_views", methods=["GET"])
@api_login_required
def get_graph_views():
view_id = UUID(request.args.get("view_id"))
view: ArgusUserView = ArgusUserView.get(id=view_id)
start_date = request.args.get("start_date")
end_date = request.args.get("end_date")
service = ResultsService()
response = {}
tests_details = {}

for test_id in view.tests:
test_uuid = test_id
graph_views = service.get_argus_graph_views(test_uuid)
if graph_views:
test_name = ArgusTest.get(id=test_uuid).name
tests_details[str(test_id)] = {"name": test_name}
view_data = []

for graph_view in graph_views:
# Get unique table names from all graphs in the view
table_names = set()
for graph_name in graph_view.graphs.keys():
table_name = graph_name.rsplit(" - ", 1)[0]
table_names.add(table_name)

# Get graphs data for these tables
start_dt = datetime.fromisoformat(start_date).astimezone(timezone.utc) if start_date else None
end_dt = datetime.fromisoformat(end_date).astimezone(timezone.utc) if end_date else None
graphs, ticks, releases_filters = service.get_test_graphs(
test_id=test_uuid,
start_date=start_dt,
end_date=end_dt,
table_names=list(table_names)
)

# filter out graphs that are not in the graph views
graphs = [graph for graph in graphs if graph["options"]["plugins"]["title"]["text"] in graph_view.graphs.keys()]

if graphs:
view_data.append({
"id": str(graph_view.id),
"name": graph_view.name,
"description": graph_view.description,
"graphs": graphs,
"ticks": ticks,
"releases_filters": releases_filters
})

response[str(test_id)] = view_data

return {
"status": "ok",
"response": response,
"tests_details": tests_details
}
7 changes: 7 additions & 0 deletions frontend/Common/ViewTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {subUnderscores, titleCase} from "./TextUtils";
import ViewHighlights from "../Views/Widgets/ViewHighlights/ViewHighlights.svelte";
import IntegerValue from "../Views/WidgetSettingTypes/IntegerValue.svelte";
import SummaryWidget from "../Views/Widgets/SummaryWidget/SummaryWidget.svelte";
import GraphWidget from "../Views/Widgets/GraphsWidget/GraphsWidget.svelte";


export class Widget {
constructor(position = -1, type = "testDashboard", settings = {}) {
Expand Down Expand Up @@ -120,6 +122,11 @@ export const WIDGET_TYPES = {
}
},
},
graphs: {
type: GraphWidget,
friendlyName: "Graphs Views",
settingDefinitions: {}
},
};


Expand Down
259 changes: 259 additions & 0 deletions frontend/Views/Widgets/GraphsWidget/GraphsWidget.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
<script lang="ts">
import { sendMessage } from "../../../Stores/AlertStore";
import ResultsGraph from "../../../TestRun/ResultsGraph.svelte";
import GraphFilters from "../../../TestRun/Components/GraphFilters.svelte";
import Filters from "../../../TestRun/Components/Filters.svelte";
import queryString from "query-string";
export let settings: any = {};
export let testIds: string[] = [];
export let dashboardObject;
let startDate = "";
let endDate = "";
let dateRange = 6;
let width = 500;
let height = 350;
let releasesFilters = {};
let testViews: Record<string, any[]> = {};
let expandedTests = new Set();
let expandedViews = new Set();
let filteredGraphsByTest: Record<string, Record<string, any[]>> = {};
let graphCounter = 0; // for chart id's
let testsDetails: Record<string, any> = {};
const generateGraphId = () => {
graphCounter += 1;
return graphCounter;
};
async function fetchGraphViews() {
try {
const params = {
view_id: dashboardObject.id,
start_date: startDate,
end_date: endDate,
};
const queryStr = queryString.stringify(params);
const res = await fetch(`/api/v1/views/widgets/graphs/graph_views?${queryStr}`);
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const data = await res.json();
if (data.status !== "ok") throw new Error(data.message);
testViews = data.response;
testsDetails = data.tests_details;
// Collect all unique releases from all tests
const allReleases = new Set<string>();
Object.values(data.response).forEach((testData: any) => {
testData.forEach((testViewData: any) => {
if (testViewData.releases_filters) {
testViewData.releases_filters.forEach((release: string) => allReleases.add(release));
}
});
});
// Initialize releases filters with all unique releases
releasesFilters = Object.fromEntries(Array.from(allReleases).map((r: string) => [r, true]));
graphCounter = 0;
// Filter out tests without graph views and add unique IDs to graphs
testViews = Object.fromEntries(
Object.entries(data.response)
.filter(([_, views]) => views && views.length > 0)
.map(([testId, views]) => [
testId,
views.map((view) => ({
...view,
graphs: view.graphs.map((graph) => ({
...graph,
uniqueId: generateGraphId(),
})),
})),
])
);
// Initialize filtered graphs for each test and view
filteredGraphsByTest = {};
Object.entries(testViews).forEach(([testId, views]) => {
// Expand test by default
expandedTests.add(testId);
filteredGraphsByTest[testId] = {};
views.forEach((view) => {
// Expand view by default
expandedViews.add(view.id);
filteredGraphsByTest[testId][view.id] = view.graphs;
});
});
// Trigger reactivity
expandedTests = expandedTests;
expandedViews = expandedViews;
} catch (e) {
sendMessage("error", "Failed to fetch graph views", "GraphsWidget");
console.error(e);
}
}
function handleDateChange(event) {
startDate = event.detail.startDate;
endDate = event.detail.endDate;
fetchGraphViews();
}
function handleReleaseChange() {
// Trigger reactivity
filteredGraphsByTest = filteredGraphsByTest;
}
function toggleTest(testId: string) {
if (expandedTests.has(testId)) {
expandedTests.delete(testId);
// Also collapse all views under this test
testViews[testId].forEach((view) => expandedViews.delete(view.id));
} else {
expandedTests.add(testId);
// Also expand all views under this test
testViews[testId].forEach((view) => expandedViews.add(view.id));
}
expandedTests = expandedTests; // Trigger reactivity
expandedViews = expandedViews;
}
function toggleView(viewId: string) {
if (expandedViews.has(viewId)) {
expandedViews.delete(viewId);
} else {
expandedViews.add(viewId);
}
expandedViews = expandedViews;
}
function handleFilterChange(testId: string, viewId: string, filteredGraphs: any[]) {
// Add unique IDs to filtered graphs if they don't have them
filteredGraphsByTest[testId][viewId] = filteredGraphs.map((graph) => ({
...graph,
uniqueId: graph.uniqueId || generateGraphId(),
}));
filteredGraphsByTest = filteredGraphsByTest; // Trigger reactivity
}
</script>

<div class="container-fluid p-3">
<GraphFilters
bind:dateRange
bind:releasesFilters
on:dateChange={handleDateChange}
on:releaseChange={handleReleaseChange}
/>

{#if Object.keys(testViews).length === 0}
<div class="alert alert-info text-center my-4">No graph views available. Add them in test's graphs window.</div>
{:else}
<div class="accordion" id="graphsAccordion">
{#each Object.entries(testViews) as [testId, views]}
<div class="card mb-3">
<div class="card-header bg-light" on:click={() => toggleTest(testId)}>
<h5 class="mb-0 d-flex align-items-center">
<i class="fas fa-chevron-{expandedTests.has(testId) ? 'down' : 'right'} me-2" />
<span>{testsDetails[testId]?.name || `Test ID: ${testId}`}</span>
</h5>
</div>

<div class="collapse {expandedTests.has(testId) ? 'show' : ''}" id="test-{testId}">
<div class="card-body">
{#each views as view}
<div class="card mb-3">
<div class="card-header bg-transparent" on:click={() => toggleView(view.id)}>
<div class="d-flex align-items-center">
<i
class="fas fa-chevron-{expandedViews.has(view.id)
? 'down'
: 'right'} me-2"
/>
<h6 class="mb-0">{view.name}</h6>
</div>
</div>

<div
class="collapse {expandedViews.has(view.id) ? 'show' : ''}"
id="view-{view.id}"
>
<div class="card-body">
{#if view.description}
<p class="text-muted mb-3">{view.description}</p>
{/if}

<Filters
graphs={view.graphs}
bind:filteredGraphs={filteredGraphsByTest[testId][view.id]}
on:filterChange={() =>
handleFilterChange(
testId,
view.id,
filteredGraphsByTest[testId][view.id]
)}
/>
<div class="charts-container">
{#key filteredGraphsByTest[testId][view.id]}
{#each filteredGraphsByTest[testId][view.id] as graph (graph.uniqueId)}
<div class="chart-container"
class:big-size={filteredGraphsByTest[testId][view.id].length < 2}>
<ResultsGraph
{graph}
index={graph.uniqueId}
ticks={view.ticks}
{releasesFilters}
height={filteredGraphsByTest[testId][view.id].length === 1
? 600
: height}
width={filteredGraphsByTest[testId][view.id].length === 1
? 1000
: width}
test_id={testId}
/>
</div>
{/each}
{/key}
</div>
</div>
</div>
</div>
{/each}
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>

<style>
.charts-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.chart-container {
display: flex;
justify-content: center;
align-items: center;
margin: 10px;
position: relative;
width: 520px; /* Fixed width to ensure proper wrapping */
}
.big-size {
width: 90%;
}
:global(.card-header) {
cursor: pointer;
}
:global(.card-header:hover) {
background-color: var(--bs-gray-200) !important;
}
</style>

0 comments on commit fee8a8a

Please sign in to comment.