portal: strict ESLint (typescript-eslint, a11y, import order)
Some checks failed
API CI / API Lint (push) Has been cancelled
API CI / API Type Check (push) Has been cancelled
API CI / API Test (push) Has been cancelled
API CI / API Build (push) Has been cancelled
CD Pipeline / Deploy to Staging (push) Has been cancelled
CI Pipeline / Lint and Type Check (push) Has been cancelled
CI Pipeline / Test Backend (push) Has been cancelled
CI Pipeline / Test Frontend (push) Has been cancelled
CI Pipeline / Security Scan (push) Has been cancelled
Deploy to Staging / Deploy to Staging (push) Has been cancelled
Portal CI / Portal Lint (push) Has been cancelled
Portal CI / Portal Type Check (push) Has been cancelled
Portal CI / Portal Test (push) Has been cancelled
Portal CI / Portal Build (push) Has been cancelled
Test Suite / frontend-tests (push) Has been cancelled
Test Suite / api-tests (push) Has been cancelled
Test Suite / blockchain-tests (push) Has been cancelled
Type Check / type-check (map[directory:. name:root]) (push) Has been cancelled
Type Check / type-check (map[directory:api name:api]) (push) Has been cancelled
Type Check / type-check (map[directory:portal name:portal]) (push) Has been cancelled
API CI / Build Docker Image (push) Has been cancelled
CD Pipeline / Deploy to Production (push) Has been cancelled
CI Pipeline / Build (push) Has been cancelled
Some checks failed
API CI / API Lint (push) Has been cancelled
API CI / API Type Check (push) Has been cancelled
API CI / API Test (push) Has been cancelled
API CI / API Build (push) Has been cancelled
CD Pipeline / Deploy to Staging (push) Has been cancelled
CI Pipeline / Lint and Type Check (push) Has been cancelled
CI Pipeline / Test Backend (push) Has been cancelled
CI Pipeline / Test Frontend (push) Has been cancelled
CI Pipeline / Security Scan (push) Has been cancelled
Deploy to Staging / Deploy to Staging (push) Has been cancelled
Portal CI / Portal Lint (push) Has been cancelled
Portal CI / Portal Type Check (push) Has been cancelled
Portal CI / Portal Test (push) Has been cancelled
Portal CI / Portal Build (push) Has been cancelled
Test Suite / frontend-tests (push) Has been cancelled
Test Suite / api-tests (push) Has been cancelled
Test Suite / blockchain-tests (push) Has been cancelled
Type Check / type-check (map[directory:. name:root]) (push) Has been cancelled
Type Check / type-check (map[directory:api name:api]) (push) Has been cancelled
Type Check / type-check (map[directory:portal name:portal]) (push) Has been cancelled
API CI / Build Docker Image (push) Has been cancelled
CD Pipeline / Deploy to Production (push) Has been cancelled
CI Pipeline / Build (push) Has been cancelled
- root .eslintrc with recommended TS rules; eslint --fix import order project-wide - Replace any/unknown in lib clients (ArgoCD, K8s, Phoenix), hooks, and key components - Form labels: htmlFor+id; escape apostrophes; remove or gate console (error boundary keep) - Crossplane VM status typing; webhook test result interface; infrastructure/resources maps typed Made-with: Cursor
This commit is contained in:
@@ -1,11 +1,35 @@
|
|||||||
{
|
{
|
||||||
"extends": "next/core-web-vitals",
|
"root": true,
|
||||||
|
"extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2022,
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": { "jsx": true }
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
"no-console": "warn",
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
|
||||||
|
],
|
||||||
"@typescript-eslint/no-empty-object-type": "off",
|
"@typescript-eslint/no-empty-object-type": "off",
|
||||||
"jsx-a11y/label-has-associated-control": "warn",
|
"@typescript-eslint/no-require-imports": "off",
|
||||||
"react/no-unescaped-entities": "warn",
|
"no-console": "error",
|
||||||
"import/order": "warn"
|
"jsx-a11y/label-has-associated-control": "error",
|
||||||
|
"react/no-unescaped-entities": "error",
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
|
||||||
|
"newlines-between": "always",
|
||||||
|
"alphabetize": { "order": "asc", "caseInsensitive": true },
|
||||||
|
"pathGroups": [
|
||||||
|
{ "pattern": "@/**", "group": "internal", "position": "after" }
|
||||||
|
],
|
||||||
|
"pathGroupsExcludedImportTypes": ["builtin"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { signIn } from 'next-auth/react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Building2, Users, CreditCard, Shield, ArrowRight } from 'lucide-react';
|
import { Building2, Users, CreditCard, Shield, ArrowRight } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { signIn } from 'next-auth/react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export default function AdminPortalPage() {
|
export default function AdminPortalPage() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
|
|
||||||
import { AdvancedAnalytics } from '@/components/analytics/AdvancedAnalytics';
|
import { AdvancedAnalytics } from '@/components/analytics/AdvancedAnalytics';
|
||||||
|
|
||||||
export default function AnalyticsPage() {
|
export default function AnalyticsPage() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import NextAuth from 'next-auth';
|
import NextAuth from 'next-auth';
|
||||||
|
|
||||||
import { authOptions } from '@/lib/auth';
|
import { authOptions } from '@/lib/auth';
|
||||||
|
|
||||||
const handler = NextAuth(authOptions);
|
const handler = NextAuth(authOptions);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
function AuthErrorContent() {
|
function AuthErrorContent() {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useSession } from 'next-auth/react'
|
|
||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
|
import { useSession } from 'next-auth/react'
|
||||||
|
|
||||||
import ArgoCDApplications from '@/components/argocd/ArgoCDApplications'
|
import ArgoCDApplications from '@/components/argocd/ArgoCDApplications'
|
||||||
|
|
||||||
export default function ArgoCDPage() {
|
export default function ArgoCDPage() {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useSession } from 'next-auth/react'
|
|
||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
|
import { useSession } from 'next-auth/react'
|
||||||
|
|
||||||
import CrossplaneResourceBrowser from '@/components/crossplane/CrossplaneResourceBrowser'
|
import CrossplaneResourceBrowser from '@/components/crossplane/CrossplaneResourceBrowser'
|
||||||
|
|
||||||
export default function CrossplanePage() {
|
export default function CrossplanePage() {
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { CostOverviewTile } from '@/components/dashboard/CostOverviewTile';
|
|
||||||
import { CostForecastingTile } from '@/components/dashboard/CostForecastingTile';
|
|
||||||
import { ResourceUsageTile } from '@/components/dashboard/ResourceUsageTile';
|
|
||||||
import { BillingTile } from '@/components/dashboard/BillingTile';
|
import { BillingTile } from '@/components/dashboard/BillingTile';
|
||||||
import { ServiceAdoptionTile } from '@/components/dashboard/ServiceAdoptionTile';
|
|
||||||
import { ComplianceStatusTile } from '@/components/dashboard/ComplianceStatusTile';
|
import { ComplianceStatusTile } from '@/components/dashboard/ComplianceStatusTile';
|
||||||
|
import { CostForecastingTile } from '@/components/dashboard/CostForecastingTile';
|
||||||
|
import { CostOverviewTile } from '@/components/dashboard/CostOverviewTile';
|
||||||
import { QuickActionsPanel } from '@/components/dashboard/QuickActionsPanel';
|
import { QuickActionsPanel } from '@/components/dashboard/QuickActionsPanel';
|
||||||
|
import { ResourceUsageTile } from '@/components/dashboard/ResourceUsageTile';
|
||||||
|
import { ServiceAdoptionTile } from '@/components/dashboard/ServiceAdoptionTile';
|
||||||
|
|
||||||
export default function BusinessDashboardPage() {
|
export default function BusinessDashboardPage() {
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
|
|
||||||
|
import { APIKeysTile } from '@/components/dashboard/APIKeysTile';
|
||||||
import { APIUsageTile } from '@/components/dashboard/APIUsageTile';
|
import { APIUsageTile } from '@/components/dashboard/APIUsageTile';
|
||||||
import { DeploymentsTile } from '@/components/dashboard/DeploymentsTile';
|
import { DeploymentsTile } from '@/components/dashboard/DeploymentsTile';
|
||||||
import { TestEnvironmentsTile } from '@/components/dashboard/TestEnvironmentsTile';
|
|
||||||
import { APIKeysTile } from '@/components/dashboard/APIKeysTile';
|
|
||||||
import { QuickActionsPanel } from '@/components/dashboard/QuickActionsPanel';
|
import { QuickActionsPanel } from '@/components/dashboard/QuickActionsPanel';
|
||||||
|
import { TestEnvironmentsTile } from '@/components/dashboard/TestEnvironmentsTile';
|
||||||
|
|
||||||
export default function DeveloperDashboardPage() {
|
export default function DeveloperDashboardPage() {
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { getDashboardRoute } from '@/lib/roles';
|
import { getDashboardRoute } from '@/lib/roles';
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { SystemHealthTile } from '@/components/dashboard/SystemHealthTile';
|
|
||||||
import { IntegrationStatusTile } from '@/components/dashboard/IntegrationStatusTile';
|
|
||||||
import { DataPipelineTile } from '@/components/dashboard/DataPipelineTile';
|
|
||||||
import { ResourceUtilizationTile } from '@/components/dashboard/ResourceUtilizationTile';
|
|
||||||
import { OptimizationEngine } from '@/components/ai/OptimizationEngine';
|
import { OptimizationEngine } from '@/components/ai/OptimizationEngine';
|
||||||
|
import { DataPipelineTile } from '@/components/dashboard/DataPipelineTile';
|
||||||
|
import { IntegrationStatusTile } from '@/components/dashboard/IntegrationStatusTile';
|
||||||
import { QuickActionsPanel } from '@/components/dashboard/QuickActionsPanel';
|
import { QuickActionsPanel } from '@/components/dashboard/QuickActionsPanel';
|
||||||
|
import { ResourceUtilizationTile } from '@/components/dashboard/ResourceUtilizationTile';
|
||||||
|
import { SystemHealthTile } from '@/components/dashboard/SystemHealthTile';
|
||||||
|
|
||||||
export default function TechnicalDashboardPage() {
|
export default function TechnicalDashboardPage() {
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { signIn } from 'next-auth/react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Key, Book, TestTube, BarChart3, Webhook, Download, ArrowRight } from 'lucide-react';
|
import { Key, Book, TestTube, BarChart3, Webhook, Download, ArrowRight } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { signIn } from 'next-auth/react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export default function DeveloperPortalPage() {
|
export default function DeveloperPortalPage() {
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Send, CheckCircle, XCircle } from 'lucide-react';
|
import { Send, CheckCircle, XCircle } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
|
interface WebhookTestResult {
|
||||||
|
success: boolean;
|
||||||
|
status?: number;
|
||||||
|
statusText?: string;
|
||||||
|
responseTime?: number;
|
||||||
|
response?: unknown;
|
||||||
|
error?: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function WebhookTestingPage() {
|
export default function WebhookTestingPage() {
|
||||||
const [url, setUrl] = useState('');
|
const [url, setUrl] = useState('');
|
||||||
const [method, setMethod] = useState('POST');
|
const [method, setMethod] = useState('POST');
|
||||||
const [headers, setHeaders] = useState('{"Content-Type": "application/json"}');
|
const [headers, setHeaders] = useState('{"Content-Type": "application/json"}');
|
||||||
const [payload, setPayload] = useState('{"event": "test", "data": {}}');
|
const [payload, setPayload] = useState('{"event": "test", "data": {}}');
|
||||||
const [testResult, setTestResult] = useState<any>(null);
|
const [testResult, setTestResult] = useState<WebhookTestResult | null>(null);
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
|
||||||
const testWebhook = async () => {
|
const testWebhook = async () => {
|
||||||
@@ -39,10 +50,10 @@ export default function WebhookTestingPage() {
|
|||||||
response: data,
|
response: data,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
setTestResult({
|
setTestResult({
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message,
|
error: error instanceof Error ? error.message : 'Request failed',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -64,8 +75,11 @@ export default function WebhookTestingPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-gray-300 mb-2">Webhook URL</label>
|
<label htmlFor="webhook-test-url" className="block text-sm text-gray-300 mb-2">
|
||||||
|
Webhook URL
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="webhook-test-url"
|
||||||
type="url"
|
type="url"
|
||||||
value={url}
|
value={url}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
@@ -75,8 +89,11 @@ export default function WebhookTestingPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-gray-300 mb-2">HTTP Method</label>
|
<label htmlFor="webhook-test-method" className="block text-sm text-gray-300 mb-2">
|
||||||
|
HTTP Method
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
|
id="webhook-test-method"
|
||||||
value={method}
|
value={method}
|
||||||
onChange={(e) => setMethod(e.target.value)}
|
onChange={(e) => setMethod(e.target.value)}
|
||||||
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
|
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
|
||||||
@@ -90,8 +107,11 @@ export default function WebhookTestingPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-gray-300 mb-2">Headers (JSON)</label>
|
<label htmlFor="webhook-test-headers" className="block text-sm text-gray-300 mb-2">
|
||||||
|
Headers (JSON)
|
||||||
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
id="webhook-test-headers"
|
||||||
value={headers}
|
value={headers}
|
||||||
onChange={(e) => setHeaders(e.target.value)}
|
onChange={(e) => setHeaders(e.target.value)}
|
||||||
className="w-full h-24 px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white font-mono text-sm"
|
className="w-full h-24 px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white font-mono text-sm"
|
||||||
@@ -100,8 +120,11 @@ export default function WebhookTestingPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-gray-300 mb-2">Payload (JSON)</label>
|
<label htmlFor="webhook-test-payload" className="block text-sm text-gray-300 mb-2">
|
||||||
|
Payload (JSON)
|
||||||
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
id="webhook-test-payload"
|
||||||
value={payload}
|
value={payload}
|
||||||
onChange={(e) => setPayload(e.target.value)}
|
onChange={(e) => setPayload(e.target.value)}
|
||||||
className="w-full h-32 px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white font-mono text-sm"
|
className="w-full h-32 px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white font-mono text-sm"
|
||||||
@@ -165,11 +188,11 @@ export default function WebhookTestingPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{testResult.response && (
|
{testResult.response !== undefined && testResult.response !== null && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400 mb-1">Response</p>
|
<p className="text-sm text-gray-400 mb-1">Response</p>
|
||||||
<pre className="p-3 bg-gray-900 rounded text-white font-mono text-xs overflow-auto max-h-64">
|
<pre className="p-3 bg-gray-900 rounded text-white font-mono text-xs overflow-auto max-h-64">
|
||||||
{JSON.stringify(testResult.response, null, 2)}
|
{JSON.stringify(testResult.response as object, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { Home, RefreshCw, AlertCircle } from 'lucide-react'
|
import { Home, RefreshCw, AlertCircle } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
@@ -12,8 +12,8 @@ export default function Error({
|
|||||||
reset: () => void
|
reset: () => void
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Log error to error reporting service
|
// eslint-disable-next-line no-console -- error boundary diagnostic until reporting hook exists
|
||||||
console.error('Application error:', error)
|
console.error('Application error:', error);
|
||||||
}, [error])
|
}, [error])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { usePhoenixInfraNodes, usePhoenixInfraStorage } from '@/hooks/usePhoenixRailing';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Server, HardDrive } from 'lucide-react';
|
import { Server, HardDrive } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
import { usePhoenixInfraNodes, usePhoenixInfraStorage } from '@/hooks/usePhoenixRailing';
|
||||||
|
|
||||||
export default function InfrastructurePage() {
|
export default function InfrastructurePage() {
|
||||||
const { data: nodesData, isLoading: nodesLoading, error: nodesError } = usePhoenixInfraNodes();
|
const { data: nodesData, isLoading: nodesLoading, error: nodesError } = usePhoenixInfraNodes();
|
||||||
const { data: storageData, isLoading: storageLoading, error: storageError } = usePhoenixInfraStorage();
|
const { data: storageData, isLoading: storageLoading, error: storageError } = usePhoenixInfraStorage();
|
||||||
@@ -28,10 +29,15 @@ export default function InfrastructurePage() {
|
|||||||
{nodesError && <p className="text-red-400">Error loading nodes</p>}
|
{nodesError && <p className="text-red-400">Error loading nodes</p>}
|
||||||
{nodesData?.nodes && (
|
{nodesData?.nodes && (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{nodesData.nodes.map((n: any) => (
|
{nodesData.nodes.map((n: Record<string, unknown>) => (
|
||||||
<li key={n.node || n.name} className="flex justify-between text-sm">
|
<li
|
||||||
<span>{n.node ?? n.name ?? n.id}</span>
|
key={String(n.node ?? n.name ?? n.id ?? '')}
|
||||||
<span className={n.status === 'online' ? 'text-green-400' : 'text-gray-400'}>{n.status ?? '—'}</span>
|
className="flex justify-between text-sm"
|
||||||
|
>
|
||||||
|
<span>{String(n.node ?? n.name ?? n.id ?? '—')}</span>
|
||||||
|
<span className={n.status === 'online' ? 'text-green-400' : 'text-gray-400'}>
|
||||||
|
{String(n.status ?? '—')}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -52,8 +58,10 @@ export default function InfrastructurePage() {
|
|||||||
{storageError && <p className="text-red-400">Error loading storage</p>}
|
{storageError && <p className="text-red-400">Error loading storage</p>}
|
||||||
{storageData?.storage && (
|
{storageData?.storage && (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{storageData.storage.slice(0, 10).map((s: any, i: number) => (
|
{storageData.storage.slice(0, 10).map((s: Record<string, unknown>, i: number) => (
|
||||||
<li key={s.storage || i} className="text-sm">{s.storage ?? s.name ?? s.id}</li>
|
<li key={String(s.storage ?? s.name ?? s.id ?? i)} className="text-sm">
|
||||||
|
{String(s.storage ?? s.name ?? s.id ?? '—')}
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useSession } from 'next-auth/react'
|
|
||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
|
import { useSession } from 'next-auth/react'
|
||||||
|
|
||||||
import KubernetesClusters from '@/components/kubernetes/KubernetesClusters'
|
import KubernetesClusters from '@/components/kubernetes/KubernetesClusters'
|
||||||
|
|
||||||
export default function KubernetesPage() {
|
export default function KubernetesPage() {
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
import { Providers } from './providers'
|
import { SessionProvider } from 'next-auth/react'
|
||||||
|
|
||||||
|
import { KeyboardShortcutsProvider } from '@/components/KeyboardShortcutsProvider'
|
||||||
|
import { MobileNavigation } from '@/components/layout/MobileNavigation'
|
||||||
|
import { PortalBreadcrumbs } from '@/components/layout/PortalBreadcrumbs'
|
||||||
import { PortalHeader } from '@/components/layout/PortalHeader'
|
import { PortalHeader } from '@/components/layout/PortalHeader'
|
||||||
import { PortalSidebar } from '@/components/layout/PortalSidebar'
|
import { PortalSidebar } from '@/components/layout/PortalSidebar'
|
||||||
import { PortalBreadcrumbs } from '@/components/layout/PortalBreadcrumbs'
|
|
||||||
import { MobileNavigation } from '@/components/layout/MobileNavigation'
|
import { Providers } from './providers'
|
||||||
import { KeyboardShortcutsProvider } from '@/components/KeyboardShortcutsProvider'
|
|
||||||
import { SessionProvider } from 'next-auth/react'
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useSession } from 'next-auth/react'
|
|
||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
|
import { useSession } from 'next-auth/react'
|
||||||
|
|
||||||
import GrafanaPanel from '@/components/monitoring/GrafanaPanel'
|
import GrafanaPanel from '@/components/monitoring/GrafanaPanel'
|
||||||
import LokiLogViewer from '@/components/monitoring/LokiLogViewer'
|
import LokiLogViewer from '@/components/monitoring/LokiLogViewer'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { Home, ArrowLeft } from 'lucide-react'
|
import { Home, ArrowLeft } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
@@ -10,7 +10,7 @@ export default function NotFound() {
|
|||||||
<h1 className="text-6xl font-bold text-white mb-4">404</h1>
|
<h1 className="text-6xl font-bold text-white mb-4">404</h1>
|
||||||
<h2 className="text-2xl font-semibold text-gray-300 mb-4">Page Not Found</h2>
|
<h2 className="text-2xl font-semibold text-gray-300 mb-4">Page Not Found</h2>
|
||||||
<p className="text-gray-400 mb-8">
|
<p className="text-gray-400 mb-8">
|
||||||
The page you're looking for doesn't exist or has been moved.
|
The page you're looking for doesn't exist or has been moved.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-4 justify-center">
|
<div className="flex gap-4 justify-center">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { OnboardingWizard } from '@/components/onboarding/OnboardingWizard';
|
import { OnboardingWizard } from '@/components/onboarding/OnboardingWizard';
|
||||||
import { WelcomeStep } from '@/components/onboarding/steps/WelcomeStep';
|
|
||||||
import { ProfileStep } from '@/components/onboarding/steps/ProfileStep';
|
|
||||||
import { PreferencesStep } from '@/components/onboarding/steps/PreferencesStep';
|
import { PreferencesStep } from '@/components/onboarding/steps/PreferencesStep';
|
||||||
|
import { ProfileStep } from '@/components/onboarding/steps/ProfileStep';
|
||||||
|
import { WelcomeStep } from '@/components/onboarding/steps/WelcomeStep';
|
||||||
|
|
||||||
const onboardingSteps = [
|
const onboardingSteps = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
|
|
||||||
import Dashboard from '@/components/Dashboard';
|
import Dashboard from '@/components/Dashboard';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { signIn } from 'next-auth/react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Handshake, TrendingUp, BookOpen, Package, ArrowRight } from 'lucide-react';
|
import { Handshake, TrendingUp, BookOpen, Package, ArrowRight } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { signIn } from 'next-auth/react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export default function PartnerPortalPage() {
|
export default function PartnerPortalPage() {
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { useTenantResources } from '@/hooks/usePhoenixRailing'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||||
|
import { useTenantResources } from '@/hooks/usePhoenixRailing'
|
||||||
|
|
||||||
export default function ResourcesPage() {
|
export default function ResourcesPage() {
|
||||||
const { status } = useSession()
|
const { status } = useSession()
|
||||||
@@ -36,10 +37,10 @@ export default function ResourcesPage() {
|
|||||||
<p className="text-sm text-muted-foreground">No resources</p>
|
<p className="text-sm text-muted-foreground">No resources</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{resources.map((r: any) => (
|
{resources.map((r: Record<string, unknown>) => (
|
||||||
<li key={r.id} className="flex justify-between text-sm">
|
<li key={String(r.id ?? r.name ?? '')} className="flex justify-between text-sm">
|
||||||
<span>{r.name}</span>
|
<span>{String(r.name ?? '—')}</span>
|
||||||
<span className="text-gray-400">{r.resource_type ?? r.provider}</span>
|
<span className="text-gray-400">{String(r.resource_type ?? r.provider ?? '—')}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Shield, CheckCircle } from 'lucide-react';
|
import { Shield, CheckCircle } from 'lucide-react';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export default function TwoFactorAuthPage() {
|
export default function TwoFactorAuthPage() {
|
||||||
const [isEnabled, setIsEnabled] = useState(false);
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
@@ -24,8 +25,8 @@ export default function TwoFactorAuthPage() {
|
|||||||
setSecret(data.secret);
|
setSecret(data.secret);
|
||||||
const qr = await QRCode.toDataURL(data.qrCodeUrl);
|
const qr = await QRCode.toDataURL(data.qrCodeUrl);
|
||||||
setQrCode(qr);
|
setQrCode(qr);
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error('Failed to enable 2FA:', error);
|
/* setup failed — surface UI toast when wired */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,8 +44,8 @@ export default function TwoFactorAuthPage() {
|
|||||||
setQrCode(null);
|
setQrCode(null);
|
||||||
setSecret(null);
|
setSecret(null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error('Verification failed:', error);
|
/* verification failed */
|
||||||
} finally {
|
} finally {
|
||||||
setIsVerifying(false);
|
setIsVerifying(false);
|
||||||
}
|
}
|
||||||
@@ -54,8 +55,8 @@ export default function TwoFactorAuthPage() {
|
|||||||
try {
|
try {
|
||||||
await fetch('/api/auth/2fa/disable', { method: 'POST' });
|
await fetch('/api/auth/2fa/disable', { method: 'POST' });
|
||||||
setIsEnabled(false);
|
setIsEnabled(false);
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error('Failed to disable 2FA:', error);
|
/* disable failed */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,6 +99,7 @@ export default function TwoFactorAuthPage() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-gray-300 mb-2">Scan this QR code with your authenticator app:</p>
|
<p className="text-gray-300 mb-2">Scan this QR code with your authenticator app:</p>
|
||||||
<div className="flex justify-center p-4 bg-white rounded">
|
<div className="flex justify-center p-4 bg-white rounded">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element -- data: URL from qrcode package */}
|
||||||
<img src={qrCode} alt="2FA QR Code" className="w-48 h-48" />
|
<img src={qrCode} alt="2FA QR Code" className="w-48 h-48" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,10 +112,11 @@ export default function TwoFactorAuthPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-gray-300 mb-2">
|
<label htmlFor="settings-2fa-verify-code" className="block text-sm text-gray-300 mb-2">
|
||||||
Enter verification code from your app:
|
Enter verification code from your app:
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="settings-2fa-verify-code"
|
||||||
type="text"
|
type="text"
|
||||||
value={verificationCode}
|
value={verificationCode}
|
||||||
onChange={(e) => setVerificationCode(e.target.value)}
|
onChange={(e) => setVerificationCode(e.target.value)}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { User, Bell, Shield, Key } from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
import { User, Bell, Shield, Key } from 'lucide-react';
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
@@ -51,11 +52,11 @@ export default function SettingsPage() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-300">Email</label>
|
<p className="text-sm font-medium text-gray-300">Email</p>
|
||||||
<p className="text-white">{session?.user?.email || 'Not available'}</p>
|
<p className="text-white">{session?.user?.email || 'Not available'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-300">Name</label>
|
<p className="text-sm font-medium text-gray-300">Name</p>
|
||||||
<p className="text-white">{session?.user?.name || 'Not available'}</p>
|
<p className="text-white">{session?.user?.name || 'Not available'}</p>
|
||||||
</div>
|
</div>
|
||||||
<button className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
<button className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||||
@@ -74,16 +75,16 @@ export default function SettingsPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2" htmlFor="settings-notify-email">
|
||||||
<input type="checkbox" className="rounded" />
|
<input id="settings-notify-email" type="checkbox" className="rounded" />
|
||||||
<span className="text-white">Email notifications</span>
|
<span className="text-white">Email notifications</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2" htmlFor="settings-notify-alert">
|
||||||
<input type="checkbox" className="rounded" />
|
<input id="settings-notify-alert" type="checkbox" className="rounded" />
|
||||||
<span className="text-white">Alert notifications</span>
|
<span className="text-white">Alert notifications</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2" htmlFor="settings-notify-weekly">
|
||||||
<input type="checkbox" className="rounded" />
|
<input id="settings-notify-weekly" type="checkbox" className="rounded" />
|
||||||
<span className="text-white">Weekly reports</span>
|
<span className="text-white">Weekly reports</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { Shield, Home, Lock } from 'lucide-react'
|
import { Shield, Home, Lock } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
export default function Unauthorized() {
|
export default function Unauthorized() {
|
||||||
return (
|
return (
|
||||||
@@ -10,7 +10,7 @@ export default function Unauthorized() {
|
|||||||
<Shield className="h-16 w-16 text-yellow-500 mx-auto mb-4" />
|
<Shield className="h-16 w-16 text-yellow-500 mx-auto mb-4" />
|
||||||
<h1 className="text-3xl font-bold text-white mb-4">Access Denied</h1>
|
<h1 className="text-3xl font-bold text-white mb-4">Access Denied</h1>
|
||||||
<p className="text-gray-400 mb-2">
|
<p className="text-gray-400 mb-2">
|
||||||
You don't have permission to access this resource.
|
You don't have permission to access this resource.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mb-8">
|
<p className="text-sm text-gray-500 mb-8">
|
||||||
Please contact your administrator if you believe this is an error.
|
Please contact your administrator if you believe this is an error.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
|
|
||||||
import VMList from '@/components/vms/VMList';
|
import VMList from '@/components/vms/VMList';
|
||||||
|
|
||||||
export default function VMsPage() {
|
export default function VMsPage() {
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { createCrossplaneClient, VM } from '@/lib/crossplane-client';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
|
|
||||||
import { Server, Activity, AlertCircle, CheckCircle, Loader2 } from 'lucide-react';
|
|
||||||
import { PhoenixHealthTile } from './dashboard/PhoenixHealthTile';
|
|
||||||
import { Badge } from './ui/badge';
|
|
||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import { useQuery as useApolloQuery } from '@apollo/client';
|
import { useQuery as useApolloQuery } from '@apollo/client';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Server, Activity, AlertCircle, CheckCircle, Loader2 } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
|
import { createCrossplaneClient, VM } from '@/lib/crossplane-client';
|
||||||
|
|
||||||
|
import { PhoenixHealthTile } from './dashboard/PhoenixHealthTile';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
|
||||||
|
|
||||||
|
|
||||||
interface ActivityItem {
|
interface ActivityItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -65,11 +68,11 @@ export default function Dashboard() {
|
|||||||
// Get recent activity from resources (last 10 created/updated)
|
// Get recent activity from resources (last 10 created/updated)
|
||||||
const recentActivity: ActivityItem[] = resources
|
const recentActivity: ActivityItem[] = resources
|
||||||
?.slice(0, 10)
|
?.slice(0, 10)
|
||||||
.map((resource: any) => ({
|
.map((resource: { id: string; type: string; name: string; status: string; updatedAt?: string; createdAt?: string }) => ({
|
||||||
id: resource.id,
|
id: resource.id,
|
||||||
type: resource.type,
|
type: resource.type,
|
||||||
description: `${resource.name} - ${resource.status}`,
|
description: `${resource.name} - ${resource.status}`,
|
||||||
timestamp: new Date(resource.updatedAt || resource.createdAt),
|
timestamp: new Date(resource.updatedAt || resource.createdAt || Date.now()),
|
||||||
}))
|
}))
|
||||||
.sort((a: ActivityItem, b: ActivityItem) => b.timestamp.getTime() - a.timestamp.getTime()) || [];
|
.sort((a: ActivityItem, b: ActivityItem) => b.timestamp.getTime() - a.timestamp.getTime()) || [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { useGlobalKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
import { useGlobalKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
||||||
|
|
||||||
import { KeyboardShortcutsHelp } from './KeyboardShortcutsHelp';
|
import { KeyboardShortcutsHelp } from './KeyboardShortcutsHelp';
|
||||||
|
|
||||||
export function KeyboardShortcutsProvider({ children }: { children: ReactNode }) {
|
export function KeyboardShortcutsProvider({ children }: { children: ReactNode }) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, type ChangeEvent } from 'react'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useState, type ChangeEvent } from 'react'
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
|
|
||||||
interface Resource {
|
interface Resource {
|
||||||
id: string
|
id: string
|
||||||
@@ -41,7 +42,7 @@ export function ResourceExplorer() {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const variables: any = {}
|
const variables: Record<string, string> = {}
|
||||||
if (filterProvider !== 'all') {
|
if (filterProvider !== 'all') {
|
||||||
variables.provider = filterProvider
|
variables.provider = filterProvider
|
||||||
}
|
}
|
||||||
@@ -71,7 +72,16 @@ export function ResourceExplorer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transform GraphQL response to component format
|
// Transform GraphQL response to component format
|
||||||
return (result.data?.resourceInventory || []).map((item: any) => ({
|
return (result.data?.resourceInventory || []).map(
|
||||||
|
(item: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
resourceType: string
|
||||||
|
provider: string
|
||||||
|
region?: string
|
||||||
|
tags?: string[]
|
||||||
|
metadata?: { status?: string }
|
||||||
|
}) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
type: item.resourceType,
|
type: item.resourceType,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
|
||||||
import { Button } from '@/components/ui/Button'
|
|
||||||
import { Server, Play, Pause, Trash2 } from 'lucide-react'
|
import { Server, Play, Pause, Trash2 } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||||
|
|
||||||
interface VM {
|
interface VM {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Sparkles, TrendingDown, TrendingUp } from 'lucide-react';
|
import { Sparkles, TrendingDown, TrendingUp } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function OptimizationEngine() {
|
export function OptimizationEngine() {
|
||||||
const [recommendations] = useState([
|
const [recommendations] = useState([
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { BarChart3, TrendingUp, Users, DollarSign } from 'lucide-react';
|
import { BarChart3, TrendingUp, Users, DollarSign } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function AdvancedAnalytics() {
|
export function AdvancedAnalytics() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useSession } from 'next-auth/react'
|
|
||||||
import { createArgoCDClient, type ArgoCDApplication } from '@/lib/argocd-client'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
|
|
||||||
import { Button } from '../ui/Button'
|
|
||||||
import { CheckCircle, XCircle, AlertCircle, RefreshCw, GitBranch } from 'lucide-react'
|
import { CheckCircle, XCircle, AlertCircle, RefreshCw, GitBranch } from 'lucide-react'
|
||||||
|
import { useSession } from 'next-auth/react'
|
||||||
|
|
||||||
|
import { createArgoCDClient, type ArgoCDApplication } from '@/lib/argocd-client'
|
||||||
|
|
||||||
|
import { Button } from '../ui/Button'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
|
||||||
|
|
||||||
export default function ArgoCDApplications() {
|
export default function ArgoCDApplications() {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { Server, Database, Network, HardDrive } from 'lucide-react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { createCrossplaneClient } from '@/lib/crossplane-client'
|
import { createCrossplaneClient } from '@/lib/crossplane-client'
|
||||||
|
|
||||||
|
import { Badge } from '../ui/badge'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
|
||||||
import { Input } from '../ui/Input'
|
import { Input } from '../ui/Input'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||||
import { Badge } from '../ui/badge'
|
|
||||||
import { Server, Database, Network, HardDrive } from 'lucide-react'
|
|
||||||
|
|
||||||
interface CrossplaneResource {
|
interface CrossplaneResource {
|
||||||
apiVersion: string
|
apiVersion: string
|
||||||
@@ -20,8 +23,8 @@ interface CrossplaneResource {
|
|||||||
creationTimestamp: string
|
creationTimestamp: string
|
||||||
labels?: Record<string, string>
|
labels?: Record<string, string>
|
||||||
}
|
}
|
||||||
spec?: any
|
spec?: unknown
|
||||||
status?: any
|
status?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CrossplaneResourceBrowser() {
|
export default function CrossplaneResourceBrowser() {
|
||||||
@@ -54,8 +57,7 @@ export default function CrossplaneResourceBrowser() {
|
|||||||
spec: vm.spec,
|
spec: vm.spec,
|
||||||
status: vm.status,
|
status: vm.status,
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error('Failed to fetch Crossplane resources:', error)
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -78,10 +80,11 @@ export default function CrossplaneResourceBrowser() {
|
|||||||
return <Server className="h-5 w-5 text-gray-500" />
|
return <Server className="h-5 w-5 text-gray-500" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const getResourceStatusColor = (status: Record<string, unknown> | undefined) => {
|
const getResourceStatusColor = (status: unknown) => {
|
||||||
if (!status) return 'bg-gray-500'
|
if (!status || typeof status !== 'object') return 'bg-gray-500'
|
||||||
|
|
||||||
const state = String(status.state ?? status.phase ?? 'Unknown')
|
const o = status as Record<string, unknown>
|
||||||
|
const state = String(o.state ?? o.phase ?? 'Unknown')
|
||||||
switch (state.toLowerCase()) {
|
switch (state.toLowerCase()) {
|
||||||
case 'running':
|
case 'running':
|
||||||
case 'ready':
|
case 'ready':
|
||||||
@@ -172,7 +175,9 @@ export default function CrossplaneResourceBrowser() {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`h-3 w-3 rounded-full ${getResourceStatusColor(resource.status)}`}
|
className={`h-3 w-3 rounded-full ${getResourceStatusColor(resource.status)}`}
|
||||||
title={resource.status?.state || 'Unknown'}
|
title={String(
|
||||||
|
(resource.status as Record<string, unknown> | undefined)?.state ?? 'Unknown'
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -190,16 +195,20 @@ export default function CrossplaneResourceBrowser() {
|
|||||||
<span className="text-sm text-gray-500">API Version:</span>
|
<span className="text-sm text-gray-500">API Version:</span>
|
||||||
<span className="text-sm text-gray-400">{resource.apiVersion}</span>
|
<span className="text-sm text-gray-400">{resource.apiVersion}</span>
|
||||||
</div>
|
</div>
|
||||||
{resource.status?.state && (
|
{Boolean((resource.status as Record<string, unknown> | undefined)?.state) && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-gray-500">State:</span>
|
<span className="text-sm text-gray-500">State:</span>
|
||||||
<Badge variant="outline">{resource.status.state}</Badge>
|
<Badge variant="outline">
|
||||||
|
{String((resource.status as Record<string, unknown>).state)}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{resource.status?.ipAddress && (
|
{Boolean((resource.status as Record<string, unknown> | undefined)?.ipAddress) && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-gray-500">IP Address:</span>
|
<span className="text-sm text-gray-500">IP Address:</span>
|
||||||
<span className="text-sm font-mono">{resource.status.ipAddress}</span>
|
<span className="text-sm font-mono">
|
||||||
|
{String((resource.status as Record<string, unknown>).ipAddress)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{resource.metadata.labels && Object.keys(resource.metadata.labels).length > 0 && (
|
{resource.metadata.labels && Object.keys(resource.metadata.labels).length > 0 && (
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Key, AlertCircle } from 'lucide-react';
|
import { Key, AlertCircle } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function APIKeysTile() {
|
export function APIKeysTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const apiKeys = {
|
const apiKeys = {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Activity, AlertCircle } from 'lucide-react';
|
import { Activity, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function APIUsageTile() {
|
export function APIUsageTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const usage = {
|
const usage = {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { CreditCard, FileText } from 'lucide-react';
|
import { CreditCard, FileText } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function BillingTile() {
|
export function BillingTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const billing = {
|
const billing = {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Shield, CheckCircle, AlertCircle } from 'lucide-react';
|
import { Shield, CheckCircle, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function ComplianceStatusTile() {
|
export function ComplianceStatusTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const compliance = {
|
const compliance = {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { TrendingUp, TrendingDown, DollarSign } from 'lucide-react';
|
import { TrendingUp, TrendingDown, DollarSign } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function CostForecastingTile() {
|
export function CostForecastingTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const forecast = {
|
const forecast = {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { DollarSign, TrendingUp, TrendingDown } from 'lucide-react';
|
import { DollarSign, TrendingUp, TrendingDown } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function CostOverviewTile() {
|
export function CostOverviewTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const costData = {
|
const costData = {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { GripVertical, Plus } from 'lucide-react';
|
import { GripVertical, Plus } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
interface DashboardTile {
|
interface DashboardTile {
|
||||||
id: string;
|
id: string;
|
||||||
component: string;
|
component: string;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { GitBranch, PlayCircle, PauseCircle, AlertCircle } from 'lucide-react';
|
import { GitBranch, PlayCircle, PauseCircle, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function DataPipelineTile() {
|
export function DataPipelineTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const pipelines = {
|
const pipelines = {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Rocket, CheckCircle, Clock, XCircle } from 'lucide-react';
|
import { Rocket, CheckCircle, Clock, XCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function DeploymentsTile() {
|
export function DeploymentsTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const deployments = {
|
const deployments = {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Link2, CheckCircle, AlertCircle, Clock } from 'lucide-react';
|
import { Link2, CheckCircle, AlertCircle, Clock } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function IntegrationStatusTile() {
|
export function IntegrationStatusTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const integrations = {
|
const integrations = {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Suspense, lazy, ComponentType } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { Suspense, lazy, ComponentType } from 'react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
interface LazyTileProps {
|
interface LazyTileProps {
|
||||||
component: ComponentType<any>;
|
component: ComponentType<Record<string, unknown>>;
|
||||||
props?: any;
|
props?: Record<string, unknown>;
|
||||||
fallback?: React.ReactNode;
|
fallback?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { usePhoenixHealthSummary } from '@/hooks/usePhoenixRailing';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Activity, CheckCircle, AlertCircle } from 'lucide-react';
|
import { Activity, CheckCircle, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
import { usePhoenixHealthSummary } from '@/hooks/usePhoenixRailing';
|
||||||
|
|
||||||
export function PhoenixHealthTile() {
|
export function PhoenixHealthTile() {
|
||||||
const { data, isLoading, error } = usePhoenixHealthSummary();
|
const { data, isLoading, error } = usePhoenixHealthSummary();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
|
||||||
import { Plus, Link as LinkIcon, FileText, Settings, Key, Rocket, Book, CreditCard, HelpCircle, Download } from 'lucide-react';
|
import { Plus, Link as LinkIcon, FileText, Settings, Key, Rocket, Book, CreditCard, HelpCircle, Download } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
interface QuickAction {
|
interface QuickAction {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -14,7 +16,7 @@ interface QuickActionsPanelProps {
|
|||||||
actions: QuickAction[];
|
actions: QuickAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMap: Record<string, any> = {
|
const iconMap: Record<string, LucideIcon> = {
|
||||||
Plus,
|
Plus,
|
||||||
Link: LinkIcon,
|
Link: LinkIcon,
|
||||||
FileText,
|
FileText,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { BarChart3 } from 'lucide-react';
|
import { BarChart3 } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function ResourceUsageTile() {
|
export function ResourceUsageTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const usageByDivision = [
|
const usageByDivision = [
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Cpu, HardDrive, Activity } from 'lucide-react';
|
import { Cpu, HardDrive, Activity } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function ResourceUtilizationTile() {
|
export function ResourceUtilizationTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const utilization = {
|
const utilization = {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { TrendingUp } from 'lucide-react';
|
import { TrendingUp } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function ServiceAdoptionTile() {
|
export function ServiceAdoptionTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const services = [
|
const services = [
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { Activity, CheckCircle, AlertCircle, XCircle, Globe, Server } from 'lucide-react';
|
import { Activity, CheckCircle, AlertCircle, XCircle, Globe, Server } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
import { useSystemHealth } from '@/hooks/useDashboardData';
|
import { useSystemHealth } from '@/hooks/useDashboardData';
|
||||||
|
|
||||||
|
type HealthSite = { status?: string };
|
||||||
|
type HealthResource = { status?: string };
|
||||||
|
|
||||||
export function SystemHealthTile() {
|
export function SystemHealthTile() {
|
||||||
const { data, loading, error } = useSystemHealth();
|
const { data, loading, error } = useSystemHealth();
|
||||||
|
|
||||||
@@ -11,21 +15,21 @@ export function SystemHealthTile() {
|
|||||||
const healthData = data ? {
|
const healthData = data ? {
|
||||||
regions: {
|
regions: {
|
||||||
total: data.sites?.length || 0,
|
total: data.sites?.length || 0,
|
||||||
healthy: data.sites?.filter((s: any) => s.status === 'ACTIVE').length || 0,
|
healthy: data.sites?.filter((s: HealthSite) => s.status === 'ACTIVE').length || 0,
|
||||||
warning: data.sites?.filter((s: any) => s.status === 'MAINTENANCE').length || 0,
|
warning: data.sites?.filter((s: HealthSite) => s.status === 'MAINTENANCE').length || 0,
|
||||||
critical: data.sites?.filter((s: any) => s.status === 'INACTIVE').length || 0,
|
critical: data.sites?.filter((s: HealthSite) => s.status === 'INACTIVE').length || 0,
|
||||||
},
|
},
|
||||||
clusters: {
|
clusters: {
|
||||||
total: data.resources?.length || 0,
|
total: data.resources?.length || 0,
|
||||||
healthy: data.resources?.filter((r: any) => r.status === 'RUNNING').length || 0,
|
healthy: data.resources?.filter((r: HealthResource) => r.status === 'RUNNING').length || 0,
|
||||||
warning: data.resources?.filter((r: any) => r.status === 'PROVISIONING').length || 0,
|
warning: data.resources?.filter((r: HealthResource) => r.status === 'PROVISIONING').length || 0,
|
||||||
critical: data.resources?.filter((r: any) => r.status === 'ERROR').length || 0,
|
critical: data.resources?.filter((r: HealthResource) => r.status === 'ERROR').length || 0,
|
||||||
},
|
},
|
||||||
nodes: {
|
nodes: {
|
||||||
total: data.resources?.length || 0,
|
total: data.resources?.length || 0,
|
||||||
healthy: data.resources?.filter((r: any) => r.status === 'RUNNING').length || 0,
|
healthy: data.resources?.filter((r: HealthResource) => r.status === 'RUNNING').length || 0,
|
||||||
warning: data.resources?.filter((r: any) => r.status === 'PROVISIONING').length || 0,
|
warning: data.resources?.filter((r: HealthResource) => r.status === 'PROVISIONING').length || 0,
|
||||||
critical: data.resources?.filter((r: any) => r.status === 'ERROR').length || 0,
|
critical: data.resources?.filter((r: HealthResource) => r.status === 'ERROR').length || 0,
|
||||||
},
|
},
|
||||||
} : {
|
} : {
|
||||||
regions: { total: 0, healthy: 0, warning: 0, critical: 0 },
|
regions: { total: 0, healthy: 0, warning: 0, critical: 0 },
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { TestTube, PlayCircle, PauseCircle } from 'lucide-react';
|
import { TestTube, PlayCircle, PauseCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
export function TestEnvironmentsTile() {
|
export function TestEnvironmentsTile() {
|
||||||
// Mock data - in production, this would come from API
|
// Mock data - in production, this would come from API
|
||||||
const environments = {
|
const environments = {
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { InfoIcon, AlertTriangleIcon, CheckCircleIcon } from 'lucide-react';
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
||||||
import { InfoIcon, AlertTriangleIcon, CheckCircleIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
// Import orchestration engine (would be from API in production)
|
|
||||||
import type { OrchestrationRequest, InputSpec, TimelineSpec } from '@/lib/fairness-orchestration';
|
import type { OrchestrationRequest, InputSpec, TimelineSpec } from '@/lib/fairness-orchestration';
|
||||||
import { orchestrate, getAvailableOutputs, getUserMessage } from '@/lib/fairness-orchestration';
|
import { orchestrate, getAvailableOutputs, getUserMessage } from '@/lib/fairness-orchestration';
|
||||||
|
|
||||||
@@ -122,26 +121,44 @@ export default function FairnessOrchestrationWizard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="dateRange">Date Range (Optional)</Label>
|
<p className="text-sm font-medium mb-2">Date range (optional)</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="fairness-date-start">Start</Label>
|
||||||
<Input
|
<Input
|
||||||
|
id="fairness-date-start"
|
||||||
type="date"
|
type="date"
|
||||||
value={inputSpec.dateRange?.start || ''}
|
value={inputSpec.dateRange?.start || ''}
|
||||||
onChange={(e) => setInputSpec(prev => ({
|
onChange={(e) =>
|
||||||
|
setInputSpec((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
dateRange: { ...prev.dateRange, start: e.target.value } as any
|
dateRange: {
|
||||||
}))}
|
start: e.target.value,
|
||||||
|
end: prev.dateRange?.end ?? '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="fairness-date-end">End</Label>
|
||||||
<Input
|
<Input
|
||||||
|
id="fairness-date-end"
|
||||||
type="date"
|
type="date"
|
||||||
value={inputSpec.dateRange?.end || ''}
|
value={inputSpec.dateRange?.end || ''}
|
||||||
onChange={(e) => setInputSpec(prev => ({
|
onChange={(e) =>
|
||||||
|
setInputSpec((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
dateRange: { ...prev.dateRange, end: e.target.value } as any
|
dateRange: {
|
||||||
}))}
|
start: prev.dateRange?.start ?? '',
|
||||||
|
end: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="sensitiveAttributes">Sensitive Attributes</Label>
|
<Label htmlFor="sensitiveAttributes">Sensitive Attributes</Label>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useSession } from 'next-auth/react'
|
|
||||||
import { createKubernetesClient } from '@/lib/kubernetes-client'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
|
|
||||||
import { Server, CheckCircle, XCircle } from 'lucide-react'
|
import { Server, CheckCircle, XCircle } from 'lucide-react'
|
||||||
|
import { useSession } from 'next-auth/react'
|
||||||
|
|
||||||
|
import { createKubernetesClient } from '@/lib/kubernetes-client'
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
|
||||||
|
|
||||||
export default function KubernetesClusters() {
|
export default function KubernetesClusters() {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Server,
|
Server,
|
||||||
@@ -15,6 +12,9 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { ChevronRight, Home } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { ChevronRight, Home } from 'lucide-react'
|
|
||||||
|
|
||||||
export function PortalBreadcrumbs() {
|
export function PortalBreadcrumbs() {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { Search, Bell, Settings, User, LogOut } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { Search, Bell, Settings, User, LogOut } from 'lucide-react'
|
|
||||||
import { signOut } from 'next-auth/react'
|
import { signOut } from 'next-auth/react'
|
||||||
|
|
||||||
export function PortalHeader() {
|
export function PortalHeader() {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { usePathname } from 'next/navigation'
|
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Server,
|
Server,
|
||||||
@@ -14,6 +12,9 @@ import {
|
|||||||
Shield,
|
Shield,
|
||||||
HelpCircle
|
HelpCircle
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
|
|
||||||
import { Button } from '../ui/Button'
|
|
||||||
import { Input } from '../ui/Input'
|
|
||||||
import { RefreshCw, Search, Download } from 'lucide-react'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { RefreshCw, Search, Download } from 'lucide-react'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
import { Button } from '../ui/Button'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
|
||||||
|
import { Input } from '../ui/Input'
|
||||||
|
|
||||||
|
|
||||||
interface LogEntry {
|
interface LogEntry {
|
||||||
stream: Record<string, string>
|
stream: Record<string, string>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
|
||||||
import { CheckCircle, ArrowRight, ArrowLeft } from 'lucide-react';
|
import { CheckCircle, ArrowRight, ArrowLeft } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
|
|
||||||
interface OnboardingStep {
|
interface OnboardingStep {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
component: React.ComponentType<any>;
|
component: React.ComponentType<{ onComplete: () => void }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OnboardingWizardProps {
|
interface OnboardingWizardProps {
|
||||||
|
|||||||
@@ -26,8 +26,11 @@ export function PreferencesStep({ onComplete }: { onComplete: () => void }) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-gray-300 mb-2">Theme</label>
|
<label htmlFor="onboarding-theme" className="block text-sm text-gray-300 mb-2">
|
||||||
|
Theme
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
|
id="onboarding-theme"
|
||||||
value={theme}
|
value={theme}
|
||||||
onChange={(e) => setTheme(e.target.value)}
|
onChange={(e) => setTheme(e.target.value)}
|
||||||
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
|
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ export function ProfileStep({ onComplete }: { onComplete: () => void }) {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-gray-300 mb-2">Full Name</label>
|
<label htmlFor="onboarding-full-name" className="block text-sm text-gray-300 mb-2">
|
||||||
|
Full Name
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="onboarding-full-name"
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
@@ -25,8 +28,11 @@ export function ProfileStep({ onComplete }: { onComplete: () => void }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-gray-300 mb-2">Role</label>
|
<label htmlFor="onboarding-role" className="block text-sm text-gray-300 mb-2">
|
||||||
|
Role
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
|
id="onboarding-role"
|
||||||
value={role}
|
value={role}
|
||||||
onChange={(e) => setRole(e.target.value)}
|
onChange={(e) => setRole(e.target.value)}
|
||||||
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
|
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function WelcomeStep({ onComplete }: { onComplete: () => void }) {
|
|||||||
Welcome to Sankofa Phoenix Nexus Console
|
Welcome to Sankofa Phoenix Nexus Console
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-400 mb-6">
|
<p className="text-gray-400 mb-6">
|
||||||
Let's get you set up in just a few steps. This will only take a minute.
|
Let's get you set up in just a few steps. This will only take a minute.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react'
|
|
||||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface AlertProps {
|
interface AlertProps {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export interface CheckboxProps {
|
export interface CheckboxProps {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
/* eslint-disable jsx-a11y/label-has-associated-control -- Primitive; pair htmlFor with inputs at call sites */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
|
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react'
|
|
||||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||||
import { Check, ChevronDown } from 'lucide-react'
|
import { Check, ChevronDown } from 'lucide-react'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
const Select = SelectPrimitive.Root
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { createCrossplaneClient } from '@/lib/crossplane-client';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card';
|
|
||||||
import { Play, Square, Trash2, Plus } from 'lucide-react';
|
import { Play, Square, Trash2, Plus } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
|
import { createCrossplaneClient, type VM } from '@/lib/crossplane-client';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card';
|
||||||
|
|
||||||
export default function VMList() {
|
export default function VMList() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@@ -39,7 +41,7 @@ export default function VMList() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{vms.map((vm: any) => (
|
{vms.map((vm: VM) => (
|
||||||
<Card key={vm.metadata.name}>
|
<Card key={vm.metadata.name}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">{vm.metadata.name}</CardTitle>
|
<CardTitle className="text-lg">{vm.metadata.name}</CardTitle>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import { GET_SYSTEM_HEALTH, GET_COST_OVERVIEW, GET_BILLING_INFO, GET_API_USAGE, GET_DEPLOYMENTS, GET_TEST_ENVIRONMENTS, GET_API_KEYS } from '@/lib/graphql/queries/dashboard'
|
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
|
|
||||||
|
import { GET_SYSTEM_HEALTH, GET_COST_OVERVIEW, GET_BILLING_INFO, GET_API_USAGE, GET_DEPLOYMENTS, GET_TEST_ENVIRONMENTS, GET_API_KEYS } from '@/lib/graphql/queries/dashboard'
|
||||||
|
|
||||||
export function useSystemHealth() {
|
export function useSystemHealth() {
|
||||||
return useQuery(GET_SYSTEM_HEALTH, {
|
return useQuery(GET_SYSTEM_HEALTH, {
|
||||||
pollInterval: 30000, // Poll every 30 seconds
|
pollInterval: 30000, // Poll every 30 seconds
|
||||||
@@ -11,7 +12,7 @@ export function useSystemHealth() {
|
|||||||
|
|
||||||
export function useCostOverview(tenantId?: string) {
|
export function useCostOverview(tenantId?: string) {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const defaultTenantId = tenantId || (session as any)?.tenantId
|
const defaultTenantId = tenantId || session?.tenantId
|
||||||
|
|
||||||
const endDate = new Date()
|
const endDate = new Date()
|
||||||
const startDate = new Date()
|
const startDate = new Date()
|
||||||
@@ -32,7 +33,7 @@ export function useCostOverview(tenantId?: string) {
|
|||||||
|
|
||||||
export function useBillingInfo(tenantId?: string) {
|
export function useBillingInfo(tenantId?: string) {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const defaultTenantId = tenantId || (session as any)?.tenantId
|
const defaultTenantId = tenantId || session?.tenantId
|
||||||
|
|
||||||
return useQuery(GET_BILLING_INFO, {
|
return useQuery(GET_BILLING_INFO, {
|
||||||
variables: {
|
variables: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
interface KeyboardShortcut {
|
interface KeyboardShortcut {
|
||||||
key: string;
|
key: string;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getInfraNodes,
|
getInfraNodes,
|
||||||
getInfraStorage,
|
getInfraStorage,
|
||||||
@@ -15,7 +16,7 @@ import {
|
|||||||
|
|
||||||
export function usePhoenixInfraNodes() {
|
export function usePhoenixInfraNodes() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const token = (session as any)?.accessToken as string | undefined;
|
const token = session?.accessToken;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['phoenix', 'infra', 'nodes'],
|
queryKey: ['phoenix', 'infra', 'nodes'],
|
||||||
queryFn: () => getInfraNodes(token),
|
queryFn: () => getInfraNodes(token),
|
||||||
@@ -25,7 +26,7 @@ export function usePhoenixInfraNodes() {
|
|||||||
|
|
||||||
export function usePhoenixInfraStorage() {
|
export function usePhoenixInfraStorage() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const token = (session as any)?.accessToken as string | undefined;
|
const token = session?.accessToken;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['phoenix', 'infra', 'storage'],
|
queryKey: ['phoenix', 'infra', 'storage'],
|
||||||
queryFn: () => getInfraStorage(token),
|
queryFn: () => getInfraStorage(token),
|
||||||
@@ -35,7 +36,7 @@ export function usePhoenixInfraStorage() {
|
|||||||
|
|
||||||
export function usePhoenixVMs(node?: string) {
|
export function usePhoenixVMs(node?: string) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const token = (session as any)?.accessToken as string | undefined;
|
const token = session?.accessToken;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['phoenix', 've', 'vms', node],
|
queryKey: ['phoenix', 've', 'vms', node],
|
||||||
queryFn: () => getVMs(token, node),
|
queryFn: () => getVMs(token, node),
|
||||||
@@ -45,7 +46,7 @@ export function usePhoenixVMs(node?: string) {
|
|||||||
|
|
||||||
export function usePhoenixHealthSummary() {
|
export function usePhoenixHealthSummary() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const token = (session as any)?.accessToken as string | undefined;
|
const token = session?.accessToken;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['phoenix', 'health', 'summary'],
|
queryKey: ['phoenix', 'health', 'summary'],
|
||||||
queryFn: () => getHealthSummary(token),
|
queryFn: () => getHealthSummary(token),
|
||||||
@@ -56,7 +57,7 @@ export function usePhoenixHealthSummary() {
|
|||||||
|
|
||||||
export function usePhoenixHealthAlerts() {
|
export function usePhoenixHealthAlerts() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const token = (session as any)?.accessToken as string | undefined;
|
const token = session?.accessToken;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['phoenix', 'health', 'alerts'],
|
queryKey: ['phoenix', 'health', 'alerts'],
|
||||||
queryFn: () => getHealthAlerts(token),
|
queryFn: () => getHealthAlerts(token),
|
||||||
@@ -67,7 +68,7 @@ export function usePhoenixHealthAlerts() {
|
|||||||
|
|
||||||
export function useTenantResources() {
|
export function useTenantResources() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const token = (session as any)?.accessToken as string | undefined;
|
const token = session?.accessToken;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['phoenix', 'tenants', 'me', 'resources'],
|
queryKey: ['phoenix', 'tenants', 'me', 'resources'],
|
||||||
queryFn: () => getTenantResources(token),
|
queryFn: () => getTenantResources(token),
|
||||||
@@ -77,7 +78,7 @@ export function useTenantResources() {
|
|||||||
|
|
||||||
export function useTenantHealth() {
|
export function useTenantHealth() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const token = (session as any)?.accessToken as string | undefined;
|
const token = session?.accessToken;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['phoenix', 'tenants', 'me', 'health'],
|
queryKey: ['phoenix', 'tenants', 'me', 'health'],
|
||||||
queryFn: () => getTenantHealth(token),
|
queryFn: () => getTenantHealth(token),
|
||||||
@@ -87,7 +88,7 @@ export function useTenantHealth() {
|
|||||||
|
|
||||||
export function useVMAction() {
|
export function useVMAction() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const token = (session as any)?.accessToken as string | undefined;
|
const token = session?.accessToken;
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ node, vmid, action, type }: { node: string; vmid: string; action: 'start' | 'stop' | 'reboot'; type?: 'qemu' | 'lxc' }) =>
|
mutationFn: ({ node, vmid, action, type }: { node: string; vmid: string; action: 'start' | 'stop' | 'reboot'; type?: 'qemu' | 'lxc' }) =>
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
|
|
||||||
import axios, { AxiosInstance } from 'axios'
|
import axios, { AxiosInstance } from 'axios'
|
||||||
|
|
||||||
|
function errorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error) return error.message
|
||||||
|
return String(error)
|
||||||
|
}
|
||||||
|
|
||||||
export interface ArgoCDApplication {
|
export interface ArgoCDApplication {
|
||||||
metadata: {
|
metadata: {
|
||||||
name: string
|
name: string
|
||||||
@@ -80,9 +85,8 @@ class ArgoCDClient {
|
|||||||
try {
|
try {
|
||||||
const response = await this.client.get('/api/v1/applications')
|
const response = await this.client.get('/api/v1/applications')
|
||||||
return response.data.items || []
|
return response.data.items || []
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('Failed to fetch ArgoCD applications:', error)
|
throw new Error(`Failed to fetch applications: ${errorMessage(error)}`)
|
||||||
throw new Error(`Failed to fetch applications: ${error.message}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,9 +99,8 @@ class ArgoCDClient {
|
|||||||
params: { namespace },
|
params: { namespace },
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error(`Failed to fetch ArgoCD application ${name}:`, error)
|
throw new Error(`Failed to fetch application: ${errorMessage(error)}`)
|
||||||
throw new Error(`Failed to fetch application: ${error.message}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,9 +125,8 @@ class ArgoCDClient {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error(`Failed to sync ArgoCD application ${name}:`, error)
|
throw new Error(`Failed to sync application: ${errorMessage(error)}`)
|
||||||
throw new Error(`Failed to sync application: ${error.message}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,9 +146,8 @@ class ArgoCDClient {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
return response.data.logs || []
|
return response.data.logs || []
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error(`Failed to fetch logs for ${name}:`, error)
|
throw new Error(`Failed to fetch logs: ${errorMessage(error)}`)
|
||||||
throw new Error(`Failed to fetch logs: ${error.message}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,15 +157,14 @@ class ArgoCDClient {
|
|||||||
async getApplicationResourceTree(
|
async getApplicationResourceTree(
|
||||||
name: string,
|
name: string,
|
||||||
namespace: string = 'argocd'
|
namespace: string = 'argocd'
|
||||||
): Promise<any> {
|
): Promise<unknown> {
|
||||||
try {
|
try {
|
||||||
const response = await this.client.get(`/api/v1/applications/${name}/resource-tree`, {
|
const response = await this.client.get(`/api/v1/applications/${name}/resource-tree`, {
|
||||||
params: { namespace },
|
params: { namespace },
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error(`Failed to fetch resource tree for ${name}:`, error)
|
throw new Error(`Failed to fetch resource tree: ${errorMessage(error)}`)
|
||||||
throw new Error(`Failed to fetch resource tree: ${error.message}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,17 @@
|
|||||||
|
|
||||||
import axios, { AxiosInstance } from 'axios'
|
import axios, { AxiosInstance } from 'axios'
|
||||||
|
|
||||||
|
function errorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error) return error.message
|
||||||
|
return String(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface K8sNode {
|
||||||
|
status?: {
|
||||||
|
conditions?: Array<{ type: string; status: string }>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface KubernetesCluster {
|
export interface KubernetesCluster {
|
||||||
name: string
|
name: string
|
||||||
server: string
|
server: string
|
||||||
@@ -28,8 +39,8 @@ export interface KubernetesResource {
|
|||||||
labels?: Record<string, string>
|
labels?: Record<string, string>
|
||||||
annotations?: Record<string, string>
|
annotations?: Record<string, string>
|
||||||
}
|
}
|
||||||
spec?: any
|
spec?: Record<string, unknown>
|
||||||
status?: any
|
status?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KubernetesNamespace {
|
export interface KubernetesNamespace {
|
||||||
@@ -69,9 +80,9 @@ class KubernetesClient {
|
|||||||
const nodesResponse = await this.client.get('/api/v1/nodes')
|
const nodesResponse = await this.client.get('/api/v1/nodes')
|
||||||
|
|
||||||
const nodes = nodesResponse.data.items || []
|
const nodes = nodesResponse.data.items || []
|
||||||
const readyNodes = nodes.filter((node: any) => {
|
const readyNodes = nodes.filter((node: K8sNode) => {
|
||||||
const conditions = node.status?.conditions || []
|
const conditions = node.status?.conditions || []
|
||||||
return conditions.some((c: any) => c.type === 'Ready' && c.status === 'True')
|
return conditions.some((c) => c.type === 'Ready' && c.status === 'True')
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -85,9 +96,8 @@ class KubernetesClient {
|
|||||||
ready: readyNodes.length,
|
ready: readyNodes.length,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('Failed to fetch cluster info:', error)
|
throw new Error(`Failed to fetch cluster info: ${errorMessage(error)}`)
|
||||||
throw new Error(`Failed to fetch cluster info: ${error.message}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +108,8 @@ class KubernetesClient {
|
|||||||
try {
|
try {
|
||||||
const response = await this.client.get('/api/v1/namespaces')
|
const response = await this.client.get('/api/v1/namespaces')
|
||||||
return response.data.items || []
|
return response.data.items || []
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('Failed to list namespaces:', error)
|
throw new Error(`Failed to list namespaces: ${errorMessage(error)}`)
|
||||||
throw new Error(`Failed to list namespaces: ${error.message}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,9 +128,8 @@ class KubernetesClient {
|
|||||||
|
|
||||||
const response = await this.client.get(path)
|
const response = await this.client.get(path)
|
||||||
return response.data.items || []
|
return response.data.items || []
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error(`Failed to list ${kind} resources:`, error)
|
throw new Error(`Failed to list resources: ${errorMessage(error)}`)
|
||||||
throw new Error(`Failed to list resources: ${error.message}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,9 +149,8 @@ class KubernetesClient {
|
|||||||
|
|
||||||
const response = await this.client.get(path)
|
const response = await this.client.get(path)
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error(`Failed to get ${kind} ${name}:`, error)
|
throw new Error(`Failed to get resource: ${errorMessage(error)}`)
|
||||||
throw new Error(`Failed to get resource: ${error.message}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ const getBaseUrl = () => {
|
|||||||
return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type JsonObject = Record<string, unknown>;
|
||||||
|
|
||||||
|
function errorTextFromBody(body: unknown, fallback: string): string {
|
||||||
|
if (body && typeof body === 'object') {
|
||||||
|
const o = body as JsonObject;
|
||||||
|
if (typeof o.message === 'string') return o.message;
|
||||||
|
if (typeof o.error === 'string') return o.error;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
export async function phoenixFetch<T>(
|
export async function phoenixFetch<T>(
|
||||||
path: string,
|
path: string,
|
||||||
token: string | undefined,
|
token: string | undefined,
|
||||||
@@ -26,23 +37,23 @@ export async function phoenixFetch<T>(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({ error: res.statusText }));
|
const err: unknown = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
throw new Error((err as any).message || (err as any).error || res.statusText);
|
throw new Error(errorTextFromBody(err, res.statusText));
|
||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getInfraNodes(token?: string) {
|
export async function getInfraNodes(token?: string) {
|
||||||
return phoenixFetch<{ nodes: any[]; stub?: boolean }>('/api/v1/infra/nodes', token);
|
return phoenixFetch<{ nodes: JsonObject[]; stub?: boolean }>('/api/v1/infra/nodes', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getInfraStorage(token?: string) {
|
export async function getInfraStorage(token?: string) {
|
||||||
return phoenixFetch<{ storage: any[]; stub?: boolean }>('/api/v1/infra/storage', token);
|
return phoenixFetch<{ storage: JsonObject[]; stub?: boolean }>('/api/v1/infra/storage', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getVMs(token?: string, node?: string) {
|
export async function getVMs(token?: string, node?: string) {
|
||||||
const qs = node ? `?node=${encodeURIComponent(node)}` : '';
|
const qs = node ? `?node=${encodeURIComponent(node)}` : '';
|
||||||
return phoenixFetch<{ vms: any[]; stub?: boolean }>(`/api/v1/ve/vms${qs}`, token);
|
return phoenixFetch<{ vms: JsonObject[]; stub?: boolean }>(`/api/v1/ve/vms${qs}`, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getVMStatus(node: string, vmid: string, token?: string, type: 'qemu' | 'lxc' = 'qemu') {
|
export async function getVMStatus(node: string, vmid: string, token?: string, type: 'qemu' | 'lxc' = 'qemu') {
|
||||||
@@ -57,15 +68,18 @@ export async function vmAction(node: string, vmid: string, action: 'start' | 'st
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getHealthSummary(token?: string) {
|
export async function getHealthSummary(token?: string) {
|
||||||
return phoenixFetch<{ status: string; updated_at: string; hosts: any[]; alerts: any[] }>('/api/v1/health/summary', token);
|
return phoenixFetch<{ status: string; updated_at: string; hosts: JsonObject[]; alerts: JsonObject[] }>(
|
||||||
|
'/api/v1/health/summary',
|
||||||
|
token
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHealthAlerts(token?: string) {
|
export async function getHealthAlerts(token?: string) {
|
||||||
return phoenixFetch<{ alerts: any[] }>('/api/v1/health/alerts', token);
|
return phoenixFetch<{ alerts: JsonObject[] }>('/api/v1/health/alerts', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTenantResources(token?: string) {
|
export async function getTenantResources(token?: string) {
|
||||||
return phoenixFetch<{ resources: any[]; tenantId: string }>('/api/v1/tenants/me/resources', token);
|
return phoenixFetch<{ resources: JsonObject[]; tenantId: string }>('/api/v1/tenants/me/resources', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTenantHealth(token?: string) {
|
export async function getTenantHealth(token?: string) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { ReactElement } from 'react'
|
|
||||||
import { render, RenderOptions } from '@testing-library/react'
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { render, RenderOptions } from '@testing-library/react'
|
||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
|
||||||
// Create a test query client
|
// Create a test query client
|
||||||
const createTestQueryClient = () =>
|
const createTestQueryClient = () =>
|
||||||
|
|||||||
2
portal/src/types/next-auth.d.ts
vendored
2
portal/src/types/next-auth.d.ts
vendored
@@ -5,6 +5,8 @@ declare module 'next-auth' {
|
|||||||
interface Session {
|
interface Session {
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
roles?: string[];
|
roles?: string[];
|
||||||
|
/** Optional tenant scope for billing/dashboard GraphQL */
|
||||||
|
tenantId?: string;
|
||||||
user?: DefaultSession['user'] & {
|
user?: DefaultSession['user'] & {
|
||||||
id?: string;
|
id?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user