From 4da1e6dbabdc3045ff2c1f1c1867387dfefde9f4 Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Tue, 2 Jan 2024 00:08:15 +0100 Subject: [PATCH 01/46] fix(auth): invalid cookie handling and wrongful basic auth invalidation --- internal/http/auth.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/http/auth.go b/internal/http/auth.go index 4603561666..59ccabbd4b 100644 --- a/internal/http/auth.go +++ b/internal/http/auth.go @@ -81,16 +81,18 @@ func (h authHandler) login(w http.ResponseWriter, r *http.Request) { if _, err := h.service.Login(r.Context(), data.Username, data.Password); err != nil { h.log.Error().Err(err).Msgf("Auth: Failed login attempt username: [%s] ip: %s", data.Username, r.RemoteAddr) - h.encoder.StatusError(w, http.StatusUnauthorized, errors.New("could not login: bad credentials")) + h.encoder.StatusError(w, http.StatusForbidden, errors.New("could not login: bad credentials")) return } // create new session - session, err := h.cookieStore.New(r, "user_session") + session, err := h.cookieStore.Get(r, "user_session") if err != nil { - h.log.Error().Err(err).Msgf("Auth: Failed to parse cookies with attempt username: [%s] ip: %s", data.Username, r.RemoteAddr) - h.encoder.StatusError(w, http.StatusUnauthorized, errors.New("could not parse cookies")) - return + err := sessions.Save(r, w) + if err != nil { + h.encoder.StatusError(w, http.StatusInternalServerError, errors.Wrap(err, "could not get user session or replace with valid session")) + return + } } // Set user as authenticated From c212da8ab9fc019f68fa26bc1fa9d52ee35c0af6 Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Tue, 2 Jan 2024 00:52:11 +0100 Subject: [PATCH 02/46] fix(auth): fix test to reflect new HTTP status code --- internal/http/auth_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/http/auth_test.go b/internal/http/auth_test.go index b5ab2709af..52c693ec2f 100644 --- a/internal/http/auth_test.go +++ b/internal/http/auth_test.go @@ -321,8 +321,8 @@ func TestAuthHandlerLoginBad(t *testing.T) { defer resp.Body.Close() // check for response, here we'll just check for 204 NoContent - if status := resp.StatusCode; status != http.StatusUnauthorized { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusUnauthorized) + if status := resp.StatusCode; status != http.StatusForbidden { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusForbidden) } } From 171ea93e336d2ffbe25ab84126c716feb51a1463 Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Tue, 2 Jan 2024 17:32:10 +0100 Subject: [PATCH 03/46] fix(auth/web): do not throw on error --- web/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 1487a55626..b018e18433 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -21,7 +21,7 @@ const queryClient = new QueryClient({ // See https://tanstack.com/query/v4/docs/guides/query-retries#retry-delay // delay = Math.min(1000 * 2 ** attemptIndex, 30000) retry: true, - throwOnError: true, + throwOnError: false, }, mutations: { onError: (error) => { From 808396798984a3f23ec1a31cf42c09dd40246e68 Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Tue, 2 Jan 2024 19:22:03 +0100 Subject: [PATCH 04/46] fix(http): replace http codes in middleware to prevent basic auth invalidation fix typo in comment --- internal/http/middleware.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/http/middleware.go b/internal/http/middleware.go index 71d87e3fae..ce9dc569a9 100644 --- a/internal/http/middleware.go +++ b/internal/http/middleware.go @@ -24,7 +24,7 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler { } } else if key := r.URL.Query().Get("apikey"); key != "" { - // check query param lke ?apikey=TOKEN + // check query param like ?apikey=TOKEN if !s.apiService.ValidateAPIKey(r.Context(), key) { http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return @@ -33,18 +33,18 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler { // check session session, err := s.cookieStore.Get(r, "user_session") if err != nil { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if session.IsNew { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) return } // Check if user is authenticated if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) return } From 7e9bb1976686ab72aa2b313925af0b0a1cd541ad Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Tue, 2 Jan 2024 19:28:41 +0100 Subject: [PATCH 05/46] fix test --- internal/http/auth_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/http/auth_test.go b/internal/http/auth_test.go index 52c693ec2f..cea21f67cc 100644 --- a/internal/http/auth_test.go +++ b/internal/http/auth_test.go @@ -272,8 +272,8 @@ func TestAuthHandlerValidateBad(t *testing.T) { defer resp.Body.Close() - if status := resp.StatusCode; status != http.StatusUnauthorized { - t.Errorf("validate: handler returned wrong status code: got %v want %v", status, http.StatusUnauthorized) + if status := resp.StatusCode; status != http.StatusForbidden { + t.Errorf("validate: handler returned wrong status code: got %v want %v", status, http.StatusForbidden) } } From ed674e86b8b99b10928fe2082dedfbb3da6ae069 Mon Sep 17 00:00:00 2001 From: ze0s Date: Tue, 2 Jan 2024 22:24:36 +0100 Subject: [PATCH 06/46] fix(web): api client handle 403 --- web/src/api/APIClient.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index 9c325a122f..fbcf00fa45 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -89,10 +89,15 @@ export async function HttpClient( case 401: { // Remove auth info from localStorage AuthContext.reset(); - } - // Show an error toast to notify the user what occurred return Promise.reject(new Error(`[401] Unauthorized: "${endpoint}"`)); + } + case 403: { + // Remove auth info from localStorage + AuthContext.reset(); + // Show an error toast to notify the user what occurred + return Promise.reject(new Error(`[403] Forbidden: "${endpoint}"`)); + } case 404: { return Promise.reject(new Error(`[404] Not found: "${endpoint}"`)); } From 0c527eff0e32e16ea57d45cc48ea31feaea0f890 Mon Sep 17 00:00:00 2001 From: ze0s Date: Tue, 2 Jan 2024 22:33:39 +0100 Subject: [PATCH 07/46] refactor(http): auth_test use testify.assert --- internal/http/auth_test.go | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/internal/http/auth_test.go b/internal/http/auth_test.go index cea21f67cc..089ece21fe 100644 --- a/internal/http/auth_test.go +++ b/internal/http/auth_test.go @@ -21,6 +21,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/gorilla/sessions" "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" ) type authServiceMock struct { @@ -144,9 +145,7 @@ func TestAuthHandlerLogin(t *testing.T) { defer resp.Body.Close() // check for response, here we'll just check for 204 NoContent - if status := resp.StatusCode; status != http.StatusNoContent { - t.Errorf("login: handler returned wrong status code: got %v want %v", status, http.StatusNoContent) - } + assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "login handler: unexpected http status") if v := resp.Header.Get("Set-Cookie"); v == "" { t.Errorf("handler returned no cookie") @@ -207,12 +206,10 @@ func TestAuthHandlerValidateOK(t *testing.T) { defer resp.Body.Close() // check for response, here we'll just check for 204 NoContent - if status := resp.StatusCode; status != http.StatusNoContent { - t.Errorf("login: handler returned wrong status code: got %v want %v", status, http.StatusNoContent) - } + assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "login handler: bad response") if v := resp.Header.Get("Set-Cookie"); v == "" { - t.Errorf("handler returned no cookie") + assert.Equalf(t, "", v, "login handler: expected Set-Cookie header") } // validate token @@ -223,9 +220,7 @@ func TestAuthHandlerValidateOK(t *testing.T) { defer resp.Body.Close() - if status := resp.StatusCode; status != http.StatusNoContent { - t.Errorf("validate: handler returned wrong status code: got %v want %v", status, http.StatusNoContent) - } + assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "validate handler: unexpected http status") } func TestAuthHandlerValidateBad(t *testing.T) { @@ -272,9 +267,7 @@ func TestAuthHandlerValidateBad(t *testing.T) { defer resp.Body.Close() - if status := resp.StatusCode; status != http.StatusForbidden { - t.Errorf("validate: handler returned wrong status code: got %v want %v", status, http.StatusForbidden) - } + assert.Equalf(t, http.StatusForbidden, resp.StatusCode, "validate handler: unexpected http status") } func TestAuthHandlerLoginBad(t *testing.T) { @@ -321,9 +314,7 @@ func TestAuthHandlerLoginBad(t *testing.T) { defer resp.Body.Close() // check for response, here we'll just check for 204 NoContent - if status := resp.StatusCode; status != http.StatusForbidden { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusForbidden) - } + assert.Equalf(t, http.StatusForbidden, resp.StatusCode, "login handler: unexpected http status") } func TestAuthHandlerLogout(t *testing.T) { @@ -384,6 +375,8 @@ func TestAuthHandlerLogout(t *testing.T) { t.Errorf("login: handler returned wrong status code: got %v want %v", status, http.StatusNoContent) } + assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "login handler: unexpected http status") + if v := resp.Header.Get("Set-Cookie"); v == "" { t.Errorf("handler returned no cookie") } @@ -396,9 +389,7 @@ func TestAuthHandlerLogout(t *testing.T) { defer resp.Body.Close() - if status := resp.StatusCode; status != http.StatusNoContent { - t.Errorf("validate: handler returned wrong status code: got %v want %v", status, http.StatusNoContent) - } + assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "validate handler: unexpected http status") // logout resp, err = client.Post(testServer.URL+"/auth/logout", "application/json", nil) @@ -408,9 +399,7 @@ func TestAuthHandlerLogout(t *testing.T) { defer resp.Body.Close() - if status := resp.StatusCode; status != http.StatusNoContent { - t.Errorf("logout: handler returned wrong status code: got %v want %v", status, http.StatusNoContent) - } + assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "logout handler: unexpected http status") //if v := resp.Header.Get("Set-Cookie"); v != "" { // t.Errorf("logout handler returned cookie") From cc1300127e16875abe517a4f0a22d09bf75768a5 Mon Sep 17 00:00:00 2001 From: ze0s Date: Wed, 3 Jan 2024 00:33:09 +0100 Subject: [PATCH 08/46] refactor(http): set session opts after valid login --- internal/http/auth.go | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/internal/http/auth.go b/internal/http/auth.go index 59ccabbd4b..f77fc37e0b 100644 --- a/internal/http/auth.go +++ b/internal/http/auth.go @@ -66,19 +66,6 @@ func (h authHandler) login(w http.ResponseWriter, r *http.Request) { return } - h.cookieStore.Options.HttpOnly = true - h.cookieStore.Options.SameSite = http.SameSiteLaxMode - h.cookieStore.Options.Path = h.config.BaseURL - - // autobrr does not support serving on TLS / https, so this is only available behind reverse proxy - // if forwarded protocol is https then set cookie secure - // SameSite Strict can only be set with a secure cookie. So we overwrite it here if possible. - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite - if r.Header.Get("X-Forwarded-Proto") == "https" { - h.cookieStore.Options.Secure = true - h.cookieStore.Options.SameSite = http.SameSiteStrictMode - } - if _, err := h.service.Login(r.Context(), data.Username, data.Password); err != nil { h.log.Error().Err(err).Msgf("Auth: Failed login attempt username: [%s] ip: %s", data.Username, r.RemoteAddr) h.encoder.StatusError(w, http.StatusForbidden, errors.New("could not login: bad credentials")) @@ -88,16 +75,28 @@ func (h authHandler) login(w http.ResponseWriter, r *http.Request) { // create new session session, err := h.cookieStore.Get(r, "user_session") if err != nil { - err := sessions.Save(r, w) - if err != nil { - h.encoder.StatusError(w, http.StatusInternalServerError, errors.Wrap(err, "could not get user session or replace with valid session")) - return - } + h.log.Error().Err(err).Msgf("could not get session from cookieStore: %s", r.RemoteAddr) + h.encoder.StatusError(w, http.StatusInternalServerError, err) + return } // Set user as authenticated session.Values["authenticated"] = true + // Set cookie options + session.Options.HttpOnly = true + session.Options.SameSite = http.SameSiteLaxMode + session.Options.Path = h.config.BaseURL + + // autobrr does not support serving on TLS / https, so this is only available behind reverse proxy + // if forwarded protocol is https then set cookie secure + // SameSite Strict can only be set with a secure cookie. So we overwrite it here if possible. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + if r.Header.Get("X-Forwarded-Proto") == "https" { + session.Options.Secure = true + session.Options.SameSite = http.SameSiteStrictMode + } + if err := session.Save(r, w); err != nil { h.encoder.StatusError(w, http.StatusInternalServerError, errors.Wrap(err, "could not save session")) return From 09b86e8286a06ff40ba1055c4db8c7f31ccc9c36 Mon Sep 17 00:00:00 2001 From: ze0s Date: Wed, 3 Jan 2024 14:11:12 +0100 Subject: [PATCH 09/46] refactor(http): send more client headers --- internal/http/middleware.go | 9 +++++++-- web/src/api/APIClient.ts | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/http/middleware.go b/internal/http/middleware.go index ce9dc569a9..e8b15e71d1 100644 --- a/internal/http/middleware.go +++ b/internal/http/middleware.go @@ -33,21 +33,26 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler { // check session session, err := s.cookieStore.Get(r, "user_session") if err != nil { + s.log.Error().Err(err).Msgf("could not get session from cookieStore") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if session.IsNew { - http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + s.log.Warn().Msgf("session isNew: %+v", session) + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } // Check if user is authenticated if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { - http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + s.log.Warn().Msg("session not authenticated") + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } + s.log.Debug().Msgf("session ok: %+v", session) + ctx := context.WithValue(r.Context(), "session", session) r = r.WithContext(ctx) } diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index fbcf00fa45..aa8c683c8f 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -30,7 +30,8 @@ export async function HttpClient( ): Promise { const init: RequestInit = { method: config.method, - headers: { "Accept": "*/*" } + headers: { "Accept": "*/*", 'x-requested-with': 'XMLHttpRequest' }, + credentials: "include", }; if (config.body) { From 0ee395ee31cee73ad0682c5d25412ff4f9f498f3 Mon Sep 17 00:00:00 2001 From: ze0s Date: Wed, 3 Jan 2024 14:14:04 +0100 Subject: [PATCH 10/46] fix(http): test --- internal/http/auth_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/http/auth_test.go b/internal/http/auth_test.go index 089ece21fe..0ba54c6cf7 100644 --- a/internal/http/auth_test.go +++ b/internal/http/auth_test.go @@ -267,7 +267,7 @@ func TestAuthHandlerValidateBad(t *testing.T) { defer resp.Body.Close() - assert.Equalf(t, http.StatusForbidden, resp.StatusCode, "validate handler: unexpected http status") + assert.Equalf(t, http.StatusUnauthorized, resp.StatusCode, "validate handler: unexpected http status") } func TestAuthHandlerLoginBad(t *testing.T) { From 47423a6168e6b35d260847170171424b51a592d5 Mon Sep 17 00:00:00 2001 From: ze0s Date: Thu, 4 Jan 2024 23:39:01 +0100 Subject: [PATCH 11/46] refactor(web): move router to tanstack/router --- web/package.json | 6 +- web/pnpm-lock.yaml | 75 +- web/src/App.tsx | 368 +++++++- web/src/api/APIClient.ts | 12 +- web/src/components/header/Header.tsx | 28 +- web/src/components/header/LeftNav.tsx | 36 +- web/src/components/header/RightNav.tsx | 6 +- web/src/components/header/_shared.ts | 3 + web/src/screens/Logs.tsx | 4 +- web/src/screens/Settings.tsx | 74 +- web/src/screens/auth/Login.tsx | 63 +- web/src/screens/dashboard/ActivityTable.tsx | 4 +- web/src/screens/dashboard/Stats.tsx | 12 +- web/src/screens/filters/Details.tsx | 99 ++- web/src/screens/filters/List.tsx | 41 +- web/src/screens/filters/sections/Actions.tsx | 6 +- web/src/screens/filters/sections/Advanced.tsx | 828 +++++++++--------- web/src/screens/filters/sections/Music.tsx | 286 +++--- web/src/screens/releases/ReleaseTable.tsx | 15 +- web/src/screens/settings/Api.tsx | 4 +- web/src/screens/settings/DownloadClient.tsx | 4 +- web/src/screens/settings/Feed.tsx | 4 +- web/src/screens/settings/Indexer.tsx | 4 +- web/src/screens/settings/Irc.tsx | 4 +- web/src/screens/settings/Logs.tsx | 8 +- web/src/screens/settings/Notifications.tsx | 4 +- web/src/utils/index.ts | 3 +- 27 files changed, 1285 insertions(+), 716 deletions(-) diff --git a/web/package.json b/web/package.json index dd4a76229b..85007109b7 100644 --- a/web/package.json +++ b/web/package.json @@ -39,6 +39,7 @@ "@tailwindcss/forms": "^0.5.7", "@tanstack/react-query": "^5.17.0", "@tanstack/react-query-devtools": "^5.8.4", + "@tanstack/react-router": "^1.1.4", "@types/node": "^20.10.6", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", @@ -76,6 +77,8 @@ "zod-formik-adapter": "^1.2.0" }, "devDependencies": { + "@microsoft/eslint-formatter-sarif": "^3.0.0", + "@tanstack/router-devtools": "^1.1.4", "@types/node": "^20.10.6", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", @@ -94,7 +97,6 @@ "typescript": "^5.3.3", "vite": "^5.0.4", "vite-plugin-pwa": "^0.17.4", - "vite-plugin-svgr": "^4.2.0", - "@microsoft/eslint-formatter-sarif": "^3.0.0" + "vite-plugin-svgr": "^4.2.0" } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index f3ce9289c9..ebed3d57b0 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -30,6 +30,9 @@ dependencies: '@tanstack/react-query-devtools': specifier: ^5.8.4 version: 5.8.4(@tanstack/react-query@5.17.0)(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-router': + specifier: ^1.1.4 + version: 1.1.4(react-dom@18.2.0)(react@18.2.0) '@types/node': specifier: ^20.10.6 version: 20.10.6 @@ -140,6 +143,9 @@ devDependencies: '@microsoft/eslint-formatter-sarif': specifier: ^3.0.0 version: 3.0.0 + '@tanstack/router-devtools': + specifier: ^1.1.4 + version: 1.1.4(react-dom@18.2.0)(react@18.2.0) eslint: specifier: ^8.56.0 version: 8.56.0 @@ -2386,6 +2392,10 @@ packages: tailwindcss: 3.3.7(ts-node@10.9.2) dev: false + /@tanstack/history@1.1.4: + resolution: {integrity: sha512-H80reryZP3Ib5HzAo9zp1B8nbGzd+zOxe0Xt6bLYY2qtgCb+iIrVadDDt5ZnaFsrMBGbFTkEsS2ITVrAUao54A==} + engines: {node: '>=12'} + /@tanstack/query-core@5.17.0: resolution: {integrity: sha512-LoBaPtbMY26kRS+ohII4thTsWkJJsXKGitOLikTo2aqPA4yy7cfFJITs8DRnuERT7tLF5xfG9Lnm33Vp/38Vmw==} dev: false @@ -2416,6 +2426,50 @@ packages: react: 18.2.0 dev: false + /@tanstack/react-router@1.1.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-X+Nak7IxZfCHpH2GIZU9vDSpzpfDUmC30QzuYgwNRhWxGmmkDRF49d07CSc/CW5FQ9RvjECO/3dqw6X519E7HQ==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.2.0 + react-dom: '>=16' + dependencies: + '@babel/runtime': 7.23.7 + '@tanstack/history': 1.1.4 + '@tanstack/react-store': 0.2.1(react-dom@18.2.0)(react@18.2.0) + '@tanstack/store': 0.1.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tiny-invariant: 1.3.1 + tiny-warning: 1.0.3 + + /@tanstack/react-store@0.2.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-tEbMCQjbeVw9KOP/202LfqZMSNAVi6zYkkp1kBom8nFuMx/965Hzes3+6G6b/comCwVxoJU8Gg9IrcF8yRPthw==} + peerDependencies: + react: ^18.2.0 + react-dom: '>=16' + dependencies: + '@tanstack/store': 0.1.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + use-sync-external-store: 1.2.0(react@18.2.0) + + /@tanstack/router-devtools@1.1.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-/chCH/ty386podf2vwON55pAJ9MQ+94vSv35tsF6LgUlTXCw8fYOL4WR1Fp6PgBsUXrPfDt3TMAueVqSitVpeA==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.2.0 + react-dom: '>=16' + dependencies: + '@babel/runtime': 7.23.7 + '@tanstack/react-router': 1.1.4(react-dom@18.2.0)(react@18.2.0) + date-fns: 2.30.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + + /@tanstack/store@0.1.3: + resolution: {integrity: sha512-GnolmC8Fr4mvsHE1fGQmR3Nm0eBO3KnZjDU0a+P3TeQNM/dDscFGxtA7p31NplQNW3KwBw4t1RVFmz0VeKLxcw==} + /@tsconfig/node10@1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} @@ -3163,6 +3217,13 @@ packages: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} dev: false + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.23.7 + dev: true + /date-fns@3.0.6: resolution: {integrity: sha512-W+G99rycpKMMF2/YD064b2lE7jJGUe+EjOES7Q8BIGY8sbNdbgcs9XFTZwvzc9Jx1f3k7LB7gZaZa7f8Agzljg==} dev: false @@ -4900,7 +4961,6 @@ packages: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: false /react-error-boundary@4.0.12(react@18.2.0): resolution: {integrity: sha512-kJdxdEYlb7CPC1A0SeUY38cHpjuu6UkvzKiAmqmOFL21VRfMhOcWxTCBgLVCO0VEMh9JhFNcVaXlV4/BTpiwOA==} @@ -5086,7 +5146,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: false /read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -5272,7 +5331,6 @@ packages: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: loose-envify: 1.4.0 - dev: false /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -5606,9 +5664,11 @@ packages: any-promise: 1.3.0 dev: false + /tiny-invariant@1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + /tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - dev: false /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} @@ -5851,6 +5911,13 @@ packages: use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.46)(react@18.2.0) dev: false + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.2.0 + /utf8@3.0.0: resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} dev: true diff --git a/web/src/App.tsx b/web/src/App.tsx index b018e18433..f002a579de 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -3,25 +3,74 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { QueryClient, QueryClientProvider, useQueryErrorResetBoundary } from "@tanstack/react-query"; +import {QueryCache, QueryClient, QueryClientProvider, useQueryErrorResetBoundary} from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ErrorBoundary } from "react-error-boundary"; import { toast, Toaster } from "react-hot-toast"; -import { LocalRouter } from "./domain/routes"; import { AuthContext, SettingsContext } from "./utils/Context"; import { ErrorPage } from "./components/alerts"; import Toast from "./components/notifications/Toast"; import { Portal } from "react-portal"; +import { + Outlet, + RouterProvider, + Link, + Router, + Route, + RootRoute, rootRouteWithContext, redirect, +} from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/router-devtools' +import {Header} from "@components/header"; +import {Suspense} from "react"; +import {SectionLoader} from "@components/SectionLoader.tsx"; +import {Dashboard} from "@screens/Dashboard.tsx"; +import {FilterDetails, Filters} from "@screens/filters"; +import {Section} from "@screens/filters/sections/_components.tsx"; +import {Actions, Advanced, External, General, MoviesTv, Music} from "@screens/filters/sections"; +import {Releases} from "@screens/Releases.tsx"; +import {z} from "zod"; +import {Settings} from "@screens/Settings.tsx"; +import LogSettings from "@screens/settings/Logs.tsx"; +import IndexerSettings from "@screens/settings/Indexer.tsx"; +import IrcSettings from "@screens/settings/Irc.tsx"; +import FeedSettings from "@screens/settings/Feed.tsx"; +import DownloadClientSettings from "@screens/settings/DownloadClient.tsx"; +import NotificationSettings from "@screens/settings/Notifications.tsx"; +import APISettings from "@screens/settings/Api.tsx"; +import ReleaseSettings from "@screens/settings/Releases.tsx"; +import AccountSettings from "@screens/settings/Account.tsx"; +import ApplicationSettings from "@screens/settings/Application.tsx"; +import {Logs} from "@screens/Logs.tsx"; +import {Login} from "@screens/auth"; +import {APIClient} from "@api/APIClient.ts"; +import {baseUrl} from "@utils"; -const queryClient = new QueryClient({ +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error, query) => { + // check for 401 and redirect here + console.error("query cache error:", error) + console.error("query cache query:", query) + // @ts-ignore + if (error?.status === 401 || error?.status === 403) { + console.error("bad status, redirect to login", error?.status) + // Redirect to login page + window.location.href = "/login"; + } + } + }), defaultOptions: { queries: { // The retries will have exponential delay. // See https://tanstack.com/query/v4/docs/guides/query-retries#retry-delay // delay = Math.min(1000 * 2 ** attemptIndex, 30000) - retry: true, + // retry: true, throwOnError: false, + retry: ( count) => { + console.log("retry: ", count) + return true + } }, mutations: { onError: (error) => { @@ -37,26 +86,321 @@ const queryClient = new QueryClient({ } }); +const dashboardRoute = new Route({ + getParentRoute: () => authIndexRoute, + path: '/', + component: Dashboard, +}) + +const filtersRoute = new Route({ + getParentRoute: () => authIndexRoute, + path: 'filters' +}) + +const filterIndexRoute = new Route({ + getParentRoute: () => filtersRoute, + path: '/', + component: Filters +}) + +export const filterRoute = new Route({ + getParentRoute: () => filtersRoute, + path: '$filterId', + component: FilterDetails +}) + +export const filterGeneralRoute = new Route({ + getParentRoute: () => filterRoute, + path: '/', + component: General +}) + +export const filterMoviesTvRoute = new Route({ + getParentRoute: () => filterRoute, + path: 'movies-tv', + component: MoviesTv +}) + +export const filterMusicRoute = new Route({ + getParentRoute: () => filterRoute, + path: 'music', + component: Music +}) + +export const filterAdvancedRoute = new Route({ + getParentRoute: () => filterRoute, + path: 'advanced', + component: Advanced +}) + +export const filterExternalRoute = new Route({ + getParentRoute: () => filterRoute, + path: 'external', + component: External +}) + +export const filterActionsRoute = new Route({ + getParentRoute: () => filterRoute, + path: 'actions', + component: Actions +}) + +const releasesRoute = new Route({ + getParentRoute: () => authIndexRoute, + path: 'releases' +}) + +export const releasesSearchSchema = z.object({ + // page: z.number().catch(1), + filter: z.string().catch(''), + // sort: z.enum(['newest', 'oldest', 'price']).catch('newest'), +}) + +type ReleasesSearch = z.infer + +export const releasesIndexRoute = new Route({ + getParentRoute: () => releasesRoute, + path: '/', + component: Releases, + validateSearch: (search) => releasesSearchSchema.parse(search), +}) + +const settingsRoute = new Route({ + getParentRoute: () => authIndexRoute, + path: 'settings', + component: Settings +}) + +export const settingsIndexRoute = new Route({ + getParentRoute: () => settingsRoute, + path: '/', + component: ApplicationSettings +}) + +export const settingsLogRoute = new Route({ + getParentRoute: () => settingsRoute, + path: 'logs', + component: LogSettings +}) + +export const settingsIndexersRoute = new Route({ + getParentRoute: () => settingsRoute, + path: 'indexers', + component: IndexerSettings +}) + +export const settingsIrcRoute = new Route({ + getParentRoute: () => settingsRoute, + path: 'irc', + component: IrcSettings +}) + +export const settingsFeedsRoute = new Route({ + getParentRoute: () => settingsRoute, + path: 'feeds', + component: FeedSettings +}) + +export const settingsClientsRoute = new Route({ + getParentRoute: () => settingsRoute, + path: 'clients', + component: DownloadClientSettings +}) + +export const settingsNotificationsRoute = new Route({ + getParentRoute: () => settingsRoute, + path: 'notifications', + component: NotificationSettings +}) + +export const settingsApiRoute = new Route({ + getParentRoute: () => settingsRoute, + path: 'api', + component: APISettings +}) + +export const settingsReleasesRoute = new Route({ + getParentRoute: () => settingsRoute, + path: 'releases', + component: ReleaseSettings +}) + +export const settingsAccountRoute = new Route({ + getParentRoute: () => settingsRoute, + path: 'account', + component: AccountSettings +}) + +export const logsRoute = new Route({ + getParentRoute: () => authIndexRoute, + path: 'logs', + component: Logs +}) + +export const loginRoute = new Route({ + getParentRoute: () => rootRoute, + path: 'login', + validateSearch: z.object({ + redirect: z.string().optional(), + }), +}).update({component: Login}) + +const RootComponent = () => { + return ( +
+ + +
+ ) +} + +export type Auth = { + isLoggedIn: boolean + username?: string + login: (username: string) => void + logout: () => void +} + +export const authRoute = new Route({ + getParentRoute: () => rootRoute, + id: 'auth', + // Before loading, authenticate the user via our auth context + // This will also happen during prefetching (e.g. hovering over links, etc) + beforeLoad: ({ context, location }) => { + console.log("before load") + + // If the user is not logged in, check for item in localStorage + if (!context.auth.isLoggedIn) { + console.log("before load: not logged in") + const key = "user_auth" + const storage = localStorage.getItem(key); + if (storage) { + try { + const json = JSON.parse(storage); + if (json === null) { + console.warn(`JSON localStorage value for '${key}' context state is null`); + } else { + console.log("local storage found", json) + console.log("ctx", context.auth) + context.auth.isLoggedIn = json.isLoggedIn + context.auth.username = json.username + // context.auth = { ...json }; + console.log("ctx", context.auth) + } + } catch (e) { + console.error(`Failed to merge ${key} context state: ${e}`); + } + } else { + // If the user is logged out, redirect them to the login page + throw redirect({ + to: loginRoute.to, + search: { + // Use the current location to power a redirect after login + // (Do not use `router.state.resolvedLocation` as it can + // potentially lag behind the actual current location) + redirect: location.href, + }, + }) + } + } + + // Otherwise, return the user in context + return { + username: auth.username, + } + }, +}) + +function AuthenticatedLayout() { + return ( +
+
+ +
+ ) +} + +export const authIndexRoute = new Route({ + getParentRoute: () => authRoute, + component: AuthenticatedLayout, + id: 'authenticated-routes', +}) + +export const rootRoute = rootRouteWithContext<{ + auth: Auth +}>()({ + component: RootComponent, +}) + +const filterRouteTree = filtersRoute.addChildren([filterIndexRoute, filterRoute.addChildren([filterGeneralRoute, filterMoviesTvRoute, filterMusicRoute, filterAdvancedRoute, filterExternalRoute, filterActionsRoute])]) +const settingsRouteTree = settingsRoute.addChildren([settingsIndexRoute, settingsLogRoute, settingsIndexersRoute, settingsIrcRoute, settingsFeedsRoute, settingsClientsRoute, settingsNotificationsRoute, settingsApiRoute, settingsReleasesRoute, settingsAccountRoute]) + +const authenticatedTree = authRoute.addChildren([authIndexRoute.addChildren([dashboardRoute, filterRouteTree, releasesRoute.addChildren([releasesIndexRoute]), settingsRouteTree, logsRoute])]) + +const routeTree = rootRoute.addChildren([ + authenticatedTree, + loginRoute +]) + +const router = new Router({ + routeTree, + context: { + auth: undefined!, // We'll inject this when we render + }, +}) + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} + +const auth: Auth = { + isLoggedIn: false, + // status: 'loggedOut', + username: undefined, + login: (username: string) => { + auth.isLoggedIn = true + auth.username = username + + localStorage.setItem("user_auth", JSON.stringify(auth)); + }, + logout: () => { + auth.isLoggedIn = false + auth.username = undefined + + localStorage.removeItem("user_auth"); + }, +} + export function App() { - const { reset } = useQueryErrorResetBoundary(); + // const { reset } = useQueryErrorResetBoundary(); - const authContext = AuthContext.useValue(); + // const authContext = AuthContext.useValue(); const settings = SettingsContext.useValue(); return ( - + // - + {/**/} + {settings.debug ? ( + <> + ) : null} - + // ); } diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index aa8c683c8f..7cbfa91058 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -89,15 +89,19 @@ export async function HttpClient( } case 401: { // Remove auth info from localStorage - AuthContext.reset(); + // auth.logout() + // AuthContext.reset(); // Show an error toast to notify the user what occurred - return Promise.reject(new Error(`[401] Unauthorized: "${endpoint}"`)); + return Promise.reject(response); + // return Promise.reject(new Error(`[401] Unauthorized: "${endpoint}"`)); } case 403: { // Remove auth info from localStorage - AuthContext.reset(); + // auth.logout() + // AuthContext.reset(); // Show an error toast to notify the user what occurred - return Promise.reject(new Error(`[403] Forbidden: "${endpoint}"`)); + // return Promise.reject(new Error(`[403] Forbidden: "${endpoint}"`)); + return Promise.reject(response); } case 404: { return Promise.reject(new Error(`[404] Not found: "${endpoint}"`)); diff --git a/web/src/components/header/Header.tsx b/web/src/components/header/Header.tsx index 30e960b236..39e49735cb 100644 --- a/web/src/components/header/Header.tsx +++ b/web/src/components/header/Header.tsx @@ -9,15 +9,19 @@ import { Disclosure } from "@headlessui/react"; import { Bars3Icon, XMarkIcon, MegaphoneIcon } from "@heroicons/react/24/outline"; import { APIClient } from "@api/APIClient"; -import { AuthContext } from "@utils/Context"; import Toast from "@components/notifications/Toast"; import { LeftNav } from "./LeftNav"; import { RightNav } from "./RightNav"; import { MobileNav } from "./MobileNav"; import { ExternalLink } from "@components/ExternalLink"; +import {authIndexRoute, authRoute} from "@app/App.tsx"; +import {redirect, useRouter} from "@tanstack/react-router"; export const Header = () => { + const router = useRouter() + const { auth } = authIndexRoute.useRouteContext() + const { isError:isConfigError, error: configError, data: config } = useQuery({ queryKey: ["config"], queryFn: () => APIClient.config.get(), @@ -29,7 +33,7 @@ export const Header = () => { console.log(configError); } - const { isError, error, data } = useQuery({ + const { isError: isUpdateError, error, data } = useQuery({ queryKey: ["updates"], queryFn: () => APIClient.updates.getLatestRelease(), retry: false, @@ -37,17 +41,27 @@ export const Header = () => { enabled: config?.check_for_updates === true }); - if (isError) { - console.log(error); + if (isUpdateError) { + console.log("update error", error); } const logoutMutation = useMutation({ mutationFn: APIClient.auth.logout, onSuccess: () => { - AuthContext.reset(); + // AuthContext.reset(); toast.custom((t) => ( )); + auth.isLoggedIn = false + auth.username = undefined + + localStorage.removeItem("user_auth"); + // redirect({ to: "/"}) + router.history.push("/") + // auth.logout() + }, + onError: (err) => { + console.error("logout error", err) } }); @@ -62,7 +76,7 @@ export const Header = () => {
- +
{/* Mobile menu button */} @@ -94,7 +108,7 @@ export const Header = () => { )}
- + )} diff --git a/web/src/components/header/LeftNav.tsx b/web/src/components/header/LeftNav.tsx index 557f63c9e2..06074280eb 100644 --- a/web/src/components/header/LeftNav.tsx +++ b/web/src/components/header/LeftNav.tsx @@ -3,7 +3,10 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { Link, NavLink } from "react-router-dom"; +// import { Link, NavLink } from "react-router-dom"; + +import { Link } from '@tanstack/react-router' + import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/solid"; import { classNames } from "@utils"; @@ -23,22 +26,27 @@ export const LeftNav = () => (
{NAV_ROUTES.map((item, itemIdx) => ( - - classNames( - "hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium", - "transition-colors duration-200", - isActive - ? "text-black dark:text-gray-50 font-bold" - : "text-gray-600 dark:text-gray-500" - ) - } - end={item.path === "/"} + // end={item.path === "/"} > - {item.name} - + {({ isActive }) => { + return ( + <> + {item.name} + + ) + }} + ))} { - const authContext = AuthContext.useValue(); return (
@@ -34,7 +32,7 @@ export const RightNav = (props: RightNavProps) => { Open user menu for{" "} - {authContext.username} + {props.auth.username} void; + auth: Auth } export const NAV_ROUTES: Array = [ diff --git a/web/src/screens/Logs.tsx b/web/src/screens/Logs.tsx index 64d161c00b..d5d252bedd 100644 --- a/web/src/screens/Logs.tsx +++ b/web/src/screens/Logs.tsx @@ -4,7 +4,7 @@ */ import { Fragment, useEffect, useRef, useState } from "react"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import {useQuery, useSuspenseQuery} from "@tanstack/react-query"; import { Menu, Transition } from "@headlessui/react"; import { DebounceInput } from "react-debounce-input"; import { @@ -174,7 +174,7 @@ export const Logs = () => { }; export const LogFiles = () => { - const { isError, error, data } = useSuspenseQuery({ + const { isError, error, data } = useQuery({ queryKey: ["log-files"], queryFn: () => APIClient.logs.files(), retry: false, diff --git a/web/src/screens/Settings.tsx b/web/src/screens/Settings.tsx index c2e807d2cd..73ae781842 100644 --- a/web/src/screens/Settings.tsx +++ b/web/src/screens/Settings.tsx @@ -4,7 +4,6 @@ */ import { Suspense } from "react"; -import { NavLink, Outlet, useLocation } from "react-router-dom"; import { BellIcon, ChatBubbleLeftRightIcon, @@ -19,22 +18,24 @@ import { import { classNames } from "@utils"; import { SectionLoader } from "@components/SectionLoader"; +import {Link, Outlet} from "@tanstack/react-router"; interface NavTabType { name: string; href: string; icon: typeof CogIcon; + exact?: boolean; } const subNavigation: NavTabType[] = [ - { name: "Application", href: "", icon: CogIcon }, + { name: "Application", href: ".", icon: CogIcon, exact: true }, { name: "Logs", href: "logs", icon: Square3Stack3DIcon }, { name: "Indexers", href: "indexers", icon: KeyIcon }, { name: "IRC", href: "irc", icon: ChatBubbleLeftRightIcon }, { name: "Feeds", href: "feeds", icon: RssIcon }, { name: "Clients", href: "clients", icon: FolderArrowDownIcon }, { name: "Notifications", href: "notifications", icon: BellIcon }, - { name: "API keys", href: "api-keys", icon: KeyIcon }, + { name: "API keys", href: "api", icon: KeyIcon }, { name: "Releases", href: "releases", icon: RectangleStackIcon }, { name: "Account", href: "account", icon: UserCircleIcon } // {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false} @@ -46,29 +47,44 @@ interface NavLinkProps { } function SubNavLink({ item }: NavLinkProps) { - const { pathname } = useLocation(); - const splitLocation = pathname.split("/"); + // const { pathname } = useLocation(); + // const splitLocation = pathname.split("/"); // we need to clean the / if it's a base root path return ( - classNames( - "transition group border-l-4 px-3 py-2 flex items-center text-sm font-medium", - isActive - ? "font-bold bg-blue-100 dark:bg-gray-700 border-sky-500 dark:border-blue-500 text-sky-700 dark:text-gray-200 hover:bg-blue-200 dark:hover:bg-gray-600 hover:text-sky-900 dark:hover:text-white" - : "border-transparent text-gray-900 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-gray-300" - )} - aria-current={splitLocation[2] === item.href ? "page" : undefined} + activeOptions={{ exact: item.exact }} + // className="transition group border-l-4 px-3 py-2 flex items-center text-sm font-medium" + // end + // className={({ isActive }) => classNames( + // "transition group border-l-4 px-3 py-2 flex items-center text-sm font-medium", + // isActive + // ? "font-bold bg-blue-100 dark:bg-gray-700 border-sky-500 dark:border-blue-500 text-sky-700 dark:text-gray-200 hover:bg-blue-200 dark:hover:bg-gray-600 hover:text-sky-900 dark:hover:text-white" + // : "border-transparent text-gray-900 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-gray-300" + // )} + // aria-current={splitLocation[2] === item.href ? "page" : undefined} > - + {({ isActive }) => { + return ( + + + ) + }} + ); } @@ -99,15 +115,15 @@ export function Settings() {
- - -
- } - > + {/**/} + {/* */} + {/*
*/} + {/* }*/} + {/*>*/} - + {/**/}
diff --git a/web/src/screens/auth/Login.tsx b/web/src/screens/auth/Login.tsx index 3834a8d516..5911f514fc 100644 --- a/web/src/screens/auth/Login.tsx +++ b/web/src/screens/auth/Login.tsx @@ -3,9 +3,9 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { useEffect } from "react"; +import React, { useEffect } from "react"; import { useForm } from "react-hook-form"; -import { useNavigate } from "react-router-dom"; +// import { useNavigate } from "react-router-dom"; import { useMutation } from "@tanstack/react-query"; import toast from "react-hot-toast"; @@ -18,6 +18,8 @@ import { Tooltip } from "@components/tooltips/Tooltip"; import { PasswordInput, TextInput } from "@components/inputs/text"; import Logo from "@app/logo.svg?react"; +import {useNavigate, useRouter, useSearch} from "@tanstack/react-router"; +import {loginRoute} from "../../App.tsx"; type LoginFormFields = { username: string; @@ -25,35 +27,42 @@ type LoginFormFields = { }; export const Login = () => { + const router = useRouter() + const { auth } = loginRoute.useRouteContext() + const search = useSearch({ from: loginRoute.id }) + const { handleSubmit, register, formState } = useForm({ defaultValues: { username: "", password: "" }, mode: "onBlur" }); const navigate = useNavigate(); - const [, setAuthContext] = AuthContext.use(); + // const [, setAuthContext] = AuthContext.use(); useEffect(() => { - // remove user session when visiting login page' - APIClient.auth.logout() - .then(() => { - AuthContext.reset(); - }); - - // Check if onboarding is available for this instance - // and redirect if needed - APIClient.auth.canOnboard() - .then(() => navigate("/onboard")) - .catch(() => { /*don't log to console PAHLLEEEASSSE*/ }); - }, [navigate]); + // // remove user session when visiting login page' + auth.logout() + // // APIClient.auth.logout() + // // .then(() => { + // // AuthContext.reset(); + // // }); + // + // // Check if onboarding is available for this instance + // // and redirect if needed + // // APIClient.auth.canOnboard() + // // .then(() => navigate("/onboard")) + // // .catch(() => { /*don't log to console PAHLLEEEASSSE*/ }); + }, []); const loginMutation = useMutation({ mutationFn: (data: LoginFormFields) => APIClient.auth.login(data.username, data.password), onSuccess: (_, variables: LoginFormFields) => { - setAuthContext({ - username: variables.username, - isLoggedIn: true - }); - navigate("/"); + // setAuthContext({ + // username: variables.username, + // isLoggedIn: true + // }); + console.log("on success") + auth.login(variables.username) + router.invalidate() }, onError: () => { toast.custom((t) => ( @@ -64,6 +73,20 @@ export const Login = () => { const onSubmit = (data: LoginFormFields) => loginMutation.mutate(data); + // Ah, the subtle nuances of client side auth. 🙄 + React.useLayoutEffect(() => { + console.log("trigger layout effect") + if (auth.isLoggedIn && search.redirect) { + console.log("trigger layout effect login change") + router.history.push(search.redirect) + // router.history.push("/") + } else if (auth.isLoggedIn) { + console.log("trigger layout effect else login push") + router.history.push("/") + } + }, [auth.isLoggedIn, search.redirect]) +// }, [auth.isLoggedIn, search.redirect]) + return (
diff --git a/web/src/screens/dashboard/ActivityTable.tsx b/web/src/screens/dashboard/ActivityTable.tsx index 76c7fd1007..fa01cffa19 100644 --- a/web/src/screens/dashboard/ActivityTable.tsx +++ b/web/src/screens/dashboard/ActivityTable.tsx @@ -4,7 +4,7 @@ */ import React, { useState } from "react"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import {useQuery, useSuspenseQuery} from "@tanstack/react-query"; import { useTable, useFilters, @@ -185,7 +185,7 @@ export const ActivityTable = () => { } ] as Column[], []); - const { isLoading, data } = useSuspenseQuery({ + const { isLoading, data } = useQuery({ queryKey: ["dash_recent_releases"], queryFn: APIClient.release.findRecent, refetchOnWindowFocus: false diff --git a/web/src/screens/dashboard/Stats.tsx b/web/src/screens/dashboard/Stats.tsx index f3e9650a5c..8ea20e9849 100644 --- a/web/src/screens/dashboard/Stats.tsx +++ b/web/src/screens/dashboard/Stats.tsx @@ -3,10 +3,10 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { useSuspenseQuery } from "@tanstack/react-query"; +import {useQuery, useSuspenseQuery} from "@tanstack/react-query"; +import {useNavigate} from "@tanstack/react-router"; import { APIClient } from "@api/APIClient"; import { classNames } from "@utils"; -import { useNavigate } from "react-router-dom"; import { LinkIcon } from "@heroicons/react/24/solid"; interface StatsItemProps { @@ -40,16 +40,16 @@ const StatsItem = ({ name, placeholder, value, onClick }: StatsItemProps) => ( ); export const Stats = () => { - const navigate = useNavigate(); + const navigate = useNavigate({ from: '/' }) const handleStatClick = (filterType: string) => { if (filterType) { - navigate(`/releases?filter=${filterType}`); + navigate({ to: '/releases', search: { filter: filterType}}) } else { - navigate("/releases"); + navigate({ to: '/releases'}) } }; - const { isLoading, data } = useSuspenseQuery({ + const { isLoading, data } = useQuery({ queryKey: ["dash_release_stats"], queryFn: APIClient.release.stats, refetchOnWindowFocus: false diff --git a/web/src/screens/filters/Details.tsx b/web/src/screens/filters/Details.tsx index 1c3be95743..e85a278eaf 100644 --- a/web/src/screens/filters/Details.tsx +++ b/web/src/screens/filters/Details.tsx @@ -4,14 +4,14 @@ */ import { Suspense, useEffect, useRef } from "react"; -import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; import { Form, Formik, useFormikContext } from "formik"; import type { FormikErrors, FormikValues } from "formik"; import { z } from "zod"; import { toast } from "react-hot-toast"; import { toFormikValidationSchema } from "zod-formik-adapter"; import { ChevronRightIcon } from "@heroicons/react/24/solid"; -import { NavLink, Route, Routes, useLocation, useNavigate, useParams } from "react-router-dom"; +// import { NavLink, Route, Routes, useLocation, useNavigate, useParams } from "react-router-dom"; import { APIClient } from "@api/APIClient"; import { useToggle } from "@hooks/hooks"; @@ -25,14 +25,17 @@ import { SectionLoader } from "@components/SectionLoader"; import { filterKeys } from "./List"; import * as Section from "./sections"; +import {filterRoute} from "../../App.tsx"; +import {Link, Outlet, useNavigate} from "@tanstack/react-router"; interface tabType { name: string; href: string; + exact?: boolean; } const tabs: tabType[] = [ - { name: "General", href: "" }, + { name: "General", href: ".", exact: true }, { name: "Movies and TV", href: "movies-tv" }, { name: "Music", href: "music" }, { name: "Advanced", href: "advanced" }, @@ -45,25 +48,42 @@ export interface NavLinkProps { } function TabNavLink({ item }: NavLinkProps) { - const location = useLocation(); - const splitLocation = location.pathname.split("/"); + // const location = useLocation(); + // const splitLocation = location.pathname.split("/"); // we need to clean the / if it's a base root path return ( - classNames( - "transition border-b-2 whitespace-nowrap py-4 duration-3000 px-1 font-medium text-sm first:rounded-tl-lg last:rounded-tr-lg", - isActive - ? "text-blue-600 dark:text-white border-blue-600 dark:border-blue-500" - : "text-gray-550 hover:text-blue-500 dark:hover:text-white border-transparent" - )} - aria-current={splitLocation[2] === item.href ? "page" : undefined} + activeOptions={{ exact: item.exact }} + // end + // className={({ isActive }) => classNames( + // "transition border-b-2 whitespace-nowrap py-4 duration-3000 px-1 font-medium text-sm first:rounded-tl-lg last:rounded-tr-lg", + // isActive + // ? "text-blue-600 dark:text-white border-blue-600 dark:border-blue-500" + // : "text-gray-550 hover:text-blue-500 dark:hover:text-white border-transparent" + // )} + // aria-current={splitLocation[2] === item.href ? "page" : undefined} + // className="transition border-b-2 whitespace-nowrap py-4 duration-3000 px-1 font-medium text-sm first:rounded-tl-lg last:rounded-tr-lg" > - {item.name} - + {/*{item.name}*/} + {({ isActive }) => { + return ( + + {item.name} + + ) + }} + ); } @@ -282,23 +302,25 @@ const schema = z.object({ export const FilterDetails = () => { const queryClient = useQueryClient(); - const navigate = useNavigate(); - const { filterId } = useParams<{ filterId: string }>(); + // const navigate = useNavigate(); + // const { filterId } = useParams<{ filterId: string }>(); + // + // if (filterId === "0" || filterId === undefined) { + // navigate("/filters"); + // } - if (filterId === "0" || filterId === undefined) { - navigate("/filters"); - } + const { filterId } = filterRoute.useParams() const id = parseInt(filterId!); - const { isLoading, isError, data: filter } = useSuspenseQuery({ + const { isLoading, isError, data: filter } = useQuery({ queryKey: filterKeys.detail(id), queryFn: ({ queryKey }) => APIClient.filters.getByID(queryKey[2]), refetchOnWindowFocus: false }); if (isError) { - navigate("/filters"); + // navigate("/filters"); } const updateMutation = useMutation({ @@ -330,7 +352,7 @@ export const FilterDetails = () => { )); // redirect - navigate("/filters"); + // navigate("/filters"); } }); @@ -362,9 +384,9 @@ export const FilterDetails = () => {

- + Filters - +

} - colors="text-cyan-700 bg-cyan-100 dark:bg-cyan-200 dark:text-cyan-800" /> - ) : null} - {values.except_releases ? ( - - Do you have a good reason to use Except releases instead of one of the other tabs? - + +

Comma separated list of release groups to ignore (takes priority over Match releases).

+ +
} - colors="text-fuchsia-700 bg-fuchsia-100 dark:bg-fuchsia-200 dark:text-fuchsia-800" /> - ) : null} - -); - -const Groups = ({ values }: ValueConsumer) => ( - - -

Comma separated list of release groups to match.

- -
- } - /> - -

Comma separated list of release groups to ignore (takes priority over Match releases).

- -
- } - /> - -); - -const Categories = ({ values }: ValueConsumer) => ( - - -

Comma separated list of categories to match.

- -
- } - /> - -

Comma separated list of categories to ignore (takes priority over Match releases).

- -
- } - /> - -); - -const Tags = ({ values }: ValueConsumer) => ( - -
+ + ); +} + +const Categories = () => { + const { values } = useFormikContext(); + + return ( + 0 || values.except_categories !== ""} + title="Categories" + subtitle="Match or exclude categories (if announced)" + > -

Comma separated list of tags to match.

- +

Comma separated list of categories to match.

+
} /> - -

Logic used to match filter tags.

- +

Comma separated list of categories to ignore (takes priority over Match releases).

+ } /> - -
+ + ); +} + +const Tags = () => { + const { values } = useFormikContext(); + + return ( + +
+ +

Comma separated list of tags to match.

+ +
+ } + /> + +

Logic used to match filter tags.

+ +
+ } + /> + +
+ +

Comma separated list of tags to ignore (takes priority over Match releases).

+ +
+ } + /> + +

Logic used to match except tags.

+ + + } + /> + +
+ ); +} + +const Uploaders = () => { + const { values } = useFormikContext(); + + return ( + -

Comma separated list of tags to ignore (takes priority over Match releases).

+

Comma separated list of uploaders to match.

} /> - -

Logic used to match except tags.

+

Comma separated list of uploaders to ignore (takes priority over Match releases). +

} /> - -
-); - -const Uploaders = ({ values }: ValueConsumer) => ( - - -

Comma separated list of uploaders to match.

- - - } - /> - -

Comma separated list of uploaders to ignore (takes priority over Match releases). -

- - - } - /> -
-); - -const Language = ({ values }: ValueConsumer) => ( - 0) || (values.except_language && values.except_language.length > 0)} - title="Language" - subtitle="Match or ignore languages (if announced)" - > - - - -); - -const Origins = ({ values }: ValueConsumer) => ( - 0 || values.except_origins && values.except_origins.length > 0)} - title="Origins" - subtitle="Match Internals, Scene, P2P, etc. (if announced)" - > - - - -); - -const Freeleech = ({ values }: ValueConsumer) => ( - - -

- Freeleech may be announced as a binary true/false value or as a - percentage (less likely), depending on the indexer. Use one or the other. - The Freeleech toggle overrides this field if it is toggled/true. -

-
-

- Refer to our documentation for more details:{" "} - -

- - } - columns={6} - placeholder="eg. 50,75-100" - /> - - + ); +} + +const Language = () => { + const { values } = useFormikContext(); + + return ( + 0) || (values.except_language && values.except_language.length > 0)} + title="Language" + subtitle="Match or ignore languages (if announced)" + > + + + + ); +} + +const Origins = () => { + const { values } = useFormikContext(); + + return ( + 0 || values.except_origins && values.except_origins.length > 0)} + title="Origins" + subtitle="Match Internals, Scene, P2P, etc. (if announced)" + > + + + + ); +} + +const Freeleech = () => { + const { values } = useFormikContext(); + + return ( + +

- Freeleech may be announced as a binary true/false value (more likely) or as a - percentage, depending on the indexer. Use one or the other. - This field overrides Freeleech percent if it is toggled/true. + Freeleech may be announced as a binary true/false value or as a + percentage (less likely), depending on the indexer. Use one or the other. + The Freeleech toggle overrides this field if it is toggled/true.


- See who uses what in the documentation:{" "} + Refer to our documentation for more details:{" "}

} + columns={6} + placeholder="eg. 50,75-100" /> -
-
-); - -const FeedSpecific = ({ values }: ValueConsumer) => ( - These options are only for Feeds such as RSS, Torznab and Newznab - } - > - - - - - -

This field has full regex support (Golang flavour).

- -
-
-

Remember to tick Use Regex below if using more than * and ?.

- - } - /> - -

This field has full regex support (Golang flavour).

- -
-
-

Remember to tick Use Regex below if using more than * and ?.

- + + +

+ Freeleech may be announced as a binary true/false value (more likely) or as a + percentage, depending on the indexer. Use one or the other. + This field overrides Freeleech percent if it is toggled/true. +

+
+

+ See who uses what in the documentation:{" "} + +

+ + } + /> +
+
+ ); +} + +const FeedSpecific = () => { + const { values } = useFormikContext(); + + return ( + These options are only for Feeds such as RSS, Torznab and Newznab } - /> - -); - -const RawReleaseTags = ({ values }: ValueConsumer) => ( - - Advanced users only - {": "}This is the raw releaseTags string from the announce. - - } - > - These might not be what you think they are. For very advanced users who know how things are parsed. + > + + + + + +

This field has full regex support (Golang flavour).

+ +
+
+

Remember to tick Use Regex below if using more than * and ?.

+ + } + /> + +

This field has full regex support (Golang flavour).

+ +
+
+

Remember to tick Use Regex below if using more than * and ?.

+ + } + /> +
+ ); +} + +const RawReleaseTags = () => { + const { values } = useFormikContext(); + + return ( + + Advanced users only + {": "}This is the raw releaseTags string from the announce. + } - /> + > + These might not be what you think they are. For very advanced users who know how things are parsed. + } + /> + + + + - - + - - - - - -); - -export const Advanced = ({ values }: { values: FormikValues; }) => ( -
- - - - - - - - - - -
-); + + ); +} + +export const Advanced = () => { + return ( +
+ + + + + + + + + + +
+ ); +} diff --git a/web/src/screens/filters/sections/Music.tsx b/web/src/screens/filters/sections/Music.tsx index c1830c19b3..31c62b013a 100644 --- a/web/src/screens/filters/sections/Music.tsx +++ b/web/src/screens/filters/sections/Music.tsx @@ -1,4 +1,4 @@ -import type { FormikValues } from "formik"; +import { useFormikContext } from "formik"; import { DocsLink } from "@components/ExternalLink"; import * as Input from "@components/inputs"; @@ -6,182 +6,186 @@ import * as Input from "@components/inputs"; import * as CONSTS from "@domain/constants"; import * as Components from "./_components"; -export const Music = ({ values }: { values: FormikValues; }) => ( - - - - -

You can use basic filtering like wildcards * or replace single characters with ?

- - - } - /> - -

You can use basic filtering like wildcards * or replace single characters with ?

- - - } - /> -
-
+export const Music = () => { + const { values } = useFormikContext(); - - - -

Will only match releases with any of the selected types.

- - - } - /> - -

This field takes a range of years and/or comma separated single years.

- - - } - /> -
-
- - - + return ( + + - -

Will only match releases with any of the selected formats. This is overridden by Perfect FLAC.

- +

You can use basic filtering like wildcards * or replace single characters with ?

+ } /> - -

Will only match releases with any of the selected qualities. This is overridden by Perfect FLAC.

- +

You can use basic filtering like wildcards * or replace single characters with ?

+ } /> +
+
+ + + -

Will only match releases with any of the selected sources. This is overridden by Perfect FLAC.

+

Will only match releases with any of the selected types.

} /> + +

This field takes a range of years and/or comma separated single years.

+ + + } + />
+
- - - + + + +

Will only match releases with any of the selected formats. This is overridden by Perfect FLAC.

+ + + } /> -
- - - +

Will only match releases with any of the selected qualities. This is overridden by Perfect FLAC.

+ + + } /> -
- - - -

Log scores go from 0 to 100. This is overridden by Perfect FLAC.

+

Will only match releases with any of the selected sources. This is overridden by Perfect FLAC.

} /> -
+
+ + + + + + + + + + + + +

Log scores go from 0 to 100. This is overridden by Perfect FLAC.

+ + + } + /> +
+
- -
- - +
+ + OR - -
+ +
- - -

Override all options about quality, source, format, and CUE/LOG/LOG score.

- - - } - /> + + +

Override all options about quality, source, format, and CUE/LOG/LOG score.

+ + + } + /> - + This is what you want in 90% of cases (instead of options above). -
-
-
-); + + + + ); +} diff --git a/web/src/screens/releases/ReleaseTable.tsx b/web/src/screens/releases/ReleaseTable.tsx index 583bb2ae00..7151c90335 100644 --- a/web/src/screens/releases/ReleaseTable.tsx +++ b/web/src/screens/releases/ReleaseTable.tsx @@ -4,8 +4,7 @@ */ import React, { useState } from "react"; -import { useLocation } from "react-router-dom"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import {useQuery} from "@tanstack/react-query"; import { Column, useFilters, usePagination, useSortBy, useTable } from "react-table"; import { ChevronDoubleLeftIcon, @@ -23,6 +22,7 @@ import * as Icons from "@components/Icons"; import * as DataTable from "@components/data-table"; import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./Filters"; +import {releasesIndexRoute} from "@app/App.tsx"; export const releaseKeys = { all: ["releases"] as const, @@ -81,9 +81,10 @@ const TableReducer = (state: TableState, action: Actions): TableState => { }; export const ReleaseTable = () => { - const location = useLocation(); - const queryParams = new URLSearchParams(location.search); - const filterTypeFromUrl = queryParams.get("filter"); + const { filter} = releasesIndexRoute.useSearch() + // const location = useLocation(); + // const queryParams = new URLSearchParams(location.search); + // const filterTypeFromUrl = queryParams.get("filter"); const columns = React.useMemo(() => [ { Header: "Age", @@ -120,7 +121,7 @@ export const ReleaseTable = () => { const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] = React.useReducer(TableReducer, initialState); - const { isLoading, error, data, isSuccess } = useSuspenseQuery({ + const { isLoading, error, data, isSuccess } = useQuery({ queryKey: releaseKeys.list(queryPageIndex, queryPageSize, queryFilters), queryFn: () => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters), staleTime: 5000 @@ -170,7 +171,7 @@ export const ReleaseTable = () => { initialState: { pageIndex: queryPageIndex, pageSize: queryPageSize, - filters: filterTypeFromUrl ? [{ id: "action_status", value: filterTypeFromUrl }] : [] + filters: filter ? [{ id: "action_status", value: filter }] : [] }, manualPagination: true, manualFilters: true, diff --git a/web/src/screens/settings/Api.tsx b/web/src/screens/settings/Api.tsx index 0b7e137d1d..26a7d673e5 100644 --- a/web/src/screens/settings/Api.tsx +++ b/web/src/screens/settings/Api.tsx @@ -4,7 +4,7 @@ */ import { useRef } from "react"; -import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; import { toast } from "react-hot-toast"; import { TrashIcon } from "@heroicons/react/24/outline"; @@ -30,7 +30,7 @@ export const apiKeys = { function APISettings() { const [addFormIsOpen, toggleAddForm] = useToggle(false); - const { isError, error, data } = useSuspenseQuery({ + const { isError, error, data } = useQuery({ queryKey: apiKeys.lists(), queryFn: APIClient.apikeys.getAll, retry: false, diff --git a/web/src/screens/settings/DownloadClient.tsx b/web/src/screens/settings/DownloadClient.tsx index 7a50e16e0f..37354253ef 100644 --- a/web/src/screens/settings/DownloadClient.tsx +++ b/web/src/screens/settings/DownloadClient.tsx @@ -4,7 +4,7 @@ */ import { useState, useMemo } from "react"; -import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; import { PlusIcon } from "@heroicons/react/24/solid"; import toast from "react-hot-toast"; @@ -140,7 +140,7 @@ function ListItem({ client }: DLSettingsItemProps) { function DownloadClientSettings() { const [addClientIsOpen, toggleAddClient] = useToggle(false); - const { error, data } = useSuspenseQuery({ + const { error, data } = useQuery({ queryKey: clientKeys.lists(), queryFn: APIClient.download_clients.getAll, refetchOnWindowFocus: false diff --git a/web/src/screens/settings/Feed.tsx b/web/src/screens/settings/Feed.tsx index 3e8dcec68a..b3b0cef050 100644 --- a/web/src/screens/settings/Feed.tsx +++ b/web/src/screens/settings/Feed.tsx @@ -4,7 +4,7 @@ */ import { Fragment, useRef, useState, useMemo } from "react"; -import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; import { Menu, Transition } from "@headlessui/react"; import { toast } from "react-hot-toast"; import { @@ -97,7 +97,7 @@ function useSort(items: ListItemProps["feed"][], config?: SortConfig) { } function FeedSettings() { - const { data } = useSuspenseQuery({ + const { data } = useQuery({ queryKey: feedKeys.lists(), queryFn: APIClient.feeds.find, refetchOnWindowFocus: false diff --git a/web/src/screens/settings/Indexer.tsx b/web/src/screens/settings/Indexer.tsx index 8c1f59ffd1..6ee913975c 100644 --- a/web/src/screens/settings/Indexer.tsx +++ b/web/src/screens/settings/Indexer.tsx @@ -5,7 +5,7 @@ import { useState, useMemo } from "react"; import toast from "react-hot-toast"; -import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; import { PlusIcon } from "@heroicons/react/24/solid"; import { useToggle } from "@hooks/hooks"; @@ -169,7 +169,7 @@ const ListItem = ({ indexer }: ListItemProps) => { function IndexerSettings() { const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false); - const { error, data } = useSuspenseQuery({ + const { error, data } = useQuery({ queryKey: indexerKeys.lists(), queryFn: APIClient.indexers.getAll, refetchOnWindowFocus: false diff --git a/web/src/screens/settings/Irc.tsx b/web/src/screens/settings/Irc.tsx index b968223c27..19b2644a05 100644 --- a/web/src/screens/settings/Irc.tsx +++ b/web/src/screens/settings/Irc.tsx @@ -4,7 +4,7 @@ */ import { Fragment, useRef, useState, useMemo, useEffect, MouseEvent } from "react"; -import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; import { LockClosedIcon, LockOpenIcon, PlusIcon } from "@heroicons/react/24/solid"; import { Menu, Transition } from "@headlessui/react"; import { toast } from "react-hot-toast"; @@ -98,7 +98,7 @@ const IrcSettings = () => { const [expandNetworks, toggleExpand] = useToggle(false); const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false); - const { data } = useSuspenseQuery({ + const { data } = useQuery({ queryKey: ircKeys.lists(), queryFn: APIClient.irc.getNetworks, refetchOnWindowFocus: false, diff --git a/web/src/screens/settings/Logs.tsx b/web/src/screens/settings/Logs.tsx index dad3ef4387..868f7b326b 100644 --- a/web/src/screens/settings/Logs.tsx +++ b/web/src/screens/settings/Logs.tsx @@ -3,9 +3,9 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; +import {Link} from "@tanstack/react-router"; import { toast } from "react-hot-toast"; -import { Link } from "react-router-dom"; import Select from "react-select"; import { APIClient } from "@api/APIClient"; @@ -56,7 +56,7 @@ const SelectWrapper = ({ id, value, onChange, options }: SelectWrapperProps) => ); function LogSettings() { - const { isError, error, isLoading, data } = useSuspenseQuery({ + const { isError, error, isLoading, data } = useQuery({ queryKey: ["config"], queryFn: APIClient.config.get, retry: false, @@ -86,7 +86,7 @@ function LogSettings() { Configure log level, log size rotation, etc. You can download your old log files {" "} on the Logs page diff --git a/web/src/screens/settings/Notifications.tsx b/web/src/screens/settings/Notifications.tsx index 39b0b1b213..0b0265a070 100644 --- a/web/src/screens/settings/Notifications.tsx +++ b/web/src/screens/settings/Notifications.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; import { APIClient } from "@api/APIClient"; import { EmptySimple } from "@components/emptystates"; @@ -27,7 +27,7 @@ export const notificationKeys = { function NotificationSettings() { const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false); - const { data } = useSuspenseQuery({ + const { data } = useQuery({ queryKey: notificationKeys.lists(), queryFn: APIClient.notifications.getAll, refetchOnWindowFocus: false diff --git a/web/src/utils/index.ts b/web/src/utils/index.ts index 996501a4ef..d94c65057c 100644 --- a/web/src/utils/index.ts +++ b/web/src/utils/index.ts @@ -15,11 +15,12 @@ export function baseUrl() { let baseUrl = ""; if (window.APP.baseUrl) { if (window.APP.baseUrl === "{{.BaseUrl}}") { - baseUrl = "/"; + baseUrl = ""; } else { baseUrl = window.APP.baseUrl; } } + console.log("baseurl", baseUrl) return baseUrl; } From e671f1662ddfee62c68b1d0fc2eb020764a83e27 Mon Sep 17 00:00:00 2001 From: ze0s Date: Fri, 5 Jan 2024 16:31:21 +0100 Subject: [PATCH 12/46] refactor(web): use route loaders and suspense --- web/src/App.tsx | 148 +++++++++++++++++--- web/src/api/APIClient.ts | 1 - web/src/components/SectionLoader.tsx | 14 ++ web/src/screens/Settings.tsx | 1 + web/src/screens/auth/Login.tsx | 4 +- web/src/screens/dashboard/ActivityTable.tsx | 2 +- web/src/screens/dashboard/Stats.tsx | 41 +++--- web/src/screens/filters/Details.tsx | 32 ++--- web/src/screens/filters/List.tsx | 14 +- web/src/screens/releases/ReleaseTable.tsx | 1 - web/src/screens/settings/Account.tsx | 6 +- web/src/screens/settings/Api.tsx | 27 ++-- web/src/screens/settings/Application.tsx | 8 +- web/src/screens/settings/DownloadClient.tsx | 15 +- web/src/screens/settings/Feed.tsx | 18 +-- web/src/screens/settings/Indexer.tsx | 29 ++-- web/src/screens/settings/Irc.tsx | 24 ++-- web/src/screens/settings/Logs.tsx | 29 ++-- web/src/screens/settings/Notifications.tsx | 14 +- web/src/utils/index.ts | 1 - 20 files changed, 270 insertions(+), 159 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index f002a579de..405fb88238 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -3,7 +3,13 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import {QueryCache, QueryClient, QueryClientProvider, useQueryErrorResetBoundary} from "@tanstack/react-query"; +import { + QueryCache, + QueryClient, + QueryClientProvider, + queryOptions, + useQueryErrorResetBoundary +} from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ErrorBoundary } from "react-error-boundary"; import { toast, Toaster } from "react-hot-toast"; @@ -18,12 +24,12 @@ import { Link, Router, Route, - RootRoute, rootRouteWithContext, redirect, + RootRoute, rootRouteWithContext, redirect, ErrorComponent, useRouterState, } from '@tanstack/react-router' import { TanStackRouterDevtools } from '@tanstack/router-devtools' import {Header} from "@components/header"; import {Suspense} from "react"; -import {SectionLoader} from "@components/SectionLoader.tsx"; +import {SectionLoader, Spinner} from "@components/SectionLoader.tsx"; import {Dashboard} from "@screens/Dashboard.tsx"; import {FilterDetails, Filters} from "@screens/filters"; import {Section} from "@screens/filters/sections/_components.tsx"; @@ -32,12 +38,12 @@ import {Releases} from "@screens/Releases.tsx"; import {z} from "zod"; import {Settings} from "@screens/Settings.tsx"; import LogSettings from "@screens/settings/Logs.tsx"; -import IndexerSettings from "@screens/settings/Indexer.tsx"; -import IrcSettings from "@screens/settings/Irc.tsx"; -import FeedSettings from "@screens/settings/Feed.tsx"; -import DownloadClientSettings from "@screens/settings/DownloadClient.tsx"; -import NotificationSettings from "@screens/settings/Notifications.tsx"; -import APISettings from "@screens/settings/Api.tsx"; +import IndexerSettings, {indexerKeys} from "@screens/settings/Indexer.tsx"; +import IrcSettings, {ircKeys} from "@screens/settings/Irc.tsx"; +import FeedSettings, {feedKeys} from "@screens/settings/Feed.tsx"; +import DownloadClientSettings, {clientKeys} from "@screens/settings/DownloadClient.tsx"; +import NotificationSettings, {notificationKeys} from "@screens/settings/Notifications.tsx"; +import APISettings, {apiKeys} from "@screens/settings/Api.tsx"; import ReleaseSettings from "@screens/settings/Releases.tsx"; import AccountSettings from "@screens/settings/Account.tsx"; import ApplicationSettings from "@screens/settings/Application.tsx"; @@ -45,6 +51,8 @@ import {Logs} from "@screens/Logs.tsx"; import {Login} from "@screens/auth"; import {APIClient} from "@api/APIClient.ts"; import {baseUrl} from "@utils"; +import * as querystring from "querystring"; +import {filterKeys} from "@screens/filters/List.tsx"; export const queryClient = new QueryClient({ queryCache: new QueryCache({ @@ -86,6 +94,62 @@ export const queryClient = new QueryClient({ } }); +const filtersQueryOptions = () => + queryOptions({ + queryKey: ['filters'], + queryFn: () => APIClient.filters.find([], "") + }) + +export const filterQueryOptions = (filterId: number) => + queryOptions({ + queryKey: filterKeys.detail(filterId), + queryFn: ({queryKey}) => APIClient.filters.getByID(queryKey[2]) + }) + +export const configQueryOptions = () => + queryOptions({ + queryKey: ["config"], + queryFn: () => APIClient.config.get() + }) + +export const indexersQueryOptions = () => + queryOptions({ + queryKey: indexerKeys.lists(), + queryFn: () => APIClient.indexers.getAll() + }) + +export const ircQueryOptions = () => + queryOptions({ + queryKey: ircKeys.lists(), + queryFn: () => APIClient.irc.getNetworks(), + refetchOnWindowFocus: false, + refetchInterval: 3000 // Refetch every 3 seconds + }) + +export const feedsQueryOptions = () => + queryOptions({ + queryKey: feedKeys.lists(), + queryFn: () => APIClient.feeds.find(), + }) + +export const downloadClientsQueryOptions = () => + queryOptions({ + queryKey: clientKeys.lists(), + queryFn: () => APIClient.download_clients.getAll(), + }) + +export const notificationsQueryOptions = () => + queryOptions({ + queryKey: notificationKeys.lists(), + queryFn: () => APIClient.notifications.getAll() + }) + +export const apikeysQueryOptions = () => + queryOptions({ + queryKey: apiKeys.lists(), + queryFn: () => APIClient.apikeys.getAll() + }) + const dashboardRoute = new Route({ getParentRoute: () => authIndexRoute, path: '/', @@ -103,9 +167,33 @@ const filterIndexRoute = new Route({ component: Filters }) +// export const filterRoute = new Route({ +// getParentRoute: () => filtersRoute, +// path: '$filterId', +// validateSearch: z.object({ +// filterId: z.number(), +// }), +// loaderDeps: ({ search }) => ({ +// filterId: search.filterId +// }), +// loader: (opts) => opts.context.queryClient.ensureQueryData(filterQueryOptions(opts.deps.filterId)), +// component: FilterDetails +// }) + export const filterRoute = new Route({ getParentRoute: () => filtersRoute, path: '$filterId', + parseParams: (params) => ({ + filterId: z.number().int().parse(Number(params.filterId)), + }), + stringifyParams: ({ filterId }) => ({ filterId: `${filterId}` }), + // validateSearch: (search) => z.object({ + // filterId: z.number(), + // }), + // loaderDeps: ({ search }) => ({ + // filterId: search.filterId + // }), + loader: (opts) => opts.context.queryClient.ensureQueryData(filterQueryOptions(opts.params.filterId)), component: FilterDetails }) @@ -165,7 +253,7 @@ export const releasesIndexRoute = new Route({ validateSearch: (search) => releasesSearchSchema.parse(search), }) -const settingsRoute = new Route({ +export const settingsRoute = new Route({ getParentRoute: () => authIndexRoute, path: 'settings', component: Settings @@ -180,42 +268,49 @@ export const settingsIndexRoute = new Route({ export const settingsLogRoute = new Route({ getParentRoute: () => settingsRoute, path: 'logs', + loader: (opts) => opts.context.queryClient.ensureQueryData(configQueryOptions()), component: LogSettings }) export const settingsIndexersRoute = new Route({ getParentRoute: () => settingsRoute, path: 'indexers', + loader: (opts) => opts.context.queryClient.ensureQueryData(indexersQueryOptions()), component: IndexerSettings }) export const settingsIrcRoute = new Route({ getParentRoute: () => settingsRoute, path: 'irc', + loader: (opts) => opts.context.queryClient.ensureQueryData(ircQueryOptions()), component: IrcSettings }) export const settingsFeedsRoute = new Route({ getParentRoute: () => settingsRoute, path: 'feeds', + loader: (opts) => opts.context.queryClient.ensureQueryData(feedsQueryOptions()), component: FeedSettings }) export const settingsClientsRoute = new Route({ getParentRoute: () => settingsRoute, path: 'clients', + loader: (opts) => opts.context.queryClient.ensureQueryData(downloadClientsQueryOptions()), component: DownloadClientSettings }) export const settingsNotificationsRoute = new Route({ getParentRoute: () => settingsRoute, path: 'notifications', + loader: (opts) => opts.context.queryClient.ensureQueryData(notificationsQueryOptions()), component: NotificationSettings }) export const settingsApiRoute = new Route({ getParentRoute: () => settingsRoute, path: 'api', + loader: (opts) => opts.context.queryClient.ensureQueryData(apikeysQueryOptions()), component: APISettings }) @@ -245,11 +340,22 @@ export const loginRoute = new Route({ }), }).update({component: Login}) +export function RouterSpinner() { + const isLoading = useRouterState({ select: (s) => s.status === 'pending' }) + return +} + const RootComponent = () => { + const settings = SettingsContext.useValue(); return (
- + {settings.debug ? ( + <> + + + + ) : null}
) } @@ -327,7 +433,8 @@ export const authIndexRoute = new Route({ }) export const rootRoute = rootRouteWithContext<{ - auth: Auth + auth: Auth, + queryClient: QueryClient }>()({ component: RootComponent, }) @@ -344,8 +451,15 @@ const routeTree = rootRoute.addChildren([ const router = new Router({ routeTree, + defaultPendingComponent: () => ( +
+ +
+ ), + defaultErrorComponent: ({ error }) => , context: { auth: undefined!, // We'll inject this when we render + queryClient }, }) @@ -395,11 +509,11 @@ export function App() { context={{ auth, }} /> - {settings.debug ? ( - <> - - - ) : null} + {/*{settings.debug ? (*/} + {/* <>*/} + {/* */} + {/* */} + {/*) : null}*/} // ); diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index 7cbfa91058..4d5002e27d 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -4,7 +4,6 @@ */ import { baseUrl, sseBaseUrl } from "@utils"; -import { AuthContext } from "@utils/Context"; import { GithubRelease } from "@app/types/Update"; type RequestBody = BodyInit | object | Record | null; diff --git a/web/src/components/SectionLoader.tsx b/web/src/components/SectionLoader.tsx index 79dd598a10..cae32cdb35 100644 --- a/web/src/components/SectionLoader.tsx +++ b/web/src/components/SectionLoader.tsx @@ -30,3 +30,17 @@ export const SectionLoader = ({ $size }: SectionLoaderProps) => { ); } }; + +export function Spinner({ show, wait }: { show?: boolean; wait?: `delay-${number}` }) { + return ( +
+ ⍥ +
+ ) +} diff --git a/web/src/screens/Settings.tsx b/web/src/screens/Settings.tsx index 73ae781842..23fec0753f 100644 --- a/web/src/screens/Settings.tsx +++ b/web/src/screens/Settings.tsx @@ -19,6 +19,7 @@ import { import { classNames } from "@utils"; import { SectionLoader } from "@components/SectionLoader"; import {Link, Outlet} from "@tanstack/react-router"; +import {RouterSpinner} from "@app/App.tsx"; interface NavTabType { name: string; diff --git a/web/src/screens/auth/Login.tsx b/web/src/screens/auth/Login.tsx index 5911f514fc..b1f0c66e1f 100644 --- a/web/src/screens/auth/Login.tsx +++ b/web/src/screens/auth/Login.tsx @@ -5,21 +5,19 @@ import React, { useEffect } from "react"; import { useForm } from "react-hook-form"; -// import { useNavigate } from "react-router-dom"; import { useMutation } from "@tanstack/react-query"; import toast from "react-hot-toast"; import { RocketLaunchIcon } from "@heroicons/react/24/outline"; import { APIClient } from "@api/APIClient"; -import { AuthContext } from "@utils/Context"; import Toast from "@components/notifications/Toast"; import { Tooltip } from "@components/tooltips/Tooltip"; import { PasswordInput, TextInput } from "@components/inputs/text"; import Logo from "@app/logo.svg?react"; import {useNavigate, useRouter, useSearch} from "@tanstack/react-router"; -import {loginRoute} from "../../App.tsx"; +import {loginRoute} from "@app/App.tsx"; type LoginFormFields = { username: string; diff --git a/web/src/screens/dashboard/ActivityTable.tsx b/web/src/screens/dashboard/ActivityTable.tsx index fa01cffa19..5cc1062cae 100644 --- a/web/src/screens/dashboard/ActivityTable.tsx +++ b/web/src/screens/dashboard/ActivityTable.tsx @@ -4,7 +4,7 @@ */ import React, { useState } from "react"; -import {useQuery, useSuspenseQuery} from "@tanstack/react-query"; +import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { useTable, useFilters, diff --git a/web/src/screens/dashboard/Stats.tsx b/web/src/screens/dashboard/Stats.tsx index 8ea20e9849..0ee66080ac 100644 --- a/web/src/screens/dashboard/Stats.tsx +++ b/web/src/screens/dashboard/Stats.tsx @@ -3,23 +3,27 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import {useQuery, useSuspenseQuery} from "@tanstack/react-query"; -import {useNavigate} from "@tanstack/react-router"; +import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; import { APIClient } from "@api/APIClient"; import { classNames } from "@utils"; import { LinkIcon } from "@heroicons/react/24/solid"; interface StatsItemProps { - name: string; - value?: number; - placeholder?: string; - onClick?: () => void; + name: string; + value?: number; + placeholder?: string; + to?: string; + eventType?: string; } -const StatsItem = ({ name, placeholder, value, onClick }: StatsItemProps) => ( -
( +
@@ -36,19 +40,10 @@ const StatsItem = ({ name, placeholder, value, onClick }: StatsItemProps) => (

{value}

-
+ ); export const Stats = () => { - const navigate = useNavigate({ from: '/' }) - const handleStatClick = (filterType: string) => { - if (filterType) { - navigate({ to: '/releases', search: { filter: filterType}}) - } else { - navigate({ to: '/releases'}) - } - }; - const { isLoading, data } = useQuery({ queryKey: ["dash_release_stats"], queryFn: APIClient.release.stats, @@ -62,11 +57,11 @@ export const Stats = () => {
- handleStatClick("")} value={data?.filtered_count ?? 0} /> + {/* */} - handleStatClick("PUSH_APPROVED")} value={data?.push_approved_count ?? 0} /> - handleStatClick("PUSH_REJECTED")} value={data?.push_rejected_count ?? 0 } /> - handleStatClick("PUSH_ERROR")} value={data?.push_error_count ?? 0} /> + + +
); diff --git a/web/src/screens/filters/Details.tsx b/web/src/screens/filters/Details.tsx index e85a278eaf..39b2151a56 100644 --- a/web/src/screens/filters/Details.tsx +++ b/web/src/screens/filters/Details.tsx @@ -4,7 +4,7 @@ */ import { Suspense, useEffect, useRef } from "react"; -import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; +import {useMutation, useSuspenseQuery} from "@tanstack/react-query"; import { Form, Formik, useFormikContext } from "formik"; import type { FormikErrors, FormikValues } from "formik"; import { z } from "zod"; @@ -25,7 +25,7 @@ import { SectionLoader } from "@components/SectionLoader"; import { filterKeys } from "./List"; import * as Section from "./sections"; -import {filterRoute} from "../../App.tsx"; +import {filterQueryOptions, filterRoute} from "@app/App.tsx"; import {Link, Outlet, useNavigate} from "@tanstack/react-router"; interface tabType { @@ -301,27 +301,13 @@ const schema = z.object({ }); export const FilterDetails = () => { - const queryClient = useQueryClient(); + const ctx = filterRoute.useRouteContext() + const queryClient = ctx.queryClient // const navigate = useNavigate(); - // const { filterId } = useParams<{ filterId: string }>(); - // - // if (filterId === "0" || filterId === undefined) { - // navigate("/filters"); - // } - const { filterId } = filterRoute.useParams() - - const id = parseInt(filterId!); - - const { isLoading, isError, data: filter } = useQuery({ - queryKey: filterKeys.detail(id), - queryFn: ({ queryKey }) => APIClient.filters.getByID(queryKey[2]), - refetchOnWindowFocus: false - }); - - if (isError) { - // navigate("/filters"); - } + const params = filterRoute.useParams() + const filterQuery = useSuspenseQuery(filterQueryOptions(params.filterId)) + const filter = filterQuery.data const updateMutation = useMutation({ mutationFn: (filter: Filter) => APIClient.filters.update(filter), @@ -345,7 +331,7 @@ export const FilterDetails = () => { onSuccess: () => { // Invalidate filters just in case, most likely not necessary but can't hurt. queryClient.invalidateQueries({ queryKey: filterKeys.lists() }); - queryClient.invalidateQueries({ queryKey: filterKeys.detail(id) }); + queryClient.invalidateQueries({ queryKey: filterKeys.detail(params.filterId) }); toast.custom((t) => ( @@ -486,7 +472,7 @@ export const FilterDetails = () => { deleteAction={deleteAction} dirty={dirty} reset={resetForm} - isLoading={isLoading} + isLoading={false} /> diff --git a/web/src/screens/filters/List.tsx b/web/src/screens/filters/List.tsx index cbf1361d42..e18e977abb 100644 --- a/web/src/screens/filters/List.tsx +++ b/web/src/screens/filters/List.tsx @@ -219,7 +219,7 @@ function FilterList({ toggleCreateFilter }: any) {
-
+ {/*
*/}
@@ -460,7 +460,7 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => { // to={filter.id.toString()} to="/filters/$filterId" params={{ - filterId: filter.id.toString() + filterId: filter.id }} className={classNames( active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300", @@ -634,7 +634,7 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) { // to={filter.id.toString()} to="/filters/$filterId" params={{ - filterId: filter.id.toString() + filterId: filter.id }} className="transition w-full break-words whitespace-wrap text-sm font-bold text-gray-800 dark:text-gray-100 hover:text-black dark:hover:text-gray-350" > @@ -653,9 +653,9 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) { @@ -678,9 +678,9 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) { ) : ( diff --git a/web/src/screens/releases/ReleaseTable.tsx b/web/src/screens/releases/ReleaseTable.tsx index 7151c90335..dab827e46f 100644 --- a/web/src/screens/releases/ReleaseTable.tsx +++ b/web/src/screens/releases/ReleaseTable.tsx @@ -32,7 +32,6 @@ export const releaseKeys = { detail: (id: number) => [...releaseKeys.details(), id] as const }; - type TableState = { queryPageIndex: number; queryPageSize: number; diff --git a/web/src/screens/settings/Account.tsx b/web/src/screens/settings/Account.tsx index 6e90df8e46..ac7a403126 100644 --- a/web/src/screens/settings/Account.tsx +++ b/web/src/screens/settings/Account.tsx @@ -12,6 +12,7 @@ import { PasswordField, TextField } from "@components/inputs"; import { AuthContext } from "@utils/Context"; import toast from "react-hot-toast"; import { UserIcon } from "@heroicons/react/24/solid"; +import { settingsAccountRoute } from "@app/App.tsx"; const AccountSettings = () => (
{ const errors: Record = {}; @@ -76,7 +76,7 @@ function Credentials() {
- {data && data.length > 0 ? ( + {apikeysQuery.data && apikeysQuery.data.length > 0 ? (
  • @@ -69,7 +72,7 @@ function APISettings() {
  • - {data.map((k, idx) => )} + {apikeysQuery.data.map((k, idx) => )}
) : ( { diff --git a/web/src/screens/settings/DownloadClient.tsx b/web/src/screens/settings/DownloadClient.tsx index 37354253ef..87bc551db9 100644 --- a/web/src/screens/settings/DownloadClient.tsx +++ b/web/src/screens/settings/DownloadClient.tsx @@ -4,7 +4,7 @@ */ import { useState, useMemo } from "react"; -import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; +import {useMutation, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; import { PlusIcon } from "@heroicons/react/24/solid"; import toast from "react-hot-toast"; @@ -17,6 +17,7 @@ import Toast from "@components/notifications/Toast"; import { Checkbox } from "@components/Checkbox"; import { Section } from "./_components"; +import { downloadClientsQueryOptions } from "@app/App.tsx"; export const clientKeys = { all: ["download_clients"] as const, @@ -140,17 +141,9 @@ function ListItem({ client }: DLSettingsItemProps) { function DownloadClientSettings() { const [addClientIsOpen, toggleAddClient] = useToggle(false); - const { error, data } = useQuery({ - queryKey: clientKeys.lists(), - queryFn: APIClient.download_clients.getAll, - refetchOnWindowFocus: false - }); - - const sortedClients = useSort(data || []); + const downloadClientsQuery = useSuspenseQuery(downloadClientsQueryOptions()) - if (error) { - return

Failed to fetch download clients

; - } + const sortedClients = useSort(downloadClientsQuery.data || []); return (
- {data && data.length > 0 ? ( + {feedsQuery.data && feedsQuery.data.length > 0 ? (
  • { function IndexerSettings() { const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false); + // const ctx = settingsIndexersRoute.useRouteContext() + // const queryClient = ctx.queryClient - const { error, data } = useQuery({ - queryKey: indexerKeys.lists(), - queryFn: APIClient.indexers.getAll, - refetchOnWindowFocus: false - }); + const indexersQuery = useSuspenseQuery(indexersQueryOptions()) - const sortedIndexers = useSort(data || []); + const indexers = indexersQuery.data - if (error) { - return (

    An error has occurred

    ); - } + // const { error, data } = useQuery({ + // queryKey: indexerKeys.lists(), + // queryFn: APIClient.indexers.getAll, + // refetchOnWindowFocus: false + // }); + + const sortedIndexers = useSort(indexers || []); + + // if (error) { + // return (

    An error has occurred

    ); + // } return (
    } > - + {/**/}
    {sortedIndexers.items.length ? ( diff --git a/web/src/screens/settings/Irc.tsx b/web/src/screens/settings/Irc.tsx index 19b2644a05..81aba84c31 100644 --- a/web/src/screens/settings/Irc.tsx +++ b/web/src/screens/settings/Irc.tsx @@ -30,6 +30,7 @@ import { Checkbox } from "@components/Checkbox"; // import { useForm } from "react-hook-form"; import { Section } from "./_components"; +import { ircQueryOptions } from "@app/App.tsx"; export const ircKeys = { all: ["irc_networks"] as const, @@ -98,14 +99,21 @@ const IrcSettings = () => { const [expandNetworks, toggleExpand] = useToggle(false); const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false); - const { data } = useQuery({ - queryKey: ircKeys.lists(), - queryFn: APIClient.irc.getNetworks, - refetchOnWindowFocus: false, - refetchInterval: 3000 // Refetch every 3 seconds - }); + // const ctx = settingsIrcRoute.useRouteContext() + // const queryClient = ctx.queryClient + + const ircQuery = useSuspenseQuery(ircQueryOptions()) + + // const networks = ircQuery.data + + // const { data } = useQuery({ + // queryKey: ircKeys.lists(), + // queryFn: APIClient.irc.getNetworks, + // refetchOnWindowFocus: false, + // refetchInterval: 3000 // Refetch every 3 seconds + // }); - const sortedNetworks = useSort(data || []); + const sortedNetworks = useSort(ircQuery.data || []); return (
    {
    - {data && data.length > 0 ? ( + {ircQuery.data && ircQuery.data.length > 0 ? (
    • ); function LogSettings() { - const { isError, error, isLoading, data } = useQuery({ - queryKey: ["config"], - queryFn: APIClient.config.get, - retry: false, - refetchOnWindowFocus: false - }); + const ctx = settingsLogRoute.useRouteContext() + const queryClient = ctx.queryClient - if (isError) { - console.log(error); - } + const configQuery = useSuspenseQuery(configQueryOptions()) - const queryClient = useQueryClient(); + const config = configQuery.data const setLogLevelUpdateMutation = useMutation({ mutationFn: (value: string) => APIClient.config.update({ log_level: value }), @@ -96,9 +91,9 @@ function LogSettings() { >
      - {!isLoading && data && ( + {!configQuery.isLoading && config && (
      - + setLogLevelUpdateMutation.mutate(value.value)} /> } /> - - + + )}
      - + {/**/}
      diff --git a/web/src/screens/settings/Notifications.tsx b/web/src/screens/settings/Notifications.tsx index 0b0265a070..aa2103781e 100644 --- a/web/src/screens/settings/Notifications.tsx +++ b/web/src/screens/settings/Notifications.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; +import {useMutation, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; import { APIClient } from "@api/APIClient"; import { EmptySimple } from "@components/emptystates"; @@ -16,6 +16,7 @@ import { Section } from "./_components"; import { PlusIcon } from "@heroicons/react/24/solid"; import { Checkbox } from "@components/Checkbox"; import { DiscordIcon, GotifyIcon, LunaSeaIcon, NotifiarrIcon, NtfyIcon, PushoverIcon, TelegramIcon } from "./_components"; +import { notificationsQueryOptions } from "@app/App.tsx"; export const notificationKeys = { all: ["notifications"] as const, @@ -27,12 +28,7 @@ export const notificationKeys = { function NotificationSettings() { const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false); - const { data } = useQuery({ - queryKey: notificationKeys.lists(), - queryFn: APIClient.notifications.getAll, - refetchOnWindowFocus: false - } - ); + const notificationsQuery = useSuspenseQuery(notificationsQueryOptions()) return (
      - {data && data.length > 0 ? ( + {notificationsQuery.data && notificationsQuery.data.length > 0 ? (
      • Enabled
        @@ -60,7 +56,7 @@ function NotificationSettings() {
        Events
      • - {data.map((n) => )} + {notificationsQuery.data.map((n) => )}
      ) : ( diff --git a/web/src/utils/index.ts b/web/src/utils/index.ts index d94c65057c..bc9586af2f 100644 --- a/web/src/utils/index.ts +++ b/web/src/utils/index.ts @@ -20,7 +20,6 @@ export function baseUrl() { baseUrl = window.APP.baseUrl; } } - console.log("baseurl", baseUrl) return baseUrl; } From 316be1c9ea63147d725fc10a0bb342387dd3f3a8 Mon Sep 17 00:00:00 2001 From: ze0s Date: Fri, 12 Jan 2024 16:38:00 +0100 Subject: [PATCH 13/46] refactor(web): useSuspense for settings --- web/src/App.tsx | 36 ++++--- web/src/screens/Logs.tsx | 9 +- web/src/screens/Settings.tsx | 12 +-- web/src/screens/dashboard/ActivityTable.tsx | 105 +++++++++++++++++++- web/src/screens/filters/Details.tsx | 13 +-- web/src/screens/filters/List.tsx | 2 +- web/src/screens/releases/Filters.tsx | 2 +- web/src/screens/settings/Logs.tsx | 2 +- 8 files changed, 136 insertions(+), 45 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 405fb88238..1bb059d3f2 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -14,17 +14,19 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ErrorBoundary } from "react-error-boundary"; import { toast, Toaster } from "react-hot-toast"; -import { AuthContext, SettingsContext } from "./utils/Context"; +import { SettingsContext } from "./utils/Context"; import { ErrorPage } from "./components/alerts"; import Toast from "./components/notifications/Toast"; import { Portal } from "react-portal"; import { Outlet, RouterProvider, - Link, Router, Route, - RootRoute, rootRouteWithContext, redirect, ErrorComponent, useRouterState, + rootRouteWithContext, + redirect, + ErrorComponent, + useRouterState, } from '@tanstack/react-router' import { TanStackRouterDevtools } from '@tanstack/router-devtools' import {Header} from "@components/header"; @@ -51,7 +53,6 @@ import {Logs} from "@screens/Logs.tsx"; import {Login} from "@screens/auth"; import {APIClient} from "@api/APIClient.ts"; import {baseUrl} from "@utils"; -import * as querystring from "querystring"; import {filterKeys} from "@screens/filters/List.tsx"; export const queryClient = new QueryClient({ @@ -153,6 +154,14 @@ export const apikeysQueryOptions = () => const dashboardRoute = new Route({ getParentRoute: () => authIndexRoute, path: '/', + loader: () => { + // https://tanstack.com/router/v1/docs/guide/deferred-data-loading#deferred-data-loading-with-defer-and-await + // TODO load stats + + // TODO load recent releases + + return {} + }, component: Dashboard, }) @@ -256,6 +265,13 @@ export const releasesIndexRoute = new Route({ export const settingsRoute = new Route({ getParentRoute: () => authIndexRoute, path: 'settings', + pendingMs: 3000, + pendingComponent: () => ( +
      + {/**/} + +
      + ), component: Settings }) @@ -452,9 +468,7 @@ const routeTree = rootRoute.addChildren([ const router = new Router({ routeTree, defaultPendingComponent: () => ( -
      - -
      + ), defaultErrorComponent: ({ error }) => , context: { @@ -490,9 +504,6 @@ const auth: Auth = { export function App() { // const { reset } = useQueryErrorResetBoundary(); - // const authContext = AuthContext.useValue(); - const settings = SettingsContext.useValue(); - return ( // - {/*{settings.debug ? (*/} - {/* <>*/} - {/* */} - {/* */} - {/*) : null}*/} // ); diff --git a/web/src/screens/Logs.tsx b/web/src/screens/Logs.tsx index d5d252bedd..d4cd419a72 100644 --- a/web/src/screens/Logs.tsx +++ b/web/src/screens/Logs.tsx @@ -4,7 +4,7 @@ */ import { Fragment, useEffect, useRef, useState } from "react"; -import {useQuery, useSuspenseQuery} from "@tanstack/react-query"; +import { useSuspenseQuery } from "@tanstack/react-query"; import { Menu, Transition } from "@headlessui/react"; import { DebounceInput } from "react-debounce-input"; import { @@ -23,7 +23,6 @@ import { EmptySimple } from "@components/emptystates"; import { RingResizeSpinner } from "@components/Icons"; import Toast from "@components/notifications/Toast"; - type LogEvent = { time: string; level: string; @@ -174,7 +173,7 @@ export const Logs = () => { }; export const LogFiles = () => { - const { isError, error, data } = useQuery({ + const { isError, error, data } = useSuspenseQuery({ queryKey: ["log-files"], queryFn: () => APIClient.logs.files(), retry: false, @@ -182,7 +181,7 @@ export const LogFiles = () => { }); if (isError) { - console.log(error); + console.log("could not load log files", error); } return ( @@ -194,7 +193,7 @@ export const LogFiles = () => {

      - {data && data.files.length > 0 ? ( + {data && data.files && data.files.length > 0 ? (
      • diff --git a/web/src/screens/Settings.tsx b/web/src/screens/Settings.tsx index 23fec0753f..f278c17b8f 100644 --- a/web/src/screens/Settings.tsx +++ b/web/src/screens/Settings.tsx @@ -57,14 +57,8 @@ function SubNavLink({ item }: NavLinkProps) { key={item.href} to={item.href} activeOptions={{ exact: item.exact }} - // className="transition group border-l-4 px-3 py-2 flex items-center text-sm font-medium" - // end - // className={({ isActive }) => classNames( - // "transition group border-l-4 px-3 py-2 flex items-center text-sm font-medium", - // isActive - // ? "font-bold bg-blue-100 dark:bg-gray-700 border-sky-500 dark:border-blue-500 text-sky-700 dark:text-gray-200 hover:bg-blue-200 dark:hover:bg-gray-600 hover:text-sky-900 dark:hover:text-white" - // : "border-transparent text-gray-900 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-gray-300" - // )} + search={{}} + params={{}} // aria-current={splitLocation[2] === item.href ? "page" : undefined} > {({ isActive }) => { @@ -98,7 +92,7 @@ function SidebarNav({ subNavigation }: SidebarNavProps) { diff --git a/web/src/screens/dashboard/ActivityTable.tsx b/web/src/screens/dashboard/ActivityTable.tsx index 5cc1062cae..af541d416d 100644 --- a/web/src/screens/dashboard/ActivityTable.tsx +++ b/web/src/screens/dashboard/ActivityTable.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import React, { useState } from "react"; +import React, {Suspense, useState} from "react"; import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { useTable, @@ -19,6 +19,7 @@ import * as Icons from "@components/Icons"; import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid"; import * as DataTable from "@components/data-table"; import { RandomLinuxIsos } from "@utils"; +import {SectionLoader} from "@components/SectionLoader.tsx"; // This is a custom filter UI for selecting // a unique option from a list @@ -159,6 +160,28 @@ function Table({ columns, data }: TableProps) { ); } +export const RecentActivityTable = () => { + return ( +
        +

        + Recent activity +

        +
        + + +
        + } + > + {/**/} + + +
        +
        + ) +} + export const ActivityTable = () => { const columns = React.useMemo(() => [ { @@ -185,7 +208,7 @@ export const ActivityTable = () => { } ] as Column[], []); - const { isLoading, data } = useQuery({ + const { isLoading, data } = useSuspenseQuery({ queryKey: ["dash_recent_releases"], queryFn: APIClient.release.findRecent, refetchOnWindowFocus: false @@ -198,7 +221,7 @@ export const ActivityTable = () => { return (

        -   + Recent activity

        @@ -245,3 +268,79 @@ export const ActivityTable = () => {
        ); }; + +export const ActivityTableContent = () => { + const columns = React.useMemo(() => [ + { + Header: "Age", + accessor: "timestamp", + Cell: DataTable.AgeCell + }, + { + Header: "Release", + accessor: "name", + Cell: DataTable.TitleCell + }, + { + Header: "Actions", + accessor: "action_status", + Cell: DataTable.ReleaseStatusCell + }, + { + Header: "Indexer", + accessor: "indexer", + Cell: DataTable.TitleCell, + Filter: SelectColumnFilter, + filter: "includes" + } + ] as Column[], []); + + const { isLoading, data } = useSuspenseQuery({ + queryKey: ["dash_recent_releases"], + queryFn: APIClient.release.findRecent, + refetchOnWindowFocus: false + }); + + const [modifiedData, setModifiedData] = useState([]); + const [showLinuxIsos, setShowLinuxIsos] = useState(false); + + if (isLoading) { + return ( + + ); + } + + const toggleReleaseNames = () => { + setShowLinuxIsos(!showLinuxIsos); + if (!showLinuxIsos && data && data.data) { + const randomNames = RandomLinuxIsos(data.data.length); + const newData: Release[] = data.data.map((item, index) => ({ + ...item, + name: `${randomNames[index]}.iso`, + indexer: index % 2 === 0 ? "distrowatch" : "linuxtracker" + })); + setModifiedData(newData); + } + }; + + const displayData = showLinuxIsos ? modifiedData : (data?.data ?? []); + + return ( + <> + + + + + ); +}; diff --git a/web/src/screens/filters/Details.tsx b/web/src/screens/filters/Details.tsx index 39b2151a56..c4d7aab5da 100644 --- a/web/src/screens/filters/Details.tsx +++ b/web/src/screens/filters/Details.tsx @@ -54,20 +54,13 @@ function TabNavLink({ item }: NavLinkProps) { // we need to clean the / if it's a base root path return ( classNames( - // "transition border-b-2 whitespace-nowrap py-4 duration-3000 px-1 font-medium text-sm first:rounded-tl-lg last:rounded-tr-lg", - // isActive - // ? "text-blue-600 dark:text-white border-blue-600 dark:border-blue-500" - // : "text-gray-550 hover:text-blue-500 dark:hover:text-white border-transparent" - // )} + search={{}} + params={{}} // aria-current={splitLocation[2] === item.href ? "page" : undefined} // className="transition border-b-2 whitespace-nowrap py-4 duration-3000 px-1 font-medium text-sm first:rounded-tl-lg last:rounded-tr-lg" > - {/*{item.name}*/} {({ isActive }) => { return ( {
        diff --git a/web/src/screens/filters/List.tsx b/web/src/screens/filters/List.tsx index e18e977abb..e90e12c04d 100644 --- a/web/src/screens/filters/List.tsx +++ b/web/src/screens/filters/List.tsx @@ -219,7 +219,7 @@ function FilterList({ toggleCreateFilter }: any) {
        - {/*
        */} +
        diff --git a/web/src/screens/releases/Filters.tsx b/web/src/screens/releases/Filters.tsx index d33cb546e6..e9c504358c 100644 --- a/web/src/screens/releases/Filters.tsx +++ b/web/src/screens/releases/Filters.tsx @@ -83,7 +83,7 @@ export const IndexerSelectColumnFilter = ({ currentValue={filterValue} onChange={setFilter} > - {isSuccess && data?.map((indexer, idx) => ( + {isSuccess && data && data?.map((indexer, idx) => ( ))} diff --git a/web/src/screens/settings/Logs.tsx b/web/src/screens/settings/Logs.tsx index 3878daeda4..634317dcd5 100644 --- a/web/src/screens/settings/Logs.tsx +++ b/web/src/screens/settings/Logs.tsx @@ -113,7 +113,7 @@ function LogSettings() { )}
        - {/**/} +
        From 75c8b4904f1ace0260cc7e67fabd1b8491975eb9 Mon Sep 17 00:00:00 2001 From: ze0s Date: Fri, 12 Jan 2024 16:39:35 +0100 Subject: [PATCH 14/46] refactor(web): invalidate cookie in middleware --- internal/http/middleware.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/http/middleware.go b/internal/http/middleware.go index e8b15e71d1..5a08e9e7dd 100644 --- a/internal/http/middleware.go +++ b/internal/http/middleware.go @@ -34,7 +34,17 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler { session, err := s.cookieStore.Get(r, "user_session") if err != nil { s.log.Error().Err(err).Msgf("could not get session from cookieStore") - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + session.Values["authenticated"] = false + + // MaxAge<0 means delete cookie immediately + session.Options.MaxAge = -1 + + if err := session.Save(r, w); err != nil { + s.log.Error().Err(err).Msgf("could not store session: %s", r.RemoteAddr) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + http.Error(w, err.Error(), http.StatusForbidden) return } From b18ba9a5f6d6447db7fd6a5947ae18407389e489 Mon Sep 17 00:00:00 2001 From: ze0s Date: Mon, 5 Feb 2024 12:52:09 +0100 Subject: [PATCH 15/46] fix: loclfile --- web/pnpm-lock.yaml | 36 ++- web/src/screens/filters/sections/Advanced.tsx | 287 +++++++++--------- 2 files changed, 168 insertions(+), 155 deletions(-) diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 4d906a7151..9cb829466a 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -143,12 +143,12 @@ devDependencies: '@microsoft/eslint-formatter-sarif': specifier: ^3.0.0 version: 3.0.0 - '@tanstack/router-devtools': - specifier: ^1.1.4 - version: 1.1.4(react-dom@18.2.0)(react@18.2.0) '@rollup/wasm-node': specifier: ^4.9.6 version: 4.9.6 + '@tanstack/router-devtools': + specifier: ^1.1.4 + version: 1.1.4(react-dom@18.2.0)(react@18.2.0) eslint: specifier: ^8.56.0 version: 8.56.0 @@ -1438,7 +1438,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.1 - dev: false /@babel/runtime@7.23.9: resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==} @@ -2399,6 +2398,17 @@ packages: react-dom: 18.2.0(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0) + /@tanstack/react-virtual@3.0.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-9XbRLPKgnhMwwmuQMnJMv+5a9sitGNCSEtf/AZXzmJdesYk7XsjYHaEDny+IrJzvPNwZliIIDwCRiaUqR3zzCA==} + peerDependencies: + react: ^18.2.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@tanstack/virtual-core': 3.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@tanstack/router-devtools@1.1.4(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-/chCH/ty386podf2vwON55pAJ9MQ+94vSv35tsF6LgUlTXCw8fYOL4WR1Fp6PgBsUXrPfDt3TMAueVqSitVpeA==} engines: {node: '>=12'} @@ -2416,17 +2426,6 @@ packages: /@tanstack/store@0.1.3: resolution: {integrity: sha512-GnolmC8Fr4mvsHE1fGQmR3Nm0eBO3KnZjDU0a+P3TeQNM/dDscFGxtA7p31NplQNW3KwBw4t1RVFmz0VeKLxcw==} - /@tanstack/react-virtual@3.0.2(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-9XbRLPKgnhMwwmuQMnJMv+5a9sitGNCSEtf/AZXzmJdesYk7XsjYHaEDny+IrJzvPNwZliIIDwCRiaUqR3zzCA==} - peerDependencies: - react: ^18.2.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@tanstack/virtual-core': 3.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - /@tanstack/virtual-core@3.0.0: resolution: {integrity: sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==} dev: false @@ -3162,6 +3161,13 @@ packages: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} dev: false + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.23.9 + dev: true + /date-fns@3.3.1: resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==} dev: false diff --git a/web/src/screens/filters/sections/Advanced.tsx b/web/src/screens/filters/sections/Advanced.tsx index b33723cf04..91a277a249 100644 --- a/web/src/screens/filters/sections/Advanced.tsx +++ b/web/src/screens/filters/sections/Advanced.tsx @@ -1,4 +1,4 @@ -import {FormikValues, useFormikContext} from "formik"; +import { useFormikContext } from "formik"; import { DocsLink } from "@components/ExternalLink"; import { WarningAlert } from "@components/alerts"; @@ -10,9 +10,9 @@ import { CollapsibleSection } from "./_components"; import * as Components from "./_components"; import { classNames } from "@utils"; -type ValueConsumer = { - values: FormikValues; -}; +// type ValueConsumer = { +// values: FormikValues; +// }; const Releases = () => { const { values } = useFormikContext(); @@ -97,7 +97,7 @@ const Releases = () => { } const Groups = () => { - const { values } = useFormikContext(); + // const { values } = useFormikContext(); return ( { } const Categories = () => { - const { values } = useFormikContext(); + // const { values } = useFormikContext(); return ( { } const Tags = () => { - const { values } = useFormikContext(); + // const { values } = useFormikContext(); return ( { } const Uploaders = () => { - const { values } = useFormikContext(); + // const { values } = useFormikContext(); return ( { } const Language = () => { - const { values } = useFormikContext(); + // const { values } = useFormikContext(); return ( { } const Origins = () => { - const { values } = useFormikContext(); + // const { values } = useFormikContext(); return ( { ); } -const FeedSpecific = ({ values }: ValueConsumer) => ( - These options are only for Feeds such as RSS, Torznab and Newznab - } - > - - { + const { values } = useFormikContext(); + return ( + These options are only for Feeds such as RSS, Torznab and Newznab + } + > + + + + + +

        This field has full regex support (Golang flavour).

        + +
        +
        +

        Remember to tick Use Regex below if using more than * and ?.

        + + } + /> + +

        This field has full regex support (Golang flavour).

        + +
        +
        +

        Remember to tick Use Regex below if using more than * and ?.

        + + } + /> + +

        Number of min seeders as specified by the respective unit. Only for Torznab

        + + + } + /> + +

        Number of max seeders as specified by the respective unit. Only for Torznab

        + + + } + /> + +

        Number of min leechers as specified by the respective unit. Only for Torznab

        + + + } + /> + +

        Number of max leechers as specified by the respective unit. Only for Torznab

        + + + } /> -
        +
        + ); +} +const RawReleaseTags = () => { + const { values } = useFormikContext(); - -

        This field has full regex support (Golang flavour).

        - -
        -
        -

        Remember to tick Use Regex below if using more than * and ?.

        - - } - /> - -

        This field has full regex support (Golang flavour).

        - -
        -
        -

        Remember to tick Use Regex below if using more than * and ?.

        - - } - /> - -

        Number of min seeders as specified by the respective unit. Only for Torznab

        - - - } - /> - -

        Number of max seeders as specified by the respective unit. Only for Torznab

        - - - } - /> - -

        Number of min leechers as specified by the respective unit. Only for Torznab

        - - - } - /> - -

        Number of max leechers as specified by the respective unit. Only for Torznab

        - - - } - /> -
        -); -const RawReleaseTags = ({ values }: ValueConsumer) => ( - - Advanced users only - {": "}This is the raw releaseTags string from the announce. - - } - > - These might not be what you think they are. For very advanced users who know how things are parsed. + return ( + + Advanced users only + {": "}This is the raw releaseTags string from the announce. + } - /> - - - + These might not be what you think they are. For very advanced users who know how things are parsed. + } /> - - - - -); + + + + + + + + ); +} export const Advanced = () => { return ( From ceca8260a3e047964179de7d1389856c806c4144 Mon Sep 17 00:00:00 2001 From: ze0s Date: Wed, 7 Feb 2024 06:37:45 +0100 Subject: [PATCH 16/46] fix: load filter/id --- web/src/App.tsx | 101 +++++++++++++------ web/src/api/APIClient.ts | 2 +- web/src/screens/dashboard/Stats.tsx | 2 +- web/src/screens/filters/sections/General.tsx | 17 ++-- web/src/screens/releases/ReleaseTable.tsx | 14 +-- web/src/utils/index.ts | 14 +++ 6 files changed, 100 insertions(+), 50 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 1bb059d3f2..26e612f753 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -29,31 +29,29 @@ import { useRouterState, } from '@tanstack/react-router' import { TanStackRouterDevtools } from '@tanstack/router-devtools' -import {Header} from "@components/header"; -import {Suspense} from "react"; -import {SectionLoader, Spinner} from "@components/SectionLoader.tsx"; -import {Dashboard} from "@screens/Dashboard.tsx"; -import {FilterDetails, Filters} from "@screens/filters"; -import {Section} from "@screens/filters/sections/_components.tsx"; -import {Actions, Advanced, External, General, MoviesTv, Music} from "@screens/filters/sections"; -import {Releases} from "@screens/Releases.tsx"; -import {z} from "zod"; -import {Settings} from "@screens/Settings.tsx"; +import { Header } from "@components/header"; +import { SectionLoader, Spinner } from "@components/SectionLoader.tsx"; +import { Dashboard } from "@screens/Dashboard.tsx"; +import { FilterDetails, Filters } from "@screens/filters"; +import { Actions, Advanced, External, General, MoviesTv, Music } from "@screens/filters/sections"; +import { Releases } from "@screens/Releases.tsx"; +import { z } from "zod"; +import { Settings } from "@screens/Settings.tsx"; import LogSettings from "@screens/settings/Logs.tsx"; -import IndexerSettings, {indexerKeys} from "@screens/settings/Indexer.tsx"; -import IrcSettings, {ircKeys} from "@screens/settings/Irc.tsx"; -import FeedSettings, {feedKeys} from "@screens/settings/Feed.tsx"; -import DownloadClientSettings, {clientKeys} from "@screens/settings/DownloadClient.tsx"; -import NotificationSettings, {notificationKeys} from "@screens/settings/Notifications.tsx"; -import APISettings, {apiKeys} from "@screens/settings/Api.tsx"; +import IndexerSettings, { indexerKeys } from "@screens/settings/Indexer.tsx"; +import IrcSettings, { ircKeys } from "@screens/settings/Irc.tsx"; +import FeedSettings, { feedKeys } from "@screens/settings/Feed.tsx"; +import DownloadClientSettings, { clientKeys } from "@screens/settings/DownloadClient.tsx"; +import NotificationSettings, { notificationKeys } from "@screens/settings/Notifications.tsx"; +import APISettings, { apiKeys } from "@screens/settings/Api.tsx"; import ReleaseSettings from "@screens/settings/Releases.tsx"; import AccountSettings from "@screens/settings/Account.tsx"; import ApplicationSettings from "@screens/settings/Application.tsx"; -import {Logs} from "@screens/Logs.tsx"; -import {Login} from "@screens/auth"; -import {APIClient} from "@api/APIClient.ts"; -import {baseUrl} from "@utils"; -import {filterKeys} from "@screens/filters/List.tsx"; +import { Logs } from "@screens/Logs.tsx"; +import { Login } from "@screens/auth"; +import { APIClient } from "@api/APIClient.ts"; +import { routerBasePath } from "@utils"; +import { filterKeys } from "@screens/filters/List.tsx"; export const queryClient = new QueryClient({ queryCache: new QueryCache({ @@ -119,6 +117,13 @@ export const indexersQueryOptions = () => queryFn: () => APIClient.indexers.getAll() }) +export const indexersOptionsQueryOptions = () => + queryOptions({ + queryKey: ["filters", "indexer_list"], + queryFn: () => APIClient.indexers.getOptions(), + refetchOnWindowFocus: false, + }) + export const ircQueryOptions = () => queryOptions({ queryKey: ircKeys.lists(), @@ -202,13 +207,42 @@ export const filterRoute = new Route({ // loaderDeps: ({ search }) => ({ // filterId: search.filterId // }), - loader: (opts) => opts.context.queryClient.ensureQueryData(filterQueryOptions(opts.params.filterId)), + // loaderDeps: (opts) => ({ + // filterId: opts.search + // }), + loader: (opts) => { + console.log("filter route loader") + return opts.context.queryClient.ensureQueryData(filterQueryOptions(opts.params.filterId)) + // const filterData = opts.context.queryClient.ensureQueryData(filterQueryOptions(opts.params.filterId)) + // const indexersData = opts.context.queryClient.ensureQueryData(indexersOptionsQueryOptions()) + // + // return { + // filterData, + // indexersData + // } + }, component: FilterDetails }) export const filterGeneralRoute = new Route({ getParentRoute: () => filterRoute, path: '/', + // path: '/$filterId', + // parseParams: (params) => ({ + // filterId: z.number().int().parse(Number(params.filterId)), + // }), + // stringifyParams: ({ filterId }) => ({ filterId: `${filterId}` }), + // loader: (opts) => { + // console.log("filter general route loader") + // return opts.context.queryClient.ensureQueryData(filterQueryOptions(opts.params.filterId)) + // // const filterData = opts.context.queryClient.ensureQueryData(filterQueryOptions(opts.params.filterId)) + // // const indexersData = opts.context.queryClient.ensureQueryData(indexersOptionsQueryOptions()) + // // + // // return { + // // filterData, + // // indexersData + // // } + // }, component: General }) @@ -248,8 +282,13 @@ const releasesRoute = new Route({ }) export const releasesSearchSchema = z.object({ - // page: z.number().catch(1), - filter: z.string().catch(''), + offset: z.number().optional(), + limit: z.number().optional(), + filter: z.string().optional(), + q: z.string().optional(), + // action_status: z.string().optional(), + action_status: z.enum(['PUSH_APPROVED', 'PUSH_REJECTED', 'PUSH_ERROR', '']).optional(), + // filters: z.array().catch(''), // sort: z.enum(['newest', 'oldest', 'price']).catch('newest'), }) @@ -389,11 +428,11 @@ export const authRoute = new Route({ // Before loading, authenticate the user via our auth context // This will also happen during prefetching (e.g. hovering over links, etc) beforeLoad: ({ context, location }) => { - console.log("before load") + console.log("auth before load") // If the user is not logged in, check for item in localStorage if (!context.auth.isLoggedIn) { - console.log("before load: not logged in") + console.log("auth before load: not logged in") const key = "user_auth" const storage = localStorage.getItem(key); if (storage) { @@ -402,15 +441,15 @@ export const authRoute = new Route({ if (json === null) { console.warn(`JSON localStorage value for '${key}' context state is null`); } else { - console.log("local storage found", json) - console.log("ctx", context.auth) + console.log("auth local storage found", json) + console.log("auth ctx", context.auth) context.auth.isLoggedIn = json.isLoggedIn context.auth.username = json.username // context.auth = { ...json }; - console.log("ctx", context.auth) + console.log("auth ctx", context.auth) } } catch (e) { - console.error(`Failed to merge ${key} context state: ${e}`); + console.error(`auth Failed to merge ${key} context state: ${e}`); } } else { // If the user is logged out, redirect them to the login page @@ -515,7 +554,7 @@ export function App() { {/**/} ( } case 403: { // Remove auth info from localStorage - AuthContext.reset(); + // AuthContext.reset(); // Show an error toast to notify the user what occurred return Promise.reject(response); diff --git a/web/src/screens/dashboard/Stats.tsx b/web/src/screens/dashboard/Stats.tsx index 0ee66080ac..2009da0b5a 100644 --- a/web/src/screens/dashboard/Stats.tsx +++ b/web/src/screens/dashboard/Stats.tsx @@ -22,7 +22,7 @@ const StatsItem = ({ name, placeholder, value, to, eventType }: StatsItemProps) className="group relative px-4 py-3 cursor-pointer overflow-hidden rounded-lg shadow-lg bg-white dark:bg-gray-800 hover:scale-110 hover:shadow-xl transition-all duration-200 ease-in-out" to={to} search={{ - filter: eventType + push_status: eventType }} >
        diff --git a/web/src/screens/filters/sections/General.tsx b/web/src/screens/filters/sections/General.tsx index 0f608aaa75..916a1407aa 100644 --- a/web/src/screens/filters/sections/General.tsx +++ b/web/src/screens/filters/sections/General.tsx @@ -1,25 +1,22 @@ -import { useQuery } from "@tanstack/react-query"; +import { useSuspenseQuery } from "@tanstack/react-query"; -import { APIClient } from "@api/APIClient"; import { downloadsPerUnitOptions } from "@domain/constants"; import { DocsLink } from "@components/ExternalLink"; import * as Input from "@components/inputs"; import * as Components from "./_components"; +import { indexersOptionsQueryOptions } from "@app/App.tsx"; const MapIndexer = (indexer: Indexer) => ( { label: indexer.name, value: indexer.id } as Input.MultiSelectOption ); export const General = () => { - const { isLoading, data } = useQuery({ - queryKey: ["filters", "indexer_list"], - queryFn: APIClient.indexers.getOptions, - refetchOnWindowFocus: false - }); + const indexersQuery = useSuspenseQuery(indexersOptionsQueryOptions()) + const indexerOptions = indexersQuery.data && indexersQuery.data.map(MapIndexer) - const indexerOptions = data?.map(MapIndexer) ?? []; + // const indexerOptions = data?.map(MapIndexer) ?? []; return ( @@ -27,9 +24,9 @@ export const General = () => { - {!isLoading && ( + {/*{!isLoading && (*/} - )} + {/*)}*/} diff --git a/web/src/screens/releases/ReleaseTable.tsx b/web/src/screens/releases/ReleaseTable.tsx index 5ae0120fdc..23afe61d8a 100644 --- a/web/src/screens/releases/ReleaseTable.tsx +++ b/web/src/screens/releases/ReleaseTable.tsx @@ -4,7 +4,6 @@ */ import React, { useState } from "react"; -import { useLocation } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { Column, useFilters, usePagination, useSortBy, useTable } from "react-table"; @@ -82,7 +81,8 @@ const TableReducer = (state: TableState, action: Actions): TableState => { }; export const ReleaseTable = () => { - const { filter} = releasesIndexRoute.useSearch() + const search = releasesIndexRoute.useSearch() + console.log("releases search", search) // const location = useLocation(); // const queryParams = new URLSearchParams(location.search); // const filterTypeFromUrl = queryParams.get("filter"); @@ -209,11 +209,11 @@ export const ReleaseTable = () => { gotoPage(0); }, [filters]); - React.useEffect(() => { - if (filterTypeFromUrl != null) { - dispatch({ type: ActionType.FILTER_CHANGED, payload: [{ id: "action_status", value: filterTypeFromUrl! }] }); - } - }, [filterTypeFromUrl]); + // React.useEffect(() => { + // if (filterTypeFromUrl != null) { + // dispatch({ type: ActionType.FILTER_CHANGED, payload: [{ id: "action_status", value: filterTypeFromUrl! }] }); + // } + // }, [filterTypeFromUrl]); if (error) { return

        Error

        ; diff --git a/web/src/utils/index.ts b/web/src/utils/index.ts index bc9586af2f..8ee2763f93 100644 --- a/web/src/utils/index.ts +++ b/web/src/utils/index.ts @@ -12,6 +12,20 @@ export function sleep(ms: number) { // get baseUrl sent from server rendered index template export function baseUrl() { + let baseUrl = "/"; + if (window.APP.baseUrl) { + if (window.APP.baseUrl === "{{.BaseUrl}}") { + baseUrl = "/"; + } else { + baseUrl = window.APP.baseUrl; + } + } + return baseUrl; +} + +// get routerBasePath sent from server rendered index template +// routerBasePath is used for RouterProvider and does not need work with trailing slash +export function routerBasePath() { let baseUrl = ""; if (window.APP.baseUrl) { if (window.APP.baseUrl === "{{.BaseUrl}}") { From f41b54371a6f7248b4d6d7bfe9f6ae235036fda7 Mon Sep 17 00:00:00 2001 From: ze0s Date: Wed, 7 Feb 2024 08:54:43 +0100 Subject: [PATCH 17/46] fix(web): login, onboard, types, imports --- web/package.json | 4 +- web/pnpm-lock.yaml | 53 --------- web/src/App.tsx | 102 +++++++++--------- web/src/api/APIClient.ts | 7 +- web/src/components/alerts/NotFound.tsx | 2 +- web/src/components/header/LeftNav.tsx | 2 +- web/src/components/header/MobileNav.tsx | 31 +++--- web/src/components/header/_shared.ts | 7 +- web/src/domain/routes.tsx | 67 ------------ web/src/forms/filters/FilterAddForm.tsx | 5 +- web/src/screens/Settings.tsx | 8 +- web/src/screens/auth/Login.tsx | 14 +-- web/src/screens/auth/Onboarding.tsx | 4 +- web/src/screens/dashboard/ActivityTable.tsx | 2 +- web/src/screens/dashboard/Stats.tsx | 5 +- web/src/screens/filters/Details.tsx | 22 +--- web/src/screens/filters/List.tsx | 18 ++-- .../action_components/ActionQBittorrent.tsx | 2 +- web/src/screens/releases/ReleaseTable.tsx | 20 ++-- 19 files changed, 125 insertions(+), 250 deletions(-) delete mode 100644 web/src/domain/routes.tsx diff --git a/web/package.json b/web/package.json index 8a596b8a27..52e6487aac 100644 --- a/web/package.json +++ b/web/package.json @@ -44,7 +44,6 @@ "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", "@types/react-portal": "^4.0.7", - "@types/react-router-dom": "^5.3.3", "@types/react-table": "^7.7.19", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", @@ -65,7 +64,6 @@ "react-popper-tooltip": "^4.4.2", "react-portal": "^4.2.2", "react-ridge-state": "4.2.9", - "react-router-dom": "6.21.3", "react-select": "^5.8.0", "react-table": "^7.8.0", "react-textarea-autosize": "^8.5.3", @@ -78,8 +76,8 @@ }, "devDependencies": { "@microsoft/eslint-formatter-sarif": "^3.0.0", - "@tanstack/router-devtools": "^1.1.4", "@rollup/wasm-node": "^4.9.6", + "@tanstack/router-devtools": "^1.1.4", "@types/node": "^20.11.6", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 9cb829466a..7cb6845c30 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -45,9 +45,6 @@ dependencies: '@types/react-portal': specifier: ^4.0.7 version: 4.0.7 - '@types/react-router-dom': - specifier: ^5.3.3 - version: 5.3.3 '@types/react-table': specifier: ^7.7.19 version: 7.7.19 @@ -108,9 +105,6 @@ dependencies: react-ridge-state: specifier: 4.2.9 version: 4.2.9(react@18.2.0) - react-router-dom: - specifier: 6.21.3 - version: 6.21.3(react-dom@18.2.0)(react@18.2.0) react-select: specifier: ^5.8.0 version: 5.8.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) @@ -1987,11 +1981,6 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false - /@remix-run/router@1.14.2: - resolution: {integrity: sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==} - engines: {node: '>=14.0.0'} - dev: false - /@rollup/plugin-babel@5.3.1(@babel/core@7.23.9)(@rollup/wasm-node@4.9.6): resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} @@ -2449,10 +2438,6 @@ packages: /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - /@types/history@4.7.11: - resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} - dev: false - /@types/hoist-non-react-statics@3.3.5: resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} dependencies: @@ -2499,21 +2484,6 @@ packages: '@types/react': 18.2.48 dev: false - /@types/react-router-dom@5.3.3: - resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.2.48 - '@types/react-router': 5.1.20 - dev: false - - /@types/react-router@5.1.20: - resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.2.48 - dev: false - /@types/react-table@7.7.19: resolution: {integrity: sha512-47jMa1Pai7ily6BXJCW33IL5ghqmCWs2VM9s+h1D4mCaK5P4uNkZOW3RMMg8MCXBvAJ0v9+sPqKjhid0PaJPQA==} dependencies: @@ -4998,29 +4968,6 @@ packages: react: 18.2.0 dev: false - /react-router-dom@6.21.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: ^18.2.0 - react-dom: '>=16.8' - dependencies: - '@remix-run/router': 1.14.2 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-router: 6.21.3(react@18.2.0) - dev: false - - /react-router@6.21.3(react@18.2.0): - resolution: {integrity: sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: ^18.2.0 - dependencies: - '@remix-run/router': 1.14.2 - react: 18.2.0 - dev: false - /react-select@5.8.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==} peerDependencies: diff --git a/web/src/App.tsx b/web/src/App.tsx index 26e612f753..3c671b659a 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -8,14 +8,11 @@ import { QueryClient, QueryClientProvider, queryOptions, - useQueryErrorResetBoundary } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import { ErrorBoundary } from "react-error-boundary"; import { toast, Toaster } from "react-hot-toast"; import { SettingsContext } from "./utils/Context"; -import { ErrorPage } from "./components/alerts"; import Toast from "./components/notifications/Toast"; import { Portal } from "react-portal"; import { @@ -48,7 +45,7 @@ import ReleaseSettings from "@screens/settings/Releases.tsx"; import AccountSettings from "@screens/settings/Account.tsx"; import ApplicationSettings from "@screens/settings/Application.tsx"; import { Logs } from "@screens/Logs.tsx"; -import { Login } from "@screens/auth"; +import { Login, Onboarding } from "@screens/auth"; import { APIClient } from "@api/APIClient.ts"; import { routerBasePath } from "@utils"; import { filterKeys } from "@screens/filters/List.tsx"; @@ -61,6 +58,7 @@ export const queryClient = new QueryClient({ console.error("query cache query:", query) // @ts-ignore if (error?.status === 401 || error?.status === 403) { + // @ts-ignore console.error("bad status, redirect to login", error?.status) // Redirect to login page window.location.href = "/login"; @@ -93,11 +91,11 @@ export const queryClient = new QueryClient({ } }); -const filtersQueryOptions = () => - queryOptions({ - queryKey: ['filters'], - queryFn: () => APIClient.filters.find([], "") - }) +// const filtersQueryOptions = () => +// queryOptions({ +// queryKey: ['filters'], +// queryFn: () => APIClient.filters.find([], "") +// }) export const filterQueryOptions = (filterId: number) => queryOptions({ @@ -227,22 +225,6 @@ export const filterRoute = new Route({ export const filterGeneralRoute = new Route({ getParentRoute: () => filterRoute, path: '/', - // path: '/$filterId', - // parseParams: (params) => ({ - // filterId: z.number().int().parse(Number(params.filterId)), - // }), - // stringifyParams: ({ filterId }) => ({ filterId: `${filterId}` }), - // loader: (opts) => { - // console.log("filter general route loader") - // return opts.context.queryClient.ensureQueryData(filterQueryOptions(opts.params.filterId)) - // // const filterData = opts.context.queryClient.ensureQueryData(filterQueryOptions(opts.params.filterId)) - // // const indexersData = opts.context.queryClient.ensureQueryData(indexersOptionsQueryOptions()) - // // - // // return { - // // filterData, - // // indexersData - // // } - // }, component: General }) @@ -286,13 +268,12 @@ export const releasesSearchSchema = z.object({ limit: z.number().optional(), filter: z.string().optional(), q: z.string().optional(), - // action_status: z.string().optional(), action_status: z.enum(['PUSH_APPROVED', 'PUSH_REJECTED', 'PUSH_ERROR', '']).optional(), // filters: z.array().catch(''), // sort: z.enum(['newest', 'oldest', 'price']).catch('newest'), }) -type ReleasesSearch = z.infer +// type ReleasesSearch = z.infer export const releasesIndexRoute = new Route({ getParentRoute: () => releasesRoute, @@ -387,12 +368,45 @@ export const logsRoute = new Route({ component: Logs }) +export const onboardRoute = new Route({ + getParentRoute: () => rootRoute, + path: 'onboard', + beforeLoad: async () => { + // Check if onboarding is available for this instance + // and redirect if needed + try { + await APIClient.auth.canOnboard() + } catch (e) { + console.error("onboarding not available, redirect to login") + + throw redirect({ + to: loginRoute.to, + }) + } + }, + component: Onboarding +}) + export const loginRoute = new Route({ getParentRoute: () => rootRoute, path: 'login', validateSearch: z.object({ redirect: z.string().optional(), }), + beforeLoad: async () => { + console.log("login beforeLoad") + + // handle canOnboard + try { + await APIClient.auth.canOnboard() + + redirect({ + to: onboardRoute.to, + }) + } catch (e) { + console.log("onboarding not available") + } + }, }).update({component: Login}) export function RouterSpinner() { @@ -415,7 +429,7 @@ const RootComponent = () => { ) } -export type Auth = { +export type AuthCtx = { isLoggedIn: boolean username?: string login: (username: string) => void @@ -428,11 +442,8 @@ export const authRoute = new Route({ // Before loading, authenticate the user via our auth context // This will also happen during prefetching (e.g. hovering over links, etc) beforeLoad: ({ context, location }) => { - console.log("auth before load") - // If the user is not logged in, check for item in localStorage if (!context.auth.isLoggedIn) { - console.log("auth before load: not logged in") const key = "user_auth" const storage = localStorage.getItem(key); if (storage) { @@ -467,7 +478,7 @@ export const authRoute = new Route({ // Otherwise, return the user in context return { - username: auth.username, + username: authCtx.username, } }, }) @@ -488,7 +499,7 @@ export const authIndexRoute = new Route({ }) export const rootRoute = rootRouteWithContext<{ - auth: Auth, + auth: AuthCtx, queryClient: QueryClient }>()({ component: RootComponent, @@ -501,7 +512,8 @@ const authenticatedTree = authRoute.addChildren([authIndexRoute.addChildren([das const routeTree = rootRoute.addChildren([ authenticatedTree, - loginRoute + loginRoute, + onboardRoute ]) const router = new Router({ @@ -522,44 +534,36 @@ declare module '@tanstack/react-router' { } } -const auth: Auth = { +export const authCtx: AuthCtx = { isLoggedIn: false, // status: 'loggedOut', username: undefined, login: (username: string) => { - auth.isLoggedIn = true - auth.username = username + authCtx.isLoggedIn = true + authCtx.username = username - localStorage.setItem("user_auth", JSON.stringify(auth)); + localStorage.setItem("user_auth", JSON.stringify(authCtx)); }, logout: () => { - auth.isLoggedIn = false - auth.username = undefined + authCtx.isLoggedIn = false + authCtx.username = undefined localStorage.removeItem("user_auth"); }, } export function App() { - // const { reset } = useQueryErrorResetBoundary(); - return ( - // - {/**/} - // ); } diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index 47a2f25945..77e62c51e5 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -79,7 +79,10 @@ export async function HttpClient( } } - const response = await window.fetch(`${baseUrl()}${endpoint}`, init); + const url = `${baseUrl()}${endpoint}` + console.debug("fetch url: ", url) + + const response = await window.fetch(url, init); switch (response.status) { case 204: { @@ -327,6 +330,8 @@ export const APIClient = { params["indexer"].push(filter.value); } else if (filter.id === "action_status") { params["push_status"].push(filter.value); + } else if (filter.id === "push_status") { + params["push_status"].push(filter.value); } else if (filter.id == "name") { params["q"].push(filter.value); } diff --git a/web/src/components/alerts/NotFound.tsx b/web/src/components/alerts/NotFound.tsx index e113538045..8ca7fa44fe 100644 --- a/web/src/components/alerts/NotFound.tsx +++ b/web/src/components/alerts/NotFound.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { Link } from "react-router-dom"; +import { Link } from "@tanstack/react-router"; import { ExternalLink } from "@components/ExternalLink"; import Logo from "@app/logo.svg?react"; diff --git a/web/src/components/header/LeftNav.tsx b/web/src/components/header/LeftNav.tsx index 06074280eb..a3cc33f1ed 100644 --- a/web/src/components/header/LeftNav.tsx +++ b/web/src/components/header/LeftNav.tsx @@ -29,7 +29,7 @@ export const LeftNav = () => ( {({ isActive }) => { return ( diff --git a/web/src/components/header/MobileNav.tsx b/web/src/components/header/MobileNav.tsx index ec7ba05c9d..4a9b241769 100644 --- a/web/src/components/header/MobileNav.tsx +++ b/web/src/components/header/MobileNav.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { NavLink } from "react-router-dom"; +import {Link} from "@tanstack/react-router"; import { Disclosure } from "@headlessui/react"; import { classNames } from "@utils"; @@ -15,21 +15,28 @@ export const MobileNav = (props: RightNavProps) => (
        {NAV_ROUTES.map((item) => ( - - classNames( - "shadow-sm border bg-gray-100 border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base", - isActive + search={{}} + params={{}} + > + {({ isActive }) => { + return ( + - {item.name} - + ) + }> + {item.name} + + ) + }} + ))} } > - {/**/} +
        {sortedIndexers.items.length ? ( diff --git a/web/src/screens/settings/Irc.tsx b/web/src/screens/settings/Irc.tsx index 64b1fe8df9..0f2cc3390a 100644 --- a/web/src/screens/settings/Irc.tsx +++ b/web/src/screens/settings/Irc.tsx @@ -4,7 +4,7 @@ */ import { Fragment, useRef, useState, useMemo, useEffect, MouseEvent } from "react"; -import {useMutation, useQuery, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; +import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { LockClosedIcon, LockOpenIcon, PlusIcon } from "@heroicons/react/24/solid"; import { Menu, Transition } from "@headlessui/react"; import { toast } from "react-hot-toast"; @@ -27,7 +27,6 @@ import { DeleteModal } from "@components/modals"; import Toast from "@components/notifications/Toast"; import { SettingsContext } from "@utils/Context"; import { Checkbox } from "@components/Checkbox"; -// import { useForm } from "react-hook-form"; import { Section } from "./_components"; import { ircQueryOptions } from "@app/App.tsx"; @@ -104,15 +103,6 @@ const IrcSettings = () => { const ircQuery = useSuspenseQuery(ircQueryOptions()) - // const networks = ircQuery.data - - // const { data } = useQuery({ - // queryKey: ircKeys.lists(), - // queryFn: APIClient.irc.getNetworks, - // refetchOnWindowFocus: false, - // refetchInterval: 3000 // Refetch every 3 seconds - // }); - const sortedNetworks = useSort(ircQuery.data || []); return ( From 47749238ef7ec06d13b0016c4d7c297fb488e0fc Mon Sep 17 00:00:00 2001 From: ze0s Date: Thu, 8 Feb 2024 15:12:24 +0100 Subject: [PATCH 20/46] fix(web): ts-expect-error --- web/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 3c671b659a..c4d27f6b33 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -56,9 +56,9 @@ export const queryClient = new QueryClient({ // check for 401 and redirect here console.error("query cache error:", error) console.error("query cache query:", query) - // @ts-ignore + // @ts-expect-error if (error?.status === 401 || error?.status === 403) { - // @ts-ignore + // @ts-expect-error console.error("bad status, redirect to login", error?.status) // Redirect to login page window.location.href = "/login"; From f618433cf0f0a49d53c96200d88b8e30700b95d0 Mon Sep 17 00:00:00 2001 From: ze0s Date: Thu, 8 Feb 2024 15:19:08 +0100 Subject: [PATCH 21/46] fix(tests): filter_test.go --- internal/database/filter_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/database/filter_test.go b/internal/database/filter_test.go index bb52b2f1eb..7d67737e4a 100644 --- a/internal/database/filter_test.go +++ b/internal/database/filter_test.go @@ -205,11 +205,10 @@ func TestFilterRepo_Delete(t *testing.T) { err = repo.Delete(context.Background(), createdFilters[0].ID) assert.NoError(t, err) - // Verify that the filter is deleted + // Verify that the filter is deleted and return error ErrRecordNotFound filter, err := repo.FindByID(context.Background(), createdFilters[0].ID) - assert.NoError(t, err) - assert.NotNil(t, filter) - assert.Equal(t, 0, filter.ID) + assert.ErrorIs(t, err, domain.ErrRecordNotFound) + assert.Nil(t, filter) }) t.Run(fmt.Sprintf("Delete_Fails_No_Record [%s]", dbType), func(t *testing.T) { From 4ba183c61b2575b0caa04a18cd11351af9781d73 Mon Sep 17 00:00:00 2001 From: ze0s Date: Thu, 8 Feb 2024 16:16:34 +0100 Subject: [PATCH 22/46] fix(filters): tests --- internal/database/filter.go | 495 ++++++++++--------------------- internal/database/filter_test.go | 5 +- internal/domain/filter.go | 149 +++++----- internal/filter/service.go | 24 +- 4 files changed, 252 insertions(+), 421 deletions(-) diff --git a/internal/database/filter.go b/internal/database/filter.go index 7cd628de0d..47e043e6a9 100644 --- a/internal/database/filter.go +++ b/internal/database/filter.go @@ -12,13 +12,11 @@ import ( "github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/internal/logger" - "github.com/autobrr/autobrr/pkg/cmp" "github.com/autobrr/autobrr/pkg/errors" sq "github.com/Masterminds/squirrel" "github.com/lib/pq" "github.com/rs/zerolog" - "golang.org/x/exp/slices" ) type FilterRepo struct { @@ -245,25 +243,8 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter "f.max_leechers", "f.created_at", "f.updated_at", - "fe.id as external_id", - "fe.name", - "fe.idx", - "fe.type", - "fe.enabled", - "fe.exec_cmd", - "fe.exec_args", - "fe.exec_expect_status", - "fe.webhook_host", - "fe.webhook_method", - "fe.webhook_data", - "fe.webhook_headers", - "fe.webhook_expect_status", - "fe.webhook_retry_status", - "fe.webhook_retry_attempts", - "fe.webhook_retry_delay_seconds", ). From("filter f"). - LeftJoin("filter_external fe ON f.id = fe.filter_id"). Where(sq.Eq{"f.id": filterID}) query, args, err := queryBuilder.ToSql() @@ -271,180 +252,132 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter return nil, errors.Wrap(err, "error building query") } - rows, err := r.db.handler.QueryContext(ctx, query, args...) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { + row := r.db.handler.QueryRowContext(ctx, query, args...) + + if row.Err() != nil { + if errors.Is(row.Err(), sql.ErrNoRows) { return nil, domain.ErrRecordNotFound } - return nil, errors.Wrap(err, "error executing query") - } - if !rows.Next() { - return nil, domain.ErrRecordNotFound + return nil, errors.Wrap(row.Err(), "error row") } var f domain.Filter - externalMap := make(map[int]domain.FilterExternal) - - for rows.Next() { - // filter - var minSize, maxSize, maxDownloadsUnit, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, matchReleaseTags, exceptReleaseTags, matchDescription, exceptDescription, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags, tagsMatchLogic, exceptTagsMatchLogic sql.NullString - var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac sql.NullBool - var delay, maxDownloads, logScore sql.NullInt32 - - // filter external - var extName, extType, extExecCmd, extExecArgs, extWebhookHost, extWebhookMethod, extWebhookHeaders, extWebhookData, extWebhookRetryStatus sql.NullString - var extId, extIndex, extWebhookStatus, extWebhookRetryAttempts, extWebhookDelaySeconds, extExecStatus sql.NullInt32 - var extEnabled sql.NullBool - - if err := rows.Scan( - &f.ID, - &f.Enabled, - &f.Name, - &minSize, - &maxSize, - &delay, - &f.Priority, - &maxDownloads, - &maxDownloadsUnit, - &matchReleases, - &exceptReleases, - &useRegex, - &matchReleaseGroups, - &exceptReleaseGroups, - &matchReleaseTags, - &exceptReleaseTags, - &f.UseRegexReleaseTags, - &matchDescription, - &exceptDescription, - &f.UseRegexDescription, - &scene, - &freeleech, - &freeleechPercent, - &f.SmartEpisode, - &shows, - &seasons, - &episodes, - pq.Array(&f.Resolutions), - pq.Array(&f.Codecs), - pq.Array(&f.Sources), - pq.Array(&f.Containers), - pq.Array(&f.MatchHDR), - pq.Array(&f.ExceptHDR), - pq.Array(&f.MatchOther), - pq.Array(&f.ExceptOther), - &years, - &artists, - &albums, - pq.Array(&f.MatchReleaseTypes), - pq.Array(&f.Formats), - pq.Array(&f.Quality), - pq.Array(&f.Media), - &logScore, - &hasLog, - &hasCue, - &perfectFlac, - &matchCategories, - &exceptCategories, - &matchUploaders, - &exceptUploaders, - pq.Array(&f.MatchLanguage), - pq.Array(&f.ExceptLanguage), - &tags, - &exceptTags, - &tagsMatchLogic, - &exceptTagsMatchLogic, - pq.Array(&f.Origins), - pq.Array(&f.ExceptOrigins), - &f.MinSeeders, - &f.MaxSeeders, - &f.MinLeechers, - &f.MaxLeechers, - &f.CreatedAt, - &f.UpdatedAt, - &extId, - &extName, - &extIndex, - &extType, - &extEnabled, - &extExecCmd, - &extExecArgs, - &extExecStatus, - &extWebhookHost, - &extWebhookMethod, - &extWebhookData, - &extWebhookHeaders, - &extWebhookStatus, - &extWebhookRetryStatus, - &extWebhookRetryAttempts, - &extWebhookDelaySeconds, - ); err != nil { - return nil, errors.Wrap(err, "error scanning row") - } - - f.MinSize = minSize.String - f.MaxSize = maxSize.String - f.Delay = int(delay.Int32) - f.MaxDownloads = int(maxDownloads.Int32) - f.MaxDownloadsUnit = domain.FilterMaxDownloadsUnit(maxDownloadsUnit.String) - f.MatchReleases = matchReleases.String - f.ExceptReleases = exceptReleases.String - f.MatchReleaseGroups = matchReleaseGroups.String - f.ExceptReleaseGroups = exceptReleaseGroups.String - f.MatchReleaseTags = matchReleaseTags.String - f.ExceptReleaseTags = exceptReleaseTags.String - f.MatchDescription = matchDescription.String - f.ExceptDescription = exceptDescription.String - f.FreeleechPercent = freeleechPercent.String - f.Shows = shows.String - f.Seasons = seasons.String - f.Episodes = episodes.String - f.Years = years.String - f.Artists = artists.String - f.Albums = albums.String - f.LogScore = int(logScore.Int32) - f.Log = hasLog.Bool - f.Cue = hasCue.Bool - f.PerfectFlac = perfectFlac.Bool - f.MatchCategories = matchCategories.String - f.ExceptCategories = exceptCategories.String - f.MatchUploaders = matchUploaders.String - f.ExceptUploaders = exceptUploaders.String - f.Tags = tags.String - f.ExceptTags = exceptTags.String - f.TagsMatchLogic = tagsMatchLogic.String - f.ExceptTagsMatchLogic = exceptTagsMatchLogic.String - f.UseRegex = useRegex.Bool - f.Scene = scene.Bool - f.Freeleech = freeleech.Bool - - if extId.Valid { - external := domain.FilterExternal{ - ID: int(extId.Int32), - Name: extName.String, - Index: int(extIndex.Int32), - Type: domain.FilterExternalType(extType.String), - Enabled: extEnabled.Bool, - ExecCmd: extExecCmd.String, - ExecArgs: extExecArgs.String, - ExecExpectStatus: int(extExecStatus.Int32), - WebhookHost: extWebhookHost.String, - WebhookMethod: extWebhookMethod.String, - WebhookData: extWebhookData.String, - WebhookHeaders: extWebhookHeaders.String, - WebhookExpectStatus: int(extWebhookStatus.Int32), - WebhookRetryStatus: extWebhookRetryStatus.String, - WebhookRetryAttempts: int(extWebhookRetryAttempts.Int32), - WebhookRetryDelaySeconds: int(extWebhookDelaySeconds.Int32), - } - externalMap[external.ID] = external + // filter + var minSize, maxSize, maxDownloadsUnit, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, matchReleaseTags, exceptReleaseTags, matchDescription, exceptDescription, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags, tagsMatchLogic, exceptTagsMatchLogic sql.NullString + var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac sql.NullBool + var delay, maxDownloads, logScore sql.NullInt32 + + err = row.Scan( + &f.ID, + &f.Enabled, + &f.Name, + &minSize, + &maxSize, + &delay, + &f.Priority, + &maxDownloads, + &maxDownloadsUnit, + &matchReleases, + &exceptReleases, + &useRegex, + &matchReleaseGroups, + &exceptReleaseGroups, + &matchReleaseTags, + &exceptReleaseTags, + &f.UseRegexReleaseTags, + &matchDescription, + &exceptDescription, + &f.UseRegexDescription, + &scene, + &freeleech, + &freeleechPercent, + &f.SmartEpisode, + &shows, + &seasons, + &episodes, + pq.Array(&f.Resolutions), + pq.Array(&f.Codecs), + pq.Array(&f.Sources), + pq.Array(&f.Containers), + pq.Array(&f.MatchHDR), + pq.Array(&f.ExceptHDR), + pq.Array(&f.MatchOther), + pq.Array(&f.ExceptOther), + &years, + &artists, + &albums, + pq.Array(&f.MatchReleaseTypes), + pq.Array(&f.Formats), + pq.Array(&f.Quality), + pq.Array(&f.Media), + &logScore, + &hasLog, + &hasCue, + &perfectFlac, + &matchCategories, + &exceptCategories, + &matchUploaders, + &exceptUploaders, + pq.Array(&f.MatchLanguage), + pq.Array(&f.ExceptLanguage), + &tags, + &exceptTags, + &tagsMatchLogic, + &exceptTagsMatchLogic, + pq.Array(&f.Origins), + pq.Array(&f.ExceptOrigins), + &f.MinSeeders, + &f.MaxSeeders, + &f.MinLeechers, + &f.MaxLeechers, + &f.CreatedAt, + &f.UpdatedAt, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, domain.ErrRecordNotFound } - } - for _, external := range externalMap { - f.External = append(f.External, external) - } + return nil, errors.Wrap(err, "error scanning row") + } + + f.MinSize = minSize.String + f.MaxSize = maxSize.String + f.Delay = int(delay.Int32) + f.MaxDownloads = int(maxDownloads.Int32) + f.MaxDownloadsUnit = domain.FilterMaxDownloadsUnit(maxDownloadsUnit.String) + f.MatchReleases = matchReleases.String + f.ExceptReleases = exceptReleases.String + f.MatchReleaseGroups = matchReleaseGroups.String + f.ExceptReleaseGroups = exceptReleaseGroups.String + f.MatchReleaseTags = matchReleaseTags.String + f.ExceptReleaseTags = exceptReleaseTags.String + f.MatchDescription = matchDescription.String + f.ExceptDescription = exceptDescription.String + f.FreeleechPercent = freeleechPercent.String + f.Shows = shows.String + f.Seasons = seasons.String + f.Episodes = episodes.String + f.Years = years.String + f.Artists = artists.String + f.Albums = albums.String + f.LogScore = int(logScore.Int32) + f.Log = hasLog.Bool + f.Cue = hasCue.Bool + f.PerfectFlac = perfectFlac.Bool + f.MatchCategories = matchCategories.String + f.ExceptCategories = exceptCategories.String + f.MatchUploaders = matchUploaders.String + f.ExceptUploaders = exceptUploaders.String + f.Tags = tags.String + f.ExceptTags = exceptTags.String + f.TagsMatchLogic = tagsMatchLogic.String + f.ExceptTagsMatchLogic = exceptTagsMatchLogic.String + f.UseRegex = useRegex.Bool + f.Scene = scene.Bool + f.Freeleech = freeleech.Bool return &f, nil } @@ -521,28 +454,10 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string "f.max_leechers", "f.created_at", "f.updated_at", - "fe.id as external_id", - "fe.name", - "fe.idx", - "fe.type", - "fe.enabled", - "fe.exec_cmd", - "fe.exec_args", - "fe.exec_expect_status", - "fe.webhook_host", - "fe.webhook_method", - "fe.webhook_data", - "fe.webhook_headers", - "fe.webhook_expect_status", - "fe.webhook_retry_status", - "fe.webhook_retry_attempts", - "fe.webhook_retry_delay_seconds", - "fe.filter_id", ). From("filter f"). Join("filter_indexer fi ON f.id = fi.filter_id"). Join("indexer i ON i.id = fi.indexer_id"). - LeftJoin("filter_external fe ON f.id = fe.filter_id"). Where(sq.Eq{"i.identifier": indexer}). Where(sq.Eq{"i.enabled": true}). Where(sq.Eq{"f.enabled": true}). @@ -560,7 +475,7 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string defer rows.Close() - filtersMap := make(map[int]*domain.Filter) + var filters []*domain.Filter for rows.Next() { var f domain.Filter @@ -569,12 +484,7 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac sql.NullBool var delay, maxDownloads, logScore sql.NullInt32 - // filter external - var extName, extType, extExecCmd, extExecArgs, extWebhookHost, extWebhookMethod, extWebhookHeaders, extWebhookData, extWebhookRetryStatus sql.NullString - var extId, extIndex, extWebhookStatus, extWebhookRetryAttempts, extWebhookDelaySeconds, extExecStatus, extFilterId sql.NullInt32 - var extEnabled sql.NullBool - - if err := rows.Scan( + err := rows.Scan( &f.ID, &f.Enabled, &f.Name, @@ -639,108 +549,52 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string &f.MaxLeechers, &f.CreatedAt, &f.UpdatedAt, - &extId, - &extName, - &extIndex, - &extType, - &extEnabled, - &extExecCmd, - &extExecArgs, - &extExecStatus, - &extWebhookHost, - &extWebhookMethod, - &extWebhookData, - &extWebhookHeaders, - &extWebhookStatus, - &extWebhookRetryStatus, - &extWebhookRetryAttempts, - &extWebhookDelaySeconds, - &extFilterId, - ); err != nil { + ) + if err != nil { return nil, errors.Wrap(err, "error scanning row") } - filter, ok := filtersMap[f.ID] - if !ok { - f.MinSize = minSize.String - f.MaxSize = maxSize.String - f.Delay = int(delay.Int32) - f.MaxDownloads = int(maxDownloads.Int32) - f.MaxDownloadsUnit = domain.FilterMaxDownloadsUnit(maxDownloadsUnit.String) - f.MatchReleases = matchReleases.String - f.ExceptReleases = exceptReleases.String - f.MatchReleaseGroups = matchReleaseGroups.String - f.ExceptReleaseGroups = exceptReleaseGroups.String - f.MatchReleaseTags = matchReleaseTags.String - f.ExceptReleaseTags = exceptReleaseTags.String - f.MatchDescription = matchDescription.String - f.ExceptDescription = exceptDescription.String - f.FreeleechPercent = freeleechPercent.String - f.Shows = shows.String - f.Seasons = seasons.String - f.Episodes = episodes.String - f.Years = years.String - f.Artists = artists.String - f.Albums = albums.String - f.LogScore = int(logScore.Int32) - f.Log = hasLog.Bool - f.Cue = hasCue.Bool - f.PerfectFlac = perfectFlac.Bool - f.MatchCategories = matchCategories.String - f.ExceptCategories = exceptCategories.String - f.MatchUploaders = matchUploaders.String - f.ExceptUploaders = exceptUploaders.String - f.Tags = tags.String - f.ExceptTags = exceptTags.String - f.TagsMatchLogic = tagsMatchLogic.String - f.ExceptTagsMatchLogic = exceptTagsMatchLogic.String - f.UseRegex = useRegex.Bool - f.Scene = scene.Bool - f.Freeleech = freeleech.Bool - - f.Rejections = []string{} - - filter = &f - filtersMap[f.ID] = filter - } - - if extId.Valid { - external := domain.FilterExternal{ - ID: int(extId.Int32), - Name: extName.String, - Index: int(extIndex.Int32), - Type: domain.FilterExternalType(extType.String), - Enabled: extEnabled.Bool, - ExecCmd: extExecCmd.String, - ExecArgs: extExecArgs.String, - ExecExpectStatus: int(extExecStatus.Int32), - WebhookHost: extWebhookHost.String, - WebhookMethod: extWebhookMethod.String, - WebhookData: extWebhookData.String, - WebhookHeaders: extWebhookHeaders.String, - WebhookExpectStatus: int(extWebhookStatus.Int32), - WebhookRetryStatus: extWebhookRetryStatus.String, - WebhookRetryAttempts: int(extWebhookRetryAttempts.Int32), - WebhookRetryDelaySeconds: int(extWebhookDelaySeconds.Int32), - FilterId: int(extFilterId.Int32), - } - filter.External = append(filter.External, external) - } - } + f.MinSize = minSize.String + f.MaxSize = maxSize.String + f.Delay = int(delay.Int32) + f.MaxDownloads = int(maxDownloads.Int32) + f.MaxDownloadsUnit = domain.FilterMaxDownloadsUnit(maxDownloadsUnit.String) + f.MatchReleases = matchReleases.String + f.ExceptReleases = exceptReleases.String + f.MatchReleaseGroups = matchReleaseGroups.String + f.ExceptReleaseGroups = exceptReleaseGroups.String + f.MatchReleaseTags = matchReleaseTags.String + f.ExceptReleaseTags = exceptReleaseTags.String + f.MatchDescription = matchDescription.String + f.ExceptDescription = exceptDescription.String + f.FreeleechPercent = freeleechPercent.String + f.Shows = shows.String + f.Seasons = seasons.String + f.Episodes = episodes.String + f.Years = years.String + f.Artists = artists.String + f.Albums = albums.String + f.LogScore = int(logScore.Int32) + f.Log = hasLog.Bool + f.Cue = hasCue.Bool + f.PerfectFlac = perfectFlac.Bool + f.MatchCategories = matchCategories.String + f.ExceptCategories = exceptCategories.String + f.MatchUploaders = matchUploaders.String + f.ExceptUploaders = exceptUploaders.String + f.Tags = tags.String + f.ExceptTags = exceptTags.String + f.TagsMatchLogic = tagsMatchLogic.String + f.ExceptTagsMatchLogic = exceptTagsMatchLogic.String + f.UseRegex = useRegex.Bool + f.Scene = scene.Bool + f.Freeleech = freeleech.Bool - var filters []*domain.Filter + f.Rejections = []string{} - for _, filter := range filtersMap { - filter := filter - filters = append(filters, filter) + filters = append(filters, &f) } - // the filterMap messes up the order, so we need to sort the filters slice - slices.SortStableFunc(filters, func(a, b *domain.Filter) int { - // TODO remove with Go 1.21 and use std lib cmp - return cmp.Compare(b.Priority, a.Priority) - }) - return filters, nil } @@ -1236,39 +1090,6 @@ func (r *FilterRepo) UpdatePartial(ctx context.Context, filter domain.FilterUpda if filter.ExceptOrigins != nil { q = q.Set("except_origins", pq.Array(filter.ExceptOrigins)) } - if filter.ExternalScriptEnabled != nil { - q = q.Set("external_script_enabled", filter.ExternalScriptEnabled) - } - if filter.ExternalScriptCmd != nil { - q = q.Set("external_script_cmd", filter.ExternalScriptCmd) - } - if filter.ExternalScriptArgs != nil { - q = q.Set("external_script_args", filter.ExternalScriptArgs) - } - if filter.ExternalScriptExpectStatus != nil { - q = q.Set("external_script_expect_status", filter.ExternalScriptExpectStatus) - } - if filter.ExternalWebhookEnabled != nil { - q = q.Set("external_webhook_enabled", filter.ExternalWebhookEnabled) - } - if filter.ExternalWebhookHost != nil { - q = q.Set("external_webhook_host", filter.ExternalWebhookHost) - } - if filter.ExternalWebhookData != nil { - q = q.Set("external_webhook_data", filter.ExternalWebhookData) - } - if filter.ExternalWebhookExpectStatus != nil { - q = q.Set("external_webhook_expect_status", filter.ExternalWebhookExpectStatus) - } - if filter.ExternalWebhookRetryStatus != nil { - q = q.Set("external_webhook_retry_status", filter.ExternalWebhookRetryStatus) - } - if filter.ExternalWebhookRetryAttempts != nil { - q = q.Set("external_webhook_retry_attempts", filter.ExternalWebhookRetryAttempts) - } - if filter.ExternalWebhookRetryDelaySeconds != nil { - q = q.Set("external_webhook_retry_delay_seconds", filter.ExternalWebhookRetryDelaySeconds) - } if filter.MinSeeders != nil { q = q.Set("min_seeders", filter.MinSeeders) } diff --git a/internal/database/filter_test.go b/internal/database/filter_test.go index 7d67737e4a..aa4a3da93c 100644 --- a/internal/database/filter_test.go +++ b/internal/database/filter_test.go @@ -450,12 +450,11 @@ func TestFilterRepo_FindByID(t *testing.T) { _ = repo.Delete(context.Background(), createdFilters[0].ID) }) - // TODO: This should succeed, but it fails because we are not handling the error correctly. Fix this. t.Run(fmt.Sprintf("FindByID_Fails_Invalid_ID [%s]", dbType), func(t *testing.T) { // Test using an invalid ID filter, err := repo.FindByID(context.Background(), -1) - assert.NoError(t, err) // should return an error - assert.NotNil(t, filter) // should be nil + assert.ErrorIs(t, err, domain.ErrRecordNotFound) // should return an error + assert.Nil(t, filter) // should be nil }) } diff --git a/internal/domain/filter.go b/internal/domain/filter.go index c0958070fb..5a3d906f55 100644 --- a/internal/domain/filter.go +++ b/internal/domain/filter.go @@ -174,86 +174,75 @@ const ( ) type FilterUpdate struct { - ID int `json:"id"` - Name *string `json:"name,omitempty"` - Enabled *bool `json:"enabled,omitempty"` - MinSize *string `json:"min_size,omitempty"` - MaxSize *string `json:"max_size,omitempty"` - Delay *int `json:"delay,omitempty"` - Priority *int32 `json:"priority,omitempty"` - MaxDownloads *int `json:"max_downloads,omitempty"` - MaxDownloadsUnit *FilterMaxDownloadsUnit `json:"max_downloads_unit,omitempty"` - MatchReleases *string `json:"match_releases,omitempty"` - ExceptReleases *string `json:"except_releases,omitempty"` - UseRegex *bool `json:"use_regex,omitempty"` - MatchReleaseGroups *string `json:"match_release_groups,omitempty"` - ExceptReleaseGroups *string `json:"except_release_groups,omitempty"` - MatchReleaseTags *string `json:"match_release_tags,omitempty"` - ExceptReleaseTags *string `json:"except_release_tags,omitempty"` - UseRegexReleaseTags *bool `json:"use_regex_release_tags,omitempty"` - MatchDescription *string `json:"match_description,omitempty"` - ExceptDescription *string `json:"except_description,omitempty"` - UseRegexDescription *bool `json:"use_regex_description,omitempty"` - Scene *bool `json:"scene,omitempty"` - Origins *[]string `json:"origins,omitempty"` - ExceptOrigins *[]string `json:"except_origins,omitempty"` - Bonus *[]string `json:"bonus,omitempty"` - Freeleech *bool `json:"freeleech,omitempty"` - FreeleechPercent *string `json:"freeleech_percent,omitempty"` - SmartEpisode *bool `json:"smart_episode,omitempty"` - Shows *string `json:"shows,omitempty"` - Seasons *string `json:"seasons,omitempty"` - Episodes *string `json:"episodes,omitempty"` - Resolutions *[]string `json:"resolutions,omitempty"` // SD, 480i, 480p, 576p, 720p, 810p, 1080i, 1080p. - Codecs *[]string `json:"codecs,omitempty"` // XviD, DivX, x264, h.264 (or h264), mpeg2 (or mpeg-2), VC-1 (or VC1), WMV, Remux, h.264 Remux (or h264 Remux), VC-1 Remux (or VC1 Remux). - Sources *[]string `json:"sources,omitempty"` // DSR, PDTV, HDTV, HR.PDTV, HR.HDTV, DVDRip, DVDScr, BDr, BD5, BD9, BDRip, BRRip, DVDR, MDVDR, HDDVD, HDDVDRip, BluRay, WEB-DL, TVRip, CAM, R5, TELESYNC, TS, TELECINE, TC. TELESYNC and TS are synonyms (you don't need both). Same for TELECINE and TC - Containers *[]string `json:"containers,omitempty"` - MatchHDR *[]string `json:"match_hdr,omitempty"` - ExceptHDR *[]string `json:"except_hdr,omitempty"` - MatchOther *[]string `json:"match_other,omitempty"` - ExceptOther *[]string `json:"except_other,omitempty"` - Years *string `json:"years,omitempty"` - Artists *string `json:"artists,omitempty"` - Albums *string `json:"albums,omitempty"` - MatchReleaseTypes *[]string `json:"match_release_types,omitempty"` // Album,Single,EP - ExceptReleaseTypes *string `json:"except_release_types,omitempty"` - Formats *[]string `json:"formats,omitempty"` // MP3, FLAC, Ogg, AAC, AC3, DTS - Quality *[]string `json:"quality,omitempty"` // 192, 320, APS (VBR), V2 (VBR), V1 (VBR), APX (VBR), V0 (VBR), q8.x (VBR), Lossless, 24bit Lossless, Other - Media *[]string `json:"media,omitempty"` // CD, DVD, Vinyl, Soundboard, SACD, DAT, Cassette, WEB, Other - PerfectFlac *bool `json:"perfect_flac,omitempty"` - Cue *bool `json:"cue,omitempty"` - Log *bool `json:"log,omitempty"` - LogScore *int `json:"log_score,omitempty"` - MatchCategories *string `json:"match_categories,omitempty"` - ExceptCategories *string `json:"except_categories,omitempty"` - MatchUploaders *string `json:"match_uploaders,omitempty"` - ExceptUploaders *string `json:"except_uploaders,omitempty"` - MatchLanguage *[]string `json:"match_language,omitempty"` - ExceptLanguage *[]string `json:"except_language,omitempty"` - Tags *string `json:"tags,omitempty"` - ExceptTags *string `json:"except_tags,omitempty"` - TagsAny *string `json:"tags_any,omitempty"` - ExceptTagsAny *string `json:"except_tags_any,omitempty"` - TagsMatchLogic *string `json:"tags_match_logic,omitempty"` - ExceptTagsMatchLogic *string `json:"except_tags_match_logic,omitempty"` - MinSeeders *int `json:"min_seeders,omitempty"` - MaxSeeders *int `json:"max_seeders,omitempty"` - MinLeechers *int `json:"min_leechers,omitempty"` - MaxLeechers *int `json:"max_leechers,omitempty"` - ExternalScriptEnabled *bool `json:"external_script_enabled,omitempty"` - ExternalScriptCmd *string `json:"external_script_cmd,omitempty"` - ExternalScriptArgs *string `json:"external_script_args,omitempty"` - ExternalScriptExpectStatus *int `json:"external_script_expect_status,omitempty"` - ExternalWebhookEnabled *bool `json:"external_webhook_enabled,omitempty"` - ExternalWebhookHost *string `json:"external_webhook_host,omitempty"` - ExternalWebhookData *string `json:"external_webhook_data,omitempty"` - ExternalWebhookExpectStatus *int `json:"external_webhook_expect_status,omitempty"` - ExternalWebhookRetryStatus *string `json:"external_webhook_retry_status,omitempty"` - ExternalWebhookRetryAttempts *int `json:"external_webhook_retry_attempts,omitempty"` - ExternalWebhookRetryDelaySeconds *int `json:"external_webhook_retry_delay_seconds,omitempty"` - Actions []*Action `json:"actions,omitempty"` - External []FilterExternal `json:"external,omitempty"` - Indexers []Indexer `json:"indexers,omitempty"` + ID int `json:"id"` + Name *string `json:"name,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + MinSize *string `json:"min_size,omitempty"` + MaxSize *string `json:"max_size,omitempty"` + Delay *int `json:"delay,omitempty"` + Priority *int32 `json:"priority,omitempty"` + MaxDownloads *int `json:"max_downloads,omitempty"` + MaxDownloadsUnit *FilterMaxDownloadsUnit `json:"max_downloads_unit,omitempty"` + MatchReleases *string `json:"match_releases,omitempty"` + ExceptReleases *string `json:"except_releases,omitempty"` + UseRegex *bool `json:"use_regex,omitempty"` + MatchReleaseGroups *string `json:"match_release_groups,omitempty"` + ExceptReleaseGroups *string `json:"except_release_groups,omitempty"` + MatchReleaseTags *string `json:"match_release_tags,omitempty"` + ExceptReleaseTags *string `json:"except_release_tags,omitempty"` + UseRegexReleaseTags *bool `json:"use_regex_release_tags,omitempty"` + MatchDescription *string `json:"match_description,omitempty"` + ExceptDescription *string `json:"except_description,omitempty"` + UseRegexDescription *bool `json:"use_regex_description,omitempty"` + Scene *bool `json:"scene,omitempty"` + Origins *[]string `json:"origins,omitempty"` + ExceptOrigins *[]string `json:"except_origins,omitempty"` + Bonus *[]string `json:"bonus,omitempty"` + Freeleech *bool `json:"freeleech,omitempty"` + FreeleechPercent *string `json:"freeleech_percent,omitempty"` + SmartEpisode *bool `json:"smart_episode,omitempty"` + Shows *string `json:"shows,omitempty"` + Seasons *string `json:"seasons,omitempty"` + Episodes *string `json:"episodes,omitempty"` + Resolutions *[]string `json:"resolutions,omitempty"` // SD, 480i, 480p, 576p, 720p, 810p, 1080i, 1080p. + Codecs *[]string `json:"codecs,omitempty"` // XviD, DivX, x264, h.264 (or h264), mpeg2 (or mpeg-2), VC-1 (or VC1), WMV, Remux, h.264 Remux (or h264 Remux), VC-1 Remux (or VC1 Remux). + Sources *[]string `json:"sources,omitempty"` // DSR, PDTV, HDTV, HR.PDTV, HR.HDTV, DVDRip, DVDScr, BDr, BD5, BD9, BDRip, BRRip, DVDR, MDVDR, HDDVD, HDDVDRip, BluRay, WEB-DL, TVRip, CAM, R5, TELESYNC, TS, TELECINE, TC. TELESYNC and TS are synonyms (you don't need both). Same for TELECINE and TC + Containers *[]string `json:"containers,omitempty"` + MatchHDR *[]string `json:"match_hdr,omitempty"` + ExceptHDR *[]string `json:"except_hdr,omitempty"` + MatchOther *[]string `json:"match_other,omitempty"` + ExceptOther *[]string `json:"except_other,omitempty"` + Years *string `json:"years,omitempty"` + Artists *string `json:"artists,omitempty"` + Albums *string `json:"albums,omitempty"` + MatchReleaseTypes *[]string `json:"match_release_types,omitempty"` // Album,Single,EP + ExceptReleaseTypes *string `json:"except_release_types,omitempty"` + Formats *[]string `json:"formats,omitempty"` // MP3, FLAC, Ogg, AAC, AC3, DTS + Quality *[]string `json:"quality,omitempty"` // 192, 320, APS (VBR), V2 (VBR), V1 (VBR), APX (VBR), V0 (VBR), q8.x (VBR), Lossless, 24bit Lossless, Other + Media *[]string `json:"media,omitempty"` // CD, DVD, Vinyl, Soundboard, SACD, DAT, Cassette, WEB, Other + PerfectFlac *bool `json:"perfect_flac,omitempty"` + Cue *bool `json:"cue,omitempty"` + Log *bool `json:"log,omitempty"` + LogScore *int `json:"log_score,omitempty"` + MatchCategories *string `json:"match_categories,omitempty"` + ExceptCategories *string `json:"except_categories,omitempty"` + MatchUploaders *string `json:"match_uploaders,omitempty"` + ExceptUploaders *string `json:"except_uploaders,omitempty"` + MatchLanguage *[]string `json:"match_language,omitempty"` + ExceptLanguage *[]string `json:"except_language,omitempty"` + Tags *string `json:"tags,omitempty"` + ExceptTags *string `json:"except_tags,omitempty"` + TagsAny *string `json:"tags_any,omitempty"` + ExceptTagsAny *string `json:"except_tags_any,omitempty"` + TagsMatchLogic *string `json:"tags_match_logic,omitempty"` + ExceptTagsMatchLogic *string `json:"except_tags_match_logic,omitempty"` + MinSeeders *int `json:"min_seeders,omitempty"` + MaxSeeders *int `json:"max_seeders,omitempty"` + MinLeechers *int `json:"min_leechers,omitempty"` + MaxLeechers *int `json:"max_leechers,omitempty"` + Actions []*Action `json:"actions,omitempty"` + External []FilterExternal `json:"external,omitempty"` + Indexers []Indexer `json:"indexers,omitempty"` } func (f *Filter) Validate() error { diff --git a/internal/filter/service.go b/internal/filter/service.go index 2a1c023501..d3002567ad 100644 --- a/internal/filter/service.go +++ b/internal/filter/service.go @@ -124,6 +124,12 @@ func (s *service) FindByID(ctx context.Context, filterID int) (*domain.Filter, e return nil, err } + externalFilters, err := s.repo.FindExternalFiltersByID(ctx, filter.ID) + if err != nil { + s.log.Error().Err(err).Msgf("could not find external filters for filter id: %v", filter.ID) + } + filter.External = externalFilters + actions, err := s.actionRepo.FindByFilterID(ctx, filter.ID, nil) if err != nil { s.log.Error().Err(err).Msgf("could not find filter actions for filter id: %v", filter.ID) @@ -142,9 +148,25 @@ func (s *service) FindByID(ctx context.Context, filterID int) (*domain.Filter, e func (s *service) FindByIndexerIdentifier(ctx context.Context, indexer string) ([]*domain.Filter, error) { // get filters for indexer + filters, err := s.repo.FindByIndexerIdentifier(ctx, indexer) + if err != nil { + return nil, err + } + // we do not load actions here since we do not need it at this stage // only load those after filter has matched - return s.repo.FindByIndexerIdentifier(ctx, indexer) + for _, filter := range filters { + filter := filter + + externalFilters, err := s.repo.FindExternalFiltersByID(ctx, filter.ID) + if err != nil { + s.log.Error().Err(err).Msgf("could not find external filters for filter id: %v", filter.ID) + } + filter.External = externalFilters + + } + + return filters, nil } func (s *service) GetDownloadsByFilterId(ctx context.Context, filterID int) (*domain.FilterDownloads, error) { From 6e08c1ae25ca3c9eb9ee9e811661d9918fdabf85 Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Sat, 10 Feb 2024 12:13:43 +0100 Subject: [PATCH 23/46] refactor: remove duplicate spinner components refactor: ReleaseTable.tsx loading animation refactor: remove dedicated `pendingComponent` for `settingsRoute` --- web/src/App.tsx | 24 ++- web/src/components/SectionLoader.tsx | 46 ----- web/src/components/modals/index.tsx | 4 +- web/src/screens/dashboard/ActivityTable.tsx | 4 +- web/src/screens/releases/ReleaseTable.tsx | 179 +++----------------- 5 files changed, 38 insertions(+), 219 deletions(-) delete mode 100644 web/src/components/SectionLoader.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index c4d27f6b33..f662af38be 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -23,11 +23,9 @@ import { rootRouteWithContext, redirect, ErrorComponent, - useRouterState, } from '@tanstack/react-router' import { TanStackRouterDevtools } from '@tanstack/router-devtools' import { Header } from "@components/header"; -import { SectionLoader, Spinner } from "@components/SectionLoader.tsx"; import { Dashboard } from "@screens/Dashboard.tsx"; import { FilterDetails, Filters } from "@screens/filters"; import { Actions, Advanced, External, General, MoviesTv, Music } from "@screens/filters/sections"; @@ -49,6 +47,7 @@ import { Login, Onboarding } from "@screens/auth"; import { APIClient } from "@api/APIClient.ts"; import { routerBasePath } from "@utils"; import { filterKeys } from "@screens/filters/List.tsx"; +import { RingResizeSpinner } from "@components/Icons.tsx"; export const queryClient = new QueryClient({ queryCache: new QueryCache({ @@ -56,9 +55,9 @@ export const queryClient = new QueryClient({ // check for 401 and redirect here console.error("query cache error:", error) console.error("query cache query:", query) - // @ts-expect-error + // @ts-expect-error TS2339: Property status does not exist on type Error if (error?.status === 401 || error?.status === 403) { - // @ts-expect-error + // @ts-expect-error TS2339: Property status does not exist on type Error console.error("bad status, redirect to login", error?.status) // Redirect to login page window.location.href = "/login"; @@ -286,12 +285,6 @@ export const settingsRoute = new Route({ getParentRoute: () => authIndexRoute, path: 'settings', pendingMs: 3000, - pendingComponent: () => ( -
        - {/**/} - -
        - ), component: Settings }) @@ -409,10 +402,13 @@ export const loginRoute = new Route({ }, }).update({component: Login}) -export function RouterSpinner() { +/* COMMENT(martylukyy): This can probably be removed since the spinner works with pendingComponent? + + export function RouterSpinner() { const isLoading = useRouterState({ select: (s) => s.status === 'pending' }) return -} + +}*/ const RootComponent = () => { const settings = SettingsContext.useValue(); @@ -519,7 +515,9 @@ const routeTree = rootRoute.addChildren([ const router = new Router({ routeTree, defaultPendingComponent: () => ( - +
        + +
        ), defaultErrorComponent: ({ error }) => , context: { diff --git a/web/src/components/SectionLoader.tsx b/web/src/components/SectionLoader.tsx deleted file mode 100644 index cae32cdb35..0000000000 --- a/web/src/components/SectionLoader.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2021 - 2024, Ludvig Lundgren and the autobrr contributors. - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import { RingResizeSpinner } from "@components/Icons"; -import { classNames } from "@utils"; - -const SIZE = { - small: "w-6 h-6", - medium: "w-8 h-8", - large: "w-12 h-12", - xlarge: "w-24 h-24" -} as const; - -interface SectionLoaderProps { - $size: keyof typeof SIZE; -} - -export const SectionLoader = ({ $size }: SectionLoaderProps) => { - if ($size === "xlarge") { - return ( -
        - -
        - ); - } else { - return ( - - ); - } -}; - -export function Spinner({ show, wait }: { show?: boolean; wait?: `delay-${number}` }) { - return ( -
        - ⍥ -
        - ) -} diff --git a/web/src/components/modals/index.tsx b/web/src/components/modals/index.tsx index 5abd48e9fe..a7fe646b4b 100644 --- a/web/src/components/modals/index.tsx +++ b/web/src/components/modals/index.tsx @@ -8,7 +8,7 @@ import { FC, Fragment, MutableRefObject, useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; -import { SectionLoader } from "@components/SectionLoader"; +import { RingResizeSpinner } from "@components/Icons.tsx"; interface ModalUpperProps { title: string; @@ -58,7 +58,7 @@ const ModalUpper = ({ title, text }: ModalUpperProps) => ( const ModalLower = ({ isOpen, isLoading, toggle, deleteAction }: ModalLowerProps) => (
        {isLoading ? ( - + ) : ( <>
        } > diff --git a/web/src/screens/releases/ReleaseTable.tsx b/web/src/screens/releases/ReleaseTable.tsx index 643cd25e0d..84d4553ac3 100644 --- a/web/src/screens/releases/ReleaseTable.tsx +++ b/web/src/screens/releases/ReleaseTable.tsx @@ -11,19 +11,20 @@ import { ChevronDoubleLeftIcon, ChevronDoubleRightIcon, ChevronLeftIcon, - ChevronRightIcon + ChevronRightIcon, + EyeIcon, + EyeSlashIcon } from "@heroicons/react/24/solid"; -import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid"; import { RandomLinuxIsos } from "@utils"; import { APIClient } from "@api/APIClient"; -import { EmptyListState } from "@components/emptystates"; import * as Icons from "@components/Icons"; +import { RingResizeSpinner } from "@components/Icons"; import * as DataTable from "@components/data-table"; import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./Filters"; -import {releasesIndexRoute} from "@app/App.tsx"; +import { releasesIndexRoute } from "@app/App.tsx"; export const releaseKeys = { all: ["releases"] as const, @@ -223,167 +224,33 @@ export const ReleaseTable = () => { if (isLoading) { return ( -
        + <>
        - {headerGroups.map((headerGroup) => - headerGroup.headers.map((column) => ( + { headerGroups.map((headerGroup) => headerGroup.headers.map((column) => ( column.Filter ? ( - {column.render("Filter")} + { column.render("Filter") } ) : null )) - )} + ) }
        -
        -
        - - - - - +
        +
        -
        - {/* Add a sort direction indicator */} - - -
        -
        + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        +
        + +
        +
           
           
           
           
        -

        Loading release table...

        -
           
           
           
           
           
        - - {/* Pagination */} -
        -
        - previousPage()} disabled={!canPreviousPage}>Previous - nextPage()} disabled={!canNextPage}>Next -
        -
        -
        - - Page {pageIndex + 1} of {pageOptions.length} - - -
        -
        - -
        -
        +
        +
        -
        - ); - } - - if (!data) { - return ; + + ) } // Render the UI for your table From 7a745a18724f9a0f88fb7a31fc19050fb7daefe2 Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Sat, 10 Feb 2024 12:16:42 +0100 Subject: [PATCH 24/46] fix: refactor missed SectionLoader to RingResizeSpinner --- web/src/components/modals/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/modals/index.tsx b/web/src/components/modals/index.tsx index a7fe646b4b..bb8c6fd08d 100644 --- a/web/src/components/modals/index.tsx +++ b/web/src/components/modals/index.tsx @@ -221,7 +221,7 @@ export const ForceRunModal: FC = (props: ForceRunModalProps)
        {props.isLoading ? ( - + ) : ( <> + +
        +
+ ); +}; diff --git a/web/src/screens/filters/index.ts b/web/src/screens/filters/index.ts index 5e341ce1e1..a83e411955 100644 --- a/web/src/screens/filters/index.ts +++ b/web/src/screens/filters/index.ts @@ -5,3 +5,4 @@ export { Filters } from "./List"; export { FilterDetails } from "./Details"; +export { FilterNotFound } from "./NotFound"; \ No newline at end of file diff --git a/web/src/screens/filters/sections/Actions.tsx b/web/src/screens/filters/sections/Actions.tsx index ca1675c98f..78cd6f3237 100644 --- a/web/src/screens/filters/sections/Actions.tsx +++ b/web/src/screens/filters/sections/Actions.tsx @@ -25,6 +25,7 @@ import { TitleSubtitle } from "@components/headings"; import * as FilterSection from "./_components"; import * as FilterActions from "./action_components"; +import { DownloadClientsQueryOptions } from "@api/queries"; // interface FilterActionsProps { // filter: Filter; @@ -34,11 +35,7 @@ import * as FilterActions from "./action_components"; export function Actions() { const { values } = useFormikContext(); - const { data } = useQuery({ - queryKey: ["filters", "download_clients"], - queryFn: () => APIClient.download_clients.getAll(), - refetchOnWindowFocus: false - }); + const { data } = useQuery(DownloadClientsQueryOptions()); const newAction: Action = { id: 0, diff --git a/web/src/screens/filters/sections/General.tsx b/web/src/screens/filters/sections/General.tsx index 916a1407aa..7785f03c90 100644 --- a/web/src/screens/filters/sections/General.tsx +++ b/web/src/screens/filters/sections/General.tsx @@ -1,19 +1,20 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { downloadsPerUnitOptions } from "@domain/constants"; +import { IndexersOptionsQueryOptions } from "@api/queries"; import { DocsLink } from "@components/ExternalLink"; import * as Input from "@components/inputs"; import * as Components from "./_components"; -import { indexersOptionsQueryOptions } from "@app/App.tsx"; + const MapIndexer = (indexer: Indexer) => ( { label: indexer.name, value: indexer.id } as Input.MultiSelectOption ); export const General = () => { - const indexersQuery = useSuspenseQuery(indexersOptionsQueryOptions()) + const indexersQuery = useSuspenseQuery(IndexersOptionsQueryOptions()) const indexerOptions = indexersQuery.data && indexersQuery.data.map(MapIndexer) // const indexerOptions = data?.map(MapIndexer) ?? []; diff --git a/web/src/screens/releases/Filters.tsx b/web/src/screens/releases/ReleaseFilters.tsx similarity index 94% rename from web/src/screens/releases/Filters.tsx rename to web/src/screens/releases/ReleaseFilters.tsx index e9c504358c..bcfd4690c4 100644 --- a/web/src/screens/releases/Filters.tsx +++ b/web/src/screens/releases/ReleaseFilters.tsx @@ -4,15 +4,15 @@ */ import * as React from "react"; -import { useQuery, keepPreviousData } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { Listbox, Transition } from "@headlessui/react"; import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/solid"; -import { APIClient } from "@api/APIClient"; import { classNames } from "@utils"; import { PushStatusOptions } from "@domain/constants"; import { FilterProps } from "react-table"; import { DebounceInput } from "react-debounce-input"; +import { ReleasesIndexersQueryOptions } from "@api/queries"; interface ListboxFilterProps { id: string; @@ -67,12 +67,7 @@ const ListboxFilter = ({ export const IndexerSelectColumnFilter = ({ column: { filterValue, setFilter, id } }: FilterProps) => { - const { data, isSuccess } = useQuery({ - queryKey: ["indexer_options"], - queryFn: () => APIClient.release.indexerOptions(), - placeholderData: keepPreviousData, - staleTime: Infinity - }); + const { data, isSuccess } = useQuery(ReleasesIndexersQueryOptions()); // Render a multi-select box return ( diff --git a/web/src/screens/releases/ReleaseTable.tsx b/web/src/screens/releases/ReleaseTable.tsx index 84d4553ac3..722093a4ed 100644 --- a/web/src/screens/releases/ReleaseTable.tsx +++ b/web/src/screens/releases/ReleaseTable.tsx @@ -16,22 +16,25 @@ import { EyeSlashIcon } from "@heroicons/react/24/solid"; +import { ReleasesIndexRoute } from "@app/routes"; +import {ReleasesListQueryOptions} from "@api/queries"; import { RandomLinuxIsos } from "@utils"; -import { APIClient } from "@api/APIClient"; import * as Icons from "@components/Icons"; import { RingResizeSpinner } from "@components/Icons"; import * as DataTable from "@components/data-table"; -import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./Filters"; -import { releasesIndexRoute } from "@app/App.tsx"; +import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./ReleaseFilters"; export const releaseKeys = { all: ["releases"] as const, lists: () => [...releaseKeys.all, "list"] as const, list: (pageIndex: number, pageSize: number, filters: ReleaseFilter[]) => [...releaseKeys.lists(), { pageIndex, pageSize, filters }] as const, details: () => [...releaseKeys.all, "detail"] as const, - detail: (id: number) => [...releaseKeys.details(), id] as const + detail: (id: number) => [...releaseKeys.details(), id] as const, + indexers: () => [...releaseKeys.all, "indexers"] as const, + stats: () => [...releaseKeys.all, "stats"] as const, + latestActivity: () => [...releaseKeys.all, "latest-activity"] as const, }; type TableState = { @@ -82,7 +85,7 @@ const TableReducer = (state: TableState, action: Actions): TableState => { }; export const ReleaseTable = () => { - const search = releasesIndexRoute.useSearch() + const search = ReleasesIndexRoute.useSearch() const columns = React.useMemo(() => [ { @@ -124,11 +127,7 @@ export const ReleaseTable = () => { const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] = React.useReducer(TableReducer, initialState); - const { isLoading, error, data, isSuccess } = useQuery({ - queryKey: releaseKeys.list(queryPageIndex, queryPageSize, queryFilters), - queryFn: () => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters), - staleTime: 5000 - }); + const { isLoading, error, data, isSuccess } = useQuery(ReleasesListQueryOptions(queryPageIndex * queryPageSize, queryPageSize, queryFilters)); const [modifiedData, setModifiedData] = useState([]); const [showLinuxIsos, setShowLinuxIsos] = useState(false); @@ -212,7 +211,6 @@ export const ReleaseTable = () => { }, [filters]); React.useEffect(() => { - console.log("search params change: ", search) if (search.action_status != null) { dispatch({ type: ActionType.FILTER_CHANGED, payload: [{ id: "action_status", value: search.action_status! }] }); } diff --git a/web/src/screens/settings/Account.tsx b/web/src/screens/settings/Account.tsx index ac7a403126..302084a943 100644 --- a/web/src/screens/settings/Account.tsx +++ b/web/src/screens/settings/Account.tsx @@ -4,15 +4,16 @@ */ import { useMutation } from "@tanstack/react-query"; -import { APIClient } from "@api/APIClient"; -import Toast from "@components/notifications/Toast"; -import { Section } from "./_components"; import { Form, Formik } from "formik"; -import { PasswordField, TextField } from "@components/inputs"; -import { AuthContext } from "@utils/Context"; import toast from "react-hot-toast"; import { UserIcon } from "@heroicons/react/24/solid"; -import { settingsAccountRoute } from "@app/App.tsx"; + +import { APIClient } from "@api/APIClient"; +import { Section } from "./_components"; +import { PasswordField, TextField } from "@components/inputs"; +import Toast from "@components/notifications/Toast"; + +import { AuthContext, SettingsAccountRoute } from "@app/routes"; const AccountSettings = () => (
{ const errors: Record = {}; @@ -51,7 +52,8 @@ function Credentials() { const logoutMutation = useMutation({ mutationFn: APIClient.auth.logout, onSuccess: () => { - AuthContext.reset(); + AuthContext.logout(); + toast.custom((t) => ( )); diff --git a/web/src/screens/settings/Api.tsx b/web/src/screens/settings/Api.tsx index 85c2463399..256c4fd85f 100644 --- a/web/src/screens/settings/Api.tsx +++ b/web/src/screens/settings/Api.tsx @@ -4,7 +4,7 @@ */ import { useRef } from "react"; -import {useMutation, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; +import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { toast } from "react-hot-toast"; import { TrashIcon } from "@heroicons/react/24/outline"; @@ -18,31 +18,20 @@ import { classNames } from "@utils"; import { EmptySimple } from "@components/emptystates"; import { Section } from "./_components"; import { PlusIcon } from "@heroicons/react/24/solid"; -import { apikeysQueryOptions } from "@app/App.tsx"; + +import { ApikeysQueryOptions } from "@api/queries"; export const apiKeys = { all: ["api_keys"] as const, lists: () => [...apiKeys.all, "list"] as const, details: () => [...apiKeys.all, "detail"] as const, - // detail: (id: number) => [...apiKeys.details(), id] as const detail: (id: string) => [...apiKeys.details(), id] as const }; function APISettings() { const [addFormIsOpen, toggleAddForm] = useToggle(false); - const apikeysQuery = useSuspenseQuery(apikeysQueryOptions()) - - // const { isError, error, data } = useQuery({ - // queryKey: apiKeys.lists(), - // queryFn: APIClient.apikeys.getAll, - // retry: false, - // refetchOnWindowFocus: false - // }); - // - // if (isError) { - // console.log(error); - // } + const apikeysQuery = useSuspenseQuery(ApikeysQueryOptions()) return (
{ - queryClient.invalidateQueries({ queryKey: ["updates"] }); + queryClient.invalidateQueries({ queryKey: settingsKeys.updates() }); } }); @@ -54,7 +45,7 @@ function ApplicationSettings() { mutationFn: (value: boolean) => APIClient.config.update({ check_for_updates: value }).then(() => value), onSuccess: (_, value: boolean) => { toast.custom(t => ); - queryClient.invalidateQueries({ queryKey: ["config"] }); + queryClient.invalidateQueries({ queryKey: settingsKeys.config() }); checkUpdateMutation.mutate(); } }); diff --git a/web/src/screens/settings/DownloadClient.tsx b/web/src/screens/settings/DownloadClient.tsx index 87bc551db9..4403964f54 100644 --- a/web/src/screens/settings/DownloadClient.tsx +++ b/web/src/screens/settings/DownloadClient.tsx @@ -12,12 +12,12 @@ import { useToggle } from "@hooks/hooks"; import { DownloadClientAddForm, DownloadClientUpdateForm } from "@forms"; import { EmptySimple } from "@components/emptystates"; import { APIClient } from "@api/APIClient"; +import { DownloadClientsQueryOptions } from "@api/queries"; import { ActionTypeNameMap } from "@domain/constants"; import Toast from "@components/notifications/Toast"; import { Checkbox } from "@components/Checkbox"; import { Section } from "./_components"; -import { downloadClientsQueryOptions } from "@app/App.tsx"; export const clientKeys = { all: ["download_clients"] as const, @@ -141,7 +141,7 @@ function ListItem({ client }: DLSettingsItemProps) { function DownloadClientSettings() { const [addClientIsOpen, toggleAddClient] = useToggle(false); - const downloadClientsQuery = useSuspenseQuery(downloadClientsQueryOptions()) + const downloadClientsQuery = useSuspenseQuery(DownloadClientsQueryOptions()) const sortedClients = useSort(downloadClientsQuery.data || []); diff --git a/web/src/screens/settings/Feed.tsx b/web/src/screens/settings/Feed.tsx index 090ae1d70a..98e6795a45 100644 --- a/web/src/screens/settings/Feed.tsx +++ b/web/src/screens/settings/Feed.tsx @@ -17,6 +17,7 @@ import { } from "@heroicons/react/24/outline"; import { APIClient } from "@api/APIClient"; +import { FeedsQueryOptions } from "@api/queries"; import { useToggle } from "@hooks/hooks"; import { baseUrl, classNames, IsEmptyDate, simplifyDate } from "@utils"; import Toast from "@components/notifications/Toast"; @@ -28,7 +29,6 @@ import { ArrowPathIcon } from "@heroicons/react/24/solid"; import { ExternalLink } from "@components/ExternalLink"; import { Section } from "./_components"; import { Checkbox } from "@components/Checkbox"; -import { feedsQueryOptions } from "@app/App.tsx"; export const feedKeys = { all: ["feeds"] as const, @@ -98,12 +98,7 @@ function useSort(items: ListItemProps["feed"][], config?: SortConfig) { } function FeedSettings() { - const feedsQuery = useSuspenseQuery(feedsQueryOptions()) - // const { data } = useQuery({ - // queryKey: feedKeys.lists(), - // queryFn: APIClient.feeds.find, - // refetchOnWindowFocus: false - // }); + const feedsQuery = useSuspenseQuery(FeedsQueryOptions()) const sortedFeeds = useSort(feedsQuery.data || []); diff --git a/web/src/screens/settings/Indexer.tsx b/web/src/screens/settings/Indexer.tsx index da8fb041dc..31b4e50935 100644 --- a/web/src/screens/settings/Indexer.tsx +++ b/web/src/screens/settings/Indexer.tsx @@ -10,6 +10,7 @@ import { PlusIcon } from "@heroicons/react/24/solid"; import { useToggle } from "@hooks/hooks"; import { APIClient } from "@api/APIClient"; +import { IndexersQueryOptions } from "@api/queries"; import { Checkbox } from "@components/Checkbox"; import Toast from "@components/notifications/Toast"; import { EmptySimple } from "@components/emptystates"; @@ -17,10 +18,11 @@ import { IndexerAddForm, IndexerUpdateForm } from "@forms"; import { componentMapType } from "@forms/settings/DownloadClientForms"; import { Section } from "./_components"; -import { indexersQueryOptions } from "@app/App.tsx"; export const indexerKeys = { all: ["indexers"] as const, + schema: () => [...indexerKeys.all, "indexer-definitions"] as const, + options: () => [...indexerKeys.all, "options"] as const, lists: () => [...indexerKeys.all, "list"] as const, // list: (indexers: string[], sortOrder: string) => [...indexerKeys.lists(), { indexers, sortOrder }] as const, details: () => [...indexerKeys.all, "detail"] as const, @@ -169,19 +171,9 @@ const ListItem = ({ indexer }: ListItemProps) => { function IndexerSettings() { const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false); - // const ctx = settingsIndexersRoute.useRouteContext() - // const queryClient = ctx.queryClient - - const indexersQuery = useSuspenseQuery(indexersQueryOptions()) + const indexersQuery = useSuspenseQuery(IndexersQueryOptions()) const indexers = indexersQuery.data - - // const { error, data } = useQuery({ - // queryKey: indexerKeys.lists(), - // queryFn: APIClient.indexers.getAll, - // refetchOnWindowFocus: false - // }); - const sortedIndexers = useSort(indexers || []); // if (error) { diff --git a/web/src/screens/settings/Irc.tsx b/web/src/screens/settings/Irc.tsx index 0f2cc3390a..9b7ecb3d36 100644 --- a/web/src/screens/settings/Irc.tsx +++ b/web/src/screens/settings/Irc.tsx @@ -22,6 +22,7 @@ import { classNames, IsEmptyDate, simplifyDate } from "@utils"; import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "@forms"; import { useToggle } from "@hooks/hooks"; import { APIClient } from "@api/APIClient"; +import { IrcQueryOptions } from "@api/queries"; import { EmptySimple } from "@components/emptystates"; import { DeleteModal } from "@components/modals"; import Toast from "@components/notifications/Toast"; @@ -29,7 +30,6 @@ import { SettingsContext } from "@utils/Context"; import { Checkbox } from "@components/Checkbox"; import { Section } from "./_components"; -import { ircQueryOptions } from "@app/App.tsx"; export const ircKeys = { all: ["irc_networks"] as const, @@ -98,10 +98,7 @@ const IrcSettings = () => { const [expandNetworks, toggleExpand] = useToggle(false); const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false); - // const ctx = settingsIrcRoute.useRouteContext() - // const queryClient = ctx.queryClient - - const ircQuery = useSuspenseQuery(ircQueryOptions()) + const ircQuery = useSuspenseQuery(IrcQueryOptions()) const sortedNetworks = useSort(ircQuery.data || []); diff --git a/web/src/screens/settings/Logs.tsx b/web/src/screens/settings/Logs.tsx index 634317dcd5..6d42f68d0e 100644 --- a/web/src/screens/settings/Logs.tsx +++ b/web/src/screens/settings/Logs.tsx @@ -3,19 +3,20 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import {useMutation, useSuspenseQuery} from "@tanstack/react-query"; -import {Link} from "@tanstack/react-router"; +import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; import { toast } from "react-hot-toast"; import Select from "react-select"; import { APIClient } from "@api/APIClient"; +import { ConfigQueryOptions } from "@api/queries"; +import { SettingsLogRoute } from "@app/routes"; import Toast from "@components/notifications/Toast"; import { LogLevelOptions, SelectOption } from "@domain/constants"; import { Section, RowItem } from "./_components"; import * as common from "@components/inputs/common"; import { LogFiles } from "@screens/Logs"; -import {configQueryOptions, settingsLogRoute} from "@app/App.tsx"; type SelectWrapperProps = { id: string; @@ -57,10 +58,10 @@ const SelectWrapper = ({ id, value, onChange, options }: SelectWrapperProps) => ); function LogSettings() { - const ctx = settingsLogRoute.useRouteContext() + const ctx = SettingsLogRoute.useRouteContext() const queryClient = ctx.queryClient - const configQuery = useSuspenseQuery(configQueryOptions()) + const configQuery = useSuspenseQuery(ConfigQueryOptions()) const config = configQuery.data diff --git a/web/src/screens/settings/Notifications.tsx b/web/src/screens/settings/Notifications.tsx index aa2103781e..37204c757d 100644 --- a/web/src/screens/settings/Notifications.tsx +++ b/web/src/screens/settings/Notifications.tsx @@ -6,6 +6,7 @@ import {useMutation, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; import { APIClient } from "@api/APIClient"; +import { NotificationsQueryOptions } from "@api/queries"; import { EmptySimple } from "@components/emptystates"; import { useToggle } from "@hooks/hooks"; import { NotificationAddForm, NotificationUpdateForm } from "@forms/settings/NotificationForms"; @@ -16,7 +17,6 @@ import { Section } from "./_components"; import { PlusIcon } from "@heroicons/react/24/solid"; import { Checkbox } from "@components/Checkbox"; import { DiscordIcon, GotifyIcon, LunaSeaIcon, NotifiarrIcon, NtfyIcon, PushoverIcon, TelegramIcon } from "./_components"; -import { notificationsQueryOptions } from "@app/App.tsx"; export const notificationKeys = { all: ["notifications"] as const, @@ -28,7 +28,7 @@ export const notificationKeys = { function NotificationSettings() { const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false); - const notificationsQuery = useSuspenseQuery(notificationsQueryOptions()) + const notificationsQuery = useSuspenseQuery(NotificationsQueryOptions()) return (
( ctxState.set(values); } +const SettingsKey = "autobrr_settings"; +const FilterListKey = "autobrr_filter_list"; + export const InitializeGlobalContext = () => { - ContextMerger("auth", AuthContextDefaults, AuthContext); + // ContextMerger("auth", AuthContextDefaults, AuthContext); ContextMerger( - "settings", + SettingsKey, SettingsContextDefaults, SettingsContext ); ContextMerger( - "filterList", + FilterListKey, FilterListContextDefaults, FilterListContext ); @@ -107,7 +110,7 @@ export const SettingsContext = newRidgeState( { onSet: (newState, prevState) => { document.documentElement.classList.toggle("dark", newState.darkTheme); - DefaultSetter("settings", newState, prevState); + DefaultSetter(SettingsKey, newState, prevState); } } ); @@ -115,6 +118,6 @@ export const SettingsContext = newRidgeState( export const FilterListContext = newRidgeState( FilterListContextDefaults, { - onSet: (newState, prevState) => DefaultSetter("filterList", newState, prevState) + onSet: (newState, prevState) => DefaultSetter(FilterListKey, newState, prevState) } ); From 6c03e8cc3d7a88cc99f25ae37a9435fd49f78350 Mon Sep 17 00:00:00 2001 From: ze0s Date: Sun, 11 Feb 2024 15:43:20 +0100 Subject: [PATCH 29/46] fix(filters): notfound get params --- web/src/routes.tsx | 3 +-- web/src/screens/filters/NotFound.tsx | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/web/src/routes.tsx b/web/src/routes.tsx index 7f6eb4207c..50769eeabb 100644 --- a/web/src/routes.tsx +++ b/web/src/routes.tsx @@ -94,8 +94,7 @@ export const FilterGetByIdRoute = createRoute({ }, component: FilterDetails, notFoundComponent: () => { - const { filterId} = FilterGetByIdRoute.useParams() - return + return }, }); diff --git a/web/src/screens/filters/NotFound.tsx b/web/src/screens/filters/NotFound.tsx index 8de8666035..3d89e5975c 100644 --- a/web/src/screens/filters/NotFound.tsx +++ b/web/src/screens/filters/NotFound.tsx @@ -4,15 +4,14 @@ */ import { Link } from "@tanstack/react-router"; +import { FilterGetByIdRoute } from "@app/routes"; import { ExternalLink } from "@components/ExternalLink"; import Logo from "@app/logo.svg?react"; -interface FilterNotFoundProps { - filterId: number; -} +export const FilterNotFound = () => { + const { filterId } = FilterGetByIdRoute.useParams() -export const FilterNotFound = ({ filterId }: FilterNotFoundProps) => { return (
From f1a117db2820ada4f090c1511d960aeb4b5cfe8a Mon Sep 17 00:00:00 2001 From: ze0s Date: Sun, 11 Feb 2024 16:19:40 +0100 Subject: [PATCH 30/46] fix(queries): colon --- web/src/api/queries.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/api/queries.ts b/web/src/api/queries.ts index 0fdcfe9ff3..faa24a0194 100644 --- a/web/src/api/queries.ts +++ b/web/src/api/queries.ts @@ -20,7 +20,7 @@ export const FiltersQueryOptions = (indexers: string[], sortOrder: string) => queryKey: filterKeys.list(indexers, sortOrder), queryFn: () => APIClient.filters.find(indexers, sortOrder), refetchOnWindowFocus: false - }) + }); export const FilterByIdQueryOptions = (filterId: number) => queryOptions({ From d849efd38d661c0b6e5e48eb3b8c9e3afa7ab0e3 Mon Sep 17 00:00:00 2001 From: ze0s Date: Sun, 11 Feb 2024 16:19:59 +0100 Subject: [PATCH 31/46] fix(queries): comments ts-ignore --- web/src/api/QueryClient.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/api/QueryClient.tsx b/web/src/api/QueryClient.tsx index 73d1f401a0..e28cbd95f5 100644 --- a/web/src/api/QueryClient.tsx +++ b/web/src/api/QueryClient.tsx @@ -38,12 +38,12 @@ export const queryClient = new QueryClient({ retry: (failureCount, error) => { console.error(`retry count ${failureCount} error: ${error}`) - // @ts-ignore + // @ts-expect-error TS2339: Ignore err if (Object.hasOwnProperty.call(error, "status") && - //@ts-ignore + // @ts-expect-error TS2339: ignore HTTP_STATUS_TO_NOT_RETRY.includes(error.status) ) { - // @ts-ignore + // @ts-expect-error TS2339: ignore console.log(`retry: Aborting retry due to ${error.status} status`); return false; } From c69ae5ea084a93c5d36f1d7875082445bf680459 Mon Sep 17 00:00:00 2001 From: ze0s Date: Sun, 11 Feb 2024 16:45:15 +0100 Subject: [PATCH 32/46] fix(queries): extract queryKeys --- web/src/App.tsx | 3 +- web/src/api/QueryClient.tsx | 2 +- web/src/api/queries.ts | 51 ++++++------ web/src/api/query_keys.ts | 77 +++++++++++++++++++ web/src/components/data-table/Cells.tsx | 8 +- web/src/components/header/_shared.ts | 2 +- web/src/forms/filters/FilterAddForm.tsx | 5 +- web/src/forms/settings/APIKeyAddForm.tsx | 4 +- .../forms/settings/DownloadClientForms.tsx | 12 +-- web/src/forms/settings/FeedForms.tsx | 7 +- web/src/forms/settings/IndexerForms.tsx | 11 ++- web/src/forms/settings/IrcForms.tsx | 8 +- web/src/forms/settings/NotificationForms.tsx | 8 +- web/src/routes.tsx | 35 ++------- web/src/screens/Settings.tsx | 7 -- web/src/screens/filters/Details.tsx | 12 +-- web/src/screens/filters/Importer.tsx | 4 +- web/src/screens/filters/List.tsx | 30 +++----- web/src/screens/releases/ReleaseTable.tsx | 13 +--- web/src/screens/settings/Account.tsx | 4 +- web/src/screens/settings/Api.tsx | 14 +--- web/src/screens/settings/Application.tsx | 11 ++- web/src/screens/settings/DownloadClient.tsx | 15 +--- web/src/screens/settings/Feed.tsx | 25 +++--- web/src/screens/settings/Indexer.tsx | 15 +--- web/src/screens/settings/Irc.tsx | 21 ++--- web/src/screens/settings/Notifications.tsx | 28 +++---- web/src/screens/settings/Releases.tsx | 4 +- web/src/utils/Context.ts | 52 +++++++++---- 29 files changed, 254 insertions(+), 234 deletions(-) create mode 100644 web/src/api/query_keys.ts diff --git a/web/src/App.tsx b/web/src/App.tsx index b7fa6ffd9a..ad9b7eaa83 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -7,9 +7,10 @@ import { RouterProvider } from "@tanstack/react-router" import { QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "react-hot-toast"; import { Portal } from "react-portal"; -import { AuthContext, Router } from "@app/routes"; +import { Router } from "@app/routes"; import { routerBasePath } from "@utils"; import { queryClient } from "@api/QueryClient"; +import { AuthContext } from "@utils/Context"; declare module '@tanstack/react-router' { interface Register { diff --git a/web/src/api/QueryClient.tsx b/web/src/api/QueryClient.tsx index e28cbd95f5..a5994039ee 100644 --- a/web/src/api/QueryClient.tsx +++ b/web/src/api/QueryClient.tsx @@ -38,7 +38,7 @@ export const queryClient = new QueryClient({ retry: (failureCount, error) => { console.error(`retry count ${failureCount} error: ${error}`) - // @ts-expect-error TS2339: Ignore err + // @ts-expect-error TS2339: Ignore err. if (Object.hasOwnProperty.call(error, "status") && // @ts-expect-error TS2339: ignore HTTP_STATUS_TO_NOT_RETRY.includes(error.status) diff --git a/web/src/api/queries.ts b/web/src/api/queries.ts index faa24a0194..d3aa1d4455 100644 --- a/web/src/api/queries.ts +++ b/web/src/api/queries.ts @@ -4,34 +4,35 @@ */ import { keepPreviousData, queryOptions } from "@tanstack/react-query"; -import { notificationKeys } from "@screens/settings/Notifications"; import { APIClient } from "@api/APIClient"; -import { clientKeys } from "@screens/settings/DownloadClient"; -import { feedKeys } from "@screens/settings/Feed"; -import { filterKeys } from "@screens/filters/List"; -import { apiKeys } from "@screens/settings/Api"; -import { indexerKeys } from "@screens/settings/Indexer"; -import { ircKeys } from "@screens/settings/Irc"; -import { settingsKeys } from "@screens/Settings"; -import { releaseKeys } from "@screens/releases/ReleaseTable"; +import { + ApiKeys, + DownloadClientKeys, + FeedKeys, + FilterKeys, + IndexerKeys, + IrcKeys, NotificationKeys, + ReleaseKeys, + SettingsKeys +} from "@api/query_keys"; export const FiltersQueryOptions = (indexers: string[], sortOrder: string) => queryOptions({ - queryKey: filterKeys.list(indexers, sortOrder), + queryKey: FilterKeys.list(indexers, sortOrder), queryFn: () => APIClient.filters.find(indexers, sortOrder), refetchOnWindowFocus: false }); export const FilterByIdQueryOptions = (filterId: number) => queryOptions({ - queryKey: filterKeys.detail(filterId), + queryKey: FilterKeys.detail(filterId), queryFn: async ({queryKey}) => await APIClient.filters.getByID(queryKey[2]), retry: false, }); export const ConfigQueryOptions = (enabled: boolean = true) => queryOptions({ - queryKey: settingsKeys.config(), + queryKey: SettingsKeys.config(), queryFn: () => APIClient.config.get(), retry: false, refetchOnWindowFocus: false, @@ -40,7 +41,7 @@ export const ConfigQueryOptions = (enabled: boolean = true) => export const UpdatesQueryOptions = (enabled: boolean) => queryOptions({ - queryKey: settingsKeys.updates(), + queryKey: SettingsKeys.updates(), queryFn: () => APIClient.updates.getLatestRelease(), retry: false, refetchOnWindowFocus: false, @@ -49,13 +50,13 @@ export const UpdatesQueryOptions = (enabled: boolean) => export const IndexersQueryOptions = () => queryOptions({ - queryKey: indexerKeys.lists(), + queryKey: IndexerKeys.lists(), queryFn: () => APIClient.indexers.getAll() }); export const IndexersOptionsQueryOptions = () => queryOptions({ - queryKey: indexerKeys.options(), + queryKey: IndexerKeys.options(), queryFn: () => APIClient.indexers.getOptions(), refetchOnWindowFocus: false, staleTime: Infinity @@ -63,7 +64,7 @@ export const IndexersOptionsQueryOptions = () => export const IndexersSchemaQueryOptions = (enabled: boolean) => queryOptions({ - queryKey: indexerKeys.schema(), + queryKey: IndexerKeys.schema(), queryFn: () => APIClient.indexers.getSchema(), refetchOnWindowFocus: false, staleTime: Infinity, @@ -72,7 +73,7 @@ export const IndexersSchemaQueryOptions = (enabled: boolean) => export const IrcQueryOptions = () => queryOptions({ - queryKey: ircKeys.lists(), + queryKey: IrcKeys.lists(), queryFn: () => APIClient.irc.getNetworks(), refetchOnWindowFocus: false, refetchInterval: 3000 // Refetch every 3 seconds @@ -80,46 +81,46 @@ export const IrcQueryOptions = () => export const FeedsQueryOptions = () => queryOptions({ - queryKey: feedKeys.lists(), + queryKey: FeedKeys.lists(), queryFn: () => APIClient.feeds.find(), }); export const DownloadClientsQueryOptions = () => queryOptions({ - queryKey: clientKeys.lists(), + queryKey: DownloadClientKeys.lists(), queryFn: () => APIClient.download_clients.getAll(), }); export const NotificationsQueryOptions = () => queryOptions({ - queryKey: notificationKeys.lists(), + queryKey: NotificationKeys.lists(), queryFn: () => APIClient.notifications.getAll() }); export const ApikeysQueryOptions = () => queryOptions({ - queryKey: apiKeys.lists(), + queryKey: ApiKeys.lists(), queryFn: () => APIClient.apikeys.getAll(), refetchOnWindowFocus: false, }); export const ReleasesListQueryOptions = (offset: number, limit: number, filters: ReleaseFilter[]) => queryOptions({ - queryKey: releaseKeys.list(offset, limit, filters), + queryKey: ReleaseKeys.list(offset, limit, filters), queryFn: () => APIClient.release.findQuery(offset, limit, filters), staleTime: 5000 }); export const ReleasesLatestQueryOptions = () => queryOptions({ - queryKey: releaseKeys.latestActivity(), + queryKey: ReleaseKeys.latestActivity(), queryFn: () => APIClient.release.findRecent(), refetchOnWindowFocus: false }); export const ReleasesStatsQueryOptions = () => queryOptions({ - queryKey: releaseKeys.stats(), + queryKey: ReleaseKeys.stats(), queryFn: () => APIClient.release.stats(), refetchOnWindowFocus: false }); @@ -127,7 +128,7 @@ export const ReleasesStatsQueryOptions = () => // ReleasesIndexersQueryOptions get basic list of used indexers by identifier export const ReleasesIndexersQueryOptions = () => queryOptions({ - queryKey: releaseKeys.indexers(), + queryKey: ReleaseKeys.indexers(), queryFn: () => APIClient.release.indexerOptions(), placeholderData: keepPreviousData, staleTime: Infinity diff --git a/web/src/api/query_keys.ts b/web/src/api/query_keys.ts new file mode 100644 index 0000000000..33807bd21e --- /dev/null +++ b/web/src/api/query_keys.ts @@ -0,0 +1,77 @@ +export const SettingsKeys = { + all: ["settings"] as const, + updates: () => [...SettingsKeys.all, "updates"] as const, + config: () => [...SettingsKeys.all, "config"] as const, + lists: () => [...SettingsKeys.all, "list"] as const, +}; + +export const FilterKeys = { + all: ["filters"] as const, + lists: () => [...FilterKeys.all, "list"] as const, + list: (indexers: string[], sortOrder: string) => [...FilterKeys.lists(), {indexers, sortOrder}] as const, + details: () => [...FilterKeys.all, "detail"] as const, + detail: (id: number) => [...FilterKeys.details(), id] as const +}; + +export const ReleaseKeys = { + all: ["releases"] as const, + lists: () => [...ReleaseKeys.all, "list"] as const, + list: (pageIndex: number, pageSize: number, filters: ReleaseFilter[]) => [...ReleaseKeys.lists(), { + pageIndex, + pageSize, + filters + }] as const, + details: () => [...ReleaseKeys.all, "detail"] as const, + detail: (id: number) => [...ReleaseKeys.details(), id] as const, + indexers: () => [...ReleaseKeys.all, "indexers"] as const, + stats: () => [...ReleaseKeys.all, "stats"] as const, + latestActivity: () => [...ReleaseKeys.all, "latest-activity"] as const, +}; + +export const ApiKeys = { + all: ["api_keys"] as const, + lists: () => [...ApiKeys.all, "list"] as const, + details: () => [...ApiKeys.all, "detail"] as const, + detail: (id: string) => [...ApiKeys.details(), id] as const +}; + +export const DownloadClientKeys = { + all: ["download_clients"] as const, + lists: () => [...DownloadClientKeys.all, "list"] as const, + // list: (indexers: string[], sortOrder: string) => [...clientKeys.lists(), { indexers, sortOrder }] as const, + details: () => [...DownloadClientKeys.all, "detail"] as const, + detail: (id: number) => [...DownloadClientKeys.details(), id] as const +}; + +export const FeedKeys = { + all: ["feeds"] as const, + lists: () => [...FeedKeys.all, "list"] as const, + // list: (indexers: string[], sortOrder: string) => [...feedKeys.lists(), { indexers, sortOrder }] as const, + details: () => [...FeedKeys.all, "detail"] as const, + detail: (id: number) => [...FeedKeys.details(), id] as const +}; + +export const IndexerKeys = { + all: ["indexers"] as const, + schema: () => [...IndexerKeys.all, "indexer-definitions"] as const, + options: () => [...IndexerKeys.all, "options"] as const, + lists: () => [...IndexerKeys.all, "list"] as const, + // list: (indexers: string[], sortOrder: string) => [...indexerKeys.lists(), { indexers, sortOrder }] as const, + details: () => [...IndexerKeys.all, "detail"] as const, + detail: (id: number) => [...IndexerKeys.details(), id] as const +}; + +export const IrcKeys = { + all: ["irc_networks"] as const, + lists: () => [...IrcKeys.all, "list"] as const, + // list: (indexers: string[], sortOrder: string) => [...ircKeys.lists(), { indexers, sortOrder }] as const, + details: () => [...IrcKeys.all, "detail"] as const, + detail: (id: number) => [...IrcKeys.details(), id] as const +}; + +export const NotificationKeys = { + all: ["notifications"] as const, + lists: () => [...NotificationKeys.all, "list"] as const, + details: () => [...NotificationKeys.all, "detail"] as const, + detail: (id: number) => [...NotificationKeys.details(), id] as const +}; \ No newline at end of file diff --git a/web/src/components/data-table/Cells.tsx b/web/src/components/data-table/Cells.tsx index fab165bf03..7a9e822ef7 100644 --- a/web/src/components/data-table/Cells.tsx +++ b/web/src/components/data-table/Cells.tsx @@ -9,7 +9,6 @@ import { formatDistanceToNowStrict } from "date-fns"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { CellProps } from "react-table"; import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/solid"; -import { ExternalLink } from "../ExternalLink"; import { ClockIcon, XMarkIcon, @@ -19,8 +18,9 @@ import { } from "@heroicons/react/24/outline"; import { APIClient } from "@api/APIClient"; -import {classNames, humanFileSize, simplifyDate} from "@utils"; -import { filterKeys } from "@screens/filters/List"; +import { FilterKeys } from "@api/query_keys"; +import { classNames, humanFileSize, simplifyDate } from "@utils"; +import { ExternalLink } from "../ExternalLink"; import Toast from "@components/notifications/Toast"; import { RingResizeSpinner } from "@components/Icons"; import { Tooltip } from "@components/tooltips/Tooltip"; @@ -164,7 +164,7 @@ const RetryActionButton = ({ status }: RetryActionButtonProps) => { mutationFn: (vars: RetryAction) => APIClient.release.replayAction(vars.releaseId, vars.actionId), onSuccess: () => { // Invalidate filters just in case, most likely not necessary but can't hurt. - queryClient.invalidateQueries({ queryKey: filterKeys.lists() }); + queryClient.invalidateQueries({ queryKey: FilterKeys.lists() }); toast.custom((t) => ( diff --git a/web/src/components/header/_shared.ts b/web/src/components/header/_shared.ts index 6b5f227374..0bd3c50fc6 100644 --- a/web/src/components/header/_shared.ts +++ b/web/src/components/header/_shared.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { AuthCtx } from "@routes"; +import { AuthCtx } from "@utils/Context"; interface NavItem { name: string; diff --git a/web/src/forms/filters/FilterAddForm.tsx b/web/src/forms/filters/FilterAddForm.tsx index e80c2841ef..3038f6f9a7 100644 --- a/web/src/forms/filters/FilterAddForm.tsx +++ b/web/src/forms/filters/FilterAddForm.tsx @@ -13,9 +13,10 @@ import type { FieldProps } from "formik"; import { Field, Form, Formik, FormikErrors, FormikValues } from "formik"; import { APIClient } from "@api/APIClient"; +import { FilterKeys } from "@api/query_keys"; import { DEBUG } from "@components/debug"; import Toast from "@components/notifications/Toast"; -import { filterKeys } from "@screens/filters/List"; + interface filterAddFormProps { isOpen: boolean; @@ -28,7 +29,7 @@ export function FilterAddForm({ isOpen, toggle }: filterAddFormProps) { const mutation = useMutation({ mutationFn: (filter: Filter) => APIClient.filters.create(filter), onSuccess: (filter) => { - queryClient.invalidateQueries({ queryKey: filterKeys.lists() }); + queryClient.invalidateQueries({ queryKey: FilterKeys.lists() }); toast.custom((t) => ); diff --git a/web/src/forms/settings/APIKeyAddForm.tsx b/web/src/forms/settings/APIKeyAddForm.tsx index fea255093d..406f08c607 100644 --- a/web/src/forms/settings/APIKeyAddForm.tsx +++ b/web/src/forms/settings/APIKeyAddForm.tsx @@ -12,9 +12,9 @@ import type { FieldProps } from "formik"; import { Field, Form, Formik, FormikErrors, FormikValues } from "formik"; import { APIClient } from "@api/APIClient"; +import { ApiKeys } from "@api/query_keys"; import { DEBUG } from "@components/debug"; import Toast from "@components/notifications/Toast"; -import { apiKeys } from "@screens/settings/Api"; interface apiKeyAddFormProps { isOpen: boolean; @@ -27,7 +27,7 @@ export function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) { const mutation = useMutation({ mutationFn: (apikey: APIKey) => APIClient.apikeys.create(apikey), onSuccess: (_, key) => { - queryClient.invalidateQueries({ queryKey: apiKeys.lists() }); + queryClient.invalidateQueries({ queryKey: ApiKeys.lists() }); toast.custom((t) => ); diff --git a/web/src/forms/settings/DownloadClientForms.tsx b/web/src/forms/settings/DownloadClientForms.tsx index 6db52ff316..9944104bd8 100644 --- a/web/src/forms/settings/DownloadClientForms.tsx +++ b/web/src/forms/settings/DownloadClientForms.tsx @@ -13,6 +13,7 @@ import { toast } from "react-hot-toast"; import { classNames, sleep } from "@utils"; import { DEBUG } from "@components/debug"; import { APIClient } from "@api/APIClient"; +import { DownloadClientKeys } from "@api/query_keys"; import { DownloadClientTypeOptions, DownloadRuleConditionOptions } from "@domain/constants"; import Toast from "@components/notifications/Toast"; import { useToggle } from "@hooks/hooks"; @@ -24,7 +25,6 @@ import { SwitchGroupWide, TextFieldWide } from "@components/inputs"; -import { clientKeys } from "@screens/settings/DownloadClient"; import { DocsLink, ExternalLink } from "@components/ExternalLink"; import { SelectFieldBasic } from "@components/inputs/select_wide"; @@ -693,7 +693,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) { const addMutation = useMutation({ mutationFn: (client: DownloadClient) => APIClient.download_clients.create(client), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: clientKeys.lists() }); + queryClient.invalidateQueries({ queryKey: DownloadClientKeys.lists() }); toast.custom((t) => ); toggle(); @@ -865,8 +865,8 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP const mutation = useMutation({ mutationFn: (client: DownloadClient) => APIClient.download_clients.update(client), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: clientKeys.lists() }); - queryClient.invalidateQueries({ queryKey: clientKeys.detail(client.id) }); + queryClient.invalidateQueries({ queryKey: DownloadClientKeys.lists() }); + queryClient.invalidateQueries({ queryKey: DownloadClientKeys.detail(client.id) }); toast.custom((t) => ); toggle(); @@ -878,8 +878,8 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP const deleteMutation = useMutation({ mutationFn: (clientID: number) => APIClient.download_clients.delete(clientID), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: clientKeys.lists() }); - queryClient.invalidateQueries({ queryKey: clientKeys.detail(client.id) }); + queryClient.invalidateQueries({ queryKey: DownloadClientKeys.lists() }); + queryClient.invalidateQueries({ queryKey: DownloadClientKeys.detail(client.id) }); toast.custom((t) => ); toggleDeleteModal(); diff --git a/web/src/forms/settings/FeedForms.tsx b/web/src/forms/settings/FeedForms.tsx index aef1c15d71..cfb1e307b7 100644 --- a/web/src/forms/settings/FeedForms.tsx +++ b/web/src/forms/settings/FeedForms.tsx @@ -9,6 +9,7 @@ import { toast } from "react-hot-toast"; import { useFormikContext } from "formik"; import { APIClient } from "@api/APIClient"; +import { FeedKeys } from "@api/query_keys"; import Toast from "@components/notifications/Toast"; import { SlideOver } from "@components/panels"; import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs"; @@ -17,7 +18,7 @@ import { componentMapType } from "./DownloadClientForms"; import { sleep } from "@utils"; import { ImplementationBadges } from "@screens/settings/Indexer"; import { FeedDownloadTypeOptions } from "@domain/constants"; -import { feedKeys } from "@screens/settings/Feed"; + interface UpdateProps { isOpen: boolean; @@ -50,7 +51,7 @@ export function FeedUpdateForm({ isOpen, toggle, feed }: UpdateProps) { const mutation = useMutation({ mutationFn: (feed: Feed) => APIClient.feeds.update(feed), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: feedKeys.lists() }); + queryClient.invalidateQueries({ queryKey: FeedKeys.lists() }); toast.custom((t) => ); toggle(); @@ -62,7 +63,7 @@ export function FeedUpdateForm({ isOpen, toggle, feed }: UpdateProps) { const deleteMutation = useMutation({ mutationFn: (feedID: number) => APIClient.feeds.delete(feedID), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: feedKeys.lists() }); + queryClient.invalidateQueries({ queryKey: FeedKeys.lists() }); toast.custom((t) => ); } diff --git a/web/src/forms/settings/IndexerForms.tsx b/web/src/forms/settings/IndexerForms.tsx index 2efc3f2f83..aeb5e68ce1 100644 --- a/web/src/forms/settings/IndexerForms.tsx +++ b/web/src/forms/settings/IndexerForms.tsx @@ -15,14 +15,13 @@ import { Dialog, Transition } from "@headlessui/react"; import { classNames, sleep } from "@utils"; import { DEBUG } from "@components/debug"; import { APIClient } from "@api/APIClient"; +import { FeedKeys, IndexerKeys } from "@api/query_keys"; import { IndexersSchemaQueryOptions } from "@api/queries"; import { SlideOver } from "@components/panels"; import Toast from "@components/notifications/Toast"; import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs"; import { SelectFieldBasic, SelectFieldCreatable } from "@components/inputs/select_wide"; import { FeedDownloadTypeOptions } from "@domain/constants"; -import { feedKeys } from "@screens/settings/Feed"; -import { indexerKeys } from "@screens/settings/Indexer"; import { DocsLink } from "@components/ExternalLink"; import * as common from "@components/inputs/common"; @@ -269,7 +268,7 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) { const mutation = useMutation({ mutationFn: (indexer: Indexer) => APIClient.indexers.create(indexer), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: indexerKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IndexerKeys.lists() }); toast.custom((t) => ); sleep(1500); @@ -287,7 +286,7 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) { const feedMutation = useMutation({ mutationFn: (feed: FeedCreate) => APIClient.feeds.create(feed), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: feedKeys.lists() }); + queryClient.invalidateQueries({ queryKey: FeedKeys.lists() }); } }); @@ -734,7 +733,7 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) { const mutation = useMutation({ mutationFn: (indexer: Indexer) => APIClient.indexers.update(indexer), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: indexerKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IndexerKeys.lists() }); toast.custom((t) => ); sleep(1500); @@ -751,7 +750,7 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) { const deleteMutation = useMutation({ mutationFn: (id: number) => APIClient.indexers.delete(id), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: indexerKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IndexerKeys.lists() }); toast.custom((t) => ); diff --git a/web/src/forms/settings/IrcForms.tsx b/web/src/forms/settings/IrcForms.tsx index faa51460fc..7b546c54fb 100644 --- a/web/src/forms/settings/IrcForms.tsx +++ b/web/src/forms/settings/IrcForms.tsx @@ -14,8 +14,8 @@ import Select from "react-select"; import { Dialog } from "@headlessui/react"; import { IrcAuthMechanismTypeOptions, OptionBasicTyped } from "@domain/constants"; -import { ircKeys } from "@screens/settings/Irc"; import { APIClient } from "@api/APIClient"; +import { IrcKeys } from "@api/query_keys"; import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs"; import { SlideOver } from "@components/panels"; import Toast from "@components/notifications/Toast"; @@ -132,7 +132,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) { const mutation = useMutation({ mutationFn: (network: IrcNetwork) => APIClient.irc.createNetwork(network), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ircKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IrcKeys.lists() }); toast.custom((t) => ); toggle(); @@ -288,7 +288,7 @@ export function IrcNetworkUpdateForm({ const updateMutation = useMutation({ mutationFn: (network: IrcNetwork) => APIClient.irc.updateNetwork(network), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ircKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IrcKeys.lists() }); toast.custom((t) => ); @@ -301,7 +301,7 @@ export function IrcNetworkUpdateForm({ const deleteMutation = useMutation({ mutationFn: (id: number) => APIClient.irc.deleteNetwork(id), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ircKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IrcKeys.lists() }); toast.custom((t) => ); diff --git a/web/src/forms/settings/NotificationForms.tsx b/web/src/forms/settings/NotificationForms.tsx index f8f57c5048..30b5a9f4ef 100644 --- a/web/src/forms/settings/NotificationForms.tsx +++ b/web/src/forms/settings/NotificationForms.tsx @@ -13,7 +13,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "react-hot-toast"; import { APIClient } from "@api/APIClient"; -import { notificationKeys } from "@screens/settings/Notifications"; +import { NotificationKeys } from "@api/query_keys"; import { EventOptions, NotificationTypeOptions, SelectOption } from "@domain/constants"; import { DEBUG } from "@components/debug"; import { SlideOver } from "@components/panels"; @@ -294,7 +294,7 @@ export function NotificationAddForm({ isOpen, toggle }: AddProps) { const createMutation = useMutation({ mutationFn: (notification: ServiceNotification) => APIClient.notifications.create(notification), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: notificationKeys.lists() }); + queryClient.invalidateQueries({ queryKey: NotificationKeys.lists() }); toast.custom((t) => ); toggle(); @@ -565,7 +565,7 @@ export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateP const mutation = useMutation({ mutationFn: (notification: ServiceNotification) => APIClient.notifications.update(notification), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: notificationKeys.lists() }); + queryClient.invalidateQueries({ queryKey: NotificationKeys.lists() }); toast.custom((t) => ); toggle(); @@ -577,7 +577,7 @@ export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateP const deleteMutation = useMutation({ mutationFn: (notificationID: number) => APIClient.notifications.delete(notificationID), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: notificationKeys.lists() }); + queryClient.invalidateQueries({ queryKey: NotificationKeys.lists() }); toast.custom((t) => ); } diff --git a/web/src/routes.tsx b/web/src/routes.tsx index 50769eeabb..52b9904fd5 100644 --- a/web/src/routes.tsx +++ b/web/src/routes.tsx @@ -7,7 +7,8 @@ import { createRootRouteWithContext, createRoute, createRouter, - ErrorComponent, notFound, + ErrorComponent, + notFound, Outlet, redirect, } from "@tanstack/react-router"; @@ -19,7 +20,7 @@ import { APIClient } from "@api/APIClient"; import { Login, Onboarding } from "@screens/auth"; import ReleaseSettings from "@screens/settings/Releases"; import { NotFound } from "@components/alerts/NotFound"; -import { FilterDetails, Filters } from "@screens/filters"; +import { FilterDetails, FilterNotFound, Filters } from "@screens/filters"; import { Settings } from "@screens/Settings"; import { ApikeysQueryOptions, @@ -45,12 +46,11 @@ import DownloadClientSettings from "@screens/settings/DownloadClient"; import FeedSettings from "@screens/settings/Feed"; import { Dashboard } from "@screens/Dashboard"; import AccountSettings from "@screens/settings/Account"; -import { SettingsContext } from "@utils/Context"; +import { AuthContext, AuthCtx, localStorageUserKey, SettingsContext } from "@utils/Context"; import { TanStackRouterDevtools } from "@tanstack/router-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { queryClient } from "@api/QueryClient"; import { LogDebug } from "@components/debug"; -import { FilterNotFound } from "@screens/filters"; const DashboardRoute = createRoute({ getParentRoute: () => AuthIndexRoute, @@ -277,20 +277,11 @@ export const LoginRoute = createRoute({ }, }).update({component: Login}); -export type AuthCtx = { - isLoggedIn: boolean - username?: string - login: (username: string) => void - logout: () => void -} - -const localStorageUserKey = "autobrr_user_auth" - export const AuthRoute = createRoute({ getParentRoute: () => RootRoute, id: 'auth', // Before loading, authenticate the user via our auth context - // This will also happen during prefetching (e.g. hovering over links, etc) + // This will also happen during prefetching (e.g. hovering over links, etc.) beforeLoad: ({context, location}) => { // If the user is not logged in, check for item in localStorage if (!context.auth.isLoggedIn) { @@ -393,19 +384,3 @@ export const Router = createRouter({ }, }); -export const AuthContext: AuthCtx = { - isLoggedIn: false, - username: undefined, - login: (username: string) => { - AuthContext.isLoggedIn = true - AuthContext.username = username - - localStorage.setItem(localStorageUserKey, JSON.stringify(AuthContext)); - }, - logout: () => { - AuthContext.isLoggedIn = false - AuthContext.username = undefined - - localStorage.removeItem(localStorageUserKey); - }, -} \ No newline at end of file diff --git a/web/src/screens/Settings.tsx b/web/src/screens/Settings.tsx index 32cb43409e..35421302ca 100644 --- a/web/src/screens/Settings.tsx +++ b/web/src/screens/Settings.tsx @@ -18,13 +18,6 @@ import { Link, Outlet } from "@tanstack/react-router"; import { classNames } from "@utils"; -export const settingsKeys = { - all: ["settings"] as const, - updates: () => [...settingsKeys.all, "updates"] as const, - config: () => [...settingsKeys.all, "config"] as const, - lists: () => [...settingsKeys.all, "list"] as const, -}; - interface NavTabType { name: string; href: string; diff --git a/web/src/screens/filters/Details.tsx b/web/src/screens/filters/Details.tsx index 3fda43fbb6..d71aa4a36c 100644 --- a/web/src/screens/filters/Details.tsx +++ b/web/src/screens/filters/Details.tsx @@ -13,6 +13,8 @@ import { toFormikValidationSchema } from "zod-formik-adapter"; import { ChevronRightIcon } from "@heroicons/react/24/solid"; import { APIClient } from "@api/APIClient"; +import { FilterByIdQueryOptions } from "@api/queries"; +import { FilterKeys } from "@api/query_keys"; import { useToggle } from "@hooks/hooks"; import { classNames } from "@utils"; import { DOWNLOAD_CLIENTS } from "@domain/constants"; @@ -21,9 +23,7 @@ import { DEBUG } from "@components/debug"; import Toast from "@components/notifications/Toast"; import { DeleteModal } from "@components/modals"; -import { filterKeys } from "./List"; import { Link, Outlet, useNavigate } from "@tanstack/react-router"; -import { FilterByIdQueryOptions } from "@api/queries"; import { FilterGetByIdRoute } from "@app/routes"; interface tabType { @@ -303,9 +303,9 @@ export const FilterDetails = () => { const updateMutation = useMutation({ mutationFn: (filter: Filter) => APIClient.filters.update(filter), onSuccess: (newFilter, variables) => { - queryClient.setQueryData(filterKeys.detail(variables.id), newFilter); + queryClient.setQueryData(FilterKeys.detail(variables.id), newFilter); - queryClient.setQueryData(filterKeys.lists(), (previous) => { + queryClient.setQueryData(FilterKeys.lists(), (previous) => { if (previous) { return previous.map((filter: Filter) => (filter.id === variables.id ? newFilter : filter)); } @@ -321,8 +321,8 @@ export const FilterDetails = () => { mutationFn: (id: number) => APIClient.filters.delete(id), onSuccess: () => { // Invalidate filters just in case, most likely not necessary but can't hurt. - queryClient.invalidateQueries({ queryKey: filterKeys.lists() }); - queryClient.removeQueries({ queryKey: filterKeys.detail(params.filterId) }); + queryClient.invalidateQueries({ queryKey: FilterKeys.lists() }); + queryClient.removeQueries({ queryKey: FilterKeys.detail(params.filterId) }); toast.custom((t) => ( diff --git a/web/src/screens/filters/Importer.tsx b/web/src/screens/filters/Importer.tsx index 1fdf2b7e37..9db8e2e584 100644 --- a/web/src/screens/filters/Importer.tsx +++ b/web/src/screens/filters/Importer.tsx @@ -4,9 +4,9 @@ import { useQueryClient } from "@tanstack/react-query"; import toast from "react-hot-toast"; import { APIClient } from "@api/APIClient"; +import { FilterKeys } from "@api/query_keys"; import Toast from "@components/notifications/Toast"; -import { filterKeys } from "./List"; import { AutodlIrssiConfigParser } from "./_configParser"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; @@ -211,7 +211,7 @@ export const Importer = ({ } finally { setIsOpen(false); // Invalidate filter cache, and trigger refresh request - await queryClient.invalidateQueries({ queryKey: filterKeys.lists() }); + await queryClient.invalidateQueries({ queryKey: FilterKeys.lists() }); } }; diff --git a/web/src/screens/filters/List.tsx b/web/src/screens/filters/List.tsx index c087f959ca..f0eced4a64 100644 --- a/web/src/screens/filters/List.tsx +++ b/web/src/screens/filters/List.tsx @@ -3,24 +3,23 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { Dispatch, FC, Fragment, MouseEventHandler, useReducer, useRef, useState, useEffect } from "react"; +import { Dispatch, FC, Fragment, MouseEventHandler, useCallback, useEffect, useReducer, useRef, useState } from "react"; import { Link } from '@tanstack/react-router' import { toast } from "react-hot-toast"; import { Listbox, Menu, Transition } from "@headlessui/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { FormikValues } from "formik"; -import { useCallback } from "react"; import { ArrowsRightLeftIcon, + ArrowUpOnSquareIcon, + ChatBubbleBottomCenterTextIcon, CheckIcon, ChevronDownIcon, - PlusIcon, DocumentDuplicateIcon, EllipsisHorizontalIcon, PencilSquareIcon, - ChatBubbleBottomCenterTextIcon, - TrashIcon, - ArrowUpOnSquareIcon + PlusIcon, + TrashIcon } from "@heroicons/react/24/outline"; import { ArrowDownTrayIcon } from "@heroicons/react/24/solid"; @@ -29,6 +28,7 @@ import { classNames } from "@utils"; import { FilterAddForm } from "@forms"; import { useToggle } from "@hooks/hooks"; import { APIClient } from "@api/APIClient"; +import { FilterKeys } from "@api/query_keys"; import { FiltersQueryOptions, IndexersOptionsQueryOptions } from "@api/queries"; import Toast from "@components/notifications/Toast"; import { EmptyListState } from "@components/emptystates"; @@ -38,14 +38,6 @@ import { Importer } from "./Importer"; import { Tooltip } from "@components/tooltips/Tooltip"; import { Checkbox } from "@components/Checkbox"; -export const filterKeys = { - all: ["filters"] as const, - lists: () => [...filterKeys.all, "list"] as const, - list: (indexers: string[], sortOrder: string) => [...filterKeys.lists(), { indexers, sortOrder }] as const, - details: () => [...filterKeys.all, "detail"] as const, - detail: (id: number) => [...filterKeys.details(), id] as const -}; - enum ActionType { INDEXER_FILTER_CHANGE = "INDEXER_FILTER_CHANGE", INDEXER_FILTER_RESET = "INDEXER_FILTER_RESET", @@ -404,8 +396,8 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => { const deleteMutation = useMutation({ mutationFn: (id: number) => APIClient.filters.delete(id), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: filterKeys.lists() }); - queryClient.invalidateQueries({ queryKey: filterKeys.detail(filter.id) }); + queryClient.invalidateQueries({ queryKey: FilterKeys.lists() }); + queryClient.invalidateQueries({ queryKey: FilterKeys.detail(filter.id) }); toast.custom((t) => ); } @@ -414,7 +406,7 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => { const duplicateMutation = useMutation({ mutationFn: (id: number) => APIClient.filters.duplicate(id), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: filterKeys.lists() }); + queryClient.invalidateQueries({ queryKey: FilterKeys.lists() }); toast.custom((t) => ); } @@ -601,8 +593,8 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) { // We need to invalidate both keys here. // The filters key is used on the /filters page, // while the ["filter", filter.id] key is used on the details page. - queryClient.invalidateQueries({ queryKey: filterKeys.lists() }); - queryClient.invalidateQueries({ queryKey: filterKeys.detail(filter.id) }); + queryClient.invalidateQueries({ queryKey: FilterKeys.lists() }); + queryClient.invalidateQueries({ queryKey: FilterKeys.detail(filter.id) }); } }); diff --git a/web/src/screens/releases/ReleaseTable.tsx b/web/src/screens/releases/ReleaseTable.tsx index 722093a4ed..c5e206e7e9 100644 --- a/web/src/screens/releases/ReleaseTable.tsx +++ b/web/src/screens/releases/ReleaseTable.tsx @@ -17,7 +17,7 @@ import { } from "@heroicons/react/24/solid"; import { ReleasesIndexRoute } from "@app/routes"; -import {ReleasesListQueryOptions} from "@api/queries"; +import { ReleasesListQueryOptions } from "@api/queries"; import { RandomLinuxIsos } from "@utils"; import * as Icons from "@components/Icons"; @@ -26,17 +26,6 @@ import * as DataTable from "@components/data-table"; import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./ReleaseFilters"; -export const releaseKeys = { - all: ["releases"] as const, - lists: () => [...releaseKeys.all, "list"] as const, - list: (pageIndex: number, pageSize: number, filters: ReleaseFilter[]) => [...releaseKeys.lists(), { pageIndex, pageSize, filters }] as const, - details: () => [...releaseKeys.all, "detail"] as const, - detail: (id: number) => [...releaseKeys.details(), id] as const, - indexers: () => [...releaseKeys.all, "indexers"] as const, - stats: () => [...releaseKeys.all, "stats"] as const, - latestActivity: () => [...releaseKeys.all, "latest-activity"] as const, -}; - type TableState = { queryPageIndex: number; queryPageSize: number; diff --git a/web/src/screens/settings/Account.tsx b/web/src/screens/settings/Account.tsx index 302084a943..8f5f10bda1 100644 --- a/web/src/screens/settings/Account.tsx +++ b/web/src/screens/settings/Account.tsx @@ -8,13 +8,13 @@ import { Form, Formik } from "formik"; import toast from "react-hot-toast"; import { UserIcon } from "@heroicons/react/24/solid"; +import { SettingsAccountRoute } from "@app/routes"; +import { AuthContext } from "@utils/Context"; import { APIClient } from "@api/APIClient"; import { Section } from "./_components"; import { PasswordField, TextField } from "@components/inputs"; import Toast from "@components/notifications/Toast"; -import { AuthContext, SettingsAccountRoute } from "@app/routes"; - const AccountSettings = () => (
[...apiKeys.all, "list"] as const, - details: () => [...apiKeys.all, "detail"] as const, - detail: (id: string) => [...apiKeys.details(), id] as const -}; function APISettings() { const [addFormIsOpen, toggleAddForm] = useToggle(false); @@ -88,8 +82,8 @@ function APIListItem({ apikey }: ApiKeyItemProps) { const deleteMutation = useMutation({ mutationFn: (key: string) => APIClient.apikeys.delete(key), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: apiKeys.lists() }); - queryClient.invalidateQueries({ queryKey: apiKeys.detail(apikey.key) }); + queryClient.invalidateQueries({ queryKey: ApiKeys.lists() }); + queryClient.invalidateQueries({ queryKey: ApiKeys.detail(apikey.key) }); toast.custom((t) => ( { - queryClient.invalidateQueries({ queryKey: settingsKeys.updates() }); + queryClient.invalidateQueries({ queryKey: SettingsKeys.updates() }); } }); @@ -45,7 +44,7 @@ function ApplicationSettings() { mutationFn: (value: boolean) => APIClient.config.update({ check_for_updates: value }).then(() => value), onSuccess: (_, value: boolean) => { toast.custom(t => ); - queryClient.invalidateQueries({ queryKey: settingsKeys.config() }); + queryClient.invalidateQueries({ queryKey: SettingsKeys.config() }); checkUpdateMutation.mutate(); } }); diff --git a/web/src/screens/settings/DownloadClient.tsx b/web/src/screens/settings/DownloadClient.tsx index 4403964f54..033ee410cc 100644 --- a/web/src/screens/settings/DownloadClient.tsx +++ b/web/src/screens/settings/DownloadClient.tsx @@ -3,8 +3,8 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { useState, useMemo } from "react"; -import {useMutation, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; +import { useMemo, useState } from "react"; +import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { PlusIcon } from "@heroicons/react/24/solid"; import toast from "react-hot-toast"; @@ -12,6 +12,7 @@ import { useToggle } from "@hooks/hooks"; import { DownloadClientAddForm, DownloadClientUpdateForm } from "@forms"; import { EmptySimple } from "@components/emptystates"; import { APIClient } from "@api/APIClient"; +import { DownloadClientKeys } from "@api/query_keys"; import { DownloadClientsQueryOptions } from "@api/queries"; import { ActionTypeNameMap } from "@domain/constants"; import Toast from "@components/notifications/Toast"; @@ -19,14 +20,6 @@ import { Checkbox } from "@components/Checkbox"; import { Section } from "./_components"; -export const clientKeys = { - all: ["download_clients"] as const, - lists: () => [...clientKeys.all, "list"] as const, - // list: (indexers: string[], sortOrder: string) => [...clientKeys.lists(), { indexers, sortOrder }] as const, - details: () => [...clientKeys.all, "detail"] as const, - detail: (id: number) => [...clientKeys.details(), id] as const -}; - interface DLSettingsItemProps { client: DownloadClient; } @@ -98,7 +91,7 @@ function ListItem({ client }: DLSettingsItemProps) { mutationFn: (client: DownloadClient) => APIClient.download_clients.update(client).then(() => client), onSuccess: (client: DownloadClient) => { toast.custom(t => ); - queryClient.invalidateQueries({ queryKey: clientKeys.lists() }); + queryClient.invalidateQueries({ queryKey: DownloadClientKeys.lists() }); } }); diff --git a/web/src/screens/settings/Feed.tsx b/web/src/screens/settings/Feed.tsx index 98e6795a45..113d469fe1 100644 --- a/web/src/screens/settings/Feed.tsx +++ b/web/src/screens/settings/Feed.tsx @@ -3,21 +3,22 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { Fragment, useRef, useState, useMemo } from "react"; -import {useMutation, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; +import { Fragment, useMemo, useRef, useState } from "react"; +import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { Menu, Transition } from "@headlessui/react"; import { toast } from "react-hot-toast"; import { ArrowsRightLeftIcon, DocumentTextIcon, EllipsisHorizontalIcon, - PencilSquareIcon, ForwardIcon, + PencilSquareIcon, TrashIcon } from "@heroicons/react/24/outline"; import { APIClient } from "@api/APIClient"; import { FeedsQueryOptions } from "@api/queries"; +import { FeedKeys } from "@api/query_keys"; import { useToggle } from "@hooks/hooks"; import { baseUrl, classNames, IsEmptyDate, simplifyDate } from "@utils"; import Toast from "@components/notifications/Toast"; @@ -30,14 +31,6 @@ import { ExternalLink } from "@components/ExternalLink"; import { Section } from "./_components"; import { Checkbox } from "@components/Checkbox"; -export const feedKeys = { - all: ["feeds"] as const, - lists: () => [...feedKeys.all, "list"] as const, - // list: (indexers: string[], sortOrder: string) => [...feedKeys.lists(), { indexers, sortOrder }] as const, - details: () => [...feedKeys.all, "detail"] as const, - detail: (id: number) => [...feedKeys.details(), id] as const -}; - interface SortConfig { key: keyof ListItemProps["feed"] | "enabled"; direction: "ascending" | "descending"; @@ -160,8 +153,8 @@ function ListItem({ feed }: ListItemProps) { const updateMutation = useMutation({ mutationFn: (status: boolean) => APIClient.feeds.toggleEnable(feed.id, status), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: feedKeys.lists() }); - queryClient.invalidateQueries({ queryKey: feedKeys.detail(feed.id) }); + queryClient.invalidateQueries({ queryKey: FeedKeys.lists() }); + queryClient.invalidateQueries({ queryKey: FeedKeys.detail(feed.id) }); toast.custom((t) => ); } @@ -237,8 +230,8 @@ const FeedItemDropdown = ({ const deleteMutation = useMutation({ mutationFn: (id: number) => APIClient.feeds.delete(id), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: feedKeys.lists() }); - queryClient.invalidateQueries({ queryKey: feedKeys.detail(feed.id) }); + queryClient.invalidateQueries({ queryKey: FeedKeys.lists() }); + queryClient.invalidateQueries({ queryKey: FeedKeys.detail(feed.id) }); toast.custom((t) => ); } @@ -254,7 +247,7 @@ const FeedItemDropdown = ({ const forceRunMutation = useMutation({ mutationFn: (id: number) => APIClient.feeds.forceRun(id), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: feedKeys.lists() }); + queryClient.invalidateQueries({ queryKey: FeedKeys.lists() }); toast.custom((t) => ); toggleForceRunModal(); }, diff --git a/web/src/screens/settings/Indexer.tsx b/web/src/screens/settings/Indexer.tsx index 31b4e50935..53c6207dfd 100644 --- a/web/src/screens/settings/Indexer.tsx +++ b/web/src/screens/settings/Indexer.tsx @@ -3,13 +3,14 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { useState, useMemo } from "react"; +import { useMemo, useState } from "react"; import toast from "react-hot-toast"; import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { PlusIcon } from "@heroicons/react/24/solid"; import { useToggle } from "@hooks/hooks"; import { APIClient } from "@api/APIClient"; +import { IndexerKeys } from "@api/query_keys"; import { IndexersQueryOptions } from "@api/queries"; import { Checkbox } from "@components/Checkbox"; import Toast from "@components/notifications/Toast"; @@ -19,16 +20,6 @@ import { componentMapType } from "@forms/settings/DownloadClientForms"; import { Section } from "./_components"; -export const indexerKeys = { - all: ["indexers"] as const, - schema: () => [...indexerKeys.all, "indexer-definitions"] as const, - options: () => [...indexerKeys.all, "options"] as const, - lists: () => [...indexerKeys.all, "list"] as const, - // list: (indexers: string[], sortOrder: string) => [...indexerKeys.lists(), { indexers, sortOrder }] as const, - details: () => [...indexerKeys.all, "detail"] as const, - detail: (id: number) => [...indexerKeys.details(), id] as const -}; - interface SortConfig { key: keyof ListItemProps["indexer"] | "enabled"; direction: "ascending" | "descending"; @@ -126,7 +117,7 @@ const ListItem = ({ indexer }: ListItemProps) => { const updateMutation = useMutation({ mutationFn: (enabled: boolean) => APIClient.indexers.toggleEnable(indexer.id, enabled), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: indexerKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IndexerKeys.lists() }); toast.custom((t) => ); } }); diff --git a/web/src/screens/settings/Irc.tsx b/web/src/screens/settings/Irc.tsx index 9b7ecb3d36..d2f1714de4 100644 --- a/web/src/screens/settings/Irc.tsx +++ b/web/src/screens/settings/Irc.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { Fragment, useRef, useState, useMemo, useEffect, MouseEvent } from "react"; +import { Fragment, MouseEvent, useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { LockClosedIcon, LockOpenIcon, PlusIcon } from "@heroicons/react/24/solid"; import { Menu, Transition } from "@headlessui/react"; @@ -22,6 +22,7 @@ import { classNames, IsEmptyDate, simplifyDate } from "@utils"; import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "@forms"; import { useToggle } from "@hooks/hooks"; import { APIClient } from "@api/APIClient"; +import { IrcKeys } from "@api/query_keys"; import { IrcQueryOptions } from "@api/queries"; import { EmptySimple } from "@components/emptystates"; import { DeleteModal } from "@components/modals"; @@ -31,14 +32,6 @@ import { Checkbox } from "@components/Checkbox"; import { Section } from "./_components"; -export const ircKeys = { - all: ["irc_networks"] as const, - lists: () => [...ircKeys.all, "list"] as const, - // list: (indexers: string[], sortOrder: string) => [...ircKeys.lists(), { indexers, sortOrder }] as const, - details: () => [...ircKeys.all, "detail"] as const, - detail: (id: number) => [...ircKeys.details(), id] as const -}; - interface SortConfig { key: keyof ListItemProps["network"] | "enabled"; direction: "ascending" | "descending"; @@ -213,7 +206,7 @@ const ListItem = ({ network, expanded }: ListItemProps) => { const updateMutation = useMutation({ mutationFn: (network: IrcNetwork) => APIClient.irc.updateNetwork(network).then(() => network), onSuccess: (network: IrcNetwork) => { - queryClient.invalidateQueries({ queryKey: ircKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IrcKeys.lists() }); toast.custom(t => ); } }); @@ -426,8 +419,8 @@ const ListItemDropdown = ({ const deleteMutation = useMutation({ mutationFn: (id: number) => APIClient.irc.deleteNetwork(id), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ircKeys.lists() }); - queryClient.invalidateQueries({ queryKey: ircKeys.detail(network.id) }); + queryClient.invalidateQueries({ queryKey: IrcKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IrcKeys.detail(network.id) }); toast.custom((t) => ); @@ -438,8 +431,8 @@ const ListItemDropdown = ({ const restartMutation = useMutation({ mutationFn: (id: number) => APIClient.irc.restartNetwork(id), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ircKeys.lists() }); - queryClient.invalidateQueries({ queryKey: ircKeys.detail(network.id) }); + queryClient.invalidateQueries({ queryKey: IrcKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IrcKeys.detail(network.id) }); toast.custom((t) => ); } diff --git a/web/src/screens/settings/Notifications.tsx b/web/src/screens/settings/Notifications.tsx index 37204c757d..ac8edd8bfd 100644 --- a/web/src/screens/settings/Notifications.tsx +++ b/web/src/screens/settings/Notifications.tsx @@ -3,27 +3,29 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import {useMutation, useQueryClient, useSuspenseQuery} from "@tanstack/react-query"; +import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import { PlusIcon } from "@heroicons/react/24/solid"; +import toast from "react-hot-toast"; import { APIClient } from "@api/APIClient"; +import { NotificationKeys } from "@api/query_keys"; import { NotificationsQueryOptions } from "@api/queries"; import { EmptySimple } from "@components/emptystates"; import { useToggle } from "@hooks/hooks"; import { NotificationAddForm, NotificationUpdateForm } from "@forms/settings/NotificationForms"; import { componentMapType } from "@forms/settings/DownloadClientForms"; import Toast from "@components/notifications/Toast"; -import toast from "react-hot-toast"; -import { Section } from "./_components"; -import { PlusIcon } from "@heroicons/react/24/solid"; +import { + DiscordIcon, + GotifyIcon, + LunaSeaIcon, + NotifiarrIcon, + NtfyIcon, + PushoverIcon, + Section, + TelegramIcon +} from "./_components"; import { Checkbox } from "@components/Checkbox"; -import { DiscordIcon, GotifyIcon, LunaSeaIcon, NotifiarrIcon, NtfyIcon, PushoverIcon, TelegramIcon } from "./_components"; - -export const notificationKeys = { - all: ["notifications"] as const, - lists: () => [...notificationKeys.all, "list"] as const, - details: () => [...notificationKeys.all, "detail"] as const, - detail: (id: number) => [...notificationKeys.details(), id] as const -}; function NotificationSettings() { const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false); @@ -90,7 +92,7 @@ function ListItem({ notification }: ListItemProps) { mutationFn: (notification: ServiceNotification) => APIClient.notifications.update(notification).then(() => notification), onSuccess: (notification: ServiceNotification) => { toast.custom(t => ); - queryClient.invalidateQueries({ queryKey: notificationKeys.lists() }); + queryClient.invalidateQueries({ queryKey: NotificationKeys.lists() }); } }); diff --git a/web/src/screens/settings/Releases.tsx b/web/src/screens/settings/Releases.tsx index 738ad7fffe..93e57cd35b 100644 --- a/web/src/screens/settings/Releases.tsx +++ b/web/src/screens/settings/Releases.tsx @@ -8,8 +8,8 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "react-hot-toast"; import { APIClient } from "@api/APIClient"; +import { ReleaseKeys } from "@api/query_keys"; import Toast from "@components/notifications/Toast"; -import { releaseKeys } from "@screens/releases/ReleaseTable"; import { useToggle } from "@hooks/hooks"; import { DeleteModal } from "@components/modals"; import { Section } from "./_components"; @@ -74,7 +74,7 @@ function DeleteReleases() { } // Invalidate filters just in case, most likely not necessary but can't hurt. - queryClient.invalidateQueries({ queryKey: releaseKeys.lists() }); + queryClient.invalidateQueries({ queryKey: ReleaseKeys.lists() }); } }); diff --git a/web/src/utils/Context.ts b/web/src/utils/Context.ts index f57905a4c2..4d01c36ca1 100644 --- a/web/src/utils/Context.ts +++ b/web/src/utils/Context.ts @@ -3,13 +3,8 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { newRidgeState } from "react-ridge-state"; import type { StateWithValue } from "react-ridge-state"; - -interface AuthInfo { - username: string; - isLoggedIn: boolean; -} +import { newRidgeState } from "react-ridge-state"; interface SettingsType { debug: boolean; @@ -26,11 +21,16 @@ export type FilterListState = { status: string; }; +// interface AuthInfo { +// username: string; +// isLoggedIn: boolean; +// } + // Default values -const AuthContextDefaults: AuthInfo = { - username: "", - isLoggedIn: false -}; +// const AuthContextDefaults: AuthInfo = { +// username: "", +// isLoggedIn: false +// }; const SettingsContextDefaults: SettingsType = { debug: false, @@ -101,9 +101,9 @@ function DefaultSetter(name: string, newState: T, prevState: T) { } } -export const AuthContext = newRidgeState(AuthContextDefaults, { - onSet: (newState, prevState) => DefaultSetter("auth", newState, prevState) -}); +// export const AuthContext = newRidgeState(AuthContextDefaults, { +// onSet: (newState, prevState) => DefaultSetter("auth", newState, prevState) +// }); export const SettingsContext = newRidgeState( SettingsContextDefaults, @@ -121,3 +121,29 @@ export const FilterListContext = newRidgeState( onSet: (newState, prevState) => DefaultSetter(FilterListKey, newState, prevState) } ); + +export type AuthCtx = { + isLoggedIn: boolean + username?: string + login: (username: string) => void + logout: () => void +} + +export const localStorageUserKey = "autobrr_user_auth" + +export const AuthContext: AuthCtx = { + isLoggedIn: false, + username: undefined, + login: (username: string) => { + AuthContext.isLoggedIn = true + AuthContext.username = username + + localStorage.setItem(localStorageUserKey, JSON.stringify(AuthContext)); + }, + logout: () => { + AuthContext.isLoggedIn = false + AuthContext.username = undefined + + localStorage.removeItem(localStorageUserKey); + }, +} \ No newline at end of file From 2970df4c26f9911b9b5032c07916af6130893f9d Mon Sep 17 00:00:00 2001 From: ze0s Date: Sun, 11 Feb 2024 16:47:56 +0100 Subject: [PATCH 33/46] fix(queries): remove err --- web/src/api/QueryClient.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/api/QueryClient.tsx b/web/src/api/QueryClient.tsx index a5994039ee..1f804c0814 100644 --- a/web/src/api/QueryClient.tsx +++ b/web/src/api/QueryClient.tsx @@ -38,7 +38,6 @@ export const queryClient = new QueryClient({ retry: (failureCount, error) => { console.error(`retry count ${failureCount} error: ${error}`) - // @ts-expect-error TS2339: Ignore err. if (Object.hasOwnProperty.call(error, "status") && // @ts-expect-error TS2339: ignore HTTP_STATUS_TO_NOT_RETRY.includes(error.status) From 63b985dd4adbc110db92b252397d68288d89b5da Mon Sep 17 00:00:00 2001 From: ze0s Date: Sun, 11 Feb 2024 16:55:13 +0100 Subject: [PATCH 34/46] fix(routes): move zob schema inline --- web/src/routes.tsx | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/web/src/routes.tsx b/web/src/routes.tsx index 52b9904fd5..4b8b12c06e 100644 --- a/web/src/routes.tsx +++ b/web/src/routes.tsx @@ -139,23 +139,21 @@ const ReleasesRoute = createRoute({ path: 'releases' }); -export const releasesSearchSchema = z.object({ - offset: z.number().optional(), - limit: z.number().optional(), - filter: z.string().optional(), - q: z.string().optional(), - action_status: z.enum(['PUSH_APPROVED', 'PUSH_REJECTED', 'PUSH_ERROR', '']).optional(), - // filters: z.array().catch(''), - // sort: z.enum(['newest', 'oldest', 'price']).catch('newest'), -}); - // type ReleasesSearch = z.infer export const ReleasesIndexRoute = createRoute({ getParentRoute: () => ReleasesRoute, path: '/', component: Releases, - validateSearch: (search) => releasesSearchSchema.parse(search), + validateSearch: (search) => z.object({ + offset: z.number().optional(), + limit: z.number().optional(), + filter: z.string().optional(), + q: z.string().optional(), + action_status: z.enum(['PUSH_APPROVED', 'PUSH_REJECTED', 'PUSH_ERROR', '']).optional(), + // filters: z.array().catch(''), + // sort: z.enum(['newest', 'oldest', 'price']).catch('newest'), + }).parse(search), }); export const SettingsRoute = createRoute({ From 66042f219a083b3ba808f27faaca5a30f9048375 Mon Sep 17 00:00:00 2001 From: ze0s Date: Sun, 11 Feb 2024 18:07:48 +0100 Subject: [PATCH 35/46] fix(auth): middleware and redirect to login --- internal/http/middleware.go | 8 -------- web/src/api/APIClient.ts | 7 ------- web/src/api/QueryClient.tsx | 21 ++++++++------------- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/internal/http/middleware.go b/internal/http/middleware.go index 78a464dfe9..84fd6b6d1e 100644 --- a/internal/http/middleware.go +++ b/internal/http/middleware.go @@ -49,12 +49,6 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler { return } - if session.IsNew { - s.log.Warn().Msgf("session isNew: %+v", session) - http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent) - return - } - // Check if user is authenticated if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { s.log.Warn().Msg("session not authenticated") @@ -63,8 +57,6 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler { return } - s.log.Debug().Msgf("session ok: %+v", session) - ctx := context.WithValue(r.Context(), "session", session) r = r.WithContext(ctx) } diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index d42617427d..47f5450ea0 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -87,16 +87,10 @@ export async function HttpClient( return Promise.resolve({} as T); } case 401: { - // Remove auth info from localStorage - // auth.logout() - // AuthContext.reset(); return Promise.reject(response); // return Promise.reject(new Error(`[401] Unauthorized: "${endpoint}"`)); } case 403: { - // Remove auth info from localStorage - // AuthContext.reset(); - return Promise.reject(response); } case 404: { @@ -104,7 +98,6 @@ export async function HttpClient( const json = isJson ? await response.json() : null; return Promise.reject(json as T); // return Promise.reject(new Error(`[404] Not Found: "${endpoint}"`)); - // return Promise.reject(response); } case 500: { const health = await window.fetch(`${baseUrl()}api/healthz/liveness`); diff --git a/web/src/api/QueryClient.tsx b/web/src/api/QueryClient.tsx index 1f804c0814..0c782b4241 100644 --- a/web/src/api/QueryClient.tsx +++ b/web/src/api/QueryClient.tsx @@ -19,7 +19,7 @@ export const queryClient = new QueryClient({ // @ts-expect-error TS2339: Property status does not exist on type Error if (error?.status === 401 || error?.status === 403) { - // @ts-expect-error TS2339: Property status does not exist on type Error + // @ts-expect-error TS2339: Property status does not exist on type Error console.error("bad status, redirect to login", error?.status) // Redirect to login page window.location.href = "/login"; @@ -33,25 +33,20 @@ export const queryClient = new QueryClient({ // The retries will have exponential delay. // See https://tanstack.com/query/v4/docs/guides/query-retries#retry-delay // delay = Math.min(1000 * 2 ** attemptIndex, 30000) - // retry: true, - // throwOnError: true, + // retry: false, + throwOnError: true, retry: (failureCount, error) => { - console.error(`retry count ${failureCount} error: ${error}`) + console.debug("retry count:", failureCount) + console.error("retry err: ", error) - if (Object.hasOwnProperty.call(error, "status") && - // @ts-expect-error TS2339: ignore - HTTP_STATUS_TO_NOT_RETRY.includes(error.status) - ) { + // @ts-expect-error TS2339: ignore + if (HTTP_STATUS_TO_NOT_RETRY.includes(error.status)) { // @ts-expect-error TS2339: ignore console.log(`retry: Aborting retry due to ${error.status} status`); return false; } - if (failureCount > MAX_RETRIES) { - return false; - } - - return true; + return failureCount <= MAX_RETRIES; }, }, mutations: { From a9618350261d70b1c3c4957c6a94d8d9a28fd324 Mon Sep 17 00:00:00 2001 From: ze0s Date: Sun, 11 Feb 2024 18:11:17 +0100 Subject: [PATCH 36/46] fix(auth): failing test --- internal/http/auth_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/http/auth_test.go b/internal/http/auth_test.go index 9c4d9e5a48..03bfa36362 100644 --- a/internal/http/auth_test.go +++ b/internal/http/auth_test.go @@ -267,7 +267,7 @@ func TestAuthHandlerValidateBad(t *testing.T) { defer resp.Body.Close() - assert.Equalf(t, http.StatusNoContent, resp.StatusCode, "validate handler: unexpected http status") + assert.Equalf(t, http.StatusForbidden, resp.StatusCode, "validate handler: unexpected http status") } func TestAuthHandlerLoginBad(t *testing.T) { From 03733231b10535c89b512ca8e381c090337dabd5 Mon Sep 17 00:00:00 2001 From: ze0s Date: Sun, 11 Feb 2024 18:27:55 +0100 Subject: [PATCH 37/46] fix(logs): invalidate correct key --- web/src/screens/settings/Logs.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/screens/settings/Logs.tsx b/web/src/screens/settings/Logs.tsx index 6d42f68d0e..396cd118cd 100644 --- a/web/src/screens/settings/Logs.tsx +++ b/web/src/screens/settings/Logs.tsx @@ -17,6 +17,7 @@ import { LogLevelOptions, SelectOption } from "@domain/constants"; import { Section, RowItem } from "./_components"; import * as common from "@components/inputs/common"; import { LogFiles } from "@screens/Logs"; +import { SettingsKeys } from "@api/query_keys.ts"; type SelectWrapperProps = { id: string; @@ -70,7 +71,7 @@ function LogSettings() { onSuccess: () => { toast.custom((t) => ); - queryClient.invalidateQueries({ queryKey: ["config"] }); + queryClient.invalidateQueries({ queryKey: SettingsKeys.config() }); } }); From 26ae66ca0e67fe7a30197ea551a0baa1dc7d4f9d Mon Sep 17 00:00:00 2001 From: ze0s Date: Sun, 11 Feb 2024 18:28:18 +0100 Subject: [PATCH 38/46] fix(logs): invalidate correct key --- web/src/screens/settings/Logs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/screens/settings/Logs.tsx b/web/src/screens/settings/Logs.tsx index 396cd118cd..0652f2aace 100644 --- a/web/src/screens/settings/Logs.tsx +++ b/web/src/screens/settings/Logs.tsx @@ -10,6 +10,7 @@ import Select from "react-select"; import { APIClient } from "@api/APIClient"; import { ConfigQueryOptions } from "@api/queries"; +import { SettingsKeys } from "@api/query_keys; import { SettingsLogRoute } from "@app/routes"; import Toast from "@components/notifications/Toast"; import { LogLevelOptions, SelectOption } from "@domain/constants"; @@ -17,7 +18,6 @@ import { LogLevelOptions, SelectOption } from "@domain/constants"; import { Section, RowItem } from "./_components"; import * as common from "@components/inputs/common"; import { LogFiles } from "@screens/Logs"; -import { SettingsKeys } from "@api/query_keys.ts"; type SelectWrapperProps = { id: string; From 815e13ace354549d717326bece81048954d4b6d2 Mon Sep 17 00:00:00 2001 From: ze0s Date: Sun, 11 Feb 2024 18:29:27 +0100 Subject: [PATCH 39/46] fix(logs): invalidate correct key --- web/src/screens/settings/Logs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/screens/settings/Logs.tsx b/web/src/screens/settings/Logs.tsx index 0652f2aace..9f3ad6616e 100644 --- a/web/src/screens/settings/Logs.tsx +++ b/web/src/screens/settings/Logs.tsx @@ -10,7 +10,7 @@ import Select from "react-select"; import { APIClient } from "@api/APIClient"; import { ConfigQueryOptions } from "@api/queries"; -import { SettingsKeys } from "@api/query_keys; +import { SettingsKeys } from "@api/query_keys"; import { SettingsLogRoute } from "@app/routes"; import Toast from "@components/notifications/Toast"; import { LogLevelOptions, SelectOption } from "@domain/constants"; From 404e736ae0c63c686ea239d4942661b005a20709 Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Sun, 11 Feb 2024 19:35:00 +0100 Subject: [PATCH 40/46] fix: JSX element stealing focus from searchbar --- web/src/screens/releases/ReleaseTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/screens/releases/ReleaseTable.tsx b/web/src/screens/releases/ReleaseTable.tsx index c5e206e7e9..9c291ece06 100644 --- a/web/src/screens/releases/ReleaseTable.tsx +++ b/web/src/screens/releases/ReleaseTable.tsx @@ -211,7 +211,7 @@ export const ReleaseTable = () => { if (isLoading) { return ( - <> +
{ headerGroups.map((headerGroup) => headerGroup.headers.map((column) => ( column.Filter ? ( @@ -236,7 +236,7 @@ export const ReleaseTable = () => {
- +
) } From 048b5008fe4842a3154f9c2edec3ea4e0b0fbe8b Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Sun, 11 Feb 2024 20:42:24 +0100 Subject: [PATCH 41/46] reimplement empty release table state text --- web/src/screens/releases/ReleaseTable.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/src/screens/releases/ReleaseTable.tsx b/web/src/screens/releases/ReleaseTable.tsx index 9c291ece06..1426067666 100644 --- a/web/src/screens/releases/ReleaseTable.tsx +++ b/web/src/screens/releases/ReleaseTable.tsx @@ -25,6 +25,7 @@ import { RingResizeSpinner } from "@components/Icons"; import * as DataTable from "@components/data-table"; import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./ReleaseFilters"; +import { EmptyListState } from "@components/emptystates"; type TableState = { queryPageIndex: number; @@ -240,6 +241,10 @@ export const ReleaseTable = () => { ) } + if (!page.length && filters.every(filter => !filter.value)) { + return ; + } + // Render the UI for your table return (
From 83fbc72055b19a1c793f7612f09a13e0b86688ba Mon Sep 17 00:00:00 2001 From: ze0s Date: Mon, 12 Feb 2024 09:16:16 +0100 Subject: [PATCH 42/46] fix(context): use deep-copy --- web/src/utils/Context.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/src/utils/Context.ts b/web/src/utils/Context.ts index 4d01c36ca1..1f66c37465 100644 --- a/web/src/utils/Context.ts +++ b/web/src/utils/Context.ts @@ -53,7 +53,7 @@ function ContextMerger( defaults: T, ctxState: StateWithValue ) { - let values = defaults; + let values = structuredClone(defaults); const storage = localStorage.getItem(key); if (storage) { @@ -62,13 +62,13 @@ function ContextMerger( if (json === null) { console.warn(`JSON localStorage value for '${key}' context state is null`); } else { - values = { ...defaults, ...json }; + values = { ...values, ...json }; } } catch (e) { console.error(`Failed to merge ${key} context state: ${e}`); } } - + ctxState.set(values); } @@ -76,7 +76,7 @@ const SettingsKey = "autobrr_settings"; const FilterListKey = "autobrr_filter_list"; export const InitializeGlobalContext = () => { - // ContextMerger("auth", AuthContextDefaults, AuthContext); + // ContextMerger(localStorageUserKey, AuthContextDefaults, AuthContextt); ContextMerger( SettingsKey, SettingsContextDefaults, @@ -101,8 +101,8 @@ function DefaultSetter(name: string, newState: T, prevState: T) { } } -// export const AuthContext = newRidgeState(AuthContextDefaults, { -// onSet: (newState, prevState) => DefaultSetter("auth", newState, prevState) +// export const AuthContextt = newRidgeState(AuthContextDefaults, { +// onSet: (newState, prevState) => DefaultSetter(localStorageUserKey, newState, prevState) // }); export const SettingsContext = newRidgeState( From 889408ace5e5e02af97e41d6c6d67c5af1dbeef0 Mon Sep 17 00:00:00 2001 From: ze0s Date: Mon, 12 Feb 2024 10:16:06 +0100 Subject: [PATCH 43/46] fix(releases): empty state and filter input warnings --- web/src/screens/releases/ReleaseFilters.tsx | 6 +- web/src/screens/releases/ReleaseTable.tsx | 216 +++++++++++--------- 2 files changed, 121 insertions(+), 101 deletions(-) diff --git a/web/src/screens/releases/ReleaseFilters.tsx b/web/src/screens/releases/ReleaseFilters.tsx index bcfd4690c4..7866b8bafc 100644 --- a/web/src/screens/releases/ReleaseFilters.tsx +++ b/web/src/screens/releases/ReleaseFilters.tsx @@ -54,7 +54,7 @@ const ListboxFilter = ({ - + {children} @@ -75,7 +75,7 @@ export const IndexerSelectColumnFilter = ({ id={id} key={id} label={filterValue ?? "Indexer"} - currentValue={filterValue} + currentValue={filterValue ?? ""} onChange={setFilter} > {isSuccess && data && data?.map((indexer, idx) => ( @@ -133,7 +133,7 @@ export const PushStatusSelectColumnFilter = ({ {PushStatusOptions.map((status, idx) => ( diff --git a/web/src/screens/releases/ReleaseTable.tsx b/web/src/screens/releases/ReleaseTable.tsx index 1426067666..74564f84ef 100644 --- a/web/src/screens/releases/ReleaseTable.tsx +++ b/web/src/screens/releases/ReleaseTable.tsx @@ -74,6 +74,25 @@ const TableReducer = (state: TableState, action: Actions): TableState => { } }; +const EmptyReleaseList = () => ( +
+ + + + + + +
+
+ +
+
+
+ +
+
+); + export const ReleaseTable = () => { const search = ReleasesIndexRoute.useSearch() @@ -241,10 +260,6 @@ export const ReleaseTable = () => { ) } - if (!page.length && filters.every(filter => !filter.value)) { - return ; - } - // Render the UI for your table return (
@@ -258,18 +273,21 @@ export const ReleaseTable = () => { )}
-
- - + {displayData.length === 0 + ? + : ( +
+
+ {headerGroups.map((headerGroup) => { - const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps(); + const {key: rowKey, ...rowRest} = headerGroup.getHeaderGroupProps(); return ( {headerGroup.headers.map((column) => { - const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps()); + const {key: columnKey, ...columnRest} = column.getHeaderProps(column.getSortByToggleProps()); return ( - // Add the sorting props to control sorting. For this example - // we can add them into the header props + // Add the sorting props to control sorting. For this example + // we can add them into the header props ); })} - - + + {page.map((row) => { prepareRow(row); - const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps(); + const {key: bodyRowKey, ...bodyRowRest} = row.getRowProps(); return ( {row.cells.map((cell) => { - const { key: cellRowKey, ...cellRowRest } = cell.getCellProps(); + const {key: cellRowKey, ...cellRowRest} = cell.getCellProps(); return ( ); })} - -
{ {column.isSorted ? ( column.isSortedDesc ? ( - + ) : ( - + ) ) : ( - + )} @@ -297,19 +315,19 @@ export const ReleaseTable = () => {
{
- {/* Pagination */} -
-
- previousPage()} disabled={!canPreviousPage}>Previous - nextPage()} disabled={!canNextPage}>Next -
-
-
+ + + {/* Pagination */} +
+
+ previousPage()} disabled={!canPreviousPage}>Previous + nextPage()} disabled={!canNextPage}>Next +
+
+
- Page {pageIndex + 1} of {pageOptions.length} + Page {pageIndex + 1} of {pageOptions.length} - -
-
- + +
+
+ +
+
+ +
-
- -
-
+ )}
); From e39c122459e84a0902bc69411c63bd8ef638db59 Mon Sep 17 00:00:00 2001 From: ze0s Date: Mon, 12 Feb 2024 10:31:50 +0100 Subject: [PATCH 44/46] fix(releases): empty states --- web/src/screens/dashboard/ActivityTable.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/web/src/screens/dashboard/ActivityTable.tsx b/web/src/screens/dashboard/ActivityTable.tsx index c0b2199f7e..4dc351a240 100644 --- a/web/src/screens/dashboard/ActivityTable.tsx +++ b/web/src/screens/dashboard/ActivityTable.tsx @@ -12,10 +12,10 @@ import { useSortBy, usePagination, FilterProps, Column } from "react-table"; +import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid"; import { EmptyListState } from "@components/emptystates"; import * as Icons from "@components/Icons"; -import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid"; import * as DataTable from "@components/data-table"; import { RandomLinuxIsos } from "@utils"; import { RingResizeSpinner } from "@components/Icons"; @@ -81,8 +81,14 @@ function Table({ columns, data }: TableProps) { usePagination ); - if (!page.length) { - return ; + if (data.length === 0) { + return ( +
+
+ +
+
+ ) } // Render the UI for your table From 30d04d920b665758bd4350b3094d18939c96b99a Mon Sep 17 00:00:00 2001 From: ze0s Date: Mon, 12 Feb 2024 11:18:43 +0100 Subject: [PATCH 45/46] fix(auth): onboarding --- web/src/routes.tsx | 21 +++++++-------------- web/src/screens/auth/Login.tsx | 1 - 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/web/src/routes.tsx b/web/src/routes.tsx index 4b8b12c06e..c1ceb7a68b 100644 --- a/web/src/routes.tsx +++ b/web/src/routes.tsx @@ -50,7 +50,6 @@ import { AuthContext, AuthCtx, localStorageUserKey, SettingsContext } from "@uti import { TanStackRouterDevtools } from "@tanstack/router-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { queryClient } from "@api/QueryClient"; -import { LogDebug } from "@components/debug"; const DashboardRoute = createRoute({ getParentRoute: () => AuthIndexRoute, @@ -261,17 +260,15 @@ export const LoginRoute = createRoute({ validateSearch: z.object({ redirect: z.string().optional(), }), - beforeLoad: async () => { + beforeLoad: ({ navigate}) => { // handle canOnboard - try { - await APIClient.auth.canOnboard() + APIClient.auth.canOnboard().then(() => { + console.info("onboarding available, redirecting") - redirect({ - to: OnboardRoute.to, - }) - } catch (e) { - console.log("onboarding not available") - } + navigate({ to: OnboardRoute.to }) + }).catch(() => { + console.info("onboarding not available, please login") + }) }, }).update({component: Login}); @@ -290,12 +287,8 @@ export const AuthRoute = createRoute({ if (json === null) { console.warn(`JSON localStorage value for '${localStorageUserKey}' context state is null`); } else { - LogDebug("auth local storage found", json) - context.auth.isLoggedIn = json.isLoggedIn context.auth.username = json.username - - LogDebug("auth ctx", context.auth) } } catch (e) { console.error(`auth Failed to merge ${localStorageUserKey} context state: ${e}`); diff --git a/web/src/screens/auth/Login.tsx b/web/src/screens/auth/Login.tsx index b4e77147fb..b13a0cd03c 100644 --- a/web/src/screens/auth/Login.tsx +++ b/web/src/screens/auth/Login.tsx @@ -42,7 +42,6 @@ export const Login = () => { const loginMutation = useMutation({ mutationFn: (data: LoginFormFields) => APIClient.auth.login(data.username, data.password), onSuccess: (_, variables: LoginFormFields) => { - console.log("login on success") auth.login(variables.username) router.invalidate() }, From b8b135a47a1f65655ab486b72317482f8f71efe6 Mon Sep 17 00:00:00 2001 From: ze0s Date: Mon, 12 Feb 2024 11:44:25 +0100 Subject: [PATCH 46/46] fix(cache): invalidate queries --- web/src/forms/settings/IndexerForms.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/forms/settings/IndexerForms.tsx b/web/src/forms/settings/IndexerForms.tsx index aeb5e68ce1..d8bb52acc3 100644 --- a/web/src/forms/settings/IndexerForms.tsx +++ b/web/src/forms/settings/IndexerForms.tsx @@ -15,7 +15,7 @@ import { Dialog, Transition } from "@headlessui/react"; import { classNames, sleep } from "@utils"; import { DEBUG } from "@components/debug"; import { APIClient } from "@api/APIClient"; -import { FeedKeys, IndexerKeys } from "@api/query_keys"; +import { FeedKeys, IndexerKeys, ReleaseKeys } from "@api/query_keys"; import { IndexersSchemaQueryOptions } from "@api/queries"; import { SlideOver } from "@components/panels"; import Toast from "@components/notifications/Toast"; @@ -269,6 +269,8 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) { mutationFn: (indexer: Indexer) => APIClient.indexers.create(indexer), onSuccess: () => { queryClient.invalidateQueries({ queryKey: IndexerKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IndexerKeys.options() }); + queryClient.invalidateQueries({ queryKey: ReleaseKeys.indexers() }); toast.custom((t) => ); sleep(1500); @@ -751,6 +753,8 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) { mutationFn: (id: number) => APIClient.indexers.delete(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: IndexerKeys.lists() }); + queryClient.invalidateQueries({ queryKey: IndexerKeys.options() }); + queryClient.invalidateQueries({ queryKey: ReleaseKeys.indexers() }); toast.custom((t) => );