create-interview-cockpit 0.17.3 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/client/src/App.tsx +3 -0
- package/template/client/src/api.ts +83 -8
- package/template/client/src/components/GithubActionsLabModal.tsx +746 -0
- package/template/client/src/components/InfraLabModal.tsx +993 -262
- package/template/client/src/components/LabsPanel.tsx +71 -5
- package/template/client/src/components/Sidebar.tsx +400 -14
- package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
- package/template/client/src/enterpriseLocalLab.ts +921 -0
- package/template/client/src/githubActionsLab.ts +287 -0
- package/template/client/src/infraLab.ts +378 -6
- package/template/client/src/reactLab.ts +409 -0
- package/template/client/src/store.ts +83 -10
- package/template/client/src/types.ts +27 -3
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/cockpit.json +1 -1
- package/template/server/src/gha-runner.ts +468 -0
- package/template/server/src/google-drive.ts +35 -24
- package/template/server/src/index.ts +241 -10
- package/template/server/src/infra-runner.ts +321 -30
- package/template/server/src/storage.ts +3 -1
|
@@ -1206,6 +1206,415 @@ export const DEFAULT_NEXTJS_LAB: FrontendLabWorkspace = {
|
|
|
1206
1206
|
files: NEXTJS_DEFAULT_FILES,
|
|
1207
1207
|
};
|
|
1208
1208
|
|
|
1209
|
+
const NEXTJS_BFF_AUTH_CLIENT_FILES: Record<string, string> = {
|
|
1210
|
+
"README.md": `# Next.js BFF Auth Client Lab
|
|
1211
|
+
|
|
1212
|
+
This lab is intentionally separate from the Infrastructure Lab.
|
|
1213
|
+
|
|
1214
|
+
## Flow
|
|
1215
|
+
|
|
1216
|
+
1. Deploy the infrastructure lab first:
|
|
1217
|
+
- terraform init
|
|
1218
|
+
- terraform plan
|
|
1219
|
+
- terraform apply -auto-approve
|
|
1220
|
+
2. Start this Next.js lab.
|
|
1221
|
+
3. Click **Sign in through BFF**.
|
|
1222
|
+
4. The browser goes to the deployed BFF at http://localhost:4300.
|
|
1223
|
+
5. The BFF redirects to the local Cognito-like provider.
|
|
1224
|
+
6. After sign-in, the BFF stores tokens in Redis and redirects back to this Next.js app.
|
|
1225
|
+
7. This app calls the BFF with credentials included.
|
|
1226
|
+
|
|
1227
|
+
## Why separate labs?
|
|
1228
|
+
|
|
1229
|
+
- Infra Lab owns deployment.
|
|
1230
|
+
- Next.js Lab owns the frontend shell/client experience.
|
|
1231
|
+
- The integration point is the BFF URL: http://localhost:4300.
|
|
1232
|
+
`,
|
|
1233
|
+
"app/layout.tsx": `import "./globals.css";
|
|
1234
|
+
|
|
1235
|
+
export const metadata = {
|
|
1236
|
+
title: "Enterprise Auth Client Lab",
|
|
1237
|
+
description: "Next.js client talking to a locally deployed BFF",
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1241
|
+
return (
|
|
1242
|
+
<html lang="en">
|
|
1243
|
+
<body>{children}</body>
|
|
1244
|
+
</html>
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
`,
|
|
1248
|
+
"app/page.tsx": `import { AuthDashboard } from "../components/AuthDashboard";
|
|
1249
|
+
|
|
1250
|
+
export default function HomePage() {
|
|
1251
|
+
return <AuthDashboard />;
|
|
1252
|
+
}
|
|
1253
|
+
`,
|
|
1254
|
+
"app/globals.css": `* {
|
|
1255
|
+
box-sizing: border-box;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
html,
|
|
1259
|
+
body {
|
|
1260
|
+
margin: 0;
|
|
1261
|
+
min-height: 100%;
|
|
1262
|
+
background:
|
|
1263
|
+
radial-gradient(circle at top left, rgba(6, 182, 212, 0.18), transparent 30rem),
|
|
1264
|
+
radial-gradient(circle at bottom right, rgba(124, 58, 237, 0.16), transparent 28rem),
|
|
1265
|
+
#020617;
|
|
1266
|
+
color: #e2e8f0;
|
|
1267
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
button,
|
|
1271
|
+
textarea {
|
|
1272
|
+
font: inherit;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
button {
|
|
1276
|
+
cursor: pointer;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
.shell {
|
|
1280
|
+
width: min(1120px, calc(100vw - 32px));
|
|
1281
|
+
margin: 0 auto;
|
|
1282
|
+
padding: 48px 0;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.hero,
|
|
1286
|
+
.card {
|
|
1287
|
+
border: 1px solid rgba(148, 163, 184, 0.18);
|
|
1288
|
+
background: rgba(15, 23, 42, 0.78);
|
|
1289
|
+
box-shadow: 0 24px 80px rgba(2, 6, 23, 0.45);
|
|
1290
|
+
backdrop-filter: blur(18px);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.hero {
|
|
1294
|
+
border-radius: 28px;
|
|
1295
|
+
padding: 40px;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.eyebrow {
|
|
1299
|
+
margin: 0 0 12px;
|
|
1300
|
+
color: #22d3ee;
|
|
1301
|
+
font-size: 0.78rem;
|
|
1302
|
+
font-weight: 800;
|
|
1303
|
+
letter-spacing: 0.22em;
|
|
1304
|
+
text-transform: uppercase;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
h1,
|
|
1308
|
+
h2,
|
|
1309
|
+
p {
|
|
1310
|
+
margin-top: 0;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
h1 {
|
|
1314
|
+
max-width: 760px;
|
|
1315
|
+
margin-bottom: 12px;
|
|
1316
|
+
color: #f8fafc;
|
|
1317
|
+
font-size: clamp(2.25rem, 6vw, 4.5rem);
|
|
1318
|
+
line-height: 0.95;
|
|
1319
|
+
letter-spacing: -0.06em;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
.hero p {
|
|
1323
|
+
max-width: 720px;
|
|
1324
|
+
color: #cbd5e1;
|
|
1325
|
+
font-size: 1.05rem;
|
|
1326
|
+
line-height: 1.7;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
.actions {
|
|
1330
|
+
display: flex;
|
|
1331
|
+
flex-wrap: wrap;
|
|
1332
|
+
gap: 12px;
|
|
1333
|
+
margin-top: 28px;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
button {
|
|
1337
|
+
border: 1px solid rgba(148, 163, 184, 0.26);
|
|
1338
|
+
border-radius: 999px;
|
|
1339
|
+
background: rgba(15, 23, 42, 0.92);
|
|
1340
|
+
color: #e2e8f0;
|
|
1341
|
+
padding: 11px 16px;
|
|
1342
|
+
font-weight: 800;
|
|
1343
|
+
transition: transform 150ms ease, border-color 150ms ease, background 150ms ease;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
button:hover:not(:disabled) {
|
|
1347
|
+
transform: translateY(-1px);
|
|
1348
|
+
border-color: rgba(34, 211, 238, 0.7);
|
|
1349
|
+
background: rgba(8, 47, 73, 0.9);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
button:disabled {
|
|
1353
|
+
cursor: not-allowed;
|
|
1354
|
+
opacity: 0.45;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
button.primary {
|
|
1358
|
+
border-color: rgba(34, 211, 238, 0.5);
|
|
1359
|
+
background: linear-gradient(135deg, #06b6d4, #8b5cf6);
|
|
1360
|
+
color: #020617;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
.grid {
|
|
1364
|
+
display: grid;
|
|
1365
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1366
|
+
gap: 18px;
|
|
1367
|
+
margin-top: 18px;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
.card {
|
|
1371
|
+
border-radius: 22px;
|
|
1372
|
+
padding: 24px;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
.card.wide {
|
|
1376
|
+
grid-column: 1 / -1;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
.cardHeader {
|
|
1380
|
+
display: flex;
|
|
1381
|
+
align-items: center;
|
|
1382
|
+
gap: 12px;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
.cardHeader span {
|
|
1386
|
+
display: inline-grid;
|
|
1387
|
+
width: 30px;
|
|
1388
|
+
height: 30px;
|
|
1389
|
+
place-items: center;
|
|
1390
|
+
border-radius: 999px;
|
|
1391
|
+
background: rgba(34, 211, 238, 0.14);
|
|
1392
|
+
color: #67e8f9;
|
|
1393
|
+
font-weight: 900;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
h2 {
|
|
1397
|
+
margin-bottom: 0;
|
|
1398
|
+
color: #f8fafc;
|
|
1399
|
+
font-size: 1rem;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
.hint {
|
|
1403
|
+
margin: 12px 0 16px;
|
|
1404
|
+
color: #94a3b8;
|
|
1405
|
+
line-height: 1.6;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
pre,
|
|
1409
|
+
textarea {
|
|
1410
|
+
width: 100%;
|
|
1411
|
+
border: 1px solid rgba(51, 65, 85, 0.95);
|
|
1412
|
+
border-radius: 16px;
|
|
1413
|
+
background: rgba(2, 6, 23, 0.8);
|
|
1414
|
+
color: #cbd5e1;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
pre {
|
|
1418
|
+
min-height: 164px;
|
|
1419
|
+
overflow: auto;
|
|
1420
|
+
padding: 16px;
|
|
1421
|
+
font-size: 0.82rem;
|
|
1422
|
+
line-height: 1.55;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
textarea {
|
|
1426
|
+
display: block;
|
|
1427
|
+
margin-bottom: 12px;
|
|
1428
|
+
padding: 14px;
|
|
1429
|
+
resize: vertical;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
@media (max-width: 820px) {
|
|
1433
|
+
.grid {
|
|
1434
|
+
grid-template-columns: 1fr;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
`,
|
|
1438
|
+
"components/AuthDashboard.tsx": `"use client";
|
|
1439
|
+
|
|
1440
|
+
import { useEffect, useMemo, useState } from "react";
|
|
1441
|
+
|
|
1442
|
+
const BFF_BASE_URL = "http://localhost:4300";
|
|
1443
|
+
|
|
1444
|
+
type ApiResult = {
|
|
1445
|
+
status: number;
|
|
1446
|
+
body: unknown;
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
function getCookie(name: string): string | null {
|
|
1450
|
+
const match = document.cookie
|
|
1451
|
+
.split("; ")
|
|
1452
|
+
.find((row) => row.startsWith(name + "="));
|
|
1453
|
+
|
|
1454
|
+
return match ? decodeURIComponent(match.split("=")[1]) : null;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
async function bffFetch(path: string, init: RequestInit = {}): Promise<ApiResult> {
|
|
1458
|
+
const method = (init.method || "GET").toUpperCase();
|
|
1459
|
+
const headers = new Headers(init.headers || {});
|
|
1460
|
+
|
|
1461
|
+
if (!["GET", "HEAD", "OPTIONS"].includes(method)) {
|
|
1462
|
+
const csrf = getCookie("XSRF-TOKEN");
|
|
1463
|
+
if (csrf) headers.set("x-csrf-token", csrf);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const response = await fetch(BFF_BASE_URL + path, {
|
|
1467
|
+
...init,
|
|
1468
|
+
headers,
|
|
1469
|
+
credentials: "include",
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
const text = await response.text();
|
|
1473
|
+
let body: unknown = text;
|
|
1474
|
+
try {
|
|
1475
|
+
body = text ? JSON.parse(text) : null;
|
|
1476
|
+
} catch {
|
|
1477
|
+
body = text;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
return { status: response.status, body };
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
export function AuthDashboard() {
|
|
1484
|
+
const [me, setMe] = useState<ApiResult | null>(null);
|
|
1485
|
+
const [claim, setClaim] = useState<ApiResult | null>(null);
|
|
1486
|
+
const [noteResult, setNoteResult] = useState<ApiResult | null>(null);
|
|
1487
|
+
const [note, setNote] = useState("Customer uploaded first notice of loss.");
|
|
1488
|
+
const [loading, setLoading] = useState<string | null>(null);
|
|
1489
|
+
|
|
1490
|
+
const isAuthenticated = useMemo(() => {
|
|
1491
|
+
if (!me || typeof me.body !== "object" || me.body === null) return false;
|
|
1492
|
+
return Boolean((me.body as { authenticated?: boolean }).authenticated);
|
|
1493
|
+
}, [me]);
|
|
1494
|
+
|
|
1495
|
+
async function run(label: string, action: () => Promise<void>) {
|
|
1496
|
+
setLoading(label);
|
|
1497
|
+
try {
|
|
1498
|
+
await action();
|
|
1499
|
+
} finally {
|
|
1500
|
+
setLoading(null);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async function refreshMe() {
|
|
1505
|
+
setMe(await bffFetch("/auth/me"));
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
useEffect(() => {
|
|
1509
|
+
void refreshMe();
|
|
1510
|
+
}, []);
|
|
1511
|
+
|
|
1512
|
+
function login() {
|
|
1513
|
+
const returnTo = encodeURIComponent(window.location.origin);
|
|
1514
|
+
window.location.href = BFF_BASE_URL + "/auth/login?returnTo=" + returnTo;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
async function logout() {
|
|
1518
|
+
await run("logout", async () => {
|
|
1519
|
+
await bffFetch("/auth/logout", { method: "POST" });
|
|
1520
|
+
await refreshMe();
|
|
1521
|
+
setClaim(null);
|
|
1522
|
+
setNoteResult(null);
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
async function loadClaim() {
|
|
1527
|
+
await run("claim", async () => {
|
|
1528
|
+
setClaim(await bffFetch("/api/claims/CLM-1001"));
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
async function addNote() {
|
|
1533
|
+
await run("note", async () => {
|
|
1534
|
+
setNoteResult(
|
|
1535
|
+
await bffFetch("/api/claims/CLM-1001/notes", {
|
|
1536
|
+
method: "POST",
|
|
1537
|
+
headers: { "content-type": "application/json" },
|
|
1538
|
+
body: JSON.stringify({ text: note }),
|
|
1539
|
+
}),
|
|
1540
|
+
);
|
|
1541
|
+
await loadClaim();
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
return (
|
|
1546
|
+
<main className="shell">
|
|
1547
|
+
<section className="hero">
|
|
1548
|
+
<p className="eyebrow">Next.js shell + deployed BFF</p>
|
|
1549
|
+
<h1>Enterprise BFF Auth Client</h1>
|
|
1550
|
+
<p>
|
|
1551
|
+
This frontend stores no bearer tokens. It redirects through the BFF,
|
|
1552
|
+
then calls BFF endpoints with cookies and CSRF protection.
|
|
1553
|
+
</p>
|
|
1554
|
+
<div className="actions">
|
|
1555
|
+
<button className="primary" onClick={login}>Sign in through BFF</button>
|
|
1556
|
+
<button onClick={() => void refreshMe()}>Refresh /auth/me</button>
|
|
1557
|
+
<button onClick={() => void logout()} disabled={!isAuthenticated || loading === "logout"}>
|
|
1558
|
+
Logout
|
|
1559
|
+
</button>
|
|
1560
|
+
</div>
|
|
1561
|
+
</section>
|
|
1562
|
+
|
|
1563
|
+
<section className="grid">
|
|
1564
|
+
<article className="card">
|
|
1565
|
+
<div className="cardHeader">
|
|
1566
|
+
<span>1</span>
|
|
1567
|
+
<h2>Session</h2>
|
|
1568
|
+
</div>
|
|
1569
|
+
<p className="hint">The browser sees only cookie-backed auth state.</p>
|
|
1570
|
+
<pre>{JSON.stringify(me?.body ?? "Not loaded", null, 2)}</pre>
|
|
1571
|
+
</article>
|
|
1572
|
+
|
|
1573
|
+
<article className="card">
|
|
1574
|
+
<div className="cardHeader">
|
|
1575
|
+
<span>2</span>
|
|
1576
|
+
<h2>Downstream API through BFF</h2>
|
|
1577
|
+
</div>
|
|
1578
|
+
<p className="hint">The client never calls the claims API directly.</p>
|
|
1579
|
+
<button onClick={() => void loadClaim()} disabled={!isAuthenticated || loading === "claim"}>
|
|
1580
|
+
Load claim CLM-1001
|
|
1581
|
+
</button>
|
|
1582
|
+
<pre>{JSON.stringify(claim?.body ?? "No claim loaded", null, 2)}</pre>
|
|
1583
|
+
</article>
|
|
1584
|
+
|
|
1585
|
+
<article className="card wide">
|
|
1586
|
+
<div className="cardHeader">
|
|
1587
|
+
<span>3</span>
|
|
1588
|
+
<h2>Write with CSRF header</h2>
|
|
1589
|
+
</div>
|
|
1590
|
+
<p className="hint">
|
|
1591
|
+
The readable XSRF-TOKEN cookie is copied into x-csrf-token for writes.
|
|
1592
|
+
</p>
|
|
1593
|
+
<textarea value={note} onChange={(event) => setNote(event.target.value)} rows={3} />
|
|
1594
|
+
<button onClick={() => void addNote()} disabled={!isAuthenticated || loading === "note"}>
|
|
1595
|
+
Add note
|
|
1596
|
+
</button>
|
|
1597
|
+
<pre>{JSON.stringify(noteResult?.body ?? "No note submitted", null, 2)}</pre>
|
|
1598
|
+
</article>
|
|
1599
|
+
</section>
|
|
1600
|
+
</main>
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
`,
|
|
1604
|
+
"components/AuthDashboard.module.css": `/* Optional scratch file: move styles here if you want CSS modules practice. */
|
|
1605
|
+
`,
|
|
1606
|
+
"app/styles.css": `/* Optional scratch file for experimenting with additional global styles. */
|
|
1607
|
+
`,
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
export const NEXTJS_BFF_AUTH_CLIENT_LAB: FrontendLabWorkspace = {
|
|
1611
|
+
version: 1,
|
|
1612
|
+
label: "Next.js BFF Auth Client",
|
|
1613
|
+
type: "nextjs",
|
|
1614
|
+
activeFile: "components/AuthDashboard.tsx",
|
|
1615
|
+
files: NEXTJS_BFF_AUTH_CLIENT_FILES,
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1209
1618
|
export const DEFAULT_MODULE_FEDERATION_LAB: FrontendLabWorkspace = {
|
|
1210
1619
|
version: 1,
|
|
1211
1620
|
label: "Webpack Module Federation Lab",
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
WorkspaceMeta,
|
|
7
7
|
InfraLabWorkspace,
|
|
8
8
|
FrontendLabWorkspace,
|
|
9
|
+
GithubActionsLabWorkspace,
|
|
9
10
|
ContextFileOrigin,
|
|
10
11
|
} from "./types";
|
|
11
12
|
import type { AiSettings } from "./api";
|
|
@@ -126,12 +127,7 @@ interface Store {
|
|
|
126
127
|
deleteWorkspace: (id: string) => Promise<void>;
|
|
127
128
|
renameWorkspace: (id: string, name: string) => Promise<void>;
|
|
128
129
|
patchWorkspace: (id: string, data: object) => Promise<void>;
|
|
129
|
-
syncWorkspace: (id: string) => Promise<
|
|
130
|
-
topicsUpserted: number;
|
|
131
|
-
filesImported: number;
|
|
132
|
-
filesSkipped: number;
|
|
133
|
-
errors: string[];
|
|
134
|
-
}>;
|
|
130
|
+
syncWorkspace: (id: string) => Promise<import("./api").SyncWorkspaceResult>;
|
|
135
131
|
linkDriveFolder: (
|
|
136
132
|
workspaceId: string,
|
|
137
133
|
url: string,
|
|
@@ -176,6 +172,12 @@ interface Store {
|
|
|
176
172
|
topicId: string,
|
|
177
173
|
parentQuestionId: string | null,
|
|
178
174
|
) => Promise<void>;
|
|
175
|
+
copyQuestion: (
|
|
176
|
+
questionId: string,
|
|
177
|
+
topicId: string,
|
|
178
|
+
parentQuestionId: string | null,
|
|
179
|
+
targetTopicId?: string,
|
|
180
|
+
) => Promise<void>;
|
|
179
181
|
removeQuestion: (questionId: string, topicId: string) => Promise<void>;
|
|
180
182
|
renameQuestion: (
|
|
181
183
|
questionId: string,
|
|
@@ -330,6 +332,13 @@ interface Store {
|
|
|
330
332
|
openInfraLab: (workspace?: InfraLabWorkspace, fileId?: string) => void;
|
|
331
333
|
closeInfraLab: () => void;
|
|
332
334
|
|
|
335
|
+
// ── GitHub Actions Lab ───────────────────────────────────────
|
|
336
|
+
showGhaLab: boolean;
|
|
337
|
+
runnerInitialGha: GithubActionsLabWorkspace | null;
|
|
338
|
+
runnerInitialGhaFileId: string | null;
|
|
339
|
+
openGhaLab: (workspace?: GithubActionsLabWorkspace, fileId?: string) => void;
|
|
340
|
+
closeGhaLab: () => void;
|
|
341
|
+
|
|
333
342
|
// ── Frontend Labs (React / Next.js / Module Federation) — open inside the sandbox ──
|
|
334
343
|
openReactLab: (
|
|
335
344
|
workspace?: FrontendLabWorkspace,
|
|
@@ -387,6 +396,9 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
387
396
|
showInfraLab: false,
|
|
388
397
|
runnerInitialInfra: null,
|
|
389
398
|
runnerInitialInfraFileId: null,
|
|
399
|
+
showGhaLab: false,
|
|
400
|
+
runnerInitialGha: null,
|
|
401
|
+
runnerInitialGhaFileId: null,
|
|
390
402
|
showCanvasLab: false,
|
|
391
403
|
canvasLabInitialCode: null,
|
|
392
404
|
canvasLabInitialFileId: null,
|
|
@@ -467,9 +479,15 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
467
479
|
|
|
468
480
|
syncWorkspace: async (id) => {
|
|
469
481
|
const result = await api.syncWorkspaceApi(id);
|
|
482
|
+
if ("needsAuth" in result && result.needsAuth) {
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
470
485
|
if (id === get().activeWorkspaceId) {
|
|
471
|
-
const topics = await
|
|
472
|
-
|
|
486
|
+
const [topics, workspaceFiles] = await Promise.all([
|
|
487
|
+
api.fetchTopics(),
|
|
488
|
+
api.fetchWorkspaceFiles(),
|
|
489
|
+
]);
|
|
490
|
+
set({ topics, workspaceFiles, questionsByTopic: {} });
|
|
473
491
|
}
|
|
474
492
|
const registry = await api.fetchWorkspaces();
|
|
475
493
|
set({ workspaces: registry.workspaces });
|
|
@@ -491,8 +509,15 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
491
509
|
if (workspaceId === get().activeWorkspaceId) {
|
|
492
510
|
set({ topics: [], questionsByTopic: {} });
|
|
493
511
|
const result = await api.syncWorkspaceApi(workspaceId);
|
|
494
|
-
|
|
495
|
-
|
|
512
|
+
if ("needsAuth" in result && result.needsAuth) {
|
|
513
|
+
window.location.href = result.authUrl;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const [topics, workspaceFiles] = await Promise.all([
|
|
517
|
+
api.fetchTopics(),
|
|
518
|
+
api.fetchWorkspaceFiles(),
|
|
519
|
+
]);
|
|
520
|
+
set({ topics, workspaceFiles, questionsByTopic: {} });
|
|
496
521
|
const reg2 = await api.fetchWorkspaces();
|
|
497
522
|
set({ workspaces: reg2.workspaces });
|
|
498
523
|
}
|
|
@@ -508,6 +533,7 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
508
533
|
selectedTopicId: null,
|
|
509
534
|
selectedQuestionId: null,
|
|
510
535
|
currentQuestion: null,
|
|
536
|
+
workspaceFiles: [],
|
|
511
537
|
});
|
|
512
538
|
},
|
|
513
539
|
|
|
@@ -629,6 +655,39 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
629
655
|
}));
|
|
630
656
|
},
|
|
631
657
|
|
|
658
|
+
copyQuestion: async (
|
|
659
|
+
questionId,
|
|
660
|
+
topicId,
|
|
661
|
+
parentQuestionId,
|
|
662
|
+
targetTopicId,
|
|
663
|
+
) => {
|
|
664
|
+
const destinationTopicId = targetTopicId ?? topicId;
|
|
665
|
+
const copied = await api.copyQuestion(questionId, {
|
|
666
|
+
parentQuestionId,
|
|
667
|
+
targetTopicId: destinationTopicId,
|
|
668
|
+
});
|
|
669
|
+
const copiedRoot = copied[0];
|
|
670
|
+
set((s) => ({
|
|
671
|
+
questionsByTopic: {
|
|
672
|
+
...s.questionsByTopic,
|
|
673
|
+
[destinationTopicId]: [
|
|
674
|
+
...(s.questionsByTopic[destinationTopicId] || []),
|
|
675
|
+
...copied,
|
|
676
|
+
],
|
|
677
|
+
},
|
|
678
|
+
selectedTopicId: copiedRoot?.id ? destinationTopicId : s.selectedTopicId,
|
|
679
|
+
selectedQuestionId: copiedRoot?.id ?? s.selectedQuestionId,
|
|
680
|
+
currentQuestion: copiedRoot ?? s.currentQuestion,
|
|
681
|
+
expandedTopics: s.expandedTopics.includes(destinationTopicId)
|
|
682
|
+
? s.expandedTopics
|
|
683
|
+
: [...s.expandedTopics, destinationTopicId],
|
|
684
|
+
}));
|
|
685
|
+
if (copiedRoot) {
|
|
686
|
+
sessionStorage.setItem("lastTopicId", destinationTopicId);
|
|
687
|
+
sessionStorage.setItem("lastQuestionId", copiedRoot.id);
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
|
|
632
691
|
removeQuestion: async (questionId, topicId) => {
|
|
633
692
|
await api.deleteQuestion(questionId);
|
|
634
693
|
set((s) => ({
|
|
@@ -1079,6 +1138,20 @@ export const useStore = create<Store>((set, get) => ({
|
|
|
1079
1138
|
openBrowserSecurityLab: () => set({ showBrowserSecurityLab: true }),
|
|
1080
1139
|
closeBrowserSecurityLab: () => set({ showBrowserSecurityLab: false }),
|
|
1081
1140
|
closeInfraLab: () => set({ showInfraLab: false }),
|
|
1141
|
+
openGhaLab: (workspace, fileId?) =>
|
|
1142
|
+
set({
|
|
1143
|
+
showGhaLab: true,
|
|
1144
|
+
showInfraLab: false,
|
|
1145
|
+
showCodeRunner: false,
|
|
1146
|
+
runnerInitialGha: workspace ?? null,
|
|
1147
|
+
runnerInitialGhaFileId: fileId ?? null,
|
|
1148
|
+
}),
|
|
1149
|
+
closeGhaLab: () =>
|
|
1150
|
+
set({
|
|
1151
|
+
showGhaLab: false,
|
|
1152
|
+
runnerInitialGha: null,
|
|
1153
|
+
runnerInitialGhaFileId: null,
|
|
1154
|
+
}),
|
|
1082
1155
|
openCanvasLab: (code?, fileId?) =>
|
|
1083
1156
|
set({
|
|
1084
1157
|
showCanvasLab: true,
|
|
@@ -8,7 +8,8 @@ export type ContextFileOrigin =
|
|
|
8
8
|
| "react"
|
|
9
9
|
| "nextjs"
|
|
10
10
|
| "module-federation"
|
|
11
|
-
| "canvas"
|
|
11
|
+
| "canvas"
|
|
12
|
+
| "github-actions";
|
|
12
13
|
|
|
13
14
|
export interface ContextFile {
|
|
14
15
|
id: string;
|
|
@@ -42,12 +43,23 @@ export interface FrontendLabWorkspace {
|
|
|
42
43
|
export interface InfraLabWorkspace {
|
|
43
44
|
version: 1;
|
|
44
45
|
label: string;
|
|
45
|
-
provider: "aws";
|
|
46
|
-
executionMode: "plan-only" | "localstack";
|
|
46
|
+
provider: "aws" | "docker";
|
|
47
|
+
executionMode: "plan-only" | "localstack" | "docker";
|
|
47
48
|
activeFile: string;
|
|
48
49
|
files: Record<string, string>;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
export interface GithubActionsLabWorkspace {
|
|
53
|
+
version: 1;
|
|
54
|
+
label: string;
|
|
55
|
+
activeFile: string;
|
|
56
|
+
files: Record<string, string>;
|
|
57
|
+
/** Optional default event the run button uses (push, pull_request, workflow_dispatch). */
|
|
58
|
+
defaultEvent?: string;
|
|
59
|
+
/** Optional default workflow file path under .github/workflows. */
|
|
60
|
+
defaultWorkflow?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
export interface WorkspaceMeta {
|
|
52
64
|
id: string;
|
|
53
65
|
name: string;
|
|
@@ -116,6 +128,16 @@ export interface ReadingBookmark {
|
|
|
116
128
|
blockIndex: number;
|
|
117
129
|
}
|
|
118
130
|
|
|
131
|
+
export interface StoredCodeAnnotation {
|
|
132
|
+
id: string;
|
|
133
|
+
lineNumber: number;
|
|
134
|
+
lineContent: string;
|
|
135
|
+
prompt: string;
|
|
136
|
+
response: string;
|
|
137
|
+
filePath: string;
|
|
138
|
+
createdAt: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
export interface Question {
|
|
120
142
|
id: string;
|
|
121
143
|
topicId: string;
|
|
@@ -127,6 +149,8 @@ export interface Question {
|
|
|
127
149
|
messages: Message[];
|
|
128
150
|
annotations?: Annotation[];
|
|
129
151
|
readingBookmark?: ReadingBookmark;
|
|
152
|
+
/** Code-line annotations keyed by file path. */
|
|
153
|
+
codeAnnotations?: { [filePath: string]: StoredCodeAnnotation[] };
|
|
130
154
|
/** IDs of other questions in the same topic whose conversation history is injected as context. */
|
|
131
155
|
linkedConversationIds?: string[];
|
|
132
156
|
createdAt: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/app.tsx","./src/api.ts","./src/browsersecuritytemplates.ts","./src/infralab.ts","./src/main.tsx","./src/reactlab.ts","./src/store.ts","./src/types.ts","./src/vite-env.d.ts","./src/components/aisettingsmodal.tsx","./src/components/annotationdialog.tsx","./src/components/browsersecuritylabmodal.tsx","./src/components/canvaslabmodal.tsx","./src/components/chatmessage.tsx","./src/components/chatview.tsx","./src/components/codecontextpanel.tsx","./src/components/codelineannotationpopup.tsx","./src/components/coderunnermodal.tsx","./src/components/deploymentlabmodal.tsx","./src/components/docrefmodal.tsx","./src/components/fileattachments.tsx","./src/components/filepickermodal.tsx","./src/components/fileviewermodal.tsx","./src/components/infralabmodal.tsx","./src/components/labspanel.tsx","./src/components/linkedconvospicker.tsx","./src/components/markdownrenderer.tsx","./src/components/mermaiddiagram.tsx","./src/components/notesmodal.tsx","./src/components/plotembed.tsx","./src/components/sidebar.tsx","./src/components/textannotator.tsx","./src/components/vizcraftembed.tsx","./src/components/workspaceswitcher.tsx"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/app.tsx","./src/api.ts","./src/browsersecuritytemplates.ts","./src/enterpriselocallab.ts","./src/githubactionslab.ts","./src/infralab.ts","./src/main.tsx","./src/reactlab.ts","./src/store.ts","./src/types.ts","./src/vite-env.d.ts","./src/components/aisettingsmodal.tsx","./src/components/annotationdialog.tsx","./src/components/browsersecuritylabmodal.tsx","./src/components/canvaslabmodal.tsx","./src/components/chatmessage.tsx","./src/components/chatview.tsx","./src/components/codecontextpanel.tsx","./src/components/codelineannotationpopup.tsx","./src/components/coderunnermodal.tsx","./src/components/deploymentlabmodal.tsx","./src/components/docrefmodal.tsx","./src/components/fileattachments.tsx","./src/components/filepickermodal.tsx","./src/components/fileviewermodal.tsx","./src/components/githubactionslabmodal.tsx","./src/components/infralabmodal.tsx","./src/components/labspanel.tsx","./src/components/linkedconvospicker.tsx","./src/components/markdownrenderer.tsx","./src/components/mermaiddiagram.tsx","./src/components/notesmodal.tsx","./src/components/plotembed.tsx","./src/components/sidebar.tsx","./src/components/textannotator.tsx","./src/components/vizcraftembed.tsx","./src/components/workspaceswitcher.tsx"],"version":"5.9.3"}
|
package/template/cockpit.json
CHANGED