Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -526,42 +526,104 @@ describe('GlobalCommandPaletteActions - search recall', () => {
});
});

describe('GlobalCommandPaletteActions - Seer XRay Mode gating', () => {
function mockOrgApis(organization: ReturnType<typeof OrganizationFixture>) {
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/group-search-views/starred/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/dashboards/starred/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/dashboards/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/explore/all-queries/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/users/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/members/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/teams/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/projects/`,
body: [],
});
function mockOrgApis(organization: ReturnType<typeof OrganizationFixture>) {
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/group-search-views/starred/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/dashboards/starred/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/dashboards/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/explore/all-queries/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/users/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/members/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/teams/`,
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/projects/`,
body: [],
});
}

describe('GlobalCommandPaletteActions - settings-v3', () => {
function renderPalette(organization: ReturnType<typeof OrganizationFixture>) {
mockOrgApis(organization);

render(
<CommandPaletteProvider>
<GlobalCommandPaletteActions />
<SlotOutlets />
<CommandPalette {...makeRenderProps(jest.fn())} />
</CommandPaletteProvider>,
{
organization,
initialRouterConfig: {
location: {pathname: `/organizations/${organization.slug}/issues/`},
},
}
);
}

it('lists account pages under Account Settings', async () => {
renderPalette(OrganizationFixture({features: ['settings-v3']}));

const input = await screen.findByRole('textbox', {name: 'Search commands'});
await userEvent.type(input, 'Personal Tokens');

expect(
await screen.findByRole('option', {name: /Account Settings.*Personal Tokens/})
).toBeInTheDocument();
expect(
screen.queryByRole('option', {name: /Org Settings.*Personal Tokens/})
).not.toBeInTheDocument();
});

it('titles the organization index page "General"', async () => {
renderPalette(OrganizationFixture({features: ['settings-v3']}));

const input = await screen.findByRole('textbox', {name: 'Search commands'});
await userEvent.type(input, 'General');

expect(
await screen.findByRole('option', {name: /Org Settings.*General/})
).toBeInTheDocument();
expect(
screen.queryByRole('option', {name: /General Settings/})
).not.toBeInTheDocument();
});

it('keeps account pages under Settings without the flag', async () => {
renderPalette(OrganizationFixture({features: []}));

const input = await screen.findByRole('textbox', {name: 'Search commands'});
await userEvent.type(input, 'Personal Tokens');

expect(
await screen.findByRole('option', {name: /Settings.*Personal Tokens/})
).toBeInTheDocument();
expect(
screen.queryByRole('option', {name: /Account Settings/})
).not.toBeInTheDocument();
});
});

describe('GlobalCommandPaletteActions - Seer XRay Mode gating', () => {
function renderPalette(organization: ReturnType<typeof OrganizationFixture>) {
render(
<CommandPaletteProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,16 @@ import {IconGithub} from '@sentry/icons/github';
import {IconGraph} from '@sentry/icons/graph';
import {IconGroup} from '@sentry/icons/group';
import {IconIssues} from '@sentry/icons/issues';
import {IconLink} from '@sentry/icons/link';
import {IconList} from '@sentry/icons/list';
import {IconLock} from '@sentry/icons/lock';
import {IconOpen} from '@sentry/icons/open';
import {IconPlay} from '@sentry/icons/play';
import {IconRepository} from '@sentry/icons/repository';
import {IconSearch} from '@sentry/icons/search';
import {IconSeer} from '@sentry/icons/seer';
import {IconSettings} from '@sentry/icons/settings';
import {IconSiren} from '@sentry/icons/siren';
import {IconStar} from '@sentry/icons/star';
import {IconSubscribed} from '@sentry/icons/subscribed';
import {IconTerminal} from '@sentry/icons/terminal';
import {IconUser} from '@sentry/icons/user';
import * as Sentry from '@sentry/react';
import {skipToken, useMutation, useQuery} from '@tanstack/react-query';
Expand Down Expand Up @@ -103,10 +100,22 @@ import {
toggleXRayMode,
useXRayModeEnabled,
} from 'sentry/views/seerExplorer/xray/xrayModeStore';
import {getUserOrgNavigationConfiguration} from 'sentry/views/settings/organization/userOrgNavigationConfiguration';
import {
getUserNavigationConfiguration,
getUserOrgNavigationConfiguration,
} from 'sentry/views/settings/organization/userOrgNavigationConfiguration';
import {getNavigationConfiguration} from 'sentry/views/settings/project/navigationConfiguration';
import {PROJECT_SETTINGS_ICONS} from 'sentry/views/settings/project/projectSettingsCommandPaletteActions';
import type {NavigationGroupProps, NavigationItem} from 'sentry/views/settings/types';
import {
getSettingsScopeIcon,
getSettingsScopeTitle,
} from 'sentry/views/settings/settingsScope';
import type {
NavigationGroupProps,
NavigationItem,
NavigationSection,
} from 'sentry/views/settings/types';
import {useSettingsV3} from 'sentry/views/settings/useSettingsV3';

/**
* Returns true if a settings navigation item should be shown in cmd+k.
Expand Down Expand Up @@ -134,20 +143,6 @@ const DSN_ICONS: React.ReactElement[] = [
<IconList key="list" />,
];

const ORG_SETTINGS_ICONS: Record<string, React.ReactElement> = {
'/settings/:orgId/api-keys/': <IconLock />,
'/settings/:orgId/auth-tokens/': <IconLock />,
'/settings/:orgId/feature-flags/': <IconFlag />,
'/settings/:orgId/projects/': <IconAllProjects />,
'/settings/:orgId/integrations/': <IconLink />,
'/settings/:orgId/mcp-cli/': <IconTerminal />,
'/settings/:orgId/members/': <IconUser />,
'/settings/:orgId/repos/': <IconRepository />,
'/settings/:orgId/seer/': <IconSeer />,
'/settings/:orgId/teams/': <IconGroup />,
'/settings/account/notifications/': <IconSubscribed />,
};

const helpSearch = new SentryGlobalSearch(['docs', 'develop']);
const EVENT_ID_PATTERN =
/^(?:[A-Fa-f0-9]{32}|[A-Fa-f0-9]{8}(?:-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12})$/;
Expand Down Expand Up @@ -311,6 +306,7 @@ export function GlobalCommandPaletteActions() {
const {projects} = useProjects();
const {organizations} = useLegacyStore(OrganizationsStore);
const sentryConfig = useLegacyStore(ConfigStore);
const settingsV3 = useSettingsV3();
const params = useParams();
const location = useLocation();
const {mutateAsync: mutateUserOptions} = useMutateUserOptions();
Expand Down Expand Up @@ -349,30 +345,43 @@ export function GlobalCommandPaletteActions() {
};
return getNavigationConfiguration({
organization,
settingsV3,
})
.flatMap(section =>
section.items.filter(navItem =>
isNavItemVisible(navItem, {...context, ...section})
)
)
.sort((a, b) => a.title.localeCompare(b.title));
}, [organization]);
}, [organization, settingsV3]);

const visibleOrgSettingsNavItems = useMemo(() => {
const {visibleOrgSettingsNavItems, visibleAccountSettingsNavItems} = useMemo(() => {
const context: Omit<NavigationGroupProps, 'items' | 'name' | 'id'> = {
access: new Set(organization.access),
features: new Set(organization.features),
isSelfHosted: sentryConfig.isSelfHosted,
organization,
};
return getUserOrgNavigationConfiguration()
.flatMap(section =>
section.items.filter(navItem =>
isNavItemVisible(navItem, {...context, ...section})
const getVisibleItems = (sections: NavigationSection[]) =>
sections
.flatMap(section =>
section.items.filter(navItem =>
isNavItemVisible(navItem, {...context, ...section})
)
)
)
.sort((a, b) => a.title.localeCompare(b.title));
}, [organization, sentryConfig.isSelfHosted]);
.sort((a, b) => a.title.localeCompare(b.title));

return {
visibleOrgSettingsNavItems: getVisibleItems(
getUserOrgNavigationConfiguration({settingsV3})
),
// With settings-v3, account pages leave the organization settings for
// their own scope, so they get their own group here too.
visibleAccountSettingsNavItems: settingsV3
? getVisibleItems(getUserNavigationConfiguration({settingsV3}))
: [],
};
}, [organization, sentryConfig.isSelfHosted, settingsV3]);

const prefix = `/organizations/${organization.slug}`;
const hasInsightsRollout = organization.features.includes(
Expand Down Expand Up @@ -621,18 +630,43 @@ export function GlobalCommandPaletteActions() {
/>
</CMDKAction>

<CMDKAction display={{label: t('Settings'), icon: <IconSettings />}} limit={4}>
<CMDKAction
display={{
label: settingsV3 ? getSettingsScopeTitle('org') : t('Settings'),
icon: settingsV3 ? getSettingsScopeIcon('org') : <IconSettings />,
}}
limit={4}
>
{visibleOrgSettingsNavItems.map(item => (
<CMDKAction
key={item.path}
display={{label: item.title, icon: ORG_SETTINGS_ICONS[item.path]}}
display={{label: item.title, icon: item.icon}}
keywords={item.keywords}
to={item.path}
/>
))}
<Override name="cmdk:global-settings-actions" />
</CMDKAction>

{settingsV3 && (
<CMDKAction
display={{
label: getSettingsScopeTitle('account'),
icon: getSettingsScopeIcon('account'),
}}
limit={4}
>
{visibleAccountSettingsNavItems.map(item => (
<CMDKAction
key={item.path}
display={{label: item.title, icon: item.icon}}
keywords={item.keywords}
to={item.path}
/>
))}
</CMDKAction>
)}

<CMDKAction
display={{label: t('Projects'), icon: <IconAllProjects />}}
to={makeProjectsPathname({path: '/', organization})}
Expand Down Expand Up @@ -704,7 +738,10 @@ export function GlobalCommandPaletteActions() {
)}

<CMDKAction
display={{label: t('Project Settings'), icon: <IconSettings />}}
display={{
label: getSettingsScopeTitle('project'),
icon: settingsV3 ? getSettingsScopeIcon('project') : <IconSettings />,
}}
limit={4}
>
{currentProjects.map(project => (
Expand Down
5 changes: 4 additions & 1 deletion static/app/components/replays/table/deleteReplays.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {useOrganization} from 'sentry/utils/useOrganization';
import {useProjectFromId} from 'sentry/utils/useProjectFromId';
import {useProjects} from 'sentry/utils/useProjects';
import type {ReplayListRecord} from 'sentry/views/explore/replays/types';
import {getSettingsScopeTitle} from 'sentry/views/settings/settingsScope';
import {useSettingsV3} from 'sentry/views/settings/useSettingsV3';

const REPLAY_COLUMNS: TableColumnConfig[] = [
{key: 'replay', width: '1fr'},
Expand Down Expand Up @@ -265,6 +267,7 @@ function ReplayPreviewTable({

function Title({children, project}: {children: React.ReactNode; project: Project}) {
const organization = useOrganization();
const settingsV3 = useSettingsV3();
const settingsPath = `/settings/${organization.slug}/projects/${project.slug}/replays/?replaySettingsTab=bulk-delete`;
return (
<Fragment>
Expand All @@ -276,7 +279,7 @@ function Title({children, project}: {children: React.ReactNode; project: Project
link: (
<LinkWithUnderline
to={settingsPath}
>{`Settings > ${project.slug} > Replays`}</LinkWithUnderline>
>{`${settingsV3 ? getSettingsScopeTitle('project') : 'Settings'} > ${project.slug} > Replays`}</LinkWithUnderline>
),
})}
</p>
Expand Down
Loading
Loading