feat(governance): Frontend integration and pages
- Integrate GovernanceRolesBlock into AgentCabinet - Add ReportButton component for creating incidents - Add GovernancePage with tabs (City/Audit/Incidents) - Add /governance route to App.tsx - Export governance components from index.ts
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAgentDashboard } from './hooks/useAgentDashboard';
|
||||
import { VisibilityCard } from './components/VisibilityCard';
|
||||
import { AgentChatWidget } from './components/AgentChatWidget';
|
||||
import { MicroDaoWizard } from './components/MicroDaoWizard';
|
||||
import { GovernanceLevelBadge } from '../governance/components/GovernanceLevelBadge';
|
||||
import { governanceApi } from '../../api/governance';
|
||||
import { GOV_LEVEL_LABELS, POWER_LABELS } from '../../types/governance';
|
||||
import type { AgentGovLevel, GovernancePower } from '../../types/governance';
|
||||
|
||||
export function AgentCabinet() {
|
||||
const { agentId } = useParams<{ agentId: string }>();
|
||||
@@ -213,6 +218,9 @@ export function AgentCabinet() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Governance & Roles Block */}
|
||||
<GovernanceRolesBlock agentId={profile.agent_id} />
|
||||
|
||||
{/* Node Info */}
|
||||
{node && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
|
||||
@@ -254,4 +262,106 @@ export function AgentCabinet() {
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Governance Roles Block
|
||||
// ============================================================================
|
||||
|
||||
function GovernanceRolesBlock({ agentId }: { agentId: string }) {
|
||||
const { data: roles, isLoading, error } = useQuery({
|
||||
queryKey: ['governance', 'agent', agentId, 'roles'],
|
||||
queryFn: () => governanceApi.getAgentRoles(agentId),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">🛡️ Ролі та Повноваження</h3>
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/2" />
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 rounded w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !roles) {
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">🛡️ Ролі та Повноваження</h3>
|
||||
<p className="text-sm text-gray-500">Governance API недоступний</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const levelLabel = GOV_LEVEL_LABELS[roles.level as AgentGovLevel] || roles.level;
|
||||
const powerLabels = roles.powers.map((p: GovernancePower) => POWER_LABELS[p] || p);
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">🛡️ Ролі та Повноваження</h3>
|
||||
|
||||
{/* Level Badge */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm text-gray-500">Рівень:</span>
|
||||
<GovernanceLevelBadge level={roles.level as AgentGovLevel} status={roles.status} />
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm text-gray-500">Статус:</span>
|
||||
<span className={`text-sm font-medium px-2 py-1 rounded ${
|
||||
roles.status === 'active' ? 'bg-green-100 text-green-800' :
|
||||
roles.status === 'suspended' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{roles.status === 'active' ? '✅ Активний' :
|
||||
roles.status === 'suspended' ? '⏸️ Призупинено' :
|
||||
'🚫 Заблоковано'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Powers */}
|
||||
{roles.powers.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<span className="text-sm text-gray-500 block mb-2">Повноваження:</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{powerLabels.map((power: string, i: number) => (
|
||||
<span key={i} className="text-xs px-2 py-1 bg-blue-50 text-blue-700 rounded">
|
||||
{power}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Assignments */}
|
||||
{roles.assignments.length > 0 && (
|
||||
<div className="border-t border-gray-100 pt-4 mt-4">
|
||||
<span className="text-sm text-gray-500 block mb-2">Призначення:</span>
|
||||
<div className="space-y-2">
|
||||
{roles.assignments.map((a: { microdaoId: string; role: string; scope: string }, i: number) => (
|
||||
<div key={i} className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">{a.microdaoId}</span>
|
||||
<span className="px-2 py-0.5 bg-purple-50 text-purple-700 rounded text-xs">
|
||||
{a.role}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions (placeholder for governance actions) */}
|
||||
<div className="border-t border-gray-100 pt-4 mt-4">
|
||||
<a
|
||||
href={`/governance/agent/${agentId}`}
|
||||
className="text-blue-600 hover:text-blue-700 text-sm font-medium flex items-center gap-1"
|
||||
>
|
||||
Governance Actions →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
192
src/features/governance/components/ReportButton.tsx
Normal file
192
src/features/governance/components/ReportButton.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Report Button
|
||||
* Universal button to create incidents from any context
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { incidentsApi } from '../../../api/incidents';
|
||||
import type { IncidentPriority, TargetScopeType } from '../../../types/governance';
|
||||
import { INCIDENT_PRIORITY_LABELS } from '../../../types/governance';
|
||||
|
||||
interface ReportButtonProps {
|
||||
targetScopeType: TargetScopeType;
|
||||
targetScopeId: string;
|
||||
actorDaisId?: string;
|
||||
variant?: 'icon' | 'text' | 'full';
|
||||
className?: string;
|
||||
defaultTitle?: string;
|
||||
}
|
||||
|
||||
export const ReportButton: React.FC<ReportButtonProps> = ({
|
||||
targetScopeType,
|
||||
targetScopeId,
|
||||
actorDaisId,
|
||||
variant = 'icon',
|
||||
className = '',
|
||||
defaultTitle = '',
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
title: defaultTitle,
|
||||
description: '',
|
||||
priority: 'medium' as IncidentPriority,
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
if (!actorDaisId) {
|
||||
throw new Error('Actor DAIS ID is required');
|
||||
}
|
||||
return incidentsApi.createIncident({
|
||||
createdByDaisId: actorDaisId,
|
||||
targetScopeType,
|
||||
targetScopeId,
|
||||
priority: form.priority,
|
||||
title: form.title,
|
||||
description: form.description || undefined,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['incidents'] });
|
||||
setIsOpen(false);
|
||||
setForm({ title: defaultTitle, description: '', priority: 'medium' });
|
||||
},
|
||||
});
|
||||
|
||||
if (!actorDaisId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderButton = () => {
|
||||
switch (variant) {
|
||||
case 'icon':
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className={`p-2 text-gray-400 hover:text-red-500 rounded-full hover:bg-red-50 transition-colors ${className}`}
|
||||
title="Поскаржитись"
|
||||
>
|
||||
⚠️
|
||||
</button>
|
||||
);
|
||||
case 'text':
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className={`text-sm text-gray-500 hover:text-red-600 transition-colors ${className}`}
|
||||
>
|
||||
Поскаржитись
|
||||
</button>
|
||||
);
|
||||
case 'full':
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className={`px-4 py-2 bg-red-50 hover:bg-red-100 text-red-700 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${className}`}
|
||||
>
|
||||
⚠️ Повідомити про проблему
|
||||
</button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderButton()}
|
||||
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl max-w-md w-full">
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
<h3 className="font-semibold text-gray-900">Повідомити про проблему</h3>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="bg-gray-50 rounded-lg p-3 text-sm">
|
||||
<div className="text-gray-500">Об'єкт скарги:</div>
|
||||
<div className="font-medium text-gray-900">
|
||||
{targetScopeType}: {targetScopeId}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Заголовок *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.title}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, title: e.target.value }))}
|
||||
placeholder="Коротко опишіть проблему..."
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Опис
|
||||
</label>
|
||||
<textarea
|
||||
value={form.description}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="Детальний опис проблеми..."
|
||||
rows={3}
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Пріоритет
|
||||
</label>
|
||||
<select
|
||||
value={form.priority}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, priority: e.target.value as IncidentPriority }))}
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
>
|
||||
{Object.entries(INCIDENT_PRIORITY_LABELS).map(([key, label]) => (
|
||||
<option key={key} value={key}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{createMutation.error && (
|
||||
<div className="bg-red-50 text-red-700 text-sm rounded-lg p-3">
|
||||
{(createMutation.error as Error).message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 p-4 border-t border-gray-200 bg-gray-50 rounded-b-xl">
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="px-4 py-2 text-gray-700 hover:bg-gray-200 rounded-lg"
|
||||
>
|
||||
Скасувати
|
||||
</button>
|
||||
<button
|
||||
onClick={() => createMutation.mutate()}
|
||||
disabled={!form.title.trim() || createMutation.isPending}
|
||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-medium disabled:opacity-50"
|
||||
>
|
||||
{createMutation.isPending ? 'Надсилання...' : 'Надіслати'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportButton;
|
||||
|
||||
11
src/features/governance/index.ts
Normal file
11
src/features/governance/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Governance Feature Exports
|
||||
*/
|
||||
|
||||
// Components
|
||||
export { GovernanceLevelBadge } from './components/GovernanceLevelBadge';
|
||||
export { CityGovernancePanel } from './components/CityGovernancePanel';
|
||||
export { AuditDashboard } from './components/AuditDashboard';
|
||||
export { IncidentsList } from './components/IncidentsList';
|
||||
export { ReportButton } from './components/ReportButton';
|
||||
|
||||
Reference in New Issue
Block a user