Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane imports
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { WorkspaceAPITokenService } from "@plane/services";
// component
import { CreateApiTokenModal } from "@/components/api-token/modal/create-token-modal";
import { ApiTokenListItem } from "@/components/api-token/token-list-item";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { SettingsHeading } from "@/components/settings/heading";
import { APITokenSettingsLoader } from "@/components/ui/loader/settings/api-token";
// constants
import { WORKSPACE_API_TOKENS_LIST } from "@/constants/fetch-keys";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// store hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserPermissions } from "@/hooks/store/user";
import type { Route } from "./+types/page";

const workspaceApiTokenService = new WorkspaceAPITokenService();

function ApiTokensPage({ params }: Route.ComponentProps) {
// states
const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false);
// router
const { workspaceSlug } = params;
// plane hooks
const { t } = useTranslation();
// store hooks
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { currentWorkspace } = useWorkspace();
// derived values
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);

const { data: tokens } = useSWR(
canPerformWorkspaceAdminActions ? WORKSPACE_API_TOKENS_LIST(workspaceSlug) : null,
canPerformWorkspaceAdminActions ? () => workspaceApiTokenService.list(workspaceSlug) : null
);

const pageTitle = currentWorkspace?.name
? `${currentWorkspace.name} - ${t("workspace_settings.settings.api_tokens.title")}`
: undefined;

if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
return <NotAuthorizedView section="settings" className="h-auto" />;
}

return (
<SettingsContentWrapper>
<PageHead title={pageTitle} />
{!tokens ? (
<APITokenSettingsLoader title={t("workspace_settings.settings.api_tokens.title")} />
) : (
<div className="w-full">
<CreateApiTokenModal
isOpen={isCreateTokenModalOpen}
onClose={() => setIsCreateTokenModalOpen(false)}
workspaceSlug={workspaceSlug}
/>
<SettingsHeading
title={t("workspace_settings.settings.api_tokens.heading")}
description={t("workspace_settings.settings.api_tokens.description")}
button={{
label: t("workspace_settings.settings.api_tokens.add_token"),
onClick: () => {
captureClick({
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_PAT_BUTTON,
});
setIsCreateTokenModalOpen(true);
},
}}
/>
{tokens.length > 0 ? (
<div className="flex h-full w-full flex-col">
<div className="h-full w-full overflow-y-auto">
{tokens.map((token) => (
<ApiTokenListItem key={token.id} token={token} workspaceSlug={workspaceSlug} />
))}
</div>
</div>
) : (
<div className="flex h-full w-full flex-col">
<div className="h-full w-full flex items-center justify-center">
<EmptyStateCompact
assetKey="token"
title={t("settings_empty_state.tokens.title")}
description={t("settings_empty_state.tokens.description")}
actions={[
{
label: t("settings_empty_state.tokens.cta_primary"),
onClick: () => {
captureClick({
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON,
});
setIsCreateTokenModalOpen(true);
},
},
]}
align="start"
rootClassName="py-20"
/>
</div>
</div>
)}
</div>
)}
</SettingsContentWrapper>
);
}

export default observer(ApiTokensPage);
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useParams, usePathname } from "next/navigation";
import { ArrowUpToLine, Building, CreditCard, Users, Webhook } from "lucide-react";
import { ArrowUpToLine, Building, CreditCard, KeyRound, Users, Webhook } from "lucide-react";
import type { LucideIcon } from "lucide-react";
// plane imports
import {
Expand All @@ -25,6 +25,7 @@ export const WORKSPACE_SETTINGS_ICONS: Record<keyof typeof WORKSPACE_SETTINGS, L
export: ArrowUpToLine,
"billing-and-plans": CreditCard,
webhooks: Webhook,
"api-tokens": KeyRound,
};

export function WorkspaceActionIcons({ type, size, className }: { type: string; size?: number; className?: string }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const apiTokenService = new APITokenService();
function ApiTokensPage() {
// states
const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false);
// router
// plane hooks
const { t } = useTranslation();
// store hooks
Expand All @@ -31,11 +30,11 @@ function ApiTokensPage() {
const { data: tokens } = useSWR(API_TOKENS_LIST, () => apiTokenService.list());

const pageTitle = currentWorkspace?.name
? `${currentWorkspace.name} - ${t("workspace_settings.settings.api_tokens.title")}`
? `${currentWorkspace.name} - ${t("account_settings.api_tokens.title")}`
: undefined;

if (!tokens) {
return <APITokenSettingsLoader />;
return <APITokenSettingsLoader title={t("account_settings.api_tokens.title")} />;
}

return (
Expand Down
4 changes: 4 additions & 0 deletions apps/web/app/routes/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@ export const coreRoutes: RouteConfigEntry[] = [
":workspaceSlug/settings/webhooks/:webhookId",
"./(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx"
),
route(
":workspaceSlug/settings/api-tokens",
"./(all)/[workspaceSlug]/(settings)/settings/(workspace)/api-tokens/page.tsx"
),
]),

// --------------------------------------------------------------------
Expand Down
28 changes: 18 additions & 10 deletions apps/web/core/components/api-token/delete-token-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import type { FC } from "react";
import { useState } from "react";
import { mutate } from "swr";
// types
import { PROFILE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { PROFILE_SETTINGS_TRACKER_EVENTS, WORKSPACE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { APITokenService } from "@plane/services";
import { APITokenService, WorkspaceAPITokenService } from "@plane/services";
import type { IApiToken } from "@plane/types";
// ui
import { AlertModalCore } from "@plane/ui";
// fetch-keys
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
import { API_TOKENS_LIST, WORKSPACE_API_TOKENS_LIST } from "@/constants/fetch-keys";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";

type Props = {
isOpen: boolean;
onClose: () => void;
tokenId: string;
workspaceSlug?: string;
};

const apiTokenService = new APITokenService();
const workspaceApiTokenService = new WorkspaceAPITokenService();

export function DeleteApiTokenModal(props: Props) {
const { isOpen, onClose, tokenId } = props;
const { isOpen, onClose, tokenId, workspaceSlug } = props;
// states
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
// router params
Expand All @@ -36,8 +37,11 @@ export function DeleteApiTokenModal(props: Props) {
const handleDeletion = async () => {
setDeleteLoading(true);

await apiTokenService
.destroy(tokenId)
const apiCall = workspaceSlug
? workspaceApiTokenService.destroy(workspaceSlug, tokenId)
: apiTokenService.destroy(tokenId);

await apiCall
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
Expand All @@ -46,12 +50,14 @@ export function DeleteApiTokenModal(props: Props) {
});

mutate<IApiToken[]>(
API_TOKENS_LIST,
workspaceSlug ? WORKSPACE_API_TOKENS_LIST(workspaceSlug) : API_TOKENS_LIST,
(prevData) => (prevData ?? []).filter((token) => token.id !== tokenId),
false
);
captureSuccess({
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_deleted,
eventName: workspaceSlug
? WORKSPACE_SETTINGS_TRACKER_EVENTS.pat_deleted
: PROFILE_SETTINGS_TRACKER_EVENTS.pat_deleted,
payload: {
token: tokenId,
},
Expand All @@ -68,7 +74,9 @@ export function DeleteApiTokenModal(props: Props) {
)
.catch((err) => {
captureError({
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_deleted,
eventName: workspaceSlug
? WORKSPACE_SETTINGS_TRACKER_EVENTS.pat_deleted
: PROFILE_SETTINGS_TRACKER_EVENTS.pat_deleted,
payload: {
token: tokenId,
},
Expand Down
24 changes: 15 additions & 9 deletions apps/web/core/components/api-token/modal/create-token-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React, { useState } from "react";
import { mutate } from "swr";
// plane imports
import { PROFILE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { PROFILE_SETTINGS_TRACKER_EVENTS, WORKSPACE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { APITokenService } from "@plane/services";
import { APITokenService, WorkspaceAPITokenService } from "@plane/services";
import type { IApiToken } from "@plane/types";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { renderFormattedDate, csvDownload } from "@plane/utils";
// constants
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
import { API_TOKENS_LIST, WORKSPACE_API_TOKENS_LIST } from "@/constants/fetch-keys";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// local imports
Expand All @@ -18,13 +18,15 @@ import { GeneratedTokenDetails } from "./generated-token-details";
type Props = {
isOpen: boolean;
onClose: () => void;
workspaceSlug?: string;
};

// services
const apiTokenService = new APITokenService();
const workspaceApiTokenService = new WorkspaceAPITokenService();

export function CreateApiTokenModal(props: Props) {
const { isOpen, onClose } = props;
const { isOpen, onClose, workspaceSlug } = props;
// states
const [neverExpires, setNeverExpires] = useState<boolean>(false);
const [generatedToken, setGeneratedToken] = useState<IApiToken | null | undefined>(null);
Expand All @@ -51,14 +53,14 @@ export function CreateApiTokenModal(props: Props) {

const handleCreateToken = async (data: Partial<IApiToken>) => {
// make the request to generate the token
await apiTokenService
.create(data)
const apiCall = workspaceSlug ? workspaceApiTokenService.create(workspaceSlug, data) : apiTokenService.create(data);
await apiCall
.then((res) => {
setGeneratedToken(res);
downloadSecretKey(res);

mutate<IApiToken[]>(
API_TOKENS_LIST,
workspaceSlug ? WORKSPACE_API_TOKENS_LIST(workspaceSlug) : API_TOKENS_LIST,
(prevData) => {
if (!prevData) return;

Expand All @@ -67,7 +69,9 @@ export function CreateApiTokenModal(props: Props) {
false
);
captureSuccess({
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_created,
eventName: workspaceSlug
? WORKSPACE_SETTINGS_TRACKER_EVENTS.pat_created
: PROFILE_SETTINGS_TRACKER_EVENTS.pat_created,
payload: {
token: res.id,
},
Expand All @@ -81,7 +85,9 @@ export function CreateApiTokenModal(props: Props) {
});

captureError({
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_created,
eventName: workspaceSlug
? WORKSPACE_SETTINGS_TRACKER_EVENTS.pat_created
: PROFILE_SETTINGS_TRACKER_EVENTS.pat_created,
});

throw err;
Expand Down
18 changes: 14 additions & 4 deletions apps/web/core/components/api-token/token-list-item.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from "react";
import { XCircle } from "lucide-react";
// plane imports
import { PROFILE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { PROFILE_SETTINGS_TRACKER_ELEMENTS, WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { Tooltip } from "@plane/propel/tooltip";
import type { IApiToken } from "@plane/types";
import { renderFormattedDate, calculateTimeAgo, renderFormattedTime } from "@plane/utils";
Expand All @@ -12,24 +12,34 @@ import { usePlatformOS } from "@/hooks/use-platform-os";

type Props = {
token: IApiToken;
workspaceSlug?: string;
};

export function ApiTokenListItem(props: Props) {
const { token } = props;
const { token, workspaceSlug } = props;
// states
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
// hooks
const { isMobile } = usePlatformOS();

const trackerElement = workspaceSlug
? WORKSPACE_SETTINGS_TRACKER_ELEMENTS.LIST_ITEM_DELETE_ICON
: PROFILE_SETTINGS_TRACKER_ELEMENTS.LIST_ITEM_DELETE_ICON;

return (
<>
<DeleteApiTokenModal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} tokenId={token.id} />
<DeleteApiTokenModal
isOpen={deleteModalOpen}
onClose={() => setDeleteModalOpen(false)}
tokenId={token.id}
workspaceSlug={workspaceSlug}
/>
<div className="group relative flex flex-col justify-center border-b border-subtle py-3">
<Tooltip tooltipContent="Delete token" isMobile={isMobile}>
<button
onClick={() => setDeleteModalOpen(true)}
className="absolute right-4 hidden place-items-center group-hover:grid"
data-ph-element={PROFILE_SETTINGS_TRACKER_ELEMENTS.LIST_ITEM_DELETE_ICON}
data-ph-element={trackerElement}
>
<XCircle className="h-4 w-4 text-red-500" />
</button>
Expand Down
12 changes: 8 additions & 4 deletions apps/web/core/components/ui/loader/settings/api-token.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { range } from "lodash-es";
import { useTranslation } from "@plane/i18n";
export function APITokenSettingsLoader() {
const { t } = useTranslation();

type Props = {
title: string;
};

export function APITokenSettingsLoader(props: Props) {
const { title } = props;
return (
<section className="w-full overflow-y-auto">
<div className="mb-2 flex items-center justify-between border-b border-subtle pb-3.5">
<h3 className="text-18 font-medium">{t("workspace_settings.settings.api_tokens.title")}</h3>
<h3 className="text-xl font-medium">{title}</h3>
<span className="h-8 w-28 bg-layer-1 rounded-sm" />
</div>
<div className="divide-y-[0.5px] divide-subtle-1">
Expand Down
2 changes: 2 additions & 0 deletions apps/web/core/constants/fetch-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId:

// api-tokens
export const API_TOKENS_LIST = `API_TOKENS_LIST`;
export const WORKSPACE_API_TOKENS_LIST = (workspaceSlug: string) =>
`WORKSPACE_API_TOKENS_LIST_${workspaceSlug.toUpperCase()}`;

// marketplace
export const APPLICATIONS_LIST = (workspaceSlug: string) => `APPLICATIONS_LIST_${workspaceSlug.toUpperCase()}`;
Expand Down
Loading
Loading