From 21be6ff7ed7d39af002e642798f3e0e985ba72cd Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:11:32 +0200 Subject: [PATCH] feat(dashboard): Secret keys domain (#7030) * setup secret keys * add secret keys * fix merge --- .../public/locales/en-US/translation.json | 62 +++++-- .../settings-layout/settings-layout.tsx | 2 +- .../data-table-root/data-table-root.tsx | 2 +- .../common/date-cell/date-cell.tsx | 7 +- .../use-sales-channel-table-columns.tsx | 18 +- .../src/providers/router-provider/v2.tsx | 40 ++++- .../api-key-management-add-sales-channels.tsx | 27 --- .../components/index.ts | 1 - .../api-key-management-create.tsx | 7 +- .../create-publishable-api-key-form.tsx | 164 ++++++++++++------ .../api-key-management-detail.tsx | 8 +- .../api-key-general-section.tsx | 73 ++++++-- .../api-key-sales-channel-section.tsx | 94 +++++----- .../api-key-management-edit.tsx | 2 +- .../api-key-management-list.tsx | 32 +++- .../api-key-management-list-table.tsx | 5 +- .../api-key-row-actions.tsx | 85 +++++++++ .../use-api-key-management-table-columns.tsx | 151 ++++++---------- .../api-key-management-sales-channels.tsx | 36 ++++ .../api-key-sales-channels-fallback.tsx | 26 +++ .../api-key-sales-channels-fallback/index.ts | 1 + .../api-key-sales-channels-form.tsx} | 66 +++---- .../api-key-sales-channels-form/index.ts | 1 + .../index.ts | 2 +- .../api-key-management/common/constants.ts | 4 + .../api-key-management/common/utils.ts | 48 +++++ 26 files changed, 628 insertions(+), 336 deletions(-) delete mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/api-key-management-add-sales-channels.tsx delete mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/components/index.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-row-actions.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/api-key-management-sales-channels.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/api-key-sales-channels-fallback.tsx create mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/index.ts rename packages/admin-next/dashboard/src/v2-routes/api-key-management/{api-key-management-add-sales-channels/components/add-sales-channels-to-api-key-form.tsx => api-key-management-sales-channels/components/api-key-sales-channels-form/api-key-sales-channels-form.tsx} (73%) create mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-form/index.ts rename packages/admin-next/dashboard/src/v2-routes/api-key-management/{api-key-management-add-sales-channels => api-key-management-sales-channels}/index.ts (61%) create mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/common/constants.ts create mode 100644 packages/admin-next/dashboard/src/v2-routes/api-key-management/common/utils.ts diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index a8dc33319b..7328ee3288 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -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", diff --git a/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx index 95da98bba2..3f375a3f7d 100644 --- a/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx +++ b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx @@ -64,7 +64,7 @@ const useDeveloperRoutes = (): NavItemProps[] => { return useMemo( () => [ { - label: t("apiKeyManagement.domainTitle"), + label: t("apiKeyManagement.domain.apiKeys"), to: "/settings/api-key-management", }, { diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx index 0c959d8346..8d0f9d8d72 100644 --- a/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx @@ -193,7 +193,7 @@ export const DataTableRoot = ({ "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, } )} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/date-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/date-cell.tsx index adabb2a31d..8537c3e3c6 100644 --- a/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/date-cell.tsx +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/date-cell.tsx @@ -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 + } + const value = new Date(date) value.setMinutes(value.getMinutes() - value.getTimezoneOffset()) diff --git a/packages/admin-next/dashboard/src/hooks/table/columns/use-sales-channel-table-columns.tsx b/packages/admin-next/dashboard/src/hooks/table/columns/use-sales-channel-table-columns.tsx index 38ec913a79..b7b9b14dc9 100644 --- a/packages/admin-next/dashboard/src/hooks/table/columns/use-sales-channel-table-columns.tsx +++ b/packages/admin-next/dashboard/src/hooks/table/columns/use-sales-channel-table-columns.tsx @@ -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() export const useSalesChannelTableColumns = () => { + const { t } = useTranslation() + return useMemo( () => [ columnHelper.accessor("name", { @@ -24,7 +29,18 @@ export const useSalesChannelTableColumns = () => { header: () => , cell: ({ getValue }) => , }), + columnHelper.accessor("is_disabled", { + header: () => , + cell: ({ getValue }) => { + const value = getValue() + return ( + + {value ? t("general.disabled") : t("general.enabled")} + + ) + }, + }), ], - [] + [t] ) } diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx index c2ea4fc6c5..fa51791561 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx @@ -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: , 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" ), }, ], diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/api-key-management-add-sales-channels.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/api-key-management-add-sales-channels.tsx deleted file mode 100644 index b6815f8161..0000000000 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/api-key-management-add-sales-channels.tsx +++ /dev/null @@ -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 ( - - {!isLoading && !!sales_channels && ( - sc.id)} - /> - )} - - ) -} diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/components/index.ts b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/components/index.ts deleted file mode 100644 index 4e4781c229..0000000000 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./add-sales-channels-to-api-key-form" diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/api-key-management-create.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/api-key-management-create.tsx index e354f1834a..dfea999afb 100644 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/api-key-management-create.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/api-key-management-create.tsx @@ -1,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 ( - + ) } diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx index b832777645..c3a9a74d0a 100644 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx @@ -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 ( - -
- -
- - + + - - -
-
- -
-
-
- - {t("apiKeyManagement.createPublishableApiKey")} - - - {t("apiKeyManagement.publishableApiKeyHint")} - -
-
- { - return ( - - {t("fields.title")} - - - - - - ) - }} - /> +
+ + +
+
+
+ + {keyType === ApiKeyType.PUBLISHABLE + ? t("apiKeyManagement.create.createPublishableHeader") + : t("apiKeyManagement.create.createSecretHeader")} + + + {keyType === ApiKeyType.PUBLISHABLE + ? t("apiKeyManagement.create.createPublishableHint") + : t("apiKeyManagement.create.createSecretHint")} + +
+
+ { + return ( + + {t("fields.title")} + + + + + + ) + }} + /> +
+
+ + + + + + + {t("apiKeyManagement.create.secretKeyCreatedHeader")} + + + {t("apiKeyManagement.create.secretKeyCreatedHint")} + + +
+
+ + {createdKey?.token} + + +
- - - + + + {t("actions.continue")} + + +
+
+ ) } diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx index af76bb6039..168d84f478 100644 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx @@ -1,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
Loading...
} + const isPublishable = api_key?.type === ApiKeyType.PUBLISHABLE + if (isError) { throw error } @@ -26,7 +30,7 @@ export const ApiKeyManagementDetail = () => { return (
- + {isPublishable && }
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx index 6e42b7c49b..95ca2e061a 100644 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx @@ -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: , - 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 (
{apiKey.title} -
- - {apiKey.revoked_at ? t("general.revoked") : t("general.active")} - +
+
+ + {apiKeyStatus.label} + +
{ />
-
+
{t("fields.key")} -
+
{apiKey.redacted} - + {apiKey.type !== "secret" && ( + + )}
-
+
- {t("apiKeyManagement.createdBy")} + {t("fields.type")} + + + {apiKeyType.label} + +
+
+ + {t("apiKeyManagement.fields.lastUsedAtLabel")} + + + {apiKey.last_used_at + ? getFullDate({ date: apiKey.last_used_at, includeTime: true }) + : "-"} + +
+
+ + {t("apiKeyManagement.fields.createdByLabel")}
{apiKey.revoked_at && (
- {t("apiKeyManagement.revokedBy")} + {t("apiKeyManagement.fields.revokedByLabel")}
diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx index 666fd0d2a7..aaa014581c 100644 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx @@ -1,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 = ({
{t("salesChannels.domain")} - + , + label: t("actions.add"), + to: "sales-channels", + }, + ], + }, + ]} + />
`/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 />
) @@ -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() -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 ( -
- - {value ? t("general.disabled") : t("general.enabled")} - -
- ) - }, - }), + ...base, columnHelper.display({ id: "actions", cell: ({ row, table }) => { + const { apiKey } = table.options.meta as { + apiKey: string + } + return ( ) }, }), ], - [t] + [base] ) } diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-edit/api-key-management-edit.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-edit/api-key-management-edit.tsx index f6d4127d1a..cdf2ea8e90 100644 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-edit/api-key-management-edit.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-edit/api-key-management-edit.tsx @@ -18,7 +18,7 @@ export const ApiKeyManagementEdit = () => { return ( - {t("apiKeyManagement.editKey")} + {t("apiKeyManagement.edit.header")} {!isLoading && !!api_key && } diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/api-key-management-list.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/api-key-management-list.tsx index bfa5fab652..b2d84b23e2 100644 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/api-key-management-list.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/api-key-management-list.tsx @@ -1,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 (
- - {/* */} +
+ + {t("apiKeyManagement.tabs.publishable")} + + + {t("apiKeyManagement.tabs.secret")} + +
+
) diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx index 226efeaeec..2782c87da1 100644 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx @@ -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 = ({
- {t(`apiKeyManagement.domain`, { keyType: upperCaseFirst(keyType) })} + {keyType === "publishable" + ? t(`apiKeyManagement.domain.publishable`) + : t("apiKeyManagement.domain.secret")} + +
+ + + + {t("apiKeyManagement.warnings.salesChannelsSecretKey")} + + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/index.ts b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/index.ts new file mode 100644 index 0000000000..b2f5c51dfb --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-fallback/index.ts @@ -0,0 +1 @@ +export * from "./api-key-sales-channels-fallback" diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/components/add-sales-channels-to-api-key-form.tsx b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-form/api-key-sales-channels-form.tsx similarity index 73% rename from packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/components/add-sales-channels-to-api-key-form.tsx rename to packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-form/api-key-sales-channels-form.tsx index 3e2fc9383c..b35ddc8749 100644 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/components/add-sales-channels-to-api-key-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-form/api-key-sales-channels-form.tsx @@ -1,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({}) - // @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")} -
@@ -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 }) => ( -
- {getValue()} -
- ), - }), - columnHelper.accessor("is_disabled", { - header: t("fields.status"), - cell: ({ getValue }) => { - const value = getValue() - return ( -
- - {value ? t("general.disabled") : t("general.enabled")} - -
- ) - }, - }), + ...base, ], - [t] + [t, base] ) } diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-form/index.ts new file mode 100644 index 0000000000..ae0400b060 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/components/api-key-sales-channels-form/index.ts @@ -0,0 +1 @@ +export * from "./api-key-sales-channels-form" diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/index.ts b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/index.ts similarity index 61% rename from packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/index.ts rename to packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/index.ts index 8c278126e7..a44498a5be 100644 --- a/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-add-sales-channels/index.ts +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/api-key-management-sales-channels/index.ts @@ -1 +1 @@ -export { ApiKeyManagementAddSalesChannels as Component } from "./api-key-management-add-sales-channels" +export { ApiKeyManagementAddSalesChannels as Component } from "./api-key-management-sales-channels" diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/common/constants.ts b/packages/admin-next/dashboard/src/v2-routes/api-key-management/common/constants.ts new file mode 100644 index 0000000000..a20bca8e6b --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/common/constants.ts @@ -0,0 +1,4 @@ +export enum ApiKeyType { + SECRET = "secret", + PUBLISHABLE = "publishable", +} diff --git a/packages/admin-next/dashboard/src/v2-routes/api-key-management/common/utils.ts b/packages/admin-next/dashboard/src/v2-routes/api-key-management/common/utils.ts new file mode 100644 index 0000000000..defba1ff23 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/api-key-management/common/utils.ts @@ -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"), + } +}