refactor(studio): ConnectivityView module, re-export, stories

- Module connectivity-view: types, useConnectivityView, WebDAV, Webhooks,
  Skeleton, orchestrator ConnectivityView
- Re-export from ConnectivityView.tsx
- Stories: Default, Loading (Skeleton); decorator min-h-layout-page
- Fix: text-[10px] -> text-xs (Mount Password hint)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
senke 2026-02-06 02:02:00 +01:00
parent 3547ce1096
commit e2f30ee28a
9 changed files with 353 additions and 192 deletions

View file

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ConnectivityView, ConnectivityViewSkeleton } from './connectivity-view';
const meta: Meta<typeof ConnectivityView> = {
title: 'Components/Features/Studio/ConnectivityView',
component: ConnectivityView,
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-layout-page p-4">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Loading: Story = {
render: () => <ConnectivityViewSkeleton />,
};

View file

@ -1,192 +1 @@
import React, { useState } from 'react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Globe, Copy, Plus, Trash2, CheckCircle, Folder } from 'lucide-react';
import { useToast } from '../../components/feedback/ToastProvider';
export const ConnectivityView: React.FC = () => {
const { addToast } = useToast();
// WebDAV State
const [webdavPass, setWebdavPass] = useState('****');
// Webhooks State
const [webhooks, setWebhooks] = useState([
{
id: '1',
url: 'https://api.myapp.com/hooks/veza',
events: ['file.upload', 'file.delete'],
},
]);
const [newHookUrl, setNewHookUrl] = useState('');
const generateWebdavPass = () => {
setWebdavPass(`wd-${Math.random().toString(36).substr(2, 10)}`);
addToast('New WebDAV password generated', 'success');
};
const addWebhook = () => {
if (!newHookUrl) return;
setWebhooks([
...webhooks,
{ id: Date.now().toString(), url: newHookUrl, events: ['file.upload'] },
]);
setNewHookUrl('');
addToast('Webhook endpoint added', 'success');
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
addToast('Copied to clipboard');
};
return (
<div className="h-full flex flex-col gap-8 animate-fadeIn">
{/* WebDAV Mount Section */}
<Card variant="default">
<div className="flex items-start justify-between mb-6">
<div>
<h3 className="text-xl font-bold text-white flex items-center gap-2">
<Folder className="w-5 h-5 text-kodo-gold" /> Directory Mount
(WebDAV)
</h3>
<p className="text-sm text-kodo-content-dim mt-1">
Access your Cloud Studio files directly from your OS file explorer
or DAW.
</p>
</div>
<div className="bg-kodo-gold/10 text-kodo-gold px-4 py-1 rounded text-xs font-bold border border-kodo-gold/20">
PROTOCOL ACTIVE
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-kodo-ink p-6 rounded-xl border border-kodo-steel">
<div>
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
Server URL
</label>
<div className="flex gap-2">
<div className="flex-1 bg-kodo-graphite border border-kodo-steel rounded px-4 py-2 text-sm text-kodo-text-main font-mono truncate">
https://webdav.veza.io/u/cyber_producer
</div>
<Button
variant="ghost"
size="icon"
className="border border-kodo-steel"
onClick={() =>
copyToClipboard('https://webdav.veza.io/u/cyber_producer')
}
>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
<div>
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
Username
</label>
<div className="flex gap-2">
<div className="flex-1 bg-kodo-graphite border border-kodo-steel rounded px-4 py-2 text-sm text-kodo-text-main font-mono">
cyber_producer
</div>
<Button
variant="ghost"
size="icon"
className="border border-kodo-steel"
onClick={() => copyToClipboard('cyber_producer')}
>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
<div className="md:col-span-2">
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
Mount Password
</label>
<div className="flex gap-2">
<div className="flex-1 bg-kodo-graphite border border-kodo-steel rounded px-4 py-2 text-sm text-white font-mono tracking-widest">
{webdavPass}
</div>
<Button variant="secondary" onClick={generateWebdavPass}>
Generate New
</Button>
</div>
<p className="text-[10px] text-kodo-content-dim mt-2">
Use this specific password for mounting. Do not use your account
login password.
</p>
</div>
</div>
</Card>
{/* Webhooks Section */}
<Card variant="default">
<div className="flex items-start justify-between mb-6">
<div>
<h3 className="text-xl font-bold text-white flex items-center gap-2">
<Globe className="w-5 h-5 text-kodo-steel" /> Storage Webhooks
</h3>
<p className="text-sm text-kodo-content-dim mt-1">
Trigger actions in external apps when files change.
</p>
</div>
</div>
<div className="space-y-4">
<div className="flex gap-2">
<Input
placeholder="https://api.your-app.com/hook"
value={newHookUrl}
onChange={(e) => setNewHookUrl(e.target.value)}
/>
<Button
variant="primary"
icon={<Plus className="w-4 h-4" />}
onClick={addWebhook}
>
Add
</Button>
</div>
<div className="space-y-2">
{webhooks.map((hook) => (
<div
key={hook.id}
className="flex items-center justify-between p-4 bg-kodo-ink rounded border border-kodo-steel"
>
<div className="flex-1 min-w-0 mr-4">
<div className="text-sm font-mono text-white truncate">
{hook.url}
</div>
<div className="text-xs text-kodo-content-dim mt-1 flex gap-2">
{hook.events.map((ev) => (
<span key={ev} className="bg-white/5 px-1 rounded">
{ev}
</span>
))}
</div>
</div>
<div className="flex items-center gap-2">
<div className="text-xs text-kodo-lime flex items-center gap-1">
<CheckCircle className="w-3 h-3" /> Active
</div>
<Button
variant="ghost"
size="icon"
className="text-kodo-red hover:bg-kodo-red/10"
onClick={() =>
setWebhooks(webhooks.filter((h) => h.id !== hook.id))
}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
))}
</div>
</div>
</Card>
</div>
);
};
export { ConnectivityView, ConnectivityViewSkeleton } from './connectivity-view';

View file

@ -0,0 +1,34 @@
import React from 'react';
import { useConnectivityView } from './useConnectivityView';
import { ConnectivityViewWebDAV } from './ConnectivityViewWebDAV';
import { ConnectivityViewWebhooks } from './ConnectivityViewWebhooks';
export function ConnectivityView() {
const {
webdavPass,
webhooks,
newHookUrl,
setNewHookUrl,
generateWebdavPass,
addWebhook,
removeWebhook,
copyToClipboard,
} = useConnectivityView();
return (
<div className="h-full flex flex-col gap-8 animate-fadeIn">
<ConnectivityViewWebDAV
webdavPass={webdavPass}
onGeneratePass={generateWebdavPass}
onCopy={copyToClipboard}
/>
<ConnectivityViewWebhooks
webhooks={webhooks}
newHookUrl={newHookUrl}
onNewHookUrlChange={setNewHookUrl}
onAddWebhook={addWebhook}
onRemoveWebhook={removeWebhook}
/>
</div>
);
}

View file

@ -0,0 +1,47 @@
/**
* Skeleton for ConnectivityView layout primitives, no arbitrary values.
*/
export function ConnectivityViewSkeleton() {
return (
<div className="h-full flex flex-col gap-8 animate-fadeIn">
<div className="rounded-xl border border-kodo-steel p-6 space-y-6">
<div className="flex items-start justify-between">
<div className="space-y-2">
<div className="h-6 w-48 rounded bg-muted animate-pulse" />
<div className="h-4 w-72 rounded bg-muted animate-pulse" />
</div>
<div className="h-6 w-28 rounded bg-muted animate-pulse" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-kodo-ink p-6 rounded-xl border border-kodo-steel">
<div className="space-y-2">
<div className="h-4 w-20 rounded bg-muted animate-pulse" />
<div className="h-11 w-full rounded bg-muted animate-pulse" />
</div>
<div className="space-y-2">
<div className="h-4 w-20 rounded bg-muted animate-pulse" />
<div className="h-11 w-full rounded bg-muted animate-pulse" />
</div>
<div className="md:col-span-2 space-y-2">
<div className="h-4 w-28 rounded bg-muted animate-pulse" />
<div className="h-11 w-full rounded bg-muted animate-pulse" />
</div>
</div>
</div>
<div className="rounded-xl border border-kodo-steel p-6 space-y-4">
<div className="space-y-2">
<div className="h-6 w-40 rounded bg-muted animate-pulse" />
<div className="h-4 w-56 rounded bg-muted animate-pulse" />
</div>
<div className="flex gap-2">
<div className="h-11 flex-1 rounded-lg bg-muted animate-pulse" />
<div className="h-11 w-20 rounded-lg bg-muted animate-pulse" />
</div>
<div className="space-y-2">
<div className="h-16 w-full rounded border border-kodo-steel bg-kodo-ink animate-pulse" />
<div className="h-16 w-full rounded border border-kodo-steel bg-kodo-ink animate-pulse" />
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,91 @@
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Folder, Copy } from 'lucide-react';
import { DEFAULT_WEBDAV_URL, DEFAULT_WEBDAV_USER } from './types';
interface ConnectivityViewWebDAVProps {
webdavPass: string;
onGeneratePass: () => void;
onCopy: (text: string) => void;
}
export function ConnectivityViewWebDAV({
webdavPass,
onGeneratePass,
onCopy,
}: ConnectivityViewWebDAVProps) {
return (
<Card variant="default">
<div className="flex items-start justify-between mb-6">
<div>
<h3 className="text-xl font-bold text-white flex items-center gap-2">
<Folder className="w-5 h-5 text-kodo-gold" /> Directory Mount (WebDAV)
</h3>
<p className="text-sm text-kodo-content-dim mt-1">
Access your Cloud Studio files directly from your OS file explorer or DAW.
</p>
</div>
<div className="bg-kodo-gold/10 text-kodo-gold px-4 py-1 rounded text-xs font-bold border border-kodo-gold/20">
PROTOCOL ACTIVE
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-kodo-ink p-6 rounded-xl border border-kodo-steel">
<div>
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
Server URL
</label>
<div className="flex gap-2">
<div className="flex-1 bg-kodo-graphite border border-kodo-steel rounded px-4 py-2 text-sm text-kodo-text-main font-mono truncate">
{DEFAULT_WEBDAV_URL}
</div>
<Button
variant="ghost"
size="icon"
className="border border-kodo-steel"
onClick={() => onCopy(DEFAULT_WEBDAV_URL)}
aria-label="Copy Server URL"
>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
<div>
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
Username
</label>
<div className="flex gap-2">
<div className="flex-1 bg-kodo-graphite border border-kodo-steel rounded px-4 py-2 text-sm text-kodo-text-main font-mono">
{DEFAULT_WEBDAV_USER}
</div>
<Button
variant="ghost"
size="icon"
className="border border-kodo-steel"
onClick={() => onCopy(DEFAULT_WEBDAV_USER)}
aria-label="Copy Username"
>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
<div className="md:col-span-2">
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
Mount Password
</label>
<div className="flex gap-2">
<div className="flex-1 bg-kodo-graphite border border-kodo-steel rounded px-4 py-2 text-sm text-white font-mono tracking-widest">
{webdavPass}
</div>
<Button variant="secondary" onClick={onGeneratePass}>
Generate New
</Button>
</div>
<p className="text-xs text-kodo-content-dim mt-2">
Use this specific password for mounting. Do not use your account login password.
</p>
</div>
</div>
</Card>
);
}

View file

@ -0,0 +1,83 @@
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Globe, Plus, Trash2, CheckCircle } from 'lucide-react';
import type { WebhookItem } from './types';
interface ConnectivityViewWebhooksProps {
webhooks: WebhookItem[];
newHookUrl: string;
onNewHookUrlChange: (value: string) => void;
onAddWebhook: () => void;
onRemoveWebhook: (id: string) => void;
}
export function ConnectivityViewWebhooks({
webhooks,
newHookUrl,
onNewHookUrlChange,
onAddWebhook,
onRemoveWebhook,
}: ConnectivityViewWebhooksProps) {
return (
<Card variant="default">
<div className="flex items-start justify-between mb-6">
<div>
<h3 className="text-xl font-bold text-white flex items-center gap-2">
<Globe className="w-5 h-5 text-kodo-steel" /> Storage Webhooks
</h3>
<p className="text-sm text-kodo-content-dim mt-1">
Trigger actions in external apps when files change.
</p>
</div>
</div>
<div className="space-y-4">
<div className="flex gap-2">
<Input
placeholder="https://api.your-app.com/hook"
value={newHookUrl}
onChange={(e) => onNewHookUrlChange(e.target.value)}
/>
<Button variant="primary" icon={<Plus className="w-4 h-4" />} onClick={onAddWebhook}>
Add
</Button>
</div>
<div className="space-y-2">
{webhooks.map((hook) => (
<div
key={hook.id}
className="flex items-center justify-between p-4 bg-kodo-ink rounded border border-kodo-steel"
>
<div className="flex-1 min-w-0 mr-4">
<div className="text-sm font-mono text-white truncate">{hook.url}</div>
<div className="text-xs text-kodo-content-dim mt-1 flex gap-2">
{hook.events.map((ev) => (
<span key={ev} className="bg-white/5 px-1 rounded">
{ev}
</span>
))}
</div>
</div>
<div className="flex items-center gap-2">
<div className="text-xs text-kodo-lime flex items-center gap-1">
<CheckCircle className="w-3 h-3" /> Active
</div>
<Button
variant="ghost"
size="icon"
className="text-kodo-red hover:bg-kodo-red/10"
onClick={() => onRemoveWebhook(hook.id)}
aria-label="Remove webhook"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
))}
</div>
</div>
</Card>
);
}

View file

@ -0,0 +1,7 @@
export { ConnectivityView } from './ConnectivityView';
export { ConnectivityViewSkeleton } from './ConnectivityViewSkeleton';
export { ConnectivityViewWebDAV } from './ConnectivityViewWebDAV';
export { ConnectivityViewWebhooks } from './ConnectivityViewWebhooks';
export { useConnectivityView } from './useConnectivityView';
export { DEFAULT_WEBDAV_URL, DEFAULT_WEBDAV_USER } from './types';
export type { WebhookItem } from './types';

View file

@ -0,0 +1,8 @@
export interface WebhookItem {
id: string;
url: string;
events: string[];
}
export const DEFAULT_WEBDAV_URL = 'https://webdav.veza.io/u/cyber_producer';
export const DEFAULT_WEBDAV_USER = 'cyber_producer';

View file

@ -0,0 +1,58 @@
import { useState, useCallback } from 'react';
import { useToast } from '@/components/feedback/ToastProvider';
import type { WebhookItem } from './types';
export function useConnectivityView() {
const { addToast } = useToast();
const [webdavPass, setWebdavPass] = useState('****');
const [webhooks, setWebhooks] = useState<WebhookItem[]>([
{
id: '1',
url: 'https://api.myapp.com/hooks/veza',
events: ['file.upload', 'file.delete'],
},
]);
const [newHookUrl, setNewHookUrl] = useState('');
const generateWebdavPass = useCallback(() => {
setWebdavPass(`wd-${Math.random().toString(36).substring(2, 12)}`);
addToast('New WebDAV password generated', 'success');
}, [addToast]);
const addWebhook = useCallback(() => {
if (!newHookUrl.trim()) return;
setWebhooks((prev) => [
...prev,
{
id: Date.now().toString(),
url: newHookUrl.trim(),
events: ['file.upload'],
},
]);
setNewHookUrl('');
addToast('Webhook endpoint added', 'success');
}, [newHookUrl, addToast]);
const removeWebhook = useCallback((id: string) => {
setWebhooks((prev) => prev.filter((h) => h.id !== id));
}, []);
const copyToClipboard = useCallback(
(text: string) => {
navigator.clipboard.writeText(text);
addToast('Copied to clipboard');
},
[addToast],
);
return {
webdavPass,
webhooks,
newHookUrl,
setNewHookUrl,
generateWebdavPass,
addWebhook,
removeWebhook,
copyToClipboard,
};
}