Skip to content

Commit

Permalink
pr comments
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Chubatiuk authored and Andrii Chubatiuk committed Jan 3, 2024
1 parent ffaff62 commit e6bd1a6
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 68 deletions.
2 changes: 2 additions & 0 deletions client/cypress/integration/dashboard/sharing_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe("Dashboard Sharing", () => {
editDashboard();
cy.getByTestId("AddWidgetButton").click();
cy.getByTestId("AddWidgetDialog").within(() => {
cy.get("input").type("Test Query");
cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click();
});
cy.contains("button", "Add to Dashboard").click();
Expand Down Expand Up @@ -183,6 +184,7 @@ describe("Dashboard Sharing", () => {
editDashboard();
cy.getByTestId("AddWidgetButton").click();
cy.getByTestId("AddWidgetDialog").within(() => {
cy.get("input").type("Test Query");
cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click();
});
cy.contains("button", "Add to Dashboard").click();
Expand Down
1 change: 1 addition & 0 deletions client/cypress/integration/dashboard/widget_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe("Widget", () => {
editDashboard();
cy.getByTestId("AddWidgetButton").click();
cy.getByTestId("AddWidgetDialog").within(() => {
cy.get("input").type("Test Query");
cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click();
});
cy.contains("button", "Add to Dashboard").click();
Expand Down
6 changes: 5 additions & 1 deletion redash/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from redash.tasks import record_event as record_event_task
from redash.utils import json_dumps

MAX_PER_PAGE = 250

routes = Blueprint("redash", __name__, template_folder=settings.fix_assets_path("templates"))


Expand Down Expand Up @@ -77,7 +79,9 @@ def get_object_or_404(fn, *args, **kwargs):


def paginate(query_set, page, per_page, serializer, **kwargs):
results = db.paginate(query_set, page=page, per_page=per_page, max_per_page=250)
if per_page > MAX_PER_PAGE:
abort(400, message=f"Page size is out of range (1-{MAX_PER_PAGE})")
results = db.paginate(query_set, page=page, per_page=per_page, max_per_page=MAX_PER_PAGE)

# support for old function based serializers
if isclass(serializer):
Expand Down
46 changes: 29 additions & 17 deletions redash/handlers/organization.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from flask_login import current_user, login_required
from sqlalchemy import distinct, func
from sqlalchemy.sql.expression import select

from redash import models
Expand All @@ -11,23 +12,34 @@
@login_required
def organization_status(org_slug=None):
counters = {
"users": len(models.db.session.scalars(models.User.all(current_org)).all()),
"alerts": len(models.db.session.scalars(models.Alert.all(group_ids=current_user.group_ids)).all()),
"data_sources": len(
models.db.session.scalars(models.DataSource.all(current_org, group_ids=current_user.group_ids)).all()
),
"queries": len(
models.db.session.scalars(
models.Query.all_queries(current_user.group_ids, current_user.id, include_drafts=True)
).all()
),
"dashboards": len(
models.db.session.scalars(
select(models.Dashboard).where(
models.Dashboard.org == current_org, models.Dashboard.is_archived.is_(False)
)
).all()
),
"users": models.db.session.execute(
models.User.all(current_org, columns=[func.count(models.User.id).label("count")])
).first()["count"],
"alerts": models.db.session.execute(
models.Alert.all(
group_ids=current_user.group_ids, columns=[func.count(models.Alert.id).label("count")], distinct=[]
)
).first()["count"],
"data_sources": models.db.session.execute(
models.DataSource.all(
current_org,
group_ids=current_user.group_ids,
columns=[func.count(models.DataSource.id).label("count")],
)
).first()["count"],
"queries": models.db.session.execute(
models.Query.all(
current_user.group_ids,
user_id=current_user.id,
include_drafts=True,
columns=[func.count(distinct(models.Query.id)).label("count")],
)
).first()["count"],
"dashboards": models.db.session.execute(
select(func.count(models.Dashboard.id).label("count")).where(
models.Dashboard.org == current_org, models.Dashboard.is_archived.is_(False)
)
).first()["count"],
}

return json_response(dict(object_counters=counters))
4 changes: 2 additions & 2 deletions redash/handlers/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def get_queries(self, search_term):
multi_byte_search=current_org.get_setting("multi_byte_search_enabled"),
)
else:
results = models.Query.all_queries(self.current_user.group_ids, self.current_user.id, include_drafts=True)
results = models.Query.all(self.current_user.group_ids, self.current_user.id, include_drafts=True)
return filter_by_tags(results, models.Query.tags)

@require_permission("view_query")
Expand Down Expand Up @@ -253,7 +253,7 @@ def get_queries(self, search_term):
multi_byte_search=current_org.get_setting("multi_byte_search_enabled"),
)
else:
return models.Query.all_queries(
return models.Query.all(
self.current_user.group_ids, self.current_user.id, include_drafts=False, include_archived=True
)

Expand Down
63 changes: 36 additions & 27 deletions redash/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,10 @@ def create_with_group(cls, *args, **kwargs):
return data_source

@classmethod
def all(cls, org, group_ids=None):
data_sources = select(cls).where(cls.org == org).order_by(cls.id.asc())
def all(cls, org, group_ids=None, columns=None):
if columns is None:
columns = [cls]
data_sources = select(*columns).where(cls.org == org)

if group_ids:
data_sources = data_sources.join(DataSourceGroup).where(DataSourceGroup.group_id.in_(group_ids))
Expand Down Expand Up @@ -363,9 +365,11 @@ def to_dict(self):
}

@classmethod
def unused(cls, days=7):
def unused(cls, columns=None, days=7):
if columns is None:
columns = [cls.id]
age_threshold = datetime.now() - timedelta(days=days)
return select(cls.id).outerjoin(Query).where(Query.id.is_(None), cls.retrieved_at < age_threshold)
return select(*columns).outerjoin(Query).where(Query.id.is_(None), cls.retrieved_at < age_threshold)

@classmethod
def get_latest(cls, data_source, query, max_age=0):
Expand Down Expand Up @@ -540,24 +544,22 @@ def create(cls, **kwargs):
return query

@classmethod
def all_queries(cls, group_ids, user_id=None, include_drafts=False, include_archived=False, columns=None):
def all(cls, group_ids, user_id=None, include_drafts=False, include_archived=False, columns=None):
distincts = None
if columns is None:
columns = [cls, User, QueryResult.runtime, QueryResult.retrieved_at]
query_ids = db.session.scalars(
select(cls.id)
.distinct(cls.id)
.join(DataSourceGroup, Query.data_source_id == DataSourceGroup.data_source_id)
.where(Query.is_archived.is_(include_archived))
.where(DataSourceGroup.group_id.in_(group_ids))
).all()
columns = [cls, User]
distincts = [cls.id]
queries = (
select(*columns)
.where(cls.id.in_(query_ids))
# Adding outer joins to be able to order by relationship
.outerjoin(User, User.id == Query.user_id)
.outerjoin(QueryResult, QueryResult.id == Query.latest_query_data_id)
.options(contains_eager(Query.user), contains_eager(Query.latest_query_data))
.outerjoin(User, User.id == cls.user_id)
.outerjoin(QueryResult, QueryResult.id == cls.latest_query_data_id)
.outerjoin(DataSourceGroup, DataSourceGroup.data_source_id == cls.data_source_id)
.where(Query.is_archived.is_(include_archived))
.where(DataSourceGroup.group_id.in_(group_ids))
)
if distincts is not None:
queries = queries.distinct(*distincts).order_by(*distincts)

if not include_drafts:
queries = queries.where(or_(Query.is_draft.is_(False), Query.user_id == user_id))
Expand All @@ -566,16 +568,14 @@ def all_queries(cls, group_ids, user_id=None, include_drafts=False, include_arch
@classmethod
def favorites(cls, user, base_query=None):
if base_query is None:
base_query = cls.all_queries(user.group_ids, user.id, include_drafts=True)
base_query = cls.all(user.group_ids, user.id, include_drafts=True)
return base_query.join(Favorite, Favorite.object_id == Query.id).where(
Favorite.user_id == user.id, Favorite.object_type == "Query"
)

@classmethod
def all_tags(cls, user, include_drafts=False):
queries = cls.all_queries(
group_ids=user.group_ids, user_id=user.id, include_drafts=include_drafts, columns=[cls.id]
)
queries = cls.all(group_ids=user.group_ids, user_id=user.id, include_drafts=include_drafts, columns=[cls.id])

tag_column = func.unnest(cls.tags).label("tag")
usage_column = func.count(1).label("usage_count")
Expand All @@ -590,7 +590,7 @@ def all_tags(cls, user, include_drafts=False):

@classmethod
def by_user(cls, user):
return cls.all_queries(user.group_ids, user.id).where(Query.user == user)
return cls.all(user.group_ids, user.id).where(Query.user == user)

@classmethod
def by_api_key(cls, api_key):
Expand Down Expand Up @@ -670,7 +670,7 @@ def search(
include_archived=False,
multi_byte_search=False,
):
all_queries = cls.all_queries(
all_queries = cls.all(
group_ids,
user_id=user_id,
include_drafts=include_drafts,
Expand Down Expand Up @@ -867,7 +867,12 @@ class Favorite(TimestampMixin, db.Model):

@classmethod
def is_favorite(cls, user, object):
return len(db.session.scalars(select(cls.id).where(cls.object == object, cls.user_id == user)).all()) > 0
return (
db.session.execute(
select(func.count(cls.id).label("count")).where(cls.object == object, cls.user_id == user)
).first()["count"]
> 0
)

@classmethod
def are_favorites(cls, user, objects):
Expand Down Expand Up @@ -946,10 +951,14 @@ class Alert(TimestampMixin, BelongsToOrgMixin, db.Model):
__tablename__ = "alerts"

@classmethod
def all(cls, group_ids):
def all(cls, group_ids, columns=None, distinct=None):
if columns is None:
columns = [cls]
if distinct is None:
distinct = [cls.id]
return (
select(cls)
.distinct(cls.id)
select(*columns)
.distinct(*distinct)
.join(Query)
.join(DataSourceGroup, DataSourceGroup.data_source_id == Query.data_source_id)
.where(DataSourceGroup.group_id.in_(group_ids))
Expand Down
10 changes: 6 additions & 4 deletions redash/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,10 @@ def permissions(self):
return list(itertools.chain(*[g.permissions for g in groups]))

@classmethod
def get_by_org(cls, org):
return select(cls).where(cls.org == org)
def get_by_org(cls, org, columns=None):
if columns is None:
columns = [cls]
return select(*columns).where(cls.org == org)

@classmethod
def get_by_id(cls, _id):
Expand All @@ -192,8 +194,8 @@ def get_by_api_key_and_org(cls, api_key, org):
return db.session.scalars(cls.get_by_org(org).where(cls.api_key == api_key)).one()

@classmethod
def all(cls, org):
return cls.get_by_org(org).where(cls.disabled_at.is_(None))
def all(cls, org, columns=None):
return cls.get_by_org(org, columns).where(cls.disabled_at.is_(None))

@classmethod
def all_disabled(cls, org):
Expand Down
11 changes: 7 additions & 4 deletions redash/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from rq import Queue, Worker
from rq.job import Job
from rq.registry import StartedJobRegistry
from sqlalchemy import func
from sqlalchemy.sql import text
from sqlalchemy.sql.expression import select

Expand All @@ -18,17 +19,19 @@ def get_redis_status():


def count(cls):
return len(db.session.scalars(select(cls)).all())
return db.session.execute(select(func.count(cls.id).label("count"))).first()["count"]


def get_object_counts():
status = {}
status["queries_count"] = count(Query)
if settings.FEATURE_SHOW_QUERY_RESULTS_COUNT:
status["query_results_count"] = count(QueryResult)
status["unused_query_results_count"] = len(
db.session.scalars(QueryResult.unused(settings.QUERY_RESULTS_CLEANUP_MAX_AGE)).all()
)
status["unused_query_results_count"] = db.session.execute(
QueryResult.unused(
columns=[func.count(QueryResult.id).label("count")], days=settings.QUERY_RESULTS_CLEANUP_MAX_AGE
)
).first()["count"]
status["dashboards_count"] = count(Dashboard)
status["widgets_count"] = count(Widget)
return status
Expand Down
2 changes: 1 addition & 1 deletion redash/tasks/queries/maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def cleanup_query_results():
settings.QUERY_RESULTS_CLEANUP_MAX_AGE,
)

unused_query_results = models.QueryResult.unused(settings.QUERY_RESULTS_CLEANUP_MAX_AGE)
unused_query_results = models.QueryResult.unused(days=settings.QUERY_RESULTS_CLEANUP_MAX_AGE)
deleted_count = len(
models.db.session.scalars(
delete(models.QueryResult)
Expand Down
24 changes: 12 additions & 12 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,12 +336,12 @@ def test_archived_query_doesnt_return_in_all(self):

query.latest_query_data = query_result
groups = db.session.scalars(select(models.Group).where(models.Group.id.in_(query.groups))).all()
self.assertIn(query, db.session.scalars(models.Query.all_queries([g.id for g in groups])))
self.assertIn(query, db.session.scalars(models.Query.all([g.id for g in groups])))
self.assertIn(query, models.Query.outdated_queries())
db.session.flush()
query.archive()

self.assertNotIn(query, db.session.scalars(models.Query.all_queries([g.id for g in groups])).all())
self.assertNotIn(query, db.session.scalars(models.Query.all([g.id for g in groups])).all())
self.assertNotIn(query, models.Query.outdated_queries())

def test_removes_associated_widgets_from_dashboards(self):
Expand Down Expand Up @@ -414,20 +414,20 @@ def test_returns_only_queries_in_given_groups(self):
]
)
db.session.flush()
self.assertIn(q1, db.session.scalars(models.Query.all_queries([group1.id])).all())
self.assertNotIn(q2, db.session.scalars(models.Query.all_queries([group1.id])).all())
self.assertIn(q1, db.session.scalars(models.Query.all_queries([group1.id, group2.id])).all())
self.assertIn(q2, db.session.scalars(models.Query.all_queries([group1.id, group2.id])).all())
self.assertIn(q1, db.session.scalars(models.Query.all([group1.id])).all())
self.assertNotIn(q2, db.session.scalars(models.Query.all([group1.id])).all())
self.assertIn(q1, db.session.scalars(models.Query.all([group1.id, group2.id])).all())
self.assertIn(q2, db.session.scalars(models.Query.all([group1.id, group2.id])).all())

def test_skips_drafts(self):
q = self.factory.create_query(is_draft=True)
self.assertNotIn(q, db.session.scalars(models.Query.all_queries([self.factory.default_group.id])).all())
self.assertNotIn(q, db.session.scalars(models.Query.all([self.factory.default_group.id])).all())

def test_includes_drafts_of_given_user(self):
q = self.factory.create_query(is_draft=True)
self.assertIn(
q,
db.session.scalars(models.Query.all_queries([self.factory.default_group.id], user_id=q.user_id)).all(),
db.session.scalars(models.Query.all([self.factory.default_group.id], user_id=q.user_id)).all(),
)

def test_order_by_relationship(self):
Expand All @@ -436,12 +436,12 @@ def test_order_by_relationship(self):
self.factory.create_query(user=u1)
self.factory.create_query(user=u2)
db.session.commit()
# have to reset the order here with None since all_queries orders by
# have to reset the order here with None since all orders by
# created_at by default
base = models.Query.all_queries([self.factory.default_group.id]).order_by(None)
qs1 = db.session.scalars(base.order_by(models.User.name)).all()
base = models.Query.all([self.factory.default_group.id]).order_by(None)
qs1 = db.session.scalars(base.order_by(models.User.name).distinct(models.User.name)).all()
self.assertEqual(["alice", "bob"], [q.user.name for q in qs1])
qs2 = db.session.scalars(base.order_by(models.User.name.desc())).all()
qs2 = db.session.scalars(base.order_by(models.User.name.desc()).distinct(models.User.name)).all()
self.assertEqual(["bob", "alice"], [q.user.name for q in qs2])

def test_update_query_hash_basesql_with_options(self):
Expand Down

0 comments on commit e6bd1a6

Please sign in to comment.