diff --git a/static/app/components/sidebar/index.tsx b/static/app/components/sidebar/index.tsx
index 11e95e7cf3d3dc..a40e2c42060c4e 100644
--- a/static/app/components/sidebar/index.tsx
+++ b/static/app/components/sidebar/index.tsx
@@ -1,9 +1,8 @@
-import * as React from 'react';
+import {Fragment, useEffect} from 'react';
import {browserHistory} from 'react-router';
import {css} from '@emotion/react';
import styled from '@emotion/styled';
import {Location} from 'history';
-import isEqual from 'lodash/isEqual';
import * as queryString from 'query-string';
import {hideSidebar, showSidebar} from 'app/actionCreators/preferences';
@@ -32,11 +31,12 @@ import ConfigStore from 'app/stores/configStore';
import HookStore from 'app/stores/hookStore';
import PreferencesStore from 'app/stores/preferencesStore';
import SidebarPanelStore from 'app/stores/sidebarPanelStore';
+import {useLegacyStore} from 'app/stores/useLegacyStore';
import space from 'app/styles/space';
import {Organization} from 'app/types';
import {getDiscoverLandingUrl} from 'app/utils/discover/urls';
import theme from 'app/utils/theme';
-import withOrganization from 'app/utils/withOrganization';
+import useMedia from 'app/utils/useMedia';
import Broadcasts from './broadcasts';
import SidebarHelp from './help';
@@ -48,131 +48,63 @@ import {SidebarOrientation, SidebarPanelKey} from './types';
const SidebarOverride = HookOrDefault({
hookName: 'sidebar:item-override',
- defaultComponent: ({children}) => {children({})},
+ defaultComponent: ({children}) => {children({})},
});
-type ActivePanelType = SidebarPanelKey | '';
-
type Props = {
- organization: Organization;
- activePanel: ActivePanelType;
- collapsed: boolean;
+ organization?: Organization;
location?: Location;
- children?: never;
};
-type State = {
- horizontal: boolean;
-};
+function Sidebar({location, organization}: Props) {
+ const config = useLegacyStore(ConfigStore);
+ const preferences = useLegacyStore(PreferencesStore);
+ const activePanel = useLegacyStore(SidebarPanelStore);
-class Sidebar extends React.Component {
- constructor(props: Props) {
- super(props);
+ const collapsed = !!preferences.collapsed;
+ const horizontal = useMedia(`(max-width: ${theme.breakpoints[1]})`);
- if (!window.matchMedia) {
- return;
- }
-
- // TODO(billy): We should consider moving this into a component
- this.mq = window.matchMedia(`(max-width: ${theme.breakpoints[1]})`);
- this.mq.addListener(this.handleMediaQueryChange);
- this.state.horizontal = this.mq.matches;
- }
-
- state: State = {
- horizontal: false,
+ const toggleCollapse = () => {
+ const action = collapsed ? showSidebar : hideSidebar;
+ action();
};
- componentDidMount() {
- document.body.classList.add('body-sidebar');
-
- this.checkHash();
- this.doCollapse(this.props.collapsed);
- }
+ const togglePanel = (panel: SidebarPanelKey) => SidebarPanelActions.togglePanel(panel);
+ const hidePanel = () => SidebarPanelActions.hidePanel();
- // Sidebar doesn't use children, so don't use it to compare
- // Also ignore location, will re-render when routes change (instead of query params)
- //
- // NOTE(epurkhiser): The comment above is why I added `children?: never` as a
- // type to this component. I'm not sure the implications of removing this so
- // I've just left it for now.
- shouldComponentUpdate(
- {children: _children, location: _location, ...nextPropsToCompare}: Props,
- nextState: State
- ) {
- const {
- children: _childrenCurrent,
- location: _locationCurrent,
- ...currentPropsToCompare
- } = this.props;
-
- return (
- !isEqual(currentPropsToCompare, nextPropsToCompare) ||
- !isEqual(this.state, nextState)
- );
- }
+ const bcl = document.body.classList;
- componentDidUpdate(prevProps: Props) {
- const {collapsed, location} = this.props;
+ // Close panel on any navigation
+ useEffect(() => void hidePanel(), [location?.pathname]);
- // Close active panel if we navigated anywhere
- if (location?.pathname !== prevProps.location?.pathname) {
- this.hidePanel();
- }
-
- // Collapse
- if (collapsed !== prevProps.collapsed) {
- this.doCollapse(collapsed);
- }
- }
-
- componentWillUnmount() {
- document.body.classList.remove('body-sidebar');
-
- if (this.mq) {
- this.mq.removeListener(this.handleMediaQueryChange);
- this.mq = null;
- }
- }
+ // Add classname to body
+ useEffect(() => {
+ bcl.add('body-sidebar');
+ return () => bcl.remove('body-sidebar');
+ }, []);
- mq: MediaQueryList | null = null;
- sidebarRef = React.createRef();
-
- doCollapse(collapsed: boolean) {
+ // Add sidebar collapse classname to body
+ useEffect(() => {
if (collapsed) {
- document.body.classList.add('collapsed');
+ bcl.add('collapsed');
} else {
- document.body.classList.remove('collapsed');
+ bcl.remove('collapsed');
}
- }
- toggleSidebar = () => {
- const {collapsed} = this.props;
-
- if (!collapsed) {
- hideSidebar();
- } else {
- showSidebar();
- }
- };
+ return () => bcl.remove('collapsed');
+ }, [collapsed]);
- checkHash = () => {
- if (window.location.hash === '#welcome') {
- this.togglePanel(SidebarPanelKey.OnboardingWizard);
+ // Trigger panels depending on the location hash
+ useEffect(() => {
+ if (location?.hash === '#welcome') {
+ togglePanel(SidebarPanelKey.OnboardingWizard);
}
- };
-
- handleMediaQueryChange = (changed: MediaQueryListEvent) => {
- this.setState({
- horizontal: changed.matches,
- });
- };
-
- togglePanel = (panel: SidebarPanelKey) => SidebarPanelActions.togglePanel(panel);
- hidePanel = () => SidebarPanelActions.hidePanel();
+ }, [location?.hash]);
- // Keep the global selection querystring values in the path
- navigateWithGlobalSelection = (
+ /**
+ * Navigate to a path, but keep the global selection query strings.
+ */
+ const navigateWithGlobalSelection = (
pathname: string,
evt: React.MouseEvent
) => {
@@ -186,11 +118,11 @@ class Sidebar extends React.Component {
'discover',
'discover/results', // Team plans do not have query landing page
'performance',
- ].map(route => `/organizations/${this.props.organization.slug}/${route}/`);
+ ].map(route => `/organizations/${organization?.slug}/${route}/`);
// Only keep the querystring if the current route matches one of the above
if (globalSelectionRoutes.includes(pathname)) {
- const query = extractSelectionParameters(this.props.location?.query);
+ const query = extractSelectionParameters(location?.query);
// Handle cmd-click (mac) and meta-click (linux)
if (evt.metaKey) {
@@ -202,370 +134,306 @@ class Sidebar extends React.Component {
evt.preventDefault();
browserHistory.push({pathname, query});
}
+ };
+
+ const hasPanel = !!activePanel;
+ const hasOrganization = !!organization;
+ const orientation: SidebarOrientation = horizontal ? 'top' : 'left';
- this.hidePanel();
+ const sidebarItemProps = {
+ orientation,
+ collapsed,
+ hasPanel,
};
- render() {
- const {activePanel, organization, collapsed} = this.props;
- const {horizontal} = this.state;
- const config = ConfigStore.getConfig();
- const user = ConfigStore.get('user');
- const hasPanel = !!activePanel;
- const orientation: SidebarOrientation = horizontal ? 'top' : 'left';
- const sidebarItemProps = {
- orientation,
- collapsed,
- hasPanel,
- };
- const hasOrganization = !!organization;
-
- const projects = hasOrganization && (
- }
- label={{t('Projects')}}
- to={`/organizations/${organization.slug}/projects/`}
- id="projects"
- />
- );
+ const projects = hasOrganization && (
+ }
+ label={{t('Projects')}}
+ to={`/organizations/${organization.slug}/projects/`}
+ id="projects"
+ />
+ );
- const issues = hasOrganization && (
-
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/issues/`,
- evt
- )
- }
- icon={}
- label={{t('Issues')}}
- to={`/organizations/${organization.slug}/issues/`}
- id="issues"
- />
- );
-
- const discover2 = hasOrganization && (
-
-
- this.navigateWithGlobalSelection(getDiscoverLandingUrl(organization), evt)
- }
- icon={}
- label={{t('Discover')}}
- to={getDiscoverLandingUrl(organization)}
- id="discover-v2"
- />
-
- );
-
- const performance = hasOrganization && (
-
-
- {(overideProps: Partial>) => (
-
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/performance/`,
- evt
- )
- }
- icon={}
- label={{t('Performance')}}
- to={`/organizations/${organization.slug}/performance/`}
- id="performance"
- {...overideProps}
- />
- )}
-
-
- );
+ const issues = hasOrganization && (
+
+ navigateWithGlobalSelection(`/organizations/${organization.slug}/issues/`, evt)
+ }
+ icon={}
+ label={{t('Issues')}}
+ to={`/organizations/${organization.slug}/issues/`}
+ id="issues"
+ />
+ );
- const releases = hasOrganization && (
+ const discover2 = hasOrganization && (
+
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/releases/`,
- evt
- )
+ navigateWithGlobalSelection(getDiscoverLandingUrl(organization), evt)
}
- icon={}
- label={{t('Releases')}}
- to={`/organizations/${organization.slug}/releases/`}
- id="releases"
+ icon={}
+ label={{t('Discover')}}
+ to={getDiscoverLandingUrl(organization)}
+ id="discover-v2"
/>
- );
+
+ );
- const userFeedback = hasOrganization && (
+ const performance = hasOrganization && (
+
+
+ {(overideProps: Partial>) => (
+
+ navigateWithGlobalSelection(
+ `/organizations/${organization.slug}/performance/`,
+ evt
+ )
+ }
+ icon={}
+ label={{t('Performance')}}
+ to={`/organizations/${organization.slug}/performance/`}
+ id="performance"
+ {...overideProps}
+ />
+ )}
+
+
+ );
+
+ const releases = hasOrganization && (
+
+ navigateWithGlobalSelection(`/organizations/${organization.slug}/releases/`, evt)
+ }
+ icon={}
+ label={{t('Releases')}}
+ to={`/organizations/${organization.slug}/releases/`}
+ id="releases"
+ />
+ );
+
+ const userFeedback = hasOrganization && (
+
+ navigateWithGlobalSelection(
+ `/organizations/${organization.slug}/user-feedback/`,
+ evt
+ )
+ }
+ icon={}
+ label={t('User Feedback')}
+ to={`/organizations/${organization.slug}/user-feedback/`}
+ id="user-feedback"
+ />
+ );
+
+ const alerts = hasOrganization && (
+
+ navigateWithGlobalSelection(
+ `/organizations/${organization.slug}/alerts/rules/`,
+ evt
+ )
+ }
+ icon={}
+ label={t('Alerts')}
+ to={`/organizations/${organization.slug}/alerts/rules/`}
+ id="alerts"
+ />
+ );
+
+ const monitors = hasOrganization && (
+
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/user-feedback/`,
+ navigateWithGlobalSelection(
+ `/organizations/${organization.slug}/monitors/`,
evt
)
}
- icon={}
- label={t('User Feedback')}
- to={`/organizations/${organization.slug}/user-feedback/`}
- id="user-feedback"
+ icon={}
+ label={t('Monitors')}
+ to={`/organizations/${organization.slug}/monitors/`}
+ id="monitors"
/>
- );
+
+ );
- const alerts = hasOrganization && (
+ const dashboards = hasOrganization && (
+
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/alerts/rules/`,
+ navigateWithGlobalSelection(
+ `/organizations/${organization.slug}/dashboards/`,
evt
)
}
- icon={}
- label={t('Alerts')}
- to={`/organizations/${organization.slug}/alerts/rules/`}
- id="alerts"
- />
- );
-
- const monitors = hasOrganization && (
-
-
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/monitors/`,
- evt
- )
- }
- icon={}
- label={t('Monitors')}
- to={`/organizations/${organization.slug}/monitors/`}
- id="monitors"
- />
-
- );
-
- const dashboards = hasOrganization && (
-
-
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/dashboards/`,
- evt
- )
- }
- icon={}
- label={t('Dashboards')}
- to={`/organizations/${organization.slug}/dashboards/`}
- id="customizable-dashboards"
- isNew
- />
-
- );
-
- const activity = hasOrganization && (
- }
- label={t('Activity')}
- to={`/organizations/${organization.slug}/activity/`}
- id="activity"
- />
- );
-
- const stats = hasOrganization && (
- }
- label={t('Stats')}
- to={`/organizations/${organization.slug}/stats/`}
- id="stats"
- />
- );
-
- const settings = hasOrganization && (
- }
- label={t('Settings')}
- to={`/settings/${organization.slug}/`}
- id="settings"
+ icon={}
+ label={t('Dashboards')}
+ to={`/organizations/${organization.slug}/dashboards/`}
+ id="customizable-dashboards"
+ isNew
/>
- );
+
+ );
- return (
-
-
-
-
-
+ const activity = hasOrganization && (
+ }
+ label={t('Activity')}
+ to={`/organizations/${organization.slug}/activity/`}
+ id="activity"
+ />
+ );
-
- {hasOrganization && (
-
-
- {projects}
- {issues}
- {performance}
- {releases}
- {userFeedback}
- {alerts}
- {discover2}
- {dashboards}
-
-
- {monitors}
-
-
- {activity}
- {stats}
-
-
- {settings}
-
- )}
-
-
-
- {hasOrganization && (
-
-
- this.togglePanel(SidebarPanelKey.OnboardingWizard)}
- hidePanel={this.hidePanel}
- {...sidebarItemProps}
- />
-
+ const stats = hasOrganization && (
+ }
+ label={t('Stats')}
+ to={`/organizations/${organization.slug}/stats/`}
+ id="stats"
+ />
+ );
-
- {HookStore.get('sidebar:bottom-items').length > 0 &&
- HookStore.get('sidebar:bottom-items')[0]({
- organization,
- ...sidebarItemProps,
- })}
-
- this.togglePanel(SidebarPanelKey.Broadcasts)}
- hidePanel={this.hidePanel}
- organization={organization}
- />
- this.togglePanel(SidebarPanelKey.StatusUpdate)}
- hidePanel={this.hidePanel}
- />
-
+ const settings = hasOrganization && (
+ }
+ label={t('Settings')}
+ to={`/settings/${organization.slug}/`}
+ id="settings"
+ />
+ );
- {!horizontal && (
+ return (
+
+
+
+
+
+
+
+ {hasOrganization && (
+
- }
- label={collapsed ? t('Expand') : t('Collapse')}
- onClick={this.toggleSidebar}
- />
+ {projects}
+ {issues}
+ {performance}
+ {releases}
+ {userFeedback}
+ {alerts}
+ {discover2}
+ {dashboards}
- )}
-
- )}
-
- );
- }
-}
-type ContainerProps = Omit;
+ {monitors}
-type ContainerState = {
- collapsed: boolean;
- activePanel: ActivePanelType;
-};
-type Preferences = typeof PreferencesStore.prefs;
+
+ {activity}
+ {stats}
+
-class SidebarContainer extends React.Component {
- state: ContainerState = {
- collapsed: !!PreferencesStore.getInitialState().collapsed,
- activePanel: '',
- };
+ {settings}
+
+ )}
+
+
- componentWillUnmount() {
- this.preferenceUnsubscribe();
- this.sidebarUnsubscribe();
- }
+ {hasOrganization && (
+
+
+ togglePanel(SidebarPanelKey.OnboardingWizard)}
+ hidePanel={hidePanel}
+ {...sidebarItemProps}
+ />
+
- preferenceUnsubscribe = PreferencesStore.listen(
- (preferences: Preferences) => this.onPreferenceChange(preferences),
- undefined
- );
+
+ {HookStore.get('sidebar:bottom-items').length > 0 &&
+ HookStore.get('sidebar:bottom-items')[0]({
+ organization,
+ ...sidebarItemProps,
+ })}
+
+ togglePanel(SidebarPanelKey.Broadcasts)}
+ hidePanel={hidePanel}
+ organization={organization}
+ />
+ togglePanel(SidebarPanelKey.StatusUpdate)}
+ hidePanel={hidePanel}
+ />
+
- sidebarUnsubscribe = SidebarPanelStore.listen(
- (activePanel: ActivePanelType) => this.onSidebarPanelChange(activePanel),
- undefined
+ {!horizontal && (
+
+ }
+ label={collapsed ? t('Expand') : t('Collapse')}
+ onClick={toggleCollapse}
+ />
+
+ )}
+
+ )}
+
);
-
- onPreferenceChange(preferences: Preferences) {
- if (preferences.collapsed === this.state.collapsed) {
- return;
- }
-
- this.setState({collapsed: !!preferences.collapsed});
- }
-
- onSidebarPanelChange(activePanel: ActivePanelType) {
- this.setState({activePanel});
- }
-
- render() {
- const {activePanel, collapsed} = this.state;
- return ;
- }
}
-export default withOrganization(SidebarContainer);
+export default Sidebar;
const responsiveFlex = css`
display: flex;
@@ -576,7 +444,7 @@ const responsiveFlex = css`
}
`;
-export const SidebarWrapper = styled('div')<{collapsed: boolean}>`
+export const SidebarWrapper = styled('nav')<{collapsed: boolean}>`
background: ${p => p.theme.sidebar.background};
background: ${p => p.theme.sidebarGradient};
color: ${p => p.theme.sidebar.color};
diff --git a/static/app/components/sidebar/onboardingStatus.tsx b/static/app/components/sidebar/onboardingStatus.tsx
index 5dccb0071e5b64..eef848d8a21ba9 100644
--- a/static/app/components/sidebar/onboardingStatus.tsx
+++ b/static/app/components/sidebar/onboardingStatus.tsx
@@ -72,9 +72,16 @@ function OnboardingStatus({
return null;
}
+ const label = t('Quick Start');
+
return (
-
+
{!collapsed && (
-
{t('Quick Start')}
+
{label}
{tct('[numberRemaining] Remaining tasks', {numberRemaining})}
{pendingCompletionSeen && }
diff --git a/static/app/components/sidebar/sidebarDropdown/index.tsx b/static/app/components/sidebar/sidebarDropdown/index.tsx
index c1f1dbe846dd38..6998ccbc2dcebe 100644
--- a/static/app/components/sidebar/sidebarDropdown/index.tsx
+++ b/static/app/components/sidebar/sidebarDropdown/index.tsx
@@ -30,10 +30,10 @@ import SwitchOrganization from './switchOrganization';
// TODO: make org and user optional props
type Props = Pick & {
api: Client;
- org: Organization;
projects: Project[];
user: User;
config: Config;
+ org?: Organization;
/**
* Set to true to hide links within the organization
*/
@@ -158,15 +158,17 @@ const SidebarDropdown = ({
{t('API keys')}
-
+ {hasOrganization && (
+
+ )}
{user.isSuperuser && (
{t('Admin')}
)}
{t('Sign out')}
diff --git a/static/app/components/sidebar/sidebarItem.tsx b/static/app/components/sidebar/sidebarItem.tsx
index a0d0f6a48123ed..c6564f36def189 100644
--- a/static/app/components/sidebar/sidebarItem.tsx
+++ b/static/app/components/sidebar/sidebarItem.tsx
@@ -126,7 +126,7 @@ const SidebarItem = ({
className={className}
onClick={(event: React.MouseEvent) => {
!(to || href) && event.preventDefault();
- typeof onClick === 'function' && onClick(id, event);
+ onClick?.(id, event);
showIsNew && localStorage.setItem(isNewSeenKey, 'true');
}}
>
diff --git a/static/app/components/sidebar/sidebarPanel.tsx b/static/app/components/sidebar/sidebarPanel.tsx
index 78baaa8215cffb..cb4ae260a09a20 100644
--- a/static/app/components/sidebar/sidebarPanel.tsx
+++ b/static/app/components/sidebar/sidebarPanel.tsx
@@ -22,7 +22,7 @@ const PanelContainer = styled('div')`
box-shadow: 1px 0 2px rgba(0, 0, 0, 0.06);
text-align: left;
animation: 200ms ${slideInLeft};
- z-index: ${p => p.theme.zIndex.sidebar};
+ z-index: ${p => p.theme.zIndex.sidebar - 1};
${p =>
p.orientation === 'top'
@@ -91,7 +91,12 @@ function SidebarPanel({
}
const sidebar = (
-
+
{title && (
{title}
diff --git a/static/app/stores/preferencesStore.tsx b/static/app/stores/preferencesStore.tsx
index 0d2d31706e79bb..a029ff32bff527 100644
--- a/static/app/stores/preferencesStore.tsx
+++ b/static/app/stores/preferencesStore.tsx
@@ -35,9 +35,7 @@ const storeConfig: Reflux.StoreDefinition & PreferenceStoreInterface = {
},
reset() {
- this.prefs = {
- collapsed: false,
- };
+ this.prefs = {collapsed: false};
},
loadInitialState(prefs: Preferences) {
@@ -46,12 +44,12 @@ const storeConfig: Reflux.StoreDefinition & PreferenceStoreInterface = {
},
onHideSidebar() {
- this.prefs.collapsed = true;
+ this.prefs = {...this.prefs, collapsed: true};
this.trigger(this.prefs);
},
onShowSidebar() {
- this.prefs.collapsed = false;
+ this.prefs = {...this.prefs, collapsed: false};
this.trigger(this.prefs);
},
diff --git a/tests/js/spec/components/sidebar/index.spec.jsx b/tests/js/spec/components/sidebar/index.spec.jsx
index 6f2a23791291c4..267e2c62a8a761 100644
--- a/tests/js/spec/components/sidebar/index.spec.jsx
+++ b/tests/js/spec/components/sidebar/index.spec.jsx
@@ -1,5 +1,12 @@
-import {mountWithTheme} from 'sentry-test/enzyme';
-import {act} from 'sentry-test/reactTestingLibrary';
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {
+ act,
+ fireEvent,
+ mountWithTheme,
+ screen,
+ waitFor,
+ waitForElementToBeRemoved,
+} from 'sentry-test/reactTestingLibrary';
import * as incidentActions from 'app/actionCreators/serviceIncidents';
import SidebarContainer from 'app/components/sidebar';
@@ -8,28 +15,23 @@ import ConfigStore from 'app/stores/configStore';
jest.mock('app/actionCreators/serviceIncidents');
describe('Sidebar', function () {
- let wrapper;
- const routerContext = TestStubs.routerContext();
- const {organization, router} = routerContext.context;
+ const {organization, router} = initializeOrg();
+ const broadcast = TestStubs.Broadcast();
const user = TestStubs.User();
const apiMocks = {};
- const createWrapper = props =>
- mountWithTheme(
- ,
- routerContext
- );
+ const location = {...router.location, ...{pathname: '/test/'}};
+
+ const getElement = props => (
+
+ );
+
+ const renderSidebar = props => mountWithTheme(getElement(props));
beforeEach(function () {
apiMocks.broadcasts = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/broadcasts/`,
- body: [TestStubs.Broadcast()],
+ body: [broadcast],
});
apiMocks.broadcastsMarkAsSeen = MockApiClient.addMockResponse({
url: '/broadcasts/',
@@ -42,250 +44,204 @@ describe('Sidebar', function () {
});
it('renders', function () {
- wrapper = mountWithTheme(
- ,
- TestStubs.routerContext()
- );
-
- expect(wrapper.find('SidebarWrapper')).toHaveLength(1);
+ renderSidebar();
+ expect(screen.getByTestId('sidebar-dropdown')).toBeInTheDocument();
});
- it('renders without org and router', function () {
- wrapper = createWrapper({
- organization: null,
- router: null,
- });
+ it('renders without org', function () {
+ const {container} = renderSidebar({organization: null});
// no org displays user details
- expect(wrapper.find('OrgOrUserName').text()).toContain(user.name);
- expect(wrapper.find('UserNameOrEmail').text()).toContain(user.email);
+ expect(screen.getByText(user.name)).toBeInTheDocument();
+ expect(screen.getByText(user.email)).toBeInTheDocument();
- wrapper.find('SidebarDropdownActor').simulate('click');
- expect(wrapper).toSnapshot();
+ fireEvent.click(screen.getByTestId('sidebar-dropdown'));
+ expect(container).toSnapshot();
});
- it('can toggle collapsed state', async function () {
- wrapper = mountWithTheme(
- ,
- routerContext
- );
+ it('has can logout', async function () {
+ const mock = MockApiClient.addMockResponse({
+ url: '/auth/',
+ method: 'DELETE',
+ status: 204,
+ });
+ jest.spyOn(window.location, 'assign').mockImplementation(() => {});
- expect(wrapper.find('OrgOrUserName').text()).toContain(organization.name);
- expect(wrapper.find('UserNameOrEmail').text()).toContain(user.name);
+ renderSidebar({
+ organization: TestStubs.Organization({access: ['member:read']}),
+ });
- wrapper.find('SidebarCollapseItem StyledSidebarItem').simulate('click');
- await tick();
- wrapper.update();
+ fireEvent.click(screen.getByTestId('sidebar-dropdown'));
+ fireEvent.click(screen.getByTestId('sidebar-signout'));
- // Because of HoCs, we can't access the collapsed prop
- // Instead check for `SidebarItemLabel` which doesn't exist in collapsed state
- expect(wrapper.find('SidebarItemLabel')).toHaveLength(0);
+ await waitFor(() => expect(mock).toHaveBeenCalled());
- wrapper.find('SidebarCollapseItem StyledSidebarItem').simulate('click');
- await tick();
- wrapper.update();
- expect(wrapper.find('SidebarItemLabel').length).toBeGreaterThan(0);
+ expect(window.location.assign).toHaveBeenCalledWith('/auth/login/');
+ window.location.assign.mockRestore();
});
- it('can have onboarding feature', async function () {
- wrapper = mountWithTheme(
- ,
- routerContext
- );
+ it('can toggle collapsed state', async function () {
+ renderSidebar();
- expect(wrapper.find('OnboardingStatus ProgressRing')).toHaveLength(1);
+ expect(screen.getByText(user.name)).toBeInTheDocument();
+ expect(screen.getByText(organization.name)).toBeInTheDocument();
- wrapper.find('OnboardingStatus ProgressRing').simulate('click');
- await tick();
- wrapper.update();
+ fireEvent.click(screen.getByTestId('sidebar-collapse'));
- expect(wrapper.find('OnboardingStatus TaskSidebarPanel').exists()).toBe(true);
+ // Check that the organization name is no longer visible
+ await waitFor(() =>
+ expect(screen.queryByText(organization.name)).not.toBeInTheDocument()
+ );
+
+ // Un-collapse he sidebar and make sure the org name is visible again
+ fireEvent.click(screen.getByTestId('sidebar-collapse'));
+ expect(await screen.findByText(organization.name)).toBeInTheDocument();
});
- describe('SidebarHelp', function () {
- it('can toggle help menu', function () {
- wrapper = createWrapper();
- wrapper.find('HelpActor').simulate('click');
- const menu = wrapper.find('HelpMenu');
- expect(menu).toHaveLength(1);
- expect(wrapper).toSnapshot();
- expect(menu.find('SidebarMenuItem')).toHaveLength(2);
- wrapper.find('HelpActor').simulate('click');
- expect(wrapper.find('HelpMenu')).toHaveLength(0);
- });
+ it('can toggle help menu', function () {
+ const {container} = renderSidebar();
+
+ fireEvent.click(screen.getByText('Help'));
+
+ expect(screen.getByText('Visit Help Center')).toBeInTheDocument();
+ expect(container).toSnapshot();
});
describe('SidebarDropdown', function () {
it('can open Sidebar org/name dropdown menu', function () {
- wrapper = createWrapper();
- wrapper.find('SidebarDropdownActor').simulate('click');
- expect(wrapper.find('OrgAndUserMenu')).toHaveLength(1);
- expect(wrapper).toSnapshot();
+ const {container} = renderSidebar();
+
+ fireEvent.click(screen.getByTestId('sidebar-dropdown'));
+
+ const orgSettingsLink = screen.getByText('Organization settings');
+ expect(orgSettingsLink).toBeInTheDocument();
+ expect(container).toSnapshot();
});
it('has link to Members settings with `member:write`', function () {
- let org = TestStubs.Organization();
- org = {
- ...org,
- access: [...org.access, 'member:read'],
- };
-
- wrapper = createWrapper({
- organization: org,
+ renderSidebar({
+ organization: TestStubs.Organization({access: ['member:read']}),
});
- wrapper.find('SidebarDropdownActor').simulate('click');
- expect(wrapper.find('OrgAndUserMenu')).toHaveLength(1);
- expect(
- wrapper.find('SidebarMenuItem[to="/settings/org-slug/members/"]')
- ).toHaveLength(1);
+
+ fireEvent.click(screen.getByTestId('sidebar-dropdown'));
+
+ expect(screen.getByText('Members')).toBeInTheDocument();
});
it('can open "Switch Organization" sub-menu', function () {
act(() => void ConfigStore.set('features', new Set(['organizations:create'])));
+
+ const {container} = renderSidebar();
+
+ fireEvent.click(screen.getByTestId('sidebar-dropdown'));
+
jest.useFakeTimers();
- wrapper = createWrapper();
- wrapper.find('SidebarDropdownActor').simulate('click');
- wrapper.find('SwitchOrganizationMenuActor').simulate('mouseEnter');
- jest.advanceTimersByTime(500);
- wrapper.update();
- expect(wrapper.find('SwitchOrganizationMenu')).toHaveLength(1);
- expect(wrapper).toSnapshot();
+ fireEvent.mouseEnter(screen.getByText('Switch organization'));
+ act(() => jest.advanceTimersByTime(500));
jest.useRealTimers();
+
+ const createOrg = screen.getByText('Create a new organization');
+ expect(createOrg).toBeInTheDocument();
+
+ expect(container).toSnapshot();
});
+ });
- it('has can logout', async function () {
- const mock = MockApiClient.addMockResponse({
- url: '/auth/',
- method: 'DELETE',
- status: 204,
- });
- jest.spyOn(window.location, 'assign').mockImplementation(() => {});
+ describe('SidebarPanel', function () {
+ it('hides when path changes', async function () {
+ const {rerender} = renderSidebar();
+
+ fireEvent.click(screen.getByText("What's new"));
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ expect(screen.getByText("What's new in Sentry")).toBeInTheDocument();
- let org = TestStubs.Organization();
- org = {
- ...org,
- access: [...org.access, 'member:read'],
- };
+ rerender(getElement({location: {...router.location, pathname: 'new-path-name'}}));
+
+ await waitForElementToBeRemoved(() => screen.queryByText("What's new in Sentry"));
+ });
- wrapper = createWrapper({
- organization: org,
- user: TestStubs.User(),
+ it('can have onboarding feature', async function () {
+ renderSidebar({
+ organization: {...organization, features: ['onboarding']},
});
- wrapper.find('SidebarDropdownActor').simulate('click');
- wrapper.find('SidebarMenuItem[data-test-id="sidebarSignout"]').simulate('click');
- expect(mock).toHaveBeenCalled();
- await tick();
- expect(window.location.assign).toHaveBeenCalledWith('/auth/login/');
- window.location.assign.mockRestore();
+ const quickStart = screen.getByText('Quick Start');
+
+ expect(quickStart).toBeInTheDocument();
+ fireEvent.click(quickStart);
+
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ expect(screen.getByText('Capture your first error')).toBeInTheDocument();
});
- });
- describe('SidebarPanel', function () {
it('displays empty panel when there are no Broadcasts', async function () {
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/broadcasts/`,
body: [],
});
- wrapper = createWrapper();
+ renderSidebar();
- wrapper.find('Broadcasts SidebarItem').simulate('click');
+ fireEvent.click(screen.getByText("What's new"));
- await tick();
- wrapper.update();
- expect(wrapper.find('SidebarPanel')).toHaveLength(1);
-
- expect(wrapper.find('SidebarPanelItem')).toHaveLength(0);
- expect(wrapper.find('SidebarPanelEmpty')).toHaveLength(1);
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ expect(screen.getByText("What's new in Sentry")).toBeInTheDocument();
+ expect(
+ screen.getByText('No recent updates from the Sentry team.')
+ ).toBeInTheDocument();
// Close the sidebar
- wrapper.find('Broadcasts SidebarItem').simulate('click');
- await tick();
- wrapper.update();
+ fireEvent.click(screen.getByText("What's new"));
+ await waitForElementToBeRemoved(() => screen.queryByText("What's new in Sentry"));
});
it('can display Broadcasts panel and mark as seen', async function () {
jest.useFakeTimers();
- wrapper = createWrapper();
- expect(apiMocks.broadcasts).toHaveBeenCalled();
-
- wrapper.find('Broadcasts SidebarItem').simulate('click');
+ const {container} = renderSidebar();
- // XXX: Need to do this for reflux since we're using fake timers
- jest.advanceTimersByTime(0);
- await Promise.resolve();
- wrapper.update();
+ expect(apiMocks.broadcasts).toHaveBeenCalled();
- expect(wrapper.find('SidebarPanel')).toHaveLength(1);
+ fireEvent.click(screen.getByText("What's new"));
- expect(wrapper.find('SidebarPanelItem')).toHaveLength(1);
- expect(wrapper.find('SidebarPanelItem').prop('hasSeen')).toBe(false);
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ expect(screen.getByText("What's new in Sentry")).toBeInTheDocument();
- expect(wrapper.find('SidebarPanelItem')).toSnapshot();
+ const broadcastTitle = screen.getByText(broadcast.title);
+ expect(broadcastTitle).toBeInTheDocument();
+ expect(container).toSnapshot();
// Should mark as seen after a delay
- jest.advanceTimersByTime(2000);
+ act(() => jest.advanceTimersByTime(2000));
+
expect(apiMocks.broadcastsMarkAsSeen).toHaveBeenCalledWith(
'/broadcasts/',
expect.objectContaining({
- data: {
- hasSeen: '1',
- },
- query: {
- id: ['8'],
- },
+ data: {hasSeen: '1'},
+ query: {id: ['8']},
})
);
jest.useRealTimers();
// Close the sidebar
- wrapper.find('Broadcasts SidebarItem').simulate('click');
- });
-
- it('can toggle display of Broadcasts SidebarPanel', async function () {
- wrapper = createWrapper();
- wrapper.update();
-
- // Show Broadcasts Panel
- wrapper.find('Broadcasts SidebarItem').simulate('click');
- await tick();
- wrapper.update();
- expect(wrapper.find('SidebarPanel')).toHaveLength(1);
-
- // Hide Broadcasts Panel
- wrapper.find('Broadcasts SidebarItem').simulate('click');
- await tick();
- wrapper.update();
- expect(wrapper.find('SidebarPanel')).toHaveLength(0);
-
- // Close the sidebar
- wrapper.find('Broadcasts SidebarItem').simulate('click');
+ fireEvent.click(screen.getByText("What's new"));
+ await waitForElementToBeRemoved(() => screen.queryByText("What's new in Sentry"));
});
it('can unmount Sidebar (and Broadcasts) and kills Broadcast timers', async function () {
jest.useFakeTimers();
- wrapper = createWrapper();
- const broadcasts = wrapper.find('Broadcasts').instance();
+ const {unmount} = renderSidebar();
// This will start timer to mark as seen
- await wrapper.find('Broadcasts SidebarItem').simulate('click');
- wrapper.update();
+ fireEvent.click(screen.getByRole('link', {name: "What's new"}));
+ expect(await screen.findByText("What's new in Sentry")).toBeInTheDocument();
- jest.advanceTimersByTime(500);
- expect(broadcasts.poller).toBeDefined();
- expect(broadcasts.timer).toBeDefined();
+ act(() => jest.advanceTimersByTime(500));
// Unmounting will cancel timers
- wrapper.unmount();
- expect(broadcasts.poller).toBe(null);
- expect(broadcasts.timer).toBe(null);
+ unmount();
- // This advances timers enough so that mark as seen should be called if it wasn't unmounted
- jest.advanceTimersByTime(600);
+ // This advances timers enough so that mark as seen should be called if
+ // it wasn't unmounted
+ act(() => jest.advanceTimersByTime(600));
expect(apiMocks.broadcastsMarkAsSeen).not.toHaveBeenCalled();
jest.useRealTimers();
});
@@ -295,42 +251,11 @@ describe('Sidebar', function () {
incidents: [TestStubs.ServiceIncident()],
}));
- wrapper = createWrapper();
- await tick();
-
- wrapper.find('ServiceIncidents').simulate('click');
- await tick();
- wrapper.update();
- expect(wrapper.find('SidebarPanel')).toHaveLength(1);
-
- expect(wrapper.find('IncidentList')).toSnapshot();
- });
-
- it('hides when path changes', async function () {
- wrapper = createWrapper();
- wrapper.update();
-
- wrapper.find('Broadcasts SidebarItem').simulate('click');
- await tick();
- wrapper.update();
- expect(wrapper.find('SidebarPanel')).toHaveLength(1);
-
- const prevProps = wrapper.props();
-
- wrapper.setProps({
- location: {...router.location, pathname: 'new-path-name'},
- });
+ const {container} = renderSidebar();
- // XXX(epurkhsier): Due to a bug in enzyme [0], componentDidUpdate is not
- // called after props have updated, it still receives _old_ `this.props`.
- // We manually call it here after the props have been correctly updated.
- //
- // [0]: https://github.com/enzymejs/enzyme/issues/2197
- wrapper.find('Sidebar').instance().componentDidUpdate(prevProps);
+ fireEvent.click(await screen.findByText('Service status'));
- await tick();
- wrapper.update();
- expect(wrapper.find('SidebarPanel')).toHaveLength(0);
+ expect(container).toSnapshot();
});
});
});