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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { formatCount } from '$/lib/formatCount'
import { ROUTES } from '$/services/routes'
import { useActiveRunsCount } from '$/stores/runs/activeRuns'
import useFeature from '$/stores/useFeature'

import { Commit } from '@latitude-data/core/schema/models/types/Commit'
import { Project } from '@latitude-data/core/schema/models/types/Project'
Expand Down Expand Up @@ -84,6 +85,7 @@ export default function ProjectSection({
limitedView?: boolean
}) {
const disableRunsNotifications = !!limitedView
const issuesFeature = useFeature('issues')
const { data: active } = useActiveRunsCount({
project: project,
realtime: !disableRunsNotifications,
Expand Down Expand Up @@ -113,6 +115,13 @@ export default function ProjectSection({
: `There are ${count} runs in progress`,
},
},
issuesFeature.isEnabled && {
label: 'Issues',
iconName: 'shieldAlert',
route: ROUTES.projects
.detail({ id: project.id })
.commits.detail({ uuid: commit.uuid }).issues.root,
},
{
label: 'Analytics',
route: ROUTES.projects
Expand All @@ -128,7 +137,13 @@ export default function ProjectSection({
iconName: 'history',
},
].filter(Boolean) as ProjectRoute[],
[project, commit, active, disableRunsNotifications],
[
project,
commit,
issuesFeature.isEnabled,
active,
disableRunsNotifications,
],
)

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export default async function DocumentPage({
await getDocumentLogsApproximatedCountCached(documentUuid)
if (approximatedCount > LIMITED_VIEW_THRESHOLD) {
return DocumentLogsLimitedPage({
workspace: workspace,
workspace,
projectId: projectId,
commit: commit,
document: document,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client'

import { useMemo, useRef } from 'react'
import { useSearchParams } from 'next/navigation'
import { useIssues } from '$/stores/issues'
import {
SafeIssuesParams,
convertIssuesParamsToQueryParams,
} from '@latitude-data/constants/issues'
import { useCurrentCommit } from '$/app/providers/CommitProvider'
import { useCurrentProject } from '$/app/providers/ProjectProvider'
import { useIssuesParameters } from '$/stores/issues/useIssuesParameters'
import { useOnce } from '$/hooks/useMount'
import { TableWithHeader } from '@latitude-data/web-ui/molecules/ListingHeader'
import { ROUTES } from '$/services/routes'
import { IssuesServerResponse } from '../../page'
import { IssuesTable } from '../IssuesTable'
import { IssuesFilters } from '../IssuesFilters'
import { SearchIssuesInput } from '../SearchIssuesInput'

export function IssuesDashboard({
serverResponse,
params,
}: {
serverResponse: IssuesServerResponse
params: SafeIssuesParams
}) {
const { init, urlParameters, onSuccessIssuesFetch } = useIssuesParameters(
(state) => ({
init: state.init,
urlParameters: state.urlParameters,
onSuccessIssuesFetch: state.onSuccessIssuesFetch,
}),
)
const queryParams = useSearchParams()
const { project } = useCurrentProject()
const { commit } = useCurrentCommit()
const page = Number(queryParams.get('page') ?? '1')
const initialPage = useRef(page)
const currentRoute = ROUTES.projects
.detail({ id: project.id })
.commits.detail({ uuid: commit.uuid }).issues.root
const searchParams = useMemo(() => {
if (!urlParameters) return convertIssuesParamsToQueryParams(params)
return urlParameters
}, [urlParameters, params])
const { data: issues, isLoading } = useIssues(
{
projectId: project.id,
commitUuid: commit.uuid,
searchParams,
initialPage: initialPage.current,
onSuccess: onSuccessIssuesFetch,
},
{
fallbackData: serverResponse,
},
)

useOnce(() => {
init({
params: {
...params,
totalCount: serverResponse.totalCount,
},
onStateChange: (queryParams) => {
// NOTE: Next.js do RSC navigation, so we need to use the History API to avoid a full page reload
window.history.replaceState(
null,
'',
`${currentRoute}?${new URLSearchParams(queryParams).toString()}`,
)
},
})
})

return (
<div className='flex flex-grow flex-col w-full p-6 gap-2 min-w-0'>
<TableWithHeader
title={<SearchIssuesInput serverParams={params} />}
actions={<IssuesFilters serverParams={params} />}
table={
<IssuesTable
issues={issues}
isLoading={isLoading}
currentRoute={currentRoute}
serverParams={params}
/>
}
/>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useCallback } from 'react'
import { useIssuesParameters } from '$/stores/issues/useIssuesParameters'
import { DatePickerRange } from '@latitude-data/web-ui/atoms/DatePicker'
import { RadioToggleInput } from '@latitude-data/web-ui/molecules/RadioToggleInput'
import { Select } from '@latitude-data/web-ui/atoms/Select'
import {
SafeIssuesParams,
ISSUE_STATUS,
IssueStatus,
} from '@latitude-data/constants/issues'
import { useDocuments } from './useDocuments'
import { useSeenAtDatePicker } from './useSeenAtDatePicker'

const STATUS_OPTIONS = [
{ label: 'Active', value: ISSUE_STATUS.active },
{ label: 'Regressed', value: ISSUE_STATUS.regressed },
{ label: 'Archived', value: ISSUE_STATUS.archived },
]
export function IssuesFilters({
serverParams,
}: {
serverParams: SafeIssuesParams
}) {
const { filters, setFilters } = useIssuesParameters((state) => ({
filters: state.filters,
setFilters: state.setFilters,
}))
const { dateWindow, onDateWindowChange } = useSeenAtDatePicker({
serverParams,
})
const { documentOptions, isLoading: isLoadingDocuments } = useDocuments()

const onStatusChange = useCallback(
(status: IssueStatus) => {
setFilters({ status })
},
[setFilters],
)

const onDocumentChange = useCallback(
(documentUuid?: string | null) => {
setFilters({ documentUuid })
},
[setFilters],
)

return (
<>
<div className='flex'>
<Select<string | null | undefined>
removable
searchable
loading={isLoadingDocuments}
width='full'
align='end'
name='document-filter'
placeholder='Select document'
options={documentOptions}
value={filters.documentUuid ?? serverParams.filters.documentUuid}
onChange={onDocumentChange}
/>
</div>
<RadioToggleInput
name='issue-status'
options={STATUS_OPTIONS}
value={
filters.status ?? serverParams.filters.status ?? ISSUE_STATUS.active
}
onChange={onStatusChange}
/>
<DatePickerRange
showPresets
align='end'
singleDatePrefix='Up to'
placeholder='Filter by date'
initialRange={dateWindow}
onCloseChange={onDateWindowChange}
/>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useMemo } from 'react'
import { useCurrentCommit } from '$/app/providers/CommitProvider'
import { useCurrentProject } from '$/app/providers/ProjectProvider'
import useDocumentVersions from '$/stores/documentVersions'

function formatDocumentPath(path: string): string {
// Remove .promptl extension if present
const cleanPath = path.replace(/\.promptl$/, '')
const segments = cleanPath.split('/')

if (segments.length === 1) return segments[0]

const filename = segments[segments.length - 1]
const firstFolder = segments[0]

if (segments.length === 2) {
return `${firstFolder}/${filename}`
}

return `${firstFolder}/.../${filename}`
}

export function useDocuments() {
const { project } = useCurrentProject()
const { commit } = useCurrentCommit()
const { data: documents, isLoading } = useDocumentVersions({
projectId: project?.id,
commitUuid: commit?.uuid,
})

const documentOptions = useMemo(
() =>
documents?.map((doc) => ({
value: doc.documentUuid,
label: formatDocumentPath(doc.path),
})),
[documents],
)

return useMemo(
() => ({
documentOptions,
isLoading,
}),
[documentOptions, isLoading],
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useMemo, useCallback } from 'react'
import { useIssuesParameters } from '$/stores/issues/useIssuesParameters'
import { SafeIssuesParams } from '@latitude-data/constants/issues'
import { DateRange } from '@latitude-data/web-ui/atoms/DatePicker'

export function useSeenAtDatePicker({
serverParams,
}: {
serverParams: SafeIssuesParams
}) {
const { filters, setFilters } = useIssuesParameters((state) => ({
filters: state.filters,
setFilters: state.setFilters,
}))
const firstSeen = filters.firstSeen ?? serverParams.filters.firstSeen
const lastSeen = filters.lastSeen ?? serverParams.filters.lastSeen
const firstSeenDate = firstSeen instanceof Date ? firstSeen : undefined
const lastSeenDate = lastSeen instanceof Date ? lastSeen : undefined

const dateWindow: DateRange | undefined = useMemo(
() =>
firstSeenDate && lastSeenDate
? { from: firstSeenDate, to: lastSeenDate }
: firstSeenDate
? { from: firstSeenDate, to: firstSeenDate }
: lastSeenDate
? { from: lastSeenDate, to: lastSeenDate }
: undefined,
[firstSeenDate, lastSeenDate],
)

const onDateWindowChange = useCallback(
(range: DateRange | undefined) => {
// Convert DateRange back to single Date values
// If only one date is selected (from without to), treat it as lastSeen filter only
// This gives "show issues last seen until this date" behavior
if (range?.from && !range?.to) {
setFilters({
firstSeen: undefined,
lastSeen: range.from,
})
} else if (range?.from && range?.to) {
// Full range: firstSeen represents the start, lastSeen represents the end
setFilters({
firstSeen: range.from,
lastSeen: range.to,
})
} else {
// Clear filters when no date is selected
setFilters({
firstSeen: undefined,
lastSeen: undefined,
})
}
},
[setFilters],
)

return useMemo(
() => ({
dateWindow,
onDateWindowChange,
}),
[dateWindow, onDateWindowChange],
)
}
Loading
Loading