(false);
+ const handleCopyToClipboard = (text: string) => {
+ copyToClipboard(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+ const tip = props.tip ?? "Click to copy";
+ return (
+
+
+
handleCopyToClipboard(props.value)}>
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/dashboard/src/settings/PersonalAccessTokens.tsx b/components/dashboard/src/settings/PersonalAccessTokens.tsx
index 06dc0e8c422800..500bc0ad9f434c 100644
--- a/components/dashboard/src/settings/PersonalAccessTokens.tsx
+++ b/components/dashboard/src/settings/PersonalAccessTokens.tsx
@@ -6,10 +6,18 @@
import { PersonalAccessToken } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_pb";
import { useContext, useEffect, useState } from "react";
-import { Redirect } from "react-router";
+import { Redirect, useHistory, useLocation } from "react-router";
+import { Link } from "react-router-dom";
+import CheckBox from "../components/CheckBox";
import { FeatureFlagContext } from "../contexts/FeatureFlagContext";
import { personalAccessTokensService } from "../service/public-api";
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
+import { settingsPathPersonalAccessTokenCreate, settingsPathPersonalAccessTokens } from "./settings.routes";
+import arrowDown from "../images/sort-arrow.svg";
+import { Timestamp } from "@bufbuild/protobuf";
+import Alert from "../components/Alert";
+import { InputWithCopy } from "../components/InputWithCopy";
+import { copyToClipboard } from "../utils";
function PersonalAccessTokens() {
const { enablePersonalAccessTokens } = useContext(FeatureFlagContext);
@@ -27,8 +35,147 @@ function PersonalAccessTokens() {
);
}
+interface EditPATData {
+ name: string;
+ expirationDays: number;
+ expirationDate: Date;
+}
+
+export function PersonalAccessTokenCreateView() {
+ const { enablePersonalAccessTokens } = useContext(FeatureFlagContext);
+
+ const history = useHistory();
+ const [errorMsg, setErrorMsg] = useState("");
+ const [value, setValue] = useState
({
+ name: "",
+ expirationDays: 30,
+ expirationDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
+ });
+
+ const update = (change: Partial) => {
+ if (change.expirationDays) {
+ change.expirationDate = new Date(Date.now() + change.expirationDays * 24 * 60 * 60 * 1000);
+ }
+ setErrorMsg("");
+ setValue({ ...value, ...change });
+ };
+
+ const createToken = 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,
+ },
+ });
+ } catch (e) {
+ setErrorMsg(e.message);
+ }
+ };
+
+ if (!enablePersonalAccessTokens) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ <>
+ {errorMsg.length > 0 && (
+
+ {errorMsg}
+
+ )}
+ >
+
+
+
New Personal Access Token
+ Create a new personal access token.
+
+
+
+
Token Name
+
{
+ update({ name: e.target.value });
+ }}
+ 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)}.
+
+
+
+
Permission
+
+
+
+
+
+
+
+ );
+}
+
+interface TokenInfo {
+ method: string;
+ data: PersonalAccessToken;
+}
+
function ListAccessTokensView() {
+ const location = useLocation();
+
const [tokens, setTokens] = useState([]);
+ const [tokenInfo, setTokenInfo] = useState();
useEffect(() => {
(async () => {
@@ -37,21 +184,73 @@ function ListAccessTokensView() {
})();
}, []);
+ useEffect(() => {
+ if (location.state) {
+ setTokenInfo(location.state as any as TokenInfo);
+ window.history.replaceState({}, "");
+ }
+ }, [location.state]);
+
+ const handleCopyToken = () => {
+ copyToClipboard(tokenInfo!.data.value);
+ };
+
return (
<>
-
+
Personal Access Tokens
Create or regenerate active personal access tokens.
+
+
+
-
-
No Personal Access Tokens (PAT)
-
- Generate a personal access token (PAT) for applications that need access to the Gitpod API.{" "}
-
-
-
+ <>
+ {tokenInfo && (
+ <>
+
+
+
+ {tokenInfo.data.name}{" "}
+
+ {tokenInfo.method.toUpperCase()}
+
+
+
+
+ Expires on{" "}
+ {Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(
+ tokenInfo.data.expirationTime?.toDate(),
+ )}
+
+ •
+
+ Created on{" "}
+ {Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(
+ tokenInfo.data.createdAt?.toDate(),
+ )}
+
+
+
+
+
Your New Personal Access Token
+
+
+ Make sure to copy your personal access token — you won't be able to access it again.
+
+
+
+
+ >
+ )}
+ >
{tokens.length > 0 && (
{tokens.map((t: PersonalAccessToken) => {
diff --git a/components/dashboard/src/settings/settings-menu.ts b/components/dashboard/src/settings/settings-menu.ts
index 2f80bcdaf6e5d1..9ba76643c194a7 100644
--- a/components/dashboard/src/settings/settings-menu.ts
+++ b/components/dashboard/src/settings/settings-menu.ts
@@ -17,6 +17,7 @@ import {
settingsPathVariables,
settingsPathSSHKeys,
settingsPathPersonalAccessTokens,
+ settingsPathPersonalAccessTokenCreate,
} from "./settings.routes";
export default function getSettingsMenu(params: {
@@ -37,7 +38,7 @@ export default function getSettingsMenu(params: {
? [
{
title: "Access Tokens",
- link: [settingsPathPersonalAccessTokens],
+ link: [settingsPathPersonalAccessTokens, settingsPathPersonalAccessTokenCreate],
},
]
: []),
diff --git a/components/dashboard/src/settings/settings.routes.ts b/components/dashboard/src/settings/settings.routes.ts
index d4520a8e50d557..f3e121c1536578 100644
--- a/components/dashboard/src/settings/settings.routes.ts
+++ b/components/dashboard/src/settings/settings.routes.ts
@@ -20,5 +20,6 @@ export const settingsPathTeamsNew = [settingsPathTeams, "new"].join("/");
export const settingsPathVariables = "/variables";
export const settingsPathPersonalAccessTokens = "/personal-tokens";
+export const settingsPathPersonalAccessTokenCreate = "/personal-tokens/create";
export const settingsPathSSHKeys = "/keys";
diff --git a/components/dashboard/src/utils.ts b/components/dashboard/src/utils.ts
index c29b45f501f179..955f07bf859e1a 100644
--- a/components/dashboard/src/utils.ts
+++ b/components/dashboard/src/utils.ts
@@ -79,3 +79,15 @@ export function inResource(pathname: string, resources: string[]): boolean {
// E.g. "api/userspace/resource" path is a part of resource "api/userspace"
return resources.map((res) => trimmedResource.startsWith(trimResource(res))).some(Boolean);
}
+
+export function copyToClipboard(text: string) {
+ const el = document.createElement("textarea");
+ el.value = text;
+ document.body.appendChild(el);
+ el.select();
+ try {
+ document.execCommand("copy");
+ } finally {
+ document.body.removeChild(el);
+ }
+}
diff --git a/components/dashboard/src/workspaces/ConnectToSSHModal.tsx b/components/dashboard/src/workspaces/ConnectToSSHModal.tsx
index 5e0366704312c8..a7aed9c30f0c19 100644
--- a/components/dashboard/src/workspaces/ConnectToSSHModal.tsx
+++ b/components/dashboard/src/workspaces/ConnectToSSHModal.tsx
@@ -6,49 +6,11 @@
import { useEffect, useState } from "react";
import Modal from "../components/Modal";
-import Tooltip from "../components/Tooltip";
-import copy from "../images/copy.svg";
import Alert from "../components/Alert";
import TabMenuItem from "../components/TabMenuItem";
import { settingsPathSSHKeys } from "../settings/settings.routes";
import { getGitpodService } from "../service/service";
-
-function InputWithCopy(props: { value: string; tip?: string; className?: string }) {
- const [copied, setCopied] = useState(false);
- const copyToClipboard = (text: string) => {
- const el = document.createElement("textarea");
- el.value = text;
- document.body.appendChild(el);
- el.select();
- try {
- document.execCommand("copy");
- } finally {
- document.body.removeChild(el);
- }
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- };
- const tip = props.tip ?? "Click to copy";
- return (
-
-
-
copyToClipboard(props.value)}>
-
-
-
-
-
-
-
- );
-}
+import { InputWithCopy } from "../components/InputWithCopy";
interface SSHProps {
workspaceId: string;