Skip to content

Commit 1f74d96

Browse files
malwilleyandrewshie-sentry
authored andcommitted
feat(issue-views): Add 'All Views' page (#88043)
1 parent 98ab3bc commit 1f74d96

File tree

4 files changed

+357
-0
lines changed

4 files changed

+357
-0
lines changed

static/app/components/nav/issueViews/issueViewNavItems.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,11 @@ export function IssueViewNavItems({
297297
/>
298298
))}
299299
</Reorder.Group>
300+
{organization.features.includes('issue-view-sharing') && (
301+
<SecondaryNav.Item to={`${baseUrl}/views/`} end>
302+
{t('All Views')}
303+
</SecondaryNav.Item>
304+
)}
300305
</SecondaryNav.Section>
301306
);
302307
}

static/app/routes.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,6 +2102,12 @@ function buildRoutes() {
21022102
const issueRoutes = (
21032103
<Route path="/issues/" component={errorHandler(IssueNavigation)} withOrgPath>
21042104
<IndexRoute component={errorHandler(OverviewWrapper)} />
2105+
<Route
2106+
path="views/"
2107+
component={make(
2108+
() => import('sentry/views/issueList/issueViews/issueViewsList/issueViewsList')
2109+
)}
2110+
/>
21052111
<Route path="views/:viewId/" component={errorHandler(OverviewWrapper)} />
21062112
<Route path="searches/:searchId/" component={errorHandler(OverviewWrapper)} />
21072113
<Route
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import {Fragment} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import * as Layout from 'sentry/components/layouts/thirds';
5+
import Pagination from 'sentry/components/pagination';
6+
import Redirect from 'sentry/components/redirect';
7+
import SearchBar from 'sentry/components/searchBar';
8+
import {t} from 'sentry/locale';
9+
import {space} from 'sentry/styles/space';
10+
import {useLocation} from 'sentry/utils/useLocation';
11+
import {useNavigate} from 'sentry/utils/useNavigate';
12+
import useOrganization from 'sentry/utils/useOrganization';
13+
import {IssueViewsTable} from 'sentry/views/issueList/issueViews/issueViewsList/issueViewsTable';
14+
import {useFetchGroupSearchViews} from 'sentry/views/issueList/queries/useFetchGroupSearchViews';
15+
import {GroupSearchViewVisibility} from 'sentry/views/issueList/types';
16+
17+
type IssueViewSectionProps = {
18+
cursorQueryParam: string;
19+
limit: number;
20+
visibility: GroupSearchViewVisibility;
21+
};
22+
23+
function IssueViewSection({visibility, limit, cursorQueryParam}: IssueViewSectionProps) {
24+
const organization = useOrganization();
25+
const navigate = useNavigate();
26+
const location = useLocation();
27+
const cursor =
28+
typeof location.query[cursorQueryParam] === 'string'
29+
? location.query[cursorQueryParam]
30+
: undefined;
31+
32+
const {
33+
data: views = [],
34+
isPending,
35+
isError,
36+
getResponseHeader,
37+
} = useFetchGroupSearchViews({
38+
orgSlug: organization.slug,
39+
visibility,
40+
limit,
41+
cursor,
42+
});
43+
44+
const pageLinks = getResponseHeader?.('Link');
45+
46+
return (
47+
<Fragment>
48+
<IssueViewsTable views={views} isPending={isPending} isError={isError} />
49+
<Pagination
50+
pageLinks={pageLinks}
51+
onCursor={newCursor => {
52+
navigate({
53+
pathname: location.pathname,
54+
query: {
55+
...location.query,
56+
[cursorQueryParam]: newCursor,
57+
},
58+
});
59+
}}
60+
/>
61+
</Fragment>
62+
);
63+
}
64+
65+
export default function IssueViewsList() {
66+
const organization = useOrganization();
67+
const navigate = useNavigate();
68+
const location = useLocation();
69+
const query = typeof location.query.query === 'string' ? location.query.query : '';
70+
71+
if (!organization.features.includes('issue-view-sharing')) {
72+
return <Redirect to={`/organizations/${organization.slug}/issues/`} />;
73+
}
74+
75+
return (
76+
<Layout.Page>
77+
<Layout.Header unified>
78+
<Layout.Title>{t('All Views')}</Layout.Title>
79+
</Layout.Header>
80+
<Layout.Body>
81+
<Layout.Main fullWidth>
82+
<SearchBar
83+
defaultQuery={query}
84+
onSearch={newQuery => {
85+
navigate({
86+
pathname: location.pathname,
87+
query: {query: newQuery},
88+
});
89+
}}
90+
placeholder=""
91+
/>
92+
<TableHeading>{t('Owned by Me')}</TableHeading>
93+
<IssueViewSection
94+
visibility={GroupSearchViewVisibility.OWNER}
95+
limit={10}
96+
cursorQueryParam="mc"
97+
/>
98+
<TableHeading>{t('Shared with Me')}</TableHeading>
99+
<IssueViewSection
100+
visibility={GroupSearchViewVisibility.ORGANIZATION}
101+
limit={10}
102+
cursorQueryParam="sc"
103+
/>
104+
</Layout.Main>
105+
</Layout.Body>
106+
</Layout.Page>
107+
);
108+
}
109+
110+
const TableHeading = styled('h2')`
111+
display: flex;
112+
justify-content: space-between;
113+
align-items: center;
114+
font-size: ${p => p.theme.fontSizeExtraLarge};
115+
margin-top: ${space(3)};
116+
margin-bottom: ${space(1.5)};
117+
`;
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import {css} from '@emotion/react';
2+
import styled from '@emotion/styled';
3+
4+
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
5+
import Link from 'sentry/components/links/link';
6+
import LoadingError from 'sentry/components/loadingError';
7+
import {PanelTable} from 'sentry/components/panels/panelTable';
8+
import {FormattedQuery} from 'sentry/components/searchQueryBuilder/formattedQuery';
9+
import {getAbsoluteSummary} from 'sentry/components/timeRangeSelector/utils';
10+
import TimeSince from 'sentry/components/timeSince';
11+
import {Tooltip} from 'sentry/components/tooltip';
12+
import {IconLock, IconStar, IconUser} from 'sentry/icons';
13+
import {t} from 'sentry/locale';
14+
import {space} from 'sentry/styles/space';
15+
import useOrganization from 'sentry/utils/useOrganization';
16+
import useProjects from 'sentry/utils/useProjects';
17+
import type {GroupSearchView} from 'sentry/views/issueList/types';
18+
import {getSortLabel} from 'sentry/views/issueList/utils';
19+
import {ProjectsRenderer} from 'sentry/views/traces/fieldRenderers';
20+
21+
type IssueViewsTableProps = {
22+
isError: boolean;
23+
isPending: boolean;
24+
views: GroupSearchView[];
25+
};
26+
27+
function StarCellContent({isStarred}: {isStarred: boolean}) {
28+
return <IconStar isSolid={isStarred} />;
29+
}
30+
31+
function ProjectsCellContent({projects}: {projects: GroupSearchView['projects']}) {
32+
const {projects: allProjects} = useProjects();
33+
34+
const projectSlugs = allProjects
35+
.filter(project => projects.includes(parseInt(project.id, 10)))
36+
.map(project => project.slug);
37+
38+
if (projects.length === 0) {
39+
return t('My Projects');
40+
}
41+
if (projects.includes(-1)) {
42+
return t('All Projects');
43+
}
44+
return <ProjectsRenderer projectSlugs={projectSlugs} maxVisibleProjects={5} />;
45+
}
46+
47+
function EnvironmentsCellContent({
48+
environments,
49+
}: {
50+
environments: GroupSearchView['environments'];
51+
}) {
52+
const environmentsLabel =
53+
environments.length === 0 ? t('All') : environments.join(', ');
54+
55+
return (
56+
<PositionedContent>
57+
<Tooltip title={environmentsLabel}>{environmentsLabel}</Tooltip>
58+
</PositionedContent>
59+
);
60+
}
61+
62+
function TimeCellContent({timeFilters}: {timeFilters: GroupSearchView['timeFilters']}) {
63+
if (timeFilters.period) {
64+
return timeFilters.period;
65+
}
66+
67+
return getAbsoluteSummary(timeFilters.start, timeFilters.end, timeFilters.utc);
68+
}
69+
70+
function SharingCellContent({visibility}: {visibility: GroupSearchView['visibility']}) {
71+
if (visibility === 'organization') {
72+
return (
73+
<Tooltip title={t('Shared with organziation')} skipWrapper>
74+
<PositionedContent>
75+
<IconUser />
76+
</PositionedContent>
77+
</Tooltip>
78+
);
79+
}
80+
return (
81+
<Tooltip title={t('Private')} skipWrapper>
82+
<PositionedContent>
83+
<IconLock locked />
84+
</PositionedContent>
85+
</Tooltip>
86+
);
87+
}
88+
89+
function LastVisitedCellContent({
90+
lastVisited,
91+
}: {
92+
lastVisited: GroupSearchView['lastVisited'];
93+
}) {
94+
if (!lastVisited) {
95+
return '-';
96+
}
97+
return <PositionedTimeSince date={lastVisited} unitStyle="short" />;
98+
}
99+
100+
export function IssueViewsTable({views, isPending, isError}: IssueViewsTableProps) {
101+
const organization = useOrganization();
102+
103+
return (
104+
<StyledPanelTable
105+
disableHeaderBorderBottom
106+
headers={[
107+
'',
108+
t('Name'),
109+
t('Project'),
110+
t('Query'),
111+
t('Envs'),
112+
t('Time'),
113+
t('Sort'),
114+
t('Sharing'),
115+
'Last Viewed',
116+
]}
117+
isLoading={isPending}
118+
isEmpty={views.length === 0}
119+
>
120+
{isError && <LoadingError />}
121+
{views.map((view, index) => (
122+
<Row key={view.id} isFirst={index === 0}>
123+
<RowHoverStateLayer />
124+
<StarCell>
125+
{/* TODO: Add isStarred when the API is update to include it */}
126+
<StarCellContent isStarred />
127+
</StarCell>
128+
<Cell>
129+
<RowLink to={`/organizations/${organization.slug}/issues/views/${view.id}/`}>
130+
{view.name}
131+
</RowLink>
132+
</Cell>
133+
<Cell>
134+
<ProjectsCellContent projects={view.projects} />
135+
</Cell>
136+
<Cell>
137+
<FormattedQuery query={view.query} />
138+
</Cell>
139+
<Cell>
140+
<EnvironmentsCellContent environments={view.environments} />
141+
</Cell>
142+
<Cell>
143+
<TimeCellContent timeFilters={view.timeFilters} />
144+
</Cell>
145+
<Cell>{getSortLabel(view.querySort, organization)}</Cell>
146+
<Cell>
147+
<SharingCellContent visibility={view.visibility} />
148+
</Cell>
149+
<Cell>
150+
<LastVisitedCellContent lastVisited={view.lastVisited} />
151+
</Cell>
152+
</Row>
153+
))}
154+
</StyledPanelTable>
155+
);
156+
}
157+
158+
const StyledPanelTable = styled(PanelTable)`
159+
white-space: nowrap;
160+
font-size: ${p => p.theme.fontSizeMedium};
161+
overflow: auto;
162+
grid-template-columns: 36px auto auto 1fr auto auto 105px 90px 115px;
163+
164+
@media (min-width: ${p => p.theme.breakpoints.small}) {
165+
overflow: hidden;
166+
}
167+
168+
& > * {
169+
padding: ${space(1)} ${space(2)};
170+
}
171+
`;
172+
173+
const Row = styled('div')<{isFirst: boolean}>`
174+
display: grid;
175+
position: relative;
176+
grid-template-columns: subgrid;
177+
grid-column: 1/-1;
178+
padding: 0;
179+
180+
${p =>
181+
p.isFirst &&
182+
css`
183+
border-top: 1px solid ${p.theme.border};
184+
`}
185+
186+
&:not(:last-child) {
187+
border-bottom: 1px solid ${p => p.theme.innerBorder};
188+
}
189+
`;
190+
191+
const Cell = styled('div')`
192+
display: flex;
193+
align-items: center;
194+
padding: ${space(1)} ${space(2)};
195+
`;
196+
197+
const StarCell = styled(Cell)`
198+
padding: 0 0 0 ${space(2)};
199+
`;
200+
201+
const RowHoverStateLayer = styled(InteractionStateLayer)``;
202+
203+
const RowLink = styled(Link)`
204+
color: ${p => p.theme.textColor};
205+
206+
&:hover {
207+
color: ${p => p.theme.textColor};
208+
text-decoration: underline;
209+
}
210+
211+
&::before {
212+
content: '';
213+
position: absolute;
214+
top: 0;
215+
left: 0;
216+
right: 0;
217+
bottom: 0;
218+
}
219+
`;
220+
221+
const PositionedTimeSince = styled(TimeSince)`
222+
position: relative;
223+
`;
224+
225+
const PositionedContent = styled('div')`
226+
position: relative;
227+
display: flex;
228+
align-items: center;
229+
`;

0 commit comments

Comments
 (0)