feat(dashboard): Secret keys domain (#7030)
* setup secret keys * add secret keys * fix merge
This commit is contained in:
committed by
GitHub
parent
8eb2a4156d
commit
21be6ff7ed
@@ -941,20 +941,54 @@
|
||||
"deleteSalesChannelWarning": "You are about to delete the sales channel {{name}}. This action cannot be undone."
|
||||
},
|
||||
"apiKeyManagement": {
|
||||
"domainTitle": "API Keys",
|
||||
"domain": "{{ keyType }} API Keys",
|
||||
"createKey": "Create key",
|
||||
"createPublishableApiKey": "Create {{ keyType }} API Key",
|
||||
"editKey": "Edit key",
|
||||
"revoke": "Revoke",
|
||||
"publishableApiKeyHint": "Publishable API keys are used to limit the scope of requests to specific sales channels.",
|
||||
"deleteKeyWarning": "You are about to delete the API key {{title}}. This action cannot be undone.",
|
||||
"revokeKeyWarning": "You are about to revoke the API key {{title}}. This action cannot be undone, and the key cannot be used in future requests.",
|
||||
"removeSalesChannelWarning": "You are about to remove the sales channel {{name}} from the API key. This action cannot be undone.",
|
||||
"removeSalesChannelsWarning_one": "You are about to remove {{count}} sales channel from the API key. This action cannot be undone.",
|
||||
"removeSalesChannelsWarning_other": "You are about to remove {{count}} sales channels from the API key. This action cannot be undone.",
|
||||
"createdBy": "Created by",
|
||||
"revokedBy": "Revoked by"
|
||||
"domain": {
|
||||
"apiKeys": "API Keys",
|
||||
"publishable": "Publishable API Keys",
|
||||
"secret": "Secret API Keys"
|
||||
},
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"revoked": "Revoked"
|
||||
},
|
||||
"type": {
|
||||
"publishable": "Publishable",
|
||||
"secret": "Secret"
|
||||
},
|
||||
"create": {
|
||||
"createPublishableHeader": "Create Publishable API Key",
|
||||
"createPublishableHint": "Create a new publishable API key to limit the scope of requests to specific sales channels.",
|
||||
"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."
|
||||
},
|
||||
"edit": {
|
||||
"header": "Edit API Key"
|
||||
},
|
||||
"tabs": {
|
||||
"secret": "Secret",
|
||||
"publishable": "Publishable"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"actions": {
|
||||
"revoke": "Revoke"
|
||||
},
|
||||
"table": {
|
||||
"lastUsedAtHeader": "Last Used At",
|
||||
"createdAtHeader": "Revoked At"
|
||||
},
|
||||
"fields": {
|
||||
"lastUsedAtLabel": "Last used at",
|
||||
"revokedByLabel": "Revoked by",
|
||||
"createdByLabel": "Created by"
|
||||
}
|
||||
},
|
||||
"returnReasons": {
|
||||
"domain": "Return Reasons",
|
||||
|
||||
@@ -64,7 +64,7 @@ const useDeveloperRoutes = (): NavItemProps[] => {
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t("apiKeyManagement.domainTitle"),
|
||||
label: t("apiKeyManagement.domain.apiKeys"),
|
||||
to: "/settings/api-key-management",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -193,7 +193,7 @@ export const DataTableRoot = <TData,>({
|
||||
"cursor-pointer": !!to,
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
"bg-ui-bg-disabled hover:bg-ui-bg-disabled":
|
||||
"!bg-ui-bg-disabled !hover:bg-ui-bg-disabled":
|
||||
isRowDisabled,
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Tooltip } from "@medusajs/ui"
|
||||
import format from "date-fns/format"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { PlaceholderCell } from "../placeholder-cell"
|
||||
|
||||
type DateCellProps = {
|
||||
date: Date | string
|
||||
date: Date | string | null
|
||||
}
|
||||
|
||||
export const DateCell = ({ date }: DateCellProps) => {
|
||||
if (!date) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
const value = new Date(date)
|
||||
value.setMinutes(value.getMinutes() - value.getTimezoneOffset())
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import { createColumnHelper } from "@tanstack/react-table"
|
||||
|
||||
import { SalesChannelDTO } from "@medusajs/types"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { StatusCell } from "../../../components/table/table-cells/common/status-cell"
|
||||
import { TextHeader } from "../../../components/table/table-cells/common/text-cell"
|
||||
import {
|
||||
DescriptionCell,
|
||||
DescriptionHeader,
|
||||
@@ -14,6 +17,8 @@ import {
|
||||
const columnHelper = createColumnHelper<SalesChannelDTO>()
|
||||
|
||||
export const useSalesChannelTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("name", {
|
||||
@@ -24,7 +29,18 @@ export const useSalesChannelTableColumns = () => {
|
||||
header: () => <DescriptionHeader />,
|
||||
cell: ({ getValue }) => <DescriptionCell description={getValue()} />,
|
||||
}),
|
||||
columnHelper.accessor("is_disabled", {
|
||||
header: () => <TextHeader text={t("fields.status")} />,
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
return (
|
||||
<StatusCell color={value ? "grey" : "green"}>
|
||||
{value ? t("general.disabled") : t("general.enabled")}
|
||||
</StatusCell>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[]
|
||||
[t]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
AdminApiKeyResponse,
|
||||
AdminCustomerGroupResponse,
|
||||
AdminProductCategoryResponse,
|
||||
SalesChannelDTO,
|
||||
UserDTO,
|
||||
AdminCustomerGroupResponse,
|
||||
} from "@medusajs/types"
|
||||
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
|
||||
import { ErrorBoundary } from "../../components/error/error-boundary"
|
||||
@@ -639,17 +639,39 @@ export const v2Routes: RouteObject[] = [
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../v2-routes/api-key-management/api-key-management-list"
|
||||
),
|
||||
element: <Outlet />,
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
path: "",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../v2-routes/api-key-management/api-key-management-create"
|
||||
"../../v2-routes/api-key-management/api-key-management-list"
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../v2-routes/api-key-management/api-key-management-create"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -673,10 +695,10 @@ export const v2Routes: RouteObject[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "add-sales-channels",
|
||||
path: "sales-channels",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../v2-routes/api-key-management/api-key-management-add-sales-channels"
|
||||
"../../v2-routes/api-key-management/api-key-management-sales-channels"
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { useParams } from "react-router-dom"
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { useSalesChannels } from "../../../hooks/api/sales-channels"
|
||||
import { AddSalesChannelsToApiKeyForm } from "./components"
|
||||
|
||||
export const ApiKeyManagementAddSalesChannels = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
const { sales_channels, isLoading, isError, error } = useSalesChannels({
|
||||
publishable_key_id: id,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
{!isLoading && !!sales_channels && (
|
||||
<AddSalesChannelsToApiKeyForm
|
||||
apiKey={id!}
|
||||
preSelected={sales_channels.map((sc) => sc.id)}
|
||||
/>
|
||||
)}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./add-sales-channels-to-api-key-form"
|
||||
@@ -1,10 +1,15 @@
|
||||
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"
|
||||
|
||||
export const ApiKeyManagementCreate = () => {
|
||||
const { pathname } = useLocation()
|
||||
const keyType = getApiKeyTypeFromPathname(pathname)
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreatePublishableApiKeyForm />
|
||||
<CreatePublishableApiKeyForm keyType={keyType} />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button, Heading, Input, Text } from "@medusajs/ui"
|
||||
import { Button, Copy, Heading, Input, Prompt, Text } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { AdminApiKeyResponse } from "@medusajs/types"
|
||||
import { Fragment, useState } from "react"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { useCreateApiKey } from "../../../../../hooks/api/api-keys"
|
||||
import { ApiKeyType } from "../../../common/constants"
|
||||
|
||||
const CreatePublishableApiKeySchema = zod.object({
|
||||
title: zod.string().min(1),
|
||||
})
|
||||
|
||||
export const CreatePublishableApiKeyForm = () => {
|
||||
type CreatePublishableApiKeyFormProps = {
|
||||
keyType: ApiKeyType
|
||||
}
|
||||
|
||||
export const CreatePublishableApiKeyForm = ({
|
||||
keyType,
|
||||
}: CreatePublishableApiKeyFormProps) => {
|
||||
const [createdKey, setCreatedKey] = useState<
|
||||
AdminApiKeyResponse["api_key"] | null
|
||||
>(null)
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
@@ -30,66 +43,115 @@ export const CreatePublishableApiKeyForm = () => {
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync(
|
||||
// @ts-ignore type is wrong compared to validation
|
||||
{ title: values.title, type: "publishable" },
|
||||
// @ts-ignore
|
||||
{ title: values.title, type: keyType },
|
||||
{
|
||||
onSuccess: ({ api_key }) => {
|
||||
handleSuccess(`/settings/api-key-management/${api_key.id}`)
|
||||
switch (keyType) {
|
||||
case ApiKeyType.PUBLISHABLE:
|
||||
handleSuccess(`/settings/api-key-management/${api_key.id}`)
|
||||
break
|
||||
case ApiKeyType.SECRET:
|
||||
setCreatedKey(api_key)
|
||||
break
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const handleGoToSecretKey = () => {
|
||||
if (!createdKey) {
|
||||
return
|
||||
}
|
||||
|
||||
handleSuccess(`/settings/api-key-management/${createdKey.id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
<Fragment>
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||
<div>
|
||||
<Heading>
|
||||
{t("apiKeyManagement.createPublishableApiKey")}
|
||||
</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("apiKeyManagement.publishableApiKeyHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.title")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||
<div>
|
||||
<Heading>
|
||||
{keyType === ApiKeyType.PUBLISHABLE
|
||||
? t("apiKeyManagement.create.createPublishableHeader")
|
||||
: t("apiKeyManagement.create.createSecretHeader")}
|
||||
</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{keyType === ApiKeyType.PUBLISHABLE
|
||||
? t("apiKeyManagement.create.createPublishableHint")
|
||||
: t("apiKeyManagement.create.createSecretHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.title")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
<Prompt variant="confirmation" open={!!createdKey}>
|
||||
<Prompt.Content className="w-fit max-w-[80%]">
|
||||
<Prompt.Header>
|
||||
<Prompt.Title>
|
||||
{t("apiKeyManagement.create.secretKeyCreatedHeader")}
|
||||
</Prompt.Title>
|
||||
<Prompt.Description>
|
||||
{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>
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
<Prompt.Footer>
|
||||
<Prompt.Action onClick={handleGoToSecretKey}>
|
||||
{t("actions.continue")}
|
||||
</Prompt.Action>
|
||||
</Prompt.Footer>
|
||||
</Prompt.Content>
|
||||
</Prompt>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { useApiKey } from "../../../hooks/api/api-keys"
|
||||
import { ApiKeyType } from "../common/constants"
|
||||
import { ApiKeyGeneralSection } from "./components/api-key-general-section"
|
||||
import { apiKeyLoader } from "./loader"
|
||||
import { ApiKeySalesChannelSection } from "./components/api-key-sales-channel-section"
|
||||
import { apiKeyLoader } from "./loader"
|
||||
|
||||
export const ApiKeyManagementDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
@@ -11,6 +12,7 @@ export const ApiKeyManagementDetail = () => {
|
||||
>
|
||||
|
||||
const { id } = useParams()
|
||||
|
||||
const { api_key, isLoading, isError, error } = useApiKey(id!, undefined, {
|
||||
initialData: initialData,
|
||||
})
|
||||
@@ -19,6 +21,8 @@ export const ApiKeyManagementDetail = () => {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
const isPublishable = api_key?.type === ApiKeyType.PUBLISHABLE
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
@@ -26,7 +30,7 @@ export const ApiKeyManagementDetail = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ApiKeyGeneralSection apiKey={api_key} />
|
||||
<ApiKeySalesChannelSection apiKey={api_key} />
|
||||
{isPublishable && <ApiKeySalesChannelSection apiKey={api_key} />}
|
||||
<JsonViewSection data={api_key} />
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Text,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
@@ -18,6 +19,8 @@ import {
|
||||
useRevokeApiKey,
|
||||
} from "../../../../../hooks/api/api-keys"
|
||||
import { useUser } from "../../../../../hooks/api/users"
|
||||
import { useDate } from "../../../../../hooks/use-date"
|
||||
import { getApiKeyStatusProps, getApiKeyTypeProps } from "../../../common/utils"
|
||||
|
||||
type ApiKeyGeneralSectionProps = {
|
||||
apiKey: ApiKeyDTO
|
||||
@@ -27,6 +30,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const prompt = usePrompt()
|
||||
const { getFullDate } = useDate()
|
||||
|
||||
const { mutateAsync: revokeAsync } = useRevokeApiKey(apiKey.id)
|
||||
const { mutateAsync: deleteAsync } = useDeleteApiKey(apiKey.id)
|
||||
@@ -34,7 +38,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("apiKeyManagement.deleteKeyWarning", {
|
||||
description: t("apiKeyManagement.warnings.delete", {
|
||||
title: apiKey.title,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
@@ -55,10 +59,10 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
const handleRevoke = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("apiKeyManagement.revokeKeyWarning", {
|
||||
description: t("apiKeyManagement.warnings.revoke", {
|
||||
title: apiKey.title,
|
||||
}),
|
||||
confirmText: t("apiKeyManagement.revoke"),
|
||||
confirmText: t("apiKeyManagement.actions.revoke"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
@@ -80,19 +84,24 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
if (!apiKey.revoked_at) {
|
||||
dangerousActions.unshift({
|
||||
icon: <XCircle />,
|
||||
label: t("apiKeyManagement.revoke"),
|
||||
label: t("apiKeyManagement.actions.revoke"),
|
||||
onClick: handleRevoke,
|
||||
})
|
||||
}
|
||||
|
||||
const apiKeyStatus = getApiKeyStatusProps(apiKey.revoked_at, t)
|
||||
const apiKeyType = getApiKeyTypeProps(apiKey.type, t)
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{apiKey.title}</Heading>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<StatusBadge color={apiKey.revoked_at ? "red" : "green"}>
|
||||
{apiKey.revoked_at ? t("general.revoked") : t("general.active")}
|
||||
</StatusBadge>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<StatusBadge color={apiKeyStatus.color}>
|
||||
{apiKeyStatus.label}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
@@ -111,31 +120,59 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="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">
|
||||
{t("fields.key")}
|
||||
</Text>
|
||||
<div className="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">
|
||||
<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>
|
||||
<Copy
|
||||
content={apiKey.token}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
{apiKey.type !== "secret" && (
|
||||
<Copy
|
||||
content={apiKey.token}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="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">
|
||||
{t("apiKeyManagement.createdBy")}
|
||||
{t("fields.type")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{apiKeyType.label}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("apiKeyManagement.fields.lastUsedAtLabel")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{apiKey.last_used_at
|
||||
? getFullDate({ date: apiKey.last_used_at, includeTime: true })
|
||||
: "-"}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("apiKeyManagement.fields.createdByLabel")}
|
||||
</Text>
|
||||
<ActionBy userId={apiKey.created_by} />
|
||||
</div>
|
||||
{apiKey.revoked_at && (
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("apiKeyManagement.revokedBy")}
|
||||
{t("apiKeyManagement.fields.revokedByLabel")}
|
||||
</Text>
|
||||
<ActionBy userId={apiKey.revoked_by} />
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Container,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { PencilSquare, Plus, Trash } from "@medusajs/icons"
|
||||
import { AdminApiKeyResponse, AdminSalesChannelResponse } from "@medusajs/types"
|
||||
import { Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||
import {
|
||||
adminPublishableApiKeysKeys,
|
||||
useAdminCustomPost,
|
||||
useAdminRemovePublishableKeySalesChannelsBatch,
|
||||
} from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useBatchRemoveSalesChannelsFromApiKey } from "../../../../../hooks/api/api-keys"
|
||||
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||
import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
|
||||
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters"
|
||||
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { AdminApiKeyResponse, AdminSalesChannelResponse } from "@medusajs/types"
|
||||
import { useBatchRemoveSalesChannelsFromApiKey } from "../../../../../hooks/api/api-keys"
|
||||
|
||||
type ApiKeySalesChannelSectionProps = {
|
||||
apiKey: AdminApiKeyResponse["api_key"]
|
||||
@@ -49,8 +38,8 @@ export const ApiKeySalesChannelSection = ({
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns({ apiKey: apiKey.id })
|
||||
// const filters = useProductTableFilters(["sales_channel_id"])
|
||||
const columns = useColumns()
|
||||
const filters = useSalesChannelTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: sales_channels ?? [],
|
||||
@@ -64,6 +53,9 @@ export const ApiKeySalesChannelSection = ({
|
||||
state: rowSelection,
|
||||
updater: setRowSelection,
|
||||
},
|
||||
meta: {
|
||||
apiKey: apiKey.id,
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync } = useBatchRemoveSalesChannelsFromApiKey(apiKey.id)
|
||||
@@ -73,7 +65,7 @@ export const ApiKeySalesChannelSection = ({
|
||||
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("apiKeyManagement.removeSalesChannelsWarning", {
|
||||
description: t("apiKeyManagement.warnings.removeSalesChannels", {
|
||||
count: keys.length,
|
||||
}),
|
||||
confirmText: t("actions.continue"),
|
||||
@@ -100,19 +92,28 @@ export const ApiKeySalesChannelSection = ({
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("salesChannels.domain")}</Heading>
|
||||
<Button variant="secondary" size="small" asChild>
|
||||
<Link to="add-sales-channels">{t("general.add")}</Link>
|
||||
</Button>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Plus />,
|
||||
label: t("actions.add"),
|
||||
to: "sales-channels",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
filters={filters}
|
||||
count={count}
|
||||
pageSize={PAGE_SIZE}
|
||||
pagination
|
||||
search
|
||||
isLoading={isLoading}
|
||||
queryObject={raw}
|
||||
navigateTo={(row) => `/settings/sales-channels/${row.id}`}
|
||||
orderBy={["name", "created_at", "updated_at"]}
|
||||
commands={[
|
||||
{
|
||||
@@ -121,6 +122,9 @@ export const ApiKeySalesChannelSection = ({
|
||||
shortcut: "r",
|
||||
},
|
||||
]}
|
||||
pageSize={PAGE_SIZE}
|
||||
pagination
|
||||
search
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
@@ -141,7 +145,7 @@ const SalesChannelActions = ({
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("apiKeyManagement.removeSalesChannelWarning", {
|
||||
description: t("apiKeyManagement.warnings.removeSalesChannel", {
|
||||
name: salesChannel.name,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
@@ -186,8 +190,8 @@ const SalesChannelActions = ({
|
||||
const columnHelper =
|
||||
createColumnHelper<AdminSalesChannelResponse["sales_channel"]>()
|
||||
|
||||
const useColumns = ({ apiKey }: { apiKey: string }) => {
|
||||
const { t } = useTranslation()
|
||||
const useColumns = () => {
|
||||
const base = useSalesChannelTableColumns()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
@@ -219,36 +223,20 @@ const useColumns = ({ apiKey }: { apiKey: string }) => {
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.accessor("description", {
|
||||
header: t("fields.description"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.accessor("is_disabled", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
return (
|
||||
<div>
|
||||
<StatusBadge color={value ? "grey" : "green"}>
|
||||
{value ? t("general.disabled") : t("general.enabled")}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
...base,
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row, table }) => {
|
||||
const { apiKey } = table.options.meta as {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
return (
|
||||
<SalesChannelActions salesChannel={row.original} apiKey={apiKey} />
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
[base]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export const ApiKeyManagementEdit = () => {
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("apiKeyManagement.editKey")}</Heading>
|
||||
<Heading>{t("apiKeyManagement.edit.header")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && !!api_key && <EditApiKeyForm apiKey={api_key} />}
|
||||
</RouteDrawer>
|
||||
|
||||
@@ -1,13 +1,35 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, Outlet, useLocation } from "react-router-dom"
|
||||
import { ApiKeyType } from "../common/constants"
|
||||
import { ApiKeyManagementListTable } from "./components/api-key-management-list-table"
|
||||
|
||||
// TODO: Add secret API keys
|
||||
|
||||
export const ApiKeyManagementList = () => {
|
||||
const { pathname } = useLocation()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const keyType = pathname.includes("secret")
|
||||
? ApiKeyType.SECRET
|
||||
: ApiKeyType.PUBLISHABLE
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ApiKeyManagementListTable keyType="publishable" />
|
||||
{/* <ApiKeyManagementListTable keyType="secret" /> */}
|
||||
<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>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Link } from "react-router-dom"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useApiKeys } from "../../../../../hooks/api/api-keys"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { upperCaseFirst } from "../../../../../lib/uppercase-first"
|
||||
import { useApiKeyManagementTableColumns } from "./use-api-key-management-table-columns"
|
||||
import { useApiKeyManagementTableFilters } from "./use-api-key-management-table-filters"
|
||||
import { useApiKeyManagementTableQuery } from "./use-api-key-management-table-query"
|
||||
@@ -54,7 +53,9 @@ export const ApiKeyManagementListTable = ({
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">
|
||||
{t(`apiKeyManagement.domain`, { keyType: upperCaseFirst(keyType) })}
|
||||
{keyType === "publishable"
|
||||
? t(`apiKeyManagement.domain.publishable`)
|
||||
: t("apiKeyManagement.domain.secret")}
|
||||
</Heading>
|
||||
<Link to="create">
|
||||
<Button variant="secondary" size="small">
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { PencilSquare, Trash, XCircle } from "@medusajs/icons"
|
||||
import { AdminApiKeyResponse } from "@medusajs/types"
|
||||
import { usePrompt } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import {
|
||||
useDeleteApiKey,
|
||||
useRevokeApiKey,
|
||||
} from "../../../../../hooks/api/api-keys"
|
||||
|
||||
export const ApiKeyRowActions = ({
|
||||
apiKey,
|
||||
}: {
|
||||
apiKey: AdminApiKeyResponse["api_key"]
|
||||
}) => {
|
||||
const { mutateAsync: revokeAsync } = useRevokeApiKey(apiKey.id)
|
||||
const { mutateAsync: deleteAsync } = useDeleteApiKey(apiKey.id)
|
||||
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("apiKeyManagement.warnings.delete", {
|
||||
title: apiKey.title,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await deleteAsync()
|
||||
}
|
||||
|
||||
const handleRevoke = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("apiKeyManagement.warnings.revoke", {
|
||||
title: apiKey.title,
|
||||
}),
|
||||
confirmText: t("apiKeyManagement.actions.revoke"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await revokeAsync(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/api-key-management/${apiKey.id}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <XCircle />,
|
||||
label: t("apiKeyManagement.actions.revoke"),
|
||||
onClick: handleRevoke,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +1,14 @@
|
||||
import { PencilSquare, Trash, XCircle } from "@medusajs/icons"
|
||||
import { PublishableApiKey } from "@medusajs/medusa"
|
||||
import { Copy, Text, usePrompt } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import {
|
||||
adminPublishableApiKeysKeys,
|
||||
useAdminCustomDelete,
|
||||
useAdminCustomPost,
|
||||
} from "medusa-react"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { AdminApiKeyResponse } from "@medusajs/types"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Copy, Text, clx } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { MouseEvent, useMemo } from "react"
|
||||
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 {
|
||||
useDeleteApiKey,
|
||||
useRevokeApiKey,
|
||||
} from "../../../../../hooks/api/api-keys"
|
||||
import { TextCell } from "../../../../../components/table/table-cells/common/text-cell"
|
||||
import { getApiKeyStatusProps, getApiKeyTypeProps } from "../../../common/utils"
|
||||
import { ApiKeyRowActions } from "./api-key-row-actions"
|
||||
|
||||
const columnHelper = createColumnHelper<AdminApiKeyResponse["api_key"]>()
|
||||
|
||||
@@ -37,34 +29,61 @@ export const useApiKeyManagementTableColumns = () => {
|
||||
header: "Token",
|
||||
cell: ({ getValue, row }) => {
|
||||
const token = getValue()
|
||||
const isSecret = row.original.type === "secret"
|
||||
|
||||
const clickHandler = !isSecret
|
||||
? (e: MouseEvent) => e.stopPropagation()
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit max-w-[220px] cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
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>
|
||||
<Copy
|
||||
content={row.original.token}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
{!isSecret && (
|
||||
<Copy
|
||||
content={row.original.token}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("type", {
|
||||
header: t("fields.type"),
|
||||
cell: ({ getValue }) => {
|
||||
const { label } = getApiKeyTypeProps(getValue(), t)
|
||||
|
||||
return <TextCell text={label} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("revoked_at", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const revokedAt = getValue()
|
||||
const { color, label } = getApiKeyStatusProps(getValue(), t)
|
||||
|
||||
return (
|
||||
<StatusCell color={revokedAt ? "red" : "green"}>
|
||||
{revokedAt ? t("general.revoked") : t("general.active")}
|
||||
</StatusCell>
|
||||
)
|
||||
return <StatusCell color={color}>{label}</StatusCell>
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("last_used_at", {
|
||||
header: t("apiKeyManagement.table.lastUsedAtHeader"),
|
||||
cell: ({ getValue }) => {
|
||||
const date = getValue()
|
||||
|
||||
return <DateCell date={date} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("created_at", {
|
||||
@@ -78,82 +97,10 @@ export const useApiKeyManagementTableColumns = () => {
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return <ApiKeyActions apiKey={row.original as any} />
|
||||
return <ApiKeyRowActions apiKey={row.original as any} />
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
|
||||
export const ApiKeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
|
||||
const { mutateAsync: revokeAsync } = useRevokeApiKey(apiKey.id)
|
||||
const { mutateAsync: deleteAsync } = useDeleteApiKey(apiKey.id)
|
||||
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("apiKeyManagement.deleteKeyWarning", {
|
||||
title: apiKey.title,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await deleteAsync()
|
||||
}
|
||||
|
||||
const handleRevoke = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("apiKeyManagement.revokeKeyWarning", {
|
||||
title: apiKey.title,
|
||||
}),
|
||||
confirmText: t("apiKeyManagement.revoke"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await revokeAsync(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/api-key-management/${apiKey.id}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <XCircle />,
|
||||
label: t("apiKeyManagement.revoke"),
|
||||
onClick: handleRevoke,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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 = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
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
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
{isSecret && <ApiKeySalesChannelsFallback />}
|
||||
{ready && (
|
||||
<ApiKeySalesChannelsForm apiKey={id!} preSelected={preSelected} />
|
||||
)}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./api-key-sales-channels-fallback"
|
||||
@@ -1,5 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button, Checkbox, Hint, StatusBadge, Tooltip } from "@medusajs/ui"
|
||||
import { AdminSalesChannelResponse } from "@medusajs/types"
|
||||
import { Button, Checkbox, Hint, Tooltip } from "@medusajs/ui"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import {
|
||||
OnChangeFn,
|
||||
RowSelectionState,
|
||||
@@ -12,30 +14,29 @@ import * as zod from "zod"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../components/route-modal"
|
||||
import { useBatchAddSalesChannelsToApiKey } from "../../../../hooks/api/api-keys"
|
||||
import { useSalesChannelTableQuery } from "../../../../hooks/table/query/use-sales-channel-table-query"
|
||||
import { useDataTable } from "../../../../hooks/use-data-table"
|
||||
import { useSalesChannels } from "../../../../hooks/api/sales-channels"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { DataTable } from "../../../../components/table/data-table"
|
||||
import { AdminSalesChannelResponse } from "@medusajs/types"
|
||||
} from "../../../../../components/route-modal"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useBatchAddSalesChannelsToApiKey } from "../../../../../hooks/api/api-keys"
|
||||
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||
import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
|
||||
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
|
||||
type AddSalesChannelsToApiKeyFormProps = {
|
||||
type ApiKeySalesChannelFormProps = {
|
||||
apiKey: string
|
||||
preSelected: string[]
|
||||
preSelected?: string[]
|
||||
}
|
||||
|
||||
const AddSalesChannelsToApiKeySchema = zod.object({
|
||||
sales_channel_ids: zod.array(zod.string()).min(1),
|
||||
sales_channel_ids: zod.array(zod.string()),
|
||||
})
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export const AddSalesChannelsToApiKeyForm = ({
|
||||
export const ApiKeySalesChannelsForm = ({
|
||||
apiKey,
|
||||
preSelected,
|
||||
}: AddSalesChannelsToApiKeyFormProps) => {
|
||||
preSelected = [],
|
||||
}: ApiKeySalesChannelFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
@@ -50,9 +51,7 @@ export const AddSalesChannelsToApiKeyForm = ({
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
// @ts-ignore
|
||||
const { mutateAsync, isLoading: isMutating } =
|
||||
useBatchAddSalesChannelsToApiKey(apiKey)
|
||||
const { mutateAsync, isPending } = useBatchAddSalesChannelsToApiKey(apiKey)
|
||||
|
||||
const { raw, searchParams } = useSalesChannelTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
@@ -127,7 +126,7 @@ export const AddSalesChannelsToApiKeyForm = ({
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isMutating}>
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -155,6 +154,7 @@ const columnHelper =
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
const base = useSalesChannelTableColumns()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
@@ -200,32 +200,8 @@ const useColumns = () => {
|
||||
return Component
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.accessor("description", {
|
||||
header: t("fields.description"),
|
||||
cell: ({ getValue }) => (
|
||||
<div className="w-[200px] truncate">
|
||||
<span>{getValue()}</span>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("is_disabled", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
return (
|
||||
<div>
|
||||
<StatusBadge color={value ? "grey" : "green"}>
|
||||
{value ? t("general.disabled") : t("general.enabled")}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
...base,
|
||||
],
|
||||
[t]
|
||||
[t, base]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./api-key-sales-channels-form"
|
||||
@@ -1 +1 @@
|
||||
export { ApiKeyManagementAddSalesChannels as Component } from "./api-key-management-add-sales-channels"
|
||||
export { ApiKeyManagementAddSalesChannels as Component } from "./api-key-management-sales-channels"
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum ApiKeyType {
|
||||
SECRET = "secret",
|
||||
PUBLISHABLE = "publishable",
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { AdminApiKeyResponse } from "@medusajs/types"
|
||||
import { TFunction } from "i18next"
|
||||
import { ApiKeyType } from "./constants"
|
||||
|
||||
export function getApiKeyTypeFromPathname(pathname: string) {
|
||||
const isSecretKey = pathname.startsWith("/settings/api-key-management/secret")
|
||||
|
||||
switch (isSecretKey) {
|
||||
case true:
|
||||
return ApiKeyType.SECRET
|
||||
case false:
|
||||
return ApiKeyType.PUBLISHABLE
|
||||
}
|
||||
}
|
||||
|
||||
export function getApiKeyStatusProps(
|
||||
revokedAt: Date | string | null,
|
||||
t: TFunction
|
||||
): { color: "red" | "green"; label: string } {
|
||||
if (!revokedAt) {
|
||||
return {
|
||||
color: "green",
|
||||
label: t("apiKeyManagement.status.active"),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
color: "red",
|
||||
label: t("apiKeyManagement.status.revoked"),
|
||||
}
|
||||
}
|
||||
|
||||
export function getApiKeyTypeProps(
|
||||
type: AdminApiKeyResponse["api_key"]["type"],
|
||||
t: TFunction
|
||||
): { color: "green" | "blue"; label: string } {
|
||||
if (type === ApiKeyType.PUBLISHABLE) {
|
||||
return {
|
||||
color: "green",
|
||||
label: t("apiKeyManagement.type.publishable"),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
color: "blue",
|
||||
label: t("apiKeyManagement.type.secret"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user