feat(13-02): add GdriveAuthToggle and BackendIcons components
- GdriveAuthToggle: two-tab segmented control (OAuth Token / Service Account) following AzureAuthToggle pattern; both fields always registered via CSS hidden/block; OAuth tab embeds OAuthInstructions component - BackendIcons: inline SVG icon map for all 18 backends (Partial<Record<BackendType,...>>) using fill="currentColor" for MD3 dark-mode compatibility, no external deps - Created src/components/icons/ directory
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
// BackendIcons.tsx
|
||||
// Inline SVG icon map keyed by BackendType.
|
||||
// All icons use fill="currentColor" for MD3 dark-mode compatibility.
|
||||
// Each icon is designed for 24x24px viewBox and works as monochrome.
|
||||
|
||||
import type { BackendType } from '../../schemas/registry';
|
||||
|
||||
export interface IconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Azure Blob Storage — cloud with A shape
|
||||
const AzureBlobIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
|
||||
<path d="M10 14l2-4 2 4h-1.25v2h-1.5v-2H10z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Amazon S3 — cloud with S shape
|
||||
const S3Icon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
|
||||
<text x="8.5" y="17" fontSize="7" fontFamily="sans-serif" fontWeight="bold">S3</text>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// S3-Compatible — generic cloud with layers
|
||||
const S3CompatibleIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M6.5 18a4.5 4.5 0 01-.5-8.97V9a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
|
||||
<rect x="9" y="14" width="6" height="1.5" rx="0.75" />
|
||||
<rect x="9" y="16.5" width="6" height="1.5" rx="0.75" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Google Cloud Storage — cloud with G
|
||||
const GCSIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
|
||||
<text x="8" y="17.5" fontSize="7" fontFamily="sans-serif" fontWeight="bold">G</text>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Backblaze B2 — cloud with B2
|
||||
const B2Icon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
|
||||
<text x="7.5" y="17.5" fontSize="6.5" fontFamily="sans-serif" fontWeight="bold">B2</text>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Azure Files — file-share icon (folder with network lines)
|
||||
const AzureFilesIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M2 6.5A2.5 2.5 0 014.5 4H9l2 2h8.5A2.5 2.5 0 0122 8.5v9a2.5 2.5 0 01-2.5 2.5h-15A2.5 2.5 0 012 17.5V6.5z" />
|
||||
<path fillRule="evenodd" d="M8 12h8v1.5H8V12zm2 2.5h4V16h-4v-1.5z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// OpenStack Swift — server/globe icon
|
||||
const SwiftIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<circle cx="12" cy="12" r="9" fillOpacity="0.15" />
|
||||
<path d="M12 3a9 9 0 100 18A9 9 0 0012 3zm0 1.5c.9 0 1.9.6 2.7 1.7H9.3C10.1 5.1 11.1 4.5 12 4.5zm-4.5 3.2h9a7.5 7.5 0 010 8.6h-9a7.5 7.5 0 010-8.6z" />
|
||||
<path d="M3 12h18M12 3v18" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.4" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// OneDrive — cloud with one-drive O shape
|
||||
const OnedriveIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M14.5 8a5.5 5.5 0 015.46 4.83A4 4 0 0120 20H7a5 5 0 01-1-9.9V10a7.5 7.5 0 017.5-7.5V8z" />
|
||||
<path d="M4 14a5 5 0 015-5h1a5.5 5.5 0 00-1 3.16A4 4 0 014.5 20H4a4 4 0 010-8z" fillOpacity="0.7" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Google Drive — drive/triangle icon
|
||||
const GdriveIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M8.5 3.5L2 15h5l6.5-11.5H8.5z" />
|
||||
<path d="M22 15l-6.5-11.5h-1.5L22 15z" fillOpacity="0.7" />
|
||||
<path d="M2 15l4 6.5h12L22 15H2z" fillOpacity="0.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Dropbox — box icon
|
||||
const DropboxIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M12 4.5L6 8l6 3.5 6-3.5-6-3.5z" />
|
||||
<path d="M6 8l-4 2.5 6 3.5 4-2.5L6 8z" fillOpacity="0.7" />
|
||||
<path d="M18 8l-6 3.5 4 2.5 6-3.5L18 8z" fillOpacity="0.7" />
|
||||
<path d="M8 15l4 2.5 4-2.5-4-2.5L8 15z" />
|
||||
<circle cx="12" cy="20" r="1.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Box — stylized box/square icon
|
||||
const BoxIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<rect x="3" y="8" width="18" height="13" rx="2" />
|
||||
<path d="M9 8V5a3 3 0 016 0v3" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
<circle cx="12" cy="15" r="2" fill="none" stroke="currentColor" strokeWidth="1.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// pCloud — cloud with p letter
|
||||
const PcloudIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
|
||||
<text x="9" y="17.5" fontSize="7" fontFamily="sans-serif" fontWeight="bold">p</text>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// SFTP — server with lock icon
|
||||
const SftpIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<rect x="3" y="4" width="18" height="5" rx="1" />
|
||||
<rect x="3" y="11" width="18" height="5" rx="1" fillOpacity="0.6" />
|
||||
<circle cx="19" cy="6.5" r="1" fill="white" fillOpacity="0.8" />
|
||||
<circle cx="19" cy="13.5" r="1" fill="white" fillOpacity="0.8" />
|
||||
<path d="M12 18a2.5 2.5 0 012.5 2.5v1h-5v-1A2.5 2.5 0 0112 18z" />
|
||||
<rect x="10" y="16" width="4" height="3" rx="1" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// FTP — server icon (simple)
|
||||
const FtpIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<rect x="3" y="4" width="18" height="5" rx="1" />
|
||||
<rect x="3" y="11" width="18" height="5" rx="1" fillOpacity="0.6" />
|
||||
<rect x="3" y="18" width="18" height="3" rx="1" fillOpacity="0.3" />
|
||||
<circle cx="19" cy="6.5" r="1" fill="white" fillOpacity="0.8" />
|
||||
<circle cx="19" cy="13.5" r="1" fill="white" fillOpacity="0.8" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// WebDAV — globe with network lines
|
||||
const WebdavIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M12 2a10 10 0 100 20A10 10 0 0012 2zm0 1.5c1 0 2.3.8 3.3 2.5H8.7C9.7 4.3 11 3.5 12 3.5zm-6.5 4h13a8.5 8.5 0 010 9H5.5a8.5 8.5 0 010-9z" fillOpacity="0.8" />
|
||||
<path d="M2 12h20M12 2v20" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// SMB / Windows Share — folder with network
|
||||
const SmbIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M2 6.5A2.5 2.5 0 014.5 4H9l2 2h8.5A2.5 2.5 0 0122 8.5v9a2.5 2.5 0 01-2.5 2.5h-15A2.5 2.5 0 012 17.5V6.5z" />
|
||||
<circle cx="9" cy="14" r="1.5" fill="white" fillOpacity="0.7" />
|
||||
<circle cx="15" cy="14" r="1.5" fill="white" fillOpacity="0.7" />
|
||||
<path d="M9 14h6" stroke="white" strokeWidth="1" strokeOpacity="0.7" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// HTTP — globe/web icon
|
||||
const HttpIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<path d="M12 2a10 10 0 100 20A10 10 0 0012 2zm0 1.5c1.1 0 2.5.9 3.5 2.5H8.5C9.5 4.4 10.9 3.5 12 3.5zm-7 5h14a8.5 8.5 0 010 7H5a8.5 8.5 0 010-7z" />
|
||||
<path d="M2 12h20" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Seafile — sea/wave icon
|
||||
const SeafileIcon = ({ className }: IconProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||
<rect x="4" y="4" width="16" height="11" rx="2" fillOpacity="0.8" />
|
||||
<path d="M4 19c2-2 4-2 6 0s4 2 6 0" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" />
|
||||
<path d="M4 22c2-2 4-2 6 0s4 2 6 0" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" opacity="0.4" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const BACKEND_ICONS: Partial<Record<BackendType, React.FC<IconProps>>> = {
|
||||
azureblob: AzureBlobIcon,
|
||||
s3: S3Icon,
|
||||
's3-compatible': S3CompatibleIcon,
|
||||
gcs: GCSIcon,
|
||||
b2: B2Icon,
|
||||
'azure-files': AzureFilesIcon,
|
||||
swift: SwiftIcon,
|
||||
onedrive: OnedriveIcon,
|
||||
gdrive: GdriveIcon,
|
||||
dropbox: DropboxIcon,
|
||||
box: BoxIcon,
|
||||
pcloud: PcloudIcon,
|
||||
sftp: SftpIcon,
|
||||
ftp: FtpIcon,
|
||||
webdav: WebdavIcon,
|
||||
smb: SmbIcon,
|
||||
http: HttpIcon,
|
||||
seafile: SeafileIcon,
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useState } from 'react';
|
||||
import type { UseFormRegister, FieldError } from 'react-hook-form';
|
||||
import { PasswordField } from '../ui/PasswordField';
|
||||
import { OAuthInstructions } from './OAuthInstructions';
|
||||
|
||||
type GdriveAuthMethod = 'oauth' | 'service-account';
|
||||
|
||||
interface GdriveAuthToggleProps {
|
||||
register: UseFormRegister<any>;
|
||||
errors: {
|
||||
token?: FieldError;
|
||||
service_account_credentials?: FieldError;
|
||||
};
|
||||
}
|
||||
|
||||
export function GdriveAuthToggle({ register, errors }: GdriveAuthToggleProps) {
|
||||
const [authMethod, setAuthMethod] = useState<GdriveAuthMethod>('oauth');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Segmented control */}
|
||||
<div className="flex rounded-md border border-outline overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAuthMethod('oauth')}
|
||||
className={[
|
||||
'flex-1 py-1.5 text-sm font-medium transition-colors',
|
||||
authMethod === 'oauth'
|
||||
? 'bg-primary text-on-primary'
|
||||
: 'bg-surface-container text-on-surface-container hover:bg-surface',
|
||||
].join(' ')}
|
||||
>
|
||||
OAuth Token
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAuthMethod('service-account')}
|
||||
className={[
|
||||
'flex-1 py-1.5 text-sm font-medium transition-colors',
|
||||
authMethod === 'service-account'
|
||||
? 'bg-primary text-on-primary'
|
||||
: 'bg-surface-container text-on-surface-container hover:bg-surface',
|
||||
].join(' ')}
|
||||
>
|
||||
Service Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Both fields always registered — only active one visible via CSS */}
|
||||
<div className={authMethod === 'oauth' ? 'block' : 'hidden'}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<OAuthInstructions
|
||||
backendName="Google Drive"
|
||||
authorizeCommand='rclone authorize "drive"'
|
||||
/>
|
||||
<PasswordField
|
||||
id="token"
|
||||
label="OAuth Token (JSON)"
|
||||
error={errors.token}
|
||||
registration={register('token')}
|
||||
placeholder='{"access_token":"...","token_type":"Bearer","refresh_token":"...","expiry":"..."}'
|
||||
helpText='Paste the JSON token from: rclone authorize "drive"'
|
||||
tooltipText='Run `rclone authorize "drive"` on a machine with a browser. A browser window opens, you sign in with Google, and rclone prints a JSON token — paste that entire JSON blob here.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={authMethod === 'service-account' ? 'block' : 'hidden'}>
|
||||
<PasswordField
|
||||
id="service_account_credentials"
|
||||
label="Service Account JSON"
|
||||
error={errors.service_account_credentials}
|
||||
registration={register('service_account_credentials')}
|
||||
placeholder='{"type":"service_account","project_id":"..."}'
|
||||
helpText="For unattended/server use: paste the full service account JSON key file content. Use this OR an OAuth token, not both."
|
||||
tooltipText="Create a service account at Google Cloud Console > IAM & Admin > Service Accounts, share the target Drive with that service account email, then download the JSON key and paste its contents here."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user