diff --git a/src/app/components/dashboard/dashboard.css b/src/app/components/dashboard/dashboard.css index 39a6f2d..ca05606 100644 --- a/src/app/components/dashboard/dashboard.css +++ b/src/app/components/dashboard/dashboard.css @@ -226,6 +226,12 @@ font-weight: 600; } +.integration-banner.slack-banner { + background: rgba(245, 232, 255, 0.9); + color: #6b21a8; + border-color: rgba(124, 58, 237, 0.22); +} + .filter-row { display: flex; flex-wrap: wrap; diff --git a/src/app/components/dashboard/dashboard.html b/src/app/components/dashboard/dashboard.html index c12a297..d113e98 100644 --- a/src/app/components/dashboard/dashboard.html +++ b/src/app/components/dashboard/dashboard.html @@ -17,10 +17,11 @@

Decision Workspace Dashboard

-

Track active workspaces and GitHub work in one place with clean structure and minimal noise.

+

Track active workspaces, Slack conversations, and GitHub work in one place with clean structure and minimal noise.

+

{{ slackBanner }}

{{ githubBanner }}

@@ -54,10 +55,11 @@

No workspaces yet

Signals

-

Assigned GitHub issues and pull requests appear here as action-ready signals.

+

Slack messages and GitHub updates appear here as action-ready signals.

+ @@ -74,7 +76,7 @@

Signals

No matching signals

-

Connect GitHub and run a sync to start pulling assigned issues and pull requests into Sentinent.

+

Connect Slack or GitHub in workspace integrations to start pulling messages and assigned work into Sentinent.

diff --git a/src/app/components/dashboard/dashboard.spec.ts b/src/app/components/dashboard/dashboard.spec.ts index 3b048cf..c5f36f4 100644 --- a/src/app/components/dashboard/dashboard.spec.ts +++ b/src/app/components/dashboard/dashboard.spec.ts @@ -28,6 +28,21 @@ describe('Dashboard', () => { mockSignalService = { getSignals: jasmine.createSpy('getSignals').and.returnValue(of([ + { + id: 'slack-1', + sourceType: 'slack', + sourceId: 'C123456', + externalId: '1', + title: 'Test Slack Signal', + content: 'Slack signal content', + author: '@ops', + status: 'unread', + receivedAt: new Date(), + metadata: { + channel: 'general', + channelId: 'C123456' + } + }, { id: 'github-1', sourceType: 'github', @@ -86,4 +101,10 @@ describe('Dashboard', () => { const compiled = fixture.nativeElement as HTMLElement; expect(compiled.textContent).toContain('Test GitHub Signal'); }); + + it('should render slack filter and signals', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.textContent).toContain('Slack'); + expect(compiled.textContent).toContain('Test Slack Signal'); + }); }); diff --git a/src/app/components/dashboard/dashboard.ts b/src/app/components/dashboard/dashboard.ts index 0d4d3be..526544b 100644 --- a/src/app/components/dashboard/dashboard.ts +++ b/src/app/components/dashboard/dashboard.ts @@ -26,6 +26,7 @@ export class Dashboard implements OnInit { signals: Signal[] = []; filters: SignalFilters = { source: 'all', status: 'all' }; githubBanner = ''; + slackBanner = ''; ngOnInit() { this.workspaceService.getWorkspaces().subscribe(ws => { @@ -34,11 +35,17 @@ export class Dashboard implements OnInit { this.route.queryParamMap.subscribe(params => { const githubStatus = params.get('github'); + const slackStatus = params.get('slack'); this.githubBanner = githubStatus === 'connected' ? 'GitHub connected successfully. Review repository access in your workspace integrations.' : githubStatus === 'error' ? 'GitHub connection failed. Please try the OAuth flow again.' : ''; + this.slackBanner = slackStatus === 'connected' + ? 'Slack connected successfully. Choose the channels you want Sentinent to monitor.' + : slackStatus === 'error' + ? 'Slack connection failed. Please try the OAuth flow again.' + : ''; }); this.loadSignals(); diff --git a/src/app/components/signal-board/signal-board.css b/src/app/components/signal-board/signal-board.css index 5d48abd..67a960b 100644 --- a/src/app/components/signal-board/signal-board.css +++ b/src/app/components/signal-board/signal-board.css @@ -31,6 +31,14 @@ font-weight: 700; } +.signal-source.slack { + background: #7c3aed; +} + +.signal-source.github { + background: #111827; +} + .signal-card h3 { margin: 0; font-size: 1.05rem; diff --git a/src/app/components/signal-board/signal-board.html b/src/app/components/signal-board/signal-board.html index 39b4ef9..aaf570d 100644 --- a/src/app/components/signal-board/signal-board.html +++ b/src/app/components/signal-board/signal-board.html @@ -2,7 +2,7 @@
- GitHub + {{ getSourceLabel(signal) }}

{{ signal.title }}

@@ -10,13 +10,12 @@

{{ signal.title }}

-

- {{ getTypeLabel(signal) }} #{{ signal.metadata.number }} in {{ signal.metadata.repository }} -

+

{{ getPrimaryContext(signal) }}

{{ signal.content }}

- {{ signal.metadata.state }} + {{ signal.metadata.state }} + #{{ getSlackChannel(signal) }} {{ label }}
@@ -28,7 +27,7 @@

{{ signal.title }}

- Open in GitHub + {{ getOpenLabel(signal) }}
diff --git a/src/app/components/signal-board/signal-board.ts b/src/app/components/signal-board/signal-board.ts index 93ba377..e96a800 100644 --- a/src/app/components/signal-board/signal-board.ts +++ b/src/app/components/signal-board/signal-board.ts @@ -19,6 +19,34 @@ export class SignalBoardComponent { } getTypeLabel(signal: Signal): string { + if (signal.sourceType === 'slack') { + return 'Slack Message'; + } + return signal.metadata.type === 'pull_request' ? 'Pull Request' : 'Issue'; } + + getSourceLabel(signal: Signal): string { + return signal.sourceType === 'slack' ? 'Slack' : 'GitHub'; + } + + getSourceClass(signal: Signal): string { + return signal.sourceType === 'slack' ? 'slack' : 'github'; + } + + getPrimaryContext(signal: Signal): string { + if (signal.sourceType === 'slack') { + return `#${this.getSlackChannel(signal)} in Slack`; + } + + return `${this.getTypeLabel(signal)} #${signal.metadata.number} in ${signal.metadata.repository}`; + } + + getOpenLabel(signal: Signal): string { + return signal.sourceType === 'slack' ? 'Open in Slack' : 'Open in GitHub'; + } + + getSlackChannel(signal: Signal): string { + return String(signal.metadata['channel'] ?? 'channel'); + } } diff --git a/src/app/components/workspace-integrations/workspace-integrations.css b/src/app/components/workspace-integrations/workspace-integrations.css index 1cff064..5fe0789 100644 --- a/src/app/components/workspace-integrations/workspace-integrations.css +++ b/src/app/components/workspace-integrations/workspace-integrations.css @@ -74,6 +74,11 @@ font-weight: 700; } +.provider-badge.slack-badge { + background: #fce7f3; + color: #9d174d; +} + .card-actions, .section-head { display: flex; @@ -100,6 +105,11 @@ color: #fff; } +.slack-btn { + border-color: #7c3aed; + background: #7c3aed; +} + .secondary-btn { border: 1px solid #cbd5e1; background: #fff; diff --git a/src/app/components/workspace-integrations/workspace-integrations.html b/src/app/components/workspace-integrations/workspace-integrations.html index 0f303f3..605c9df 100644 --- a/src/app/components/workspace-integrations/workspace-integrations.html +++ b/src/app/components/workspace-integrations/workspace-integrations.html @@ -2,19 +2,73 @@

Workspace Integration

-

GitHub Activity Feed

+

Workspace Integrations

- Connect GitHub to pull assigned issues and pull requests into Sentinent without leaving your workspace flow. + Connect Slack and GitHub so Sentinent can pull team messages and assigned development work into one dashboard.

Back to workspace
+
+
+ Slack +

Slack connection

+

+ Connected to {{ slackWorkspaceName }} + ({{ slackWorkspaceUrl }}). +

+ +

Not connected yet. Start the OAuth flow to import Slack messages into Sentinent.

+
+
+ +
+ + +
+
+ +

{{ slackFeedbackMessage }}

+

{{ slackErrorMessage }}

+ +
+
+
+

Channel selection

+

Choose which Slack channels should appear as signals in the dashboard.

+
+ +
+ +
+ +
+
+ +
+

Slack sync status

+

Last channel update: {{ slackLastSyncAt | date: 'medium' }}

+

No Slack sync has run yet.

+
+
GitHub -

Connection status

-

+

GitHub connection

+

Connected as {{ accountName }} ({{ accountHandle }}).

@@ -24,25 +78,25 @@

Connection status

- - - + +
-

{{ feedbackMessage }}

-

{{ errorMessage }}

+

{{ githubFeedbackMessage }}

+

{{ githubErrorMessage }}

-
+

Repository selection

Choose which repositories should contribute GitHub signals to the dashboard.

-
@@ -61,14 +115,14 @@

Repository selection

-
-

Sync status

-

Last sync: {{ lastSyncAt | date: 'medium' }}

-

No sync has run yet.

-

+

+

GitHub sync status

+

Last sync: {{ githubLastSyncAt | date: 'medium' }}

+

No sync has run yet.

+

Current status: - {{ syncStatus.status }} - ({{ syncStatus.itemsSynced }} items) + {{ githubSyncStatus.status }} + ({{ githubSyncStatus.itemsSynced }} items)

diff --git a/src/app/components/workspace-integrations/workspace-integrations.spec.ts b/src/app/components/workspace-integrations/workspace-integrations.spec.ts index b88f2b2..da8cf55 100644 --- a/src/app/components/workspace-integrations/workspace-integrations.spec.ts +++ b/src/app/components/workspace-integrations/workspace-integrations.spec.ts @@ -11,6 +11,11 @@ describe('WorkspaceIntegrationsComponent', () => { beforeEach(async () => { mockIntegrationService = jasmine.createSpyObj('IntegrationService', [ + 'getSlackAuthUrl', + 'getSlackChannels', + 'connectSlack', + 'updateSlackChannels', + 'disconnectSlack', 'getGitHubAuthUrl', 'getGitHubRepos', 'connectGitHub', @@ -20,6 +25,20 @@ describe('WorkspaceIntegrationsComponent', () => { 'getSyncStatus' ]); + mockIntegrationService.getSlackChannels.and.returnValue(of({ + connected: true, + workspaceName: 'Sentinent Ops', + workspaceUrl: 'sentinent.slack.com', + channels: [ + { id: 'C123', name: 'general', isConnected: true } + ], + lastSyncAt: new Date('2026-03-24T09:00:00Z') + })); + mockIntegrationService.updateSlackChannels.and.returnValue(of(void 0)); + mockIntegrationService.disconnectSlack.and.returnValue(of(void 0)); + mockIntegrationService.getSlackAuthUrl.and.returnValue(of({ authUrl: 'https://slack.com/mock' })); + mockIntegrationService.connectSlack.and.returnValue(of({ connected: true })); + mockIntegrationService.getGitHubRepos.and.returnValue(of({ connected: true, accountName: 'Sentinent Engineering', @@ -65,6 +84,8 @@ describe('WorkspaceIntegrationsComponent', () => { const compiled = fixture.nativeElement as HTMLElement; expect(compiled.textContent).toContain('frontend-angular'); expect(compiled.textContent).toContain('Sentinent Engineering'); + expect(compiled.textContent).toContain('Sentinent Ops'); + expect(compiled.textContent).toContain('general'); }); it('should trigger sync and show refreshed item count', () => { @@ -72,6 +93,13 @@ describe('WorkspaceIntegrationsComponent', () => { fixture.detectChanges(); expect(mockIntegrationService.syncGitHub).toHaveBeenCalled(); - expect(component.feedbackMessage).toContain('6 items refreshed'); + expect(component.githubFeedbackMessage).toContain('6 items refreshed'); + }); + + it('should save slack channel selection', () => { + component.saveSlackChannelSelection(); + + expect(mockIntegrationService.updateSlackChannels).toHaveBeenCalled(); + expect(component.slackFeedbackMessage).toContain('saved'); }); }); diff --git a/src/app/components/workspace-integrations/workspace-integrations.ts b/src/app/components/workspace-integrations/workspace-integrations.ts index 76d68de..ac08faa 100644 --- a/src/app/components/workspace-integrations/workspace-integrations.ts +++ b/src/app/components/workspace-integrations/workspace-integrations.ts @@ -3,6 +3,7 @@ import { Component, OnInit, inject } from '@angular/core'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { IntegrationService } from '../../services/integration.service'; import { GitHubRepo, SyncStatus } from '../../models/github-integration.model'; +import { SlackChannel } from '../../models/slack-integration.model'; @Component({ selector: 'app-workspace-integrations', @@ -16,29 +17,58 @@ export class WorkspaceIntegrationsComponent implements OnInit { private readonly integrationService = inject(IntegrationService); workspaceId = ''; + slackChannels: SlackChannel[] = []; + selectedSlackChannelIds: string[] = []; + isSlackConnected = false; + slackWorkspaceName = ''; + slackWorkspaceUrl = ''; + slackLastSyncAt?: Date; + slackFeedbackMessage = ''; + slackErrorMessage = ''; + isSlackSaving = false; + repos: GitHubRepo[] = []; selectedRepoIds: number[] = []; - isConnected = false; + isGitHubConnected = false; accountName = ''; accountHandle = ''; - lastSyncAt?: Date; - syncStatus?: SyncStatus; - feedbackMessage = ''; - errorMessage = ''; - isSaving = false; + githubLastSyncAt?: Date; + githubSyncStatus?: SyncStatus; + githubFeedbackMessage = ''; + githubErrorMessage = ''; + isGitHubSaving = false; isSyncing = false; ngOnInit(): void { this.workspaceId = this.route.snapshot.paramMap.get('id') ?? ''; + this.loadSlackChannels(); this.loadRepos(); } + connectSlack(): void { + this.slackErrorMessage = ''; + this.slackFeedbackMessage = 'Starting Slack connection...'; + this.integrationService.getSlackAuthUrl().subscribe(() => { + this.integrationService.connectSlack().subscribe(() => { + this.slackFeedbackMessage = 'Slack workspace connected. Choose the channels you want to monitor.'; + this.loadSlackChannels(); + }); + }); + } + + disconnectSlack(): void { + this.integrationService.disconnectSlack().subscribe(() => { + this.slackFeedbackMessage = 'Slack integration disconnected.'; + this.loadSlackChannels(); + }); + } + connectGitHub(): void { - this.errorMessage = ''; - this.feedbackMessage = 'Starting GitHub connection...'; + this.githubErrorMessage = ''; + this.githubFeedbackMessage = 'Starting GitHub connection...'; this.integrationService.getGitHubAuthUrl().subscribe(() => { this.integrationService.connectGitHub().subscribe(() => { - this.feedbackMessage = 'GitHub account connected. Select the repositories you want Sentinent to monitor.'; + this.githubFeedbackMessage = 'GitHub account connected. Select the repositories you want Sentinent to monitor.'; this.loadRepos(); }); }); @@ -46,12 +76,23 @@ export class WorkspaceIntegrationsComponent implements OnInit { disconnectGitHub(): void { this.integrationService.disconnectGitHub().subscribe(() => { - this.syncStatus = undefined; - this.feedbackMessage = 'GitHub integration disconnected.'; + this.githubSyncStatus = undefined; + this.githubFeedbackMessage = 'GitHub integration disconnected.'; this.loadRepos(); }); } + toggleSlackChannel(channelId: string, checked: boolean): void { + this.selectedSlackChannelIds = checked + ? [...this.selectedSlackChannelIds, channelId] + : this.selectedSlackChannelIds.filter(id => id !== channelId); + } + + onSlackChannelToggle(channelId: string, event: Event): void { + const input = event.target as HTMLInputElement | null; + this.toggleSlackChannel(channelId, input?.checked ?? false); + } + toggleRepo(repoId: number, checked: boolean): void { this.selectedRepoIds = checked ? [...this.selectedRepoIds, repoId] @@ -63,55 +104,86 @@ export class WorkspaceIntegrationsComponent implements OnInit { this.toggleRepo(repoId, input?.checked ?? false); } + saveSlackChannelSelection(): void { + this.isSlackSaving = true; + this.slackErrorMessage = ''; + this.integrationService.updateSlackChannels(this.selectedSlackChannelIds).subscribe({ + next: () => { + this.isSlackSaving = false; + this.slackFeedbackMessage = 'Slack channel selection saved.'; + this.loadSlackChannels(); + }, + error: () => { + this.isSlackSaving = false; + this.slackErrorMessage = 'Could not save Slack channel selection.'; + } + }); + } + saveRepoSelection(): void { - this.isSaving = true; - this.errorMessage = ''; + this.isGitHubSaving = true; + this.githubErrorMessage = ''; this.integrationService.updateGitHubRepos(this.selectedRepoIds).subscribe({ next: () => { - this.isSaving = false; - this.feedbackMessage = 'Repository selection saved.'; + this.isGitHubSaving = false; + this.githubFeedbackMessage = 'Repository selection saved.'; this.loadRepos(); }, error: () => { - this.isSaving = false; - this.errorMessage = 'Could not save repository selection.'; + this.isGitHubSaving = false; + this.githubErrorMessage = 'Could not save repository selection.'; } }); } syncNow(): void { this.isSyncing = true; - this.errorMessage = ''; + this.githubErrorMessage = ''; this.integrationService.syncGitHub().subscribe({ next: ({ syncId }) => { this.integrationService.getSyncStatus(syncId).subscribe(status => { - this.syncStatus = status; + this.githubSyncStatus = status; this.isSyncing = false; - this.lastSyncAt = status.completedAt; - this.feedbackMessage = status.status === 'completed' + this.githubLastSyncAt = status.completedAt; + this.githubFeedbackMessage = status.status === 'completed' ? `Sync completed. ${status.itemsSynced ?? 0} items refreshed.` : 'GitHub sync failed.'; }); }, error: (error: Error) => { this.isSyncing = false; - this.errorMessage = error.message; + this.githubErrorMessage = error.message; } }); } + isSlackChannelSelected(channelId: string): boolean { + return this.selectedSlackChannelIds.includes(channelId); + } + isRepoSelected(repoId: number): boolean { return this.selectedRepoIds.includes(repoId); } + private loadSlackChannels(): void { + this.integrationService.getSlackChannels().subscribe(response => { + this.isSlackConnected = response.connected; + this.slackChannels = response.channels; + this.selectedSlackChannelIds = response.channels.filter(channel => channel.isConnected).map(channel => channel.id); + this.slackWorkspaceName = response.workspaceName ?? ''; + this.slackWorkspaceUrl = response.workspaceUrl ?? ''; + this.slackLastSyncAt = response.lastSyncAt; + }); + } + private loadRepos(): void { this.integrationService.getGitHubRepos().subscribe(response => { - this.isConnected = response.connected; + this.isGitHubConnected = response.connected; this.repos = response.repos; this.selectedRepoIds = response.repos.filter(repo => repo.isConnected).map(repo => repo.id); this.accountName = response.accountName ?? ''; this.accountHandle = response.accountHandle ?? ''; - this.lastSyncAt = response.lastSyncAt; + this.githubLastSyncAt = response.lastSyncAt; }); } } diff --git a/src/app/models/slack-integration.model.ts b/src/app/models/slack-integration.model.ts new file mode 100644 index 0000000..91f3d81 --- /dev/null +++ b/src/app/models/slack-integration.model.ts @@ -0,0 +1,13 @@ +export interface SlackChannel { + id: string; + name: string; + isConnected: boolean; +} + +export interface SlackConnectionState { + connected: boolean; + workspaceName?: string; + workspaceUrl?: string; + channels: SlackChannel[]; + lastSyncAt?: Date; +} diff --git a/src/app/services/integration.service.ts b/src/app/services/integration.service.ts index 1255b0e..9fea9e2 100644 --- a/src/app/services/integration.service.ts +++ b/src/app/services/integration.service.ts @@ -1,11 +1,21 @@ import { Injectable } from '@angular/core'; import { Observable, of, throwError } from 'rxjs'; import { GitHubConnectionState, GitHubRepo, SyncStatus } from '../models/github-integration.model'; +import { SlackChannel, SlackConnectionState } from '../models/slack-integration.model'; @Injectable({ providedIn: 'root' }) export class IntegrationService { + private slackState: SlackConnectionState = { + connected: false, + channels: [ + { id: 'C123456', name: 'general', isConnected: true }, + { id: 'C456789', name: 'engineering', isConnected: true }, + { id: 'C987654', name: 'product-launch', isConnected: false } + ] + }; + private githubState: GitHubConnectionState = { connected: false, repos: [ @@ -17,6 +27,62 @@ export class IntegrationService { private latestSync: SyncStatus | null = null; + getSlackAuthUrl(): Observable<{ authUrl: string }> { + return of({ + authUrl: 'https://slack.com/oauth/v2/authorize?client_id=sentinent-demo&scope=channels:read%20chat:write&state=mock-state' + }); + } + + getSlackChannels(): Observable<{ connected: boolean; channels: SlackChannel[]; workspaceName?: string; workspaceUrl?: string; lastSyncAt?: Date }> { + return of({ + connected: this.slackState.connected, + channels: this.slackState.channels, + workspaceName: this.slackState.workspaceName, + workspaceUrl: this.slackState.workspaceUrl, + lastSyncAt: this.slackState.lastSyncAt + }); + } + + connectSlack(): Observable<{ connected: boolean }> { + this.slackState = { + ...this.slackState, + connected: true, + workspaceName: 'Sentinent Ops', + workspaceUrl: 'sentinent.slack.com' + }; + + return of({ connected: true }); + } + + updateSlackChannels(channelIds: string[]): Observable { + this.slackState = { + ...this.slackState, + channels: this.slackState.channels.map(channel => ({ + ...channel, + isConnected: channelIds.includes(channel.id) + })), + lastSyncAt: new Date() + }; + + return of(void 0); + } + + disconnectSlack(): Observable { + this.slackState = { + ...this.slackState, + connected: false, + workspaceName: undefined, + workspaceUrl: undefined, + lastSyncAt: undefined, + channels: this.slackState.channels.map(channel => ({ + ...channel, + isConnected: false + })) + }; + + return of(void 0); + } + getGitHubAuthUrl(): Observable<{ authUrl: string }> { return of({ authUrl: 'https://github.com/login/oauth/authorize?client_id=sentinent-demo&scope=read:user%20read:org%20repo&state=mock-state' diff --git a/src/app/services/signal.service.ts b/src/app/services/signal.service.ts index c4b4e04..ca31527 100644 --- a/src/app/services/signal.service.ts +++ b/src/app/services/signal.service.ts @@ -7,6 +7,42 @@ import { Signal, SignalFilters } from '../models/signal.model'; }) export class SignalService { private signals: Signal[] = [ + { + id: 'slack-101', + sourceType: 'slack', + sourceId: 'C123456', + externalId: '1735200123.0001', + title: 'Message from #general', + content: 'Heads up: the deployment window starts at 5 PM Eastern. Please confirm your checklist items.', + author: '@sam.ops', + status: 'unread', + receivedAt: new Date('2026-03-24T09:10:00Z'), + url: 'https://sentinent.slack.com/archives/C123456/p17352001230001', + metadata: { + channel: 'general', + channelId: 'C123456', + timestamp: '1735200123.0001', + user: 'U123456' + } + }, + { + id: 'slack-102', + sourceType: 'slack', + sourceId: 'C456789', + externalId: '1735201123.0002', + title: 'Message from #engineering', + content: 'Can someone review the API rate-limit handling before we push the Slack integration branch?', + author: '@jordan.dev', + status: 'read', + receivedAt: new Date('2026-03-24T10:05:00Z'), + url: 'https://sentinent.slack.com/archives/C456789/p17352011230002', + metadata: { + channel: 'engineering', + channelId: 'C456789', + timestamp: '1735201123.0002', + user: 'U456789' + } + }, { id: 'github-101', sourceType: 'github',