diff --git a/hooks/use-data-table.tsx b/hooks/use-data-table.tsx
index 6ceb5f4..3c3580c 100644
--- a/hooks/use-data-table.tsx
+++ b/hooks/use-data-table.tsx
@@ -1,6 +1,7 @@
"use client"
import { Checkbox } from "@/components/ui/checkbox"
+import { resolveDataTablePagination } from "@/lib/data-table-pagination"
import type {
ColumnDef,
ColumnFiltersState,
@@ -23,6 +24,7 @@ export interface UseDataTableOptions
{
data: TData[]
columns: ColumnDef[]
pageSize?: number
+ disablePagination?: boolean
manualPagination?: boolean
manualSorting?: boolean
getRowId?: TableOptions["getRowId"]
@@ -62,12 +64,23 @@ function createSelectColumn(): ColumnDef {
}
export function useDataTable(options: UseDataTableOptions): UseDataTableReturn {
+ const paginationConfig = useMemo(
+ () =>
+ resolveDataTablePagination({
+ disablePagination: options.disablePagination,
+ manualPagination: options.manualPagination,
+ pageSize: options.pageSize,
+ dataLength: options.data.length,
+ }),
+ [options.data.length, options.disablePagination, options.manualPagination, options.pageSize],
+ )
+
const [sorting, setSorting] = useState(options.initialSorting ?? [])
const [columnFilters, setColumnFilters] = useState([])
const [rowSelection, setRowSelection] = useState({})
const [pagination, setPagination] = useState({
pageIndex: 0,
- pageSize: options.pageSize ?? 10,
+ pageSize: paginationConfig.pageSize,
})
const columns = useMemo(() => {
@@ -91,7 +104,7 @@ export function useDataTable(options: UseDataTableOptions
enableSorting: !options.manualSorting,
enableRowSelection: options.enableRowSelection ?? false,
manualSorting: options.manualSorting ?? false,
- manualPagination: options.manualPagination ?? false,
+ manualPagination: paginationConfig.manualPagination,
getRowId: options.getRowId,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
@@ -100,7 +113,7 @@ export function useDataTable(options: UseDataTableOptions
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
- getPaginationRowModel: options.manualPagination ? undefined : getPaginationRowModel(),
+ getPaginationRowModel: paginationConfig.manualPagination ? undefined : getPaginationRowModel(),
// Library resets pageIndex during render when data changes, which can trigger setState
// before commit (e.g. on route change). We do the reset in useEffect instead.
autoResetPageIndex: false,
@@ -112,6 +125,12 @@ export function useDataTable(options: UseDataTableOptions
setPagination((prev) => (prev.pageIndex === 0 ? prev : { ...prev, pageIndex: 0 }))
}, [options.data])
+ useEffect(() => {
+ setPagination((prev) =>
+ prev.pageSize === paginationConfig.pageSize ? prev : { ...prev, pageSize: paginationConfig.pageSize },
+ )
+ }, [paginationConfig.pageSize])
+
const selectedRows = table.getSelectedRowModel().rows.map((row) => row.original)
const selectedRowIds = table.getSelectedRowModel().rows.map((row) => row.id)
diff --git a/hooks/use-policies.ts b/hooks/use-policies.ts
index f0a0549..4238a93 100644
--- a/hooks/use-policies.ts
+++ b/hooks/use-policies.ts
@@ -47,6 +47,80 @@ export function usePolicies() {
[api],
)
+ const getPolicyByUserName = useCallback(
+ async (userName: string) => {
+ const userInfo = (await api.get(`/user-info?accessKey=${encodeURIComponent(userName)}`)) as {
+ policyName?: string
+ memberOf?: string[]
+ }
+
+ const directPolicyNames = userInfo?.policyName?.split(",").filter(Boolean) ?? []
+ const memberOf = userInfo?.memberOf ?? []
+ const groupPoliciesMap: Record = {}
+
+ if (memberOf.length > 0) {
+ await Promise.all(
+ memberOf.map(async (groupName) => {
+ const groupInfo = (await api.get(`/group?group=${encodeURIComponent(groupName)}`)) as { policy?: string }
+ const groupPolicyNames = groupInfo.policy?.split(",").filter(Boolean) ?? []
+ if (groupPolicyNames.length > 0) {
+ groupPoliciesMap[groupName] = groupPolicyNames
+ }
+ }),
+ )
+ }
+
+ const allPolicyNames = new Set()
+ directPolicyNames.forEach((policyName) => allPolicyNames.add(policyName))
+ Object.values(groupPoliciesMap).forEach((policyNames) => {
+ policyNames.forEach((policyName) => allPolicyNames.add(policyName))
+ })
+
+ const policyStatement: Record[] = []
+
+ if (allPolicyNames.size > 0) {
+ await Promise.all(
+ Array.from(allPolicyNames).map(async (policyName) => {
+ const policyInfo = (await getPolicy(policyName)) as { policy?: string }
+ const policyDocument = JSON.parse(policyInfo.policy ?? "{}") as {
+ Statement?: Record[]
+ }
+
+ if (!Array.isArray(policyDocument.Statement)) return
+
+ const groupOrigins = Object.entries(groupPoliciesMap)
+ .filter(([, policies]) => policies.includes(policyName))
+ .map(([groupName]) => groupName)
+
+ policyDocument.Statement.forEach((statement) => {
+ policyStatement.push({
+ ...statement,
+ Sid: statement.Sid ?? policyName,
+ Origin: {
+ type:
+ directPolicyNames.includes(policyName) && groupOrigins.length > 0
+ ? "both"
+ : directPolicyNames.includes(policyName)
+ ? "direct"
+ : "group",
+ groups: groupOrigins,
+ policyName,
+ },
+ })
+ })
+ }),
+ )
+ }
+
+ return {
+ ID: "",
+ Version: "2012-10-17",
+ Statement: policyStatement,
+ }
+ },
+ [api, getPolicy],
+ )
+
return {
listPolicies,
getPolicy,
@@ -56,5 +130,6 @@ export function usePolicies() {
listGroupsForPolicy,
setPolicyMultiple,
setUserOrGroupPolicy,
+ getPolicyByUserName,
}
}
diff --git a/hooks/use-users.ts b/hooks/use-users.ts
index dd99082..aa2cccc 100644
--- a/hooks/use-users.ts
+++ b/hooks/use-users.ts
@@ -2,6 +2,10 @@
import { useCallback } from "react"
import { useApiOptional } from "@/contexts/api-context"
+import {
+ buildCreateUserServiceAccountRequest,
+ buildListUserServiceAccountsRequest,
+} from "@/lib/user-service-account-api"
export function useUsers() {
const api = useApiOptional()
@@ -88,7 +92,8 @@ export function useUsers() {
const listAllUserServiceAccounts = useCallback(
async (name: string) => {
if (!api) return null
- return api.get(`/user/${encodeURIComponent(name)}/service-accounts`)
+ const request = buildListUserServiceAccountsRequest(name)
+ return api.get(request.url, { params: request.params })
},
[api],
)
@@ -96,7 +101,8 @@ export function useUsers() {
const createAUserServiceAccount = useCallback(
async (name: string, data: Record) => {
if (!api) return null
- return api.post(`/user/${encodeURIComponent(name)}/service-accounts`, data)
+ const request = buildCreateUserServiceAccountRequest(name, data)
+ return api.put(request.url, request.body)
},
[api],
)
diff --git a/lib/data-table-pagination.js b/lib/data-table-pagination.js
new file mode 100644
index 0000000..86973fd
--- /dev/null
+++ b/lib/data-table-pagination.js
@@ -0,0 +1 @@
+export { resolveDataTablePagination } from "./data-table-pagination.ts"
diff --git a/lib/data-table-pagination.ts b/lib/data-table-pagination.ts
new file mode 100644
index 0000000..86a89a8
--- /dev/null
+++ b/lib/data-table-pagination.ts
@@ -0,0 +1,18 @@
+interface ResolveDataTablePaginationOptions {
+ disablePagination?: boolean
+ manualPagination?: boolean
+ pageSize?: number
+ dataLength?: number
+}
+
+export function resolveDataTablePagination({
+ disablePagination = false,
+ manualPagination = false,
+ pageSize,
+ dataLength = 0,
+}: ResolveDataTablePaginationOptions) {
+ return {
+ manualPagination: disablePagination || manualPagination,
+ pageSize: disablePagination ? Math.max(dataLength, 1) : (pageSize ?? 10),
+ }
+}
diff --git a/lib/user-edit-tabs.js b/lib/user-edit-tabs.js
new file mode 100644
index 0000000..ac732cd
--- /dev/null
+++ b/lib/user-edit-tabs.js
@@ -0,0 +1 @@
+export { getAvailableUserEditTabs } from "./user-edit-tabs.ts"
diff --git a/lib/user-edit-tabs.ts b/lib/user-edit-tabs.ts
new file mode 100644
index 0000000..0f16c84
--- /dev/null
+++ b/lib/user-edit-tabs.ts
@@ -0,0 +1,24 @@
+export type UserEditTab = "account" | "groups" | "policy" | "access-keys"
+
+interface UserEditTabAvailability {
+ canEditAccount: boolean
+ canAssignGroups: boolean
+ canEditPolicies: boolean
+ canManageAccessKeys: boolean
+}
+
+export function getAvailableUserEditTabs({
+ canEditAccount,
+ canAssignGroups,
+ canEditPolicies,
+ canManageAccessKeys,
+}: UserEditTabAvailability): UserEditTab[] {
+ const tabs: UserEditTab[] = []
+
+ if (canEditAccount) tabs.push("account")
+ if (canAssignGroups) tabs.push("groups")
+ if (canEditPolicies) tabs.push("policy")
+ if (canManageAccessKeys) tabs.push("access-keys")
+
+ return tabs
+}
diff --git a/lib/user-service-account-api.js b/lib/user-service-account-api.js
new file mode 100644
index 0000000..168e7dd
--- /dev/null
+++ b/lib/user-service-account-api.js
@@ -0,0 +1,4 @@
+export {
+ buildCreateUserServiceAccountRequest,
+ buildListUserServiceAccountsRequest,
+} from "./user-service-account-api.ts"
diff --git a/lib/user-service-account-api.ts b/lib/user-service-account-api.ts
new file mode 100644
index 0000000..95b88f9
--- /dev/null
+++ b/lib/user-service-account-api.ts
@@ -0,0 +1,16 @@
+export function buildListUserServiceAccountsRequest(userName: string) {
+ return {
+ url: "/list-service-accounts",
+ params: { user: userName },
+ }
+}
+
+export function buildCreateUserServiceAccountRequest(userName: string, data: Record) {
+ return {
+ url: "/add-service-account",
+ body: {
+ targetUser: userName,
+ ...data,
+ },
+ }
+}
diff --git a/tests/lib/data-table-pagination.test.ts b/tests/lib/data-table-pagination.test.ts
new file mode 100644
index 0000000..aebfbcc
--- /dev/null
+++ b/tests/lib/data-table-pagination.test.ts
@@ -0,0 +1,33 @@
+import test from "node:test"
+import assert from "node:assert/strict"
+import { resolveDataTablePagination } from "../../lib/data-table-pagination.js"
+
+test("resolveDataTablePagination disables pagination and expands page size when requested", () => {
+ assert.deepEqual(
+ resolveDataTablePagination({
+ disablePagination: true,
+ manualPagination: false,
+ pageSize: 10,
+ dataLength: 27,
+ }),
+ {
+ manualPagination: true,
+ pageSize: 27,
+ },
+ )
+})
+
+test("resolveDataTablePagination keeps the configured page size when pagination stays enabled", () => {
+ assert.deepEqual(
+ resolveDataTablePagination({
+ disablePagination: false,
+ manualPagination: false,
+ pageSize: 10,
+ dataLength: 27,
+ }),
+ {
+ manualPagination: false,
+ pageSize: 10,
+ },
+ )
+})
diff --git a/tests/lib/user-edit-tabs.test.ts b/tests/lib/user-edit-tabs.test.ts
new file mode 100644
index 0000000..84d2503
--- /dev/null
+++ b/tests/lib/user-edit-tabs.test.ts
@@ -0,0 +1,27 @@
+import test from "node:test"
+import assert from "node:assert/strict"
+import { getAvailableUserEditTabs } from "../../lib/user-edit-tabs.js"
+
+test("getAvailableUserEditTabs includes access-keys when the user can manage service accounts", () => {
+ assert.deepEqual(
+ getAvailableUserEditTabs({
+ canEditAccount: true,
+ canAssignGroups: true,
+ canEditPolicies: true,
+ canManageAccessKeys: true,
+ }),
+ ["account", "groups", "policy", "access-keys"],
+ )
+})
+
+test("getAvailableUserEditTabs omits access-keys when the user lacks service account permissions", () => {
+ assert.deepEqual(
+ getAvailableUserEditTabs({
+ canEditAccount: true,
+ canAssignGroups: true,
+ canEditPolicies: true,
+ canManageAccessKeys: false,
+ }),
+ ["account", "groups", "policy"],
+ )
+})
diff --git a/tests/lib/user-service-account-api.test.ts b/tests/lib/user-service-account-api.test.ts
new file mode 100644
index 0000000..0cad2bc
--- /dev/null
+++ b/tests/lib/user-service-account-api.test.ts
@@ -0,0 +1,32 @@
+import test from "node:test"
+import assert from "node:assert/strict"
+import {
+ buildCreateUserServiceAccountRequest,
+ buildListUserServiceAccountsRequest,
+} from "../../lib/user-service-account-api.js"
+
+test("buildListUserServiceAccountsRequest targets the shared list endpoint with a user query", () => {
+ assert.deepEqual(buildListUserServiceAccountsRequest("testuser"), {
+ url: "/list-service-accounts",
+ params: { user: "testuser" },
+ })
+})
+
+test("buildCreateUserServiceAccountRequest targets the shared create endpoint with targetUser in the body", () => {
+ assert.deepEqual(
+ buildCreateUserServiceAccountRequest("testuser", {
+ accessKey: "svc-test",
+ secretKey: "secret123",
+ name: "svc",
+ }),
+ {
+ url: "/add-service-account",
+ body: {
+ targetUser: "testuser",
+ accessKey: "svc-test",
+ secretKey: "secret123",
+ name: "svc",
+ },
+ },
+ )
+})