Skip to content

Commit 890776b

Browse files
JAORMXclaude
andcommitted
fix(dashboard): adapt UI to actual Minder API response formats
Update the dashboard to handle the actual API response structures: - minder_list_profiles returns array directly (not wrapped in object) - minder_list_repositories returns { results: [...], has_more } - minder_get_profile_status returns nested { profile_status: {...} } The MCP client methods now transform API responses into dashboard- friendly formats, keeping the API as the source of truth rather than modifying the server to match UI expectations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b73a6f0 commit 890776b

File tree

2 files changed

+87
-38
lines changed

2 files changed

+87
-38
lines changed

ui/compliance-dashboard/src/dashboard.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {
22
MCPAppsClient,
33
Profile,
4-
ProfilesResult,
54
ProfileStatusResult,
5+
ProfileStatusApiResponse,
66
Repository,
7-
RepositoriesResult,
7+
RepositoriesApiResponse,
88
type ToolResultParams,
99
type ToolInputParams,
1010
} from './mcp-client.js';
@@ -185,27 +185,36 @@ function handleToolResult(result: ToolResultParams): void {
185185
}
186186

187187
// Try to identify the data type and update accordingly
188-
if (isProfilesResult(data)) {
188+
// API returns profiles as array directly
189+
if (isProfilesArray(data)) {
189190
console.log(
190191
'[Dashboard] Received profiles data:',
191-
data.profiles.length,
192+
data.length,
192193
'profiles'
193194
);
194-
profiles = data.profiles || [];
195+
profiles = data;
195196
updateSummaryCards();
196197
renderProfiles();
197-
} else if (isProfileStatusResult(data)) {
198-
console.log('[Dashboard] Received profile status:', data.profile_name);
199-
profileStatuses.set(data.profile_id, data);
198+
// API returns profile status with nested profile_status object
199+
} else if (isProfileStatusApiResponse(data)) {
200+
const status: ProfileStatusResult = {
201+
profile_id: data.profile_status.profile_id,
202+
profile_name: data.profile_status.profile_name,
203+
profile_status: data.profile_status.profile_status,
204+
rule_evaluation_status: data.rule_evaluation_status,
205+
};
206+
console.log('[Dashboard] Received profile status:', status.profile_name);
207+
profileStatuses.set(status.profile_id, status);
200208
updateSummaryCards();
201209
renderProfiles();
202-
} else if (isRepositoriesResult(data)) {
210+
// API returns repositories as { results: [...] }
211+
} else if (isRepositoriesApiResponse(data)) {
203212
console.log(
204213
'[Dashboard] Received repositories data:',
205-
data.repositories.length,
214+
data.results.length,
206215
'repos'
207216
);
208-
repositories = data.repositories || [];
217+
repositories = data.results || [];
209218
updateSummaryCards();
210219
renderRepositories();
211220
} else {
@@ -214,38 +223,35 @@ function handleToolResult(result: ToolResultParams): void {
214223
}
215224

216225
/**
217-
* Type guard for ProfilesResult
226+
* Type guard for ProfilesResult - API returns array directly
218227
*/
219-
function isProfilesResult(data: unknown): data is ProfilesResult {
220-
return (
221-
typeof data === 'object' &&
222-
data !== null &&
223-
'profiles' in data &&
224-
Array.isArray((data as ProfilesResult).profiles)
228+
function isProfilesArray(data: unknown): data is Profile[] {
229+
return Array.isArray(data) && data.every(item =>
230+
typeof item === 'object' && item !== null && 'name' in item
225231
);
226232
}
227233

228234
/**
229-
* Type guard for ProfileStatusResult
235+
* Type guard for ProfileStatusApiResponse - API returns nested structure
230236
*/
231-
function isProfileStatusResult(data: unknown): data is ProfileStatusResult {
237+
function isProfileStatusApiResponse(data: unknown): data is ProfileStatusApiResponse {
232238
return (
233239
typeof data === 'object' &&
234240
data !== null &&
235-
'profile_id' in data &&
236-
'profile_name' in data
241+
'profile_status' in data &&
242+
typeof (data as ProfileStatusApiResponse).profile_status === 'object'
237243
);
238244
}
239245

240246
/**
241-
* Type guard for RepositoriesResult
247+
* Type guard for RepositoriesApiResponse - API returns { results: [...] }
242248
*/
243-
function isRepositoriesResult(data: unknown): data is RepositoriesResult {
249+
function isRepositoriesApiResponse(data: unknown): data is RepositoriesApiResponse {
244250
return (
245251
typeof data === 'object' &&
246252
data !== null &&
247-
'repositories' in data &&
248-
Array.isArray((data as RepositoriesResult).repositories)
253+
'results' in data &&
254+
Array.isArray((data as RepositoriesApiResponse).results)
249255
);
250256
}
251257

@@ -323,9 +329,9 @@ async function loadDashboard(): Promise<void> {
323329
profileStatuses = new Map();
324330
const statusPromises = profiles.map(async (profile) => {
325331
try {
326-
// Use project_id from profile context if available, or fall back to current context
332+
// Use project from profile context if available, or fall back to current context
327333
const profileProjectId =
328-
profile.context?.project_id ?? currentProjectId ?? undefined;
334+
profile.context?.project ?? currentProjectId ?? undefined;
329335
const status = await client.getProfileStatus({
330336
name: profile.name,
331337
projectId: profileProjectId,
@@ -591,7 +597,7 @@ function renderProfileRules(status: ProfileStatusResult | undefined): string {
591597
const safeStatus = escapeAttr(rule.status || 'pending');
592598
return `
593599
<div class="rule-item">
594-
<span class="rule-name">${escapeHtml(rule.rule_name || rule.rule_type || 'Unknown rule')}</span>
600+
<span class="rule-name">${escapeHtml(rule.rule_name || rule.rule_type_name || 'Unknown rule')}</span>
595601
<span class="status-badge ${safeStatus}">
596602
<span class="status-dot ${safeStatus}"></span>
597603
${escapeHtml(rule.status || 'pending')}

ui/compliance-dashboard/src/mcp-client.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,9 @@ export class MCPAppsClient {
196196
if (projectId) {
197197
args.project_id = projectId;
198198
}
199-
return this.callTool<ProfilesResult>('minder_list_profiles', args);
199+
// API returns array directly, wrap it for dashboard
200+
const profiles = await this.callTool<Profile[]>('minder_list_profiles', args);
201+
return { profiles: Array.isArray(profiles) ? profiles : [] };
200202
}
201203

202204
async getProfileStatus(options: {
@@ -214,10 +216,17 @@ export class MCPAppsClient {
214216
if (options.projectId) {
215217
args.project_id = options.projectId;
216218
}
217-
return this.callTool<ProfileStatusResult>(
219+
// API returns nested structure, flatten it for dashboard
220+
const response = await this.callTool<ProfileStatusApiResponse>(
218221
'minder_get_profile_status',
219222
args
220223
);
224+
return {
225+
profile_id: response.profile_status?.profile_id ?? '',
226+
profile_name: response.profile_status?.profile_name ?? '',
227+
profile_status: response.profile_status?.profile_status ?? '',
228+
rule_evaluation_status: response.rule_evaluation_status,
229+
};
221230
}
222231

223232
async listRepositories(options?: {
@@ -239,33 +248,59 @@ export class MCPAppsClient {
239248
if (options?.limit) {
240249
args.limit = options.limit;
241250
}
242-
return this.callTool<RepositoriesResult>('minder_list_repositories', args);
251+
// API returns { results: [...], has_more, next_cursor }
252+
const response = await this.callTool<RepositoriesApiResponse>(
253+
'minder_list_repositories',
254+
args
255+
);
256+
return {
257+
repositories: response.results ?? [],
258+
cursor: response.next_cursor,
259+
};
243260
}
244261
}
245262

246-
// Type definitions for Minder data
263+
// Type definitions for Minder data - matching actual API responses
264+
247265
export interface Profile {
248266
id: string;
249267
name: string;
250268
labels?: string[];
251269
context?: {
252-
project_id?: string;
270+
project?: string;
253271
};
254272
}
255273

274+
// API returns array directly, this wraps it for dashboard use
256275
export interface ProfilesResult {
257276
profiles: Profile[];
258277
}
259278

260279
export interface RuleEvaluationStatus {
261280
rule_name?: string;
262-
rule_type?: string;
281+
rule_type_name?: string;
263282
status: 'success' | 'failure' | 'error' | 'pending' | 'skipped';
264-
entity_name?: string;
283+
entity_info?: {
284+
name?: string;
285+
entity_type?: string;
286+
};
265287
remediation_status?: string;
266-
alert_status?: string;
288+
alert?: {
289+
status?: string;
290+
};
267291
}
268292

293+
// API returns nested structure with profile_status object
294+
export interface ProfileStatusApiResponse {
295+
profile_status: {
296+
profile_id: string;
297+
profile_name: string;
298+
profile_status: string;
299+
};
300+
rule_evaluation_status?: RuleEvaluationStatus[];
301+
}
302+
303+
// Flattened version for dashboard use
269304
export interface ProfileStatusResult {
270305
profile_id: string;
271306
profile_name: string;
@@ -274,7 +309,7 @@ export interface ProfileStatusResult {
274309
}
275310

276311
export interface Repository {
277-
id: string;
312+
id?: string;
278313
name: string;
279314
owner: string;
280315
provider?: string;
@@ -287,6 +322,14 @@ export interface Repository {
287322
default_branch?: string;
288323
}
289324

325+
// API returns { results: [...], has_more: bool }
326+
export interface RepositoriesApiResponse {
327+
results: Repository[];
328+
has_more: boolean;
329+
next_cursor?: string;
330+
}
331+
332+
// Dashboard-friendly version
290333
export interface RepositoriesResult {
291334
repositories: Repository[];
292335
cursor?: string;

0 commit comments

Comments
 (0)