diff --git a/interface/app/$libraryId/Explorer/util.ts b/interface/app/$libraryId/Explorer/util.ts
index 8daccaf8e..373b27a03 100644
--- a/interface/app/$libraryId/Explorer/util.ts
+++ b/interface/app/$libraryId/Explorer/util.ts
@@ -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;
+}
diff --git a/interface/app/$libraryId/overview/index.tsx b/interface/app/$libraryId/overview/index.tsx
index 71569d20c..b4578d36c 100644
--- a/interface/app/$libraryId/overview/index.tsx
+++ b/interface/app/$libraryId/overview/index.tsx
@@ -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' });
diff --git a/interface/app/$libraryId/settings/node/libraries/DeleteDeviceDialog.tsx b/interface/app/$libraryId/settings/node/libraries/DeleteDeviceDialog.tsx
new file mode 100644
index 000000000..db19933de
--- /dev/null
+++ b/interface/app/$libraryId/settings/node/libraries/DeleteDeviceDialog.tsx
@@ -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 (
+
+ );
+}
diff --git a/interface/app/$libraryId/settings/node/libraries/DeviceItem.tsx b/interface/app/$libraryId/settings/node/libraries/DeviceItem.tsx
index 1fc9fca0e..8c0e734c4 100644
--- a/interface/app/$libraryId/settings/node/libraries/DeviceItem.tsx
+++ b/interface/app/$libraryId/settings/node/libraries/DeviceItem.tsx
@@ -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) => {
}}
>
-
+ {
+ dialogManager.create((dp) => (
+
+ ));
+ }}
+ className="size-4"
+ />
diff --git a/interface/app/$libraryId/settings/node/libraries/ListItem.tsx b/interface/app/$libraryId/settings/node/libraries/ListItem.tsx
index b6ff11a8e..78bc8beec 100644
--- a/interface/app/$libraryId/settings/node/libraries/ListItem.tsx
+++ b/interface/app/$libraryId/settings/node/libraries/ListItem.tsx
@@ -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);
};
diff --git a/interface/hooks/index.ts b/interface/hooks/index.ts
index 8d1b54ac1..fa9720835 100644
--- a/interface/hooks/index.ts
+++ b/interface/hooks/index.ts
@@ -32,3 +32,4 @@ export * from './useZodParams';
export * from './useZodRouteParams';
export * from './useZodSearchParams';
export * from './useDeeplinkEventHandler';
+export * from './useAccessToken';
diff --git a/interface/hooks/useAccessToken.ts b/interface/hooks/useAccessToken.ts
new file mode 100644
index 000000000..d0a20a1e9
--- /dev/null
+++ b/interface/hooks/useAccessToken.ts
@@ -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();
+}
diff --git a/interface/locales/en/common.json b/interface/locales/en/common.json
index 3f5af2450..0f97989b7 100644
--- a/interface/locales/en/common.json
+++ b/interface/locales/en/common.json
@@ -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",
diff --git a/packages/ui/src/forms/Form.tsx b/packages/ui/src/forms/Form.tsx
index 00165b408..cb7c7bdb0 100644
--- a/packages/ui/src/forms/Form.tsx
+++ b/packages/ui/src/forms/Form.tsx
@@ -50,7 +50,7 @@ export const Form = ({
};
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' ? (
- {message}
+ {message}
) : null;
})}