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