From 3e2d7dd41a435c7aafb52c278616a42c01d38aa6 Mon Sep 17 00:00:00 2001 From: Jean Pierre Date: Thu, 24 Nov 2022 20:15:54 +0000 Subject: [PATCH] Add Delete, Regenerate, and Update PAT UI Co-authored-by: Huiwen --- components/dashboard/src/App.tsx | 6 + .../src/settings/PersonalAccessTokens.tsx | 305 ++++++++++++++---- .../dashboard/src/settings/TokenEntry.tsx | 74 +++-- .../dashboard/src/settings/settings-menu.ts | 7 +- .../dashboard/src/settings/settings.routes.ts | 1 + 5 files changed, 312 insertions(+), 81 deletions(-) diff --git a/components/dashboard/src/App.tsx b/components/dashboard/src/App.tsx index f1fc62cd6bfe65..23cff5b0e393ee 100644 --- a/components/dashboard/src/App.tsx +++ b/components/dashboard/src/App.tsx @@ -38,6 +38,7 @@ import { usagePathMain, settingsPathPersonalAccessTokens, settingsPathPersonalAccessTokenCreate, + settingsPathPersonalAccessTokenEdit, } from "./settings/settings.routes"; import { projectsPathInstallGitHubApp, @@ -410,6 +411,11 @@ function App() { exact component={PersonalAccessTokenCreateView} /> + diff --git a/components/dashboard/src/settings/PersonalAccessTokens.tsx b/components/dashboard/src/settings/PersonalAccessTokens.tsx index a453f5f0b4abe3..1a1f9d8a806528 100644 --- a/components/dashboard/src/settings/PersonalAccessTokens.tsx +++ b/components/dashboard/src/settings/PersonalAccessTokens.tsx @@ -6,9 +6,10 @@ import { PersonalAccessToken } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_pb"; import { useContext, useEffect, useState } from "react"; -import { Redirect, useHistory, useLocation } from "react-router"; +import { Redirect, useHistory, useLocation, useParams } from "react-router"; import { Link } from "react-router-dom"; import CheckBox from "../components/CheckBox"; +import Modal from "../components/Modal"; import { FeatureFlagContext } from "../contexts/FeatureFlagContext"; import { personalAccessTokensService } from "../service/public-api"; import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu"; @@ -42,10 +43,74 @@ interface EditPATData { expirationDate: Date; } +interface TokenModalProps { + token: PersonalAccessToken; + title: string; + description: string; + descriptionImportant: string; + actionDescription: string; + children?: React.ReactNode; + onSave?: () => void; + onClose?: () => void; +} + +enum Method { + Create = "CREATED", + Regerenrate = "REGENERATED", +} + +export function ShowTokenModal(props: TokenModalProps) { + const onEnter = () => { + if (props.onSave) { + props.onSave(); + } + return true; + }; + + return ( + { + props.onClose && props.onClose(); + }} + > + Cancel + , + , + ]} + visible={true} + onClose={() => { + props.onClose && props.onClose(); + }} + onEnter={onEnter} + > +
+ {props.description} {props.descriptionImportant} +
+
+
{props.token.name}
+
+ Expires on{" "} + {Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(props.token.expirationTime?.toDate())} +
+
+ {props.children ?
{props.children}
: <>} +
+ ); +} + export function PersonalAccessTokenCreateView() { const { enablePersonalAccessTokens } = useContext(FeatureFlagContext); - const history = useHistory(); + const params = useParams(); + const history = useHistory(); + + const [editTokenID, setEditTokenID] = useState(null); const [errorMsg, setErrorMsg] = useState(""); const [value, setValue] = useState({ name: "", @@ -53,6 +118,34 @@ export function PersonalAccessTokenCreateView() { expirationDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), }); + const [showModal, setShowModal] = useState(false); + const [modalData, setModalData] = useState(); + + function backToListView(tokenInfo?: TokenInfo) { + history.push({ + pathname: settingsPathPersonalAccessTokens, + state: tokenInfo, + }); + } + + useEffect(() => { + (async () => { + try { + const { tokenId } = params as { tokenId: string }; + if (!tokenId) { + return; + } + setEditTokenID(tokenId); + const resp = await personalAccessTokensService.getPersonalAccessToken({ id: tokenId }); + const token = resp.token; + value.name = token!.name; + setModalData(token!); + } catch (e) { + setErrorMsg(e.message); + } + })(); + }, []); + const update = (change: Partial) => { if (change.expirationDays) { change.expirationDate = new Date(Date.now() + change.expirationDays * 24 * 60 * 60 * 1000); @@ -61,26 +154,45 @@ export function PersonalAccessTokenCreateView() { setValue({ ...value, ...change }); }; - const createToken = async () => { + const regenerate = async () => { + if (!editTokenID) { + return; + } + try { + const resp = await personalAccessTokensService.regeneratePersonalAccessToken({ + id: editTokenID, + expirationTime: Timestamp.fromDate(value.expirationDate), + }); + backToListView({ method: Method.Regerenrate, data: resp.token! }); + } catch (e) { + setErrorMsg(e.message); + } + }; + + const handleConfirm = async () => { if (value.name.length < 3) { setErrorMsg("Token Name should have at least three characters."); return; } try { - const resp = await personalAccessTokensService.createPersonalAccessToken({ - token: { - name: value.name, - expirationTime: Timestamp.fromDate(value.expirationDate), - scopes: ["function:*", "resource:default"], - }, - }); - history.push({ - pathname: settingsPathPersonalAccessTokens, - state: { - method: "CREATED", - data: resp.token, - }, - }); + const resp = editTokenID + ? await personalAccessTokensService.updatePersonalAccessToken({ + token: { + id: editTokenID, + name: value.name, + scopes: ["function:*", "resource:default"], + }, + updateMask: { paths: ["name", "scopes"] }, + }) + : await personalAccessTokensService.createPersonalAccessToken({ + token: { + name: value.name, + expirationTime: Timestamp.fromDate(value.expirationDate), + scopes: ["function:*", "resource:default"], + }, + }); + + backToListView(editTokenID ? undefined : { method: Method.Create, data: resp.token! }); } catch (e) { setErrorMsg(e.message); } @@ -93,7 +205,7 @@ export function PersonalAccessTokenCreateView() { return (
-
+
+ {editTokenID && ( + + )}
<> {errorMsg.length > 0 && ( @@ -110,10 +230,58 @@ export function PersonalAccessTokenCreateView() { )} + <> + {showModal && ( + { + regenerate(); + }} + onClose={() => { + setShowModal(false); + }} + > +
+

Expiration Date

+ +

+ The token will expire on{" "} + {Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(value.expirationDate)}. +

+
+
+ )} +
-

New Personal Access Token

-

Create a new personal access token.

+

{editTokenID ? "Edit" : "New"} Personal Access Token

+ {editTokenID ? ( + <> +

+ Update token name, expiration date, permissions, or regenerate token. +

+ + ) : ( + <> +

+ Create a new personal access token. +

+ + )}
@@ -127,28 +295,30 @@ export function PersonalAccessTokenCreateView() { type="text" placeholder="Token Name" /> -

+

The application name using the token or the purpose of the token.

-
-

Expiration Date

- -

- The token will expire on{" "} - {Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(value.expirationDate)}. -

-
+ {!editTokenID && ( +
+

Expiration Date

+ +

+ The token will expire on{" "} + {Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(value.expirationDate)}. +

+
+ )}

Permission

- +
+ {editTokenID && ( + + + + )} + +
); } interface TokenInfo { - method: string; + method: Method; data: PersonalAccessToken; } @@ -178,11 +357,13 @@ function ListAccessTokensView() { const [tokens, setTokens] = useState([]); const [tokenInfo, setTokenInfo] = useState(); + async function loadTokens() { + const response = await personalAccessTokensService.listPersonalAccessTokens({}); + setTokens(response.tokens); + } + useEffect(() => { - (async () => { - const response = await personalAccessTokensService.listPersonalAccessTokens({}); - setTokens(response.tokens); - })(); + loadTokens(); }, []); useEffect(() => { @@ -196,6 +377,13 @@ function ListAccessTokensView() { copyToClipboard(tokenInfo!.data.value); }; + const handleDeleteToken = (tokenId: string) => { + if (tokenId === tokenInfo?.data.id) { + setTokenInfo(undefined); + } + loadTokens(); + }; + return ( <>
@@ -212,15 +400,22 @@ function ListAccessTokensView() { <> {tokenInfo && ( <> -
+
-
- {tokenInfo.data.name}{" "} - +
+ {tokenInfo.data.name} + {tokenInfo.method.toUpperCase()}
-
+
Expires on{" "} {Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format( @@ -237,13 +432,15 @@ function ListAccessTokensView() {
-
Your New Personal Access Token
+
+ Your New Personal Access Token +
-
+
Make sure to copy your personal access token — you won't be able to access it again.
) : ( <> -
+

Token Name

Permissions

Expires

{tokens.map((t: PersonalAccessToken) => { - return ; + return ; })} )} diff --git a/components/dashboard/src/settings/TokenEntry.tsx b/components/dashboard/src/settings/TokenEntry.tsx index 721c8b606aef96..2271ccf09287d1 100644 --- a/components/dashboard/src/settings/TokenEntry.tsx +++ b/components/dashboard/src/settings/TokenEntry.tsx @@ -5,47 +5,56 @@ */ import { PersonalAccessToken } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_pb"; +import { useState } from "react"; import { ContextMenuEntry } from "../components/ContextMenu"; import { ItemFieldContextMenu } from "../components/ItemsList"; +import { personalAccessTokensService } from "../service/public-api"; +import { ShowTokenModal } from "./PersonalAccessTokens"; +import { settingsPathPersonalAccessTokenEdit } from "./settings.routes"; -const menuEntries: ContextMenuEntry[] = [ - { - title: "Edit", - href: "", - onClick: () => {}, - }, - { - title: "Regenerate", - href: "", - onClick: () => {}, - }, - { - title: "Delete", - href: "", - onClick: () => {}, - }, -]; +function TokenEntry(props: { token: PersonalAccessToken; onDelete: (tokenId: string) => void }) { + const [showDelModal, setShowDelModal] = useState(false); -function TokenEntry(t: { token: PersonalAccessToken }) { - if (!t) { - return <>; - } + const menuEntries: ContextMenuEntry[] = [ + { + title: "Edit", + link: `${settingsPathPersonalAccessTokenEdit}/${props.token.id}`, + }, + { + title: "Delete", + href: "", + customFontStyle: "text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300", + onClick: () => { + setShowDelModal(true); + }, + }, + ]; + + const doDeletePAT = async () => { + try { + await personalAccessTokensService.deletePersonalAccessToken({ id: props.token.id }); + setShowDelModal(false); + props.onDelete(props.token.id); + } catch (e) { + // TODO: show error + } + }; const getDate = () => { - if (!t.token.expirationTime) { + if (!props.token.expirationTime) { return ""; } - const date = t.token.expirationTime?.toDate(); + const date = props.token.expirationTime?.toDate(); return date.toDateString(); }; const defaultAllScope = ["function:*", "resource:default"]; const getScopes = () => { - if (!t.token.scopes) { + if (!props.token.scopes) { return ""; } - if (t.token.scopes.every((v) => defaultAllScope.includes(v))) { + if (props.token.scopes.every((v) => defaultAllScope.includes(v))) { return "Access the user's API"; } else { return "No access"; @@ -56,7 +65,7 @@ function TokenEntry(t: { token: PersonalAccessToken }) { <>
- {t.token.name || ""} + {props.token.name || ""}
{getScopes()} @@ -68,6 +77,19 @@ function TokenEntry(t: { token: PersonalAccessToken }) {
+ {showDelModal && ( + { + setShowDelModal(false); + }} + /> + )} ); } diff --git a/components/dashboard/src/settings/settings-menu.ts b/components/dashboard/src/settings/settings-menu.ts index 9ba76643c194a7..2666d920079dd4 100644 --- a/components/dashboard/src/settings/settings-menu.ts +++ b/components/dashboard/src/settings/settings-menu.ts @@ -18,6 +18,7 @@ import { settingsPathSSHKeys, settingsPathPersonalAccessTokens, settingsPathPersonalAccessTokenCreate, + settingsPathPersonalAccessTokenEdit, } from "./settings.routes"; export default function getSettingsMenu(params: { @@ -38,7 +39,11 @@ export default function getSettingsMenu(params: { ? [ { title: "Access Tokens", - link: [settingsPathPersonalAccessTokens, settingsPathPersonalAccessTokenCreate], + link: [ + settingsPathPersonalAccessTokens, + settingsPathPersonalAccessTokenCreate, + settingsPathPersonalAccessTokenEdit, + ], }, ] : []), diff --git a/components/dashboard/src/settings/settings.routes.ts b/components/dashboard/src/settings/settings.routes.ts index f3e121c1536578..414fc65999db27 100644 --- a/components/dashboard/src/settings/settings.routes.ts +++ b/components/dashboard/src/settings/settings.routes.ts @@ -21,5 +21,6 @@ export const settingsPathTeamsNew = [settingsPathTeams, "new"].join("/"); export const settingsPathVariables = "/variables"; export const settingsPathPersonalAccessTokens = "/personal-tokens"; export const settingsPathPersonalAccessTokenCreate = "/personal-tokens/create"; +export const settingsPathPersonalAccessTokenEdit = "/personal-tokens/edit"; export const settingsPathSSHKeys = "/keys";