Skip to content

Commit

Permalink
feat(renterd): keys batch operations
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Oct 16, 2024
1 parent 6a0eb54 commit c8599c7
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-comics-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renterd': minor
---

The key management table now supports multiselect and batch deletion.
2 changes: 1 addition & 1 deletion apps/renterd/components/Keys/KeyContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function KeyContextMenu({ s3Key, contentProps, buttonProps }: Props) {
return (
<DropdownMenu
trigger={
<Button variant="ghost" icon="hover" {...buttonProps}>
<Button icon="hover" size="none" {...buttonProps}>
<CaretDown16 />
</Button>
}
Expand Down
79 changes: 79 additions & 0 deletions apps/renterd/components/Keys/KeysBatchMenu/KeysBatchDelete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
Button,
Paragraph,
triggerSuccessToast,
triggerErrorToast,
} from '@siafoundation/design-system'
import { Delete16 } from '@siafoundation/react-icons'
import {
useSettingsS3,
useSettingsS3Update,
} from '@siafoundation/renterd-react'
import { useCallback, useMemo } from 'react'
import { omit } from '@technically/lodash'
import { useDialog } from '../../../contexts/dialog'
import { useKeys } from '../../../contexts/keys'

export function KeysBatchDelete() {
const { selectionMap, deselect } = useKeys()

const ids = useMemo(
() => Object.entries(selectionMap).map(([_, item]) => item.id),
[selectionMap]
)
const keys = useMemo(
() => Object.entries(selectionMap).map(([_, item]) => item.key),
[selectionMap]
)
const { openConfirmDialog } = useDialog()
const settingsS3 = useSettingsS3()
const settingsS3Update = useSettingsS3Update()
const deleteKeys = useCallback(async () => {
if (!settingsS3.data) {
triggerErrorToast({ title: 'Error deleting key' })
return
}
const newKeys = omit(settingsS3.data?.authentication.v4Keypairs, keys)
const response = await settingsS3Update.put({
payload: {
...settingsS3.data,
authentication: {
...settingsS3.data.authentication,
v4Keypairs: newKeys,
},
},
})
deselect(ids)
if (response.error) {
triggerErrorToast({ title: 'Error deleting keys', body: response.error })
} else {
triggerSuccessToast({ title: `Keys deleted` })
}
}, [settingsS3.data, settingsS3Update, deselect, keys, ids])

return (
<Button
tip="Delete keys"
onClick={() => {
openConfirmDialog({
title: `Delete keys`,
action: 'Remove',
variant: 'red',
body: (
<div className="flex flex-col gap-1">
<Paragraph size="14">
Are you sure you would like to delete the{' '}
{ids.length.toLocaleString()} selected keys?
</Paragraph>
</div>
),
onConfirm: async () => {
deleteKeys()
},
})
}}
>
<Delete16 />
</Button>
)
}
21 changes: 21 additions & 0 deletions apps/renterd/components/Keys/KeysBatchMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MultiSelectionMenu } from '@siafoundation/design-system'
import { useKeys } from '../../../contexts/keys'
import { KeysBatchDelete } from './KeysBatchDelete'

export function KeysBatchMenu() {
const { selectionCount, isPageAllSelected, pageCount, deselectAll } =
useKeys()

return (
<MultiSelectionMenu
isVisible={selectionCount > 0}
selectionCount={selectionCount}
isPageAllSelected={isPageAllSelected}
deselectAll={deselectAll}
pageCount={pageCount}
entityWord="key"
>
<KeysBatchDelete />
</MultiSelectionMenu>
)
}
4 changes: 4 additions & 0 deletions apps/renterd/components/Keys/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { StateNoneYet } from './StateNoneYet'
import { KeysActionsMenu } from './KeysActionsMenu'
import { StateError } from './StateError'
import { useKeys } from '../../contexts/keys'
import { KeysBatchMenu } from './KeysBatchMenu'

export function Keys() {
const { openDialog } = useDialog()
Expand All @@ -20,6 +21,7 @@ export function Keys() {
toggleSort,
limit,
dataState,
cellContext,
} = useKeys()

return (
Expand All @@ -31,6 +33,7 @@ export function Keys() {
actions={<KeysActionsMenu />}
>
<div className="p-6 min-w-fit">
<KeysBatchMenu />
<Table
isLoading={dataState === 'loading'}
emptyState={
Expand All @@ -45,6 +48,7 @@ export function Keys() {
sortableColumns={sortableColumns}
pageSize={limit}
data={datasetPage}
context={cellContext}
columns={columns}
sortDirection={sortDirection}
sortField={sortField}
Expand Down
29 changes: 24 additions & 5 deletions apps/renterd/contexts/keys/columns.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { TableColumn, ValueCopyable } from '@siafoundation/design-system'
import { KeyData, TableColumnId } from './types'
import {
Checkbox,
ControlGroup,
TableColumn,
ValueCopyable,
} from '@siafoundation/design-system'
import { KeyData, CellContext, TableColumnId } from './types'
import { KeyContextMenu } from '../../components/Keys/KeyContextMenu'

type KeysTableColumn = TableColumn<TableColumnId, KeyData, never> & {
type KeysTableColumn = TableColumn<TableColumnId, KeyData, CellContext> & {
fixed?: boolean
category?: string
}
Expand All @@ -12,8 +17,22 @@ export const columns: KeysTableColumn[] = [
id: 'actions',
label: '',
fixed: true,
cellClassName: 'w-[50px] !pl-2 !pr-4 [&+*]:!pl-0',
render: ({ data: { key } }) => <KeyContextMenu s3Key={key} />,
contentClassName: '!pl-3 !pr-4',
cellClassName: 'w-[20px] !pl-0 !pr-0',
heading: ({ context: { onSelectPage, isPageAllSelected } }) => (
<ControlGroup className="flex h-4">
<Checkbox onClick={onSelectPage} checked={isPageAllSelected} />
</ControlGroup>
),
render: ({ data: { id, key }, context: { selectionMap, onSelect } }) => (
<ControlGroup className="flex h-4">
<Checkbox
onClick={(e) => onSelect(id, e)}
checked={!!selectionMap[id]}
/>
<KeyContextMenu s3Key={key} />
</ControlGroup>
),
},
{
id: 'key',
Expand Down
30 changes: 30 additions & 0 deletions apps/renterd/contexts/keys/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import {
useDatasetEmptyState,
useClientFilters,
useClientFilteredDataset,
useMultiSelect,
} from '@siafoundation/design-system'
import { useRouter } from 'next/router'
import { createContext, useContext, useMemo } from 'react'
import {
CellContext,
KeyData,
columnsDefaultVisible,
defaultSortField,
Expand Down Expand Up @@ -99,6 +101,27 @@ function useKeysMain() {
filters
)

const {
onSelect,
deselect,
deselectAll,
selectionMap,
selectionCount,
onSelectPage,
isPageAllSelected,
} = useMultiSelect(dataset)

const cellContext = useMemo(
() =>
({
selectionMap,
onSelect,
onSelectPage,
isPageAllSelected,
} as CellContext),
[selectionMap, onSelect, onSelectPage, isPageAllSelected]
)

return {
dataState,
limit,
Expand All @@ -109,6 +132,13 @@ function useKeysMain() {
datasetCount: dataset?.length || 0,
datasetFilteredCount: datasetFiltered?.length || 0,
columns: filteredTableColumns,
selectionMap,
selectionCount,
onSelectPage,
isPageAllSelected,
deselect,
deselectAll,
cellContext,
dataset,
datasetPage,
configurableColumns,
Expand Down
11 changes: 10 additions & 1 deletion apps/renterd/contexts/keys/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { MouseEvent } from 'react'

export type KeyData = {
id: string
key: string
secret: string
}

export type TableColumnId = 'actions' | 'key' | 'secret'
export type CellContext = {
selectionMap: Record<string, KeyData>
onSelect: (id: string, e: MouseEvent<HTMLButtonElement>) => void
onSelectPage: () => void
isPageAllSelected: boolean | 'indeterminate'
}

export type TableColumnId = 'selection' | 'actions' | 'key' | 'secret'

export const columnsDefaultVisible: TableColumnId[] = ['key', 'secret']

Expand Down

0 comments on commit c8599c7

Please sign in to comment.