feat(dashboard,medusa): Update Pub. API key table and add query params to endpoint (#6483)
**What** - Updates table to use DataTable - Adds some query params to sort/filter the returned pub. keys
This commit is contained in:
committed by
GitHub
parent
557d86afbf
commit
e076590ff2
6
.changeset/shaggy-fishes-sit.md
Normal file
6
.changeset/shaggy-fishes-sit.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@medusajs/client-types": patch
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
fix(medusa): Add query params to Pub. API key endpoint
|
||||
@@ -411,6 +411,7 @@
|
||||
"product": "Product",
|
||||
"createdAt": "Created at",
|
||||
"updatedAt": "Updated at",
|
||||
"revokedAt": "Revoked at",
|
||||
"true": "True",
|
||||
"false": "False",
|
||||
"giftCard": "Gift Card",
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
useAdminUser,
|
||||
} from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||
import { UserLink } from "../../../../../components/common/user-link"
|
||||
@@ -24,6 +25,7 @@ type ApiKeyGeneralSectionProps = {
|
||||
|
||||
export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync: revokeAsync } = useAdminRevokePublishableApiKey(
|
||||
@@ -47,7 +49,11 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
return
|
||||
}
|
||||
|
||||
await deleteAsync()
|
||||
await deleteAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
navigate("..", { replace: true })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleRevoke = async () => {
|
||||
|
||||
@@ -1,34 +1,4 @@
|
||||
import { PublishableApiKey, SalesChannel } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Container,
|
||||
FocusModal,
|
||||
Heading,
|
||||
Hint,
|
||||
Input,
|
||||
Label,
|
||||
Table,
|
||||
Text,
|
||||
clx,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import {
|
||||
useAdminCreatePublishableApiKey,
|
||||
useAdminSalesChannels,
|
||||
} from "medusa-react"
|
||||
import { useMemo } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { Form } from "../../../components/common/form"
|
||||
import { ApiKeyManagementListTable } from "./components/api-key-management-list-table"
|
||||
|
||||
export const ApiKeyManagementList = () => {
|
||||
@@ -39,221 +9,3 @@ export const ApiKeyManagementList = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<PublishableApiKey>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.accessor("id", {
|
||||
header: "ID",
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
const CreatePublishableApiKeySchema = zod.object({
|
||||
title: zod.string().min(1),
|
||||
sales_channel_ids: zod.array(zod.string()).min(1),
|
||||
})
|
||||
|
||||
type CreatePublishableApiKeySchema = zod.infer<
|
||||
typeof CreatePublishableApiKeySchema
|
||||
>
|
||||
|
||||
type CreatePublishableApiKeyProps = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
const salesChannelColumnHelper = createColumnHelper<SalesChannel>()
|
||||
|
||||
const useSalesChannelColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
salesChannelColumnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
salesChannelColumnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
salesChannelColumnHelper.accessor("description", {
|
||||
header: t("fields.description"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
const CreatePublishableApiKey = (props: CreatePublishableApiKeyProps) => {
|
||||
const form = useForm<CreatePublishableApiKeySchema>({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
sales_channel_ids: [],
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync } = useAdminCreatePublishableApiKey()
|
||||
|
||||
const { sales_channels, isLoading, isError, error } = useAdminSalesChannels()
|
||||
const columns = useSalesChannelColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
data: sales_channels || [],
|
||||
columns: columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
const onSubmit = form.handleSubmit(async ({ title, sales_channel_ids }) => {
|
||||
await mutateAsync({
|
||||
title,
|
||||
})
|
||||
})
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<FocusModal {...props}>
|
||||
<Form {...form}>
|
||||
<FocusModal.Content>
|
||||
<FocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button size="small" type="submit">
|
||||
Publish API Key
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex flex-col items-center py-16">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-4">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Heading>Create API Key</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
Create and manage API keys. API keys are used to limit the
|
||||
scope of requests to specific sales channels.
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<div className="grid grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.title")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label weight="plus">Sales Channels</Label>
|
||||
<Hint></Hint>
|
||||
<Container className="overflow-hidden p-0">
|
||||
<div className="px-8 pb-4 pt-6">
|
||||
<Heading level="h2">Sales Channels</Heading>
|
||||
</div>
|
||||
<Table>
|
||||
<Table.Header>
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg last-of-type:border-b-0 [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FocusModal.Body>
|
||||
</FocusModal.Content>
|
||||
</Form>
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,78 +1,43 @@
|
||||
import { PencilSquare, Trash, XCircle } from "@medusajs/icons"
|
||||
import { PublishableApiKey } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Copy,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Table,
|
||||
Text,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { format } from "date-fns"
|
||||
import {
|
||||
useAdminDeletePublishableApiKey,
|
||||
useAdminPublishableApiKeys,
|
||||
useAdminRevokePublishableApiKey,
|
||||
} from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { Button, Container, Heading } from "@medusajs/ui"
|
||||
import { useAdminPublishableApiKeys } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
import { Link } from "react-router-dom"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
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"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const ApiKeyManagementListTable = () => {
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { searchParams, raw } = useApiKeyManagementTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const { publishable_api_keys, count, isLoading, isError, error } =
|
||||
useAdminPublishableApiKeys({})
|
||||
useAdminPublishableApiKeys(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
const filters = useApiKeyManagementTableFilters()
|
||||
const columns = useApiKeyManagementTableColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
const { table } = useDataTable({
|
||||
data: publishable_api_keys || [],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
@@ -91,222 +56,19 @@ export const ApiKeyManagementListTable = () => {
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
{(publishable_api_keys?.length ?? 0) > 0 ? (
|
||||
<div>
|
||||
<Table>
|
||||
<Table.Header className="border-t-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className="[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/4"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
}
|
||||
)}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/settings/api-key-management/${row.original.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<NoRecords
|
||||
action={{
|
||||
label: t("apiKeyManagement.createKey"),
|
||||
to: "create",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
filters={filters}
|
||||
columns={columns}
|
||||
count={count}
|
||||
pageSize={PAGE_SIZE}
|
||||
orderBy={["title", "created_at", "updated_at", "revoked_at"]}
|
||||
navigateTo={(row) => `/settings/api-key-management/${row.id}`}
|
||||
pagination
|
||||
search
|
||||
queryObject={raw}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
|
||||
const { mutateAsync: revokeAsync } = useAdminRevokePublishableApiKey(
|
||||
apiKey.id
|
||||
)
|
||||
const { mutateAsync: deleteAsync } = useAdminDeletePublishableApiKey(
|
||||
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()
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<PublishableApiKey>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.accessor("id", {
|
||||
header: "Key",
|
||||
cell: ({ getValue }) => {
|
||||
const token = getValue()
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit max-w-[220px] cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Text size="xsmall" leading="compact" className="truncate">
|
||||
{token}
|
||||
</Text>
|
||||
<Copy
|
||||
content={token}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("revoked_at", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const revokedAt = getValue()
|
||||
|
||||
return (
|
||||
<StatusBadge color={revokedAt ? "red" : "green"}>
|
||||
{revokedAt ? t("general.revoked") : t("general.active")}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("created_at", {
|
||||
header: t("fields.created"),
|
||||
cell: ({ getValue }) => {
|
||||
const date = getValue()
|
||||
|
||||
return format(new Date(date), "dd MMM, yyyy")
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return <KeyActions apiKey={row.original} />
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
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 {
|
||||
useAdminDeletePublishableApiKey,
|
||||
useAdminRevokePublishableApiKey,
|
||||
} from "medusa-react"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
|
||||
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<PublishableApiKey>()
|
||||
|
||||
export const useApiKeyManagementTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
cell: ({ getValue }) => (
|
||||
<div className="flex size-full items-center">
|
||||
<span className="truncate">{getValue()}</span>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("id", {
|
||||
header: "Key",
|
||||
cell: ({ getValue }) => {
|
||||
const token = getValue()
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit max-w-[220px] cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Text size="xsmall" leading="compact" className="truncate">
|
||||
{token}
|
||||
</Text>
|
||||
<Copy
|
||||
content={token}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("revoked_at", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const revokedAt = getValue()
|
||||
|
||||
return (
|
||||
<StatusCell color={revokedAt ? "red" : "green"}>
|
||||
{revokedAt ? t("general.revoked") : t("general.active")}
|
||||
</StatusCell>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("created_at", {
|
||||
header: t("fields.created"),
|
||||
cell: ({ getValue }) => {
|
||||
const date = getValue()
|
||||
|
||||
return <DateCell date={date} />
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return <ApiKeyActions apiKey={row.original} />
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
|
||||
const ApiKeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
|
||||
const { mutateAsync: revokeAsync } = useAdminRevokePublishableApiKey(
|
||||
apiKey.id
|
||||
)
|
||||
const { mutateAsync: deleteAsync } = useAdminDeletePublishableApiKey(
|
||||
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()
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/api-key-management/${apiKey.id}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <XCircle />,
|
||||
label: t("apiKeyManagement.revoke"),
|
||||
onClick: handleRevoke,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useApiKeyManagementTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
let filters: Filter[] = []
|
||||
|
||||
const dateFilters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
{ label: t("fields.revokedAt"), key: "revoked_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
filters = [...filters, ...dateFilters]
|
||||
|
||||
return filters
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { GetPublishableApiKeysParams } from "@medusajs/medusa"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
type UseApiKeyManagementTableQueryProps = {
|
||||
prefix?: string
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const useApiKeyManagementTableQuery = ({
|
||||
prefix,
|
||||
pageSize = 20,
|
||||
}: UseApiKeyManagementTableQueryProps) => {
|
||||
const queryObject = useQueryParams(
|
||||
["offset", "q", "created_at", "updated_at", "revoked_at", "order"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, created_at, updated_at, revoked_at, q, order } = queryObject
|
||||
|
||||
const searchParams: GetPublishableApiKeysParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
revoked_at: revoked_at ? JSON.parse(revoked_at) : undefined,
|
||||
order,
|
||||
q,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw: queryObject,
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,71 @@ export interface GetPublishableApiKeysParams {
|
||||
* Comma-separated fields that should be included in the returned publishable API keys.
|
||||
*/
|
||||
fields?: string
|
||||
/**
|
||||
* A field to sort-order the retrieved publishable API keys by.
|
||||
*/
|
||||
order?: string
|
||||
/**
|
||||
* Filter by a creation date range.
|
||||
*/
|
||||
created_at?: {
|
||||
/**
|
||||
* filter by dates less than this date
|
||||
*/
|
||||
lt?: string
|
||||
/**
|
||||
* filter by dates greater than this date
|
||||
*/
|
||||
gt?: string
|
||||
/**
|
||||
* filter by dates less than or equal to this date
|
||||
*/
|
||||
lte?: string
|
||||
/**
|
||||
* filter by dates greater than or equal to this date
|
||||
*/
|
||||
gte?: string
|
||||
}
|
||||
/**
|
||||
* Filter by a update date range.
|
||||
*/
|
||||
updated_at?: {
|
||||
/**
|
||||
* filter by dates less than this date
|
||||
*/
|
||||
lt?: string
|
||||
/**
|
||||
* filter by dates greater than this date
|
||||
*/
|
||||
gt?: string
|
||||
/**
|
||||
* filter by dates less than or equal to this date
|
||||
*/
|
||||
lte?: string
|
||||
/**
|
||||
* filter by dates greater than or equal to this date
|
||||
*/
|
||||
gte?: string
|
||||
}
|
||||
/**
|
||||
* Filter by a revocation date range.
|
||||
*/
|
||||
revoked_at?: {
|
||||
/**
|
||||
* filter by dates less than this date
|
||||
*/
|
||||
lt?: string
|
||||
/**
|
||||
* filter by dates greater than this date
|
||||
*/
|
||||
gt?: string
|
||||
/**
|
||||
* filter by dates less than or equal to this date
|
||||
*/
|
||||
lte?: string
|
||||
/**
|
||||
* filter by dates greater than or equal to this date
|
||||
*/
|
||||
gte?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { IsOptional, IsString, ValidateNested } from "class-validator"
|
||||
import { Request, Response } from "express"
|
||||
import { IsOptional, IsString } from "class-validator"
|
||||
|
||||
import { extendedFindParamsMixin } from "../../../../types/common"
|
||||
import { Type } from "class-transformer"
|
||||
import PublishableApiKeyService from "../../../../services/publishable-api-key"
|
||||
import {
|
||||
DateComparisonOperator,
|
||||
extendedFindParamsMixin,
|
||||
} from "../../../../types/common"
|
||||
|
||||
/**
|
||||
* @oas [get] /admin/publishable-api-keys
|
||||
@@ -16,6 +20,76 @@ import PublishableApiKeyService from "../../../../services/publishable-api-key"
|
||||
* - (query) offset=0 {number} The number of publishable API keys to skip when retrieving the publishable API keys.
|
||||
* - (query) expand {string} Comma-separated relations that should be expanded in the returned publishable API keys.
|
||||
* - (query) fields {string} Comma-separated fields that should be included in the returned publishable API keys.
|
||||
* - (query) order {string} A field to sort-order the retrieved publishable API keys by.
|
||||
* - in: query
|
||||
* name: created_at
|
||||
* required: false
|
||||
* description: Filter by a creation date range.
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* lt:
|
||||
* type: string
|
||||
* description: filter by dates less than this date
|
||||
* format: date
|
||||
* gt:
|
||||
* type: string
|
||||
* description: filter by dates greater than this date
|
||||
* format: date
|
||||
* lte:
|
||||
* type: string
|
||||
* description: filter by dates less than or equal to this date
|
||||
* format: date
|
||||
* gte:
|
||||
* type: string
|
||||
* description: filter by dates greater than or equal to this date
|
||||
* format: date
|
||||
* - in: query
|
||||
* name: updated_at
|
||||
* required: false
|
||||
* description: Filter by a update date range.
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* lt:
|
||||
* type: string
|
||||
* description: filter by dates less than this date
|
||||
* format: date
|
||||
* gt:
|
||||
* type: string
|
||||
* description: filter by dates greater than this date
|
||||
* format: date
|
||||
* lte:
|
||||
* type: string
|
||||
* description: filter by dates less than or equal to this date
|
||||
* format: date
|
||||
* gte:
|
||||
* type: string
|
||||
* description: filter by dates greater than or equal to this date
|
||||
* format: date
|
||||
* - in: query
|
||||
* name: revoked_at
|
||||
* required: false
|
||||
* description: Filter by a revocation date range.
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* lt:
|
||||
* type: string
|
||||
* description: filter by dates less than this date
|
||||
* format: date
|
||||
* gt:
|
||||
* type: string
|
||||
* description: filter by dates greater than this date
|
||||
* format: date
|
||||
* lte:
|
||||
* type: string
|
||||
* description: filter by dates less than or equal to this date
|
||||
* format: date
|
||||
* gte:
|
||||
* type: string
|
||||
* description: filter by dates greater than or equal to this date
|
||||
* format: date
|
||||
* x-codegen:
|
||||
* method: list
|
||||
* queryParams: GetPublishableApiKeysParams
|
||||
@@ -129,4 +203,35 @@ export class GetPublishableApiKeysParams extends extendedFindParamsMixin({
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
q?: string
|
||||
|
||||
/**
|
||||
* A field to sort-order the retrieved publishable API keys by.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
order?: string
|
||||
|
||||
/**
|
||||
* Date filters to apply on the publishable API keys' `created_at` date.
|
||||
*/
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
created_at?: DateComparisonOperator
|
||||
|
||||
/**
|
||||
* Date filters to apply on the publishable API keys' `updated_at` date.
|
||||
*/
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
updated_at?: DateComparisonOperator
|
||||
|
||||
/**
|
||||
* Date filters to apply on the publishable API keys' `revoked_at` date.
|
||||
*/
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
revoked_at?: DateComparisonOperator
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager, FindOptionsWhere, ILike } from "typeorm"
|
||||
|
||||
import { selectorConstraintsToString } from "@medusajs/utils"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { PublishableApiKey, SalesChannel } from "../models"
|
||||
import { PublishableApiKeyRepository } from "../repositories/publishable-api-key"
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
} from "../types/publishable-api-key"
|
||||
import { buildQuery, isString } from "../utils"
|
||||
import EventBusService from "./event-bus"
|
||||
import {selectorConstraintsToString} from "@medusajs/utils";
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
|
||||
Reference in New Issue
Block a user