updated tenant filter

This commit is contained in:
Oliver Kieselbach
2026-03-30 16:56:52 +02:00
parent c0879311cc
commit 359a46d516
4 changed files with 119 additions and 9 deletions
@@ -1 +1 @@
721
722
@@ -76,6 +76,7 @@ interface SessionTableProps {
onTenantIdFilterChange: (value: string) => void;
onTenantIdFilterSubmit: () => void;
onTenantIdFilterClear: () => void;
tenantList: { tenantId: string; domainName: string }[];
blockedDevicesSet: Set<string>;
isPreviewBlocked: boolean;
user: { isGlobalAdmin?: boolean } | null;
@@ -112,6 +113,7 @@ export function SessionTable({
onTenantIdFilterChange,
onTenantIdFilterSubmit,
onTenantIdFilterClear,
tenantList,
blockedDevicesSet,
isPreviewBlocked,
user,
@@ -126,6 +128,18 @@ export function SessionTable({
const [openFilterColumn, setOpenFilterColumn] = useState<string | null>(null);
const columnSelectorRef = useRef<HTMLDivElement>(null);
const filterDropdownRef = useRef<HTMLDivElement>(null);
const tenantDropdownRef = useRef<HTMLDivElement>(null);
const [showTenantSuggestions, setShowTenantSuggestions] = useState(false);
const [tenantSelectedIndex, setTenantSelectedIndex] = useState(-1);
// Fuzzy-match tenants by domain name or tenant ID
const tenantSuggestions = (() => {
const q = tenantIdFilter.trim().toLowerCase();
if (q.length < 2 || tenantList.length === 0) return [];
return tenantList
.filter((t) => t.domainName.toLowerCase().includes(q) || t.tenantId.toLowerCase().includes(q))
.slice(0, 8);
})();
// Persist visible columns to localStorage
useEffect(() => {
@@ -141,12 +155,15 @@ export function SessionTable({
if (filterDropdownRef.current && !filterDropdownRef.current.contains(e.target as Node)) {
setOpenFilterColumn(null);
}
if (tenantDropdownRef.current && !tenantDropdownRef.current.contains(e.target as Node)) {
setShowTenantSuggestions(false);
}
}
if (showColumnSelector || openFilterColumn) {
if (showColumnSelector || openFilterColumn || showTenantSuggestions) {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}
}, [showColumnSelector, openFilterColumn]);
}, [showColumnSelector, openFilterColumn, showTenantSuggestions]);
// Compute unique values for a filterable field from all sessions (not filtered)
const getUniqueValues = (field: keyof Session): string[] => {
@@ -283,14 +300,46 @@ export function SessionTable({
{/* Tenant ID Filter (Global Admin only) */}
{globalAdminMode && (
<div className="mb-3 flex items-center gap-2">
<div className="relative flex-1">
<div className="relative flex-1" ref={tenantDropdownRef}>
<input
type="text"
placeholder="Filter by Tenant ID (paste a GUID and press Enter)"
placeholder="Filter by Tenant ID or domain name"
value={tenantIdFilter}
onChange={(e) => onTenantIdFilterChange(e.target.value)}
onChange={(e) => {
onTenantIdFilterChange(e.target.value);
setShowTenantSuggestions(true);
setTenantSelectedIndex(-1);
}}
onFocus={() => {
if (tenantSuggestions.length > 0) setShowTenantSuggestions(true);
}}
onKeyDown={(e) => {
if (showTenantSuggestions && tenantSuggestions.length > 0) {
if (e.key === "ArrowDown") {
e.preventDefault();
setTenantSelectedIndex((i) => Math.min(i + 1, tenantSuggestions.length - 1));
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
setTenantSelectedIndex((i) => Math.max(i - 1, -1));
return;
}
if (e.key === "Enter" && tenantSelectedIndex >= 0) {
e.preventDefault();
const selected = tenantSuggestions[tenantSelectedIndex];
onTenantIdFilterChange(selected.tenantId);
setShowTenantSuggestions(false);
setTenantSelectedIndex(-1);
return;
}
if (e.key === "Escape") {
setShowTenantSuggestions(false);
return;
}
}
if (e.key === "Enter") {
setShowTenantSuggestions(false);
onTenantIdFilterSubmit();
}
}}
@@ -298,7 +347,10 @@ export function SessionTable({
/>
{tenantIdFilter && (
<button
onClick={onTenantIdFilterClear}
onClick={() => {
onTenantIdFilterClear();
setShowTenantSuggestions(false);
}}
className="absolute right-3 top-2.5 text-gray-400 hover:text-gray-600 transition-colors"
title="Clear tenant filter"
>
@@ -307,9 +359,35 @@ export function SessionTable({
</svg>
</button>
)}
{/* Tenant suggestions dropdown */}
{showTenantSuggestions && tenantSuggestions.length > 0 && (
<div className="absolute z-50 mt-1 w-full bg-white border border-purple-200 rounded-lg shadow-lg max-h-64 overflow-y-auto">
{tenantSuggestions.map((t, idx) => (
<button
key={t.tenantId}
onClick={() => {
onTenantIdFilterChange(t.tenantId);
setShowTenantSuggestions(false);
setTenantSelectedIndex(-1);
}}
className={`w-full text-left px-4 py-2.5 flex flex-col gap-0.5 transition-colors ${
idx === tenantSelectedIndex
? "bg-purple-100"
: "hover:bg-purple-50"
}`}
>
<span className="text-sm font-medium text-gray-900">{t.domainName}</span>
<span className="text-xs text-gray-500 font-mono">{t.tenantId}</span>
</button>
))}
</div>
)}
</div>
<button
onClick={onTenantIdFilterSubmit}
onClick={() => {
setShowTenantSuggestions(false);
onTenantIdFilterSubmit();
}}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium shrink-0"
>
Filter
@@ -24,6 +24,11 @@ interface TenantConfigurationSummary {
validateAutopilotDevice: boolean;
}
interface TenantListItem {
tenantId: string;
domainName: string;
}
export default function Home() {
const router = useRouter();
const { user, logout, getAccessToken, isPreviewBlocked } = useAuth();
@@ -37,6 +42,7 @@ export default function Home() {
const [serialValidationEnabled, setSerialValidationEnabled] = useState<boolean | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [tenantIdFilter, setTenantIdFilter] = useState("");
const [tenantList, setTenantList] = useState<TenantListItem[]>([]);
const [statusFilter, setStatusFilter] = useState<string | null>(null);
const [sortColumn, setSortColumn] = useState<keyof Session | null>(null);
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
@@ -389,6 +395,31 @@ export default function Home() {
fetchSessions();
}, [globalAdminMode]);
// Fetch tenant list for fuzzy search when entering global admin mode
useEffect(() => {
if (!globalAdminMode) {
setTenantList([]);
return;
}
let cancelled = false;
(async () => {
try {
const res = await authenticatedFetch(api.config.all(), getAccessToken);
if (!res.ok || cancelled) return;
const configs: { tenantId?: string; domainName?: string }[] = await res.json();
if (cancelled) return;
setTenantList(
configs
.filter((c): c is { tenantId: string; domainName: string } => !!c.tenantId && !!c.domainName)
.map((c) => ({ tenantId: c.tenantId, domainName: c.domainName }))
);
} catch {
// Non-critical — tenant autocomplete just won't work
}
})();
return () => { cancelled = true; };
}, [globalAdminMode, getAccessToken]);
// Reset to page 1 when search query, status filter, sort, or page size changes
useEffect(() => {
setCurrentPage(1);
@@ -680,6 +711,7 @@ export default function Home() {
onTenantIdFilterChange={applyTenantIdFilter}
onTenantIdFilterSubmit={submitTenantIdFilter}
onTenantIdFilterClear={clearTenantIdFilter}
tenantList={tenantList}
blockedDevicesSet={blockedDevicesSet}
isPreviewBlocked={isPreviewBlocked}
user={user}
File diff suppressed because one or more lines are too long