diff --git a/site/e2e/pom/WorkspacesPage.ts b/site/e2e/pom/WorkspacesPage.ts
index eecdea38c89c2..33f2dd38f1b52 100644
--- a/site/e2e/pom/WorkspacesPage.ts
+++ b/site/e2e/pom/WorkspacesPage.ts
@@ -2,7 +2,7 @@ import { Page } from "@playwright/test"
import { BasePom } from "./BasePom"
export class WorkspacesPage extends BasePom {
- constructor(baseURL: string | undefined, page: Page) {
- super(baseURL, "/workspaces", page)
+ constructor(baseURL: string | undefined, page: Page, params?: string) {
+ super(baseURL, `/workspaces${params && params}`, page)
}
}
diff --git a/site/e2e/tests/login.spec.ts b/site/e2e/tests/login.spec.ts
index 7127f84e9562e..0e6482e602072 100644
--- a/site/e2e/tests/login.spec.ts
+++ b/site/e2e/tests/login.spec.ts
@@ -10,7 +10,7 @@ test("Login takes user to /workspaces", async ({ baseURL, page }) => {
const signInPage = new SignInPage(baseURL, page)
await signInPage.submitBuiltInAuthentication(email, password)
- const workspacesPage = new WorkspacesPage(baseURL, page)
+ const workspacesPage = new WorkspacesPage(baseURL, page, "?filter=owner%3Ame")
await waitForClientSideNavigation(page, { to: workspacesPage.url })
await page.waitForSelector("text=Workspaces")
diff --git a/site/package.json b/site/package.json
index ca1a388e08028..ffe816e7939e3 100644
--- a/site/package.json
+++ b/site/package.json
@@ -42,9 +42,10 @@
"formik": "2.2.9",
"front-matter": "4.0.2",
"history": "5.3.0",
+ "just-debounce-it": "3.0.1",
"react": "17.0.2",
"react-dom": "17.0.2",
- "react-helmet": "^6.1.0",
+ "react-helmet": "6.1.0",
"react-markdown": "8.0.3",
"react-router-dom": "6.3.0",
"sourcemapped-stacktrace": "1.1.11",
@@ -74,7 +75,7 @@
"@types/node": "14.18.16",
"@types/react": "17.0.44",
"@types/react-dom": "17.0.16",
- "@types/react-helmet": "^6.1.5",
+ "@types/react-helmet": "6.1.5",
"@types/superagent": "4.1.15",
"@types/uuid": "8.3.4",
"@typescript-eslint/eslint-plugin": "5.27.0",
diff --git a/site/src/components/SearchBarWithFilter/SearchBarWithFilter.test.tsx b/site/src/components/SearchBarWithFilter/SearchBarWithFilter.test.tsx
new file mode 100644
index 0000000000000..48061e1d9b369
--- /dev/null
+++ b/site/src/components/SearchBarWithFilter/SearchBarWithFilter.test.tsx
@@ -0,0 +1,38 @@
+import { fireEvent, screen } from "@testing-library/react"
+import userEvent from "@testing-library/user-event"
+import { render } from "../../testHelpers/renderHelpers"
+import { SearchBarWithFilter } from "./SearchBarWithFilter"
+
+// mock the debounce utility
+jest.mock("just-debounce-it", () =>
+ jest.fn((fn) => {
+ fn.cancel = jest.fn()
+ return fn
+ }),
+)
+
+describe("SearchBarWithFilter", () => {
+ it("calls the onFilter handler on keystroke", async () => {
+ // When
+ const onFilter = jest.fn()
+ render()
+
+ const searchInput = screen.getByRole("textbox")
+ await userEvent.type(searchInput, "workspace") // 9 characters
+
+ // Then
+ expect(onFilter).toBeCalledTimes(10) // 9 characters + 1 on component mount
+ })
+
+ it("calls the onFilter handler on submit", async () => {
+ // When
+ const onFilter = jest.fn()
+ render()
+
+ const searchInput = screen.getByRole("textbox")
+ await fireEvent.keyDown(searchInput, { key: "Enter", code: "Enter", charCode: 13 })
+
+ // Then
+ expect(onFilter).toBeCalledTimes(1)
+ })
+})
diff --git a/site/src/components/SearchBarWithFilter/SearchBarWithFilter.tsx b/site/src/components/SearchBarWithFilter/SearchBarWithFilter.tsx
index 95c4685c36527..b7bd4906c706d 100644
--- a/site/src/components/SearchBarWithFilter/SearchBarWithFilter.tsx
+++ b/site/src/components/SearchBarWithFilter/SearchBarWithFilter.tsx
@@ -7,7 +7,8 @@ import { makeStyles } from "@material-ui/core/styles"
import TextField from "@material-ui/core/TextField"
import SearchIcon from "@material-ui/icons/Search"
import { FormikErrors, useFormik } from "formik"
-import { useState } from "react"
+import debounce from "just-debounce-it"
+import { useCallback, useEffect, useState } from "react"
import { getValidationErrorMessage } from "../../api/errors"
import { getFormHelpers } from "../../util/formUtils"
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
@@ -53,6 +54,23 @@ export const SearchBarWithFilter: React.FC = ({
},
})
+ // debounce query string entry by user
+ // we want the dependency array empty here
+ // as we don't need to redefine the function
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const debouncedOnFilter = useCallback(
+ debounce((debouncedQueryString: string) => {
+ onFilter(debouncedQueryString)
+ }, 300),
+ [],
+ )
+
+ // update the query params while typing
+ useEffect(() => {
+ debouncedOnFilter(form.values.query)
+ return () => debouncedOnFilter.cancel()
+ }, [debouncedOnFilter, form.values.query])
+
const getFieldHelpers = getFormHelpers(form)
const [anchorEl, setAnchorEl] = useState(null)
diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx
index 1e3987e6d9e6b..90e5fb80f1b2d 100644
--- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx
+++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx
@@ -12,6 +12,7 @@ const WorkspacesPage: FC = () => {
const [searchParams, setSearchParams] = useSearchParams()
const { workspaceRefs } = workspacesState.context
+ // On page load, populate the table with workspaces
useEffect(() => {
const filter = searchParams.get("filter")
const query = filter ?? workspaceFilterQuery.me
@@ -20,7 +21,8 @@ const WorkspacesPage: FC = () => {
type: "GET_WORKSPACES",
query,
})
- }, [searchParams, send])
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
return (
<>
@@ -33,8 +35,11 @@ const WorkspacesPage: FC = () => {
loading={workspacesState.hasTag("loading")}
workspaceRefs={workspaceRefs}
onFilter={(query) => {
- searchParams.set("filter", query)
- setSearchParams(searchParams)
+ setSearchParams({ filter: query })
+ send({
+ type: "GET_WORKSPACES",
+ query,
+ })
}}
/>
>
diff --git a/site/yarn.lock b/site/yarn.lock
index 23a265d0d434b..76e1f3fd78860 100644
--- a/site/yarn.lock
+++ b/site/yarn.lock
@@ -3041,7 +3041,7 @@
dependencies:
"@types/react" "^17"
-"@types/react-helmet@^6.1.5":
+"@types/react-helmet@6.1.5":
version "6.1.5"
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.5.tgz#35f89a6b1646ee2bc342a33a9a6c8777933f9083"
integrity sha512-/ICuy7OHZxR0YCAZLNg9r7I9aijWUWvxaPR6uTuyxe8tAj5RL4Sw1+R6NhXUtOsarkGYPmaHdBDvuXh2DIN/uA==
@@ -9122,6 +9122,11 @@ junk@^3.1.0:
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
+just-debounce-it@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/just-debounce-it/-/just-debounce-it-3.0.1.tgz#8c8a4c9327c9523366ec79ac9a959a938153bd2f"
+ integrity sha512-6EQWOpRV8fm/ame6XvGBSxvsjoMbqj7JS9TV/4Q9aOXt9DQw22GBfTGP6gTAqcBNN/PbzlwtwH7jtM0k9oe9pg==
+
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -11487,7 +11492,7 @@ react-helmet-async@^1.0.7:
react-fast-compare "^3.2.0"
shallowequal "^1.1.0"
-react-helmet@^6.1.0:
+react-helmet@6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"
integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==