olivetin/frontend/resources/vue/stores/searchIndex.js

229 lines
5.0 KiB
JavaScript

import { reactive, computed } from 'vue'
import {
CellsIcon,
DashboardSquare01Icon,
LeftToRightListDashIcon,
PlayIcon,
Wrench01Icon
} from '@hugeicons/core-free-icons'
export const MAX_SEARCH_HINT_ACTIONS = 100
export const MAX_SEARCH_HINT_DASHBOARDS = 100
export const MAX_SEARCH_HINT_ENTITIES_PER_TYPE = 50
const SOURCE_ENTITIES = 'entities'
const SOURCE_ACTIONS = 'actions'
const SOURCE_DASHBOARDS = 'dashboards'
const SOURCE_NAVIGATION = 'navigation'
// QuickSearch keeps insertion order; prefer entities over actions for equal matches.
const CATEGORY_PRIORITY = {
Navigation: 0,
Dashboards: 1,
Entities: 2,
Actions: 3
}
const state = reactive({
itemsById: {},
sourceToIds: {}
})
export const searchIndexItems = computed(() => {
return Object.values(state.itemsById).sort((a, b) => {
return categoryPriority(a.category) - categoryPriority(b.category)
})
})
function categoryPriority (category) {
return CATEGORY_PRIORITY[category] ?? 50
}
export function clearSearchIndex () {
state.itemsById = {}
state.sourceToIds = {}
}
function replaceSource (source, items) {
const previousIds = state.sourceToIds[source] || []
for (const itemId of previousIds) {
delete state.itemsById[itemId]
}
const nextIds = []
for (const item of items) {
if (!item?.id) {
continue
}
state.itemsById[item.id] = item
nextIds.push(item.id)
}
state.sourceToIds[source] = nextIds
}
export function dashboardRoutePath (title, entityType, entityKey) {
if (!title) {
return '/'
}
if (title === 'Actions' && !entityType && !entityKey) {
return '/'
}
let path = `/dashboards/${encodeURIComponent(title)}`
if (entityType && entityKey) {
path += `/${encodeURIComponent(entityType)}/${encodeURIComponent(entityKey)}`
}
return path
}
/**
* Indexes search hints from Init (entities and actions).
* Dashboards are indexed separately from rootDashboardEntries.
*/
export function indexSearchHints (searchHints) {
replaceSource(SOURCE_ENTITIES, entityItemsFromHints(searchHints?.entities))
replaceSource(SOURCE_ACTIONS, actionItemsFromHints(searchHints?.actions))
}
/**
* Indexes dashboards from Init.rootDashboardEntries (already ACL-filtered).
*/
export function indexRootDashboardEntries (entries) {
replaceSource(SOURCE_DASHBOARDS, dashboardItemsFromRootEntries(entries))
}
function entityItemsFromHints (hints) {
if (!Array.isArray(hints)) {
return []
}
const countsByType = {}
const items = []
for (const hint of hints) {
if (!hint?.uniqueKey || !hint?.type) {
continue
}
const count = countsByType[hint.type] || 0
if (count >= MAX_SEARCH_HINT_ENTITIES_PER_TYPE) {
continue
}
countsByType[hint.type] = count + 1
items.push({
id: `entity:${hint.type}:${hint.uniqueKey}`,
title: hint.title || hint.uniqueKey,
description: hint.type,
category: 'Entities',
type: 'route',
path: `/entity-details/${encodeURIComponent(hint.type)}/${encodeURIComponent(hint.uniqueKey)}`,
icon: CellsIcon
})
}
return items
}
function actionItemsFromHints (hints) {
if (!Array.isArray(hints)) {
return []
}
const items = []
for (const hint of hints.slice(0, MAX_SEARCH_HINT_ACTIONS)) {
if (!hint?.bindingId) {
continue
}
items.push({
id: `action:${hint.bindingId}`,
title: hint.title || hint.bindingId,
category: 'Actions',
type: 'route',
path: `/action/${encodeURIComponent(hint.bindingId)}`,
icon: PlayIcon
})
}
return items
}
function dashboardItemsFromRootEntries (entries) {
if (!Array.isArray(entries)) {
return []
}
const items = []
for (const entry of entries.slice(0, MAX_SEARCH_HINT_DASHBOARDS)) {
if (!entry?.title) {
continue
}
const section = (entry.category || '').trim()
items.push({
id: `dashboard:${entry.title}`,
title: entry.title,
description: section,
category: 'Dashboards',
type: 'route',
path: dashboardRoutePath(entry.title),
icon: DashboardSquare01Icon
})
}
return items
}
/**
* Indexes system navigation destinations that the user can already reach from
* the sidebar, respecting Init visibility flags.
*/
export function indexSystemNavigation ({
showLogs = false,
showDiagnostics = false,
entitiesTitle = 'Entities',
logsTitle = 'Logs',
diagnosticsTitle = 'Diagnostics'
} = {}) {
const items = [{
id: 'nav:entities',
title: entitiesTitle,
category: 'Navigation',
type: 'route',
path: '/entities',
icon: CellsIcon
}]
if (showLogs) {
items.push({
id: 'nav:logs',
title: logsTitle,
category: 'Navigation',
type: 'route',
path: '/logs',
icon: LeftToRightListDashIcon
})
}
if (showDiagnostics) {
items.push({
id: 'nav:diagnostics',
title: diagnosticsTitle,
category: 'Navigation',
type: 'route',
path: '/diagnostics',
icon: Wrench01Icon
})
}
replaceSource(SOURCE_NAVIGATION, items)
}