Option to update media type tab in real time, add media type table in details

This commit is contained in:
simon987 2022-01-08 18:23:22 -05:00
parent 64b8aab8bf
commit 625f3d0d6e
13 changed files with 258 additions and 57 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -256,20 +256,31 @@ class Sist2Api {
}); });
} }
getMimeTypes() { getMimeTypes(query = undefined) {
return this.esQuery({ const AGGS = {
aggs: {
mimeTypes: { mimeTypes: {
terms: { terms: {
field: "mime", field: "mime",
size: 10000 size: 10000
} }
} }
}, };
if (!query) {
query = {
aggs: AGGS,
size: 0, size: 0,
}).then(resp => { };
} else {
query.size = 0;
query.aggs = AGGS;
}
return this.esQuery(query).then(resp => {
const mimeMap: any[] = []; const mimeMap: any[] = [];
resp["aggregations"]["mimeTypes"]["buckets"].sort((a: any, b: any) => a.key > b.key).forEach((bucket: any) => { const buckets = resp["aggregations"]["mimeTypes"]["buckets"];
buckets.sort((a: any, b: any) => a.key > b.key).forEach((bucket: any) => {
const tmp = bucket["key"].split("/"); const tmp = bucket["key"].split("/");
const category = tmp[0]; const category = tmp[0];
const mime = tmp[1]; const mime = tmp[1];
@ -289,11 +300,18 @@ class Sist2Api {
}); });
if (!category_exists) { if (!category_exists) {
mimeMap.push({"text": category, children: [child]}); mimeMap.push({text: category, children: [child], id: category});
} }
}) })
return mimeMap; 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};
}); });
} }

View File

@ -7,40 +7,24 @@ import InspireTree from "inspire-tree";
import InspireTreeDOM from "inspire-tree-dom"; import InspireTreeDOM from "inspire-tree-dom";
import "inspire-tree-dom/dist/inspire-tree-light.min.css"; import "inspire-tree-dom/dist/inspire-tree-light.min.css";
import {getSelectedTreeNodes} from "@/util"; import {getSelectedTreeNodes, getTreeNodeAttributes} from "@/util";
import Sist2Api from "@/Sist2Api";
import Sist2Query from "@/Sist2Query";
export default { export default {
name: "MimePicker", name: "MimePicker",
data() { data() {
return { return {
mimeTree: null, mimeTree: null,
stashedMimeTreeAttributes: null
} }
}, },
mounted() { mounted() {
this.$store.subscribe((mutation) => { this.$store.subscribe((mutation) => {
if (mutation.type === "setUiMimeMap") { if (mutation.type === "setUiMimeMap" && this.mimeTree === null) {
const mimeMap = mutation.payload.slice(); this.initializeTree();
} else if (mutation.type === "busSearch") {
const elem = document.getElementById("mimeTree"); this.updateTree();
console.log(elem);
this.mimeTree = new InspireTree({
selection: {
mode: 'checkbox'
},
data: mimeMap
});
new InspireTreeDOM(this.mimeTree, {
target: '#mimeTree'
});
this.mimeTree.on("node.state.changed", this.handleTreeClick);
this.mimeTree.deselect();
if (this.$store.state._onLoadSelectedMimeTypes.length > 0) {
this.$store.state._onLoadSelectedMimeTypes.forEach(mime => {
this.mimeTree.node(mime).select();
});
}
} }
}); });
}, },
@ -52,6 +36,73 @@ export default {
this.$store.commit("setSelectedMimeTypes", getSelectedTreeNodes(this.mimeTree)); this.$store.commit("setSelectedMimeTypes", getSelectedTreeNodes(this.mimeTree));
}, },
updateTree() {
if (this.$store.getters.optUpdateMimeMap === false) {
return;
}
if (this.stashedMimeTreeAttributes === null) {
this.stashedMimeTreeAttributes = getTreeNodeAttributes(this.mimeTree);
}
const query = Sist2Query.searchQuery();
Sist2Api.getMimeTypes(query).then(({buckets, mimeMap}) => {
this.$store.commit("setUiMimeMap", mimeMap);
this.$store.commit("setUiDetailsMimeAgg", buckets);
this.mimeTree.removeAll();
this.mimeTree.addNodes(mimeMap);
// Restore selected mimes
if (this.stashedMimeTreeAttributes === null) {
// NOTE: This happens when successive fast searches are triggered
this.stashedMimeTreeAttributes = {};
// Always add the selected mime types
this.$store.state.selectedMimeTypes.forEach(mime => {
this.stashedMimeTreeAttributes[mime] = {
checked: true
}
});
}
Object.entries(this.stashedMimeTreeAttributes).forEach(([mime, attributes]) => {
if (this.mimeTree.node(mime)) {
if (attributes.checked) {
this.mimeTree.node(mime).select();
}
if (attributes.collapsed === false) {
this.mimeTree.node(mime).expand();
}
}
});
this.stashedMimeTreeAttributes = null;
});
},
initializeTree() {
const mimeMap = this.$store.state.uiMimeMap;
this.mimeTree = new InspireTree({
selection: {
mode: "checkbox"
},
data: mimeMap
});
new InspireTreeDOM(this.mimeTree, {
target: "#mimeTree"
});
this.mimeTree.on("node.state.changed", this.handleTreeClick);
this.mimeTree.deselect();
if (this.$store.state._onLoadSelectedMimeTypes.length > 0) {
this.$store.state._onLoadSelectedMimeTypes.forEach(mime => {
this.mimeTree.node(mime).select();
});
}
}
} }
} }
</script> </script>

View File

@ -3,7 +3,10 @@
<span>{{ hitCount }} {{ hitCount === 1 ? $t("hit") : $t("hits") }}</span> <span>{{ hitCount }} {{ hitCount === 1 ? $t("hit") : $t("hits") }}</span>
<div style="float: right"> <div style="float: right">
<b-button v-b-toggle.collapse-1 variant="primary" class="not-mobile">{{ $t("details") }}</b-button> <b-button v-b-toggle.collapse-1 variant="primary" class="not-mobile" @click="onToggle()">{{
$t("details")
}}
</b-button>
<template v-if="hitCount !== 0"> <template v-if="hitCount !== 0">
<SortSelect class="ml-2"></SortSelect> <SortSelect class="ml-2"></SortSelect>
@ -14,22 +17,42 @@
<b-collapse id="collapse-1" class="pt-2" style="clear:both;"> <b-collapse id="collapse-1" class="pt-2" style="clear:both;">
<b-card> <b-card>
<b-table :items="tableItems" small borderless thead-class="hidden" class="mb-0"></b-table> <b-table :items="tableItems" small borderless bordered 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-card>
</b-collapse> </b-collapse>
</b-card> </b-card>
</template> </template>
<script lang="ts"> <script lang="ts">
import {EsResult} from "@/Sist2Api"; import Sist2Api, {EsResult} from "@/Sist2Api";
import Vue from "vue"; import Vue from "vue";
import {humanFileSize} from "@/util"; import {humanFileSize} from "@/util";
import DisplayModeToggle from "@/components/DisplayModeToggle.vue"; import DisplayModeToggle from "@/components/DisplayModeToggle.vue";
import SortSelect from "@/components/SortSelect.vue"; import SortSelect from "@/components/SortSelect.vue";
import Preloader from "@/components/Preloader.vue";
import Sist2Query from "@/Sist2Query";
import ClipboardIcon from "@/components/icons/ClipboardIcon.vue";
export default Vue.extend({ export default Vue.extend({
name: "ResultsCard", name: "ResultsCard",
components: {SortSelect, DisplayModeToggle}, components: {ClipboardIcon, Preloader, SortSelect, DisplayModeToggle},
created() {
},
computed: { computed: {
lastResultsLoaded() { lastResultsLoaded() {
return this.$store.state.lastQueryResults != null; return this.$store.state.lastQueryResults != null;
@ -54,6 +77,39 @@ export default Vue.extend({
totalSize() { totalSize() {
return humanFileSize((this.$store.state.lastQueryResults as EsResult).aggregations.total_size.value); 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);
});
}
}, },
}); });

View File

@ -120,7 +120,7 @@ export default {
}, },
mounted() { mounted() {
this.$store.subscribe((mutation) => { this.$store.subscribe((mutation) => {
if (mutation.type === "setUiMimeMap") { if (mutation.type === "setUiMimeMap" && this.tagTree === null) {
this.initializeTree(); this.initializeTree();
this.updateTree(); this.updateTree();
} else if (mutation.type === "busUpdateTags") { } else if (mutation.type === "busUpdateTags") {
@ -147,6 +147,7 @@ export default {
this.tagTree.on("node.state.changed", this.handleTreeClick); this.tagTree.on("node.state.changed", this.handleTreeClick);
}, },
updateTree() { updateTree() {
// TODO: remember which tags are selected and restore?
const tagMap = []; const tagMap = [];
Sist2Api.getTags().then(tags => { Sist2Api.getTags().then(tags => {
tags.forEach(tag => addTag(tagMap, tag.id, tag.id, tag.count)); tags.forEach(tag => addTag(tagMap, tag.id, tag.id, tag.count));

View File

@ -0,0 +1,21 @@
<template>
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M17,9H7V7H17M17,13H7V11H17M14,17H7V15H14M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z"/>
</svg>
</template>
<script>
export default {
name: "ClipboardIcon"
}
</script>
<style scoped>
svg {
display: inline-block;
width: 20px;
height: 20px;
}
</style>

View File

@ -66,7 +66,8 @@ export default {
resultSize: "Number of results per page", resultSize: "Number of results per page",
tagOrOperator: "Use OR operator when specifying multiple tags.", tagOrOperator: "Use OR operator when specifying multiple tags.",
hideDuplicates: "Hide duplicate results based on checksum", hideDuplicates: "Hide duplicate results based on checksum",
hideLegacy: "Hide the 'legacyES' Elasticsearch notice" hideLegacy: "Hide the 'legacyES' Elasticsearch notice",
updateMimeMap: "Update the Media Types tree in real time"
}, },
queryMode: { queryMode: {
simple: "Simple", simple: "Simple",
@ -129,7 +130,8 @@ export default {
esQueryErr: "Could not parse or execute query, please check the Advanced search documentation. " + esQueryErr: "Could not parse or execute query, please check the Advanced search documentation. " +
"See server logs for more information.", "See server logs for more information.",
dupeTagTitle: "Duplicate tag", dupeTagTitle: "Duplicate tag",
dupeTag: "This tag already exists for this document." dupeTag: "This tag already exists for this document.",
copiedToClipboard: "Copied to clipboard"
}, },
saveTagModalTitle: "Add tag", saveTagModalTitle: "Add tag",
saveTagPlaceholder: "Tag name", saveTagPlaceholder: "Tag name",
@ -226,7 +228,8 @@ export default {
resultSize: "Nombre de résultats par page", resultSize: "Nombre de résultats par page",
tagOrOperator: "Utiliser l'opérateur OU lors de la spécification de plusieurs tags", tagOrOperator: "Utiliser l'opérateur OU lors de la spécification de plusieurs tags",
hideDuplicates: "Masquer les résultats en double", hideDuplicates: "Masquer les résultats en double",
hideLegacy: "Masquer la notice 'legacyES' Elasticsearch" hideLegacy: "Masquer la notice 'legacyES' Elasticsearch",
updateMimeMap: "Mettre à jour l'arbre de Types de médias en temps réel"
}, },
queryMode: { queryMode: {
simple: "Simple", simple: "Simple",
@ -290,7 +293,8 @@ export default {
esQueryErr: "Impossible d'analyser ou d'exécuter la requête, veuillez consulter la documentation sur la " + esQueryErr: "Impossible d'analyser ou d'exécuter la requête, veuillez consulter la documentation sur la " +
"recherche avancée. Voir les journaux du serveur pour plus d'informations.", "recherche avancée. Voir les journaux du serveur pour plus d'informations.",
dupeTagTitle: "Tag en double", dupeTagTitle: "Tag en double",
dupeTag: "Ce tag existe déjà pour ce document." dupeTag: "Ce tag existe déjà pour ce document.",
copiedToClipboard: "Copié dans le presse-papier"
}, },
saveTagModalTitle: "Ajouter un tag", saveTagModalTitle: "Ajouter un tag",
saveTagPlaceholder: "Nom du tag", saveTagPlaceholder: "Nom du tag",
@ -386,7 +390,8 @@ export default {
resultSize: "每页结果数", resultSize: "每页结果数",
tagOrOperator: "使用或操作OR匹配多个标签。", tagOrOperator: "使用或操作OR匹配多个标签。",
hideDuplicates: "使用校验码隐藏重复结果", hideDuplicates: "使用校验码隐藏重复结果",
hideLegacy: "隐藏'legacyES' Elasticsearch 通知" hideLegacy: "隐藏'legacyES' Elasticsearch 通知",
updateMimeMap: "媒体类型树的实时更新"
}, },
queryMode: { queryMode: {
simple: "简单", simple: "简单",
@ -449,7 +454,8 @@ export default {
esQueryErr: "无法识别或执行查询,请查阅高级搜索文档。" + esQueryErr: "无法识别或执行查询,请查阅高级搜索文档。" +
"查看服务日志以获取更多信息。", "查看服务日志以获取更多信息。",
dupeTagTitle: "重复标签", dupeTagTitle: "重复标签",
dupeTag: "该标签已存在于此文档。" dupeTag: "该标签已存在于此文档。",
copiedToClipboard: "复制到剪贴板"
}, },
saveTagModalTitle: "增加标签", saveTagModalTitle: "增加标签",
saveTagPlaceholder: "标签名", saveTagPlaceholder: "标签名",

View File

@ -48,6 +48,7 @@ export default new Vuex.Store({
optLightboxLoadOnlyCurrent: false, optLightboxLoadOnlyCurrent: false,
optLightboxSlideDuration: 15, optLightboxSlideDuration: 15,
optHideLegacy: false, optHideLegacy: false,
optUpdateMimeMap: true,
_onLoadSelectedIndices: [] as string[], _onLoadSelectedIndices: [] as string[],
_onLoadSelectedMimeTypes: [] as string[], _onLoadSelectedMimeTypes: [] as string[],
@ -72,9 +73,14 @@ export default new Vuex.Store({
uiLightboxSlide: 0, uiLightboxSlide: 0,
uiReachedScrollEnd: false, uiReachedScrollEnd: false,
uiDetailsMimeAgg: null,
uiShowDetails: false,
uiMimeMap: [] as any[] uiMimeMap: [] as any[]
}, },
mutations: { mutations: {
setUiShowDetails: (state, val) => state.uiShowDetails = val,
setUiDetailsMimeAgg: (state, val) => state.uiDetailsMimeAgg = val,
setUiReachedScrollEnd: (state, val) => state.uiReachedScrollEnd = val, setUiReachedScrollEnd: (state, val) => state.uiReachedScrollEnd = val,
setTags: (state, val) => state.tags = val, setTags: (state, val) => state.tags = val,
setPathText: (state, val) => state.pathText = val, setPathText: (state, val) => state.pathText = val,
@ -150,6 +156,7 @@ export default new Vuex.Store({
setOptTreemapSize: (state, val) => state.optTreemapSize = val, setOptTreemapSize: (state, val) => state.optTreemapSize = val,
setOptTreemapColor: (state, val) => state.optTreemapColor = val, setOptTreemapColor: (state, val) => state.optTreemapColor = val,
setOptHideLegacy: (state, val) => state.optHideLegacy = val, setOptHideLegacy: (state, val) => state.optHideLegacy = val,
setOptUpdateMimeMap: (state, val) => state.optUpdateMimeMap = val,
setOptLightboxLoadOnlyCurrent: (state, val) => state.optLightboxLoadOnlyCurrent = val, setOptLightboxLoadOnlyCurrent: (state, val) => state.optLightboxLoadOnlyCurrent = val,
setOptLightboxSlideDuration: (state, val) => state.optLightboxSlideDuration = val, setOptLightboxSlideDuration: (state, val) => state.optLightboxSlideDuration = val,
@ -162,6 +169,9 @@ export default new Vuex.Store({
busUpdateTags: () => { busUpdateTags: () => {
// noop // noop
}, },
busSearch: () => {
// noop
},
}, },
actions: { actions: {
setSist2Info: (store, val) => { setSist2Info: (store, val) => {
@ -290,6 +300,7 @@ export default new Vuex.Store({
commit("setUiLightboxTypes", []); commit("setUiLightboxTypes", []);
commit("setUiLightboxCaptions", []); commit("setUiLightboxCaptions", []);
commit("setUiLightboxKey", 0); commit("setUiLightboxKey", 0);
commit("setUiDetailsMimeAgg", null);
} }
}, },
modules: {}, modules: {},
@ -354,5 +365,6 @@ export default new Vuex.Store({
optLightboxSlideDuration: state => state.optLightboxSlideDuration, optLightboxSlideDuration: state => state.optLightboxSlideDuration,
optResultSize: state => state.size, optResultSize: state => state.size,
optHideLegacy: state => state.optHideLegacy, optHideLegacy: state => state.optHideLegacy,
optUpdateMimeMap: state => state.optUpdateMimeMap,
} }
}) })

View File

@ -97,6 +97,30 @@ export function getSelectedTreeNodes(tree: any) {
return Array.from(selectedNodes); return Array.from(selectedNodes);
} }
export function getTreeNodeAttributes(tree: any) {
const nodes = tree.selectable();
const attributes = {};
for (let i = 0; i < nodes.length; i++) {
let id = null;
if (nodes[i].text.indexOf("(") !== -1 && nodes[i].values) {
id = nodes[i].values.slice(-1)[0];
} else {
id = nodes[i].id
}
attributes[id] = {
checked: nodes[i].itree.state.checked,
collapsed: nodes[i].itree.state.collapsed,
}
}
return attributes;
}
export function serializeMimes(mimes: string[]): string | undefined { export function serializeMimes(mimes: string[]): string | undefined {
if (mimes.length == 0) { if (mimes.length == 0) {
return undefined; return undefined;

View File

@ -37,6 +37,10 @@
<b-form-checkbox :checked="optHideLegacy" @input="setOptHideLegacy"> <b-form-checkbox :checked="optHideLegacy" @input="setOptHideLegacy">
{{ $t("opt.hideLegacy") }} {{ $t("opt.hideLegacy") }}
</b-form-checkbox> </b-form-checkbox>
<b-form-checkbox :checked="optUpdateMimeMap" @input="setOptUpdateMimeMap">
{{ $t("opt.updateMimeMap") }}
</b-form-checkbox>
</b-card> </b-card>
<br/> <br/>
@ -224,6 +228,7 @@ export default {
"optLang", "optLang",
"optHideDuplicates", "optHideDuplicates",
"optHideLegacy", "optHideLegacy",
"optUpdateMimeMap",
]), ]),
clientWidth() { clientWidth() {
return window.innerWidth; return window.innerWidth;
@ -266,7 +271,8 @@ export default {
"setOptTagOrOperator", "setOptTagOrOperator",
"setOptLang", "setOptLang",
"setOptHideDuplicates", "setOptHideDuplicates",
"setOptHideLegacy" "setOptHideLegacy",
"setOptUpdateMimeMap"
]), ]),
onResetClick() { onResetClick() {
localStorage.removeItem("sist2_configuration"); localStorage.removeItem("sist2_configuration");

View File

@ -139,7 +139,7 @@ export default Vue.extend({
this.setSist2Info(data); this.setSist2Info(data);
this.setIndices(data.indices); this.setIndices(data.indices);
Sist2Api.getMimeTypes().then(mimeMap => { Sist2Api.getMimeTypes(Sist2Query.searchQuery()).then(({mimeMap}) => {
this.$store.commit("setUiMimeMap", mimeMap); this.$store.commit("setUiMimeMap", mimeMap);
this.uiLoading = false; this.uiLoading = false;
this.search(true); this.search(true);
@ -185,6 +185,7 @@ export default Vue.extend({
async searchNow(q: any) { async searchNow(q: any) {
this.searchBusy = true; this.searchBusy = true;
await this.$store.dispatch("incrementQuerySequence"); await this.$store.dispatch("incrementQuerySequence");
this.$store.commit("busSearch");
Sist2Api.esQuery(q).then(async (resp: EsResult) => { Sist2Api.esQuery(q).then(async (resp: EsResult) => {
await this.handleSearch(resp); await this.handleSearch(resp);
@ -286,6 +287,11 @@ export default Vue.extend({
border: none; border: none;
} }
.toast-header-info, .toast-body-info {
background: #2196f3;
color: #fff !important;
}
.toast-header-error, .toast-body-error { .toast-header-error, .toast-body-error {
background: #a94442; background: #a94442;
color: #f2dede !important; color: #f2dede !important;

File diff suppressed because one or more lines are too long