Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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