feat: add "+ App" shortcut button to sidebar Applications header
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m20s
CI / docker (push) Successful in 1m12s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s

Adds a subtle "+ App" button in the sidebar section header for quick
app creation without navigating to the Deployments tab first. Only
visible to OPERATOR and ADMIN roles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-14 09:10:41 +02:00
parent ac680b7f3f
commit 199d0259cd
2 changed files with 59 additions and 19 deletions

View File

@@ -43,6 +43,34 @@
opacity: 0.6; opacity: 0.6;
} }
.appSectionWrap {
position: relative;
}
.addAppBtn {
position: absolute;
top: 6px;
right: 6px;
z-index: 1;
display: flex;
align-items: center;
gap: 2px;
background: none;
border: none;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
color: var(--sidebar-muted);
cursor: pointer;
transition: color 0.12s, background 0.12s;
}
.addAppBtn:hover {
color: var(--amber);
background: rgba(255, 255, 255, 0.06);
}
.mainContent { .mainContent {
flex: 1; flex: 1;
display: flex; display: flex;

View File

@@ -21,7 +21,7 @@ import {
} from '@cameleer/design-system'; } from '@cameleer/design-system';
import type { SearchResult, SidebarTreeNode, DropdownItem, ButtonGroupItem, ExchangeStatus } from '@cameleer/design-system'; import type { SearchResult, SidebarTreeNode, DropdownItem, ButtonGroupItem, ExchangeStatus } from '@cameleer/design-system';
import sidebarLogo from '@cameleer/design-system/assets/cameleer3-logo.svg'; import sidebarLogo from '@cameleer/design-system/assets/cameleer3-logo.svg';
import { Box, Settings, FileText, ChevronRight, Square, Pause, Star, X, User } from 'lucide-react'; import { Box, Settings, FileText, ChevronRight, Square, Pause, Star, X, User, Plus } from 'lucide-react';
import { AboutMeDialog } from './AboutMeDialog'; import { AboutMeDialog } from './AboutMeDialog';
import css from './LayoutShell.module.css'; import css from './LayoutShell.module.css';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
@@ -31,7 +31,7 @@ import { useSearchExecutions, useAttributeKeys } from '../api/queries/executions
import { useUsers, useGroups, useRoles } from '../api/queries/admin/rbac'; import { useUsers, useGroups, useRoles } from '../api/queries/admin/rbac';
import { useEnvironments } from '../api/queries/admin/environments'; import { useEnvironments } from '../api/queries/admin/environments';
import type { UserDetail, GroupDetail, RoleDetail } from '../api/queries/admin/rbac'; import type { UserDetail, GroupDetail, RoleDetail } from '../api/queries/admin/rbac';
import { useAuthStore, useIsAdmin } from '../auth/auth-store'; import { useAuthStore, useIsAdmin, useCanControl } from '../auth/auth-store';
import { useEnvironmentStore } from '../api/environment-store'; import { useEnvironmentStore } from '../api/environment-store';
import { useState, useMemo, useCallback, useEffect, useRef, createElement } from 'react'; import { useState, useMemo, useCallback, useEffect, useRef, createElement } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@@ -296,6 +296,7 @@ function LayoutContent() {
// --- Role checks ---------------------------------------------------- // --- Role checks ----------------------------------------------------
const isAdmin = useIsAdmin(); const isAdmin = useIsAdmin();
const canControl = useCanControl();
// --- Environment filtering ----------------------------------------- // --- Environment filtering -----------------------------------------
const selectedEnv = useEnvironmentStore((s) => s.environment); const selectedEnv = useEnvironmentStore((s) => s.environment);
@@ -692,23 +693,34 @@ function LayoutContent() {
/> />
{/* Applications section */} {/* Applications section */}
<Sidebar.Section <div className={css.appSectionWrap}>
icon={createElement(Box, { size: 16 })} {canControl && (
label="Applications" <button
open={appsOpen} className={css.addAppBtn}
onToggle={toggleApps} onClick={(e) => { e.stopPropagation(); navigate('/apps/new'); }}
> title="Create App"
<SidebarTree >
nodes={appTreeNodes} <Plus size={12} /> App
selectedPath={effectiveSelectedPath} </button>
isStarred={isStarred} )}
onToggleStar={toggleStar} <Sidebar.Section
filterQuery={filterQuery} icon={createElement(Box, { size: 16 })}
persistKey="apps" label="Applications"
autoRevealPath={sidebarRevealPath} open={appsOpen}
onNavigate={handleSidebarNavigate} onToggle={toggleApps}
/> >
</Sidebar.Section> <SidebarTree
nodes={appTreeNodes}
selectedPath={effectiveSelectedPath}
isStarred={isStarred}
onToggleStar={toggleStar}
filterQuery={filterQuery}
persistKey="apps"
autoRevealPath={sidebarRevealPath}
onNavigate={handleSidebarNavigate}
/>
</Sidebar.Section>
</div>
{/* Starred section — only when there are starred items */} {/* Starred section — only when there are starred items */}
{starredItems.length > 0 && ( {starredItems.length > 0 && (