create-interview-cockpit 0.28.0 → 0.30.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.
@@ -3,6 +3,8 @@ import {
3
3
  ChevronDown,
4
4
  ChevronRight,
5
5
  Copy,
6
+ Eye,
7
+ EyeOff,
6
8
  FilePlus,
7
9
  Folder,
8
10
  GitPullRequest,
@@ -24,6 +26,8 @@ import {
24
26
  X,
25
27
  } from "lucide-react";
26
28
  import MonacoEditorLib from "@monaco-editor/react";
29
+ import ReactMarkdown from "react-markdown";
30
+ import remarkGfm from "remark-gfm";
27
31
  import type {
28
32
  BeforeMount,
29
33
  Monaco,
@@ -346,6 +350,11 @@ export default function GithubActionsLabModal() {
346
350
  const [historyNonce, setHistoryNonce] = useState(0);
347
351
  const [leftCollapsed, setLeftCollapsed] = useState(false);
348
352
  const [rightCollapsed, setRightCollapsed] = useState(false);
353
+ // When viewing a markdown file, let the user toggle between the raw
354
+ // editor and a rendered preview. We default to preview because most
355
+ // of the .md files in the governance template are explanatory docs
356
+ // — readers want them to look like real GitHub markdown, not source.
357
+ const [mdPreview, setMdPreview] = useState(true);
349
358
  const abortRef = useRef<AbortController | null>(null);
350
359
  // ── Concurrency engine state ───────────────────────────────────────
351
360
  // The concurrency tab is no longer a simulator — these records track
@@ -2188,48 +2197,80 @@ interface ImportMeta {
2188
2197
  {activeFile || "(no file selected)"}
2189
2198
  </span>
2190
2199
  </div>
2191
- <button
2192
- onClick={() => setRightCollapsed((v) => !v)}
2193
- className="p-1 rounded text-slate-500 hover:text-amber-300 hover:bg-slate-800/60 shrink-0"
2194
- title={
2195
- rightCollapsed
2196
- ? "Show console/jobs panel"
2197
- : "Hide console/jobs panel"
2198
- }
2199
- >
2200
- {rightCollapsed ? (
2201
- <PanelRightOpen className="w-3.5 h-3.5" />
2202
- ) : (
2203
- <PanelRightClose className="w-3.5 h-3.5" />
2200
+ <div className="flex items-center gap-1">
2201
+ {/* Markdown preview toggle. Only meaningful for .md / .markdown
2202
+ files; hidden otherwise so it doesn't clutter the header. */}
2203
+ {activeFile && /\.(md|markdown)$/i.test(activeFile) && (
2204
+ <button
2205
+ onClick={() => setMdPreview((v) => !v)}
2206
+ className="flex items-center gap-1 p-1 rounded text-slate-500 hover:text-amber-300 hover:bg-slate-800/60"
2207
+ title={
2208
+ mdPreview
2209
+ ? "Show raw markdown source"
2210
+ : "Render markdown preview"
2211
+ }
2212
+ >
2213
+ {mdPreview ? (
2214
+ <EyeOff className="w-3.5 h-3.5" />
2215
+ ) : (
2216
+ <Eye className="w-3.5 h-3.5" />
2217
+ )}
2218
+ <span className="text-[10px] uppercase tracking-wide">
2219
+ {mdPreview ? "Source" : "Preview"}
2220
+ </span>
2221
+ </button>
2204
2222
  )}
2205
- </button>
2223
+ <button
2224
+ onClick={() => setRightCollapsed((v) => !v)}
2225
+ className="p-1 rounded text-slate-500 hover:text-amber-300 hover:bg-slate-800/60 shrink-0"
2226
+ title={
2227
+ rightCollapsed
2228
+ ? "Show console/jobs panel"
2229
+ : "Hide console/jobs panel"
2230
+ }
2231
+ >
2232
+ {rightCollapsed ? (
2233
+ <PanelRightOpen className="w-3.5 h-3.5" />
2234
+ ) : (
2235
+ <PanelRightClose className="w-3.5 h-3.5" />
2236
+ )}
2237
+ </button>
2238
+ </div>
2206
2239
  </div>
2207
2240
  <div className="flex-1 min-h-0">
2208
- {activeFile && workspace.files[activeFile] !== undefined && (
2209
- <MonacoEditorLib
2210
- key={activeFile}
2211
- height="100%"
2212
- width="100%"
2213
- language={getEditorLanguage(activeFile)}
2214
- theme="gha-lab-dark"
2215
- path={getGhaLabModelPath(activeFile)}
2216
- value={workspace.files[activeFile]}
2217
- beforeMount={handleBeforeMount}
2218
- onMount={handleMount}
2219
- onChange={handleEditorChange}
2220
- options={{
2221
- fontFamily: EDITOR_FONT,
2222
- fontSize: 13,
2223
- lineHeight: 22,
2224
- minimap: { enabled: false },
2225
- automaticLayout: true,
2226
- scrollBeyondLastLine: false,
2227
- wordWrap: "off",
2228
- tabSize: 2,
2229
- insertSpaces: true,
2230
- }}
2231
- />
2232
- )}
2241
+ {activeFile &&
2242
+ workspace.files[activeFile] !== undefined &&
2243
+ (mdPreview && /\.(md|markdown)$/i.test(activeFile) ? (
2244
+ <div className="h-full w-full overflow-auto px-6 py-4 text-slate-200 gha-md-preview">
2245
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
2246
+ {workspace.files[activeFile]}
2247
+ </ReactMarkdown>
2248
+ </div>
2249
+ ) : (
2250
+ <MonacoEditorLib
2251
+ key={activeFile}
2252
+ height="100%"
2253
+ width="100%"
2254
+ language={getEditorLanguage(activeFile)}
2255
+ theme="gha-lab-dark"
2256
+ path={getGhaLabModelPath(activeFile)}
2257
+ value={workspace.files[activeFile]}
2258
+ beforeMount={handleBeforeMount}
2259
+ onMount={handleMount}
2260
+ onChange={handleEditorChange}
2261
+ options={{
2262
+ fontFamily: EDITOR_FONT,
2263
+ fontSize: 13,
2264
+ lineHeight: 22,
2265
+ minimap: { enabled: false },
2266
+ automaticLayout: true,
2267
+ scrollBeyondLastLine: false,
2268
+ wordWrap: "off",
2269
+ tabSize: 2,
2270
+ insertSpaces: true,
2271
+ }}
2272
+ />
2273
+ ))}
2233
2274
  </div>
2234
2275
  </div>
2235
2276
 
@@ -8,11 +8,14 @@ import {
8
8
  parseInfraLabWorkspace,
9
9
  } from "../infraLab";
10
10
  import {
11
+ AWS_GOVERNANCE_GHA_LAB,
11
12
  DEFAULT_GHA_LAB,
13
+ GOVERNANCE_GHA_LAB,
12
14
  parseGhaLabWorkspace,
13
15
  REACT_VITE_TYPESCRIPT_GHA_LAB,
14
16
  } from "../githubActionsLab";
15
17
  import { ENTERPRISE_LOCAL_AUTH_LAB } from "../enterpriseLocalLab";
18
+ import { AWS_GOVERNANCE_IAM_LAB } from "../awsGovernanceIamLab";
16
19
  import {
17
20
  parseFrontendLabWorkspace,
18
21
  ISOLATED_MODULE_FEDERATION_LAB,
@@ -671,6 +674,12 @@ export default function LabsPanel() {
671
674
  "Terraform deploys NestJS BFF, OIDC mock, Redis & claims API",
672
675
  onClick: () => openInfraLab(ENTERPRISE_LOCAL_AUTH_LAB),
673
676
  },
677
+ {
678
+ label: "AWS Governance IAM",
679
+ description:
680
+ "Roles, policies, attachments, users & OIDC mirroring a real governance-iam module on LocalStack",
681
+ onClick: () => openInfraLab(AWS_GOVERNANCE_IAM_LAB),
682
+ },
674
683
  ]}
675
684
  onOpen={openInfraFile}
676
685
  openTitle="Open in Infrastructure Lab"
@@ -696,6 +705,18 @@ export default function LabsPanel() {
696
705
  "Multi-job CI workflow with a local composite action and a matrix build",
697
706
  onClick: () => openGhaLab(DEFAULT_GHA_LAB),
698
707
  },
708
+ {
709
+ label: "Platform Governance Template",
710
+ description:
711
+ "PLF-style mono-repo: CODEOWNERS, PR template, Azure PIM/Policy + AWS IAM deploy workflows, offboarding",
712
+ onClick: () => openGhaLab(GOVERNANCE_GHA_LAB),
713
+ },
714
+ {
715
+ label: "AWS Governance IAM via GitHub Actions",
716
+ description:
717
+ "PR plan + main-branch apply workflows that drive a real Terraform IAM module against LocalStack via a reusable composite action",
718
+ onClick: () => openGhaLab(AWS_GOVERNANCE_GHA_LAB),
719
+ },
699
720
  ]}
700
721
  onOpen={openGhaFile}
701
722
  openTitle="Open in GitHub Lab"
@@ -26,6 +26,7 @@ import { useMemo, useState } from "react";
26
26
  import {
27
27
  Check,
28
28
  CircleDot,
29
+ FileText,
29
30
  GitBranch,
30
31
  GitPullRequest,
31
32
  Info,
@@ -52,10 +53,12 @@ import {
52
53
  evaluateCodeOwners,
53
54
  evaluateMergeability,
54
55
  findCodeOwnersPath,
56
+ findPullRequestTemplates,
55
57
  latestReviewByAuthor,
56
58
  parseCodeOwners,
57
59
  type CheckStatus,
58
60
  type MergeabilityResult,
61
+ type PullRequestTemplate,
59
62
  } from "../codeowners";
60
63
 
61
64
  interface PullRequestPanelProps {
@@ -119,6 +122,13 @@ export default function PullRequestPanel({
119
122
  [evaluation, pr, workspace.rulesets, org, baseBranch, defaultBranch],
120
123
  );
121
124
 
125
+ // PR templates discovered from .github/PULL_REQUEST_TEMPLATE[.md]
126
+ // (just like real github.com — no Settings UI, file-based only).
127
+ const templates = useMemo(
128
+ () => findPullRequestTemplates(workspace.files),
129
+ [workspace.files],
130
+ );
131
+
122
132
  // ── Mutators ────────────────────────────────────────────────────
123
133
  const updatePR = (next: Partial<GithubLabPullRequest>) =>
124
134
  onChange((prev) => ({
@@ -206,6 +216,13 @@ export default function PullRequestPanel({
206
216
  </div>
207
217
  </header>
208
218
 
219
+ {/* ── Description (PR body + template picker) ── */}
220
+ <PullRequestBody
221
+ body={pr.body ?? ""}
222
+ templates={templates}
223
+ onChange={(body) => updatePR({ body })}
224
+ />
225
+
209
226
  {!codeownersPath && (
210
227
  <div className="mb-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-2 text-amber-200">
211
228
  No CODEOWNERS file found. Add one at <code>.github/CODEOWNERS</code>{" "}
@@ -578,6 +595,114 @@ function labelForStatus(s: string): string {
578
595
  }
579
596
  }
580
597
 
598
+ // ─── PR description body + template picker ────────────────────────────
599
+ //
600
+ // On github.com the description box is auto-prefilled from a PR
601
+ // template when you open a new PR, and you can choose between
602
+ // templates via ?template= when there's a directory of them. Here we
603
+ // surface the same mechanic explicitly: a textarea for the body, plus
604
+ // "Apply template" buttons that overwrite the body with the chosen
605
+ // template (with a confirm if the body has been edited away from any
606
+ // template's contents).
607
+
608
+ function PullRequestBody({
609
+ body,
610
+ templates,
611
+ onChange,
612
+ }: {
613
+ body: string;
614
+ templates: PullRequestTemplate[];
615
+ onChange: (next: string) => void;
616
+ }) {
617
+ const [collapsed, setCollapsed] = useState(false);
618
+ // Map body → matching template name (so we can mark the active chip).
619
+ const matchingTemplate = templates.find((t) => t.body === body);
620
+
621
+ const apply = (t: PullRequestTemplate) => {
622
+ if (
623
+ body.trim().length > 0 &&
624
+ !templates.some((tpl) => tpl.body === body) &&
625
+ !window.confirm(
626
+ `Replace the current description with template "${t.name}"?`,
627
+ )
628
+ ) {
629
+ return;
630
+ }
631
+ onChange(t.body);
632
+ };
633
+
634
+ return (
635
+ <section className="mb-3 rounded-xl border border-slate-800/60 bg-slate-900/40">
636
+ <header className="flex items-center justify-between gap-2 px-3 py-2">
637
+ <button
638
+ onClick={() => setCollapsed((c) => !c)}
639
+ className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-slate-400 hover:text-amber-200"
640
+ >
641
+ <FileText className="h-3 w-3" />
642
+ Description
643
+ </button>
644
+ {templates.length > 0 && (
645
+ <div className="flex flex-wrap items-center gap-1">
646
+ <span className="text-[10px] text-slate-500">Templates:</span>
647
+ {templates.map((t) => {
648
+ const active = matchingTemplate?.path === t.path;
649
+ return (
650
+ <button
651
+ key={t.path}
652
+ onClick={() => apply(t)}
653
+ title={t.path}
654
+ className={`rounded px-1.5 py-0.5 text-[10px] ${
655
+ active
656
+ ? "bg-amber-500/25 text-amber-100"
657
+ : "bg-slate-800 text-slate-300 hover:bg-slate-700"
658
+ }`}
659
+ >
660
+ {t.name}
661
+ </button>
662
+ );
663
+ })}
664
+ {body.length > 0 && (
665
+ <button
666
+ onClick={() => onChange("")}
667
+ className="rounded px-1.5 py-0.5 text-[10px] text-slate-500 hover:text-rose-300"
668
+ title="Clear the description"
669
+ >
670
+ Clear
671
+ </button>
672
+ )}
673
+ </div>
674
+ )}
675
+ </header>
676
+ {!collapsed && (
677
+ <div className="px-3 pb-3">
678
+ {templates.length === 0 && body.length === 0 && (
679
+ <p className="mb-1.5 text-[10px] text-slate-500">
680
+ Tip: add{" "}
681
+ <code className="rounded bg-slate-800 px-1 py-px text-[10px] text-amber-200">
682
+ .github/pull_request_template.md
683
+ </code>{" "}
684
+ to the workspace and a one-click "Default" template will appear
685
+ here — same as github.com.
686
+ </p>
687
+ )}
688
+ <textarea
689
+ value={body}
690
+ onChange={(e) => onChange(e.target.value)}
691
+ placeholder="Leave a description (Markdown supported)…"
692
+ className="h-32 w-full resize-y rounded border border-slate-700 bg-slate-950/60 p-2 font-mono text-[11px] text-slate-200 outline-none focus:border-emerald-500/40"
693
+ />
694
+ {matchingTemplate && (
695
+ <p className="mt-1 text-[10px] text-slate-500">
696
+ Using template{" "}
697
+ <code className="text-slate-400">{matchingTemplate.path}</code>
698
+ </p>
699
+ )}
700
+ </div>
701
+ )}
702
+ </section>
703
+ );
704
+ }
705
+
581
706
  // ─── Rulesets summary (read-only) ──────────────────────────────────────
582
707
  //
583
708
  // On real github.com the PR sidebar shows a tiny summary of which
@@ -1379,17 +1379,20 @@ function EnvironmentsSection({
1379
1379
 
1380
1380
  function Field({
1381
1381
  label,
1382
+ help,
1382
1383
  children,
1383
1384
  }: {
1384
1385
  label: string;
1386
+ help?: string;
1385
1387
  children: React.ReactNode;
1386
1388
  }) {
1387
1389
  return (
1388
- <div>
1390
+ <div className="mt-3">
1389
1391
  <div className="mb-1 text-[11px] font-semibold text-slate-200">
1390
1392
  {label}
1391
1393
  </div>
1392
1394
  {children}
1395
+ {help && <p className="mt-1 text-[11px] text-slate-500">{help}</p>}
1393
1396
  </div>
1394
1397
  );
1395
1398
  }