Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
191 changes: 191 additions & 0 deletions cabotage/client/static/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,197 @@ a.live-commit-sha:hover {
letter-spacing: 0.05em;
}

/* Dashboard Resource Tree */
.dash-tree {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.dash-org {
border-radius: 0.75rem;
background: var(--surface-card);
border: 1px solid var(--border-color);
overflow: hidden;
}
.dash-org-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.875rem 1rem;
text-decoration: none;
transition: background 0.15s ease;
}
.dash-org-header:hover {
background: var(--hover-overlay);
}
.dash-org-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 0.5rem;
background: oklch(from var(--color-primary) l c h / 0.1);
color: var(--color-primary);
flex-shrink: 0;
}
.dash-org-name {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-bright);
}
.dash-org-slug {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.6875rem;
color: var(--text-faint);
margin-left: 0.5rem;
}
.dash-org-meta {
font-size: 0.6875rem;
color: var(--text-faint);
flex-shrink: 0;
}

.dash-projects {
border-top: 1px solid var(--border-color-subtle);
}
.dash-project {
border-bottom: 1px solid var(--border-color-subtle);
}
.dash-project:last-child {
border-bottom: none;
}
.dash-project-header {
padding: 0 1rem;
}
.dash-project-link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 0;
padding-left: 2.75rem;
text-decoration: none;
transition: background 0.1s ease;
}
.dash-project-link:hover .dash-project-name {
color: var(--text-bright);
}
.dash-project-icon {
color: var(--text-faint);
flex-shrink: 0;
}
.dash-project-name {
font-size: 0.8125rem;
font-weight: 500;
color: var(--text-muted);
transition: color 0.1s ease;
}
.dash-project-slug {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.6875rem;
color: var(--text-ghost);
}
.dash-count-pill {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.625rem;
font-weight: 600;
color: var(--text-faint);
background: var(--hover-overlay);
padding: 0.0625rem 0.375rem;
border-radius: 9999px;
margin-left: auto;
}

.dash-apps {
padding: 0.125rem 1rem 0.5rem;
padding-left: 4.25rem;
display: flex;
flex-direction: column;
gap: 0;
}
.dash-app {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.3125rem 0.5rem;
margin: 0 -0.5rem;
border-radius: 0.375rem;
text-decoration: none;
transition: background 0.1s ease;
}
.dash-app:hover {
background: var(--hover-overlay);
}
.dash-app-dot {
width: 0.375rem;
height: 0.375rem;
border-radius: 50%;
background: var(--accent-text);
opacity: 0.5;
flex-shrink: 0;
}
.dash-app:hover .dash-app-dot {
opacity: 1;
}
.dash-app-name {
font-size: 0.8125rem;
color: var(--text-muted);
transition: color 0.1s ease;
}
.dash-app:hover .dash-app-name {
color: var(--accent-text);
}
.dash-app-slug {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.625rem;
color: var(--text-ghost);
}
.dash-dot-deploying {
background: #3b82f6;
opacity: 1;
animation: dash-pulse 1.5s ease-in-out infinite;
}
.dash-dot-building {
background: #f59e0b;
opacity: 1;
animation: dash-pulse 1.5s ease-in-out infinite;
}
.dash-dot-error {
background: #ef4444;
opacity: 1;
}
@keyframes dash-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.dash-status-badge {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.5625rem;
font-weight: 600;
letter-spacing: 0.02em;
padding: 0.0625rem 0.375rem;
border-radius: 9999px;
margin-left: auto;
flex-shrink: 0;
text-transform: uppercase;
}
.dash-badge-deploying {
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
}
.dash-badge-building {
background: rgba(245, 158, 11, 0.15);
color: #fbbf24;
}
.dash-badge-error {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
}
.dash-empty-branch {
padding: 0.75rem 1rem 0.75rem 3.75rem;
border-top: 1px solid var(--border-color-subtle);
}

/* Process Card (legacy, see Process Grid below) */
.pod-dot {
width: 0.5rem;
Expand Down
131 changes: 77 additions & 54 deletions cabotage/client/templates/main/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,70 +21,93 @@ <h1 class="text-xl font-semibold text-base-content">Dashboard</h1>
</a>
</div>

{% set org_count = current_user.organizations|length if current_user.organizations else 0 %}
{% set org_count = user_organizations|length %}
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-8">
{{ stat_card(org_count, 'Organizations', 'building', 'primary') }}
{{ stat_card(project_count, 'Projects', 'folder', 'secondary') }}
{{ stat_card(app_count, 'Applications', 'box', 'accent') }}
{{ stat_card(deploy_count, 'Deployments', 'deploy', 'success') }}
</div>

<h2 class="text-sm font-medium text-base-content/40 uppercase tracking-wider mb-3">Quick Actions</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<a href="{{ url_for('user.organizations') }}"
class="service-card card-interactive block no-underline">
<div class="service-card-header">
<div class="service-card-icon bg-primary/10 text-primary">{{ icon('building', 'w-5 h-5') }}</div>
<div class="flex-1">
<span class="text-sm font-medium text-base-content">Organizations</span>
<p class="text-xs text-base-content/40">Manage teams and access</p>
</div>
<svg class="w-4 h-4 text-base-content/20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2">
<polyline points="9 18 15 12 9 6" />
</svg>
</div>
</a>
{% if user_organizations %}
<h2 class="text-sm font-medium text-base-content/40 uppercase tracking-wider mb-4">Your Resources</h2>

<a href="{{ url_for('user.projects') }}"
class="service-card card-interactive block no-underline">
<div class="service-card-header">
<div class="service-card-icon bg-secondary/10 text-secondary">{{ icon('folder', 'w-5 h-5') }}</div>
<div class="flex-1">
<span class="text-sm font-medium text-base-content">Projects</span>
<p class="text-xs text-base-content/40">View all projects</p>
</div>
<svg class="w-4 h-4 text-base-content/20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2">
<polyline points="9 18 15 12 9 6" />
</svg>
</div>
</a>
<div class="dash-tree">
{% for org in user_organizations %}
<div class="dash-org">
<a href="{{ url_for('user.organization', org_slug=org.slug) }}"
class="dash-org-header group">
<div class="dash-org-icon">{{ icon('building', 'w-4 h-4') }}</div>
<div class="flex-1 min-w-0">
<span class="dash-org-name">{{ org.name }}</span>
<span class="dash-org-slug">{{ org.slug }}</span>
</div>
<span class="dash-org-meta">{{ org.projects|length }} project{{ 's' if org.projects|length != 1 }}</span>
<svg class="w-4 h-4 text-base-content/20 group-hover:text-base-content/40 transition-colors flex-shrink-0"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6" />
</svg>
</a>

<a href="{{ url_for('user.organization_create') }}"
class="service-card card-interactive block no-underline border-dashed">
<div class="service-card-header">
<div class="service-card-icon bg-accent/10 text-accent">{{ icon('plus', 'w-5 h-5') }}</div>
<div class="flex-1">
<span class="text-sm font-medium text-base-content">New Organization</span>
<p class="text-xs text-base-content/40">Get started deploying</p>
{% if org.projects %}
<div class="dash-projects">
{% for project in org.projects|sort(attribute='name') %}
<div class="dash-project">
<div class="dash-project-header">
<a href="{{ url_for('user.project', org_slug=org.slug, project_slug=project.slug) }}"
class="dash-project-link group">
<span class="dash-project-icon">{{ icon('folder', 'w-3.5 h-3.5') }}</span>
<span class="dash-project-name">{{ project.name }}</span>
<span class="dash-project-slug">{{ project.slug }}</span>
{% if project.project_applications %}
<span class="dash-count-pill">{{ project.project_applications|length }}</span>
{% endif %}
</a>
</div>

{% if project.project_applications %}
<div class="dash-apps">
{% for app in project.project_applications|sort(attribute='name') %}
{% set status = app_statuses.get(app.id|string, 'ok') %}
<a href="{{ url_for('user.project_application', org_slug=org.slug, project_slug=project.slug, app_slug=app.slug) }}"
class="dash-app group">
<span class="dash-app-dot {% if status == 'deploying' %}dash-dot-deploying{% elif status == 'building' %}dash-dot-building{% elif status in ('deploy-error', 'build-error') %}dash-dot-error{% endif %}"></span>
<span class="dash-app-name">{{ app.name }}</span>
<span class="dash-app-slug">{{ app.slug }}</span>
{% if status == 'deploying' %}
<span class="dash-status-badge dash-badge-deploying">deploying</span>
{% elif status == 'building' %}
<span class="dash-status-badge dash-badge-building">building</span>
{% elif status == 'deploy-error' %}
<span class="dash-status-badge dash-badge-error">deploy failed</span>
{% elif status == 'build-error' %}
<span class="dash-status-badge dash-badge-error">build failed</span>
{% endif %}
</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="dash-empty-branch">
<span class="text-base-content/25 text-xs">No projects yet</span>
</div>
{% endif %}
</div>
<svg class="w-4 h-4 text-base-content/20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2">
<polyline points="9 18 15 12 9 6" />
</svg>
</div>
</a>
</div>
{% endfor %}
</div>

{% else %}
{{ empty_state(
'No organizations',
'Create an organization to start deploying applications.',
url_for('user.organization_create'),
'Create Organization',
'building'
) }}
{% endif %}
</div>

{% else %}
Expand Down
Loading
Loading