Skip to content

Commit c08e609

Browse files
committed
Build issues dashboard query system and add factories for future testing
1 parent c629eae commit c08e609

File tree

45 files changed

+2216
-55
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2216
-55
lines changed

apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/ProjectSection/index.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export default function ProjectSection({
8585
limitedView?: boolean
8686
}) {
8787
const runs = useFeature('runs')
88+
const issuesFeature = useFeature('issues')
8889

8990
const disableRunsNotifications = limitedView || !runs.isEnabled
9091
const { data: active } = useActiveRunsCount({
@@ -116,6 +117,13 @@ export default function ProjectSection({
116117
: `There are ${count} runs in progress`,
117118
},
118119
},
120+
issuesFeature.isEnabled && {
121+
label: 'Issues',
122+
iconName: 'shieldAlert',
123+
route: ROUTES.projects
124+
.detail({ id: project.id })
125+
.commits.detail({ uuid: commit.uuid }).issues.root,
126+
},
119127
{
120128
label: 'Analytics',
121129
route: ROUTES.projects
@@ -131,7 +139,14 @@ export default function ProjectSection({
131139
iconName: 'history',
132140
},
133141
].filter(Boolean) as ProjectRoute[],
134-
[project, commit, runs, active, disableRunsNotifications],
142+
[
143+
project,
144+
commit,
145+
runs,
146+
issuesFeature.isEnabled,
147+
active,
148+
disableRunsNotifications,
149+
],
135150
)
136151

137152
return (

apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/(withTabs)/logs/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export default async function DocumentPage({
103103
await getDocumentLogsApproximatedCountCached(documentUuid)
104104
if (approximatedCount > LIMITED_VIEW_THRESHOLD) {
105105
return DocumentLogsLimitedPage({
106-
workspace: workspace,
106+
workspace,
107107
projectId: projectId,
108108
commit: commit,
109109
document: document,
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use client'
2+
3+
import { useMemo, useRef } from 'react'
4+
import { useSearchParams } from 'next/navigation'
5+
import { useIssues } from '$/stores/issues'
6+
import {
7+
SafeIssuesParams,
8+
convertIssuesParamsToQueryParams,
9+
} from '@latitude-data/constants/issues'
10+
import { useCurrentCommit } from '$/app/providers/CommitProvider'
11+
import { useCurrentProject } from '$/app/providers/ProjectProvider'
12+
import { useIssuesParameters } from '$/stores/issues/useIssuesParameters'
13+
import { useOnce } from '$/hooks/useMount'
14+
import { TableWithHeader } from '@latitude-data/web-ui/molecules/ListingHeader'
15+
import { ROUTES } from '$/services/routes'
16+
import { IssuesServerResponse } from '../../page'
17+
import { IssuesTable } from '../IssuesTable'
18+
import { IssuesFilters } from '../IssuesFilters'
19+
import { SearchIssuesInput } from '../SearchIssuesInput'
20+
21+
export function IssuesDashboard({
22+
serverResponse,
23+
params,
24+
}: {
25+
serverResponse: IssuesServerResponse
26+
params: SafeIssuesParams
27+
}) {
28+
const { init, urlParameters, onSuccessIssuesFetch } = useIssuesParameters(
29+
(state) => ({
30+
init: state.init,
31+
urlParameters: state.urlParameters,
32+
onSuccessIssuesFetch: state.onSuccessIssuesFetch,
33+
}),
34+
)
35+
const queryParams = useSearchParams()
36+
const { project } = useCurrentProject()
37+
const { commit } = useCurrentCommit()
38+
const page = Number(queryParams.get('page') ?? '1')
39+
const initialPage = useRef(page)
40+
const currentRoute = ROUTES.projects
41+
.detail({ id: project.id })
42+
.commits.detail({ uuid: commit.uuid }).issues.root
43+
const searchParams = useMemo(() => {
44+
if (!urlParameters) return convertIssuesParamsToQueryParams(params)
45+
return urlParameters
46+
}, [urlParameters, params])
47+
const { data: issues, isLoading } = useIssues(
48+
{
49+
projectId: project.id,
50+
commitUuid: commit.uuid,
51+
searchParams,
52+
initialPage: initialPage.current,
53+
onSuccess: onSuccessIssuesFetch,
54+
},
55+
{
56+
fallbackData: serverResponse,
57+
},
58+
)
59+
60+
useOnce(() => {
61+
init({
62+
params: {
63+
...params,
64+
totalCount: serverResponse.totalCount,
65+
},
66+
onStateChange: (queryParams) => {
67+
// NOTE: Next.js do RSC navigation, so we need to use the History API to avoid a full page reload
68+
window.history.replaceState(
69+
null,
70+
'',
71+
`${currentRoute}?${new URLSearchParams(queryParams).toString()}`,
72+
)
73+
},
74+
})
75+
})
76+
77+
return (
78+
<div className='flex flex-grow flex-col w-full p-6 gap-2 min-w-0'>
79+
<TableWithHeader
80+
title={<SearchIssuesInput serverParams={params} />}
81+
actions={<IssuesFilters serverParams={params} />}
82+
table={
83+
<IssuesTable
84+
issues={issues}
85+
isLoading={isLoading}
86+
currentRoute={currentRoute}
87+
serverParams={params}
88+
/>
89+
}
90+
/>
91+
</div>
92+
)
93+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useCallback } from 'react'
2+
import { useIssuesParameters } from '$/stores/issues/useIssuesParameters'
3+
import { DatePickerRange } from '@latitude-data/web-ui/atoms/DatePicker'
4+
import { RadioToggleInput } from '@latitude-data/web-ui/molecules/RadioToggleInput'
5+
import { Select } from '@latitude-data/web-ui/atoms/Select'
6+
import {
7+
SafeIssuesParams,
8+
ISSUE_STATUS,
9+
IssueStatus,
10+
} from '@latitude-data/constants/issues'
11+
import { useDocuments } from './useDocuments'
12+
import { useSeenAtDatePicker } from './useSeenAtDatePicker'
13+
14+
const STATUS_OPTIONS = [
15+
{ label: 'Active', value: ISSUE_STATUS.active },
16+
{ label: 'Regressed', value: ISSUE_STATUS.regressed },
17+
{ label: 'Archived', value: ISSUE_STATUS.archived },
18+
]
19+
export function IssuesFilters({
20+
serverParams,
21+
}: {
22+
serverParams: SafeIssuesParams
23+
}) {
24+
const { filters, setFilters } = useIssuesParameters((state) => ({
25+
filters: state.filters,
26+
setFilters: state.setFilters,
27+
}))
28+
const { dateWindow, onDateWindowChange } = useSeenAtDatePicker({
29+
serverParams,
30+
})
31+
const { documentOptions, isLoading: isLoadingDocuments } = useDocuments()
32+
33+
const onStatusChange = useCallback(
34+
(status: IssueStatus) => {
35+
setFilters({ status })
36+
},
37+
[setFilters],
38+
)
39+
40+
const onDocumentChange = useCallback(
41+
(documentUuid?: string | null) => {
42+
setFilters({ documentUuid })
43+
},
44+
[setFilters],
45+
)
46+
47+
return (
48+
<>
49+
<div className='flex'>
50+
<Select<string | null | undefined>
51+
removable
52+
searchable
53+
loading={isLoadingDocuments}
54+
width='full'
55+
align='end'
56+
name='document-filter'
57+
placeholder='Select document'
58+
options={documentOptions}
59+
value={filters.documentUuid ?? serverParams.filters.documentUuid}
60+
onChange={onDocumentChange}
61+
/>
62+
</div>
63+
<RadioToggleInput
64+
name='issue-status'
65+
options={STATUS_OPTIONS}
66+
value={
67+
filters.status ?? serverParams.filters.status ?? ISSUE_STATUS.active
68+
}
69+
onChange={onStatusChange}
70+
/>
71+
<DatePickerRange
72+
showPresets
73+
align='end'
74+
singleDatePrefix='Up to'
75+
placeholder='Filter by date'
76+
initialRange={dateWindow}
77+
onCloseChange={onDateWindowChange}
78+
/>
79+
</>
80+
)
81+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useMemo } from 'react'
2+
import { useCurrentCommit } from '$/app/providers/CommitProvider'
3+
import { useCurrentProject } from '$/app/providers/ProjectProvider'
4+
import useDocumentVersions from '$/stores/documentVersions'
5+
6+
function formatDocumentPath(path: string): string {
7+
// Remove .promptl extension if present
8+
const cleanPath = path.replace(/\.promptl$/, '')
9+
const segments = cleanPath.split('/')
10+
11+
if (segments.length === 1) return segments[0]
12+
13+
const filename = segments[segments.length - 1]
14+
const firstFolder = segments[0]
15+
16+
if (segments.length === 2) {
17+
return `${firstFolder}/${filename}`
18+
}
19+
20+
return `${firstFolder}/.../${filename}`
21+
}
22+
23+
export function useDocuments() {
24+
const { project } = useCurrentProject()
25+
const { commit } = useCurrentCommit()
26+
const { data: documents, isLoading } = useDocumentVersions({
27+
projectId: project?.id,
28+
commitUuid: commit?.uuid,
29+
})
30+
31+
const documentOptions = useMemo(
32+
() =>
33+
documents?.map((doc) => ({
34+
value: doc.documentUuid,
35+
label: formatDocumentPath(doc.path),
36+
})),
37+
[documents],
38+
)
39+
40+
return useMemo(
41+
() => ({
42+
documentOptions,
43+
isLoading,
44+
}),
45+
[documentOptions, isLoading],
46+
)
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { useMemo, useCallback } from 'react'
2+
import { useIssuesParameters } from '$/stores/issues/useIssuesParameters'
3+
import { SafeIssuesParams } from '@latitude-data/constants/issues'
4+
import { DateRange } from '@latitude-data/web-ui/atoms/DatePicker'
5+
6+
export function useSeenAtDatePicker({
7+
serverParams,
8+
}: {
9+
serverParams: SafeIssuesParams
10+
}) {
11+
const { filters, setFilters } = useIssuesParameters((state) => ({
12+
filters: state.filters,
13+
setFilters: state.setFilters,
14+
}))
15+
const firstSeen = filters.firstSeen ?? serverParams.filters.firstSeen
16+
const lastSeen = filters.lastSeen ?? serverParams.filters.lastSeen
17+
const firstSeenDate = firstSeen instanceof Date ? firstSeen : undefined
18+
const lastSeenDate = lastSeen instanceof Date ? lastSeen : undefined
19+
20+
const dateWindow: DateRange | undefined = useMemo(
21+
() =>
22+
firstSeenDate && lastSeenDate
23+
? { from: firstSeenDate, to: lastSeenDate }
24+
: firstSeenDate
25+
? { from: firstSeenDate, to: firstSeenDate }
26+
: lastSeenDate
27+
? { from: lastSeenDate, to: lastSeenDate }
28+
: undefined,
29+
[firstSeenDate, lastSeenDate],
30+
)
31+
32+
const onDateWindowChange = useCallback(
33+
(range: DateRange | undefined) => {
34+
// Convert DateRange back to single Date values
35+
// If only one date is selected (from without to), treat it as lastSeen filter only
36+
// This gives "show issues last seen until this date" behavior
37+
if (range?.from && !range?.to) {
38+
setFilters({
39+
firstSeen: undefined,
40+
lastSeen: range.from,
41+
})
42+
} else if (range?.from && range?.to) {
43+
// Full range: firstSeen represents the start, lastSeen represents the end
44+
setFilters({
45+
firstSeen: range.from,
46+
lastSeen: range.to,
47+
})
48+
} else {
49+
// Clear filters when no date is selected
50+
setFilters({
51+
firstSeen: undefined,
52+
lastSeen: undefined,
53+
})
54+
}
55+
},
56+
[setFilters],
57+
)
58+
59+
return useMemo(
60+
() => ({
61+
dateWindow,
62+
onDateWindowChange,
63+
}),
64+
[dateWindow, onDateWindowChange],
65+
)
66+
}

0 commit comments

Comments
 (0)