From dedbcf6565ef5711ef52657e1bdf35320957fd78 Mon Sep 17 00:00:00 2001 From: cesco Date: Wed, 10 Aug 2022 21:16:29 -0300 Subject: [PATCH] add search for the test list at frontend home (#1026) * add search for the test list at frontend home * fixb BE search * add description to search fields * simplify typings Co-authored-by: Sebastian Choren --- .gitignore | 1 + api/openapi.yaml | 5 +++ server/http/controller.go | 4 +- server/model/repository.go | 2 +- server/openapi/api.go | 2 +- server/openapi/api_api.go | 3 +- server/openapi/impl.go | 2 +- server/testdb/mock.go | 4 +- server/testdb/tests.go | 26 +++++++++---- server/testdb/tests_test.go | 57 ++++++++++++++++++++--------- web/src/hooks/useInfiniteScroll.ts | 14 ++++++- web/src/pages/Home/HomeContent.tsx | 10 +++-- web/src/pages/Home/TestList.tsx | 21 ++++++----- web/src/redux/apis/TraceTest.api.ts | 4 +- 14 files changed, 106 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index 8422b9dda3..158e63c07c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ swagger/ # Common text editors .vscode/ .idea/ +server/vendor diff --git a/api/openapi.yaml b/api/openapi.yaml index 88c35e1de0..ea975f1883 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -26,6 +26,11 @@ paths: schema: type: integer default: 0 + - in: query + name: query + description: "query to search tests, based on test name and description" + schema: + type: string responses: 200: description: successful operation diff --git a/server/http/controller.go b/server/http/controller.go index 5d439db904..bae5dd7413 100644 --- a/server/http/controller.go +++ b/server/http/controller.go @@ -224,13 +224,13 @@ func (c *controller) GetTestRuns(ctx context.Context, testID string, take, skip return openapi.Response(200, c.mappers.Out.Runs(runs)), nil } -func (c *controller) GetTests(ctx context.Context, take, skip int32) (openapi.ImplResponse, error) { +func (c *controller) GetTests(ctx context.Context, take, skip int32, query string) (openapi.ImplResponse, error) { analytics.SendEvent("Test List", "test") if take == 0 { take = 20 } - tests, err := c.testDB.GetTests(ctx, take, skip) + tests, err := c.testDB.GetTests(ctx, take, skip, query) if err != nil { return handleDBError(err), err } diff --git a/server/model/repository.go b/server/model/repository.go index dbde5bfc20..702f77f9af 100644 --- a/server/model/repository.go +++ b/server/model/repository.go @@ -15,7 +15,7 @@ type TestRepository interface { IDExists(context.Context, uuid.UUID) (bool, error) GetLatestTestVersion(context.Context, uuid.UUID) (Test, error) GetTestVersion(_ context.Context, _ uuid.UUID, verson int) (Test, error) - GetTests(_ context.Context, take, skip int32) ([]Test, error) + GetTests(_ context.Context, take, skip int32, query string) ([]Test, error) } type DefinitionRepository interface { diff --git a/server/openapi/api.go b/server/openapi/api.go index 769265e883..b6b64d011b 100644 --- a/server/openapi/api.go +++ b/server/openapi/api.go @@ -58,7 +58,7 @@ type ApiApiServicer interface { GetTestRun(context.Context, string, string) (ImplResponse, error) GetTestRuns(context.Context, string, int32, int32) (ImplResponse, error) GetTestVersionDefinitionFile(context.Context, string, int32) (ImplResponse, error) - GetTests(context.Context, int32, int32) (ImplResponse, error) + GetTests(context.Context, int32, int32, string) (ImplResponse, error) ImportTestRun(context.Context, ExportedTestInformation) (ImplResponse, error) RerunTestRun(context.Context, string, string) (ImplResponse, error) RunTest(context.Context, string) (ImplResponse, error) diff --git a/server/openapi/api_api.go b/server/openapi/api_api.go index 65fbc8f49e..4f6c458ec2 100644 --- a/server/openapi/api_api.go +++ b/server/openapi/api_api.go @@ -452,7 +452,8 @@ func (c *ApiApiController) GetTests(w http.ResponseWriter, r *http.Request) { c.errorHandler(w, r, &ParsingError{Err: err}, nil) return } - result, err := c.service.GetTests(r.Context(), takeParam, skipParam) + queryParam := query.Get("query") + result, err := c.service.GetTests(r.Context(), takeParam, skipParam, queryParam) // If an error occurred, encode the error with the status code if err != nil { c.errorHandler(w, r, err, &result) diff --git a/server/openapi/impl.go b/server/openapi/impl.go index 4a13874ac5..1eead40fbe 100644 --- a/server/openapi/impl.go +++ b/server/openapi/impl.go @@ -9,7 +9,7 @@ package openapi -//Implementation response defines an error code with the associated body +// Implementation response defines an error code with the associated body type ImplResponse struct { Code int Body interface{} diff --git a/server/testdb/mock.go b/server/testdb/mock.go index 8a98fa7afb..8282f751a5 100644 --- a/server/testdb/mock.go +++ b/server/testdb/mock.go @@ -56,8 +56,8 @@ func (m *MockRepository) GetLatestTestVersion(_ context.Context, id uuid.UUID) ( return args.Get(0).(model.Test), args.Error(1) } -func (m *MockRepository) GetTests(_ context.Context, take int32, skip int32) ([]model.Test, error) { - args := m.Called(take, skip) +func (m *MockRepository) GetTests(_ context.Context, take, skip int32, query string) ([]model.Test, error) { + args := m.Called(take, skip, query) return args.Get(0).([]model.Test), args.Error(1) } diff --git a/server/testdb/tests.go b/server/testdb/tests.go index ab920da22e..155d88491e 100644 --- a/server/testdb/tests.go +++ b/server/testdb/tests.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "fmt" + "strings" "time" "github.com/google/uuid" @@ -178,21 +179,32 @@ func (td *postgresDB) GetLatestTestVersion(ctx context.Context, id uuid.UUID) (m return test, nil } -func (td *postgresDB) GetTests(ctx context.Context, take, skip int32) ([]model.Test, error) { - stmt, err := td.db.Prepare(getTestSQL + ` +func (td *postgresDB) GetTests(ctx context.Context, take, skip int32, query string) ([]model.Test, error) { + hasSearchQuery := query != "" + params := []any{take, skip} + + sql := getTestSQL + ` INNER JOIN ( SELECT id as idx, max(version) as latest_version FROM tests GROUP BY idx ) as latestTests ON latestTests.idx = t.id - WHERE t.version = latestTests.latest_version - ORDER BY (t.test ->> 'CreatedAt')::timestamp DESC - LIMIT $1 OFFSET $2 - `) + WHERE t.version = latestTests.latest_version ` + if hasSearchQuery { + params = append(params, "%"+strings.ReplaceAll(query, " ", "%")+"%") + sql += ` AND ( + (t.test ->> 'Name') ilike $3 + OR (t.test ->> 'Description') ilike $3 + )` + } + + sql += ` ORDER BY (t.test ->> 'CreatedAt')::timestamp DESC LIMIT $1 OFFSET $2` + + stmt, err := td.db.Prepare(sql) if err != nil { return nil, err } defer stmt.Close() - rows, err := stmt.QueryContext(ctx, take, skip) + rows, err := stmt.QueryContext(ctx, params...) if err != nil { return nil, err } diff --git a/server/testdb/tests_test.go b/server/testdb/tests_test.go index cf3d82a448..be9d929fc5 100644 --- a/server/testdb/tests_test.go +++ b/server/testdb/tests_test.go @@ -92,22 +92,45 @@ func TestGetTests(t *testing.T) { db, clean := getDB() defer clean() - createTestWithName(t, db, "1") - createTestWithName(t, db, "2") - createTestWithName(t, db, "3") - - actual, err := db.GetTests(context.TODO(), 20, 0) - require.NoError(t, err) - assert.Len(t, actual, 3) - - // test order - assert.Equal(t, "3", actual[0].Name) - assert.Equal(t, "2", actual[1].Name) - assert.Equal(t, "1", actual[2].Name) - - actual, err = db.GetTests(context.TODO(), 20, 10) - require.NoError(t, err) - assert.Len(t, actual, 0) + createTestWithName(t, db, "one") + createTestWithName(t, db, "two") + createTestWithName(t, db, "three") + + t.Run("Order", func(t *testing.T) { + actual, err := db.GetTests(context.TODO(), 20, 0, "") + require.NoError(t, err) + assert.Len(t, actual, 3) + + // test order + assert.Equal(t, "three", actual[0].Name) + assert.Equal(t, "two", actual[1].Name) + assert.Equal(t, "one", actual[2].Name) + }) + + t.Run("Pagination", func(t *testing.T) { + actual, err := db.GetTests(context.TODO(), 20, 10, "") + require.NoError(t, err) + assert.Len(t, actual, 0) + }) + + t.Run("SearchByName", func(t *testing.T) { + _, _ = db.CreateTest(context.TODO(), model.Test{Name: "VerySpecificName"}) + actual, err := db.GetTests(context.TODO(), 10, 0, "specif") + require.NoError(t, err) + assert.Len(t, actual, 1) + + assert.Equal(t, "VerySpecificName", actual[0].Name) + }) + + t.Run("SearchByDescription", func(t *testing.T) { + _, _ = db.CreateTest(context.TODO(), model.Test{Description: "VeryUniqueText"}) + + actual, err := db.GetTests(context.TODO(), 10, 0, "nique") + require.NoError(t, err) + assert.Len(t, actual, 1) + + assert.Equal(t, "VeryUniqueText", actual[0].Description) + }) } func TestGetTestsWithMultipleVersions(t *testing.T) { @@ -126,7 +149,7 @@ func TestGetTestsWithMultipleVersions(t *testing.T) { _, err = db.UpdateTest(context.TODO(), test2) require.NoError(t, err) - tests, err := db.GetTests(context.TODO(), 20, 0) + tests, err := db.GetTests(context.TODO(), 20, 0, "") assert.NoError(t, err) assert.Len(t, tests, 2) diff --git a/web/src/hooks/useInfiniteScroll.ts b/web/src/hooks/useInfiniteScroll.ts index dcc26a4d64..7fd11cab7b 100644 --- a/web/src/hooks/useInfiniteScroll.ts +++ b/web/src/hooks/useInfiniteScroll.ts @@ -6,10 +6,20 @@ type TUseInfiniteScrollParams

= P & { take?: number; }; +export interface InfiniteScrollModel { + isLoading: boolean; + localPage: number; + loadMore: () => void; + hasMore: boolean; + isFetching: boolean; + refresh: () => void; + list: T[]; +} + const useInfiniteScroll = ( useGetDataListQuery: UseQuery, {take = 20, ...queryParams}: TUseInfiniteScrollParams

-) => { +): InfiniteScrollModel => { const [localPage, setLocalPage] = useState(0); const [list, setList] = useState([]); const [lastCount, setLastCount] = useState(0); @@ -34,7 +44,7 @@ const useInfiniteScroll = ( setLastCount(currentList.length); } - }, [currentList]); + }, [currentList, localPage]); const refresh = useCallback(() => { setLocalPage(1); diff --git a/web/src/pages/Home/HomeContent.tsx b/web/src/pages/Home/HomeContent.tsx index 986d3a24ed..13339634a3 100644 --- a/web/src/pages/Home/HomeContent.tsx +++ b/web/src/pages/Home/HomeContent.tsx @@ -1,17 +1,19 @@ -import TestList from './TestList'; +import {useState} from 'react'; +import SearchInput from '../../components/SearchInput'; import * as S from './Home.styled'; import HomeActions from './HomeActions'; -import SearchInput from '../../components/SearchInput'; +import TestList from './TestList'; const HomeContent: React.FC = () => { + const [query, setQuery] = useState(''); return ( All Tests - console.log('onSearch')} placeholder="Search test (Not implemented yet)" /> + setQuery(value)} placeholder="Search test" /> - + ); }; diff --git a/web/src/pages/Home/TestList.tsx b/web/src/pages/Home/TestList.tsx index 2653ecf309..4fa828cdf9 100644 --- a/web/src/pages/Home/TestList.tsx +++ b/web/src/pages/Home/TestList.tsx @@ -1,23 +1,26 @@ -import {useCallback} from 'react'; -import {useNavigate} from 'react-router-dom'; - import InfiniteScroll from 'components/InfiniteScroll'; import TestCard from 'components/TestCard'; -import useInfiniteScroll from 'hooks/useInfiniteScroll'; +import {useCallback} from 'react'; +import {useNavigate} from 'react-router-dom'; import {useGetTestListQuery, useRunTestMutation} from 'redux/apis/TraceTest.api'; import HomeAnalyticsService from 'services/Analytics/HomeAnalytics.service'; import TestAnalyticsService from 'services/Analytics/TestAnalytics.service'; -import {TTest} from 'types/Test.types'; +import useInfiniteScroll from '../../hooks/useInfiniteScroll'; +import {TTest} from '../../types/Test.types'; import * as S from './Home.styled'; import NoResults from './NoResults'; import {useMenuDeleteCallback} from './useMenuDeleteCallback'; const {onTestClick} = HomeAnalyticsService; -const TestList = () => { +interface IProps { + query: string; +} + +const TestList = ({query}: IProps) => { + const {list, isLoading, loadMore, hasMore} = useInfiniteScroll(useGetTestListQuery, {query}); const navigate = useNavigate(); const [runTest] = useRunTestMutation(); - const {list: resultList, hasMore, loadMore, isLoading} = useInfiniteScroll(useGetTestListQuery, {}); const onClick = useCallback( (testId: string) => { @@ -45,11 +48,11 @@ const TestList = () => { loadMore={loadMore} isLoading={isLoading} hasMore={hasMore} - shouldTrigger={Boolean(resultList.length)} + shouldTrigger={Boolean(list.length)} emptyComponent={} > - {resultList?.map(test => ( + {list?.map(test => ( ))} diff --git a/web/src/redux/apis/TraceTest.api.ts b/web/src/redux/apis/TraceTest.api.ts index bae9e27cbb..3e3cd3905b 100644 --- a/web/src/redux/apis/TraceTest.api.ts +++ b/web/src/redux/apis/TraceTest.api.ts @@ -48,8 +48,8 @@ const TraceTestAPI = createApi({ {type: Tags.TEST, id: test?.id}, ], }), - getTestList: build.query({ - query: ({take = 25, skip = 0}) => `/tests?take=${take}&skip=${skip}`, + getTestList: build.query({ + query: ({take = 25, skip = 0, query = ''}) => `/tests?take=${take}&skip=${skip}&query=${query}`, providesTags: () => [{type: Tags.TEST, id: 'LIST'}], transformResponse: (rawTestList: TTest[]) => rawTestList.map(rawTest => Test(rawTest)), }),