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} + +
+
+
- + {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("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("***", `•••`) +}