improvements to key manager ui

This commit is contained in:
Jamie Pine
2022-10-17 21:01:40 -07:00
parent a82655973b
commit f300049873
11 changed files with 303 additions and 204 deletions

View File

@@ -1,4 +1,4 @@
import { ChevronLeftIcon, ChevronRightIcon, PhotoIcon } from '@heroicons/react/24/outline';
import { ChevronLeftIcon, ChevronRightIcon, TagIcon } from '@heroicons/react/24/outline';
import {
OperatingSystem,
getExplorerStore,
@@ -264,12 +264,12 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
// </Tooltip>
}
>
<div className="block w-[350px] h-[435px]">
<div className="block w-[350px]">
<KeyManager />
</div>
</OverlayPanel>
<Tooltip label="Cloud">
<TopBarButton icon={Cloud} />
<Tooltip label="Tag Assign Mode">
<TopBarButton icon={TagIcon} />
</Tooltip>
<Tooltip label="Refresh">
<TopBarButton icon={ArrowsClockwise} />

View File

@@ -0,0 +1,103 @@
import { InformationCircleIcon } from '@heroicons/react/24/outline';
import {
EllipsisVerticalIcon,
EyeIcon,
EyeSlashIcon,
KeyIcon,
LockClosedIcon,
LockOpenIcon,
PlusIcon,
TrashIcon,
XMarkIcon
} from '@heroicons/react/24/solid';
import { Button, Input, Select, SelectOption } from '@sd/ui';
import clsx from 'clsx';
import { Eject, EjectSimple, Plus } from 'phosphor-react';
import { useState } from 'react';
import { Toggle } from '../primitive';
import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip';
export type KeyManagerProps = DefaultProps;
// TODO: Replace this with Prisma type when integrating with backend
export interface Key {
id: string;
name: string;
mounted?: boolean;
locked?: boolean;
stats?: {
objectCount?: number;
containerCount?: number;
};
// Nodes this key is mounted on
nodes?: string[]; // will be node object
}
export const Key: React.FC<{ data: Key; index: number }> = ({ data, index }) => {
const odd = (index || 0) % 2 === 0;
return (
<div
className={clsx(
'flex items-center justify-between px-2 py-1.5 shadow-gray-900/20 text-sm text-gray-300 bg-gray-500/30 shadow-lg border-gray-500 rounded-lg'
// !odd && 'bg-opacity-10'
)}
>
<div className="flex items-center">
<KeyIcon
className={clsx(
'w-5 h-5 ml-1 mr-3',
data.mounted
? data.locked
? 'text-primary-600'
: 'text-primary-600'
: 'text-gray-400/80'
)}
/>
<div className="flex flex-col ">
<div className="flex flex-row items-center">
<div className="font-semibold">{data.name}</div>
{data.mounted && (
<div className="inline ml-2 px-1 text-[8pt] font-medium text-gray-300 bg-gray-500 rounded">
{data.nodes?.length || 0 > 0 ? `${data.nodes?.length || 0} nodes` : 'This node'}
</div>
)}
</div>
{/* <div className="text-xs text-gray-300 opacity-30">#{data.id}</div> */}
{data.stats ? (
<div className="flex flex-row mt-[1px] space-x-3">
{data.stats.objectCount && (
<div className="text-[8pt] font-medium text-gray-200 opacity-30">
{data.stats.objectCount} Objects
</div>
)}
{data.stats.containerCount && (
<div className="text-[8pt] font-medium text-gray-200 opacity-30">
{data.stats.containerCount} Containers
</div>
)}
</div>
) : (
!data.mounted && (
<div className="text-[8pt] font-medium text-gray-200 opacity-30">Key not mounted</div>
)
)}
</div>
</div>
<div className="space-x-1">
{data.mounted && (
<Tooltip label="Browse files">
<Button noPadding>
<EyeIcon className="w-4 h-4 text-gray-400" />
</Button>
</Tooltip>
)}
<Button noPadding>
<EllipsisVerticalIcon className="w-4 h-4 text-gray-400" />
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,59 @@
import { Button, CategoryHeading, Input, Select, SelectOption } from '@sd/ui';
import clsx from 'clsx';
import { Eject, EjectSimple, Plus } from 'phosphor-react';
import { useState } from 'react';
import { Toggle } from '../primitive';
import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip';
import { Key } from './Key';
export type KeyListProps = DefaultProps;
export function KeyList(props: KeyListProps) {
return (
<div className="flex flex-col h-full max-h-[360px]">
<div className="p-3 custom-scroll overlay-scroll">
<div className="">
{/* <CategoryHeading>Mounted keys</CategoryHeading> */}
<div className="space-y-1.5">
<Key
index={0}
data={{
id: 'af5570f5a1810b7a',
name: 'OBS Recordings',
mounted: true,
nodes: ['node1', 'node2'],
stats: { objectCount: 235, containerCount: 2 }
}}
/>
<Key
index={1}
data={{
id: 'af5570f5a1810b7a',
name: 'Unknown Key',
locked: true,
mounted: true,
stats: { objectCount: 45 }
}}
/>
<Key index={2} data={{ id: '7324695a52da67b1', name: 'Spacedrive Company' }} />
<Key index={3} data={{ id: 'b02303d68d05a562', name: 'Key 4' }} />
<Key index={3} data={{ id: 'b02303d68d05a562', name: 'Key 5' }} />
<Key index={3} data={{ id: 'b02303d68d05a562', name: 'Key 6' }} />
</div>
</div>
</div>
<div className="flex w-full p-2 bg-gray-600 border-t border-gray-500 rounded-b-md">
<Button size="sm" variant="gray">
Unmount All
</Button>
<div className="flex-grow" />
<Button size="sm" variant="gray">
Close
</Button>
</div>
</div>
);
}

View File

@@ -1,15 +1,4 @@
import { InformationCircleIcon } from '@heroicons/react/24/outline';
import {
EyeIcon,
EyeSlashIcon,
KeyIcon,
LockClosedIcon,
LockOpenIcon,
PlusIcon,
TrashIcon,
XMarkIcon
} from '@heroicons/react/24/solid';
import { Button, Input } from '@sd/ui';
import { Button, Input, Select, SelectOption, Tabs } from '@sd/ui';
import clsx from 'clsx';
import { Eject, EjectSimple, Plus } from 'phosphor-react';
import { useState } from 'react';
@@ -17,189 +6,31 @@ import { useState } from 'react';
import { Toggle } from '../primitive';
import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip';
import { Key } from './Key';
import { KeyList } from './KeyList';
import { KeyMounter } from './KeyMounter';
export type KeyManagerProps = DefaultProps;
interface FakeKey {
id: string;
name: string;
mounted?: boolean;
locked?: boolean;
stats?: {
objectCount?: number;
containerCount?: number;
};
// Nodes this key is mounted on
nodes?: string[]; // will be node object
}
const Heading: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="mt-1 mb-1 text-xs font-semibold text-gray-300">{children}</div>
);
const Key: React.FC<{ data: FakeKey; index: number }> = ({ data, index }) => {
const odd = (index || 0) % 2 === 0;
return (
<div
className={clsx(
'flex items-center justify-between px-2 py-1.5 shadow-gray-900/20 text-sm text-gray-300 bg-gray-500/30 shadow-lg border-gray-500 rounded-lg'
// !odd && 'bg-opacity-10'
)}
>
<div className="flex items-center">
<KeyIcon
className={clsx(
'w-5 h-5 ml-1 mr-3',
data.mounted
? data.locked
? 'text-primary-600'
: 'text-primary-600'
: 'text-gray-400/80'
)}
/>
<div className="flex flex-col ">
<div className="flex flex-row items-center">
<div className="font-semibold">{data.name}</div>
{data.mounted && (
<div className="inline ml-2 px-1 text-[8pt] font-medium text-gray-300 bg-gray-500 rounded">
{data.nodes?.length || 0 > 0 ? `${data.nodes?.length || 0} nodes` : 'This node'}
</div>
)}
</div>
{/* <div className="text-xs text-gray-300 opacity-30">#{data.id}</div> */}
{data.stats && (
<div className="flex flex-row mt-[1px] space-x-3">
{data.stats.objectCount && (
<div className="text-[8pt] font-medium text-gray-200 opacity-30">
{data.stats.objectCount} Objects
</div>
)}
{data.stats.containerCount && (
<div className="text-[8pt] font-medium text-gray-200 opacity-30">
{data.stats.containerCount} Containers
</div>
)}
</div>
)}
</div>
</div>
<div className="space-x-1">
{data.mounted ? (
<>
<Tooltip label="Browse files">
<Button noPadding>
<EyeIcon className="w-4 h-4 text-gray-400" />
</Button>
</Tooltip>
{data.locked ? (
<Tooltip label="Unlock key">
<Button noPadding>
<LockClosedIcon className="w-4 h-4 text-gray-400" />
</Button>
</Tooltip>
) : (
<Tooltip label="Lock key">
<Button noPadding>
<LockOpenIcon className="w-4 h-4 text-gray-400" />
</Button>
</Tooltip>
)}
</>
) : (
<Tooltip label="Dismount key">
<Button noPadding>
<XMarkIcon className="w-4 h-4 text-gray-400" />
</Button>
</Tooltip>
)}
</div>
</div>
);
};
export function KeyManager(props: KeyManagerProps) {
const [showKey, setShowKey] = useState(false);
const [toggle, setToggle] = useState(false);
const CurrentEyeIcon = showKey ? EyeSlashIcon : EyeIcon;
return (
<div className="flex flex-col h-full">
<div className="p-3 pt-3">
<Heading>Mount key</Heading>
<div className="flex space-x-2">
<div className="relative flex flex-grow">
<Input autoFocus type={showKey ? 'text' : 'password'} className="flex-grow !py-0.5" />
<Button
onClick={() => setShowKey(!showKey)}
noBorder
noPadding
className="absolute right-[5px] top-[5px]"
>
<CurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
<Tooltip className="flex" label="Mount key">
<Button variant="gray" noPadding>
<Plus weight="fill" className="w-4 h-4 mx-1" />
</Button>
</Tooltip>
</div>
<div className="flex flex-row items-center mt-3 mb-1">
<Toggle className="dark:bg-gray-400/30" size="sm" value={toggle} onChange={setToggle} />
<span className="ml-3 mt-[1px] font-medium text-xs">Sync with Library</span>
<Tooltip label="This key will be mounted on all devices running your Library">
<InformationCircleIcon className="w-4 h-4 ml-1.5 text-gray-400" />
</Tooltip>
</div>
<p className="pt-1.5 ml-0.5 text-[8pt] leading-snug text-gray-300 opacity-50 w-[90%]">
Files encrypted with this key will be revealed and decrypted on the fly.
</p>
</div>
<hr className="border-gray-500" />
<div className="p-3 custom-scroll overlay-scroll">
<div className="">
<Heading>Mounted keys</Heading>
<div className="pt-1 space-y-1.5">
<Key
index={0}
data={{
id: 'af5570f5a1810b7a',
name: 'OBS Recordings',
mounted: true,
nodes: ['node1', 'node2'],
stats: { objectCount: 235, containerCount: 2 }
}}
/>
<Key
index={1}
data={{
id: 'af5570f5a1810b7a',
name: 'Unknown Key',
locked: true,
mounted: true,
stats: { objectCount: 45 }
}}
/>
<Key index={2} data={{ id: '7324695a52da67b1', name: 'Spacedrive Company' }} />
<Key index={3} data={{ id: 'b02303d68d05a562', name: 'Key 4' }} />
<Key index={3} data={{ id: 'b02303d68d05a562', name: 'Key 5' }} />
<Key index={3} data={{ id: 'b02303d68d05a562', name: 'Key 6' }} />
</div>
</div>
</div>
<div className="flex w-full p-2 bg-gray-600 border-t border-gray-500 rounded-b-md">
<Button size="sm" variant="gray">
Unmount All
</Button>
<div className="flex-grow" />
<Button size="sm" variant="gray">
Close
</Button>
</div>
<div>
<Tabs.Root defaultValue="mount">
<Tabs.List>
<Tabs.Trigger className="text-sm font-medium text-gray-300" value="mount">
Mount
</Tabs.Trigger>
<Tabs.Trigger className="text-sm font-medium text-gray-300" value="keys">
Keys
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="keys">
<KeyList />
</Tabs.Content>
<Tabs.Content value="mount">
<KeyMounter />
</Tabs.Content>
</Tabs.Root>
</div>
);
}

View File

@@ -0,0 +1,87 @@
import { InformationCircleIcon } from '@heroicons/react/24/outline';
import {
EllipsisVerticalIcon,
EyeIcon,
EyeSlashIcon,
KeyIcon,
LockClosedIcon,
LockOpenIcon,
PlusIcon,
TrashIcon,
XMarkIcon
} from '@heroicons/react/24/solid';
import { Button, CategoryHeading, Input, Select, SelectOption } from '@sd/ui';
import clsx from 'clsx';
import { Eject, EjectSimple, Plus } from 'phosphor-react';
import { useState } from 'react';
import { Toggle } from '../primitive';
import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip';
import { Key } from './Key';
export function KeyMounter() {
const [showKey, setShowKey] = useState(false);
const [toggle, setToggle] = useState(false);
const [key, setKey] = useState('');
const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305');
const [hashingAlgo, setHashingAlgo] = useState('Argon2id');
const CurrentEyeIcon = showKey ? EyeSlashIcon : EyeIcon;
return (
<div className="p-3 pt-3 mb-1">
<CategoryHeading>Mount key</CategoryHeading>
<div className="flex space-x-2">
<div className="relative flex flex-grow">
<Input
value={key}
onChange={(e) => setKey(e.target.value)}
autoFocus
type={showKey ? 'text' : 'password'}
className="flex-grow !py-0.5"
/>
<Button
onClick={() => setShowKey(!showKey)}
noBorder
noPadding
className="absolute right-[5px] top-[5px]"
>
<CurrentEyeIcon className="w-4 h-4" />
</Button>
</div>
</div>
<div className="flex flex-row items-center mt-3 mb-1">
<Toggle className="dark:bg-gray-400/30" size="sm" value={toggle} onChange={setToggle} />
<span className="ml-3 mt-[1px] font-medium text-xs">Sync with Library</span>
<Tooltip label="This key will be mounted on all devices running your Library">
<InformationCircleIcon className="w-4 h-4 ml-1.5 text-gray-400" />
</Tooltip>
</div>
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
<div className="flex flex-col">
<span className="text-xs font-bold">Encryption</span>
<Select className="mt-2" onChange={setEncryptionAlgo} value={encryptionAlgo}>
<SelectOption value="XChaCha20Poly1305">XChaCha20Poly1305</SelectOption>
<SelectOption value="Aes256Gcm">Aes256Gcm</SelectOption>
</Select>
</div>
<div className="flex flex-col">
<span className="text-xs font-bold">Hashing</span>
<Select className="mt-2" onChange={setHashingAlgo} value={hashingAlgo}>
<SelectOption value="Argon2id">Argon2id</SelectOption>
</Select>
</div>
</div>
<p className="pt-1.5 ml-0.5 text-[8pt] leading-snug text-gray-300 opacity-50 w-[90%]">
Files encrypted with this key will be revealed and decrypted on the fly.
</p>
<Button className="w-full mt-2" variant="primary">
Mount Key
</Button>
</div>
);
}

View File

@@ -2,7 +2,7 @@ import { CogIcon, LockClosedIcon, PhotoIcon } from '@heroicons/react/24/outline'
import { PlusIcon } from '@heroicons/react/24/solid';
import { useCurrentLibrary, useLibraryMutation, useLibraryQuery, usePlatform } from '@sd/client';
import { LocationCreateArgs } from '@sd/client';
import { Button, Dropdown, OverlayPanel } from '@sd/ui';
import { Button, CategoryHeading, Dropdown, OverlayPanel } from '@sd/ui';
import clsx from 'clsx';
import { CheckCircle, CirclesFour, Planet, WaveTriangle } from 'phosphor-react';
import { NavLink, NavLinkProps, useNavigate } from 'react-router-dom';
@@ -37,10 +37,6 @@ const Icon = ({ component: Icon, ...props }: any) => (
<Icon weight="bold" {...props} className={clsx('w-4 h-4 mr-2', props.className)} />
);
const Heading: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="mt-5 mb-1 ml-1 text-xs font-semibold text-gray-300">{children}</div>
);
// cute little helper to decrease code clutter
const macOnly = (platform: string | undefined, classnames: string) =>
platform === 'macOS' ? classnames : '';
@@ -71,7 +67,7 @@ function LibraryScopedSection() {
return (
<>
<div>
<Heading>Locations</Heading>
<CategoryHeading>Locations</CategoryHeading>
{locations?.map((location) => {
return (
<div key={location.id} className="flex flex-row items-center">
@@ -135,7 +131,7 @@ function LibraryScopedSection() {
</div>
{tags?.length ? (
<div>
<Heading>Tags</Heading>
<CategoryHeading>Tags</CategoryHeading>
<div className="mb-2">
{tags?.slice(0, 6).map((tag, index) => (
<SidebarLink key={index} to={`tag/${tag.id}`} className="">

View File

@@ -20,21 +20,23 @@
"@headlessui/react": "^1.7.3",
"@heroicons/react": "^2.0.12",
"@radix-ui/react-context-menu": "^1.0.0",
"@radix-ui/react-select": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dropdown-menu": "^1.0.0",
"@radix-ui/react-select": "^1.0.0",
"@radix-ui/react-tabs": "^1.0.0",
"@sd/assets": "workspace:*",
"@tailwindcss/forms": "^0.5.3",
"class-variance-authority": "^0.2.3",
"@sd/assets": "workspace:*",
"clsx": "^1.2.1",
"phosphor-react": "^1.4.1",
"postcss": "^8.4.17",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "6.4.2",
"react-loading-icons": "^1.1.0",
"react-router-dom": "6.4.2",
"react-spring": "^9.5.5",
"storybook": "^6.5.12",
"tailwind-styled-components": "2.1.7",
"tailwindcss": "^3.1.8",
"tailwindcss-radix": "^2.6.0"
},

16
packages/ui/src/Tabs.tsx Normal file
View File

@@ -0,0 +1,16 @@
import * as TabsPrimitive from '@radix-ui/react-tabs';
import tw from 'tailwind-styled-components';
export const Root = tw(TabsPrimitive.Root)`
flex flex-col
`;
export const Content = tw(TabsPrimitive.TabsContent)``;
export const List = tw(TabsPrimitive.TabsList)`
flex flex-row p-2 items-center space-x-1 border-b border-gray-500/30
`;
export const Trigger = tw(TabsPrimitive.TabsTrigger)`
text-white px-1.5 py-0.5 rounded text-sm font-medium radix-state-active:bg-primary
`;

View File

@@ -0,0 +1,3 @@
import tw from 'tailwind-styled-components';
export const CategoryHeading = tw.h3`mt-1 mb-1 text-xs font-semibold text-gray-300`;

View File

@@ -6,3 +6,5 @@ export * as ContextMenu from './ContextMenu';
export * from './OverlayPanel';
export * from './Input';
export * from './Select';
export * as Tabs from './Tabs';
export * from './Typography';

BIN
pnpm-lock.yaml generated
View File

Binary file not shown.