style(ui): collapse long error messages into issue details
This commit is contained in:
1 parent
70fd3db19f
commit
3b3a18992b
10 files changed
+163
-150
No files matched your search
@@ -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 ? (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+16
-30
@@ -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
|
||||
|
||||
@@ -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
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user