Skip to content
Merged
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
9 changes: 3 additions & 6 deletions static/app/views/alerts/list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ import SentryDocumentTitle from 'app/components/sentryDocumentTitle';
import {IconInfo} from 'app/icons';
import {t, tct} from 'app/locale';
import space from 'app/styles/space';
import {Organization, Project, Team} from 'app/types';
import {Organization, Project} from 'app/types';
import {trackAnalyticsEvent} from 'app/utils/analytics';
import Projects from 'app/utils/projects';
import withOrganization from 'app/utils/withOrganization';
import withTeams from 'app/utils/withTeams';

import TeamFilter, {getTeamParams} from '../rules/teamFilter';
import {Incident} from '../types';
Expand All @@ -37,7 +36,6 @@ const DOCS_URL =

type Props = RouteComponentProps<{orgId: string}, {}> & {
organization: Organization;
teams: Team[];
};

type State = {
Expand Down Expand Up @@ -176,15 +174,14 @@ class IncidentsList extends AsyncComponent<Props, State & AsyncComponent['state'
};

renderFilterBar() {
const {teams, location} = this.props;
const {location} = this.props;
const selectedTeams = new Set(getTeamParams(location.query.team));
const selectedStatus = new Set(this.getQueryStatus(location.query.status));

return (
<FilterWrapper>
<TeamFilter
showStatus
teams={teams}
selectedStatus={selectedStatus}
selectedTeams={selectedTeams}
handleChangeFilter={this.handleChangeFilter}
Expand Down Expand Up @@ -386,4 +383,4 @@ const EmptyStateAction = styled('p')`
font-size: ${p => p.theme.fontSizeLarge};
`;

export default withOrganization(withTeams(IncidentsListContainer));
export default withOrganization(IncidentsListContainer);
203 changes: 103 additions & 100 deletions static/app/views/alerts/rules/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import SentryDocumentTitle from 'app/components/sentryDocumentTitle';
import {IconArrow} from 'app/icons';
import {t, tct} from 'app/locale';
import space from 'app/styles/space';
import {GlobalSelection, Organization, Project, Team} from 'app/types';
import {GlobalSelection, Organization, Project} from 'app/types';
import {trackAnalyticsEvent} from 'app/utils/analytics';
import Projects from 'app/utils/projects';
import Teams from 'app/utils/teams';
import withGlobalSelection from 'app/utils/withGlobalSelection';
import withTeams from 'app/utils/withTeams';

import AlertHeader from '../list/header';
import {CombinedMetricIssueAlerts} from '../types';
Expand All @@ -34,7 +34,6 @@ const DOCS_URL = 'https://docs.sentry.io/product/alerts-notifications/metric-ale
type Props = RouteComponentProps<{orgId: string}, {}> & {
organization: Organization;
selection: GlobalSelection;
teams: Team[];
};

type State = {
Expand Down Expand Up @@ -113,13 +112,12 @@ class AlertRulesList extends AsyncComponent<Props, State & AsyncComponent['state
}

renderFilterBar() {
const {teams, location} = this.props;
const {location} = this.props;
const selectedTeams = new Set(getTeamParams(location.query.team));

return (
<FilterWrapper>
<TeamFilter
teams={teams}
selectedTeams={selectedTeams}
handleChangeFilter={this.handleChangeFilter}
/>
Expand All @@ -137,7 +135,6 @@ class AlertRulesList extends AsyncComponent<Props, State & AsyncComponent['state
params: {orgId},
location: {query},
organization,
teams,
router,
} = this.props;
const {loading, ruleList = [], ruleListPageLinks} = this.state;
Expand All @@ -160,105 +157,111 @@ class AlertRulesList extends AsyncComponent<Props, State & AsyncComponent['state
<IconArrow color="gray300" size="xs" direction={sort.asc ? 'up' : 'down'} />
);

const userTeams = new Set(teams.filter(({isMember}) => isMember).map(({id}) => id));

return (
<StyledLayoutBody>
<Layout.Main fullWidth>
{this.renderFilterBar()}
<StyledPanelTable
headers={[
<StyledSortLink
key="name"
role="columnheader"
aria-sort={
sort.field !== 'name' ? 'none' : sort.asc ? 'ascending' : 'descending'
}
to={{
pathname: location.pathname,
query: {
...currentQuery,
// sort by name should start by ascending on first click
asc: sort.field === 'name' && sort.asc ? undefined : '1',
sort: 'name',
},
}}
>
{t('Alert Rule')} {sort.field === 'name' && sortArrow}
</StyledSortLink>,

<StyledSortLink
key="status"
role="columnheader"
aria-sort={
!isAlertRuleSort ? 'none' : sort.asc ? 'ascending' : 'descending'
<Teams provideUserTeams>
{({initiallyLoaded: loadedTeams, teams}) => (
<StyledPanelTable
headers={[
<StyledSortLink
key="name"
role="columnheader"
aria-sort={
sort.field !== 'name'
? 'none'
: sort.asc
? 'ascending'
: 'descending'
}
to={{
pathname: location.pathname,
query: {
...currentQuery,
// sort by name should start by ascending on first click
asc: sort.field === 'name' && sort.asc ? undefined : '1',
sort: 'name',
},
}}
>
{t('Alert Rule')} {sort.field === 'name' && sortArrow}
</StyledSortLink>,

<StyledSortLink
key="status"
role="columnheader"
aria-sort={
!isAlertRuleSort ? 'none' : sort.asc ? 'ascending' : 'descending'
}
to={{
pathname: location.pathname,
query: {
...currentQuery,
asc: isAlertRuleSort && !sort.asc ? '1' : undefined,
sort: ['incident_status', 'date_triggered'],
},
}}
>
{t('Status')} {isAlertRuleSort && sortArrow}
</StyledSortLink>,

t('Project'),
t('Team'),
<StyledSortLink
key="dateAdded"
role="columnheader"
aria-sort={
sort.field !== 'date_added'
? 'none'
: sort.asc
? 'ascending'
: 'descending'
}
to={{
pathname: location.pathname,
query: {
...currentQuery,
asc: sort.field === 'date_added' && !sort.asc ? '1' : undefined,
sort: 'date_added',
},
}}
>
{t('Created')} {sort.field === 'date_added' && sortArrow}
</StyledSortLink>,
t('Actions'),
]}
isLoading={loading || !loadedTeams}
isEmpty={ruleList?.length === 0}
emptyMessage={t('No alert rules found for the current query.')}
emptyAction={
<EmptyStateAction>
{tct('Learn more about [link:Alerts]', {
link: <ExternalLink href={DOCS_URL} />,
})}
</EmptyStateAction>
}
to={{
pathname: location.pathname,
query: {
...currentQuery,
asc: isAlertRuleSort && !sort.asc ? '1' : undefined,
sort: ['incident_status', 'date_triggered'],
},
}}
>
{t('Status')} {isAlertRuleSort && sortArrow}
</StyledSortLink>,

t('Project'),
t('Team'),
<StyledSortLink
key="dateAdded"
role="columnheader"
aria-sort={
sort.field !== 'date_added'
? 'none'
: sort.asc
? 'ascending'
: 'descending'
}
to={{
pathname: location.pathname,
query: {
...currentQuery,
asc: sort.field === 'date_added' && !sort.asc ? '1' : undefined,
sort: 'date_added',
},
}}
>
{t('Created')} {sort.field === 'date_added' && sortArrow}
</StyledSortLink>,
t('Actions'),
]}
isLoading={loading}
isEmpty={ruleList?.length === 0}
emptyMessage={t('No alert rules found for the current query.')}
emptyAction={
<EmptyStateAction>
{tct('Learn more about [link:Alerts]', {
link: <ExternalLink href={DOCS_URL} />,
})}
</EmptyStateAction>
}
>
<Projects orgId={orgId} slugs={Array.from(allProjectsFromIncidents)}>
{({initiallyLoaded, projects}) =>
ruleList.map(rule => (
<RuleListRow
// Metric and issue alerts can have the same id
key={`${isIssueAlert(rule) ? 'metric' : 'issue'}-${rule.id}`}
projectsLoaded={initiallyLoaded}
projects={projects as Project[]}
rule={rule}
orgId={orgId}
onDelete={this.handleDeleteRule}
organization={organization}
userTeams={userTeams}
/>
))
}
</Projects>
</StyledPanelTable>
<Projects orgId={orgId} slugs={Array.from(allProjectsFromIncidents)}>
{({initiallyLoaded, projects}) =>
ruleList.map(rule => (
<RuleListRow
// Metric and issue alerts can have the same id
key={`${isIssueAlert(rule) ? 'metric' : 'issue'}-${rule.id}`}
projectsLoaded={initiallyLoaded}
projects={projects as Project[]}
rule={rule}
orgId={orgId}
onDelete={this.handleDeleteRule}
organization={organization}
userTeams={new Set(teams.map(team => team.id))}
/>
))
}
</Projects>
</StyledPanelTable>
)}
</Teams>
<Pagination
pageLinks={ruleListPageLinks}
onCursor={(cursor, path, _direction) => {
Expand Down Expand Up @@ -328,7 +331,7 @@ class AlertRulesListContainer extends Component<Props> {
}
}

export default withGlobalSelection(withTeams(AlertRulesListContainer));
export default withGlobalSelection(AlertRulesListContainer);

const StyledLayoutBody = styled(Layout.Body)`
margin-bottom: -20px;
Expand Down
49 changes: 34 additions & 15 deletions static/app/views/alerts/rules/teamFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import {useState} from 'react';
import styled from '@emotion/styled';
import debounce from 'lodash/debounce';

import Input from 'app/components/forms/input';
import LoadingIndicator from 'app/components/loadingIndicator';
import {DEFAULT_DEBOUNCE_DURATION} from 'app/constants';
import {t} from 'app/locale';
import {Team} from 'app/types';
import space from 'app/styles/space';
import useTeams from 'app/utils/useTeams';

import Filter from './filter';

const ALERT_LIST_QUERY_DEFAULT_TEAMS = ['myteams', 'unassigned'];

type Props = {
teams: Team[];
selectedTeams: Set<string>;
handleChangeFilter: (sectionId: string, activeFilters: Set<string>) => void;
showStatus?: boolean;
Expand All @@ -34,12 +37,13 @@ export function getTeamParams(team?: string | string[]): string[] {
}

function TeamFilter({
teams,
selectedTeams,
showStatus = false,
selectedStatus = new Set(),
handleChangeFilter,
}: Props) {
const {teams, onSearch, fetching} = useTeams();
const debouncedSearch = debounce(onSearch, DEFAULT_DEBOUNCE_DURATION);
const [teamFilterSearch, setTeamFilterSearch] = useState<string | undefined>();

const statusOptions = [
Expand Down Expand Up @@ -83,17 +87,22 @@ function TeamFilter({
return (
<Filter
header={
<StyledInput
autoFocus
placeholder={t('Filter by team slug')}
onClick={event => {
event.stopPropagation();
}}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setTeamFilterSearch(event.target.value);
}}
value={teamFilterSearch || ''}
/>
<InputWrapper>
<StyledInput
autoFocus
placeholder={t('Filter by team slug')}
onClick={event => {
event.stopPropagation();
}}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const search = event.target.value;
setTeamFilterSearch(search);
debouncedSearch(search);
}}
value={teamFilterSearch || ''}
/>
{fetching && <StyledLoadingIndicator size={16} mini />}
</InputWrapper>
}
onFilterChange={handleChangeFilter}
dropdownSections={[
Expand All @@ -118,8 +127,18 @@ function TeamFilter({

export default TeamFilter;

const InputWrapper = styled('div')`
position: relative;
`;

const StyledInput = styled(Input)`
border: none;
border-bottom: 1px solid ${p => p.theme.gray200};
border-bottom: 1px solid transparent;
border-radius: 0;
`;

const StyledLoadingIndicator = styled(LoadingIndicator)`
position: absolute;
right: 0;
top: ${space(0.75)};
`;
Loading