mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-30 00:27:11 +09:00
redesign(services): WordPress-style grouped Settings sub-nav
Restructure the service Settings tab from a flat stack into a grouped left sub-nav + content pane (.svc-settings grid), mirroring the WordPress detail page: General -> Environment, Connections -> Repository, Advanced -> Danger Zone, each with an icon. Section is local state; the existing panels are unchanged, only reorganized. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6fb0454ac6
commit
e46c0d685e
2 files changed
+195
-87
No files matched your search
@@ -1,14 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SlidersHorizontal, GitBranch, AlertTriangle } from 'lucide-react';
|
||||
import api from '../../services/api';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import { DangerZone } from '../DangerZone';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select';
|
||||
|
||||
// Grouped left sub-nav for the service Settings tab — mirrors the WordPress
|
||||
// detail page's settings layout: an uppercase mono group label per section with
|
||||
// the existing setting panels on the right. Groups give it structure (and room
|
||||
// to grow) instead of one long flat stack.
|
||||
const SVC_SETTINGS_GROUPS = [
|
||||
{ label: 'General', items: [{ id: 'environment', label: 'Environment', icon: SlidersHorizontal }] },
|
||||
{ label: 'Connections', items: [{ id: 'repository', label: 'Repository', icon: GitBranch }] },
|
||||
{ label: 'Advanced', items: [{ id: 'danger', label: 'Danger Zone', icon: AlertTriangle }] },
|
||||
];
|
||||
|
||||
const SettingsTab = ({ app, deployConfig, onUpdate, onOpenGitModal }) => {
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [section, setSection] = useState('environment');
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [environmentType, setEnvironmentType] = useState(app.environment_type || 'standalone');
|
||||
const [savingEnvironment, setSavingEnvironment] = useState(false);
|
||||
@@ -67,100 +79,127 @@ const SettingsTab = ({ app, deployConfig, onUpdate, onOpenGitModal }) => {
|
||||
|
||||
return (
|
||||
<div className="svc-settings">
|
||||
{/* Repository Configuration */}
|
||||
<div className="svc-settings__section">
|
||||
<h3 className="svc-settings__section-title">Repository</h3>
|
||||
<div className="card">
|
||||
<div className="settings-row">
|
||||
<div className="settings-label">
|
||||
<span>Connected Repository</span>
|
||||
<span className="settings-hint">
|
||||
{deployConfig
|
||||
? `${deployConfig.repo_url} (${deployConfig.branch || 'main'})`
|
||||
: 'No repository connected'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="settings-control">
|
||||
<Button variant="outline" onClick={onOpenGitModal}>
|
||||
{deployConfig ? 'Edit' : 'Connect'}
|
||||
</Button>
|
||||
</div>
|
||||
<nav className="svc-settings__nav" aria-label="Service settings sections">
|
||||
{SVC_SETTINGS_GROUPS.map(g => (
|
||||
<div className="svc-settings__group" key={g.label}>
|
||||
<div className="svc-settings__grouplabel">{g.label}</div>
|
||||
{g.items.map(s => (
|
||||
<button
|
||||
type="button"
|
||||
key={s.id}
|
||||
className={`svc-settings__navitem ${section === s.id ? 'is-active' : ''}`}
|
||||
onClick={() => setSection(s.id)}
|
||||
>
|
||||
<s.icon size={15} />
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Environment Configuration */}
|
||||
<div className="svc-settings__section">
|
||||
<h3 className="svc-settings__section-title">Environment</h3>
|
||||
<div className="card settings-section">
|
||||
<div className="settings-row">
|
||||
<div className="settings-label">
|
||||
<span>Environment Type</span>
|
||||
<span className="settings-hint">
|
||||
{app.has_linked_app
|
||||
? 'This app is linked. Unlink to change environment type.'
|
||||
: 'Set how this application is used in your workflow.'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="settings-control">
|
||||
{app.has_linked_app ? (
|
||||
<span className={`env-badge env-${app.environment_type}`}>
|
||||
{envLabels[app.environment_type] || app.environment_type}
|
||||
</span>
|
||||
) : (
|
||||
<Select
|
||||
value={environmentType}
|
||||
onValueChange={handleEnvironmentChange}
|
||||
disabled={savingEnvironment}
|
||||
>
|
||||
<SelectTrigger className="settings-select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="standalone">Standalone</SelectItem>
|
||||
<SelectItem value="development">Development</SelectItem>
|
||||
<SelectItem value="staging">Staging</SelectItem>
|
||||
<SelectItem value="production">Production</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="svc-settings__content">
|
||||
{/* Environment Configuration */}
|
||||
{section === 'environment' && (
|
||||
<div className="svc-settings__section">
|
||||
<h3 className="svc-settings__section-title">Environment</h3>
|
||||
<div className="card settings-section">
|
||||
<div className="settings-row">
|
||||
<div className="settings-label">
|
||||
<span>Environment Type</span>
|
||||
<span className="settings-hint">
|
||||
{app.has_linked_app
|
||||
? 'This app is linked. Unlink to change environment type.'
|
||||
: 'Set how this application is used in your workflow.'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="settings-control">
|
||||
{app.has_linked_app ? (
|
||||
<span className={`env-badge env-${app.environment_type}`}>
|
||||
{envLabels[app.environment_type] || app.environment_type}
|
||||
</span>
|
||||
) : (
|
||||
<Select
|
||||
value={environmentType}
|
||||
onValueChange={handleEnvironmentChange}
|
||||
disabled={savingEnvironment}
|
||||
>
|
||||
<SelectTrigger className="settings-select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="standalone">Standalone</SelectItem>
|
||||
<SelectItem value="development">Development</SelectItem>
|
||||
<SelectItem value="staging">Staging</SelectItem>
|
||||
<SelectItem value="production">Production</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{savingEnvironment && <span className="settings-saving">Saving...</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{app.has_linked_app && (
|
||||
<div className="settings-row">
|
||||
<div className="settings-label">
|
||||
<span>Linked Application</span>
|
||||
<span className="settings-hint">
|
||||
Unlinking will reset both apps to standalone mode.
|
||||
</span>
|
||||
</div>
|
||||
<div className="settings-control">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleUnlink}
|
||||
disabled={unlinking}
|
||||
>
|
||||
{unlinking ? 'Unlinking...' : 'Unlink'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{savingEnvironment && <span className="settings-saving">Saving...</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{app.has_linked_app && (
|
||||
<div className="settings-row">
|
||||
<div className="settings-label">
|
||||
<span>Linked Application</span>
|
||||
<span className="settings-hint">
|
||||
Unlinking will reset both apps to standalone mode.
|
||||
</span>
|
||||
</div>
|
||||
<div className="settings-control">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleUnlink}
|
||||
disabled={unlinking}
|
||||
>
|
||||
{unlinking ? 'Unlinking...' : 'Unlink'}
|
||||
</Button>
|
||||
{/* Repository Configuration */}
|
||||
{section === 'repository' && (
|
||||
<div className="svc-settings__section">
|
||||
<h3 className="svc-settings__section-title">Repository</h3>
|
||||
<div className="card">
|
||||
<div className="settings-row">
|
||||
<div className="settings-label">
|
||||
<span>Connected Repository</span>
|
||||
<span className="settings-hint">
|
||||
{deployConfig
|
||||
? `${deployConfig.repo_url} (${deployConfig.branch || 'main'})`
|
||||
: 'No repository connected'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="settings-control">
|
||||
<Button variant="outline" onClick={onOpenGitModal}>
|
||||
{deployConfig ? 'Edit' : 'Connect'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Danger Zone */}
|
||||
<div className="svc-settings__section">
|
||||
<h3 className="svc-settings__section-title">Danger Zone</h3>
|
||||
<DangerZone
|
||||
description="Once you delete a service, there is no going back. All data will be permanently removed."
|
||||
action={
|
||||
<Button variant="destructive" onClick={handleDelete} disabled={deleting}>
|
||||
{deleting ? 'Deleting...' : 'Delete Service'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{/* Danger Zone */}
|
||||
{section === 'danger' && (
|
||||
<div className="svc-settings__section">
|
||||
<h3 className="svc-settings__section-title">Danger Zone</h3>
|
||||
<DangerZone
|
||||
description="Once you delete a service, there is no going back. All data will be permanently removed."
|
||||
action={
|
||||
<Button variant="destructive" onClick={handleDelete} disabled={deleting}>
|
||||
{deleting ? 'Deleting...' : 'Delete Service'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -719,12 +719,81 @@ h3.svc-eyebrow {
|
||||
}
|
||||
}
|
||||
|
||||
// Settings tab within service detail
|
||||
// Settings tab within service detail — grouped left sub-nav + content pane,
|
||||
// mirroring the WordPress detail page's settings layout (.wp-settings).
|
||||
.svc-settings {
|
||||
max-width: 820px;
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: $space-4;
|
||||
align-items: start;
|
||||
|
||||
@media (max-width: $breakpoint-md) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&__nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
position: sticky;
|
||||
top: $space-4;
|
||||
|
||||
@media (max-width: $breakpoint-md) {
|
||||
position: static;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
// Grouped left nav — an uppercase mono label per group.
|
||||
&__group + &__group {
|
||||
margin-top: $space-4;
|
||||
}
|
||||
|
||||
&__grouplabel {
|
||||
padding: 0 12px;
|
||||
margin-bottom: 6px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
font-weight: $font-weight-semibold;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
&__navitem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 9px 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: none;
|
||||
color: var(--text-dim);
|
||||
font-size: 13.5px;
|
||||
font-weight: $font-weight-medium;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 0.14s, color 0.14s;
|
||||
|
||||
svg { color: var(--text-faint); flex-shrink: 0; }
|
||||
|
||||
&:hover { background: var(--surface-hover); color: var(--text); }
|
||||
&.is-active {
|
||||
background: var(--accent-bg);
|
||||
color: var(--accent-bright);
|
||||
svg { color: var(--accent-bright); }
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__section {
|
||||
margin-bottom: $space-6;
|
||||
|
||||
&:last-child { margin-bottom: 0; }
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
|
||||
Reference in new issue
Block a user