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:
Juan Denis committed 2026-11-03 15:24:20 -05:00
1 parent c1a578fe9e
commit 2bd871e820
6 files changed
+79 -57

No files matched your search

+15 -13
View File
@@ -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.")}
+12 -10
View File
@@ -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">
+13 -13
View File
@@ -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">
+11 -9
View File
@@ -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">
+12 -12
View File
@@ -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; }
}