Skip to content

Commit

Permalink
chore: Migrate /superset/recent_activity/<user_id>/ to /api/v1/ (#22789)
Browse files Browse the repository at this point in the history
  • Loading branch information
jfrag1 authored Jan 25, 2023
1 parent 6f0fed7 commit 050cbe9
Show file tree
Hide file tree
Showing 17 changed files with 519 additions and 126 deletions.
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

0 comments on commit 050cbe9

Please sign in to comment.