completed frontend for devices in settings

This commit is contained in:
myung03
2024-09-16 14:06:40 -07:00
parent 6f3d45c464
commit f68459cbfe
9 changed files with 143 additions and 19 deletions

View File

@@ -185,3 +185,12 @@ export function translateKindName(kindName: string): string {
return kindName;
}
}
export function fetchAccessToken(): string {
const accessToken: string =
JSON.parse(window.localStorage.getItem('frontendCookies') ?? '[]')
.find((cookie: string) => cookie.startsWith('st-access-token'))
?.split('=')[1]
.split(';')[0] || '';
return accessToken;
}

View File

@@ -1,7 +1,7 @@
import { Key } from 'react';
import { Link } from 'react-router-dom';
import { HardwareModel, useBridgeQuery, useLibraryQuery } from '@sd/client';
import { useLocale, useOperatingSystem } from '~/hooks';
import { useAccessToken, useLocale, useOperatingSystem } from '~/hooks';
import { useRouteTitle } from '~/hooks/useRouteTitle';
import { hardwareModelToIcon } from '~/util/hardware';
@@ -28,16 +28,14 @@ export const Component = () => {
const os = useOperatingSystem();
const { t } = useLocale();
const accessToken = useAccessToken();
const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true });
const locations = locationsQuery.data ?? [];
// not sure if we'll need the node state in the future, as it should be returned with the cloud.devices.list query
// const { data: node } = useBridgeQuery(['nodeState']);
const cloudDevicesList = useBridgeQuery(['cloud.devices.list'], {
suspense: true,
retry: false
});
const cloudDevicesList = useBridgeQuery(['cloud.devices.list', { access_token: accessToken }]);
const search = useSearchFromSearchParams({ defaultTarget: 'paths' });

View File

@@ -0,0 +1,94 @@
import { useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { useNavigate } from 'react-router';
import { HardwareModel, useBridgeMutation, useBridgeQuery, useZodForm } from '@sd/client';
import { Dialog, ErrorMessage, useDialog, UseDialogProps } from '@sd/ui';
import { Icon } from '~/components';
import { useAccessToken, useLocale } from '~/hooks';
import { hardwareModelToIcon } from '~/util/hardware';
import { usePlatform } from '~/util/Platform';
interface Props extends UseDialogProps {
pubId: string;
name: string;
device_model: string;
}
interface CorePubId {
Uuid: string;
}
export default function DeleteLibraryDialog(props: Props) {
const { t } = useLocale();
const queryClient = useQueryClient();
const platform = usePlatform();
const navigate = useNavigate();
const accessToken = useAccessToken();
const { data: node } = useBridgeQuery(['nodeState']);
const deleteDevice = useBridgeMutation('cloud.devices.delete');
const deviceAmount = useBridgeQuery(['cloud.devices.list', { access_token: accessToken }]).data
?.length;
const form = useZodForm();
// Check if the current device matches the UUID or if it's the only device
useEffect(() => {
if (deviceAmount === 1) {
form.setError('pubId', {
type: 'manual',
message: t('error_only_device')
});
} else if ((node?.id as CorePubId).Uuid === props.pubId) {
form.setError('pubId', {
type: 'manual',
message: t('error_current_device')
});
}
}, [form, node, props.pubId, deviceAmount, t]);
const onSubmit = form.handleSubmit(async () => {
try {
// Check for form errors before proceeding
if (form.formState.errors.pubId) {
return;
}
await deleteDevice.mutateAsync({
access_token: accessToken,
pub_id: props.pubId
});
queryClient.invalidateQueries(['library.list']);
platform.refreshMenuBar && platform.refreshMenuBar();
navigate('/');
} catch (e) {
alert(`Failed to delete device: ${e}`);
}
});
return (
<Dialog
form={form}
onSubmit={onSubmit}
dialog={useDialog(props)}
title={t('delete_device')}
closeLabel={t('close')}
description={t('delete_device_description')}
ctaDanger
ctaLabel={t('delete')}
>
<div className="mt-5 flex flex-col items-center justify-center gap-2">
<Icon
// once backend endpoint is populated need to check if this is working correctly i.e fetching correct icons for devices
name={hardwareModelToIcon(props.device_model as HardwareModel)}
alt="Device icon"
size={56}
className="mr-2"
/>
<p className="text-medium mb-4 rounded-full text-sm font-medium">{props.name}</p>
<ErrorMessage name="pubId" className="mb-4" />
</div>
</Dialog>
);
}

View File

@@ -2,11 +2,13 @@ import { Trash } from '@phosphor-icons/react';
import { iconNames } from '@sd/assets/util';
import { Key } from 'react';
import { HardwareModel, humanizeSize } from '@sd/client';
import { Button, Card, Tooltip } from '@sd/ui';
import { Button, Card, dialogManager, Tooltip } from '@sd/ui';
import { Icon } from '~/components';
import { useLocale } from '~/hooks';
import { useAccessToken, useLocale } from '~/hooks';
import { hardwareModelToIcon } from '~/util/hardware';
import DeleteDeviceDialog from './DeleteDeviceDialog';
interface DeviceItemProps {
pub_id: Key | null | undefined;
name: string;
@@ -46,7 +48,19 @@ export default (props: DeviceItemProps) => {
}}
>
<Tooltip label={t('Delete device')}>
<Trash className="size-4" />
<Trash
onClick={() => {
dialogManager.create((dp) => (
<DeleteDeviceDialog
name={props.name}
device_model={props.device_model}
pubId={String(props.pub_id)}
{...dp}
/>
));
}}
className="size-4"
/>
</Tooltip>
</Button>
</Card>

View File

@@ -4,7 +4,7 @@ import { Key, useState } from 'react';
import { LibraryConfigWrapped, useBridgeQuery } from '@sd/client';
import { Button, ButtonLink, Card, dialogManager, Tooltip } from '@sd/ui';
import { Icon } from '~/components';
import { useLocale } from '~/hooks';
import { useAccessToken, useLocale } from '~/hooks';
import DeleteDialog from './DeleteDialog';
import DeviceItem from './DeviceItem';
@@ -18,12 +18,8 @@ export default (props: Props) => {
const { t } = useLocale();
const [isExpanded, setIsExpanded] = useState(false);
const cloudDevicesList = useBridgeQuery(['cloud.devices.list'], {
suspense: true,
retry: false
});
console.log(cloudDevicesList);
const accessToken = useAccessToken();
const cloudDevicesList = useBridgeQuery(['cloud.devices.list', { access_token: accessToken }]);
const toggleExpansion = () => {
setIsExpanded((prev) => !prev);
};

View File

@@ -32,3 +32,4 @@ export * from './useZodParams';
export * from './useZodRouteParams';
export * from './useZodSearchParams';
export * from './useDeeplinkEventHandler';
export * from './useAccessToken';

View File

@@ -0,0 +1,8 @@
export function useAccessToken(): string {
const accessToken: string =
JSON.parse(window.localStorage.getItem('frontendCookies') ?? '[]')
.find((cookie: string) => cookie.startsWith('st-access-token'))
?.split('=')[1]
.split(';')[0] || '';
return accessToken.trim();
}

View File

@@ -168,11 +168,13 @@
"default": "Default",
"default_settings": "Default Settings",
"delete": "Delete",
"delete_device": "Delete device",
"delete_device_description": "This is permanent! This device will lose access to its the corresponding library and be removed. ",
"delete_dialog_title": "Delete {{prefix}} {{type}}",
"delete_forever": "Delete Forever",
"delete_info": "This will not delete the actual folder on disk. Preview media will be deleted.",
"delete_library": "Delete Library",
"delete_library_description": "This is permanent! Original files will not be deleted, only the Spacedrive library.",
"delete_library_description": "This is permanent! Only the Spacedrive library will be deleted, and original files will remain untouched.",
"delete_location": "Delete Location",
"delete_location_description": "Deleting a location will also remove all files associated with it from the Spacedrive database, the files themselves will not be deleted.",
"delete_object": "Delete object",
@@ -191,7 +193,6 @@
"dialog": "Dialog",
"dialog_shortcut_description": "To perform actions and operations",
"direction": "Direction",
"drop_files_here_to_send_with": "Drop files here to send with Spacedrop",
"directory_one": "directory",
"directory_other": "directories",
"disabled": "Disabled",
@@ -212,6 +213,7 @@
"download": "Download",
"downloading_update": "Downloading Update",
"drag_to_resize": "Drag to resize",
"drop_files_here_to_send_with": "Drop files here to send with Spacedrop",
"duplicate": "Duplicate",
"duplicate_object": "Duplicate object",
"duplicate_success": "Items duplicated",
@@ -242,8 +244,10 @@
"erase_a_file": "Erase a file",
"erase_a_file_description": "Configure your erasure settings.",
"error": "Error",
"error_current_device": "You are currently on this device, and cannot delete the device from the library. Please use another device if you'd like to remove this device.",
"error_loading_original_file": "Error loading original file",
"error_message": "Error: {{error}}.",
"error_only_device": "You cannot delete this device as it is the only device that belongs to this library.",
"error_unknown": "An unknown error occurred.",
"executable": "Executable",
"executable_one": "Executable",

View File

@@ -50,7 +50,7 @@ export const Form = <T extends FieldValues>({
};
export const errorStyles = cva(
'flex justify-center gap-2 break-all rounded border border-red-500/40 bg-red-800/40 px-3 py-2 text-white',
'flex justify-center gap-2 whitespace-normal break-words rounded border border-red-500/40 bg-red-800/40 px-3 py-2 text-white',
{
variants: {
variant: {
@@ -89,7 +89,7 @@ export const ErrorMessage = ({ name, variant, className }: ErrorMessageProps) =>
return typeof message === 'string' ? (
<animated.div style={styles} className={errorStyles({ variant, className })}>
<Warning className="size-4" />
<p>{message}</p>
<p className="whitespace-normal">{message}</p>
</animated.div>
) : null;
})}