style: improve web-server panel of list service card in config server modal
This commit is contained in:
1 parent
4f634530da
commit
af92445c6c
4 files changed
+327
-100
No files matched your search
+217
-3
@@ -407,9 +407,223 @@ body {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.server-config-component-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.server-config-component-card {
|
||||
min-width: 0;
|
||||
min-height: 100%;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.server-config-component-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.server-config-component-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.server-config-component-description {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.server-config-component-divider {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.server-config-badge-list {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.server-config-readiness-notices {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.server-config-readiness-notice {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(245, 158, 11, 0.24);
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
color: #b45309;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.server-config-readiness-notice svg {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.server-config-readiness-notice span {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.server-config-component-issue-summary {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 9px 11px;
|
||||
border-radius: 8px;
|
||||
color: #ef4444;
|
||||
background: rgba(248, 113, 113, 0.15);
|
||||
border: 1px solid rgba(248, 113, 113, 0.3);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.server-config-component-issue-summary:hover {
|
||||
background: rgba(248, 113, 113, 0.2);
|
||||
border-color: rgba(248, 113, 113, 0.42);
|
||||
}
|
||||
|
||||
.server-config-component-issue-copy {
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.server-config-component-issue-copy span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.server-config-component-issue-action {
|
||||
flex-shrink: 0;
|
||||
color: #dc2626;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.server-config-issue-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1003;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: rgba(3, 7, 18, 0.58);
|
||||
}
|
||||
|
||||
.server-config-issue-dialog {
|
||||
width: min(520px, 100%);
|
||||
max-height: min(70vh, 560px);
|
||||
padding: 18px;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
overflow: auto;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.server-config-issue-dialog-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.server-config-issue-close {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
background: rgba(15, 23, 42, 0.16);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.server-config-issue-close:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.server-config-issue-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.server-config-issue-note {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(248, 113, 113, 0.3);
|
||||
background: rgba(248, 113, 113, 0.14);
|
||||
color: var(--text-danger);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.server-config-component-actions {
|
||||
margin-top: auto;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.server-config-component-actions .btn {
|
||||
min-width: 94px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.server-config-component-card {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.server-config-component-header {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.server-config-component-header > .ui-badge {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.server-config-component-actions .btn {
|
||||
flex: 1 1 140px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Progress bars */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Eye,
|
||||
@@ -638,19 +639,20 @@ export default function ServerConfigModal({
|
||||
gap: 18,
|
||||
}}
|
||||
>
|
||||
{pendingConfirm ? (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
background: "rgba(3,7,18,0.58)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 20,
|
||||
zIndex: 1002,
|
||||
}}
|
||||
>
|
||||
{pendingConfirm && typeof document !== "undefined"
|
||||
? createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
background: "rgba(3,7,18,0.58)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 20,
|
||||
zIndex: 1002,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="card"
|
||||
style={{
|
||||
@@ -895,8 +897,10 @@ export default function ServerConfigModal({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -73,6 +73,7 @@ export function UserBadge({
|
||||
return (
|
||||
<span
|
||||
className="ui-badge"
|
||||
title={label}
|
||||
style={{
|
||||
color: style.color,
|
||||
background: style.background,
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Loader2, Plus, RefreshCw, Trash2 } from "lucide-react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Loader2,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type {
|
||||
WebStackComponentStatus,
|
||||
ServerConfigSnapshot,
|
||||
@@ -39,6 +47,10 @@ export default function ServerConfigWebServerPanel({
|
||||
const [activeGroup, setActiveGroup] = useState<
|
||||
"infrastructure" | "runtime-tools"
|
||||
>("infrastructure");
|
||||
const [activeIssueDetail, setActiveIssueDetail] = useState<{
|
||||
label: string;
|
||||
notes: string[];
|
||||
} | null>(null);
|
||||
|
||||
const infrastructureComponents = snapshot.webServer.components.filter(
|
||||
(component) =>
|
||||
@@ -55,31 +67,13 @@ export default function ServerConfigWebServerPanel({
|
||||
: runtimeToolComponents;
|
||||
|
||||
const renderComponentCard = (component: WebStackComponentStatus) => (
|
||||
<div
|
||||
key={component.key}
|
||||
className="card"
|
||||
style={{ padding: 16, display: "grid", gap: 14 }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div key={component.key} className="card server-config-component-card">
|
||||
<div className="server-config-component-header">
|
||||
<div className="server-config-component-copy">
|
||||
<strong style={{ color: "var(--text-primary)", fontSize: 14 }}>
|
||||
{component.label}
|
||||
</strong>
|
||||
<p
|
||||
style={{
|
||||
marginTop: 6,
|
||||
fontSize: 12,
|
||||
color: "var(--text-muted)",
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
<p className="server-config-component-description">
|
||||
{component.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -88,11 +82,9 @@ export default function ServerConfigWebServerPanel({
|
||||
tone={component.installed ? "success" : "warning"}
|
||||
/>
|
||||
</div>
|
||||
<hr style={{ borderColor: "var(--border)" }} />
|
||||
<hr className="server-config-component-divider" />
|
||||
|
||||
<div
|
||||
style={{ display: "flex", gap: 8, maxHeight: "20px", flexWrap: "wrap" }}
|
||||
>
|
||||
<div className="server-config-badge-list">
|
||||
<UserBadge label={component.category} tone="neutral" />
|
||||
{component.version ? (
|
||||
<UserBadge label={component.version} tone="neutral" />
|
||||
@@ -108,48 +100,36 @@ export default function ServerConfigWebServerPanel({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ display: "flex", gap: 8, maxHeight: "20px", flexWrap: "wrap" }}
|
||||
>
|
||||
<div className="server-config-badge-list">
|
||||
{component.recommendedFor.map((target) => (
|
||||
<UserBadge key={target} label={target} tone="neutral" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{component.notes.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: 6,
|
||||
lineHeight: 1.4,
|
||||
padding: 12,
|
||||
borderRadius: 12,
|
||||
background: "rgba(248, 113, 113, 0.15)",
|
||||
border: "1px solid rgba(248, 113, 113, 0.3)",
|
||||
}}
|
||||
<button
|
||||
type="button"
|
||||
className="server-config-component-issue-summary"
|
||||
onClick={() =>
|
||||
setActiveIssueDetail({
|
||||
label: component.label,
|
||||
notes: component.notes,
|
||||
})
|
||||
}
|
||||
>
|
||||
{component.notes.map((note) => (
|
||||
<div
|
||||
key={note}
|
||||
style={{ color: "var(--text-danger)", fontSize: 12 }}
|
||||
>
|
||||
{note}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="server-config-component-issue-copy">
|
||||
<AlertTriangle size={13} />
|
||||
<span>
|
||||
{component.notes.length === 1
|
||||
? "1 issue detected"
|
||||
: `${component.notes.length} issues detected`}
|
||||
</span>
|
||||
</span>
|
||||
<span className="server-config-component-issue-action">Details</span>
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<hr style={{ borderColor: "var(--border)" }} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 6,
|
||||
maxHeight: "20px",
|
||||
padding: "0 0 24px 0",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<div className="server-config-component-actions">
|
||||
{component.availableActions.length > 0 ? (
|
||||
component.availableActions.map((action) => {
|
||||
const actionStyle = getWebStackActionStyle(action);
|
||||
@@ -192,6 +172,51 @@ export default function ServerConfigWebServerPanel({
|
||||
|
||||
return (
|
||||
<div style={{ display: "grid", gap: 16 }}>
|
||||
{activeIssueDetail && 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 }}
|
||||
>
|
||||
{activeIssueDetail.label} Issues
|
||||
</strong>
|
||||
<p
|
||||
style={{
|
||||
marginTop: 6,
|
||||
color: "var(--text-muted)",
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Service information returned by the current server
|
||||
snapshot.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveIssueDetail(null)}
|
||||
aria-label="Close issue details"
|
||||
className="server-config-issue-close"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="server-config-issue-list">
|
||||
{activeIssueDetail.notes.map((note) => (
|
||||
<div key={note} className="server-config-issue-note">
|
||||
{note}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
|
||||
<div className="card" style={{ padding: 18, display: "grid", gap: 14 }}>
|
||||
<div
|
||||
style={{
|
||||
@@ -303,22 +328,11 @@ export default function ServerConfigWebServerPanel({
|
||||
/>
|
||||
</div>
|
||||
{snapshot.webServer.notes.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: 6,
|
||||
padding: 12,
|
||||
borderRadius: 12,
|
||||
background: "rgba(15,23,42,0.18)",
|
||||
border: "1px solid rgba(148,163,184,0.14)",
|
||||
}}
|
||||
>
|
||||
<div className="server-config-readiness-notices">
|
||||
{snapshot.webServer.notes.map((note) => (
|
||||
<div
|
||||
key={note}
|
||||
style={{ color: "var(--text-muted)", fontSize: 12 }}
|
||||
>
|
||||
{note}
|
||||
<div key={note} className="server-config-readiness-notice">
|
||||
<AlertTriangle size={13} />
|
||||
<span>{note}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -437,13 +451,7 @@ export default function ServerConfigWebServerPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<div className="server-config-component-grid">
|
||||
{visibleComponents.map((component) => renderComponentCard(component))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user