Skip to content
This repository was archived by the owner on Jul 8, 2025. It is now read-only.

feat: support PII on the dashboard #326

Merged
merged 11 commits into from
Feb 17, 2025
4 changes: 4 additions & 0 deletions src/constants/empty-state-strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const emptyStateStrings = {
anErrorOccurred: 'An error occurred',
noLeakedSecretsDetected: 'No leaked secrets detected',
noMaliciousPackagesDetected: 'No malicious packages detected',
noPIIDetected:
'No leaked personally identifiable information (PII) detected',
noSearchResultsFor: (x: string | undefined): string =>
!x ? 'No search results' : `No search results for "${x}"`,
},
Expand All @@ -22,6 +24,8 @@ export const emptyStateStrings = {
'Messages are issues that CodeGate has detected and mitigated in your interactions with the LLM.',
secretsDesc:
'CodeGate helps you protect sensitive information from being accidentally exposed to AI models and third-party AI provider systems by redacting detected secrets from your prompts using encryption.',
piiDesc:
'CodeGate helps you protect sensitive personally identifiable information (PII) from being accidentally exposed to AI models and third-party AI provider systems by redacting detected PII from your prompts using encryption.',
maliciousDesc:
"CodeGate's dependency risk insight helps protect your codebase from malicious or vulnerable dependencies. It identifies potentially risky packages and suggests fixed versions or alternative packages to consider.",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ it('shows zero in alerts counts when no alerts', async () => {
name: /secrets count/i,
})
).toHaveTextContent('0')
expect(
screen.getByRole('button', {
name: /personally identifiable information.*count/i,
})
).toHaveTextContent('0')
})

it('shows count of malicious alerts in row', async () => {
Expand Down Expand Up @@ -80,3 +85,26 @@ it('shows count of secret alerts in row', async () => {
})
).toHaveTextContent('10')
})

it('shows count of pii alerts in row', async () => {
server.use(
http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () =>
HttpResponse.json([
mockConversation({
alertsConfig: { numAlerts: 10, type: 'pii' },
}),
])
)
)
render(<TableMessages />)

await waitFor(() => {
expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument()
})

expect(
screen.getByRole('button', {
name: /pii/i,
})
).toHaveTextContent('10')
})
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,48 @@ const TEST_CASES: TestCase[] = [
actions: null,
},
},
{
testDescription: 'Has alerts, view is "pii"',
handlers: [
http.get(mswEndpoint('/api/v1/workspaces'), () => {
return HttpResponse.json({
workspaces: [
{
name: 'default',
is_active: true,
},
{
name: 'foo-bar',
is_active: false,
},
],
})
}),
http.get(mswEndpoint('/api/v1/workspaces/archive'), () => {
return HttpResponse.json({
workspaces: [],
})
}),
http.get(
mswEndpoint('/api/v1/workspaces/:workspace_name/messages'),
() => {
return HttpResponse.json(
Array.from({ length: 10 }).map(() => mockAlert({ type: 'pii' }))
)
}
),
],
searchParams: {
view: AlertsFilterView.PII,
search: null,
},
expected: {
title: emptyStateStrings.title.noPIIDetected,
body: emptyStateStrings.body.piiDesc,
illustrationTestId: IllustrationTestId.DONE,
actions: null,
},
},
]

test.each(TEST_CASES)('$testDescription', async (testCase) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,56 +40,42 @@ test('shows correct count of all packages', async () => {
})
})

test('shows correct count of malicious packages', async () => {
server.use(
http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => {
return HttpResponse.json(
Array.from({ length: 13 }).map(() =>
mockConversation({
alertsConfig: {
type: 'malicious',
numAlerts: 1,
},
})
)
const filteredCases = [
{ tabLabel: /malicious/i, alertType: 'malicious' as const, count: 13 },
{ tabLabel: /secrets/i, alertType: 'secret' as const, count: 10 },
{ tabLabel: /pii/i, alertType: 'pii' as const, count: 9 },
]

filteredCases.forEach(({ tabLabel, alertType, count }) => {
test(`shows correct count of ${alertType} packages`, async () => {
server.use(
http.get(
mswEndpoint('/api/v1/workspaces/:workspace_name/messages'),
() => {
return HttpResponse.json(
Array.from({ length: count }).map(() =>
mockConversation({
alertsConfig: {
type: alertType,
numAlerts: 1,
},
})
)
)
}
)
})
)
)

const { getByRole } = render(
<TabsMessages>
<div>foo</div>
</TabsMessages>
)
const { getByRole } = render(
<TabsMessages>
<div>foo</div>
</TabsMessages>
)

await waitFor(() => {
expect(getByRole('tab', { name: /malicious/i })).toHaveTextContent('13')
})
})

test('shows correct count of secret packages', async () => {
server.use(
http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => {
return HttpResponse.json(
Array.from({ length: 13 }).map(() =>
mockConversation({
alertsConfig: {
type: 'secret',
numAlerts: 1,
},
})
)
await waitFor(() => {
expect(getByRole('tab', { name: tabLabel })).toHaveTextContent(
String(count)
)
})
)

const { getByRole } = render(
<TabsMessages>
<div>foo</div>
</TabsMessages>
)

await waitFor(() => {
expect(getByRole('tab', { name: /secrets/i })).toHaveTextContent('13')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Hash01,
Key01,
PackageX,
Passport,
Server05,
} from '@untitled-ui/icons-react'

Expand Down Expand Up @@ -51,8 +52,8 @@ function AlertsSummaryCount({
}: {
count: number
type: {
singular: 'malicious package' | 'secret'
plural: 'malicious packages' | 'secrets'
singular: string
plural: string
}
}) {
const typeText = count === 1 ? type.singular : type.plural
Expand Down Expand Up @@ -97,9 +98,9 @@ export function ConversationSummary({
}: {
conversation: Conversation
}) {
const { malicious, secrets } = conversation.alerts
const { malicious, secrets, pii } = conversation.alerts
? countConversationAlerts(conversation.alerts)
: { malicious: 0, secrets: 0 }
: { malicious: 0, secrets: 0, pii: 0 }

return (
<div className="flex gap-4">
Expand Down Expand Up @@ -166,6 +167,19 @@ export function ConversationSummary({
/>
}
/>
<ConversationSummaryListItem
icon={Passport}
title="PII"
value={
<AlertsSummaryCount
type={{
singular: 'personally identifiable information',
plural: 'personally identifiable information',
}}
count={pii}
/>
}
/>
</ConversationSummaryList>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ function EmptyStateSecrets() {
)
}

function EmptyStatePII() {
return (
<EmptyState
title={emptyStateStrings.title.noPIIDetected}
body={emptyStateStrings.body.piiDesc}
illustration={IllustrationDone}
actions={null}
/>
)
}

export function EmptyStateError() {
return (
<EmptyState
Expand Down Expand Up @@ -209,6 +220,15 @@ export function TableMessagesEmptyState() {
},
() => <EmptyStateNoMessagesInWorkspace />
)
.with(
{
hasWorkspaceMessages: true,
hasMultipleWorkspaces: P.any,
view: AlertsFilterView.PII,
isLoading: false,
},
() => <EmptyStatePII />
)
.with(
{
hasWorkspaceMessages: true,
Expand Down
15 changes: 13 additions & 2 deletions src/features/dashboard-messages/components/table-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useClientSidePagination } from '@/hooks/useClientSidePagination'
import { TableAlertTokenUsage } from './table-alert-token-usage'

import { useMessagesFilterSearchParams } from '../hooks/use-messages-filter-search-params'
import { Key01, PackageX } from '@untitled-ui/icons-react'
import { Key01, PackageX, Passport } from '@untitled-ui/icons-react'
import {
EmptyStateError,
TableMessagesEmptyState,
Expand All @@ -31,6 +31,7 @@ import {
TableMessagesColumn,
} from '../constants/table-messages-columns'
import { formatTime } from '@/lib/format-time'
import { isAlertPii } from '@/lib/is-alert-pii'

const getPromptText = (conversation: Conversation) => {
return (conversation.question_answers[0]?.question?.message ?? 'N/A')
Expand All @@ -52,10 +53,12 @@ function getTypeText(type: QuestionType) {
function countAlerts(alerts: Alert[]): {
secrets: number
malicious: number
pii: number
} {
return {
secrets: alerts.filter(isAlertSecret).length,
malicious: alerts.filter(isAlertMalicious).length,
pii: alerts.filter(isAlertPii).length,
}
}

Expand Down Expand Up @@ -93,7 +96,7 @@ function AlertsSummaryCount({
}

function AlertsSummaryCellContent({ alerts }: { alerts: Alert[] }) {
const { malicious, secrets } = countAlerts(alerts)
const { malicious, secrets, pii } = countAlerts(alerts)

return (
<div className="flex items-center gap-2">
Expand All @@ -113,6 +116,14 @@ function AlertsSummaryCellContent({ alerts }: { alerts: Alert[] }) {
count={secrets}
icon={Key01}
/>
<AlertsSummaryCount
strings={{
singular: 'personally identifiable information (PII)',
plural: 'personally identifiable information (PII)',
}}
count={pii}
icon={Passport}
/>
</div>
)
}
Expand Down
6 changes: 6 additions & 0 deletions src/features/dashboard-messages/components/tabs-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import {
import { SearchFieldMessages } from './search-field-messages'
import { tv } from 'tailwind-variants'
import { useQueryGetWorkspaceMessages } from '@/hooks/use-query-get-workspace-messages'
import { isConversationWithPII } from '@/lib/is-alert-pii'

type AlertsCount = {
all: number
malicious: number
secrets: number
pii: number
}

function select(data: V1GetWorkspaceMessagesResponse): AlertsCount {
Expand All @@ -36,10 +38,13 @@ function select(data: V1GetWorkspaceMessagesResponse): AlertsCount {
isConversationWithSecretAlerts,
]).length

const pii: number = multiFilter(data, [isConversationWithPII]).length

return {
all,
malicious,
secrets,
pii,
}
}

Expand Down Expand Up @@ -103,6 +108,7 @@ export function TabsMessages({ children }: { children: React.ReactNode }) {
count={data?.secrets ?? 0}
id={AlertsFilterView.SECRETS}
/>
<Tab title="PII" count={data?.pii ?? 0} id={AlertsFilterView.PII} />
</TabList>

<SearchFieldMessages className="ml-auto" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum AlertsFilterView {
ALL = 'all',
MALICIOUS = 'malicious',
SECRETS = 'secrets',
PII = 'pii',
}

const alertsFilterSchema = z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { isConversationWithMaliciousAlerts } from '../../../lib/is-alert-malicio
import { isConversationWithSecretAlerts } from '../../../lib/is-alert-secret'
import { filterMessagesBySubstring } from '../lib/filter-messages-by-substring'
import { useQueryGetWorkspaceMessages } from '@/hooks/use-query-get-workspace-messages'
import { isConversationWithPII } from '@/lib/is-alert-pii'

const FILTER: Record<
AlertsFilterView,
Expand All @@ -17,6 +18,7 @@ const FILTER: Record<
all: () => true,
malicious: isConversationWithMaliciousAlerts,
secrets: isConversationWithSecretAlerts,
pii: isConversationWithPII,
}

export function useQueryGetWorkspaceMessagesTable() {
Expand Down
Loading
Loading