mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-10 05:53:05 +09:00
Keep in-page view switches in the page body on tab-group pages
Fleet, Config templates, Cloud provision, FTP and Status pages put their SegControl in the top bar, which crowded the group tabs. Move it to the top of the body (same pattern as Jobs) and keep only real actions in the bar.
This commit is contained in:
1 parent
c1a578fe9e
commit
2bd871e820
6 files changed
+79
-57
No files matched your search
@@ -107,19 +107,6 @@ const AgentFleet = () => {
|
||||
// re-registers on the values the node reads so spinner/counts stay in sync.
|
||||
useTopbarActions(() => (
|
||||
<>
|
||||
<SegControl
|
||||
aria-label={t('app.agentFleet.views', 'Fleet views')}
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
options={[
|
||||
{ value: 'dashboard', icon: <Activity size={16} />, label: t('app.agentFleet.dashboard', 'Dashboard') },
|
||||
{ value: 'versions', icon: <Package size={16} />, label: t('app.agentFleet.versions', 'Versions') },
|
||||
{ value: 'rollouts', icon: <Zap size={16} />, label: t('app.agentFleet.rollouts', 'Rollouts') },
|
||||
{ value: 'queue', icon: <Clock size={16} />, label: t('app.agentFleet.commandQueue', 'Command queue'), count: queuedCommands.length || undefined },
|
||||
{ value: 'discovery', icon: <Search size={16} />, label: t('app.agentFleet.discovery', 'Discovery') },
|
||||
{ value: 'approvals', icon: <Shield size={16} />, label: t('app.agentFleet.approvals', 'Approvals'), count: pendingServers.length || undefined },
|
||||
]}
|
||||
/>
|
||||
<Button size="sm" onClick={fetchData} disabled={loading}>
|
||||
<RefreshCw size={18} className={loading ? 'fleet-refresh-spinner' : ''} />
|
||||
{t('common.actions.refresh', 'Refresh')}
|
||||
@@ -474,6 +461,21 @@ const AgentFleet = () => {
|
||||
|
||||
return (
|
||||
<div className="sk-tabgroup__inner">
|
||||
<div className="sk-viewswitch">
|
||||
<SegControl
|
||||
aria-label={t('app.agentFleet.views', 'Fleet views')}
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
options={[
|
||||
{ value: 'dashboard', icon: <Activity size={16} />, label: t('app.agentFleet.dashboard', 'Dashboard') },
|
||||
{ value: 'versions', icon: <Package size={16} />, label: t('app.agentFleet.versions', 'Versions') },
|
||||
{ value: 'rollouts', icon: <Zap size={16} />, label: t('app.agentFleet.rollouts', 'Rollouts') },
|
||||
{ value: 'queue', icon: <Clock size={16} />, label: t('app.agentFleet.commandQueue', 'Command queue'), count: queuedCommands.length || undefined },
|
||||
{ value: 'discovery', icon: <Search size={16} />, label: t('app.agentFleet.discovery', 'Discovery') },
|
||||
{ value: 'approvals', icon: <Shield size={16} />, label: t('app.agentFleet.approvals', 'Approvals'), count: pendingServers.length || undefined },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{loadError && !tabHasData ? (
|
||||
<ErrorState
|
||||
title={t('app.agentFleet.couldntLoadFleetData', "Couldn't load fleet data.")}
|
||||
|
||||
@@ -58,16 +58,6 @@ const CloudProvision = () => {
|
||||
// Publish the admin actions to the shared tab-group top bar.
|
||||
useTopbarActions(() => (
|
||||
<>
|
||||
<SegControl
|
||||
aria-label={t('app.cloudProvision.views', 'Cloud views')}
|
||||
value={view}
|
||||
onChange={setView}
|
||||
options={[
|
||||
{ value: 'servers', label: t('common.labels.servers', 'Servers') },
|
||||
{ value: 'providers', label: t('app.cloudProvision.providers', 'Providers') },
|
||||
{ value: 'costs', label: t('app.cloudProvision.costs', 'Costs') },
|
||||
]}
|
||||
/>
|
||||
{user?.is_admin && (
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={() => setShowCreateProvider(true)}>{t('app.cloudProvision.addProvider', 'Add provider')}</Button>
|
||||
@@ -125,6 +115,18 @@ const CloudProvision = () => {
|
||||
|
||||
return (
|
||||
<div className="sk-tabgroup__inner cloud-provision-page">
|
||||
<div className="sk-viewswitch">
|
||||
<SegControl
|
||||
aria-label={t('app.cloudProvision.views', 'Cloud views')}
|
||||
value={view}
|
||||
onChange={setView}
|
||||
options={[
|
||||
{ value: 'servers', label: t('common.labels.servers', 'Servers') },
|
||||
{ value: 'providers', label: t('app.cloudProvision.providers', 'Providers') },
|
||||
{ value: 'costs', label: t('app.cloudProvision.costs', 'Costs') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<Tabs value={view} onValueChange={setView}>
|
||||
<TabsContent value="servers">
|
||||
<div className="cloud-servers-grid">
|
||||
|
||||
@@ -506,19 +506,6 @@ function FTPServer() {
|
||||
|
||||
useTopbarActions(() =>
|
||||
<>
|
||||
{isInstalled && (
|
||||
<SegControl
|
||||
aria-label={t('app.fTPServer.views', 'FTP views')}
|
||||
value={activeTab}
|
||||
onChange={selectTab}
|
||||
options={[
|
||||
{ value: 'overview', label: t('common.labels.overview', 'Overview') },
|
||||
{ value: 'users', label: t('app.fTPServer.users', 'Users') },
|
||||
{ value: 'connections', label: t('app.fTPServer.connections', 'Connections') },
|
||||
{ value: 'logs', label: t('common.labels.logs', 'Logs') },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{!isInstalled ? (
|
||||
<Button onClick={() => setShowInstallModal(true)}>
|
||||
{t('app.fTPServer.installFtpServer', 'Install FTP server')}
|
||||
@@ -620,6 +607,19 @@ function FTPServer() {
|
||||
/>
|
||||
</KpiBand>
|
||||
|
||||
<div className="sk-viewswitch">
|
||||
<SegControl
|
||||
aria-label={t('app.fTPServer.views', 'FTP views')}
|
||||
value={activeTab}
|
||||
onChange={selectTab}
|
||||
options={[
|
||||
{ value: 'overview', label: t('common.labels.overview', 'Overview') },
|
||||
{ value: 'users', label: t('app.fTPServer.users', 'Users') },
|
||||
{ value: 'connections', label: t('app.fTPServer.connections', 'Connections') },
|
||||
{ value: 'logs', label: t('common.labels.logs', 'Logs') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={selectTab}>
|
||||
<TabsContent value="overview">
|
||||
<div className="overview-tab">
|
||||
|
||||
@@ -340,15 +340,6 @@ const ServerTemplates = () => {
|
||||
// on a fresh panel Templates is empty; the default follows the data.
|
||||
useTopbarActions(() => (
|
||||
<>
|
||||
<SegControl
|
||||
aria-label={t('app.serverTemplates.views', 'Template views')}
|
||||
value={activeTab || 'library'}
|
||||
onChange={setActiveTab}
|
||||
options={[
|
||||
{ value: 'library', label: t('app.serverTemplates.library', 'Library') },
|
||||
{ value: 'templates', label: t('app.serverTemplates.templates', 'Templates'), count: templates.length || undefined },
|
||||
]}
|
||||
/>
|
||||
{user?.is_admin && (
|
||||
<Button size="sm" onClick={() => setShowCreateModal(true)}>{t('app.serverTemplates.createTemplate', 'Create template')}</Button>
|
||||
)}
|
||||
@@ -566,6 +557,17 @@ const ServerTemplates = () => {
|
||||
|
||||
return (
|
||||
<div className="sk-tabgroup__inner server-templates-page">
|
||||
<div className="sk-viewswitch">
|
||||
<SegControl
|
||||
aria-label={t('app.serverTemplates.views', 'Template views')}
|
||||
value={activeTab || 'library'}
|
||||
onChange={setActiveTab}
|
||||
options={[
|
||||
{ value: 'library', label: t('app.serverTemplates.library', 'Library') },
|
||||
{ value: 'templates', label: t('app.serverTemplates.templates', 'Templates'), count: templates.length || undefined },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{topbarChrome}
|
||||
<Tabs value={activeTab || 'library'} onValueChange={setActiveTab}>
|
||||
<TabsContent value="templates">
|
||||
|
||||
@@ -332,18 +332,6 @@ const StatusPages = () => {
|
||||
useTopbarActions(() =>
|
||||
(
|
||||
<>
|
||||
{selectedPage && (
|
||||
<SegControl
|
||||
aria-label={t('app.statusPages.views', 'Status page views')}
|
||||
value={detailTab}
|
||||
onChange={setDetailTab}
|
||||
options={[
|
||||
{ value: 'components', label: t('app.statusPages.components', 'Components') },
|
||||
{ value: 'incidents', label: t('app.statusPages.incidents', 'Incidents') },
|
||||
{ value: 'settings', label: t('app.statusPages.page', 'Page') },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Button size="sm" variant="outline" onClick={loadPages}>
|
||||
<RefreshCw size={16} />
|
||||
{t('common.actions.refresh', 'Refresh')}
|
||||
@@ -436,6 +424,18 @@ const StatusPages = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sk-viewswitch">
|
||||
<SegControl
|
||||
aria-label={t('app.statusPages.views', 'Status page views')}
|
||||
value={detailTab}
|
||||
onChange={setDetailTab}
|
||||
options={[
|
||||
{ value: 'components', label: t('app.statusPages.components', 'Components') },
|
||||
{ value: 'incidents', label: t('app.statusPages.incidents', 'Incidents') },
|
||||
{ value: 'settings', label: t('app.statusPages.page', 'Page') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<Tabs value={detailTab} onValueChange={setDetailTab}>
|
||||
<TabsContent value="components">
|
||||
<div className="status-actions-bar">
|
||||
|
||||
@@ -2037,3 +2037,19 @@ $sk-tones: ('accent', 'green', 'cyan', 'amber', 'red', 'violet');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In-page view switch for a tab-group child ("Dashboard / Versions / …" on one
|
||||
// page): the group's bar owns the tab row, so the page's own sub-views are a
|
||||
// SegControl at the top of the body — the same pattern Jobs uses. Scrolls
|
||||
// sideways rather than wrapping when the views don't fit a phone.
|
||||
.sk-viewswitch {
|
||||
display: flex;
|
||||
margin-bottom: $space-4;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
|
||||
.sk-seg { flex-shrink: 0; }
|
||||
}
|
||||
Reference in new issue
Block a user