Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions backend/internal/models/customize.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type CustomizeItem struct {
TemplateValidation CustomizeVariable `key:"templateValidation" meta:"label=Template Validation;type=boolean;keywords=validation,check,verify,lint,syntax,schema;category=templates;description=Enable validation for template files"`

// Registries category
ContainerRegistries CustomizeVariable `key:"containerRegistries" meta:"label=Container Registries;type=array;keywords=registry,docker,images,hub,private,authentication,credentials;category=registries;description=Manage container registry connections" catmeta:"id=registries;title=Registries;icon=package;url=/customize/registries;description=Configure container registries and authentication"`
ContainerRegistries CustomizeVariable `key:"containerRegistries" meta:"label=Container Registries;type=array;keywords=registry,docker,images,hub,private,authentication,credentials;category=registries;description=Manage container registry connections" catmeta:"id=registries;title=Container Registries;icon=package;url=/customize/registries;description=Configure container registries and authentication"`
RegistryCredentials CustomizeVariable `key:"registryCredentials" meta:"label=Registry Credentials;type=secure;keywords=credentials,auth,username,password,token,login,security;category=registries;description=Configure authentication for container registries"`
RegistryMirrors CustomizeVariable `key:"registryMirrors" meta:"label=Registry Mirrors;type=array;keywords=mirrors,proxy,cache,performance,cdn,regional;category=registries;description=Configure registry mirrors and proxies"`

Expand All @@ -24,10 +24,8 @@ type CustomizeItem struct {
SecretVariables CustomizeVariable `key:"secretVariables" meta:"label=Secret Variables;type=secure;keywords=secrets,sensitive,secure,encrypted,password,api,key;category=variables;description=Manage sensitive and encrypted variables"`
VariableTemplates CustomizeVariable `key:"variableTemplates" meta:"label=Variable Templates;type=array;keywords=templates,reusable,preset,configuration,standard,common;category=variables;description=Create reusable variable configurations"`

// Git Repositories category
GitRepositories CustomizeVariable `key:"gitRepositories" meta:"label=Git Repositories;type=array;keywords=git,repository,repositories,source,code,version,control,github,gitlab,bitbucket;category=git-repositories;description=Manage git repository connections for GitOps" catmeta:"id=git-repositories;title=Git Repositories;icon=git-branch;url=/customize/git-repositories;description=Configure git repositories for Git synchronization"`
GitRepositoryDefaults CustomizeVariable `key:"gitRepositoryDefaults" meta:"label=Repository Defaults;type=object;keywords=defaults,settings,configuration,branch,auth,authentication;category=git-repositories;description=Set default settings for git repositories"`
GitRepositoryTemplates CustomizeVariable `key:"gitRepositoryTemplates" meta:"label=Repository Templates;type=array;keywords=templates,presets,common,reusable,standard;category=git-repositories;description=Create reusable repository configurations"`
// Git repositories category
GitRepositories CustomizeVariable `key:"gitRepositories" meta:"label=Git Repositories;type=array;keywords=git,repositories,repo,source,code,version,control,clone,ssh,http;category=git-repositories;description=Manage connected git repository sources" catmeta:"id=git-repositories;title=Git Repositories;icon=git-branch;url=/customize/git-repositories;description=Connect and manage git repositories for use in projects and deployments"`
}

type CustomizeVariable struct {
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/models/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ type Settings struct {
// API Keys category (admin management page - no actual settings)
ApiKeysCategoryPlaceholder SettingVariable `key:"apiKeysCategory,internal" meta:"label=API Keys;type=internal;keywords=api,keys,tokens,authentication,access,programmatic,integration;category=apikeys;description=Manage API keys for programmatic access" catmeta:"id=apikeys;title=API Keys;icon=apikey;url=/settings/api-keys;description=Create and manage API keys for programmatic access to Arcane"`

// Environments category (environment-scoped settings page)
EnvironmentsCategoryPlaceholder SettingVariable `key:"environmentsCategory,internal" meta:"label=Environments;type=internal;keywords=environments,docker,remote,agent,connection,settings,configuration,gitops;category=environments;description=Manage environment-specific settings and connection behavior" catmeta:"id=environments;title=Environments;icon=environment;url=/settings/environments;description=Manage environment-specific Docker settings, connections, security, jobs, and agent behavior"`

// Timeout category
DockerAPITimeout SettingVariable `key:"dockerApiTimeout,envOverride" meta:"label=Docker API Timeout;type=number;keywords=docker,api,timeout,seconds,list,operations;category=timeouts;description=Timeout for Docker list operations in seconds (default: 30)" catmeta:"id=timeouts;title=Timeouts;icon=clock;url=/settings/timeouts;description=Configure operation timeouts for slow networks or hardware"`
DockerImagePullTimeout SettingVariable `key:"dockerImagePullTimeout,envOverride" meta:"label=Docker Image Pull Timeout;type=number;keywords=docker,image,pull,timeout,seconds,download;category=timeouts;description=Timeout for Docker image pulls in seconds (default: 600 = 10 minutes)"`
Expand Down
8 changes: 4 additions & 4 deletions docker/compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ services:
- ${HOST_PROJECT_ROOT:-..}/backend:/app/backend:cached
- ${HOST_PROJECT_ROOT:-..}/backend/.air.toml:/app/backend/.air.toml:ro
- /var/run/docker.sock:/var/run/docker.sock
- arcane-dev-data:/app/backend/data
- arcane-dev-data:/app/data
- arcane-builds:/builds
- go-build-cache:/go/cache
- go-mod-cache:/go/pkg/mod
Expand All @@ -63,7 +63,7 @@ services:
- PGID=1000
- ENCRYPTION_KEY=dev-encryption-key-replace-in-production-must-be-32-chars
- JWT_SECRET=dev-jwt-secret-replace-in-production-must-be-long-enough
- DATABASE_URL=file:data/arcane.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate
- DATABASE_URL=file:/app/data/arcane.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate
working_dir: /app/backend
networks:
- arcane-dev
Expand Down Expand Up @@ -101,10 +101,10 @@ services:
- EDGE_TRANSPORT=websocket
- ENCRYPTION_KEY=dev-encryption-key-replace-in-production-must-be-32-chars
- JWT_SECRET=dev-jwt-secret-replace-in-production-must-be-long-enough
- DATABASE_URL=file:data/arcane.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate
- DATABASE_URL=file:/app/data/arcane.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- agent-dev-data:/app/backend/data
- agent-dev-data:/app/data
- arcane-builds:/builds
- ${HOST_PROJECT_ROOT:-..}/go.work:/app/go.work:ro
- ${HOST_PROJECT_ROOT:-..}/types:/app/types:cached
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1841,6 +1841,7 @@
"sidebar_management": "Management",
"sidebar_resources": "Resources",
"sidebar_administration": "Administration",
"sidebar_gitops": "GitOps",
"sidebar_settings": "Settings",
"_comment_quick_actions": "=== QUICK ACTIONS & PROGRESS ===",
"quick_actions_title": "Quick Actions",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@

function handleOpenSettings(env: Environment) {
closeDialog();
goto(`/environments/${env.id}`);
goto(`/settings/environments?environment=${env.id}`);
}

function getConnectionString(env: Environment): string {
Expand Down
111 changes: 62 additions & 49 deletions frontend/src/lib/components/mobile-nav/mobile-nav-sheet.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { navigationItems, getBuildAndDeploymentItems } from '$lib/config/navigation-config';
import { navigationItems, getGitOpsItems } from '$lib/config/navigation-config';
import type { NavigationItem } from '$lib/config/navigation-config';
import { cn } from '$lib/utils';
import { page } from '$app/state';
Expand Down Expand Up @@ -42,7 +42,7 @@
const currentPath = $derived(page.url.pathname);
const memoizedUser = $derived.by(() => user ?? storeUser);
const currentEnvId = $derived(environmentStore.selected?.id || '0');
const buildDeploymentItems = $derived(getBuildAndDeploymentItems(currentEnvId));
const gitOpsItems = $derived(getGitOpsItems(currentEnvId));

let upgrading = $state(false);
let showConfirmDialog = $state(false);
Expand Down Expand Up @@ -152,22 +152,63 @@
</h4>
<div class="space-y-2">
{#each navigationItems.managementItems as item (item.url)}
{@const IconComponent = item.icon}
<a
href={item.url}
onclick={handleItemClick}
class={cn(
'flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium transition-all duration-200 ease-out',
'focus-visible:ring-muted-foreground/50 hover:scale-[1.01] focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:ring-offset-transparent',
isActiveItem(item)
? 'bg-muted text-foreground hover:bg-muted/70 shadow-sm'
: 'text-foreground hover:bg-muted/50'
)}
aria-current={isActiveItem(item) ? 'page' : undefined}
>
<IconComponent size={20} />
<span>{item.title}</span>
</a>
{#if item.items}
{@const IconComponent = item.icon}
<div class="space-y-2">
<a
href={item.url}
onclick={handleItemClick}
class={cn(
'flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium transition-all duration-200 ease-out',
'focus-visible:ring-muted-foreground/50 hover:scale-[1.01] focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:ring-offset-transparent',
isActiveItem(item)
? 'bg-muted text-foreground hover:bg-muted/70 shadow-sm'
: 'text-foreground hover:bg-muted/50'
)}
aria-current={isActiveItem(item) ? 'page' : undefined}
>
<IconComponent size={20} />
<span>{item.title}</span>
</a>
<div class="ml-6 space-y-1">
{#each item.items as subItem (subItem.url)}
{@const SubIconComponent = subItem.icon}
<a
href={subItem.url}
onclick={handleItemClick}
class={cn(
'flex items-center gap-3 rounded-xl px-4 py-2 text-sm transition-all duration-200 ease-out',
'focus-visible:ring-muted-foreground/50 hover:scale-[1.01] focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:ring-offset-transparent',
isActiveItem(subItem)
? 'bg-muted/70 text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
)}
aria-current={isActiveItem(subItem) ? 'page' : undefined}
>
<SubIconComponent size={16} />
<span>{subItem.title}</span>
</a>
{/each}
</div>
</div>
{:else}
{@const IconComponent = item.icon}
<a
href={item.url}
onclick={handleItemClick}
class={cn(
'flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium transition-all duration-200 ease-out',
'focus-visible:ring-muted-foreground/50 hover:scale-[1.01] focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:ring-offset-transparent',
isActiveItem(item)
? 'bg-muted text-foreground hover:bg-muted/70 shadow-sm'
: 'text-foreground hover:bg-muted/50'
)}
aria-current={isActiveItem(item) ? 'page' : undefined}
>
<IconComponent size={20} />
<span>{item.title}</span>
</a>
{/if}
{/each}
</div>
</section>
Expand Down Expand Up @@ -240,37 +281,9 @@
</section>

<section>
<h4 class="text-muted-foreground/70 mb-4 px-3 text-[11px] font-bold tracking-widest uppercase">
{m.builds_and_deployments()}
</h4>
<div class="space-y-2">
{#each buildDeploymentItems as item (item.url)}
{@const IconComponent = item.icon}
<a
href={item.url}
onclick={handleItemClick}
class={cn(
'flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium transition-all duration-200 ease-out',
'focus-visible:ring-muted-foreground/50 hover:scale-[1.01] focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:ring-offset-transparent',
isActiveItem(item)
? 'bg-muted text-foreground hover:bg-muted/70 shadow-sm'
: 'text-foreground hover:bg-muted/50'
)}
aria-current={isActiveItem(item) ? 'page' : undefined}
>
<IconComponent size={20} />
<span>{item.title}</span>
</a>
{/each}
</div>
</section>

<section>
<h4 class="text-muted-foreground/70 mb-4 px-3 text-[11px] font-bold tracking-widest uppercase">
{m.security_title()}
</h4>
<h4 class="text-muted-foreground/70 mb-4 px-3 text-[11px] font-bold tracking-widest uppercase">{m.sidebar_gitops()}</h4>
<div class="space-y-2">
{#each navigationItems.securityItems as item (item.url)}
{#each gitOpsItems as item (item.url)}
{@const IconComponent = item.icon}
<a
href={item.url}
Expand All @@ -297,7 +310,7 @@
{m.swarm_title()}
</h4>
<div class="space-y-2">
{#each swarmItems as item}
{#each swarmItems as item (item.url)}
{@const IconComponent = item.icon}
<a
href={item.url}
Expand Down
71 changes: 36 additions & 35 deletions frontend/src/lib/components/sidebar/sidebar-itemgroup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
}

let openStates = $state<Record<string, boolean>>({});
let hoveredGroup = $state<string | null>(null);

const enhancedItems = $derived(
items.map((item) => {
Expand All @@ -62,9 +63,6 @@
);

function getIsOpen(itemTitle: string, isActive: boolean): boolean {
if (sidebar.hoverExpansionEnabled) {
return isActive;
}
if (openStates[itemTitle] === undefined) {
return isActive;
}
Expand All @@ -82,45 +80,50 @@
{#each enhancedItems as item (item.title)}
{#if (item.items?.length ?? 0) > 0}
{#if sidebar.state === 'collapsed' && !sidebar.hoverExpansionEnabled}
<!-- In collapsed mode without hover expansion, show parent and children as separate icon buttons -->
{#snippet tooltipContent()}
<SidebarItemTooltipContent title={item.title} shortcut={item.shortcut} includeTitle={true} />
{/snippet}
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={item.isActive} {tooltipContent}>
{#snippet child({ props })}
{@const Icon = item.icon}
<a href={item.url} {...props}>
{#if item.icon}
<Icon />
{/if}
<span>{item.title}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
<!-- Separator before sub-items -->
<div class="flex justify-center px-2 py-1">
<Sidebar.Separator class="my-0 w-6" />
</div>
{#each item.items ?? [] as subItem (subItem.title)}
{#snippet subItemTooltipContent()}
<SidebarItemTooltipContent title={subItem.title} shortcut={subItem.shortcut} includeTitle={true} />
{/snippet}
{@const groupExpanded = hoveredGroup === item.title}
<div
class={['rounded-lg transition-colors duration-150', groupExpanded && 'bg-sidebar-accent/40 py-0.5']}
role="group"
onmouseenter={() => (hoveredGroup = item.title)}
onmouseleave={() => (hoveredGroup = null)}
>
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={subItem.isActive} tooltipContent={subItemTooltipContent}>
<Sidebar.MenuButton isActive={item.isActive} {tooltipContent}>
{#snippet child({ props })}
{@const SubIcon = subItem.icon}
<a href={subItem.url} {...props}>
{#if subItem.icon}
<SubIcon />
{@const Icon = item.icon}
<a href={item.url} {...props}>
{#if item.icon}
<Icon />
{/if}
<span>{subItem.title}</span>
<span>{item.title}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
{#if groupExpanded}
{#each item.items ?? [] as subItem (subItem.title)}
{#snippet subItemTooltipContent()}
<SidebarItemTooltipContent title={subItem.title} shortcut={subItem.shortcut} includeTitle={true} />
{/snippet}
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={subItem.isActive} tooltipContent={subItemTooltipContent}>
{#snippet child({ props })}
{@const SubIcon = subItem.icon}
<a href={subItem.url} {...props}>
{#if subItem.icon}
<SubIcon />
{/if}
<span>{subItem.title}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
{/if}
</div>
{:else}
{#snippet collapsibleSubMenu()}
<Collapsible.Content>
Expand Down Expand Up @@ -153,9 +156,7 @@
{includeTitleInTooltip}
{getIsOpen}
onOpenChange={(open) => {
if (!sidebar.hoverExpansionEnabled) {
openStates[item.title] = open;
}
openStates[item.title] = open;
}}
content={collapsibleSubMenu}
/>
Expand Down
19 changes: 4 additions & 15 deletions frontend/src/lib/components/sidebar/sidebar.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" module>
import { navigationItems, getBuildAndDeploymentItems, getSwarmNavigationItems } from '$lib/config/navigation-config';
import { navigationItems, getGitOpsItems, getSwarmNavigationItems } from '$lib/config/navigation-config';
</script>

<script lang="ts">
Expand Down Expand Up @@ -54,18 +54,8 @@
const isAdmin = $derived(!!effectiveUser?.roles?.includes('admin'));
let envSwitcherOpen = $state(false);

// Filter out sub-items for settings on desktop since we have a dedicated settings sidebar
const desktopSettingsItems =
navigationItems.settingsItems?.map((item) => {
if (item.url === '/settings') {
const { items, ...rest } = item;
return rest;
}
return item;
}) ?? [];

const currentEnvId = $derived(environmentStore.selected?.id || '0');
const buildDeploymentItems = $derived(getBuildAndDeploymentItems(currentEnvId));
const gitOpsItems = $derived(getGitOpsItems(currentEnvId));
const swarmItems = $derived(getSwarmNavigationItems(swarmEnabled));
</script>

Expand Down Expand Up @@ -103,13 +93,12 @@
<Sidebar.Content class={!isCollapsed ? '-mt-2' : ''}>
<SidebarItemGroup label={m.sidebar_management()} items={navigationItems.managementItems} />
<SidebarItemGroup label={m.sidebar_resources()} items={navigationItems.resourceItems} />
<SidebarItemGroup label={m.builds_and_deployments()} items={buildDeploymentItems} />
<SidebarItemGroup label={m.sidebar_gitops()} items={gitOpsItems} />
{#if swarmItems.length > 0}
<SidebarItemGroup label={m.swarm_title()} items={swarmItems} />
{/if}
<SidebarItemGroup label={m.security_title()} items={navigationItems.securityItems} />
{#if isAdmin}
<SidebarItemGroup label={m.sidebar_administration()} items={desktopSettingsItems} />
<SidebarItemGroup label={m.sidebar_administration()} items={navigationItems.settingsItems} />
{/if}
</Sidebar.Content>
<Sidebar.Footer>
Expand Down
Loading
Loading