Skip to content

Commit ef643de

Browse files
authored
Merge pull request #44 from oslabs-beta/configpagechanges
Configpagechanges
2 parents f8a09f9 + 4272176 commit ef643de

File tree

10 files changed

+961
-526
lines changed

10 files changed

+961
-526
lines changed

client/src/components/ui/Badge.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as React from "react";
2+
import { cn } from "@/lib/utils"; // if you have this; if not, see note below
3+
4+
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
5+
variant?: "default" | "secondary" | "outline";
6+
}
7+
8+
const badgeVariants: Record<BadgeProps["variant"], string> = {
9+
default:
10+
"bg-primary text-primary-foreground",
11+
secondary:
12+
"bg-secondary text-secondary-foreground",
13+
outline:
14+
"border border-border text-foreground bg-background",
15+
};
16+
17+
export function Badge({
18+
className,
19+
variant = "default",
20+
...props
21+
}: BadgeProps) {
22+
return (
23+
<span
24+
className={cn(
25+
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold",
26+
badgeVariants[variant],
27+
className
28+
)}
29+
{...props}
30+
/>
31+
);
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from "react";
2+
import { cn } from "@/lib/utils";
3+
4+
/**
5+
* Lightweight ScrollArea replacement that avoids the Radix dependency.
6+
* It simply wraps children in a div with overflow and optional sizing.
7+
*/
8+
export function ScrollArea({
9+
className,
10+
children,
11+
...props
12+
}: React.HTMLAttributes<HTMLDivElement>) {
13+
return (
14+
<div
15+
className={cn(
16+
"relative overflow-auto rounded-md scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent",
17+
className
18+
)}
19+
{...props}
20+
>
21+
{children}
22+
</div>
23+
);
24+
}

client/src/lib/api.ts

Lines changed: 204 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
import { usePipelineStore } from "../store/usePipelineStore";
1+
// In dev: talk to Vite dev server proxy at /api
2+
// In prod: use the real backend URL from VITE_API_BASE (e.g. https://api.autodeploy.app)
3+
const DEFAULT_API_BASE =
4+
import.meta.env.MODE === "development" ? "/api" : "";
25

36
export const BASE =
4-
import.meta.env.VITE_API_BASE ?? "http://localhost:3000/api";
7+
import.meta.env.VITE_API_BASE || DEFAULT_API_BASE;
58

6-
// Derive the server base without any trailing "/api" for MCP calls
7-
const SERVER_BASE = BASE.replace(/\/api$/, "");
9+
// SERVER_BASE is the same as BASE but without trailing /api,
10+
// so we can call /mcp and /auth directly.
11+
const SERVER_BASE = BASE.endsWith("/api")
12+
? BASE.slice(0, -4)
13+
: BASE;
814

915
// Generic REST helper for /api/* endpoints
1016
async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
@@ -23,24 +29,73 @@ async function mcp<T>(
2329
tool: string,
2430
input: Record<string, any> = {}
2531
): Promise<T> {
26-
const res = await fetch(
27-
`${SERVER_BASE}/mcp/v1/${encodeURIComponent(tool)}`,
28-
{
29-
method: "POST",
30-
headers: { "Content-Type": "application/json" },
31-
credentials: "include",
32-
body: JSON.stringify(input),
33-
}
34-
);
32+
const url = `${SERVER_BASE}/mcp/v1/${encodeURIComponent(tool)}`;
33+
const res = await fetch(url, {
34+
method: "POST",
35+
headers: { "Content-Type": "application/json" },
36+
credentials: "include",
37+
body: JSON.stringify(input),
38+
});
3539
const payload = await res.json().catch(() => ({}));
3640
if (!res.ok || (payload as any)?.success === false) {
3741
const msg = (payload as any)?.error || res.statusText || "MCP error";
3842
throw new Error(msg);
3943
}
40-
// payload = { success: true, data: {...} }
4144
return (payload as any).data as T;
4245
}
4346

47+
48+
// A single saved YAML version from pipeline_history
49+
export type PipelineVersion = {
50+
id: string;
51+
user_id: string;
52+
repo_full_name: string;
53+
branch: string;
54+
workflow_path: string;
55+
yaml: string;
56+
yaml_hash: string;
57+
source: string;
58+
created_at: string;
59+
};
60+
61+
// // Derive the server base without any trailing "/api" for MCP calls
62+
// const SERVER_BASE = BASE.replace(/\/api$/, "");
63+
64+
// // Generic REST helper for /api/* endpoints
65+
// async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
66+
// const res = await fetch(`${BASE}${path}`, {
67+
// headers: { "Content-Type": "application/json" },
68+
// credentials: "include",
69+
// ...opts,
70+
// });
71+
// const data = await res.json().catch(() => ({}));
72+
// if (!res.ok) throw new Error((data as any)?.error || res.statusText);
73+
// return data as T;
74+
// }
75+
76+
// // Helper for MCP tool calls on the server at /mcp/v1/:tool_name
77+
// async function mcp<T>(
78+
// tool: string,
79+
// input: Record<string, any> = {}
80+
// ): Promise<T> {
81+
// const res = await fetch(
82+
// `${SERVER_BASE}/mcp/v1/${encodeURIComponent(tool)}`,
83+
// {
84+
// method: "POST",
85+
// headers: { "Content-Type": "application/json" },
86+
// credentials: "include",
87+
// body: JSON.stringify(input),
88+
// }
89+
// );
90+
// const payload = await res.json().catch(() => ({}));
91+
// if (!res.ok || (payload as any)?.success === false) {
92+
// const msg = (payload as any)?.error || res.statusText || "MCP error";
93+
// throw new Error(msg);
94+
// }
95+
// // payload = { success: true, data: {...} }
96+
// return (payload as any).data as T;
97+
// }
98+
4499
// Simple in-memory cache for AWS roles to avoid hammering MCP
45100
let cachedAwsRoles: string[] | null = null;
46101
let awsRolesAttempted = false;
@@ -51,6 +106,58 @@ const cachedBranches = new Map<string, string[]>();
51106

52107
export const api = {
53108

109+
// ===== Pipeline history + rollback =====
110+
111+
async getPipelineHistory(params: {
112+
repoFullName: string;
113+
branch?: string;
114+
path?: string;
115+
limit?: number;
116+
}): Promise<PipelineVersion[]> {
117+
const { repoFullName, branch, path, limit } = params;
118+
119+
const qs = new URLSearchParams();
120+
qs.set("repoFullName", repoFullName);
121+
if (branch) qs.set("branch", branch);
122+
if (path) qs.set("path", path);
123+
if (limit) qs.set("limit", String(limit));
124+
125+
const res = await fetch(
126+
`${SERVER_BASE}/mcp/v1/pipeline_history?${qs.toString()}`,
127+
{
128+
method: "GET",
129+
credentials: "include",
130+
}
131+
);
132+
133+
const payload = await res.json().catch(() => ({} as any));
134+
if (!res.ok || !payload.ok) {
135+
throw new Error(payload.error || res.statusText || "History failed");
136+
}
137+
138+
// Back-end shape: { ok: true, versions: { rows: [...] } }
139+
const rows = (payload.versions?.rows ?? []) as PipelineVersion[];
140+
return rows;
141+
},
142+
143+
async rollbackPipeline(versionId: string): Promise<any> {
144+
const res = await fetch(`${SERVER_BASE}/mcp/v1/pipeline_rollback`, {
145+
method: "POST",
146+
headers: { "Content-Type": "application/json" },
147+
credentials: "include",
148+
body: JSON.stringify({ versionId }),
149+
});
150+
151+
const payload = await res.json().catch(() => ({}));
152+
if (!res.ok || !payload.ok) {
153+
throw new Error(payload.error || res.statusText || "Rollback failed");
154+
}
155+
156+
// Backend mention: data.data.github.commit.html_url, etc
157+
return payload.data;
158+
},
159+
160+
54161
async listAwsRoles(): Promise<{ roles: string[] }> {
55162
// If we've already successfully fetched roles, reuse them.
56163
if (cachedAwsRoles && cachedAwsRoles.length > 0) {
@@ -181,10 +288,91 @@ export const api = {
181288
return { branches: cachedBranches.get(repo) ?? [] };
182289
}
183290
},
291+
// async listRepos(): Promise<{ repos: string[] }> {
292+
// // If we already have repos cached, reuse them.
293+
// if (cachedRepos && cachedRepos.length > 0) {
294+
// return { repos: cachedRepos };
295+
// }
296+
297+
// // If we've already tried once and failed, don't hammer the server.
298+
// if (reposAttempted && !cachedRepos) {
299+
// return { repos: [] };
300+
// }
301+
302+
// reposAttempted = true;
303+
304+
// try {
305+
// const outer = await mcp<{
306+
// provider: string;
307+
// user: string;
308+
// repositories: { full_name: string }[];
309+
// }>("repo_reader", {});
310+
311+
// const repos = outer?.repositories?.map((r) => r.full_name) ?? [];
312+
// cachedRepos = repos;
313+
// return { repos };
314+
// } catch (err) {
315+
// console.error("[api.listRepos] failed:", err);
316+
// // Don't throw to avoid retry loops from effects; just return whatever we have (or empty).
317+
// return { repos: cachedRepos ?? [] };
318+
// }
319+
// },
320+
321+
// async listBranches(repo: string): Promise<{ branches: string[] }> {
322+
// // ✅ If we already have branches cached for this repo, reuse them.
323+
// const cached = cachedBranches.get(repo);
324+
// if (cached) {
325+
// return { branches: cached };
326+
// }
327+
328+
// try {
329+
// // For now we still use repo_reader, but we only call it
330+
// // when the cache is cold. We can later swap this to a
331+
// // more specific MCP tool like "repo_branches".
332+
// const outer = await mcp<{
333+
// success?: boolean;
334+
// data?: { repositories: { full_name: string; branches?: string[] }[] };
335+
// repositories?: { full_name: string; branches?: string[] }[];
336+
// }>("repo_reader", {
337+
// // This extra input is safe: current server ignores it,
338+
// // future server can use it to optimize.
339+
// repoFullName: repo,
340+
// });
341+
342+
// // Unwrap the payload (tool responses come back as { success, data })
343+
// const body = (outer as any)?.data ?? outer;
344+
345+
// const match = body?.repositories?.find((r: any) => r.full_name === repo);
346+
// const branches = match?.branches ?? [];
347+
348+
// // Cache even empty arrays so we don't re-query a repo with no branches
349+
// cachedBranches.set(repo, branches);
350+
351+
// return { branches };
352+
// } catch (err) {
353+
// console.error("[api.listBranches] failed:", err);
354+
355+
// // If we have anything cached (even empty), use it.
356+
// const fallback = cachedBranches.get(repo) ?? [];
357+
// return { branches: fallback };
358+
// }
359+
// },
184360

185361
async createPipeline(payload: any) {
186-
const { repo, branch, template = "node_app", options } = payload || {};
187-
const data = await mcp("pipeline_generator", payload);
362+
const {
363+
repo,
364+
branch,
365+
template = "node_app",
366+
provider = "aws",
367+
options,
368+
} = payload || {};
369+
const data = await mcp("pipeline_generator", {
370+
repo,
371+
branch,
372+
provider,
373+
template,
374+
options: options || {},
375+
});
188376
return data;
189377
},
190378

client/src/lib/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ import { twMerge } from "tailwind-merge"
44
export function cn(...inputs: ClassValue[]) {
55
return twMerge(clsx(inputs))
66
}
7+
8+
// export function cn(...classes: Array<string | false | null | undefined>) {
9+
// return classes.filter(Boolean).join(" ");
10+
// }

0 commit comments

Comments
 (0)