Skip to content

Commit

Permalink
feature(graphs-views): show predefined list of graphs
Browse files Browse the repository at this point in the history
Users are allowed to create 'Graph Views' which can store specific
graphs. Created Graph Views are visible in test's graph window and can
be selected for quick look on saved graphs list. This way saving time
for playing with filters.

Graph Views may have a name and description.

closes: scylladb#554
  • Loading branch information
soyacz committed Jan 9, 2025
1 parent b8e19dd commit c9ea6a1
Show file tree
Hide file tree
Showing 7 changed files with 494 additions and 112 deletions.
34 changes: 33 additions & 1 deletion argus/backend/controller/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,10 +408,42 @@ def test_results():
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)
graph_views = service.get_argus_graph_views(test_id=UUID(test_id))

return {
"status": "ok",
"response": {"graphs": graphs, "ticks": ticks, "releases_filters": releases_filters}
"response": {"graphs": graphs, "ticks": ticks, "releases_filters": releases_filters, "graph_views": graph_views}
}

@bp.route("/create-graph-view", methods=["POST"])
@api_login_required
def create_graph_view():
payload = get_payload(request)
service = ResultsService()
test_id = payload["testId"]
name = payload["name"]
description = payload["description"]
graph_view = service.create_argus_graph_view(test_id=UUID(test_id), name=name, description=description)
return {
"status": "ok",
"response": graph_view
}

@bp.route("/update-graph-view", methods=["POST"])
@api_login_required
def update_graph_view():
payload = get_payload(request)
service = ResultsService()
test_id = payload["testId"]
id = payload["id"]
name = payload["name"]
description = payload["description"]
graphs = payload["graphs"]
graph_view = service.update_argus_graph_view(test_id=UUID(test_id), view_id=UUID(id), name=name, description=description,
graphs=graphs)
return {
"status": "ok",
"response": graph_view
}

@bp.route("/test_run/comment/get", methods=["GET"]) # TODO: remove
Expand Down
8 changes: 8 additions & 0 deletions argus/backend/models/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,11 @@ class ArgusBestResultData(Model):
key = columns.Ascii(primary_key=True) # represents pair column:row
value = columns.Double()
run_id = columns.UUID()

class ArgusGraphView(Model):
__table_name__ = "graph_view_v1"
test_id = columns.UUID(partition_key=True)
id = columns.UUID(primary_key=True)
name = columns.Text()
description = columns.Text()
graphs = columns.Map(key_type=columns.Text(), value_type=columns.Ascii()) # key: graph name, value: graph properties (e.g. size)
3 changes: 2 additions & 1 deletion argus/backend/models/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from cassandra.util import uuid_from_time, unix_time_from_uuid1 # pylint: disable=no-name-in-module

from argus.backend.models.plan import ArgusReleasePlan
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData, ArgusBestResultData
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData, ArgusBestResultData, ArgusGraphView
from argus.backend.models.view_widgets import WidgetHighlights, WidgetComment


Expand Down Expand Up @@ -390,6 +390,7 @@ class WebFileStorage(Model):
ArgusReleasePlan,
WidgetHighlights,
WidgetComment,
ArgusGraphView,
]

USED_TYPES: list[UserType] = [
Expand Down
39 changes: 37 additions & 2 deletions argus/backend/service/results_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
from datetime import datetime, timezone
from functools import partial, cache
from typing import List, Dict, Any
from uuid import UUID
from uuid import UUID, uuid4

from dataclasses import dataclass
from argus.backend.db import ScyllaCluster
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData, ArgusBestResultData, ColumnMetadata
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData, ArgusBestResultData, ColumnMetadata, ArgusGraphView
from argus.backend.plugins.sct.udt import PackageVersion
from argus.backend.service.testrun import TestRunService

Expand Down Expand Up @@ -622,3 +622,38 @@ def get_tests_by_version(self, sut_package_name: str, test_ids: list[UUID]) -> d
'versions': {version: dict(tests) for version, tests in result.items()},
'test_info': test_info
}

def create_argus_graph_view(self, test_id: UUID, name: str, description: str) -> ArgusGraphView:
view_id = uuid4()
graph_view = ArgusGraphView(test_id=test_id, id=view_id)
graph_view.name = name
graph_view.description = description
graph_view.save()
return graph_view

def update_argus_graph_view(self, test_id: UUID, view_id: UUID, name: str, description: str,
graphs: dict[str, str]) -> ArgusGraphView:
try:
graph_view = ArgusGraphView.get(test_id=test_id, id=view_id)
except ArgusGraphView.DoesNotExist:
raise ValueError(f"GraphView with id {view_id} does not exist for test {test_id}")

existing_keys = set(graph_view.graphs.keys())
new_keys = set(graphs.keys())
keys_to_remove = existing_keys - new_keys

for key in keys_to_remove:
ArgusGraphView.objects(test_id=test_id, id=view_id).update(graphs={key: None})

if graphs:
ArgusGraphView.objects(test_id=test_id, id=view_id).update(graphs=graphs)

ArgusGraphView.objects(test_id=test_id, id=view_id).update(
name=name,
description=description
)

return ArgusGraphView.get(test_id=test_id, id=view_id)

def get_argus_graph_views(self, test_id: UUID) -> list[ArgusGraphView]:
return list(ArgusGraphView.objects(test_id=test_id))
31 changes: 30 additions & 1 deletion argus/backend/tests/results_service/test_results_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from uuid import uuid4

import pytest
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData, ColumnMetadata
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData, ColumnMetadata, ArgusGraphView
from argus.backend.plugins.sct.testrun import SCTTestRun
from argus.backend.plugins.sct.udt import PackageVersion
from argus.backend.service.results_service import ResultsService
Expand Down Expand Up @@ -170,3 +170,32 @@ def test_get_tests_by_version_groups_runs_correctly(argus_db):
'started_by': None,
'status': 'created'}}}}}
assert result == expected_result


def test_create_update_argus_graph_view_should_create() -> None:
service = ResultsService()
test_id = uuid4()
service.create_argus_graph_view(test_id, "MyView", "MyDescription")
result = service.get_argus_graph_views(test_id)[0]
assert result is not None
assert result.name == "MyView"
assert result.description == "MyDescription"
assert result.graphs == {}

def test_create_update_argus_graph_view_should_update() -> None:
service = ResultsService()
test_id = uuid4()
graph_view = service.create_argus_graph_view(test_id, "OldName", "OldDesc")
service.update_argus_graph_view(test_id, graph_view.id, "NewName", "NewDesc", {"graph2": "new_data"})
updated = service.get_argus_graph_views(test_id)[0]
assert updated.name == "NewName"
assert updated.description == "NewDesc"
assert updated.graphs == {"graph2": "new_data"}

def test_get_argus_graph_views_should_return_list() -> None:
service = ResultsService()
test_id = uuid4()
service.create_argus_graph_view(test_id, "View1", "Desc1")
service.create_argus_graph_view(test_id, "View2", "Desc2")
views = service.get_argus_graph_views(test_id)
assert len(views) == 2
82 changes: 41 additions & 41 deletions frontend/TestRun/Components/Filters.svelte
Original file line number Diff line number Diff line change
@@ -1,87 +1,87 @@
<script lang="ts">
import {onMount} from 'svelte'
import {onMount} from 'svelte';
export let graphs: any[] = []
export let filteredGraphs: any[] = []
let tableFilters: { level: number; items: string[] }[] = []
let columnFilters: string[] = []
let selectedTableFilters: { name: string; level: number }[] = []
let selectedColumnFilters: string[] = []
export let graphs: any[] = [];
export let filteredGraphs: any[] = [];
let tableFilters: { level: number; items: string[] }[] = [];
let columnFilters: string[] = [];
let selectedTableFilters: { name: string; level: number }[] = [];
let selectedColumnFilters: string[] = [];
function extractTableFilters() {
let fltrs = new Map<number, Set<string>>()
let fltrs = new Map<number, Set<string>>();
for (let g of graphs) {
const parts = g.options.plugins.title.text.split("-").slice(0, -1).map(p => p.trim())
const parts = g.options.plugins.title.text.split("-").slice(0, -1).map(p => p.trim());
parts.forEach((p, i) => {
const lvl = i + 1
if (!fltrs.has(lvl)) fltrs.set(lvl, new Set())
fltrs.get(lvl)?.add(p)
})
const lvl = i + 1;
if (!fltrs.has(lvl)) fltrs.set(lvl, new Set());
fltrs.get(lvl)?.add(p);
});
}
tableFilters = [...fltrs.entries()]
.sort((a, b) => a[0] - b[0])
.map(([level, items]) => ({level, items: [...items]}))
.map(([level, items]) => ({level, items: [...items]}));
}
function extractColumnFilters() {
let fltrs = new Set<string>()
let fltrs = new Set<string>();
for (let g of graphs) {
const parts = g.options.plugins.title.text.split("-").slice(-1).map(p => p.trim())
parts.forEach(p => fltrs.add(p))
const parts = g.options.plugins.title.text.split("-").slice(-1).map(p => p.trim());
parts.forEach(p => fltrs.add(p));
}
columnFilters = [...fltrs]
columnFilters = [...fltrs];
}
function toggleTableFilter(filter: string, level: number) {
const existing = selectedTableFilters.find(f => f.level === level)
const existing = selectedTableFilters.find(f => f.level === level);
if (existing && existing.name === filter) {
selectedTableFilters = selectedTableFilters.filter(f => f.level !== level)
selectedTableFilters = selectedTableFilters.filter(f => f.level !== level);
} else {
selectedTableFilters = [...selectedTableFilters.filter(f => f.level !== level), {name: filter, level}]
selectedTableFilters = [...selectedTableFilters.filter(f => f.level !== level), {name: filter, level}];
}
filterTables()
filterTables();
}
function toggleColumnFilter(filter: string) {
selectedColumnFilters = selectedColumnFilters.includes(filter)
? selectedColumnFilters.filter(f => f !== filter)
: [...selectedColumnFilters, filter]
filterTables()
: [...selectedColumnFilters, filter];
filterTables();
}
function filterTables() {
if (!selectedTableFilters.length && !selectedColumnFilters.length) {
filteredGraphs = graphs
return
filteredGraphs = graphs;
return;
}
filteredGraphs = graphs.filter(g => {
const parts = g.options.plugins.title.text.split("-").map(p => p.trim())
const matchTable = selectedTableFilters.every(f => parts.includes(f.name))
const matchCol = !selectedColumnFilters.length || selectedColumnFilters.some(f => parts.includes(f))
return matchTable && matchCol
})
const parts = g.options.plugins.title.text.split("-").map(p => p.trim());
const matchTable = selectedTableFilters.every(f => parts.includes(f.name));
const matchCol = !selectedColumnFilters.length || selectedColumnFilters.some(f => parts.includes(f));
return matchTable && matchCol;
});
}
function clearTableFilters() {
selectedTableFilters = []
filterTables()
selectedTableFilters = [];
filterTables();
}
function clearColumnFilters() {
selectedColumnFilters = []
filterTables()
selectedColumnFilters = [];
filterTables();
}
function getTableFilterColor(level: number) {
const colors = ["#B8EFFF", "#FECBA1", "#D6B3E6", "#FFE699", "#F0A5C5", "#C4C8CA"]
return colors[(level - 1) % colors.length]
const colors = ["#B8EFFF", "#FECBA1", "#D6B3E6", "#FFE699", "#F0A5C5", "#C4C8CA"];
return colors[(level - 1) % colors.length];
}
onMount(() => {
extractTableFilters()
extractColumnFilters()
filterTables()
})
extractTableFilters();
extractColumnFilters();
filterTables();
});
</script>

<span>Filters:</span>
Expand Down
Loading

0 comments on commit c9ea6a1

Please sign in to comment.