From 687d7d824f45676c871f0025826449304f156696 Mon Sep 17 00:00:00 2001 From: John Williams Date: Tue, 7 Apr 2026 14:52:26 -0400 Subject: [PATCH] feat(indexd): add public key filtering for hosts and contracts --- .changeset/purple-impalas-beam.md | 5 ++ .../ContractFilterCmdGroups/PublicKey.tsx | 66 +++++++++++++++ .../ContractFilterCmdGroups/index.tsx | 8 +- .../ContractFilterNav/index.tsx | 14 +++- .../ContractsCmd/ContractsFilterCmd/index.tsx | 6 +- .../indexd/components/Data/Contracts/types.ts | 14 +++- .../Data/Contracts/useContracts.tsx | 4 + .../HostFilterCmdGroups/PublicKey.tsx | 66 +++++++++++++++ .../HostFilterCmdGroups/index.tsx | 4 +- .../HostsFilterCmd/HostFilterNav/index.tsx | 16 +++- .../Hosts/HostsCmd/HostsFilterCmd/index.tsx | 6 +- apps/indexd/components/Data/Hosts/types.ts | 9 ++ .../indexd/components/Data/Hosts/useHosts.tsx | 4 + apps/indexd/contexts/dialog.tsx | 12 +++ .../ContractsFilterPublicKeyDialog.tsx | 84 +++++++++++++++++++ .../dialogs/HostsFilterPublicKeyDialog.tsx | 84 +++++++++++++++++++ 16 files changed, 390 insertions(+), 12 deletions(-) create mode 100644 .changeset/purple-impalas-beam.md create mode 100644 apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterCmdGroups/PublicKey.tsx create mode 100644 apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterCmdGroups/PublicKey.tsx create mode 100644 apps/indexd/dialogs/ContractsFilterPublicKeyDialog.tsx create mode 100644 apps/indexd/dialogs/HostsFilterPublicKeyDialog.tsx diff --git a/.changeset/purple-impalas-beam.md b/.changeset/purple-impalas-beam.md new file mode 100644 index 000000000..62d0438e8 --- /dev/null +++ b/.changeset/purple-impalas-beam.md @@ -0,0 +1,5 @@ +--- +'indexd': minor +--- + +Added public key filtering for hosts and contracts. Closes https://github.com/SiaFoundation/indexd/issues/752 diff --git a/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterCmdGroups/PublicKey.tsx b/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterCmdGroups/PublicKey.tsx new file mode 100644 index 000000000..482ef5197 --- /dev/null +++ b/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterCmdGroups/PublicKey.tsx @@ -0,0 +1,66 @@ +import { + CommandGroup, + CommandItemNav, + CommandItemSearch, +} from '../../../../../CmdRoot/Item' +import { Page } from '../../../../../CmdRoot/types' +import { useDialog } from '../../../../../../contexts/dialog' + +export const contractsFilterPublicKeyPage = { + namespace: 'contracts/filterPublicKey', + label: 'Contracts filter by public key', +} + +export function PublicKeyCmdGroup({ + select, + currentPage, +}: { + currentPage: Page + select: () => void +}) { + const { openDialog } = useDialog() + return ( + + { + select() + openDialog('contractsFilterPublicKey') + }} + > + Filter by host public key + + + ) +} + +export function PublicKeyCmdNav({ + select, + currentPage, + parentPage, + commandPage, +}: { + currentPage: Page + parentPage?: Page + commandPage: Page + select: () => void +}) { + const { openDialog } = useDialog() + return ( + { + select() + openDialog('contractsFilterPublicKey') + }} + > + {contractsFilterPublicKeyPage.label} + + ) +} diff --git a/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterCmdGroups/index.tsx b/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterCmdGroups/index.tsx index 46998bce8..f2107a6e2 100644 --- a/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterCmdGroups/index.tsx +++ b/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterCmdGroups/index.tsx @@ -1,14 +1,18 @@ import { Page } from '../../../../../CmdRoot/types' import { StatusCmdGroup } from './Status' +import { PublicKeyCmdGroup } from './PublicKey' import { ContractFilter } from '../../../types' type Props = { currentPage: Page - select: (filter: ContractFilter) => void + select: (filter?: ContractFilter) => void } export function ContractFilterCmdGroups({ currentPage, select }: Props) { return ( - + <> + + + ) } diff --git a/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterNav/index.tsx b/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterNav/index.tsx index 9262c5b24..b17d5f1dd 100644 --- a/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterNav/index.tsx +++ b/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/ContractFilterNav/index.tsx @@ -1,6 +1,7 @@ import { CommandItemNav } from '../../../../../CmdRoot/Item' import { Page } from '../../../../../CmdRoot/types' import { contractsFilterStatusPage } from '../ContractFilterCmdGroups/Status' +import { PublicKeyCmdNav } from '../ContractFilterCmdGroups/PublicKey' import { ContractFilter } from '../../../types' export const commandPage = { @@ -12,16 +13,18 @@ type Props = { currentPage: Page parentPage?: Page pushPage: (page: Page) => void - select: (filter: ContractFilter) => void + select: (filter?: ContractFilter) => void } export function ContractFilterNav({ currentPage, parentPage, pushPage, + select, }: Props) { return ( - + {contractsFilterStatusPage.label} + + ) } diff --git a/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/index.tsx b/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/index.tsx index a63d53155..193887d15 100644 --- a/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/index.tsx +++ b/apps/indexd/components/Data/Contracts/ContractsCmd/ContractsFilterCmd/index.tsx @@ -21,11 +21,13 @@ export function ContractsFilterCmd({ const { addColumnFilter } = useContractsParams() const select = useCallback( - (filter: ContractFilter) => { + (filter?: ContractFilter) => { if (beforeSelect) { beforeSelect() } - addColumnFilter(filter) + if (filter) { + addColumnFilter(filter) + } if (afterSelect) { afterSelect() } diff --git a/apps/indexd/components/Data/Contracts/types.ts b/apps/indexd/components/Data/Contracts/types.ts index 5ec0010dd..279ee1571 100644 --- a/apps/indexd/components/Data/Contracts/types.ts +++ b/apps/indexd/components/Data/Contracts/types.ts @@ -1,4 +1,5 @@ import { AdminContractsSortBy, Contract } from '@siafoundation/indexd-types' +import { truncate } from '@siafoundation/design-system' import { CurrencyOption } from '@siafoundation/react-core' import BigNumber from 'bignumber.js' import { @@ -17,7 +18,15 @@ export type ContractFilterRevisable = { value: boolean } -export type ContractFilter = ContractFilterStatus | ContractFilterRevisable +export type ContractFilterPublicKey = { + id: 'hostkey' + value: string +} + +export type ContractFilter = + | ContractFilterStatus + | ContractFilterRevisable + | ContractFilterPublicKey export type ContractFilters = ContractFilter[] export type ContractSorts = DataTableSortColumn[] @@ -29,6 +38,9 @@ export function getFilterLabel(filter: ContractFilter): string { if (filter.id === 'revisable') { return filter.value ? 'Revisable' : 'Not revisable' } + if (filter.id === 'hostkey') { + return `Public key is ${truncate(filter.value, 20)}` + } return '' } diff --git a/apps/indexd/components/Data/Contracts/useContracts.tsx b/apps/indexd/components/Data/Contracts/useContracts.tsx index fc6ef4c46..a42877665 100644 --- a/apps/indexd/components/Data/Contracts/useContracts.tsx +++ b/apps/indexd/components/Data/Contracts/useContracts.tsx @@ -26,6 +26,10 @@ export function useContracts() { if (revisable !== undefined) { filters.revisable = revisable } + const hostkey = columnFilters.find((f) => f.id === 'hostkey')?.value + if (hostkey !== undefined) { + filters.hostkey = [hostkey] + } // Map all active sorts to API sortby and desc arrays. if (columnSorts.length > 0) { const sortby: AdminContractsSortBy[] = [] diff --git a/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterCmdGroups/PublicKey.tsx b/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterCmdGroups/PublicKey.tsx new file mode 100644 index 000000000..4f555a3a6 --- /dev/null +++ b/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterCmdGroups/PublicKey.tsx @@ -0,0 +1,66 @@ +import { + CommandGroup, + CommandItemNav, + CommandItemSearch, +} from '../../../../../CmdRoot/Item' +import { Page } from '../../../../../CmdRoot/types' +import { useDialog } from '../../../../../../contexts/dialog' + +export const hostsFilterPublicKeyPage = { + namespace: 'hosts/filterPublicKey', + label: 'Hosts filter by public key', +} + +export function PublicKeyCmdGroup({ + select, + currentPage, +}: { + currentPage: Page + select: () => void +}) { + const { openDialog } = useDialog() + return ( + + { + select() + openDialog('hostsFilterPublicKey') + }} + > + Filter by public key + + + ) +} + +export function PublicKeyCmdNav({ + select, + currentPage, + parentPage, + commandPage, +}: { + currentPage: Page + parentPage?: Page + commandPage: Page + select: () => void +}) { + const { openDialog } = useDialog() + return ( + { + select() + openDialog('hostsFilterPublicKey') + }} + > + {hostsFilterPublicKeyPage.label} + + ) +} diff --git a/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterCmdGroups/index.tsx b/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterCmdGroups/index.tsx index d202b2661..472844f60 100644 --- a/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterCmdGroups/index.tsx +++ b/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterCmdGroups/index.tsx @@ -2,11 +2,12 @@ import { Page } from '../../../../../CmdRoot/types' import { UsableCmdGroup } from './Usable' import { BlockedCmdGroup } from './Blocked' import { ActiveContractsCmdGroup } from './ActiveContracts' +import { PublicKeyCmdGroup } from './PublicKey' import { HostFilter } from '../../../types' type Props = { currentPage: Page - select: (filter: HostFilter) => void + select: (filter?: HostFilter) => void } export function HostFilterCmdGroups({ currentPage, select }: Props) { @@ -15,6 +16,7 @@ export function HostFilterCmdGroups({ currentPage, select }: Props) { + ) } diff --git a/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterNav/index.tsx b/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterNav/index.tsx index d8d45b1dc..461d81d8e 100644 --- a/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterNav/index.tsx +++ b/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/HostFilterNav/index.tsx @@ -3,6 +3,7 @@ import { Page } from '../../../../../CmdRoot/types' import { hostsFilterUsablePage } from '../HostFilterCmdGroups/Usable' import { hostsFilterBlockedPage } from '../HostFilterCmdGroups/Blocked' import { hostsFilterActiveContractsPage } from '../HostFilterCmdGroups/ActiveContracts' +import { PublicKeyCmdNav } from '../HostFilterCmdGroups/PublicKey' import { HostFilter } from '../../../types' export const commandPage = { @@ -14,10 +15,15 @@ type Props = { currentPage: Page parentPage?: Page pushPage: (page: Page) => void - select: (filter: HostFilter) => void + select: (filter?: HostFilter) => void } -export function HostFilterNav({ currentPage, parentPage, pushPage }: Props) { +export function HostFilterNav({ + currentPage, + parentPage, + pushPage, + select, +}: Props) { return ( <> {hostsFilterActiveContractsPage.label} + ) } diff --git a/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/index.tsx b/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/index.tsx index fb90eaa52..b79d1f7cd 100644 --- a/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/index.tsx +++ b/apps/indexd/components/Data/Hosts/HostsCmd/HostsFilterCmd/index.tsx @@ -21,11 +21,13 @@ export function HostsFilterCmd({ const { addColumnFilter } = useHostsParams() const select = useCallback( - (filter: HostFilter) => { + (filter?: HostFilter) => { if (beforeSelect) { beforeSelect() } - addColumnFilter(filter) + if (filter) { + addColumnFilter(filter) + } if (afterSelect) { afterSelect() } diff --git a/apps/indexd/components/Data/Hosts/types.ts b/apps/indexd/components/Data/Hosts/types.ts index ff955622e..0d9e94399 100644 --- a/apps/indexd/components/Data/Hosts/types.ts +++ b/apps/indexd/components/Data/Hosts/types.ts @@ -1,4 +1,5 @@ import { AdminHostsSortBy, Host } from '@siafoundation/indexd-types' +import { truncate } from '@siafoundation/design-system' import { CurrencyOption } from '@siafoundation/react-core' import BigNumber from 'bignumber.js' import { @@ -21,10 +22,16 @@ export type HostFilterActiveContracts = { value: boolean } +export type HostFilterPublicKey = { + id: 'hostkey' + value: string +} + export type HostFilter = | HostFilterUsable | HostFilterBlocked | HostFilterActiveContracts + | HostFilterPublicKey export type HostFilters = HostFilter[] export type HostSorts = DataTableSortColumn[] @@ -37,6 +44,8 @@ export function getFilterLabel(filter: HostFilter): string { return filter.value ? 'Blocked' : 'Not Blocked' case 'activecontracts': return filter.value ? 'Active Contracts' : 'No Active Contracts' + case 'hostkey': + return `Public key is ${truncate(filter.value, 20)}` default: return '' } diff --git a/apps/indexd/components/Data/Hosts/useHosts.tsx b/apps/indexd/components/Data/Hosts/useHosts.tsx index 8efe3521c..50131e644 100644 --- a/apps/indexd/components/Data/Hosts/useHosts.tsx +++ b/apps/indexd/components/Data/Hosts/useHosts.tsx @@ -27,6 +27,10 @@ export function useHosts() { if (activecontracts !== undefined) { filters.activecontracts = activecontracts } + const hostkey = columnFilters.find((f) => f.id === 'hostkey')?.value + if (hostkey !== undefined) { + filters.hostkey = [hostkey] + } // Map all active sorts to API sortby and desc arrays. if (columnSorts.length > 0) { const sortby: AdminHostsSortBy[] = [] diff --git a/apps/indexd/contexts/dialog.tsx b/apps/indexd/contexts/dialog.tsx index 83540bb77..d953a3e06 100644 --- a/apps/indexd/contexts/dialog.tsx +++ b/apps/indexd/contexts/dialog.tsx @@ -25,6 +25,8 @@ import { AccountFilterConnectKeyDialog } from '../dialogs/AccountFilterConnectKe import { QuotaCreateDialog } from '../dialogs/QuotaCreateDialog' import { QuotaDeleteDialog } from '../dialogs/QuotaDeleteDialog' import { KeyQuotaReassignDialog } from '../dialogs/KeyQuotaReassignDialog' +import { HostsFilterPublicKeyDialog } from '../dialogs/HostsFilterPublicKeyDialog' +import { ContractsFilterPublicKeyDialog } from '../dialogs/ContractsFilterPublicKeyDialog' export type DialogType = | 'cmdk' @@ -43,6 +45,8 @@ export type DialogType = | 'quotaDelete' | 'keyQuotaReassign' | 'hostBlocklistAdd' + | 'hostsFilterPublicKey' + | 'contractsFilterPublicKey' type DialogData = { hostBlocklistAdd?: { @@ -231,6 +235,14 @@ export function Dialogs() { open={dialog === 'accountFilterConnectKey'} onOpenChange={onOpenChange} /> + + ) } diff --git a/apps/indexd/dialogs/ContractsFilterPublicKeyDialog.tsx b/apps/indexd/dialogs/ContractsFilterPublicKeyDialog.tsx new file mode 100644 index 000000000..dd3d4873f --- /dev/null +++ b/apps/indexd/dialogs/ContractsFilterPublicKeyDialog.tsx @@ -0,0 +1,84 @@ +import { + Dialog, + useOnInvalid, + FormSubmitButton, + FieldText, + ConfigFields, + useDialogFormHelpers, +} from '@siafoundation/design-system' +import { useCallback } from 'react' +import { useForm } from 'react-hook-form' +import { useContractsParams } from '../components/Data/Contracts/useContractsParams' + +type Props = { + trigger?: React.ReactNode + open: boolean + onOpenChange: (val: boolean) => void +} + +const defaultValues = { + publicKey: '', +} + +type Values = typeof defaultValues + +const fields: ConfigFields = { + publicKey: { + type: 'text', + title: 'Public key', + placeholder: 'ed25519:b050c0c6...', + validation: { + required: 'required', + }, + }, +} + +export function ContractsFilterPublicKeyDialog({ + trigger, + open, + onOpenChange, +}: Props) { + const { addColumnFilter } = useContractsParams() + + const form = useForm({ + mode: 'all', + defaultValues, + }) + + const { closeAndReset, handleOpenChange } = useDialogFormHelpers({ + form, + onOpenChange, + defaultValues, + }) + + const onSubmit = useCallback( + async (values: Values) => { + addColumnFilter({ + id: 'hostkey', + value: values.publicKey, + }) + closeAndReset() + }, + [addColumnFilter, closeAndReset], + ) + + const onInvalid = useOnInvalid(fields) + + return ( + +
+ + Apply filter +
+
+ ) +} diff --git a/apps/indexd/dialogs/HostsFilterPublicKeyDialog.tsx b/apps/indexd/dialogs/HostsFilterPublicKeyDialog.tsx new file mode 100644 index 000000000..b5db585a5 --- /dev/null +++ b/apps/indexd/dialogs/HostsFilterPublicKeyDialog.tsx @@ -0,0 +1,84 @@ +import { + Dialog, + useOnInvalid, + FormSubmitButton, + FieldText, + ConfigFields, + useDialogFormHelpers, +} from '@siafoundation/design-system' +import { useCallback } from 'react' +import { useForm } from 'react-hook-form' +import { useHostsParams } from '../components/Data/Hosts/useHostsParams' + +type Props = { + trigger?: React.ReactNode + open: boolean + onOpenChange: (val: boolean) => void +} + +const defaultValues = { + publicKey: '', +} + +type Values = typeof defaultValues + +const fields: ConfigFields = { + publicKey: { + type: 'text', + title: 'Public key', + placeholder: 'ed25519:b050c0c6...', + validation: { + required: 'required', + }, + }, +} + +export function HostsFilterPublicKeyDialog({ + trigger, + open, + onOpenChange, +}: Props) { + const { addColumnFilter } = useHostsParams() + + const form = useForm({ + mode: 'all', + defaultValues, + }) + + const { closeAndReset, handleOpenChange } = useDialogFormHelpers({ + form, + onOpenChange, + defaultValues, + }) + + const onSubmit = useCallback( + async (values: Values) => { + addColumnFilter({ + id: 'hostkey', + value: values.publicKey, + }) + closeAndReset() + }, + [addColumnFilter, closeAndReset], + ) + + const onInvalid = useOnInvalid(fields) + + return ( + +
+ + Apply filter +
+
+ ) +}