Update frontend base

This commit is contained in:
jeffvli
2022-10-24 22:19:52 -07:00
parent 921c688c94
commit f8e7d02daf
35 changed files with 1464 additions and 491 deletions
@@ -1,85 +0,0 @@
import {
Button,
Checkbox,
Modal,
ModalProps,
PasswordInput,
SegmentedControl,
Stack,
TextInput,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { useTranslation } from 'react-i18next';
import { useCreateServer, validateServer } from '../queries/useCreateServer';
export const AddServerModal = ({ ...rest }: ModalProps) => {
const { t } = useTranslation();
const form = useForm({
initialValues: {
legacyAuth: false,
name: '',
password: '',
serverType: 'jellyfin',
url: 'http://',
username: '',
},
});
const createServerMutation = useCreateServer();
return (
<Modal centered title={t('server.add.title')} {...rest}>
<form
onSubmit={form.onSubmit(async (values) => {
const res = await validateServer(values);
if (res?.token) {
createServerMutation.mutate({
...values,
remoteUserId: res.userId,
token: res.token,
});
}
})}
>
<Stack>
<SegmentedControl
data={[
{ label: 'Jellyfin', value: 'jellyfin' },
{ label: 'Subsonic', value: 'subsonic' },
]}
{...form.getInputProps('serverType')}
/>
<TextInput
required
label={t('server.name')}
{...form.getInputProps('name')}
/>
<TextInput
required
label={t('server.url')}
{...form.getInputProps('url')}
/>
<TextInput
required
label={t('server.username')}
{...form.getInputProps('username')}
/>
<PasswordInput
required
label={t('server.password')}
{...form.getInputProps('password')}
/>
{form.getInputProps('serverType').value === 'subsonic' && (
<Checkbox
label={t('server.legacyauth')}
{...form.getInputProps('legacyAuth', { type: 'checkbox' })}
/>
)}
<Button type="submit">{t('server.submit')}</Button>
</Stack>
</form>
</Modal>
);
};
@@ -1,63 +0,0 @@
import {
Button,
Checkbox,
ModalProps,
PasswordInput,
SegmentedControl,
Stack,
TextInput,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { useTranslation } from 'react-i18next';
import { ServerResponse } from '../../../api/types';
interface EditServerModalProps extends ModalProps {
server: ServerResponse | undefined;
}
export const EditServerModal = ({ server }: EditServerModalProps) => {
const { t } = useTranslation();
const form = useForm({
initialValues: {
legacyAuth: false,
name: server?.name,
password: '',
serverType: server?.serverType,
url: server?.url,
username: server?.username,
},
});
return (
<form onSubmit={form.onSubmit(async () => {})}>
<Stack>
<SegmentedControl
disabled
data={[
{ label: 'Jellyfin', value: 'jellyfin' },
{ label: 'Subsonic', value: 'subsonic' },
]}
{...form.getInputProps('serverType')}
/>
<TextInput label={t('server.name')} {...form.getInputProps('name')} />
<TextInput label={t('server.url')} {...form.getInputProps('url')} />
<TextInput
label={t('server.username')}
{...form.getInputProps('username')}
/>
<PasswordInput
label={t('server.password')}
{...form.getInputProps('password')}
/>
{form.getInputProps('serverType').value === 'subsonic' && (
<Checkbox
label={t('server.legacyauth')}
{...form.getInputProps('legacyAuth', { type: 'checkbox' })}
/>
)}
<Button type="submit">{t('server.submit')}</Button>
</Stack>
</form>
);
};
@@ -1,8 +0,0 @@
.item {
display: flex;
justify-content: space-between;
max-width: 50vw;
margin: 1rem;
padding: 1rem;
outline: 1px #fff solid;
}
@@ -1,75 +0,0 @@
import { useEffect, useState } from 'react';
import { Button, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { ServerResponse } from '../../../api/types';
import { useServers } from '../queries/useServers';
import { EditServerModal } from './EditServerModal';
import styles from './ServerList.module.scss';
export const ServerList = () => {
const { data: servers } = useServers();
const [editServerModal, editServerHandlers] = useDisclosure(false);
const [opened, setOpened] = useState(false);
const [selectedServer, setSelectedServer] = useState<
ServerResponse | undefined
>();
const handleClickServer = (server: ServerResponse) => {
setSelectedServer(server);
editServerHandlers.open();
};
const handleKeyDownServer = (
e: React.KeyboardEvent<HTMLDivElement>,
server: ServerResponse
) => {
if (e.key === ' ' || e.key === 'Enter') {
setSelectedServer(server);
editServerHandlers.open();
}
};
const handleCloseModal = () => {
setOpened(false);
editServerHandlers.close();
};
useEffect(() => {
if (editServerModal === true) {
setTimeout(() => setOpened(true));
} else {
setTimeout(() => setSelectedServer(undefined), 100);
}
}, [editServerModal]);
return (
<>
{servers &&
servers.data.map((server) => (
<>
<div
className={styles.item}
role="button"
tabIndex={0}
onClick={() => handleClickServer(server)}
onKeyDown={(e) => handleKeyDownServer(e, server)}
>
<div>
{server.name}
<Text>Hello</Text>
</div>
<Button onClick={() => editServerHandlers.toggle()}>Edit</Button>
</div>
{selectedServer && (
<EditServerModal
opened={opened}
server={selectedServer}
onClose={handleCloseModal}
/>
)}
</>
))}
</>
);
};
@@ -0,0 +1,105 @@
import { useState } from 'react';
import { Checkbox, Stack, Group } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import { useFocusTrap } from '@mantine/hooks';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { remoteServerLogin } from '@/renderer/api/shared.api';
import { Server, ServerType } from '@/renderer/api/types';
import { Button, PasswordInput, TextInput, toast } from '@/renderer/components';
import { useAuthStore } from '@/renderer/store';
import { randomString } from '@/renderer/utils';
interface AddServerCredentialFormProps {
onCancel: () => void;
server: Server;
}
const schema = z.object({
legacyAuth: z.boolean().optional(),
password: z.string().min(1),
username: z.string().min(1),
});
export const AddServerCredentialForm = ({
onCancel,
server,
}: AddServerCredentialFormProps) => {
const { t } = useTranslation();
const [isSubmitting, setIsSubmitting] = useState(false);
const focusTrapRef = useFocusTrap(true);
const addServerCredential = useAuthStore(
(state) => state.addServerCredential
);
const form = useForm({
initialValues: {
legacy: false,
password: '',
url: 'http://',
username: '',
},
validate: zodResolver(schema),
});
const handleSubmit = form.onSubmit(async (values) => {
setIsSubmitting(true);
const auth = await remoteServerLogin({
...values,
type: server.type,
url: server.url,
});
if (auth.type === 'error') {
setIsSubmitting(false);
return toast.show({
message: auth.message,
title: 'Failed to add credential',
type: 'error',
});
}
addServerCredential({
enabled: false,
id: randomString(),
serverId: server.id,
token: auth.token,
username: auth.username!,
});
setIsSubmitting(false);
return onCancel();
});
return (
<form onSubmit={handleSubmit}>
<Stack ref={focusTrapRef}>
<TextInput
data-autofocus
required
label="username"
{...form.getInputProps('username')}
/>
<PasswordInput
required
label="password"
{...form.getInputProps('password')}
/>
{server.type === ServerType.SUBSONIC && (
<Checkbox
label={t('modal.add_server.legacy_label')}
{...form.getInputProps('legacy', { type: 'checkbox' })}
/>
)}
<Group position="right">
<Button disabled={isSubmitting} variant="subtle" onClick={onCancel}>
Cancel
</Button>
<Button loading={isSubmitting} type="submit" variant="filled">
Add
</Button>
</Group>
</Stack>
</form>
);
};
@@ -0,0 +1,95 @@
import { Checkbox, Stack, Group } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useFocusTrap } from '@mantine/hooks';
import { closeAllModals } from '@mantine/modals';
import { useTranslation } from 'react-i18next';
import { ServerType } from '@/renderer/api/types';
import {
Button,
PasswordInput,
TextInput,
SegmentedControl,
} from '@/renderer/components';
import { useCreateServer } from '@/renderer/features/servers/mutations/use-create-server';
const SERVER_TYPES = [
{ label: 'Jellyfin', value: ServerType.JELLYFIN },
{ label: 'Navidrome', value: ServerType.NAVIDROME },
{ label: 'Subsonic', value: ServerType.SUBSONIC },
];
interface AddServerFormProps {
onCancel: () => void;
}
export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
const { t } = useTranslation();
const focusTrapRef = useFocusTrap(true);
const form = useForm({
initialValues: {
legacyAuth: false,
name: '',
password: '',
type: ServerType.JELLYFIN,
url: 'http://',
username: '',
},
});
const createServerMutation = useCreateServer();
return (
<form
onSubmit={form.onSubmit(async (values) => {
createServerMutation.mutate(
{ body: values },
{ onSuccess: () => closeAllModals() }
);
})}
>
<Stack ref={focusTrapRef}>
<SegmentedControl data={SERVER_TYPES} {...form.getInputProps('type')} />
<TextInput
data-autofocus
required
label={t('modal.add_server.name_label')}
{...form.getInputProps('name')}
/>
<TextInput
required
label={t('modal.add_server.url_label')}
{...form.getInputProps('url')}
/>
<TextInput
required
label={t('modal.add_server.username_label')}
{...form.getInputProps('username')}
/>
<PasswordInput
required
label={t('modal.add_server.password_label')}
{...form.getInputProps('password')}
/>
{form.values.type === ServerType.SUBSONIC && (
<Checkbox
label={t('modal.add_server.legacy_label')}
{...form.getInputProps('legacyAuth', { type: 'checkbox' })}
/>
)}
<Group position="right">
<Button variant="subtle" onClick={onCancel}>
Cancel
</Button>
<Button
loading={createServerMutation.isLoading}
type="submit"
variant="filled"
>
{t('modal.add_server.submit_label')}
</Button>
</Group>
</Stack>
</form>
);
};
@@ -0,0 +1,59 @@
import { Stack, Group } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import { useFocusTrap } from '@mantine/hooks';
import { z } from 'zod';
import { Button, TextInput } from '@/renderer/components';
import { useCreateServerUrl } from '@/renderer/features/servers/mutations/use-create-server-url';
interface AddServerUrlFormProps {
onCancel: () => void;
serverId: string;
}
const schema = z.object({
url: z.string().url({ message: 'Invalid URL' }),
});
export const AddServerUrlForm = ({
serverId,
onCancel,
}: AddServerUrlFormProps) => {
const focusTrapRef = useFocusTrap(true);
const form = useForm({
initialValues: {
url: 'http://',
},
validate: zodResolver(schema),
});
const mutation = useCreateServerUrl();
const handleSubmit = form.onSubmit((values) => {
mutation.mutate(
{ body: values, query: { serverId } },
{ onSuccess: () => onCancel() }
);
});
return (
<form onSubmit={handleSubmit}>
<Stack ref={focusTrapRef}>
<TextInput
data-autofocus
required
label="URL"
{...form.getInputProps('url')}
/>
<Group position="right">
<Button variant="subtle" onClick={onCancel}>
Cancel
</Button>
<Button type="submit" variant="filled">
Add
</Button>
</Group>
</Stack>
</form>
);
};
@@ -0,0 +1,102 @@
import { Checkbox, Stack, Group } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useTranslation } from 'react-i18next';
import { RiInformationLine } from 'react-icons/ri';
import { Server, ServerType } from '@/renderer/api/types';
import { Button, PasswordInput, TextInput } from '@/renderer/components';
import { useUpdateServer } from '@/renderer/features/servers/mutations/use-update-server';
interface EditServerFormProps {
onCancel: () => void;
server: Server;
}
export const EditServerForm = ({ server, onCancel }: EditServerFormProps) => {
const { t } = useTranslation();
const updateServer = useUpdateServer();
const serverDetailsForm = useForm({
initialValues: {
legacyAuth: false,
name: server?.name,
password: '',
type: server?.type,
url: server?.url,
username: server?.username,
},
});
const isSubsonic = serverDetailsForm.values.type === ServerType.SUBSONIC;
const handleSubmit = serverDetailsForm.onSubmit(async (values) => {
updateServer.mutate(
{
body: {
name: values.name,
password: values.password,
type: values.type,
url: values.url,
username: values.username,
},
query: { serverId: server.id },
},
{ onSuccess: onCancel }
);
});
return (
<form onSubmit={handleSubmit}>
<Stack>
<TextInput
required
label={t('generic.name')}
rightSection={
serverDetailsForm.isDirty('name') && (
<RiInformationLine color="red" />
)
}
{...serverDetailsForm.getInputProps('name')}
/>
<TextInput
required
label={t('generic.url')}
rightSection={
serverDetailsForm.isDirty('url') && (
<RiInformationLine color="red" />
)
}
{...serverDetailsForm.getInputProps('url')}
/>
<TextInput
label={t('generic.username')}
rightSection={
serverDetailsForm.isDirty('username') && (
<RiInformationLine color="red" />
)
}
{...serverDetailsForm.getInputProps('username')}
/>
<PasswordInput
label={t('generic.password')}
{...serverDetailsForm.getInputProps('password')}
/>
{isSubsonic && (
<Checkbox
label={t('generic.legacyauth')}
{...serverDetailsForm.getInputProps('legacyAuth', {
type: 'checkbox',
})}
/>
)}
<Group position="right">
<Button variant="subtle" onClick={onCancel}>
Cancel
</Button>
<Button type="submit" variant="filled">
Update
</Button>
</Group>
</Stack>
</form>
);
};
@@ -0,0 +1,222 @@
import { Stack, Group } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { RiDeleteBin2Fill, RiEdit2Fill } from 'react-icons/ri';
import { Server } from '@/renderer/api/types';
import { Button, Text } from '@/renderer/components';
import { AddServerCredentialForm } from '@/renderer/features/servers/components/add-server-credential-form';
import { AddServerUrlForm } from '@/renderer/features/servers/components/add-server-url-form';
import { EditServerForm } from '@/renderer/features/servers/components/edit-server-form';
import { ServerSection } from '@/renderer/features/servers/components/server-section';
import { useDeleteServerUrl } from '@/renderer/features/servers/mutations/use-delete-server-url';
import { useDisableServerUrl } from '@/renderer/features/servers/mutations/use-disable-server-url';
import { useEnableServerUrl } from '@/renderer/features/servers/mutations/use-enable-server-url';
import { usePermissions } from '@/renderer/features/shared';
import { useAuthStore } from '@/renderer/store';
interface ServerListItemProps {
server: Server;
}
export const ServerListItem = ({ server }: ServerListItemProps) => {
const [edit, editHandlers] = useDisclosure(false);
const [addUrl, addUrlHandlers] = useDisclosure(false);
const [addCredential, addCredentialHandlers] = useDisclosure(false);
const permissions = usePermissions();
const enableServerUrl = useEnableServerUrl();
const disableServerUrl = useDisableServerUrl();
const deleteServerUrl = useDeleteServerUrl();
const serverCredentials = useAuthStore((state) => state.serverCredentials);
const enableServerCredential = useAuthStore(
(state) => state.enableServerCredential
);
const disableServerCredential = useAuthStore(
(state) => state.disableServerCredential
);
const deleteServerCredential = useAuthStore(
(state) => state.deleteServerCredential
);
const handleToggleCredential = (credentialId: string, enabled: boolean) => {
if (enabled) {
return disableServerCredential({ id: credentialId });
}
return enableServerCredential({ id: credentialId });
};
const handleDeleteCredential = (credentialId: string) => {
deleteServerCredential({ id: credentialId });
};
const handleToggleUrl = (urlId: string, enabled: boolean) => {
if (enabled) {
return disableServerUrl.mutate({ query: { serverId: server.id, urlId } });
}
return enableServerUrl.mutate({ query: { serverId: server.id, urlId } });
};
const handleDeleteUrl = (urlId: string) => {
deleteServerUrl.mutate({
query: {
serverId: server.id,
urlId,
},
});
};
return (
<>
<Stack spacing="xl">
<ServerSection title="Server details">
{edit ? (
<EditServerForm
server={server}
onCancel={() => editHandlers.toggle()}
/>
) : (
<Group position="apart">
<Group>
<Stack>
<Text>URL</Text>
<Text>Username</Text>
</Stack>
<Stack>
<Text>{server.url}</Text>
<Text>{server.username}</Text>
</Stack>
</Group>
<Group>
{permissions.editServer && (
<Button
tooltip={{ label: 'Edit server details' }}
variant="default"
onClick={() => editHandlers.toggle()}
>
<RiEdit2Fill color="white" />
</Button>
)}
</Group>
</Group>
)}
</ServerSection>
<ServerSection title="Server URLs">
{addUrl ? (
<AddServerUrlForm
serverId={server.id}
onCancel={() => addUrlHandlers.close()}
/>
) : (
<>
<Stack>
{server.serverUrls?.map((serverUrl) => (
<Group key={serverUrl.id} position="apart">
<Text>{serverUrl.url}</Text>
<Group>
<Button
compact
px={10}
radius="lg"
variant={serverUrl.enabled ? 'filled' : 'subtle'}
onClick={() =>
handleToggleUrl(serverUrl.id, serverUrl.enabled)
}
>
{serverUrl.enabled ? 'Disable' : 'Enable'}
</Button>
{permissions.deleteServerUrl && (
<Button
compact
radius="xl"
variant="subtle"
onClick={() => handleDeleteUrl(serverUrl.id)}
>
<RiDeleteBin2Fill />
</Button>
)}
</Group>
</Group>
))}
</Stack>
{permissions.createServerUrl && (
<Button
compact
mt={10}
variant="default"
onClick={() => addUrlHandlers.open()}
>
Add URL
</Button>
)}
</>
)}
</ServerSection>
<ServerSection title="Server Credentials">
{addCredential ? (
<AddServerCredentialForm
server={server}
onCancel={() => addCredentialHandlers.close()}
/>
) : (
<>
<Stack>
{serverCredentials?.map((credential) => (
<Group key={credential.id} position="apart">
<Text>{credential.username}</Text>
<Group>
<Button
compact
px={10}
radius="lg"
variant={credential.enabled ? 'filled' : 'subtle'}
onClick={() =>
handleToggleCredential(
credential.id,
credential.enabled
)
}
>
{credential.enabled ? 'Disable' : 'Enable'}
</Button>
{permissions.deleteServerCredential && (
<Button
compact
radius="xl"
variant="subtle"
onClick={() => handleDeleteCredential(credential.id)}
>
<RiDeleteBin2Fill />
</Button>
)}
</Group>
</Group>
))}
</Stack>
{permissions.createServerCredential && (
<Button
compact
mt={10}
variant="default"
onClick={() => addCredentialHandlers.open()}
>
Add credential
</Button>
)}
</>
)}
</ServerSection>
<ServerSection title="Danger zone">
{permissions.deleteServer && (
<Button compact color="red" leftIcon={<RiDeleteBin2Fill />}>
Delete server
</Button>
)}
</ServerSection>
</Stack>
</>
);
};
@@ -0,0 +1,65 @@
import { Accordion, Group } from '@mantine/core';
import { RiRefreshLine, RiRestartLine, RiServerFill } from 'react-icons/ri';
import { Button, Text } from '@/renderer/components';
import { ServerListItem } from '@/renderer/features/servers/components/server-list-item';
import { useServerList } from '@/renderer/features/servers/queries/use-server-list';
import { Font } from '@/renderer/styles';
import { titleCase } from '@/renderer/utils';
export const ServerList = () => {
const { data: servers } = useServerList();
const handleQuickScan = (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
e.stopPropagation();
};
const handleFullScan = (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
e.stopPropagation();
};
return (
<>
<Accordion variant="separated">
{servers?.data?.map((s) => (
<Accordion.Item key={s.id} value={s.name}>
<Accordion.Control icon={<RiServerFill size={15} />}>
<Group position="apart">
<Text font={Font.GOTHAM}>
{titleCase(s.type)} - {s.name}
</Text>
<Group spacing="xs">
<Button
component="div"
px={10}
role="button"
tabIndex={0}
tooltip={{ label: 'Full scan' }}
variant="subtle"
onClick={handleFullScan}
>
<RiRefreshLine color="white" size={20} />
</Button>
<Button
px={10}
tooltip={{ label: 'Quick scan' }}
variant="subtle"
onClick={handleQuickScan}
>
<RiRestartLine color="white" size={20} />
</Button>
</Group>
</Group>
</Accordion.Control>
<Accordion.Panel>
<ServerListItem server={s} />
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
</>
);
};
@@ -0,0 +1,27 @@
import React from 'react';
import styled from '@emotion/styled';
import { Text } from '@/renderer/components';
import { Font } from '@/renderer/styles';
interface ServerSectionProps {
children: React.ReactNode;
title: string | React.ReactNode;
}
const Container = styled.div``;
const Section = styled.div`
padding: 1rem;
border: 1px solid var(--generic-border-color);
`;
export const ServerSection = ({ title, children }: ServerSectionProps) => {
return (
<Container>
<Text font={Font.EPILOGUE} size="sm">
{title}
</Text>
<Section>{children}</Section>
</Container>
);
};
+4 -5
View File
@@ -1,5 +1,4 @@
export * from './routes/ServersRoute';
export * from './queries/useCreateServer';
export * from './queries/useServers';
export * from './components/AddServerModal';
export * from './components/ServerList';
export * from './mutations/use-create-server';
export * from './components/add-server-form';
export * from './components/server-list';
export * from './queries/use-server-list';
@@ -0,0 +1,35 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { ServerListResponse, UrlResponse } from '@/renderer/api/servers.api';
import { ApiError } from '@/renderer/api/types';
import { toast } from '@/renderer/components';
export const useCreateServerUrl = () => {
const queryClient = useQueryClient();
const mutation = useMutation<
UrlResponse,
AxiosError<ApiError>,
{
body: { url: string };
query: { serverId: string };
},
{ previous: ServerListResponse | undefined }
>({
mutationFn: ({ query, body }) => api.servers.createUrl(query, body),
onError: (err) => {
toast.show({
message: `${err.response?.data?.error?.message || err.message}`,
title: 'Failed to add server URL',
type: 'error',
});
},
onSettled: () => {
queryClient.invalidateQueries(queryKeys.servers.list());
},
});
return mutation;
};
@@ -0,0 +1,36 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { CreateServerBody, ServerResponse } from '@/renderer/api/servers.api';
import { ApiError } from '@/renderer/api/types';
import { toast } from '@/renderer/components';
export const useCreateServer = () => {
const queryClient = useQueryClient();
return useMutation<
ServerResponse,
AxiosError<ApiError>,
{ body: CreateServerBody },
null
>({
mutationFn: ({ body }) => api.servers.createServer(body),
onError: (err: any) => {
toast.show({
message: `${err.response?.data?.error?.message || err.message}`,
title: 'Failed to add server',
type: 'error',
});
},
onSettled: () => {
queryClient.invalidateQueries(queryKeys.servers.list());
},
onSuccess: (data) => {
toast.show({
message: `${data.data.name} was added successfully`,
title: 'Server added',
type: 'success',
});
},
});
};
@@ -0,0 +1,60 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { ServerListResponse } from '@/renderer/api/servers.api';
import { ApiError, NullResponse } from '@/renderer/api/types';
import { toast } from '@/renderer/components';
export const useDeleteServerUrl = () => {
const queryClient = useQueryClient();
const mutation = useMutation<
NullResponse,
AxiosError<ApiError>,
{ query: { serverId: string; urlId: string } },
{ previous: ServerListResponse | undefined }
>({
mutationFn: ({ query }) => api.servers.deleteUrl(query),
onError: (err, _variables, context) => {
toast.show({
message: `${err.response?.data?.error?.message || err.message}`,
title: 'Failed to delete server URL',
type: 'error',
});
if (!context?.previous) return;
queryClient.setQueryData(queryKeys.servers.list(), context.previous);
},
onMutate: async (variables) => {
const queryKey = queryKeys.servers.list();
await queryClient.cancelQueries(queryKey);
const previous = queryClient.getQueryData<ServerListResponse>(queryKey);
if (!previous) return undefined;
const data = previous.data.map((server) => {
if (server.id === variables.query.serverId) {
return {
...server,
serverUrls: server.serverUrls?.filter(
(url) => url.id !== variables.query.urlId
),
};
}
return server;
});
queryClient.setQueryData(queryKey, { ...previous, data });
return { previous };
},
onSettled: () => {
queryClient.invalidateQueries(queryKeys.servers.list());
},
});
return mutation;
};
@@ -0,0 +1,55 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { ServerListResponse } from '@/renderer/api/servers.api';
import { ApiError, NullResponse } from '@/renderer/api/types';
export const useDisableServerUrl = () => {
const queryClient = useQueryClient();
const mutation = useMutation<
NullResponse,
AxiosError<ApiError>,
{ query: { serverId: string; urlId: string } },
{ previous: ServerListResponse | undefined }
>({
mutationFn: ({ query }) => api.servers.disableUrl(query),
onError: (_err, _variables, context) => {
if (!context?.previous) return;
queryClient.setQueryData(queryKeys.servers.list(), context.previous);
},
onMutate: async (variables) => {
const queryKey = queryKeys.servers.list();
await queryClient.cancelQueries(queryKey);
const previous = queryClient.getQueryData<ServerListResponse>(queryKey);
if (!previous) return undefined;
const data = previous.data.map((server) => {
if (server.id === variables.query.serverId) {
return {
...server,
serverUrls: server.serverUrls?.map((url) =>
url.id === variables.query.urlId
? { ...url, enabled: false }
: url
),
};
}
return server;
});
queryClient.setQueryData(queryKey, { ...previous, data });
return { previous };
},
onSettled: () => {
queryClient.invalidateQueries(queryKeys.servers.list());
},
});
return mutation;
};
@@ -0,0 +1,55 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { ServerListResponse } from '@/renderer/api/servers.api';
import { ApiError, NullResponse } from '@/renderer/api/types';
export const useEnableServerUrl = () => {
const queryClient = useQueryClient();
const mutation = useMutation<
NullResponse,
AxiosError<ApiError>,
{ query: { serverId: string; urlId: string } },
{ previous: ServerListResponse | undefined }
>({
mutationFn: ({ query }) => api.servers.enableUrl(query),
onError: (_err, _variables, context) => {
if (!context?.previous) return;
queryClient.setQueryData(queryKeys.servers.list(), context.previous);
},
onMutate: async (variables) => {
const queryKey = queryKeys.servers.list();
await queryClient.cancelQueries(queryKey);
const previous = queryClient.getQueryData<ServerListResponse>(queryKey);
if (!previous) return undefined;
const data = previous.data.map((server) => {
if (server.id === variables.query.serverId) {
return {
...server,
serverUrls: server.serverUrls?.map((url) =>
url.id === variables.query.urlId
? { ...url, enabled: true }
: { ...url, enabled: false }
),
};
}
return server;
});
queryClient.setQueryData(queryKey, { ...previous, data });
return { previous };
},
onSettled: () => {
queryClient.invalidateQueries(queryKeys.servers.list());
},
});
return mutation;
};
@@ -0,0 +1,69 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import {
ServerResponse,
ServerListResponse,
CreateServerBody,
} from '@/renderer/api/servers.api';
import { ApiError } from '@/renderer/api/types';
import { toast } from '@/renderer/components';
export const useUpdateServer = () => {
const queryClient = useQueryClient();
const mutation = useMutation<
ServerResponse,
AxiosError<ApiError>,
{ body: Partial<CreateServerBody>; query: { serverId: string } },
{ previous: ServerListResponse | undefined }
>({
mutationFn: ({ query, body }) => api.servers.updateServer(query, body),
onError: (err, _variables, context) => {
toast.show({
message: `${err.response?.data.error.message}`,
type: 'error',
});
if (context?.previous) {
queryClient.setQueryData(queryKeys.servers.list(), context.previous);
}
},
onMutate: async (variables) => {
const queryKey = queryKeys.servers.list();
await queryClient.cancelQueries(queryKey);
const previous = queryClient.getQueryData<ServerListResponse>(queryKey);
if (!previous) return undefined;
const data = previous.data.map((server) => {
if (server.id === variables.query.serverId) {
return {
...server,
name: variables.body.name,
username: variables.body.username,
};
}
return server;
});
queryClient.setQueryData(queryKey, { ...previous, data });
return { previous };
},
onSettled: () => {
queryClient.invalidateQueries(queryKeys.servers.list());
},
onSuccess: (data) => {
toast.show({
message: `Server "${data.data.name}" updated`,
type: 'success',
});
queryClient.invalidateQueries(queryKeys.servers.list());
},
});
return mutation;
};
@@ -0,0 +1,67 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { ServerListResponse } from '@/renderer/api/servers.api';
import { QueryOptions } from '@/renderer/lib/react-query';
export const useServerList = (options?: QueryOptions<ServerListResponse>) => {
// return useQuery({
// // onSuccess: (servers) => {
// // const { serverUrl } = JSON.parse(
// // localStorage.getItem('authentication') || '{}'
// // );
// // const storedServersKey = `servers_${md5(serverUrl)}`;
// // const serversFromLocalStorage = localStorage.getItem(storedServersKey);
// // // If a custom account/token is set for a server, use that instead of the default one
// // if (serversFromLocalStorage) {
// // const existingServers = JSON.parse(serversFromLocalStorage);
// // // The 'locked' property determines whether or not to skip updating the server auth
// // const skipped = existingServers.filter(
// // (server: ServerFolderAuth) => server.locked
// // );
// // const store = servers?.data?.flatMap((server) =>
// // server.serverFolders?.map((serverFolder: ServerFolder) => {
// // if (skipped.includes(serverFolder.id)) {
// // return existingServers.find(
// // (s: ServerFolderAuth) => s.id === serverFolder.id
// // );
// // }
// // return {
// // id: serverFolder.id,
// // locked: false,
// // serverId: server.id,
// // token: server.token,
// // type: server.type,
// // url: server.url,
// // userId: server.remoteUserId,
// // username: server.username,
// // };
// // })
// // );
// // return localStorage.setItem(storedServersKey, JSON.stringify(store));
// // }
// // const store = servers?.data?.flatMap((server) =>
// // server.serverFolders?.map((serverFolder: ServerFolder) => ({
// // id: serverFolder.id,
// // locked: false,
// // serverId: server.id,
// // token: server.token,
// // type: server.type,
// // url: server.url,
// // userId: server.remoteUserId,
// // username: server.username,
// // }))
// // );
// // return localStorage.setItem(storedServersKey, JSON.stringify(store));
// // },
// queryFn: () => api.servers.getServerList(),
// queryKey: queryKeys.server.list,
// ...options,
// });
return useQuery({
queryFn: () => api.servers.getServerList(),
queryKey: queryKeys.servers.list(),
...options,
});
};
@@ -1,68 +0,0 @@
import md5 from 'md5';
import { useQuery } from 'react-query';
import { ServerFolderAuth } from '../../../../types';
import { queryKeys } from '../../../api/queryKeys';
import { serversApi } from '../../../api/serversApi';
import { ServerFolderResponse } from '../../../api/types';
export const useServers = () => {
return useQuery({
onSuccess: (servers) => {
const { serverUrl } = JSON.parse(
localStorage.getItem('authentication') || '{}'
);
const storedServersKey = `servers_${md5(serverUrl)}`;
const serversFromLocalStorage = localStorage.getItem(storedServersKey);
// If a custom account/token is set for a server, use that instead of the default one
if (serversFromLocalStorage) {
const existingServers = JSON.parse(serversFromLocalStorage);
// The 'locked' property determines whether or not to skip updating the server auth
const skipped = existingServers.filter(
(server: ServerFolderAuth) => server.locked
);
const store = servers?.data?.flatMap((server) =>
server.serverFolder?.map((serverFolder: ServerFolderResponse) => {
if (skipped.includes(serverFolder.id)) {
return existingServers.find(
(s: ServerFolderAuth) => s.id === serverFolder.id
);
}
return {
id: serverFolder.id,
locked: false,
serverId: server.id,
token: server.token,
type: server.serverType,
url: server.url,
userId: server.remoteUserId,
username: server.username,
};
})
);
return localStorage.setItem(storedServersKey, JSON.stringify(store));
}
const store = servers?.data?.flatMap((server) =>
server.serverFolder?.map((serverFolder: ServerFolderResponse) => ({
id: serverFolder.id,
locked: false,
serverId: server.id,
token: server.token,
type: server.serverType,
url: server.url,
userId: server.remoteUserId,
username: server.username,
}))
);
return localStorage.setItem(storedServersKey, JSON.stringify(store));
},
queryFn: () => serversApi.getServers(),
queryKey: queryKeys.servers,
});
};
@@ -1,11 +0,0 @@
import { Title } from '@mantine/core';
import { ServerList } from '../components/ServerList';
export const ServersRoute = () => {
return (
<div>
<Title>Servers</Title>
<ServerList />
</div>
);
};
@@ -1,21 +1,20 @@
import axios from 'axios';
import md5 from 'md5';
import { useMutation } from 'react-query';
import { serversApi } from '../../../api/serversApi';
import { randomString } from '../../../utils';
import { ServerType } from '@/renderer/api/types';
import { randomString } from '@/renderer/utils';
export const validateServer = async (options: {
export const validateServerCredential = async (options: {
legacyAuth: boolean;
password: string;
serverType: string;
type: ServerType;
url: string;
username: string;
}) => {
const { serverType, url, username, password, legacyAuth } = options;
const { type, url, username, password, legacyAuth } = options;
const cleanServerUrl = url.replace(/\/$/, '');
try {
if (serverType === 'subsonic') {
if (type === ServerType.SUBSONIC) {
let testConnection;
let token;
if (legacyAuth) {
@@ -60,11 +59,3 @@ export const validateServer = async (options: {
return null;
};
export const useCreateServer = () => {
return useMutation({
mutationFn: serversApi.createServer,
onError: (e) => console.log(e),
onSuccess: (e) => console.log(e),
});
};