From ef29981a54dee8ee40c8fcd21de10795e7da3a3f Mon Sep 17 00:00:00 2001
From: Kasper Fabricius Kristensen
<45367945+kasperkristensen@users.noreply.github.com>
Date: Mon, 22 Apr 2024 22:03:28 +0200
Subject: [PATCH] fix(dashboard): Update API keys domains with new design
(#7123)
**What**
- Adds new design
- Separates secret and publishable keys into two domains (re-uses the same code).
- Adds skeleton layout for loading state of details page.
- Adds toasts.
---
.../public/locales/en-US/translation.json | 42 ++++--
.../components/common/skeleton/skeleton.tsx | 76 ++++++++++
.../settings-layout/settings-layout.tsx | 8 +-
.../data-table-skeleton.tsx | 138 ------------------
.../data-table/data-table-skeleton/index.ts | 1 -
.../table/data-table/data-table.tsx | 9 +-
.../src/providers/router-provider/v2.tsx | 72 ++++++---
.../api-key-management-create.tsx | 4 +-
.../api-key-create-form.tsx} | 96 +++++++++---
.../components/api-key-create-form/index.ts | 1 +
.../create-publishable-api-key-form/index.ts | 1 -
.../api-key-management-detail.tsx | 14 +-
.../api-key-general-section.tsx | 77 ++++++----
.../api-key-sales-channel-section.tsx | 45 +++++-
.../edit-api-key-form/edit-api-key-form.tsx | 16 +-
.../api-key-management-list.tsx | 26 +---
.../api-key-management-list-table.tsx | 2 +-
.../api-key-row-actions.tsx | 61 +++++++-
.../use-api-key-management-table-columns.tsx | 50 +++----
.../api-key-management-sales-channels.tsx | 6 +-
.../api-key-sales-channels-fallback.tsx | 26 ----
.../api-key-sales-channels-fallback/index.ts | 1 -
.../api-key-sales-channels-form.tsx | 23 ++-
.../api-key-management/common/utils.ts | 16 +-
24 files changed, 480 insertions(+), 331 deletions(-)
delete mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx
delete mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/index.ts
rename packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/{create-publishable-api-key-form/create-publishable-api-key-form.tsx => api-key-create-form/api-key-create-form.tsx} (60%)
create mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/api-key-create-form/index.ts
delete mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/index.ts
delete mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/api-key-sales-channels-fallback.tsx
delete mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/index.ts
diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json
index 67a11df248..35e739c5b8 100644
--- a/packages/admin-next/dashboard/public/locales/en-US/translation.json
+++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json
@@ -1168,7 +1168,6 @@
},
"apiKeyManagement": {
"domain": {
- "apiKeys": "API Keys",
"publishable": "Publishable API Keys",
"secret": "Secret API Keys"
},
@@ -1186,25 +1185,40 @@
"createSecretHeader": "Create Secret API Key",
"createSecretHint": "Create a new secret API key to access the Medusa API.",
"secretKeyCreatedHeader": "Secret Key Created",
- "secretKeyCreatedHint": "Your new secret key has been generated. Copy and securely store it now. This is the only time it will be displayed."
+ "secretKeyCreatedHint": "Your new secret key has been generated. Copy and securely store it now. This is the only time it will be displayed.",
+ "copySecretTokenSuccess": "Secret key was copied to clipboard.",
+ "copySecretTokenFailure": "Failed to copy secret key to clipboard.",
+ "successToast": "API key was successfully created."
},
"edit": {
- "header": "Edit API Key"
+ "header": "Edit API Key",
+ "successToast": "API key {{title}} was successfully updated."
},
- "tabs": {
- "secret": "Secret",
- "publishable": "Publishable"
+ "salesChannels": {
+ "successToast_one": "{{count}} sales channel was successfully added to the API key.",
+ "successToast_other": "{{count}} sales channels were successfully added to the API key.",
+ "alreadyAddedTooltip": "The sales channel has already been added to the API key."
},
- "warnings": {
- "delete": "You are about to delete the API key {{title}}. This action cannot be undone.",
- "revoke": "You are about to revoke the API key {{title}}. This action cannot be undone, and the key cannot be used in future requests.",
- "removeSalesChannel": "You are about to remove the sales channel {{name}} from the API key. This action cannot be undone.",
- "removeSalesChannels_one": "You are about to remove {{count}} sales channel from the API key. This action cannot be undone.",
- "removeSalesChannels_other": "You are about to remove {{count}} sales channels from the API key. This action cannot be undone.",
- "salesChannelsSecretKey": "Sales channels cannot be added to a secret API key."
+ "delete": {
+ "warning": "You are about to delete the API key {{title}}. This action cannot be undone.",
+ "successToast": "API key {{title}} was successfully deleted."
+ },
+ "revoke": {
+ "warning": "You are about to revoke the API key {{title}}. This action cannot be undone.",
+ "successToast": "API key {{title}} was successfully revoked."
+ },
+ "removeSalesChannel": {
+ "warning": "You are about to remove the sales channel {{name}} from the API key. This action cannot be undone.",
+ "warningBatch_one": "You are about to remove {{count}} sales channel from the API key. This action cannot be undone.",
+ "warningBatch_other": "You are about to remove {{count}} sales channels from the API key. This action cannot be undone.",
+ "successToast": "Sales channel was successfully removed from the API key.",
+ "successToastBatch_one": "{{count}} sales channel was successfully removed from the API key.",
+ "successToastBatch_other": "{{count}} sales channels were successfully removed from the API key."
},
"actions": {
- "revoke": "Revoke"
+ "revoke": "Revoke API key",
+ "copy": "Copy API key",
+ "copySuccessToast": "API key was copied to clipboard."
},
"table": {
"lastUsedAtHeader": "Last Used At",
diff --git a/packages/admin-next/dashboard/src/components/common/skeleton/skeleton.tsx b/packages/admin-next/dashboard/src/components/common/skeleton/skeleton.tsx
index 0b2b2c8c17..04eef58ec1 100644
--- a/packages/admin-next/dashboard/src/components/common/skeleton/skeleton.tsx
+++ b/packages/admin-next/dashboard/src/components/common/skeleton/skeleton.tsx
@@ -136,6 +136,82 @@ export const GeneralSectionSkeleton = ({
)
}
+type TableSkeletonProps = {
+ rowCount?: number
+ search?: boolean
+ filters?: boolean
+ orderBy?: boolean
+ pagination?: boolean
+ layout?: "fit" | "fill"
+}
+
+export const TableSkeleton = ({
+ rowCount = 10,
+ search = true,
+ filters = true,
+ orderBy = true,
+ pagination = true,
+ layout = "fit",
+}: TableSkeletonProps) => {
+ // Row count + header row
+ const totalRowCount = rowCount + 1
+
+ const rows = Array.from({ length: totalRowCount }, (_, i) => i)
+ const hasToolbar = search || filters || orderBy
+
+ return (
+
+ {hasToolbar && (
+
+ {filters &&
}
+ {(search || orderBy) && (
+
+ {search && }
+ {orderBy && }
+
+ )}
+
+ )}
+
+ {rows.map((row) => (
+
+ ))}
+
+ {pagination && (
+
+ )}
+
+ )
+}
+
+export const TableSectionSkeleton = (props: TableSkeletonProps) => {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
export const JsonViewSectionSkeleton = () => {
return (
diff --git a/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx
index 65ff538085..8fe858e5e7 100644
--- a/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx
+++ b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx
@@ -68,8 +68,12 @@ const useDeveloperRoutes = (): NavItemProps[] => {
return useMemo(
() => [
{
- label: t("apiKeyManagement.domain.apiKeys"),
- to: "/settings/api-key-management",
+ label: t("apiKeyManagement.domain.publishable"),
+ to: "/settings/publishable-api-keys",
+ },
+ {
+ label: t("apiKeyManagement.domain.secret"),
+ to: "/settings/secret-api-keys",
},
{
label: t("workflowExecutions.domain"),
diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx
deleted file mode 100644
index 5bbc986fbb..0000000000
--- a/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import { Table, clx } from "@medusajs/ui"
-import { ColumnDef } from "@tanstack/react-table"
-import { Skeleton } from "../../../common/skeleton"
-
-type DataTableSkeletonProps = {
- columns: ColumnDef[]
- rowCount: number
- searchable: boolean
- orderBy: boolean
- filterable: boolean
- pagination: boolean
- layout?: "fit" | "fill"
-}
-
-export const DataTableSkeleton = ({
- columns,
- rowCount,
- filterable,
- searchable,
- orderBy,
- pagination,
- layout = "fit",
-}: DataTableSkeletonProps) => {
- const rows = Array.from({ length: rowCount }, (_, i) => i)
-
- const hasToolbar = filterable || searchable || orderBy
- const hasSearchOrOrder = searchable || orderBy
-
- const hasSelect = columns.find((c) => c.id === "select")
- const hasActions = columns.find((c) => c.id === "actions")
- const colCount = columns.length - (hasSelect ? 1 : 0) - (hasActions ? 1 : 0)
- const colWidth = 100 / colCount
-
- return (
-
- {hasToolbar && (
-
- {filterable &&
}
- {hasSearchOrOrder && (
-
- {searchable && }
- {orderBy && }
-
- )}
-
- )}
-
-
-
-
-
- {columns.map((col, i) => {
- const isSelectHeader = col.id === "select"
- const isActionsHeader = col.id === "actions"
-
- const isSpecialHeader = isSelectHeader || isActionsHeader
-
- return (
-
- {isActionsHeader ? null : (
-
- )}
-
- )
- })}
-
-
-
- {rows.map((_, j) => (
-
- {columns.map((col, k) => {
- const isSpecialCell =
- col.id === "select" || col.id === "actions"
-
- return (
-
-
-
- )
- })}
-
- ))}
-
-
-
- {pagination && (
-
- )}
-
-
- )
-}
diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/index.ts
deleted file mode 100644
index fbed89a8c7..0000000000
--- a/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./data-table-skeleton"
diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx
index 47163264b2..bdf8393ea6 100644
--- a/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx
+++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx
@@ -1,9 +1,9 @@
import { clx } from "@medusajs/ui"
import { memo } from "react"
import { NoRecords } from "../../common/empty-table-content"
+import { TableSkeleton } from "../../common/skeleton"
import { DataTableQuery, DataTableQueryProps } from "./data-table-query"
import { DataTableRoot, DataTableRootProps } from "./data-table-root"
-import { DataTableSkeleton } from "./data-table-skeleton"
interface DataTableProps
extends Omit, "noResults">,
@@ -35,12 +35,11 @@ export const DataTable = ({
}: DataTableProps) => {
if (isLoading) {
return (
-
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
index b7f8dece69..c63e856544 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
@@ -745,10 +745,10 @@ export const v2Routes: RouteObject[] = [
],
},
{
- path: "api-key-management",
+ path: "publishable-api-keys",
element: ,
handle: {
- crumb: () => "API Key Management",
+ crumb: () => "Publishable API Keys",
},
children: [
{
@@ -771,22 +771,6 @@ export const v2Routes: RouteObject[] = [
},
],
},
- {
- path: "secret",
- lazy: () =>
- import(
- "../../v2-routes/api-key-management/api-key-management-list"
- ),
- children: [
- {
- path: "create",
- lazy: () =>
- import(
- "../../v2-routes/api-key-management/api-key-management-create"
- ),
- },
- ],
- },
],
},
{
@@ -819,6 +803,58 @@ export const v2Routes: RouteObject[] = [
},
],
},
+ {
+ path: "secret-api-keys",
+ element: ,
+ handle: {
+ crumb: () => "Secret API Keys",
+ },
+ children: [
+ {
+ path: "",
+ element: ,
+ children: [
+ {
+ path: "",
+ lazy: () =>
+ import(
+ "../../v2-routes/api-key-management/api-key-management-list"
+ ),
+ children: [
+ {
+ path: "create",
+ lazy: () =>
+ import(
+ "../../v2-routes/api-key-management/api-key-management-create"
+ ),
+ },
+ ],
+ },
+ ],
+ },
+ {
+ path: ":id",
+ lazy: () =>
+ import(
+ "../../v2-routes/api-key-management/api-key-management-detail"
+ ),
+ handle: {
+ crumb: (data: AdminApiKeyResponse) => {
+ return data.api_key.title
+ },
+ },
+ children: [
+ {
+ path: "edit",
+ lazy: () =>
+ import(
+ "../../v2-routes/api-key-management/api-key-management-edit"
+ ),
+ },
+ ],
+ },
+ ],
+ },
{
path: "taxes",
element: ,
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/api-key-management-create.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/api-key-management-create.tsx
index dfea999afb..7763930475 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/api-key-management-create.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/api-key-management-create.tsx
@@ -1,7 +1,7 @@
import { useLocation } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { getApiKeyTypeFromPathname } from "../common/utils"
-import { CreatePublishableApiKeyForm } from "./components/create-publishable-api-key-form"
+import { ApiKeyCreateForm } from "./components/api-key-create-form"
export const ApiKeyManagementCreate = () => {
const { pathname } = useLocation()
@@ -9,7 +9,7 @@ export const ApiKeyManagementCreate = () => {
return (
-
+
)
}
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/api-key-create-form/api-key-create-form.tsx
similarity index 60%
rename from packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx
rename to packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/api-key-create-form/api-key-create-form.tsx
index c3a9a74d0a..bf8ecbf03a 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/api-key-create-form/api-key-create-form.tsx
@@ -1,9 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod"
-import { Button, Copy, Heading, Input, Prompt, Text } from "@medusajs/ui"
+import { Button, Heading, Input, Prompt, Text, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
+import { Eye, EyeSlash } from "@medusajs/icons"
import { AdminApiKeyResponse } from "@medusajs/types"
import { Fragment, useState } from "react"
import { Form } from "../../../../../components/common/form"
@@ -14,29 +15,40 @@ import {
import { useCreateApiKey } from "../../../../../hooks/api/api-keys"
import { ApiKeyType } from "../../../common/constants"
-const CreatePublishableApiKeySchema = zod.object({
+const ApiKeyCreateSchema = zod.object({
title: zod.string().min(1),
})
-type CreatePublishableApiKeyFormProps = {
+type ApiKeyCreateFormProps = {
keyType: ApiKeyType
}
-export const CreatePublishableApiKeyForm = ({
- keyType,
-}: CreatePublishableApiKeyFormProps) => {
+function getRedactedKey(key?: string) {
+ if (!key) {
+ return ""
+ }
+
+ // Replace all characters except the first four and last two with bullets
+ const firstThree = key.slice(0, 4)
+ const lastTwo = key.slice(-2)
+
+ return `${firstThree}${"•".repeat(key.length - 6)}${lastTwo}`
+}
+
+export const ApiKeyCreateForm = ({ keyType }: ApiKeyCreateFormProps) => {
const [createdKey, setCreatedKey] = useState<
AdminApiKeyResponse["api_key"] | null
>(null)
+ const [showRedactedKey, setShowRedactedKey] = useState(true)
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
- const form = useForm>({
+ const form = useForm>({
defaultValues: {
title: "",
},
- resolver: zodResolver(CreatePublishableApiKeySchema),
+ resolver: zodResolver(ApiKeyCreateSchema),
})
const { mutateAsync, isPending } = useCreateApiKey()
@@ -47,25 +59,51 @@ export const CreatePublishableApiKeyForm = ({
{ title: values.title, type: keyType },
{
onSuccess: ({ api_key }) => {
+ toast.success(t("general.success"), {
+ description: t("apiKeyManagement.create.successToast"),
+ dismissLabel: t("general.close"),
+ })
+
switch (keyType) {
case ApiKeyType.PUBLISHABLE:
- handleSuccess(`/settings/api-key-management/${api_key.id}`)
+ handleSuccess(`/settings/publishable-api-keys/${api_key.id}`)
break
case ApiKeyType.SECRET:
setCreatedKey(api_key)
break
}
},
+ onError: (err) => {
+ toast.error(t("general.error"), {
+ description: err.message,
+ dismissLabel: t("general.close"),
+ })
+ },
}
)
})
+ const handleCopyToken = () => {
+ if (!createdKey) {
+ toast.error(t("general.error"), {
+ dismissLabel: t("general.close"),
+ description: t("apiKeyManagement.create.copySecretTokenFailure"),
+ })
+ }
+
+ navigator.clipboard.writeText(createdKey?.token ?? "")
+ toast.success(t("general.success"), {
+ description: t("apiKeyManagement.create.copySecretTokenSuccess"),
+ dismissLabel: t("general.close"),
+ })
+ }
+
const handleGoToSecretKey = () => {
if (!createdKey) {
return
}
- handleSuccess(`/settings/api-key-management/${createdKey.id}`)
+ handleSuccess(`/settings/secret-api-keys/${createdKey.id}`)
}
return (
@@ -125,7 +163,7 @@ export const CreatePublishableApiKeyForm = ({
-
+
{t("apiKeyManagement.create.secretKeyCreatedHeader")}
@@ -134,18 +172,34 @@ export const CreatePublishableApiKeyForm = ({
{t("apiKeyManagement.create.secretKeyCreatedHint")}
-
-
-
- {createdKey?.token}
-
-
+
+
+
+
+ {showRedactedKey
+ ? getRedactedKey(createdKey?.token)
+ : createdKey?.token}
+
+
+
setShowRedactedKey(!showRedactedKey)}
+ >
+ {showRedactedKey ? : }
+
+
+ {t("apiKeyManagement.actions.copy")}
+
-
+
{t("actions.continue")}
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/api-key-create-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/api-key-create-form/index.ts
new file mode 100644
index 0000000000..3b118afb7a
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/api-key-create-form/index.ts
@@ -0,0 +1 @@
+export * from "./api-key-create-form"
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/index.ts
deleted file mode 100644
index e5aa57664b..0000000000
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./create-publishable-api-key-form"
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx
index 168d84f478..dde10d82ce 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx
@@ -1,5 +1,11 @@
import { Outlet, useLoaderData, useParams } from "react-router-dom"
+
import { JsonViewSection } from "../../../components/common/json-view-section"
+import {
+ GeneralSectionSkeleton,
+ JsonViewSectionSkeleton,
+ TableSectionSkeleton,
+} from "../../../components/common/skeleton"
import { useApiKey } from "../../../hooks/api/api-keys"
import { ApiKeyType } from "../common/constants"
import { ApiKeyGeneralSection } from "./components/api-key-general-section"
@@ -18,7 +24,13 @@ export const ApiKeyManagementDetail = () => {
})
if (isLoading || !api_key) {
- return Loading...
+ return (
+
+ )
}
const isPublishable = api_key?.type === ApiKeyType.PUBLISHABLE
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx
index 95ca2e061a..2808dc5a5b 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx
@@ -1,12 +1,13 @@
import { PencilSquare, Trash, XCircle } from "@medusajs/icons"
import { ApiKeyDTO } from "@medusajs/types"
import {
+ Badge,
Container,
Copy,
Heading,
StatusBadge,
Text,
- clx,
+ toast,
usePrompt,
} from "@medusajs/ui"
import { useTranslation } from "react-i18next"
@@ -20,7 +21,11 @@ import {
} from "../../../../../hooks/api/api-keys"
import { useUser } from "../../../../../hooks/api/users"
import { useDate } from "../../../../../hooks/use-date"
-import { getApiKeyStatusProps, getApiKeyTypeProps } from "../../../common/utils"
+import {
+ getApiKeyStatusProps,
+ getApiKeyTypeProps,
+ prettifyRedactedToken,
+} from "../../../common/utils"
type ApiKeyGeneralSectionProps = {
apiKey: ApiKeyDTO
@@ -38,7 +43,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
const handleDelete = async () => {
const res = await prompt({
title: t("general.areYouSure"),
- description: t("apiKeyManagement.warnings.delete", {
+ description: t("apiKeyManagement.delete.warning", {
title: apiKey.title,
}),
confirmText: t("actions.delete"),
@@ -51,15 +56,27 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
await deleteAsync(undefined, {
onSuccess: () => {
+ toast.success(t("general.success"), {
+ description: t("apiKeyManagement.delete.successToast", {
+ title: apiKey.title,
+ }),
+ dismissLabel: t("general.close"),
+ })
navigate("..", { replace: true })
},
+ onError: (err) => {
+ toast.error(t("general.error"), {
+ description: err.message,
+ dismissLabel: t("general.close"),
+ })
+ },
})
}
const handleRevoke = async () => {
const res = await prompt({
title: t("general.areYouSure"),
- description: t("apiKeyManagement.warnings.revoke", {
+ description: t("apiKeyManagement.revoke.warning", {
title: apiKey.title,
}),
confirmText: t("apiKeyManagement.actions.revoke"),
@@ -70,7 +87,22 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
return
}
- await revokeAsync()
+ await revokeAsync(undefined, {
+ onSuccess: () => {
+ toast.success(t("general.success"), {
+ description: t("apiKeyManagement.revoke.successToast", {
+ title: apiKey.title,
+ }),
+ dismissLabel: t("general.close"),
+ })
+ },
+ onError: (err) => {
+ toast.error(t("general.error"), {
+ description: err.message,
+ dismissLabel: t("general.close"),
+ })
+ },
+ })
}
const dangerousActions = [
@@ -109,7 +141,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
{
label: t("actions.edit"),
icon: ,
- to: `/settings/api-key-management/${apiKey.id}/edit`,
+ to: "edit",
},
],
},
@@ -124,26 +156,19 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
{t("fields.key")}
-
-
- {apiKey.redacted}
-
- {apiKey.type !== "secret" && (
-
- )}
-
+ {apiKey.type === "secret" ? (
+
+ {prettifyRedactedToken(apiKey.redacted)}
+
+ ) : (
+
+
+
+ {prettifyRedactedToken(apiKey.redacted)}
+
+
+
+ )}
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx
index aaa014581c..6c49f01c22 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx
@@ -1,6 +1,6 @@
import { PencilSquare, Plus, Trash } from "@medusajs/icons"
import { AdminApiKeyResponse, AdminSalesChannelResponse } from "@medusajs/types"
-import { Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
+import { Checkbox, Container, Heading, toast, usePrompt } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import { useMemo, useState } from "react"
@@ -65,7 +65,7 @@ export const ApiKeySalesChannelSection = ({
const res = await prompt({
title: t("general.areYouSure"),
- description: t("apiKeyManagement.warnings.removeSalesChannels", {
+ description: t("apiKeyManagement.removeSalesChannel.warningBatch", {
count: keys.length,
}),
confirmText: t("actions.continue"),
@@ -82,8 +82,23 @@ export const ApiKeySalesChannelSection = ({
},
{
onSuccess: () => {
+ toast.success(t("general.success"), {
+ description: t(
+ "apiKeyManagement.removeSalesChannel.successToastBatch",
+ {
+ count: keys.length,
+ }
+ ),
+ dismissLabel: t("general.close"),
+ })
setRowSelection({})
},
+ onError: (err) => {
+ toast.error(t("general.error"), {
+ description: err.message,
+ dismissLabel: t("general.close"),
+ })
+ },
}
)
}
@@ -145,7 +160,7 @@ const SalesChannelActions = ({
const handleDelete = async () => {
const res = await prompt({
title: t("general.areYouSure"),
- description: t("apiKeyManagement.warnings.removeSalesChannel", {
+ description: t("apiKeyManagement.removeSalesChannel.warning", {
name: salesChannel.name,
}),
confirmText: t("actions.delete"),
@@ -156,9 +171,27 @@ const SalesChannelActions = ({
return
}
- await mutateAsync({
- sales_channel_ids: [salesChannel.id],
- })
+ await mutateAsync(
+ {
+ sales_channel_ids: [salesChannel.id],
+ },
+ {
+ onSuccess: () => {
+ toast.success(t("general.success"), {
+ description: t("apiKeyManagement.removeSalesChannel.successToast", {
+ count: 1,
+ }),
+ dismissLabel: t("general.close"),
+ })
+ },
+ onError: (err) => {
+ toast.error(t("general.error"), {
+ description: err.message,
+ dismissLabel: t("general.close"),
+ })
+ },
+ }
+ )
}
return (
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-edit/components/edit-api-key-form/edit-api-key-form.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-edit/components/edit-api-key-form/edit-api-key-form.tsx
index 35dc124e58..0cf3264c50 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-edit/components/edit-api-key-form/edit-api-key-form.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-edit/components/edit-api-key-form/edit-api-key-form.tsx
@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"
-import { Button, Input } from "@medusajs/ui"
+import { Button, Input, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
@@ -35,9 +35,21 @@ export const EditApiKeyForm = ({ apiKey }: EditApiKeyFormProps) => {
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, {
- onSuccess: () => {
+ onSuccess: ({ api_key }) => {
+ toast.success(t("general.success"), {
+ description: t("apiKeyManagement.edit.successToast", {
+ title: api_key.title,
+ }),
+ dismissLabel: t("general.close"),
+ })
handleSuccess()
},
+ onError: (err) => {
+ toast.error(t("general.error"), {
+ description: err.message,
+ dismissLabel: t("general.close"),
+ })
+ },
})
})
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/api-key-management-list.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/api-key-management-list.tsx
index b2d84b23e2..5e074f4299 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/api-key-management-list.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/api-key-management-list.tsx
@@ -1,34 +1,14 @@
-import { useTranslation } from "react-i18next"
-import { Link, Outlet, useLocation } from "react-router-dom"
-import { ApiKeyType } from "../common/constants"
+import { Outlet, useLocation } from "react-router-dom"
+import { getApiKeyTypeFromPathname } from "../common/utils"
import { ApiKeyManagementListTable } from "./components/api-key-management-list-table"
export const ApiKeyManagementList = () => {
const { pathname } = useLocation()
- const { t } = useTranslation()
- const keyType = pathname.includes("secret")
- ? ApiKeyType.SECRET
- : ApiKeyType.PUBLISHABLE
+ const keyType = getApiKeyTypeFromPathname(pathname)
return (
-
-
- {t("apiKeyManagement.tabs.publishable")}
-
-
- {t("apiKeyManagement.tabs.secret")}
-
-
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx
index 2782c87da1..64aabcd634 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx
@@ -70,7 +70,7 @@ export const ApiKeyManagementListTable = ({
count={count}
pageSize={PAGE_SIZE}
orderBy={["title", "created_at", "updated_at", "revoked_at"]}
- navigateTo={(row) => `/settings/api-key-management/${row.id}`}
+ navigateTo={(row) => row.id}
pagination
search
queryObject={raw}
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-row-actions.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-row-actions.tsx
index a1de37035d..6b84fceb45 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-row-actions.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-row-actions.tsx
@@ -1,6 +1,6 @@
-import { PencilSquare, Trash, XCircle } from "@medusajs/icons"
+import { PencilSquare, SquareTwoStack, Trash, XCircle } from "@medusajs/icons"
import { AdminApiKeyResponse } from "@medusajs/types"
-import { usePrompt } from "@medusajs/ui"
+import { toast, usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import {
@@ -22,7 +22,7 @@ export const ApiKeyRowActions = ({
const handleDelete = async () => {
const res = await prompt({
title: t("general.areYouSure"),
- description: t("apiKeyManagement.warnings.delete", {
+ description: t("apiKeyManagement.delete.warning", {
title: apiKey.title,
}),
confirmText: t("actions.delete"),
@@ -33,13 +33,28 @@ export const ApiKeyRowActions = ({
return
}
- await deleteAsync()
+ await deleteAsync(undefined, {
+ onSuccess: () => {
+ toast.success(t("general.success"), {
+ description: t("apiKeyManagement.delete.successToast", {
+ title: apiKey.title,
+ }),
+ dismissLabel: t("general.close"),
+ })
+ },
+ onError: (err) => {
+ toast.error(t("general.error"), {
+ description: err.message,
+ dismissLabel: t("general.close"),
+ })
+ },
+ })
}
const handleRevoke = async () => {
const res = await prompt({
title: t("general.areYouSure"),
- description: t("apiKeyManagement.warnings.revoke", {
+ description: t("apiKeyManagement.revoke.warning", {
title: apiKey.title,
}),
confirmText: t("apiKeyManagement.actions.revoke"),
@@ -50,7 +65,30 @@ export const ApiKeyRowActions = ({
return
}
- await revokeAsync(undefined)
+ await revokeAsync(undefined, {
+ onSuccess: () => {
+ toast.success(t("general.success"), {
+ description: t("apiKeyManagement.revoke.successToast", {
+ title: apiKey.title,
+ }),
+ dismissLabel: t("general.close"),
+ })
+ },
+ onError: (err) => {
+ toast.error(t("general.error"), {
+ description: err.message,
+ dismissLabel: t("general.close"),
+ })
+ },
+ })
+ }
+
+ const handleCopyToken = () => {
+ navigator.clipboard.writeText(apiKey.token)
+ toast.success(t("general.success"), {
+ description: t("apiKeyManagement.actions.copySuccessToast"),
+ dismissLabel: t("general.close"),
+ })
}
return (
@@ -61,8 +99,17 @@ export const ApiKeyRowActions = ({
{
icon: ,
label: t("actions.edit"),
- to: `/settings/api-key-management/${apiKey.id}`,
+ to: `${apiKey.id}/edit`,
},
+ ...(apiKey.type !== "secret"
+ ? [
+ {
+ label: t("apiKeyManagement.actions.copy"),
+ onClick: handleCopyToken,
+ icon: ,
+ },
+ ]
+ : []),
],
},
{
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/use-api-key-management-table-columns.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/use-api-key-management-table-columns.tsx
index 0bcef93ed7..d0f2a833c5 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/use-api-key-management-table-columns.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/use-api-key-management-table-columns.tsx
@@ -1,5 +1,5 @@
import { AdminApiKeyResponse } from "@medusajs/types"
-import { Copy, Text, clx } from "@medusajs/ui"
+import { Badge, Copy, Text } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { MouseEvent, useMemo } from "react"
import { useTranslation } from "react-i18next"
@@ -7,7 +7,11 @@ import { useTranslation } from "react-i18next"
import { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
import { TextCell } from "../../../../../components/table/table-cells/common/text-cell"
-import { getApiKeyStatusProps, getApiKeyTypeProps } from "../../../common/utils"
+import {
+ getApiKeyStatusProps,
+ getApiKeyTypeProps,
+ prettifyRedactedToken,
+} from "../../../common/utils"
import { ApiKeyRowActions } from "./api-key-row-actions"
const columnHelper = createColumnHelper()
@@ -31,34 +35,24 @@ export const useApiKeyManagementTableColumns = () => {
const token = getValue()
const isSecret = row.original.type === "secret"
- const clickHandler = !isSecret
- ? (e: MouseEvent) => e.stopPropagation()
- : undefined
+ if (isSecret) {
+ return {prettifyRedactedToken(token)}
+ }
+
+ const clickHandler = (e: MouseEvent) => e.stopPropagation()
return (
-
-
- {token}
-
- {!isSecret && (
-
- )}
-
+
+
+
+ {prettifyRedactedToken(token)}
+
+
+
)
},
}),
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/api-key-management-sales-channels.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/api-key-management-sales-channels.tsx
index baf016325b..12c5d0bad3 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/api-key-management-sales-channels.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/api-key-management-sales-channels.tsx
@@ -3,8 +3,6 @@ import { useParams } from "react-router-dom"
import { AdminApiKeyResponse, AdminSalesChannelResponse } from "@medusajs/types"
import { RouteFocusModal } from "../../../components/route-modal"
import { useApiKey } from "../../../hooks/api/api-keys"
-import { ApiKeyType } from "../common/constants"
-import { ApiKeySalesChannelsFallback } from "./components/api-key-sales-channels-fallback"
import { ApiKeySalesChannelsForm } from "./components/api-key-sales-channels-form"
export const ApiKeyManagementAddSalesChannels = () => {
@@ -12,14 +10,13 @@ export const ApiKeyManagementAddSalesChannels = () => {
const { api_key, isLoading, isError, error } = useApiKey(id!)
- const isSecret = api_key?.type === ApiKeyType.SECRET
const preSelected = (
api_key as AdminApiKeyResponse["api_key"] & {
sales_channels: AdminSalesChannelResponse["sales_channel"][] | null
}
)?.sales_channels?.map((sc) => sc.id)
- const ready = !isLoading && api_key && !isSecret
+ const ready = !isLoading && api_key
if (isError) {
throw error
@@ -27,7 +24,6 @@ export const ApiKeyManagementAddSalesChannels = () => {
return (
- {isSecret && }
{ready && (
)}
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/api-key-sales-channels-fallback.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/api-key-sales-channels-fallback.tsx
deleted file mode 100644
index 7d13693cad..0000000000
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/api-key-sales-channels-fallback.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Alert, Button } from "@medusajs/ui"
-import { useTranslation } from "react-i18next"
-import { RouteFocusModal } from "../../../../../components/route-modal"
-
-export const ApiKeySalesChannelsFallback = () => {
- const { t } = useTranslation()
-
- return (
-
-
-
-
-
- {t("actions.cancel")}
-
-
-
-
-
-
- {t("apiKeyManagement.warnings.salesChannelsSecretKey")}
-
-
-
- )
-}
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/index.ts b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/index.ts
deleted file mode 100644
index b2f5c51dfb..0000000000
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./api-key-sales-channels-fallback"
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-form/api-key-sales-channels-form.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-form/api-key-sales-channels-form.tsx
index b35ddc8749..95ebaacfba 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-form/api-key-sales-channels-form.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-form/api-key-sales-channels-form.tsx
@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { AdminSalesChannelResponse } from "@medusajs/types"
-import { Button, Checkbox, Hint, Tooltip } from "@medusajs/ui"
+import { Button, Checkbox, Hint, Tooltip, toast } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import {
OnChangeFn,
@@ -102,8 +102,21 @@ export const ApiKeySalesChannelsForm = ({
},
{
onSuccess: () => {
+ toast.success(t("general.success"), {
+ description: t("apiKeyManagement.salesChannels.successToast", {
+ count: values.sales_channel_ids.length,
+ }),
+ dismissLabel: t("general.close"),
+ })
+
handleSuccess()
},
+ onError: (err) => {
+ toast.error(t("general.error"), {
+ description: err.message,
+ dismissLabel: t("general.close"),
+ })
+ },
}
)
})
@@ -142,6 +155,7 @@ export const ApiKeySalesChannelsForm = ({
isLoading={isLoading}
queryObject={raw}
orderBy={["name", "created_at", "updated_at"]}
+ layout="fill"
/>
@@ -191,7 +205,12 @@ const useColumns = () => {
if (isPreSelected) {
return (
-
+
{Component}
)
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/common/utils.ts b/packages/admin-next/dashboard/src/v2-routes/api-key-management/common/utils.ts
index defba1ff23..b4daa868df 100644
--- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/common/utils.ts
+++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/common/utils.ts
@@ -3,7 +3,7 @@ import { TFunction } from "i18next"
import { ApiKeyType } from "./constants"
export function getApiKeyTypeFromPathname(pathname: string) {
- const isSecretKey = pathname.startsWith("/settings/api-key-management/secret")
+ const isSecretKey = pathname.startsWith("/settings/secret-api-keys")
switch (isSecretKey) {
case true:
@@ -46,3 +46,17 @@ export function getApiKeyTypeProps(
label: t("apiKeyManagement.type.secret"),
}
}
+
+/**
+ * Returns a prettified version of the token with redacted symbols replaced with a bullet point.
+ * @param token - The token to prettify.
+ * @returns The prettified token, with redacted symbols replaced with a bullet point.
+ *
+ * @example
+ * ```ts
+ * const token = "sk_a***yx"
+ * const prettifiedToken = replaceRedactedSymbol(token) // "sk_a•••yx"
+ */
+export const prettifyRedactedToken = (token: string) => {
+ return token.replace("***", `•••`)
+}