autobrr/web/src/screens/settings/Api.tsx
stacksmash76 2fed48e0dd
enhancement(web): add react suspense and improve DX (#1089)
* add react suspense, fix broken stuff, clean up code, improve DX

enhancement: added react suspense + spinner to show loading (still can be added in certain places)
chore: cleaned up Header/NavBar code
chore: cleaned up DeleteModal code
chore: cleaned up other relevant code
enhancement: changed remove button style to be much more pleasant (see e.g. filter tabs)
fix: made active tab on filters page to be blue (as it should've been) when active
fix: fixed ghost delimiter which was only visible when DeleteModal was active in FormButtonGroup
chore: removed most of linter warnings/errors
fix: fixed incorrect/double modal transition in FilterExternalItem
fix: fixed incorrect z-height on Options popover in Settings/IRC (would've been visible when Add new was clicked)
enhancement: improved robustness of all Context classes to support seamless new-feature expansion (#866)
enhancement: improved expand logic (see #994 comments)

* reverted irc expand view to previous design

* forgot to propagate previous z-height fix

* jinxed it

* add license header to new files

---------

Co-authored-by: martylukyy <35452459+martylukyy@users.noreply.github.com>
Co-authored-by: Kyle Sanderson <kyle.leet@gmail.com>
2023-09-10 12:35:43 +02:00

175 lines
6.1 KiB
TypeScript

/*
* Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import { useRef } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
import { TrashIcon } from "@heroicons/react/24/outline";
import { KeyField } from "@components/fields/text";
import { DeleteModal } from "@components/modals";
import { APIKeyAddForm } from "@forms/settings/APIKeyAddForm";
import Toast from "@components/notifications/Toast";
import { APIClient } from "@api/APIClient";
import { useToggle } from "@hooks/hooks";
import { classNames } from "@utils";
import { EmptySimple } from "@components/emptystates";
export const apiKeys = {
all: ["api_keys"] as const,
lists: () => [...apiKeys.all, "list"] as const,
details: () => [...apiKeys.all, "detail"] as const,
// detail: (id: number) => [...apiKeys.details(), id] as const
detail: (id: string) => [...apiKeys.details(), id] as const
};
function APISettings() {
const [addFormIsOpen, toggleAddForm] = useToggle(false);
const { data } = useQuery({
queryKey: apiKeys.lists(),
queryFn: APIClient.apikeys.getAll,
retry: false,
refetchOnWindowFocus: false,
onError: (err) => console.log(err)
});
return (
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
<div className="pb-6 py-6 px-4 sm:p-6 lg:pb-8">
<APIKeyAddForm isOpen={addFormIsOpen} toggle={toggleAddForm} />
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
API keys
</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage API keys.
</p>
</div>
<div className="ml-4 mt-4 flex-shrink-0">
<button
type="button"
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
onClick={toggleAddForm}
>
Add new
</button>
</div>
</div>
{data && data.length > 0 ? (
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
<ol className="min-w-full relative">
<li className="hidden sm:grid grid-cols-12 gap-4 mb-2 border-b border-gray-200 dark:border-gray-700">
<div className="col-span-5 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Name
</div>
<div className="col-span-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Key
</div>
</li>
{data && data.map((k, idx) => <APIListItem key={idx} apikey={k} />)}
</ol>
</section>
) : (
<EmptySimple
title="No API keys"
subtitle=""
buttonAction={toggleAddForm}
buttonText="Create API key"
/>
)}
</div>
</div>
);
}
interface ApiKeyItemProps {
apikey: APIKey;
}
function APIListItem({ apikey }: ApiKeyItemProps) {
const cancelModalButtonRef = useRef(null);
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const queryClient = useQueryClient();
const deleteMutation = useMutation({
mutationFn: (key: string) => APIClient.apikeys.delete(key),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiKeys.lists() });
queryClient.invalidateQueries({ queryKey: apiKeys.detail(apikey.key) });
toast.custom((t) => (
<Toast
type="success"
body={`API key ${apikey?.name} was deleted`}
t={t}
/>
));
}
});
return (
<li className="text-gray-500 dark:text-gray-400">
<DeleteModal
isOpen={deleteModalIsOpen}
isLoading={deleteMutation.isLoading}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={() => {
deleteMutation.mutate(apikey.key);
toggleDeleteModal();
}}
title={`Remove API key: ${apikey.name}`}
text="Are you sure you want to remove this API key? This action cannot be undone."
/>
<div className="sm:grid grid-cols-12 gap-4 items-center py-2">
<div className="col-span-5 px-2 sm:px-6 py-2 sm:py-0 truncate block sm:text-sm text-md font-medium text-gray-900 dark:text-white">
<div className="flex justify-between">
<div className="pl-1 py-2">{apikey.name}</div>
<div>
<button
className={classNames(
"text-gray-900 dark:text-gray-300",
"sm:hidden font-medium group rounded-md items-center px-2 py-2 text-sm"
)}
onClick={toggleDeleteModal}
title="Delete key"
>
<TrashIcon
className="text-red-500 w-5 h-5"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
<div className="col-span-6 flex items-center text-sm font-medium text-gray-900 dark:text-white">
<KeyField value={apikey.key} />
</div>
<div className="col-span-1 hidden sm:flex items-center text-sm font-medium text-gray-900 dark:text-white">
<button
className={classNames(
"text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center px-2 py-2 text-sm"
)}
onClick={toggleDeleteModal}
title="Delete key"
>
<TrashIcon className="text-red-500 w-5 h-5" aria-hidden="true" />
</button>
</div>
</div>
</li>
);
}
export default APISettings;