@checkstack/gitops-frontend 0.2.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/CHANGELOG.md +37 -0
- package/package.json +29 -0
- package/src/components/GitOpsLockBanner.tsx +35 -0
- package/src/components/GitOpsMenuItem.tsx +28 -0
- package/src/components/KindRegistryMenuItem.tsx +28 -0
- package/src/components/ProvenanceStatus.tsx +246 -0
- package/src/components/ProviderEditor.tsx +214 -0
- package/src/components/ProviderList.tsx +294 -0
- package/src/components/SecretEditor.tsx +110 -0
- package/src/components/SecretList.tsx +204 -0
- package/src/components/SecretRotateDialog.tsx +79 -0
- package/src/hooks/useProvenanceLock.ts +32 -0
- package/src/index.tsx +46 -0
- package/src/pages/GitOpsPage.tsx +56 -0
- package/src/pages/KindRegistryPage.tsx +516 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @checkstack/gitops-frontend
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6c40b5b: Generalized provenance system and GitOps frontend plugin
|
|
8
|
+
|
|
9
|
+
**Breaking**: `EntityKindDefinition.reconcile()` now returns `{ entityId: string }` instead of `void`. Plugins must return the plugin-specific entity ID (e.g., catalog system UUID) so the engine can store it in provenance.
|
|
10
|
+
|
|
11
|
+
- Added `entityId` column to the provenance table (non-nullable)
|
|
12
|
+
- Reconciler engine passes `existingEntityId` to plugins for updates
|
|
13
|
+
- `getProvenance` now supports lookup by `entityId` in addition to `entityName`
|
|
14
|
+
- Added provider CRUD endpoints: `createProvider`, `updateProvider`, `deleteProvider`
|
|
15
|
+
- Created `gitops-frontend` plugin with provider management, secret management, and sync status dashboard
|
|
16
|
+
- Removed `gitops_entity_name` metadata markers from catalog entities
|
|
17
|
+
- Removed `findSystemByGitOpsName`, `deleteSystemByGitOpsName` (and Group equivalents) from EntityService
|
|
18
|
+
- Added provenance-based UI locking in catalog-frontend: edit/delete/drag disabled for GitOps-managed systems and groups
|
|
19
|
+
|
|
20
|
+
- 6c40b5b: Add Kind Registry browser and developer documentation
|
|
21
|
+
|
|
22
|
+
- Added `gitopsAccess.kinds.read` access rule for standalone Kind Registry access
|
|
23
|
+
- Added `describeKinds()` method to the internal entity kind registry, serializing Zod schemas to JSON Schema
|
|
24
|
+
- Added `listKinds` RPC endpoint gated by the new access rule
|
|
25
|
+
- Created standalone Kind Registry page with schema visualization, extension listing, and auto-generated YAML examples
|
|
26
|
+
- Added Kind Registry link to the user menu
|
|
27
|
+
- Created developer documentation for entity kind and extension registration in `docs/backend/gitops-entity-kinds.md`
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [6c40b5b]
|
|
32
|
+
- Updated dependencies [6c40b5b]
|
|
33
|
+
- Updated dependencies [6c40b5b]
|
|
34
|
+
- Updated dependencies [6c40b5b]
|
|
35
|
+
- Updated dependencies [4b0934d]
|
|
36
|
+
- @checkstack/gitops-common@0.1.0
|
|
37
|
+
- @checkstack/ui@1.3.6
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/gitops-frontend",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"checkstack": {
|
|
7
|
+
"type": "frontend"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"lint": "bun run lint:code",
|
|
12
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@checkstack/common": "0.6.5",
|
|
16
|
+
"@checkstack/frontend-api": "0.3.9",
|
|
17
|
+
"@checkstack/gitops-common": "0.0.1",
|
|
18
|
+
"@checkstack/ui": "1.3.5",
|
|
19
|
+
"lucide-react": "^0.344.0",
|
|
20
|
+
"react": "^18.2.0",
|
|
21
|
+
"react-router-dom": "^6.22.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.0.0",
|
|
25
|
+
"@types/react": "^18.2.0",
|
|
26
|
+
"@checkstack/tsconfig": "0.0.5",
|
|
27
|
+
"@checkstack/scripts": "0.1.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { GitBranch, ExternalLink } from "lucide-react";
|
|
3
|
+
import type { Provenance } from "@checkstack/gitops-common";
|
|
4
|
+
|
|
5
|
+
interface GitOpsLockBannerProps {
|
|
6
|
+
provenance: Provenance;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Banner displayed on entity detail pages when the entity is managed by GitOps.
|
|
11
|
+
* Informs the user that manual edits are disabled and shows the Git source.
|
|
12
|
+
*/
|
|
13
|
+
export const GitOpsLockBanner: React.FC<GitOpsLockBannerProps> = ({
|
|
14
|
+
provenance,
|
|
15
|
+
}) => {
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex items-center gap-3 p-3 rounded-lg border border-primary/20 bg-primary/5 text-sm">
|
|
18
|
+
<GitBranch className="w-5 h-5 text-primary shrink-0" />
|
|
19
|
+
<div className="min-w-0 flex-1">
|
|
20
|
+
<span className="font-medium text-foreground">
|
|
21
|
+
Managed by GitOps
|
|
22
|
+
</span>
|
|
23
|
+
<span className="text-muted-foreground ml-1">
|
|
24
|
+
— edits are disabled. Changes must be made in Git.
|
|
25
|
+
</span>
|
|
26
|
+
<div className="text-xs text-muted-foreground mt-0.5 flex items-center gap-1">
|
|
27
|
+
<span className="truncate">
|
|
28
|
+
{provenance.repository}/{provenance.filePath}
|
|
29
|
+
</span>
|
|
30
|
+
<ExternalLink className="w-3 h-3 shrink-0" />
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useNavigate } from "react-router-dom";
|
|
2
|
+
import { GitBranch } from "lucide-react";
|
|
3
|
+
import { DropdownMenuItem } from "@checkstack/ui";
|
|
4
|
+
import type { UserMenuItemsContext } from "@checkstack/frontend-api";
|
|
5
|
+
import { resolveRoute } from "@checkstack/common";
|
|
6
|
+
import { pluginMetadata, gitopsAccess, gitopsRoutes } from "@checkstack/gitops-common";
|
|
7
|
+
import React from "react";
|
|
8
|
+
|
|
9
|
+
const REQUIRED_ACCESS_RULE = `${pluginMetadata.pluginId}.${gitopsAccess.provider.read.id}`;
|
|
10
|
+
|
|
11
|
+
export function GitOpsMenuItem({
|
|
12
|
+
accessRules: userPerms,
|
|
13
|
+
}: UserMenuItemsContext) {
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
const canView =
|
|
16
|
+
userPerms.includes("*") || userPerms.includes(REQUIRED_ACCESS_RULE);
|
|
17
|
+
|
|
18
|
+
if (!canView) return <React.Fragment />;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<DropdownMenuItem
|
|
22
|
+
onClick={() => navigate(resolveRoute(gitopsRoutes.routes.home))}
|
|
23
|
+
icon={<GitBranch className="h-4 w-4" />}
|
|
24
|
+
>
|
|
25
|
+
GitOps
|
|
26
|
+
</DropdownMenuItem>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useNavigate } from "react-router-dom";
|
|
2
|
+
import { Blocks } from "lucide-react";
|
|
3
|
+
import { DropdownMenuItem } from "@checkstack/ui";
|
|
4
|
+
import type { UserMenuItemsContext } from "@checkstack/frontend-api";
|
|
5
|
+
import { resolveRoute } from "@checkstack/common";
|
|
6
|
+
import { pluginMetadata, gitopsAccess, gitopsRoutes } from "@checkstack/gitops-common";
|
|
7
|
+
import React from "react";
|
|
8
|
+
|
|
9
|
+
const REQUIRED_ACCESS_RULE = `${pluginMetadata.pluginId}.${gitopsAccess.kinds.read.id}`;
|
|
10
|
+
|
|
11
|
+
export function KindRegistryMenuItem({
|
|
12
|
+
accessRules: userPerms,
|
|
13
|
+
}: UserMenuItemsContext) {
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
const canView =
|
|
16
|
+
userPerms.includes("*") || userPerms.includes(REQUIRED_ACCESS_RULE);
|
|
17
|
+
|
|
18
|
+
if (!canView) return <React.Fragment />;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<DropdownMenuItem
|
|
22
|
+
onClick={() => navigate(resolveRoute(gitopsRoutes.routes.kinds))}
|
|
23
|
+
icon={<Blocks className="h-4 w-4" />}
|
|
24
|
+
>
|
|
25
|
+
Kind Registry
|
|
26
|
+
</DropdownMenuItem>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { usePluginClient, useApi, accessApiRef } from "@checkstack/frontend-api";
|
|
2
|
+
import { GitOpsApi, gitopsAccess } from "@checkstack/gitops-common";
|
|
3
|
+
import type { Provenance } from "@checkstack/gitops-common";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardHeaderRow,
|
|
8
|
+
CardTitle,
|
|
9
|
+
CardContent,
|
|
10
|
+
Button,
|
|
11
|
+
Badge,
|
|
12
|
+
EmptyState,
|
|
13
|
+
ConfirmationModal,
|
|
14
|
+
useToast,
|
|
15
|
+
} from "@checkstack/ui";
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { CheckCircle, AlertTriangle, XCircle, Trash2, X } from "lucide-react";
|
|
18
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
19
|
+
|
|
20
|
+
export const ProvenanceStatus = () => {
|
|
21
|
+
const client = usePluginClient(GitOpsApi);
|
|
22
|
+
const accessApi = useApi(accessApiRef);
|
|
23
|
+
const toast = useToast();
|
|
24
|
+
|
|
25
|
+
const { allowed: canManage } = accessApi.useAccess(gitopsAccess.provider.manage);
|
|
26
|
+
|
|
27
|
+
const [confirmModal, setConfirmModal] = useState<{
|
|
28
|
+
isOpen: boolean;
|
|
29
|
+
provenanceId: string;
|
|
30
|
+
entityName: string;
|
|
31
|
+
}>({ isOpen: false, provenanceId: "", entityName: "" });
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
data: provenanceEntries,
|
|
35
|
+
isLoading,
|
|
36
|
+
refetch,
|
|
37
|
+
} = client.listProvenance.useQuery({});
|
|
38
|
+
|
|
39
|
+
const confirmDeleteMutation = client.confirmOrphanDeletion.useMutation({
|
|
40
|
+
onSuccess: () => {
|
|
41
|
+
toast.success("Orphan deleted successfully");
|
|
42
|
+
setConfirmModal({ isOpen: false, provenanceId: "", entityName: "" });
|
|
43
|
+
void refetch();
|
|
44
|
+
},
|
|
45
|
+
onError: (error) => {
|
|
46
|
+
toast.error(extractErrorMessage(error, "Failed to delete orphan"));
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const dismissMutation = client.dismissOrphan.useMutation({
|
|
51
|
+
onSuccess: () => {
|
|
52
|
+
toast.success("Orphan dismissed — entity is no longer tracked by GitOps");
|
|
53
|
+
void refetch();
|
|
54
|
+
},
|
|
55
|
+
onError: (error) => {
|
|
56
|
+
toast.error(extractErrorMessage(error, "Failed to dismiss orphan"));
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const entries = provenanceEntries ?? [];
|
|
61
|
+
const synced = entries.filter((e) => e.status === "synced");
|
|
62
|
+
const errors = entries.filter((e) => e.status === "error");
|
|
63
|
+
const orphaned = entries.filter((e) => e.status === "orphaned");
|
|
64
|
+
|
|
65
|
+
const statusIcon = (status: Provenance["status"]) => {
|
|
66
|
+
switch (status) {
|
|
67
|
+
case "synced": {
|
|
68
|
+
return <CheckCircle className="w-4 h-4 text-emerald-500" />;
|
|
69
|
+
}
|
|
70
|
+
case "error": {
|
|
71
|
+
return <XCircle className="w-4 h-4 text-destructive" />;
|
|
72
|
+
}
|
|
73
|
+
case "orphaned": {
|
|
74
|
+
return <AlertTriangle className="w-4 h-4 text-amber-500" />;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const statusBadge = (status: Provenance["status"]) => {
|
|
80
|
+
const variants: Record<Provenance["status"], "default" | "destructive" | "outline"> = {
|
|
81
|
+
synced: "default",
|
|
82
|
+
error: "destructive",
|
|
83
|
+
orphaned: "outline",
|
|
84
|
+
};
|
|
85
|
+
return <Badge variant={variants[status]}>{status}</Badge>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const renderEntryList = (list: Provenance[], showOrphanActions: boolean) => (
|
|
89
|
+
<div className="space-y-2">
|
|
90
|
+
{list.map((entry) => (
|
|
91
|
+
<div
|
|
92
|
+
key={entry.id}
|
|
93
|
+
className="flex items-center justify-between p-3 rounded-lg border border-border bg-background/50"
|
|
94
|
+
>
|
|
95
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
96
|
+
{statusIcon(entry.status)}
|
|
97
|
+
<div className="min-w-0">
|
|
98
|
+
<div className="text-sm font-medium">
|
|
99
|
+
<span className="text-muted-foreground">{entry.kind}/</span>
|
|
100
|
+
{entry.entityName}
|
|
101
|
+
</div>
|
|
102
|
+
<div className="text-xs text-muted-foreground mt-0.5 truncate">
|
|
103
|
+
{entry.repository}/{entry.filePath}
|
|
104
|
+
</div>
|
|
105
|
+
{entry.errorMessage && (
|
|
106
|
+
<div className="text-xs text-destructive mt-0.5 truncate">
|
|
107
|
+
{entry.errorMessage}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
114
|
+
{statusBadge(entry.status)}
|
|
115
|
+
{showOrphanActions && canManage && (
|
|
116
|
+
<>
|
|
117
|
+
<Button
|
|
118
|
+
variant="ghost"
|
|
119
|
+
size="icon"
|
|
120
|
+
onClick={() =>
|
|
121
|
+
setConfirmModal({
|
|
122
|
+
isOpen: true,
|
|
123
|
+
provenanceId: entry.id,
|
|
124
|
+
entityName: `${entry.kind}/${entry.entityName}`,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
title="Confirm deletion"
|
|
128
|
+
>
|
|
129
|
+
<Trash2 className="w-4 h-4" />
|
|
130
|
+
</Button>
|
|
131
|
+
<Button
|
|
132
|
+
variant="ghost"
|
|
133
|
+
size="icon"
|
|
134
|
+
onClick={() => dismissMutation.mutate({ provenanceId: entry.id })}
|
|
135
|
+
title="Dismiss orphan"
|
|
136
|
+
>
|
|
137
|
+
<X className="w-4 h-4" />
|
|
138
|
+
</Button>
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<>
|
|
149
|
+
<div className="space-y-6">
|
|
150
|
+
{/* Summary */}
|
|
151
|
+
<div className="grid grid-cols-3 gap-4">
|
|
152
|
+
<Card>
|
|
153
|
+
<CardContent className="pt-6">
|
|
154
|
+
<div className="flex items-center gap-2">
|
|
155
|
+
<CheckCircle className="w-5 h-5 text-emerald-500" />
|
|
156
|
+
<span className="text-2xl font-bold">{synced.length}</span>
|
|
157
|
+
<span className="text-sm text-muted-foreground">Synced</span>
|
|
158
|
+
</div>
|
|
159
|
+
</CardContent>
|
|
160
|
+
</Card>
|
|
161
|
+
<Card>
|
|
162
|
+
<CardContent className="pt-6">
|
|
163
|
+
<div className="flex items-center gap-2">
|
|
164
|
+
<XCircle className="w-5 h-5 text-destructive" />
|
|
165
|
+
<span className="text-2xl font-bold">{errors.length}</span>
|
|
166
|
+
<span className="text-sm text-muted-foreground">Errors</span>
|
|
167
|
+
</div>
|
|
168
|
+
</CardContent>
|
|
169
|
+
</Card>
|
|
170
|
+
<Card>
|
|
171
|
+
<CardContent className="pt-6">
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<AlertTriangle className="w-5 h-5 text-amber-500" />
|
|
174
|
+
<span className="text-2xl font-bold">{orphaned.length}</span>
|
|
175
|
+
<span className="text-sm text-muted-foreground">Orphaned</span>
|
|
176
|
+
</div>
|
|
177
|
+
</CardContent>
|
|
178
|
+
</Card>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Orphaned entities — shown first if any */}
|
|
182
|
+
{orphaned.length > 0 && (
|
|
183
|
+
<Card>
|
|
184
|
+
<CardHeader>
|
|
185
|
+
<CardHeaderRow>
|
|
186
|
+
<CardTitle className="flex items-center gap-2">
|
|
187
|
+
<AlertTriangle className="w-5 h-5 text-amber-500" />
|
|
188
|
+
Orphaned Entities
|
|
189
|
+
</CardTitle>
|
|
190
|
+
</CardHeaderRow>
|
|
191
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
192
|
+
These entities were removed from Git. Confirm deletion or dismiss to keep the entity.
|
|
193
|
+
</p>
|
|
194
|
+
</CardHeader>
|
|
195
|
+
<CardContent>{renderEntryList(orphaned, true)}</CardContent>
|
|
196
|
+
</Card>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{/* Errors */}
|
|
200
|
+
{errors.length > 0 && (
|
|
201
|
+
<Card>
|
|
202
|
+
<CardHeader>
|
|
203
|
+
<CardTitle className="flex items-center gap-2">
|
|
204
|
+
<XCircle className="w-5 h-5 text-destructive" />
|
|
205
|
+
Sync Errors
|
|
206
|
+
</CardTitle>
|
|
207
|
+
</CardHeader>
|
|
208
|
+
<CardContent>{renderEntryList(errors, false)}</CardContent>
|
|
209
|
+
</Card>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Synced entities */}
|
|
213
|
+
<Card>
|
|
214
|
+
<CardHeader>
|
|
215
|
+
<CardTitle className="flex items-center gap-2">
|
|
216
|
+
<CheckCircle className="w-5 h-5 text-emerald-500" />
|
|
217
|
+
Synced Entities
|
|
218
|
+
</CardTitle>
|
|
219
|
+
</CardHeader>
|
|
220
|
+
<CardContent>
|
|
221
|
+
{isLoading ? (
|
|
222
|
+
<div className="text-center py-8 text-muted-foreground">Loading...</div>
|
|
223
|
+
) : synced.length === 0 ? (
|
|
224
|
+
<EmptyState
|
|
225
|
+
title="No synced entities"
|
|
226
|
+
description="Entities will appear here after a successful sync."
|
|
227
|
+
/>
|
|
228
|
+
) : (
|
|
229
|
+
renderEntryList(synced, false)
|
|
230
|
+
)}
|
|
231
|
+
</CardContent>
|
|
232
|
+
</Card>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<ConfirmationModal
|
|
236
|
+
isOpen={confirmModal.isOpen}
|
|
237
|
+
onClose={() => setConfirmModal({ isOpen: false, provenanceId: "", entityName: "" })}
|
|
238
|
+
onConfirm={() => confirmDeleteMutation.mutate({ provenanceId: confirmModal.provenanceId })}
|
|
239
|
+
title="Confirm Orphan Deletion"
|
|
240
|
+
message={`Are you sure you want to permanently delete "${confirmModal.entityName}"? This will remove the entity from the system.`}
|
|
241
|
+
confirmText="Delete"
|
|
242
|
+
variant="danger"
|
|
243
|
+
/>
|
|
244
|
+
</>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Input,
|
|
5
|
+
Label,
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogDescription,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
DialogFooter,
|
|
17
|
+
} from "@checkstack/ui";
|
|
18
|
+
|
|
19
|
+
interface ProviderEditorProps {
|
|
20
|
+
open: boolean;
|
|
21
|
+
onClose: () => void;
|
|
22
|
+
onSave: (data: {
|
|
23
|
+
type: "github" | "gitlab";
|
|
24
|
+
target: string;
|
|
25
|
+
pathPattern: string;
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
authToken?: string;
|
|
28
|
+
syncInterval?: number;
|
|
29
|
+
deletionPolicy?: "orphan" | "auto";
|
|
30
|
+
}) => void;
|
|
31
|
+
initialData?: {
|
|
32
|
+
id: string;
|
|
33
|
+
type: "github" | "gitlab";
|
|
34
|
+
target: string;
|
|
35
|
+
pathPattern: string;
|
|
36
|
+
baseUrl?: string;
|
|
37
|
+
syncInterval: number;
|
|
38
|
+
deletionPolicy: "orphan" | "auto";
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const ProviderEditor: React.FC<ProviderEditorProps> = ({
|
|
43
|
+
open,
|
|
44
|
+
onClose,
|
|
45
|
+
onSave,
|
|
46
|
+
initialData,
|
|
47
|
+
}) => {
|
|
48
|
+
const [type, setType] = useState<"github" | "gitlab">(initialData?.type ?? "github");
|
|
49
|
+
const [target, setTarget] = useState(initialData?.target ?? "");
|
|
50
|
+
const [pathPattern, setPathPattern] = useState(initialData?.pathPattern ?? ".checkstack/**/*.yaml");
|
|
51
|
+
const [baseUrl, setBaseUrl] = useState(initialData?.baseUrl ?? "");
|
|
52
|
+
const [authToken, setAuthToken] = useState("");
|
|
53
|
+
const [syncInterval, setSyncInterval] = useState(String(initialData?.syncInterval ?? 300));
|
|
54
|
+
const [deletionPolicy, setDeletionPolicy] = useState<"orphan" | "auto">(
|
|
55
|
+
initialData?.deletionPolicy ?? "orphan",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (open) {
|
|
60
|
+
setType(initialData?.type ?? "github");
|
|
61
|
+
setTarget(initialData?.target ?? "");
|
|
62
|
+
setPathPattern(initialData?.pathPattern ?? ".checkstack/**/*.yaml");
|
|
63
|
+
setBaseUrl(initialData?.baseUrl ?? "");
|
|
64
|
+
setAuthToken("");
|
|
65
|
+
setSyncInterval(String(initialData?.syncInterval ?? 300));
|
|
66
|
+
setDeletionPolicy(initialData?.deletionPolicy ?? "orphan");
|
|
67
|
+
}
|
|
68
|
+
}, [open, initialData]);
|
|
69
|
+
|
|
70
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
if (!target.trim() || !pathPattern.trim()) return;
|
|
73
|
+
|
|
74
|
+
onSave({
|
|
75
|
+
type,
|
|
76
|
+
target: target.trim(),
|
|
77
|
+
pathPattern: pathPattern.trim(),
|
|
78
|
+
baseUrl: baseUrl.trim() || undefined,
|
|
79
|
+
authToken: authToken.trim() || undefined,
|
|
80
|
+
syncInterval: Number(syncInterval) || 300,
|
|
81
|
+
deletionPolicy,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
|
87
|
+
<DialogContent size="default">
|
|
88
|
+
<form onSubmit={handleSubmit}>
|
|
89
|
+
<DialogHeader>
|
|
90
|
+
<DialogTitle>
|
|
91
|
+
{initialData ? "Edit Provider" : "Add Provider"}
|
|
92
|
+
</DialogTitle>
|
|
93
|
+
<DialogDescription className="sr-only">
|
|
94
|
+
{initialData
|
|
95
|
+
? "Modify the settings for this Git provider"
|
|
96
|
+
: "Configure a new Git provider for GitOps syncing"}
|
|
97
|
+
</DialogDescription>
|
|
98
|
+
</DialogHeader>
|
|
99
|
+
|
|
100
|
+
<div className="space-y-4 py-4">
|
|
101
|
+
<div className="space-y-2">
|
|
102
|
+
<Label htmlFor="provider-type">Provider Type</Label>
|
|
103
|
+
<Select
|
|
104
|
+
value={type}
|
|
105
|
+
onValueChange={(v) => setType(v as "github" | "gitlab")}
|
|
106
|
+
disabled={!!initialData}
|
|
107
|
+
>
|
|
108
|
+
<SelectTrigger id="provider-type">
|
|
109
|
+
<SelectValue />
|
|
110
|
+
</SelectTrigger>
|
|
111
|
+
<SelectContent>
|
|
112
|
+
<SelectItem value="github">GitHub</SelectItem>
|
|
113
|
+
<SelectItem value="gitlab">GitLab</SelectItem>
|
|
114
|
+
</SelectContent>
|
|
115
|
+
</Select>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="space-y-2">
|
|
119
|
+
<Label htmlFor="provider-target">Target</Label>
|
|
120
|
+
<Input
|
|
121
|
+
id="provider-target"
|
|
122
|
+
placeholder="e.g. my-org or my-org/my-repo"
|
|
123
|
+
value={target}
|
|
124
|
+
onChange={(e) => setTarget(e.target.value)}
|
|
125
|
+
required
|
|
126
|
+
/>
|
|
127
|
+
<p className="text-xs text-muted-foreground">
|
|
128
|
+
Organization name for org-wide scanning, or owner/repo for a single repository.
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="space-y-2">
|
|
133
|
+
<Label htmlFor="provider-path">Path Pattern</Label>
|
|
134
|
+
<Input
|
|
135
|
+
id="provider-path"
|
|
136
|
+
placeholder=".checkstack/**/*.yaml"
|
|
137
|
+
value={pathPattern}
|
|
138
|
+
onChange={(e) => setPathPattern(e.target.value)}
|
|
139
|
+
required
|
|
140
|
+
/>
|
|
141
|
+
<p className="text-xs text-muted-foreground">
|
|
142
|
+
Glob pattern for matching descriptor files in repositories.
|
|
143
|
+
</p>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
<Label htmlFor="provider-base-url">Base URL (optional)</Label>
|
|
148
|
+
<Input
|
|
149
|
+
id="provider-base-url"
|
|
150
|
+
placeholder="https://github.example.com/api/v3"
|
|
151
|
+
value={baseUrl}
|
|
152
|
+
onChange={(e) => setBaseUrl(e.target.value)}
|
|
153
|
+
/>
|
|
154
|
+
<p className="text-xs text-muted-foreground">
|
|
155
|
+
For GitHub Enterprise or self-hosted GitLab instances.
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div className="space-y-2">
|
|
160
|
+
<Label htmlFor="provider-auth-token">
|
|
161
|
+
Auth Token {initialData ? "(leave empty to keep current)" : "(optional)"}
|
|
162
|
+
</Label>
|
|
163
|
+
<Input
|
|
164
|
+
id="provider-auth-token"
|
|
165
|
+
type="password"
|
|
166
|
+
placeholder="ghp_xxxx..."
|
|
167
|
+
value={authToken}
|
|
168
|
+
onChange={(e) => setAuthToken(e.target.value)}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div className="grid grid-cols-2 gap-4">
|
|
173
|
+
<div className="space-y-2">
|
|
174
|
+
<Label htmlFor="provider-interval">Sync Interval (seconds)</Label>
|
|
175
|
+
<Input
|
|
176
|
+
id="provider-interval"
|
|
177
|
+
type="number"
|
|
178
|
+
min={60}
|
|
179
|
+
value={syncInterval}
|
|
180
|
+
onChange={(e) => setSyncInterval(e.target.value)}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="space-y-2">
|
|
185
|
+
<Label htmlFor="provider-deletion">Deletion Policy</Label>
|
|
186
|
+
<Select
|
|
187
|
+
value={deletionPolicy}
|
|
188
|
+
onValueChange={(v) => setDeletionPolicy(v as "orphan" | "auto")}
|
|
189
|
+
>
|
|
190
|
+
<SelectTrigger id="provider-deletion">
|
|
191
|
+
<SelectValue />
|
|
192
|
+
</SelectTrigger>
|
|
193
|
+
<SelectContent>
|
|
194
|
+
<SelectItem value="orphan">Orphan (manual review)</SelectItem>
|
|
195
|
+
<SelectItem value="auto">Auto-delete</SelectItem>
|
|
196
|
+
</SelectContent>
|
|
197
|
+
</Select>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<DialogFooter>
|
|
203
|
+
<Button type="button" variant="outline" onClick={onClose}>
|
|
204
|
+
Cancel
|
|
205
|
+
</Button>
|
|
206
|
+
<Button type="submit" disabled={!target.trim() || !pathPattern.trim()}>
|
|
207
|
+
{initialData ? "Save Changes" : "Add Provider"}
|
|
208
|
+
</Button>
|
|
209
|
+
</DialogFooter>
|
|
210
|
+
</form>
|
|
211
|
+
</DialogContent>
|
|
212
|
+
</Dialog>
|
|
213
|
+
);
|
|
214
|
+
};
|