Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
294ada1
Add team-based channel analysis routes and pages
halsk Apr 20, 2025
1191f5a
Fix API URL for channel analysis endpoint
halsk Apr 20, 2025
6a31917
Fix API URL and add debugging for channel analysis
halsk Apr 20, 2025
7044587
Add mock channel support and fix API URL construction
halsk Apr 20, 2025
4e63089
Fix channel loading with multiple fallback methods
halsk Apr 20, 2025
cb28bbb
Fix external ID mapping and add detailed logging
halsk Apr 20, 2025
66c6f91
Fix URL construction for direct channel fetching
halsk Apr 20, 2025
36b97dd
Improve URL path handling for API endpoints
halsk Apr 20, 2025
74e00c7
Refactor to use slackApiClient instead of direct fetch
halsk Apr 20, 2025
2c64117
Refactor to use integrationService for fetching resource
halsk Apr 20, 2025
578fbe2
Fix getResource method to use getResources and filter by ID
halsk Apr 20, 2025
a7bb8f1
Simplify channel loading with better error handling
halsk Apr 20, 2025
d2790c9
Fix integration loading with better state handling
halsk Apr 20, 2025
ae22127
Fix SlackApiClient base URL and analyze endpoint URL construction
halsk Apr 20, 2025
880dd9b
Use database UUIDs instead of Slack IDs for channel analysis
halsk Apr 20, 2025
c524c08
Simplify code by removing fallbacks and unnecessary logging
halsk Apr 20, 2025
689b216
Fix integration loading with separate useEffect hooks
halsk Apr 20, 2025
502e8ad
Fix message synchronization for channel analysis
halsk Apr 21, 2025
5ddb933
Fix channel name display in analysis results
halsk Apr 21, 2025
d6f08f6
Fix duplicate API path issue and add analysis history button to chann…
halsk Apr 21, 2025
2680d6d
Remove unused env import from TeamChannelAnalysisHistoryPage
halsk Apr 21, 2025
e443d42
Fix TypeScript errors in team channel analysis features
halsk Apr 21, 2025
f5ae8ea
Add missing property to Channel interface
halsk Apr 21, 2025
5ec0175
Fix isort errors and build failures
halsk Apr 21, 2025
d5e8c0b
Fix CI issues and test failures
halsk Apr 21, 2025
0cdc4f0
Code formatting improvements
halsk Apr 21, 2025
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
1,124 changes: 1,122 additions & 2 deletions backend/app/api/v1/integration/router.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion backend/app/services/slack/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ async def select_channels_for_analysis(
if install_bot:
api_client = SlackApiClient(workspace.access_token)

# Unselect all channels if we are setting for_analysis=True
# Unselect all channels if we are setting for_analysis=True
# This is for backward compatibility with old behavior
if for_analysis:
# First, unselect all channels
Expand Down
8 changes: 6 additions & 2 deletions backend/tests/services/slack/test_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,9 @@ async def test_select_channels_for_analysis_without_bot_install(
assert "bot_installation" not in result

# Verify the db operations
assert mock_db_session.execute.call_count == 5 # Including all_channels query for debugging
assert (
mock_db_session.execute.call_count == 5
) # Including all_channels query for debugging
assert mock_db_session.commit.called


Expand Down Expand Up @@ -371,5 +373,7 @@ async def test_select_channels_for_analysis_with_bot_install(
mock_client.join_channel.assert_called_once_with(channel_without_bot.slack_id)

# Verify the db operations
assert mock_db_session.execute.call_count == 5 # Including all_channels query for debugging
assert (
mock_db_session.execute.call_count == 5
) # Including all_channels query for debugging
assert mock_db_session.commit.called
33 changes: 33 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ import {
IntegrationDetailPage,
IntegrationConnectPage,
TeamChannelSelectorPage,
TeamChannelAnalysisPage,
TeamAnalysisResultPage,
TeamChannelAnalysisHistoryPage,
} from './pages/integration'

// Profile Pages
Expand Down Expand Up @@ -266,6 +269,36 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/dashboard/integrations/:integrationId/channels/:channelId/analyze"
element={
<ProtectedRoute>
<AppLayout>
<TeamChannelAnalysisPage />
</AppLayout>
</ProtectedRoute>
}
/>
<Route
path="/dashboard/integrations/:integrationId/channels/:channelId/analysis/:analysisId"
element={
<ProtectedRoute>
<AppLayout>
<TeamAnalysisResultPage />
</AppLayout>
</ProtectedRoute>
}
/>
<Route
path="/dashboard/integrations/:integrationId/channels/:channelId/history"
element={
<ProtectedRoute>
<AppLayout>
<TeamChannelAnalysisHistoryPage />
</AppLayout>
</ProtectedRoute>
}
/>

{/* Profile routes */}
<Route
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import TeamChannelSelector from '../../../components/integration/TeamChannelSele
import IntegrationContext from '../../../context/IntegrationContext'
import { ResourceType } from '../../../lib/integrationService'

// Mock React Router
vi.mock('react-router-dom', () => ({
useNavigate: () => vi.fn(),
Link: ({ children }: { children: React.ReactNode }) => children,
}))

// Mock the useIntegration hook functionality through context
const mockIntegrationContext = {
// State
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/__tests__/context/IntegrationContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ vi.mock('../../lib/integrationService', () => ({
getSelectedChannels: vi.fn(),
selectChannelsForAnalysis: vi.fn(),
analyzeChannel: vi.fn(),
analyzeResource: vi.fn(),
},
IntegrationType: {
SLACK: 'slack',
Expand Down Expand Up @@ -221,7 +222,7 @@ describe('IntegrationContext', () => {
status: 'success',
message: 'Channels selected for analysis',
})
vi.mocked(integrationService.analyzeChannel).mockResolvedValue(
vi.mocked(integrationService.analyzeResource).mockResolvedValue(
mockAnalysisResult
)

Expand Down Expand Up @@ -706,7 +707,7 @@ describe('IntegrationContext', () => {
})

// Check that the service was called correctly
expect(integrationService.analyzeChannel).toHaveBeenCalledWith(
expect(integrationService.analyzeResource).toHaveBeenCalledWith(
'test-int-1',
'res-1',
options
Expand Down
62 changes: 47 additions & 15 deletions frontend/src/components/integration/TeamChannelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,16 @@ import {
Button,
Flex,
useToast,
Tooltip,
} from '@chakra-ui/react'
import { FiSearch, FiSettings, FiCheck, FiBarChart2 } from 'react-icons/fi'
import {
FiSearch,
FiSettings,
FiCheck,
FiBarChart2,
FiClock,
} from 'react-icons/fi'
import { useNavigate } from 'react-router-dom'
import { ResourceType } from '../../lib/integrationService'
import useIntegration from '../../context/useIntegration'

Expand All @@ -36,6 +44,7 @@ const TeamChannelSelector: React.FC<TeamChannelSelectorProps> = ({
integrationId,
}) => {
const toast = useToast()
const navigate = useNavigate()
const {
currentResources,
loadingResources,
Expand Down Expand Up @@ -348,20 +357,43 @@ const TeamChannelSelector: React.FC<TeamChannelSelectorProps> = ({
</Td>
<Td>
<HStack spacing={1}>
<IconButton
aria-label="Analyze channel"
icon={<FiBarChart2 />}
size="sm"
variant="ghost"
title="View analysis"
/>
<IconButton
aria-label="Channel settings"
icon={<FiSettings />}
size="sm"
variant="ghost"
title="Settings"
/>
<Tooltip label="Analyze channel">
<IconButton
aria-label="Analyze channel"
icon={<FiBarChart2 />}
size="sm"
variant="ghost"
colorScheme="blue"
onClick={() =>
navigate(
`/dashboard/integrations/${integrationId}/channels/${channel.id}/analyze`
)
}
/>
</Tooltip>
<Tooltip label="Analysis history">
<IconButton
aria-label="Analysis history"
icon={<FiClock />}
size="sm"
variant="ghost"
colorScheme="teal"
onClick={() =>
navigate(
`/dashboard/integrations/${integrationId}/channels/${channel.id}/history`
)
}
/>
</Tooltip>
<Tooltip label="Channel settings">
<IconButton
aria-label="Channel settings"
icon={<FiSettings />}
size="sm"
variant="ghost"
title="Settings"
/>
</Tooltip>
</HStack>
</Td>
</Tr>
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/context/IntegrationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1095,7 +1095,7 @@ export const IntegrationProvider: React.FC<{ children: React.ReactNode }> = ({
}))

try {
const result = await integrationService.analyzeChannel(
const result = await integrationService.analyzeResource(
integrationId,
channelId,
options
Expand All @@ -1115,7 +1115,12 @@ export const IntegrationProvider: React.FC<{ children: React.ReactNode }> = ({
loadingChannelSelection: false,
}))

return result
// Cast the result to the expected return type
return {
status: 'success',
analysis_id:
(result as { analysis_id?: string }).analysis_id || 'unknown',
}
} catch (error) {
setState((prev) => ({
...prev,
Expand Down
102 changes: 75 additions & 27 deletions frontend/src/lib/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class ApiClient {
protected async get<T>(
path: string,
params?: Record<string, string>
): Promise<T> {
): Promise<T | ApiError> {
const url = new URL(`${this.baseUrl}${path}`)

if (params) {
Expand All @@ -43,43 +43,91 @@ export class ApiClient {
})
}

const response = await fetch(url.toString(), {
method: 'GET',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
try {
const response = await fetch(url.toString(), {
method: 'GET',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})

if (!response.ok) {
throw new Error(`API Error: ${response.status} ${response.statusText}`)
}
// If the response is not OK, return an ApiError instead of throwing
if (!response.ok) {
let errorDetail: string
try {
// Try to parse error details from response
const errorJson = await response.json()
errorDetail =
errorJson.detail || errorJson.message || response.statusText
} catch {
// If can't parse JSON, use status text
errorDetail = response.statusText
}

return response.json()
return {
status: response.status,
message: `API Error: ${response.status} ${response.statusText}`,
detail: errorDetail,
} as ApiError
}

return response.json()
} catch (error) {
// Network errors or other fetch exceptions
return {
status: 'NETWORK_ERROR',
message: `Network Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
} as ApiError
}
}

// Standard POST request
protected async post<T>(
path: string,
data?: Record<string, unknown>
): Promise<T> {
): Promise<T | ApiError> {
const url = `${this.baseUrl}${path}`

const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
})
try {
const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
})

if (!response.ok) {
throw new Error(`API Error: ${response.status} ${response.statusText}`)
}
// If the response is not OK, return an ApiError instead of throwing
if (!response.ok) {
let errorDetail: string
try {
// Try to parse error details from response
const errorJson = await response.json()
errorDetail =
errorJson.detail || errorJson.message || response.statusText
} catch {
// If can't parse JSON, use status text
errorDetail = response.statusText
}

return response.json()
return {
status: response.status,
message: `API Error: ${response.status} ${response.statusText}`,
detail: errorDetail,
} as ApiError
}

return response.json()
} catch (error) {
// Network errors or other fetch exceptions
return {
status: 'NETWORK_ERROR',
message: `Network Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
} as ApiError
}
}
}
Loading
Loading