import {useState} from 'react'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import { K8sEngineDetails } from "@/features/k8s/K8sEngineDetails";
import { LogsExplorerPanel } from "@/components/LogsExplorerPanel";
import { LogsQueryPanel } from "@/components/LogsQueryPanel";
import type { CloudProvider } from "@/types/cloud";
import type { CloudResource, StorageObject } from "@/types/resource";
import {formatBytes} from "@/lib/format";
import {CopyValueButton} from "@/components/CopyValueButton";
import {rebootCloudResource, startCloudResource, stopCloudResource} from "@/api/cloudProxyClient";
import {cloudQueryKeys} from "@/api/queries/cloudQueries";
import {decodeRawMime, htmlMimePart, inlineMimeImages, parseMimeAttachments} from '@/lib/emailMime'
interface ResourceInspectorProps {
resource?: CloudResource;
object?: StorageObject;
cloud?: CloudProvider;
runtimeReachable?: boolean;
serviceName?: string;
}
export function ResourceInspector({
resource,
object,
cloud,
runtimeReachable,
serviceName,
}: ResourceInspectorProps) {
if (!resource) {
return (
Resource Inspector
Select a resource
Inspect normalized metadata returned by the cloud proxy.
);
}
if (object) {
return (
);
}
if (resource.type === 'email') {
return
}
const tags = getTags(resource.metadata.tags);
const tagsUnavailable = getBooleanMetadata(resource.metadata.tagsUnavailable) ?? false;
const versioning = getStringMetadata(resource.metadata.versioning);
const versioningEnabled = getBooleanMetadata(
resource.metadata.versioningEnabled,
);
const isDatabase =
resource.service === "database" || resource.type === "db-instance";
const isAwsDatabase = isDatabase && resource.cloud === "aws";
const isK8sEngine = resource.service === "k8s" || resource.type === "cluster";
const isLambda =
resource.service === "serverless" || resource.type === "lambda";
const isLogGroup = resource.cloud === "aws" && (resource.service === "logs" || resource.type === "log-group");
return (
{resource.name}
{resource.type}
{resource.status && (
)}
{resource.engine && (
)}
{resource.version && (
)}
{resource.instanceClass && (
)}
{versioning && }
{versioningEnabled !== null && (
)}
{isLambda && (
<>
>
)}
Tags
{tagsUnavailable ? (
Tags unavailable: the provider denied or failed the tag lookup.
) : tags.length === 0 ? (
No tags returned for this resource.
) : (
{tags.map((tag) => (
{tag.key}
{tag.value}
))}
)}
{isDatabase && (
)}
{isAwsDatabase && }
{isDatabase && !isAwsDatabase && (
)}
{isK8sEngine && (
)}
{isLambda && (
)}
{JSON.stringify(resource.metadata, null, 2)}
{isLogGroup && cloud && (
<>
>
)}
);
}
function EmailInspector({resource}: {resource: CloudResource}) {
const [tab, setTab] = useState<'preview' | 'text' | 'raw'>('preview')
const source = getStringMetadata(resource.metadata.source) ?? '-'
const toAddresses = getStringList(resource.metadata.toAddresses)
const ccAddresses = getStringList(resource.metadata.ccAddresses)
const bccAddresses = getStringList(resource.metadata.bccAddresses)
const replyToAddresses = getStringList(resource.metadata.replyToAddresses)
const textBody = getStringMetadata(resource.metadata.textBody)
const htmlBody = getStringMetadata(resource.metadata.htmlBody)
const rawData = getStringMetadata(resource.metadata.rawData)
const rawMime = rawData ? decodeRawMime(rawData) : null
const attachments = rawMime ? parseMimeAttachments(rawMime) : []
const inlineImages = rawMime ? inlineMimeImages(rawMime) : {}
const rawHtmlBody = rawMime ? htmlMimePart(rawMime) : null
const previewHtml = htmlBody ?? rawHtmlBody
const hasPreview = Boolean(previewHtml || textBody)
const activeTab = tab === 'preview' && !hasPreview ? (rawData ? 'raw' : 'text') : tab
return (
{resource.name}
{getStringMetadata(resource.metadata.messageType) ?? 'email'}
{ccAddresses.length > 0 && }
{bccAddresses.length > 0 && }
{replyToAddresses.length > 0 && }
setTab('preview')}>Preview
setTab('text')}>Text
setTab('raw')}>Raw
{rawMime && downloadRawEmail(resource.id, rawMime)}>Download MIME }
{activeTab === 'preview' && previewHtml ? (
) : activeTab === 'preview' ? (
{textBody ?? 'No preview content captured.'}
) : activeTab === 'text' ? (
{textBody ?? 'No text body captured.'}
) : (
{rawMime ?? 'No raw MIME data captured.'}
)}
{attachments.length > 0 &&
MIME attachments
{attachments.map((attachment, index) =>
{attachment.filename ?? '(unnamed MIME part)'}{attachment.contentType} ยท {attachment.disposition} {attachment.contentId && Content-ID: {attachment.contentId} ({attachment.disposition === 'inline' ? 'inline' : 'referenced part'}) }
)}
}
)
}
function downloadRawEmail(id: string, rawData: string): void {
const url = URL.createObjectURL(new Blob([rawData], {type: 'message/rfc822'}))
const link = document.createElement('a')
link.href = url
link.download = `${id || 'message'}.eml`
link.click()
URL.revokeObjectURL(url)
}
function safeEmailDocument(html: string, inlineImages: Record = {}): string {
const withInlineImages = html.replace(/cid:([^"'\s>]+)/gi, (reference, contentId: string) => inlineImages[contentId] ?? reference)
return ` ${withInlineImages}`
}
function ProviderDatabaseSection({ cloud }: { cloud: string }) {
const label =
cloud === "azure"
? "Azure database"
: cloud === "gcp"
? "Google Cloud SQL"
: "Database";
return (
Lifecycle
{label} lifecycle and snapshot actions are exposed through the
normalized resource metadata only.
);
}
function DatabaseConnectionsSection({
metadata,
engine,
status,
}: {
metadata: Record;
engine?: string | null;
status?: string | null;
}) {
const endpoint = getRecordMetadata(metadata.endpoint);
const subnetGroup = getRecordMetadata(metadata.subnetGroup);
const securityGroups = getSecurityGroups(metadata.vpcSecurityGroups);
const address = getStringMetadata(endpoint?.address);
const port = getNumberMetadata(endpoint?.port);
const vpcId = getStringMetadata(subnetGroup?.vpcId);
const subnetGroupName = getStringMetadata(subnetGroup?.name);
const subnetGroupStatus = getStringMetadata(subnetGroup?.status);
return (
Connections
{address && port !== null && engine && }
{securityGroups.length > 0 && (
{securityGroups.map((group) => (
{group.id}
{group.status}
))}
)}
);
}
function DatabaseConnectionHealth({status, hasEndpoint}: {status?: string | null; hasEndpoint: boolean}) {
const normalized = status?.toLowerCase()
const state = normalized === 'available' && hasEndpoint
? {label: 'Ready', detail: 'The provider reports this instance as available with a connection endpoint.'}
: normalized === 'failed'
? {label: 'Unavailable', detail: 'The provider reports the backing database as failed.'}
: !hasEndpoint
? {label: 'Endpoint unavailable', detail: 'The provider has not returned a usable database endpoint.'}
: {label: status ?? 'Unknown', detail: 'Connection readiness follows the provider-reported instance status.'}
return Connection health: {state.label}. {state.detail}
}
function DatabaseConnectionCommand({ engine, address, port, database, username }: { engine: string; address: string; port: number; database: string; username: string }) {
const host = address.startsWith('172.') ? 'localhost' : address
const command = engine.includes('postgres')
? `psql "host=${host} port=${port} dbname=${database} user=${username} password="`
: engine.includes('mysql') || engine.includes('mariadb')
? `mysql --host=${host} --port=${port} --user=${username} --password ${database}`
: `sqlcmd -S ${host},${port} -U ${username} -P -d ${database}`
const uri = engine.includes('postgres')
? `postgresql://${username}:@${host}:${port}/${database}`
: engine.includes('mysql') || engine.includes('mariadb')
? `mysql://${username}:@${host}:${port}/${database}`
: `Server=${host},${port};Database=${database};User Id=${username};Password=;`
return Terminal connection
{command}
Client connection URI
{uri}
Paste the URI into a compatible database client, then replace the password placeholder. The password is never retained by the console.
}
function DatabaseLifecycleSection({resource}: {resource: CloudResource}) {
const queryClient = useQueryClient()
const lifecycle = useMutation({
mutationFn: async (action: "start" | "stop" | "reboot") => {
if (action === "start") return startCloudResource(resource.cloud, resource.service, resource.id)
if (action === "stop") return stopCloudResource(resource.cloud, resource.service, resource.id)
return rebootCloudResource(resource.cloud, resource.service, resource.id)
},
onSuccess: () => {
void queryClient.invalidateQueries({queryKey: cloudQueryKeys.resources(resource.cloud, resource.service)})
void queryClient.invalidateQueries({queryKey: cloudQueryKeys.resource(resource.cloud, resource.service, resource.id)})
},
})
const status = resource.status?.toLowerCase()
const isPending = lifecycle.isPending
return (
Lifecycle
lifecycle.mutate("start")}>
Start
lifecycle.mutate("stop")}>
Stop
lifecycle.mutate("reboot")}>
Reboot
Current status: {resource.status ?? "unknown"}.
{lifecycle.error && {lifecycle.error instanceof Error ? lifecycle.error.message : "Unable to update DB instance lifecycle."}
}
);
}
function InspectorItem({ label, value, copyable = false }: { label: string; value: string; copyable?: boolean }) {
return (
{label}
{value}
{copyable && value !== "-" && }
);
}
function MetadataPanel({ metadata }: { metadata: Record }) {
const rows = Object.entries(metadata).filter(
([, value]) => value !== undefined && value !== null && value !== "",
);
if (rows.length === 0) {
return (
No metadata returned
);
}
return (
{rows.map(([key, value]) => (
{humanizeKey(key)}
{formatMetadataValue(value)}
{isCopyableMetadata(key, value) && }
))}
);
}
function isCopyableMetadata(key: string, value: unknown): value is string | number {
return (typeof value === "string" || typeof value === "number") && /id$|arn|ip|endpoint|url|uri|key|hash|token/i.test(key);
}
function formatMetadataValue(value: unknown): string {
return typeof value === "object" ? JSON.stringify(value) : String(value);
}
function humanizeKey(value: string): string {
return value
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/[_-]+/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
}
function getStringMetadata(value: unknown): string | null {
return typeof value === "string" ? value : null;
}
function getStringList(value: unknown): string[] {
return Array.isArray(value)
? value.filter((entry): entry is string => typeof entry === "string")
: [];
}
function getBooleanMetadata(value: unknown): boolean | null {
return typeof value === "boolean" ? value : null;
}
function getNumberMetadata(value: unknown): number | null {
return typeof value === "number" ? value : null;
}
function getRecordMetadata(value: unknown): Record | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record)
: null;
}
function getTags(value: unknown): Array<{ key: string; value: string }> {
if (!Array.isArray(value)) return [];
return value.flatMap((item) => {
if (!item || typeof item !== "object") return [];
const tag = item as Record;
return typeof tag.key === "string" && typeof tag.value === "string"
? [{ key: tag.key, value: tag.value }]
: [];
});
}
function getSecurityGroups(
value: unknown,
): Array<{ id: string; status: string }> {
if (!Array.isArray(value)) return [];
return value.flatMap((item) => {
if (!item || typeof item !== "object") return [];
const group = item as Record;
return typeof group.id === "string"
? [
{
id: group.id,
status: typeof group.status === "string" ? group.status : "-",
},
]
: [];
});
}