From fd69830c2681985e4fd3c5336a2b75c9fb7bc5d4 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sat, 11 Jan 2025 20:14:12 +0100 Subject: [PATCH] feat: add sorting for tables --- .../controller/audit_log_controller.go | 12 ++-- .../internal/controller/oidc_controller.go | 11 +-- .../internal/controller/user_controller.go | 11 +-- .../controller/user_group_controller.go | 15 ++-- backend/internal/model/audit_log.go | 10 +-- backend/internal/model/base.go | 4 +- backend/internal/model/oidc.go | 2 +- backend/internal/model/user.go | 10 +-- backend/internal/model/user_group.go | 4 +- backend/internal/service/audit_log_service.go | 6 +- backend/internal/service/oidc_service.go | 4 +- .../internal/service/user_group_service.go | 16 ++++- backend/internal/service/user_service.go | 4 +- backend/internal/utils/paging_util.go | 36 +++++++++- backend/internal/utils/string_util.go | 21 ++++++ .../src/lib/components/advanced-table.svelte | 72 +++++++++++++++---- .../src/lib/services/audit-log-service.ts | 6 +- frontend/src/lib/services/oidc-service.ts | 27 ++++--- .../src/lib/services/user-group-service.ts | 9 +-- frontend/src/lib/services/user-service.ts | 9 +-- frontend/src/lib/types/pagination.type.ts | 11 +++ .../oidc-clients/oidc-client-list.svelte | 24 +++---- .../admin/user-groups/user-group-list.svelte | 21 ++++-- .../admin/user-groups/user-selection.svelte | 11 ++- .../settings/admin/users/user-list.svelte | 51 +++++++------ .../routes/settings/audit-log/+page.server.ts | 6 +- .../settings/audit-log/audit-log-list.svelte | 19 ++--- 27 files changed, 294 insertions(+), 138 deletions(-) diff --git a/backend/internal/controller/audit_log_controller.go b/backend/internal/controller/audit_log_controller.go index a032645..bd402b5 100644 --- a/backend/internal/controller/audit_log_controller.go +++ b/backend/internal/controller/audit_log_controller.go @@ -3,8 +3,8 @@ package controller import ( "github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/middleware" + "github.com/stonith404/pocket-id/backend/internal/utils" "net/http" - "strconv" "github.com/gin-gonic/gin" "github.com/stonith404/pocket-id/backend/internal/service" @@ -23,12 +23,16 @@ type AuditLogController struct { } func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) { + var sortedPaginationRequest utils.SortedPaginationRequest + if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil { + c.Error(err) + return + } + userID := c.GetString("userID") - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) // Fetch audit logs for the user - logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(userID, page, pageSize) + logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(userID, sortedPaginationRequest) if err != nil { c.Error(err) return diff --git a/backend/internal/controller/oidc_controller.go b/backend/internal/controller/oidc_controller.go index e3950be..bd449d5 100644 --- a/backend/internal/controller/oidc_controller.go +++ b/backend/internal/controller/oidc_controller.go @@ -5,8 +5,8 @@ import ( "github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/middleware" "github.com/stonith404/pocket-id/backend/internal/service" + "github.com/stonith404/pocket-id/backend/internal/utils" "net/http" - "strconv" "strings" ) @@ -153,11 +153,14 @@ func (oc *OidcController) getClientHandler(c *gin.Context) { } func (oc *OidcController) listClientsHandler(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) searchTerm := c.Query("search") + var sortedPaginationRequest utils.SortedPaginationRequest + if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil { + c.Error(err) + return + } - clients, pagination, err := oc.oidcService.ListClients(searchTerm, page, pageSize) + clients, pagination, err := oc.oidcService.ListClients(searchTerm, sortedPaginationRequest) if err != nil { c.Error(err) return diff --git a/backend/internal/controller/user_controller.go b/backend/internal/controller/user_controller.go index 54df2ae..8854cd9 100644 --- a/backend/internal/controller/user_controller.go +++ b/backend/internal/controller/user_controller.go @@ -6,9 +6,9 @@ import ( "github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/middleware" "github.com/stonith404/pocket-id/backend/internal/service" + "github.com/stonith404/pocket-id/backend/internal/utils" "golang.org/x/time/rate" "net/http" - "strconv" "time" ) @@ -37,11 +37,14 @@ type UserController struct { } func (uc *UserController) listUsersHandler(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) searchTerm := c.Query("search") + var sortedPaginationRequest utils.SortedPaginationRequest + if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil { + c.Error(err) + return + } - users, pagination, err := uc.UserService.ListUsers(searchTerm, page, pageSize) + users, pagination, err := uc.UserService.ListUsers(searchTerm, sortedPaginationRequest) if err != nil { c.Error(err) return diff --git a/backend/internal/controller/user_group_controller.go b/backend/internal/controller/user_group_controller.go index e7fcbb3..ebdf727 100644 --- a/backend/internal/controller/user_group_controller.go +++ b/backend/internal/controller/user_group_controller.go @@ -1,13 +1,12 @@ package controller import ( - "net/http" - "strconv" - "github.com/gin-gonic/gin" "github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/middleware" "github.com/stonith404/pocket-id/backend/internal/service" + "github.com/stonith404/pocket-id/backend/internal/utils" + "net/http" ) func NewUserGroupController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, userGroupService *service.UserGroupService) { @@ -28,16 +27,20 @@ type UserGroupController struct { } func (ugc *UserGroupController) list(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) searchTerm := c.Query("search") + var sortedPaginationRequest utils.SortedPaginationRequest + if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil { + c.Error(err) + return + } - groups, pagination, err := ugc.UserGroupService.List(searchTerm, page, pageSize) + groups, pagination, err := ugc.UserGroupService.List(searchTerm, sortedPaginationRequest) if err != nil { c.Error(err) return } + // Map the user groups to DTOs. The user count can't be mapped directly, so we have to do it manually. var groupsDto = make([]dto.UserGroupDtoWithUserCount, len(groups)) for i, group := range groups { var groupDto dto.UserGroupDtoWithUserCount diff --git a/backend/internal/model/audit_log.go b/backend/internal/model/audit_log.go index b8cadab..bef022b 100644 --- a/backend/internal/model/audit_log.go +++ b/backend/internal/model/audit_log.go @@ -9,11 +9,11 @@ import ( type AuditLog struct { Base - Event AuditLogEvent - IpAddress string - Country string - City string - UserAgent string + Event AuditLogEvent `sortable:"true"` + IpAddress string `sortable:"true"` + Country string `sortable:"true"` + City string `sortable:"true"` + UserAgent string `sortable:"true"` UserID string Data AuditLogData } diff --git a/backend/internal/model/base.go b/backend/internal/model/base.go index 0392633..e0412e7 100644 --- a/backend/internal/model/base.go +++ b/backend/internal/model/base.go @@ -9,8 +9,8 @@ import ( // Base contains common columns for all tables. type Base struct { - ID string `gorm:"primaryKey;not null"` - CreatedAt model.DateTime + ID string `gorm:"primaryKey;not null"` + CreatedAt model.DateTime `sortable:"true"` } func (b *Base) BeforeCreate(_ *gorm.DB) (err error) { diff --git a/backend/internal/model/oidc.go b/backend/internal/model/oidc.go index 12e4713..e651848 100644 --- a/backend/internal/model/oidc.go +++ b/backend/internal/model/oidc.go @@ -36,7 +36,7 @@ type OidcAuthorizationCode struct { type OidcClient struct { Base - Name string + Name string `sortable:"true"` Secret string CallbackURLs CallbackURLs ImageType *string diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go index 7682bd7..8a8e619 100644 --- a/backend/internal/model/user.go +++ b/backend/internal/model/user.go @@ -9,11 +9,11 @@ import ( type User struct { Base - Username string - Email string - FirstName string - LastName string - IsAdmin bool + Username string `sortable:"true"` + Email string `sortable:"true"` + FirstName string `sortable:"true"` + LastName string `sortable:"true"` + IsAdmin bool `sortable:"true"` CustomClaims []CustomClaim UserGroups []UserGroup `gorm:"many2many:user_groups_users;"` diff --git a/backend/internal/model/user_group.go b/backend/internal/model/user_group.go index 15648d4..786a8b0 100644 --- a/backend/internal/model/user_group.go +++ b/backend/internal/model/user_group.go @@ -2,8 +2,8 @@ package model type UserGroup struct { Base - FriendlyName string - Name string `gorm:"unique"` + FriendlyName string `sortable:"true"` + Name string `gorm:"unique" sortable:"true"` Users []User `gorm:"many2many:user_groups_users;"` CustomClaims []CustomClaim } diff --git a/backend/internal/service/audit_log_service.go b/backend/internal/service/audit_log_service.go index 695f408..7c318d1 100644 --- a/backend/internal/service/audit_log_service.go +++ b/backend/internal/service/audit_log_service.go @@ -84,11 +84,11 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ipAddress, userAgent, userID } // ListAuditLogsForUser retrieves all audit logs for a given user ID -func (s *AuditLogService) ListAuditLogsForUser(userID string, page int, pageSize int) ([]model.AuditLog, utils.PaginationResponse, error) { +func (s *AuditLogService) ListAuditLogsForUser(userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.AuditLog, utils.PaginationResponse, error) { var logs []model.AuditLog - query := s.db.Model(&model.AuditLog{}).Where("user_id = ?", userID).Order("created_at desc") + query := s.db.Model(&model.AuditLog{}).Where("user_id = ?", userID) - pagination, err := utils.Paginate(page, pageSize, query, &logs) + pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &logs) return logs, pagination, err } diff --git a/backend/internal/service/oidc_service.go b/backend/internal/service/oidc_service.go index c49833b..9a3b9eb 100644 --- a/backend/internal/service/oidc_service.go +++ b/backend/internal/service/oidc_service.go @@ -167,7 +167,7 @@ func (s *OidcService) GetClient(clientID string) (model.OidcClient, error) { return client, nil } -func (s *OidcService) ListClients(searchTerm string, page int, pageSize int) ([]model.OidcClient, utils.PaginationResponse, error) { +func (s *OidcService) ListClients(searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.OidcClient, utils.PaginationResponse, error) { var clients []model.OidcClient query := s.db.Preload("CreatedBy").Model(&model.OidcClient{}) @@ -176,7 +176,7 @@ func (s *OidcService) ListClients(searchTerm string, page int, pageSize int) ([] query = query.Where("name LIKE ?", searchPattern) } - pagination, err := utils.Paginate(page, pageSize, query, &clients) + pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &clients) if err != nil { return nil, utils.PaginationResponse{}, err } diff --git a/backend/internal/service/user_group_service.go b/backend/internal/service/user_group_service.go index 065f9e7..0d97132 100644 --- a/backend/internal/service/user_group_service.go +++ b/backend/internal/service/user_group_service.go @@ -17,14 +17,26 @@ func NewUserGroupService(db *gorm.DB) *UserGroupService { return &UserGroupService{db: db} } -func (s *UserGroupService) List(name string, page int, pageSize int) (groups []model.UserGroup, response utils.PaginationResponse, err error) { +func (s *UserGroupService) List(name string, sortedPaginationRequest utils.SortedPaginationRequest) (groups []model.UserGroup, response utils.PaginationResponse, err error) { query := s.db.Preload("CustomClaims").Model(&model.UserGroup{}) if name != "" { query = query.Where("name LIKE ?", "%"+name+"%") } - response, err = utils.Paginate(page, pageSize, query, &groups) + // As userCount is not a column we need to manually sort it + isValidSortDirection := sortedPaginationRequest.Sort.Direction == "asc" || sortedPaginationRequest.Sort.Direction == "desc" + if sortedPaginationRequest.Sort.Column == "userCount" && isValidSortDirection { + query = query.Select("user_groups.*, COUNT(user_groups_users.user_id)"). + Joins("LEFT JOIN user_groups_users ON user_groups.id = user_groups_users.user_group_id"). + Group("user_groups.id"). + Order("COUNT(user_groups_users.user_id) " + sortedPaginationRequest.Sort.Direction) + + response, err := utils.Paginate(sortedPaginationRequest.Pagination.Page, sortedPaginationRequest.Pagination.Limit, query, &groups) + return groups, response, err + } + + response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &groups) return groups, response, err } diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index 7d1007e..1ffb349 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -21,7 +21,7 @@ func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditL return &UserService{db: db, jwtService: jwtService, auditLogService: auditLogService} } -func (s *UserService) ListUsers(searchTerm string, page int, pageSize int) ([]model.User, utils.PaginationResponse, error) { +func (s *UserService) ListUsers(searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) { var users []model.User query := s.db.Model(&model.User{}) @@ -30,7 +30,7 @@ func (s *UserService) ListUsers(searchTerm string, page int, pageSize int) ([]mo query = query.Where("email LIKE ? OR first_name LIKE ? OR username LIKE ?", searchPattern, searchPattern, searchPattern) } - pagination, err := utils.Paginate(page, pageSize, query, &users) + pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &users) return users, pagination, err } diff --git a/backend/internal/utils/paging_util.go b/backend/internal/utils/paging_util.go index ccdf8df..13143d7 100644 --- a/backend/internal/utils/paging_util.go +++ b/backend/internal/utils/paging_util.go @@ -2,6 +2,7 @@ package utils import ( "gorm.io/gorm" + "reflect" ) type PaginationResponse struct { @@ -11,7 +12,36 @@ type PaginationResponse struct { ItemsPerPage int `json:"itemsPerPage"` } -func Paginate(page int, pageSize int, db *gorm.DB, result interface{}) (PaginationResponse, error) { +type SortedPaginationRequest struct { + Pagination struct { + Page int `form:"pagination[page]"` + Limit int `form:"pagination[limit]"` + } `form:"pagination"` + Sort struct { + Column string `form:"sort[column]"` + Direction string `form:"sort[direction]"` + } `form:"sort"` +} + +func PaginateAndSort(sortedPaginationRequest SortedPaginationRequest, query *gorm.DB, result interface{}) (PaginationResponse, error) { + pagination := sortedPaginationRequest.Pagination + sort := sortedPaginationRequest.Sort + + capitalizedSortColumn := CapitalizeFirstLetter(sort.Column) + + sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn) + isSortable := sortField.Tag.Get("sortable") == "true" + isValidSortOrder := sort.Direction == "asc" || sort.Direction == "desc" + + if sortFieldFound && isSortable && isValidSortOrder { + query = query.Order(CamelCaseToSnakeCase(sort.Column) + " " + sort.Direction) + } + + return Paginate(pagination.Page, pagination.Limit, query, result) + +} + +func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (PaginationResponse, error) { if page < 1 { page = 1 } @@ -25,11 +55,11 @@ func Paginate(page int, pageSize int, db *gorm.DB, result interface{}) (Paginati offset := (page - 1) * pageSize var totalItems int64 - if err := db.Count(&totalItems).Error; err != nil { + if err := query.Count(&totalItems).Error; err != nil { return PaginationResponse{}, err } - if err := db.Offset(offset).Limit(pageSize).Find(result).Error; err != nil { + if err := query.Offset(offset).Limit(pageSize).Find(result).Error; err != nil { return PaginationResponse{}, err } diff --git a/backend/internal/utils/string_util.go b/backend/internal/utils/string_util.go index 442ff9f..acf1a73 100644 --- a/backend/internal/utils/string_util.go +++ b/backend/internal/utils/string_util.go @@ -5,6 +5,7 @@ import ( "fmt" "math/big" "net/url" + "unicode" ) // GenerateRandomAlphanumericString generates a random alphanumeric string of the given length @@ -41,3 +42,23 @@ func GetHostnameFromURL(rawURL string) string { func StringPointer(s string) *string { return &s } + +func CapitalizeFirstLetter(s string) string { + if s == "" { + return s + } + runes := []rune(s) + runes[0] = unicode.ToUpper(runes[0]) + return string(runes) +} + +func CamelCaseToSnakeCase(s string) string { + var result []rune + for i, r := range s { + if unicode.IsUpper(r) && i > 0 { + result = append(result, '_') + } + result = append(result, unicode.ToLower(r)) + } + return string(result) +} diff --git a/frontend/src/lib/components/advanced-table.svelte b/frontend/src/lib/components/advanced-table.svelte index 651e810..2f1c431 100644 --- a/frontend/src/lib/components/advanced-table.svelte +++ b/frontend/src/lib/components/advanced-table.svelte @@ -5,26 +5,44 @@ import * as Select from '$lib/components/ui/select'; import * as Table from '$lib/components/ui/table/index.js'; import Empty from '$lib/icons/empty.svelte'; - import type { Paginated } from '$lib/types/pagination.type'; + import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import { debounced } from '$lib/utils/debounce-util'; + import { cn } from '$lib/utils/style'; + import { ChevronDown } from 'lucide-svelte'; import type { Snippet } from 'svelte'; + import Button from './ui/button/button.svelte'; let { items, + requestOptions = $bindable(), selectedIds = $bindable(), withoutSearch = false, - fetchItems, + defaultSort, + onRefresh, columns, rows }: { items: Paginated; + requestOptions?: SearchPaginationSortRequest; selectedIds?: string[]; withoutSearch?: boolean; - fetchItems: (search: string, page: number, limit: number) => Promise>; - columns: (string | { label: string; hidden?: boolean })[]; + defaultSort?: { column: string; direction: 'asc' | 'desc' }; + onRefresh: (requestOptions: SearchPaginationSortRequest) => Promise>; + columns: { label: string; hidden?: boolean; sortColumn?: string }[]; rows: Snippet<[{ item: T }]>; } = $props(); + if (!requestOptions) { + requestOptions = { + search: '', + sort: defaultSort, + pagination: { + page: items.pagination.currentPage, + limit: items.pagination.itemsPerPage + } + }; + } + let availablePageSizes: number[] = [10, 20, 50, 100]; let allChecked = $derived.by(() => { @@ -38,7 +56,8 @@ }); const onSearch = debounced(async (searchValue: string) => { - items = await fetchItems(searchValue, 1, items.pagination.itemsPerPage); + requestOptions.search = searchValue; + onRefresh(requestOptions); }, 300); async function onAllCheck(checked: boolean) { @@ -59,11 +78,20 @@ } async function onPageChange(page: number) { - items = await fetchItems('', page, items.pagination.itemsPerPage); + requestOptions!.pagination = { limit: items.pagination.itemsPerPage, page }; + onRefresh(requestOptions!); } async function onPageSizeChange(size: number) { - items = await fetchItems('', 1, size); + requestOptions!.pagination = { limit: size, page: 1 }; + onRefresh(requestOptions!); + } + + async function onSort(column?: string, direction: 'asc' | 'desc' = 'asc') { + if (!column) return; + + requestOptions!.sort = { column, direction }; + onRefresh(requestOptions!); } @@ -92,11 +120,31 @@ {/if} {#each columns as column} - {#if typeof column === 'string'} - {column} - {:else} - {column.label} - {/if} + + {#if column.sortColumn} + + {:else} + {column.label} + {/if} + {/each} diff --git a/frontend/src/lib/services/audit-log-service.ts b/frontend/src/lib/services/audit-log-service.ts index 61fe0cb..393e762 100644 --- a/frontend/src/lib/services/audit-log-service.ts +++ b/frontend/src/lib/services/audit-log-service.ts @@ -1,11 +1,11 @@ import type { AuditLog } from '$lib/types/audit-log.type'; -import type { Paginated, PaginationRequest } from '$lib/types/pagination.type'; +import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import APIService from './api-service'; class AuditLogService extends APIService { - async list(pagination?: PaginationRequest) { + async list(options?: SearchPaginationSortRequest) { const res = await this.api.get('/audit-logs', { - params: pagination + params: options }); return res.data as Paginated; } diff --git a/frontend/src/lib/services/oidc-service.ts b/frontend/src/lib/services/oidc-service.ts index 6bdce0c..bf248df 100644 --- a/frontend/src/lib/services/oidc-service.ts +++ b/frontend/src/lib/services/oidc-service.ts @@ -1,9 +1,16 @@ import type { AuthorizeResponse, OidcClient, OidcClientCreate } from '$lib/types/oidc.type'; -import type { Paginated, PaginationRequest } from '$lib/types/pagination.type'; +import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import APIService from './api-service'; class OidcService extends APIService { - async authorize(clientId: string, scope: string, callbackURL: string, nonce?: string, codeChallenge?: string, codeChallengeMethod?: string) { + async authorize( + clientId: string, + scope: string, + callbackURL: string, + nonce?: string, + codeChallenge?: string, + codeChallengeMethod?: string + ) { const res = await this.api.post('/oidc/authorize', { scope, nonce, @@ -16,7 +23,14 @@ class OidcService extends APIService { return res.data as AuthorizeResponse; } - async authorizeNewClient(clientId: string, scope: string, callbackURL: string, nonce?: string, codeChallenge?: string, codeChallengeMethod?: string) { + async authorizeNewClient( + clientId: string, + scope: string, + callbackURL: string, + nonce?: string, + codeChallenge?: string, + codeChallengeMethod?: string + ) { const res = await this.api.post('/oidc/authorize/new-client', { scope, nonce, @@ -29,12 +43,9 @@ class OidcService extends APIService { return res.data as AuthorizeResponse; } - async listClients(search?: string, pagination?: PaginationRequest) { + async listClients(options?: SearchPaginationSortRequest) { const res = await this.api.get('/oidc/clients', { - params: { - search, - ...pagination - } + params: options }); return res.data as Paginated; } diff --git a/frontend/src/lib/services/user-group-service.ts b/frontend/src/lib/services/user-group-service.ts index 6c752de..2f353e5 100644 --- a/frontend/src/lib/services/user-group-service.ts +++ b/frontend/src/lib/services/user-group-service.ts @@ -1,4 +1,4 @@ -import type { Paginated, PaginationRequest } from '$lib/types/pagination.type'; +import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { UserGroupCreate, UserGroupWithUserCount, @@ -7,12 +7,9 @@ import type { import APIService from './api-service'; export default class UserGroupService extends APIService { - async list(search?: string, pagination?: PaginationRequest) { + async list(options?: SearchPaginationSortRequest) { const res = await this.api.get('/user-groups', { - params: { - search, - ...pagination - } + params: options }); return res.data as Paginated; } diff --git a/frontend/src/lib/services/user-service.ts b/frontend/src/lib/services/user-service.ts index e93de92..bd84fe0 100644 --- a/frontend/src/lib/services/user-service.ts +++ b/frontend/src/lib/services/user-service.ts @@ -1,14 +1,11 @@ -import type { Paginated, PaginationRequest } from '$lib/types/pagination.type'; +import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { User, UserCreate } from '$lib/types/user.type'; import APIService from './api-service'; export default class UserService extends APIService { - async list(search?: string, pagination?: PaginationRequest) { + async list(options?: SearchPaginationSortRequest) { const res = await this.api.get('/users', { - params: { - search, - ...pagination - } + params: options }); return res.data as Paginated; } diff --git a/frontend/src/lib/types/pagination.type.ts b/frontend/src/lib/types/pagination.type.ts index a463c00..aef696f 100644 --- a/frontend/src/lib/types/pagination.type.ts +++ b/frontend/src/lib/types/pagination.type.ts @@ -3,6 +3,17 @@ export type PaginationRequest = { limit: number; }; +export type SortRequest = { + column: string; + direction: "asc" | "desc"; +}; + +export type SearchPaginationSortRequest = { + search?: string, + pagination?: PaginationRequest; + sort?: SortRequest; +} + export type PaginationResponse = { totalPages: number; totalItems: number; diff --git a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-list.svelte b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-list.svelte index 5a08fb1..50c4aa3 100644 --- a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-list.svelte +++ b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-list.svelte @@ -5,7 +5,7 @@ import * as Table from '$lib/components/ui/table'; import OIDCService from '$lib/services/oidc-service'; import type { OidcClient } from '$lib/types/oidc.type'; - import type { Paginated, PaginationRequest } from '$lib/types/pagination.type'; + import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import { axiosErrorToast } from '$lib/utils/error-util'; import { LucidePencil, LucideTrash } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; @@ -14,6 +14,7 @@ let { clients: initialClients }: { clients: Paginated } = $props(); let clients = $state>(initialClients); let oneTimeLink = $state(null); + let requestOptions: SearchPaginationSortRequest | undefined = $state(); $effect(() => { clients = initialClients; @@ -21,12 +22,6 @@ const oidcService = new OIDCService(); - let pagination = $state({ - page: 1, - limit: 10 - }); - let search = $state(''); - async function deleteClient(client: OidcClient) { openConfirmDialog({ title: `Delete ${client.name}`, @@ -37,7 +32,7 @@ action: async () => { try { await oidcService.removeClient(client.id); - clients = await oidcService.listClients(search, pagination); + clients = await oidcService.listClients(requestOptions!); toast.success('OIDC client deleted successfully'); } catch (e) { axiosErrorToast(e); @@ -46,16 +41,17 @@ } }); } - - async function fetchItems(search: string, page: number, limit: number) { - return oidcService.listClients(search, { page, limit }); - } clients = await oidcService.listClients(o)} + columns={[ + { label: 'Logo' }, + { label: 'Name', sortColumn: 'name' }, + { label: 'Actions', hidden: true } + ]} > {#snippet rows({ item })} diff --git a/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte b/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte index 5239078..a342a04 100644 --- a/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte +++ b/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte @@ -5,7 +5,7 @@ import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; import * as Table from '$lib/components/ui/table'; import UserGroupService from '$lib/services/user-group-service'; - import type { Paginated } from '$lib/types/pagination.type'; + import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { UserGroup, UserGroupWithUserCount } from '$lib/types/user-group.type'; import { axiosErrorToast } from '$lib/utils/error-util'; import { LucidePencil, LucideTrash } from 'lucide-svelte'; @@ -16,6 +16,7 @@ $props(); let userGroups = $state>(initialUserGroups); + let requestOptions: SearchPaginationSortRequest | undefined = $state(); const userGroupService = new UserGroupService(); @@ -29,7 +30,7 @@ action: async () => { try { await userGroupService.remove(userGroup.id); - userGroups = await userGroupService.list(); + userGroups = await userGroupService.list(requestOptions!); } catch (e) { axiosErrorToast(e); } @@ -38,13 +39,19 @@ } }); } - - async function fetchItems(search: string, page: number, limit: number) { - return userGroupService.list(search, { page, limit }); - } - + (userGroups = await userGroupService.list(o))} + {requestOptions} + columns={[ + { label: 'Friendly Name', sortColumn: 'friendlyName' }, + { label: 'Name', sortColumn: 'name' }, + { label: 'User Count', sortColumn: 'userCount' }, + { label: 'Actions', hidden: true } + ]} +> {#snippet rows({ item })} {item.friendlyName} {item.name} diff --git a/frontend/src/routes/settings/admin/user-groups/user-selection.svelte b/frontend/src/routes/settings/admin/user-groups/user-selection.svelte index 5919beb..c5a0fab 100644 --- a/frontend/src/routes/settings/admin/user-groups/user-selection.svelte +++ b/frontend/src/routes/settings/admin/user-groups/user-selection.svelte @@ -13,16 +13,15 @@ const userService = new UserService(); let users = $state(initialUsers); - - function fetchItems(search: string, page: number, limit: number) { - return userService.list(search, { page, limit }); - } (users = await userService.list(o))} + columns={[ + { label: 'Name', sortColumn: 'name' }, + { label: 'Email', sortColumn: 'email' } + ]} bind:selectedIds={selectedUserIds} > {#snippet rows({ item })} diff --git a/frontend/src/routes/settings/admin/users/user-list.svelte b/frontend/src/routes/settings/admin/users/user-list.svelte index 5faa2c2..aaad2e4 100644 --- a/frontend/src/routes/settings/admin/users/user-list.svelte +++ b/frontend/src/routes/settings/admin/users/user-list.svelte @@ -7,7 +7,7 @@ import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; import * as Table from '$lib/components/ui/table'; import UserService from '$lib/services/user-service'; - import type { Paginated } from '$lib/types/pagination.type'; + import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { User } from '$lib/types/user.type'; import { axiosErrorToast } from '$lib/utils/error-util'; import { LucideLink, LucidePencil, LucideTrash } from 'lucide-svelte'; @@ -15,20 +15,13 @@ import { toast } from 'svelte-sonner'; import OneTimeLinkModal from './one-time-link-modal.svelte'; - let { users: initialUsers }: { users: Paginated } = $props(); - let users = $state>(initialUsers); - $effect(() => { - users = initialUsers; - }); + let { users = $bindable() }: { users: Paginated } = $props(); + let requestOptions: SearchPaginationSortRequest | undefined = $state(); - let userIdToCreateOneTimeLink: string | null = $state(null);; + let userIdToCreateOneTimeLink: string | null = $state(null); const userService = new UserService(); - function fetchItems(search: string, page: number, limit: number) { - return userService.list(search, { page, limit }); - } - async function deleteUser(user: User) { openConfirmDialog({ title: `Delete ${user.firstName} ${user.lastName}`, @@ -39,7 +32,7 @@ action: async () => { try { await userService.remove(user.id); - users = await userService.list(); + users = await userService.list(requestOptions!); } catch (e) { axiosErrorToast(e); } @@ -52,16 +45,34 @@ (users = await userService.list(options))} columns={[ - 'First name', - 'Last name', - 'Email', - 'Username', - 'Role', - { label: 'Actions', hidden: true } + { + label: 'First name', + sortColumn: 'firstName' + }, + { + label: 'Last name', + sortColumn: 'lastName' + }, + { + label: 'Email', + sortColumn: 'email' + }, + { + label: 'Username', + sortColumn: 'username' + }, + { + label: 'Role', + sortColumn: 'isAdmin' + }, + { + label: 'Actions', + hidden: true + } ]} - withoutSearch > {#snippet rows({ item })} {item.firstName} diff --git a/frontend/src/routes/settings/audit-log/+page.server.ts b/frontend/src/routes/settings/audit-log/+page.server.ts index 2ca3d8c..124789c 100644 --- a/frontend/src/routes/settings/audit-log/+page.server.ts +++ b/frontend/src/routes/settings/audit-log/+page.server.ts @@ -4,8 +4,10 @@ import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ cookies }) => { const auditLogService = new AuditLogService(cookies.get('access_token')); const auditLogs = await auditLogService.list({ - limit: 15, - page: 1, + sort: { + column: 'createdAt', + direction: 'desc' + } }); return { auditLogs diff --git a/frontend/src/routes/settings/audit-log/audit-log-list.svelte b/frontend/src/routes/settings/audit-log/audit-log-list.svelte index 603eaaf..585cd72 100644 --- a/frontend/src/routes/settings/audit-log/audit-log-list.svelte +++ b/frontend/src/routes/settings/audit-log/audit-log-list.svelte @@ -11,13 +11,6 @@ const auditLogService = new AuditLogService(); - async function fetchItems(search: string, page: number, limit: number) { - return await auditLogService.list({ - page, - limit - }); - } - function toFriendlyEventString(event: string) { const words = event.split('_'); const capitalizedWords = words.map((word) => { @@ -29,8 +22,16 @@ (auditLogs = await auditLogService.list(options))} + defaultSort={{ column: 'createdAt', direction: 'desc' }} + columns={[ + { label: 'Time', sortColumn: 'createdAt' }, + { label: 'Event', sortColumn: 'event' }, + { label: 'Approximate Location', sortColumn: 'city' }, + { label: 'IP Address', sortColumn: 'ipAddress' }, + { label: 'Device', sortColumn: 'device' }, + { label: 'Client' } + ]} withoutSearch > {#snippet rows({ item })}