Skip to content

Commit

Permalink
ref(whats-new): Revamp "Whats New" (#76818)
Browse files Browse the repository at this point in the history
  • Loading branch information
priscilawebdev authored and c298lee committed Sep 10, 2024
1 parent de8cc2a commit f3a0456
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 8 deletions.
100 changes: 100 additions & 0 deletions static/app/components/sidebar/broadcastPanelItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {useCallback} from 'react';
import styled from '@emotion/styled';

import Badge from 'sentry/components/badge/badge';
import Tag from 'sentry/components/badge/tag';
import ExternalLink from 'sentry/components/links/externalLink';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Broadcast} from 'sentry/types/system';
import {trackAnalytics} from 'sentry/utils/analytics';
import useOrganization from 'sentry/utils/useOrganization';

export const BROADCAST_CATEGORIES: Record<NonNullable<Broadcast['category']>, string> = {
announcement: t('Announcement'),
feature: t('New Feature'),
blog: t('Blog Post'),
event: t('Event'),
video: t('Video'),
};

interface BroadcastPanelItemProps
extends Pick<
Broadcast,
'hasSeen' | 'category' | 'title' | 'message' | 'link' | 'mediaUrl'
> {}

export function BroadcastPanelItem({
hasSeen,
title,
message,
link,
mediaUrl,
category,
}: BroadcastPanelItemProps) {
const organization = useOrganization();

const handlePanelClicked = useCallback(() => {
trackAnalytics('whats_new.link_clicked', {organization, title, category});
}, [organization, title, category]);

return (
<SidebarPanelItemRoot>
<TextBlock>
{category &&
(hasSeen ? (
<CategoryTag>{BROADCAST_CATEGORIES[category]}</CategoryTag>
) : (
<CategoryBadge type="new">{BROADCAST_CATEGORIES[category]}</CategoryBadge>
))}
<Title hasSeen={hasSeen} href={link} onClick={handlePanelClicked}>
{title}
</Title>
<Message>{message}</Message>
</TextBlock>
{mediaUrl && <Media src={mediaUrl} alt={title} />}
</SidebarPanelItemRoot>
);
}

const SidebarPanelItemRoot = styled('div')`
line-height: 1.5;
background: ${p => p.theme.background};
margin: 0 ${space(3)};
padding: ${space(2)} 0;
:not(:first-child) {
border-top: 1px solid ${p => p.theme.border};
}
`;

const Title = styled(ExternalLink)<Pick<BroadcastPanelItemProps, 'hasSeen'>>`
font-size: ${p => p.theme.fontSizeLarge};
color: ${p => p.theme.blue400};
${p => !p.hasSeen && `font-weight: ${p.theme.fontWeightBold}`};
`;

const Message = styled('div')`
color: ${p => p.theme.subText};
`;

const TextBlock = styled('div')`
margin-bottom: ${space(1.5)};
display: flex;
flex-direction: column;
`;

const Media = styled('img')`
border-radius: ${p => p.theme.borderRadius};
border: 1px solid ${p => p.theme.translucentGray200};
max-width: 100%;
`;

const CategoryTag = styled(Tag)`
margin-bottom: ${space(1)};
`;

const CategoryBadge = styled(Badge)`
margin-left: 0;
margin-bottom: ${space(1)};
`;
110 changes: 110 additions & 0 deletions static/app/components/sidebar/broadcasts.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {BroadcastFixture} from 'sentry-fixture/broadcast';
import {OrganizationFixture} from 'sentry-fixture/organization';

import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';

import {BROADCAST_CATEGORIES} from 'sentry/components/sidebar/broadcastPanelItem';
import Broadcasts from 'sentry/components/sidebar/broadcasts';
import {SidebarPanelKey} from 'sentry/components/sidebar/types';
import type {Broadcast} from 'sentry/types/system';
import {trackAnalytics} from 'sentry/utils/analytics';

jest.mock('sentry/utils/analytics');

function renderMockRequests({
orgSlug,
broadcastsResponse,
}: {
orgSlug: string;
broadcastsResponse?: Broadcast[];
}) {
MockApiClient.addMockResponse({
url: '/broadcasts/',
method: 'PUT',
});
MockApiClient.addMockResponse({
url: `/organizations/${orgSlug}/broadcasts/`,
body: broadcastsResponse ?? [],
});
}

describe('Broadcasts', function () {
const category = 'blog';

it('renders empty state', async function () {
const organization = OrganizationFixture();

renderMockRequests({orgSlug: organization.slug});

render(
<Broadcasts
orientation="left"
collapsed={false}
currentPanel={SidebarPanelKey.BROADCASTS}
onShowPanel={() => jest.fn()}
hidePanel={jest.fn()}
organization={organization}
/>
);

expect(await screen.findByText(/No recent updates/)).toBeInTheDocument();
});

it('renders a broadcast item with media content correctly', async function () {
const organization = OrganizationFixture({features: ['what-is-new-revamp']});
const broadcast = BroadcastFixture({
mediaUrl:
'https://images.ctfassets.net/em6l9zw4tzag/2vWdw7ZaApWxygugalbyOC/285525e5b7c9fbfa8fb814a69ab214cd/PerformancePageSketches_hero.jpg?w=2520&h=945&q=50&fm=webp',
category,
});

renderMockRequests({orgSlug: organization.slug, broadcastsResponse: [broadcast]});

render(
<Broadcasts
orientation="left"
collapsed={false}
currentPanel={SidebarPanelKey.BROADCASTS}
onShowPanel={() => jest.fn()}
hidePanel={jest.fn()}
organization={organization}
/>
);

// Verify that the broadcast content is rendered correctly
expect(await screen.findByText(BROADCAST_CATEGORIES[category])).toBeInTheDocument();
const titleLink = screen.getByRole('link', {name: broadcast.title});
expect(titleLink).toHaveAttribute('href', broadcast.link);
expect(screen.getByText(/Source maps are JSON/)).toBeInTheDocument();

// Simulate click and check if analytics tracking is called
await userEvent.click(titleLink);
expect(trackAnalytics).toHaveBeenCalledWith(
'whats_new.link_clicked',
expect.objectContaining({
title: broadcast.title,
category,
})
);
});

it('renders deprecated broadcast experience', async function () {
const organization = OrganizationFixture();
const broadcast = BroadcastFixture();

renderMockRequests({orgSlug: organization.slug, broadcastsResponse: [broadcast]});

render(
<Broadcasts
orientation="left"
collapsed={false}
currentPanel={SidebarPanelKey.BROADCASTS}
onShowPanel={() => jest.fn()}
hidePanel={jest.fn()}
organization={organization}
/>
);

expect(await screen.findByRole('link', {name: broadcast.cta})).toBeInTheDocument();
});
});
16 changes: 15 additions & 1 deletion static/app/components/sidebar/broadcasts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {getAllBroadcasts, markBroadcastsAsSeen} from 'sentry/actionCreators/broa
import type {Client} from 'sentry/api';
import DemoModeGate from 'sentry/components/acl/demoModeGate';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {BroadcastPanelItem} from 'sentry/components/sidebar/broadcastPanelItem';
import SidebarItem from 'sentry/components/sidebar/sidebarItem';
import SidebarPanel from 'sentry/components/sidebar/sidebarPanel';
import SidebarPanelEmpty from 'sentry/components/sidebar/sidebarPanelEmpty';
Expand Down Expand Up @@ -115,10 +116,11 @@ class Broadcasts extends Component<Props, State> {
}

render() {
const {orientation, collapsed, currentPanel, hidePanel} = this.props;
const {orientation, collapsed, currentPanel, hidePanel, organization} = this.props;
const {broadcasts, loading} = this.state;

const unseenPosts = this.unseenIds;
const whatIsNewRevampFeature = organization.features.includes('what-is-new-revamp');

return (
<DemoModeGate>
Expand Down Expand Up @@ -149,6 +151,18 @@ class Broadcasts extends Component<Props, State> {
<SidebarPanelEmpty>
{t('No recent updates from the Sentry team.')}
</SidebarPanelEmpty>
) : whatIsNewRevampFeature ? (
broadcasts.map(item => (
<BroadcastPanelItem
key={item.id}
hasSeen={item.hasSeen}
title={item.title}
message={item.message}
link={item.link}
mediaUrl={item.mediaUrl}
category={item.category}
/>
))
) : (
broadcasts.map(item => (
<SidebarPanelItem
Expand Down
9 changes: 6 additions & 3 deletions static/app/components/sidebar/sidebarPanelItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,12 @@ function SidebarPanelItem({
<Text>
<ExternalLink
href={link}
onClick={() =>
trackAnalytics('whats_new.link_clicked', {organization, title})
}
onClick={() => {
if (!title) {
return;
}
trackAnalytics('whats_new.link_clicked', {organization, title});
}}
>
{cta || t('Read More')}
</ExternalLink>
Expand Down
27 changes: 24 additions & 3 deletions static/app/types/system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,17 +228,38 @@ export type PipelineInitialData = {
props: Record<string, any>;
};

export type Broadcast = {
cta: string;
export interface Broadcast {
dateCreated: string;
dateExpires: string;
/**
* Has the item been seen? affects the styling of the panel item
*/
hasSeen: boolean;
id: string;
isActive: boolean;
/**
* The URL to use for the CTA
*/
link: string;
/**
* A message with muted styling which appears above the children content
*/
message: string;
title: string;
};
/**
* Category of the broadcast.
* Synced with https://github.com/getsentry/sentry/blob/master/src/sentry/models/broadcast.py#L14
*/
category?: 'announcement' | 'feature' | 'blog' | 'event' | 'video';
/**
* The text for the CTA link at the bottom of the panel item
*/
cta?: string;
/**
* Image url
*/
mediaUrl?: string;
}

// XXX(epurkhiser): The components list can be generated using jq
//
Expand Down
4 changes: 3 additions & 1 deletion static/app/utils/analytics/issueAnalyticsEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {SourceMapProcessingIssueType} from 'sentry/components/events/interf
import type {FieldValue} from 'sentry/components/forms/model';
import type {PriorityLevel} from 'sentry/types/group';
import type {IntegrationType} from 'sentry/types/integrations';
import type {Broadcast} from 'sentry/types/system';
import type {BaseEventAnalyticsParams} from 'sentry/utils/analytics/workflowAnalyticsEvents';
import type {CommonGroupAnalyticsData} from 'sentry/utils/events';

Expand Down Expand Up @@ -296,7 +297,8 @@ export type IssueEventParameters = {
'tag.clicked': {
is_clickable: boolean;
};
'whats_new.link_clicked': {title?: string};
'whats_new.link_clicked': Pick<Broadcast, 'title'> &
Partial<Pick<Broadcast, 'category'>>;
};

export type IssueEventKey = keyof IssueEventParameters;
Expand Down

0 comments on commit f3a0456

Please sign in to comment.