From 1a23b69bcf20494266e369e10c4f825d96f4a12e Mon Sep 17 00:00:00 2001 From: martylukyy <35452459+martylukyy@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:07:00 +0100 Subject: [PATCH] feat(web): move from react-router to @tanstack/router (#1338) * fix(auth): invalid cookie handling and wrongful basic auth invalidation * fix(auth): fix test to reflect new HTTP status code * fix(auth/web): do not throw on error * fix(http): replace http codes in middleware to prevent basic auth invalidation fix typo in comment * fix test * fix(web): api client handle 403 * refactor(http): auth_test use testify.assert * refactor(http): set session opts after valid login * refactor(http): send more client headers * fix(http): test * refactor(web): move router to tanstack/router * refactor(web): use route loaders and suspense * refactor(web): useSuspense for settings * refactor(web): invalidate cookie in middleware * fix: loclfile * fix: load filter/id * fix(web): login, onboard, types, imports * fix(web): filter load * fix(web): build errors * fix(web): ts-expect-error * fix(tests): filter_test.go * fix(filters): tests * refactor: remove duplicate spinner components refactor: ReleaseTable.tsx loading animation refactor: remove dedicated `pendingComponent` for `settingsRoute` * fix: refactor missed SectionLoader to RingResizeSpinner * fix: substitute divides with borders to account for unloaded elements * fix(api): action status URL param * revert: action status URL param add comment * fix(routing): notfound handling and split files * fix(filters): notfound get params * fix(queries): colon * fix(queries): comments ts-ignore * fix(queries): extract queryKeys * fix(queries): remove err * fix(routes): move zob schema inline * fix(auth): middleware and redirect to login * fix(auth): failing test * fix(logs): invalidate correct key * fix(logs): invalidate correct key * fix(logs): invalidate correct key * fix: JSX element stealing focus from searchbar * reimplement empty release table state text * fix(context): use deep-copy * fix(releases): empty state and filter input warnings * fix(releases): empty states * fix(auth): onboarding * fix(cache): invalidate queries --------- Co-authored-by: ze0s <43699394+zze0s@users.noreply.github.com> --- internal/database/filter.go | 493 ++++------ internal/database/filter_test.go | 12 +- internal/domain/filter.go | 149 ++- internal/filter/service.go | 24 +- internal/http/auth_test.go | 33 +- internal/http/encoder.go | 10 + internal/http/filter.go | 2 +- internal/http/middleware.go | 8 +- web/package.json | 4 +- web/pnpm-lock.yaml | 209 ++-- web/src/App.tsx | 64 +- web/src/api/APIClient.ts | 21 +- web/src/api/QueryClient.tsx | 64 ++ web/src/api/queries.ts | 135 +++ web/src/api/query_keys.ts | 77 ++ web/src/components/SectionLoader.tsx | 32 - web/src/components/alerts/NotFound.tsx | 7 +- web/src/components/data-table/Cells.tsx | 8 +- web/src/components/debug.tsx | 8 + web/src/components/header/Header.tsx | 38 +- web/src/components/header/LeftNav.tsx | 36 +- web/src/components/header/MobileNav.tsx | 31 +- web/src/components/header/RightNav.tsx | 6 +- web/src/components/header/_shared.ts | 6 +- web/src/components/modals/index.tsx | 6 +- web/src/domain/routes.tsx | 67 -- web/src/forms/filters/FilterAddForm.tsx | 10 +- 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 | 23 +- web/src/forms/settings/IrcForms.tsx | 8 +- web/src/forms/settings/NotificationForms.tsx | 8 +- web/src/routes.tsx | 377 ++++++++ web/src/screens/Logs.tsx | 5 +- web/src/screens/Settings.tsx | 68 +- web/src/screens/auth/Login.tsx | 42 +- web/src/screens/auth/Onboarding.tsx | 4 +- web/src/screens/dashboard/ActivityTable.tsx | 119 ++- web/src/screens/dashboard/Stats.tsx | 50 +- web/src/screens/filters/Details.tsx | 110 +-- web/src/screens/filters/Importer.tsx | 4 +- web/src/screens/filters/List.tsx | 89 +- web/src/screens/filters/NotFound.tsx | 61 ++ web/src/screens/filters/index.ts | 1 + web/src/screens/filters/sections/Actions.tsx | 23 +- web/src/screens/filters/sections/Advanced.tsx | 900 +++++++++--------- web/src/screens/filters/sections/General.tsx | 18 +- web/src/screens/filters/sections/Music.tsx | 286 +++--- .../action_components/ActionQBittorrent.tsx | 2 +- .../{Filters.tsx => ReleaseFilters.tsx} | 19 +- web/src/screens/releases/ReleaseTable.tsx | 424 +++------ web/src/screens/settings/Account.tsx | 20 +- web/src/screens/settings/Api.tsx | 28 +- web/src/screens/settings/Application.tsx | 30 +- web/src/screens/settings/DownloadClient.tsx | 26 +- web/src/screens/settings/Feed.tsx | 34 +- web/src/screens/settings/Indexer.tsx | 30 +- web/src/screens/settings/Irc.tsx | 34 +- web/src/screens/settings/Logs.tsx | 35 +- web/src/screens/settings/Notifications.tsx | 38 +- web/src/screens/settings/Releases.tsx | 4 +- web/src/utils/Context.ts | 71 +- web/src/utils/index.ts | 16 +- 64 files changed, 2521 insertions(+), 2069 deletions(-) create mode 100644 web/src/api/QueryClient.tsx create mode 100644 web/src/api/queries.ts create mode 100644 web/src/api/query_keys.ts delete mode 100644 web/src/components/SectionLoader.tsx delete mode 100644 web/src/domain/routes.tsx create mode 100644 web/src/routes.tsx create mode 100644 web/src/screens/filters/NotFound.tsx rename web/src/screens/releases/{Filters.tsx => ReleaseFilters.tsx} (91%) diff --git a/internal/database/filter.go b/internal/database/filter.go index 2aa4c9b200..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,176 +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") + + 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 } @@ -517,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}). @@ -556,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 @@ -565,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, @@ -635,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 } @@ -1232,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 bb52b2f1eb..aa4a3da93c 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) { @@ -451,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) { diff --git a/internal/http/auth_test.go b/internal/http/auth_test.go index a5d74f1793..03bfa36362 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.StatusNoContent { - t.Errorf("validate: handler returned wrong status code: got %v want %v", status, http.StatusNoContent) - } + 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 403 Forbidden - 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") diff --git a/internal/http/encoder.go b/internal/http/encoder.go index ab73e86283..697ebafccb 100644 --- a/internal/http/encoder.go +++ b/internal/http/encoder.go @@ -67,6 +67,16 @@ func (e encoder) StatusNotFound(w http.ResponseWriter) { w.WriteHeader(http.StatusNotFound) } +func (e encoder) NotFoundErr(w http.ResponseWriter, err error) { + res := errorResponse{ + Message: err.Error(), + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(res) +} + func (e encoder) StatusInternalError(w http.ResponseWriter) { w.WriteHeader(http.StatusInternalServerError) } diff --git a/internal/http/filter.go b/internal/http/filter.go index 744dea60e2..b0473877ee 100644 --- a/internal/http/filter.go +++ b/internal/http/filter.go @@ -119,7 +119,7 @@ func (h filterHandler) getByID(w http.ResponseWriter, r *http.Request) { filter, err := h.service.FindByID(ctx, id) if err != nil { if errors.Is(err, domain.ErrRecordNotFound) { - h.encoder.StatusNotFound(w) + h.encoder.NotFoundErr(w, errors.New("filter with id %d not found", id)) return } diff --git a/internal/http/middleware.go b/internal/http/middleware.go index 340e5ad0d9..84fd6b6d1e 100644 --- a/internal/http/middleware.go +++ b/internal/http/middleware.go @@ -38,7 +38,6 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler { // MaxAge<0 means delete cookie immediately session.Options.MaxAge = -1 - session.Options.Path = s.config.Config.BaseURL if err := session.Save(r, w); err != nil { @@ -50,13 +49,10 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler { return } - if session.IsNew { - 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") + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) return } diff --git a/web/package.json b/web/package.json index 90fbd8a280..39ccbcf0c0 100644 --- a/web/package.json +++ b/web/package.json @@ -39,11 +39,11 @@ "@tailwindcss/forms": "^0.5.7", "@tanstack/react-query": "^5.17.19", "@tanstack/react-query-devtools": "^5.8.4", + "@tanstack/react-router": "^1.16.0", "@types/node": "^20.11.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", @@ -64,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,6 +77,7 @@ "devDependencies": { "@microsoft/eslint-formatter-sarif": "^3.0.0", "@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 80a5113673..ccc590efbe 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.19)(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-router': + specifier: ^1.16.0 + version: 1.16.0(react-dom@18.2.0)(react@18.2.0) '@types/node': specifier: ^20.11.6 version: 20.11.6 @@ -42,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 @@ -105,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) @@ -143,6 +140,9 @@ devDependencies: '@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 @@ -175,7 +175,7 @@ devDependencies: version: 0.17.5(vite@5.0.12)(workbox-build@7.0.0)(workbox-window@7.0.0) vite-plugin-svgr: specifier: ^4.2.0 - version: 4.2.0(@rollup/wasm-node@4.9.6)(typescript@5.3.3)(vite@5.0.12) + version: 4.2.0(@rollup/wasm-node@4.10.0)(typescript@5.3.3)(vite@5.0.12) packages: @@ -1432,7 +1432,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==} @@ -1982,12 +1981,7 @@ 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): + /@rollup/plugin-babel@5.3.1(@babel/core@7.23.9)(@rollup/wasm-node@4.10.0): resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} peerDependencies: @@ -2000,36 +1994,36 @@ packages: dependencies: '@babel/core': 7.23.9 '@babel/helper-module-imports': 7.22.15 - '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.9.6) - rollup: /@rollup/wasm-node@4.9.6 + '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.10.0) + rollup: /@rollup/wasm-node@4.10.0 dev: true - /@rollup/plugin-node-resolve@11.2.1(@rollup/wasm-node@4.9.6): + /@rollup/plugin-node-resolve@11.2.1(@rollup/wasm-node@4.10.0): resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} engines: {node: '>= 10.0.0'} peerDependencies: rollup: npm:@rollup/wasm-node dependencies: - '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.9.6) + '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.10.0) '@types/resolve': 1.17.1 builtin-modules: 3.3.0 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.8 - rollup: /@rollup/wasm-node@4.9.6 + rollup: /@rollup/wasm-node@4.10.0 dev: true - /@rollup/plugin-replace@2.4.2(@rollup/wasm-node@4.9.6): + /@rollup/plugin-replace@2.4.2(@rollup/wasm-node@4.10.0): resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} peerDependencies: rollup: npm:@rollup/wasm-node dependencies: - '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.9.6) + '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.10.0) magic-string: 0.25.9 - rollup: /@rollup/wasm-node@4.9.6 + rollup: /@rollup/wasm-node@4.10.0 dev: true - /@rollup/pluginutils@3.1.0(@rollup/wasm-node@4.9.6): + /@rollup/pluginutils@3.1.0(@rollup/wasm-node@4.10.0): resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} engines: {node: '>= 8.0.0'} peerDependencies: @@ -2038,10 +2032,10 @@ packages: '@types/estree': 0.0.39 estree-walker: 1.0.1 picomatch: 2.3.1 - rollup: /@rollup/wasm-node@4.9.6 + rollup: /@rollup/wasm-node@4.10.0 dev: true - /@rollup/pluginutils@5.0.5(@rollup/wasm-node@4.9.6): + /@rollup/pluginutils@5.0.5(@rollup/wasm-node@4.10.0): resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} engines: {node: '>=14.0.0'} peerDependencies: @@ -2053,9 +2047,18 @@ packages: '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: /@rollup/wasm-node@4.9.6 + rollup: /@rollup/wasm-node@4.10.0 dev: true + /@rollup/wasm-node@4.10.0: + resolution: {integrity: sha512-wH/ih4T/iP2PUyTrkyioZqDoFY/gmu63LPLTOM5Q21gSB/D3Ejw3UBpUOMLt86fIbN3mV+wL45MyA71XAj1ytg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + fsevents: 2.3.3 + /@rollup/wasm-node@4.9.6: resolution: {integrity: sha512-B3FpAkroTE6q+MRHzv8XLBgPbxdjJiy5UnduZNQ/4lxeF1JT2O/OAr0JPpXeRG/7zpKm/kdqU/4m6AULhmnSqw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2064,6 +2067,7 @@ packages: '@types/estree': 1.0.5 optionalDependencies: fsevents: 2.3.3 + dev: true /@surma/rollup-plugin-off-main-thread@2.2.3: resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} @@ -2332,6 +2336,16 @@ packages: tailwindcss: 3.4.1(ts-node@10.9.2) dev: false + /@tanstack/history@1.1.4: + resolution: {integrity: sha512-H80reryZP3Ib5HzAo9zp1B8nbGzd+zOxe0Xt6bLYY2qtgCb+iIrVadDDt5ZnaFsrMBGbFTkEsS2ITVrAUao54A==} + engines: {node: '>=12'} + dev: true + + /@tanstack/history@1.15.13: + resolution: {integrity: sha512-ToaeMtK5S4YaxCywAlYexc7KPFN0esjyTZ4vXzJhXEWAkro9iHgh7m/4ozPJb7oTo65WkHWX0W9GjcZbInSD8w==} + engines: {node: '>=12'} + dev: false + /@tanstack/query-core@5.17.19: resolution: {integrity: sha512-Lzw8FUtnLCc9Jwz0sw9xOjZB+/mCCmJev38v2wHMUl/ioXNIhnNWeMxu0NKUjIhAd62IRB3eAtvxAGDJ55UkyA==} dev: false @@ -2362,6 +2376,49 @@ 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 + dev: true + + /@tanstack/react-router@1.16.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-jY/mbRsdtIcaj56Jys+pr1Z17rFKIGcOwDTI5V6615e/ZzNUaPRxEvz3dAk3mWDqKNTxBUiCU5UOz5dJKx2UOg==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.2.0 + react-dom: '>=16' + dependencies: + '@tanstack/history': 1.15.13 + '@tanstack/react-store': 0.2.1(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tiny-invariant: 1.3.1 + tiny-warning: 1.0.3 + dev: false + + /@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/react-virtual@3.0.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-9XbRLPKgnhMwwmuQMnJMv+5a9sitGNCSEtf/AZXzmJdesYk7XsjYHaEDny+IrJzvPNwZliIIDwCRiaUqR3zzCA==} peerDependencies: @@ -2373,6 +2430,23 @@ packages: 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'} + 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==} + /@tanstack/virtual-core@3.0.0: resolution: {integrity: sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==} dev: false @@ -2396,10 +2470,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: @@ -2446,21 +2516,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: @@ -3108,6 +3163,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 @@ -4838,7 +4900,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==} @@ -4939,29 +5000,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: @@ -5024,7 +5062,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==} @@ -5138,7 +5175,7 @@ packages: dependencies: glob: 7.2.3 - /rollup-plugin-terser@7.0.2(@rollup/wasm-node@4.9.6): + /rollup-plugin-terser@7.0.2(@rollup/wasm-node@4.10.0): resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser peerDependencies: @@ -5146,7 +5183,7 @@ packages: dependencies: '@babel/code-frame': 7.23.5 jest-worker: 26.6.2 - rollup: /@rollup/wasm-node@4.9.6 + rollup: /@rollup/wasm-node@4.10.0 serialize-javascript: 4.0.0 terser: 5.27.0 dev: true @@ -5182,7 +5219,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==} @@ -5516,9 +5552,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==} @@ -5750,6 +5788,13 @@ packages: use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.48)(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 @@ -5779,12 +5824,12 @@ packages: - supports-color dev: true - /vite-plugin-svgr@4.2.0(@rollup/wasm-node@4.9.6)(typescript@5.3.3)(vite@5.0.12): + /vite-plugin-svgr@4.2.0(@rollup/wasm-node@4.10.0)(typescript@5.3.3)(vite@5.0.12): resolution: {integrity: sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==} peerDependencies: vite: ^2.6.0 || 3 || 4 || 5 dependencies: - '@rollup/pluginutils': 5.0.5(@rollup/wasm-node@4.9.6) + '@rollup/pluginutils': 5.0.5(@rollup/wasm-node@4.10.0) '@svgr/core': 8.1.0(typescript@5.3.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0) vite: 5.0.12(@types/node@20.11.6) @@ -5825,7 +5870,7 @@ packages: '@types/node': 20.11.6 esbuild: 0.19.12 postcss: 8.4.33 - rollup: /@rollup/wasm-node@4.9.6 + rollup: /@rollup/wasm-node@4.10.0 optionalDependencies: fsevents: 2.3.3 @@ -5923,9 +5968,9 @@ packages: '@babel/core': 7.23.9 '@babel/preset-env': 7.23.9(@babel/core@7.23.9) '@babel/runtime': 7.23.9 - '@rollup/plugin-babel': 5.3.1(@babel/core@7.23.9)(@rollup/wasm-node@4.9.6) - '@rollup/plugin-node-resolve': 11.2.1(@rollup/wasm-node@4.9.6) - '@rollup/plugin-replace': 2.4.2(@rollup/wasm-node@4.9.6) + '@rollup/plugin-babel': 5.3.1(@babel/core@7.23.9)(@rollup/wasm-node@4.10.0) + '@rollup/plugin-node-resolve': 11.2.1(@rollup/wasm-node@4.10.0) + '@rollup/plugin-replace': 2.4.2(@rollup/wasm-node@4.10.0) '@surma/rollup-plugin-off-main-thread': 2.2.3 ajv: 8.12.0 common-tags: 1.8.2 @@ -5934,8 +5979,8 @@ packages: glob: 7.2.3 lodash: 4.17.21 pretty-bytes: 5.6.0 - rollup: /@rollup/wasm-node@4.9.6 - rollup-plugin-terser: 7.0.2(@rollup/wasm-node@4.9.6) + rollup: /@rollup/wasm-node@4.10.0 + rollup-plugin-terser: 7.0.2(@rollup/wasm-node@4.10.0) source-map: 0.8.0-beta.0 stringify-object: 3.3.0 strip-comments: 2.0.1 diff --git a/web/src/App.tsx b/web/src/App.tsx index 1487a55626..ad9b7eaa83 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -3,60 +3,34 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { 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 { RouterProvider } from "@tanstack/react-router" +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "react-hot-toast"; import { Portal } from "react-portal"; +import { Router } from "@app/routes"; +import { routerBasePath } from "@utils"; +import { queryClient } from "@api/QueryClient"; +import { AuthContext } from "@utils/Context"; -const queryClient = new QueryClient({ - 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, - throwOnError: true, - }, - mutations: { - onError: (error) => { - // Use a format string to convert the error object to a proper string without much hassle. - const message = ( - typeof (error) === "object" && typeof ((error as Error).message) ? - (error as Error).message : - `${error}` - ); - toast.custom((t) => ); - } - } +declare module '@tanstack/react-router' { + interface Register { + router: typeof Router } -}); +} export function App() { - const { reset } = useQueryErrorResetBoundary(); - - const authContext = AuthContext.useValue(); - const settings = SettingsContext.useValue(); - return ( - - - {settings.debug ? ( - - ) : null} + - ); -} +} \ No newline at end of file diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index e556930e5e..47f5450ea0 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; @@ -30,7 +29,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) { @@ -87,22 +87,17 @@ export async function HttpClient( return Promise.resolve({} as T); } 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}"`)); return Promise.reject(response); + // 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(response); } case 404: { - return Promise.reject(new Error(`[404] Not found: "${endpoint}"`)); + const isJson = response.headers.get("Content-Type")?.includes("application/json"); + const json = isJson ? await response.json() : null; + return Promise.reject(json as T); + // return Promise.reject(new Error(`[404] Not Found: "${endpoint}"`)); } case 500: { const health = await window.fetch(`${baseUrl()}api/healthz/liveness`); @@ -326,6 +321,8 @@ export const APIClient = { if (filter.id == "indexer") { params["indexer"].push(filter.value); } else if (filter.id === "action_status") { + params["push_status"].push(filter.value); // push_status is the correct value here otherwise the releases table won't load when filtered by push status + } 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/api/QueryClient.tsx b/web/src/api/QueryClient.tsx new file mode 100644 index 0000000000..0c782b4241 --- /dev/null +++ b/web/src/api/QueryClient.tsx @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2021 - 2024, Ludvig Lundgren and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import { QueryCache, QueryClient } from "@tanstack/react-query"; +import { toast } from "react-hot-toast"; +import Toast from "@components/notifications/Toast"; + +const MAX_RETRIES = 6; +const HTTP_STATUS_TO_NOT_RETRY = [400, 401, 403, 404]; + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error ) => { + console.error("query client error: ", error); + + toast.custom((t) => ); + + // @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 + console.error("bad status, redirect to login", error?.status) + // Redirect to login page + window.location.href = "/login"; + + return + } + } + }), + 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: false, + throwOnError: true, + retry: (failureCount, error) => { + console.debug("retry count:", failureCount) + console.error("retry err: ", error) + + // @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; + } + + return failureCount <= MAX_RETRIES; + }, + }, + mutations: { + onError: (error) => { + // Use a format string to convert the error object to a proper string without much hassle. + const message = ( + typeof (error) === "object" && typeof ((error as Error).message) ? + (error as Error).message : + `${error}` + ); + toast.custom((t) => ); + } + } + } +}); \ No newline at end of file diff --git a/web/src/api/queries.ts b/web/src/api/queries.ts new file mode 100644 index 0000000000..d3aa1d4455 --- /dev/null +++ b/web/src/api/queries.ts @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021 - 2024, Ludvig Lundgren and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import { keepPreviousData, queryOptions } from "@tanstack/react-query"; +import { APIClient } from "@api/APIClient"; +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), + queryFn: () => APIClient.filters.find(indexers, sortOrder), + refetchOnWindowFocus: false + }); + +export const FilterByIdQueryOptions = (filterId: number) => + queryOptions({ + 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(), + queryFn: () => APIClient.config.get(), + retry: false, + refetchOnWindowFocus: false, + enabled: enabled, + }); + +export const UpdatesQueryOptions = (enabled: boolean) => + queryOptions({ + queryKey: SettingsKeys.updates(), + queryFn: () => APIClient.updates.getLatestRelease(), + retry: false, + refetchOnWindowFocus: false, + enabled: enabled, + }); + +export const IndexersQueryOptions = () => + queryOptions({ + queryKey: IndexerKeys.lists(), + queryFn: () => APIClient.indexers.getAll() + }); + +export const IndexersOptionsQueryOptions = () => + queryOptions({ + queryKey: IndexerKeys.options(), + queryFn: () => APIClient.indexers.getOptions(), + refetchOnWindowFocus: false, + staleTime: Infinity + }); + +export const IndexersSchemaQueryOptions = (enabled: boolean) => + queryOptions({ + queryKey: IndexerKeys.schema(), + queryFn: () => APIClient.indexers.getSchema(), + refetchOnWindowFocus: false, + staleTime: Infinity, + enabled: enabled + }); + +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: DownloadClientKeys.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(), + refetchOnWindowFocus: false, + }); + +export const ReleasesListQueryOptions = (offset: number, limit: number, filters: ReleaseFilter[]) => + queryOptions({ + queryKey: ReleaseKeys.list(offset, limit, filters), + queryFn: () => APIClient.release.findQuery(offset, limit, filters), + staleTime: 5000 + }); + +export const ReleasesLatestQueryOptions = () => + queryOptions({ + queryKey: ReleaseKeys.latestActivity(), + queryFn: () => APIClient.release.findRecent(), + refetchOnWindowFocus: false + }); + +export const ReleasesStatsQueryOptions = () => + queryOptions({ + queryKey: ReleaseKeys.stats(), + queryFn: () => APIClient.release.stats(), + refetchOnWindowFocus: false + }); + +// ReleasesIndexersQueryOptions get basic list of used indexers by identifier +export const ReleasesIndexersQueryOptions = () => + queryOptions({ + 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/SectionLoader.tsx b/web/src/components/SectionLoader.tsx deleted file mode 100644 index 79dd598a10..0000000000 --- a/web/src/components/SectionLoader.tsx +++ /dev/null @@ -1,32 +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 ( - - ); - } -}; diff --git a/web/src/components/alerts/NotFound.tsx b/web/src/components/alerts/NotFound.tsx index e113538045..3ac1f795cb 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"; @@ -12,8 +12,11 @@ export const NotFound = () => { return (
- +
+

+ 404 Page not found +

Oops, looks like there was a little too much brr!

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/debug.tsx b/web/src/components/debug.tsx index 5b11fe863f..95bbe9866f 100644 --- a/web/src/components/debug.tsx +++ b/web/src/components/debug.tsx @@ -23,3 +23,11 @@ export const DEBUG: FC = ({ values }) => {
); }; + +export function LogDebug(...data: any[]): void { + if (process.env.NODE_ENV !== "development") { + return; + } + + console.log(...data) +} diff --git a/web/src/components/header/Header.tsx b/web/src/components/header/Header.tsx index 30e960b236..34b9d6af40 100644 --- a/web/src/components/header/Header.tsx +++ b/web/src/components/header/Header.tsx @@ -5,11 +5,11 @@ import toast from "react-hot-toast"; import { useMutation, useQuery } from "@tanstack/react-query"; +import { useRouter } from "@tanstack/react-router"; 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"; @@ -17,37 +17,35 @@ import { RightNav } from "./RightNav"; import { MobileNav } from "./MobileNav"; import { ExternalLink } from "@components/ExternalLink"; +import { AuthIndexRoute } from "@app/routes"; +import { ConfigQueryOptions, UpdatesQueryOptions } from "@api/queries"; + export const Header = () => { - const { isError:isConfigError, error: configError, data: config } = useQuery({ - queryKey: ["config"], - queryFn: () => APIClient.config.get(), - retry: false, - refetchOnWindowFocus: false - }); + const router = useRouter() + const { auth } = AuthIndexRoute.useRouteContext() + const { isError:isConfigError, error: configError, data: config } = useQuery(ConfigQueryOptions(true)); if (isConfigError) { console.log(configError); } - const { isError, error, data } = useQuery({ - queryKey: ["updates"], - queryFn: () => APIClient.updates.getLatestRelease(), - retry: false, - refetchOnWindowFocus: false, - enabled: config?.check_for_updates === true - }); - - if (isError) { - console.log(error); + const { isError: isUpdateError, error, data } = useQuery(UpdatesQueryOptions(config?.check_for_updates === true)); + if (isUpdateError) { + console.log("update error", error); } const logoutMutation = useMutation({ mutationFn: APIClient.auth.logout, onSuccess: () => { - AuthContext.reset(); toast.custom((t) => ( )); + auth.logout() + + router.history.push("/") + }, + onError: (err) => { + console.error("logout error", err) } }); @@ -62,7 +60,7 @@ export const Header = () => {
- +
{/* Mobile menu button */} @@ -94,7 +92,7 @@ export const Header = () => { )}
- + )} diff --git a/web/src/components/header/LeftNav.tsx b/web/src/components/header/LeftNav.tsx index 557f63c9e2..a3cc33f1ed 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 === "/"} + params={{}} > - {item.name} - + {({ isActive }) => { + return ( + <> + {item.name} + + ) + }} + ))} (
{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} + + ) + }} + ))}
- {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 c2e807d2cd..35421302ca 100644 --- a/web/src/screens/Settings.tsx +++ b/web/src/screens/Settings.tsx @@ -3,8 +3,6 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { Suspense } from "react"; -import { NavLink, Outlet, useLocation } from "react-router-dom"; import { BellIcon, ChatBubbleLeftRightIcon, @@ -16,25 +14,26 @@ import { Square3Stack3DIcon, UserCircleIcon } from "@heroicons/react/24/outline"; +import { Link, Outlet } from "@tanstack/react-router"; import { classNames } from "@utils"; -import { SectionLoader } from "@components/SectionLoader"; 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 +45,38 @@ 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 }} + search={{}} + params={{}} + // aria-current={splitLocation[2] === item.href ? "page" : undefined} > - + {({ isActive }) => { + return ( + + + ) + }} + ); } @@ -78,10 +86,10 @@ interface SidebarNavProps { function SidebarNav({ subNavigation }: SidebarNavProps) { return ( -