mirror of
https://github.com/Lissy93/dashy.git
synced 2026-06-01 14:24:35 -04:00
⚡ Improve search performance, with pre-filter (#2156)
This commit is contained in:
@@ -44,6 +44,7 @@ export default {
|
||||
input: '', // Users current search term
|
||||
akn: new ArrowKeyNavigation(), // Class that manages arrow key naviagtion
|
||||
getCustomKeyShortcuts,
|
||||
emitFrame: null, // Pending requestAnimationFrame id, for coalescing keystrokes
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -67,6 +68,7 @@ export default {
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('keydown', this.handleKeyPress);
|
||||
if (this.emitFrame != null) cancelAnimationFrame(this.emitFrame);
|
||||
},
|
||||
methods: {
|
||||
/* Call correct function dependending on which key is pressed */
|
||||
@@ -93,14 +95,22 @@ export default {
|
||||
},
|
||||
/* Emmits users's search term up to parent */
|
||||
userIsTypingSomething() {
|
||||
this.$emit('user-is-searchin', this.input);
|
||||
if (this.emitFrame != null) return;
|
||||
this.emitFrame = requestAnimationFrame(() => {
|
||||
this.emitFrame = null;
|
||||
this.$emit('user-is-searchin', this.input);
|
||||
});
|
||||
},
|
||||
/* Resets everything to initial state, when user is finished */
|
||||
clearFilterInput() {
|
||||
this.input = ''; // Clear input model
|
||||
this.userIsTypingSomething(); // Emmit new empty value
|
||||
document.activeElement.blur(); // Remove focus
|
||||
this.akn.resetIndex(); // Reset current element index
|
||||
this.input = '';
|
||||
if (this.emitFrame != null) {
|
||||
cancelAnimationFrame(this.emitFrame);
|
||||
this.emitFrame = null;
|
||||
}
|
||||
this.$emit('user-is-searchin', '');
|
||||
document.activeElement.blur();
|
||||
this.akn.resetIndex();
|
||||
},
|
||||
/* If configured, launch specific app when hotkey pressed */
|
||||
handleHotKey(key) {
|
||||
|
||||
@@ -36,7 +36,8 @@ import SideBarItem from '@/components/Workspace/SideBarItem.vue';
|
||||
import SideBarSection from '@/components/Workspace/SideBarSection.vue';
|
||||
import IconHome from '@/assets/interface-icons/application-home.svg';
|
||||
import IconMinimalView from '@/assets/interface-icons/application-minimal.svg';
|
||||
import { checkItemVisibility } from '@/utils/CheckItemVisibility';
|
||||
import { getCurrentUser, isLoggedInAsGuest } from '@/utils/auth/Auth';
|
||||
import { isVisibleToUser } from '@/utils/IsVisibleToUser';
|
||||
import { makeRoutePath, resolveRouteIntent } from '@/utils/config/ConfigHelpers';
|
||||
|
||||
export default {
|
||||
@@ -100,11 +101,11 @@ export default {
|
||||
},
|
||||
/* Return a list with visible items on a section to the user or guest */
|
||||
filterTiles(allTiles) {
|
||||
if (!allTiles) {
|
||||
return [];
|
||||
}
|
||||
return allTiles.filter((tile) => checkItemVisibility(tile)
|
||||
&& !tile.displayData?.hideFromWorkspace);
|
||||
if (!allTiles) return [];
|
||||
const currentUser = getCurrentUser();
|
||||
const isGuest = isLoggedInAsGuest();
|
||||
return allTiles.filter((tile) => !tile.displayData?.hideFromWorkspace
|
||||
&& isVisibleToUser(tile.displayData || {}, currentUser, isGuest));
|
||||
},
|
||||
/* Build a URL for the given view, preserving the current sub-page and section */
|
||||
pathFor(view) {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import Defaults, { localStorageKeys, iconCdns } from '@/utils/config/defaults';
|
||||
import Keys from '@/utils/StoreMutations';
|
||||
import { searchTiles } from '@/utils/Search';
|
||||
import { checkItemVisibility } from '@/utils/CheckItemVisibility';
|
||||
import { getCurrentUser, isLoggedInAsGuest } from '@/utils/auth/Auth';
|
||||
import { isVisibleToUser } from '@/utils/IsVisibleToUser';
|
||||
import { resolveRouteIntent, PAGE_STATUS } from '@/utils/config/ConfigHelpers';
|
||||
|
||||
const HomeMixin = {
|
||||
@@ -88,10 +89,12 @@ const HomeMixin = {
|
||||
},
|
||||
/* Returns only the tiles that match the users search query */
|
||||
filterTiles(allTiles) {
|
||||
if (!allTiles) {
|
||||
return [];
|
||||
}
|
||||
const visibleTiles = allTiles.filter((tile) => checkItemVisibility(tile));
|
||||
if (!allTiles) return [];
|
||||
const currentUser = getCurrentUser();
|
||||
const isGuest = isLoggedInAsGuest();
|
||||
const visibleTiles = allTiles.filter(
|
||||
(tile) => isVisibleToUser(tile.displayData || {}, currentUser, isGuest),
|
||||
);
|
||||
return searchTiles(visibleTiles, this.searchValue);
|
||||
},
|
||||
/* Checks if any sections or items use icons from a given CDN */
|
||||
|
||||
@@ -25,9 +25,9 @@ const determineIntersection = (source = [], target = []) => {
|
||||
};
|
||||
|
||||
/* Returns false if the displayData of a section/item
|
||||
should not be rendered for the current user/ guest */
|
||||
export const isVisibleToUser = (displayData, currentUser) => {
|
||||
const isGuest = isLoggedInAsGuest(); // Check if current user is a guest
|
||||
* says it should not be rendered for current user/guest */
|
||||
export const isVisibleToUser = (displayData, currentUser, isGuest) => {
|
||||
if (isGuest === undefined) isGuest = isLoggedInAsGuest();
|
||||
|
||||
// Checks if user explicitly has access to a certain section
|
||||
const checkVisibility = () => {
|
||||
|
||||
@@ -15,17 +15,28 @@ const getDomainFromUrl = (url) => {
|
||||
return domainPattern ? domainPattern[1] : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares search term to a given data attribute
|
||||
* Ignores case, special characters and order
|
||||
* @param {string or other} compareStr The value to compare to
|
||||
* @param {string} searchStr The users search term
|
||||
* @returns {boolean} true if a match, otherwise false
|
||||
*/
|
||||
const filterHelper = (compareStr, searchStr) => {
|
||||
if (!compareStr) return false;
|
||||
const process = (input) => input?.toString().toLowerCase().replace(/[^\w\s\p{Alpha}]/giu, '');
|
||||
return process(searchStr).split(/\s/).every(word => process(compareStr).includes(word));
|
||||
/* Normalize a string for case/punctuation-insensitive matching */
|
||||
const NORMALIZE_RE = /[^\w\s\p{Alpha}]/giu;
|
||||
const normalize = (input) => (input == null ? '' : input.toString().toLowerCase().replace(NORMALIZE_RE, ''));
|
||||
|
||||
/* Per-tile cache of the concatenated searchable text */
|
||||
const haystackCache = new WeakMap();
|
||||
|
||||
const buildHaystack = (tile) => {
|
||||
const {
|
||||
title, description, provider, url, tags,
|
||||
} = tile;
|
||||
const tagsStr = Array.isArray(tags) ? tags.join(' ') : (tags || '');
|
||||
return normalize(`${title || ''} ${provider || ''} ${description || ''} ${tagsStr} ${getDomainFromUrl(url)}`);
|
||||
};
|
||||
|
||||
const getHaystack = (tile) => {
|
||||
let h = haystackCache.get(tile);
|
||||
if (h === undefined) {
|
||||
h = buildHaystack(tile);
|
||||
haystackCache.set(tile, h);
|
||||
}
|
||||
return h;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -37,17 +48,13 @@ const filterHelper = (compareStr, searchStr) => {
|
||||
* @returns A filtered array of tiles
|
||||
*/
|
||||
export const searchTiles = (allTiles, searchTerm) => {
|
||||
if (!searchTerm) return allTiles; // If no search term, then return all
|
||||
if (!allTiles) return []; // If no data, then skip
|
||||
if (!searchTerm) return allTiles;
|
||||
if (!allTiles) return [];
|
||||
const words = normalize(searchTerm).split(/\s+/).filter(Boolean);
|
||||
if (!words.length) return allTiles;
|
||||
return allTiles.filter((tile) => {
|
||||
const {
|
||||
title, description, provider, url, tags,
|
||||
} = tile;
|
||||
return filterHelper(title, searchTerm)
|
||||
|| filterHelper(provider, searchTerm)
|
||||
|| filterHelper(description, searchTerm)
|
||||
|| filterHelper(tags, searchTerm)
|
||||
|| filterHelper(getDomainFromUrl(url), searchTerm);
|
||||
const haystack = getHaystack(tile);
|
||||
return words.every((word) => haystack.includes(word));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ import { isOidcEnabled } from '@/utils/auth/OidcAuth';
|
||||
/* Uses config accumulator to get and return app config */
|
||||
const getAppConfig = () => {
|
||||
const Accumulator = new ConfigAccumulator();
|
||||
const config = Accumulator.config();
|
||||
return config.appConfig || {};
|
||||
return Accumulator.appConfig() || {};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,8 +7,7 @@ import { toast } from '@/utils/Toast';
|
||||
|
||||
const getAppConfig = () => {
|
||||
const Accumulator = new ConfigAccumulator();
|
||||
const config = Accumulator.config();
|
||||
return config.appConfig || {};
|
||||
return Accumulator.appConfig() || {};
|
||||
};
|
||||
|
||||
const isKeycloakGuestAccessEnabled = () => {
|
||||
|
||||
@@ -13,8 +13,7 @@ const SIGNIN_GUARD_THRESHOLD_MS = 5 * 1000;
|
||||
|
||||
const getAppConfig = () => {
|
||||
const Accumulator = new ConfigAccumulator();
|
||||
const config = Accumulator.config();
|
||||
return config.appConfig || {};
|
||||
return Accumulator.appConfig() || {};
|
||||
};
|
||||
|
||||
const isOidcGuestAccessEnabled = () => {
|
||||
|
||||
Reference in New Issue
Block a user