Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement(ViewDashboard): Per-widget item filtering #574

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion argus/backend/controller/view_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,11 @@ def view_stats():
version = request.args.get("productVersion", None)
include_no_version = bool(int(request.args.get("includeNoVersion", True)))
force = bool(int(request.args.get("force", 0)))
widget_id = request.args.get("widgetId", None)
if widget_id:
widget_id = int(widget_id)
collector = ViewStatsCollector(view_id=view_id, filter=version)
stats = collector.collect(limited=limited, force=force, include_no_version=include_no_version)
stats = collector.collect(limited=limited, force=force, include_no_version=include_no_version, widget_id=widget_id)

res = jsonify({
"status": "ok",
Expand Down Expand Up @@ -164,3 +167,13 @@ def view_resolve(view_id: str):
"status": "ok",
"response": res
}

@bp.route("/<string:view_id>/resolve/tests", methods=["GET"])
@api_login_required
def view_resolve_tests(view_id: str):
service = UserViewService()
res = service.resolve_view_tests(view_id)
return {
"status": "ok",
"response": res
}
3 changes: 3 additions & 0 deletions argus/backend/service/planner_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class PlanningService:
{
"position": 1,
"type": "githubIssues",
"filter": [],
"settings": {
"submitDisabled": True,
"aggregateByIssue": True
Expand All @@ -89,6 +90,7 @@ class PlanningService:
{
"position": 2,
"type": "releaseStats",
"filter": [],
"settings": {
"horizontal": False,
"displayExtendedStats": True,
Expand All @@ -98,6 +100,7 @@ class PlanningService:
{
"position": 3,
"type": "testDashboard",
"filter": [],
"settings": {
"targetVersion": True,
"versionsIncludeNoVersion": False,
Expand Down
12 changes: 10 additions & 2 deletions argus/backend/service/stats.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from collections import defaultdict
from functools import reduce
import json
import logging

from datetime import datetime
from typing import TypedDict
from typing import Any, TypedDict
from uuid import UUID

from cassandra.cqlengine.models import Model
Expand Down Expand Up @@ -531,11 +532,18 @@ def __init__(self, view_id: UUID, filter: str | None = None) -> None:
self.view_id = view_id
self.filter = filter

def collect(self, limited=False, force=False, include_no_version=False) -> dict:
def collect(self, limited=False, force=False, include_no_version = False, widget_id: int = None) -> dict:
self.view: ArgusUserView = ArgusUserView.get(id=self.view_id)
widget: dict[str, Any] | None = None
if isinstance(widget_id, int):
settings = json.loads(self.view.widget_settings)
widget = next((widget for widget in settings if widget["position"] == widget_id), None)
all_tests: list[ArgusTest] = []
for slice in chunk(self.view.tests):
all_tests.extend(ArgusTest.filter(id__in=slice).all())

if widget and widget.get("filter"):
all_tests = [test for test in all_tests if any(str(test[key]) in widget["filter"] for key in ["id", "group_id", "release_id"])]
build_ids = reduce(lambda acc, test: acc[test.plugin_name or "unknown"].append(test.build_system_id) or acc, all_tests, defaultdict(list))
self.view_rows = [futures for plugin in all_plugin_models()
for futures in plugin.get_stats_for_release(release=self.view, build_ids=build_ids.get(plugin._plugin_name, []))]
Expand Down
20 changes: 18 additions & 2 deletions frontend/AdminPanel/ViewWidget.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
<script>
import Fa from "svelte-fa";
import { WIDGET_TYPES, Widget } from "../Common/ViewTypes";
import { faEdit, faList, faTrash } from "@fortawesome/free-solid-svg-icons";
import { faEdit, faList, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
import { createEventDispatcher, onMount } from "svelte";
import { titleCase } from "../Common/TextUtils";


/**
* @type {Widget}
*/
export let widgetSettings = {};
export let items = [];

let editingSettings = false;
const dispatch = createEventDispatcher();
Expand All @@ -24,7 +26,10 @@
});
};

onMount(() => populateWidgetSettings());
onMount(populateWidgetSettings);
onMount(() => {
if (!widgetSettings.filter) widgetSettings.filter = [];
});
</script>

<div class="mb-2 rounded bg-white p-2">
Expand Down Expand Up @@ -62,6 +67,17 @@
</div>
</div>
<div>
<div>
<div class="mb-2">
Item Filter <span title="Only selected items will be shown in the widget. No selection - Show all"><Fa icon={faQuestionCircle}/></span>
<button class="btn btn-sm btn-outline-danger" on:click={() => (widgetSettings.filter = [])}>Clear All</button>
</div>
<select class="form-select" multiple size=10 bind:value={widgetSettings.filter}>
{#each items as item}
<option value="{item.id}"><span class="fw-bold">[{titleCase(item.type)}]</span> {item.pretty_name || item.name}</option>
{/each}
</select>
</div>
{#each Object.entries(WIDGET_DEF.settingDefinitions) as [settingName, definition] (settingName)}
{#if typeof definition.type !== "string"}
<svelte:component this={definition.type} bind:settings={widgetSettings.settings} {definition} {settingName} />
Expand Down
2 changes: 1 addition & 1 deletion frontend/AdminPanel/ViewsManager.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@
</button>
</div>
{#each newWidgets as widget (widget.position)}
<ViewWidget bind:widgetSettings={widget} on:removeWidget={removeWidget}/>
<ViewWidget bind:widgetSettings={widget} items={newView.items} on:removeWidget={removeWidget}/>
{/each}
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions frontend/Common/ViewTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ export class Widget {
this.position = position;
this.type = type;
this.settings = settings;
this.filter = [];
}
}

export const GLOBAL_STATS_KEY = "da39a3ee5e6b4b0d3255bfef95601890afd80709";

export const WIDGET_TYPES = {
UNSUPPORTED: {
Expand Down
2 changes: 2 additions & 0 deletions frontend/ReleaseDashboard/TestDashboard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import { timestampToISODate } from "../Common/DateUtils";
export let dashboardObject;
export let dashboardObjectType = "release";
export let widgetId;
export let clickedTests = {};
export let productVersion;
export let settings = {};
Expand Down Expand Up @@ -142,6 +143,7 @@
viewId: dashboardObject.id,
limited: new Number(false),
force: new Number(true),
widgetId: widgetId,
includeNoVersion: new Number(versionsIncludeNoVersion),
productVersion: productVersion ?? "",
});
Expand Down
5 changes: 3 additions & 2 deletions frontend/ReleasePlanner/ReleasePlan.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
import ViewDashboard from "../Views/ViewDashboard.svelte";
import ReleaseStats from "../Stats/ReleaseStats.svelte";
import { faEdit } from "@fortawesome/free-regular-svg-icons";
import { GLOBAL_STATS_KEY } from "../Common/ViewTypes";


export let plan;
export let users;
export let expandedPlans;
let planStats;
let planStats = {};

let owner;
$: owner = getUser(plan.owner, users);
Expand Down Expand Up @@ -66,7 +67,7 @@
{plan.target_version}
{#if planStats}
<div>
<ReleaseStats releaseStats={planStats} />
<ReleaseStats releaseStats={planStats[GLOBAL_STATS_KEY]} />
</div>
{/if}
</div>
Expand Down
98 changes: 82 additions & 16 deletions frontend/Views/ViewDashboard.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
<script>
import { WIDGET_TYPES } from "../Common/ViewTypes";
import { GLOBAL_STATS_KEY, WIDGET_TYPES } from "../Common/ViewTypes";
import { sendMessage } from "../Stores/AlertStore";
import sha1 from "js-sha1";
export let view;
export let stats = undefined;
export let stats = {};
export let productVersion;
let clickedTests = {};
let resolvedTests = [];
const versionDispatch = {
GLOBAL_STATS_KEY: productVersion,
};

console.log(versionDispatch);

const handleTestClick = function (detail) {
if (detail.start_time == 0) {
Expand All @@ -20,6 +27,40 @@
}
};

const resolveViewTests = async function() {
if (resolvedTests.length > 0) {
return resolvedTests;
}
try {
let response = await fetch(`/api/v1/views/${view.id}/resolve/tests`);
if (response.status !== 200) {
throw new Error("Non-200 status code returned from API.");
}
let json = await response.json();
if (json.status === "ok") {
resolvedTests = json.response;
return resolvedTests;
} else {
throw json;
}
} catch (error) {
if (error?.status === "error") {
sendMessage(
"error",
`API Error resolving view tests.\nMessage: ${error.response.arguments[0]}`,
"ViewDashboard::resolveViewTests"
);
} else {
sendMessage(
"error",
"A backend error occurred during view test resolution.",
"ViewDashboard::resolveViewTests"
);
console.log(error);
}
}
};

const handleDeleteRequest = function(e) {
let key = e.detail.id;
if (clickedTests[key]) {
Expand All @@ -45,6 +86,26 @@
});
};

const calculateWidgetStatsKey = function (widget) {
return sha1((widget.filter ?? []).join(""));
};

const filterViewForWidget = async function (widget) {
let viewCopy = structuredClone(view);
if (!widget.filter || widget.filter.length === 0) {
return viewCopy;
}
let tests = await resolveViewTests();
tests = tests.reduce((acc, test) => {
acc[test.id] = test;
return acc;
}, {});
viewCopy.tests = viewCopy.tests.filter((item) => {
const test = tests[item.id] ?? {};
return ["id", "release_id", "group_id"].some((key) => widget.filter.includes(test[key]));
});
return viewCopy;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this way we return view object in case of no filters and view copy in case of filters present.
Maybe we should align it to not cause future issues?
Or even to add new filteredTests attribute which would be used in widgets?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was deliberating whether or not always return a copy, so I think for safety we should always return a copy here. Fixed

};
</script>

<div class="rounded bg-white m-2 shadow-sm p-2">
Expand All @@ -54,20 +115,25 @@
<div class="mb-2">
{#each view.widget_settings as widget}
<div class="mb-2">
<svelte:component
this={WIDGET_TYPES[widget.type]?.type ?? WIDGET_TYPES.UNSUPPORTED.type}
dashboardObject={view}
dashboardObjectType="view"
settings={widget.settings}
bind:stats={stats}
bind:productVersion={productVersion}
bind:clickedTests={clickedTests}
on:statsUpdate
on:testClick={(e) => handleTestClick(e.detail)}
on:versionChange
on:quickSelect={handleQuickSelect}
on:deleteRequest={handleDeleteRequest}
/>
{#await filterViewForWidget(widget)}
<span class="spinner-grow spinner-grow-sm"></span> Loading...
{:then view}
<svelte:component
this={WIDGET_TYPES[widget.type]?.type ?? WIDGET_TYPES.UNSUPPORTED.type}
widgetId={widget.position}
dashboardObject={view}
dashboardObjectType="view"
settings={widget.settings}
bind:stats={stats[calculateWidgetStatsKey(widget)]}
bind:productVersion={versionDispatch[calculateWidgetStatsKey(widget)]}
bind:clickedTests={clickedTests}
on:statsUpdate
on:testClick={(e) => handleTestClick(e.detail)}
on:versionChange
on:quickSelect={handleQuickSelect}
on:deleteRequest={handleDeleteRequest}
/>
{/await}
</div>
{:else}
<div class="alert alert-danger">No widgets defined for view!</div>
Expand Down
2 changes: 2 additions & 0 deletions frontend/Views/Widgets/ViewTestDashboard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export let settings;
export let productVersion;
export let clickedTests;
export let widgetId;

import TestDashboard from "../../ReleaseDashboard/TestDashboard.svelte";
import TestPopoutSelector from "../../ReleaseDashboard/TestPopoutSelector.svelte";
Expand All @@ -16,6 +17,7 @@
dashboardObject={dashboardObject}
{dashboardObjectType}
{settings}
{widgetId}
bind:productVersion={productVersion}
bind:stats={stats}
bind:clickedTests={clickedTests}
Expand Down
Loading