feat: use FileInput DS component for file uploads, fix certs volume perms
- Replace inline FileField and native <input type="file"> with FileInput from @cameleer/design-system (drag-and-drop, icons, clear) - Update CertificatesPage and SsoPage to use FileInput + FormField - Fix /certs volume permissions (chmod 775) so cameleer user can write Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
8
ui/package-lock.json
generated
8
ui/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@cameleer/design-system": "^0.1.42",
|
||||
"@cameleer/design-system": "^0.0.0-snapshot.20260411.fd08d7a",
|
||||
"@logto/react": "^4.0.13",
|
||||
"@tanstack/react-query": "^5.90.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
@@ -309,9 +309,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@cameleer/design-system": {
|
||||
"version": "0.1.42",
|
||||
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.42/design-system-0.1.42.tgz",
|
||||
"integrity": "sha512-Cyy+2HsbBPLKRZaSGFxMUvIwI+g8ocdjcojFTGgtq5vrpE/8IYJLgxdtM9+eDoF2Zewk7MrBzfpNqjEYlQO3ng==",
|
||||
"version": "0.0.0-snapshot.20260411.fd08d7a",
|
||||
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.0.0-snapshot.20260411.fd08d7a/design-system-0.0.0-snapshot.20260411.fd08d7a.tgz",
|
||||
"integrity": "sha512-ehTXpF6U7bZpvqwj/aaZxdD75sDB8zpn1QVQMDzVUmQGH6RMf9O261DiV1agsOSqPNUB9UPVcdzevMo8ooWTFg==",
|
||||
"dependencies": {
|
||||
"lucide-react": "^1.7.0",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"postinstall": "node -e \"const fs=require('fs'),p='node_modules/@cameleer/design-system/assets/';if(fs.existsSync('public')){fs.copyFileSync(p+'cameleer3-logo.svg','public/favicon.svg')}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@cameleer/design-system": "^0.1.42",
|
||||
"@cameleer/design-system": "^0.0.0-snapshot.20260411.fd08d7a",
|
||||
"@logto/react": "^4.0.13",
|
||||
"@tanstack/react-query": "^5.90.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import {
|
||||
Alert, AlertDialog, Badge, Button, Card, DataTable,
|
||||
EmptyState, FormField, Input, Spinner, useToast,
|
||||
EmptyState, FileInput, FormField, Input, Spinner, useToast,
|
||||
} from '@cameleer/design-system';
|
||||
import type { Column } from '@cameleer/design-system';
|
||||
import type { Column, FileInputHandle } from '@cameleer/design-system';
|
||||
import { Shield, Plus, Trash2, FlaskConical, Upload, ShieldCheck } from 'lucide-react';
|
||||
import {
|
||||
useSsoConnectors, useCreateSsoConnector, useDeleteSsoConnector, useTestSsoConnector,
|
||||
@@ -331,12 +331,12 @@ function CaCertificatesSection() {
|
||||
const deleteMutation = useDeleteCaCert();
|
||||
const { toast } = useToast();
|
||||
|
||||
const certInputRef = useRef<HTMLInputElement>(null);
|
||||
const certRef = useRef<FileInputHandle>(null);
|
||||
const [label, setLabel] = useState('');
|
||||
const [deleteTarget, setDeleteTarget] = useState<CaCertResponse | null>(null);
|
||||
|
||||
async function handleUpload() {
|
||||
const file = certInputRef.current?.files?.[0];
|
||||
const file = certRef.current?.file;
|
||||
if (!file) {
|
||||
toast({ title: 'Select a CA certificate file', variant: 'error' });
|
||||
return;
|
||||
@@ -348,7 +348,7 @@ function CaCertificatesSection() {
|
||||
try {
|
||||
await stageMutation.mutateAsync(formData);
|
||||
toast({ title: 'CA certificate staged', variant: 'success' });
|
||||
if (certInputRef.current) certInputRef.current.value = '';
|
||||
certRef.current?.clear();
|
||||
setLabel('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Upload failed', description: String(err), variant: 'error' });
|
||||
@@ -409,21 +409,9 @@ function CaCertificatesSection() {
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: 13, color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
|
||||
CA Certificate (PEM) *
|
||||
</label>
|
||||
<input
|
||||
ref={certInputRef}
|
||||
type="file"
|
||||
accept=".pem,.crt,.cer"
|
||||
style={{
|
||||
fontSize: 13, color: 'var(--text-primary)',
|
||||
background: 'var(--bg-inset)', border: '1px solid var(--border)',
|
||||
borderRadius: 6, padding: '6px 8px', width: '100%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FormField label="CA Certificate (PEM) *">
|
||||
<FileInput ref={certRef} accept=".pem,.crt,.cer" icon={<ShieldCheck size={16} />} />
|
||||
</FormField>
|
||||
<Button variant="primary" onClick={handleUpload} loading={stageMutation.isPending}>
|
||||
<Upload size={14} style={{ marginRight: 6 }} />
|
||||
Stage Certificate
|
||||
|
||||
70
ui/src/pages/vendor/CertificatesPage.tsx
vendored
70
ui/src/pages/vendor/CertificatesPage.tsx
vendored
@@ -4,11 +4,14 @@ import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
FileInput,
|
||||
FormField,
|
||||
Input,
|
||||
Spinner,
|
||||
useToast,
|
||||
} from '@cameleer/design-system';
|
||||
import { Upload, ShieldCheck, RotateCcw, Trash2 } from 'lucide-react';
|
||||
import type { FileInputHandle } from '@cameleer/design-system';
|
||||
import { Upload, ShieldCheck, RotateCcw, Trash2, FileKey, KeyRound, ShieldPlus } from 'lucide-react';
|
||||
import {
|
||||
useVendorCertificates,
|
||||
useStageCertificate,
|
||||
@@ -101,34 +104,6 @@ function CertCard({
|
||||
);
|
||||
}
|
||||
|
||||
function FileField({ label, inputRef, accept }: {
|
||||
label: string;
|
||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
||||
accept: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<label style={{ fontSize: 13, color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept={accept}
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: 'var(--text-primary)',
|
||||
background: 'var(--bg-inset)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 6,
|
||||
padding: '6px 8px',
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CertificatesPage() {
|
||||
const { toast } = useToast();
|
||||
const { data, isLoading, isError } = useVendorCertificates();
|
||||
@@ -137,9 +112,9 @@ export function CertificatesPage() {
|
||||
const restoreMutation = useRestoreCertificate();
|
||||
const discardMutation = useDiscardStaged();
|
||||
|
||||
const certInputRef = useRef<HTMLInputElement>(null);
|
||||
const keyInputRef = useRef<HTMLInputElement>(null);
|
||||
const caInputRef = useRef<HTMLInputElement>(null);
|
||||
const certRef = useRef<FileInputHandle>(null);
|
||||
const keyRef = useRef<FileInputHandle>(null);
|
||||
const caRef = useRef<FileInputHandle>(null);
|
||||
const [keyPassword, setKeyPassword] = useState('');
|
||||
|
||||
if (isLoading) {
|
||||
@@ -161,9 +136,9 @@ export function CertificatesPage() {
|
||||
}
|
||||
|
||||
async function handleUpload() {
|
||||
const certFile = certInputRef.current?.files?.[0];
|
||||
const keyFile = keyInputRef.current?.files?.[0];
|
||||
const caFile = caInputRef.current?.files?.[0];
|
||||
const certFile = certRef.current?.file;
|
||||
const keyFile = keyRef.current?.file;
|
||||
const caFile = caRef.current?.file;
|
||||
|
||||
if (!certFile || !keyFile) {
|
||||
toast({ title: 'Certificate and key files are required', variant: 'error' });
|
||||
@@ -180,9 +155,9 @@ export function CertificatesPage() {
|
||||
const result = await stageMutation.mutateAsync(formData);
|
||||
if (result.valid) {
|
||||
toast({ title: 'Certificate staged successfully', variant: 'success' });
|
||||
if (certInputRef.current) certInputRef.current.value = '';
|
||||
if (keyInputRef.current) keyInputRef.current.value = '';
|
||||
if (caInputRef.current) caInputRef.current.value = '';
|
||||
certRef.current?.clear();
|
||||
keyRef.current?.clear();
|
||||
caRef.current?.clear();
|
||||
setKeyPassword('');
|
||||
} else {
|
||||
toast({ title: 'Validation failed', description: result.errors.join(', '), variant: 'error' });
|
||||
@@ -287,20 +262,23 @@ export function CertificatesPage() {
|
||||
{/* Upload card */}
|
||||
<Card title="Upload Certificate">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<FileField label="Certificate (PEM) *" inputRef={certInputRef} accept=".pem,.crt,.cer" />
|
||||
<FileField label="Private Key (PEM) *" inputRef={keyInputRef} accept=".pem,.key" />
|
||||
<div>
|
||||
<label style={{ fontSize: 13, color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
|
||||
Key Password (if encrypted)
|
||||
</label>
|
||||
<FormField label="Certificate (PEM) *">
|
||||
<FileInput ref={certRef} accept=".pem,.crt,.cer" icon={<FileKey size={16} />} />
|
||||
</FormField>
|
||||
<FormField label="Private Key (PEM) *">
|
||||
<FileInput ref={keyRef} accept=".pem,.key" icon={<KeyRound size={16} />} />
|
||||
</FormField>
|
||||
<FormField label="Key Password (if encrypted)">
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Leave empty if key is not encrypted"
|
||||
value={keyPassword}
|
||||
onChange={(e) => setKeyPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<FileField label="CA Bundle (PEM, optional)" inputRef={caInputRef} accept=".pem,.crt,.cer" />
|
||||
</FormField>
|
||||
<FormField label="CA Bundle (PEM, optional)">
|
||||
<FileInput ref={caRef} accept=".pem,.crt,.cer" icon={<ShieldPlus size={16} />} />
|
||||
</FormField>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleUpload}
|
||||
|
||||
Reference in New Issue
Block a user