style(ui): collapse long error messages into issue details

This commit is contained in:
KodekaTeam committed 2026-06-10 14:10:25 +07:00
1 parent 70fd3db19f
commit 3b3a18992b
10 files changed
+163 -150

No files matched your search

+6 -15
View File
@@ -2,6 +2,7 @@
import ConfirmActionDialog from "@/components/ConfirmActionDialog";
import GuardedPage from "@/components/GuardedPage";
import IssueDetailsSummary from "@/components/IssueDetailsSummary";
import {
apiKeys as apiKeysApi,
type ApiKeyExpiryOption,
@@ -529,21 +530,11 @@ export default function ApiKeysPage() {
</div>
{error && (
<div
className="card"
style={{
padding: 14,
border: "1px solid rgba(239,68,68,0.25)",
background: "rgba(239,68,68,0.08)",
color: "#fca5a5",
display: "flex",
alignItems: "center",
gap: 10,
}}
>
<AlertCircle size={16} />
<span style={{ fontSize: 12 }}>{error}</span>
</div>
<IssueDetailsSummary
label="API Keys"
message={error}
description="API key data could not be loaded or updated."
/>
)}
{loading ? (
+11 -26
View File
@@ -24,6 +24,7 @@ import {
} from "lucide-react";
import DashboardLayout from "@/components/DashboardLayout";
import ConfirmActionDialog from "@/components/ConfirmActionDialog";
import IssueDetailsSummary from "@/components/IssueDetailsSummary";
import ToastViewport from "@/components/ToastViewport";
import {
AppInstall,
@@ -1043,19 +1044,11 @@ function InstallOperationsModal({
</div>
{install.error ? (
<div
style={{
background: "rgba(239,68,68,0.08)",
border: "1px solid rgba(239,68,68,0.2)",
borderRadius: 10,
padding: "12px 14px",
color: "#ef4444",
fontSize: 12,
lineHeight: 1.6,
}}
>
{install.error}
</div>
<IssueDetailsSummary
label={install.appName}
message={install.error}
description="Installation error returned by the app runtime."
/>
) : null}
</div>
) : null}
@@ -1852,19 +1845,11 @@ export default function InstalledAppsPage() {
</div>
{install.error ? (
<div
style={{
background: "rgba(239,68,68,0.08)",
border: "1px solid rgba(239,68,68,0.2)",
borderRadius: 8,
padding: "10px 12px",
color: "#ef4444",
fontSize: 12,
lineHeight: 1.5,
}}
>
{install.error}
</div>
<IssueDetailsSummary
label={install.appName}
message={install.error}
description="Installation error returned by the app runtime."
/>
) : null}
<div
+6 -16
View File
@@ -2,6 +2,7 @@
import ConfirmActionDialog from "@/components/ConfirmActionDialog";
import DashboardLayout from "@/components/DashboardLayout";
import IssueDetailsSummary from "@/components/IssueDetailsSummary";
import TablePagination from "@/components/TablePagination";
import ToastViewport from "@/components/ToastViewport";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -15,7 +16,6 @@ import {
ToggleRight,
Loader2,
Server as ServerIcon,
AlertTriangle,
} from "lucide-react";
import {
security as securityApi,
@@ -464,21 +464,11 @@ export default function FirewallPage() {
style={{ display: "flex", flexDirection: "column", gap: 16 }}
>
{error && (
<div
className="card"
style={{
padding: 14,
border: "1px solid rgba(239,68,68,0.25)",
background: "rgba(239,68,68,0.08)",
color: "#ef4444",
display: "flex",
alignItems: "center",
gap: 10,
}}
>
<AlertTriangle size={16} />
<span style={{ fontSize: 13 }}>{error}</span>
</div>
<IssueDetailsSummary
label="Firewall"
message={error}
description="Firewall data could not be loaded or refreshed."
/>
)}
<div
+2
View File
@@ -592,6 +592,8 @@ body {
color: var(--text-danger);
font-size: 12px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
.server-config-component-actions {
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import ConfirmActionDialog from "@/components/ConfirmActionDialog";
import DashboardLayout from "@/components/DashboardLayout";
import IssueDetailsSummary from "@/components/IssueDetailsSummary";
import ProcessLogsModal, {
type ProcessLogStep,
useProcessLogsModal,
@@ -1214,41 +1215,26 @@ export default function AppContainerDetailPage() {
Loading container detail...
</section>
) : error ? (
<section
className="card"
style={{
padding: 16,
color: "var(--text-danger)",
borderColor: "rgba(239,68,68,0.35)",
}}
>
{error}
</section>
<IssueDetailsSummary
label="Container Detail"
message={error}
description="Container detail could not be loaded from the current project environment."
/>
) : appDetail ? (
<>
{runtimeNotice ? (
<section
className="card"
style={{
padding: 14,
color: "var(--accent-yellow)",
borderColor: "rgba(245,158,11,0.35)",
}}
>
Runtime stats are unavailable: {runtimeNotice}
</section>
<IssueDetailsSummary
label="Runtime Stats"
message={runtimeNotice}
description="Runtime stats are unavailable for this container."
/>
) : null}
{actionError ? (
<section
className="card"
style={{
padding: 14,
color: "var(--text-danger)",
borderColor: "rgba(239,68,68,0.35)",
}}
>
{actionError}
</section>
<IssueDetailsSummary
label="Container Action"
message={actionError}
description="The latest container action returned an error."
/>
) : null}
<AppDetailHeader
app={appDetail}
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useParams } from "next/navigation";
import DashboardLayout from "@/components/DashboardLayout";
import IssueDetailsSummary from "@/components/IssueDetailsSummary";
import ProcessLogsModal, {
useProcessLogsModal,
} from "@/components/ProcessLogsModal";
@@ -275,16 +276,11 @@ export default function EnvironmentContainersPage() {
<EnvironmentHeader projectId={params.projectId} summary={summary} />
) : null}
{error ? (
<section
className="card"
style={{
padding: 16,
color: "var(--text-danger)",
borderColor: "rgba(239,68,68,0.35)",
}}
>
{error}
</section>
<IssueDetailsSummary
label="Environment Containers"
message={error}
description="Environment container data could not be loaded or synced."
/>
) : null}
<EnvironmentContainersSummary containers={environmentContainers} />
<EnvironmentContainersToolbar
+7 -16
View File
@@ -2,9 +2,10 @@
import ConfirmActionDialog from "@/components/ConfirmActionDialog";
import DashboardLayout from "@/components/DashboardLayout";
import IssueDetailsSummary from "@/components/IssueDetailsSummary";
import ToastViewport from "@/components/ToastViewport";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Loader2, AlertTriangle } from "lucide-react";
import { Loader2 } from "lucide-react";
import {
security as securityApi,
SecurityOverviewItem,
@@ -566,21 +567,11 @@ export default function SecurityPage() {
style={{ display: "flex", flexDirection: "column", gap: 16 }}
>
{error && (
<div
className="card"
style={{
padding: 14,
border: "1px solid rgba(239,68,68,0.25)",
background: "rgba(239,68,68,0.08)",
color: "#ef4444",
display: "flex",
alignItems: "center",
gap: 10,
}}
>
<AlertTriangle size={16} />
<span style={{ fontSize: 13 }}>{error}</span>
</div>
<IssueDetailsSummary
label="Security"
message={error}
description="Security data could not be loaded or refreshed."
/>
)}
<SecuritySummary summary={summary} />
@@ -47,6 +47,7 @@ import ServerConfigOverviewPanel from "@/app/servers/components/server-config/Se
import ServerConfigServicesPanel from "@/app/servers/components/server-config/ServerConfigServicesPanel";
import ServerConfigUsersPanel from "@/app/servers/components/server-config/ServerConfigUsersPanel";
import ServerConfigWebServerPanel from "@/app/servers/components/server-config/ServerConfigWebServerPanel";
import IssueDetailsSummary from "@/components/IssueDetailsSummary";
interface ServerConfigModalProps {
server: ServerType;
@@ -1098,18 +1099,11 @@ export default function ServerConfigModal({
) : null}
{error ? (
<div
style={{
background: "rgba(239,68,68,0.1)",
border: "1px solid rgba(239,68,68,0.3)",
borderRadius: 10,
padding: "12px 14px",
color: "#ef4444",
fontSize: 13,
}}
>
{error}
</div>
<IssueDetailsSummary
label="Server Config"
message={error}
description="The latest server configuration action returned an error."
/>
) : null}
{snapshotLoadError ? (
@@ -1140,15 +1134,11 @@ export default function ServerConfigModal({
available, especially in the Actions tab.
</p>
</div>
<div
style={{
color: "#fbbf24",
fontSize: 12,
lineHeight: 1.6,
}}
>
Latest error: {snapshotLoadError}
</div>
<IssueDetailsSummary
label="Configuration Snapshot"
message={snapshotLoadError}
description="The server did not return a full live configuration snapshot."
/>
</div>
) : null}
+6 -16
View File
@@ -2,8 +2,8 @@
import ConfirmActionDialog from "@/components/ConfirmActionDialog";
import GuardedPage from "@/components/GuardedPage";
import IssueDetailsSummary from "@/components/IssueDetailsSummary";
import ToastViewport from "@/components/ToastViewport";
import { AlertCircle } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
CreateUserInvitationBody,
@@ -383,21 +383,11 @@ export default function UsersPage() {
style={{ display: "flex", flexDirection: "column", gap: 16 }}
>
{error && (
<div
className="card"
style={{
padding: "12px 16px",
display: "flex",
alignItems: "center",
gap: 10,
color: "#ef4444",
border: "1px solid rgba(239,68,68,0.28)",
background: "rgba(239,68,68,0.08)",
}}
>
<AlertCircle size={15} />
<span style={{ fontSize: 12 }}>{error}</span>
</div>
<IssueDetailsSummary
label="Users"
message={error}
description="User data could not be loaded or updated."
/>
)}
<UsersRoleSummary roleCounts={roleCounts} />
+92
View File
@@ -0,0 +1,92 @@
"use client";
import { useState } from "react";
import { createPortal } from "react-dom";
import { AlertTriangle, X } from "lucide-react";
type IssueDetailsSummaryProps = {
label: string;
message?: string;
notes?: string[];
description?: string;
};
export default function IssueDetailsSummary({
label,
message,
notes,
description = "Issue details returned by the current operation.",
}: IssueDetailsSummaryProps) {
const [detailOpen, setDetailOpen] = useState(false);
const issueNotes = (notes?.length ? notes : message ? [message] : []).filter(
Boolean,
);
const issueCount = issueNotes.length || 1;
return (
<>
<button
type="button"
className="server-config-component-issue-summary"
onClick={() => setDetailOpen(true)}
>
<span className="server-config-component-issue-copy">
<AlertTriangle size={13} />
<span>
{issueCount === 1
? "1 issue detected"
: `${issueCount} issues detected`}
</span>
</span>
<span className="server-config-component-issue-action">Details</span>
</button>
{detailOpen && typeof document !== "undefined"
? createPortal(
<div className="server-config-issue-overlay">
<div className="card server-config-issue-dialog">
<div className="server-config-issue-dialog-header">
<div>
<strong
style={{ color: "var(--text-primary)", fontSize: 15 }}
>
{label} Issues
</strong>
<p
style={{
marginTop: 6,
color: "var(--text-muted)",
fontSize: 12,
lineHeight: 1.5,
}}
>
{description}
</p>
</div>
<button
type="button"
onClick={() => setDetailOpen(false)}
aria-label="Close issue details"
className="server-config-issue-close"
>
<X size={14} />
</button>
</div>
<div className="server-config-issue-list">
{issueNotes.map((note, index) => (
<div
key={`${index}-${note}`}
className="server-config-issue-note"
>
{note}
</div>
))}
</div>
</div>
</div>,
document.body,
)
: null}
</>
);
}