Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions platform/frontend/src/app/agents/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ function Agents({ initialData }: { initialData?: AgentsInitialData }) {
actions={!isDeletedView ? <TableCardViewToggle /> : undefined}
>
<SearchInput
isLoading={isFetching}
objectNamePlural="agents"
searchFields={["name"]}
paramName="name"
Expand Down
1 change: 1 addition & 0 deletions platform/frontend/src/app/apps/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export default function AppsPage() {
<TableCardView storageKey="archestra-apps-view">
<FilterBar className="mb-6" actions={<TableCardViewToggle />}>
<SearchInput
isLoading={isFetching}
paramName="search"
placeholder="Search apps"
className={filterSearchClass}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@ export function AuditLogTable() {
]}
>
<SearchInput
isLoading={isFetching}
objectNamePlural="audit events"
searchFields={["actor", "path", "resource"]}
paramName="search"
Expand Down
28 changes: 28 additions & 0 deletions platform/frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -750,3 +750,31 @@ button.plugin-featured-action::after {
[data-radix-popper-content-wrapper]:has(> [data-pointer-events-none]) {
pointer-events: none;
}

/* ── Data table: request in flight ──
The sweeping half of `<DataTable>`'s loading bar. The bar's track, colours
and fade-in live in the component; only the animation needs a keyframe, and
the width belongs with it because the two are one motion. */

@keyframes archestra-table-loading-sweep {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(340%);
}
}

.archestra-table-loading-sweep {
width: 30%;
animation: archestra-table-loading-sweep 1.1s ease-in-out infinite;
}

/* Motion sensitivity: hold the bar still and fill the track instead. It still
marks the table as busy, which is the information the bar carries. */
@media (prefers-reduced-motion: reduce) {
.archestra-table-loading-sweep {
width: 100%;
animation: none;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ export function ConnectorDocumentsTable({
<div className="space-y-4">
<FilterBar className="!mb-3">
<SearchInput
isLoading={isFetching}
value={search}
syncQueryParams={false}
placeholder="Search documents by title"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,11 @@ function ConnectorsList() {
<FilterBar
actions={!isDeletedView ? <TableCardViewToggle /> : undefined}
>
<SearchInput paramName="search" className={filterSearchClass} />
<SearchInput
paramName="search"
className={filterSearchClass}
isLoading={isFetching}
/>
<Select
value={connectorTypeFilter}
onValueChange={handleConnectorTypeChange}
Expand Down
1 change: 1 addition & 0 deletions platform/frontend/src/app/knowledge/files/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ export default function KnowledgeFilesPage() {
)}

<SearchInput
isLoading={isLoading}
value={search}
onSearchChange={(value) => {
setSearch(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,11 @@ function KnowledgeBasesList() {
<FilterBar
actions={!isDeletedView ? <TableCardViewToggle /> : undefined}
>
<SearchInput paramName="search" className={filterSearchClass} />
<SearchInput
paramName="search"
className={filterSearchClass}
isLoading={isFetching}
/>
<ResourceDeletedStatusFilter
deletePermission={{ knowledgeSource: ["delete"] }}
/>
Expand Down
1 change: 1 addition & 0 deletions platform/frontend/src/app/llm/logs/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ function SessionsTable() {
input so toggling it never reflows the filter bar or the table. */}
<div className={filterSearchClass}>
<SearchInput
isLoading={isFetching}
objectNamePlural="logs"
searchFields={["session ID"]}
paramName="search"
Expand Down
1 change: 1 addition & 0 deletions platform/frontend/src/app/llm/model-providers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,7 @@ export default function ApiKeysPage() {
<div className="space-y-4">
<FilterBar className="!mb-3">
<SearchInput
isLoading={isFetching}
objectNamePlural="credentials"
searchFields={["name"]}
paramName="search"
Expand Down
1 change: 1 addition & 0 deletions platform/frontend/src/app/llm/proxy/oauth-clients/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ function OauthClientsTable() {
<div className="mb-3">
<FilterBar actions={<TableCardViewToggle />}>
<SearchInput
isLoading={query.isFetching}
objectNamePlural="OAuth clients"
searchFields={["name"]}
paramName="search"
Expand Down
1 change: 1 addition & 0 deletions platform/frontend/src/app/llm/proxy/virtual-keys/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ function VirtualKeysTable() {
<div className="mb-3">
<FilterBar actions={<TableCardViewToggle />}>
<SearchInput
isLoading={query.isFetching}
objectNamePlural="keys"
searchFields={["name"]}
paramName="search"
Expand Down
1 change: 1 addition & 0 deletions platform/frontend/src/app/mcp/gateways/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,7 @@ function McpGateways({
actions={!isDeletedView ? <TableCardViewToggle /> : undefined}
>
<SearchInput
isLoading={isFetching}
objectNamePlural="gateways"
searchFields={["name"]}
paramName="name"
Expand Down
1 change: 1 addition & 0 deletions platform/frontend/src/app/mcp/logs/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ function McpToolCallsTable({
// Shared search input component
const searchInputComponent = (
<SearchInput
isLoading={isFetching}
objectNamePlural="tool calls"
searchFields={["tool name", "server name"]}
paramName="search"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ export function AssignedToolsTable({
<div className="space-y-6">
<FilterBar className="!mb-3">
<SearchInput
isLoading={isLoading}
objectNamePlural="tools"
searchFields={["name"]}
paramName="search"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ export function ChannelsSection({
}
>
<SearchInput
isLoading={isFetching}
placeholder="Search channels..."
paramName="search"
className={filterSearchClass}
Expand Down
1 change: 1 addition & 0 deletions platform/frontend/src/app/projects/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ function ProjectsList() {
{!isDeletedView && (
<>
<SearchInput
isLoading={isFetching}
placeholder="Search projects"
paramName="search"
className={filterSearchClass}
Expand Down
1 change: 1 addition & 0 deletions platform/frontend/src/app/settings/users/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ function MembersTab({
actions={<TabButtons activeTab={activeTab} onTabChange={onTabChange} />}
>
<SearchInput
isLoading={isFetching}
objectNamePlural="users"
searchFields={["name", "email"]}
paramName="name"
Expand Down
1 change: 1 addition & 0 deletions platform/frontend/src/app/skills/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ function SkillsList() {
actions={!isDeletedView ? <TableCardViewToggle /> : undefined}
>
<SearchInput
isLoading={isFetching}
paramName="search"
className={filterSearchClass}
/>
Expand Down
103 changes: 102 additions & 1 deletion platform/frontend/src/components/debounced-input.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DebouncedInput } from "./debounced-input";

Expand Down Expand Up @@ -129,4 +129,105 @@ describe("DebouncedInput", () => {
expect(onChange).toHaveBeenCalledWith("hel");
expect(onChange).toHaveBeenCalledTimes(1);
});
/**
* The pending window drives the search box's spinner. Every case below is a
* way that indicator has to behave: start at the keystroke, survive the
* hand-off to whatever the commit triggers, and never stick on.
*/
describe("pending window", () => {
const lastPending = (onPendingChange: ReturnType<typeof vi.fn>) =>
onPendingChange.mock.lastCall?.[0];

it("opens on the keystroke rather than when the debounce fires", () => {
const onPendingChange = vi.fn();
render(
<DebouncedInput
initialValue=""
onChange={() => {}}
debounceMs={400}
onPendingChange={onPendingChange}
/>,
);

act(() => typeInInput(screen.getByRole("textbox"), "no"));

// The whole point: the gap being covered starts at the keystroke, not
// 400ms later once the request goes out.
expect(lastPending(onPendingChange)).toBe(true);
});

it("stays open across the commit until the caller takes the new value", () => {
const onPendingChange = vi.fn();
const { rerender } = render(
<DebouncedInput
initialValue=""
onChange={() => {}}
debounceMs={400}
onPendingChange={onPendingChange}
/>,
);

act(() => typeInInput(screen.getByRole("textbox"), "notion"));
act(() => void vi.advanceTimersByTime(400));

// Closing here would blink the indicator off in the gap between the
// commit and the request it triggers.
expect(lastPending(onPendingChange)).toBe(true);

rerender(
<DebouncedInput
initialValue="notion"
onChange={() => {}}
debounceMs={400}
onPendingChange={onPendingChange}
/>,
);

expect(lastPending(onPendingChange)).toBe(false);
});

it("closes on its own when the caller never commits the value back", () => {
const onPendingChange = vi.fn();
render(
<DebouncedInput
initialValue=""
onChange={() => {}}
debounceMs={400}
onPendingChange={onPendingChange}
/>,
);

act(() => typeInInput(screen.getByRole("textbox"), "notion"));
act(() => void vi.advanceTimersByTime(400));
expect(lastPending(onPendingChange)).toBe(true);

// A caller that keeps the query somewhere other than `initialValue`
// never closes the window, so the bound has to.
act(() => void vi.advanceTimersByTime(1000));
expect(lastPending(onPendingChange)).toBe(false);
});

it("never opens for an edit that lands back on the committed value", () => {
const onPendingChange = vi.fn();
render(
<DebouncedInput
initialValue="notion"
onChange={() => {}}
debounceMs={400}
onPendingChange={onPendingChange}
/>,
);

const input = screen.getByRole("textbox");
act(() => typeInInput(input, "notio"));
expect(lastPending(onPendingChange)).toBe(true);

// Nothing will change, so nothing should claim to be loading.
act(() => typeInInput(input, "notion"));
expect(lastPending(onPendingChange)).toBe(false);

act(() => void vi.advanceTimersByTime(400));
expect(lastPending(onPendingChange)).toBe(false);
});
});
});
Loading
Loading