~bigbes/lethe

ref: 4abe63ea5d809ec3a7e87e349ccc9493a377b1bc lethe/web/src/routes/project.$.tsx -rw-r--r-- 4.1 KiB
4abe63ea — Eugene Blikh docs: conclude lethe collector task 24 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import React, { useEffect, useCallback } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { SubBar } from '../shell/SubBar'
import { Sub, EmptyState } from '../primitives'
import { ProjectHeader } from '../features/projects/ProjectHeader'
import { SessionsTable } from '../features/home/SessionsTable'
import { useSessions } from '../features/home/useSessions'
import { useProjects } from '../features/projects/useProjects'
import { useHomeCursor } from '../features/home/useHomeCursor'
import { useKeyboardCursor } from './__root'
import { AuthError, APIError } from '../api/client'
import type { Session } from '../api/adapters'
import '../styles/projects.css'

export const Route = createFileRoute('/project/$')({
  component: ProjectRoute,
})

function ProjectRoute(): React.JSX.Element {
  const navigate = useNavigate()
  const { _splat } = Route.useParams()
  const cwd = decodeURIComponent(_splat ?? '')

  const { data: sessions, isLoading: sessionsLoading, error: sessionsError } = useSessions({ cwd, since: 'all' })
  const { data: projects, isLoading: projectsLoading } = useProjects({ since: 'all' })

  const project = projects?.find(p => p.cwd === cwd)

  const isLoading = sessionsLoading || projectsLoading
  const error = sessionsError

  const handleOpen = useCallback((s: Session) => {
    void navigate({ to: '/session/$tool/$host/$id', params: { tool: s.tool, host: s.host, id: s.sessionId } })
  }, [navigate])

  const { cursor, move, activate, jumpTo } = useHomeCursor(
    sessions?.length ?? 0,
    useCallback((idx: number) => {
      if (sessions && sessions[idx]) handleOpen(sessions[idx])
    }, [sessions, handleOpen]),
  )

  const cursorRef = useKeyboardCursor()
  useEffect(() => {
    cursorRef.current = { move, activate }
    return () => {
      cursorRef.current = {
        move: (_d: 1 | -1) => { /* no-op */ },
        activate: () => { /* no-op */ },
      }
    }
  }, [cursorRef, move, activate])

  // Collect unique hosts from the loaded sessions
  const hosts = sessions != null
    ? Array.from(new Set(sessions.map(s => s.host))).sort()
    : (project?.hosts ?? [])

  const sessionCount = sessions?.length ?? project?.sessions ?? 0

  if (isLoading) {
    return (
      <>
        <SubBar>
          <span className="mono muted truncate">{cwd}</span>
        </SubBar>
        <div className="body body-pad">
          <Sub>loading</Sub>
        </div>
      </>
    )
  }

  if (error != null) {
    if (error instanceof AuthError) {
      return (
        <div className="body body-pad" style={{ display: 'flex', justifyContent: 'center', paddingTop: 60 }}>
          <div className="card" style={{ padding: '24px 32px', textAlign: 'center' }}>
            <div className="uppercase-mono" style={{ marginBottom: 8 }}>not authenticated</div>
            <div className="muted">Sign in to view this project.</div>
          </div>
        </div>
      )
    }
    const detail = error instanceof APIError ? error.message : String(error)
    return (
      <div className="body body-pad" style={{ display: 'flex', justifyContent: 'center', paddingTop: 60 }}>
        <div className="card" style={{ padding: '24px 32px', textAlign: 'center' }}>
          <div className="uppercase-mono" style={{ marginBottom: 8 }}>error</div>
          <div className="muted">{detail}</div>
        </div>
      </div>
    )
  }

  // If the project was not found in the index and sessions came back empty,
  // render an EmptyState rather than a blank page.
  if (project === undefined && (sessions == null || sessions.length === 0)) {
    return (
      <>
        <ProjectHeader cwd={cwd} project={undefined} sessionCount={0} hosts={[]} />
        <div className="body body-pad">
          <EmptyState glyph="∅" copy="no sessions found for this project" />
        </div>
      </>
    )
  }

  return (
    <>
      <ProjectHeader cwd={cwd} project={project} sessionCount={sessionCount} hosts={hosts} />
      <SubBar>
        <span className="mono muted">{sessionCount} sessions</span>
      </SubBar>
      <SessionsTable
        sessions={sessions ?? []}
        cursor={cursor}
        onCursor={jumpTo}
        onOpen={handleOpen}
      />
    </>
  )
}