Skip to content

Commit

Permalink
Add Api token management screen (#791)
Browse files Browse the repository at this point in the history
* Initial API token management in settings

* update style to match figma

* fix api breaking change

* update nav item style and message when api key is copied

* update change request

* close toast when navigate outside of api management screen

* Add save toast, word changes, CSS fix.

* update change request

* update change request

---------

Co-authored-by: ElementalCrisis <9443295+ElementalCrisis@users.noreply.github.com>
  • Loading branch information
duehoa1211 and ElementalCrisis authored Jan 23, 2024
1 parent ca846f1 commit 7f75679
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 61 deletions.
41 changes: 7 additions & 34 deletions src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import React, { useContext, useEffect, useMemo, useRef } from 'react';
import { mdiCloseCircleOutline } from '@mdi/js';
import { Icon } from '@mdi/react';
import cx from 'classnames';

import { BodyVisibleContext } from '@/core/router';
import useEventCallback from '@/hooks/useEventCallback';

import Button from './Button';

type Props = {
id: string;
label?: string;
type: string;
type: React.HTMLInputTypeAttribute;
placeholder?: string;
value: string | number;
onChange: React.ChangeEventHandler<HTMLInputElement>;
Expand Down Expand Up @@ -67,11 +63,6 @@ function Input(props: Props) {
onToggleOverlay?.(false);
}, [isOverlay, onToggleOverlay]);

const handleOverlayClick = useEventCallback(() => {
setIsShow(prev => !prev);
onToggleOverlay?.(!isShow);
});

const inputContainerClassName = useMemo(() => {
const combier = (input: string) => cx([overlayClassName, input]);
if (isOverlay && inline) {
Expand All @@ -91,10 +82,10 @@ function Input(props: Props) {

return (
<div
className={cx({
className,
'flex-row gap-x-2 flex': isOverlay,
})}
className={cx([
className ?? '',
isOverlay && 'flex-row gap-x-2 flex',
])}
>
<label
htmlFor={id}
Expand All @@ -118,7 +109,7 @@ function Input(props: Props) {
)}
<input
className={cx([
inputClassName,
inputClassName ?? '',
'appearance-none bg-panel-input w-full focus:shadow-none focus:outline-none px-3 py-2 rounded transition ease-in-out border border-panel-border focus:ring-2 focus:ring-panel-icon-action focus:ring-inset',
center && 'text-center',
startIcon && '!pl-11',
Expand All @@ -132,7 +123,7 @@ function Input(props: Props) {
disabled={disabled}
ref={inputRef}
/>
{(endIcons?.length ?? isOverlay) && (
{endIcons?.length && (
<div className="absolute right-3 top-1/2 flex -translate-y-1/2 flex-row gap-x-2">
{endIcons?.map(icon => (
<div
Expand All @@ -143,28 +134,10 @@ function Input(props: Props) {
<Icon path={icon.icon} size={1} />
</div>
), [] as React.ReactNode[]) ?? []}
{isOverlay && isShow && (
<div
key="input-toggler"
onClick={handleOverlayClick}
className={cx('cursor-pointer text-panel-text 2xl:hidden')}
>
<Icon path={mdiCloseCircleOutline} size={1} />
</div>
)}
</div>
)}
</div>
</label>
{isOverlay && startIcon && !isShow && (
<Button
buttonType="secondary"
className="inline p-2.5 2xl:hidden"
onClick={handleOverlayClick}
>
<Icon path={startIcon} size={1} />
</Button>
)}
</div>
);
}
Expand Down
21 changes: 21 additions & 0 deletions src/core/react-query/auth/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,24 @@ export const useLoginMutation = () =>
}));
},
});

export const useCreateApiToken = () =>
useMutation<string, unknown, string>({
mutationFn: (key: string) =>
axios.post('auth/apikey', key, {
headers: {
'Content-Type': 'application/json',
},
}),
});

export const useDeleteApiToken = () =>
useMutation({
mutationFn: (key: string) =>
axios.delete('auth', {
headers: {
'Content-Type': 'application/json',
},
data: key,
}),
});
11 changes: 11 additions & 0 deletions src/core/react-query/auth/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query';

import { axiosV2 } from '@/core/axios';

import type { AuthToken } from '@/core/types/api/authToken';

export const useApiKeyQuery = () =>
useQuery<AuthToken[]>({
queryKey: ['auth', 'apikey'],
queryFn: () => axiosV2.get('auth'),
});
2 changes: 2 additions & 0 deletions src/core/router/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import LogsPage from '@/pages/logs/LogsPage';
import MainPage from '@/pages/main/MainPage';
import SettingsPage from '@/pages/settings/SettingsPage';
import AniDBSettings from '@/pages/settings/tabs/AniDBSettings';
import ApiKey from '@/pages/settings/tabs/ApiKeys';
import GeneralSettings from '@/pages/settings/tabs/GeneralSettings';
import ImportSettings from '@/pages/settings/tabs/ImportSettings';
import MetadataSitesSettings from '@/pages/settings/tabs/MetadataSitesSettings';
Expand Down Expand Up @@ -110,6 +111,7 @@ const router = sentryCreateBrowserRouter(
<Route path="anidb" element={<AniDBSettings />} />
<Route path="metadata-sites" element={<MetadataSitesSettings />} />
<Route path="user-management" element={<UserManagementSettings />} />
<Route path="api-keys" element={<ApiKey />} />
</Route>
</Route>
</Route>
Expand Down
5 changes: 5 additions & 0 deletions src/core/types/api/authToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type AuthToken = {
UserID: number;
Username: string;
Device: string;
};
63 changes: 37 additions & 26 deletions src/pages/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { useDispatch } from 'react-redux';
import { useMediaQuery } from 'react-responsive';
import { Outlet } from 'react-router';
import { NavLink, useLocation } from 'react-router-dom';
import { mdiInformationOutline, mdiLoading } from '@mdi/js';
import { mdiLoading } from '@mdi/js';
import { Icon } from '@mdi/react';
import cx from 'classnames';
import { isEqual } from 'lodash';

import Button from '@/components/Input/Button';
import toast from '@/components/Toast';
import TransitionDiv from '@/components/TransitionDiv';
import { usePatchSettingsMutation } from '@/core/react-query/settings/mutations';
import { useSettingsQuery } from '@/core/react-query/settings/queries';
Expand All @@ -23,6 +24,7 @@ const items = [
// { name: 'Display', path: 'display' },
{ name: 'User Management', path: 'user-management' },
// { name: 'Themes', path: 'themes' },
{ name: 'API Keys', path: 'api-keys' },
];

function SettingsPage() {
Expand All @@ -36,7 +38,6 @@ function SettingsPage() {

const [newSettings, setNewSettings] = useState(settings);
const [showNav, setShowNav] = useState(false);

const isSm = useMediaQuery({ minWidth: 0, maxWidth: 767 });

useEffect(() => {
Expand All @@ -46,6 +47,19 @@ function SettingsPage() {

const unsavedChanges = useMemo(() => !isEqual(settings, newSettings), [newSettings, settings]);

if (unsavedChanges) {
toast.info('Unsaved Changes', 'Please save before leaving this page.', {
autoClose: 99999999999,
toastId: 'save-changes',
});
} else {
toast.dismiss('save-changes');
}

useEffect(() => () => {
toast.dismiss('save-changes');
}, []);

const updateSetting = (type: string, key: string, value: string | string[] | boolean) => {
if (key === 'theme' && typeof value === 'string') {
globalThis.localStorage.setItem('theme', value);
Expand All @@ -62,6 +76,18 @@ function SettingsPage() {
}
};

const isShowFooter = useMemo(() => {
const path = pathname.split('/').pop();
if (!path) return true;
return !['user-management', 'api-keys'].includes(path);
}, [pathname]);

const settingContext = {
newSettings,
setNewSettings,
updateSetting,
};

return (
<div className="flex min-h-full grow justify-center gap-x-8" onClick={() => setShowNav(false)}>
<TransitionDiv
Expand All @@ -72,12 +98,14 @@ function SettingsPage() {
enterTo="translate-x-0"
>
<div className="sticky top-8">
<div className="mb-8 text-xl opacity-100">Settings</div>
<div className="flex flex-col gap-y-4">
<div className="mb-8 text-center text-xl opacity-100">Settings</div>
<div className="flex flex-col items-center">
{items.map(item => (
<NavLink
to={item.path}
className={({ isActive }) => (isActive ? 'text-panel-text-primary' : '')}
className={({ isActive }) => (isActive
? 'w-full text-center bg-button-primary text-button-primary-text border-2 !border-button-primary-border rounded-md hover:bg-button-primary-hover py-4 px-2'
: 'w-full text-center py-4 px-2')}
key={item.path}
>
{item.name}
Expand All @@ -96,7 +124,7 @@ function SettingsPage() {
{/* </div> */}
{/* </div> */}
{/* )} */}
<div className="flex min-h-full w-[37.5rem] flex-col gap-y-8 overflow-y-visible rounded-md border border-panel-border bg-panel-background-transparent p-8">
<div className="flex min-h-full w-[41rem] flex-col gap-y-8 overflow-y-visible rounded-md border border-panel-border bg-panel-background-transparent p-8">
{settingsQuery.isPending
? (
<div className="flex grow items-center justify-center text-panel-text-primary">
Expand All @@ -106,14 +134,10 @@ function SettingsPage() {
: (
<>
<Outlet
context={{
newSettings,
setNewSettings,
updateSetting,
}}
context={settingContext}
/>
{pathname.split('/').pop() !== 'user-management' && (
<div className="flex max-w-[34rem] justify-end font-semibold">
{isShowFooter && (
<div className="flex justify-end font-semibold">
<Button
onClick={() => setNewSettings(settings)}
buttonType="secondary"
Expand All @@ -134,19 +158,6 @@ function SettingsPage() {
</>
)}
</div>
<div
className={cx(
'flex w-96 bg-panel-background-transparent border border-panel-border rounded-md p-8 gap-x-2 font-semibold items-center sticky top-0 transition-opacity h-full',
unsavedChanges ? 'opacity-100' : 'opacity-0',
)}
>
<Icon path={mdiInformationOutline} size={1} className="text-panel-text-primary" />
Whoa! You Have Unsaved Changes!
</div>
<div
className="fixed left-0 top-0 -z-10 h-full w-full opacity-20"
style={{ background: 'center / cover no-repeat url(/api/v3/Image/Random/Fanart)' }}
/>
</div>
);
}
Expand Down
Loading

0 comments on commit 7f75679

Please sign in to comment.