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:
committed by
GitHub
parent
38c971f111
commit
ef29981a54
@@ -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",
|
||||
|
||||
@@ -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 = () => {
|
||||
return (
|
||||
<Container className="divide-y p-0" aria-hidden>
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export * from "./data-table-skeleton"
|
||||
@@ -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<TData>
|
||||
extends Omit<DataTableRootProps<TData>, "noResults">,
|
||||
@@ -35,12 +35,11 @@ export const DataTable = <TData,>({
|
||||
}: DataTableProps<TData>) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<DataTableSkeleton
|
||||
<TableSkeleton
|
||||
layout={layout}
|
||||
columns={columns}
|
||||
rowCount={pageSize}
|
||||
searchable={search}
|
||||
filterable={!!filters?.length}
|
||||
search={search}
|
||||
filters={!!filters?.length}
|
||||
orderBy={!!orderBy?.length}
|
||||
pagination={!!pagination}
|
||||
/>
|
||||
|
||||
@@ -745,10 +745,10 @@ export const v2Routes: RouteObject[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "api-key-management",
|
||||
path: "publishable-api-keys",
|
||||
element: <Outlet />,
|
||||
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: <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",
|
||||
element: <Outlet />,
|
||||
|
||||
@@ -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 (
|
||||
<RouteFocusModal>
|
||||
<CreatePublishableApiKeyForm keyType={keyType} />
|
||||
<ApiKeyCreateForm keyType={keyType} />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<zod.infer<typeof CreatePublishableApiKeySchema>>({
|
||||
const form = useForm<zod.infer<typeof ApiKeyCreateSchema>>({
|
||||
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 = ({
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
<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.Title>
|
||||
{t("apiKeyManagement.create.secretKeyCreatedHeader")}
|
||||
@@ -134,18 +172,34 @@ export const CreatePublishableApiKeyForm = ({
|
||||
{t("apiKeyManagement.create.secretKeyCreatedHint")}
|
||||
</Prompt.Description>
|
||||
</Prompt.Header>
|
||||
<div className="px-6 pt-6">
|
||||
<div className="shadow-borders-base bg-ui-bg-component flex items-center gap-x-2 rounded-md px-4 py-2.5">
|
||||
<Text family="mono" size="small">
|
||||
{createdKey?.token}
|
||||
</Text>
|
||||
<Copy
|
||||
className="text-ui-fg-subtle"
|
||||
content={createdKey?.token!}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-3 px-6 py-4">
|
||||
<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">
|
||||
{showRedactedKey
|
||||
? getRedactedKey(createdKey?.token)
|
||||
: createdKey?.token}
|
||||
</Text>
|
||||
</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>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
className="w-full"
|
||||
onClick={handleCopyToken}
|
||||
>
|
||||
{t("apiKeyManagement.actions.copy")}
|
||||
</Button>
|
||||
</div>
|
||||
<Prompt.Footer>
|
||||
<Prompt.Footer className="border-t py-4">
|
||||
<Prompt.Action onClick={handleGoToSecretKey}>
|
||||
{t("actions.continue")}
|
||||
</Prompt.Action>
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./api-key-create-form"
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./create-publishable-api-key-form"
|
||||
@@ -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 <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
|
||||
|
||||
@@ -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: <PencilSquare />,
|
||||
to: `/settings/api-key-management/${apiKey.id}/edit`,
|
||||
to: "edit",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -124,26 +156,19 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.key")}
|
||||
</Text>
|
||||
<div
|
||||
className={clx(
|
||||
"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",
|
||||
{
|
||||
"pr-2": apiKey.type === "secret",
|
||||
"cursor-pointer": apiKey.type !== "secret",
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Text size="xsmall" leading="compact" className="truncate">
|
||||
{apiKey.redacted}
|
||||
</Text>
|
||||
{apiKey.type !== "secret" && (
|
||||
<Copy
|
||||
content={apiKey.token}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{apiKey.type === "secret" ? (
|
||||
<Badge size="2xsmall" className="w-fit">
|
||||
{prettifyRedactedToken(apiKey.redacted)}
|
||||
</Badge>
|
||||
) : (
|
||||
<Copy asChild content={apiKey.token}>
|
||||
<Badge size="2xsmall" className="w-fit max-w-40 cursor-pointer">
|
||||
<Text size="xsmall" leading="compact" className="truncate">
|
||||
{prettifyRedactedToken(apiKey.redacted)}
|
||||
</Text>
|
||||
</Badge>
|
||||
</Copy>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"),
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<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} />
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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: <PencilSquare />,
|
||||
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 />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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<AdminApiKeyResponse["api_key"]>()
|
||||
@@ -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 <Badge size="2xsmall">{prettifyRedactedToken(token)}</Badge>
|
||||
}
|
||||
|
||||
const clickHandler = (e: MouseEvent) => e.stopPropagation()
|
||||
|
||||
return (
|
||||
<div
|
||||
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
|
||||
content={row.original.token}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Badge size="2xsmall" className="max-w-40" onClick={clickHandler}>
|
||||
<Copy
|
||||
content={row.original.token}
|
||||
className="text-ui-fg-subtle"
|
||||
asChild
|
||||
>
|
||||
<Text size="xsmall" leading="compact" className="truncate">
|
||||
{prettifyRedactedToken(token)}
|
||||
</Text>
|
||||
</Copy>
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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 (
|
||||
<RouteFocusModal>
|
||||
{isSecret && <ApiKeySalesChannelsFallback />}
|
||||
{ready && (
|
||||
<ApiKeySalesChannelsForm apiKey={id!} preSelected={preSelected} />
|
||||
)}
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export * from "./api-key-sales-channels-fallback"
|
||||
@@ -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"
|
||||
/>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
@@ -191,7 +205,12 @@ const useColumns = () => {
|
||||
|
||||
if (isPreSelected) {
|
||||
return (
|
||||
<Tooltip content={t("store.currencyAlreadyAdded")} side="right">
|
||||
<Tooltip
|
||||
content={t(
|
||||
"apiKeyManagement.salesChannels.alreadyAddedTooltip"
|
||||
)}
|
||||
side="right"
|
||||
>
|
||||
{Component}
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -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("***", `•••`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user