Skip to content

[dashboard] allow no expiration date PAT #19327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions components/dashboard/src/user-settings/PersonalAccessTokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,31 @@ export enum TokenAction {
Delete = "DELETE",
}

export const TokenExpirationDays = [
{ value: "7", label: "7 Days" },
{ value: "30", label: "30 Days" },
{ value: "60", label: "60 Days" },
{ value: "180", label: "180 Days" },
];
const expirationOptions = [7, 30, 60, 180].map((d) => ({
label: `${d} Days`,
value: `${d} Days`,
getDate: () => dayjs().add(d, "days").toDate(),
}));

// Max value of timestamp(6) in mysql is 2038-01-19 03:14:17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ thanks for checking that

const NoExpiresDate = dayjs("2038-01-01T00:00:00+00:00").toDate();
export function getTokenExpirationDays(showForever: boolean) {
if (!showForever) {
return expirationOptions;
}
return [...expirationOptions, { label: "No expiration", value: "No expiration", getDate: () => NoExpiresDate }];
}

export function isNeverExpired(date: Date) {
return date.getTime() >= NoExpiresDate.getTime();
}

export function getTokenExpirationDescription(date: Date) {
if (isNeverExpired(date)) {
return "The token will never expire!";
}
return `The token will expire on ${dayjs(date).format("MMM D, YYYY")}`;
}

export const AllPermissions: PermissionDetail[] = [
{
Expand Down Expand Up @@ -202,7 +221,11 @@ function ListAccessTokensView() {
</div>
<div className="text-gray-400 dark:text-gray-300">
<span>
Expires on {dayjs(tokenInfo.data.expirationTime!.toDate()).format("MMM D, YYYY")}
{isNeverExpired(tokenInfo.data.expirationTime!.toDate())
? "Never expires!"
: `Expires on ${dayjs(tokenInfo.data.expirationTime!.toDate()).format(
"MMM D, YYYY",
)}`}
</span>
<span> · </span>
<span>Created on {dayjs(tokenInfo.data.createdAt!.toDate()).format("MMM D, YYYY")}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,33 @@
*/

import { PersonalAccessToken } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_pb";
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Redirect, useHistory, useParams } from "react-router";
import Alert from "../components/Alert";
import DateSelector from "../components/DateSelector";
import { SpinnerOverlayLoader } from "../components/Loader";
import { personalAccessTokensService } from "../service/public-api";
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
import { AllPermissions, TokenAction, TokenExpirationDays, TokenInfo } from "./PersonalAccessTokens";
import {
AllPermissions,
TokenAction,
getTokenExpirationDays,
TokenInfo,
getTokenExpirationDescription,
} from "./PersonalAccessTokens";
import { settingsPathPersonalAccessTokens } from "./settings.routes";
import ShowTokenModal from "./ShowTokenModal";
import { Timestamp } from "@bufbuild/protobuf";
import arrowDown from "../images/sort-arrow.svg";
import { Heading2, Subheading } from "../components/typography/headings";
import { useFeatureFlag } from "../data/featureflag-query";
import { useFeatureFlag, useIsDataOps } from "../data/featureflag-query";
import { LinkButton } from "@podkit/buttons/LinkButton";
import { Button } from "@podkit/buttons/Button";
import { TextInputField } from "../components/forms/TextInputField";

interface EditPATData {
name: string;
expirationDays: string;
expirationValue: string;
expirationDate: Date;
scopes: Set<string>;
}
Expand All @@ -44,8 +49,8 @@ function PersonalAccessTokenCreateView() {
const [editToken, setEditToken] = useState<PersonalAccessToken>();
const [token, setToken] = useState<EditPATData>({
name: "",
expirationDays: "30",
expirationDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
expirationValue: "30 Days",
expirationDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // default option 30 days
scopes: new Set<string>(AllPermissions[0].scopes), // default to all permissions
});
const [modalData, setModalData] = useState<{ token: PersonalAccessToken }>();
Expand All @@ -59,6 +64,9 @@ function PersonalAccessTokenCreateView() {
});
}

const isDataOps = useIsDataOps();
const TokenExpirationDays = useMemo(() => getTokenExpirationDays(isDataOps), [isDataOps]);

useEffect(() => {
(async () => {
try {
Expand All @@ -84,8 +92,9 @@ function PersonalAccessTokenCreateView() {
}, []);

const update = (change: Partial<EditPATData>, addScopes?: string[], removeScopes?: string[]) => {
if (change.expirationDays) {
change.expirationDate = new Date(Date.now() + Number(change.expirationDays) * 24 * 60 * 60 * 1000);
if (change.expirationValue) {
const found = TokenExpirationDays.find((e) => e.value === change.expirationValue);
change.expirationDate = found?.getDate();
}
const data = { ...token, ...change };
if (addScopes) {
Expand Down Expand Up @@ -218,13 +227,11 @@ function PersonalAccessTokenCreateView() {
{!isEditing && (
<DateSelector
title="Expiration Date"
description={`The token will expire on ${dayjs(token.expirationDate).format(
"MMM D, YYYY",
)}`}
description={getTokenExpirationDescription(token.expirationDate)}
options={TokenExpirationDays}
value={TokenExpirationDays.find((i) => i.value === token.expirationDays)?.value}
value={TokenExpirationDays.find((i) => i.value === token.expirationValue)?.value}
onChange={(value) => {
update({ expirationDays: value });
update({ expirationValue: value });
}}
/>
)}
Expand Down
29 changes: 17 additions & 12 deletions components/dashboard/src/user-settings/ShowTokenModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
*/

import { PersonalAccessToken } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_pb";
import dayjs from "dayjs";
import { useState } from "react";
import { useMemo, useState } from "react";
import DateSelector from "../components/DateSelector";
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal";
import { TokenExpirationDays } from "./PersonalAccessTokens";
import { getTokenExpirationDays, getTokenExpirationDescription } from "./PersonalAccessTokens";
import { Button } from "@podkit/buttons/Button";
import { useIsDataOps } from "../data/featureflag-query";

interface TokenModalProps {
token: PersonalAccessToken;
Expand All @@ -25,7 +25,7 @@ interface TokenModalProps {

function ShowTokenModal(props: TokenModalProps) {
const [expiration, setExpiration] = useState({
expirationDays: "30",
expirationDays: "30 Days",
expirationDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
});

Expand All @@ -34,6 +34,9 @@ function ShowTokenModal(props: TokenModalProps) {
props.onClose();
};

const isDataOps = useIsDataOps();
const TokenExpirationDays = useMemo(() => getTokenExpirationDays(isDataOps), [isDataOps]);

return (
<Modal visible onClose={props.onClose} onSubmit={save}>
<ModalHeader>{props.title}</ModalHeader>
Expand All @@ -44,24 +47,26 @@ function ShowTokenModal(props: TokenModalProps) {
<div className="p-4 mt-2 rounded-xl bg-gray-50 dark:bg-gray-800">
<div className="font-semibold text-gray-700 dark:text-gray-200">{props.token.name}</div>
<div className="font-medium text-gray-400 dark:text-gray-300">
Expires on {dayjs(props.token.expirationTime!.toDate()).format("MMM D, YYYY")}
{getTokenExpirationDescription(props.token.expirationTime!.toDate())}
</div>
</div>
<div className="mt-4">
{props.showDateSelector && (
<DateSelector
title="Expiration Date"
description={`The token will expire on ${dayjs(expiration.expirationDate).format(
"MMM D, YYYY",
)}`}
description={getTokenExpirationDescription(expiration.expirationDate)}
options={TokenExpirationDays}
value={TokenExpirationDays.find((i) => i.value === expiration.expirationDays)?.value}
onChange={(value) =>
onChange={(value) => {
const date = TokenExpirationDays.find((e) => e.value === value)?.getDate();
if (!date) {
return;
}
setExpiration({
expirationDays: value,
expirationDate: new Date(Date.now() + Number(value) * 24 * 60 * 60 * 1000),
})
}
expirationDate: date,
});
}}
/>
)}
</div>
Expand Down
42 changes: 32 additions & 10 deletions components/dashboard/src/user-settings/TokenEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,41 @@ import { ContextMenuEntry } from "../components/ContextMenu";
import { ItemFieldContextMenu } from "../components/ItemsList";
import Tooltip from "../components/Tooltip";
import { ReactComponent as ExclamationIcon } from "../images/exclamation.svg";
import { AllPermissions } from "./PersonalAccessTokens";
import { ReactComponent as WarningIcon } from "../images/exclamation2.svg";
import { AllPermissions, isNeverExpired } from "./PersonalAccessTokens";
import { useMemo } from "react";

interface TokenEntryProps {
token: PersonalAccessToken;
menuEntries: ContextMenuEntry[];
}

function TokenEntry(props: TokenEntryProps) {
const expirationDay = dayjs(props.token.expirationTime!.toDate());
const expired = expirationDay.isBefore(dayjs());
const expirationDateString = expirationDay.format("MMM D, YYYY, hh:mm A");
const expiredInfo = useMemo(() => {
const expiration = props.token.expirationTime!.toDate();
if (isNeverExpired(expiration)) {
return {
expired: false,
content: "Never expires!",
tooltip: {
content: "The token will never expire!",
icon: <WarningIcon className="h-4 w-4" />,
},
};
}
const expirationTime = dayjs(expiration);
const expired = expirationTime.isBefore(dayjs());
return {
expired,
content: expirationTime.format("MMM D, YYYY"),
tooltip: expired
? {
content: expirationTime.format("MMM D, YYYY, hh:mm A"),
icon: <ExclamationIcon fill="#D97706" className="h-4 w-4" />,
}
: undefined,
};
}, [props.token.expirationTime]);

const getScopes = () => {
if (!props.token.scopes) {
Expand All @@ -43,13 +67,11 @@ function TokenEntry(props: TokenEntryProps) {
<span className="truncate whitespace-pre-line">{getScopes()}</span>
</div>
<div className="flex items-center w-3/12 text-gray-400">
<span className={"flex items-center gap-1 truncate" + (expired ? " text-orange-600" : "")}>
<span>{expirationDay.format("MMM D, YYYY")}</span>
{expired && (
<Tooltip content={expirationDateString}>
<ExclamationIcon fill="#D97706" className="h-4 w-4" />
</Tooltip>
<span className={"flex items-center gap-1 truncate" + (expiredInfo.expired ? " text-orange-600" : "")}>
{expiredInfo.tooltip && (
<Tooltip content={expiredInfo.tooltip.content}>{expiredInfo.tooltip.icon}</Tooltip>
)}
<span>{expiredInfo.content}</span>
</span>
</div>
<div className="flex items-center justify-end w-1/12">
Expand Down