Skip to content
This repository was archived by the owner on Jul 8, 2025. It is now read-only.
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
4 changes: 4 additions & 0 deletions src/constants/empty-state-strings.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ import {
Hash01,
Key01,
PackageX,
Passport,
Server05,
} from '@untitled-ui/icons-react'

Expand DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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