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.
This commit is contained in:
1 parent
38c971f111
commit
ef29981a54
24 files changed
+470
-321
No files matched your search
@@ -1168,7 +1168,6 @@
|
|||||||
},
|
},
|
||||||
"apiKeyManagement": {
|
"apiKeyManagement": {
|
||||||
"domain": {
|
"domain": {
|
||||||
"apiKeys": "API Keys",
|
|
||||||
"publishable": "Publishable API Keys",
|
"publishable": "Publishable API Keys",
|
||||||
"secret": "Secret API Keys"
|
"secret": "Secret API Keys"
|
||||||
},
|
},
|
||||||
@@ -1186,25 +1185,40 @@
|
|||||||
"createSecretHeader": "Create Secret API Key",
|
"createSecretHeader": "Create Secret API Key",
|
||||||
"createSecretHint": "Create a new secret API key to access the Medusa API.",
|
"createSecretHint": "Create a new secret API key to access the Medusa API.",
|
||||||
"secretKeyCreatedHeader": "Secret Key Created",
|
"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": {
|
"edit": {
|
||||||
"header": "Edit API Key"
|
"header": "Edit API Key",
|
||||||
|
"successToast": "API key {{title}} was successfully updated."
|
||||||
},
|
},
|
||||||
"tabs": {
|
"salesChannels": {
|
||||||
"secret": "Secret",
|
"successToast_one": "{{count}} sales channel was successfully added to the API key.",
|
||||||
"publishable": "Publishable"
|
"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": {
|
||||||
"delete": "You are about to delete the API key {{title}}. This action cannot be undone.",
|
"warning": "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.",
|
"successToast": "API key {{title}} was successfully deleted."
|
||||||
"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.",
|
"revoke": {
|
||||||
"removeSalesChannels_other": "You are about to remove {{count}} sales channels from the API key. This action cannot be undone.",
|
"warning": "You are about to revoke the API key {{title}}. This action cannot be undone.",
|
||||||
"salesChannelsSecretKey": "Sales channels cannot be added to a secret API key."
|
"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": {
|
"actions": {
|
||||||
"revoke": "Revoke"
|
"revoke": "Revoke API key",
|
||||||
|
"copy": "Copy API key",
|
||||||
|
"copySuccessToast": "API key was copied to clipboard."
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"lastUsedAtHeader": "Last Used At",
|
"lastUsedAtHeader": "Last Used At",
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className={clx({
|
||||||
|
"flex h-full flex-col overflow-hidden": layout === "fill",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{hasToolbar && (
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
{filters && <Skeleton className="h-7 w-full max-w-[135px]" />}
|
||||||
|
{(search || orderBy) && (
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
{search && <Skeleton className="h-7 w-[160px]" />}
|
||||||
|
{orderBy && <Skeleton className="h-7 w-7" />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col divide-y border-y">
|
||||||
|
{rows.map((row) => (
|
||||||
|
<Skeleton key={row} className="h-10 w-full rounded-none" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{pagination && (
|
||||||
|
<div
|
||||||
|
className={clx("flex items-center justify-between p-4", {
|
||||||
|
"border-t": layout === "fill",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Skeleton className="h-7 w-[138px]" />
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Skeleton className="h-7 w-24" />
|
||||||
|
<Skeleton className="h-7 w-11" />
|
||||||
|
<Skeleton className="h-7 w-11" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableSectionSkeleton = (props: TableSkeletonProps) => {
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0" aria-hidden>
|
||||||
|
<div className="flex items-center justify-between px-6 py-4" aria-hidden>
|
||||||
|
<HeadingSkeleton level="h2" characters={16} />
|
||||||
|
<IconButtonSkeleton />
|
||||||
|
</div>
|
||||||
|
<TableSkeleton {...props} />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const JsonViewSectionSkeleton = () => {
|
export const JsonViewSectionSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
<Container className="divide-y p-0" aria-hidden>
|
<Container className="divide-y p-0" aria-hidden>
|
||||||
|
|||||||
+6
-2
@@ -68,8 +68,12 @@ const useDeveloperRoutes = (): NavItemProps[] => {
|
|||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
label: t("apiKeyManagement.domain.apiKeys"),
|
label: t("apiKeyManagement.domain.publishable"),
|
||||||
to: "/settings/api-key-management",
|
to: "/settings/publishable-api-keys",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("apiKeyManagement.domain.secret"),
|
||||||
|
to: "/settings/secret-api-keys",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("workflowExecutions.domain"),
|
label: t("workflowExecutions.domain"),
|
||||||
|
|||||||
-138
@@ -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<any, any>[]
|
|
||||||
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 (
|
|
||||||
<div
|
|
||||||
className={clx({
|
|
||||||
"flex h-full flex-col overflow-hidden": layout === "fill",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{hasToolbar && (
|
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
|
||||||
{filterable && <Skeleton className="h-7 w-full max-w-[160px]" />}
|
|
||||||
{hasSearchOrOrder && (
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
{searchable && <Skeleton className="h-7 w-[160px]" />}
|
|
||||||
{orderBy && <Skeleton className="h-7 w-7" />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={clx("flex w-full flex-col overflow-hidden", {
|
|
||||||
"flex flex-1 flex-col": layout === "fill",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clx("w-full", {
|
|
||||||
"min-h-0 flex-grow overflow-hidden": layout === "fill",
|
|
||||||
"overflow-x-auto": layout === "fit",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Table>
|
|
||||||
<Table.Header>
|
|
||||||
<Table.Row
|
|
||||||
className={clx({
|
|
||||||
"border-b-0 [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap":
|
|
||||||
hasActions,
|
|
||||||
"[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap":
|
|
||||||
hasSelect,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{columns.map((col, i) => {
|
|
||||||
const isSelectHeader = col.id === "select"
|
|
||||||
const isActionsHeader = col.id === "actions"
|
|
||||||
|
|
||||||
const isSpecialHeader = isSelectHeader || isActionsHeader
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table.HeaderCell
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
width: !isSpecialHeader ? `${colWidth}%` : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isActionsHeader ? null : (
|
|
||||||
<Skeleton
|
|
||||||
className={clx("h-7", {
|
|
||||||
"w-7": isSelectHeader,
|
|
||||||
"w-full": !isSelectHeader,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Table.HeaderCell>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body>
|
|
||||||
{rows.map((_, j) => (
|
|
||||||
<Table.Row key={j}>
|
|
||||||
{columns.map((col, k) => {
|
|
||||||
const isSpecialCell =
|
|
||||||
col.id === "select" || col.id === "actions"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table.Cell key={k}>
|
|
||||||
<Skeleton
|
|
||||||
className={clx("h-7", {
|
|
||||||
"w-7": isSpecialCell,
|
|
||||||
"w-full": !isSpecialCell,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Table.Cell>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
{pagination && (
|
|
||||||
<div
|
|
||||||
className={clx("flex items-center justify-between p-4", {
|
|
||||||
"border-t": layout === "fill",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Skeleton className="h-7 w-[138px]" />
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<Skeleton className="h-7 w-24" />
|
|
||||||
<Skeleton className="h-7 w-11" />
|
|
||||||
<Skeleton className="h-7 w-11" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./data-table-skeleton"
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { clx } from "@medusajs/ui"
|
import { clx } from "@medusajs/ui"
|
||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
import { NoRecords } from "../../common/empty-table-content"
|
import { NoRecords } from "../../common/empty-table-content"
|
||||||
|
import { TableSkeleton } from "../../common/skeleton"
|
||||||
import { DataTableQuery, DataTableQueryProps } from "./data-table-query"
|
import { DataTableQuery, DataTableQueryProps } from "./data-table-query"
|
||||||
import { DataTableRoot, DataTableRootProps } from "./data-table-root"
|
import { DataTableRoot, DataTableRootProps } from "./data-table-root"
|
||||||
import { DataTableSkeleton } from "./data-table-skeleton"
|
|
||||||
|
|
||||||
interface DataTableProps<TData>
|
interface DataTableProps<TData>
|
||||||
extends Omit<DataTableRootProps<TData>, "noResults">,
|
extends Omit<DataTableRootProps<TData>, "noResults">,
|
||||||
@@ -35,12 +35,11 @@ export const DataTable = <TData,>({
|
|||||||
}: DataTableProps<TData>) => {
|
}: DataTableProps<TData>) => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<DataTableSkeleton
|
<TableSkeleton
|
||||||
layout={layout}
|
layout={layout}
|
||||||
columns={columns}
|
|
||||||
rowCount={pageSize}
|
rowCount={pageSize}
|
||||||
searchable={search}
|
search={search}
|
||||||
filterable={!!filters?.length}
|
filters={!!filters?.length}
|
||||||
orderBy={!!orderBy?.length}
|
orderBy={!!orderBy?.length}
|
||||||
pagination={!!pagination}
|
pagination={!!pagination}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -745,10 +745,10 @@ export const v2Routes: RouteObject[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "api-key-management",
|
path: "publishable-api-keys",
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "API Key Management",
|
crumb: () => "Publishable API Keys",
|
||||||
},
|
},
|
||||||
children: [
|
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: <Outlet />,
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Secret API Keys",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
element: <Outlet />,
|
||||||
|
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",
|
path: "taxes",
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { useLocation } from "react-router-dom"
|
import { useLocation } from "react-router-dom"
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
import { getApiKeyTypeFromPathname } from "../common/utils"
|
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 = () => {
|
export const ApiKeyManagementCreate = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
@@ -9,7 +9,7 @@ export const ApiKeyManagementCreate = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
<CreatePublishableApiKeyForm keyType={keyType} />
|
<ApiKeyCreateForm keyType={keyType} />
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+73
-19
@@ -1,9 +1,10 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
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 { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Eye, EyeSlash } from "@medusajs/icons"
|
||||||
import { AdminApiKeyResponse } from "@medusajs/types"
|
import { AdminApiKeyResponse } from "@medusajs/types"
|
||||||
import { Fragment, useState } from "react"
|
import { Fragment, useState } from "react"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
@@ -14,29 +15,40 @@ import {
|
|||||||
import { useCreateApiKey } from "../../../../../hooks/api/api-keys"
|
import { useCreateApiKey } from "../../../../../hooks/api/api-keys"
|
||||||
import { ApiKeyType } from "../../../common/constants"
|
import { ApiKeyType } from "../../../common/constants"
|
||||||
|
|
||||||
const CreatePublishableApiKeySchema = zod.object({
|
const ApiKeyCreateSchema = zod.object({
|
||||||
title: zod.string().min(1),
|
title: zod.string().min(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
type CreatePublishableApiKeyFormProps = {
|
type ApiKeyCreateFormProps = {
|
||||||
keyType: ApiKeyType
|
keyType: ApiKeyType
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreatePublishableApiKeyForm = ({
|
function getRedactedKey(key?: string) {
|
||||||
keyType,
|
if (!key) {
|
||||||
}: CreatePublishableApiKeyFormProps) => {
|
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<
|
const [createdKey, setCreatedKey] = useState<
|
||||||
AdminApiKeyResponse["api_key"] | null
|
AdminApiKeyResponse["api_key"] | null
|
||||||
>(null)
|
>(null)
|
||||||
|
const [showRedactedKey, setShowRedactedKey] = useState(true)
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof CreatePublishableApiKeySchema>>({
|
const form = useForm<zod.infer<typeof ApiKeyCreateSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: "",
|
title: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreatePublishableApiKeySchema),
|
resolver: zodResolver(ApiKeyCreateSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isPending } = useCreateApiKey()
|
const { mutateAsync, isPending } = useCreateApiKey()
|
||||||
@@ -47,25 +59,51 @@ export const CreatePublishableApiKeyForm = ({
|
|||||||
{ title: values.title, type: keyType },
|
{ title: values.title, type: keyType },
|
||||||
{
|
{
|
||||||
onSuccess: ({ api_key }) => {
|
onSuccess: ({ api_key }) => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("apiKeyManagement.create.successToast"),
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
|
||||||
switch (keyType) {
|
switch (keyType) {
|
||||||
case ApiKeyType.PUBLISHABLE:
|
case ApiKeyType.PUBLISHABLE:
|
||||||
handleSuccess(`/settings/api-key-management/${api_key.id}`)
|
handleSuccess(`/settings/publishable-api-keys/${api_key.id}`)
|
||||||
break
|
break
|
||||||
case ApiKeyType.SECRET:
|
case ApiKeyType.SECRET:
|
||||||
setCreatedKey(api_key)
|
setCreatedKey(api_key)
|
||||||
break
|
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 = () => {
|
const handleGoToSecretKey = () => {
|
||||||
if (!createdKey) {
|
if (!createdKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSuccess(`/settings/api-key-management/${createdKey.id}`)
|
handleSuccess(`/settings/secret-api-keys/${createdKey.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -125,7 +163,7 @@ export const CreatePublishableApiKeyForm = ({
|
|||||||
</form>
|
</form>
|
||||||
</RouteFocusModal.Form>
|
</RouteFocusModal.Form>
|
||||||
<Prompt variant="confirmation" open={!!createdKey}>
|
<Prompt variant="confirmation" open={!!createdKey}>
|
||||||
<Prompt.Content className="w-fit max-w-[80%]">
|
<Prompt.Content className="w-fit max-w-[42.5%]">
|
||||||
<Prompt.Header>
|
<Prompt.Header>
|
||||||
<Prompt.Title>
|
<Prompt.Title>
|
||||||
{t("apiKeyManagement.create.secretKeyCreatedHeader")}
|
{t("apiKeyManagement.create.secretKeyCreatedHeader")}
|
||||||
@@ -134,18 +172,34 @@ export const CreatePublishableApiKeyForm = ({
|
|||||||
{t("apiKeyManagement.create.secretKeyCreatedHint")}
|
{t("apiKeyManagement.create.secretKeyCreatedHint")}
|
||||||
</Prompt.Description>
|
</Prompt.Description>
|
||||||
</Prompt.Header>
|
</Prompt.Header>
|
||||||
<div className="px-6 pt-6">
|
<div className="flex flex-col gap-y-3 px-6 py-4">
|
||||||
<div className="shadow-borders-base bg-ui-bg-component flex items-center gap-x-2 rounded-md px-4 py-2.5">
|
<div className="shadow-borders-base bg-ui-bg-component grid h-8 grid-cols-[1fr_32px] items-center overflow-hidden rounded-md">
|
||||||
|
<div className="flex items-center px-2">
|
||||||
<Text family="mono" size="small">
|
<Text family="mono" size="small">
|
||||||
{createdKey?.token}
|
{showRedactedKey
|
||||||
|
? getRedactedKey(createdKey?.token)
|
||||||
|
: createdKey?.token}
|
||||||
</Text>
|
</Text>
|
||||||
<Copy
|
|
||||||
className="text-ui-fg-subtle"
|
|
||||||
content={createdKey?.token!}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
className="transition-fg hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed text-ui-fg-muted active:text-ui-fg-subtle flex size-8 appearance-none items-center justify-center border-l"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowRedactedKey(!showRedactedKey)}
|
||||||
|
>
|
||||||
|
{showRedactedKey ? <EyeSlash /> : <Eye />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Prompt.Footer>
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleCopyToken}
|
||||||
|
>
|
||||||
|
{t("apiKeyManagement.actions.copy")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Prompt.Footer className="border-t py-4">
|
||||||
<Prompt.Action onClick={handleGoToSecretKey}>
|
<Prompt.Action onClick={handleGoToSecretKey}>
|
||||||
{t("actions.continue")}
|
{t("actions.continue")}
|
||||||
</Prompt.Action>
|
</Prompt.Action>
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./api-key-create-form"
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./create-publishable-api-key-form"
|
|
||||||
+13
-1
@@ -1,5 +1,11 @@
|
|||||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import {
|
||||||
|
GeneralSectionSkeleton,
|
||||||
|
JsonViewSectionSkeleton,
|
||||||
|
TableSectionSkeleton,
|
||||||
|
} from "../../../components/common/skeleton"
|
||||||
import { useApiKey } from "../../../hooks/api/api-keys"
|
import { useApiKey } from "../../../hooks/api/api-keys"
|
||||||
import { ApiKeyType } from "../common/constants"
|
import { ApiKeyType } from "../common/constants"
|
||||||
import { ApiKeyGeneralSection } from "./components/api-key-general-section"
|
import { ApiKeyGeneralSection } from "./components/api-key-general-section"
|
||||||
@@ -18,7 +24,13 @@ export const ApiKeyManagementDetail = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (isLoading || !api_key) {
|
if (isLoading || !api_key) {
|
||||||
return <div>Loading...</div>
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<GeneralSectionSkeleton rowCount={4} />
|
||||||
|
<TableSectionSkeleton />
|
||||||
|
<JsonViewSectionSkeleton />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPublishable = api_key?.type === ApiKeyType.PUBLISHABLE
|
const isPublishable = api_key?.type === ApiKeyType.PUBLISHABLE
|
||||||
|
|||||||
+48
-23
@@ -1,12 +1,13 @@
|
|||||||
import { PencilSquare, Trash, XCircle } from "@medusajs/icons"
|
import { PencilSquare, Trash, XCircle } from "@medusajs/icons"
|
||||||
import { ApiKeyDTO } from "@medusajs/types"
|
import { ApiKeyDTO } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Container,
|
Container,
|
||||||
Copy,
|
Copy,
|
||||||
Heading,
|
Heading,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
Text,
|
Text,
|
||||||
clx,
|
toast,
|
||||||
usePrompt,
|
usePrompt,
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@@ -20,7 +21,11 @@ import {
|
|||||||
} from "../../../../../hooks/api/api-keys"
|
} from "../../../../../hooks/api/api-keys"
|
||||||
import { useUser } from "../../../../../hooks/api/users"
|
import { useUser } from "../../../../../hooks/api/users"
|
||||||
import { useDate } from "../../../../../hooks/use-date"
|
import { useDate } from "../../../../../hooks/use-date"
|
||||||
import { getApiKeyStatusProps, getApiKeyTypeProps } from "../../../common/utils"
|
import {
|
||||||
|
getApiKeyStatusProps,
|
||||||
|
getApiKeyTypeProps,
|
||||||
|
prettifyRedactedToken,
|
||||||
|
} from "../../../common/utils"
|
||||||
|
|
||||||
type ApiKeyGeneralSectionProps = {
|
type ApiKeyGeneralSectionProps = {
|
||||||
apiKey: ApiKeyDTO
|
apiKey: ApiKeyDTO
|
||||||
@@ -38,7 +43,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("apiKeyManagement.warnings.delete", {
|
description: t("apiKeyManagement.delete.warning", {
|
||||||
title: apiKey.title,
|
title: apiKey.title,
|
||||||
}),
|
}),
|
||||||
confirmText: t("actions.delete"),
|
confirmText: t("actions.delete"),
|
||||||
@@ -51,15 +56,27 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
|||||||
|
|
||||||
await deleteAsync(undefined, {
|
await deleteAsync(undefined, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("apiKeyManagement.delete.successToast", {
|
||||||
|
title: apiKey.title,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
navigate("..", { replace: true })
|
navigate("..", { replace: true })
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: err.message,
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRevoke = async () => {
|
const handleRevoke = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("apiKeyManagement.warnings.revoke", {
|
description: t("apiKeyManagement.revoke.warning", {
|
||||||
title: apiKey.title,
|
title: apiKey.title,
|
||||||
}),
|
}),
|
||||||
confirmText: t("apiKeyManagement.actions.revoke"),
|
confirmText: t("apiKeyManagement.actions.revoke"),
|
||||||
@@ -70,7 +87,22 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
|||||||
return
|
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 = [
|
const dangerousActions = [
|
||||||
@@ -109,7 +141,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
|||||||
{
|
{
|
||||||
label: t("actions.edit"),
|
label: t("actions.edit"),
|
||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
to: `/settings/api-key-management/${apiKey.id}/edit`,
|
to: "edit",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -124,27 +156,20 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
|||||||
<Text size="small" leading="compact" weight="plus">
|
<Text size="small" leading="compact" weight="plus">
|
||||||
{t("fields.key")}
|
{t("fields.key")}
|
||||||
</Text>
|
</Text>
|
||||||
<div
|
{apiKey.type === "secret" ? (
|
||||||
className={clx(
|
<Badge size="2xsmall" className="w-fit">
|
||||||
"bg-ui-bg-subtle border-ui-border-base box-border flex w-fit cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1",
|
{prettifyRedactedToken(apiKey.redacted)}
|
||||||
{
|
</Badge>
|
||||||
"pr-2": apiKey.type === "secret",
|
) : (
|
||||||
"cursor-pointer": apiKey.type !== "secret",
|
<Copy asChild content={apiKey.token}>
|
||||||
}
|
<Badge size="2xsmall" className="w-fit max-w-40 cursor-pointer">
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Text size="xsmall" leading="compact" className="truncate">
|
<Text size="xsmall" leading="compact" className="truncate">
|
||||||
{apiKey.redacted}
|
{prettifyRedactedToken(apiKey.redacted)}
|
||||||
</Text>
|
</Text>
|
||||||
{apiKey.type !== "secret" && (
|
</Badge>
|
||||||
<Copy
|
</Copy>
|
||||||
content={apiKey.token}
|
|
||||||
variant="mini"
|
|
||||||
className="text-ui-fg-subtle"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<Text size="small" leading="compact" weight="plus">
|
||||||
{t("fields.type")}
|
{t("fields.type")}
|
||||||
|
|||||||
+37
-4
@@ -1,6 +1,6 @@
|
|||||||
import { PencilSquare, Plus, Trash } from "@medusajs/icons"
|
import { PencilSquare, Plus, Trash } from "@medusajs/icons"
|
||||||
import { AdminApiKeyResponse, AdminSalesChannelResponse } from "@medusajs/types"
|
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 { keepPreviousData } from "@tanstack/react-query"
|
||||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
@@ -65,7 +65,7 @@ export const ApiKeySalesChannelSection = ({
|
|||||||
|
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("apiKeyManagement.warnings.removeSalesChannels", {
|
description: t("apiKeyManagement.removeSalesChannel.warningBatch", {
|
||||||
count: keys.length,
|
count: keys.length,
|
||||||
}),
|
}),
|
||||||
confirmText: t("actions.continue"),
|
confirmText: t("actions.continue"),
|
||||||
@@ -82,8 +82,23 @@ export const ApiKeySalesChannelSection = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t(
|
||||||
|
"apiKeyManagement.removeSalesChannel.successToastBatch",
|
||||||
|
{
|
||||||
|
count: keys.length,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
setRowSelection({})
|
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 handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("apiKeyManagement.warnings.removeSalesChannel", {
|
description: t("apiKeyManagement.removeSalesChannel.warning", {
|
||||||
name: salesChannel.name,
|
name: salesChannel.name,
|
||||||
}),
|
}),
|
||||||
confirmText: t("actions.delete"),
|
confirmText: t("actions.delete"),
|
||||||
@@ -156,9 +171,27 @@ const SalesChannelActions = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync(
|
||||||
|
{
|
||||||
sales_channel_ids: [salesChannel.id],
|
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 (
|
return (
|
||||||
|
|||||||
+14
-2
@@ -1,5 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
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 { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
@@ -35,9 +35,21 @@ export const EditApiKeyForm = ({ apiKey }: EditApiKeyFormProps) => {
|
|||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
await mutateAsync(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()
|
handleSuccess()
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: err.message,
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+3
-23
@@ -1,34 +1,14 @@
|
|||||||
import { useTranslation } from "react-i18next"
|
import { Outlet, useLocation } from "react-router-dom"
|
||||||
import { Link, Outlet, useLocation } from "react-router-dom"
|
import { getApiKeyTypeFromPathname } from "../common/utils"
|
||||||
import { ApiKeyType } from "../common/constants"
|
|
||||||
import { ApiKeyManagementListTable } from "./components/api-key-management-list-table"
|
import { ApiKeyManagementListTable } from "./components/api-key-management-list-table"
|
||||||
|
|
||||||
export const ApiKeyManagementList = () => {
|
export const ApiKeyManagementList = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const keyType = pathname.includes("secret")
|
const keyType = getApiKeyTypeFromPathname(pathname)
|
||||||
? ApiKeyType.SECRET
|
|
||||||
: ApiKeyType.PUBLISHABLE
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
<div className="flex items-center">
|
|
||||||
<Link
|
|
||||||
to="/settings/api-key-management"
|
|
||||||
data-state={keyType === ApiKeyType.PUBLISHABLE ? "active" : ""}
|
|
||||||
className="txt-compact-small-plus transition-fg text-ui-fg-subtle hover:text-ui-fg-base focus-visible:border-ui-border-interactive focus-visible:!shadow-borders-focus focus-visible:bg-ui-bg-base data-[state=active]:text-ui-fg-base data-[state=active]:bg-ui-bg-base data-[state=active]:shadow-elevation-card-rest inline-flex items-center justify-center rounded-full border border-transparent bg-transparent px-2.5 py-1 outline-none"
|
|
||||||
>
|
|
||||||
{t("apiKeyManagement.tabs.publishable")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/settings/api-key-management/secret"
|
|
||||||
data-state={keyType === ApiKeyType.SECRET ? "active" : ""}
|
|
||||||
className="txt-compact-small-plus transition-fg text-ui-fg-subtle hover:text-ui-fg-base focus-visible:border-ui-border-interactive focus-visible:!shadow-borders-focus focus-visible:bg-ui-bg-base data-[state=active]:text-ui-fg-base data-[state=active]:bg-ui-bg-base data-[state=active]:shadow-elevation-card-rest inline-flex items-center justify-center rounded-full border border-transparent bg-transparent px-2.5 py-1 outline-none"
|
|
||||||
>
|
|
||||||
{t("apiKeyManagement.tabs.secret")}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<ApiKeyManagementListTable keyType={keyType} />
|
<ApiKeyManagementListTable keyType={keyType} />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ export const ApiKeyManagementListTable = ({
|
|||||||
count={count}
|
count={count}
|
||||||
pageSize={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
orderBy={["title", "created_at", "updated_at", "revoked_at"]}
|
orderBy={["title", "created_at", "updated_at", "revoked_at"]}
|
||||||
navigateTo={(row) => `/settings/api-key-management/${row.id}`}
|
navigateTo={(row) => row.id}
|
||||||
pagination
|
pagination
|
||||||
search
|
search
|
||||||
queryObject={raw}
|
queryObject={raw}
|
||||||
|
|||||||
+54
-7
@@ -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 { AdminApiKeyResponse } from "@medusajs/types"
|
||||||
import { usePrompt } from "@medusajs/ui"
|
import { toast, usePrompt } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import {
|
import {
|
||||||
@@ -22,7 +22,7 @@ export const ApiKeyRowActions = ({
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("apiKeyManagement.warnings.delete", {
|
description: t("apiKeyManagement.delete.warning", {
|
||||||
title: apiKey.title,
|
title: apiKey.title,
|
||||||
}),
|
}),
|
||||||
confirmText: t("actions.delete"),
|
confirmText: t("actions.delete"),
|
||||||
@@ -33,13 +33,28 @@ export const ApiKeyRowActions = ({
|
|||||||
return
|
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 handleRevoke = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
description: t("apiKeyManagement.warnings.revoke", {
|
description: t("apiKeyManagement.revoke.warning", {
|
||||||
title: apiKey.title,
|
title: apiKey.title,
|
||||||
}),
|
}),
|
||||||
confirmText: t("apiKeyManagement.actions.revoke"),
|
confirmText: t("apiKeyManagement.actions.revoke"),
|
||||||
@@ -50,7 +65,30 @@ export const ApiKeyRowActions = ({
|
|||||||
return
|
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 (
|
return (
|
||||||
@@ -61,8 +99,17 @@ export const ApiKeyRowActions = ({
|
|||||||
{
|
{
|
||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
label: t("actions.edit"),
|
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: <SquareTwoStack />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+19
-25
@@ -1,5 +1,5 @@
|
|||||||
import { AdminApiKeyResponse } from "@medusajs/types"
|
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 { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { MouseEvent, useMemo } from "react"
|
import { MouseEvent, useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
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 { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
|
||||||
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
|
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
|
||||||
import { TextCell } from "../../../../../components/table/table-cells/common/text-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"
|
import { ApiKeyRowActions } from "./api-key-row-actions"
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<AdminApiKeyResponse["api_key"]>()
|
const columnHelper = createColumnHelper<AdminApiKeyResponse["api_key"]>()
|
||||||
@@ -31,34 +35,24 @@ export const useApiKeyManagementTableColumns = () => {
|
|||||||
const token = getValue()
|
const token = getValue()
|
||||||
const isSecret = row.original.type === "secret"
|
const isSecret = row.original.type === "secret"
|
||||||
|
|
||||||
const clickHandler = !isSecret
|
if (isSecret) {
|
||||||
? (e: MouseEvent) => e.stopPropagation()
|
return <Badge size="2xsmall">{prettifyRedactedToken(token)}</Badge>
|
||||||
: undefined
|
}
|
||||||
|
|
||||||
|
const clickHandler = (e: MouseEvent) => e.stopPropagation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Badge size="2xsmall" className="max-w-40" onClick={clickHandler}>
|
||||||
className={clx(
|
|
||||||
"bg-ui-bg-subtle border-ui-border-base box-border flex w-fit max-w-[220px] items-center gap-x-0.5 overflow-hidden rounded-full border pl-2",
|
|
||||||
{
|
|
||||||
"cursor-default pr-1": !isSecret,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pr-2": isSecret,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onClick={clickHandler}
|
|
||||||
>
|
|
||||||
<Text size="xsmall" leading="compact" className="truncate">
|
|
||||||
{token}
|
|
||||||
</Text>
|
|
||||||
{!isSecret && (
|
|
||||||
<Copy
|
<Copy
|
||||||
content={row.original.token}
|
content={row.original.token}
|
||||||
variant="mini"
|
|
||||||
className="text-ui-fg-subtle"
|
className="text-ui-fg-subtle"
|
||||||
/>
|
asChild
|
||||||
)}
|
>
|
||||||
</div>
|
<Text size="xsmall" leading="compact" className="truncate">
|
||||||
|
{prettifyRedactedToken(token)}
|
||||||
|
</Text>
|
||||||
|
</Copy>
|
||||||
|
</Badge>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
+1
-5
@@ -3,8 +3,6 @@ import { useParams } from "react-router-dom"
|
|||||||
import { AdminApiKeyResponse, AdminSalesChannelResponse } from "@medusajs/types"
|
import { AdminApiKeyResponse, AdminSalesChannelResponse } from "@medusajs/types"
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
import { useApiKey } from "../../../hooks/api/api-keys"
|
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"
|
import { ApiKeySalesChannelsForm } from "./components/api-key-sales-channels-form"
|
||||||
|
|
||||||
export const ApiKeyManagementAddSalesChannels = () => {
|
export const ApiKeyManagementAddSalesChannels = () => {
|
||||||
@@ -12,14 +10,13 @@ export const ApiKeyManagementAddSalesChannels = () => {
|
|||||||
|
|
||||||
const { api_key, isLoading, isError, error } = useApiKey(id!)
|
const { api_key, isLoading, isError, error } = useApiKey(id!)
|
||||||
|
|
||||||
const isSecret = api_key?.type === ApiKeyType.SECRET
|
|
||||||
const preSelected = (
|
const preSelected = (
|
||||||
api_key as AdminApiKeyResponse["api_key"] & {
|
api_key as AdminApiKeyResponse["api_key"] & {
|
||||||
sales_channels: AdminSalesChannelResponse["sales_channel"][] | null
|
sales_channels: AdminSalesChannelResponse["sales_channel"][] | null
|
||||||
}
|
}
|
||||||
)?.sales_channels?.map((sc) => sc.id)
|
)?.sales_channels?.map((sc) => sc.id)
|
||||||
|
|
||||||
const ready = !isLoading && api_key && !isSecret
|
const ready = !isLoading && api_key
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
@@ -27,7 +24,6 @@ export const ApiKeyManagementAddSalesChannels = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
{isSecret && <ApiKeySalesChannelsFallback />}
|
|
||||||
{ready && (
|
{ready && (
|
||||||
<ApiKeySalesChannelsForm apiKey={id!} preSelected={preSelected} />
|
<ApiKeySalesChannelsForm apiKey={id!} preSelected={preSelected} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
-26
@@ -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 (
|
|
||||||
<div className="flex size-full flex-col">
|
|
||||||
<RouteFocusModal.Header>
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<RouteFocusModal.Close asChild>
|
|
||||||
<Button variant="secondary" size="small">
|
|
||||||
{t("actions.cancel")}
|
|
||||||
</Button>
|
|
||||||
</RouteFocusModal.Close>
|
|
||||||
</div>
|
|
||||||
</RouteFocusModal.Header>
|
|
||||||
<RouteFocusModal.Body className="flex flex-1 items-center justify-center">
|
|
||||||
<Alert variant="warning">
|
|
||||||
{t("apiKeyManagement.warnings.salesChannelsSecretKey")}
|
|
||||||
</Alert>
|
|
||||||
</RouteFocusModal.Body>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./api-key-sales-channels-fallback"
|
|
||||||
+21
-2
@@ -1,6 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { AdminSalesChannelResponse } from "@medusajs/types"
|
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 { keepPreviousData } from "@tanstack/react-query"
|
||||||
import {
|
import {
|
||||||
OnChangeFn,
|
OnChangeFn,
|
||||||
@@ -102,8 +102,21 @@ export const ApiKeySalesChannelsForm = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("apiKeyManagement.salesChannels.successToast", {
|
||||||
|
count: values.sales_channel_ids.length,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: err.message,
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -142,6 +155,7 @@ export const ApiKeySalesChannelsForm = ({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
queryObject={raw}
|
queryObject={raw}
|
||||||
orderBy={["name", "created_at", "updated_at"]}
|
orderBy={["name", "created_at", "updated_at"]}
|
||||||
|
layout="fill"
|
||||||
/>
|
/>
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
</form>
|
</form>
|
||||||
@@ -191,7 +205,12 @@ const useColumns = () => {
|
|||||||
|
|
||||||
if (isPreSelected) {
|
if (isPreSelected) {
|
||||||
return (
|
return (
|
||||||
<Tooltip content={t("store.currencyAlreadyAdded")} side="right">
|
<Tooltip
|
||||||
|
content={t(
|
||||||
|
"apiKeyManagement.salesChannels.alreadyAddedTooltip"
|
||||||
|
)}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
{Component}
|
{Component}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { TFunction } from "i18next"
|
|||||||
import { ApiKeyType } from "./constants"
|
import { ApiKeyType } from "./constants"
|
||||||
|
|
||||||
export function getApiKeyTypeFromPathname(pathname: string) {
|
export function getApiKeyTypeFromPathname(pathname: string) {
|
||||||
const isSecretKey = pathname.startsWith("/settings/api-key-management/secret")
|
const isSecretKey = pathname.startsWith("/settings/secret-api-keys")
|
||||||
|
|
||||||
switch (isSecretKey) {
|
switch (isSecretKey) {
|
||||||
case true:
|
case true:
|
||||||
@@ -46,3 +46,17 @@ export function getApiKeyTypeProps(
|
|||||||
label: t("apiKeyManagement.type.secret"),
|
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("***", `•••`)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user