From ecefe83ed37a5262adda5118a6493bd02d45c6a7 Mon Sep 17 00:00:00 2001 From: Frank Jogeleit Date: Sat, 8 Mar 2025 07:56:02 +0100 Subject: [PATCH] Extend CustomBoard with filters Signed-off-by: Frank Jogeleit --- backend/pkg/config/config.go | 13 +- backend/pkg/config/mapper.go | 28 +++- backend/pkg/server/api/handler.go | 24 ++- backend/pkg/server/api/model.go | 9 ++ backend/pkg/service/model.go | 15 ++ backend/pkg/service/service.go | 148 +++++++++-------- frontend/bun.lockb | Bin 313942 -> 310600 bytes frontend/composables/infinite.ts | 8 +- frontend/layouts/default.vue | 3 +- frontend/modules/core/components/Scroller.vue | 2 +- .../core/components/graph/SourcesStatus.vue | 8 +- .../core/components/resource/Scroller.vue | 2 +- .../core/components/result/ClusterTable.vue | 150 ++++++++++++++++++ .../modules/core/components/result/Table.vue | 145 +++++++++++++++++ frontend/modules/core/types.ts | 5 + frontend/pages/custom-boards/[id].vue | 13 +- frontend/plugins/02.vuetify.ts | 2 + 17 files changed, 493 insertions(+), 82 deletions(-) create mode 100644 frontend/modules/core/components/result/ClusterTable.vue create mode 100644 frontend/modules/core/components/result/Table.vue diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index d6ab76be..16d315c2 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -205,10 +205,21 @@ type Boards struct { AccessControl AccessControl `koanf:"accessControl"` } +type Filter struct { + NamespaceKinds []string `koanf:"namespaceKinds"` + ClusterKinds []string `koanf:"clusterKinds"` + Results []string `koanf:"results"` + Severities []string `koanf:"severities"` +} + type CustomBoard struct { Name string `koanf:"name"` AccessControl AccessControl `koanf:"accessControl"` - Namespaces struct { + Filter struct { + Include Filter `koanf:"include"` + } `koanf:"filter"` + Display string `json:"display"` + Namespaces struct { Selector map[string]string `koanf:"selector"` List []string `koanf:"list"` } `koanf:"namespaces"` diff --git a/backend/pkg/config/mapper.go b/backend/pkg/config/mapper.go index 29bc087d..b50c8aa8 100644 --- a/backend/pkg/config/mapper.go +++ b/backend/pkg/config/mapper.go @@ -73,8 +73,10 @@ func MapCustomBoards(customBoards []CustomBoard) map[string]api.CustomBoard { id := slug.Make(c.Name) configs[id] = api.CustomBoard{ - Name: c.Name, - ID: id, + Name: c.Name, + ID: id, + Display: c.Display, + Filter: MapFilter(c.Filter.Include), Namespaces: api.Namespaces{ Selector: c.Namespaces.Selector, List: c.Namespaces.List, @@ -105,3 +107,25 @@ func MapClusterPermissions(c *Config) map[string]auth.Permissions { return permissions } + +func MapFilter(f Filter) api.Includes { + if f.NamespaceKinds == nil { + f.NamespaceKinds = make([]string, 0) + } + if f.ClusterKinds == nil { + f.ClusterKinds = make([]string, 0) + } + if f.Results == nil { + f.Results = make([]string, 0) + } + if f.Severities == nil { + f.Severities = make([]string, 0) + } + + return api.Includes{ + NamespaceKinds: f.NamespaceKinds, + ClusterKinds: f.ClusterKinds, + Results: f.Results, + Severities: f.Severities, + } +} diff --git a/backend/pkg/server/api/handler.go b/backend/pkg/server/api/handler.go index 6bbc5271..f28b3ed2 100644 --- a/backend/pkg/server/api/handler.go +++ b/backend/pkg/server/api/handler.go @@ -199,6 +199,11 @@ func (h *Handler) GetCustomBoard(ctx *gin.Context) { SingleSource: len(sources) == 1, MultipleSource: len(sources) > 1, Namespaces: make([]string, 0), + Display: config.Display, + Severities: config.Filter.Severities, + Status: config.Filter.Results, + NamespaceKinds: config.Filter.NamespaceKinds, + ClusterKinds: config.Filter.ClusterKinds, Charts: service.Charts{ ClusterScope: make(map[string]map[string]int), NamespaceScope: make(map[string]*service.ChartVariants), @@ -210,7 +215,17 @@ func (h *Handler) GetCustomBoard(ctx *gin.Context) { query["namespaces"] = namespaces - dashboard, err := h.service.Dashboard(ctx, ctx.Param("cluster"), sources, namespaces, config.ClusterScope, query) + dashboard, err := h.service.Dashboard(ctx, service.DashboardOptions{ + Cluster: ctx.Param("cluster"), + Sources: sources, + Namespaces: namespaces, + Display: config.Display, + ClusterScope: config.ClusterScope, + Status: config.Filter.Results, + Severities: config.Filter.Severities, + NamespaceKinds: config.Filter.NamespaceKinds, + ClusterKinds: config.Filter.ClusterKinds, + }, query) if err != nil { zap.L().Error("failed to generate dashboard", zap.Error(err)) ctx.AbortWithStatus(http.StatusInternalServerError) @@ -322,7 +337,12 @@ func (h *Handler) Dashboard(ctx *gin.Context) { return } - dashboard, err := h.service.Dashboard(ctx, ctx.Param("cluster"), sources, namespaces, true, ctx.Request.URL.Query()) + dashboard, err := h.service.Dashboard(ctx, service.DashboardOptions{ + Cluster: ctx.Param("cluster"), + Sources: sources, + Namespaces: namespaces, + ClusterScope: true, + }, ctx.Request.URL.Query()) if err != nil { zap.L().Error("failed to generate dashboard", zap.Error(err)) ctx.AbortWithStatus(http.StatusInternalServerError) diff --git a/backend/pkg/server/api/model.go b/backend/pkg/server/api/model.go index a41bd5fb..308140b5 100644 --- a/backend/pkg/server/api/model.go +++ b/backend/pkg/server/api/model.go @@ -27,6 +27,13 @@ type Excludes struct { Severities []string `json:"severities"` } +type Includes struct { + NamespaceKinds []string `json:"namespaceKinds"` + ClusterKinds []string `json:"clusterKinds"` + Results []string `json:"results"` + Severities []string `json:"severities"` +} + type Source struct { Name string `json:"name"` ViewType string `mapstructure:"type"` @@ -58,6 +65,8 @@ type CustomBoard struct { auth.Permissions `json:"-"` Name string `json:"name"` ID string `json:"id"` + Display string `json:"display"` + Filter Includes `json:"filter"` ClusterScope bool `json:"-"` Namespaces Namespaces `json:"-"` Sources Sources `json:"-"` diff --git a/backend/pkg/service/model.go b/backend/pkg/service/model.go index fa461755..1c169fd3 100644 --- a/backend/pkg/service/model.go +++ b/backend/pkg/service/model.go @@ -56,6 +56,7 @@ type Total struct { type Dashboard struct { Title string `json:"title"` Type string `json:"type"` + Display string `json:"display"` FilterSources []string `json:"filterSources,omitempty"` ClusterScope bool `json:"clusterScope"` Sources []string `json:"sources"` @@ -69,6 +70,8 @@ type Dashboard struct { ShowResults []string `json:"showResults"` Status []string `json:"status"` Severities []string `json:"severities"` + NamespaceKinds []string `json:"namespaceKinds"` + ClusterKinds []string `json:"clusterKinds"` } type ResourceDetails struct { @@ -152,3 +155,15 @@ type ExceptionRequest struct { Category string `json:"category"` Policies []ExceptionPolicy `json:"policies"` } + +type DashboardOptions struct { + Status []string + Severities []string + Sources []string + Namespaces []string + NamespaceKinds []string + ClusterKinds []string + Cluster string + Display string + ClusterScope bool +} diff --git a/backend/pkg/service/service.go b/backend/pkg/service/service.go index 76477b97..c703a902 100644 --- a/backend/pkg/service/service.go +++ b/backend/pkg/service/service.go @@ -391,15 +391,15 @@ func (s *Service) ResourceDetails(ctx context.Context, cluster, id string, query }, nil } -func (s *Service) Dashboard(ctx context.Context, cluster string, sources []string, namespaces []string, clusterScope bool, query url.Values) (*Dashboard, error) { - if s.viewType(sources) == model.Severity { - config, ok := s.configs[sources[0]] +func (s *Service) Dashboard(ctx context.Context, o DashboardOptions, query url.Values) (*Dashboard, error) { + if s.viewType(o.Sources) == model.Severity { + config, ok := s.configs[o.Sources[0]] if ok && config.ViewType == model.Severity { - return s.SeverityDashboard(ctx, cluster, sources, namespaces, clusterScope, query) + return s.SeverityDashboard(ctx, o, query) } } - client, err := s.core(cluster) + client, err := s.core(o.Cluster) if err != nil { return nil, err } @@ -407,26 +407,30 @@ func (s *Service) Dashboard(ctx context.Context, cluster string, sources []strin g := &errgroup.Group{} combinedFilter, namespaceFilter, clusterFilter := BuildFilters(query) - combinedFilter.Set("namespaced", strconv.FormatBool(!clusterScope)) + combinedFilter.Set("namespaced", strconv.FormatBool(!o.ClusterScope)) - namespaceResults := make(map[string]core.NamespaceStatusCounts, len(sources)) - clusterResults := make(map[string]map[string]int, len(sources)) - showResults := make([]string, 0, len(sources)) + namespaceResults := make(map[string]core.NamespaceStatusCounts, len(o.Sources)) + clusterResults := make(map[string]map[string]int, len(o.Sources)) + showResults := make([]string, 0, len(o.Sources)) mx := &sync.Mutex{} cmx := &sync.Mutex{} - status := s.filterEnabled(sources, func(c model.SourceConfig) []string { - return c.EnabledResults() - }) + if len(o.Status) == 0 { + o.Status = s.filterEnabled(o.Sources, func(c model.SourceConfig) []string { + return c.EnabledResults() + }) + } - severities := s.filterEnabled(sources, func(c model.SourceConfig) []string { - return c.EnabledSeverities() - }) + if len(o.Severities) == 0 { + o.Severities = s.filterEnabled(o.Sources, func(c model.SourceConfig) []string { + return c.EnabledSeverities() + }) + } - combinedFilter["status"] = status - namespaceFilter["status"] = status - clusterFilter["status"] = status + combinedFilter["status"] = o.Status + namespaceFilter["status"] = o.Status + clusterFilter["status"] = o.Status var findings *core.Findings g.Go(func() error { @@ -436,7 +440,7 @@ func (s *Service) Dashboard(ctx context.Context, cluster string, sources []strin return err }) - for _, source := range sources { + for _, source := range o.Sources { g.Go(func() error { result, err := client.GetNamespaceStatusCounts(ctx, source, namespaceFilter) if err != nil { @@ -458,7 +462,7 @@ func (s *Service) Dashboard(ctx context.Context, cluster string, sources []strin return nil }) - if clusterScope { + if o.ClusterScope { g.Go(func() error { result, err := client.GetClusterStatusCounts(ctx, source, clusterFilter) if err != nil { @@ -478,41 +482,46 @@ func (s *Service) Dashboard(ctx context.Context, cluster string, sources []strin return nil, err } - if namespaces == nil { - namespaces = make([]string, 0) + if o.Namespaces == nil { + o.Namespaces = make([]string, 0) } - singleSource := len(sources) == 1 + singleSource := len(o.Sources) == 1 var exceptions bool if singleSource { - exceptions = s.configs[sources[0]].Exceptions + exceptions = s.configs[o.Sources[0]].Exceptions } var findingChart any - if len(sources) > 1 { + if len(o.Sources) > 1 { findingChart = MapFindingSourcesToFindingCharts(findings) - } else if len(sources) == 1 { - findingChart = MapFindingsToSourceStatusChart(sources[0], findings) + } else if len(o.Sources) == 1 { + findingChart = MapFindingsToSourceStatusChart(o.Sources[0], findings) + } else { + findingChart = MapFindingsToSourceStatusChart("", &core.Findings{}) } return &Dashboard{ Type: model.Status, FilterSources: make([]string, 0), - ClusterScope: clusterScope, - MultipleSource: len(sources) > 1, + ClusterScope: o.ClusterScope, + MultipleSource: len(o.Sources) > 1, SingleSource: singleSource, Exceptions: exceptions, - Sources: sources, - Namespaces: namespaces, + Sources: o.Sources, + Namespaces: o.Namespaces, ShowResults: showResults, SourcesNavi: MapFindingSourcesToSourceItem(findings), - Status: status, - Severities: severities, + Status: o.Status, + Severities: o.Severities, + Display: o.Display, + NamespaceKinds: o.NamespaceKinds, + ClusterKinds: o.ClusterKinds, Charts: Charts{ ClusterScope: clusterResults, Findings: findingChart, - NamespaceScope: MapNamespaceStatusCountsToCharts(namespaceResults, model.Status, status, allStatus), + NamespaceScope: MapNamespaceStatusCountsToCharts(namespaceResults, model.Status, o.Status, allStatus), }, Total: Total{ Count: findings.Total, @@ -521,8 +530,8 @@ func (s *Service) Dashboard(ctx context.Context, cluster string, sources []strin }, nil } -func (s *Service) SeverityDashboard(ctx context.Context, cluster string, sources []string, namespaces []string, clusterScope bool, query url.Values) (*Dashboard, error) { - client, err := s.core(cluster) +func (s *Service) SeverityDashboard(ctx context.Context, o DashboardOptions, query url.Values) (*Dashboard, error) { + client, err := s.core(o.Cluster) if err != nil { return nil, err } @@ -530,26 +539,30 @@ func (s *Service) SeverityDashboard(ctx context.Context, cluster string, sources g := &errgroup.Group{} combinedFilter, namespaceFilter, clusterFilter := BuildFilters(query) - combinedFilter.Set("namespaced", strconv.FormatBool(!clusterScope)) + combinedFilter.Set("namespaced", strconv.FormatBool(!o.ClusterScope)) - namespaceResults := make(map[string]core.NamespaceStatusCounts, len(sources)) - clusterResults := make(map[string]map[string]int, len(sources)) - showResults := make([]string, 0, len(sources)) + namespaceResults := make(map[string]core.NamespaceStatusCounts, len(o.Sources)) + clusterResults := make(map[string]map[string]int, len(o.Sources)) + showResults := make([]string, 0, len(o.Sources)) mx := &sync.Mutex{} cmx := &sync.Mutex{} - status := s.filterEnabled(sources, func(c model.SourceConfig) []string { - return c.EnabledResults() - }) + if len(o.Status) == 0 { + o.Status = s.filterEnabled(o.Sources, func(c model.SourceConfig) []string { + return c.EnabledResults() + }) + } - severities := s.filterEnabled(sources, func(c model.SourceConfig) []string { - return c.EnabledSeverities() - }) + if len(o.Severities) == 0 { + o.Severities = s.filterEnabled(o.Sources, func(c model.SourceConfig) []string { + return c.EnabledSeverities() + }) + } - combinedFilter["severity"] = severities - namespaceFilter["severiy"] = severities - clusterFilter["severity"] = severities + combinedFilter["severity"] = o.Severities + namespaceFilter["severiy"] = o.Severities + clusterFilter["severity"] = o.Severities var findings *core.Findings g.Go(func() error { @@ -559,7 +572,7 @@ func (s *Service) SeverityDashboard(ctx context.Context, cluster string, sources return err }) - for _, source := range sources { + for _, source := range o.Sources { g.Go(func() error { result, err := client.GetNamespaceSeverityCounts(ctx, source, namespaceFilter) if err != nil { @@ -581,7 +594,7 @@ func (s *Service) SeverityDashboard(ctx context.Context, cluster string, sources return nil }) - if clusterScope { + if o.ClusterScope { g.Go(func() error { result, err := client.GetClusterSeverityCounts(ctx, source, clusterFilter) if err != nil { @@ -601,41 +614,46 @@ func (s *Service) SeverityDashboard(ctx context.Context, cluster string, sources return nil, err } - if namespaces == nil { - namespaces = make([]string, 0) + if o.Namespaces == nil { + o.Namespaces = make([]string, 0) } - singleSource := len(sources) == 1 + singleSource := len(o.Sources) == 1 var exceptions bool if singleSource { - exceptions = s.configs[sources[0]].Exceptions + exceptions = s.configs[o.Sources[0]].Exceptions } var findingChart any - if len(sources) > 1 { + if len(o.Sources) > 1 { findingChart = MapFindingSourcesToFindingCharts(findings) - } else if len(sources) == 1 { - findingChart = MapSeverityFindingsToSourceStatusChart(sources[0], findings) + } else if len(o.Sources) == 1 { + findingChart = MapSeverityFindingsToSourceStatusChart(o.Sources[0], findings) + } else { + findingChart = MapSeverityFindingsToSourceStatusChart("", &core.Findings{}) } return &Dashboard{ Type: model.Severity, FilterSources: make([]string, 0), - ClusterScope: clusterScope, - MultipleSource: len(sources) > 1, + ClusterScope: o.ClusterScope, + MultipleSource: len(o.Sources) > 1, SingleSource: singleSource, Exceptions: exceptions, - Sources: sources, - Namespaces: namespaces, + Sources: o.Sources, + Namespaces: o.Namespaces, ShowResults: showResults, SourcesNavi: MapFindingSourcesToSourceItem(findings), - Status: status, - Severities: severities, + Status: o.Status, + Severities: o.Severities, + Display: o.Display, + NamespaceKinds: o.NamespaceKinds, + ClusterKinds: o.ClusterKinds, Charts: Charts{ ClusterScope: clusterResults, Findings: findingChart, - NamespaceScope: MapNamespaceStatusCountsToCharts(namespaceResults, model.Severity, severities, allSeverities), + NamespaceScope: MapNamespaceStatusCountsToCharts(namespaceResults, model.Severity, o.Severities, allSeverities), }, Total: Total{ Count: findings.Total, diff --git a/frontend/bun.lockb b/frontend/bun.lockb index a1e69bef09697aab9222f6f19dfee4306a1fe9c8..f34541f272a8298f89ccfbe1a9492bfdfddcf99e 100755 GIT binary patch delta 19421 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 delta 21487 zcmeI4cT^PT|Lja?|?DVKAGj#|2wr%&D}n_BvcFug3@+jrTnH2KHAf}lm|ba!l#AM_mDiMR(So3$Cd8a zIicXK@G1F4_*9+qN%>(dmw3}tO1BU5War@1;77uzTE|9@b!**95LP5A=@y|F%04M- zTxbl|W1}-lHOAtf!>8f=T#eGJQgA9SA|^CCI&``)ep1vJEF^DXPqrY`MZ|hoYJM^- z4MSwa3MVbLh>hLYcFuu@(Ue58+05WSQRIsw#0 zhL}jhzzcd5L=)vus1)2E1=2c=4vUGJ^mUlfr$|}n-C(JqI#}xXRivi@JIC!oSmGU# zA8kx%${SV-)=FrDmfuub`~a3ReoR#=ehf=JwklCJP#P@tD5XUHt8Q=8S$CCWQ|`*W ztX$$t@A2kiNznLgyRVei7Gdg^{{v;0H-=BmnK?%(Ei5i^}8a6y){4|V}F!7;mYVA_<9_pud ze1&?c{7H}G&ekqs?t_f4tX(Yno+=98C{I0AwAJeGUtiVVKd(pL$^O1CI@gh}tiRWC zz++8q;ji*~i6?743%=Fs#K+^Sx*Z=oHmC9P=<)4+dtZ%t-*bOgsn9348d2_HwMsT}JbyQPWZcDLQ#=t(cEoE09kBnubFV&P?*B}sM(Lr|g^UzA` z1i=UG6692!PSYP=SJ}bGTeB3Qc5?Af8}!ZXZUdid+pqi~AMvo(zCc79WKhYsTj{F1 zt`~$(02ts>!Rpn40owhqXUwCRc z)K90K4X>5KOYNZ(_id1Ecll}`0(t_9lwEY%D5vc56@xd*1u$tFW!v4p8ex+l_{l3< zd29U;qVhyJw7X6-3!ax8($-tM?-NHM8uQJRKE%gc+Z!QFE~WR;X?}*M^gNrm95UQn zD{c`44`cP(R`95Nl^ohxr=0?i`eGOc%~p7xG;Z1wgs2}F&p@46bE}-P*H_zhs~|N0 zRL~T7RIEx)_19_FeD;EM+Dq`LQHblR)0%8kDlzn5(-~ebxfoqtfRGcsQg zTx8c!RtSX~!t-~(k2vZM+z@v5vvhy|c z4jv7YiR_Ggmb(R^FFXtyW;hgHZyF}e4urbPD?Pn6FA?f$nDurU%HXT1Mlmi!wmsyl zeFo@mC>$f9ZNBHz6rvd;;ZdVdI5tQMyv}mSx8B+!&e26IZR@?2R;o4>UT;Hr7;fzj zc&*@J@Yi{n!08M}W60KEpK-J)VJJMR3JI}4R>JEEPn2(O(`o;Nr>r*`8&}*DC~KYK z{>~JH!JkqFW(mTuPu@m&BR+ZJ?}9M$lQ$aP=uh4ecw;_!_4ilKHw)h9d`0jEeoE;3=?xEPuI!&!pbac@H z>W>h$8bh;9r}+_HPkH4KtaOBElj306>}7J=c=XU-i-Fh1upKrbq;#1UIh?MB9O$ap z;*4By!B-P@2E8%t!Bm90BN~0j%vU}8X?AfA4}piBigSB8LbM_@G*j9e@ciHz)`!L> zn;Kob-ba-!7hLkyWWyh5D7)S{Wt1e^uG(;T9SylsQO03-G@UqY;Jt!JE!N7;cf3r_ zD-ISAt~7Jc%PE(Awb>k+7)CrQM@eEhL$zDr(cED5w(~OK4n`Eq&-sGVRHflj@MtV> zaP{;ufkS=5sr;=@`;2>4Xs)y^E-EEreVo>bu@`0AYrfiJfRq7;Ku4WMxI{}oq^Gy& zcS$Zl^ke`&WjGZ!Q#2CesDfAa*+Z9HbLE0OUvXTnYu;Ay~VZH<&+{{vGlrJ0Mk5Qwk`J6M&=tYh|XC$ z@sE5trPx>Oc|$Hh+#1{_Qzh7;dYz^S9txW7qbiV7Zu)8m6(}Pi$e|-~a>Ju0D+8GW zuP;1h-?|nWCzO^-IJ_ZlTcnZ%+y~=cq?z3JwAKXB6Oc9!&dxiEhaG|$8}=XGBJLU5pgjeT23D!^Z+M;H zRX5!5=Y@9gI+Gb59U-hV0IVblSe~sQt=yAsJrpPhJH)<#eVc z*y6q*1Q_GQZ|}>)%Fum48hf;?vrc?}UoHR~{6MJ>Q&6PS%J7uUOZ)pRJX)C8%ALJT zN|g$5y$IB4m%&pK(xosD9yLX|vuIeR)PrLRO&JM~wvH0F6drAO93YtgJf2e7Hx}he z96XG1UwEC7Qn__o0*}@djvc$vVVwX2NLMeZq;JO1(M z6i4sE;L(UF`<-zQI~*N43vZxO0h;3QNU5%xq0w>hlrsy;eKUHYc`4V%2oxwlykCG;IDwyz@~D$5f=Z1t@w`_tX?gA1!D*Z zR6r)ha!VNxa{vE@Rk0kEsxd24sZ8RjuEV@+vQ^=q;(ldIo#o#*{vWLt=`Qp9l`YkC z1)S_vZm+3@c({Uqz$Z(if1UeZvJ_RwB&+@o^t=GTB+XvDB?LHsD&?h z0hMI)$x``|+(vOumWrFgc{Jx_ zDSsTS#1b7;R`IlQj0@mPmg0Y)SV#reQKXbpbB|eAq%CafZ3|0FsS}T{Y$;!7&dE{(x^lm3yfIM8 zQiAT>CrbtN;C^LG4GiG%fjpip1^eSarm!PmsY9b+shrUiS;;#86IAwTjgQyony9M% zdk0{+6QBX9eC4N+B%en3-#dVR?*RV21K%lA;m7oI3vyFd z)Z2Zf@tw?W!KRuE2&256@AR{Sv-5CH2az zQ^Pm+t(%TJGnMe6#s{)=_s2hK*AkaKLd-gZTn!7>gQH&JzBJH*JczOVBb31DlM{g z=KI3!%@Qj$PpaI!S5eCkZ1ejyaz?;czD^ak&20npp1NCO#{6A1(S4=&l+3C-(+V7d z^K8=1R-Ne`;IyvA!;U`>dop`Yz3S(74j5I-ZYS$-8O?jK_2a@H_m<7x@!X@+?3DVq z_g)x%^6F>oA1_oadDj2K#71{6b}Q~UaD-dW)z;?m`=@)ps!`+SpEeca zjy(A5V}m+#TFrVGU@_ss*3uP0I}e3~|HU3t^Xgi5NK2d^v1-w#EsoRX#r>*1QCnwCZ#<`BqhHCcY2)rse{#6XnB7AHV_)Yd_wcOLysDL( z=TK*m|IQy;X1@E-W3bg#-$;GK1(xUb#HiOKZCDcCI;Y5a^w|cB9=Y30%4o087#_U2 zq{)__&G-It{mK0Fw&r=aCaBoTD`;M)G57O7ZrfRZWa=BK&FojfLzkBoqNVs{pmw(5nDv2pl9(hiR_?#N`7-UIVaV zSp-~f0663U*szE^fP4Zu1nij2b%41A05h)xIIyz>yb1x_@&O##^n8Fa0>uQJS(6(8 z%ZdOJZvZr4Hwg4B2Ix=#(1^_|0C-K{F@eUcT_M2cn*b{d0h+LK0>QTcf{Fl|vE(8E zlM;Xm0&dK|7$B2CS}{Ni_J+WO+W;Xq0a~&xHvz2g0GQnZXw8P+0ysn9Ac3|_TLKVw z7a+0(K*zEOxZYEl1twHKSfrb9W$-tXzDtUIxw=~Ql9?NwTIMBpIDPKX((is6GojhF z*rX|i^VNAvtQXd<^2>0~3ysU|`lB6vfW&(MUDyo*J<9+(+z05!=G|8f zQFUjx$@F0D9>DZui^=%2ax%SGr&5>zmP{s)JtGsu{L5f^vs5yD*c&o^S-*0aeryYw z{!H}{W&j&XW+2-|W)Rc<0W+9|k{QCX$OJRXM=(QK1esy%D4F5R<}nh?eTt^ee2k`s zu(Jfbo&mW12{4LH{}Z6>naZ5q{8QCS70Q}C0a*4Ib@vHUjAb_n^n6a;eF_l9<~;>? zP2e$s@vPl5fXy!eRz3rWVC4jYUjhXE1u&5%{{>+3H$Vk}Nap_>Ad^7abATxJhQNeZ z03j~`rm`(B0IXjFn7ssuVMAX6oFQ@tKu5SSxUI9#J5w8I9 z3FHv?j@i5hnEMW3=4*hN>?{GV_W*8h0OHy7HvnY>iV1wrn!E*ARsoRs7T`yAgFw#@ z03F@|{KV$H19(l~F@ZU(-Ftw|9|2ar2bjmo0g{463`Yh0Us!Sl5}2p}DhMPp{|^9} z1kyeLEM#v8Ob`J=J_0OeTRs9`O=4OV{5UnzN2&lA%Oc>a z0dNojl39cZkWV0oz;b3I0nDueFjE3p$<7k+stVwy23XCes{zUg6cb2gO*8<@OaKx! z0PENd0zI_=9jX9qVDqW~ye9CNz$VtND!^t_fR$ANwy<&n!PNkQOaQjAWD@|B>Hrl4 z(wM&%Ad^6v7GMW^Ltug#K!_>8F1E!Kz}g(ZtQtTD8(IzE41t3L_A+gCfVdg}k<|h8 zEQ^4v1%Lw$U>1uo1IQB#>4c;39iNV1l)17O3vjGvDvwg;6`CVfSC{mb$IlD=etu*yo#X zZajZpI$%!CAjj;QYwDd(oOn??vv~2^y%iSoJ`VrbW6;s-izoNWaqLp*u*_v!>Y!L_ z8`NV~7xi3WL+b*ZA#jkuHKw%!h_eNVv;w%!vIw}^0XSHT0f9FvU)<0A^k`%4EvugO zw#xWYkhuLyKFa%AM3cJ}nu+W8tQ$D(w=H+Ax1WnY@NxO8 z=5L$1e5GgS?L-@vgJM;M%*F=w%(X{7Gi?Bh*;xW!4ghYp0Jqq5TYxeG#RP7%CUyYJ z>H#F$0o-Lb2=sIW=wJ_UpUtxecun9jfl}7a0bsKez)A;za#l_t*cl+G9>5d`7J+t7~lb z>DXzhnfKwVDMNaFRbGb`yNcGV80Cs8i8XZ*2aBS_ejqDJtbnXqVyzm(Y9uzFY!!*! zAzM{q?OkC_B(?-rrPY_aiodwBgb*>%@E>`NhdrK3RkQak#n{hSiQ74(AsanNG||8B zFV0YDa(3V$VfL)Ic$a8bjpH8tc&m3FEe=tA&S@;r%=jqwnS*#DSxJSs+J?$3bfSg6 z>rl~4C5E47vxbQUnm*ZhAp9SN`B7ri$>Fhvzmqmc^8Oy^@?i zz5Sw4UvIQHYT4)Rd?D1qbhC=S48~6uS~lmk*nxgT{qRxw5dIC5kBx2k#@Oi7cV@IE zn)C1GlPjg69~$VB0!C@*JAZS|R`7I`KMA7GO3oM#hzLHnc|vOAJPPlOCgw9gk{;s31hqGI`HgbA*R+Rslwn>ITi_Mbt&dsqgO4e9mkD zs532~9}u8*WXl2ljn`^3a2B7Iu5&m8K0@~ z;;cK^7CtiV!0?YAK@w8HXifM4P{lnVRA0b9UXch35P(Lqh09ukgz8ufk< zwUCCqCr^kcvV{G7O8hw+0QNg)y*L{Pwug^k0B3{1mV?pM(D6XggQ4*VbcXH6(+xrW z!UUeVKZn6!!JG}?Y$#Z7&IWQe42&M1peY>0*>Hrr@^pha8v%9^quLF22xlP(r-Q}Q zyasbP5@F*5VnaC_1r~rF(|ir%Y&61j0MchTXQ2qEayEjqF<|ug1${y|8;dYKT0)g_}zalI}}=v~-1U3N>R9Dyff~BZ5wlGgKdH05yafK`u~Z$Q5b=Rl{G6 z(z&{(2%11z=mVPZ5fZAf6jAaGatCV-jf3cC{7`5tbPEkBf#@Lxx)|ypFQ^^V9`c6h zg4hA_g*rlhtdS%+YUt6V_RL?B8aJXR3I;<%A-Yu3lLvGqyakm&cc6PLNs?}0H%|g4n8^% z>F|7wxWCy{jr5I{o|K@+Dmt-e8p*NFUs!=f&^@RWqNDc_YlNHMczXQgJaiE{03C!5 zL5CrFk|rIZgOHxOp~G)J^b52AqJwWCvtxstZ{`){qT!5^bcz@d$JjItCqwPCy%>P0(hD4of-+7eb4m1#D(j zX`<@6zPzd=sp7ArC_GwYc%0`L>~SayqNjuCG@+-57_ME{F~V znI&nZ)giy5ti2FD^0*0>9+O%DO@(M*_lNpI{h-;1KZYRC>&m&ThVBL!XAmP3im z%}jE%`5VD^&`XG}QSTvonBoZwGm{#7W+3xYh)(_SP#81>q63-^Upj1eLupU}8h-=& zP0bFONv-1dA!1EoSyP$V=0>H}Ry-tOEUL^uf=j7|3p9p4CTKwLCL$Klsd zC`3ozOxW+BIOuoyWmd|mH=FJtu*@c!OAd{+2tPvPLx^r~%3$w9_$RzWC3G`QN^W+79pTe$=^n)UB21UN-B3gL$4>0Pp6yU~ zio-V)l9i?busNIVB-sU;A;=K345~n`>hP;U5~PCYPfQVZH{zK9D!m9twg2Aez~(ur#YQo75OrSemFV;7N3QNHftHP7S~{2-EFzC-{C46+oFO zt_S=}u-#z0L#Gfv3H3twFswi9L0Gz_>xnS^rWOcGcUE-!H=ZzEm6E*;mxZSv7^Z zHIVG$zenU+C>4r@)<7vxG(>U4VxV{^0h$Gcqf+8Cpy^N?^bIr(`WpHU`WBiAQ9epL zA5x?5RK}0c4-m?UH$={cGY_JrG6$Lq{S5U&hMze56?PG{5K4p=K)*o7L8f^6k(^e- z5@<2B6tV*Q4fX>XEyL2^yvaQ7HxVXOzppytUqT#0A~{V_8RzgD39VX(vxA+5LbE6(S9ockAi5= zg~QUJ*b#OLd)7$mqP>IQE$BAu;38QKD@Bl2`UBX*urLBL8hm4zYAJ_f0!usn4=4&@ zy45Z~VhNU}fX-|+!ha$jUvs9rNcFH6a$F?G_$t6Dh!$W!BDw;4iLCEoA460%6>$cP zveT(eCo`SOdl62BeMjjKegpdo`Wt!)y?~xW&me#3Df9&T3p#-G8|i%~ui;R_w-B}c z9wMkBs`vw&*I2SM)u3%O6!_*gy|Gl+Tm_$^seAZ#_i|&&#WYj7zzXj9Omvm#O9nyT z*j4&tS{vhQZHzBO5LJ)Y8oqoHQxAU9OF9`3cDm z#8a};4__tyqOwoMY+$7)?Ad(h+Zwv`7ouubD??_yibpT5k?d7?2@i8hm8?D2A}PN2 zOgP)I;QXPv%l9B@OC)VA2*$Vk1SKER-8Eg1E~qrz zp25bEJ(j_iZIHT(=QG&N4bnidAcM8ph`LKN^gnErJXK=vz53rbNuo-;zmFZ-jF@me ztG-1F^fbQf$GYr;d%(hJr+E9(GyXcDgl0kIcV_LpdRC3OX-mx$lJ)G@Ef^K!3xu3b zJeBfWcI)>!h4B?bOVg8bzWyV3_UD*#Ju5<9d*e%qZnf|2Iq+GRsLv_tXEKYeDBbuP zqZR{;+vOH^?({i@@kK{NcR!prs>c4GKF55Y$wpFM<;{x>KdE*Ife1%Nq4*a zI`8C#prp?+g_&$S^4j-3q`cqehE401rrL!)4D-^GIv#q6m2Ab>O+Ccix1ko}dynk% z-WTeI?4^`A4QN#u-;7kvy#BAlIt?&4l!3X!74i_9fxPy{cPX8j>};NUW#3+;Xl2aH zc5agr?8}cTyJn$F>Wq1-`u$#8)f8h-pBG12;C7TD9#dA(vzN`c{;W=Y^ErjZF@3^z zX|55%>NxY=fp#0;jCAg?Cb*N!VNcZEM%lVq$CZ<={s`Mi%hxn^ zG_;#Gf^hOUTY$Xw#+Nt^Yx&%M)YK}Yk-`&sF*-_B=o!kuSIY_id+BvfuopWpevv0w z=bhLA|2^g56Kv5=Dctko38jboFFzQwIcCosLwj1&I69wHM#s6Z#mL4Zol9}1v~@R( zu<0q*d>8h_xl?R3*{#RfZ@aMX8l7S9cj4st@3kW3xZOzc#j*`6p!5tozZ-k`;tBR( zH+o=vx70cB?%wRX4ShZzg3c#dn+(*xD4R_no1D#7WMKT(XDdguP0gB<1~#a16+Pl> zW=}S|lp&22H=JW0d!+W_#Pe*@9@G_gp8dQ>YE|1VN6Gp6?ROK71^>1|RH<{=`8|?T zZ9l}&NiuHe@nfrMtkH<7W;sjuB4P|8%n;Gi^w}RxyVq8Ws@^%Q&0cA(72}LcHuUd#lF*WBjdh!kZZh>W+p@!q52sesp?XWiv7{Ws|S!?`6`JWKkXy ze@CTxd93g67QxqSSkgswZ?U{&^GPK|G) zT56HdsdY$tH{`%6jXXlq4K{hd6mD;P?bOvZ9UpeO{eA?x&UYl{^67rmYJ9O&>wV5i zJH-v}L>0D%;XFQ9$b1i=(^m^w@Byi7!{S2RRHJs|E2`Fg_4TyrABH<9Wn#k!-HO-| z%F(BY6&#St#0NLoiG%3jlbfvOA*_OTH(B%{tOBbNmVO8mlTu>%)h@vz_{z6+>eO5> zs&K8NjVYv+useq^0m?PNjdh$URb@`;_)})*VX0KrjwSS$9N6{clDnGrDVw@NYRl}8 z;+`aOAR_u6mG+5K?lO~On7mncna?pPMJ&Ci&p$?&i*5H=(Q&D%xaU4IIUzZT{txs` zPtd`V`+)5^h4$WipchX|k5pn@DSLVbV0J0Jzzby*b{2a-TUCNQ3#s_Uz4x-s!ZO^OX%s)(uh=~oG zjKA+1pNza>o}Lkrp&T+BH+-9L{nV2`DSiSHc-=Ysbj)C@k^hW0z9g+d-u$Df>t}p} zm!-LX6@x#lcNG-6TvsJEy{t^w+Ut8Qc3rWLpVbL~?pYQWW~JD#m{&Uxel7Tqx0%$O z8hQWSDEPFsO8aM@{;E*>&E#ou5!8#xO|%XTZ)GN=-BvoVv|wTVh!w9IQoP&b@j`si z_3wJ@9(C{w@v)SD=KT2Pg93f*cYP^=+t^7FPrMH7Jm;Dr{WJbdfijGT&g$P@l$KXh zwbU;yl#bZ3@7t)IS%R;+s(#ihX}+b(L$B4Uv+Ptl{qctCCPAuJ`o~k%%~hs``(C%P zc-J3mc~!D&`Ppyb*5b3{X1L{bj~)N5-;}5+Qz9nE#Iy*D8yhwSk2u(?>pA|DMnp|U zekBFMY|%IBMqLcWeJYmjO+DPkgvEwpNB&2_Ej^ejT-~tNew(QwG$yuKmqdr)TbxBk^vG?DpwfdNNwW}NEZN+vq g{yJN$Q9J36?@-q;Wo>RrPRt@$U0wenOWjNQUuOYxfB*mh diff --git a/frontend/composables/infinite.ts b/frontend/composables/infinite.ts index 74afea1a..aeeda378 100644 --- a/frontend/composables/infinite.ts +++ b/frontend/composables/infinite.ts @@ -1,7 +1,7 @@ import type { Ref, UnwrapRef } from "vue"; import type { UnwrapRefSimple } from "@vue/reactivity"; -export const useInfinite = (list: Ref, defaultLoadings = 3) => { +export const useInfinite = (list: Ref, total: number, defaultLoadings = 3) => { const loaded = ref([]) const index = ref(0) @@ -20,9 +20,8 @@ export const useInfinite = (list: Ref, defaultLoadings = 3) => { } if (oldLength > 0 && oldLength < length) { - loaded.value = l.slice(0, oldLength + 1) as UnwrapRefSimple[] - index.value = oldLength + 1 - + loaded.value = [...l] + index.value = length return } @@ -60,7 +59,6 @@ export const useInfinite = (list: Ref, defaultLoadings = 3) => { } } - return { load, loaded } } diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 063e7f20..41324ad4 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -13,9 +13,8 @@ +