Skip to content

Commit 5ce88c3

Browse files
authored
[dashboard] allow no expiration date PAT (#19327)
* [dashboard] allow no expiration date PAT * fix iso datetime * Fix entry * nit
1 parent 8df43f3 commit 5ce88c3

File tree

4 files changed

+100
-43
lines changed

4 files changed

+100
-43
lines changed

components/dashboard/src/user-settings/PersonalAccessTokens.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,31 @@ export enum TokenAction {
4747
Delete = "DELETE",
4848
}
4949

50-
export const TokenExpirationDays = [
51-
{ value: "7", label: "7 Days" },
52-
{ value: "30", label: "30 Days" },
53-
{ value: "60", label: "60 Days" },
54-
{ value: "180", label: "180 Days" },
55-
];
50+
const expirationOptions = [7, 30, 60, 180].map((d) => ({
51+
label: `${d} Days`,
52+
value: `${d} Days`,
53+
getDate: () => dayjs().add(d, "days").toDate(),
54+
}));
55+
56+
// Max value of timestamp(6) in mysql is 2038-01-19 03:14:17
57+
const NoExpiresDate = dayjs("2038-01-01T00:00:00+00:00").toDate();
58+
export function getTokenExpirationDays(showForever: boolean) {
59+
if (!showForever) {
60+
return expirationOptions;
61+
}
62+
return [...expirationOptions, { label: "No expiration", value: "No expiration", getDate: () => NoExpiresDate }];
63+
}
64+
65+
export function isNeverExpired(date: Date) {
66+
return date.getTime() >= NoExpiresDate.getTime();
67+
}
68+
69+
export function getTokenExpirationDescription(date: Date) {
70+
if (isNeverExpired(date)) {
71+
return "The token will never expire!";
72+
}
73+
return `The token will expire on ${dayjs(date).format("MMM D, YYYY")}`;
74+
}
5675

5776
export const AllPermissions: PermissionDetail[] = [
5877
{
@@ -202,7 +221,11 @@ function ListAccessTokensView() {
202221
</div>
203222
<div className="text-gray-400 dark:text-gray-300">
204223
<span>
205-
Expires on {dayjs(tokenInfo.data.expirationTime!.toDate()).format("MMM D, YYYY")}
224+
{isNeverExpired(tokenInfo.data.expirationTime!.toDate())
225+
? "Never expires!"
226+
: `Expires on ${dayjs(tokenInfo.data.expirationTime!.toDate()).format(
227+
"MMM D, YYYY",
228+
)}`}
206229
</span>
207230
<span> · </span>
208231
<span>Created on {dayjs(tokenInfo.data.createdAt!.toDate()).format("MMM D, YYYY")}</span>

components/dashboard/src/user-settings/PersonalAccessTokensCreateView.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,33 @@
55
*/
66

77
import { PersonalAccessToken } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_pb";
8-
import dayjs from "dayjs";
9-
import { useEffect, useState } from "react";
8+
import { useEffect, useMemo, useState } from "react";
109
import { Redirect, useHistory, useParams } from "react-router";
1110
import Alert from "../components/Alert";
1211
import DateSelector from "../components/DateSelector";
1312
import { SpinnerOverlayLoader } from "../components/Loader";
1413
import { personalAccessTokensService } from "../service/public-api";
1514
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
16-
import { AllPermissions, TokenAction, TokenExpirationDays, TokenInfo } from "./PersonalAccessTokens";
15+
import {
16+
AllPermissions,
17+
TokenAction,
18+
getTokenExpirationDays,
19+
TokenInfo,
20+
getTokenExpirationDescription,
21+
} from "./PersonalAccessTokens";
1722
import { settingsPathPersonalAccessTokens } from "./settings.routes";
1823
import ShowTokenModal from "./ShowTokenModal";
1924
import { Timestamp } from "@bufbuild/protobuf";
2025
import arrowDown from "../images/sort-arrow.svg";
2126
import { Heading2, Subheading } from "../components/typography/headings";
22-
import { useFeatureFlag } from "../data/featureflag-query";
27+
import { useFeatureFlag, useIsDataOps } from "../data/featureflag-query";
2328
import { LinkButton } from "@podkit/buttons/LinkButton";
2429
import { Button } from "@podkit/buttons/Button";
2530
import { TextInputField } from "../components/forms/TextInputField";
2631

2732
interface EditPATData {
2833
name: string;
29-
expirationDays: string;
34+
expirationValue: string;
3035
expirationDate: Date;
3136
scopes: Set<string>;
3237
}
@@ -44,8 +49,8 @@ function PersonalAccessTokenCreateView() {
4449
const [editToken, setEditToken] = useState<PersonalAccessToken>();
4550
const [token, setToken] = useState<EditPATData>({
4651
name: "",
47-
expirationDays: "30",
48-
expirationDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
52+
expirationValue: "30 Days",
53+
expirationDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // default option 30 days
4954
scopes: new Set<string>(AllPermissions[0].scopes), // default to all permissions
5055
});
5156
const [modalData, setModalData] = useState<{ token: PersonalAccessToken }>();
@@ -59,6 +64,9 @@ function PersonalAccessTokenCreateView() {
5964
});
6065
}
6166

67+
const isDataOps = useIsDataOps();
68+
const TokenExpirationDays = useMemo(() => getTokenExpirationDays(isDataOps), [isDataOps]);
69+
6270
useEffect(() => {
6371
(async () => {
6472
try {
@@ -84,8 +92,9 @@ function PersonalAccessTokenCreateView() {
8492
}, []);
8593

8694
const update = (change: Partial<EditPATData>, addScopes?: string[], removeScopes?: string[]) => {
87-
if (change.expirationDays) {
88-
change.expirationDate = new Date(Date.now() + Number(change.expirationDays) * 24 * 60 * 60 * 1000);
95+
if (change.expirationValue) {
96+
const found = TokenExpirationDays.find((e) => e.value === change.expirationValue);
97+
change.expirationDate = found?.getDate();
8998
}
9099
const data = { ...token, ...change };
91100
if (addScopes) {
@@ -218,13 +227,11 @@ function PersonalAccessTokenCreateView() {
218227
{!isEditing && (
219228
<DateSelector
220229
title="Expiration Date"
221-
description={`The token will expire on ${dayjs(token.expirationDate).format(
222-
"MMM D, YYYY",
223-
)}`}
230+
description={getTokenExpirationDescription(token.expirationDate)}
224231
options={TokenExpirationDays}
225-
value={TokenExpirationDays.find((i) => i.value === token.expirationDays)?.value}
232+
value={TokenExpirationDays.find((i) => i.value === token.expirationValue)?.value}
226233
onChange={(value) => {
227-
update({ expirationDays: value });
234+
update({ expirationValue: value });
228235
}}
229236
/>
230237
)}

components/dashboard/src/user-settings/ShowTokenModal.tsx

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
*/
66

77
import { PersonalAccessToken } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_pb";
8-
import dayjs from "dayjs";
9-
import { useState } from "react";
8+
import { useMemo, useState } from "react";
109
import DateSelector from "../components/DateSelector";
1110
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal";
12-
import { TokenExpirationDays } from "./PersonalAccessTokens";
11+
import { getTokenExpirationDays, getTokenExpirationDescription } from "./PersonalAccessTokens";
1312
import { Button } from "@podkit/buttons/Button";
13+
import { useIsDataOps } from "../data/featureflag-query";
1414

1515
interface TokenModalProps {
1616
token: PersonalAccessToken;
@@ -25,7 +25,7 @@ interface TokenModalProps {
2525

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

@@ -34,6 +34,9 @@ function ShowTokenModal(props: TokenModalProps) {
3434
props.onClose();
3535
};
3636

37+
const isDataOps = useIsDataOps();
38+
const TokenExpirationDays = useMemo(() => getTokenExpirationDays(isDataOps), [isDataOps]);
39+
3740
return (
3841
<Modal visible onClose={props.onClose} onSubmit={save}>
3942
<ModalHeader>{props.title}</ModalHeader>
@@ -44,24 +47,26 @@ function ShowTokenModal(props: TokenModalProps) {
4447
<div className="p-4 mt-2 rounded-xl bg-gray-50 dark:bg-gray-800">
4548
<div className="font-semibold text-gray-700 dark:text-gray-200">{props.token.name}</div>
4649
<div className="font-medium text-gray-400 dark:text-gray-300">
47-
Expires on {dayjs(props.token.expirationTime!.toDate()).format("MMM D, YYYY")}
50+
{getTokenExpirationDescription(props.token.expirationTime!.toDate())}
4851
</div>
4952
</div>
5053
<div className="mt-4">
5154
{props.showDateSelector && (
5255
<DateSelector
5356
title="Expiration Date"
54-
description={`The token will expire on ${dayjs(expiration.expirationDate).format(
55-
"MMM D, YYYY",
56-
)}`}
57+
description={getTokenExpirationDescription(expiration.expirationDate)}
5758
options={TokenExpirationDays}
5859
value={TokenExpirationDays.find((i) => i.value === expiration.expirationDays)?.value}
59-
onChange={(value) =>
60+
onChange={(value) => {
61+
const date = TokenExpirationDays.find((e) => e.value === value)?.getDate();
62+
if (!date) {
63+
return;
64+
}
6065
setExpiration({
6166
expirationDays: value,
62-
expirationDate: new Date(Date.now() + Number(value) * 24 * 60 * 60 * 1000),
63-
})
64-
}
67+
expirationDate: date,
68+
});
69+
}}
6570
/>
6671
)}
6772
</div>

components/dashboard/src/user-settings/TokenEntry.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,41 @@ import { ContextMenuEntry } from "../components/ContextMenu";
1010
import { ItemFieldContextMenu } from "../components/ItemsList";
1111
import Tooltip from "../components/Tooltip";
1212
import { ReactComponent as ExclamationIcon } from "../images/exclamation.svg";
13-
import { AllPermissions } from "./PersonalAccessTokens";
13+
import { ReactComponent as WarningIcon } from "../images/exclamation2.svg";
14+
import { AllPermissions, isNeverExpired } from "./PersonalAccessTokens";
15+
import { useMemo } from "react";
1416

1517
interface TokenEntryProps {
1618
token: PersonalAccessToken;
1719
menuEntries: ContextMenuEntry[];
1820
}
1921

2022
function TokenEntry(props: TokenEntryProps) {
21-
const expirationDay = dayjs(props.token.expirationTime!.toDate());
22-
const expired = expirationDay.isBefore(dayjs());
23-
const expirationDateString = expirationDay.format("MMM D, YYYY, hh:mm A");
23+
const expiredInfo = useMemo(() => {
24+
const expiration = props.token.expirationTime!.toDate();
25+
if (isNeverExpired(expiration)) {
26+
return {
27+
expired: false,
28+
content: "Never expires!",
29+
tooltip: {
30+
content: "The token will never expire!",
31+
icon: <WarningIcon className="h-4 w-4" />,
32+
},
33+
};
34+
}
35+
const expirationTime = dayjs(expiration);
36+
const expired = expirationTime.isBefore(dayjs());
37+
return {
38+
expired,
39+
content: expirationTime.format("MMM D, YYYY"),
40+
tooltip: expired
41+
? {
42+
content: expirationTime.format("MMM D, YYYY, hh:mm A"),
43+
icon: <ExclamationIcon fill="#D97706" className="h-4 w-4" />,
44+
}
45+
: undefined,
46+
};
47+
}, [props.token.expirationTime]);
2448

2549
const getScopes = () => {
2650
if (!props.token.scopes) {
@@ -43,13 +67,11 @@ function TokenEntry(props: TokenEntryProps) {
4367
<span className="truncate whitespace-pre-line">{getScopes()}</span>
4468
</div>
4569
<div className="flex items-center w-3/12 text-gray-400">
46-
<span className={"flex items-center gap-1 truncate" + (expired ? " text-orange-600" : "")}>
47-
<span>{expirationDay.format("MMM D, YYYY")}</span>
48-
{expired && (
49-
<Tooltip content={expirationDateString}>
50-
<ExclamationIcon fill="#D97706" className="h-4 w-4" />
51-
</Tooltip>
70+
<span className={"flex items-center gap-1 truncate" + (expiredInfo.expired ? " text-orange-600" : "")}>
71+
{expiredInfo.tooltip && (
72+
<Tooltip content={expiredInfo.tooltip.content}>{expiredInfo.tooltip.icon}</Tooltip>
5273
)}
74+
<span>{expiredInfo.content}</span>
5375
</span>
5476
</div>
5577
<div className="flex items-center justify-end w-1/12">

0 commit comments

Comments
 (0)