Просмотр исходного кода

feat(discv2): query avatars (#17949)

* smoosh commits

* fix - Serializing obj.created_by even in posts

- The user object is a SimpleLazyObject, so the serializer doesn't know
  what do do. So this Tells it to use the UserSerializer

* consolidated avatars

* adjusted avatar shape

* ref - Selecting created_by ahead of time

* replace starred boolean with user

Co-authored-by: Dora Chan <dora.lchan@gmail.com>
Co-authored-by: William Mak <william@wmak.io>
Dora 5 лет назад
Родитель
Сommit
b8fcf7b9de

+ 5 - 2
src/sentry/api/serializers/models/discoversavedquery.py

@@ -1,7 +1,8 @@
 from __future__ import absolute_import
 
 import six
-from sentry.api.serializers import Serializer, register
+from sentry.api.serializers import Serializer, register, serialize
+from sentry.api.serializers.models.user import UserSerializer
 from sentry.constants import ALL_ACCESS_PROJECTS
 from sentry.discover.models import DiscoverSavedQuery
 
@@ -32,7 +33,9 @@ class DiscoverSavedQuerySerializer(Serializer):
             "version": obj.version or obj.query.get("version", 1),
             "dateCreated": obj.date_created,
             "dateUpdated": obj.date_updated,
-            "createdBy": six.text_type(obj.created_by_id) if obj.created_by_id else None,
+            "createdBy": serialize(obj.created_by, serializer=UserSerializer())
+            if obj.created_by
+            else None,
         }
 
         for key in query_keys:

+ 4 - 2
src/sentry/discover/endpoints/discover_saved_queries.py

@@ -28,8 +28,10 @@ class DiscoverSavedQueriesEndpoint(OrganizationEndpoint):
         if not self.has_feature(organization, request):
             return self.respond(status=404)
 
-        queryset = DiscoverSavedQuery.objects.filter(organization=organization).prefetch_related(
-            "projects"
+        queryset = (
+            DiscoverSavedQuery.objects.filter(organization=organization)
+            .select_related("created_by")
+            .prefetch_related("projects")
         )
         query = request.query_params.get("query")
         if query:

+ 7 - 4
src/sentry/static/sentry/app/components/activity/item/avatar.tsx

@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
 
 import {AvatarUser} from 'app/types';
 import UserAvatar from 'app/components/avatar/userAvatar';
-import InlineSvg from 'app/components/inlineSvg';
+import {IconSentry} from 'app/icons';
 import Placeholder from 'app/components/placeholder';
 import SentryTypes from 'app/sentryTypes';
 
@@ -24,7 +24,7 @@ function ActivityAvatar({className, type, user, size = 38}: Props) {
     // Return Sentry avatar
     return (
       <SystemAvatar className={className} size={size}>
-        <Logo src="icon-sentry" size={`${Math.round(size * 0.8)}px`} />
+        <StyledIconSentry size="md" />
       </SystemAvatar>
     );
   }
@@ -57,8 +57,11 @@ const SystemAvatar = styled('span')<SystemAvatarProps>`
   align-items: center;
   width: ${p => p.size}px;
   height: ${p => p.size}px;
+  background-color: ${p => p.theme.gray5};
+  color: ${p => p.theme.white};
+  border-radius: 50%;
 `;
 
-const Logo = styled(InlineSvg)`
-  color: ${p => p.theme.gray5};
+const StyledIconSentry = styled(IconSentry)`
+  padding-bottom: 3px;
 `;

+ 1 - 1
src/sentry/static/sentry/app/types/index.tsx

@@ -937,13 +937,13 @@ export type NewQuery = {
   environment?: Readonly<string[]>;
   tags?: Readonly<string[]>;
   yAxis?: string;
+  createdBy?: User;
 };
 
 export type SavedQuery = NewQuery & {
   id: string;
   dateCreated: string;
   dateUpdated: string;
-  createdBy?: string;
 };
 
 export type SavedQueryState = {

+ 7 - 1
src/sentry/static/sentry/app/utils/discover/eventView.tsx

@@ -8,8 +8,8 @@ import uniqBy from 'lodash/uniqBy';
 import moment from 'moment';
 
 import {DEFAULT_PER_PAGE} from 'app/constants';
-import {SavedQuery, NewQuery, SelectValue} from 'app/types';
 import {EventQuery} from 'app/actionCreators/events';
+import {SavedQuery, NewQuery, SelectValue, User} from 'app/types';
 import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams';
 import {COL_WIDTH_UNDEFINED} from 'app/components/gridEditable';
 import {TableColumn, TableColumnSort} from 'app/views/eventsV2/table/types';
@@ -256,6 +256,7 @@ class EventView {
   statsPeriod: string | undefined;
   environment: Readonly<string[]>;
   yAxis: string | undefined;
+  createdBy: User | undefined;
 
   constructor(props: {
     id: string | undefined;
@@ -269,6 +270,7 @@ class EventView {
     statsPeriod: string | undefined;
     environment: Readonly<string[]>;
     yAxis: string | undefined;
+    createdBy: User | undefined;
   }) {
     const fields: Field[] = Array.isArray(props.fields) ? props.fields : [];
     let sorts: Sort[] = Array.isArray(props.sorts) ? props.sorts : [];
@@ -297,6 +299,7 @@ class EventView {
     this.statsPeriod = props.statsPeriod;
     this.environment = environment;
     this.yAxis = props.yAxis;
+    this.createdBy = props.createdBy;
   }
 
   static fromLocation(location: Location): EventView {
@@ -314,6 +317,7 @@ class EventView {
       statsPeriod: decodeScalar(statsPeriod),
       environment: collectQueryStringByKey(location.query, 'environment'),
       yAxis: decodeScalar(location.query.yAxis),
+      createdBy: undefined,
     });
   }
 
@@ -379,6 +383,7 @@ class EventView {
         'environment'
       ),
       yAxis,
+      createdBy: saved.createdBy,
     });
   }
 
@@ -555,6 +560,7 @@ class EventView {
       statsPeriod: this.statsPeriod,
       environment: this.environment,
       yAxis: this.yAxis,
+      createdBy: this.createdBy,
     });
   }
 

+ 2 - 1
src/sentry/static/sentry/app/views/eventsV2/queryList.tsx

@@ -121,6 +121,7 @@ class QueryList extends React.Component<Props> {
           title={eventView.name}
           subtitle={eventView.statsPeriod ? recentTimeline : customTimeline}
           queryDetail={eventView.query}
+          createdBy={eventView.createdBy}
           renderGraph={() => (
             <MiniGraph
               location={location}
@@ -164,10 +165,10 @@ class QueryList extends React.Component<Props> {
         <QueryCard
           key={`${index}-${eventView.id}`}
           to={to}
-          starred
           title={eventView.name}
           subtitle={eventView.statsPeriod ? recentTimeline : customTimeline}
           queryDetail={eventView.query}
+          createdBy={eventView.createdBy}
           onEventClick={() => {
             trackAnalyticsEvent({
               eventKey: 'discover_v2.prebuilt_query_click',

+ 34 - 33
src/sentry/static/sentry/app/views/eventsV2/querycard.tsx

@@ -2,21 +2,20 @@ import React from 'react';
 import styled from '@emotion/styled';
 import {browserHistory} from 'react-router';
 
-import {SubHeading} from 'app/components/charts/styles';
-import Link from 'app/components/links/link';
+import ActivityAvatar from 'app/components/activity/item/avatar';
 import overflowEllipsis from 'app/styles/overflowEllipsis';
-import theme from 'app/utils/theme';
-import {IconBookmark} from 'app/icons/iconBookmark';
+import Link from 'app/components/links/link';
 import space from 'app/styles/space';
 import {callIfFunction} from 'app/utils/callIfFunction';
+import {User} from 'app/types';
 import Card from 'app/components/card';
 
 type Props = {
   title?: string;
   subtitle?: string;
   queryDetail?: string;
-  starred?: boolean;
   to: object;
+  createdBy?: User | undefined;
   onEventClick?: () => void;
   renderGraph: () => React.ReactNode;
   renderContextMenu?: () => React.ReactNode;
@@ -35,27 +34,27 @@ class QueryCard extends React.PureComponent<Props> {
     const {
       title,
       subtitle,
-      starred,
       queryDetail,
       renderContextMenu,
       renderGraph,
+      createdBy,
     } = this.props;
 
     return (
       <Link data-test-id={`card-${title}`} onClick={this.handleClick} to={this.props.to}>
         <StyledQueryCard interactive>
           <QueryCardHeader>
-            <CardHeading>
-              {title}
-              {starred && (
-                <StyledIconBookmark
-                  color={theme.yellow}
-                  data-test-id="is-saved-query"
-                  solid
-                />
+            <QueryCardContent>
+              <QueryTitle>{title}</QueryTitle>
+              <QueryDetail>{queryDetail}</QueryDetail>
+            </QueryCardContent>
+            <AvatarWrapper>
+              {createdBy ? (
+                <ActivityAvatar type="user" user={createdBy} size={34} />
+              ) : (
+                <ActivityAvatar type="system" size={34} />
               )}
-            </CardHeading>
-            <StyledQueryDetail>{queryDetail}</StyledQueryDetail>
+            </AvatarWrapper>
           </QueryCardHeader>
           <QueryCardBody>{renderGraph()}</QueryCardBody>
           <QueryCardFooter>
@@ -68,6 +67,18 @@ class QueryCard extends React.PureComponent<Props> {
   }
 }
 
+const AvatarWrapper = styled('span')`
+  border: 3px solid ${p => p.theme.offWhite2};
+  border-radius: 50%;
+  height: 100%;
+`;
+
+const QueryCardContent = styled('div')`
+  flex-grow: 1;
+  overflow: hidden;
+  margin-right: ${space(1)};
+`;
+
 const StyledQueryCard = styled(Card)`
   justify-content: space-between;
   height: 100%;
@@ -78,31 +89,21 @@ const StyledQueryCard = styled(Card)`
 `;
 
 const QueryCardHeader = styled('div')`
-  position: relative;
+  display: flex;
   padding: ${space(1.5)} ${space(2)};
-  overflow: hidden;
-  line-height: 1.4;
-  flex-grow: 1;
-`;
-
-const StyledIconBookmark = styled(IconBookmark)`
-  position: absolute;
-  top: 14px;
-  right: ${space(2)};
 `;
 
-const CardHeading = styled(SubHeading)`
-  width: 95%;
+const QueryTitle = styled('div')`
+  color: ${p => p.theme.textColor};
+  ${overflowEllipsis};
 `;
 
-const StyledQueryDetail = styled('div')`
+const QueryDetail = styled('div')`
   font-family: ${p => p.theme.text.familyMono};
   font-size: ${p => p.theme.fontSizeSmall};
   color: ${p => p.theme.gray2};
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  width: 100%;
+  line-height: 1.5;
+  ${overflowEllipsis};
 `;
 
 const QueryCardBody = styled('div')`

+ 3 - 3
tests/js/spec/views/eventsV2/queryList.spec.jsx

@@ -80,7 +80,7 @@ describe('EventsV2 > QueryList', function() {
       TestStubs.routerContext()
     );
     let card = wrapper.find('QueryCard').last();
-    expect(card.find('CardHeading').text()).toEqual(savedQueries[1].name);
+    expect(card.find('QueryCardContent').text()).toEqual(savedQueries[1].name);
 
     openContextMenu(card);
     wrapper.update();
@@ -108,7 +108,7 @@ describe('EventsV2 > QueryList', function() {
       TestStubs.routerContext()
     );
     let card = wrapper.find('QueryCard').last();
-    expect(card.find('CardHeading').text()).toEqual(savedQueries[1].name);
+    expect(card.find('QueryCardContent').text()).toEqual(savedQueries[1].name);
 
     openContextMenu(card);
     wrapper.update();
@@ -135,7 +135,7 @@ describe('EventsV2 > QueryList', function() {
       TestStubs.routerContext()
     );
     let card = wrapper.find('QueryCard').last();
-    expect(card.find('CardHeading').text()).toEqual(savedQueries[1].name);
+    expect(card.find('QueryCardContent').text()).toEqual(savedQueries[1].name);
 
     // Open the context menu
     openContextMenu(card);

+ 2 - 0
tests/snuba/api/endpoints/test_discover_saved_queries.py

@@ -42,6 +42,8 @@ class DiscoverSavedQueriesTest(DiscoverSavedQueryBase):
         assert response.data[0]["conditions"] == []
         assert response.data[0]["limit"] == 10
         assert response.data[0]["version"] == 1
+        assert "createdBy" in response.data[0]
+        assert response.data[0]["createdBy"]["username"] == self.user.username
 
     def test_get_version_filter(self):
         url = reverse("sentry-api-0-discover-saved-queries", args=[self.org.slug])