mirror of
https://github.com/tabler/tabler.git
synced 2026-10-21 07:14:30 +04:00
feat: rework deployment-failed and project-settings pages on shared components
- shared ProjectHeader with breadcrumb, badge and section tabs - Datagrid, ListGroup and FormGroup instead of hand-rolled markup; menu entries and changeset
This commit is contained in:
1 parent
0cdb7b1d25
commit
f7d42f36d0
5 files changed
+499
-510
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Added the deployment failed and project settings preview pages with a shared `ProjectHeader` component.
|
||||
@@ -1,269 +1,191 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
|
||||
import Breadcrumb from '@shared/components/ui/Breadcrumb.astro';
|
||||
import Avatar from '@shared/components/ui/Avatar.astro';
|
||||
import Badge from '@shared/components/ui/Badge.astro';
|
||||
import Button from '@shared/components/Button.astro';
|
||||
import Alert from '@shared/components/Alert.astro';
|
||||
import Icon from '@shared/components/Icon.astro';
|
||||
import StatusDot from '@shared/components/ui/StatusDot.astro';
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import ProjectHeader from '@shared/components/parts/ProjectHeader.astro'
|
||||
import Alert from '@ui/Alert.astro'
|
||||
import Avatar from '@ui/Avatar.astro'
|
||||
import Badge from '@ui/Badge.astro'
|
||||
import Button from '@ui/Button.astro'
|
||||
import ButtonList from '@ui/ButtonList.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardActions from '@ui/CardActions.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardFooter from '@ui/CardFooter.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import Datagrid from '@ui/Datagrid.astro'
|
||||
import DatagridItem from '@ui/DatagridItem.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import StatusDot from '@ui/StatusDot.astro'
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Overview' },
|
||||
{ label: 'Deployments', href: './deployment-failed.html', active: true },
|
||||
{ label: 'Analytics' },
|
||||
{ label: 'Logs' },
|
||||
{ label: 'Settings', href: './project-settings.html' },
|
||||
];
|
||||
type BuildStep = {
|
||||
status: 'success' | 'danger' | 'secondary'
|
||||
icon?: string
|
||||
name: string
|
||||
time: string
|
||||
detail: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
const buildSteps = [
|
||||
{
|
||||
status: 'success',
|
||||
icon: 'check',
|
||||
name: 'Clone',
|
||||
time: '2s',
|
||||
detail: 'Fetched <code>feat/redis-limiter</code> at <code>2e77b09</code>',
|
||||
},
|
||||
{
|
||||
status: 'success',
|
||||
icon: 'check',
|
||||
name: 'Install',
|
||||
time: '38s',
|
||||
detail: 'pnpm install — 412 packages',
|
||||
},
|
||||
{
|
||||
status: 'success',
|
||||
icon: 'check',
|
||||
name: 'Build',
|
||||
time: '24s',
|
||||
detail: 'pnpm build completed',
|
||||
},
|
||||
{
|
||||
status: 'danger',
|
||||
icon: 'x',
|
||||
name: 'Compile',
|
||||
time: '8s',
|
||||
detail: 'TypeScript exited with code 2',
|
||||
error: `src/lib/rate-limit.ts:47:12 — error TS2345:
|
||||
const buildSteps: BuildStep[] = [
|
||||
{ status: 'success', icon: 'check', name: 'Clone', time: '2s', detail: 'Fetched <code>feat/redis-limiter</code> at <code>2e77b09</code>' },
|
||||
{ status: 'success', icon: 'check', name: 'Install', time: '38s', detail: 'pnpm install, 412 packages' },
|
||||
{ status: 'success', icon: 'check', name: 'Build', time: '24s', detail: 'pnpm build completed' },
|
||||
{
|
||||
status: 'danger',
|
||||
icon: 'x',
|
||||
name: 'Compile',
|
||||
time: '8s',
|
||||
detail: 'TypeScript exited with code 2',
|
||||
error: `src/lib/rate-limit.ts:47:12 - error TS2345:
|
||||
Argument of type 'RedisClient' is not assignable to
|
||||
parameter of type 'RateLimiterStore'.
|
||||
Property 'incr' is missing in type 'RedisClient'.
|
||||
|
||||
src/lib/rate-limit.ts:61:5 — error TS2322:
|
||||
src/lib/rate-limit.ts:61:5 - error TS2322:
|
||||
Type 'Promise<number | null>' is not assignable to
|
||||
type 'Promise<number>'.
|
||||
|
||||
Found 2 errors in 1 file.`,
|
||||
},
|
||||
{
|
||||
status: 'secondary',
|
||||
name: 'Deploy',
|
||||
time: '—',
|
||||
detail: 'Skipped',
|
||||
muted: true,
|
||||
},
|
||||
];
|
||||
|
||||
const failureSummary = [
|
||||
{ label: 'Exit code', value: '<code>2</code>' },
|
||||
{ label: 'Failed step', value: 'Compile' },
|
||||
{ label: 'Errors', value: '2 TypeScript errors' },
|
||||
{ label: 'File', value: '<code>src/lib/rate-limit.ts</code>' },
|
||||
{ label: 'Duration', value: '1m 12s' },
|
||||
{ label: 'Region', value: '<code>fra1</code>' },
|
||||
];
|
||||
},
|
||||
{ status: 'secondary', name: 'Deploy', time: '-', detail: 'Skipped' },
|
||||
]
|
||||
|
||||
const nextSteps = [
|
||||
{
|
||||
title: 'Fix type errors in <code>rate-limit.ts</code>',
|
||||
sub: 'Align <code>RedisClient</code> with <code>RateLimiterStore</code>',
|
||||
},
|
||||
{
|
||||
title: 'Push to <a href="#">feat/redis-limiter</a>',
|
||||
sub: 'A new preview deploy starts automatically',
|
||||
},
|
||||
{
|
||||
title: 'Or retry this deployment',
|
||||
sub: 'Re-runs the same commit without a new push',
|
||||
},
|
||||
];
|
||||
{ title: 'Fix type errors in <code>rate-limit.ts</code>', description: 'Align <code>RedisClient</code> with <code>RateLimiterStore</code>' },
|
||||
{ title: 'Push to <a href="#">feat/redis-limiter</a>', description: 'A new preview deploy starts automatically' },
|
||||
{ title: 'Or retry this deployment', description: 'Re-runs the same commit without a new push' },
|
||||
]
|
||||
---
|
||||
|
||||
<DefaultLayout>
|
||||
<Breadcrumb pages="Projects,pulse-api,Deployments" />
|
||||
<DefaultLayout title="Deployment failed" pageHeader={false} pageMenu="extra.deployment-failed" description="Failed deployment details with build steps, error log, failure summary and next steps.">
|
||||
<ProjectHeader name="pulse-api" section="Deployments" badge="Failed" badgeColor="danger" active="deployments">
|
||||
<ButtonList>
|
||||
<Button text="View full logs" />
|
||||
<Button text="Retry deployment" color="primary" icon="refresh" />
|
||||
</ButtonList>
|
||||
</ProjectHeader>
|
||||
|
||||
<div class="d-flex flex-column flex-sm-row align-items-sm-center justify-content-between mt-3 gap-3">
|
||||
<div class="d-flex align-items-center gap-2 overflow-hidden">
|
||||
<Avatar placeholder="N" shape="square" />
|
||||
<h1 class="m-0 text-truncate">pulse-api</h1>
|
||||
<Badge text="Failed" color="danger" light class="badge-pill flex-shrink-0" />
|
||||
</div>
|
||||
<div class="btn-list flex-column flex-sm-row">
|
||||
<Button text="View full logs" />
|
||||
<Button text="Retry Deployment" color="dark" icon="refresh" />
|
||||
</div>
|
||||
</div>
|
||||
<Alert color="danger" title="Build failed at Compile" description="Preview deployment stopped after 1m 12s. Production on <a href='#'>api.pulsehq.dev</a> is unchanged." class="mb-3" />
|
||||
|
||||
<div class="overflow-x-auto my-3">
|
||||
<ul class="nav nav-bordered flex-nowrap" aria-label="Project sections">
|
||||
{
|
||||
navItems.map((item) => (
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class:list={['nav-link', 'text-nowrap', { active: item.active }]}
|
||||
href={item.href ?? '#'}
|
||||
aria-current={item.active ? 'page' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment</CardTitle>
|
||||
<CardActions>
|
||||
<span class="text-secondary">pulse-api-2e77b09.relay.app, 1 day ago</span>
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Datagrid>
|
||||
<DatagridItem title="Status">
|
||||
<span class="d-inline-flex align-items-center gap-2">
|
||||
<StatusDot color="danger" />
|
||||
Error
|
||||
</span>
|
||||
<span class="text-secondary ms-1">failed at Compile, 1m 12s</span>
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Source">
|
||||
<a href="#" class="text-reset">
|
||||
<Icon name="git-branch" class="icon-inline" />
|
||||
feat/redis-limiter
|
||||
</a>
|
||||
<span class="text-secondary ms-1"><code>2e77b09</code> Migrate rate limiter to Redis</span>
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Created">
|
||||
Jul 28, 2026 at 09:41
|
||||
<span class="text-secondary">by k.adamski</span>
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Environment">
|
||||
<Badge text="Preview" color="secondary" light />
|
||||
</DatagridItem>
|
||||
</Datagrid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
type="danger"
|
||||
title="Build failed at Compile"
|
||||
description="Preview deployment stopped after 1m 12s. Production on <a href='#'>api.pulsehq.dev</a> is unchanged."
|
||||
class="mb-3"
|
||||
/>
|
||||
<div class="col-lg-7">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Build steps</CardTitle>
|
||||
<CardActions>
|
||||
<span class="text-secondary">3 of 5 completed</span>
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ul class="timeline mb-0">
|
||||
{
|
||||
buildSteps.map((step) => (
|
||||
<li class="timeline-event">
|
||||
<div class:list={['timeline-event-icon', `bg-${step.status}-lt`, `text-${step.status}`]}>{step.icon && <Icon name={step.icon} />}</div>
|
||||
<Card class="timeline-event-card">
|
||||
<CardBody class:list={{ 'text-secondary': step.status === 'secondary' }}>
|
||||
<div class="d-flex justify-content-between gap-2">
|
||||
<strong class:list={{ 'text-danger': step.status === 'danger' }}>{step.name}</strong>
|
||||
<span class="text-secondary">{step.time}</span>
|
||||
</div>
|
||||
<div class="text-secondary" set:html={step.detail} />
|
||||
{step.error && (
|
||||
<>
|
||||
{/* prettier-ignore */}
|
||||
<pre class="mt-2 mb-2"><code set:text={step.error} /></pre>
|
||||
<a href="#">View full logs</a>
|
||||
</>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header flex-column flex-sm-row align-items-sm-center justify-content-between gap-1">
|
||||
<h2 class="card-title mb-0">Deployment</h2>
|
||||
<span class="text-secondary text-break">pulse-api-2e77b09.relay.app · 1d ago</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-12 col-sm-3 text-secondary mb-1 mb-sm-3">Status</dt>
|
||||
<dd class="col-12 col-sm-9 mb-3">
|
||||
<div class="d-flex flex-wrap align-items-center column-gap-2 row-gap-1">
|
||||
<span class="d-inline-flex align-items-center gap-2">
|
||||
<StatusDot color="danger" />
|
||||
Error
|
||||
</span>
|
||||
<span class="text-secondary">failed at Compile · 1m 12s</span>
|
||||
</div>
|
||||
</dd>
|
||||
|
||||
<dt class="col-12 col-sm-3 text-secondary mb-1 mb-sm-3">Source</dt>
|
||||
<dd class="col-12 col-sm-9 mb-3">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<a href="#" class="badge badge-outline text-secondary badge-pill">
|
||||
<Icon name="git-branch" class="icon-sm" />
|
||||
feat/redis-limiter
|
||||
</a>
|
||||
<a href="#" class="text-reset text-break">
|
||||
<code>2e77b09</code>
|
||||
<span class="ms-1">Migrate rate limiter to Redis</span>
|
||||
</a>
|
||||
</div>
|
||||
</dd>
|
||||
|
||||
<dt class="col-12 col-sm-3 text-secondary mb-1 mb-sm-3">Created</dt>
|
||||
<dd class="col-12 col-sm-9 mb-3">
|
||||
Jul 28, 2026 at 09:41
|
||||
<span class="text-secondary">by k.adamski</span>
|
||||
</dd>
|
||||
|
||||
<dt class="col-12 col-sm-3 text-secondary mb-1 mb-sm-0">Environment</dt>
|
||||
<dd class="col-12 col-sm-9 mb-0">
|
||||
<Badge text="Preview" class="badge-outline text-secondary badge-pill" />
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid mt-3">
|
||||
<div class="g-col-12 g-col-md-7">
|
||||
<div class="card">
|
||||
<div class="card-header flex-column flex-sm-row align-items-sm-center justify-content-between gap-1">
|
||||
<h2 class="card-title mb-0">Build steps</h2>
|
||||
<span class="text-secondary">3 of 5 completed</span>
|
||||
</div>
|
||||
<div class="card-body overflow-hidden">
|
||||
<ul class="timeline mb-0">
|
||||
{
|
||||
buildSteps.map((step) => (
|
||||
<li class="timeline-event">
|
||||
<div class={`timeline-event-icon bg-${step.status}-lt text-${step.status}`}>
|
||||
{step.icon && <Icon name={step.icon} />}
|
||||
</div>
|
||||
<div class:list={['timeline-event-card', 'overflow-hidden', { 'text-secondary': step.muted }]}>
|
||||
<div class="d-flex justify-content-between gap-2">
|
||||
<strong class:list={{ 'text-danger': step.status === 'danger' }}>{step.name}</strong>
|
||||
<span class="text-secondary flex-shrink-0">{step.time}</span>
|
||||
</div>
|
||||
<div class:list={['text-secondary', 'text-break', { 'mb-2': step.error }]} set:html={step.detail} />
|
||||
{step.error && (
|
||||
<>
|
||||
<pre class="mb-2 overflow-x-auto"><code set:text={step.error} /></pre>
|
||||
<a href="#">View full logs →</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="g-col-12 g-col-md-5 space-y">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Failure summary</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
{
|
||||
failureSummary.map((row, i) => {
|
||||
const last = i === failureSummary.length - 1;
|
||||
return (
|
||||
<>
|
||||
<dt class:list={['col-12 col-sm-5 text-secondary mb-1', { 'mb-sm-3': !last, 'mb-sm-0': last }]}>
|
||||
{row.label}
|
||||
</dt>
|
||||
<dd
|
||||
class:list={['col-12 col-sm-7 text-break', { 'mb-3': !last, 'mb-0': last }]}
|
||||
set:html={row.value}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Next steps</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled mb-0 space-y">
|
||||
{
|
||||
nextSteps.map((step, i) => (
|
||||
<li class="d-flex gap-3">
|
||||
<Avatar placeholder={String(i + 1)} size="sm" class="flex-shrink-0" />
|
||||
<div class="overflow-hidden text-break">
|
||||
<div set:html={step.title} />
|
||||
<div class="text-secondary" set:html={step.sub} />
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="btn-list flex-column">
|
||||
<Button text="Retry deployment" color="dark" block icon="refresh" />
|
||||
<Button text="View full logs" block />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
<CardHeader title="Failure summary" />
|
||||
<CardBody>
|
||||
<Datagrid>
|
||||
<DatagridItem title="Exit code"><code>2</code></DatagridItem>
|
||||
<DatagridItem title="Failed step">Compile</DatagridItem>
|
||||
<DatagridItem title="Errors">2 TypeScript errors</DatagridItem>
|
||||
<DatagridItem title="File"><code>src/lib/rate-limit.ts</code></DatagridItem>
|
||||
<DatagridItem title="Duration">1m 12s</DatagridItem>
|
||||
<DatagridItem title="Region"><code>fra1</code></DatagridItem>
|
||||
</Datagrid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
<CardHeader title="Next steps" />
|
||||
<CardBody>
|
||||
<ol class="list-unstyled space-y mb-0">
|
||||
{
|
||||
nextSteps.map((step, index) => (
|
||||
<li class="d-flex gap-3">
|
||||
<Avatar placeholder={String(index + 1)} size="sm" />
|
||||
<div>
|
||||
<div set:html={step.title} />
|
||||
<div class="text-secondary" set:html={step.description} />
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ol>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<ButtonList class="flex-column">
|
||||
<Button text="Retry deployment" color="primary" icon="refresh" block />
|
||||
<Button text="View full logs" block />
|
||||
</ButtonList>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
@@ -1,274 +1,263 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
|
||||
import Breadcrumb from '@shared/components/ui/Breadcrumb.astro';
|
||||
import Avatar from '@shared/components/ui/Avatar.astro';
|
||||
import Badge from '@shared/components/ui/Badge.astro';
|
||||
import Button from '@shared/components/Button.astro';
|
||||
import NavSegmented from '@shared/components/ui/NavSegmented.astro';
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Overview' },
|
||||
{ label: 'Deployments', href: './deployment-failed.html' },
|
||||
{ label: 'Analytics' },
|
||||
{ label: 'Logs' },
|
||||
{ label: 'Settings', href: './project-settings.html', active: true },
|
||||
];
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import ProjectHeader from '@shared/components/parts/ProjectHeader.astro'
|
||||
import Badge from '@ui/Badge.astro'
|
||||
import Button from '@ui/Button.astro'
|
||||
import ButtonList from '@ui/ButtonList.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardActions from '@ui/CardActions.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import FormGroup from '@ui/FormGroup.astro'
|
||||
import ListGroup from '@ui/ListGroup.astro'
|
||||
import ListGroupItem from '@ui/ListGroupItem.astro'
|
||||
import NavSegmented from '@ui/NavSegmented.astro'
|
||||
|
||||
const settingsNav = [
|
||||
{ label: 'General', href: '#general', active: true },
|
||||
{ label: 'Domains', href: '#domains' },
|
||||
{ label: 'Environment', href: '#env' },
|
||||
{ label: 'Git', href: '#git' },
|
||||
{ label: 'Danger zone', href: '#danger', danger: true },
|
||||
];
|
||||
{ label: 'General', href: '#general', active: true },
|
||||
{ label: 'Domains', href: '#domains' },
|
||||
{ label: 'Environment', href: '#env' },
|
||||
{ label: 'Git', href: '#git' },
|
||||
{ label: 'Danger zone', href: '#danger', danger: true },
|
||||
]
|
||||
|
||||
const domains = [
|
||||
{ host: 'api.pulsehq.dev', description: 'Production, valid configuration', badge: 'Primary', color: 'success' },
|
||||
{ host: 'pulse-api.relay.app', description: 'Production, auto-assigned', badge: 'System', color: 'secondary' },
|
||||
{ host: 'staging-api.pulsehq.dev', description: 'Preview, DNS pending', badge: 'Pending', color: 'warning' },
|
||||
]
|
||||
|
||||
const envVars = [
|
||||
{ key: 'DATABASE_URL', value: '••••••••••••••••••••', action: 'Reveal', masked: true },
|
||||
{ key: 'REDIS_URL', value: '••••••••••••••••', action: 'Reveal', masked: true },
|
||||
{ key: 'WEBHOOK_SECRET', value: '••••••••••••', action: 'Reveal', masked: true },
|
||||
{ key: 'NEXT_PUBLIC_APP_URL', value: 'https://api.pulsehq.dev', action: 'Copy' },
|
||||
];
|
||||
{ key: 'DATABASE_URL', value: '••••••••••••••••••••', masked: true },
|
||||
{ key: 'REDIS_URL', value: '••••••••••••••••', masked: true },
|
||||
{ key: 'WEBHOOK_SECRET', value: '••••••••••••', masked: true },
|
||||
{ key: 'NEXT_PUBLIC_APP_URL', value: 'https://api.pulsehq.dev', masked: false },
|
||||
]
|
||||
|
||||
const switches = [
|
||||
{ id: 'fluid-compute', label: 'Fluid compute', description: 'Scale functions with shared concurrency' },
|
||||
{ id: 'auto-deploy', label: 'Auto-deploy on push', description: 'Create a deployment for every commit' },
|
||||
{ id: 'preview-deploys', label: 'Preview deployments', description: 'Deploy pull request branches automatically' },
|
||||
]
|
||||
---
|
||||
|
||||
<DefaultLayout>
|
||||
<Breadcrumb pages="Projects,pulse-api,Settings" />
|
||||
<DefaultLayout title="Project settings" pageHeader={false} pageMenu="extra.project-settings" description="Project settings with general options, domains, environment variables, git integration and a danger zone.">
|
||||
<ProjectHeader name="pulse-api" section="Settings" badge="Production" badgeColor="success" active="settings">
|
||||
<ButtonList>
|
||||
<Button text="Discard" />
|
||||
<Button text="Save changes" color="primary" />
|
||||
</ButtonList>
|
||||
</ProjectHeader>
|
||||
|
||||
<div class="d-flex flex-column flex-sm-row align-items-sm-center justify-content-between mt-3 gap-3">
|
||||
<div class="d-flex align-items-center gap-2 overflow-hidden">
|
||||
<Avatar placeholder="N" shape="square" />
|
||||
<h1 class="m-0 text-truncate">pulse-api</h1>
|
||||
<Badge text="Production" color="success" light class="badge-pill flex-shrink-0" />
|
||||
</div>
|
||||
<div class="btn-list flex-column flex-sm-row">
|
||||
<Button text="Discard" />
|
||||
<Button text="Save changes" color="dark" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cards">
|
||||
<div class="col-lg-3">
|
||||
<Card class="sticky-top">
|
||||
<CardBody class="p-2">
|
||||
<nav class="nav nav-vertical" aria-label="Settings sections">
|
||||
{
|
||||
settingsNav.map((item) => (
|
||||
<a href={item.href} class:list={['nav-link', { 'active': item.active, 'text-danger': item.danger }]} aria-current={item.active ? 'true' : undefined}>
|
||||
{item.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto my-3">
|
||||
<ul class="nav nav-bordered flex-nowrap" aria-label="Project sections">
|
||||
{
|
||||
navItems.map((item) => (
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class:list={['nav-link', 'text-nowrap', { active: item.active }]}
|
||||
href={item.href ?? '#'}
|
||||
aria-current={item.active ? 'page' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<Card as="form" id="general">
|
||||
<CardHeader>
|
||||
<CardTitle>General</CardTitle>
|
||||
<CardActions>
|
||||
<span class="text-secondary">Project identity and runtime</span>
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<FormGroup label="Project name" for="project-name">
|
||||
<input class="form-control" type="text" id="project-name" value="pulse-api" />
|
||||
</FormGroup>
|
||||
<FormGroup label="Framework" for="framework">
|
||||
<select class="form-select" id="framework">
|
||||
<option value="nextjs" selected>Next.js</option>
|
||||
<option value="nodejs">Node.js</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</FormGroup>
|
||||
<FormGroup label="Node.js version" for="node-version">
|
||||
<select class="form-select" id="node-version">
|
||||
<option value="20">20.x</option>
|
||||
<option value="22" selected>22.x</option>
|
||||
</select>
|
||||
</FormGroup>
|
||||
<FormGroup label="Region" for="region">
|
||||
<select class="form-select" id="region">
|
||||
<option value="fra1" selected>fra1, Frankfurt, Germany</option>
|
||||
<option value="iad1">iad1, Washington, D.C., USA</option>
|
||||
<option value="sfo1">sfo1, San Francisco, USA</option>
|
||||
</select>
|
||||
</FormGroup>
|
||||
{
|
||||
switches.slice(0, 1).map((item) => (
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<label class="form-label mb-0" for={item.id}>
|
||||
{item.label}
|
||||
</label>
|
||||
<div class="text-secondary">{item.description}</div>
|
||||
</div>
|
||||
<label class="form-check form-switch m-0">
|
||||
<input class="form-check-input" type="checkbox" id={item.id} checked />
|
||||
</label>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="g-col-12 g-col-md-2">
|
||||
<div class="card sticky-top" style="top: 1rem;">
|
||||
<div class="card-body p-1">
|
||||
<nav class="nav nav-vertical" aria-label="Settings sections">
|
||||
{
|
||||
settingsNav.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
class:list={['nav-link', 'rounded-2', { active: item.active, 'text-danger': item.danger }]}
|
||||
aria-current={item.active ? 'true' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<Card id="domains">
|
||||
<CardHeader>
|
||||
<CardTitle>Domains</CardTitle>
|
||||
<CardActions>
|
||||
<Button text="Add domain" size="sm" icon="plus" />
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<ListGroup class="card-list-group">
|
||||
{
|
||||
domains.map((domain) => (
|
||||
<ListGroupItem>
|
||||
<div class="row align-items-center g-3">
|
||||
<div class="col">
|
||||
<div class="fw-medium">{domain.host}</div>
|
||||
<div class="text-secondary">{domain.description}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<Badge text={domain.badge} color={domain.color} light />
|
||||
</div>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="g-col-12 g-col-md-10 space-y">
|
||||
<div class="card" id="general">
|
||||
<div class="card-header justify-content-between">
|
||||
<h2 class="card-title">General</h2>
|
||||
<span class="card-meta">Project identity and runtime</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Project name</label>
|
||||
<input class="form-control" type="text" value="pulse-api" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<Card id="env">
|
||||
<CardHeader>
|
||||
<CardTitle>Environment variables</CardTitle>
|
||||
<CardActions>
|
||||
<span class="text-secondary">18 variables, updated Jul 20</span>
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="d-flex flex-column flex-sm-row align-items-sm-center justify-content-between gap-2">
|
||||
<NavSegmented items={['Production', 'Preview', 'Development']} size="sm" />
|
||||
<Button text="Add variable" size="sm" icon="plus" />
|
||||
</div>
|
||||
</CardBody>
|
||||
<ListGroup class="card-list-group">
|
||||
{
|
||||
envVars.map((item) => (
|
||||
<ListGroupItem>
|
||||
<div class="row align-items-center g-3">
|
||||
<div class="col-12 col-sm-auto">
|
||||
<code>{item.key}</code>
|
||||
</div>
|
||||
<div class="col">
|
||||
<span class:list={['text-secondary', 'text-truncate', 'd-block', { 'font-monospace': !item.masked }]} aria-label={item.masked ? 'Masked value' : undefined}>
|
||||
{item.value}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<Button text={item.masked ? 'Reveal' : 'Copy'} icon={item.masked ? 'eye' : 'copy'} size="sm" variant="ghost" />
|
||||
</div>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Framework</label>
|
||||
<select class="form-select">
|
||||
<option value="1" selected>Next.js</option>
|
||||
<option value="2">Node.js</option>
|
||||
<option value="3">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<Card as="form" id="git">
|
||||
<CardHeader>
|
||||
<CardTitle>Git</CardTitle>
|
||||
<CardActions>
|
||||
<span class="text-secondary">Connected repository</span>
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Repository</div>
|
||||
<a href="#">pulsehq/pulse-api</a>
|
||||
</div>
|
||||
<FormGroup label="Production branch" for="production-branch">
|
||||
<input class="form-control" type="text" id="production-branch" value="main" />
|
||||
</FormGroup>
|
||||
<div class="space-y">
|
||||
{
|
||||
switches.slice(1).map((item) => (
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<label class="form-label mb-0" for={item.id}>
|
||||
{item.label}
|
||||
</label>
|
||||
<div class="text-secondary">{item.description}</div>
|
||||
</div>
|
||||
<label class="form-check form-switch m-0">
|
||||
<input class="form-check-input" type="checkbox" id={item.id} checked />
|
||||
</label>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Node.js version</label>
|
||||
<select class="form-select">
|
||||
<option value="1">20.x</option>
|
||||
<option value="2" selected>22.x</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Region</label>
|
||||
<select class="form-select">
|
||||
<option value="1" selected>fra1 — Frankfurt, Germany</option>
|
||||
<option value="2">iad1 — Washington, D.C., USA</option>
|
||||
<option value="3">sfo1 — San Francisco, USA</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<label class="form-label">Fluid compute</label>
|
||||
<span class="text-secondary">Scale functions with shared concurrency</span>
|
||||
</div>
|
||||
<label class="form-check form-switch m-0"><input class="form-check-input" type="checkbox" checked /></label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="domains">
|
||||
<div class="card-header justify-content-between">
|
||||
<h2 class="card-title">Domains</h2>
|
||||
<Button text="Add domain" size="sm" />
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="m-0">api.pulsehq.dev</p>
|
||||
<span class="text-secondary fs-5">Production · Valid configuration</span>
|
||||
</div>
|
||||
|
||||
<Badge text="Primary" color="success" light class="badge-pill" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="m-0">pulse-api.relay.app</p>
|
||||
<span class="text-secondary fs-5">Production · Auto-assigned</span>
|
||||
</div>
|
||||
|
||||
<Badge text="System" color="gray" light class="badge-pill" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="m-0">staging-api.pulsehq.dev</p>
|
||||
<span class="text-secondary fs-5">Preview · DNS pending</span>
|
||||
</div>
|
||||
|
||||
<Badge text="Pending" color="warning" light class="badge-pill" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="env">
|
||||
<div class="card-header justify-content-between">
|
||||
<h2 class="card-title">Environment variables</h2>
|
||||
<span class="text-secondary">18 variables · updated Jul 20</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column flex-sm-row align-items-sm-center justify-content-between gap-2">
|
||||
<NavSegmented items="Production,Preview,Development" size="sm" />
|
||||
<Button text="Add variable" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group list-group-flush">
|
||||
{
|
||||
envVars.map((item) => (
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 column-gap-3">
|
||||
<code class="flex-shrink-0">{item.key}</code>
|
||||
<span
|
||||
class:list={['text-secondary', 'text-truncate', 'flex-fill', { 'font-monospace': !item.masked }]}
|
||||
aria-label={item.masked ? 'Masked value' : undefined}
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
<Button text={item.action} link size="sm" class="flex-shrink-0 ms-auto text-black" />
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="git">
|
||||
<div class="card-header justify-content-between">
|
||||
<h2 class="card-title">Git</h2>
|
||||
<span class="card-meta">Connected repository</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Repository</label>
|
||||
<a class="text-link-strong" href="#">pulsehq/pulse-api</a>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Production branch</label>
|
||||
<input class="form-control" type="text" value="main" />
|
||||
</div>
|
||||
|
||||
<div class="mb-4 d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<label class="form-label">Auto-deploy on push</label>
|
||||
<span class="text-secondary">Create a deployment for every commit</span>
|
||||
</div>
|
||||
<label class="form-check form-switch m-0"><input class="form-check-input" type="checkbox" checked /></label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<label class="form-label">Preview deployments</label>
|
||||
<span class="text-secondary">Deploy pull request branches automatically</span>
|
||||
</div>
|
||||
<label class="form-check form-switch m-0"><input class="form-check-input" type="checkbox" checked /></label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-danger" id="danger">
|
||||
<div class="card-header justify-content-between">
|
||||
<h2 class="card-title text-danger">Danger zone</h2>
|
||||
<span class="text-secondary">Irreversible actions</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column flex-sm-row align-items-sm-center justify-content-between gap-2">
|
||||
<div>
|
||||
<div>Transfer project</div>
|
||||
<div class="text-secondary">Move pulse-api to another team or personal account</div>
|
||||
</div>
|
||||
<Button text="Transfer" class="flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column flex-sm-row align-items-sm-center justify-content-between gap-2">
|
||||
<div>
|
||||
<div>Delete project</div>
|
||||
<div class="text-secondary">Permanently remove deployments, domains, and environment variables</div>
|
||||
</div>
|
||||
<Button text="Delete project" color="danger" class="flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
<div class="col-12">
|
||||
<Card class="border-danger" id="danger">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-danger">Danger zone</CardTitle>
|
||||
<CardActions>
|
||||
<span class="text-secondary">Irreversible actions</span>
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<ListGroup class="card-list-group">
|
||||
<ListGroupItem>
|
||||
<div class="row align-items-center g-3">
|
||||
<div class="col">
|
||||
<div class="fw-medium">Transfer project</div>
|
||||
<div class="text-secondary">Move pulse-api to another team or personal account</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<Button text="Transfer" />
|
||||
</div>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
<ListGroupItem>
|
||||
<div class="row align-items-center g-3">
|
||||
<div class="col">
|
||||
<div class="fw-medium">Delete project</div>
|
||||
<div class="text-secondary">Permanently remove deployments, domains, and environment variables</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<Button text="Delete project" color="danger" />
|
||||
</div>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
import Avatar from '@ui/Avatar.astro'
|
||||
import Badge from '@ui/Badge.astro'
|
||||
import Breadcrumb from '@ui/Breadcrumb.astro'
|
||||
import PageTitle from '@ui/PageTitle.astro'
|
||||
|
||||
// Shared header for the project pages (deployment-failed, project-settings):
|
||||
// breadcrumb, project name with a status badge, actions slot and section tabs.
|
||||
|
||||
type Tab = 'overview' | 'deployments' | 'analytics' | 'logs' | 'settings'
|
||||
|
||||
interface Props {
|
||||
/** project name, also the avatar placeholder letter */
|
||||
name: string
|
||||
/** last breadcrumb item */
|
||||
section: string
|
||||
/** status badge text */
|
||||
badge: string
|
||||
/** status badge color, e.g. "danger" */
|
||||
badgeColor: string
|
||||
/** highlighted tab */
|
||||
active: Tab
|
||||
}
|
||||
|
||||
const { name, section, badge, badgeColor, active } = Astro.props
|
||||
|
||||
const tabs: { key: Tab; label: string; href?: string }[] = [
|
||||
{ key: 'overview', label: 'Overview' },
|
||||
{ key: 'deployments', label: 'Deployments', href: './deployment-failed.html' },
|
||||
{ key: 'analytics', label: 'Analytics' },
|
||||
{ key: 'logs', label: 'Logs' },
|
||||
{ key: 'settings', label: 'Settings', href: './project-settings.html' },
|
||||
]
|
||||
---
|
||||
|
||||
<Breadcrumb pages={['Projects', name, section]} />
|
||||
|
||||
<div class="row g-3 align-items-center mt-1">
|
||||
<div class="col">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<Avatar placeholder={name.charAt(0).toUpperCase()} shape="square" />
|
||||
<PageTitle class="text-truncate">{name}</PageTitle>
|
||||
<Badge text={badge} color={badgeColor} light />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto my-3">
|
||||
<ul class="nav nav-bordered flex-nowrap" aria-label="Project sections">
|
||||
{
|
||||
tabs.map((tab) => (
|
||||
<li class="nav-item">
|
||||
<a class:list={['nav-link', 'text-nowrap', { active: tab.key === active }]} href={tab.href ?? '#'} aria-current={tab.key === active ? 'page' : undefined}>
|
||||
{tab.label}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -346,6 +346,11 @@
|
||||
"title": "Cookie banner",
|
||||
"url": "cookie-banner.html"
|
||||
},
|
||||
"deployment-failed": {
|
||||
"title": "Deployment failed",
|
||||
"url": "deployment-failed.html",
|
||||
"badge": "New"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Empty page",
|
||||
"url": "empty.html"
|
||||
@@ -403,6 +408,11 @@
|
||||
"url": "pricing-table.html",
|
||||
"title": "Pricing table"
|
||||
},
|
||||
"project-settings": {
|
||||
"title": "Project settings",
|
||||
"url": "project-settings.html",
|
||||
"badge": "New"
|
||||
},
|
||||
"search-results": {
|
||||
"title": "Search results",
|
||||
"url": "search-results.html"
|
||||
|
||||
Reference in new issue
Block a user