mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-27 11:43:54 +00:00
UI (Keys Page) - Support cross filtering, filter by user id, filter by key hash (#10322)
* feat(filter.tsx): initial commit making filter component more generic - same style as user table filters
* refactor(all_keys_table.tsx): refactor to simplify update logic
* fix: partially revert changes - reduce scope of pr
* fix(filter_logic.tsx): fix filter update logic
* fix(all_keys_table.tsx): fix filtering + search logic
* refactor: cleanup unused params
* Revert "fix(all_keys_table.tsx): fix filtering + search logic"
This reverts commit 5fbc331970
.
* feat(filter_logic.tsx): allow filter by user id
* fix(key_management_endpoints.py): support filtering `/key/list` by key hash
Enables lookup by key hash on ui
* fix(key_list.tsx): fix update
* fix(key_management.py): fix linting error
* test: update testing
* fix(prometheus.py): fix key hash
* style(all_keys_table.tsx): style improvements
* test: fix test
This commit is contained in:
parent
339351c579
commit
c66c821f96
12 changed files with 292 additions and 350 deletions
|
@ -1000,9 +1000,9 @@ class PrometheusLogger(CustomLogger):
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
verbose_logger.debug("setting remaining tokens requests metric")
|
verbose_logger.debug("setting remaining tokens requests metric")
|
||||||
standard_logging_payload: Optional[StandardLoggingPayload] = (
|
standard_logging_payload: Optional[
|
||||||
request_kwargs.get("standard_logging_object")
|
StandardLoggingPayload
|
||||||
)
|
] = request_kwargs.get("standard_logging_object")
|
||||||
|
|
||||||
if standard_logging_payload is None:
|
if standard_logging_payload is None:
|
||||||
return
|
return
|
||||||
|
@ -1453,6 +1453,7 @@ class PrometheusLogger(CustomLogger):
|
||||||
user_id=None,
|
user_id=None,
|
||||||
team_id=None,
|
team_id=None,
|
||||||
key_alias=None,
|
key_alias=None,
|
||||||
|
key_hash=None,
|
||||||
exclude_team_id=UI_SESSION_TOKEN_TEAM_ID,
|
exclude_team_id=UI_SESSION_TOKEN_TEAM_ID,
|
||||||
return_full_object=True,
|
return_full_object=True,
|
||||||
organization_id=None,
|
organization_id=None,
|
||||||
|
@ -1771,10 +1772,10 @@ class PrometheusLogger(CustomLogger):
|
||||||
from litellm.integrations.custom_logger import CustomLogger
|
from litellm.integrations.custom_logger import CustomLogger
|
||||||
from litellm.integrations.prometheus import PrometheusLogger
|
from litellm.integrations.prometheus import PrometheusLogger
|
||||||
|
|
||||||
prometheus_loggers: List[CustomLogger] = (
|
prometheus_loggers: List[
|
||||||
litellm.logging_callback_manager.get_custom_loggers_for_type(
|
CustomLogger
|
||||||
callback_type=PrometheusLogger
|
] = litellm.logging_callback_manager.get_custom_loggers_for_type(
|
||||||
)
|
callback_type=PrometheusLogger
|
||||||
)
|
)
|
||||||
# we need to get the initialized prometheus logger instance(s) and call logger.initialize_remaining_budget_metrics() on them
|
# we need to get the initialized prometheus logger instance(s) and call logger.initialize_remaining_budget_metrics() on them
|
||||||
verbose_logger.debug("found %s prometheus loggers", len(prometheus_loggers))
|
verbose_logger.debug("found %s prometheus loggers", len(prometheus_loggers))
|
||||||
|
|
|
@ -1861,6 +1861,7 @@ async def validate_key_list_check(
|
||||||
team_id: Optional[str],
|
team_id: Optional[str],
|
||||||
organization_id: Optional[str],
|
organization_id: Optional[str],
|
||||||
key_alias: Optional[str],
|
key_alias: Optional[str],
|
||||||
|
key_hash: Optional[str],
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
) -> Optional[LiteLLM_UserTable]:
|
) -> Optional[LiteLLM_UserTable]:
|
||||||
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:
|
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:
|
||||||
|
@ -1924,6 +1925,31 @@ async def validate_key_list_check(
|
||||||
param="organization_id",
|
param="organization_id",
|
||||||
code=status.HTTP_403_FORBIDDEN,
|
code=status.HTTP_403_FORBIDDEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if key_hash:
|
||||||
|
try:
|
||||||
|
key_info = await prisma_client.db.litellm_verificationtoken.find_unique(
|
||||||
|
where={"token": key_hash},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise ProxyException(
|
||||||
|
message="Key Hash not found.",
|
||||||
|
type=ProxyErrorTypes.bad_request_error,
|
||||||
|
param="key_hash",
|
||||||
|
code=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
can_user_query_key_info = await _can_user_query_key_info(
|
||||||
|
user_api_key_dict=user_api_key_dict,
|
||||||
|
key=key_hash,
|
||||||
|
key_info=key_info,
|
||||||
|
)
|
||||||
|
if not can_user_query_key_info:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="You are not allowed to access this key's info. Your role={}".format(
|
||||||
|
user_api_key_dict.user_role
|
||||||
|
),
|
||||||
|
)
|
||||||
return complete_user_info
|
return complete_user_info
|
||||||
|
|
||||||
|
|
||||||
|
@ -1972,6 +1998,7 @@ async def list_keys(
|
||||||
organization_id: Optional[str] = Query(
|
organization_id: Optional[str] = Query(
|
||||||
None, description="Filter keys by organization ID"
|
None, description="Filter keys by organization ID"
|
||||||
),
|
),
|
||||||
|
key_hash: Optional[str] = Query(None, description="Filter keys by key hash"),
|
||||||
key_alias: Optional[str] = Query(None, description="Filter keys by key alias"),
|
key_alias: Optional[str] = Query(None, description="Filter keys by key alias"),
|
||||||
return_full_object: bool = Query(False, description="Return full key object"),
|
return_full_object: bool = Query(False, description="Return full key object"),
|
||||||
include_team_keys: bool = Query(
|
include_team_keys: bool = Query(
|
||||||
|
@ -2004,6 +2031,7 @@ async def list_keys(
|
||||||
team_id=team_id,
|
team_id=team_id,
|
||||||
organization_id=organization_id,
|
organization_id=organization_id,
|
||||||
key_alias=key_alias,
|
key_alias=key_alias,
|
||||||
|
key_hash=key_hash,
|
||||||
prisma_client=prisma_client,
|
prisma_client=prisma_client,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2029,6 +2057,7 @@ async def list_keys(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
team_id=team_id,
|
team_id=team_id,
|
||||||
key_alias=key_alias,
|
key_alias=key_alias,
|
||||||
|
key_hash=key_hash,
|
||||||
return_full_object=return_full_object,
|
return_full_object=return_full_object,
|
||||||
organization_id=organization_id,
|
organization_id=organization_id,
|
||||||
admin_team_ids=admin_team_ids,
|
admin_team_ids=admin_team_ids,
|
||||||
|
@ -2065,6 +2094,7 @@ async def _list_key_helper(
|
||||||
team_id: Optional[str],
|
team_id: Optional[str],
|
||||||
organization_id: Optional[str],
|
organization_id: Optional[str],
|
||||||
key_alias: Optional[str],
|
key_alias: Optional[str],
|
||||||
|
key_hash: Optional[str],
|
||||||
exclude_team_id: Optional[str] = None,
|
exclude_team_id: Optional[str] = None,
|
||||||
return_full_object: bool = False,
|
return_full_object: bool = False,
|
||||||
admin_team_ids: Optional[
|
admin_team_ids: Optional[
|
||||||
|
@ -2111,6 +2141,8 @@ async def _list_key_helper(
|
||||||
user_condition["team_id"] = {"not": exclude_team_id}
|
user_condition["team_id"] = {"not": exclude_team_id}
|
||||||
if organization_id and isinstance(organization_id, str):
|
if organization_id and isinstance(organization_id, str):
|
||||||
user_condition["organization_id"] = organization_id
|
user_condition["organization_id"] = organization_id
|
||||||
|
if key_hash and isinstance(key_hash, str):
|
||||||
|
user_condition["token"] = key_hash
|
||||||
|
|
||||||
if user_condition:
|
if user_condition:
|
||||||
or_conditions.append(user_condition)
|
or_conditions.append(user_condition)
|
||||||
|
|
|
@ -30,6 +30,7 @@ async def test_list_keys():
|
||||||
"team_id": None,
|
"team_id": None,
|
||||||
"organization_id": None,
|
"organization_id": None,
|
||||||
"key_alias": None,
|
"key_alias": None,
|
||||||
|
"key_hash": None,
|
||||||
"exclude_team_id": None,
|
"exclude_team_id": None,
|
||||||
"return_full_object": True,
|
"return_full_object": True,
|
||||||
"admin_team_ids": ["28bd3181-02c5-48f2-b408-ce790fb3d5ba"],
|
"admin_team_ids": ["28bd3181-02c5-48f2-b408-ce790fb3d5ba"],
|
||||||
|
|
|
@ -989,6 +989,7 @@ async def test_list_key_helper(prisma_client):
|
||||||
user_id=None,
|
user_id=None,
|
||||||
team_id=None,
|
team_id=None,
|
||||||
key_alias=None,
|
key_alias=None,
|
||||||
|
key_hash=None,
|
||||||
organization_id=None,
|
organization_id=None,
|
||||||
)
|
)
|
||||||
assert len(result["keys"]) == 2, "Should return exactly 2 keys"
|
assert len(result["keys"]) == 2, "Should return exactly 2 keys"
|
||||||
|
@ -1004,6 +1005,7 @@ async def test_list_key_helper(prisma_client):
|
||||||
user_id=test_user_id,
|
user_id=test_user_id,
|
||||||
team_id=None,
|
team_id=None,
|
||||||
key_alias=None,
|
key_alias=None,
|
||||||
|
key_hash=None,
|
||||||
organization_id=None,
|
organization_id=None,
|
||||||
)
|
)
|
||||||
assert len(result["keys"]) == 3, "Should return exactly 3 keys for test user"
|
assert len(result["keys"]) == 3, "Should return exactly 3 keys for test user"
|
||||||
|
@ -1016,6 +1018,7 @@ async def test_list_key_helper(prisma_client):
|
||||||
user_id=None,
|
user_id=None,
|
||||||
team_id=test_team_id,
|
team_id=test_team_id,
|
||||||
key_alias=None,
|
key_alias=None,
|
||||||
|
key_hash=None,
|
||||||
organization_id=None,
|
organization_id=None,
|
||||||
)
|
)
|
||||||
assert len(result["keys"]) == 2, "Should return exactly 2 keys for test team"
|
assert len(result["keys"]) == 2, "Should return exactly 2 keys for test team"
|
||||||
|
@ -1028,6 +1031,7 @@ async def test_list_key_helper(prisma_client):
|
||||||
user_id=None,
|
user_id=None,
|
||||||
team_id=None,
|
team_id=None,
|
||||||
key_alias=test_key_alias,
|
key_alias=test_key_alias,
|
||||||
|
key_hash=None,
|
||||||
organization_id=None,
|
organization_id=None,
|
||||||
)
|
)
|
||||||
assert len(result["keys"]) == 1, "Should return exactly 1 key with test alias"
|
assert len(result["keys"]) == 1, "Should return exactly 1 key with test alias"
|
||||||
|
@ -1040,6 +1044,7 @@ async def test_list_key_helper(prisma_client):
|
||||||
user_id=test_user_id,
|
user_id=test_user_id,
|
||||||
team_id=None,
|
team_id=None,
|
||||||
key_alias=None,
|
key_alias=None,
|
||||||
|
key_hash=None,
|
||||||
return_full_object=True,
|
return_full_object=True,
|
||||||
organization_id=None,
|
organization_id=None,
|
||||||
)
|
)
|
||||||
|
@ -1141,6 +1146,7 @@ async def test_list_key_helper_team_filtering(prisma_client):
|
||||||
user_id=None,
|
user_id=None,
|
||||||
team_id=None,
|
team_id=None,
|
||||||
key_alias=None,
|
key_alias=None,
|
||||||
|
key_hash=None,
|
||||||
return_full_object=True,
|
return_full_object=True,
|
||||||
organization_id=None,
|
organization_id=None,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState, useCallback, useRef } from "react";
|
||||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||||
import { DataTable } from "./view_logs/table";
|
import { DataTable } from "./view_logs/table";
|
||||||
import { Select, SelectItem } from "@tremor/react"
|
import { Select, SelectItem } from "@tremor/react"
|
||||||
|
@ -9,13 +9,16 @@ import { Tooltip } from "antd";
|
||||||
import { Team, KeyResponse } from "./key_team_helpers/key_list";
|
import { Team, KeyResponse } from "./key_team_helpers/key_list";
|
||||||
import FilterComponent from "./common_components/filter";
|
import FilterComponent from "./common_components/filter";
|
||||||
import { FilterOption } from "./common_components/filter";
|
import { FilterOption } from "./common_components/filter";
|
||||||
import { Organization, userListCall } from "./networking";
|
import { keyListCall, Organization, userListCall } from "./networking";
|
||||||
import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn";
|
import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn";
|
||||||
import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn";
|
import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn";
|
||||||
import { useFilterLogic } from "./key_team_helpers/filter_logic";
|
import { useFilterLogic } from "./key_team_helpers/filter_logic";
|
||||||
import { Setter } from "@/types";
|
import { Setter } from "@/types";
|
||||||
import { updateExistingKeys } from "@/utils/dataUtils";
|
import { updateExistingKeys } from "@/utils/dataUtils";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import { defaultPageSize } from "./constants";
|
||||||
|
import { fetchAllTeams } from "./key_team_helpers/filter_helpers";
|
||||||
|
import { fetchAllOrganizations } from "./key_team_helpers/filter_helpers";
|
||||||
interface AllKeysTableProps {
|
interface AllKeysTableProps {
|
||||||
keys: KeyResponse[];
|
keys: KeyResponse[];
|
||||||
setKeys: Setter<KeyResponse[]>;
|
setKeys: Setter<KeyResponse[]>;
|
||||||
|
@ -90,6 +93,8 @@ const TeamFilter = ({
|
||||||
* AllKeysTable – a new table for keys that mimics the table styling used in view_logs.
|
* AllKeysTable – a new table for keys that mimics the table styling used in view_logs.
|
||||||
* The team selector and filtering have been removed so that all keys are shown.
|
* The team selector and filtering have been removed so that all keys are shown.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
export function AllKeysTable({
|
export function AllKeysTable({
|
||||||
keys,
|
keys,
|
||||||
setKeys,
|
setKeys,
|
||||||
|
@ -111,8 +116,8 @@ export function AllKeysTable({
|
||||||
}: AllKeysTableProps) {
|
}: AllKeysTableProps) {
|
||||||
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null);
|
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null);
|
||||||
const [userList, setUserList] = useState<UserResponse[]>([]);
|
const [userList, setUserList] = useState<UserResponse[]>([]);
|
||||||
|
|
||||||
// Use the filter logic hook
|
// Use the filter logic hook
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filters,
|
filters,
|
||||||
filteredKeys,
|
filteredKeys,
|
||||||
|
@ -126,11 +131,10 @@ export function AllKeysTable({
|
||||||
teams,
|
teams,
|
||||||
organizations,
|
organizations,
|
||||||
accessToken,
|
accessToken,
|
||||||
setSelectedTeam,
|
|
||||||
setCurrentOrg,
|
|
||||||
setSelectedKeyAlias
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
const user_IDs = keys.map(key => key.user_id).filter(id => id !== null);
|
const user_IDs = keys.map(key => key.user_id).filter(id => id !== null);
|
||||||
|
@ -208,7 +212,7 @@ export function AllKeysTable({
|
||||||
accessorKey: "team_id", // Change to access the team_id
|
accessorKey: "team_id", // Change to access the team_id
|
||||||
cell: ({ row, getValue }) => {
|
cell: ({ row, getValue }) => {
|
||||||
const teamId = getValue() as string;
|
const teamId = getValue() as string;
|
||||||
const team = allTeams?.find(t => t.team_id === teamId);
|
const team = teams?.find(t => t.team_id === teamId);
|
||||||
return team?.team_alias || "Unknown";
|
return team?.team_alias || "Unknown";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -379,7 +383,18 @@ export function AllKeysTable({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'User ID',
|
||||||
|
label: 'User ID',
|
||||||
|
isSearchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Key Hash',
|
||||||
|
label: 'Key Hash',
|
||||||
|
isSearchable: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@ -414,34 +429,35 @@ export function AllKeysTable({
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="border-b py-4 flex-1 overflow-hidden">
|
<div className="border-b py-4 flex-1 overflow-hidden">
|
||||||
<div className="flex items-center justify-between w-full mb-2">
|
<div className="w-full mb-6">
|
||||||
<FilterComponent options={filterOptions} onApplyFilters={handleFilterChange} initialValues={filters} onResetFilters={handleFilterReset}/>
|
<FilterComponent options={filterOptions} onApplyFilters={handleFilterChange} initialValues={filters} onResetFilters={handleFilterReset}/>
|
||||||
<div className="flex items-center gap-4">
|
</div>
|
||||||
<span className="inline-flex text-sm text-gray-700">
|
|
||||||
Showing {isLoading ? "..." : `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`} of {isLoading ? "..." : pagination.totalCount} results
|
<div className="flex items-center justify-between w-full mb-4">
|
||||||
|
<span className="inline-flex text-sm text-gray-700">
|
||||||
|
Showing {isLoading ? "..." : `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`} of {isLoading ? "..." : pagination.totalCount} results
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="inline-flex items-center gap-2">
|
||||||
|
<span className="text-sm text-gray-700">
|
||||||
|
Page {isLoading ? "..." : pagination.currentPage} of {isLoading ? "..." : pagination.totalPages}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="inline-flex items-center gap-2">
|
<button
|
||||||
<span className="text-sm text-gray-700">
|
onClick={() => onPageChange(pagination.currentPage - 1)}
|
||||||
Page {isLoading ? "..." : pagination.currentPage} of {isLoading ? "..." : pagination.totalPages}
|
disabled={isLoading || pagination.currentPage === 1}
|
||||||
</span>
|
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onPageChange(pagination.currentPage - 1)}
|
onClick={() => onPageChange(pagination.currentPage + 1)}
|
||||||
disabled={isLoading || pagination.currentPage === 1}
|
disabled={isLoading || pagination.currentPage === pagination.totalPages}
|
||||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Previous
|
Next
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => onPageChange(pagination.currentPage + 1)}
|
|
||||||
disabled={isLoading || pagination.currentPage === pagination.totalPages}
|
|
||||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[75vh] overflow-auto">
|
<div className="h-[75vh] overflow-auto">
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { Button, Input, Dropdown, MenuProps, Select, Spin } from 'antd';
|
import { Button, Input, Select } from 'antd';
|
||||||
import { Card, Button as TremorButton } from '@tremor/react';
|
import { FilterIcon } from '@heroicons/react/outline';
|
||||||
import {
|
|
||||||
FilterIcon,
|
|
||||||
XIcon,
|
|
||||||
CheckIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronUpIcon,
|
|
||||||
SearchIcon
|
|
||||||
} from '@heroicons/react/outline';
|
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
export interface FilterOption {
|
export interface FilterOption {
|
||||||
|
@ -35,257 +27,114 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
||||||
onApplyFilters,
|
onApplyFilters,
|
||||||
onResetFilters,
|
onResetFilters,
|
||||||
initialValues = {},
|
initialValues = {},
|
||||||
buttonLabel = "Filter",
|
buttonLabel = "Filters",
|
||||||
}) => {
|
}) => {
|
||||||
const [showFilters, setShowFilters] = useState<boolean>(false);
|
const [showFilters, setShowFilters] = useState<boolean>(false);
|
||||||
const [selectedFilter, setSelectedFilter] = useState<string>(options[0]?.name || '');
|
|
||||||
const [filterValues, setFilterValues] = useState<FilterValues>(initialValues);
|
|
||||||
const [tempValues, setTempValues] = useState<FilterValues>(initialValues);
|
const [tempValues, setTempValues] = useState<FilterValues>(initialValues);
|
||||||
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
|
const [searchOptionsMap, setSearchOptionsMap] = useState<{ [key: string]: Array<{ label: string; value: string }> }>({});
|
||||||
const [searchOptions, setSearchOptions] = useState<Array<{ label: string; value: string }>>([]);
|
const [searchLoadingMap, setSearchLoadingMap] = useState<{ [key: string]: boolean }>({});
|
||||||
const [searchLoading, setSearchLoading] = useState<boolean>(false);
|
const [searchInputValueMap, setSearchInputValueMap] = useState<{ [key: string]: string }>({});
|
||||||
const [searchInputValue, setSearchInputValue] = useState<string>('');
|
|
||||||
|
|
||||||
const filtersRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
if (filtersRef.current &&
|
|
||||||
!filtersRef.current.contains(target) &&
|
|
||||||
!target.closest('.ant-dropdown') &&
|
|
||||||
!target.closest('.ant-select-dropdown')) {
|
|
||||||
setShowFilters(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (options.length > 0 && options[0].isSearchable && options[0].searchFn) {
|
|
||||||
loadInitialOptions(options[0]);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadInitialOptions = async (option: FilterOption) => {
|
|
||||||
if (!option.isSearchable || !option.searchFn) return;
|
|
||||||
|
|
||||||
setSearchLoading(true);
|
|
||||||
try {
|
|
||||||
const results = await option.searchFn('');
|
|
||||||
setSearchOptions(results);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading initial options:', error);
|
|
||||||
setSearchOptions([]);
|
|
||||||
} finally {
|
|
||||||
setSearchLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showFilters && currentOption?.isSearchable && currentOption?.searchFn) {
|
|
||||||
loadInitialOptions(currentOption);
|
|
||||||
}
|
|
||||||
}, [showFilters, selectedFilter]);
|
|
||||||
|
|
||||||
const handleFilterSelect = (key: string) => {
|
|
||||||
setSelectedFilter(key);
|
|
||||||
setDropdownOpen(false);
|
|
||||||
|
|
||||||
const newOption = options.find(opt => opt.name === key);
|
|
||||||
if (newOption?.isSearchable && newOption?.searchFn) {
|
|
||||||
loadInitialOptions(newOption);
|
|
||||||
} else {
|
|
||||||
setSearchOptions([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedSearch = useCallback(
|
const debouncedSearch = useCallback(
|
||||||
debounce(async (value: string, option: FilterOption) => {
|
debounce(async (value: string, option: FilterOption) => {
|
||||||
if (!option.isSearchable || !option.searchFn) return;
|
if (!option.isSearchable || !option.searchFn) return;
|
||||||
|
|
||||||
setSearchLoading(true);
|
setSearchLoadingMap(prev => ({ ...prev, [option.name]: true }));
|
||||||
try {
|
try {
|
||||||
const results = await option.searchFn(value);
|
const results = await option.searchFn(value);
|
||||||
setSearchOptions(results);
|
setSearchOptionsMap(prev => ({ ...prev, [option.name]: results }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error searching:', error);
|
console.error('Error searching:', error);
|
||||||
setSearchOptions([]);
|
setSearchOptionsMap(prev => ({ ...prev, [option.name]: [] }));
|
||||||
} finally {
|
} finally {
|
||||||
setSearchLoading(false);
|
setSearchLoadingMap(prev => ({ ...prev, [option.name]: false }));
|
||||||
}
|
}
|
||||||
}, 300),
|
}, 300),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFilterChange = (value: string) => {
|
const handleFilterChange = (name: string, value: string) => {
|
||||||
setTempValues(prev => ({
|
const newValues = {
|
||||||
...prev,
|
...tempValues,
|
||||||
[selectedFilter]: value
|
[name]: value
|
||||||
}));
|
};
|
||||||
|
setTempValues(newValues);
|
||||||
|
onApplyFilters(newValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearFilters = () => {
|
|
||||||
const emptyValues: FilterValues = {};
|
|
||||||
options.forEach(option => {
|
|
||||||
emptyValues[option.name] = '';
|
|
||||||
});
|
|
||||||
setTempValues(emptyValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApplyFilters = () => {
|
|
||||||
setFilterValues(tempValues);
|
|
||||||
onApplyFilters(tempValues);
|
|
||||||
setShowFilters(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dropdownItems: MenuProps['items'] = options.map(option => ({
|
|
||||||
key: option.name,
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{selectedFilter === option.name && (
|
|
||||||
<CheckIcon className="h-4 w-4 text-blue-600" />
|
|
||||||
)}
|
|
||||||
{option.label || option.name}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const currentOption = options.find(option => option.name === selectedFilter);
|
|
||||||
|
|
||||||
const resetFilters = () => {
|
const resetFilters = () => {
|
||||||
const emptyValues: FilterValues = {};
|
const emptyValues: FilterValues = {};
|
||||||
options.forEach(option => {
|
options.forEach(option => {
|
||||||
emptyValues[option.name] = '';
|
emptyValues[option.name] = '';
|
||||||
});
|
});
|
||||||
setTempValues(emptyValues);
|
setTempValues(emptyValues);
|
||||||
setFilterValues(emptyValues);
|
onResetFilters();
|
||||||
setSearchInputValue('');
|
|
||||||
setSearchOptions([]);
|
|
||||||
onResetFilters(); // Call the parent's reset function
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Define the order of filters
|
||||||
|
const orderedFilters = [
|
||||||
|
'Team ID',
|
||||||
|
'Organization ID',
|
||||||
|
'Key Alias',
|
||||||
|
'User ID',
|
||||||
|
'Key Hash'
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" ref={filtersRef}>
|
<div className="w-full">
|
||||||
<TremorButton
|
<div className="flex items-center gap-2 mb-6">
|
||||||
icon={FilterIcon}
|
<Button
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
icon={<FilterIcon className="h-4 w-4" />}
|
||||||
variant="secondary"
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
size='xs'
|
className="flex items-center gap-2"
|
||||||
className="flex items-center pr-2"
|
>
|
||||||
>
|
{buttonLabel}
|
||||||
{buttonLabel}
|
</Button>
|
||||||
</TremorButton>
|
<Button onClick={resetFilters}>Reset Filters</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{showFilters && (
|
{showFilters && (
|
||||||
<Card className="absolute left-0 mt-2 w-[500px] z-50 border border-gray-200 shadow-lg">
|
<div className="grid grid-cols-3 gap-x-6 gap-y-4 mb-6">
|
||||||
<div className="flex flex-col gap-4">
|
{orderedFilters.map((filterName) => {
|
||||||
<div className="flex items-center gap-2">
|
const option = options.find(opt => opt.label === filterName || opt.name === filterName);
|
||||||
<span className="text-sm font-medium">Where</span>
|
if (!option) return null;
|
||||||
|
|
||||||
<Dropdown
|
return (
|
||||||
menu={{
|
<div key={option.name} className="flex flex-col gap-2">
|
||||||
items: dropdownItems,
|
<label className="text-sm text-gray-600">
|
||||||
onClick: ({ key }) => handleFilterSelect(key),
|
{option.label || option.name}
|
||||||
style: { minWidth: '200px' }
|
</label>
|
||||||
}}
|
{option.isSearchable ? (
|
||||||
onOpenChange={setDropdownOpen}
|
<Select
|
||||||
open={dropdownOpen}
|
showSearch
|
||||||
trigger={['click']}
|
className="w-full"
|
||||||
>
|
placeholder={`Search ${option.label || option.name}...`}
|
||||||
<Button className="min-w-40 text-left flex justify-between items-center">
|
value={tempValues[option.name] || undefined}
|
||||||
{currentOption?.label || selectedFilter}
|
onChange={(value) => handleFilterChange(option.name, value)}
|
||||||
{dropdownOpen ? (
|
onSearch={(value) => {
|
||||||
<ChevronUpIcon className="h-4 w-4" />
|
setSearchInputValueMap(prev => ({ ...prev, [option.name]: value }));
|
||||||
) : (
|
if (option.searchFn) {
|
||||||
<ChevronDownIcon className="h-4 w-4" />
|
debouncedSearch(value, option);
|
||||||
)}
|
}
|
||||||
</Button>
|
}}
|
||||||
</Dropdown>
|
filterOption={false}
|
||||||
|
loading={searchLoadingMap[option.name]}
|
||||||
{currentOption?.isSearchable ? (
|
options={searchOptionsMap[option.name] || []}
|
||||||
<Select
|
allowClear
|
||||||
showSearch
|
/>
|
||||||
placeholder={`Search ${currentOption.label || selectedFilter}...`}
|
) : (
|
||||||
value={tempValues[selectedFilter] || undefined}
|
<Input
|
||||||
onChange={(value) => handleFilterChange(value)}
|
className="w-full"
|
||||||
onSearch={(value) => {
|
placeholder={`Enter ${option.label || option.name}...`}
|
||||||
setSearchInputValue(value);
|
value={tempValues[option.name] || ''}
|
||||||
debouncedSearch(value, currentOption);
|
onChange={(e) => handleFilterChange(option.name, e.target.value)}
|
||||||
}}
|
allowClear
|
||||||
onInputKeyDown={(e) => {
|
/>
|
||||||
if (e.key === 'Enter' && searchInputValue) {
|
)}
|
||||||
// Allow manual entry of the value on Enter
|
</div>
|
||||||
handleFilterChange(searchInputValue);
|
);
|
||||||
e.preventDefault();
|
})}
|
||||||
}
|
</div>
|
||||||
}}
|
|
||||||
filterOption={false}
|
|
||||||
className="flex-1 w-full max-w-full truncate min-w-100"
|
|
||||||
loading={searchLoading}
|
|
||||||
options={searchOptions}
|
|
||||||
allowClear
|
|
||||||
notFoundContent={
|
|
||||||
searchLoading ? <Spin size="small" /> : (
|
|
||||||
<div className="p-2">
|
|
||||||
{searchInputValue && (
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
className="p-0 mt-1"
|
|
||||||
onClick={() => {
|
|
||||||
handleFilterChange(searchInputValue);
|
|
||||||
// Close the dropdown/select after selecting the value
|
|
||||||
const selectElement = document.activeElement as HTMLElement;
|
|
||||||
if (selectElement) {
|
|
||||||
selectElement.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Use “{searchInputValue}” as filter value
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
placeholder="Enter value..."
|
|
||||||
value={tempValues[selectedFilter] || ''}
|
|
||||||
onChange={(e) => handleFilterChange(e.target.value)}
|
|
||||||
className="px-3 py-1.5 border rounded-md text-sm flex-1 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
suffix={
|
|
||||||
tempValues[selectedFilter] ? (
|
|
||||||
<XIcon
|
|
||||||
className="h-4 w-4 cursor-pointer text-gray-400 hover:text-gray-500"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleFilterChange('');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex gap-2 justify-end">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
clearFilters();
|
|
||||||
onResetFilters();
|
|
||||||
setShowFilters(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleApplyFilters}>
|
|
||||||
Apply Filters
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,3 +13,5 @@ export const useBaseUrl = () => {
|
||||||
|
|
||||||
return baseUrl;
|
return baseUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultPageSize = 25;
|
|
@ -1,13 +1,15 @@
|
||||||
import { keyListCall, teamListCall, organizationListCall } from '../networking';
|
import { keyListCall, teamListCall, organizationListCall } from "../networking";
|
||||||
import { Team } from './key_list';
|
import { Team } from "./key_list";
|
||||||
import { Organization } from '../networking';
|
import { Organization } from "../networking";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches all key aliases across all pages
|
* Fetches all key aliases across all pages
|
||||||
* @param accessToken The access token for API authentication
|
* @param accessToken The access token for API authentication
|
||||||
* @returns Array of all unique key aliases
|
* @returns Array of all unique key aliases
|
||||||
*/
|
*/
|
||||||
export const fetchAllKeyAliases = async (accessToken: string | null): Promise<string[]> => {
|
export const fetchAllKeyAliases = async (
|
||||||
|
accessToken: string | null
|
||||||
|
): Promise<string[]> => {
|
||||||
if (!accessToken) return [];
|
if (!accessToken) return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -22,6 +24,8 @@ export const fetchAllKeyAliases = async (accessToken: string | null): Promise<st
|
||||||
null, // organization_id
|
null, // organization_id
|
||||||
"", // team_id
|
"", // team_id
|
||||||
null, // selectedKeyAlias
|
null, // selectedKeyAlias
|
||||||
|
null, // user_id
|
||||||
|
null, // key_hash
|
||||||
currentPage,
|
currentPage,
|
||||||
100 // larger page size to reduce number of requests
|
100 // larger page size to reduce number of requests
|
||||||
);
|
);
|
||||||
|
@ -55,7 +59,10 @@ export const fetchAllKeyAliases = async (accessToken: string | null): Promise<st
|
||||||
* @param organizationId Optional organization ID to filter teams
|
* @param organizationId Optional organization ID to filter teams
|
||||||
* @returns Array of all teams
|
* @returns Array of all teams
|
||||||
*/
|
*/
|
||||||
export const fetchAllTeams = async (accessToken: string | null, organizationId?: string | null): Promise<Team[]> => {
|
export const fetchAllTeams = async (
|
||||||
|
accessToken: string | null,
|
||||||
|
organizationId?: string | null
|
||||||
|
): Promise<Team[]> => {
|
||||||
if (!accessToken) return [];
|
if (!accessToken) return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -67,11 +74,11 @@ export const fetchAllTeams = async (accessToken: string | null, organizationId?:
|
||||||
const response = await teamListCall(
|
const response = await teamListCall(
|
||||||
accessToken,
|
accessToken,
|
||||||
organizationId || null,
|
organizationId || null,
|
||||||
null,
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add teams from this page
|
// Add teams from this page
|
||||||
allTeams = [...allTeams, ...response.teams];
|
allTeams = [...allTeams, ...response];
|
||||||
|
|
||||||
// Check if there are more pages
|
// Check if there are more pages
|
||||||
if (currentPage < response.total_pages) {
|
if (currentPage < response.total_pages) {
|
||||||
|
@ -93,7 +100,9 @@ export const fetchAllTeams = async (accessToken: string | null, organizationId?:
|
||||||
* @param accessToken The access token for API authentication
|
* @param accessToken The access token for API authentication
|
||||||
* @returns Array of all organizations
|
* @returns Array of all organizations
|
||||||
*/
|
*/
|
||||||
export const fetchAllOrganizations = async (accessToken: string | null): Promise<Organization[]> => {
|
export const fetchAllOrganizations = async (
|
||||||
|
accessToken: string | null
|
||||||
|
): Promise<Organization[]> => {
|
||||||
if (!accessToken) return [];
|
if (!accessToken) return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -102,12 +111,10 @@ export const fetchAllOrganizations = async (accessToken: string | null): Promise
|
||||||
let hasMorePages = true;
|
let hasMorePages = true;
|
||||||
|
|
||||||
while (hasMorePages) {
|
while (hasMorePages) {
|
||||||
const response = await organizationListCall(
|
const response = await organizationListCall(accessToken);
|
||||||
accessToken
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add organizations from this page
|
// Add organizations from this page
|
||||||
allOrganizations = [...allOrganizations, ...response.organizations];
|
allOrganizations = [...allOrganizations, ...response];
|
||||||
|
|
||||||
// Check if there are more pages
|
// Check if there are more pages
|
||||||
if (currentPage < response.total_pages) {
|
if (currentPage < response.total_pages) {
|
||||||
|
|
|
@ -1,45 +1,81 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState, useRef } from "react";
|
||||||
import { KeyResponse } from "../key_team_helpers/key_list";
|
import { KeyResponse } from "../key_team_helpers/key_list";
|
||||||
import { Organization } from "../networking";
|
import { keyListCall, Organization } from "../networking";
|
||||||
import { Team } from "../key_team_helpers/key_list";
|
import { Team } from "../key_team_helpers/key_list";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchAllKeyAliases, fetchAllOrganizations, fetchAllTeams } from "./filter_helpers";
|
import { fetchAllKeyAliases, fetchAllOrganizations, fetchAllTeams } from "./filter_helpers";
|
||||||
import { Setter } from "@/types";
|
import { Setter } from "@/types";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import { defaultPageSize } from "../constants";
|
||||||
|
|
||||||
export interface FilterState {
|
export interface FilterState {
|
||||||
'Team ID': string;
|
'Team ID': string;
|
||||||
'Organization ID': string;
|
'Organization ID': string;
|
||||||
'Key Alias': string;
|
'Key Alias': string;
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
|
'User ID': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function useFilterLogic({
|
export function useFilterLogic({
|
||||||
keys,
|
keys,
|
||||||
teams,
|
teams,
|
||||||
organizations,
|
organizations,
|
||||||
accessToken,
|
accessToken,
|
||||||
setSelectedTeam,
|
|
||||||
setCurrentOrg,
|
|
||||||
setSelectedKeyAlias
|
|
||||||
}: {
|
}: {
|
||||||
keys: KeyResponse[];
|
keys: KeyResponse[];
|
||||||
teams: Team[] | null;
|
teams: Team[] | null;
|
||||||
organizations: Organization[] | null;
|
organizations: Organization[] | null;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
setSelectedTeam: (team: Team | null) => void;
|
|
||||||
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>;
|
|
||||||
setSelectedKeyAlias: Setter<string | null>
|
|
||||||
}) {
|
}) {
|
||||||
const [filters, setFilters] = useState<FilterState>({
|
const defaultFilters: FilterState = {
|
||||||
'Team ID': '',
|
'Team ID': '',
|
||||||
'Organization ID': '',
|
'Organization ID': '',
|
||||||
'Key Alias': ''
|
'Key Alias': '',
|
||||||
});
|
'User ID': ''
|
||||||
|
}
|
||||||
|
const [filters, setFilters] = useState<FilterState>(defaultFilters);
|
||||||
const [allTeams, setAllTeams] = useState<Team[]>(teams || []);
|
const [allTeams, setAllTeams] = useState<Team[]>(teams || []);
|
||||||
const [allOrganizations, setAllOrganizations] = useState<Organization[]>(organizations || []);
|
const [allOrganizations, setAllOrganizations] = useState<Organization[]>(organizations || []);
|
||||||
const [filteredKeys, setFilteredKeys] = useState<KeyResponse[]>(keys);
|
const [filteredKeys, setFilteredKeys] = useState<KeyResponse[]>(keys);
|
||||||
|
const lastSearchTimestamp = useRef(0);
|
||||||
|
const debouncedSearch = useCallback(
|
||||||
|
debounce(async (filters: FilterState) => {
|
||||||
|
if (!accessToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTimestamp = Date.now();
|
||||||
|
lastSearchTimestamp.current = currentTimestamp;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Make the API call using userListCall with all filter parameters
|
||||||
|
const data = await keyListCall(
|
||||||
|
accessToken,
|
||||||
|
filters["Organization ID"] || null,
|
||||||
|
filters["Team ID"] || null,
|
||||||
|
filters["Key Alias"] || null,
|
||||||
|
filters["User ID"] || null,
|
||||||
|
filters["Key Hash"] || null,
|
||||||
|
1, // Reset to first page when searching
|
||||||
|
defaultPageSize
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only update state if this is the most recent search
|
||||||
|
if (currentTimestamp === lastSearchTimestamp.current) {
|
||||||
|
if (data) {
|
||||||
|
setFilteredKeys(data.keys);
|
||||||
|
console.log("called from debouncedSearch filters:", JSON.stringify(filters));
|
||||||
|
console.log("called from debouncedSearch data:", JSON.stringify(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching users:", error);
|
||||||
|
}
|
||||||
|
}, 300),
|
||||||
|
[accessToken]
|
||||||
|
);
|
||||||
// Apply filters to keys whenever keys or filters change
|
// Apply filters to keys whenever keys or filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!keys) {
|
if (!keys) {
|
||||||
|
@ -120,44 +156,24 @@ export function useFilterLogic({
|
||||||
setFilters({
|
setFilters({
|
||||||
'Team ID': newFilters['Team ID'] || '',
|
'Team ID': newFilters['Team ID'] || '',
|
||||||
'Organization ID': newFilters['Organization ID'] || '',
|
'Organization ID': newFilters['Organization ID'] || '',
|
||||||
'Key Alias': newFilters['Key Alias'] || ''
|
'Key Alias': newFilters['Key Alias'] || '',
|
||||||
|
'User ID': newFilters['User ID'] || ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle Team change
|
// Fetch keys based on new filters
|
||||||
if (newFilters['Team ID']) {
|
const updatedFilters = {
|
||||||
const selectedTeamData = allTeams?.find(team => team.team_id === newFilters['Team ID']);
|
...filters,
|
||||||
if (selectedTeamData) {
|
...newFilters
|
||||||
setSelectedTeam(selectedTeamData);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
debouncedSearch(updatedFilters);
|
||||||
// Handle Org change
|
|
||||||
if (newFilters['Organization ID']) {
|
|
||||||
const selectedOrg = allOrganizations?.find(org => org.organization_id === newFilters['Organization ID']);
|
|
||||||
if (selectedOrg) {
|
|
||||||
setCurrentOrg(selectedOrg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyAlias = newFilters['Key Alias'];
|
|
||||||
const selectedKeyAlias = keyAlias
|
|
||||||
? allKeyAliases.find((k) => k === keyAlias) || null
|
|
||||||
: null;
|
|
||||||
setSelectedKeyAlias(selectedKeyAlias)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterReset = () => {
|
const handleFilterReset = () => {
|
||||||
// Reset filters state
|
// Reset filters state
|
||||||
setFilters({
|
setFilters(defaultFilters);
|
||||||
'Team ID': '',
|
|
||||||
'Organization ID': '',
|
|
||||||
'Key Alias': ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset selections
|
// Reset selections
|
||||||
setSelectedTeam(null);
|
debouncedSearch(defaultFilters);
|
||||||
setCurrentOrg(null);
|
|
||||||
setSelectedKeyAlias(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -133,10 +133,12 @@ const useKeyList = ({
|
||||||
|
|
||||||
const data = await keyListCall(
|
const data = await keyListCall(
|
||||||
accessToken,
|
accessToken,
|
||||||
currentOrg?.organization_id || null,
|
null,
|
||||||
selectedTeam?.team_id || "",
|
null,
|
||||||
selectedKeyAlias,
|
null,
|
||||||
params.page as number || 1,
|
null,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
50,
|
50,
|
||||||
);
|
);
|
||||||
console.log("data", data);
|
console.log("data", data);
|
||||||
|
|
|
@ -2689,6 +2689,8 @@ export const keyListCall = async (
|
||||||
organizationID: string | null,
|
organizationID: string | null,
|
||||||
teamID: string | null,
|
teamID: string | null,
|
||||||
selectedKeyAlias: string | null,
|
selectedKeyAlias: string | null,
|
||||||
|
userID: string | null,
|
||||||
|
keyHash: string | null,
|
||||||
page: number,
|
page: number,
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
) => {
|
) => {
|
||||||
|
@ -2712,6 +2714,14 @@ export const keyListCall = async (
|
||||||
queryParams.append('key_alias', selectedKeyAlias)
|
queryParams.append('key_alias', selectedKeyAlias)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (keyHash) {
|
||||||
|
queryParams.append('key_hash', keyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userID) {
|
||||||
|
queryParams.append('user_id', userID.toString());
|
||||||
|
}
|
||||||
|
|
||||||
if (page) {
|
if (page) {
|
||||||
queryParams.append('page', page.toString());
|
queryParams.append('page', page.toString());
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,7 +171,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
|
|
||||||
// NEW: Declare filter states for team and key alias.
|
// NEW: Declare filter states for team and key alias.
|
||||||
const [teamFilter, setTeamFilter] = useState<string>(selectedTeam?.team_id || "");
|
const [teamFilter, setTeamFilter] = useState<string>(selectedTeam?.team_id || "");
|
||||||
const [keyAliasFilter, setKeyAliasFilter] = useState<string>("");
|
|
||||||
|
|
||||||
// Keep the team filter in sync with the incoming prop.
|
// Keep the team filter in sync with the incoming prop.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue