diff --git a/docs/configuring.md b/docs/configuring.md index 62b876f7ab..4670130585 100644 --- a/docs/configuring.md +++ b/docs/configuring.md @@ -294,6 +294,7 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)** **`timeout`** | `number` | _Optional_ | Request timeout in milliseconds, defaults to ½ a second (`500`) **`ignoreErrors`** | `boolean` | _Optional_ | Prevent an error message being displayed, if a network request or something else fails. Useful for false-positives **`label`** | `string` | _Optional_ | Add custom label to a given widget. Useful for identification, if there are multiple of the same type of widget in a single section +**`category`** | string | _Optional_ | Free-text category for this widget (e.g., Monitoring, CI/CD). Used by category filtering/search. Up to the user whatever category they want to set for a widget. **[⬆️ Back to Top](#configuring)** @@ -317,6 +318,7 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)** **`hideForGuests`** | `boolean` | _Optional_ | Current section will be visible for logged in users, but not for guests (see `appConfig.enableGuestAccess`). Defaults to `false` **`hideForKeycloakUsers`** | `object` | _Optional_ | Current section will be visible to all keycloak users, except for those configured via these groups and roles. See `hideForKeycloakUsers` **`showForKeycloakUsers`** | `object` | _Optional_ | Current section will be hidden from all keycloak users, except for those configured via these groups and roles. See `showForKeycloakUsers` +**`widgetCategories`** | string[] | _Optional_ | Free-text tags for this section, used for category filtering/search of widgets `section.widgets` from within the UI. Editable in the Edit Section modal. Example: ["DevOps", "Monitoring"]. This will work only if corrosponding widget category is setup in the widgets 'category' field. **[⬆️ Back to Top](#configuring)** diff --git a/src/components/InteractiveEditor/EditSection.vue b/src/components/InteractiveEditor/EditSection.vue index 7222a7a7ef..f5228c405c 100644 --- a/src/components/InteractiveEditor/EditSection.vue +++ b/src/components/InteractiveEditor/EditSection.vue @@ -69,6 +69,7 @@ export default { cols: displayDataSchema.cols, collapsed: displayDataSchema.collapsed, hideForGuests: displayDataSchema.hideForGuests, + widgetCategories: displayDataSchema.widgetCategories, }, }, }, diff --git a/src/mixins/HomeMixin.js b/src/mixins/HomeMixin.js index 6f163f88ef..a06da7088e 100644 --- a/src/mixins/HomeMixin.js +++ b/src/mixins/HomeMixin.js @@ -103,7 +103,11 @@ const HomeMixin = { return []; } const visibleTiles = allTiles.filter((tile) => checkItemVisibility(tile)); - return searchTiles(visibleTiles, this.searchValue); + return searchTiles(visibleTiles, this.searchValue, this.areWidgets(allTiles)); + }, + /* Checks if titles are widgets */ + areWidgets(allTiles) { + return Array.isArray(allTiles) && allTiles.length > 0 && !('title' in allTiles[0]); }, /* Checks if any sections or items use icons from a given CDN */ checkIfIconLibraryNeeded(prefix) { diff --git a/src/utils/ConfigSchema.json b/src/utils/ConfigSchema.json index 92039798ab..e463aed0ea 100644 --- a/src/utils/ConfigSchema.json +++ b/src/utils/ConfigSchema.json @@ -825,6 +825,16 @@ "default": 1, "description": "The amount of space that the section spans horizontally" }, + "widgetCategories": { + "type": "array", + "title": "Widget Categories", + "description": "Tags for this section (add/remove text tags).", + "items": { + "type": "string" + }, + "uniqueItems": false, + "default": [] + }, "sectionLayout": { "title": "Layout Type", "type": "string", @@ -1170,6 +1180,11 @@ "title": "Widget Options", "type": "object", "description": "Configuration options for widget. Varies depending on widget type, see docs for all options" + }, + "category": { + "title": "Category", + "type": "string", + "description": "Single category tag." } } } diff --git a/src/utils/Search.js b/src/utils/Search.js index 99f3248403..0cdce3e53a 100644 --- a/src/utils/Search.js +++ b/src/utils/Search.js @@ -35,22 +35,31 @@ const filterHelper = (compareStr, searchStr) => { * @param {array} allTiles An array of tiles * @param {string} searchTerm The users search term * @returns A filtered array of tiles + * if widgets are sent as allTiles, then the search is done based on options.category */ -export const searchTiles = (allTiles, searchTerm) => { +export const searchTiles = (allTiles, searchTerm, isWidgets) => { if (!searchTerm) return allTiles; // If no search term, then return all if (!allTiles) return []; // If no data, then skip - 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); - }); + if (!isWidgets) { + 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); + }); + } else { + return allTiles.filter((tile) => { + const { + category, + } = tile; + return filterHelper(category, searchTerm); + }); + } }; - /* From a list of search bangs, return the URL associated with it */ export const getSearchEngineFromBang = (searchQuery, bangList) => { const bangNames = Object.keys(bangList); diff --git a/src/views/Home.vue b/src/views/Home.vue index 004b181ac9..8a960c8006 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -29,13 +29,15 @@ :displayData="getDisplayData(section)" :groupId="makeSectionId(section)" :items="section.filteredItems" - :widgets="section.widgets" + :widgets="section.filteredWidgets" :searchTerm="searchValue" :itemSize="itemSizeBound" @itemClicked="finishedSearching()" @change-modal-visibility="updateModalVisibility" :isWide="!!singleSectionView || layoutOrientation === 'horizontal'" - :class="(searchValue && section.filteredItems.length === 0) ? 'no-results' : ''" + :class="(searchValue && + (section.filteredItems.length === 0 && + section.filteredWidgets.length === 0)) ? 'no-results' : ''" /> @@ -102,6 +104,15 @@ export default { return sections.map((_section) => { const section = _section; section.filteredItems = this.filterTiles(section.items, this.searchValue); + + const searchedWidgets = this.filterTiles(section.widgets, this.searchValue); + const widgetCategoriesArray = this.normalizeWidgetCats( + this.getDisplayData(section).widgetCategories, + ); + section.filteredWidgets = this.filterWidgetsByCategories( + searchedWidgets, widgetCategoriesArray, + ); + return section; }); }, @@ -123,6 +134,18 @@ export default { }, }, methods: { + /* returns only widgets that have category that are present in + widgetCategories array in section config displayData */ + filterWidgetsByCategories(widgets, cats) { + if (!cats.length) return widgets || []; + return (widgets || []).filter(w => (typeof w.category === 'string' + && cats.includes(w.category.toLowerCase().trim()))); + }, + /* Normalization of widget categories text inputs by user */ + normalizeWidgetCats(cats) { + if (!Array.isArray(cats)) return []; + return cats.map(c => String(c).toLowerCase().trim()).filter(Boolean); + }, /* Clears input field, once a searched item is opened */ finishedSearching() { if (this.$refs.filterComp) this.$refs.filterComp.clearFilterInput();