mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-22 23:42:11 +01:00
Add additional help to quick search and an advanced search documentation page (#1757)
Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
This commit is contained in:
committed by
GitHub
parent
f2249cea73
commit
ddd2fc1055
2
gradle/changelog/search_syntax.yaml
Normal file
2
gradle/changelog/search_syntax.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: Added
|
||||
description: Add additional help to quick search and an advanced search documentation page ([#1757](https://github.com/scm-manager/scm-manager/pull/1757)
|
||||
49
scm-core/src/main/java/sonia/scm/search/SearchableField.java
Normal file
49
scm-core/src/main/java/sonia/scm/search/SearchableField.java
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
package sonia.scm.search;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
/**
|
||||
* A field of a {@link SearchableType}.
|
||||
*
|
||||
* @since 2.23.0
|
||||
*/
|
||||
@Beta
|
||||
public interface SearchableField {
|
||||
|
||||
/**
|
||||
* Returns the name of the searchable field.
|
||||
*
|
||||
* @return field name
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Returns the type of the searchable field.
|
||||
*
|
||||
* @return field type
|
||||
*/
|
||||
Class<?> getType();
|
||||
}
|
||||
@@ -26,6 +26,8 @@ package sonia.scm.search;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* A type which can be searched with the {@link SearchEngine}.
|
||||
*
|
||||
@@ -47,4 +49,12 @@ public interface SearchableType {
|
||||
* @return class of type
|
||||
*/
|
||||
Class<?> getType();
|
||||
|
||||
/**
|
||||
* Returns collection of searchable fields.
|
||||
*
|
||||
* @return collection of searchable fields
|
||||
* @since 2.23.0
|
||||
*/
|
||||
Collection<? extends SearchableField> getFields();
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ public class VndMediaType {
|
||||
public static final String NOTIFICATION_COLLECTION = PREFIX + "notificationCollection" + SUFFIX;
|
||||
|
||||
public static final String QUERY_RESULT = PREFIX + "queryResult" + SUFFIX;
|
||||
public static final String SEARCHABLE_TYPE_COLLECTION = PREFIX + "searchableTypeCollection" + SUFFIX;
|
||||
|
||||
private VndMediaType() {
|
||||
}
|
||||
|
||||
33
scm-ui/ui-api/src/help/search/modal.de.ts
Normal file
33
scm-ui/ui-api/src/help/search/modal.de.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
export default `Unsere Suche basiert auf Lucene. Sie können die [vollständige Syntax](http://lucene.apache.org/core/8_9_0/index.html) nutzen.
|
||||
|
||||
Um mehr über die fortgeschrittene Suche zu erfahren, lesen sie unsere [Expertenseite](/help/search-syntax).
|
||||
|
||||
### Einfache Suche
|
||||
|
||||
- Die relevantesten Repositories werden in den Quick Results angezeigt.
|
||||
- Über die Eingabe-Taste oder den Button "Alle Ergebnisse anzeigen" bekommen Sie Ergebnisse aller durchsuchten Entitäten wie Nutzern oder Gruppen.
|
||||
- Eine Wildcard für eine beliebige Anzahl an beliebigen Zeichen wird Ihrer Suche standardmäßig angehängt.
|
||||
- Geben Sie keine Wildcards vor dem Suchbegriff ein!`;
|
||||
33
scm-ui/ui-api/src/help/search/modal.en.ts
Normal file
33
scm-ui/ui-api/src/help/search/modal.en.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
export default `Our search uses Lucene and you may use the [full syntax](http://lucene.apache.org/core/8_9_0/index.html).
|
||||
|
||||
To learn about advanced search read our [Expert Search Site](/help/search-syntax).
|
||||
|
||||
### Basic Search
|
||||
|
||||
- The most relevant repositories are shown in the quick results.
|
||||
- Press "enter" or click the "Show all results" button to find more results for all entities like users or groups.
|
||||
- A multi-character wildcard (*) is added to your search by default.
|
||||
- Do not enter Wildcards in front of the search!`;
|
||||
148
scm-ui/ui-api/src/help/search/syntax.de.ts
Normal file
148
scm-ui/ui-api/src/help/search/syntax.de.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
export default `## Modifikatoren
|
||||
|
||||
Hinweis: Sie können keine Wildcards als erstes Zeichen einer Suche verwenden.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<th>Beispiel</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>? - Einzelzeichen-Wildcard</td>
|
||||
<td>Ultimate?Repo - findet z.B. Ultimate-Repo, Ultimate Repo, Ultimate+Repo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>* - mehrstelliger Platzhalter</td>
|
||||
<td>Ultimat*y - findet z.B. Ultimate Repository, Ultimates-Spezial-Repository, Ultimate</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
### Bereiche
|
||||
|
||||
Bereichsabfragen ermöglichen den Abgleich von Dokumenten, deren Feldwerte zwischen der unteren und der oberen Grenze liegen, die in der Bereichsabfrage angegeben sind. Bereichsabfragen können die obere und untere Grenze einschließen oder ausschließen. Die Sortierung erfolgt lexikografisch.
|
||||
|
||||
Bereiche sind nicht auf numerische Felder beschränkt.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<th>Beispiel</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>[ … TO … ] - inklusiver Bereich</td>
|
||||
<td>creationDate:[1609459200000 TO 1612137600000] – findet z.B. Repositories, die zwischen dem 01.01.2021 und dem 01.02.2021 angelegt wurden.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{… TO …} - ausschließender Bereich</td>
|
||||
<td>name:{Aida TO Carmen} – findet Namen zwischen Aida und Carmen, jedoch ohne die beiden Namen einzuschließen.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## Boosten
|
||||
|
||||
Mit dem Boosting können Sie die Relevanz eines Dokuments steuern, indem Sie seinen Term verstärken.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<th>Beispiel</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>term^Zahl</td>
|
||||
<td>ultimate^2 repository – erhöht die Relevanz von „ultimate"</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
Standardmäßig ist der Boost-Faktor 1. Obwohl der Boost-Faktor positiv sein muss, kann er kleiner als 1 sein (z. B. 0,2)
|
||||
|
||||
Standardmäßig werden Repository-Namen um 1,5 und Namespace-Namen um 1,25 geboostet.
|
||||
|
||||
## Boolesche Operatoren
|
||||
|
||||
Hinweis: Logische Operatoren müssen in Großbuchstaben eingegeben werden (z. B. „AND").
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<th>Beispiel</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AND – beide Terme müssen enthalten sein</td>
|
||||
<td>Ultimate AND Repository – findet z.B. Ultimate Repository, Ultimate Special Repository
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OR – mindestens einer der Terme muss enthalten sein</td>
|
||||
<td>Ultimate OR Repository – findet z.B.. Ultimate Repository, Ultimate User, Special Repository</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NOT – der nachfolgende Term darf nicht enthalten sein. „!" kann alternativ verwendet werden.</td>
|
||||
<td>Ultimate NOT Repository – findet z.B.. Ultimate user, nicht jedoch z.B. Ultimate Repository</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>– schließt den folgenden Term von der Suche aus</td>
|
||||
<td>Ultimate Repository -Special – findet z.B. Ultimate Repository, schließt z.B. Ultimate Special Repository aus</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>– der folgende Term muss enthalten sein</td>
|
||||
<td>Ultimate +Repository – findet z.B. my Repository, Ultimate Repository</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## Gruppieren
|
||||
|
||||
Die Suche unterstützt die Verwendung von Klammern zur Gruppierung von Begriffen, um Teilabfragen zu bilden. Dies kann sehr nützlich sein, wenn Sie die boolesche Logik für eine Abfrage steuern möchten.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<th>Beispiel</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>() – Terme zwischen den Klammern werden gruppiert</td>
|
||||
<td>(Ultimate OR my) AND Repository – findet z.B.. Ultimate Repository, my Repository, schließt z.B. Super Repository. Entweder "Ultimate" oder “My” müssen im Ergebnis existieren, “Repository” muss immer enthalten sein.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## Umgang mit Sonderzeichen
|
||||
|
||||
Die Suche unterstützt Sonderzeichen, die Teil der Abfragesyntax sind. Die aktuellen Sonderzeichen der Liste sind
|
||||
|
||||
+ - && || ! ( ) { } [ ] ^ " ~ * ? : \\ /
|
||||
|
||||
Um diese Zeichen zu nutzen, verwenden Sie das „\\" vor dem jeweiligen Zeichen. Um zum Beispiel nach (1+1):2 zu suchen, verwenden Sie diese Abfrage:
|
||||
|
||||
\\(1\\+1\\)\\:2
|
||||
|
||||
Partiell übersetzt mit www.DeepL.com/Translator (kostenlose Version)
|
||||
|
||||
Quelle [https://javadoc.io/static/org.apache.lucene/lucene-queryparser/8.9.0/org/apache/lucene/queryparser/classic/package-summary.html#package.description](https://javadoc.io/static/org.apache.lucene/lucene-queryparser/8.9.0/org/apache/lucene/queryparser/classic/package-summary.html#package.description)`;
|
||||
145
scm-ui/ui-api/src/help/search/syntax.en.ts
Normal file
145
scm-ui/ui-api/src/help/search/syntax.en.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
export default `### Modifiers
|
||||
|
||||
Note: You can not use wildcards as the first character of a search.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>? - single character Wildcard</td>
|
||||
<td>Ultimate?Repo – finds e.g. Ultimate-Repo, Ultimate Repo, Ultimate+Repo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>* - multiple character Wildcard</td>
|
||||
<td>Ultimat*y - finds e.g. Ultimate Repository, Ultimate-Special-Repository, Ultimately</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
### Ranges
|
||||
|
||||
Range Queries allow one to match documents whose field(s) values are between the lower and upper bound specified by the Range Query. Range Queries can be inclusive or exclusive of the upper and lower bounds. Sorting is done lexicographically.
|
||||
|
||||
Ranges are not reserved to numerical fields.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>[ … TO … ] - inclusive range</td>
|
||||
<td>creationDate:[1609459200000 TO 1612137600000] – finds e.G. repositories created between 2021-01-01 and 2021-02-01 </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{… TO …} - exclusive range</td>
|
||||
<td>name:{Aida TO Carmen} – finds e.G. repositories with names between Aida and Carmen, excluding these to values.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
### Boosting
|
||||
|
||||
Boosting allows you to control the relevance of a document by boosting its term.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>term^number</td>
|
||||
<td>ultimate^2 repository – makes the term "ultimate" more relevant. </td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
By default, the boost factor is 1. Although the boost factor must be positive, it can be less than 1 (e.g. 0.2)
|
||||
|
||||
By default Repository names are boosted by 1.5, namespace by 1.25.
|
||||
|
||||
## Boolean Operators
|
||||
|
||||
Note: Logical Operators must be entered in upper case (e.g. "AND").
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AND – both terms must be included</td>
|
||||
<td>Ultimate AND Repository – finds e.g. Ultimate Repository, Ultimate Special Repository</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OR – at least one of the terms must be included</td>
|
||||
<td>Ultimate OR Repository – finds e.g. Ultimate Repository, Ultimate User, Special Repository</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NOT – following term may not be included, "!" may be used alternatively</td>
|
||||
<td>Ultimate NOT Repository – finds e.g. Ultimate user, excludes e.g. Ultimate Repository</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>– excludes following term from search</td>
|
||||
<td>Ultimate Repository -Special – finds e.g. Ultimate Repository, excludes e.g. Ultimate Special Repository</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>– following term must be included</td>
|
||||
<td>Ultimate +Repository – finds e.g. my Repository, Ultimate Repository</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## Grouping
|
||||
|
||||
Search supports using parentheses to group clauses to form sub queries. This can be very useful if you want to control the boolean logic for a query.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>() – terms inside parentheses are grouped together</td>
|
||||
<td>(Ultimate OR my) AND Repository – finds e.g. Ultimate Repository, my Repository, excludes e.g. Super Repository. Either "Ultimate" or “My” must exist, “Repository” must always exist.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## Escaping Special Characters
|
||||
|
||||
The search supports escaping special characters that are part of the query syntax. The current list special characters are
|
||||
|
||||
+ - && || ! ( ) { } [ ] ^ " ~ * ? : \\ /
|
||||
|
||||
To escape these characters use the "\\" before the character. For example to search for (1+1):2 use the query:
|
||||
|
||||
\\(1\\+1\\)\\:2
|
||||
|
||||
Source: [https://javadoc.io/static/org.apache.lucene/lucene-queryparser/8.9.0/org/apache/lucene/queryparser/classic/package-summary.html#package.description](https://javadoc.io/static/org.apache.lucene/lucene-queryparser/8.9.0/org/apache/lucene/queryparser/classic/package-summary.html#package.description)`;
|
||||
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
|
||||
import * as urls from "./urls";
|
||||
|
||||
export { urls };
|
||||
|
||||
export * from "./errors";
|
||||
|
||||
@@ -22,11 +22,12 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { ApiResult, useIndexLinks } from "./base";
|
||||
import { Link, QueryResult } from "@scm-manager/ui-types";
|
||||
import { ApiResult, useIndexJsonResource, useIndexLinks } from "./base";
|
||||
import { Link, QueryResult, SearchableType } from "@scm-manager/ui-types";
|
||||
import { apiClient } from "./apiclient";
|
||||
import { createQueryString } from "./utils";
|
||||
import { useQueries, useQuery } from "react-query";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export type SearchOptions = {
|
||||
type: string;
|
||||
@@ -46,6 +47,8 @@ export const useSearchTypes = () => {
|
||||
.filter(isString);
|
||||
};
|
||||
|
||||
export const useSearchableTypes = () => useIndexJsonResource<SearchableType[]>("searchableTypes");
|
||||
|
||||
export const useSearchCounts = (types: string[], query: string) => {
|
||||
const searchLinks = useSearchLinks();
|
||||
const queries = useQueries(
|
||||
@@ -113,3 +116,31 @@ export const useSearch = (query: string, optionParam = defaultSearchOptions): Ap
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const useObserveAsync = <D extends any[], R, E = Error>(fn: (...args: D) => Promise<R>, deps: D) => {
|
||||
const [data, setData] = useState<R>();
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<E>();
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fn(...deps)
|
||||
.then(setData)
|
||||
.catch(setError)
|
||||
.finally(() => setLoading(false));
|
||||
}, deps);
|
||||
return { data, isLoading, error };
|
||||
};
|
||||
|
||||
const supportedLanguages = ["de", "en"];
|
||||
|
||||
const pickLang = (language: string) => {
|
||||
if (!supportedLanguages.includes(language)) {
|
||||
return "en";
|
||||
}
|
||||
return language;
|
||||
};
|
||||
|
||||
export const useSearchHelpContent = (language: string) =>
|
||||
useObserveAsync((lang) => import(`./help/search/modal.${pickLang(lang)}`).then((module) => module.default), [language]);
|
||||
export const useSearchSyntaxContent = (language: string) =>
|
||||
useObserveAsync((lang) => import(`./help/search/syntax.${pickLang(lang)}`).then((module) => module.default), [language]);
|
||||
|
||||
@@ -21,16 +21,13 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC } from "react";
|
||||
import Icon from "./Icon";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default class HelpIcon extends React.Component<Props> {
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
return <Icon name="question-circle" color="blue-light" className={className} />;
|
||||
}
|
||||
}
|
||||
const HelpIcon: FC<Props> = ({ className }) => <Icon name="question-circle" color="blue-light" className={className} />;
|
||||
|
||||
export default HelpIcon;
|
||||
|
||||
32
scm-ui/ui-components/src/buttons/LinkStyleButton.tsx
Normal file
32
scm-ui/ui-components/src/buttons/LinkStyleButton.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import styled from "styled-components";
|
||||
import NoStyleButton from "./NoStyleButton";
|
||||
|
||||
const LinkStyleButton = styled(NoStyleButton)`
|
||||
text-decoration: underline;
|
||||
`;
|
||||
|
||||
export default LinkStyleButton;
|
||||
38
scm-ui/ui-components/src/buttons/NoStyleButton.tsx
Normal file
38
scm-ui/ui-components/src/buttons/NoStyleButton.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import styled from "styled-components";
|
||||
|
||||
const NoStyleButton = styled.button`
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
text-align: inherit;
|
||||
`;
|
||||
|
||||
export default NoStyleButton;
|
||||
@@ -35,3 +35,5 @@ export { default as ButtonGroup } from "./ButtonGroup";
|
||||
export { default as ButtonAddons } from "./ButtonAddons";
|
||||
export { default as OpenInFullscreenButton } from "./OpenInFullscreenButton";
|
||||
export { default as RemoveEntryOfTableButton } from "./RemoveEntryOfTableButton";
|
||||
export { default as NoStyleButton } from "./NoStyleButton";
|
||||
export { default as LinkStyleButton } from "./LinkStyleButton";
|
||||
|
||||
@@ -29,15 +29,15 @@ import { urls } from "@scm-manager/ui-api";
|
||||
|
||||
// not sure if it is required
|
||||
import {
|
||||
AnnotationFactory,
|
||||
AnnotationFactoryContext,
|
||||
BaseContext,
|
||||
Change,
|
||||
DiffEventContext,
|
||||
DiffEventHandler,
|
||||
File,
|
||||
FileChangeType,
|
||||
Hunk,
|
||||
Change,
|
||||
BaseContext,
|
||||
AnnotationFactory,
|
||||
AnnotationFactoryContext,
|
||||
DiffEventHandler,
|
||||
DiffEventContext
|
||||
} from "./repos";
|
||||
|
||||
export { validation, repositories };
|
||||
@@ -81,6 +81,7 @@ export { default as SplitAndReplace, Replacement } from "./SplitAndReplace";
|
||||
export { regExpPattern as changesetShortLinkRegex } from "./markdown/remarkChangesetShortLinkParser";
|
||||
export * from "./markdown/PluginApi";
|
||||
export * from "./devices";
|
||||
export { default as copyToClipboard } from "./CopyToClipboard";
|
||||
|
||||
export { default as comparators } from "./comparators";
|
||||
|
||||
@@ -108,7 +109,7 @@ export {
|
||||
AnnotationFactory,
|
||||
AnnotationFactoryContext,
|
||||
DiffEventHandler,
|
||||
DiffEventContext
|
||||
DiffEventContext,
|
||||
};
|
||||
|
||||
// Re-export from ui-api
|
||||
@@ -125,7 +126,7 @@ export {
|
||||
MissingLinkError,
|
||||
createBackendError,
|
||||
isBackendError,
|
||||
TOKEN_EXPIRED_ERROR_CODE
|
||||
TOKEN_EXPIRED_ERROR_CODE,
|
||||
} from "@scm-manager/ui-api";
|
||||
|
||||
export { urls };
|
||||
|
||||
@@ -37,7 +37,7 @@ type Props = {
|
||||
// and use something different than a string for the title property.
|
||||
documentTitle?: string;
|
||||
afterTitle?: ReactNode;
|
||||
subtitle?: string;
|
||||
subtitle?: ReactNode;
|
||||
loading?: boolean;
|
||||
error?: Error | null;
|
||||
showContentOnError?: boolean;
|
||||
@@ -123,7 +123,7 @@ export default class Page extends React.Component<Props> {
|
||||
<Title title={this.getTextualTitle()}>{this.getTitleComponent()}</Title>
|
||||
{afterTitle && <MarginLeft>{afterTitle}</MarginLeft>}
|
||||
</FlexContainer>
|
||||
<Subtitle subtitle={subtitle} />
|
||||
{subtitle ? <Subtitle>{subtitle}</Subtitle> : null}
|
||||
</div>
|
||||
{pageActions}
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { HalRepresentationWithEmbedded, PagedCollection } from "./hal";
|
||||
import { HalRepresentation, HalRepresentationWithEmbedded, PagedCollection } from "./hal";
|
||||
import { Repository } from "./Repositories";
|
||||
|
||||
export type ValueHitField = {
|
||||
@@ -54,3 +54,13 @@ export type QueryResult = PagedCollection<HitEmbedded> & {
|
||||
type: string;
|
||||
totalHits: number;
|
||||
};
|
||||
|
||||
export type SearchableField = {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type SearchableType = HalRepresentation & {
|
||||
name: string;
|
||||
fields: SearchableField[];
|
||||
};
|
||||
|
||||
@@ -156,11 +156,49 @@
|
||||
"subtitle": "{{type}} Ergebnisse für \"{{query}}\"",
|
||||
"types": "Ergebnisse",
|
||||
"noHits": "Die Suche ergab keine Treffer",
|
||||
"syntaxHelp": "Finden Sie bessere Ergebnisse durch die Nutzung der vollen <0>Such-Syntax</0>",
|
||||
"quickSearch": {
|
||||
"resultHeading": "Top-Ergebnisse Repositories",
|
||||
"parseError": "Der Suchstring is ungültig",
|
||||
"parseError": "Der Suchstring is ungültig.",
|
||||
"parseErrorHelp": "Hinweise zu ihrer Suche",
|
||||
"moreResults": "Mehr Ergebnisse",
|
||||
"noResults": "Es konnten keine Repositories gefunden werden"
|
||||
"noResults": "Es konnten keine Repositories gefunden werden",
|
||||
"hintsIcon": "Suchtipps",
|
||||
"hints": "Hinweise zu ihrer Suche"
|
||||
},
|
||||
"syntax": {
|
||||
"title": "Experten-Suche",
|
||||
"subtitle": "Erfahren Sie mehr über die Felder der Entitäten, Modifikatoren und Operatoren.",
|
||||
"exampleQueriesAndFields": {
|
||||
"title": "Beispielabfragen und Felder",
|
||||
"description": "Felder können einzeln abgefragt werden, um Ihre Suche einzugrenzen, z.B. name:ultimate sucht nur nach ultimate im Feld \"name\" der Entitäten. Einige Felder können in allen oder den meisten Entitäten gefunden werden (z.B. Name, creationDate). Andere Felder sind nur bei einzelnen Entitäten vorhanden."
|
||||
},
|
||||
"fields": {
|
||||
"name": "Name",
|
||||
"type": "Typ",
|
||||
"exampleValue": "Beispielwert",
|
||||
"hints": "Hinweise"
|
||||
},
|
||||
"exampleQueries": {
|
||||
"title": "Beispielabfragen",
|
||||
"description": "Felder mit Modifikatoren und Operatoren um Repositories zu finden.",
|
||||
"table": {
|
||||
"description": "Wonach gesucht wird",
|
||||
"query": "Abfrage",
|
||||
"explanation": "Erklärung"
|
||||
}
|
||||
},
|
||||
"utilities": {
|
||||
"title": "Hilfsmittel",
|
||||
"description": "Wandeln Sie menschlich-lesbare Zeitmarken in standard Millisekunden um, um diese in Ihren Queries zu verwenden. Das Datum ist ein Pflichtfeld, Stunde, Minute und Sekunde sind optional.",
|
||||
"datetime":{
|
||||
"label": "Datum in Standardzeitstempel umwandeln",
|
||||
"format": "yyyy-mm-dd hh:mm:ss",
|
||||
"convertButtonLabel": "Konvertieren"
|
||||
},
|
||||
"timestampPlaceholder": "Standard Zeitstempel",
|
||||
"copyTimestampTooltip": "Zeitstempel in Zwischenablage kopieren"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,11 +157,49 @@
|
||||
"subtitle": "{{type}} results for \"{{query}}\"",
|
||||
"types": "Results",
|
||||
"noHits": "No results found",
|
||||
"syntaxHelp": "Find better results by using the full <0>search syntax</0>",
|
||||
"quickSearch": {
|
||||
"resultHeading": "Top repository results",
|
||||
"parseError": "Failed to parse query",
|
||||
"parseError": "Failed to parse query.",
|
||||
"parseErrorHelp": "Hints for your Search",
|
||||
"noResults": "Could not find matching repository",
|
||||
"moreResults": "More Results"
|
||||
"moreResults": "More Results",
|
||||
"hintsIcon": "Search Hints",
|
||||
"hints": "Hints for your Search"
|
||||
},
|
||||
"syntax": {
|
||||
"title": "Expert Search",
|
||||
"subtitle": "Learn About the fields of the entities, modifiers and operators.",
|
||||
"exampleQueriesAndFields": {
|
||||
"title": "Example Queries and Fields",
|
||||
"description": "Fields may be queried individually to narrow down your search, e.g. name:ultimate will query for ultimate only in the field “name” of entities. Some fields may be found in all or most entities (e.g. Name, creationDate). Other fields only exist with single entities."
|
||||
},
|
||||
"fields": {
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"exampleValue": "Example Value",
|
||||
"hints": "Hints"
|
||||
},
|
||||
"exampleQueries": {
|
||||
"title": "Example Queries",
|
||||
"description": "Combine Fields with Modifiers and Operators to find your repositories.",
|
||||
"table": {
|
||||
"description": "What you search for",
|
||||
"query": "Query",
|
||||
"explanation": "Explanation"
|
||||
}
|
||||
},
|
||||
"utilities": {
|
||||
"title": "Utilities",
|
||||
"description": "Convert human-readable timestamps to Epoch Milliseconds to use in your query. Date is mandatory, hour, minute and seconds are optional.",
|
||||
"datetime":{
|
||||
"label": "Convert timestamps to Epoch Milliseconds",
|
||||
"format": "yyyy-mm-dd hh:mm:ss",
|
||||
"convertButtonLabel": "Convert"
|
||||
},
|
||||
"timestampPlaceholder": "Epoch Timestamp",
|
||||
"copyTimestampTooltip": "Copy Timestamp to Clipboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import ImportLog from "../repos/importlog/ImportLog";
|
||||
import CreateRepositoryRoot from "../repos/containers/CreateRepositoryRoot";
|
||||
import styled from "styled-components";
|
||||
import Search from "../search/Search";
|
||||
import Syntax from "../search/Syntax";
|
||||
|
||||
type Props = {
|
||||
me: Me;
|
||||
@@ -109,6 +110,7 @@ const Main: FC<Props> = (props) => {
|
||||
<Redirect exact strict from="/search/:type" to="/search/:type/" />
|
||||
<ProtectedRoute path="/search/:type/:page" component={Search} authenticated={authenticated} />
|
||||
<ProtectedRoute path="/search/:type/" component={Search} authenticated={authenticated} />
|
||||
<ProtectedRoute path="/help/search-syntax/" component={Syntax} authenticated={authenticated} />
|
||||
<ExtensionPoint name="main.route" renderAll={true} props={props} />
|
||||
</Switch>
|
||||
</StyledMain>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, KeyboardEvent as ReactKeyboardEvent, MouseEvent, useCallback, useState, useEffect } from "react";
|
||||
import React, { FC, KeyboardEvent as ReactKeyboardEvent, MouseEvent, useCallback, useEffect, useState } from "react";
|
||||
import { Hit, Links, ValueHitField } from "@scm-manager/ui-types";
|
||||
import styled from "styled-components";
|
||||
import { BackendError, useSearch } from "@scm-manager/ui-api";
|
||||
@@ -32,10 +32,13 @@ import {
|
||||
Button,
|
||||
ErrorNotification,
|
||||
HitProps,
|
||||
LinkStyleButton,
|
||||
Notification,
|
||||
RepositoryAvatar,
|
||||
useStringHitFieldValue,
|
||||
} from "@scm-manager/ui-components";
|
||||
import SyntaxHelp from "../search/SyntaxHelp";
|
||||
import SyntaxModal from "../search/SyntaxModal";
|
||||
|
||||
const Field = styled.div`
|
||||
margin-bottom: 0 !important;
|
||||
@@ -58,6 +61,7 @@ const namespaceAndName = (hit: Hit) => {
|
||||
type HitsProps = {
|
||||
hits: Hit[];
|
||||
index: number;
|
||||
showHelp: () => void;
|
||||
gotoDetailSearch: () => void;
|
||||
clear: () => void;
|
||||
};
|
||||
@@ -68,26 +72,28 @@ type GotoProps = {
|
||||
gotoDetailSearch: () => void;
|
||||
};
|
||||
|
||||
const EmptyHits: FC<GotoProps> = ({ gotoDetailSearch }) => {
|
||||
const EmptyHits: FC = () => {
|
||||
const [t] = useTranslation("commons");
|
||||
return (
|
||||
<QuickSearchNotification>
|
||||
<Notification type="info">{t("search.quickSearch.noResults")}</Notification>
|
||||
<MoreResults gotoDetailSearch={gotoDetailSearch} />
|
||||
</QuickSearchNotification>
|
||||
<Notification className="m-4" type="info">
|
||||
{t("search.quickSearch.noResults")}
|
||||
</Notification>
|
||||
);
|
||||
};
|
||||
|
||||
type ErrorProps = {
|
||||
error: Error;
|
||||
showHelp: () => void;
|
||||
};
|
||||
|
||||
const ParseErrorNotification: FC = () => {
|
||||
const ParseErrorNotification: FC<ErrorProps> = ({ showHelp }) => {
|
||||
const [t] = useTranslation("commons");
|
||||
// TODO add link to query syntax page/modal
|
||||
return (
|
||||
<QuickSearchNotification>
|
||||
<Notification type="warning">{t("search.quickSearch.parseError")}</Notification>
|
||||
<Notification type="warning">
|
||||
<p>{t("search.quickSearch.parseError")}</p>
|
||||
<LinkStyleButton onClick={showHelp}>{t("search.quickSearch.parseErrorHelp")}</LinkStyleButton>
|
||||
</Notification>
|
||||
</QuickSearchNotification>
|
||||
);
|
||||
};
|
||||
@@ -96,10 +102,10 @@ const isBackendError = (error: Error | BackendError): error is BackendError => {
|
||||
return (error as BackendError).errorCode !== undefined;
|
||||
};
|
||||
|
||||
const SearchErrorNotification: FC<ErrorProps> = ({ error }) => {
|
||||
const SearchErrorNotification: FC<ErrorProps> = ({ error, showHelp }) => {
|
||||
// 5VScek8Xp1 is the id of sonia.scm.search.QueryParseException
|
||||
if (isBackendError(error) && error.errorCode === "5VScek8Xp1") {
|
||||
return <ParseErrorNotification />;
|
||||
return <ParseErrorNotification error={error} showHelp={showHelp} />;
|
||||
}
|
||||
return (
|
||||
<QuickSearchNotification>
|
||||
@@ -113,6 +119,9 @@ const ResultHeading = styled.h3`
|
||||
margin: 0 0.5rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const DropdownMenu = styled.div`
|
||||
@@ -153,17 +162,13 @@ const MoreResults: FC<GotoProps> = ({ gotoDetailSearch }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Hits: FC<HitsProps> = ({ hits, index, clear, gotoDetailSearch }) => {
|
||||
const HitsList: FC<HitsProps> = ({ hits, index, clear, gotoDetailSearch }) => {
|
||||
const id = useCallback(namespaceAndName, [hits]);
|
||||
const [t] = useTranslation("commons");
|
||||
|
||||
if (hits.length === 0) {
|
||||
return <EmptyHits gotoDetailSearch={gotoDetailSearch} />;
|
||||
return <EmptyHits />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div aria-expanded="true" role="listbox" className="dropdown-content">
|
||||
<ResultHeading className="dropdown-item">{t("search.quickSearch.resultHeading")}</ResultHeading>
|
||||
<>
|
||||
{hits.map((hit, idx) => (
|
||||
<div key={id(hit)} onMouseDown={(e) => e.preventDefault()} onClick={clear}>
|
||||
<Link
|
||||
@@ -175,12 +180,29 @@ const Hits: FC<HitsProps> = ({ hits, index, clear, gotoDetailSearch }) => {
|
||||
role="option"
|
||||
data-omnisearch="true"
|
||||
>
|
||||
<AvatarSection hit={hit} /> {id(hit)}
|
||||
<AvatarSection hit={hit} />
|
||||
{id(hit)}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<MoreResults gotoDetailSearch={gotoDetailSearch} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Hits: FC<HitsProps> = ({ showHelp, gotoDetailSearch, ...rest }) => {
|
||||
const [t] = useTranslation("commons");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div aria-expanded="true" role="listbox" className="dropdown-content">
|
||||
<ResultHeading className="dropdown-item">
|
||||
<span>{t("search.quickSearch.resultHeading")}</span>
|
||||
<SyntaxHelp onClick={showHelp} />
|
||||
</ResultHeading>
|
||||
<HitsList showHelp={showHelp} gotoDetailSearch={gotoDetailSearch} {...rest} />
|
||||
<MoreResults gotoDetailSearch={gotoDetailSearch} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -305,11 +327,12 @@ const OmniSearch: FC = () => {
|
||||
const debouncedQuery = useDebounce(query, 250);
|
||||
const { data, isLoading, error } = useSearch(debouncedQuery, { type: "repository", pageSize: 5 });
|
||||
const { showResults, hideResults, ...handlers } = useShowResultsOnFocus();
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const history = useHistory();
|
||||
|
||||
const clearQuery = () => {
|
||||
setQuery("");
|
||||
};
|
||||
const openHelp = () => setShowHelp(true);
|
||||
const closeHelp = () => setShowHelp(false);
|
||||
const clearQuery = () => setQuery("");
|
||||
|
||||
const gotoDetailSearch = () => {
|
||||
history.push(`/search/repository/?q=${query}`);
|
||||
@@ -320,6 +343,7 @@ const OmniSearch: FC = () => {
|
||||
|
||||
return (
|
||||
<Field className="navbar-item field">
|
||||
{showHelp ? <SyntaxModal close={closeHelp} /> : null}
|
||||
<div
|
||||
className={classNames("control", "has-icons-right", {
|
||||
"is-loading": isLoading,
|
||||
@@ -346,9 +370,15 @@ const OmniSearch: FC = () => {
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenu className="dropdown-menu" onMouseDown={(e) => e.preventDefault()}>
|
||||
{error ? <SearchErrorNotification error={error} /> : null}
|
||||
{error ? <SearchErrorNotification error={error} showHelp={openHelp} /> : null}
|
||||
{!error && data ? (
|
||||
<Hits gotoDetailSearch={gotoDetailSearch} clear={clearQuery} index={index} hits={data._embedded.hits} />
|
||||
<Hits
|
||||
showHelp={openHelp}
|
||||
gotoDetailSearch={gotoDetailSearch}
|
||||
clear={clearQuery}
|
||||
index={index}
|
||||
hits={data._embedded.hits}
|
||||
/>
|
||||
) : null}
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@@ -33,10 +33,10 @@ import {
|
||||
Tag,
|
||||
urls,
|
||||
} from "@scm-manager/ui-components";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { Link, useLocation, useParams } from "react-router-dom";
|
||||
import { useSearch, useSearchCounts, useSearchTypes } from "@scm-manager/ui-api";
|
||||
import Results from "./Results";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
type PathParams = {
|
||||
type: string;
|
||||
@@ -85,6 +85,27 @@ const orderTypes = (left: string, right: string) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
selectedType: string;
|
||||
query: string;
|
||||
};
|
||||
|
||||
const SyntaxHelpLink: FC = ({ children }) => <Link to="/help/search-syntax">{children}</Link>;
|
||||
|
||||
const SearchSubTitle: FC<Props> = ({ selectedType, query }) => {
|
||||
const [t] = useTranslation("commons");
|
||||
return (
|
||||
<>
|
||||
{t("search.subtitle", {
|
||||
query,
|
||||
type: t(`plugins:search.types.${selectedType}.subtitle`, selectedType),
|
||||
})}
|
||||
<br />
|
||||
<Trans i18nKey="search.syntaxHelp" components={[<SyntaxHelpLink />]} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Search: FC = () => {
|
||||
const [t] = useTranslation(["commons", "plugins"]);
|
||||
const { query, selectedType, page } = usePageParams();
|
||||
@@ -112,10 +133,7 @@ const Search: FC = () => {
|
||||
return (
|
||||
<Page
|
||||
title={t("search.title")}
|
||||
subtitle={t("search.subtitle", {
|
||||
query,
|
||||
type: t(`plugins:search.types.${selectedType}.subtitle`, selectedType),
|
||||
})}
|
||||
subtitle={<SearchSubTitle query={query} selectedType={selectedType} />}
|
||||
loading={isLoading}
|
||||
error={error}
|
||||
>
|
||||
|
||||
194
scm-ui/ui-webapp/src/search/Syntax.tsx
Normal file
194
scm-ui/ui-webapp/src/search/Syntax.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, useState } from "react";
|
||||
import { useSearchableTypes, useSearchSyntaxContent } from "@scm-manager/ui-api";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Button,
|
||||
copyToClipboard,
|
||||
Icon,
|
||||
InputField,
|
||||
Loading,
|
||||
MarkdownView,
|
||||
Page,
|
||||
Tooltip,
|
||||
} from "@scm-manager/ui-components";
|
||||
import { parse } from "date-fns";
|
||||
import styled from "styled-components";
|
||||
import classNames from "classnames";
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
height: 40px;
|
||||
`;
|
||||
|
||||
type ExpandableProps = {
|
||||
header: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Expandable: FC<ExpandableProps> = ({ header, children, className }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
return (
|
||||
<div className={classNames("card", className)}>
|
||||
<header onClick={() => setExpanded(!expanded)} className="card-header is-clickable">
|
||||
<span className="card-header-title">{header}</span>
|
||||
<span className="card-header-icon">
|
||||
<Icon name={expanded ? "chevron-down" : "chevron-left"} />
|
||||
</span>
|
||||
</header>
|
||||
{expanded ? <div className="card-content">{children}</div> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type Example = {
|
||||
description: string;
|
||||
query: string;
|
||||
explanation: string;
|
||||
};
|
||||
|
||||
const Syntax: FC = () => {
|
||||
const { t, i18n } = useTranslation(["commons", "plugins"]);
|
||||
const { loading: isLoading, data: helpModalContent } = useSearchSyntaxContent(i18n.languages[0]);
|
||||
const [datetime, setDatetime] = useState("");
|
||||
const [timestamp, setTimestamp] = useState("");
|
||||
const [copying, setCopying] = useState(false);
|
||||
const { isLoading: isLoadingSearchableTypes, data: searchableTypes } = useSearchableTypes();
|
||||
|
||||
const convert = () => {
|
||||
const format = "yyyy-MM-dd HH:mm:ss";
|
||||
const date = parse(datetime, format, new Date());
|
||||
const newTimestamp = date.getTime();
|
||||
setTimestamp(String(newTimestamp));
|
||||
};
|
||||
const copyTimestamp = () => {
|
||||
setCopying(true);
|
||||
copyToClipboard(timestamp).finally(() => setCopying(false));
|
||||
};
|
||||
|
||||
if (isLoading || isLoadingSearchableTypes) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const searchableTypesContent = searchableTypes!.map((searchableType) => {
|
||||
const examples = t<Example[]>(`plugins:search.types.${searchableType.name}.examples`, {
|
||||
returnObjects: true,
|
||||
defaultValue: [],
|
||||
});
|
||||
return (
|
||||
<Expandable className="mb-1" header={t(`plugins:search.types.${searchableType.name}.title`)}>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{t("search.syntax.fields.name")}</th>
|
||||
<th>{t("search.syntax.fields.type")}</th>
|
||||
<th>{t("search.syntax.fields.exampleValue")}</th>
|
||||
<th>{t("search.syntax.fields.hints")}</th>
|
||||
</tr>
|
||||
{searchableType.fields.map((searchableField) => (
|
||||
<tr>
|
||||
<th>{t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.name`)}</th>
|
||||
<td>{searchableField.type}</td>
|
||||
<td>
|
||||
{t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.exampleValue`, {
|
||||
defaultValue: "",
|
||||
})}
|
||||
</td>
|
||||
<td>
|
||||
{t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.hints`, {
|
||||
defaultValue: "",
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
{examples.length > 0 ? (
|
||||
<>
|
||||
<h5 className="title mt-5">{t("search.syntax.exampleQueries.title")}</h5>
|
||||
<div className="mb-2">{t("search.syntax.exampleQueries.description")}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{t("search.syntax.exampleQueries.table.description")}</th>
|
||||
<th>{t("search.syntax.exampleQueries.table.query")}</th>
|
||||
<th>{t("search.syntax.exampleQueries.table.explanation")}</th>
|
||||
</tr>
|
||||
{examples.map((example) => (
|
||||
<tr>
|
||||
<td>{example.description}</td>
|
||||
<td>{example.query}</td>
|
||||
<td>{example.explanation}</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</>
|
||||
) : null}
|
||||
</Expandable>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Page title={t("search.syntax.title")} subtitle={t("search.syntax.subtitle")} loading={isLoading}>
|
||||
<div className="content">
|
||||
<h4 className="title">{t("search.syntax.exampleQueriesAndFields.title")}</h4>
|
||||
<p>{t("search.syntax.exampleQueriesAndFields.description")}</p>
|
||||
{searchableTypesContent}
|
||||
</div>
|
||||
<MarkdownView content={helpModalContent!} basePath="/" />
|
||||
<h3 className="title">{t("search.syntax.utilities.title")}</h3>
|
||||
<p>{t("search.syntax.utilities.description")}</p>
|
||||
<h6 className="title is-6 mt-4">{t("search.syntax.utilities.datetime.label")}</h6>
|
||||
<div className="is-flex">
|
||||
<span className="is-flex mr-5">
|
||||
<InputField
|
||||
value={datetime}
|
||||
onChange={setDatetime}
|
||||
placeholder={t("search.syntax.utilities.datetime.format")}
|
||||
/>
|
||||
<Button color="primary" action={convert} className="ml-2">
|
||||
{t("search.syntax.utilities.datetime.convertButtonLabel")}
|
||||
</Button>
|
||||
</span>
|
||||
<span className="is-flex">
|
||||
<InputField
|
||||
className="mr-4"
|
||||
value={timestamp}
|
||||
readOnly={true}
|
||||
placeholder={t("search.syntax.utilities.timestampPlaceholder")}
|
||||
/>
|
||||
<StyledTooltip
|
||||
message={t("search.syntax.utilities.copyTimestampTooltip")}
|
||||
className="is-flex is-align-items-center"
|
||||
>
|
||||
{copying ? (
|
||||
<span className="small-loading-spinner" />
|
||||
) : (
|
||||
<Icon name="clipboard" color="inherit" className="is-size-4 fa-fw is-clickable" onClick={copyTimestamp} />
|
||||
)}
|
||||
</StyledTooltip>
|
||||
</span>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Syntax;
|
||||
44
scm-ui/ui-webapp/src/search/SyntaxHelp.tsx
Normal file
44
scm-ui/ui-webapp/src/search/SyntaxHelp.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HelpIcon, NoStyleButton } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const SyntaxHelp: FC<Props> = ({ onClick }) => {
|
||||
const [t] = useTranslation("commons");
|
||||
return (
|
||||
<>
|
||||
<NoStyleButton title={t("search.quickSearch.hintsIcon")} onClick={onClick}>
|
||||
<HelpIcon />
|
||||
</NoStyleButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SyntaxHelp;
|
||||
70
scm-ui/ui-webapp/src/search/SyntaxModal.tsx
Normal file
70
scm-ui/ui-webapp/src/search/SyntaxModal.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC, MouseEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchHelpContent } from "@scm-manager/ui-api";
|
||||
import { ErrorNotification, Loading, MarkdownView, Modal } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
const SyntaxModalContent: FC<Props> = ({ close }) => {
|
||||
const { i18n } = useTranslation("commons");
|
||||
const { isLoading, data, error } = useSearchHelpContent(i18n.languages[0]);
|
||||
|
||||
const handleClickEvent = (e: MouseEvent<HTMLElement>) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === "A") {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
} else if (isLoading || !data) {
|
||||
return <Loading />;
|
||||
} else {
|
||||
return (
|
||||
<div onClick={handleClickEvent}>
|
||||
<MarkdownView content={data} basePath="/" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const SyntaxModal: FC<Props> = ({ close }) => {
|
||||
const [t] = useTranslation("commons");
|
||||
return (
|
||||
<Modal
|
||||
active={true}
|
||||
title={t("search.quickSearch.hints")}
|
||||
body={<SyntaxModalContent close={close} />}
|
||||
closeFunction={close}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SyntaxModal;
|
||||
@@ -143,6 +143,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
||||
builder.single(link("importLog", resourceLinks.repository().importLog("IMPORT_LOG_ID").replace("IMPORT_LOG_ID", "{logId}")));
|
||||
|
||||
builder.array(searchLinks());
|
||||
builder.single(link("searchableTypes", resourceLinks.search().searchableTypes()));
|
||||
} else {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
|
||||
@@ -93,5 +93,7 @@ public class MapperModule extends AbstractModule {
|
||||
bind(RepositoryImportDtoToRepositoryImportParametersMapper.class).to(Mappers.getMapperClass(RepositoryImportDtoToRepositoryImportParametersMapper.class));
|
||||
|
||||
bind(QueryResultMapper.class).to(Mappers.getMapperClass(QueryResultMapper.class));
|
||||
bind(SearchableTypeMapper.class).to(Mappers.getMapperClass(SearchableTypeMapper.class));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1128,6 +1128,7 @@ class ResourceLinks {
|
||||
public String query(String type) {
|
||||
return searchLinkBuilder.method("query").parameters(type).href();
|
||||
}
|
||||
public String searchableTypes() { return searchLinkBuilder.method("searchableTypes").parameters().href(); }
|
||||
}
|
||||
|
||||
public InitialAdminAccountLinks initialAdminAccount() {
|
||||
|
||||
@@ -42,7 +42,9 @@ import javax.ws.rs.BeanParam;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path(SearchResource.PATH)
|
||||
@OpenAPIDefinition(tags = {
|
||||
@@ -53,12 +55,14 @@ public class SearchResource {
|
||||
static final String PATH = "v2/search";
|
||||
|
||||
private final SearchEngine engine;
|
||||
private final QueryResultMapper mapper;
|
||||
private final QueryResultMapper queryResultMapper;
|
||||
private final SearchableTypeMapper searchableTypeMapper;
|
||||
|
||||
@Inject
|
||||
public SearchResource(SearchEngine engine, QueryResultMapper mapper) {
|
||||
public SearchResource(SearchEngine engine, QueryResultMapper mapper, SearchableTypeMapper searchableTypeMapper) {
|
||||
this.engine = engine;
|
||||
this.mapper = mapper;
|
||||
this.queryResultMapper = mapper;
|
||||
this.searchableTypeMapper = searchableTypeMapper;
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -110,6 +114,34 @@ public class SearchResource {
|
||||
return search(params);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("searchableTypes")
|
||||
@Produces(VndMediaType.SEARCHABLE_TYPE_COLLECTION)
|
||||
@Operation(
|
||||
summary = "Searchable types",
|
||||
description = "Returns a collection of all searchable types.",
|
||||
tags = "Search",
|
||||
operationId = "searchable_types"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.SEARCHABLE_TYPE_COLLECTION
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Collection<SearchableTypeDto> searchableTypes() {
|
||||
return engine.getSearchableTypes().stream().map(searchableTypeMapper::map).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private QueryResultDto search(SearchParameters params) {
|
||||
QueryResult result = engine.forType(params.getType())
|
||||
.search()
|
||||
@@ -117,7 +149,7 @@ public class SearchResource {
|
||||
.limit(params.getPageSize())
|
||||
.execute(params.getQuery());
|
||||
|
||||
return mapper.map(params, result);
|
||||
return queryResultMapper.map(params, result);
|
||||
}
|
||||
|
||||
private QueryResultDto count(SearchParameters params) {
|
||||
@@ -125,7 +157,7 @@ public class SearchResource {
|
||||
.search()
|
||||
.count(params.getQuery());
|
||||
|
||||
return mapper.map(params, new QueryResult(result.getTotalHits(), result.getType(), Collections.emptyList()));
|
||||
return queryResultMapper.map(params, new QueryResult(result.getTotalHits(), result.getType(), Collections.emptyList()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SearchableFieldDto {
|
||||
private String name;
|
||||
private String type;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("java:S2160") // we need no equals for dtos
|
||||
public class SearchableTypeDto extends HalRepresentation {
|
||||
private String name;
|
||||
private Collection<SearchableFieldDto> fields;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import sonia.scm.search.SearchableField;
|
||||
import sonia.scm.search.SearchableType;
|
||||
import sonia.scm.search.TypeCheck;
|
||||
|
||||
@Mapper
|
||||
public abstract class SearchableTypeMapper {
|
||||
|
||||
@Mapping(target = "attributes", ignore = true)
|
||||
public abstract SearchableTypeDto map(SearchableType searchableType);
|
||||
|
||||
public abstract SearchableFieldDto map(SearchableField searchableField);
|
||||
|
||||
public String map(Class<?> type) {
|
||||
if (TypeCheck.isString(type)) {
|
||||
return "string";
|
||||
} else if (TypeCheck.isInstant(type) || TypeCheck.isNumber(type)) {
|
||||
return "number";
|
||||
} else if (TypeCheck.isBoolean(type)) {
|
||||
return "boolean";
|
||||
} else {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -66,14 +66,14 @@ public class AnalyzerFactory {
|
||||
Analyzer defaultAnalyzer = create(options);
|
||||
|
||||
Map<String, Analyzer> analyzerMap = new HashMap<>();
|
||||
for (SearchableField field : type.getFields()) {
|
||||
for (LuceneSearchableField field : type.getFields()) {
|
||||
addFieldAnalyzer(analyzerMap, field);
|
||||
}
|
||||
|
||||
return new PerFieldAnalyzerWrapper(defaultAnalyzer, analyzerMap);
|
||||
}
|
||||
|
||||
private void addFieldAnalyzer(Map<String, Analyzer> analyzerMap, SearchableField field) {
|
||||
private void addFieldAnalyzer(Map<String, Analyzer> analyzerMap, LuceneSearchableField field) {
|
||||
if (field.getAnalyzer() != Indexed.Analyzer.DEFAULT) {
|
||||
analyzerMap.put(field.getName(), new NonNaturalLanguageAnalyzer());
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
@Getter
|
||||
class SearchableField {
|
||||
class LuceneSearchableField implements SearchableField {
|
||||
|
||||
private final String name;
|
||||
private final Class<?> type;
|
||||
@@ -43,7 +43,7 @@ class SearchableField {
|
||||
private final PointsConfig pointsConfig;
|
||||
private final Indexed.Analyzer analyzer;
|
||||
|
||||
SearchableField(Field field, Indexed indexed) {
|
||||
LuceneSearchableField(Field field, Indexed indexed) {
|
||||
this.name = name(field, indexed);
|
||||
this.type = field.getType();
|
||||
this.valueExtractor = ValueExtractors.create(name, type);
|
||||
@@ -65,4 +65,5 @@ class SearchableField {
|
||||
}
|
||||
return field.getName();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import com.google.common.base.Strings;
|
||||
import lombok.Value;
|
||||
import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -42,13 +43,13 @@ public class LuceneSearchableType implements SearchableType {
|
||||
Class<?> type;
|
||||
String name;
|
||||
String permission;
|
||||
List<SearchableField> fields;
|
||||
List<LuceneSearchableField> fields;
|
||||
String[] fieldNames;
|
||||
Map<String, Float> boosts;
|
||||
Map<String, PointsConfig> pointsConfig;
|
||||
TypeConverter typeConverter;
|
||||
|
||||
LuceneSearchableType(Class<?> type, IndexedType annotation, List<SearchableField> fields) {
|
||||
|
||||
public LuceneSearchableType(Class<?> type, IndexedType annotation, List<LuceneSearchableField> fields) {
|
||||
this.type = type;
|
||||
this.name = name(type, annotation);
|
||||
this.permission = Strings.emptyToNull(annotation.permission());
|
||||
@@ -72,16 +73,16 @@ public class LuceneSearchableType implements SearchableType {
|
||||
return nameFromAnnotation;
|
||||
}
|
||||
|
||||
private String[] fieldNames(List<SearchableField> fields) {
|
||||
private String[] fieldNames(List<LuceneSearchableField> fields) {
|
||||
return fields.stream()
|
||||
.filter(SearchableField::isDefaultQuery)
|
||||
.map(SearchableField::getName)
|
||||
.filter(LuceneSearchableField::isDefaultQuery)
|
||||
.map(LuceneSearchableField::getName)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
private Map<String, Float> boosts(List<SearchableField> fields) {
|
||||
private Map<String, Float> boosts(List<LuceneSearchableField> fields) {
|
||||
Map<String, Float> map = new HashMap<>();
|
||||
for (SearchableField field : fields) {
|
||||
for (LuceneSearchableField field : fields) {
|
||||
if (field.isDefaultQuery() && field.getBoost() != DEFAULT_BOOST) {
|
||||
map.put(field.getName(), field.getBoost());
|
||||
}
|
||||
@@ -89,9 +90,9 @@ public class LuceneSearchableType implements SearchableType {
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
private Map<String, PointsConfig> pointsConfig(List<SearchableField> fields) {
|
||||
private Map<String, PointsConfig> pointsConfig(List<LuceneSearchableField> fields) {
|
||||
Map<String, PointsConfig> map = new HashMap<>();
|
||||
for (SearchableField field : fields) {
|
||||
for (LuceneSearchableField field : fields) {
|
||||
PointsConfig config = field.getPointsConfig();
|
||||
if (config != null) {
|
||||
map.put(field.getName(), config);
|
||||
@@ -99,4 +100,8 @@ public class LuceneSearchableType implements SearchableType {
|
||||
}
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
public Collection<LuceneSearchableField> getFields() {
|
||||
return Collections.unmodifiableCollection(fields);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,13 +77,13 @@ public class QueryResultFactory {
|
||||
private Hit createHit(ScoreDoc scoreDoc) throws IOException, InvalidTokenOffsetsException {
|
||||
Document document = searcher.doc(scoreDoc.doc);
|
||||
Map<String, Hit.Field> fields = new HashMap<>();
|
||||
for (SearchableField field : searchableType.getFields()) {
|
||||
for (LuceneSearchableField field : searchableType.getFields()) {
|
||||
field(document, field).ifPresent(f -> fields.put(field.getName(), f));
|
||||
}
|
||||
return new Hit(document.get(FieldNames.ID), document.get(FieldNames.REPOSITORY), scoreDoc.score, fields);
|
||||
}
|
||||
|
||||
private Optional<Hit.Field> field(Document document, SearchableField field) throws IOException, InvalidTokenOffsetsException {
|
||||
private Optional<Hit.Field> field(Document document, LuceneSearchableField field) throws IOException, InvalidTokenOffsetsException {
|
||||
Object value = field.value(document);
|
||||
if (value != null) {
|
||||
if (field.isHighlighted()) {
|
||||
@@ -97,7 +97,7 @@ public class QueryResultFactory {
|
||||
return empty();
|
||||
}
|
||||
|
||||
private String[] createFragments(SearchableField field, String value) throws InvalidTokenOffsetsException, IOException {
|
||||
private String[] createFragments(LuceneSearchableField field, String value) throws InvalidTokenOffsetsException, IOException {
|
||||
return highlighter.getBestFragments(analyzer, field.getName(), value, 5);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ final class SearchableTypes {
|
||||
}
|
||||
|
||||
static LuceneSearchableType create(Class<?> type) {
|
||||
List<SearchableField> fields = new ArrayList<>();
|
||||
List<LuceneSearchableField> fields = new ArrayList<>();
|
||||
IndexedType annotation = type.getAnnotation(IndexedType.class);
|
||||
if (annotation == null) {
|
||||
throw new IllegalArgumentException(
|
||||
@@ -45,7 +45,7 @@ final class SearchableTypes {
|
||||
return new LuceneSearchableType(type, annotation, fields);
|
||||
}
|
||||
|
||||
private static void collectFields(Class<?> type, List<SearchableField> fields) {
|
||||
private static void collectFields(Class<?> type, List<LuceneSearchableField> fields) {
|
||||
Class<?> parent = type.getSuperclass();
|
||||
if (parent != null) {
|
||||
collectFields(parent, fields);
|
||||
@@ -53,7 +53,7 @@ final class SearchableTypes {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
Indexed indexed = field.getAnnotation(Indexed.class);
|
||||
if (indexed != null) {
|
||||
fields.add(new SearchableField(field, indexed));
|
||||
fields.add(new LuceneSearchableField(field, indexed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ package sonia.scm.search;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
final class TypeCheck {
|
||||
public final class TypeCheck {
|
||||
|
||||
private TypeCheck() {
|
||||
}
|
||||
@@ -50,4 +50,6 @@ final class TypeCheck {
|
||||
public static boolean isString(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
|
||||
public static boolean isNumber(Class<?> type) { return isLong(type) || isInteger(type); }
|
||||
}
|
||||
|
||||
@@ -438,15 +438,102 @@
|
||||
"types": {
|
||||
"repository": {
|
||||
"subtitle": "Repository",
|
||||
"navItem": "Repositories"
|
||||
"navItem": "Repositories",
|
||||
"title": "Repositories",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "Name",
|
||||
"exampleValue": "scm-manager"
|
||||
},
|
||||
"namespace": {
|
||||
"name": "Namespace",
|
||||
"exampleValue": "cloudogu"
|
||||
},
|
||||
"type": {
|
||||
"name": "Typ",
|
||||
"exampleValue": "git, hg oder svn"
|
||||
},
|
||||
"description": {
|
||||
"name": "Beschreibung",
|
||||
"exampleValue": "Dieses Repository enthält den Quellcode des SCM-Managers."
|
||||
},
|
||||
"creationDate": {
|
||||
"name": "Erstellungsdatum",
|
||||
"exampleValue": "1628077286",
|
||||
"hints": "Zeitstempel der Erstellung in ms seit dem 01.01.1970. Bereichssuche möglich."
|
||||
},
|
||||
"lastModified": {
|
||||
"name": "Zuletzt geändert",
|
||||
"exampleValue": "1623057235",
|
||||
"hints": "Zeitstempel der letzten Änderung in ms seit dem 01.01.1970. Bereichssuche möglich."
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"description": "...einem Repository mit dem Namen ultimate-Repository oder ultimate_Repository. Sie sind nicht sicher, welches Zeichen die beiden Begriffe verbindet",
|
||||
"query": "name:ultimate?Repository",
|
||||
"explanation": "Sucht nach „ultimate“ und „Repository“ verbunden mit einem einzelnen Zeichen."
|
||||
},
|
||||
{
|
||||
"description": "...alle Repositories, die zwischen dem 01.01.2021 und dem 01.02.2021 im Namespace ultimate geändert wurden.",
|
||||
"query": "nameSpace:ultimate AND lastModified:[1609459200000 TO 1612137600000]",
|
||||
"explanation": "Sucht nach Repositories im Namespace „ultimate“, die im gewünschten Zeitraum geändert wurden Der Zeitraum wird über Zeitstemple in Epoch Millisekunden angegeben. Rechnen Sie Zeitstempel mit unserem Converter in Epoch Millisekunden um."
|
||||
}
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"subtitle": "Benutzer",
|
||||
"navItem": "Benutzer"
|
||||
"navItem": "Benutzer",
|
||||
"title": "Benutzer",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "Name",
|
||||
"exampleValue": "admin"
|
||||
},
|
||||
"displayName": {
|
||||
"name": "Anzeigename",
|
||||
"exampleValue": "Administrator"
|
||||
},
|
||||
"creationDate": {
|
||||
"name": "Erstellungsdatum",
|
||||
"exampleValue": "1628077286",
|
||||
"hints": "Zeitstempel der Erstellung in ms seit dem 01.01.1970. Bereichssuche möglich."
|
||||
},
|
||||
"lastModified": {
|
||||
"name": "Zuletzt geändert",
|
||||
"exampleValue": "1623057235",
|
||||
"hints": "Zeitstempel der letzten Änderung in ms seit dem 01.01.1970. Bereichssuche möglich."
|
||||
},
|
||||
"mail": {
|
||||
"name": "Mail",
|
||||
"exampleValue": "admin@hitchhiker.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"subtitle": "Gruppe",
|
||||
"navItem": "Gruppen"
|
||||
"navItem": "Gruppen",
|
||||
"title": "Gruppen",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "Name",
|
||||
"exampleValue": "Entwickler"
|
||||
},
|
||||
"description": {
|
||||
"name": "Beschreibung",
|
||||
"exampleValue": "Diese Gruppe enthält alle Softwareentwickler."
|
||||
},
|
||||
"creationDate": {
|
||||
"name": "Erstellungsdatum",
|
||||
"exampleValue": "1628077286",
|
||||
"hints": "Zeitstempel der Erstellung in ms seit dem 01.01.1970. Bereichssuche möglich."
|
||||
},
|
||||
"lastModified": {
|
||||
"name": "Zuletzt geändert",
|
||||
"exampleValue": "1623057235",
|
||||
"hints": "Zeitstempel der letzten Änderung in ms seit dem 01.01.1970. Bereichssuche möglich."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,16 +382,104 @@
|
||||
"types": {
|
||||
"repository": {
|
||||
"subtitle": "Repository",
|
||||
"navItem": "Repositories"
|
||||
"navItem": "Repositories",
|
||||
"title": "Repositories",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "Name",
|
||||
"exampleValue": "scm-manager"
|
||||
},
|
||||
"namespace": {
|
||||
"name": "Namespace",
|
||||
"exampleValue": "cloudogu"
|
||||
},
|
||||
"type": {
|
||||
"name": "Type",
|
||||
"exampleValue": "git, hg or svn"
|
||||
},
|
||||
"description": {
|
||||
"name": "Description",
|
||||
"exampleValue": "This repository contains the source code for the SCM-Manager."
|
||||
},
|
||||
"creationDate": {
|
||||
"name": "Creation Date",
|
||||
"exampleValue": "1628077286",
|
||||
"hints": "Timestamp of creation in ms since 01-01-1970. Range search possible."
|
||||
},
|
||||
"lastModified": {
|
||||
"name": "Last Modified",
|
||||
"exampleValue": "1623057235",
|
||||
"hints": "Timestamp of last modification in ms since 01-01-1970. Range search possible."
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"description": "a repository called ultimate-Repository or ultimate_repository. You are not sure about the connecting character.",
|
||||
"query": "name:ultimate?Repository",
|
||||
"explanation": "queries \"ultimate\" and \"repository\" with a single character in between in the field \"name\"."
|
||||
},
|
||||
{
|
||||
"description": "all repositories changed between 01-01-2021 and 01-02-2021 in the Namespace \"ultimate\"",
|
||||
"query": "nameSpace:ultimate AND lastModified:[1609459200000 TO 1612137600000]",
|
||||
"explanation": "queries for repositories that live in the namespace \"ultimate\" and were last changed in the chosen period (displayed in Epoch milliseconds). You can convert datetimes to timestamps with our converter at the bottom of this page."
|
||||
}
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"subtitle": "User",
|
||||
"navItem": "Users"
|
||||
"navItem": "Users",
|
||||
"title": "Users",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "Name",
|
||||
"exampleValue": "admin"
|
||||
},
|
||||
"displayName": {
|
||||
"name": "Display Name",
|
||||
"exampleValue": "Administrator"
|
||||
},
|
||||
"creationDate": {
|
||||
"name": "Creation Date",
|
||||
"exampleValue": "1628077286",
|
||||
"hints": "Timestamp of creation in ms since 01-01-1970. Range search possible."
|
||||
},
|
||||
"lastModified": {
|
||||
"name": "Last Modified",
|
||||
"exampleValue": "1623057235",
|
||||
"hints": "Timestamp of last modification in ms since 01-01-1970. Range search possible."
|
||||
},
|
||||
"mail": {
|
||||
"name": "Mail",
|
||||
"exampleValue": "admin@hitchhiker.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"subtitle": "Group",
|
||||
"navItem": "Groups"
|
||||
"navItem": "Groups",
|
||||
"title": "Groups",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "Name",
|
||||
"exampleValue": "Developers"
|
||||
},
|
||||
"description": {
|
||||
"name": "Description",
|
||||
"exampleValue": "This group contains all software developers."
|
||||
},
|
||||
"creationDate": {
|
||||
"name": "Creation Date",
|
||||
"exampleValue": "1628077286",
|
||||
"hints": "Timestamp of creation in ms since 01-01-1970. Range search possible."
|
||||
},
|
||||
"lastModified": {
|
||||
"name": "Last Modified",
|
||||
"exampleValue": "1623057235",
|
||||
"hints": "Timestamp of last modification in ms since 01-01-1970. Range search possible."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -228,5 +228,6 @@ class IndexDtoGeneratorTest {
|
||||
when(resourceLinks.namespaceCollection()).thenReturn(new ResourceLinks.NamespaceCollectionLinks(scmPathInfo));
|
||||
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(scmPathInfo, new ResourceLinks.UserLinks(scmPathInfo)));
|
||||
when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(scmPathInfo));
|
||||
when(resourceLinks.search()).thenReturn(new ResourceLinks.SearchLinks(scmPathInfo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
@@ -45,6 +46,7 @@ import sonia.scm.search.Hit;
|
||||
import sonia.scm.search.QueryCountResult;
|
||||
import sonia.scm.search.QueryResult;
|
||||
import sonia.scm.search.SearchEngine;
|
||||
import sonia.scm.search.SearchableType;
|
||||
import sonia.scm.web.JsonMockHttpResponse;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
@@ -80,23 +82,47 @@ class SearchResourceTest {
|
||||
@Mock
|
||||
private HalEnricherRegistry enricherRegistry;
|
||||
|
||||
@Mock
|
||||
private SearchableType searchableTypeOne;
|
||||
|
||||
@Mock
|
||||
private SearchableType searchableTypeTwo;
|
||||
|
||||
@BeforeEach
|
||||
void setUpDispatcher() {
|
||||
ScmPathInfoStore scmPathInfoStore = new ScmPathInfoStore();
|
||||
scmPathInfoStore.set(() -> URI.create("/"));
|
||||
|
||||
QueryResultMapper mapper = Mappers.getMapper(QueryResultMapper.class);
|
||||
mapper.setRepositoryManager(repositoryManager);
|
||||
mapper.setResourceLinks(new ResourceLinks(scmPathInfoStore));
|
||||
QueryResultMapper queryResultMapper = Mappers.getMapper(QueryResultMapper.class);
|
||||
queryResultMapper.setRepositoryManager(repositoryManager);
|
||||
queryResultMapper.setResourceLinks(new ResourceLinks(scmPathInfoStore));
|
||||
|
||||
mapper.setRegistry(enricherRegistry);
|
||||
SearchableTypeMapper searchableTypeMapper = Mappers.getMapper(SearchableTypeMapper.class);
|
||||
queryResultMapper.setRegistry(enricherRegistry);
|
||||
SearchResource resource = new SearchResource(
|
||||
searchEngine, mapper
|
||||
searchEngine, queryResultMapper, searchableTypeMapper
|
||||
);
|
||||
dispatcher = new RestDispatcher();
|
||||
dispatcher.addSingletonResource(resource);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSearchableTypes() throws URISyntaxException {
|
||||
when(searchEngine.getSearchableTypes()).thenReturn(Lists.list(searchableTypeOne, searchableTypeTwo));
|
||||
when(searchableTypeOne.getName()).thenReturn("Type One");
|
||||
when(searchableTypeTwo.getName()).thenReturn("Type Two");
|
||||
|
||||
MockHttpRequest request = MockHttpRequest.get("/v2/search/searchableTypes");
|
||||
JsonMockHttpResponse response = new JsonMockHttpResponse();
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
JsonNode contentAsJson = response.getContentAsJson();
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(contentAsJson.isArray()).isTrue();
|
||||
assertThat(contentAsJson.get(0).get("name").asText()).isEqualTo("Type One");
|
||||
assertThat(contentAsJson.get(1).get("name").asText()).isEqualTo("Type Two");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEnrichQueryResult() throws IOException, URISyntaxException {
|
||||
when(enricherRegistry.allByType(QueryResult.class))
|
||||
@@ -330,7 +356,6 @@ class SearchResourceTest {
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class SampleEmbedded extends HalRepresentation {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.search.SearchableField;
|
||||
import sonia.scm.search.SearchableType;
|
||||
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SearchableTypeMapperTest {
|
||||
|
||||
@Mock
|
||||
private SearchableType searchableType;
|
||||
|
||||
@Mock
|
||||
private SearchableField searchableField;
|
||||
|
||||
private final SearchableTypeMapper searchableTypeMapper = Mappers.getMapper(SearchableTypeMapper.class);
|
||||
|
||||
@Test
|
||||
void shouldMapType() {
|
||||
when(searchableType.getName()).thenReturn("HitchhikerType");
|
||||
doReturn(ImmutableList.of(searchableField)).when(searchableType).getFields();
|
||||
when(searchableField.getName()).thenReturn("HitchhikerField");
|
||||
doReturn(Integer.class).when(searchableField).getType();
|
||||
final SearchableTypeDto typeDto = searchableTypeMapper.map(searchableType);
|
||||
Assertions.assertThat(typeDto.getName()).isEqualTo("HitchhikerType");
|
||||
final SearchableFieldDto searchableFieldDto = typeDto.getFields().stream().findFirst().get();
|
||||
Assertions.assertThat(searchableFieldDto.getName()).isEqualTo("HitchhikerField");
|
||||
Assertions.assertThat(searchableFieldDto.getType()).isEqualTo("number");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user