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

chore: Migrate /superset/recent_activity/<user_id>/ to /api/v1/ #22789

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e606bf4
Create new endpoint and replace on frontend
jfrag1 Jan 19, 2023
68a321f
Formatting
jfrag1 Jan 19, 2023
731a5c6
Replace openapi.json
jfrag1 Jan 19, 2023
6f0777b
Fix openapi.json
jfrag1 Jan 19, 2023
fe6782d
Oops
jfrag1 Jan 19, 2023
5552e7d
Revert openapi.json changes
jfrag1 Jan 19, 2023
6695fb8
Fix lint
jfrag1 Jan 19, 2023
4f6a128
Add apache licensing text to new file
jfrag1 Jan 19, 2023
1f82cd5
Fix formatting
jfrag1 Jan 19, 2023
ccdc53d
Try this
jfrag1 Jan 19, 2023
e0db4a1
Fix mypy
jfrag1 Jan 19, 2023
de91238
Fix isort
jfrag1 Jan 19, 2023
1f9862e
Fix create_test_dashboard
jfrag1 Jan 19, 2023
d7c4018
Debugging
jfrag1 Jan 19, 2023
55989cf
Clean up created dashboards in tests
jfrag1 Jan 19, 2023
06ddbb9
Fix unrelated test that left a hanging dash
jfrag1 Jan 19, 2023
7d347d9
session.merge -> session.add
jfrag1 Jan 19, 2023
abdfa6b
Lower expected count to account for removed hanging dash
jfrag1 Jan 19, 2023
ff7e286
Cleanup another leftover dash
jfrag1 Jan 19, 2023
9159d05
Cleanup all the dashboards
jfrag1 Jan 19, 2023
5ec97a0
Debugging
jfrag1 Jan 19, 2023
1185660
Try debugging again
jfrag1 Jan 19, 2023
37585bd
Try deleting before test
jfrag1 Jan 19, 2023
391752b
Remove debugging code
jfrag1 Jan 19, 2023
32307ee
description -> summary
jfrag1 Jan 24, 2023
567b524
Avoid instantiating sqlalchemy objects
jfrag1 Jan 24, 2023
cc155b6
Fix openapi spec
jfrag1 Jan 24, 2023
f472bfd
Add proper pagination
jfrag1 Jan 24, 2023
ea82cd4
Lint
jfrag1 Jan 24, 2023
61b905d
Lint
jfrag1 Jan 24, 2023
ca799ec
Try adding sleep between log inserts
jfrag1 Jan 24, 2023
2307bf0
Fix mypy
jfrag1 Jan 24, 2023
945afff
Update query param on FE
jfrag1 Jan 24, 2023
f0aeb3f
Increase sleep interval in test
jfrag1 Jan 24, 2023
2d7faa4
Set dttm of logs manually
jfrag1 Jan 24, 2023
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
10 changes: 6 additions & 4 deletions superset-frontend/src/profile/components/RecentActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
*/
import React from 'react';
import moment from 'moment';
import rison from 'rison';

import TableLoader from '../../components/TableLoader';
import { Activity } from '../types';
import { ActivityResult } from '../types';
import { BootstrapUser } from '../../types/bootstrapTypes';

interface RecentActivityProps {
Expand All @@ -29,8 +30,8 @@ interface RecentActivityProps {

export default function RecentActivity({ user }: RecentActivityProps) {
const rowLimit = 50;
const mutator = function (data: Activity[]) {
return data
const mutator = function (data: ActivityResult) {
return data.result
.filter(row => row.action === 'dashboard' || row.action === 'explore')
.map(row => ({
name: <a href={row.item_url}>{row.item_title}</a>,
Expand All @@ -39,13 +40,14 @@ export default function RecentActivity({ user }: RecentActivityProps) {
_time: row.time,
}));
};
const params = rison.encode({ page_size: rowLimit });
return (
<div>
<TableLoader
className="table-condensed"
mutator={mutator}
sortable
dataEndpoint={`/superset/recent_activity/${user?.userId}/?limit=${rowLimit}`}
dataEndpoint={`/api/v1/log/recent_activity/${user?.userId}/?q=${params}`}
/>
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions superset-frontend/src/profile/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ export type Activity = {
item_url: string;
time: number;
};

export type ActivityResult = {
result: Activity[];
};
2 changes: 1 addition & 1 deletion superset-frontend/src/views/CRUD/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export const getRecentAcitivtyObjs = (
return Promise.all(newBatch)
.then(([chartRes, dashboardRes]) => {
res.other = [...chartRes.json.result, ...dashboardRes.json.result];
res.viewed = recentsRes.json;
res.viewed = recentsRes.json.result;
return res;
})
.catch(errMsg =>
Expand Down
2 changes: 1 addition & 1 deletion superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import EmptyState from './EmptyState';
import { WelcomeTable } from './types';

/**
* Return result from /superset/recent_activity/{user_id}
* Return result from /api/v1/log/recent_activity/{user_id}/
*/
interface RecentActivity {
action: string;
Expand Down
6 changes: 3 additions & 3 deletions superset-frontend/src/views/CRUD/welcome/Welcome.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const dashboardFavoriteStatusEndpoint =
'glob:*/api/v1/dashboard/favorite_status?*';
const savedQueryEndpoint = 'glob:*/api/v1/saved_query/?*';
const savedQueryInfoEndpoint = 'glob:*/api/v1/saved_query/_info?*';
const recentActivityEndpoint = 'glob:*/superset/recent_activity/*';
const recentActivityEndpoint = 'glob:*/api/v1/log/recent_activity/*';

fetchMock.get(chartsEndpoint, {
result: [
Expand Down Expand Up @@ -142,7 +142,7 @@ describe('Welcome with sql role', () => {
it('calls api methods in parallel on page load', () => {
const chartCall = fetchMock.calls(/chart\/\?q/);
const savedQueryCall = fetchMock.calls(/saved_query\/\?q/);
const recentCall = fetchMock.calls(/superset\/recent_activity\/*/);
const recentCall = fetchMock.calls(/api\/v1\/log\/recent_activity\/*/);
const dashboardCall = fetchMock.calls(/dashboard\/\?q/);
expect(chartCall).toHaveLength(2);
expect(recentCall).toHaveLength(1);
Expand Down Expand Up @@ -186,7 +186,7 @@ describe('Welcome without sql role', () => {
it('calls api methods in parallel on page load', () => {
const chartCall = fetchMock.calls(/chart\/\?q/);
const savedQueryCall = fetchMock.calls(/saved_query\/\?q/);
const recentCall = fetchMock.calls(/superset\/recent_activity\/*/);
const recentCall = fetchMock.calls(/api\/v1\/log\/recent_activity\/*/);
const dashboardCall = fetchMock.calls(/dashboard\/\?q/);
expect(chartCall).toHaveLength(2);
expect(recentCall).toHaveLength(1);
Expand Down
4 changes: 3 additions & 1 deletion superset-frontend/src/views/CRUD/welcome/Welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
styled,
t,
} from '@superset-ui/core';
import rison from 'rison';
import Collapse from 'src/components/Collapse';
import { User } from 'src/types/bootstrapTypes';
import { reject } from 'lodash';
Expand Down Expand Up @@ -165,7 +166,8 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
const canAccessSqlLab = canUserAccessSqlLab(user);
const userid = user.userId;
const id = userid!.toString(); // confident that user is not a guest user
const recent = `/superset/recent_activity/${user.userId}/?limit=6`;
const params = rison.encode({ page_size: 6 });
const recent = `/api/v1/log/recent_activity/${user.userId}/?q=${params}`;
const [activeChild, setActiveChild] = useState('Loading');
const userKey = dangerouslyGetItemDoNotUse(id, null);
let defaultChecked = false;
Expand Down
7 changes: 6 additions & 1 deletion superset/models/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import logging
from collections import defaultdict
from functools import partial
from typing import Any, Callable, Dict, List, Set, Tuple, Type, Union
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union

import sqlalchemy as sqla
from flask_appbuilder import Model
Expand Down Expand Up @@ -177,6 +177,11 @@ def __repr__(self) -> str:
def url(self) -> str:
return f"/superset/dashboard/{self.slug or self.id}/"

@staticmethod
def get_url(id_: int, slug: Optional[str] = None) -> str:
# To be able to generate URL's without instanciating a Dashboard object
return f"/superset/dashboard/{slug or id_}/"

@property
def datasources(self) -> Set[BaseDatasource]:
# Verbose but efficient database enumeration of dashboard datasources.
Expand Down
10 changes: 8 additions & 2 deletions superset/models/slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,18 @@ def get_explore_url(
self,
base_url: str = "/explore",
overrides: Optional[Dict[str, Any]] = None,
) -> str:
return self.build_explore_url(self.id, base_url, overrides)

@staticmethod
def build_explore_url(
id_: int, base_url: str = "/explore", overrides: Optional[Dict[str, Any]] = None
) -> str:
overrides = overrides or {}
form_data = {"slice_id": self.id}
form_data = {"slice_id": id_}
form_data.update(overrides)
params = parse.quote(json.dumps(form_data))
return f"{base_url}/?slice_id={self.id}&form_data={params}"
return f"{base_url}/?slice_id={id_}&form_data={params}"

@property
def slice_url(self) -> str:
Expand Down
102 changes: 6 additions & 96 deletions superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@
import logging
import re
from contextlib import closing
from datetime import datetime, timedelta
from datetime import datetime
from typing import Any, Callable, cast, Dict, List, Optional, Union
from urllib import parse

import backoff
import humanize
import pandas as pd
import simplejson as json
from flask import abort, flash, g, redirect, render_template, request, Response
Expand All @@ -41,7 +40,6 @@
from sqlalchemy import and_, or_
from sqlalchemy.exc import DBAPIError, NoSuchModuleError, SQLAlchemyError
from sqlalchemy.orm.session import Session
from sqlalchemy.sql import functions as func

from superset import (
app,
Expand Down Expand Up @@ -98,7 +96,7 @@
from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError
from superset.extensions import async_query_manager, cache_manager
from superset.jinja_context import get_template_processor
from superset.models.core import Database, FavStar, Log
from superset.models.core import Database, FavStar
from superset.models.dashboard import Dashboard
from superset.models.datasource_access_request import DatasourceAccessRequest
from superset.models.slice import Slice
Expand Down Expand Up @@ -155,6 +153,7 @@
json_success,
validate_sqlatable,
)
from superset.views.log.dao import LogDAO
from superset.views.sql_lab.schemas import SqlJsonPayloadSchema
from superset.views.utils import (
_deserialize_results_payload,
Expand Down Expand Up @@ -1438,9 +1437,8 @@ def get_user_activity_access_error(user_id: int) -> Optional[FlaskResponse]:
@has_access_api
@event_logger.log_this
@expose("/recent_activity/<int:user_id>/", methods=["GET"])
def recent_activity( # pylint: disable=too-many-locals
self, user_id: int
) -> FlaskResponse:
@deprecated()
def recent_activity(self, user_id: int) -> FlaskResponse:
"""Recent activity (actions) for a given user"""
error_obj = self.get_user_activity_access_error(user_id)
if error_obj:
Expand All @@ -1452,96 +1450,8 @@ def recent_activity( # pylint: disable=too-many-locals
# whether to get distinct subjects
distinct = request.args.get("distinct") != "false"

has_subject_title = or_(
and_(
Dashboard.dashboard_title is not None,
Dashboard.dashboard_title != "",
),
and_(Slice.slice_name is not None, Slice.slice_name != ""),
)

if distinct:
one_year_ago = datetime.today() - timedelta(days=365)
subqry = (
db.session.query(
Log.dashboard_id,
Log.slice_id,
Log.action,
func.max(Log.dttm).label("dttm"),
)
.group_by(Log.dashboard_id, Log.slice_id, Log.action)
.filter(
and_(
Log.action.in_(actions),
Log.user_id == user_id,
# limit to one year of data to improve performance
Log.dttm > one_year_ago,
or_(Log.dashboard_id.isnot(None), Log.slice_id.isnot(None)),
)
)
.subquery()
)
qry = (
db.session.query(
subqry,
Dashboard.slug.label("dashboard_slug"),
Dashboard.dashboard_title,
Slice.slice_name,
)
.outerjoin(Dashboard, Dashboard.id == subqry.c.dashboard_id)
.outerjoin(
Slice,
Slice.id == subqry.c.slice_id,
)
.filter(has_subject_title)
.order_by(subqry.c.dttm.desc())
.limit(limit)
)
else:
qry = (
db.session.query(
Log.dttm,
Log.action,
Log.dashboard_id,
Log.slice_id,
Dashboard.slug.label("dashboard_slug"),
Dashboard.dashboard_title,
Slice.slice_name,
)
.outerjoin(Dashboard, Dashboard.id == Log.dashboard_id)
.outerjoin(Slice, Slice.id == Log.slice_id)
.filter(has_subject_title)
.order_by(Log.dttm.desc())
.limit(limit)
)
payload = LogDAO.get_recent_activity(user_id, actions, distinct, 0, limit)

payload = []
for log in qry.all():
item_url = None
item_title = None
item_type = None
if log.dashboard_id:
item_type = "dashboard"
item_url = Dashboard(id=log.dashboard_id, slug=log.dashboard_slug).url
item_title = log.dashboard_title
elif log.slice_id:
slc = Slice(id=log.slice_id, slice_name=log.slice_name)
item_type = "slice"
item_url = slc.slice_url
item_title = slc.chart

payload.append(
{
"action": log.action,
"item_type": item_type,
"item_url": item_url,
"item_title": item_title,
"time": log.dttm,
"time_delta_humanized": humanize.naturaltime(
datetime.now() - log.dttm
),
}
)
return json_success(json.dumps(payload, default=utils.json_int_dttm_ser))

@api
Expand Down
Loading