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

ref(whats-new): Revamp "Whats New" #76818

Merged
merged 23 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
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
Loading