From 7ad036092066efaecc72135c2ef3508e5d0acbaa Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:14:06 +0530 Subject: [PATCH] chore: revamp the API tokens workflow (#2880) * chore: added getLayout method to api tokens pages * revamp: api tokens workflow * chore: add title validation and update types * chore: minor UI updates * chore: update route --- .../src/ui/components/summary-popover.tsx | 2 +- packages/ui/src/dropdowns/custom-select.tsx | 4 +- .../api-token/delete-token-modal.tsx | 86 +++--- web/components/api-token/empty-state.tsx | 30 +-- web/components/api-token/form/index.tsx | 160 ------------ .../api-token/form/token-description.tsx | 56 ---- .../api-token/form/token-expiry.tsx | 111 -------- .../api-token/form/token-key-section.tsx | 59 ----- web/components/api-token/form/token-title.tsx | 69 ----- web/components/api-token/index.ts | 2 +- .../api-token/modal/create-token-modal.tsx | 133 ++++++++++ web/components/api-token/modal/form.tsx | 247 ++++++++++++++++++ .../modal/generated-token-details.tsx | 61 +++++ web/components/api-token/modal/index.ts | 3 + web/components/api-token/token-list-item.tsx | 83 +++--- .../pages/pages-list/recent-pages-list.tsx | 8 +- web/helpers/date-time.helper.ts | 38 +++ web/helpers/download.helper.ts | 19 +- .../settings-layout/workspace/sidebar.tsx | 10 +- .../[workspaceSlug]/settings/api-tokens.tsx | 86 ++++++ .../settings/api-tokens/[tokenId].tsx | 69 ----- .../settings/api-tokens/create.tsx | 36 --- .../settings/api-tokens/index.tsx | 65 ----- web/services/api_token.service.ts | 3 +- web/types/api_token.d.ts | 18 +- 25 files changed, 711 insertions(+), 747 deletions(-) delete mode 100644 web/components/api-token/form/index.tsx delete mode 100644 web/components/api-token/form/token-description.tsx delete mode 100644 web/components/api-token/form/token-expiry.tsx delete mode 100644 web/components/api-token/form/token-key-section.tsx delete mode 100644 web/components/api-token/form/token-title.tsx create mode 100644 web/components/api-token/modal/create-token-modal.tsx create mode 100644 web/components/api-token/modal/form.tsx create mode 100644 web/components/api-token/modal/generated-token-details.tsx create mode 100644 web/components/api-token/modal/index.ts create mode 100644 web/pages/[workspaceSlug]/settings/api-tokens.tsx delete mode 100644 web/pages/[workspaceSlug]/settings/api-tokens/[tokenId].tsx delete mode 100644 web/pages/[workspaceSlug]/settings/api-tokens/create.tsx delete mode 100644 web/pages/[workspaceSlug]/settings/api-tokens/index.tsx diff --git a/packages/editor/document-editor/src/ui/components/summary-popover.tsx b/packages/editor/document-editor/src/ui/components/summary-popover.tsx index 46b99d048..67054212d 100644 --- a/packages/editor/document-editor/src/ui/components/summary-popover.tsx +++ b/packages/editor/document-editor/src/ui/components/summary-popover.tsx @@ -44,7 +44,7 @@ export const SummaryPopover: React.FC = (props) => { {!sidePeekVisible && (
{ diff --git a/web/components/api-token/delete-token-modal.tsx b/web/components/api-token/delete-token-modal.tsx index 3aa666674..aefcd26f8 100644 --- a/web/components/api-token/delete-token-modal.tsx +++ b/web/components/api-token/delete-token-modal.tsx @@ -1,61 +1,71 @@ -//react import { useState, Fragment, FC } from "react"; -//next import { useRouter } from "next/router"; -//ui -import { Button } from "@plane/ui"; -//hooks -import useToast from "hooks/use-toast"; -//services -import { APITokenService } from "services/api_token.service"; -//headless ui +import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; +// services +import { APITokenService } from "services/api_token.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { Button } from "@plane/ui"; +// types +import { IApiToken } from "types/api_token"; +// fetch-keys +import { API_TOKENS_LIST } from "constants/fetch-keys"; type Props = { isOpen: boolean; - handleClose: () => void; - tokenId?: string; + onClose: () => void; + tokenId: string; }; const apiTokenService = new APITokenService(); -export const DeleteTokenModal: FC = (props) => { - const { isOpen, handleClose, tokenId } = props; +export const DeleteApiTokenModal: FC = (props) => { + const { isOpen, onClose, tokenId } = props; // states const [deleteLoading, setDeleteLoading] = useState(false); // hooks const { setToastAlert } = useToast(); // router const router = useRouter(); - const { workspaceSlug, tokenId: tokenIdFromQuery } = router.query; + const { workspaceSlug } = router.query; + + const handleClose = () => { + onClose(); + setDeleteLoading(false); + }; const handleDeletion = () => { - if (!workspaceSlug || (!tokenIdFromQuery && !tokenId)) return; - - const token = tokenId || tokenIdFromQuery; + if (!workspaceSlug) return; setDeleteLoading(true); + apiTokenService - .deleteApiToken(workspaceSlug.toString(), token!.toString()) + .deleteApiToken(workspaceSlug.toString(), tokenId) .then(() => { setToastAlert({ - message: "Token deleted successfully", type: "success", - title: "Success", + title: "Success!", + message: "Token deleted successfully.", }); - router.replace(`/${workspaceSlug}/settings/api-tokens/`); + + mutate( + API_TOKENS_LIST(workspaceSlug.toString()), + (prevData) => (prevData ?? []).filter((token) => token.id !== tokenId), + false + ); + + handleClose(); }) - .catch((err) => { + .catch((err) => setToastAlert({ - message: err?.message, type: "error", title: "Error", - }); - }) - .finally(() => { - setDeleteLoading(false); - handleClose(); - }); + message: err?.message ?? "Something went wrong. Please try again.", + }) + ) + .finally(() => setDeleteLoading(false)); }; return ( @@ -85,22 +95,24 @@ export const DeleteTokenModal: FC = (props) => { leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
+
-

Are you sure you want to revoke access?

+

+ Are you sure you want to delete the token? +

-

- Any applications Using this developer key will no longer have the access to Plane Data. This - Action cannot be undone. +

+ Any application using this token will no longer have the access to Plane data. This action cannot + be undone.

- -
diff --git a/web/components/api-token/empty-state.tsx b/web/components/api-token/empty-state.tsx index 96516ed32..474407703 100644 --- a/web/components/api-token/empty-state.tsx +++ b/web/components/api-token/empty-state.tsx @@ -1,15 +1,16 @@ -// react import React from "react"; -// next import Image from "next/image"; -import { useRouter } from "next/router"; // ui import { Button } from "@plane/ui"; // assets import emptyApiTokens from "public/empty-state/api-token.svg"; -export const APITokenEmptyState = () => { - const router = useRouter(); +type Props = { + onClick: () => void; +}; + +export const ApiTokenEmptyState: React.FC = (props) => { + const { onClick } = props; return (
{ >
empty -
No API Tokens
- { -

- Create API tokens for safe and easy data sharing with external apps, maintaining control and security -

- } -
diff --git a/web/components/api-token/form/index.tsx b/web/components/api-token/form/index.tsx deleted file mode 100644 index 621820322..000000000 --- a/web/components/api-token/form/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { Dispatch, SetStateAction, useState, FC } from "react"; -import { useForm } from "react-hook-form"; -import { useRouter } from "next/router"; -// helpers -import { addDays, renderDateFormat } from "helpers/date-time.helper"; -import { csvDownload } from "helpers/download.helper"; -// types -import { IApiToken } from "types/api_token"; -// hooks -import { useMobxStore } from "lib/mobx/store-provider"; -import useToast from "hooks/use-toast"; -// services -import { APITokenService } from "services/api_token.service"; -// components -import { APITokenTitle } from "./token-title"; -import { APITokenDescription } from "./token-description"; -import { APITokenExpiry, EXPIRY_OPTIONS } from "./token-expiry"; -import { APITokenKeySection } from "./token-key-section"; -// ui -import { Button } from "@plane/ui"; - -interface APITokenFormProps { - generatedToken: IApiToken | null | undefined; - setGeneratedToken: Dispatch>; - setDeleteTokenModal: Dispatch>; -} - -export interface APIFormFields { - never_expires: boolean; - title: string; - description: string; -} - -const apiTokenService = new APITokenService(); - -export const APITokenForm: FC = (props) => { - const { generatedToken, setGeneratedToken, setDeleteTokenModal } = props; - // states - const [loading, setLoading] = useState(false); - const [neverExpires, setNeverExpire] = useState(false); - const [focusTitle, setFocusTitle] = useState(false); - const [focusDescription, setFocusDescription] = useState(false); - const [selectedExpiry, setSelectedExpiry] = useState(1); - // hooks - const { setToastAlert } = useToast(); - // store - const { - theme: { sidebarCollapsed }, - } = useMobxStore(); - // router - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { - control, - handleSubmit, - formState: { errors }, - } = useForm({ - defaultValues: { - never_expires: false, - title: "", - description: "", - }, - }); - - const getExpiryDate = (): string | null => { - if (neverExpires === true) return null; - return addDays({ date: new Date(), days: EXPIRY_OPTIONS[selectedExpiry].days }).toISOString(); - }; - - function renderExpiry(): string { - return renderDateFormat(addDays({ date: new Date(), days: EXPIRY_OPTIONS[selectedExpiry].days }), true); - } - - const downloadSecretKey = (token: IApiToken) => { - const csvData = { - Label: token.label, - Description: token.description, - Expiry: renderDateFormat(token.expired_at ?? null), - "Secret Key": token.token, - }; - csvDownload(csvData, `Secret-key-${Date.now()}`); - }; - - const generateToken = async (data: any) => { - if (!workspaceSlug) return; - setLoading(true); - await apiTokenService - .createApiToken(workspaceSlug.toString(), { - label: data.title, - description: data.description, - expired_at: getExpiryDate(), - }) - .then((res) => { - setGeneratedToken(res); - downloadSecretKey(res); - setLoading(false); - }) - .catch((err) => { - setToastAlert({ - message: err.message, - type: "error", - title: "Error", - }); - }); - }; - - return ( -
{ - if (err.title) { - setFocusTitle(true); - } - })} - className={`${sidebarCollapsed ? "xl:w-[50%] lg:w-[60%] " : "w-[60%]"} mx-auto py-8`} - > -
- - {errors.title && focusTitle &&

{errors.title.message}

} - -
- - {!generatedToken && ( -
- <> - - - -
- )} - - - ); -}; diff --git a/web/components/api-token/form/token-description.tsx b/web/components/api-token/form/token-description.tsx deleted file mode 100644 index 606c32bff..000000000 --- a/web/components/api-token/form/token-description.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Dispatch, FC, SetStateAction } from "react"; -import { Control, Controller } from "react-hook-form"; -// ui -import { TextArea } from "@plane/ui"; -// types -import { IApiToken } from "types/api_token"; -import type { APIFormFields } from "./index"; - -interface APITokenDescriptionProps { - generatedToken: IApiToken | null | undefined; - control: Control; - focusDescription: boolean; - setFocusTitle: Dispatch>; - setFocusDescription: Dispatch>; -} - -export const APITokenDescription: FC = (props) => { - const { generatedToken, control, focusDescription, setFocusTitle, setFocusDescription } = props; - - return ( - - focusDescription ? ( -