import {Fragment} from 'react'; import styled from '@emotion/styled'; import {ActivityAvatar} from 'sentry/components/activity/item/avatar'; import UserAvatar from 'sentry/components/avatar/userAvatar'; import DateTime from 'sentry/components/dateTime'; import SelectControl from 'sentry/components/forms/controls/selectControl'; import Link from 'sentry/components/links/link'; import Pagination, {CursorHandler} from 'sentry/components/pagination'; import PanelTable from 'sentry/components/panels/panelTable'; import Tag from 'sentry/components/tag'; import {Tooltip} from 'sentry/components/tooltip'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {AuditLog, Organization, User} from 'sentry/types'; import {shouldUse24Hours} from 'sentry/utils/dates'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; import { projectDetectorSettingsId, retentionPrioritiesLabels, } from 'sentry/views/settings/projectPerformance/projectPerformance'; const avatarStyle = { width: 36, height: 36, marginRight: 8, }; const getAvatarDisplay = (logEntryUser: User | undefined) => { // Display Sentry's avatar for system or superuser-initiated events if ( logEntryUser?.isSuperuser || (logEntryUser?.name === 'Sentry' && logEntryUser?.email === undefined) ) { return ; } // Display user's avatar for non-superusers-initiated events if (logEntryUser !== undefined) { return ; } return null; }; const addUsernameDisplay = (logEntryUser: User | undefined) => { if (logEntryUser?.isSuperuser) { return ( {logEntryUser.name} {t('Sentry Staff')} ); } if (logEntryUser !== undefined) { return {logEntryUser.name}; } return null; }; const getTypeDisplay = (event: string) => { if (event.startsWith('rule.')) { return event.replace('rule.', 'issue-alert.'); } if (event.startsWith('alertrule.')) { return event.replace('alertrule.', 'metric-alert.'); } return event; }; const getEventOptions = (eventTypes: string[] | null) => eventTypes ?.map(type => { // Having both rule.x and alertrule.x may be confusing, so we'll replace their labels to be more descriptive. // We need to maintain the values here so we still fetch the correct audit log events from the backend should we want // to filter. // See https://github.com/getsentry/sentry/issues/46997 if (type.startsWith('rule.')) { return { label: type.replace('rule.', 'issue-alert.'), value: type, }; } if (type.startsWith('alertrule.')) { return { label: type.replace('alertrule.', 'metric-alert.'), value: type, }; } return { label: type, value: type, }; }) .sort((a, b) => a.label.localeCompare(b.label)); function AuditNote({ entry, orgSlug, }: { entry: NonNullable; orgSlug: Organization['slug']; }) { const {projects} = useProjects(); const project = projects.find(p => p.id === String(entry.data.id)); if (entry.event.startsWith('rule.')) { return {entry.note.replace('rule', 'issue alert rule')}; } if (!project) { return {entry.note}; } if (entry.event === 'project.create') { return ( {tct('Created project [projectSettingsLink]', { projectSettingsLink: ( {entry.data.slug} ), })} ); } if (entry.event === 'project.edit') { if (entry.data.old_slug && entry.data.new_slug) { return ( {tct('Renamed project slug from [old-slug] to [new-slug]', { 'old-slug': entry.data.old_slug, 'new-slug': ( {entry.data.new_slug} ), })} ); } return ( {tct('Edited project [projectSettingsLink] [note]', { projectSettingsLink: ( {entry.data.slug} ), note: entry.note.replace('edited project settings ', ''), })} ); } if (entry.event === 'project.change-performance-issue-detection') { return ( {tct('Edited project [projectSettingsLink] [note]', { projectSettingsLink: ( {entry.data.slug} performance issue detector settings ), note: entry.note.replace( 'edited project performance issue detector settings ', '' ), })} ); } if (entry.event === 'sampling_priority.enabled') { return ( {tct( 'Enabled retention priority "[biasLabel]" in project [samplingInProjectSettingsLink]', { samplingInProjectSettingsLink: ( {entry.data.slug} ), biasLabel: retentionPrioritiesLabels[entry.data.name], } )} ); } if (entry.event === 'sampling_priority.disabled') { return ( {tct( 'Disabled retention priority "[biasLabel]" in project [samplingInProjectSettingsLink]', { samplingInProjectSettingsLink: ( {entry.data.slug} ), biasLabel: retentionPrioritiesLabels[entry.data.name], } )} ); } return {entry.note}; } type Props = { entries: AuditLog[] | null; eventType: string | undefined; eventTypes: string[] | null; isLoading: boolean; onCursor: CursorHandler | undefined; onEventSelect: (value: string) => void; pageLinks: string | null; }; function AuditLogList({ isLoading, pageLinks, entries, eventType, eventTypes, onCursor, onEventSelect, }: Props) { const is24Hours = shouldUse24Hours(); const organization = useOrganization(); const hasEntries = entries && entries.length > 0; const ipv4Length = 15; const action = ( { onEventSelect(options?.value); }} /> ); return (
{(entries ?? []).map(entry => { if (!entry) { return null; } return (
{getAvatarDisplay(entry.actor)}
{addUsernameDisplay(entry.actor)}
{getTypeDisplay(entry.event)} {entry.ipAddress && ( {entry.ipAddress} )}
); })}
{pageLinks && }
); } const SentryAvatar = styled(ActivityAvatar)` margin-right: ${space(1)}; `; const Name = styled('strong')` font-size: ${p => p.theme.fontSizeMedium}; `; const StaffTag = styled(Tag)` padding: ${space(1)}; `; const EventSelector = styled(SelectControl)` width: 250px; `; const UserInfo = styled('div')` display: flex; align-items: center; line-height: 1.2; font-size: ${p => p.theme.fontSizeSmall}; min-width: 250px; `; const NameContainer = styled('div')` display: flex; flex-direction: column; justify-content: center; `; const Note = styled('div')` font-size: ${p => p.theme.fontSizeSmall}; word-break: break-word; margin-top: ${space(0.5)}; `; const FlexCenter = styled('div')` display: flex; align-items: center; `; const IpAddressOverflow = styled('div')` ${p => p.theme.overflowEllipsis}; min-width: 90px; `; const MonoDetail = styled('code')` font-size: ${p => p.theme.fontSizeMedium}; white-space: no-wrap; `; const TimestampInfo = styled('div')` display: grid; grid-template-rows: auto auto; gap: ${space(1)}; font-size: ${p => p.theme.fontSizeMedium}; `; export default AuditLogList;