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:
parent
3547ce1096
commit
e2f30ee28a
9 changed files with 353 additions and 192 deletions
24
apps/web/src/components/studio/ConnectivityView.stories.tsx
Normal file
24
apps/web/src/components/studio/ConnectivityView.stories.tsx
Normal 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 />,
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue