• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacklok / codegate-ui / 13366180314

17 Feb 2025 08:42AM UTC coverage: 69.895% (+0.2%) from 69.732%
13366180314

Pull #326

github

web-flow
Merge ee0500726 into c85e1aa80
Pull Request #326: feat: support PII on the dashboard

419 of 657 branches covered (63.77%)

Branch coverage included in aggregate %.

10 of 10 new or added lines in 7 files covered. (100.0%)

3 existing lines in 1 file now uncovered.

844 of 1150 relevant lines covered (73.39%)

70.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.47
/src/features/dashboard-messages/components/table-messages.tsx
1
import {
2
  Cell,
3
  Column,
4
  Row,
5
  Table,
6
  TableBody,
7
  TableHeader,
8
  Button,
9
  ResizableTableContainer,
10
  Tooltip,
11
  TooltipTrigger,
12
} from '@stacklok/ui-kit'
13
import { Alert, Conversation, QuestionType } from '@/api/generated'
14

15
import { useClientSidePagination } from '@/hooks/useClientSidePagination'
16
import { TableAlertTokenUsage } from './table-alert-token-usage'
17

18
import { useMessagesFilterSearchParams } from '../hooks/use-messages-filter-search-params'
19
import { Key01, PackageX } from '@untitled-ui/icons-react'
20
import {
21
  EmptyStateError,
22
  TableMessagesEmptyState,
23
} from './table-messages-empty-state'
24
import { hrefs } from '@/lib/hrefs'
25
import { isAlertMalicious } from '../../../lib/is-alert-malicious'
26
import { isAlertSecret } from '../../../lib/is-alert-secret'
27
import { twMerge } from 'tailwind-merge'
28
import { useQueryGetWorkspaceMessagesTable } from '../hooks/use-query-get-workspace-messages-table'
29
import {
30
  TABLE_MESSAGES_COLUMNS,
31
  TableMessagesColumn,
32
} from '../constants/table-messages-columns'
33
import { formatTime } from '@/lib/format-time'
34

35
const getPromptText = (conversation: Conversation) => {
7✔
36
  return (conversation.question_answers[0]?.question?.message ?? 'N/A')
185!
37
    .trim()
38
    .slice(0, 200) // arbitrary slice to prevent long prompts
39
}
40

41
function getTypeText(type: QuestionType) {
42
  switch (type) {
185!
43
    case QuestionType.CHAT:
44
      return 'Chat'
185✔
45
    case QuestionType.FIM:
UNCOV
46
      return 'Fill in the middle (FIM)'
×
47
    default:
UNCOV
48
      return 'Unknown'
×
49
  }
50
}
51

52
function countAlerts(alerts: Alert[]): {
53
  secrets: number
54
  malicious: number
55
} {
56
  return {
185✔
57
    secrets: alerts.filter(isAlertSecret).length,
58
    malicious: alerts.filter(isAlertMalicious).length,
59
  }
60
}
61

62
function AlertsSummaryCount({
63
  count,
64
  icon: Icon,
65
  strings,
66
}: {
67
  count: number
68
  icon: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element
69
  strings: {
70
    singular: string
71
    plural: string
72
  }
73
}) {
74
  const tooltipText = `${count} ${count === 1 ? strings.singular : strings.plural} detected`
370✔
75

76
  return (
77
    <TooltipTrigger delay={0}>
78
      <Button
79
        aria-label={`${strings?.plural} count`}
80
        variant="tertiary"
81
        isIcon
82
        className={twMerge(
83
          'flex items-center gap-1',
84
          count > 0 ? 'text-secondary' : 'text-disabled'
370✔
85
        )}
86
      >
87
        <Icon className="size-4" />
88
        {count}
89
      </Button>
90
      <Tooltip>{tooltipText}</Tooltip>
91
    </TooltipTrigger>
92
  )
93
}
94

95
function AlertsSummaryCellContent({ alerts }: { alerts: Alert[] }) {
96
  const { malicious, secrets } = countAlerts(alerts)
185✔
97

98
  return (
99
    <div className="flex items-center gap-2">
100
      <AlertsSummaryCount
101
        strings={{
102
          singular: 'malicious package',
103
          plural: 'malicious packages',
104
        }}
105
        count={malicious}
106
        icon={PackageX}
107
      />
108
      <AlertsSummaryCount
109
        strings={{
110
          singular: 'secret',
111
          plural: 'secrets',
112
        }}
113
        count={secrets}
114
        icon={Key01}
115
      />
116
    </div>
117
  )
118
}
119

120
function CellRenderer({
121
  column,
122
  row,
123
}: {
124
  column: TableMessagesColumn
125
  row: Conversation
126
}) {
127
  switch (column.id) {
925!
128
    case 'time':
129
      return (
130
        <span className="whitespace-nowrap text-secondary">
131
          {formatTime(new Date(row.conversation_timestamp))}
132
        </span>
133
      )
134
    case 'type':
135
      return getTypeText(row.type)
185✔
136
    case 'prompt':
137
      return getPromptText(row)
185✔
138
    case 'alerts':
139
      return <AlertsSummaryCellContent alerts={row.alerts ?? []} />
185!
140
    case 'token_usage':
141
      return <TableAlertTokenUsage usage={row.token_usage_agg} />
142

143
    default:
UNCOV
144
      return column.id satisfies never
×
145
  }
146
}
147

148
export function TableMessages() {
149
  const { state, prevPage, nextPage } = useMessagesFilterSearchParams()
82✔
150

151
  const { data = [], isError } = useQueryGetWorkspaceMessagesTable()
82✔
152
  const { dataView, hasNextPage, hasPreviousPage } = useClientSidePagination(
82✔
153
    data,
154
    state.page,
155
    15
156
  )
157

158
  return (
159
    <>
160
      <ResizableTableContainer>
161
        <Table data-testid="messages-table" aria-label="Alerts table">
162
          <TableHeader columns={TABLE_MESSAGES_COLUMNS}>
163
            {(column) => <Column {...column} id={column.id} />}
164
          </TableHeader>
165
          <TableBody
166
            renderEmptyState={() => {
167
              if (isError) return <EmptyStateError />
168

169
              return <TableMessagesEmptyState />
170
            }}
171
            items={dataView}
172
          >
173
            {(row) => (
174
              <Row
175
                columns={TABLE_MESSAGES_COLUMNS}
176
                id={row.chat_id}
177
                href={hrefs.prompt(row.chat_id)}
178
                data-timestamp={row.conversation_timestamp}
179
              >
180
                {(column) => (
181
                  <Cell
182
                    className="h-5 truncate py-1 group-last/row:border-b-0"
183
                    alignment={column.alignment}
184
                    id={column.id}
185
                  >
186
                    <CellRenderer column={column} row={row} />
187
                  </Cell>
188
                )}
189
              </Row>
190
            )}
191
          </TableBody>
192
        </Table>
193
      </ResizableTableContainer>
194

195
      {hasNextPage || hasPreviousPage ? (
238✔
196
        <div className="flex w-full justify-center p-4">
197
          <div className="grid grid-cols-2 gap-2">
198
            <Button
199
              variant="secondary"
200
              isDisabled={!hasPreviousPage}
201
              onPress={prevPage}
202
            >
203
              Previous
204
            </Button>
205
            <Button
206
              variant="secondary"
207
              isDisabled={!hasNextPage}
208
              onPress={nextPage}
209
            >
210
              Next
211
            </Button>
212
          </div>
213
        </div>
214
      ) : null}
215
    </>
216
  )
217
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc