Skip to content

Commit 0c4f0de

Browse files
committed
feat: add pipeline badges, insert buttons, collapsible catalog and RJSF form
1 parent 118a986 commit 0c4f0de

File tree

3 files changed

+119
-47
lines changed

3 files changed

+119
-47
lines changed

gravitee-gamma/src/app/policy-studio/components/PhaseRow.tsx

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useDroppable } from '@dnd-kit/core';
22
import { SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable';
3-
import { ChevronRight } from 'lucide-react';
3+
import { ChevronRight, Plus, Circle } from 'lucide-react';
44
import { cn } from '@baros/lib/utils';
55
import type { Step, Phase, StepKey } from '../types';
66
import { StepCard } from './StepCard';
@@ -13,14 +13,45 @@ interface PhaseRowProps {
1313
readonly selectedStepKey: StepKey | null;
1414
readonly onStepSelect: (key: StepKey) => void;
1515
readonly onStepRemove: (phase: Phase, stepIndex: number) => void;
16+
readonly onInsertStep?: (phase: Phase, atIndex: number) => void;
1617
readonly dropState?: 'compatible' | 'incompatible' | null;
1718
}
1819

1920
function Arrow() {
2021
return <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground/50" />;
2122
}
2223

23-
export function PhaseRow({ label, phase, steps, flowIndex, selectedStepKey, onStepSelect, onStepRemove, dropState }: PhaseRowProps) {
24+
function EndpointBadge({ label, variant }: { label: string; variant: 'start' | 'end' }) {
25+
return (
26+
<div className={cn(
27+
'flex shrink-0 items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium',
28+
variant === 'start'
29+
? 'border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-300'
30+
: 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-300',
31+
)}>
32+
<Circle className="h-2 w-2 fill-current" />
33+
{label}
34+
</div>
35+
);
36+
}
37+
38+
function InsertButton({ onClick }: { onClick: () => void }) {
39+
return (
40+
<button
41+
type="button"
42+
onClick={onClick}
43+
className="group/insert flex shrink-0 items-center"
44+
aria-label="Insert policy here"
45+
>
46+
<Arrow />
47+
<span className="flex h-5 w-5 items-center justify-center rounded-full border border-dashed border-muted-foreground/30 opacity-0 transition-opacity group-hover/insert:opacity-100 hover:border-primary hover:text-primary">
48+
<Plus className="h-3 w-3" />
49+
</span>
50+
</button>
51+
);
52+
}
53+
54+
export function PhaseRow({ label, phase, steps, flowIndex, selectedStepKey, onStepSelect, onStepRemove, onInsertStep, dropState }: PhaseRowProps) {
2455
const droppableId = `phase-${flowIndex}-${phase}`;
2556
const { setNodeRef, isOver } = useDroppable({
2657
id: droppableId,
@@ -32,6 +63,9 @@ export function PhaseRow({ label, phase, steps, flowIndex, selectedStepKey, onSt
3263
const isSelected = (index: number) =>
3364
selectedStepKey?.phase === phase && selectedStepKey.index === index;
3465

66+
const startLabel = phase === 'request' ? 'Entrypoint' : 'Endpoint';
67+
const endLabel = phase === 'request' ? 'Endpoint' : 'Entrypoint';
68+
3569
return (
3670
<div className="flex flex-col gap-2">
3771
<h3 className={cn(
@@ -50,14 +84,24 @@ export function PhaseRow({ label, phase, steps, flowIndex, selectedStepKey, onSt
5084
!isOver && 'border-border/50 bg-muted/30',
5185
)}
5286
>
87+
<EndpointBadge label={startLabel} variant="start" />
88+
5389
{steps.length === 0 && (
54-
<div className="px-4 py-2 text-xs text-muted-foreground">
55-
Drop a policy here
56-
</div>
90+
<>
91+
<Arrow />
92+
<div className="px-4 py-2 text-xs text-muted-foreground">
93+
Drop a policy here
94+
</div>
95+
</>
5796
)}
97+
5898
{steps.map((step, index) => (
5999
<div key={step.id} className="flex items-center gap-1">
60-
{index > 0 && <Arrow />}
100+
{onInsertStep ? (
101+
<InsertButton onClick={() => onInsertStep(phase, index)} />
102+
) : (
103+
<Arrow />
104+
)}
61105
<StepCard
62106
step={step}
63107
phase={phase}
@@ -69,6 +113,10 @@ export function PhaseRow({ label, phase, steps, flowIndex, selectedStepKey, onSt
69113
/>
70114
</div>
71115
))}
116+
117+
<Arrow />
118+
<EndpointBadge label={endLabel} variant="end" />
119+
72120
{isOver && dropState === 'incompatible' && (
73121
<div className="ml-2 text-xs text-destructive">
74122
Incompatible phase

gravitee-gamma/src/app/policy-studio/components/PolicyCatalog.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { useState, useMemo } from 'react';
2-
import { Search } from 'lucide-react';
2+
import { Search, ChevronDown } from 'lucide-react';
3+
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@baros/components/ui/collapsible';
34
import { ScrollArea } from '@baros/components/ui/scroll-area';
45
import { Separator } from '@baros/components/ui/separator';
6+
import { cn } from '@baros/lib/utils';
57
import type { PolicyPlugin } from '../types';
68
import { PolicyCatalogItem } from './PolicyCatalogItem';
79

10+
const UNCATEGORIZED = 'Others';
11+
812
interface PolicyCatalogProps {
913
readonly policies: PolicyPlugin[];
1014
}
@@ -20,6 +24,23 @@ export function PolicyCatalog({ policies }: PolicyCatalogProps) {
2024
[policies, search],
2125
);
2226

27+
const grouped = useMemo(() => {
28+
const map = new Map<string, PolicyPlugin[]>();
29+
for (const p of filtered) {
30+
const cat = p.category || UNCATEGORIZED;
31+
const list = map.get(cat);
32+
if (list) list.push(p);
33+
else map.set(cat, [p]);
34+
}
35+
return [...map.entries()].sort(([a], [b]) => {
36+
if (a === UNCATEGORIZED) return 1;
37+
if (b === UNCATEGORIZED) return -1;
38+
return a.localeCompare(b);
39+
});
40+
}, [filtered]);
41+
42+
const isSearching = search.length > 0;
43+
2344
return (
2445
<div className="flex w-64 shrink-0 flex-col border-l">
2546
<div className="px-3 py-2">
@@ -38,14 +59,30 @@ export function PolicyCatalog({ policies }: PolicyCatalogProps) {
3859
/>
3960
</div>
4061
<ScrollArea className="flex-1">
41-
<div className="flex flex-col gap-1 p-2">
62+
<div className="flex flex-col p-2">
4263
{filtered.length === 0 && (
4364
<div className="px-2 py-8 text-center text-xs text-muted-foreground">
4465
No policies found
4566
</div>
4667
)}
47-
{filtered.map((policy) => (
48-
<PolicyCatalogItem key={policy.id} policy={policy} />
68+
{grouped.map(([category, items]) => (
69+
<Collapsible key={category} defaultOpen open={isSearching ? true : undefined} className="group/collapsible">
70+
<CollapsibleTrigger className="flex w-full items-center gap-1 px-2 py-1.5 text-xs font-semibold text-muted-foreground hover:text-foreground">
71+
<ChevronDown className={cn(
72+
'h-3 w-3 transition-transform',
73+
'group-data-[state=closed]/collapsible:-rotate-90',
74+
)} />
75+
{category}
76+
<span className="ml-auto text-[10px] font-normal">{items.length}</span>
77+
</CollapsibleTrigger>
78+
<CollapsibleContent>
79+
<div className="flex flex-col gap-1 pb-1">
80+
{items.map((policy) => (
81+
<PolicyCatalogItem key={policy.id} policy={policy} />
82+
))}
83+
</div>
84+
</CollapsibleContent>
85+
</Collapsible>
4986
))}
5087
</div>
5188
</ScrollArea>

gravitee-gamma/src/app/policy-studio/components/StepConfigSheet.tsx

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { useState, useEffect } from 'react';
2+
import Form from '@rjsf/core';
3+
import validator from '@rjsf/validator-ajv8';
4+
import type { IChangeEvent } from '@rjsf/core';
25
import {
36
Sheet,
47
SheetContent,
@@ -21,31 +24,25 @@ interface StepConfigSheetProps {
2124

2225
export function StepConfigSheet({ open, onOpenChange, step, phase, onSave }: StepConfigSheetProps) {
2326
const { schema, loading, error, fetchSchema } = usePolicySchema();
24-
const [configJson, setConfigJson] = useState('');
25-
const [parseError, setParseError] = useState<string | null>(null);
27+
const [formData, setFormData] = useState<Record<string, unknown>>({});
2628

2729
const stepId = step?.id ?? null;
2830
const stepPolicy = step?.policy ?? null;
2931

3032
useEffect(() => {
3133
if (open && step && stepPolicy) {
3234
fetchSchema(stepPolicy);
33-
setConfigJson(JSON.stringify(step.configuration ?? {}, null, 2));
34-
setParseError(null);
35+
setFormData(step.configuration ?? {});
3536
}
36-
// Depend on stable values, not the step object reference
37-
// eslint-disable-next-line react-hooks/exhaustive-deps
3837
}, [open, stepId, stepPolicy, fetchSchema]);
3938

39+
function handleChange(e: IChangeEvent) {
40+
setFormData(e.formData);
41+
}
42+
4043
function handleSave() {
41-
try {
42-
const parsed = JSON.parse(configJson);
43-
setParseError(null);
44-
onSave(parsed);
45-
onOpenChange(false);
46-
} catch {
47-
setParseError('Invalid JSON');
48-
}
44+
onSave(formData);
45+
onOpenChange(false);
4946
}
5047

5148
const policyName = step?.name ?? step?.policy ?? 'Unknown policy';
@@ -61,36 +58,26 @@ export function StepConfigSheet({ open, onOpenChange, step, phase, onSave }: Ste
6158
</SheetDescription>
6259
</SheetHeader>
6360

64-
<div className="flex-1 overflow-auto space-y-4 py-4">
61+
<div className="flex-1 overflow-auto py-4">
6562
{loading && (
6663
<div className="text-sm text-muted-foreground">Loading schema...</div>
6764
)}
6865
{error && (
6966
<div className="text-sm text-destructive">Failed to load schema: {error.message}</div>
7067
)}
7168
{schema && !loading && (
72-
<div className="space-y-3">
73-
<div>
74-
<h4 className="text-xs font-semibold text-muted-foreground mb-1">JSON Schema</h4>
75-
<pre className="rounded-md bg-muted p-3 text-xs overflow-auto max-h-40">
76-
{JSON.stringify(schema, null, 2)}
77-
</pre>
78-
</div>
79-
<div>
80-
<h4 className="text-xs font-semibold text-muted-foreground mb-1">Configuration</h4>
81-
<textarea
82-
value={configJson}
83-
onChange={(e) => {
84-
setConfigJson(e.target.value);
85-
setParseError(null);
86-
}}
87-
className="h-48 w-full rounded-md border border-input bg-background p-3 font-mono text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
88-
spellCheck={false}
89-
/>
90-
{parseError && (
91-
<div className="mt-1 text-xs text-destructive">{parseError}</div>
92-
)}
93-
</div>
69+
<div className="rjsf-sheet">
70+
<Form
71+
schema={schema}
72+
formData={formData}
73+
validator={validator}
74+
onChange={handleChange}
75+
liveValidate
76+
showErrorList={false}
77+
>
78+
{/* Hide default submit button — we use our own in SheetFooter */}
79+
<></>
80+
</Form>
9481
</div>
9582
)}
9683
</div>

0 commit comments

Comments
 (0)