From 37dfbd998f7c72b7a9c6ba411876122fa7b95e92 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Wed, 26 Mar 2025 15:33:37 -0700 Subject: [PATCH 1/2] ref(issue-views): Add visibility and pagination params to issue views hook --- .../nav/issueViews/issueViewNavItems.tsx | 9 +++++- .../views/issueList/issueViewsHeader.spec.tsx | 11 ++++++- .../queries/useFetchGroupSearchViews.tsx | 30 +++++++++++++++---- static/app/views/issueList/types.tsx | 10 ++++++- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/static/app/components/nav/issueViews/issueViewNavItems.tsx b/static/app/components/nav/issueViews/issueViewNavItems.tsx index 7bdd6166fef02d..9734b487da03ba 100644 --- a/static/app/components/nav/issueViews/issueViewNavItems.tsx +++ b/static/app/components/nav/issueViews/issueViewNavItems.tsx @@ -20,7 +20,10 @@ import type {IssueView} from 'sentry/views/issueList/issueViews/issueViews'; import {generateTempViewId} from 'sentry/views/issueList/issueViews/issueViews'; import {useUpdateGroupSearchViews} from 'sentry/views/issueList/mutations/useUpdateGroupSearchViews'; import {makeFetchGroupSearchViewsKey} from 'sentry/views/issueList/queries/useFetchGroupSearchViews'; -import type {GroupSearchView} from 'sentry/views/issueList/types'; +import { + type GroupSearchView, + GroupSearchViewVisibility, +} from 'sentry/views/issueList/types'; interface IssueViewNavItemsProps { baseUrl: string; @@ -201,6 +204,8 @@ export function IssueViewNavItems({ ...v, isAllProjects: isEqual(v.projects, [-1]), name: v.label, + lastVisited: null, + visibility: GroupSearchViewVisibility.OWNER, })) ); }, @@ -242,6 +247,8 @@ export function IssueViewNavItems({ ...v, isAllProjects: isEqual(v.projects, [-1]), name: v.label, + lastVisited: null, + visibility: GroupSearchViewVisibility.OWNER, })) ); } diff --git a/static/app/views/issueList/issueViewsHeader.spec.tsx b/static/app/views/issueList/issueViewsHeader.spec.tsx index e9080ca2451327..693c76674f568a 100644 --- a/static/app/views/issueList/issueViewsHeader.spec.tsx +++ b/static/app/views/issueList/issueViewsHeader.spec.tsx @@ -6,7 +6,10 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import OrganizationStore from 'sentry/stores/organizationStore'; import IssueViewsIssueListHeader from 'sentry/views/issueList/issueViewsHeader'; -import type {GroupSearchView} from 'sentry/views/issueList/types'; +import { + type GroupSearchView, + GroupSearchViewVisibility, +} from 'sentry/views/issueList/types'; import {IssueSortOptions} from 'sentry/views/issueList/utils'; describe('IssueViewsHeader', () => { @@ -34,6 +37,8 @@ describe('IssueViewsHeader', () => { start: '2024-01-02', utc: false, }, + visibility: GroupSearchViewVisibility.OWNER, + lastVisited: null, }, { id: '2', @@ -48,6 +53,8 @@ describe('IssueViewsHeader', () => { period: '1d', utc: null, }, + visibility: GroupSearchViewVisibility.ORGANIZATION, + lastVisited: null, }, { id: '3', @@ -62,6 +69,8 @@ describe('IssueViewsHeader', () => { start: '2024-01-02', utc: true, }, + visibility: GroupSearchViewVisibility.ORGANIZATION, + lastVisited: null, }, ]; diff --git a/static/app/views/issueList/queries/useFetchGroupSearchViews.tsx b/static/app/views/issueList/queries/useFetchGroupSearchViews.tsx index 98dbd95b2ee3f6..2de7eab6434553 100644 --- a/static/app/views/issueList/queries/useFetchGroupSearchViews.tsx +++ b/static/app/views/issueList/queries/useFetchGroupSearchViews.tsx @@ -1,21 +1,39 @@ -import type {UseApiQueryOptions} from 'sentry/utils/queryClient'; +import type {ApiQueryKey, UseApiQueryOptions} from 'sentry/utils/queryClient'; import {useApiQuery} from 'sentry/utils/queryClient'; -import type {GroupSearchView} from 'sentry/views/issueList/types'; +import type { + GroupSearchView, + GroupSearchViewVisibility, +} from 'sentry/views/issueList/types'; type FetchGroupSearchViewsParameters = { orgSlug: string; + cursor?: string; + limit?: number; + visibility?: GroupSearchViewVisibility; }; export const makeFetchGroupSearchViewsKey = ({ orgSlug, -}: FetchGroupSearchViewsParameters) => - [`/organizations/${orgSlug}/group-search-views/`] as const; + visibility, + limit, + cursor, +}: FetchGroupSearchViewsParameters): ApiQueryKey => + [ + `/organizations/${orgSlug}/group-search-views/`, + { + query: { + per_page: limit, + visibility, + cursor, + }, + }, + ] as const; export const useFetchGroupSearchViews = ( - {orgSlug}: FetchGroupSearchViewsParameters, + parameters: FetchGroupSearchViewsParameters, options: Partial> = {} ) => { - return useApiQuery(makeFetchGroupSearchViewsKey({orgSlug}), { + return useApiQuery(makeFetchGroupSearchViewsKey(parameters), { staleTime: Infinity, ...options, }); diff --git a/static/app/views/issueList/types.tsx b/static/app/views/issueList/types.tsx index f4f6043726ca19..b4ce1c5cfc8903 100644 --- a/static/app/views/issueList/types.tsx +++ b/static/app/views/issueList/types.tsx @@ -16,17 +16,25 @@ export type IssueUpdateData = | MarkReviewed | GroupStatusResolution; +export enum GroupSearchViewVisibility { + OWNER = 'owner', + ORGANIZATION = 'organization', +} + export type GroupSearchView = { environments: string[]; id: string; + lastVisited: string | null; name: string; projects: number[]; query: string; querySort: IssueSortOptions; timeFilters: PageFilters['datetime']; + visibility: GroupSearchViewVisibility; }; -export interface UpdateGroupSearchViewPayload extends Omit { +export interface UpdateGroupSearchViewPayload + extends Omit { environments: string[]; projects: number[]; timeFilters: PageFilters['datetime']; From 951e3e9df9774f988775a03fa0c06b09a1b4c243 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Wed, 26 Mar 2025 12:03:20 -0700 Subject: [PATCH 2/2] feat(issue-views): Add 'All Views' page --- .../nav/issueViews/issueViewNavItems.tsx | 5 + static/app/routes.tsx | 6 + .../issueViewsList/issueViewsList.tsx | 117 +++++++++ .../issueViewsList/issueViewsTable.tsx | 229 ++++++++++++++++++ 4 files changed, 357 insertions(+) create mode 100644 static/app/views/issueList/issueViews/issueViewsList/issueViewsList.tsx create mode 100644 static/app/views/issueList/issueViews/issueViewsList/issueViewsTable.tsx diff --git a/static/app/components/nav/issueViews/issueViewNavItems.tsx b/static/app/components/nav/issueViews/issueViewNavItems.tsx index 9734b487da03ba..17eaea598b0b48 100644 --- a/static/app/components/nav/issueViews/issueViewNavItems.tsx +++ b/static/app/components/nav/issueViews/issueViewNavItems.tsx @@ -297,6 +297,11 @@ export function IssueViewNavItems({ /> ))} + {organization.features.includes('issue-view-sharing') && ( + + {t('All Views')} + + )} ); } diff --git a/static/app/routes.tsx b/static/app/routes.tsx index f9d598fcbd171a..18d155dcd06d2b 100644 --- a/static/app/routes.tsx +++ b/static/app/routes.tsx @@ -2101,6 +2101,12 @@ function buildRoutes() { const issueRoutes = ( + import('sentry/views/issueList/issueViews/issueViewsList/issueViewsList') + )} + /> + + { + navigate({ + pathname: location.pathname, + query: { + ...location.query, + [cursorQueryParam]: newCursor, + }, + }); + }} + /> + + ); +} + +export default function IssueViewsList() { + const organization = useOrganization(); + const navigate = useNavigate(); + const location = useLocation(); + const query = typeof location.query.query === 'string' ? location.query.query : ''; + + if (!organization.features.includes('issue-view-sharing')) { + return ; + } + + return ( + + + {t('All Views')} + + + + { + navigate({ + pathname: location.pathname, + query: {query: newQuery}, + }); + }} + placeholder="" + /> + {t('Owned by Me')} + + {t('Shared with Me')} + + + + + ); +} + +const TableHeading = styled('h2')` + display: flex; + justify-content: space-between; + align-items: center; + font-size: ${p => p.theme.fontSizeExtraLarge}; + margin-top: ${space(3)}; + margin-bottom: ${space(1.5)}; +`; diff --git a/static/app/views/issueList/issueViews/issueViewsList/issueViewsTable.tsx b/static/app/views/issueList/issueViews/issueViewsList/issueViewsTable.tsx new file mode 100644 index 00000000000000..a7985848dca3ad --- /dev/null +++ b/static/app/views/issueList/issueViews/issueViewsList/issueViewsTable.tsx @@ -0,0 +1,229 @@ +import {css} from '@emotion/react'; +import styled from '@emotion/styled'; + +import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import Link from 'sentry/components/links/link'; +import LoadingError from 'sentry/components/loadingError'; +import {PanelTable} from 'sentry/components/panels/panelTable'; +import {FormattedQuery} from 'sentry/components/searchQueryBuilder/formattedQuery'; +import {getAbsoluteSummary} from 'sentry/components/timeRangeSelector/utils'; +import TimeSince from 'sentry/components/timeSince'; +import {Tooltip} from 'sentry/components/tooltip'; +import {IconLock, IconStar, IconUser} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import useOrganization from 'sentry/utils/useOrganization'; +import useProjects from 'sentry/utils/useProjects'; +import type {GroupSearchView} from 'sentry/views/issueList/types'; +import {getSortLabel} from 'sentry/views/issueList/utils'; +import {ProjectsRenderer} from 'sentry/views/traces/fieldRenderers'; + +type IssueViewsTableProps = { + isError: boolean; + isPending: boolean; + views: GroupSearchView[]; +}; + +function StarCellContent({isStarred}: {isStarred: boolean}) { + return ; +} + +function ProjectsCellContent({projects}: {projects: GroupSearchView['projects']}) { + const {projects: allProjects} = useProjects(); + + const projectSlugs = allProjects + .filter(project => projects.includes(parseInt(project.id, 10))) + .map(project => project.slug); + + if (projects.length === 0) { + return t('My Projects'); + } + if (projects.includes(-1)) { + return t('All Projects'); + } + return ; +} + +function EnvironmentsCellContent({ + environments, +}: { + environments: GroupSearchView['environments']; +}) { + const environmentsLabel = + environments.length === 0 ? t('All') : environments.join(', '); + + return ( + + {environmentsLabel} + + ); +} + +function TimeCellContent({timeFilters}: {timeFilters: GroupSearchView['timeFilters']}) { + if (timeFilters.period) { + return timeFilters.period; + } + + return getAbsoluteSummary(timeFilters.start, timeFilters.end, timeFilters.utc); +} + +function SharingCellContent({visibility}: {visibility: GroupSearchView['visibility']}) { + if (visibility === 'organization') { + return ( + + + + + + ); + } + return ( + + + + + + ); +} + +function LastVisitedCellContent({ + lastVisited, +}: { + lastVisited: GroupSearchView['lastVisited']; +}) { + if (!lastVisited) { + return '-'; + } + return ; +} + +export function IssueViewsTable({views, isPending, isError}: IssueViewsTableProps) { + const organization = useOrganization(); + + return ( + + {isError && } + {views.map((view, index) => ( + + + + {/* TODO: Add isStarred when the API is update to include it */} + + + + + {view.name} + + + + + + + + + + + + + + + {getSortLabel(view.querySort, organization)} + + + + + + + + ))} + + ); +} + +const StyledPanelTable = styled(PanelTable)` + white-space: nowrap; + font-size: ${p => p.theme.fontSizeMedium}; + overflow: auto; + grid-template-columns: 36px auto auto 1fr auto auto 105px 90px 115px; + + @media (min-width: ${p => p.theme.breakpoints.small}) { + overflow: hidden; + } + + & > * { + padding: ${space(1)} ${space(2)}; + } +`; + +const Row = styled('div')<{isFirst: boolean}>` + display: grid; + position: relative; + grid-template-columns: subgrid; + grid-column: 1/-1; + padding: 0; + + ${p => + p.isFirst && + css` + border-top: 1px solid ${p.theme.border}; + `} + + &:not(:last-child) { + border-bottom: 1px solid ${p => p.theme.innerBorder}; + } +`; + +const Cell = styled('div')` + display: flex; + align-items: center; + padding: ${space(1)} ${space(2)}; +`; + +const StarCell = styled(Cell)` + padding: 0 0 0 ${space(2)}; +`; + +const RowHoverStateLayer = styled(InteractionStateLayer)``; + +const RowLink = styled(Link)` + color: ${p => p.theme.textColor}; + + &:hover { + color: ${p => p.theme.textColor}; + text-decoration: underline; + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } +`; + +const PositionedTimeSince = styled(TimeSince)` + position: relative; +`; + +const PositionedContent = styled('div')` + position: relative; + display: flex; + align-items: center; +`;