diff --git a/estela-api/api/serializers/project.py b/estela-api/api/serializers/project.py index c6830ebd..886d3042 100644 --- a/estela-api/api/serializers/project.py +++ b/estela-api/api/serializers/project.py @@ -220,3 +220,12 @@ class ProjectActivitySerializer(serializers.Serializer): count = serializers.IntegerField( required=True, help_text="Project activities count." ) + + +class ProjectSearchSerializer(serializers.Serializer): + results = ProjectSerializer( + many=True, required=True, help_text="Project search results." + ) + count = serializers.IntegerField( + required=True, help_text="Project search results count." + ) diff --git a/estela-api/api/views/project.py b/estela-api/api/views/project.py index fa7aaf81..25410f4f 100644 --- a/estela-api/api/views/project.py +++ b/estela-api/api/views/project.py @@ -17,6 +17,7 @@ ActivitySerializer, ProjectActivitySerializer, ProjectSerializer, + ProjectSearchSerializer, ProjectUpdateSerializer, ProjectUsageSerializer, UsageRecordSerializer, @@ -59,6 +60,53 @@ def get_queryset(self): else self.request.user.project_set.filter(deleted=False) ) + @swagger_auto_schema( + methods=["GET"], + manual_parameters=[ + openapi.Parameter( + "page", + openapi.IN_QUERY, + description="A page number within the paginated result set.", + type=openapi.TYPE_NUMBER, + required=False, + ), + openapi.Parameter( + "page_size", + openapi.IN_QUERY, + description="Number of results to return per page.", + type=openapi.TYPE_NUMBER, + required=False, + ), + openapi.Parameter( + "search", + openapi.IN_QUERY, + description="Search for a project by name.", + type=openapi.TYPE_STRING, + required=False, + ), + ], + responses={status.HTTP_200_OK: ProjectSearchSerializer()}, + ) + @action(methods=["GET"], detail=False) + def search(self, request, *args, **kwargs): + page, page_size = self.get_parameters(request) + if page_size > self.MAX_PAGINATION_SIZE or page_size < self.MIN_PAGINATION_SIZE: + raise ParseError({"error": errors.INVALID_PAGE_SIZE}) + if page < 1: + raise ParseError({"error": errors.INVALID_PAGE_SIZE}) + search = request.query_params.get("search", None) + projects = self.get_queryset() + if search: + projects = projects.filter(name__icontains=search) + + paginator_result = Paginator(projects, page_size) + page_result = paginator_result.page(page) + serializer = ProjectSerializer(page_result, many=True) + return Response( + {"results": serializer.data, "count": projects.count()}, + status=status.HTTP_200_OK, + ) + def perform_create(self, serializer): instance = serializer.save() instance.users.add( diff --git a/estela-api/docs/api.yaml b/estela-api/docs/api.yaml index dcfaef48..d9c965b8 100644 --- a/estela-api/docs/api.yaml +++ b/estela-api/docs/api.yaml @@ -437,6 +437,34 @@ paths: tags: - api parameters: [] + /api/projects/search: + get: + operationId: api_projects_search + description: '' + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: number + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: number + - name: search + in: query + description: Search for a project by name. + required: false + type: string + responses: + '200': + description: '' + schema: + $ref: '#/definitions/ProjectSearch' + tags: + - api + parameters: [] /api/projects/{pid}: get: operationId: api_projects_read @@ -1797,6 +1825,21 @@ definitions: type: integer maximum: 65535 minimum: 0 + ProjectSearch: + required: + - results + - count + type: object + properties: + results: + description: Project search results. + type: array + items: + $ref: '#/definitions/Project' + count: + title: Count + description: Project search results count. + type: integer ProjectUpdate: type: object properties: diff --git a/estela-web/src/pages/ProjectListPage/index.tsx b/estela-web/src/pages/ProjectListPage/index.tsx index 95b1becf..d9b8e23a 100644 --- a/estela-web/src/pages/ProjectListPage/index.tsx +++ b/estela-web/src/pages/ProjectListPage/index.tsx @@ -9,7 +9,13 @@ import Bug from "../../assets/icons/bug.svg"; import FolderDotted from "../../assets/icons/folderDotted.svg"; import WelcomeProjects from "../../assets/images/welcomeProjects.svg"; import history from "../../history"; -import { ApiProjectsListRequest, ApiProjectsCreateRequest, Project, ProjectCategoryEnum } from "../../services/api"; +import { + ApiProjectsCreateRequest, + ApiProjectsSearchRequest, + Project, + ProjectSearch, + ProjectCategoryEnum, +} from "../../services/api"; import { incorrectDataNotification, Spin, PaginationItem } from "../../shared"; import { UserContext, UserContextProps } from "../../context/UserContext"; @@ -28,10 +34,14 @@ interface ProjectList { interface ProjectsPageState { projects: ProjectList[]; + recentProjects: ProjectList[]; + recentProjectsLoaded: boolean; username: string; loaded: boolean; + loadedProjects: boolean; count: number; current: number; + query: string; modalNewProject: boolean; modalWelcome: boolean; newProjectName: string; @@ -39,17 +49,21 @@ interface ProjectsPageState { } export class ProjectListPage extends Component { - PAGE_SIZE = 10; + PAGE_SIZE = 3; totalProjects = 0; state: ProjectsPageState = { projects: [], + recentProjects: [], + recentProjectsLoaded: false, username: "", loaded: false, + loadedProjects: false, count: 0, current: 0, + query: "", modalNewProject: false, - modalWelcome: false, + modalWelcome: true, newProjectName: "", newProjectCategory: ProjectCategoryEnum.NotSpecified, }; @@ -104,7 +118,7 @@ export class ProjectListPage extends Component { emptyText = (): ReactElement => ( -

No projects yet.

+

No projects

); @@ -112,26 +126,7 @@ export class ProjectListPage extends Component { const { updateRole } = this.context as UserContextProps; updateRole && updateRole(""); AuthService.removeFramework(); - const data = await this.getProjects(1); - const projectData: ProjectList[] = data.data.map((project: Project, id: number) => { - return { - name: project.name, - category: project.category, - framework: project.framework, - pid: project.pid, - role: - project.users?.find((user) => user.user?.username === AuthService.getUserUsername())?.permission || - "ADMIN", - key: id, - }; - }); - this.setState({ - projects: [...projectData], - count: data.count, - current: data.current, - loaded: true, - modalWelcome: data.count === 0, - }); + this.updateFilteredProjects(this.state.query, 1); } handleInputChange = (event: React.ChangeEvent): void => { @@ -175,17 +170,8 @@ export class ProjectListPage extends Component { return String(AuthService.getUserUsername()); }; - async getProjects(page: number): Promise<{ data: Project[]; count: number; current: number }> { - const requestParams: ApiProjectsListRequest = { page, pageSize: this.PAGE_SIZE }; - const data = await this.apiService.apiProjectsList(requestParams); - this.totalProjects = data.count; - return { data: data.results, count: data.count, current: page }; - } - - onPageChange = async (page: number): Promise => { - this.setState({ loaded: false }); - const data = await this.getProjects(page); - const projectData: ProjectList[] = data.data.map((project: Project, id: number) => { + formatProjectData = (response: Project[]): void => { + const projectData: ProjectList[] = response.map((project: Project, id: number) => { return { name: project.name, pid: project.pid, @@ -196,18 +182,61 @@ export class ProjectListPage extends Component { key: id, }; }); + if (!this.state.recentProjectsLoaded) { + this.setState({ + recentProjects: [...projectData], + recentProjectsLoaded: true, + modalWelcome: projectData.length === 0, + }); + } this.setState({ projects: [...projectData], - count: data.count, - current: data.current, loaded: true, - modalWelcome: data.count === 0, }); }; + updateFilteredProjects = async (query: string, page: number): Promise => { + const requestParams: ApiProjectsSearchRequest = { search: query, page: page, pageSize: this.PAGE_SIZE }; + this.apiService.apiProjectsSearch(requestParams).then((response: ProjectSearch) => { + this.formatProjectData(response.results); + this.setState({ + count: response.count, + current: page, + loadedProjects: true, + }); + }); + }; + + onPageChange = async (page: number): Promise => { + this.setState({ loadedProjects: false }); + this.updateFilteredProjects(this.state.query, page); + }; + + onQueryChange = (event: React.ChangeEvent): void => { + this.setState({ loadedProjects: false }); + const { + target: { value, name }, + } = event; + if (name === "query") { + this.updateFilteredProjects(value, 1); + } + this.setState({ query: value }); + }; + render(): JSX.Element { - const { projects, count, current, loaded, modalNewProject, modalWelcome, newProjectName, newProjectCategory } = - this.state; + const { + projects, + recentProjects, + count, + current, + loaded, + loadedProjects, + modalNewProject, + modalWelcome, + newProjectName, + newProjectCategory, + query, + } = this.state; return ( <> {loaded ? ( @@ -216,19 +245,19 @@ export class ProjectListPage extends Component { { this.setState({ modalWelcome: false }); }} > - - WELCOME SCRAPER! - + + WELCOME SCRAPER! + Start by creating a project to be able to deploy your spiders and start with your scraping. - + Remember to install the  {  to be able to deploy your spiders! - + @@ -267,8 +296,8 @@ export class ProjectListPage extends Component { - {projects.map((project: ProjectList, index) => { - return index < 3 ? ( + {recentProjects.map((project: ProjectList, index) => { + return index < 4 ? (