diff --git a/api-users.go b/api-users.go index a4e6214..bbaa9d7 100644 --- a/api-users.go +++ b/api-users.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "os" + "slices" "strings" "time" @@ -949,19 +950,26 @@ func (app *appContext) SearchUsers(gc *gin.Context) { } var resp getUsersDTO - userList, err := app.userCache.Gen(app, false) + userList, err := app.userCache.Gen(app, req.SortByField == USER_DEFAULT_SORT_FIELD) if err != nil { app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) respond(500, "Couldn't get users", gc) return } var filtered []*respUser - if (req.SearchTerms != nil && len(req.SearchTerms) != 0) || (req.Queries != nil && len(req.Queries) != 0) { + if len(req.SearchTerms) != 0 || len(req.Queries) != 0 { filtered = app.userCache.Filter(userList, req.SearchTerms, req.Queries) } else { - filtered = userList + filtered = slices.Clone(userList) + } + + if req.SortByField == USER_DEFAULT_SORT_FIELD { + if req.Ascending != USER_DEFAULT_SORT_ASCENDING { + slices.Reverse(filtered) + } + } else { + app.userCache.Sort(filtered, req.SortByField, req.Ascending) } - app.userCache.Sort(filtered, req.SortByField, req.Ascending) startIndex := (req.Page * req.Limit) if startIndex < len(filtered) { diff --git a/css/tooltip.css b/css/tooltip.css index 85a289e..fc92a1d 100644 --- a/css/tooltip.css +++ b/css/tooltip.css @@ -27,6 +27,12 @@ right: 0; } +.tooltip.above .content { + bottom: 2.5rem; + left: 0; + right: 0; +} + .tooltip.darker .content { background-color: rgba(0, 0, 0, 0.8); } diff --git a/html/admin.html b/html/admin.html index 2a9a8fc..23e70e9 100644 --- a/html/admin.html +++ b/html/admin.html @@ -726,7 +726,10 @@
- + {{ .strings.searchAllRecords }}
@@ -810,20 +813,23 @@
-
- {{ .strings.noResultsFound }} - {{ .strings.keepSearchingDescription }} +
+ {{ .strings.noResultsFound }} + {{ .strings.noResultsFoundLocally }}
-
-
- - +
+ + +
@@ -843,7 +849,7 @@
- + {{ .strings.searchAllRecords }}
@@ -867,9 +873,9 @@
-
- {{ .strings.noResultsFound }} - {{ .strings.keepSearchingDescription }} +
+ {{ .strings.noResultsFound }} + {{ .strings.noResultsFoundLocally }}
-
- - +
+ + +
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index ff3dec1..d420ee8 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -58,6 +58,7 @@ "disabled": "Disabled", "sendPWR": "Send Password Reset", "noResultsFound": "No Results Found", + "noResultsFoundLocally": "Only loaded records were searched. You can load more, or perform the search over all records on the server.", "keepSearching": "Keep Searching", "keepSearchingDescription": "Only the current loaded activities were searched. Click below if you wish to search all activities.", "contactThrough": "Contact through:", @@ -136,6 +137,7 @@ "filters": "Filters", "clickToRemoveFilter": "Click to remove this filter.", "clearSearch": "Clear search", + "searchAll": "Search all", "searchAllRecords": "Search all records (on server)", "actions": "Actions", "searchOptions": "Search Options", diff --git a/scripts/account-gen/main.go b/scripts/account-gen/main.go index 47faa6f..c61928f 100644 --- a/scripts/account-gen/main.go +++ b/scripts/account-gen/main.go @@ -14,8 +14,9 @@ import ( ) var ( - names = []string{"Aaron", "Agnes", "Bridget", "Brandon", "Dolly", "Drake", "Elizabeth", "Erika", "Geoff", "Graham", "Haley", "Halsey", "Josie", "John", "Kayleigh", "Luka", "Melissa", "Nasreen", "Paul", "Ross", "Sam", "Talib", "Veronika", "Zaynab"} - COUNT = 3000 + names = []string{"Aaron", "Agnes", "Bridget", "Brandon", "Dolly", "Drake", "Elizabeth", "Erika", "Geoff", "Graham", "Haley", "Halsey", "Josie", "John", "Kayleigh", "Luka", "Melissa", "Nasreen", "Paul", "Ross", "Sam", "Talib", "Veronika", "Zaynab", "Graig", "Rhoda", "Tyler", "Quentin", "Melinda", "Zelma", "Jack", "Clifton", "Sherry", "Boyce", "Elma", "Jere", "Shelby", "Caitlin", "Bertie", "Mallory", "Thelma", "Charley", "Santo", "Merrill", "Royal", "Jefferson", "Ester", "Dee", "Susanna", "Adriana", "Alfonso", "Lillie", "Carmen", "Federico", "Ernie", "Kory", "Kimberly", "Donn", "Lilian", "Irvin", "Sherri", "Cordell", "Adrienne", "Edwin", "Serena", "Otis", "Latasha", "Johanna", "Clarence", "Noe", "Mindy", "Felix", "Audra"} + COUNT = 4000 + DELAY = 1 * time.Millisecond ) const ( @@ -101,11 +102,12 @@ func main() { rand.Seed(time.Now().Unix()) for i := 0; i < COUNT; i++ { - name := names[rand.Intn(len(names))] + strconv.Itoa(rand.Intn(100)) + name := names[rand.Intn(len(names))] + strconv.Itoa(rand.Intn(500)) user, status, err := jf.NewUser(name, PASSWORD) if (status != 200 && status != 201 && status != 204) || err != nil { - log.Fatalf("Acc no %d: Failed to create user \"%s\" (%d): %+v\n", i, name, status, err) + log.Printf("Acc no %d: Failed to create user \"%s\" (%d): %+v\n", i, name, status, err) + continue } if rand.Intn(100) > 65 { @@ -116,13 +118,17 @@ func main() { user.Policy.IsDisabled = true } + time.Sleep(DELAY / 4) status, err = jf.SetPolicy(user.ID, user.Policy) if (status != 200 && status != 201 && status != 204) || err != nil { log.Fatalf("Acc no %d: Failed to set policy for user \"%s\" (%d): %+v\n", i, name, status, err) } if rand.Intn(100) > 20 { + time.Sleep(DELAY / 4) jfTemp.Authenticate(name, PASSWORD) } + log.Printf("Acc %d done\n", i) + time.Sleep(DELAY / 4) } } diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index a95b14f..46f50b3 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -9,6 +9,9 @@ import { PaginatedList } from "./list.js"; declare var window: GlobalWindow; +const USER_DEFAULT_SORT_FIELD = "name"; +const USER_DEFAULT_SORT_ASCENDING = true; + const dateParser = require("any-date-parser"); interface User { @@ -911,16 +914,14 @@ export class accountsList extends PaginatedList { loadMoreButton: document.getElementById("accounts-load-more") as HTMLButtonElement, loadAllButton: document.getElementById("accounts-load-all") as HTMLButtonElement, refreshButton: document.getElementById("accounts-refresh") as HTMLButtonElement, - keepSearchingDescription: document.getElementById("accounts-keep-searching-description"), - keepSearchingButton: document.getElementById("accounts-keep-searching"), filterArea: document.getElementById("accounts-filter-area"), searchOptionsHeader: document.getElementById("accounts-search-options-header"), searchBox: document.getElementById("accounts-search") as HTMLInputElement, - notFoundPanel: document.getElementById("accounts-not-found"), recordCounter: document.getElementById("accounts-record-counter"), totalEndpoint: "/users/count", getPageEndpoint: "/users", - limit: 4, + itemsPerPage: 40, + maxItemsLoadedForSearch: 200, newElementsFromPage: (resp: paginatedDTO) => { for (let u of ((resp as UsersDTO).users || [])) { if (u.id in this.users) { @@ -962,7 +963,8 @@ export class accountsList extends PaginatedList { this._search.ascending ); }, - defaultSortField: "name", + defaultSortField: USER_DEFAULT_SORT_FIELD, + defaultSortAscending: USER_DEFAULT_SORT_ASCENDING, pageLoadCallback: (req: XMLHttpRequest) => { if (req.readyState != 4) return; // FIXME: Error message @@ -975,7 +977,8 @@ export class accountsList extends PaginatedList { filterArea: this._c.filterArea, sortingByButton: this._sortingByButton, searchOptionsHeader: this._c.searchOptionsHeader, - notFoundPanel: this._c.notFoundPanel, + notFoundPanel: document.getElementById("accounts-not-found"), + notFoundLocallyText: document.getElementById("accounts-no-local-results"), filterList: document.getElementById("accounts-filter-list"), search: this._c.searchBox, queries: queries(), @@ -1188,7 +1191,7 @@ export class accountsList extends PaginatedList { // Start off sorting by username (this._c.defaultSortField) const defaultSort = () => { document.dispatchEvent(new CustomEvent("header-click", { detail: this._c.defaultSortField })); - this._columns[this._c.defaultSortField].ascending = true; + this._columns[this._c.defaultSortField].ascending = this._c.defaultSortAscending; this._columns[this._c.defaultSortField].hideIcon(); this._sortingByButton.parentElement.classList.add("hidden"); this._search.showHideSearchOptionsHeader(); @@ -1209,15 +1212,17 @@ export class accountsList extends PaginatedList { this._search.onSearchBoxChange(); } else { this.setVisibility(this._search.ordering, true); - this._c.notFoundPanel.classList.add("unfocused"); + this._search.setNotFoundPanelVisibility(false); + this._search.setServerSearchButtonsDisabled( + event.detail == this._c.defaultSortField && this._columns[event.detail].ascending == this._c.defaultSortAscending + ); } this._search.showHideSearchOptionsHeader(); }); defaultSort(); - this._search.showHideSearchOptionsHeader(); - this._search.generateFilterList(); + this._search.showHideSearchOptionsHeader(); this.registerURLListener(); } @@ -2168,7 +2173,7 @@ class Column { // Returns the inner HTML to show in the "Sorting By" button. get buttonContent() { - return `` + window.lang.strings("sortingBy") + ": " + `` + this._headerContent; + return `` + window.lang.strings("sortingBy") + ": " + `` + this._headerContent; } get ascending() { return this._ascending; } diff --git a/ts/modules/activity.ts b/ts/modules/activity.ts index 5780962..7f63017 100644 --- a/ts/modules/activity.ts +++ b/ts/modules/activity.ts @@ -6,6 +6,9 @@ import { PaginatedList } from "./list.js"; declare var window: GlobalWindow; +const ACTIVITY_DEFAULT_SORT_FIELD = "time"; +const ACTIVITY_DEFAULT_SORT_ASCENDING = false; + export interface activity { id: string; type: string; @@ -488,16 +491,14 @@ export class activityList extends PaginatedList { loadMoreButton: document.getElementById("activity-load-more") as HTMLButtonElement, loadAllButton: document.getElementById("activity-load-all") as HTMLButtonElement, refreshButton: document.getElementById("activity-refresh") as HTMLButtonElement, - keepSearchingDescription: document.getElementById("activity-keep-searching-description"), - keepSearchingButton: document.getElementById("activity-keep-searching"), filterArea: document.getElementById("activity-filter-area"), searchOptionsHeader: document.getElementById("activity-search-options-header"), searchBox: document.getElementById("activity-search") as HTMLInputElement, - notFoundPanel: document.getElementById("activity-not-found"), recordCounter: document.getElementById("activity-record-counter"), totalEndpoint: "/activity/count", getPageEndpoint: "/activity", - limit: 10, + itemsPerPage: 20, + maxItemsLoadedForSearch: 200, newElementsFromPage: (resp: paginatedDTO) => { let ordering: string[] = this._search.ordering; for (let act of ((resp as ActivitiesDTO).activities || [])) { @@ -513,7 +514,8 @@ export class activityList extends PaginatedList { this._search.setOrdering([], this._c.defaultSortField, this.ascending); this._c.newElementsFromPage(resp); }, - defaultSortField: "time", + defaultSortField: ACTIVITY_DEFAULT_SORT_FIELD, + defaultSortAscending: ACTIVITY_DEFAULT_SORT_ASCENDING, pageLoadCallback: (req: XMLHttpRequest) => { if (req.readyState != 4) return; if (req.status != 200) { @@ -531,7 +533,8 @@ export class activityList extends PaginatedList { // Exclude this: We only sort by date, and don't want to show a redundant header indicating so. // sortingByButton: this._sortingByButton, searchOptionsHeader: this._c.searchOptionsHeader, - notFoundPanel: this._c.notFoundPanel, + notFoundPanel: document.getElementById("activity-not-found"), + notFoundLocallyText: document.getElementById("activity-no-local-results"), search: this._c.searchBox, clearSearchButtonSelector: ".activity-search-clear", serverSearchButtonSelector: ".activity-search-server", @@ -546,7 +549,7 @@ export class activityList extends PaginatedList { this.initSearch(searchConfig); - this.ascending = false; + this.ascending = this._c.defaultSortAscending; this._sortDirection.addEventListener("click", () => this.ascending = !this.ascending); } diff --git a/ts/modules/list.ts b/ts/modules/list.ts index 0978317..414b550 100644 --- a/ts/modules/list.ts +++ b/ts/modules/list.ts @@ -76,19 +76,18 @@ export interface PaginatedListConfig { loadMoreButton: HTMLButtonElement; loadAllButton: HTMLButtonElement; refreshButton: HTMLButtonElement; - keepSearchingDescription: HTMLElement; - keepSearchingButton: HTMLElement; - notFoundPanel: HTMLElement; filterArea: HTMLElement; searchOptionsHeader: HTMLElement; searchBox: HTMLInputElement; recordCounter: HTMLElement; totalEndpoint: string; getPageEndpoint: string; - limit: number; + itemsPerPage: number; + maxItemsLoadedForSearch: number; newElementsFromPage: (resp: paginatedDTO) => void; updateExistingElementsFromPage: (resp: paginatedDTO) => void; defaultSortField: string; + defaultSortAscending: boolean; pageLoadCallback?: (req: XMLHttpRequest) => void; } @@ -109,13 +108,13 @@ export abstract class PaginatedList { if (v) { this._c.loadAllButton.classList.add("unfocused"); this._c.loadMoreButton.textContent = window.lang.strings("noMoreResults"); + this._search.setServerSearchButtonsDisabled(this._search.inServerSearch); this._c.loadMoreButton.disabled = true; } else { this._c.loadMoreButton.textContent = window.lang.strings("loadMore"); this._c.loadMoreButton.disabled = false; - if (this._search.inSearch) { - this._c.loadAllButton.classList.remove("unfocused"); - } + this._search.setServerSearchButtonsDisabled(false); + this._c.loadAllButton.classList.remove("unfocused"); } } @@ -160,9 +159,15 @@ export abstract class PaginatedList { // if (this._search.inSearch && !this.lastPage) this._c.loadAllButton.classList.remove("unfocused"); // else this._c.loadAllButton.classList.add("unfocused"); - + + if (this._search.sortField == this._c.defaultSortField && this._search.ascending == this._c.defaultSortAscending) { + this._search.setServerSearchButtonsDisabled(!this._search.inSearch) + } else { + this._search.setServerSearchButtonsDisabled(false) + } + // FIXME: Figure out why this makes sense and make it clearer. - if ((visibleCount < this._c.limit && !this.lastPage) || loadAll) { + if ((visibleCount < this._c.itemsPerPage && this._counter.loaded < this._c.maxItemsLoadedForSearch && !this.lastPage) || loadAll) { if (!newItems || this._previousPageSize != visibleCount || (visibleCount == 0 && !this.lastPage) || @@ -183,6 +188,7 @@ export abstract class PaginatedList { if (previousServerSearch) previousServerSearch(params, newSearch); }; searchConfig.clearServerSearch = () => { + console.log("Clearing server search"); this._page = 0; this.reload(); } @@ -195,6 +201,7 @@ export abstract class PaginatedList { public abstract setVisibility: (elements: string[], visible: boolean) => void; // Removes all elements, and reloads the first page. + // FIXME: Share more code between reload and loadMore, and go over the logic, it's messy. public abstract reload: () => void; protected _reload = ( callback?: (req: XMLHttpRequest) => void @@ -206,7 +213,7 @@ export abstract class PaginatedList { this._counter.getTotal(this._c.totalEndpoint); // Reload all currently visible elements, i.e. Load a new page of size (limit*(page+1)). - let limit = this._c.limit; + let limit = this._c.itemsPerPage; if (this._page != 0) { limit *= this._page+1; } @@ -216,7 +223,7 @@ export abstract class PaginatedList { params.page = 0; if (params.sortByField == "") { params.sortByField = this._c.defaultSortField; - params.ascending = true; + params.ascending = this._c.defaultSortAscending; } _post(this._c.getPageEndpoint, params, (req: XMLHttpRequest) => { @@ -246,8 +253,7 @@ export abstract class PaginatedList { } else { this._counter.shown = this._counter.loaded; this.setVisibility(this._search.ordering, true); - this._c.loadAllButton.classList.add("unfocused"); - this._c.notFoundPanel.classList.add("unfocused"); + // this._search.showHideNotFoundPanel(false); } if (this._c.pageLoadCallback) this._c.pageLoadCallback(req); if (callback) callback(req); @@ -268,11 +274,11 @@ export abstract class PaginatedList { this._page += 1; let params = this._search.inServerSearch ? this._searchParams : this.defaultParams(); - params.limit = this._c.limit; + params.limit = this._c.itemsPerPage; params.page = this._page; if (params.sortByField == "") { params.sortByField = this._c.defaultSortField; - params.ascending = true; + params.ascending = this._c.defaultSortAscending; } _post(this._c.getPageEndpoint, params, (req: XMLHttpRequest) => { @@ -304,7 +310,7 @@ export abstract class PaginatedList { this._search.onSearchBoxChange(true, loadAll); } else { this.setVisibility(this._search.ordering, true); - this._c.notFoundPanel.classList.add("unfocused"); + this._search.setNotFoundPanelVisibility(false); } if (this._c.pageLoadCallback) this._c.pageLoadCallback(req); if (callback) callback(req); diff --git a/ts/modules/search.ts b/ts/modules/search.ts index e93b653..92f99ec 100644 --- a/ts/modules/search.ts +++ b/ts/modules/search.ts @@ -35,6 +35,7 @@ export interface SearchConfiguration { sortingByButton?: HTMLButtonElement; searchOptionsHeader: HTMLElement; notFoundPanel: HTMLElement; + notFoundLocallyText: HTMLElement; notFoundCallback?: (notFound: boolean) => void; filterList: HTMLElement; clearSearchButtonSelector: string; @@ -91,6 +92,16 @@ export abstract class Query { out.operator = this._operator; return out; } + + get subject(): QueryType { return this._subject; } + + getValueFromItem(item: SearchableItem): any { + return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(item), this.subject.getter).get.call(item); + } + + compareItem(item: SearchableItem): boolean { + return this.compare(this.getValueFromItem(item)); + } } export class BoolQuery extends Query { @@ -278,30 +289,19 @@ export class Search { private _inServerSearch: boolean = false; get inServerSearch(): boolean { return this._inServerSearch; } set inServerSearch(v: boolean) { + const previous = this._inServerSearch; this._inServerSearch = v; - if (!v) { + if (!v && previous != v) { this._c.clearServerSearch(); } } private _serverSearchButtons: HTMLElement[]; - // Returns a list of identifiers (keys in items, values in ordering). - search = (query: String): string[] => { - this._c.filterArea.textContent = ""; - + static tokenizeSearch = (query: string): string[] => { query = query.toLowerCase(); - let result: string[] = [...this._ordering]; - // If we're in a server search already, the results are already correct. - if (this.inServerSearch) return result; - - let words: string[] = []; - let queries = []; - let searchTerms = []; - - let quoteSymbol = ``; let queryStart = -1; let lastQuote = -1; @@ -335,19 +335,17 @@ export class Search { } } } + return words; + } - query = ""; - for (let word of words) { + parseTokens = (tokens: string[]): [string[], Query[]] => { + let queries: Query[] = []; + let searchTerms: string[] = []; + + for (let word of tokens) { // 1. Normal search text, no filters or anything if (!word.includes(":")) { searchTerms.push(word); - let cachedResult = [...result]; - for (let id of cachedResult) { - const u = this.items[id]; - if (!u.matchesSearch(word)) { - result.splice(result.indexOf(id), 1); - } - } continue; } // 2. A filter query of some sort. @@ -369,21 +367,6 @@ export class Search { } this._c.search.oninput((null as Event)); }; - - this._c.filterArea.appendChild(q.asElement()); - - // So removing elements doesn't affect us - let cachedResult = [...result]; - for (let id of cachedResult) { - const u = this.items[id]; - const value = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u); - // Remove from result if not matching query - if (!q.compare(value)) { - // console.log("not matching, result is", result); - result.splice(result.indexOf(id), 1); - } else { - } - } } } else if (queryFormat.string) { q = new StringQuery(queryFormat, split[1]); @@ -395,17 +378,6 @@ export class Search { } this._c.search.oninput((null as Event)); } - - this._c.filterArea.appendChild(q.asElement()); - - let cachedResult = [...result]; - for (let id of cachedResult) { - const u = this.items[id]; - const value = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u).toLowerCase(); - if (!q.compare(value)) { - result.splice(result.indexOf(id), 1); - } - } } else if (queryFormat.date) { let [parsedDate, op, isDate] = DateQuery.paramsFromString(split[1]); if (!isDate) continue; @@ -419,13 +391,61 @@ export class Search { this._c.search.oninput((null as Event)); } - - this._c.filterArea.appendChild(q.asElement()); + } + + if (q != null) queries.push(q); + } + return [searchTerms, queries]; + } - let cachedResult = [...result]; + // Returns a list of identifiers (used as keys in items, values in ordering). + search = (query: string): string[] => { + this._c.filterArea.textContent = ""; + + let result: string[] = [...this._ordering]; + // If we're in a server search already, the results are already correct. + if (this.inServerSearch) return result; + + const [searchTerms, queries] = this.parseTokens(Search.tokenizeSearch(query)); + + query = ""; + + for (let term of searchTerms) { + let cachedResult = [...result]; + for (let id of cachedResult) { + const u = this.items[id]; + if (!u.matchesSearch(term)) { + result.splice(result.indexOf(id), 1); + } + } + } + for (let q of queries) { + this._c.filterArea.appendChild(q.asElement()); + let cachedResult = [...result]; + if (q.subject.bool) { for (let id of cachedResult) { const u = this.items[id]; - const unixValue = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u); + // Remove from result if not matching query + if (!q.compareItem(u)) { + // console.log("not matching, result is", result); + result.splice(result.indexOf(id), 1); + } + } + } else if (q.subject.string) { + for (let id of cachedResult) { + const u = this.items[id]; + // We want to compare case-insensitively, so we get value, lower-case it then compare, + // rather than doing both with compareItem. + const value = q.getValueFromItem(u).toLowerCase(); + if (!q.compare(value)) { + result.splice(result.indexOf(id), 1); + } + } + } else if(q.subject.date) { + for (let id of cachedResult) { + const u = this.items[id]; + // Getter here returns a unix timestamp rather than a date, so we can't use compareItem. + const unixValue = q.getValueFromItem(u); if (unixValue == 0) { result.splice(result.indexOf(id), 1); continue; @@ -437,12 +457,11 @@ export class Search { } } } - - if (q != null) queries.push(q); } this._queries = queries; this._searchTerms = searchTerms; + return result; } @@ -486,25 +505,37 @@ export class Search { const results = this.search(query); this._c.setVisibility(results, true); this._c.onSearchCallback(results.length, newItems, loadAll); - if (this.inServerSearch) { - this._serverSearchButtons.forEach((v: HTMLElement) => { - v.classList.add("@low"); - v.classList.remove("@high"); - }); - } else { - this._serverSearchButtons.forEach((v: HTMLElement) => { - v.classList.add("@high"); - v.classList.remove("@low"); - }); + if (this.inSearch) { + if (this.inServerSearch) { + this._serverSearchButtons.forEach((v: HTMLElement) => { + v.classList.add("@low"); + v.classList.remove("@high"); + }); + } else { + this._serverSearchButtons.forEach((v: HTMLElement) => { + v.classList.add("@high"); + v.classList.remove("@low"); + }); + } } this.showHideSearchOptionsHeader(); - if (results.length == 0) { + this.setNotFoundPanelVisibility(results.length == 0); + if (this._c.notFoundCallback) this._c.notFoundCallback(results.length == 0); + } + + setNotFoundPanelVisibility = (visible: boolean) => { + if (this._inServerSearch || !this.inSearch) { + this._c.notFoundLocallyText.classList.add("unfocused"); + } else if (this.inSearch) { + this._c.notFoundLocallyText.classList.remove("unfocused"); + } + if (visible) { + console.log("showing not found panel"); this._c.notFoundPanel.classList.remove("unfocused"); } else { + console.log("hiding not found panel"); this._c.notFoundPanel.classList.add("unfocused"); - } - if (this._c.notFoundCallback) this._c.notFoundCallback(results.length == 0); } fillInFilter = (name: string, value: string, offset?: number) => { @@ -610,6 +641,10 @@ export class Search { return req; } + setServerSearchButtonsDisabled = (disabled: boolean) => { + this._serverSearchButtons.forEach((v: HTMLButtonElement) => v.disabled = disabled); + } + constructor(c: SearchConfiguration) { // FIXME: Remove! if (c.search.id.includes("accounts")) { diff --git a/usercache.go b/usercache.go index 9b2e050..7864667 100644 --- a/usercache.go +++ b/usercache.go @@ -11,8 +11,12 @@ import ( const ( // FIXME: Follow mediabrowser, or make tuneable, or both - WEB_USER_CACHE_SYNC = 30 * time.Second - USER_DEFAULT_SORT_FIELD = "name" + // After cache is this old, re-sync, but do it in the background and return the old cache. + WEB_USER_CACHE_SYNC = 30 * time.Second + // After cache is this old, re-sync and wait for it and return the new cache. + WEB_USER_CACHE_WAIT_FOR_SYNC = 5 * time.Minute + USER_DEFAULT_SORT_FIELD = "name" + USER_DEFAULT_SORT_ASCENDING = true ) type UserCache struct { @@ -20,30 +24,67 @@ type UserCache struct { Ref []*respUser Sorted bool LastSync time.Time - Lock sync.Mutex + SyncLock sync.Mutex + Syncing bool + SortLock sync.Mutex + Sorting bool } +// FIXME: If shouldSync, sync in background and return old version. If shouldWaitForSync, wait for sync and return new one. +// FIXME: If locked, just wait for unlock and return someone elses work. func (c *UserCache) gen(app *appContext) error { - // FIXME: I don't like this. - if !time.Now().After(c.LastSync.Add(WEB_USER_CACHE_SYNC)) { + shouldWaitForSync := time.Now().After(c.LastSync.Add(WEB_USER_CACHE_WAIT_FOR_SYNC)) || c.Ref == nil || len(c.Ref) == 0 + shouldSync := time.Now().After(c.LastSync.Add(WEB_USER_CACHE_SYNC)) + + if !shouldSync { return nil } - c.Lock.Lock() - users, err := app.jf.GetUsers(false) - if err != nil { + + syncStatus := make(chan error) + + go func(status chan error, c *UserCache) { + c.SyncLock.Lock() + alreadySyncing := c.Syncing + // We're either already syncing or will be + c.Syncing = true + c.SyncLock.Unlock() + if !alreadySyncing { + users, err := app.jf.GetUsers(false) + if err != nil { + c.SyncLock.Lock() + c.Syncing = false + c.SyncLock.Unlock() + status <- err + return + } + cache := make([]respUser, len(users)) + for i, jfUser := range users { + cache[i] = app.userSummary(jfUser) + } + ref := make([]*respUser, len(cache)) + for i := range cache { + ref[i] = &(cache[i]) + } + c.Cache = cache + c.Ref = ref + c.Sorted = false + c.LastSync = time.Now() + + c.SyncLock.Lock() + c.Syncing = false + c.SyncLock.Unlock() + } else { + for c.Syncing { + continue + } + } + status <- nil + }(syncStatus, c) + + if shouldWaitForSync { + err := <-syncStatus return err } - c.Cache = make([]respUser, len(users)) - for i, jfUser := range users { - c.Cache[i] = app.userSummary(jfUser) - } - c.Ref = make([]*respUser, len(c.Cache)) - for i := range c.Cache { - c.Ref[i] = &(c.Cache[i]) - } - c.Sorted = false - c.LastSync = time.Now() - c.Lock.Unlock() return nil } @@ -52,11 +93,21 @@ func (c *UserCache) Gen(app *appContext, sorted bool) ([]*respUser, error) { return nil, err } if sorted && !c.Sorted { - c.Lock.Lock() - // FIXME: Check we want ascending! - c.Sort(c.Ref, USER_DEFAULT_SORT_FIELD, true) - c.Sorted = true - c.Lock.Unlock() + c.SortLock.Lock() + alreadySorting := c.Sorting + c.Sorting = true + c.SortLock.Unlock() + if !alreadySorting { + c.Sort(c.Ref, USER_DEFAULT_SORT_FIELD, USER_DEFAULT_SORT_ASCENDING) + c.Sorted = true + c.SortLock.Lock() + c.Sorting = false + c.SortLock.Unlock() + } else { + for c.Sorting { + continue + } + } } return c.Ref, nil }