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: 6 additions & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ function NeedRepo({ children }: { children: JSX.Element }) {
}
function NeedPipeline({ children }: { children: JSX.Element }) {
const { result } = usePipelineStore();
return !result?.generated_yaml ? <Navigate to="/configure" replace /> : children;
const hasYaml =
result?.generated_yaml ||
result?.yaml ||
result?.data?.generated_yaml;

return !hasYaml ? <Navigate to="/configure" replace /> : children;
}

export default function App() {
Expand Down
7 changes: 3 additions & 4 deletions client/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const BASE =
import.meta.env.VITE_API_BASE ?? "http://localhost:3333/api";
import.meta.env.VITE_API_BASE ?? "http://localhost:3000/api";

// Derive the server base without any trailing "/api" for MCP calls
const SERVER_BASE = BASE.replace(/\/api$/, "");
Expand Down Expand Up @@ -37,7 +37,7 @@ export const api = {
const data = await mcp<{
repositories: { name: string; full_name: string; branches?: string[] }[];
}>("repo_reader", {});
const repos = (data?.repositories ?? []).map((r) => r.full_name);
const repos = (data?.data?.repositories ?? []).map((r) => r.full_name);
return { repos };
},

Expand All @@ -46,7 +46,7 @@ export const api = {
const data = await mcp<{
repositories: { name: string; full_name: string; branches?: string[] }[];
}>("repo_reader", {});
const item = (data?.repositories ?? []).find((r) => r.full_name === repo);
const item = (data?.data?.repositories ?? []).find((r) => r.full_name === repo);
return { branches: item?.branches ?? [] };
},

Expand Down Expand Up @@ -204,4 +204,3 @@ function writeSecrets(repo: string, env: string, obj: Record<string, string>) {

// in-memory job storage for mock deploys
const JOBS: Map<string, any> = new Map();

65 changes: 56 additions & 9 deletions client/src/pages/ConfigurePage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { useRepoStore } from "../store/useRepoStore";
import { usePipelineStore } from "../store/usePipelineStore";

export default function ConfigurePage() {
const { repo, branch } = useRepoStore();
const pipeline = usePipelineStore();
const navigate = useNavigate();

// Load available AWS roles once
// Log on mount with repo, branch, and navigate check
useEffect(() => {
pipeline.loadAwsRoles?.().catch(console.error);
console.log("[ConfigurePage] Mounted. Repo:", repo, "Branch:", branch);
if (!navigate) console.warn("[ConfigurePage] ⚠️ navigate() not initialized!");
}, [repo, branch, navigate]);

// Load available AWS roles once, safely
useEffect(() => {
let loaded = false;

async function init() {
if (loaded) return;
loaded = true;
try {
console.log("[ConfigurePage] Loading AWS roles once...");
await pipeline.loadAwsRoles?.();

// Re-read roles from store after load completes
const updatedRoles = usePipelineStore.getState().roles;
console.log("[ConfigurePage] Roles (after load):", updatedRoles);
} catch (err) {
console.error("Failed to load AWS roles:", err);
}
}

init();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Expand All @@ -22,6 +46,7 @@ export default function ConfigurePage() {
setBusy(false);
}

console.log("[ConfigurePage] pipeline.result:", pipeline.result);
return (
<section style={{ display: "grid", gap: 16 }}>
<h1>Configure Pipeline</h1>
Expand Down Expand Up @@ -60,23 +85,45 @@ export default function ConfigurePage() {

<label>
AWS Role (OIDC)
<select value={pipeline.options.awsRoleArn ?? ""} onChange={(e)=>pipeline.setOption("awsRoleArn", e.target.value)} style={{ display: "block", padding: 8 }}>
<select disabled={busy} value={pipeline.options.awsRoleArn ?? ""} onChange={(e)=>pipeline.setOption("awsRoleArn", e.target.value)} style={{ display: "block", padding: 8 }}>
<option value="">-- select --</option>
{pipeline.roles?.map((r) => <option key={r} value={r}>{r}</option>)}
{pipeline.roles?.map((r) => (
<option key={r.arn} value={r.arn}>
{r.name}
</option>
))}
</select>
</label>

<div style={{ display: "flex", gap: 8 }}>
<button onClick={onGenerate} disabled={busy}>{busy ? "Generating…" : "Generate Pipeline"}</button>
<Link to="/secrets">
<button disabled={!pipeline.result?.generated_yaml}>Continue → Secrets</button>
</Link>
<button
onClick={() => {
console.log("[ConfigurePage] Navigate button clicked.");
console.log("[ConfigurePage] Pipeline result before navigating:", pipeline.result);
try {
navigate("/secrets", { state: { pipeline: pipeline.result } });
console.log("[ConfigurePage] ✅ Navigation triggered successfully.");
} catch (err) {
console.error("[ConfigurePage] ❌ Navigation failed:", err);
}
}}
disabled={
!(
pipeline.result?.yaml ||
pipeline.result?.generated_yaml ||
pipeline.result?.data?.generated_yaml
)
}
>
Continue → Secrets
</button>
</div>

<div>
<div>YAML Preview</div>
<pre style={{ maxHeight: 400, overflow: "auto", background: "#f6f6f6", padding: 12 }}>
{pipeline.result?.generated_yaml ?? "Click Generate Pipeline to preview YAML…"}
{pipeline.result?.yaml ?? pipeline.result?.generated_yaml ?? "Click Generate Pipeline to preview YAML…"}
</pre>
</div>
</section>
Expand Down
60 changes: 52 additions & 8 deletions client/src/store/usePipelineStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type PipelineState = {
result?: McpPipeline;

// local UI state
roles: string[];
roles: { name: string; arn: string }[];
editing: boolean;
editedYaml?: string;
status: "idle" | "loading" | "success" | "error";
Expand Down Expand Up @@ -67,22 +67,66 @@ export const usePipelineStore = create<PipelineState & PipelineActions>()((set,
setOption: (k, v) => set({ options: { ...get().options, [k]: v } }),

async loadAwsRoles() {
const { roles } = await api.listAwsRoles();
set({ roles });
const { options } = get();
if (!options.awsRoleArn && roles[0]) set({ options: { ...options, awsRoleArn: roles[0] } });
try {
const res = await api.listAwsRoles();

// Normalize both fetch-style and axios-style responses
const payload = res?.data ?? res; // if axios -> res.data, if fetch -> res
// Roles can live at payload.data.roles or payload.roles depending on server/helper
const roles =
payload?.data?.roles ??
payload?.roles ??
payload?.data?.data?.roles ??
[];

console.log("[usePipelineStore] Raw roles payload:", payload);
console.log("[usePipelineStore] Loaded roles (final):", roles);

// Normalize to objects even if backend returned strings
const normalizedRoles = roles.map((r: any) =>
typeof r === "string" ? { name: r.split("/").pop(), arn: r } : r
);

set({ roles: normalizedRoles });

const { options } = get();
if (!options.awsRoleArn && normalizedRoles[0]) {
set({ options: { ...options, awsRoleArn: normalizedRoles[0].arn } });
}
} catch (err) {
console.error("[usePipelineStore] Failed to load AWS roles:", err);
set({ roles: [] });
}
},

async regenerate({ repo, branch }) {
set({ status: "loading", error: undefined });
try {
const { template, stages, options } = get();
const data = await api.createPipeline({
repo, branch, service: "ci-cd-generator", template,
const res = await api.createPipeline({
repo,
branch,
service: "ci-cd-generator",
template,
options: { ...options, stages },
});
set({ result: data, status: "success", editing: false, editedYaml: undefined });

const generated_yaml =
res?.data?.data?.generated_yaml ||
res?.data?.generated_yaml ||
res?.generated_yaml ||
"";

set({
result: { ...res, yaml: generated_yaml },
status: "success",
editing: false,
editedYaml: undefined,
});

console.log("[usePipelineStore] YAML generated:", generated_yaml.slice(0, 80));
} catch (e: any) {
console.error("[usePipelineStore] regenerate error:", e);
set({ status: "error", error: e.message });
}
},
Expand Down
Loading