This is the codeAbility Sharing Platform! Learn more about the codeAbility Sharing Platform.

Skip to content
Snippets Groups Projects
Commit bc30be06 authored by Lukas Kaltenbrunner's avatar Lukas Kaltenbrunner
Browse files

Merge branch 'clickable_search_info' into 'master'

Clickable search info

See merge request csar9407/gitsearch!2
parents 139ddf77 8c6faf27
Branches
Tags
No related merge requests found
Showing
with 264 additions and 105 deletions
version: '2'
services:
gitsearch-app:
image: docker.uibk.ac.at:443/csar9407/gitsearch:master-3797948b22c16f14441df6f2ced71aacd9e76feb
image: docker.uibk.ac.at:443/csar9407/gitsearch:clickable-search-info-14052f10d9a7662598e87bae3e51c2cc48b2e7ae
environment:
- _JAVA_OPTIONS=-Xmx512m -Xms256m
- SPRING_PROFILES_ACTIVE=prod,swagger
- MANAGEMENT_METRICS_EXPORT_PROMETHEUS_ENABLED=true
- SPRING_DATASOURCE_URL=jdbc:mysql://gitsearch-mysql:3306/gitsearch?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=UTC&createDatabaseIfNotExist=true
- SPRING_DATASOURCE_URL=jdbc:mysql://sharing_mysql:3307/gitsearch?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=UTC&createDatabaseIfNotExist=true
- JHIPSTER_SLEEP=30 # gives time for other services to boot before the application
- SPRING_DATA_JEST_URI=http://gitsearch-elasticsearch:9200
- SPRING_ELASTICSEARCH_REST_URIS=http://gitsearch-elasticsearch:9200
- SPRING_DATA_JEST_URI=http://sharing_elasticsearch:9200
- SPRING_ELASTICSEARCH_REST_URIS=http://sharing_elasticsearch:9200
ports:
- 8080:8080
gitsearch-mysql:
networks:
- backend
- frontend
sharing_mysql:
extends:
file: mysql.yml
service: gitsearch-mysql
gitsearch-elasticsearch:
extends:
file: elasticsearch.yml
service: gitsearch-elasticsearch
service: sharing_mysql
networks:
- backend
networks:
frontend:
name: sharing_frontend
driver: bridge
backend:
name: sharing_backend
driver: bridge
internal: true
version: '2'
services:
gitsearch-mysql:
sharing_mysql:
image: mysql:8.0.20
# volumes:
# - ~/volumes/jhipster/gitsearch/mysql/:/var/lib/mysql/
......@@ -9,5 +9,5 @@ services:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
- MYSQL_DATABASE=gitsearch
ports:
- 3306:3306
- 3307:3306
command: mysqld --lower_case_table_names=1 --skip-ssl --character_set_server=utf8mb4 --explicit_defaults_for_timestamp
// The code in this file and the files using this file is partially taken from
// https://stackoverflow.com/questions/46487255/pass-observable-data-from-parent-component-to-a-child-component-created-with-com
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { IMetadataSelection, MetadataSelection } from 'app/search/metadata/metadata-selection.component';
@Injectable()
export class MetadataMessageService {
filterSelection$: Observable<IMetadataSelection>;
private filterSelectionSubject: Subject<IMetadataSelection> = new Subject<IMetadataSelection>();
constructor() {
this.filterSelection$ = this.filterSelectionSubject.asObservable();
}
public updateFilterSelection(metadataCategory: string, value: string): void {
this.filterSelectionSubject.next(new MetadataSelection(metadataCategory, value));
}
}
export interface IMetadataSelection {
category: string;
value: string;
}
export class MetadataSelection implements IMetadataSelection {
constructor(public category: string, public value: string) {}
}
<div class="solid" [hidden]="frequencies == null">
<span class="meta" jhiTranslate="search.metadata.{{parameter}}">file format</span>
<li class="meta" *ngFor="let frequency of frequencies">
<span class="clear" float="right" jhiTranslate="search.metadata.clearLocalFilters"
(click)="clearFilters()">clear all</span>
<li class="unselected" *ngFor="let frequency of frequencies"
(click)="toggleSelection(frequency.key)"
[ngClass]="isSelected(frequency.key) ? 'selected' : 'unselected'">
<div>
<a (click)="suggestionWasClicked(frequency)" class="meta">{{frequency.key}}<span
class="count">{{frequency.value}}</span></a>
<a class="meta">{{frequency.key}}
<span class="count">{{frequency.value}}</span>
</a>
</div>
</li>
</div>
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {IFrequency} from "app/shared/model/frequency.model";
import { Component, Input } from '@angular/core';
import { IFrequency } from 'app/shared/model/frequency.model';
import { MetadataMessageService } from 'app/search/metadata/metadata-message.service';
@Component({
selector: 'jhi-home-metadata',
templateUrl: './metadata.component.html',
styleUrls: ['./metadata.scss'],
})
export class MetadataComponent<T> {
@Input() frequencies!: IFrequency<T>[] | undefined;
export class MetadataComponent {
@Input() frequencies!: IFrequency<string>[] | undefined;
@Input() parameter!: string;
@Output() onSuggest: EventEmitter<IFrequency<T>> = new EventEmitter();
constructor() {
testString = '';
selectedItems = new Set<string>();
constructor(public messageService: MetadataMessageService) {}
toggleSelection(name: string): void {
this.messageService.updateFilterSelection(this.parameter, name);
if (this.selectedItems.has(name)) {
this.selectedItems.delete(name);
} else {
this.selectedItems.add(name);
}
}
isSelected(name: string): boolean {
return this.selectedItems.has(name);
}
suggestionWasClicked(frequency: IFrequency<T>): void {
this.onSuggest.emit(frequency);
clearFilters(): void {
for (const selectedItem of this.selectedItems) {
this.messageService.updateFilterSelection(this.parameter, selectedItem);
}
this.selectedItems = new Set<string>();
}
}
li.meta:hover {
background-color: #CACACA;
background-color: #cacaca;
}
li.meta {
list-style-type: none;
......@@ -23,3 +23,24 @@ div.solid {
border-color: rgb(223, 226, 230);
margin-bottom: 20px;
}
span.clear {
float: right;
padding-left: 10px;
padding-right: 10px;
cursor: pointer;
font-size: 0.9em;
}
span.clear:hover {
background-color: rgba(0, 0, 0, 0.1);
}
li.selected {
list-style-type: none;
padding-left: 20px;
padding-right: 20px;
background-color: rgb(200, 200, 200);
}
li.unselected {
list-style-type: none;
padding-left: 20px;
padding-right: 20px;
}
......@@ -5,61 +5,53 @@
<span jhiTranslate="search.usage">The search mask allows you to search for full texts in the sharing platform based on various search criteria (e.g., full-text search, programming languages, keywords, etc.). Boolean operators can be used in the search mask for full texts. Apart from the full-text search, no further fields are to be specified.</span>&nbsp;
<div [ngSwitch]="isAuthenticated()">
<div class="row">
<div class="col-sm-12">
<form name="searchForm" class="form-inline">
<div class="input-group w-100 mt-3">
<ng-container [queryParamGroup]="paramGroup">
<ng-container [queryParamGroup]="paramGroup">
<div class="row">
<div class="col-sm-12">
<form name="searchForm" class="form-inline">
<div class="input-group w-100 mt-3">
<input type="text" class="form-control" queryParamName="searchText"
placeholder="{{ 'search.filters.search' | translate }}"/>
</ng-container>
</div>
</form>
</div>
</form>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-3">
<form name="searchForm" class="form-inline">
<div class="input-group w-100 mt-3">
<ng-container [queryParamGroup]="paramGroup">
<div class="row">
<div class="col-sm-3">
<form name="searchForm" class="form-inline">
<div class="input-group w-100 mt-3">
<input type="text" class="form-control" queryParamName="programmingLanguage"
placeholder="{{ 'search.filters.programmingLanguage' | translate }}"/>
</ng-container>
</div>
</form>
</div>
<div class="col-sm-3">
<form name="searchForm" class="form-inline">
<div class="input-group w-100 mt-3">
<ng-container [queryParamGroup]="paramGroup">
</div>
</form>
</div>
<div class="col-sm-3">
<form name="searchForm" class="form-inline">
<div class="input-group w-100 mt-3">
<input type="text" class="form-control" queryParamName="keywords"
placeholder="{{ 'search.filters.keywords' | translate }}"/>
</ng-container>
</div>
</form>
</div>
<div class="col-sm-3">
<form name="searchForm" class="form-inline">
<div class="input-group w-100 mt-3">
<ng-container [queryParamGroup]="paramGroup">
</div>
</form>
</div>
<div class="col-sm-3">
<form name="searchForm" class="form-inline">
<div class="input-group w-100 mt-3">
<input type="text" class="form-control" queryParamName="author"
placeholder="{{ 'search.filters.author' | translate }}"/>
</ng-container>
</div>
</form>
</div>
<div class="col-sm-3">
<form name="searchForm" class="form-inline">
<div class="input-group w-100 mt-3">
<ng-container [queryParamGroup]="paramGroup">
</div>
</form>
</div>
<div class="col-sm-3">
<form name="searchForm" class="form-inline">
<div class="input-group w-100 mt-3">
<input type="text" class="form-control" queryParamName="license"
placeholder="{{ 'search.filters.license' | translate }}"/>
</ng-container>
</div>
</form>
</div>
</form>
</div>
</div>
</div>
</ng-container>
<div class="row">
<div class="col-2">
......
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Subject, Subscription} from 'rxjs';
import {LoginModalService} from 'app/core/login/login-modal.service';
import {AccountService} from 'app/core/auth/account.service';
import {Account} from 'app/core/user/account.model';
import {HttpResponse} from '@angular/common/http';
import {ActivatedRoute, Router} from '@angular/router';
import {QueryParam, QueryParamBuilder, QueryParamGroup} from '@ngqp/core';
import {takeUntil} from 'rxjs/operators';
import {IFrequency} from 'app/shared/model/frequency.model';
import {IGitFilesPageDetails} from 'app/shared/model/git-files-page-details.model';
import {IGitFilesAggregation} from 'app/shared/model/git-files-aggregation';
import {IGitFiles} from 'app/shared/model/git-files.model';
import {SearchInput} from "app/shared/model/search-input.model";
import {SearchResultService} from "app/search/search/search-result.service";
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { LoginModalService } from 'app/core/login/login-modal.service';
import { AccountService } from 'app/core/auth/account.service';
import { Account } from 'app/core/user/account.model';
import { HttpResponse } from '@angular/common/http';
import { ActivatedRoute, Router } from '@angular/router';
import { QueryParam, QueryParamBuilder, QueryParamGroup } from '@ngqp/core';
import { takeUntil } from 'rxjs/operators';
import { IFrequency } from 'app/shared/model/frequency.model';
import { IGitFilesPageDetails, GitFilesPageDetails } from 'app/shared/model/git-files-page-details.model';
import { IGitFilesAggregation } from 'app/shared/model/git-files-aggregation';
import { IGitFiles } from 'app/shared/model/git-files.model';
import { SearchInput } from 'app/shared/model/search-input.model';
import { SearchResultService } from 'app/search/search/search-result.service';
import { MetadataMessageService } from 'app/search/metadata/metadata-message.service';
import { IMetadataSelection } from 'app/search/metadata/metadata-selection.component';
@Component({
selector: 'jhi-search',
......@@ -26,26 +28,34 @@ export class SearchComponent implements OnInit, OnDestroy {
account: Account | null = null;
authSubscription?: Subscription;
// Stores all search results for the current query.
// Used to locally apply filters for e.g. repository and file format.
// When local filters change, the results are recalculated from this variable.
private queryResult?: IGitFilesPageDetails;
gitFilesPageDetails?: IGitFilesPageDetails;
gitFilesAggregation?: IGitFilesAggregation;
private filterSelectionSubscription: Subscription = new Subscription();
private selectedRepositories = new Set<string>();
private selectedUniversities = new Set<string>();
private selectedFileFormats = new Set<string>();
public paramGroup: QueryParamGroup;
private componentDestroyed$ = new Subject<void>();
public searchInput: SearchInput;
constructor(
protected searchResultService: SearchResultService,
private accountService: AccountService,
private loginModalService: LoginModalService,
protected activatedRoute: ActivatedRoute,
private router: Router,
private qpb: QueryParamBuilder
private qpb: QueryParamBuilder,
private filterSelectionService: MetadataMessageService
) {
this.searchInput = new SearchInput()
this.searchInput = new SearchInput();
this.paramGroup = qpb.group({
searchText: qpb.stringParam('q', {
......@@ -100,6 +110,9 @@ export class SearchComponent implements OnInit, OnDestroy {
this.loadAggregation();
this.loadPageDetails();
this.authSubscription = this.accountService.getAuthenticationState().subscribe(account => (this.account = account));
this.filterSelectionSubscription.add(
this.filterSelectionService.filterSelection$.subscribe(selection => this.updateSearchInfoFilter(selection))
);
}
loadAggregation(): void {
......@@ -116,14 +129,88 @@ export class SearchComponent implements OnInit, OnDestroy {
loadPageDetails(): void {
if (this.searchInput.fulltextQuery) {
this.searchResultService
.searchPageDetails(this.searchInput, this.pageSize)
.subscribe((res: HttpResponse<IGitFilesPageDetails>) => {
this.gitFilesPageDetails = res.body || undefined;
});
this.searchResultService.searchPageDetails(this.searchInput, this.pageSize).subscribe((res: HttpResponse<IGitFilesPageDetails>) => {
if (res.body === null) {
this.queryResult = undefined;
} else {
this.queryResult = new GitFilesPageDetails(res.body.gitFiles, res.body.gitFiles.length);
this.updatePageDetails();
}
});
}
}
// Applies local filters to search results
// and sets this.gitFilesPageDetails accordingly.
private updatePageDetails(): void {
if (this.queryResult !== undefined) {
const matchingResults = this.queryResult.gitFiles.filter(element => this.matchesSelection(element));
if (matchingResults.length > 0) {
this.gitFilesPageDetails = new GitFilesPageDetails(matchingResults, matchingResults.length);
} else {
this.gitFilesPageDetails = undefined;
}
}
}
// When the user updates the metadata filter selection
// this function is called.
// It updates the local filters.
// Currently, for any category (at the moment repository and file format)
// there is a set of allowed values.
// If the selected item is not in the corresponding set it is added.
// Otherwise it is removed.
// If a set is empty the corresponding filter is not applied.
// For non-empty sets, the results which will be displayed must match one of the set's elements.
// (e.g. if the selectedRepositories set contains "foo" and "bar",
// only results from these two repos will be shown.)
public updateSearchInfoFilter(selection: IMetadataSelection): void {
switch (selection.category) {
case 'repository': {
if (this.selectedRepositories.has(selection.value)) {
this.selectedRepositories.delete(selection.value);
} else {
this.selectedRepositories.add(selection.value);
}
break;
}
case 'university': {
// University seems to be a derived attribute.
// Currently not implemented.
alert('Filtering by university is not supported at the moment. Cause: University is not in the interface IGitFiles.');
/* if (this.selectedUniversities.has(selection.value)) {
this.selectedUniversities.delete(selection.value);
} else {
this.selectedUniversities.add(selection.value);
} */
break;
}
case 'fileFormat': {
if (this.selectedFileFormats.has(selection.value)) {
this.selectedFileFormats.delete(selection.value);
} else {
this.selectedFileFormats.add(selection.value);
}
break;
}
}
this.updatePageDetails();
}
// Checks if a given file matches the local filters.
matchesSelection(file: IGitFiles): boolean {
if (this.selectedRepositories.size > 0 && !this.selectedRepositories.has(file.repository)) {
return false;
}
/* if (this.selectedUniversities.size > 0 && !this.selectedUniversities.has(file.university)) {
return false;
} */
if (this.selectedFileFormats.size > 0 && !this.selectedFileFormats.has(file.fileFormat)) {
return false;
}
return true;
}
isAuthenticated(): boolean {
return this.accountService.isAuthenticated();
}
......@@ -136,6 +223,9 @@ export class SearchComponent implements OnInit, OnDestroy {
if (this.authSubscription) {
this.authSubscription.unsubscribe();
}
if (this.filterSelectionSubscription) {
this.filterSelectionSubscription.unsubscribe();
}
this.componentDestroyed$.next();
this.componentDestroyed$.complete();
}
......@@ -173,4 +263,3 @@ export class SearchComponent implements OnInit, OnDestroy {
return hits ? hits : 0;
}
}
......@@ -2,20 +2,17 @@ import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { GitSearchV2SharedModule } from 'app/shared/shared.module';
import {SearchComponent} from "app/search/search.component";
import {SEARCH_ROUTE} from "app/search/search-routing.module";
import {MetadataComponent} from "app/search/metadata/metadata.component";
import {HighlightingComponent} from "app/search/highlighting/highlighting.component";
import {QueryParamModule} from "@ngqp/core";
import {NgSelectModule} from "@ng-select/ng-select";
import { SearchComponent } from 'app/search/search.component';
import { SEARCH_ROUTE } from 'app/search/search-routing.module';
import { MetadataComponent } from 'app/search/metadata/metadata.component';
import { HighlightingComponent } from 'app/search/highlighting/highlighting.component';
import { QueryParamModule } from '@ngqp/core';
import { NgSelectModule } from '@ng-select/ng-select';
import { MetadataMessageService } from 'app/search/metadata/metadata-message.service';
@NgModule({
imports: [GitSearchV2SharedModule, RouterModule.forChild([SEARCH_ROUTE]), QueryParamModule, NgSelectModule],
declarations: [
SearchComponent,
MetadataComponent,
HighlightingComponent
],
declarations: [SearchComponent, MetadataComponent, HighlightingComponent],
providers: [MetadataMessageService],
})
export class SearchModule {}
......@@ -5,6 +5,7 @@
"usage": "Die Suchmaske erlaubt es aufbauend auf diversen Suchkriterien (z.B. Volltextsuche, Programmiersprachen, Schlüsselwörter usw.) nach Volltexten in der Sharing Plattform zu suchen. In der Suchmaske für Volltexte können boolsche Operatoren verwendet werden. Abgesehen von der Volltextsuche sind keine weitern Felder angegeben werden.",
"metadata": {
"filter": "Suchfilter",
"clearLocalFilters": "Alle löschen",
"information": "Suchinformation",
"fileFormat": "Dateiformat",
"repository": "Repository",
......
......@@ -5,6 +5,7 @@
"usage": "The search mask allows you to search for full texts in the sharing platform based on various search criteria (e.g. full text search, programming languages, keywords, etc.). Boolean operators can be used in the search mask for full texts. Apart from the full text search, no further fields are to be specified.",
"metadata": {
"filter": "Search filter",
"clearLocalFilters": "Clear all",
"information": "Search information",
"fileFormat": "File format",
"repository": "Repository",
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment