grpctestify 1.8.6

gRPC testing utility written in Rust
Documentation
import { useState, useEffect } from 'react';
import { btn, colors } from '../../lib/theme';
import { Upload, FileText, Check, AlertCircle, Loader2 } from 'lucide-react';

interface ProtoFile {
  path: string;
  name: string;
  size: number;
}

export function ProtoManager() {
  const [files, setFiles] = useState<ProtoFile[]>([]);
  const [loading, setLoading] = useState(false);
  const [uploadName, setUploadName] = useState('');
  const [uploadContent, setUploadContent] = useState('');
  const [uploading, setUploading] = useState(false);
  const [uploadError, setUploadError] = useState<string | null>(null);
  const [uploadSuccess, setUploadSuccess] = useState(false);

  const fetchFiles = async () => {
    setLoading(true);
    try {
      const res = await fetch('/api/proto-files');
      if (res.ok) setFiles(await res.json());
    } catch {  }
    setLoading(false);
  };

  useEffect(() => { fetchFiles(); }, []);

  const handleUpload = async () => {
    if (!uploadName.trim() || !uploadContent.trim()) return;
    setUploading(true);
    setUploadError(null);
    setUploadSuccess(false);
    try {
      const res = await fetch('/api/proto-upload', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ filename: uploadName.trim(), content: uploadContent }),
      });
      if (res.ok) {
        setUploadSuccess(true);
        setUploadName('');
        setUploadContent('');
        fetchFiles();
        setTimeout(() => setUploadSuccess(false), 3000);
      } else {
        const text = await res.text();
        setUploadError(text || 'Upload failed');
      }
    } catch (err) {
      setUploadError(String(err));
    } finally {
      setUploading(false);
    }
  };

  const exampleProto = `syntax = "proto3";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
`;

  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
        <span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.6px' }}>
          Proto Files
        </span>
        <span style={{ flex: 1 }} />
        {loading && <Loader2 size={12} className="animate-spin" />}
      </div>

      {}
      <div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: 8, marginBottom: 8 }}>
        <input value={uploadName} onChange={e => setUploadName(e.target.value)} placeholder="filename.proto"
          style={{ width: '100%', border: '1px solid var(--border)', borderRadius: 4, padding: '4px 8px', fontSize: 12, fontFamily: 'monospace', background: 'var(--bg-primary)', color: 'var(--text-primary)', outline: 'none', boxSizing: 'border-box', marginBottom: 4 }} />
        <textarea value={uploadContent} onChange={e => setUploadContent(e.target.value)} placeholder={`Paste .proto content here...\n\n${exampleProto}`} rows={4}
          style={{ width: '100%', border: '1px solid var(--border)', borderRadius: 4, padding: '4px 8px', fontSize: 11, fontFamily: 'monospace', background: 'var(--bg-primary)', color: 'var(--text-primary)', outline: 'none', resize: 'vertical', boxSizing: 'border-box', marginBottom: 4 }} />
        <button onClick={handleUpload} disabled={uploading || !uploadName.trim() || !uploadContent.trim()}
          style={{ ...btn('primary'), width: '100%', opacity: uploading ? 0.5 : 1, cursor: uploading ? 'not-allowed' : 'pointer', fontSize: 12 }}>
          {uploading ? <Loader2 size={12} className="animate-spin" /> : <Upload size={12} />}
          {uploading ? 'Uploading…' : 'Upload .proto'}
        </button>

        {uploadError && <div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: colors.error, marginTop: 4 }}><AlertCircle size={11} /> {uploadError}</div>}
        {uploadSuccess && <div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: colors.success, marginTop: 4 }}><Check size={11} /> Uploaded</div>}
      </div>

      {}
      {files.length === 0 && (
        <div style={{ fontSize: 12, color: 'var(--text-muted)', padding: '8px 0', textAlign: 'center' }}>
          No .proto files uploaded yet.
        </div>
      )}

      {files.map(f => (
        <div key={f.path} style={{
          display: 'flex', alignItems: 'center', gap: 6, padding: '4px 6px', borderRadius: 4,
          marginBottom: 2, fontSize: 12,
        }}
          onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-tertiary)'; }}
          onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
        >
          <FileText size={13} color={colors.accent} />
          <span style={{ fontFamily: 'monospace', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
          <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>{(f.size / 1024).toFixed(1)}KB</span>
        </div>
      ))}
    </div>
  );
}