Skip to content
Merged
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
7 changes: 4 additions & 3 deletions docs/UX.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@ Signed-in operators work inside a workspace. Primary sidebar navigation (desktop

**Header:** stack name + optional runtime strip (health, container count, image, last refresh)

**Tabs:** Compose | Files | Docs | Environment | Secrets | Changes | Deployments
**Tabs:** Editor | Docs | Environment | Changes | History | Settings (Secrets and Deployments arrive later)

No primary nav items for Containers / Images / Networks / Volumes.

Repository pages have **Overview**, **Files**, **Docs** and **Changes** tabs: the same editor, docs and review
views scoped to the whole repository, for files outside any stack (root README, `docs/`, shared `.env.example`).

Implemented in PR #3: **Editor** (explorer, tabs, problems, status bar), **Docs**, **Environment**, **Changes** and
**Settings** tabs. Secrets and Deployments arrive with PR #6 and PR #5.
Implemented: **Editor**, **Docs**, **Environment**, **Changes**, **History** (stack-scoped commits at the
fetched snapshot) and **Settings**. Draft counts appear on the Changes tab and stack header when work is outstanding.
Secrets and Deployments arrive with PR #6 and PR #5.

## Desktop

Expand Down
1 change: 1 addition & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Each stack has these tabs:
| **Docs** | Read the stack's Markdown files rendered (README first), follow links between them and edit them as drafts. |
| **Environment** | Variables the Compose file references, whether `.env.example` documents them, env files it expects, and values that look like hard-coded secrets. Values are never shown. |
| **Changes** | Every draft as a line-by-line diff against the fetched commit. Select files, commit with your Account Git identity, and push when ready. Discard drafts you don't want. |
| **History** | Commits that touched this stack at the last fetched snapshot. Open a commit to inspect the scoped file diffs. |
| **Settings** | Rename the stack, change its Compose file or remove it (files and drafts are kept). |

Files that look like secrets (`.env`, private keys, certificates, anything in a `secrets/` folder) are listed but
Expand Down
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ stack-manager looks after the Git source they are deployed from.
stack-manager is in early development (0.x). Available today: sign-in, workspaces, encrypted credentials, repository
connections, stacks, dashboard and settings, the editor with drafts, docs, the environment inventory and change review.

Coming in v0.5.0: committing drafts, safe push and Git history. After that on the [roadmap](plans/MVP_PLAN.md):
Stack-scoped Git history is on each stack's History tab. Draft commit and safe push are on Changes. Next on the
[roadmap](plans/MVP_PLAN.md):
deploying only the stacks a commit changed (through Portainer and others), then optional read-only runtime feedback.

## Documentation
Expand Down
93 changes: 93 additions & 0 deletions src/app/styles/source.css
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,99 @@ select.compact-select {
font-size: var(--text-xs);
font-weight: 600;
white-space: nowrap;
text-decoration: none;
}

.page-title-row .draft-badge {
margin-left: 0;
}

/* ---------- Stack history ---------- */
.history-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: var(--space-3);
}

.history-item {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
overflow: hidden;
}

.history-row {
display: flex;
align-items: flex-start;
gap: var(--space-3);
width: 100%;
padding: var(--space-3) var(--space-4);
border: 0;
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
}

.history-row:hover,
.history-row:focus-visible {
background: var(--surface-2);
}

.history-row-main {
display: grid;
gap: var(--space-1);
min-width: 0;
flex: 1;
}

.history-subject {
font-weight: 600;
overflow-wrap: anywhere;
}

.history-meta {
display: flex;
flex-wrap: wrap;
gap: 0 0.25rem;
}

.history-files,
.history-files-empty {
margin: 0;
padding: 0 var(--space-4) var(--space-3);
padding-left: calc(var(--space-4) + 1.5rem);
list-style: none;
display: grid;
gap: var(--space-1);
font-size: var(--text-sm);
}

.history-files li {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
min-width: 0;
}

.history-files .mono {
overflow-wrap: anywhere;
}

.history-detail {
border-top: 1px solid var(--border);
}

.history-detail-title {
margin: 0;
font-size: var(--text-md);
}

.history-file-diff + .history-file-diff {
border-top: 1px solid var(--border);
}

/* Flat lists (grouped by repository only, or not at all) have no folder labels above their first row. */
Expand Down
19 changes: 19 additions & 0 deletions src/app/w/[workspaceId]/stacks/[stackId]/history/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from 'next';
import { getContainer } from '@/server/container';
import { HistoryView } from '@/ui/source/history-view';
import { orNotFound } from '../../../../../_lib/session';

export const metadata: Metadata = { title: 'History' };

export default async function StackHistoryPage({
params,
}: {
params: Promise<{ workspaceId: string; stackId: string }>;
}) {
const { workspaceId, stackId } = await params;
const { stacks } = getContainer();
const stack = await orNotFound(stacks.get(workspaceId, stackId));
return (
<HistoryView apiBase={`/api/workspaces/${workspaceId}/stacks/${stackId}`} rootPath={stack.rootPath} />
);
}
15 changes: 14 additions & 1 deletion src/app/w/[workspaceId]/stacks/[stackId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Folder, FolderGit2, GitBranch, GitCommitHorizontal, Hourglass } from 'lucide-react';
import { Folder, FolderGit2, GitBranch, GitCommitHorizontal, Hourglass, PencilLine } from 'lucide-react';
import Link from 'next/link';
import { getContainer } from '@/server/container';
import { EmptyState } from '@/ui/primitives/empty-state';
Expand All @@ -24,6 +24,7 @@ export default async function StackLayout({
stacks.listForRepository(workspaceId, stack.repository.id),
]);
const base = `/w/${workspaceId}/stacks/${stackId}`;
const draftLabel = `${drafts} draft${drafts === 1 ? '' : 's'}`;

return (
<div className="wide-page">
Expand All @@ -37,6 +38,18 @@ export default async function StackLayout({
/>
}
title={stack.name}
status={
drafts > 0 ? (
<Link
href={`${base}/changes`}
className="draft-badge"
aria-label={`${draftLabel}, open Changes`}
>
<PencilLine className="icon" aria-hidden="true" />
{draftLabel}
</Link>
) : null
}
actions={
<StackSwitcher
workspaceId={workspaceId}
Expand Down
45 changes: 2 additions & 43 deletions src/ui/source/changes-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Alert } from '../primitives/alert';
import { Button, ButtonLink } from '../primitives/button';
import { ConfirmButton } from '../primitives/confirm-button';
import { EmptyState } from '../primitives/empty-state';
import { UnifiedDiff } from './unified-diff';

type GitIdentity = { name: string; email: string };

Expand All @@ -24,49 +25,7 @@ function shortSha(sha: string) {

function Diff({ before, after }: { before: string; after: string }) {
const patch = structuredPatch('before', 'after', before, after, '', '', { context: 3 });
if (patch.hunks.length === 0) return <p className="fine-print change-head">No textual changes.</p>;
return (
<div className="diff" role="table" aria-label="Line changes">
{patch.hunks.map((hunk, h) => {
let oldLine = hunk.oldStart;
let newLine = hunk.newStart;
return (
<div key={h} role="rowgroup">
<div className="diff-hunk" role="row">
<span role="cell">
@@ -{hunk.oldStart},{hunk.oldLines} +{hunk.newStart},{hunk.newLines} @@
</span>
</div>
{hunk.lines.map((line, i) => {
const sign = line[0];
if (sign === '\\') return null;
const kind = sign === '+' ? 'add' : sign === '-' ? 'remove' : 'same';
const oldNo = kind === 'add' ? '' : oldLine++;
const newNo = kind === 'remove' ? '' : newLine++;
return (
<div key={i} className={`diff-line ${kind}`} role="row">
<span className="ln" role="cell">
{oldNo}
</span>
<span className="ln" role="cell">
{newNo}
</span>
<span
className="sign"
role="cell"
aria-label={kind === 'same' ? undefined : kind === 'add' ? 'added' : 'removed'}
>
{kind === 'same' ? ' ' : sign}
</span>
<span role="cell">{line.slice(1)}</span>
</div>
);
})}
</div>
);
})}
</div>
);
return <UnifiedDiff hunks={patch.hunks} />;
}

function problemLabel(problem: GitWorkflowProblem) {
Expand Down
Loading
Loading