mirror of
https://github.com/simon987/sist2.git
synced 2025-12-11 14:38:54 +00:00
SQLite search backend
This commit is contained in:
@@ -21,6 +21,8 @@ import {mapActions, mapGetters, mapMutations} from "vuex";
|
||||
import Sist2Api from "@/Sist2Api";
|
||||
import ModelsRepo from "@/ml/modelsRepo";
|
||||
import {setupAuth0} from "@/main";
|
||||
import Sist2ElasticsearchQuery from "@/Sist2ElasticsearchQuery";
|
||||
import Sist2SqliteQuery from "@/Sist2SqliteQuery";
|
||||
|
||||
export default {
|
||||
components: {NavBar},
|
||||
@@ -88,6 +90,13 @@ export default {
|
||||
|
||||
this.setSist2Info(data);
|
||||
this.setIndices(data.indices)
|
||||
|
||||
if (Sist2Api.backend() === "sqlite") {
|
||||
Sist2Api.init(Sist2SqliteQuery.searchQuery);
|
||||
this.$store.commit("setUiSqliteMode", true);
|
||||
} else {
|
||||
Sist2Api.init(Sist2ElasticsearchQuery.searchQuery);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import axios from "axios";
|
||||
import {ext, strUnescape, lum} from "./util";
|
||||
import Sist2Query from "@/Sist2ElasticsearchQuery";
|
||||
import store from "@/store";
|
||||
|
||||
export interface EsTag {
|
||||
id: string
|
||||
@@ -99,12 +101,22 @@ export interface EsResult {
|
||||
|
||||
class Sist2Api {
|
||||
|
||||
private baseUrl: string
|
||||
private readonly baseUrl: string
|
||||
private sist2Info: any
|
||||
private queryfunc: Function;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
init(queryFunc: Function) {
|
||||
this.queryfunc = queryFunc;
|
||||
}
|
||||
|
||||
backend() {
|
||||
return this.sist2Info.searchBackend;
|
||||
}
|
||||
|
||||
getSist2Info(): Promise<any> {
|
||||
return axios.get(`${this.baseUrl}i`).then(resp => {
|
||||
const indices = resp.data.indices as Index[];
|
||||
@@ -119,6 +131,8 @@ class Sist2Api {
|
||||
} as Index;
|
||||
});
|
||||
|
||||
this.sist2Info = resp.data;
|
||||
|
||||
return resp.data;
|
||||
})
|
||||
}
|
||||
@@ -219,6 +233,14 @@ class Sist2Api {
|
||||
} as Tag;
|
||||
}
|
||||
|
||||
search(): Promise<EsResult> {
|
||||
if (this.backend() == "sqlite") {
|
||||
return this.ftsQuery(this.queryfunc())
|
||||
} else {
|
||||
return this.esQuery(this.queryfunc());
|
||||
}
|
||||
}
|
||||
|
||||
esQuery(query: any): Promise<EsResult> {
|
||||
return axios.post(`${this.baseUrl}es`, query).then(resp => {
|
||||
const res = resp.data as EsResult;
|
||||
@@ -237,7 +259,30 @@ class Sist2Api {
|
||||
});
|
||||
}
|
||||
|
||||
getMimeTypes(query = undefined) {
|
||||
ftsQuery(query: any): Promise<EsResult> {
|
||||
return axios.post(`${this.baseUrl}fts/search`, query).then(resp => {
|
||||
const res = resp.data as any;
|
||||
|
||||
if (res.hits.hits) {
|
||||
res.hits.hits.forEach(hit => {
|
||||
hit["_source"]["name"] = strUnescape(hit["_source"]["name"]);
|
||||
hit["_source"]["path"] = strUnescape(hit["_source"]["path"]);
|
||||
|
||||
this.setHitProps(hit);
|
||||
this.setHitTags(hit);
|
||||
|
||||
if ("highlight" in hit) {
|
||||
hit["highlight"]["name"] = [hit["highlight"]["name"]];
|
||||
hit["highlight"]["content"] = [hit["highlight"]["content"]];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
private getMimeTypesEs(query) {
|
||||
const AGGS = {
|
||||
mimeTypes: {
|
||||
terms: {
|
||||
@@ -258,48 +303,70 @@ class Sist2Api {
|
||||
}
|
||||
|
||||
return this.esQuery(query).then(resp => {
|
||||
const mimeMap: any[] = [];
|
||||
const buckets = resp["aggregations"]["mimeTypes"]["buckets"];
|
||||
return resp["aggregations"]["mimeTypes"]["buckets"].map(bucket => ({
|
||||
mime: bucket.key,
|
||||
count: bucket.doc_count
|
||||
}));
|
||||
|
||||
buckets.sort((a: any, b: any) => a.key > b.key).forEach((bucket: any) => {
|
||||
const tmp = bucket["key"].split("/");
|
||||
const category = tmp[0];
|
||||
const mime = tmp[1];
|
||||
});
|
||||
}
|
||||
|
||||
let category_exists = false;
|
||||
private getMimeTypesSqlite(): Promise<[{ mime: string, count: number }]> {
|
||||
return axios.get(`${this.baseUrl}fts/mimetypes`)
|
||||
.then(resp => {
|
||||
return resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
const child = {
|
||||
"id": bucket["key"],
|
||||
"text": `${mime} (${bucket["doc_count"]})`
|
||||
};
|
||||
async getMimeTypes(query = undefined) {
|
||||
let buckets;
|
||||
|
||||
mimeMap.forEach(node => {
|
||||
if (node.text === category) {
|
||||
node.children.push(child);
|
||||
category_exists = true;
|
||||
}
|
||||
});
|
||||
if (this.backend() == "sqlite") {
|
||||
buckets = await this.getMimeTypesSqlite();
|
||||
} else {
|
||||
buckets = await this.getMimeTypesEs(query);
|
||||
}
|
||||
|
||||
if (!category_exists) {
|
||||
mimeMap.push({text: category, children: [child], id: category});
|
||||
}
|
||||
})
|
||||
const mimeMap: any[] = [];
|
||||
|
||||
buckets.sort((a: any, b: any) => a.mime > b.mime).forEach((bucket: any) => {
|
||||
const tmp = bucket.mime.split("/");
|
||||
const category = tmp[0];
|
||||
const mime = tmp[1];
|
||||
|
||||
let category_exists = false;
|
||||
|
||||
const child = {
|
||||
"id": bucket.mime,
|
||||
"text": `${mime} (${bucket.count})`
|
||||
};
|
||||
|
||||
mimeMap.forEach(node => {
|
||||
if (node.children) {
|
||||
node.children.sort((a, b) => a.id.localeCompare(b.id));
|
||||
if (node.text === category) {
|
||||
node.children.push(child);
|
||||
category_exists = true;
|
||||
}
|
||||
})
|
||||
mimeMap.sort((a, b) => a.id.localeCompare(b.id))
|
||||
});
|
||||
|
||||
return {buckets, mimeMap};
|
||||
});
|
||||
if (!category_exists) {
|
||||
mimeMap.push({text: category, children: [child], id: category});
|
||||
}
|
||||
})
|
||||
|
||||
mimeMap.forEach(node => {
|
||||
if (node.children) {
|
||||
node.children.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
})
|
||||
mimeMap.sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
return {buckets, mimeMap};
|
||||
}
|
||||
|
||||
_createEsTag(tag: string, count: number): EsTag {
|
||||
const tokens = tag.split(".");
|
||||
|
||||
if (/.*\.#[0-9a-f]{6}/.test(tag)) {
|
||||
if (/.*\.#[0-9a-fA-F]{6}/.test(tag)) {
|
||||
return {
|
||||
id: tokens.slice(0, -1).join("."),
|
||||
color: tokens.pop(),
|
||||
@@ -316,32 +383,48 @@ class Sist2Api {
|
||||
};
|
||||
}
|
||||
|
||||
getTags() {
|
||||
private getTagsEs() {
|
||||
return this.esQuery({
|
||||
aggs: {
|
||||
tags: {
|
||||
terms: {
|
||||
field: "tag",
|
||||
size: 10000
|
||||
size: 65535
|
||||
}
|
||||
}
|
||||
},
|
||||
size: 0,
|
||||
}).then(resp => {
|
||||
const seen = new Set();
|
||||
|
||||
const tags = resp["aggregations"]["tags"]["buckets"]
|
||||
return resp["aggregations"]["tags"]["buckets"]
|
||||
.sort((a: any, b: any) => a["key"].localeCompare(b["key"]))
|
||||
.map((bucket: any) => this._createEsTag(bucket["key"], bucket["doc_count"]));
|
||||
});
|
||||
}
|
||||
|
||||
// Remove duplicates (same tag with different color)
|
||||
return tags.filter((t: EsTag) => {
|
||||
if (seen.has(t.id)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(t.id);
|
||||
return true;
|
||||
private getTagsSqlite() {
|
||||
return axios.get(`${this.baseUrl}/fts/tags`)
|
||||
.then(resp => {
|
||||
return resp.data.map(tag => this._createEsTag(tag.tag, tag.count))
|
||||
});
|
||||
}
|
||||
|
||||
async getTags(): Promise<EsTag[]> {
|
||||
let tags;
|
||||
if (this.backend() == "sqlite") {
|
||||
tags = await this.getTagsSqlite();
|
||||
} else {
|
||||
tags = await this.getTagsEs();
|
||||
}
|
||||
|
||||
// Remove duplicates (same tag with different color)
|
||||
const seen = new Set();
|
||||
|
||||
return tags.filter((t: EsTag) => {
|
||||
if (seen.has(t.id)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(t.id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -361,6 +444,144 @@ class Sist2Api {
|
||||
});
|
||||
}
|
||||
|
||||
searchPaths(indexId, minDepth, maxDepth, prefix = null) {
|
||||
if (this.backend() == "sqlite") {
|
||||
return this.searchPathsSqlite(indexId, minDepth, minDepth, prefix);
|
||||
} else {
|
||||
return this.searchPathsEs(indexId, minDepth, maxDepth, prefix);
|
||||
}
|
||||
}
|
||||
|
||||
private searchPathsSqlite(indexId, minDepth, maxDepth, prefix) {
|
||||
return axios.post(`${this.baseUrl}fts/paths`, {
|
||||
indexId, minDepth, maxDepth, prefix
|
||||
}).then(resp => {
|
||||
return resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
private searchPathsEs(indexId, minDepth, maxDepth, prefix): Promise<[{ path: string, count: number }]> {
|
||||
|
||||
const query = {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{term: {index: indexId}},
|
||||
{range: {_depth: {gte: minDepth, lte: maxDepth}}},
|
||||
]
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
paths: {
|
||||
terms: {
|
||||
field: "path",
|
||||
size: 10000
|
||||
}
|
||||
}
|
||||
},
|
||||
size: 0
|
||||
};
|
||||
|
||||
if (prefix != null) {
|
||||
query["query"]["bool"]["must"] = {
|
||||
prefix: {
|
||||
path: prefix,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return this.esQuery(query).then(resp => {
|
||||
const buckets = resp["aggregations"]["paths"]["buckets"];
|
||||
|
||||
if (!buckets) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return buckets
|
||||
.map(bucket => ({
|
||||
path: bucket.key,
|
||||
count: bucket.doc_count
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private getDateRangeSqlite() {
|
||||
return axios.get(`${this.baseUrl}fts/dateRange`)
|
||||
.then(resp => ({
|
||||
min: resp.data.dateMin,
|
||||
max: resp.data.dateMax,
|
||||
}));
|
||||
}
|
||||
|
||||
getDateRange(): Promise<{ min: number, max: number }> {
|
||||
if (this.backend() == "sqlite") {
|
||||
return this.getDateRangeSqlite();
|
||||
} else {
|
||||
return this.getDateRangeEs();
|
||||
}
|
||||
}
|
||||
|
||||
private getDateRangeEs() {
|
||||
return this.esQuery({
|
||||
// TODO: filter current selected indices
|
||||
aggs: {
|
||||
dateMin: {min: {field: "mtime"}},
|
||||
dateMax: {max: {field: "mtime"}},
|
||||
},
|
||||
size: 0
|
||||
}).then(res => {
|
||||
const range = {
|
||||
min: res.aggregations.dateMin.value,
|
||||
max: res.aggregations.dateMax.value,
|
||||
}
|
||||
|
||||
if (range.min == null) {
|
||||
range.min = 0;
|
||||
range.max = 1;
|
||||
} else if (range.min == range.max) {
|
||||
range.max += 1;
|
||||
}
|
||||
|
||||
return range;
|
||||
});
|
||||
}
|
||||
|
||||
private getPathSuggestionsSqlite(text: string) {
|
||||
return axios.post(`${this.baseUrl}fts/paths`, {
|
||||
prefix: text,
|
||||
minDepth: 1,
|
||||
maxDepth: 10000
|
||||
}).then(resp => {
|
||||
return resp.data.map(bucket => bucket.path);
|
||||
})
|
||||
}
|
||||
|
||||
private getPathSuggestionsEs(text) {
|
||||
return this.esQuery({
|
||||
suggest: {
|
||||
path: {
|
||||
prefix: text,
|
||||
completion: {
|
||||
field: "suggest-path",
|
||||
skip_duplicates: true,
|
||||
size: 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
}).then(resp => {
|
||||
return resp["suggest"]["path"][0]["options"]
|
||||
.map(opt => opt["_source"]["path"]);
|
||||
});
|
||||
}
|
||||
|
||||
getPathSuggestions(text: string): Promise<string[]> {
|
||||
if (this.backend() == "sqlite") {
|
||||
return this.getPathSuggestionsSqlite(text);
|
||||
} else {
|
||||
return this.getPathSuggestionsEs(text)
|
||||
}
|
||||
}
|
||||
|
||||
getTreemapStat(indexId: string) {
|
||||
return `${this.baseUrl}s/${indexId}/TMAP`;
|
||||
}
|
||||
@@ -376,6 +597,111 @@ class Sist2Api {
|
||||
getDateStat(indexId: string) {
|
||||
return `${this.baseUrl}s/${indexId}/DAGG`;
|
||||
}
|
||||
|
||||
private getDocumentEs(docId: string, highlight: boolean, fuzzy: boolean) {
|
||||
const query = Sist2Query.searchQuery();
|
||||
|
||||
if (highlight) {
|
||||
const fields = fuzzy
|
||||
? {"content.nGram": {}}
|
||||
: {content: {}};
|
||||
|
||||
query.highlight = {
|
||||
pre_tags: ["<mark>"],
|
||||
post_tags: ["</mark>"],
|
||||
number_of_fragments: 0,
|
||||
fields,
|
||||
};
|
||||
|
||||
if (!store.state.sist2Info.esVersionLegacy) {
|
||||
query.highlight.max_analyzed_offset = 999_999;
|
||||
}
|
||||
}
|
||||
|
||||
if ("function_score" in query.query) {
|
||||
query.query = query.query.function_score.query;
|
||||
}
|
||||
|
||||
if (!("must" in query.query.bool)) {
|
||||
query.query.bool.must = [];
|
||||
} else if (!Array.isArray(query.query.bool.must)) {
|
||||
query.query.bool.must = [query.query.bool.must];
|
||||
}
|
||||
|
||||
query.query.bool.must.push({match: {_id: docId}});
|
||||
|
||||
delete query["sort"];
|
||||
delete query["aggs"];
|
||||
delete query["search_after"];
|
||||
delete query.query["function_score"];
|
||||
|
||||
query._source = {
|
||||
includes: ["content", "name", "path", "extension"]
|
||||
}
|
||||
|
||||
query.size = 1;
|
||||
|
||||
return this.esQuery(query).then(resp => {
|
||||
if (resp.hits.hits.length === 1) {
|
||||
return resp.hits.hits[0];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private getDocumentSqlite(docId: string): Promise<EsHit> {
|
||||
return axios.get(`${this.baseUrl}/fts/d/${docId}`)
|
||||
.then(resp => ({
|
||||
_source: resp.data
|
||||
} as EsHit));
|
||||
}
|
||||
|
||||
getDocument(docId: string, highlight: boolean, fuzzy: boolean): Promise<EsHit | null> {
|
||||
if (this.backend() == "sqlite") {
|
||||
return this.getDocumentSqlite(docId);
|
||||
} else {
|
||||
return this.getDocumentEs(docId, highlight, fuzzy);
|
||||
}
|
||||
}
|
||||
|
||||
getTagSuggestions(prefix: string): Promise<string[]> {
|
||||
if (this.backend() == "sqlite") {
|
||||
return this.getTagSuggestionsSqlite(prefix);
|
||||
} else {
|
||||
return this.getTagSuggestionsEs(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
private getTagSuggestionsSqlite(prefix): Promise<string[]> {
|
||||
return axios.post(`${this.baseUrl}/fts/suggestTags`, prefix)
|
||||
.then(resp => (resp.data));
|
||||
}
|
||||
|
||||
private getTagSuggestionsEs(prefix): Promise<string[]> {
|
||||
return this.esQuery({
|
||||
suggest: {
|
||||
tag: {
|
||||
prefix: prefix,
|
||||
completion: {
|
||||
field: "suggest-tag",
|
||||
skip_duplicates: true,
|
||||
size: 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
}).then(resp => {
|
||||
const result = [];
|
||||
resp["suggest"]["tag"][0]["options"].map(opt => opt["_source"]["tag"]).forEach(tags => {
|
||||
tags.forEach(tag => {
|
||||
const t = tag.slice(0, -8);
|
||||
if (!result.find(x => x.slice(0, -8) === t)) {
|
||||
result.push(tag);
|
||||
}
|
||||
});
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Sist2Api("");
|
||||
@@ -67,7 +67,7 @@ interface SortMode {
|
||||
}
|
||||
|
||||
|
||||
class Sist2Query {
|
||||
class Sist2ElasticsearchQuery {
|
||||
|
||||
searchQuery(blankSearch: boolean = false): any {
|
||||
|
||||
@@ -249,4 +249,5 @@ class Sist2Query {
|
||||
}
|
||||
}
|
||||
|
||||
export default new Sist2Query();
|
||||
|
||||
export default new Sist2ElasticsearchQuery();
|
||||
111
sist2-vue/src/Sist2SqliteQuery.ts
Normal file
111
sist2-vue/src/Sist2SqliteQuery.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import store from "./store";
|
||||
import {EsHit, Index} from "@/Sist2Api";
|
||||
|
||||
const SORT_MODES = {
|
||||
score: {
|
||||
"sort": "score",
|
||||
},
|
||||
random: {
|
||||
"sort": "random"
|
||||
},
|
||||
dateAsc: {
|
||||
"sort": "mtime"
|
||||
},
|
||||
dateDesc: {
|
||||
"sort": "mtime",
|
||||
"sortAsc": false
|
||||
},
|
||||
sizeAsc: {
|
||||
"sort": "size",
|
||||
},
|
||||
sizeDesc: {
|
||||
"sort": "size",
|
||||
"sortAsc": false
|
||||
},
|
||||
nameAsc: {
|
||||
"sort": "name",
|
||||
},
|
||||
nameDesc: {
|
||||
"sort": "name",
|
||||
"sortAsc": false
|
||||
}
|
||||
} as any;
|
||||
|
||||
interface SortMode {
|
||||
text: string
|
||||
mode: any[]
|
||||
key: (hit: EsHit) => any
|
||||
}
|
||||
|
||||
|
||||
class Sist2ElasticsearchQuery {
|
||||
|
||||
searchQuery(): any {
|
||||
|
||||
const getters = store.getters;
|
||||
|
||||
const searchText = getters.searchText;
|
||||
const pathText = getters.pathText;
|
||||
const sizeMin = getters.sizeMin;
|
||||
const sizeMax = getters.sizeMax;
|
||||
const dateMin = getters.dateMin;
|
||||
const dateMax = getters.dateMax;
|
||||
const size = getters.size;
|
||||
const after = getters.lastDoc;
|
||||
const selectedIndexIds = getters.selectedIndices.map((idx: Index) => idx.id)
|
||||
const selectedMimeTypes = getters.selectedMimeTypes;
|
||||
const selectedTags = getters.selectedTags;
|
||||
|
||||
const q = {
|
||||
"pageSize": size
|
||||
}
|
||||
|
||||
Object.assign(q, SORT_MODES[getters.sortMode]);
|
||||
|
||||
if (!after) {
|
||||
q["fetchAggregations"] = true;
|
||||
}
|
||||
if (searchText) {
|
||||
q["query"] = searchText;
|
||||
}
|
||||
if (pathText) {
|
||||
q["path"] = pathText.endsWith("/") ? pathText.slice(0, -1) : pathText;
|
||||
}
|
||||
if (sizeMin) {
|
||||
q["sizeMin"] = sizeMin;
|
||||
}
|
||||
if (sizeMax) {
|
||||
q["sizeMax"] = sizeMax;
|
||||
}
|
||||
if (dateMin) {
|
||||
q["dateMin"] = dateMin;
|
||||
}
|
||||
if (dateMax) {
|
||||
q["dateMax"] = dateMax;
|
||||
}
|
||||
if (after) {
|
||||
q["after"] = after.sort;
|
||||
}
|
||||
if (selectedIndexIds.length > 0) {
|
||||
q["indexIds"] = selectedIndexIds;
|
||||
}
|
||||
if (selectedMimeTypes.length > 0) {
|
||||
q["mimeTypes"] = selectedMimeTypes;
|
||||
}
|
||||
if (selectedTags.length > 0) {
|
||||
q["tags"] = selectedTags
|
||||
}
|
||||
if (getters.sortMode == "random") {
|
||||
q["seed"] = getters.seed;
|
||||
}
|
||||
if (getters.optHighlight) {
|
||||
q["highlight"] = true;
|
||||
q["highlightContextSize"] = Number(getters.optFragmentSize);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default new Sist2ElasticsearchQuery();
|
||||
@@ -1,41 +1,56 @@
|
||||
<template>
|
||||
<b-card v-if="$store.state.sist2Info.showDebugInfo" class="mb-4 mt-4">
|
||||
<b-card-title><DebugIcon class="mr-1"></DebugIcon>{{ $t("debug") }}</b-card-title>
|
||||
<p v-html="$t('debugDescription')"></p>
|
||||
<b-card v-if="$store.state.sist2Info.showDebugInfo" class="mb-4 mt-4">
|
||||
<b-card-title>
|
||||
<DebugIcon class="mr-1"></DebugIcon>
|
||||
{{ $t("debug") }}
|
||||
</b-card-title>
|
||||
<p v-html="$t('debugDescription')"></p>
|
||||
|
||||
<b-card-body>
|
||||
<b-card-body>
|
||||
|
||||
<b-table :items="tableItems" small borderless responsive="md" thead-class="hidden" class="mb-0"></b-table>
|
||||
<b-table :items="tableItems" small borderless responsive="md" thead-class="hidden" class="mb-0"></b-table>
|
||||
|
||||
<hr />
|
||||
<IndexDebugInfo v-for="idx of $store.state.sist2Info.indices" :key="idx.id" :index="idx" class="mt-2"></IndexDebugInfo>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
<hr/>
|
||||
<IndexDebugInfo v-for="idx of $store.state.sist2Info.indices" :key="idx.id" :index="idx"
|
||||
class="mt-2"></IndexDebugInfo>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IndexDebugInfo from "@/components/IndexDebugInfo";
|
||||
import DebugIcon from "@/components/icons/DebugIcon";
|
||||
import {mapGetters} from "vuex";
|
||||
|
||||
export default {
|
||||
name: "DebugInfo.vue",
|
||||
components: {DebugIcon, IndexDebugInfo},
|
||||
computed: {
|
||||
tableItems() {
|
||||
return [
|
||||
{key: "version", value: this.$store.state.sist2Info.version},
|
||||
{key: "platform", value: this.$store.state.sist2Info.platform},
|
||||
{key: "debugBinary", value: this.$store.state.sist2Info.debug},
|
||||
{key: "sist2CommitHash", value: this.$store.state.sist2Info.sist2Hash},
|
||||
{key: "esIndex", value: this.$store.state.sist2Info.esIndex},
|
||||
{key: "tagline", value: this.$store.state.sist2Info.tagline},
|
||||
{key: "dev", value: this.$store.state.sist2Info.dev},
|
||||
{key: "mongooseVersion", value: this.$store.state.sist2Info.mongooseVersion},
|
||||
{key: "esVersion", value: this.$store.state.sist2Info.esVersion},
|
||||
{key: "esVersionSupported", value: this.$store.state.sist2Info.esVersionSupported},
|
||||
{key: "esVersionLegacy", value: this.$store.state.sist2Info.esVersionLegacy},
|
||||
]
|
||||
name: "DebugInfo.vue",
|
||||
components: {DebugIcon, IndexDebugInfo},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
"uiSqliteMode",
|
||||
]),
|
||||
tableItems() {
|
||||
const items = [
|
||||
{key: "version", value: this.$store.state.sist2Info.version},
|
||||
{key: "platform", value: this.$store.state.sist2Info.platform},
|
||||
{key: "debugBinary", value: this.$store.state.sist2Info.debug},
|
||||
{key: "sist2CommitHash", value: this.$store.state.sist2Info.sist2Hash},
|
||||
{key: "esIndex", value: this.$store.state.sist2Info.esIndex},
|
||||
{key: "tagline", value: this.$store.state.sist2Info.tagline},
|
||||
{key: "dev", value: this.$store.state.sist2Info.dev},
|
||||
{key: "mongooseVersion", value: this.$store.state.sist2Info.mongooseVersion},
|
||||
];
|
||||
|
||||
if (!this.uiSqliteMode) {
|
||||
items.push(
|
||||
{key: "esVersion", value: this.$store.state.sist2Info.esVersion},
|
||||
{key: "esVersionSupported", value: this.$store.state.sist2Info.esVersionSupported},
|
||||
{key: "esVersionLegacy", value: this.$store.state.sist2Info.esVersionLegacy}
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,44 +1,44 @@
|
||||
<template>
|
||||
<div class="doc-card" :class="{'sub-document': doc._props.isSubDocument}" :style="`width: ${width}px`"
|
||||
@click="$store.commit('busTnTouchStart', null)">
|
||||
<b-card
|
||||
no-body
|
||||
img-top
|
||||
>
|
||||
<!-- Info modal-->
|
||||
<DocInfoModal :show="showInfo" :doc="doc" @close="showInfo = false"></DocInfoModal>
|
||||
<div class="doc-card" :class="{'sub-document': doc._props.isSubDocument}" :style="`width: ${width}px`"
|
||||
@click="$store.commit('busTnTouchStart', null)">
|
||||
<b-card
|
||||
no-body
|
||||
img-top
|
||||
>
|
||||
<!-- Info modal-->
|
||||
<DocInfoModal :show="showInfo" :doc="doc" @close="showInfo = false"></DocInfoModal>
|
||||
|
||||
<ContentDiv :doc="doc"></ContentDiv>
|
||||
<ContentDiv :doc="doc"></ContentDiv>
|
||||
|
||||
<!-- Thumbnail-->
|
||||
<FullThumbnail :doc="doc" :small-badge="smallBadge" @onThumbnailClick="onThumbnailClick()"></FullThumbnail>
|
||||
<!-- Thumbnail-->
|
||||
<FullThumbnail :doc="doc" :small-badge="smallBadge" @onThumbnailClick="onThumbnailClick()"></FullThumbnail>
|
||||
|
||||
<!-- Audio player-->
|
||||
<audio v-if="doc._props.isAudio" ref="audio" preload="none" class="audio-fit fit" controls
|
||||
:type="doc._source.mime"
|
||||
:src="`f/${doc._id}`"
|
||||
@play="onAudioPlay()"></audio>
|
||||
<!-- Audio player-->
|
||||
<audio v-if="doc._props.isAudio" ref="audio" preload="none" class="audio-fit fit" controls
|
||||
:type="doc._source.mime"
|
||||
:src="`f/${doc._source.index}/${doc._id}`"
|
||||
@play="onAudioPlay()"></audio>
|
||||
|
||||
<b-card-body class="padding-03">
|
||||
<b-card-body class="padding-03">
|
||||
|
||||
<!-- Title line -->
|
||||
<div style="display: flex">
|
||||
<span class="info-icon" @click="onInfoClick()"></span>
|
||||
<DocFileTitle :doc="doc"></DocFileTitle>
|
||||
</div>
|
||||
<!-- Title line -->
|
||||
<div style="display: flex">
|
||||
<span class="info-icon" @click="onInfoClick()"></span>
|
||||
<DocFileTitle :doc="doc"></DocFileTitle>
|
||||
</div>
|
||||
|
||||
<!-- Featured line -->
|
||||
<div style="display: flex">
|
||||
<FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine>
|
||||
</div>
|
||||
<!-- Featured line -->
|
||||
<div style="display: flex">
|
||||
<FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="card-text">
|
||||
<TagContainer :hit="doc"></TagContainer>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
<!-- Tags -->
|
||||
<div class="card-text">
|
||||
<TagContainer :hit="doc"></TagContainer>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -52,91 +52,91 @@ import FeaturedFieldsLine from "@/components/FeaturedFieldsLine";
|
||||
|
||||
|
||||
export default {
|
||||
components: {FeaturedFieldsLine, FullThumbnail, ContentDiv, DocInfoModal, DocFileTitle, TagContainer},
|
||||
props: ["doc", "width"],
|
||||
data() {
|
||||
return {
|
||||
ext: ext,
|
||||
showInfo: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
smallBadge() {
|
||||
return this.width < 150;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
humanFileSize: humanFileSize,
|
||||
humanTime: humanTime,
|
||||
onInfoClick() {
|
||||
this.showInfo = true;
|
||||
},
|
||||
async onThumbnailClick() {
|
||||
this.$store.commit("setUiLightboxSlide", this.doc._seq);
|
||||
await this.$store.dispatch("showLightbox");
|
||||
},
|
||||
onAudioPlay() {
|
||||
document.getElementsByTagName("audio").forEach((el) => {
|
||||
if (el !== this.$refs["audio"]) {
|
||||
el.pause();
|
||||
components: {FeaturedFieldsLine, FullThumbnail, ContentDiv, DocInfoModal, DocFileTitle, TagContainer},
|
||||
props: ["doc", "width"],
|
||||
data() {
|
||||
return {
|
||||
ext: ext,
|
||||
showInfo: false,
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
smallBadge() {
|
||||
return this.width < 150;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
humanFileSize: humanFileSize,
|
||||
humanTime: humanTime,
|
||||
onInfoClick() {
|
||||
this.showInfo = true;
|
||||
},
|
||||
async onThumbnailClick() {
|
||||
this.$store.commit("setUiLightboxSlide", this.doc._seq);
|
||||
await this.$store.dispatch("showLightbox");
|
||||
},
|
||||
onAudioPlay() {
|
||||
Array.prototype.slice.call(document.getElementsByTagName("audio")).forEach((el) => {
|
||||
if (el !== this.$refs["audio"]) {
|
||||
el.pause();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.fit {
|
||||
display: block;
|
||||
min-width: 64px;
|
||||
max-width: 100%;
|
||||
/*max-height: 400px;*/
|
||||
margin: 0 auto 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
display: block;
|
||||
min-width: 64px;
|
||||
max-width: 100%;
|
||||
/*max-height: 400px;*/
|
||||
margin: 0 auto 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.audio-fit {
|
||||
height: 39px;
|
||||
vertical-align: bottom;
|
||||
display: inline;
|
||||
width: 100%;
|
||||
height: 39px;
|
||||
vertical-align: bottom;
|
||||
display: inline;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.padding-03 {
|
||||
padding: 0.3rem;
|
||||
padding: 0.3rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-top: 1em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
margin-top: 1em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.3rem;
|
||||
padding: 0.3rem;
|
||||
}
|
||||
|
||||
.doc-card {
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.sub-document .card {
|
||||
background: #AB47BC1F !important;
|
||||
background: #AB47BC1F !important;
|
||||
}
|
||||
|
||||
.theme-black .sub-document .card {
|
||||
background: #37474F !important;
|
||||
background: #37474F !important;
|
||||
}
|
||||
|
||||
.sub-document .fit {
|
||||
padding: 4px 4px 0 4px;
|
||||
padding: 4px 4px 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a :href="`f/${doc._id}`" class="file-title-anchor" target="_blank">
|
||||
<a :href="`f/${doc._source.index}/${doc._id}`" class="file-title-anchor" target="_blank">
|
||||
<div class="file-title" :title="doc._source.path + '/' + doc._source.name + ext(doc)"
|
||||
v-html="fileName() + ext(doc)"></div>
|
||||
</a>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<img v-if="doc._props.isPlayableImage || doc._props.isPlayableVideo"
|
||||
:src="(doc._props.isGif && hover) ? `f/${doc._id}` : `t/${doc._source.index}/${doc._id}`"
|
||||
:src="(doc._props.isGif && hover) ? `f/${doc._source.index}/${doc._id}` : `t/${doc._source.index}/${doc._id}`"
|
||||
alt=""
|
||||
class="pointer fit-sm" @click="onThumbnailClick()">
|
||||
<img v-else :src="`t/${doc._source.index}/${doc._id}`" alt=""
|
||||
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
const doc = this.doc;
|
||||
const props = doc._props;
|
||||
if (props.isGif && this.hover) {
|
||||
return `f/${doc._id}`;
|
||||
return `f/${doc._source.index}/${doc._id}`;
|
||||
}
|
||||
return (this.currentThumbnailNum === 0)
|
||||
? `t/${doc._source.index}/${doc._id}`
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<b-progress v-if="mlLoading" variant="warning" show-progress :max="1" class="mb-3"
|
||||
>
|
||||
<b-progress-bar :value="modelLoadingProgress">
|
||||
<strong>{{ ((modelLoadingProgress * modelSize) / (1024*1024)).toFixed(1) }}MB / {{
|
||||
<strong>{{ ((modelLoadingProgress * modelSize) / (1024 * 1024)).toFixed(1) }}MB / {{
|
||||
(modelSize / (1024 * 1024)).toFixed(1)
|
||||
}}MB</strong>
|
||||
</b-progress-bar>
|
||||
@@ -36,7 +36,7 @@
|
||||
<script>
|
||||
import Sist2Api from "@/Sist2Api";
|
||||
import Preloader from "@/components/Preloader";
|
||||
import Sist2Query from "@/Sist2Query";
|
||||
import Sist2Query from "@/Sist2ElasticsearchQuery";
|
||||
import store from "@/store";
|
||||
import BertNerModel from "@/ml/BertNerModel";
|
||||
import AnalyzedContentSpansContainer from "@/components/AnalyzedContentSpanContainer.vue";
|
||||
@@ -69,58 +69,19 @@ export default {
|
||||
this.mlModel = ModelsRepo.getDefaultModel();
|
||||
}
|
||||
|
||||
const query = Sist2Query.searchQuery();
|
||||
Sist2Api
|
||||
.getDocument(this.docId, this.$store.state.optHighlight, this.$store.state.fuzzy)
|
||||
.then(doc => {
|
||||
this.loading = false;
|
||||
|
||||
if (this.$store.state.optHighlight) {
|
||||
const fields = this.$store.state.fuzzy
|
||||
? {"content.nGram": {}}
|
||||
: {content: {}};
|
||||
if (doc) {
|
||||
this.content = this.getContent(doc)
|
||||
}
|
||||
|
||||
query.highlight = {
|
||||
pre_tags: ["<mark>"],
|
||||
post_tags: ["</mark>"],
|
||||
number_of_fragments: 0,
|
||||
fields,
|
||||
};
|
||||
|
||||
if (!store.state.sist2Info.esVersionLegacy) {
|
||||
query.highlight.max_analyzed_offset = 999_999;
|
||||
}
|
||||
}
|
||||
|
||||
if ("function_score" in query.query) {
|
||||
query.query = query.query.function_score.query;
|
||||
}
|
||||
|
||||
if (!("must" in query.query.bool)) {
|
||||
query.query.bool.must = [];
|
||||
} else if (!Array.isArray(query.query.bool.must)) {
|
||||
query.query.bool.must = [query.query.bool.must];
|
||||
}
|
||||
|
||||
query.query.bool.must.push({match: {_id: this.docId}});
|
||||
|
||||
delete query["sort"];
|
||||
delete query["aggs"];
|
||||
delete query["search_after"];
|
||||
delete query.query["function_score"];
|
||||
|
||||
query._source = {
|
||||
includes: ["content", "name", "path", "extension"]
|
||||
}
|
||||
|
||||
query.size = 1;
|
||||
|
||||
Sist2Api.esQuery(query).then(resp => {
|
||||
this.loading = false;
|
||||
if (resp.hits.hits.length === 1) {
|
||||
this.content = this.getContent(resp.hits.hits[0]);
|
||||
}
|
||||
|
||||
if (this.optAutoAnalyze) {
|
||||
this.mlAnalyze();
|
||||
}
|
||||
});
|
||||
if (this.optAutoAnalyze) {
|
||||
this.mlAnalyze();
|
||||
}
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["optAutoAnalyze"]),
|
||||
|
||||
@@ -9,7 +9,7 @@ import InspireTreeDOM from "inspire-tree-dom";
|
||||
import "inspire-tree-dom/dist/inspire-tree-light.min.css";
|
||||
import {getSelectedTreeNodes, getTreeNodeAttributes} from "@/util";
|
||||
import Sist2Api from "@/Sist2Api";
|
||||
import Sist2Query from "@/Sist2Query";
|
||||
import Sist2Query from "@/Sist2ElasticsearchQuery";
|
||||
|
||||
export default {
|
||||
name: "MimePicker",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<span class="badge badge-pill version" v-if="$store && $store.state.sist2Info">
|
||||
v{{ sist2Version() }}<span v-if="isDebug()">-dbg</span><span v-if="isLegacy() && !hideLegacy()">-<a
|
||||
href="https://github.com/simon987/sist2/blob/master/docs/USAGE.md#elasticsearch"
|
||||
target="_blank">legacyES</a></span>
|
||||
target="_blank">legacyES</a></span><span v-if="$store.state.uiSqliteMode">-SQLite</span>
|
||||
</span>
|
||||
|
||||
<span v-if="$store && $store.state.sist2Info" class="tagline" v-html="tagline()"></span>
|
||||
|
||||
@@ -1,40 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="input-group" style="margin-bottom: 0.5em; margin-top: 1em">
|
||||
<div class="input-group-prepend">
|
||||
<div>
|
||||
<div class="input-group" style="margin-bottom: 0.5em; margin-top: 1em">
|
||||
<div class="input-group-prepend">
|
||||
|
||||
<b-button variant="outline-secondary" @click="$refs['path-modal'].show()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="20px">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M288 224h224a32 32 0 0 0 32-32V64a32 32 0 0 0-32-32H400L368 0h-80a32 32 0 0 0-32 32v64H64V8a8 8 0 0 0-8-8H40a8 8 0 0 0-8 8v392a16 16 0 0 0 16 16h208v64a32 32 0 0 0 32 32h224a32 32 0 0 0 32-32V352a32 32 0 0 0-32-32H400l-32-32h-80a32 32 0 0 0-32 32v64H64V128h192v64a32 32 0 0 0 32 32zm0 96h66.74l32 32H512v128H288zm0-288h66.74l32 32H512v128H288z"
|
||||
/>
|
||||
</svg>
|
||||
</b-button>
|
||||
</div>
|
||||
<b-button variant="outline-secondary" @click="$refs['path-modal'].show()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="20px">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M288 224h224a32 32 0 0 0 32-32V64a32 32 0 0 0-32-32H400L368 0h-80a32 32 0 0 0-32 32v64H64V8a8 8 0 0 0-8-8H40a8 8 0 0 0-8 8v392a16 16 0 0 0 16 16h208v64a32 32 0 0 0 32 32h224a32 32 0 0 0 32-32V352a32 32 0 0 0-32-32H400l-32-32h-80a32 32 0 0 0-32 32v64H64V128h192v64a32 32 0 0 0 32 32zm0 96h66.74l32 32H512v128H288zm0-288h66.74l32 32H512v128H288z"
|
||||
/>
|
||||
</svg>
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<VueSimpleSuggest
|
||||
class="form-control-fix-flex"
|
||||
@input="setPathText"
|
||||
:value="getPathText"
|
||||
:list="suggestPath"
|
||||
:max-suggestions="0"
|
||||
:placeholder="$t('pathBar.placeholder')"
|
||||
:debounce="200"
|
||||
>
|
||||
<!-- Suggestion item template-->
|
||||
<div slot="suggestion-item" slot-scope="{ suggestion, query }">
|
||||
<div class="suggestion-line" :title="suggestion">
|
||||
<strong>{{ query }}</strong>{{ getSuggestionWithoutQueryPrefix(suggestion, query) }}
|
||||
</div>
|
||||
</div>
|
||||
</VueSimpleSuggest>
|
||||
|
||||
<VueSimpleSuggest
|
||||
class="form-control-fix-flex"
|
||||
@input="setPathText"
|
||||
:value="getPathText"
|
||||
:list="suggestPath"
|
||||
:max-suggestions="0"
|
||||
:placeholder="$t('pathBar.placeholder')"
|
||||
>
|
||||
<!-- Suggestion item template-->
|
||||
<div slot="suggestion-item" slot-scope="{ suggestion, query }">
|
||||
<div class="suggestion-line" :title="suggestion">
|
||||
<strong>{{ query }}</strong>{{ getSuggestionWithoutQueryPrefix(suggestion, query) }}
|
||||
</div>
|
||||
</div>
|
||||
</VueSimpleSuggest>
|
||||
|
||||
<b-modal ref="path-modal" :title="$t('pathBar.modalTitle')" size="lg" :hide-footer="true" static>
|
||||
<div id="pathTree"></div>
|
||||
</b-modal>
|
||||
</div>
|
||||
|
||||
<b-modal ref="path-modal" :title="$t('pathBar.modalTitle')" size="lg" :hide-footer="true" static>
|
||||
<div id="pathTree"></div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -48,198 +49,153 @@ import VueSimpleSuggest from 'vue-simple-suggest'
|
||||
import 'vue-simple-suggest/dist/styles.css' // Optional CSS
|
||||
|
||||
export default {
|
||||
name: "PathTree",
|
||||
components: {
|
||||
VueSimpleSuggest
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mimeTree: null,
|
||||
pathItems: [],
|
||||
tmpPath: ""
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["getPathText"])
|
||||
},
|
||||
mounted() {
|
||||
this.$store.subscribe((mutation) => {
|
||||
// Wait until indices are loaded to get the root paths
|
||||
if (mutation.type === "setIndices") {
|
||||
let pathTree = new InspireTree({
|
||||
data: (node, resolve, reject) => {
|
||||
return this.getNextDepth(node);
|
||||
},
|
||||
sort: "text"
|
||||
});
|
||||
|
||||
this.$store.state.indices.forEach(idx => {
|
||||
pathTree.addNode({
|
||||
id: "/" + idx.id,
|
||||
values: ["/" + idx.id],
|
||||
text: `/[${idx.name}]`,
|
||||
index: idx.id,
|
||||
depth: 0,
|
||||
children: true
|
||||
})
|
||||
});
|
||||
|
||||
new InspireTreeDOM(pathTree, {
|
||||
target: "#pathTree"
|
||||
});
|
||||
|
||||
pathTree.on("node.click", this.handleTreeClick);
|
||||
pathTree.expand();
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["setPathText"]),
|
||||
getSuggestionWithoutQueryPrefix(suggestion, query) {
|
||||
return suggestion.slice(query.length)
|
||||
name: "PathTree",
|
||||
components: {
|
||||
VueSimpleSuggest
|
||||
},
|
||||
async getPathChoices() {
|
||||
return new Promise(getPaths => {
|
||||
const q = {
|
||||
suggest: {
|
||||
path: {
|
||||
prefix: this.getPathText,
|
||||
completion: {
|
||||
field: "suggest-path",
|
||||
skip_duplicates: true,
|
||||
size: 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
data() {
|
||||
return {
|
||||
mimeTree: null,
|
||||
pathItems: [],
|
||||
tmpPath: ""
|
||||
|
||||
Sist2Api.esQuery(q)
|
||||
.then(resp => getPaths(resp["suggest"]["path"][0]["options"].map(opt => opt["_source"]["path"])));
|
||||
})
|
||||
},
|
||||
async suggestPath(term) {
|
||||
if (!this.$store.state.optSuggestPath) {
|
||||
return []
|
||||
}
|
||||
|
||||
term = term.toLowerCase();
|
||||
|
||||
const choices = await this.getPathChoices();
|
||||
|
||||
let matches = [];
|
||||
for (let i = 0; i < choices.length; i++) {
|
||||
if (~choices[i].toLowerCase().indexOf(term)) {
|
||||
matches.push(choices[i]);
|
||||
}
|
||||
}
|
||||
return matches.sort((a, b) => a.length - b.length);
|
||||
},
|
||||
getNextDepth(node) {
|
||||
const q = {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{term: {index: node.index}},
|
||||
{range: {_depth: {gte: node.depth + 1, lte: node.depth + 3}}},
|
||||
]
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
paths: {
|
||||
terms: {
|
||||
field: "path",
|
||||
size: 10000
|
||||
computed: {
|
||||
...mapGetters(["getPathText"])
|
||||
},
|
||||
mounted() {
|
||||
this.$store.subscribe((mutation) => {
|
||||
// Wait until indices are loaded to get the root paths
|
||||
if (mutation.type === "setIndices") {
|
||||
let pathTree = new InspireTree({
|
||||
data: (node, resolve, reject) => {
|
||||
return this.getNextDepth(node);
|
||||
},
|
||||
sort: "text"
|
||||
});
|
||||
|
||||
this.$store.state.indices.forEach(idx => {
|
||||
pathTree.addNode({
|
||||
id: "/" + idx.id,
|
||||
values: ["/" + idx.id],
|
||||
text: `/[${idx.name}]`,
|
||||
index: idx.id,
|
||||
depth: 0,
|
||||
children: true
|
||||
})
|
||||
});
|
||||
|
||||
new InspireTreeDOM(pathTree, {
|
||||
target: "#pathTree"
|
||||
});
|
||||
|
||||
pathTree.on("node.click", this.handleTreeClick);
|
||||
pathTree.expand();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["setPathText"]),
|
||||
getSuggestionWithoutQueryPrefix(suggestion, query) {
|
||||
return suggestion.slice(query.length)
|
||||
},
|
||||
size: 0
|
||||
};
|
||||
async getPathChoices() {
|
||||
return new Promise(getPaths => {
|
||||
Sist2Api.getPathSuggestions(this.getPathText).then(getPaths);
|
||||
});
|
||||
},
|
||||
async suggestPath(term) {
|
||||
if (!this.$store.state.optSuggestPath) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (node.depth > 0) {
|
||||
q.query.bool.must = {
|
||||
prefix: {
|
||||
path: node.id,
|
||||
}
|
||||
};
|
||||
}
|
||||
term = term.toLowerCase();
|
||||
|
||||
return Sist2Api.esQuery(q).then(resp => {
|
||||
const buckets = resp["aggregations"]["paths"]["buckets"];
|
||||
if (!buckets) {
|
||||
return false;
|
||||
}
|
||||
const choices = await this.getPathChoices();
|
||||
|
||||
const paths = [];
|
||||
let matches = [];
|
||||
for (let i = 0; i < choices.length; i++) {
|
||||
if (~choices[i].toLowerCase().indexOf(term)) {
|
||||
matches.push(choices[i]);
|
||||
}
|
||||
}
|
||||
return matches.sort((a, b) => a.length - b.length);
|
||||
},
|
||||
getNextDepth(node) {
|
||||
return Sist2Api
|
||||
.searchPaths(node.index, node.depth + 1, node.depth + 3, node.depth > 0 ? node.id : null)
|
||||
.then(buckets => {
|
||||
const paths = [];
|
||||
|
||||
return buckets
|
||||
.filter(bucket => bucket.key.length > node.id.length || node.id.startsWith("/"))
|
||||
.sort((a, b) => a.key > b.key)
|
||||
.map(bucket => {
|
||||
return buckets
|
||||
.filter(bucket => bucket.path.length > node.id.length || node.id.startsWith("/"))
|
||||
.sort((a, b) => a.path > b.path ? 1 : -1)
|
||||
.map(bucket => {
|
||||
if (paths.some(n => bucket.path.startsWith(n))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (paths.some(n => bucket.key.startsWith(n))) {
|
||||
return null;
|
||||
}
|
||||
const name = node.id.startsWith("/") ? bucket.path : bucket.path.slice(node.id.length + 1);
|
||||
|
||||
const name = node.id.startsWith("/") ? bucket.key : bucket.key.slice(node.id.length + 1);
|
||||
paths.push(bucket.path);
|
||||
|
||||
paths.push(bucket.key);
|
||||
return {
|
||||
id: bucket.path,
|
||||
text: `${name}/ (${bucket.count})`,
|
||||
depth: node.depth + 1,
|
||||
index: node.index,
|
||||
values: [bucket.path],
|
||||
children: true,
|
||||
}
|
||||
})
|
||||
.filter(bucket => bucket !== null);
|
||||
});
|
||||
},
|
||||
handleTreeClick(e, node, handler) {
|
||||
if (node.depth !== 0) {
|
||||
this.setPathText(node.id);
|
||||
this.$refs['path-modal'].hide()
|
||||
|
||||
return {
|
||||
id: bucket.key,
|
||||
text: `${name}/ (${bucket.doc_count})`,
|
||||
depth: node.depth + 1,
|
||||
index: node.index,
|
||||
values: [bucket.key],
|
||||
children: true,
|
||||
}
|
||||
}).filter(x => x !== null)
|
||||
});
|
||||
this.$emit("search");
|
||||
}
|
||||
|
||||
handler();
|
||||
},
|
||||
},
|
||||
handleTreeClick(e, node, handler) {
|
||||
if (node.depth !== 0) {
|
||||
this.setPathText(node.id);
|
||||
this.$refs['path-modal'].hide()
|
||||
|
||||
this.$emit("search");
|
||||
}
|
||||
|
||||
handler();
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#mimeTree {
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.form-control-fix-flex {
|
||||
flex: 1 1 auto;
|
||||
width: 1%;
|
||||
min-width: 0;
|
||||
margin-bottom: 0;
|
||||
flex: 1 1 auto;
|
||||
width: 1%;
|
||||
min-width: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.suggestion-line {
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.1;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.suggestions {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.theme-black .suggestions {
|
||||
color: black
|
||||
color: black
|
||||
}
|
||||
</style>
|
||||
@@ -1,40 +1,46 @@
|
||||
<template>
|
||||
<b-card v-if="lastResultsLoaded" id="results">
|
||||
<span>{{ hitCount }} {{ hitCount === 1 ? $t("hit") : $t("hits") }}</span>
|
||||
<b-card v-if="lastResultsLoaded" id="results">
|
||||
<span>{{ hitCount }} {{ hitCount === 1 ? $t("hit") : $t("hits") }}</span>
|
||||
|
||||
<div style="float: right">
|
||||
<b-button v-b-toggle.collapse-1 variant="primary" class="not-mobile" @click="onToggle()">{{
|
||||
$t("details")
|
||||
}}
|
||||
</b-button>
|
||||
<div style="float: right">
|
||||
<b-button v-b-toggle.collapse-1 variant="primary" class="not-mobile" @click="onToggle()">{{
|
||||
$t("details")
|
||||
}}
|
||||
</b-button>
|
||||
|
||||
<template v-if="hitCount !== 0">
|
||||
<SortSelect class="ml-2"></SortSelect>
|
||||
<template v-if="hitCount !== 0">
|
||||
<SortSelect class="ml-2"></SortSelect>
|
||||
|
||||
<DisplayModeToggle class="ml-2"></DisplayModeToggle>
|
||||
</template>
|
||||
</div>
|
||||
<DisplayModeToggle class="ml-2"></DisplayModeToggle>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<b-collapse id="collapse-1" class="pt-2" style="clear:both;">
|
||||
<b-card>
|
||||
<b-table :items="tableItems" small borderless bordered thead-class="hidden" class="mb-0"></b-table>
|
||||
<b-collapse id="collapse-1" class="pt-2" style="clear:both;">
|
||||
<b-card>
|
||||
<b-table :items="tableItems" small borderless thead-class="hidden" class="mb-0"></b-table>
|
||||
|
||||
<br/>
|
||||
<h4>
|
||||
{{$t("mimeTypes")}}
|
||||
<b-button size="sm" variant="primary" class="float-right" @click="onCopyClick"><ClipboardIcon/></b-button>
|
||||
</h4>
|
||||
<Preloader v-if="$store.state.uiDetailsMimeAgg == null"></Preloader>
|
||||
<b-table
|
||||
v-else
|
||||
sort-by="doc_count"
|
||||
:sort-desc="true"
|
||||
thead-class="hidden"
|
||||
:items="$store.state.uiDetailsMimeAgg" small bordered class="mb-0"
|
||||
></b-table>
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
<template v-if="!$store.state.uiSqliteMode">
|
||||
|
||||
<br/>
|
||||
<h4>
|
||||
{{ $t("mimeTypes") }}
|
||||
<b-button size="sm" variant="primary" class="float-right" @click="onCopyClick">
|
||||
<ClipboardIcon/>
|
||||
</b-button>
|
||||
</h4>
|
||||
<Preloader v-if="$store.state.uiDetailsMimeAgg == null"></Preloader>
|
||||
<b-table
|
||||
v-else
|
||||
sort-by="doc_count"
|
||||
:sort-desc="true"
|
||||
thead-class="hidden"
|
||||
bordered
|
||||
:items="$store.state.uiDetailsMimeAgg" small class="mb-0"
|
||||
></b-table>
|
||||
</template>
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -44,91 +50,96 @@ import {humanFileSize} from "@/util";
|
||||
import DisplayModeToggle from "@/components/DisplayModeToggle.vue";
|
||||
import SortSelect from "@/components/SortSelect.vue";
|
||||
import Preloader from "@/components/Preloader.vue";
|
||||
import Sist2Query from "@/Sist2Query";
|
||||
import Sist2Query from "@/Sist2ElasticsearchQuery";
|
||||
import ClipboardIcon from "@/components/icons/ClipboardIcon.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "ResultsCard",
|
||||
components: {ClipboardIcon, Preloader, SortSelect, DisplayModeToggle},
|
||||
created() {
|
||||
name: "ResultsCard",
|
||||
components: {ClipboardIcon, Preloader, SortSelect, DisplayModeToggle},
|
||||
created() {
|
||||
|
||||
},
|
||||
computed: {
|
||||
lastResultsLoaded() {
|
||||
return this.$store.state.lastQueryResults != null;
|
||||
},
|
||||
hitCount() {
|
||||
return (this.$store.state.lastQueryResults as EsResult).aggregations.total_count.value;
|
||||
computed: {
|
||||
lastResultsLoaded() {
|
||||
return this.$store.state.lastQueryResults != null;
|
||||
},
|
||||
hitCount() {
|
||||
return (this.$store.state.firstQueryResults as EsResult).aggregations.total_count.value;
|
||||
},
|
||||
tableItems() {
|
||||
const items = [];
|
||||
|
||||
if (!this.$store.state.uiSqliteMode) {
|
||||
items.push({key: this.$t("queryTime"), value: this.took()});
|
||||
}
|
||||
items.push({key: this.$t("totalSize"), value: this.totalSize()});
|
||||
|
||||
return items;
|
||||
}
|
||||
},
|
||||
tableItems() {
|
||||
const items = [];
|
||||
methods: {
|
||||
took() {
|
||||
return (this.$store.state.lastQueryResults as EsResult).took + "ms";
|
||||
},
|
||||
totalSize() {
|
||||
return humanFileSize((this.$store.state.firstQueryResults as EsResult).aggregations.total_size.value);
|
||||
},
|
||||
onToggle() {
|
||||
const show = !document.getElementById("collapse-1").classList.contains("show");
|
||||
this.$store.commit("setUiShowDetails", show);
|
||||
|
||||
if (this.$store.state.uiSqliteMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.push({key: this.$t("queryTime"), value: this.took()});
|
||||
items.push({key: this.$t("totalSize"), value: this.totalSize()});
|
||||
if (show && this.$store.state.uiDetailsMimeAgg == null && !this.$store.state.optUpdateMimeMap) {
|
||||
// Mime aggs are not updated automatically, update now
|
||||
this.forceUpdateMimeAgg();
|
||||
}
|
||||
},
|
||||
onCopyClick() {
|
||||
let tsvString = "";
|
||||
this.$store.state.uiDetailsMimeAgg.slice().sort((a, b) => b["doc_count"] - a["doc_count"]).forEach(row => {
|
||||
tsvString += `${row["key"]}\t${row["doc_count"]}\n`;
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
took() {
|
||||
return (this.$store.state.lastQueryResults as EsResult).took + "ms";
|
||||
navigator.clipboard.writeText(tsvString);
|
||||
|
||||
this.$bvToast.toast(
|
||||
this.$t("toast.copiedToClipboard"),
|
||||
{
|
||||
title: null,
|
||||
noAutoHide: false,
|
||||
toaster: "b-toaster-bottom-right",
|
||||
headerClass: "hidden",
|
||||
bodyClass: "toast-body-info",
|
||||
});
|
||||
},
|
||||
forceUpdateMimeAgg() {
|
||||
const query = Sist2Query.searchQuery();
|
||||
Sist2Api.getMimeTypes(query).then(({buckets}) => {
|
||||
this.$store.commit("setUiDetailsMimeAgg", buckets);
|
||||
});
|
||||
}
|
||||
},
|
||||
totalSize() {
|
||||
return humanFileSize((this.$store.state.lastQueryResults as EsResult).aggregations.total_size.value);
|
||||
},
|
||||
onToggle() {
|
||||
const show = !document.getElementById("collapse-1").classList.contains("show");
|
||||
this.$store.commit("setUiShowDetails", show);
|
||||
|
||||
if (show && this.$store.state.uiDetailsMimeAgg == null && !this.$store.state.optUpdateMimeMap) {
|
||||
// Mime aggs are not updated automatically, update now
|
||||
this.forceUpdateMimeAgg();
|
||||
}
|
||||
},
|
||||
onCopyClick() {
|
||||
let tsvString = "";
|
||||
this.$store.state.uiDetailsMimeAgg.slice().sort((a,b) => b["doc_count"] - a["doc_count"]).forEach(row => {
|
||||
tsvString += `${row["key"]}\t${row["doc_count"]}\n`;
|
||||
});
|
||||
|
||||
navigator.clipboard.writeText(tsvString);
|
||||
|
||||
this.$bvToast.toast(
|
||||
this.$t("toast.copiedToClipboard"),
|
||||
{
|
||||
title: null,
|
||||
noAutoHide: false,
|
||||
toaster: "b-toaster-bottom-right",
|
||||
headerClass: "hidden",
|
||||
bodyClass: "toast-body-info",
|
||||
});
|
||||
},
|
||||
forceUpdateMimeAgg() {
|
||||
const query = Sist2Query.searchQuery();
|
||||
Sist2Api.getMimeTypes(query).then(({buckets}) => {
|
||||
this.$store.commit("setUiDetailsMimeAgg", buckets);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#results {
|
||||
margin-top: 1em;
|
||||
margin-top: 1em;
|
||||
|
||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#results .card-body {
|
||||
padding: 0.7em 1.25em;
|
||||
padding: 0.7em 1.25em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
@input="setSearchText($event)"></b-form-input>
|
||||
|
||||
<template #prepend>
|
||||
<b-input-group-text>
|
||||
<b-input-group-text v-if="!$store.state.uiSqliteMode">
|
||||
<b-form-checkbox :checked="fuzzy" title="Toggle fuzzy searching" @change="setFuzzy($event)">
|
||||
{{ $t("searchBar.fuzzy") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
@@ -1,76 +1,78 @@
|
||||
<template>
|
||||
<div @mouseenter="showAddButton = true" @mouseleave="showAddButton = false">
|
||||
<div @mouseenter="showAddButton = true" @mouseleave="showAddButton = false">
|
||||
|
||||
<b-modal v-model="showModal" :title="$t('saveTagModalTitle')" hide-footer no-fade centered size="lg" static lazy>
|
||||
<b-row>
|
||||
<b-col style="flex-grow: 2" sm>
|
||||
<VueSimpleSuggest
|
||||
ref="suggest"
|
||||
:value="tagText"
|
||||
@select="setTagText($event)"
|
||||
@input="setTagText($event)"
|
||||
class="form-control-fix-flex"
|
||||
style="margin-top: 17px"
|
||||
:list="suggestTag"
|
||||
:max-suggestions="0"
|
||||
:placeholder="$t('saveTagPlaceholder')"
|
||||
>
|
||||
<!-- Suggestion item template-->
|
||||
<div slot="suggestion-item" slot-scope="{ suggestion, query}"
|
||||
>
|
||||
<div class="suggestion-line">
|
||||
<b-modal v-model="showModal" :title="$t('saveTagModalTitle')" hide-footer no-fade centered size="lg" static
|
||||
lazy>
|
||||
<b-row>
|
||||
<b-col style="flex-grow: 2" sm>
|
||||
<VueSimpleSuggest
|
||||
ref="suggest"
|
||||
:value="tagText"
|
||||
@select="setTagText($event)"
|
||||
@input="setTagText($event)"
|
||||
class="form-control-fix-flex"
|
||||
style="margin-top: 17px"
|
||||
:list="suggestTag"
|
||||
:max-suggestions="0"
|
||||
:placeholder="$t('saveTagPlaceholder')"
|
||||
>
|
||||
<!-- Suggestion item template-->
|
||||
<div slot="suggestion-item" slot-scope="{ suggestion, query}"
|
||||
>
|
||||
<div class="suggestion-line">
|
||||
<span
|
||||
class="badge badge-suggestion"
|
||||
:style="{background: getBg(suggestion), color: getFg(suggestion)}"
|
||||
class="badge badge-suggestion"
|
||||
:style="{background: getBg(suggestion), color: getFg(suggestion)}"
|
||||
>
|
||||
<strong>{{ query }}</strong>{{ getSuggestionWithoutQueryPrefix(suggestion, query) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VueSimpleSuggest>
|
||||
</b-col>
|
||||
<b-col class="mt-4">
|
||||
<TwitterColorPicker v-model="color" triangle="hide" :width="252" class="mr-auto ml-auto"></TwitterColorPicker>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
</VueSimpleSuggest>
|
||||
</b-col>
|
||||
<b-col class="mt-4">
|
||||
<TwitterColorPicker v-model="color" triangle="hide" :width="252"
|
||||
class="mr-auto ml-auto"></TwitterColorPicker>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-button variant="primary" style="float: right" class="mt-2" @click="saveTag()">{{ $t("confirm") }}
|
||||
</b-button>
|
||||
</b-modal>
|
||||
<b-button variant="primary" style="float: right" class="mt-2" @click="saveTag()">{{ $t("confirm") }}
|
||||
</b-button>
|
||||
</b-modal>
|
||||
|
||||
|
||||
<template v-for="tag in hit._tags">
|
||||
<!-- User tag-->
|
||||
<div v-if="tag.userTag" :key="tag.rawText" style="display: inline-block">
|
||||
<template v-for="tag in hit._tags">
|
||||
<!-- User tag-->
|
||||
<div v-if="tag.userTag" :key="tag.rawText" style="display: inline-block">
|
||||
<span
|
||||
:id="hit._id+tag.rawText"
|
||||
:title="tag.text"
|
||||
tabindex="-1"
|
||||
class="badge pointer"
|
||||
:style="badgeStyle(tag)" :class="badgeClass(tag)"
|
||||
@click.right="onTagRightClick(tag, $event)"
|
||||
:id="hit._id+tag.rawText"
|
||||
:title="tag.text"
|
||||
tabindex="-1"
|
||||
class="badge pointer"
|
||||
:style="badgeStyle(tag)" :class="badgeClass(tag)"
|
||||
@click.right="onTagRightClick(tag, $event)"
|
||||
>{{ tag.text.split(".").pop() }}</span>
|
||||
|
||||
<b-popover :target="hit._id+tag.rawText" triggers="focus blur" placement="top">
|
||||
<b-button variant="danger" @click="onTagDeleteClick(tag, $event)">{{ $t("deleteTag") }}</b-button>
|
||||
</b-popover>
|
||||
</div>
|
||||
<b-popover :target="hit._id+tag.rawText" triggers="focus blur" placement="top">
|
||||
<b-button variant="danger" @click="onTagDeleteClick(tag, $event)">{{ $t("deleteTag") }}</b-button>
|
||||
</b-popover>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-else :key="tag.text"
|
||||
class="badge"
|
||||
:style="badgeStyle(tag)" :class="badgeClass(tag)"
|
||||
>{{ tag.text.split(".").pop() }}</span>
|
||||
</template>
|
||||
<span
|
||||
v-else :key="tag.text"
|
||||
class="badge"
|
||||
:style="badgeStyle(tag)" :class="badgeClass(tag)"
|
||||
>{{ tag.text.split(".").pop() }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Add button -->
|
||||
<small v-if="showAddButton" class="badge add-tag-button" @click="tagAdd()">{{$t("addTag")}}</small>
|
||||
<!-- Add button -->
|
||||
<small v-if="showAddButton" class="badge add-tag-button" @click="tagAdd()">{{ $t("addTag") }}</small>
|
||||
|
||||
<!-- Size tag-->
|
||||
<small v-else class="text-muted badge-size" style="padding-left: 2px">{{
|
||||
humanFileSize(hit._source.size)
|
||||
}}</small>
|
||||
</div>
|
||||
<!-- Size tag-->
|
||||
<small v-else class="text-muted badge-size" style="padding-left: 2px">{{
|
||||
humanFileSize(hit._source.size)
|
||||
}}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -81,170 +83,136 @@ import Sist2Api from "@/Sist2Api";
|
||||
import VueSimpleSuggest from 'vue-simple-suggest'
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
"TwitterColorPicker": Twitter,
|
||||
VueSimpleSuggest
|
||||
},
|
||||
props: ["hit"],
|
||||
data() {
|
||||
return {
|
||||
showAddButton: false,
|
||||
showModal: false,
|
||||
tagText: null,
|
||||
color: {
|
||||
hex: "#e0e0e0",
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tagHover() {
|
||||
return this.$store.getters["uiTagHover"];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
humanFileSize: humanFileSize,
|
||||
getSuggestionWithoutQueryPrefix(suggestion, query) {
|
||||
return suggestion.id.slice(query.length, -8)
|
||||
components: {
|
||||
"TwitterColorPicker": Twitter,
|
||||
VueSimpleSuggest
|
||||
},
|
||||
getBg(suggestion) {
|
||||
return suggestion.id.slice(-7);
|
||||
},
|
||||
getFg(suggestion) {
|
||||
return lum(suggestion.id.slice(-7)) > 50 ? "#000" : "#fff";
|
||||
},
|
||||
setTagText(value) {
|
||||
this.$refs.suggest.clearSuggestions();
|
||||
|
||||
if (typeof value === "string") {
|
||||
this.tagText = {
|
||||
id: value,
|
||||
title: value
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
this.color = {
|
||||
hex: "#" + value.id.split("#")[1]
|
||||
}
|
||||
|
||||
this.tagText = value;
|
||||
},
|
||||
badgeClass(tag) {
|
||||
return `badge-${tag.style}`;
|
||||
},
|
||||
badgeStyle(tag) {
|
||||
return {
|
||||
background: tag.bg,
|
||||
color: tag.fg,
|
||||
};
|
||||
},
|
||||
onTagHover(tag) {
|
||||
if (tag.userTag) {
|
||||
this.$store.commit("setUiTagHover", tag);
|
||||
}
|
||||
},
|
||||
onTagLeave() {
|
||||
this.$store.commit("setUiTagHover", null);
|
||||
},
|
||||
onTagDeleteClick(tag, e) {
|
||||
this.hit._tags = this.hit._tags.filter(t => t !== tag);
|
||||
|
||||
Sist2Api.deleteTag(tag.rawText, this.hit).then(() => {
|
||||
//toast
|
||||
this.$store.commit("busUpdateWallItems");
|
||||
this.$store.commit("busUpdateTags");
|
||||
});
|
||||
},
|
||||
tagAdd() {
|
||||
this.showModal = true;
|
||||
},
|
||||
saveTag() {
|
||||
if (this.tagText.id.includes("#")) {
|
||||
this.$bvToast.toast(
|
||||
this.$t("toast.invalidTag"),
|
||||
{
|
||||
title: this.$t("toast.invalidTagTitle"),
|
||||
noAutoHide: true,
|
||||
toaster: "b-toaster-bottom-right",
|
||||
headerClass: "toast-header-error",
|
||||
bodyClass: "toast-body-error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let tag = this.tagText.id + this.color.hex.replace("#", ".#");
|
||||
const userTags = this.hit._tags.filter(t => t.userTag);
|
||||
|
||||
if (userTags.find(t => t.rawText === tag) != null) {
|
||||
this.$bvToast.toast(
|
||||
this.$t("toast.dupeTag"),
|
||||
{
|
||||
title: this.$t("toast.dupeTagTitle"),
|
||||
noAutoHide: true,
|
||||
toaster: "b-toaster-bottom-right",
|
||||
headerClass: "toast-header-error",
|
||||
bodyClass: "toast-body-error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.hit._tags.push(Sist2Api.createUserTag(tag));
|
||||
|
||||
Sist2Api.saveTag(tag, this.hit).then(() => {
|
||||
this.tagText = null;
|
||||
this.showModal = false;
|
||||
this.$store.commit("busUpdateWallItems");
|
||||
this.$store.commit("busUpdateTags");
|
||||
// TODO: toast
|
||||
});
|
||||
},
|
||||
async suggestTag(term) {
|
||||
term = term.toLowerCase();
|
||||
|
||||
const choices = await this.getTagChoices(term);
|
||||
|
||||
let matches = [];
|
||||
for (let i = 0; i < choices.length; i++) {
|
||||
if (~choices[i].toLowerCase().indexOf(term)) {
|
||||
matches.push(choices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return matches.sort().map(match => {
|
||||
props: ["hit"],
|
||||
data() {
|
||||
return {
|
||||
title: match.split(".").slice(0, -1).join("."),
|
||||
id: match
|
||||
showAddButton: false,
|
||||
showModal: false,
|
||||
tagText: null,
|
||||
color: {
|
||||
hex: "#e0e0e0",
|
||||
},
|
||||
}
|
||||
});
|
||||
},
|
||||
getTagChoices(prefix) {
|
||||
return new Promise(getPaths => {
|
||||
Sist2Api.esQuery({
|
||||
suggest: {
|
||||
tag: {
|
||||
prefix: prefix,
|
||||
completion: {
|
||||
field: "suggest-tag",
|
||||
skip_duplicates: true,
|
||||
size: 10000
|
||||
}
|
||||
methods: {
|
||||
humanFileSize: humanFileSize,
|
||||
getSuggestionWithoutQueryPrefix(suggestion, query) {
|
||||
return suggestion.id.slice(query.length, -8)
|
||||
},
|
||||
getBg(suggestion) {
|
||||
return suggestion.id.slice(-7);
|
||||
},
|
||||
getFg(suggestion) {
|
||||
return lum(suggestion.id.slice(-7)) > 50 ? "#000" : "#fff";
|
||||
},
|
||||
setTagText(value) {
|
||||
this.$refs.suggest.clearSuggestions();
|
||||
|
||||
if (typeof value === "string") {
|
||||
this.tagText = {
|
||||
id: value,
|
||||
title: value
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}).then(resp => {
|
||||
const result = [];
|
||||
resp["suggest"]["tag"][0]["options"].map(opt => opt["_source"]["tag"]).forEach(tags => {
|
||||
tags.forEach(tag => {
|
||||
const t = tag.slice(0, -8);
|
||||
if (!result.find(x => x.slice(0, -8) === t)) {
|
||||
result.push(tag);
|
||||
}
|
||||
|
||||
this.color = {
|
||||
hex: "#" + value.id.split("#")[1]
|
||||
}
|
||||
|
||||
this.tagText = value;
|
||||
},
|
||||
badgeClass(tag) {
|
||||
return `badge-${tag.style}`;
|
||||
},
|
||||
badgeStyle(tag) {
|
||||
return {
|
||||
background: tag.bg,
|
||||
color: tag.fg,
|
||||
};
|
||||
},
|
||||
onTagDeleteClick(tag, e) {
|
||||
this.hit._tags = this.hit._tags.filter(t => t !== tag);
|
||||
|
||||
Sist2Api.deleteTag(tag.rawText, this.hit).then(() => {
|
||||
//toast
|
||||
this.$store.commit("busUpdateWallItems");
|
||||
this.$store.commit("busUpdateTags");
|
||||
});
|
||||
});
|
||||
getPaths(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
tagAdd() {
|
||||
this.showModal = true;
|
||||
},
|
||||
saveTag() {
|
||||
if (this.tagText.id.includes("#")) {
|
||||
this.$bvToast.toast(
|
||||
this.$t("toast.invalidTag"),
|
||||
{
|
||||
title: this.$t("toast.invalidTagTitle"),
|
||||
noAutoHide: true,
|
||||
toaster: "b-toaster-bottom-right",
|
||||
headerClass: "toast-header-error",
|
||||
bodyClass: "toast-body-error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let tag = this.tagText.id + this.color.hex.replace("#", ".#");
|
||||
const userTags = this.hit._tags.filter(t => t.userTag);
|
||||
|
||||
if (userTags.find(t => t.rawText === tag) != null) {
|
||||
this.$bvToast.toast(
|
||||
this.$t("toast.dupeTag"),
|
||||
{
|
||||
title: this.$t("toast.dupeTagTitle"),
|
||||
noAutoHide: true,
|
||||
toaster: "b-toaster-bottom-right",
|
||||
headerClass: "toast-header-error",
|
||||
bodyClass: "toast-body-error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.hit._tags.push(Sist2Api.createUserTag(tag));
|
||||
|
||||
Sist2Api.saveTag(tag, this.hit).then(() => {
|
||||
this.tagText = null;
|
||||
this.showModal = false;
|
||||
this.$store.commit("busUpdateWallItems");
|
||||
this.$store.commit("busUpdateTags");
|
||||
// TODO: toast
|
||||
});
|
||||
},
|
||||
async suggestTag(term) {
|
||||
term = term.toLowerCase();
|
||||
|
||||
const choices = await this.getTagChoices(term);
|
||||
|
||||
let matches = [];
|
||||
for (let i = 0; i < choices.length; i++) {
|
||||
if (~choices[i].toLowerCase().indexOf(term)) {
|
||||
matches.push(choices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return matches.sort().map(match => {
|
||||
return {
|
||||
title: match.split(".").slice(0, -1).join("."),
|
||||
id: match
|
||||
}
|
||||
});
|
||||
},
|
||||
getTagChoices(prefix) {
|
||||
return new Promise(getPaths => {
|
||||
Sist2Api.getTagSuggestions(prefix)
|
||||
.then(paths => getPaths(paths))
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -252,87 +220,87 @@ export default Vue.extend({
|
||||
|
||||
|
||||
.badge-video {
|
||||
color: #FFFFFF;
|
||||
background-color: #F27761;
|
||||
color: #FFFFFF;
|
||||
background-color: #F27761;
|
||||
}
|
||||
|
||||
.badge-image {
|
||||
color: #FFFFFF;
|
||||
background-color: #AA99C9;
|
||||
color: #FFFFFF;
|
||||
background-color: #AA99C9;
|
||||
}
|
||||
|
||||
.badge-audio {
|
||||
color: #FFFFFF;
|
||||
background-color: #00ADEF;
|
||||
color: #FFFFFF;
|
||||
background-color: #00ADEF;
|
||||
}
|
||||
|
||||
.badge-user {
|
||||
color: #212529;
|
||||
background-color: #e0e0e0;
|
||||
color: #212529;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.badge-user:hover, .add-tag-button:hover {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
color: #FFFFFF;
|
||||
background-color: #FAAB3C;
|
||||
color: #FFFFFF;
|
||||
background-color: #FAAB3C;
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin-right: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.badge-delete {
|
||||
margin-right: -2px;
|
||||
margin-left: 2px;
|
||||
margin-top: -1px;
|
||||
font-family: monospace;
|
||||
font-size: 90%;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 0.1em 0.4em;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
margin-right: -2px;
|
||||
margin-left: 2px;
|
||||
margin-top: -1px;
|
||||
font-family: monospace;
|
||||
font-size: 90%;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 0.1em 0.4em;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.badge-size {
|
||||
width: 50px;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.add-tag-button {
|
||||
cursor: pointer;
|
||||
color: #212529;
|
||||
background-color: #e0e0e0;
|
||||
width: 50px;
|
||||
cursor: pointer;
|
||||
color: #212529;
|
||||
background-color: #e0e0e0;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.badge-suggestion {
|
||||
font-size: 90%;
|
||||
font-weight: normal;
|
||||
font-size: 90%;
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.vc-twitter-body {
|
||||
padding: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.vc-twitter {
|
||||
box-shadow: none !important;
|
||||
background: none !important;
|
||||
box-shadow: none !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
border: none;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-input-group v-if="showSearchBar" id="tag-picker-filter-bar">
|
||||
<b-form-input :value="filter"
|
||||
:placeholder="$t('tagFilter')"
|
||||
@input="onFilter($event)"></b-form-input>
|
||||
</b-input-group>
|
||||
<div>
|
||||
<b-input-group v-if="showSearchBar" id="tag-picker-filter-bar">
|
||||
<b-form-input :value="filter"
|
||||
:placeholder="$t('tagFilter')"
|
||||
@input="onFilter($event)"></b-form-input>
|
||||
</b-input-group>
|
||||
|
||||
<div id="tagTree"></div>
|
||||
</div>
|
||||
<div id="tagTree"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -19,191 +19,195 @@ import {getSelectedTreeNodes} from "@/util";
|
||||
import Sist2Api from "@/Sist2Api";
|
||||
|
||||
function resetState(node) {
|
||||
node._tree.defaultState.forEach(function (val, prop) {
|
||||
node.state(prop, val);
|
||||
});
|
||||
node._tree.defaultState.forEach(function (val, prop) {
|
||||
node.state(prop, val);
|
||||
});
|
||||
|
||||
return node;
|
||||
return node;
|
||||
}
|
||||
|
||||
function baseStateChange(prop, value, verb, node, deep) {
|
||||
if (node.state(prop) !== value) {
|
||||
node._tree.batch();
|
||||
if (node.state(prop) !== value) {
|
||||
node._tree.batch();
|
||||
|
||||
if (node._tree.config.nodes.resetStateOnRestore && verb === 'restored') {
|
||||
resetState(node);
|
||||
if (node._tree.config.nodes.resetStateOnRestore && verb === 'restored') {
|
||||
resetState(node);
|
||||
}
|
||||
|
||||
node.state(prop, value);
|
||||
|
||||
node._tree.emit('node.' + verb, node, false);
|
||||
|
||||
if (deep && node.hasChildren()) {
|
||||
node.children.recurseDown(function (child) {
|
||||
baseStateChange(prop, value, verb, child);
|
||||
});
|
||||
}
|
||||
|
||||
node.markDirty();
|
||||
node._tree.end();
|
||||
}
|
||||
|
||||
node.state(prop, value);
|
||||
|
||||
node._tree.emit('node.' + verb, node, false);
|
||||
|
||||
if (deep && node.hasChildren()) {
|
||||
node.children.recurseDown(function (child) {
|
||||
baseStateChange(prop, value, verb, child);
|
||||
});
|
||||
}
|
||||
|
||||
node.markDirty();
|
||||
node._tree.end();
|
||||
}
|
||||
|
||||
return node;
|
||||
return node;
|
||||
}
|
||||
|
||||
function addTag(map, tag, id, count) {
|
||||
const tags = tag.split(".");
|
||||
const tags = tag.split(".");
|
||||
|
||||
const child = {
|
||||
id: id,
|
||||
count: count,
|
||||
text: tags.length !== 1 ? tags[0] : `${tags[0]} (${count})`,
|
||||
name: tags[0],
|
||||
children: [],
|
||||
// Overwrite base functions
|
||||
blur: function () {
|
||||
// noop
|
||||
},
|
||||
select: function () {
|
||||
this.state("selected", true);
|
||||
return this.check()
|
||||
},
|
||||
deselect: function () {
|
||||
this.state("selected", false);
|
||||
return this.uncheck()
|
||||
},
|
||||
uncheck: function () {
|
||||
baseStateChange('checked', false, 'unchecked', this, false);
|
||||
this.state('indeterminate', false);
|
||||
const child = {
|
||||
id: id,
|
||||
count: count,
|
||||
text: tags.length !== 1 ? tags[0] : `${tags[0]} (${count})`,
|
||||
name: tags[0],
|
||||
children: [],
|
||||
// Overwrite base functions
|
||||
blur: function () {
|
||||
// noop
|
||||
},
|
||||
select: function () {
|
||||
this.state("selected", true);
|
||||
return this.check()
|
||||
},
|
||||
deselect: function () {
|
||||
this.state("selected", false);
|
||||
return this.uncheck()
|
||||
},
|
||||
uncheck: function () {
|
||||
baseStateChange('checked', false, 'unchecked', this, false);
|
||||
this.state('indeterminate', false);
|
||||
|
||||
if (this.hasParent()) {
|
||||
this.getParent().refreshIndeterminateState();
|
||||
}
|
||||
if (this.hasParent()) {
|
||||
this.getParent().refreshIndeterminateState();
|
||||
}
|
||||
|
||||
this._tree.end();
|
||||
return this;
|
||||
},
|
||||
check: function () {
|
||||
baseStateChange('checked', true, 'checked', this, false);
|
||||
this._tree.end();
|
||||
return this;
|
||||
},
|
||||
check: function () {
|
||||
baseStateChange('checked', true, 'checked', this, false);
|
||||
|
||||
if (this.hasParent()) {
|
||||
this.getParent().refreshIndeterminateState();
|
||||
}
|
||||
if (this.hasParent()) {
|
||||
this.getParent().refreshIndeterminateState();
|
||||
}
|
||||
|
||||
this._tree.end();
|
||||
return this;
|
||||
this._tree.end();
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
let found = false;
|
||||
map.forEach(node => {
|
||||
if (node.name === child.name) {
|
||||
found = true;
|
||||
if (tags.length !== 1) {
|
||||
addTag(node.children, tags.slice(1).join("."), id, count);
|
||||
} else {
|
||||
// Same name, different color
|
||||
console.error("FIXME: Duplicate tag?")
|
||||
console.trace(node)
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!found) {
|
||||
if (tags.length !== 1) {
|
||||
addTag(child.children, tags.slice(1).join("."), id, count);
|
||||
map.push(child);
|
||||
} else {
|
||||
map.push(child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let found = false;
|
||||
map.forEach(node => {
|
||||
if (node.name === child.name) {
|
||||
found = true;
|
||||
if (tags.length !== 1) {
|
||||
addTag(node.children, tags.slice(1).join("."), id, count);
|
||||
} else {
|
||||
// Same name, different color
|
||||
console.error("FIXME: Duplicate tag?")
|
||||
console.trace(node)
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!found) {
|
||||
if (tags.length !== 1) {
|
||||
addTag(child.children, tags.slice(1).join("."), id, count);
|
||||
map.push(child);
|
||||
} else {
|
||||
map.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "TagPicker",
|
||||
props: ["showSearchBar"],
|
||||
data() {
|
||||
return {
|
||||
tagTree: null,
|
||||
loadedFromArgs: false,
|
||||
filter: ""
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$store.subscribe((mutation) => {
|
||||
if (mutation.type === "setUiMimeMap" && this.tagTree === null) {
|
||||
this.initializeTree();
|
||||
this.updateTree();
|
||||
} else if (mutation.type === "busUpdateTags") {
|
||||
window.setTimeout(this.updateTree, 2000);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onFilter(value) {
|
||||
this.filter = value;
|
||||
this.tagTree.search(value);
|
||||
},
|
||||
initializeTree() {
|
||||
const tagMap = [];
|
||||
this.tagTree = new InspireTree({
|
||||
selection: {
|
||||
mode: "checkbox",
|
||||
autoDeselect: false,
|
||||
},
|
||||
checkbox: {
|
||||
autoCheckChildren: false,
|
||||
},
|
||||
data: tagMap
|
||||
});
|
||||
new InspireTreeDOM(this.tagTree, {
|
||||
target: '#tagTree'
|
||||
});
|
||||
this.tagTree.on("node.state.changed", this.handleTreeClick);
|
||||
},
|
||||
updateTree() {
|
||||
// TODO: remember which tags are selected and restore?
|
||||
const tagMap = [];
|
||||
Sist2Api.getTags().then(tags => {
|
||||
tags.forEach(tag => addTag(tagMap, tag.id, tag.id, tag.count));
|
||||
this.tagTree.removeAll();
|
||||
this.tagTree.addNodes(tagMap);
|
||||
|
||||
if (this.$store.state._onLoadSelectedTags.length > 0 && !this.loadedFromArgs) {
|
||||
this.$store.state._onLoadSelectedTags.forEach(mime => {
|
||||
this.tagTree.node(mime).select();
|
||||
this.loadedFromArgs = true;
|
||||
});
|
||||
name: "TagPicker",
|
||||
props: ["showSearchBar"],
|
||||
data() {
|
||||
return {
|
||||
tagTree: null,
|
||||
loadedFromArgs: false,
|
||||
filter: ""
|
||||
}
|
||||
});
|
||||
},
|
||||
handleTreeClick(node, e) {
|
||||
if (e === "indeterminate" || e === "collapsed" || e === 'rendered' || e === "focused"
|
||||
|| e === "matched" || e === "hidden") {
|
||||
return;
|
||||
}
|
||||
mounted() {
|
||||
this.$store.subscribe((mutation) => {
|
||||
if (mutation.type === "setUiMimeMap" && this.tagTree === null) {
|
||||
this.initializeTree();
|
||||
this.updateTree();
|
||||
} else if (mutation.type === "busUpdateTags") {
|
||||
if (this.$store.state.uiSqliteMode) {
|
||||
this.updateTree();
|
||||
} else {
|
||||
window.setTimeout(this.updateTree, 2000);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onFilter(value) {
|
||||
this.filter = value;
|
||||
this.tagTree.search(value);
|
||||
},
|
||||
initializeTree() {
|
||||
const tagMap = [];
|
||||
this.tagTree = new InspireTree({
|
||||
selection: {
|
||||
mode: "checkbox",
|
||||
autoDeselect: false,
|
||||
},
|
||||
checkbox: {
|
||||
autoCheckChildren: false,
|
||||
},
|
||||
data: tagMap
|
||||
});
|
||||
new InspireTreeDOM(this.tagTree, {
|
||||
target: '#tagTree'
|
||||
});
|
||||
this.tagTree.on("node.state.changed", this.handleTreeClick);
|
||||
},
|
||||
updateTree() {
|
||||
// TODO: remember which tags are selected and restore?
|
||||
const tagMap = [];
|
||||
Sist2Api.getTags().then(tags => {
|
||||
tags.forEach(tag => addTag(tagMap, tag.id, tag.id, tag.count));
|
||||
this.tagTree.removeAll();
|
||||
this.tagTree.addNodes(tagMap);
|
||||
|
||||
this.$store.commit("setSelectedTags", getSelectedTreeNodes(this.tagTree));
|
||||
},
|
||||
}
|
||||
if (this.$store.state._onLoadSelectedTags.length > 0 && !this.loadedFromArgs) {
|
||||
this.$store.state._onLoadSelectedTags.forEach(mime => {
|
||||
this.tagTree.node(mime).select();
|
||||
this.loadedFromArgs = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
handleTreeClick(node, e) {
|
||||
if (e === "indeterminate" || e === "collapsed" || e === 'rendered' || e === "focused"
|
||||
|| e === "matched" || e === "hidden") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit("setSelectedTags", getSelectedTreeNodes(this.tagTree));
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#mimeTree {
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.inspire-tree .focused > .wholerow {
|
||||
border: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#tag-picker-filter-bar {
|
||||
padding: 10px 4px 4px;
|
||||
padding: 10px 4px 4px;
|
||||
}
|
||||
|
||||
.theme-black .inspire-tree .matched > .wholerow {
|
||||
background: rgba(251, 191, 41, 0.25);
|
||||
background: rgba(251, 191, 41, 0.25);
|
||||
}
|
||||
</style>
|
||||
@@ -57,7 +57,7 @@ export default {
|
||||
fuzzy: "Set fuzzy search by default",
|
||||
searchInPath: "Enable matching query against document path",
|
||||
suggestPath: "Enable auto-complete in path filter bar",
|
||||
fragmentSize: "Highlight context size in characters",
|
||||
fragmentSize: "Highlight context size",
|
||||
queryMode: "Search mode",
|
||||
displayMode: "Display",
|
||||
columns: "Column count",
|
||||
@@ -239,7 +239,7 @@ export default {
|
||||
fuzzy: "Aktiviere Fuzzy-Suche standardmäßig",
|
||||
searchInPath: "Abgleich der Abfrage mit dem Dokumentpfad aktivieren",
|
||||
suggestPath: "Aktiviere Auto-Vervollständigung in Pfadfilter-Leiste",
|
||||
fragmentSize: "Kontextgröße in Zeichen hervorheben",
|
||||
fragmentSize: "Kontextgröße",
|
||||
queryMode: "Such-Modus",
|
||||
displayMode: "Ansicht",
|
||||
columns: "Anzahl Spalten",
|
||||
@@ -413,7 +413,7 @@ export default {
|
||||
fuzzy: "Activer la recherche approximative par défaut",
|
||||
searchInPath: "Activer la recherche dans le chemin des documents",
|
||||
suggestPath: "Activer l'autocomplétion dans la barre de filtre de chemin",
|
||||
fragmentSize: "Longueur du contexte de surlignage, en nombre de caractères",
|
||||
fragmentSize: "Longueur du contexte de surlignage",
|
||||
queryMode: "Mode de recherche",
|
||||
displayMode: "Affichage",
|
||||
columns: "Nombre de colonnes",
|
||||
|
||||
@@ -69,11 +69,12 @@ export default new Vuex.Store({
|
||||
selectedTags: [] as string[],
|
||||
|
||||
lastQueryResults: null,
|
||||
firstQueryResults: null,
|
||||
|
||||
keySequence: 0,
|
||||
querySequence: 0,
|
||||
|
||||
uiTagHover: null as Tag | null,
|
||||
uiSqliteMode: false,
|
||||
uiLightboxIsOpen: false,
|
||||
uiShowLightbox: false,
|
||||
uiLightboxSources: [] as string[],
|
||||
@@ -130,13 +131,13 @@ export default new Vuex.Store({
|
||||
setSearchText: (state, val) => state.searchText = val,
|
||||
setFuzzy: (state, val) => state.fuzzy = val,
|
||||
setLastQueryResult: (state, val) => state.lastQueryResults = val,
|
||||
setFirstQueryResult: (state, val) => state.firstQueryResults = val,
|
||||
_setOnLoadSelectedIndices: (state, val) => state._onLoadSelectedIndices = val,
|
||||
_setOnLoadSelectedMimeTypes: (state, val) => state._onLoadSelectedMimeTypes = val,
|
||||
_setOnLoadSelectedTags: (state, val) => state._onLoadSelectedTags = val,
|
||||
setSelectedIndices: (state, val) => state.selectedIndices = val,
|
||||
setSelectedMimeTypes: (state, val) => state.selectedMimeTypes = val,
|
||||
setSelectedTags: (state, val) => state.selectedTags = val,
|
||||
setUiTagHover: (state, val: Tag | null) => state.uiTagHover = val,
|
||||
setUiLightboxIsOpen: (state, val: boolean) => state.uiLightboxIsOpen = val,
|
||||
_setUiShowLightbox: (state, val: boolean) => state.uiShowLightbox = val,
|
||||
setUiLightboxKey: (state, val: number) => state.uiLightboxKey = val,
|
||||
@@ -154,6 +155,7 @@ export default new Vuex.Store({
|
||||
setUiLightboxThumbs: (state, val) => state.uiLightboxThumbs = val,
|
||||
setUiLightboxTypes: (state, val) => state.uiLightboxTypes = val,
|
||||
setUiLightboxCaptions: (state, val) => state.uiLightboxCaptions = val,
|
||||
setUiSqliteMode: (state, val) => state.uiSqliteMode = val,
|
||||
|
||||
setOptTheme: (state, val) => state.optTheme = val,
|
||||
setOptDisplay: (state, val) => state.optDisplay = val,
|
||||
@@ -179,9 +181,15 @@ export default new Vuex.Store({
|
||||
setOptVidPreviewInterval: (state, val) => state.optVidPreviewInterval = val,
|
||||
setOptSimpleLightbox: (state, val) => state.optSimpleLightbox = val,
|
||||
setOptShowTagPickerFilter: (state, val) => state.optShowTagPickerFilter = val,
|
||||
setOptAutoAnalyze: (state, val) => {state.optAutoAnalyze = val},
|
||||
setOptMlRepositories: (state, val) => {state.optMlRepositories = val},
|
||||
setOptMlDefaultModel: (state, val) => {state.optMlDefaultModel = val},
|
||||
setOptAutoAnalyze: (state, val) => {
|
||||
state.optAutoAnalyze = val
|
||||
},
|
||||
setOptMlRepositories: (state, val) => {
|
||||
state.optMlRepositories = val
|
||||
},
|
||||
setOptMlDefaultModel: (state, val) => {
|
||||
state.optMlDefaultModel = val
|
||||
},
|
||||
|
||||
setOptLightboxLoadOnlyCurrent: (state, val) => state.optLightboxLoadOnlyCurrent = val,
|
||||
setOptLightboxSlideDuration: (state, val) => state.optLightboxSlideDuration = val,
|
||||
@@ -340,6 +348,7 @@ export default new Vuex.Store({
|
||||
commit("_setUiShowLightbox", !state.uiShowLightbox);
|
||||
},
|
||||
clearResults({commit}) {
|
||||
commit("setFirstQueryResult", null);
|
||||
commit("setLastQueryResult", null);
|
||||
commit("_setKeySequence", 0);
|
||||
commit("_setUiShowLightbox", false);
|
||||
@@ -392,7 +401,6 @@ export default new Vuex.Store({
|
||||
|
||||
return (state.lastQueryResults as unknown as EsResult).hits.hits.slice(-1)[0];
|
||||
},
|
||||
uiTagHover: state => state.uiTagHover,
|
||||
uiShowLightbox: state => state.uiShowLightbox,
|
||||
uiLightboxSources: state => state.uiLightboxSources,
|
||||
uiLightboxThumbs: state => state.uiLightboxThumbs,
|
||||
@@ -400,6 +408,7 @@ export default new Vuex.Store({
|
||||
uiLightboxTypes: state => state.uiLightboxTypes,
|
||||
uiLightboxKey: state => state.uiLightboxKey,
|
||||
uiLightboxSlide: state => state.uiLightboxSlide,
|
||||
uiSqliteMode: state => state.uiSqliteMode,
|
||||
|
||||
optHideDuplicates: state => state.optHideDuplicates,
|
||||
optLang: state => state.optLang,
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
{{ $t("opt.lightboxLoadOnlyCurrent") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox :checked="optHideLegacy" @input="setOptHideLegacy">
|
||||
<b-form-checkbox :disabled="uiSqliteMode" :checked="optHideLegacy" @input="setOptHideLegacy">
|
||||
{{ $t("opt.hideLegacy") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox :checked="optUpdateMimeMap" @input="setOptUpdateMimeMap">
|
||||
<b-form-checkbox :disabled="uiSqliteMode" :checked="optUpdateMimeMap" @input="setOptUpdateMimeMap">
|
||||
{{ $t("opt.updateMimeMap") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
@@ -132,8 +132,11 @@
|
||||
$t("opt.tagOrOperator")
|
||||
}}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox :checked="optFuzzy" @input="setOptFuzzy">{{ $t("opt.fuzzy") }}</b-form-checkbox>
|
||||
<b-form-checkbox :checked="optSearchInPath" @input="setOptSearchInPath">{{
|
||||
<b-form-checkbox :disabled="uiSqliteMode" :checked="optFuzzy" @input="setOptFuzzy">
|
||||
{{ $t("opt.fuzzy") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox :disabled="uiSqliteMode" :checked="optSearchInPath" @input="setOptSearchInPath">{{
|
||||
$t("opt.searchInPath")
|
||||
}}
|
||||
</b-form-checkbox>
|
||||
@@ -151,8 +154,8 @@
|
||||
<b-form-input :value="optResultSize" type="number" min="10"
|
||||
@input="setOptResultSize"></b-form-input>
|
||||
|
||||
<label>{{ $t("opt.queryMode") }}</label>
|
||||
<b-form-select :options="queryModeOptions" :value="optQueryMode"
|
||||
<label :class="{'text-muted': uiSqliteMode}">{{ $t("opt.queryMode") }}</label>
|
||||
<b-form-select :disabled="uiSqliteMode" :options="queryModeOptions" :value="optQueryMode"
|
||||
@input="setOptQueryMode"></b-form-select>
|
||||
|
||||
<label>{{ $t("opt.slideDuration") }}</label>
|
||||
@@ -170,7 +173,7 @@
|
||||
<b-textarea rows="3" :value="optMlRepositories" @input="setOptMlRepositories"></b-textarea>
|
||||
<br>
|
||||
<b-form-checkbox :checked="optAutoAnalyze" @input="setOptAutoAnalyze">{{
|
||||
$t("opt.autoAnalyze")
|
||||
$t("opt.autoAnalyze")
|
||||
}}
|
||||
</b-form-checkbox>
|
||||
</b-card>
|
||||
@@ -300,6 +303,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
"uiSqliteMode",
|
||||
"optTheme",
|
||||
"optDisplay",
|
||||
"optColumns",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<!-- Audio player-->
|
||||
<audio v-if="doc._props.isAudio" ref="audio" preload="none" class="audio-fit fit" controls
|
||||
:type="doc._source.mime"
|
||||
:src="`f/${doc._id}`"></audio>
|
||||
:src="`f/${doc._source.index}/${doc._id}`"></audio>
|
||||
|
||||
<InfoTable :doc="doc" v-if="doc"></InfoTable>
|
||||
|
||||
@@ -54,7 +54,7 @@ export default Vue.extend({
|
||||
methods: {
|
||||
ext: ext,
|
||||
onThumbnailClick() {
|
||||
window.open(`/f/${this.doc._id}`, "_blank");
|
||||
window.open(`/f/${this.doc.index}/${this.doc._id}`, "_blank");
|
||||
},
|
||||
findByCustomField(field, id) {
|
||||
return {
|
||||
|
||||
@@ -66,7 +66,7 @@ import Sist2Api, {EsHit, EsResult} from "../Sist2Api";
|
||||
import SearchBar from "@/components/SearchBar.vue";
|
||||
import IndexPicker from "@/components/IndexPicker.vue";
|
||||
import Vue from "vue";
|
||||
import Sist2Query from "@/Sist2Query";
|
||||
import Sist2Query from "@/Sist2ElasticsearchQuery";
|
||||
import _debounce from "lodash/debounce";
|
||||
import DocCardWall from "@/components/DocCardWall.vue";
|
||||
import Lightbox from "@/components/Lightbox.vue";
|
||||
@@ -79,6 +79,7 @@ import DateSlider from "@/components/DateSlider.vue";
|
||||
import TagPicker from "@/components/TagPicker.vue";
|
||||
import DocList from "@/components/DocList.vue";
|
||||
import HelpDialog from "@/components/HelpDialog.vue";
|
||||
import Sist2SqliteQuery from "@/Sist2SqliteQuery";
|
||||
|
||||
|
||||
export default Vue.extend({
|
||||
@@ -114,7 +115,7 @@ export default Vue.extend({
|
||||
await this.clearResults();
|
||||
}
|
||||
|
||||
await this.searchNow(Sist2Query.searchQuery());
|
||||
await this.searchNow();
|
||||
|
||||
}, 350, {leading: false});
|
||||
|
||||
@@ -137,7 +138,7 @@ export default Vue.extend({
|
||||
|
||||
this.setIndices(this.$store.getters["sist2Info"].indices)
|
||||
|
||||
this.getDateRange().then((range: { min: number, max: number }) => {
|
||||
Sist2Api.getDateRange().then((range) => {
|
||||
this.setDateBoundsMin(range.min);
|
||||
this.setDateBoundsMax(range.max);
|
||||
|
||||
@@ -191,12 +192,12 @@ export default Vue.extend({
|
||||
bodyClass: "toast-body-warning",
|
||||
});
|
||||
},
|
||||
async searchNow(q: any) {
|
||||
async searchNow() {
|
||||
this.searchBusy = true;
|
||||
await this.$store.dispatch("incrementQuerySequence");
|
||||
this.$store.commit("busSearch");
|
||||
|
||||
Sist2Api.esQuery(q).then(async (resp: EsResult) => {
|
||||
Sist2Api.search().then(async (resp: EsResult) => {
|
||||
await this.handleSearch(resp);
|
||||
this.searchBusy = false;
|
||||
}).catch(err => {
|
||||
@@ -238,7 +239,7 @@ export default Vue.extend({
|
||||
if (hit._props.isPlayableImage || hit._props.isPlayableVideo) {
|
||||
hit._seq = await this.$store.dispatch("getKeySequence");
|
||||
this.$store.commit("addLightboxSource", {
|
||||
source: `f/${hit._id}`,
|
||||
source: `f/${hit._source.index}/${hit._id}`,
|
||||
thumbnail: hit._props.hasThumbnail
|
||||
? `t/${hit._source.index}/${hit._id}`
|
||||
: null,
|
||||
@@ -253,38 +254,17 @@ export default Vue.extend({
|
||||
|
||||
await this.$store.dispatch("remountLightbox");
|
||||
this.$store.commit("setLastQueryResult", resp);
|
||||
if (this.$store.state.firstQueryResults == null) {
|
||||
this.$store.commit("setFirstQueryResult", resp);
|
||||
}
|
||||
|
||||
this.docs.push(...resp.hits.hits);
|
||||
|
||||
resp.hits.hits.forEach(hit => this.docIds.add(hit._id));
|
||||
},
|
||||
getDateRange(): Promise<{ min: number, max: number }> {
|
||||
return sist2.esQuery({
|
||||
// TODO: filter current selected indices
|
||||
aggs: {
|
||||
dateMin: {min: {field: "mtime"}},
|
||||
dateMax: {max: {field: "mtime"}},
|
||||
},
|
||||
size: 0
|
||||
}).then(res => {
|
||||
const range = {
|
||||
min: res.aggregations.dateMin.value,
|
||||
max: res.aggregations.dateMax.value,
|
||||
}
|
||||
|
||||
if (range.min == null) {
|
||||
range.min = 0;
|
||||
range.max = 1;
|
||||
} else if (range.min == range.max) {
|
||||
range.max += 1;
|
||||
}
|
||||
|
||||
return range;
|
||||
});
|
||||
},
|
||||
appendFunc() {
|
||||
if (!this.$store.state.uiReachedScrollEnd && this.search && !this.searchBusy) {
|
||||
this.searchNow(Sist2Query.searchQuery());
|
||||
this.searchNow();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
<template>
|
||||
<b-container>
|
||||
<b-container>
|
||||
|
||||
<b-card v-if="loading">
|
||||
<Preloader></Preloader>
|
||||
</b-card>
|
||||
<template>
|
||||
<b-card>
|
||||
<b-card-body>
|
||||
<b-select v-model="selectedIndex" :options="indexOptions">
|
||||
<template #first>
|
||||
<b-form-select-option :value="null" disabled>{{
|
||||
$t("indexPickerPlaceholder")
|
||||
}}
|
||||
</b-form-select-option>
|
||||
</template>
|
||||
</b-select>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<template v-else>
|
||||
<b-card>
|
||||
<b-card-body>
|
||||
<b-select v-model="selectedIndex" :options="indexOptions">
|
||||
<template #first>
|
||||
<b-form-select-option :value="null" disabled>{{ $t("indexPickerPlaceholder") }}</b-form-select-option>
|
||||
</template>
|
||||
</b-select>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<b-card v-if="selectedIndex !== null" class="mt-3">
|
||||
<b-card-body>
|
||||
<D3Treemap :index-id="selectedIndex"></D3Treemap>
|
||||
<b-card v-if="selectedIndex !== null" class="mt-3">
|
||||
<b-card-body>
|
||||
<D3Treemap :index-id="selectedIndex"></D3Treemap>
|
||||
|
||||
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<b-card v-if="selectedIndex !== null" class="stats-card mt-3">
|
||||
<D3MimeBarCount :index-id="selectedIndex"></D3MimeBarCount>
|
||||
<D3MimeBarSize :index-id="selectedIndex"></D3MimeBarSize>
|
||||
<D3DateHistogram :index-id="selectedIndex"></D3DateHistogram>
|
||||
<D3SizeHistogram :index-id="selectedIndex"></D3SizeHistogram>
|
||||
</b-card>
|
||||
</template>
|
||||
</b-container>
|
||||
<b-card v-if="selectedIndex !== null" class="stats-card mt-3">
|
||||
<D3MimeBarCount :index-id="selectedIndex"></D3MimeBarCount>
|
||||
<D3MimeBarSize :index-id="selectedIndex"></D3MimeBarSize>
|
||||
<D3DateHistogram :index-id="selectedIndex"></D3DateHistogram>
|
||||
<D3SizeHistogram :index-id="selectedIndex"></D3SizeHistogram>
|
||||
</b-card>
|
||||
</template>
|
||||
</b-container>
|
||||
</template>
|
||||
<script>
|
||||
import D3Treemap from "@/components/D3Treemap";
|
||||
@@ -43,37 +42,30 @@ import D3DateHistogram from "@/components/D3DateHistogram";
|
||||
import D3SizeHistogram from "@/components/D3SizeHistogram";
|
||||
|
||||
export default {
|
||||
components: {D3SizeHistogram, D3DateHistogram, D3MimeBarSize, D3MimeBarCount, D3Treemap, Preloader},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
selectedIndex: null,
|
||||
indices: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
indexOptions() {
|
||||
return this.indices.map(idx => {
|
||||
components: {D3SizeHistogram, D3DateHistogram, D3MimeBarSize, D3MimeBarCount, D3Treemap, Preloader},
|
||||
data() {
|
||||
return {
|
||||
text: idx.name,
|
||||
value: idx.id
|
||||
};
|
||||
})
|
||||
selectedIndex: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
indexOptions() {
|
||||
return this.indices.map(idx => {
|
||||
return {
|
||||
text: idx.name,
|
||||
value: idx.id
|
||||
};
|
||||
})
|
||||
},
|
||||
indices: () => this.$store.state.indices
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
Sist2Api.getSist2Info().then(data => {
|
||||
this.indices = data.indices;
|
||||
this.loading = false;
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.stats-card {
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user