2024-04-11 23:04:30 +02:00
// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
2022-07-29 06:29:21 +02:00
// All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2022-06-21 17:52:36 +02:00
package routerConfig
2022-02-03 07:25:36 +01:00
import (
2022-03-24 10:35:52 +01:00
"fmt"
2022-02-03 07:25:36 +01:00
"net/http"
2022-02-03 11:35:42 +01:00
"net/url"
2023-07-19 08:25:14 +02:00
"os"
2022-02-03 11:35:42 +01:00
"strconv"
2022-02-03 07:25:36 +01:00
"strings"
2022-02-28 11:08:43 +01:00
"time"
2022-02-03 07:25:36 +01:00
2024-09-24 11:13:39 +02:00
"github.com/ClusterCockpit/cc-backend/internal/config"
2022-06-21 17:52:36 +02:00
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/internal/repository"
2023-07-19 08:25:14 +02:00
"github.com/ClusterCockpit/cc-backend/internal/util"
2022-06-21 17:52:36 +02:00
"github.com/ClusterCockpit/cc-backend/pkg/log"
2023-08-17 10:29:00 +02:00
"github.com/ClusterCockpit/cc-backend/pkg/schema"
2022-07-06 15:00:08 +02:00
"github.com/ClusterCockpit/cc-backend/web"
2022-02-03 07:25:36 +01:00
"github.com/gorilla/mux"
)
type InfoType map [ string ] interface { }
type Route struct {
Route string
Template string
Title string
Filter bool
Setup func ( i InfoType , r * http . Request ) InfoType
}
2022-03-15 08:29:29 +01:00
var routes [ ] Route = [ ] Route {
{ "/" , "home.tmpl" , "ClusterCockpit" , false , setupHomeRoute } ,
{ "/config" , "config.tmpl" , "Settings" , false , func ( i InfoType , r * http . Request ) InfoType { return i } } ,
{ "/monitoring/jobs/" , "monitoring/jobs.tmpl" , "Jobs - ClusterCockpit" , true , func ( i InfoType , r * http . Request ) InfoType { return i } } ,
{ "/monitoring/job/{id:[0-9]+}" , "monitoring/job.tmpl" , "Job <ID> - ClusterCockpit" , false , setupJobRoute } ,
{ "/monitoring/users/" , "monitoring/list.tmpl" , "Users - ClusterCockpit" , true , func ( i InfoType , r * http . Request ) InfoType { i [ "listType" ] = "USER" ; return i } } ,
{ "/monitoring/projects/" , "monitoring/list.tmpl" , "Projects - ClusterCockpit" , true , func ( i InfoType , r * http . Request ) InfoType { i [ "listType" ] = "PROJECT" ; return i } } ,
{ "/monitoring/tags/" , "monitoring/taglist.tmpl" , "Tags - ClusterCockpit" , false , setupTaglistRoute } ,
{ "/monitoring/user/{id}" , "monitoring/user.tmpl" , "User <ID> - ClusterCockpit" , true , setupUserRoute } ,
{ "/monitoring/systems/{cluster}" , "monitoring/systems.tmpl" , "Cluster <ID> - ClusterCockpit" , false , setupClusterRoute } ,
{ "/monitoring/node/{cluster}/{hostname}" , "monitoring/node.tmpl" , "Node <ID> - ClusterCockpit" , false , setupNodeRoute } ,
2023-02-01 15:46:50 +01:00
{ "/monitoring/analysis/{cluster}" , "monitoring/analysis.tmpl" , "Analysis - ClusterCockpit" , true , setupAnalysisRoute } ,
2022-03-24 10:35:52 +01:00
{ "/monitoring/status/{cluster}" , "monitoring/status.tmpl" , "Status of <ID> - ClusterCockpit" , false , setupClusterRoute } ,
2022-03-15 08:29:29 +01:00
}
func setupHomeRoute ( i InfoType , r * http . Request ) InfoType {
2022-09-05 17:46:38 +02:00
jobRepo := repository . GetJobRepository ( )
2023-06-09 09:09:41 +02:00
groupBy := model . AggregateCluster
2022-03-15 08:29:29 +01:00
2023-06-09 09:09:41 +02:00
stats , err := jobRepo . JobCountGrouped ( r . Context ( ) , nil , & groupBy )
2022-03-15 08:29:29 +01:00
if err != nil {
2023-02-01 11:58:27 +01:00
log . Warnf ( "failed to count jobs: %s" , err . Error ( ) )
2022-03-15 08:29:29 +01:00
}
2023-06-09 09:09:41 +02:00
stats , err = jobRepo . AddJobCountGrouped ( r . Context ( ) , nil , & groupBy , stats , "running" )
if err != nil {
log . Warnf ( "failed to count running jobs: %s" , err . Error ( ) )
2022-03-15 08:29:29 +01:00
}
2023-06-09 09:09:41 +02:00
i [ "clusters" ] = stats
2023-07-19 08:25:14 +02:00
if util . CheckFileExists ( "./var/notice.txt" ) {
msg , err := os . ReadFile ( "./var/notice.txt" )
if err != nil {
log . Warnf ( "failed to read notice.txt file: %s" , err . Error ( ) )
} else {
i [ "message" ] = string ( msg )
}
}
2022-03-15 08:29:29 +01:00
return i
}
func setupJobRoute ( i InfoType , r * http . Request ) InfoType {
i [ "id" ] = mux . Vars ( r ) [ "id" ]
2024-09-27 13:45:44 +02:00
if config . Keys . EmissionConstant != 0 {
i [ "emission" ] = config . Keys . EmissionConstant
}
2022-03-15 08:29:29 +01:00
return i
}
func setupUserRoute ( i InfoType , r * http . Request ) InfoType {
2022-03-15 11:04:54 +01:00
username := mux . Vars ( r ) [ "id" ]
i [ "id" ] = username
i [ "username" ] = username
2023-01-27 18:36:58 +01:00
// TODO: If forbidden (== err exists), redirect to error page
2023-08-17 10:29:00 +02:00
if user , _ := repository . GetUserRepository ( ) . FetchUserInCtx ( r . Context ( ) , username ) ; user != nil {
2022-03-15 11:04:54 +01:00
i [ "name" ] = user . Name
i [ "email" ] = user . Email
}
2022-03-15 08:29:29 +01:00
return i
}
func setupClusterRoute ( i InfoType , r * http . Request ) InfoType {
vars := mux . Vars ( r )
i [ "id" ] = vars [ "cluster" ]
i [ "cluster" ] = vars [ "cluster" ]
from , to := r . URL . Query ( ) . Get ( "from" ) , r . URL . Query ( ) . Get ( "to" )
if from != "" || to != "" {
i [ "from" ] = from
i [ "to" ] = to
}
return i
}
func setupNodeRoute ( i InfoType , r * http . Request ) InfoType {
vars := mux . Vars ( r )
i [ "cluster" ] = vars [ "cluster" ]
i [ "hostname" ] = vars [ "hostname" ]
2022-03-24 10:35:52 +01:00
i [ "id" ] = fmt . Sprintf ( "%s (%s)" , vars [ "cluster" ] , vars [ "hostname" ] )
2022-03-15 08:29:29 +01:00
from , to := r . URL . Query ( ) . Get ( "from" ) , r . URL . Query ( ) . Get ( "to" )
if from != "" || to != "" {
i [ "from" ] = from
i [ "to" ] = to
}
return i
}
func setupAnalysisRoute ( i InfoType , r * http . Request ) InfoType {
i [ "cluster" ] = mux . Vars ( r ) [ "cluster" ]
return i
}
func setupTaglistRoute ( i InfoType , r * http . Request ) InfoType {
2022-09-05 17:46:38 +02:00
jobRepo := repository . GetJobRepository ( )
2024-08-01 18:59:24 +02:00
tags , counts , err := jobRepo . CountTags ( r . Context ( ) )
2022-03-15 08:29:29 +01:00
tagMap := make ( map [ string ] [ ] map [ string ] interface { } )
if err != nil {
2023-02-01 11:58:27 +01:00
log . Warnf ( "GetTags failed: %s" , err . Error ( ) )
2022-03-15 08:29:29 +01:00
i [ "tagmap" ] = tagMap
return i
}
2024-10-09 13:23:06 +02:00
// Reduces displayed tags for unauth'd users
userAuthlevel := repository . GetUserFromContext ( r . Context ( ) ) . GetAuthLevel ( )
2024-09-19 15:21:32 +02:00
// Uses tag.ID as second Map-Key component to differentiate tags with identical names
2024-10-09 13:23:06 +02:00
if userAuthlevel >= 4 { // Support+ : Show tags for all scopes, regardless of count
for _ , tag := range tags {
tagItem := map [ string ] interface { } {
"id" : tag . ID ,
"name" : tag . Name ,
"scope" : tag . Scope ,
"count" : counts [ fmt . Sprint ( tag . Name , tag . ID ) ] ,
}
tagMap [ tag . Type ] = append ( tagMap [ tag . Type ] , tagItem )
2022-03-15 08:29:29 +01:00
}
2024-10-09 13:23:06 +02:00
} else if userAuthlevel < 4 && userAuthlevel >= 2 { // User+ : Show global and admin scope only if at least 1 tag used, private scope regardless of count
for _ , tag := range tags {
tagCount := counts [ fmt . Sprint ( tag . Name , tag . ID ) ]
if ( ( tag . Scope == "global" || tag . Scope == "admin" ) && tagCount >= 1 ) || ( tag . Scope != "global" && tag . Scope != "admin" ) {
tagItem := map [ string ] interface { } {
"id" : tag . ID ,
"name" : tag . Name ,
"scope" : tag . Scope ,
"count" : tagCount ,
}
tagMap [ tag . Type ] = append ( tagMap [ tag . Type ] , tagItem )
}
}
} // auth < 2 return nothing for this route
2022-03-15 08:29:29 +01:00
i [ "tagmap" ] = tagMap
return i
}
2022-02-03 11:35:42 +01:00
func buildFilterPresets ( query url . Values ) map [ string ] interface { } {
filterPresets := map [ string ] interface { } { }
if query . Get ( "cluster" ) != "" {
filterPresets [ "cluster" ] = query . Get ( "cluster" )
}
if query . Get ( "partition" ) != "" {
filterPresets [ "partition" ] = query . Get ( "partition" )
}
if query . Get ( "project" ) != "" {
filterPresets [ "project" ] = query . Get ( "project" )
filterPresets [ "projectMatch" ] = "eq"
}
2023-01-11 16:25:02 +01:00
if query . Get ( "jobName" ) != "" {
filterPresets [ "jobName" ] = query . Get ( "jobName" )
}
2023-02-20 10:20:08 +01:00
if len ( query [ "user" ] ) != 0 {
if len ( query [ "user" ] ) == 1 {
filterPresets [ "user" ] = query . Get ( "user" )
filterPresets [ "userMatch" ] = "contains"
} else {
filterPresets [ "user" ] = query [ "user" ]
filterPresets [ "userMatch" ] = "in"
}
2022-02-03 16:00:08 +01:00
}
2022-03-21 13:30:19 +01:00
if len ( query [ "state" ] ) != 0 {
filterPresets [ "state" ] = query [ "state" ]
2022-02-03 11:35:42 +01:00
}
if rawtags , ok := query [ "tag" ] ; ok {
tags := make ( [ ] int , len ( rawtags ) )
for i , tid := range rawtags {
var err error
tags [ i ] , err = strconv . Atoi ( tid )
if err != nil {
tags [ i ] = - 1
}
}
filterPresets [ "tags" ] = tags
}
2022-03-21 13:30:19 +01:00
if query . Get ( "duration" ) != "" {
parts := strings . Split ( query . Get ( "duration" ) , "-" )
if len ( parts ) == 2 {
a , e1 := strconv . Atoi ( parts [ 0 ] )
b , e2 := strconv . Atoi ( parts [ 1 ] )
if e1 == nil && e2 == nil {
filterPresets [ "duration" ] = map [ string ] int { "from" : a , "to" : b }
}
}
}
2023-06-30 12:01:27 +02:00
if query . Get ( "node" ) != "" {
filterPresets [ "node" ] = query . Get ( "node" )
}
2022-02-03 11:35:42 +01:00
if query . Get ( "numNodes" ) != "" {
parts := strings . Split ( query . Get ( "numNodes" ) , "-" )
if len ( parts ) == 2 {
a , e1 := strconv . Atoi ( parts [ 0 ] )
b , e2 := strconv . Atoi ( parts [ 1 ] )
if e1 == nil && e2 == nil {
filterPresets [ "numNodes" ] = map [ string ] int { "from" : a , "to" : b }
}
}
}
2022-03-21 13:30:19 +01:00
if query . Get ( "numAccelerators" ) != "" {
parts := strings . Split ( query . Get ( "numAccelerators" ) , "-" )
if len ( parts ) == 2 {
a , e1 := strconv . Atoi ( parts [ 0 ] )
b , e2 := strconv . Atoi ( parts [ 1 ] )
if e1 == nil && e2 == nil {
filterPresets [ "numAccelerators" ] = map [ string ] int { "from" : a , "to" : b }
}
}
}
2022-02-03 11:35:42 +01:00
if query . Get ( "jobId" ) != "" {
2023-06-30 16:55:34 +02:00
if len ( query [ "jobId" ] ) == 1 {
filterPresets [ "jobId" ] = query . Get ( "jobId" )
filterPresets [ "jobIdMatch" ] = "eq"
} else {
filterPresets [ "jobId" ] = query [ "jobId" ]
filterPresets [ "jobIdMatch" ] = "in"
}
2022-02-03 11:35:42 +01:00
}
if query . Get ( "arrayJobId" ) != "" {
if num , err := strconv . Atoi ( query . Get ( "arrayJobId" ) ) ; err == nil {
filterPresets [ "arrayJobId" ] = num
}
}
2022-02-28 11:08:43 +01:00
if query . Get ( "startTime" ) != "" {
parts := strings . Split ( query . Get ( "startTime" ) , "-" )
2024-10-10 18:35:53 +02:00
if len ( parts ) == 2 { // Time in seconds, from - to
2022-02-28 11:08:43 +01:00
a , e1 := strconv . ParseInt ( parts [ 0 ] , 10 , 64 )
b , e2 := strconv . ParseInt ( parts [ 1 ] , 10 , 64 )
if e1 == nil && e2 == nil {
filterPresets [ "startTime" ] = map [ string ] string {
"from" : time . Unix ( a , 0 ) . Format ( time . RFC3339 ) ,
"to" : time . Unix ( b , 0 ) . Format ( time . RFC3339 ) ,
}
}
2024-10-10 18:35:53 +02:00
} else { // named range
filterPresets [ "startTime" ] = map [ string ] string {
"range" : query . Get ( "startTime" ) ,
}
2022-02-28 11:08:43 +01:00
}
}
2022-02-03 11:35:42 +01:00
return filterPresets
}
2023-06-22 19:56:21 +02:00
func SetupRoutes ( router * mux . Router , buildInfo web . Build ) {
2022-09-05 17:46:38 +02:00
userCfgRepo := repository . GetUserCfgRepo ( )
2022-02-03 07:25:36 +01:00
for _ , route := range routes {
2022-02-03 11:35:42 +01:00
route := route
router . HandleFunc ( route . Route , func ( rw http . ResponseWriter , r * http . Request ) {
2023-08-17 10:29:00 +02:00
conf , err := userCfgRepo . GetUIConfig ( repository . GetUserFromContext ( r . Context ( ) ) )
2022-02-03 07:25:36 +01:00
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
return
}
2022-03-03 13:45:41 +01:00
title := route . Title
2022-02-15 10:03:09 +01:00
infos := route . Setup ( map [ string ] interface { } { } , r )
2022-02-03 10:25:52 +01:00
if id , ok := infos [ "id" ] ; ok {
2022-03-03 13:45:41 +01:00
title = strings . Replace ( route . Title , "<ID>" , id . ( string ) , 1 )
2022-02-03 11:35:42 +01:00
}
2023-03-06 11:44:38 +01:00
// Get User -> What if NIL?
2023-08-17 10:29:00 +02:00
user := repository . GetUserFromContext ( r . Context ( ) )
2023-03-06 11:44:38 +01:00
// Get Roles
2023-08-17 10:29:00 +02:00
availableRoles , _ := schema . GetValidRolesMap ( user )
2022-02-11 16:51:49 +01:00
2022-07-06 15:00:08 +02:00
page := web . Page {
2024-09-24 11:13:39 +02:00
Title : title ,
User : * user ,
Roles : availableRoles ,
Build : buildInfo ,
Config : conf ,
Resampling : config . Keys . EnableResampling ,
Infos : infos ,
2022-02-03 11:35:42 +01:00
}
if route . Filter {
page . FilterPresets = buildFilterPresets ( r . URL . Query ( ) )
2022-02-03 10:25:52 +01:00
}
2022-02-03 07:25:36 +01:00
2023-08-17 10:29:00 +02:00
web . RenderTemplate ( rw , route . Template , & page )
2022-02-03 07:25:36 +01:00
} )
}
}
2023-02-09 10:11:11 +01:00
2023-06-22 19:56:21 +02:00
func HandleSearchBar ( rw http . ResponseWriter , r * http . Request , buildInfo web . Build ) {
2023-08-17 10:29:00 +02:00
user := repository . GetUserFromContext ( r . Context ( ) )
availableRoles , _ := schema . GetValidRolesMap ( user )
2023-06-22 19:56:21 +02:00
2023-02-09 10:11:11 +01:00
if search := r . URL . Query ( ) . Get ( "searchId" ) ; search != "" {
2023-06-22 16:26:09 +02:00
repo := repository . GetJobRepository ( )
2023-02-09 10:11:11 +01:00
splitSearch := strings . Split ( search , ":" )
2023-02-15 09:50:27 +01:00
if len ( splitSearch ) == 2 {
2023-02-09 10:11:11 +01:00
switch strings . Trim ( splitSearch [ 0 ] , " " ) {
case "jobId" :
2023-06-22 10:58:36 +02:00
http . Redirect ( rw , r , "/monitoring/jobs/?jobId=" + url . QueryEscape ( strings . Trim ( splitSearch [ 1 ] , " " ) ) , http . StatusFound ) // All Users: Redirect to Tablequery
2023-02-09 10:11:11 +01:00
case "jobName" :
2024-05-23 11:53:23 +02:00
// Add Last 30 Days to migitate timeouts
untilTime := strconv . FormatInt ( time . Now ( ) . Unix ( ) , 10 )
fromTime := strconv . FormatInt ( ( time . Now ( ) . Unix ( ) - int64 ( 30 * 24 * 3600 ) ) , 10 )
http . Redirect ( rw , r , "/monitoring/jobs/?startTime=" + fromTime + "-" + untilTime + "&jobName=" + url . QueryEscape ( strings . Trim ( splitSearch [ 1 ] , " " ) ) , http . StatusFound ) // All Users: Redirect to Tablequery
2023-02-09 10:11:11 +01:00
case "projectId" :
2023-06-22 10:58:36 +02:00
http . Redirect ( rw , r , "/monitoring/jobs/?projectMatch=eq&project=" + url . QueryEscape ( strings . Trim ( splitSearch [ 1 ] , " " ) ) , http . StatusFound ) // All Users: Redirect to Tablequery
2023-07-14 16:42:49 +02:00
case "arrayJobId" :
2024-05-23 11:53:23 +02:00
// Add Last 30 Days to migitate timeouts
untilTime := strconv . FormatInt ( time . Now ( ) . Unix ( ) , 10 )
fromTime := strconv . FormatInt ( ( time . Now ( ) . Unix ( ) - int64 ( 30 * 24 * 3600 ) ) , 10 )
http . Redirect ( rw , r , "/monitoring/jobs/?startTime=" + fromTime + "-" + untilTime + "&arrayJobId=" + url . QueryEscape ( strings . Trim ( splitSearch [ 1 ] , " " ) ) , http . StatusFound ) // All Users: Redirect to Tablequery
2023-02-09 10:11:11 +01:00
case "username" :
2023-08-17 10:29:00 +02:00
if user . HasAnyRole ( [ ] schema . Role { schema . RoleAdmin , schema . RoleSupport , schema . RoleManager } ) {
2023-06-22 10:58:36 +02:00
http . Redirect ( rw , r , "/monitoring/users/?user=" + url . QueryEscape ( strings . Trim ( splitSearch [ 1 ] , " " ) ) , http . StatusFound )
2023-02-09 10:11:11 +01:00
} else {
2023-08-17 10:29:00 +02:00
web . RenderTemplate ( rw , "message.tmpl" , & web . Page { Title : "Error" , MsgType : "alert-danger" , Message : "Missing Access Rights" , User : * user , Roles : availableRoles , Build : buildInfo } )
2023-02-17 10:45:27 +01:00
}
case "name" :
2023-06-22 16:26:09 +02:00
usernames , _ := repo . FindColumnValues ( user , strings . Trim ( splitSearch [ 1 ] , " " ) , "user" , "username" , "name" )
2023-02-22 16:30:01 +01:00
if len ( usernames ) != 0 {
2023-02-20 10:20:08 +01:00
joinedNames := strings . Join ( usernames , "&user=" )
2023-06-22 10:58:36 +02:00
http . Redirect ( rw , r , "/monitoring/users/?user=" + joinedNames , http . StatusFound )
2023-02-17 10:45:27 +01:00
} else {
2023-08-17 10:29:00 +02:00
if user . HasAnyRole ( [ ] schema . Role { schema . RoleAdmin , schema . RoleSupport , schema . RoleManager } ) {
2023-06-22 10:58:36 +02:00
http . Redirect ( rw , r , "/monitoring/users/?user=NoUserNameFound" , http . StatusPermanentRedirect )
2023-02-22 16:30:01 +01:00
} else {
2023-08-17 10:29:00 +02:00
web . RenderTemplate ( rw , "message.tmpl" , & web . Page { Title : "Error" , MsgType : "alert-danger" , Message : "Missing Access Rights" , User : * user , Roles : availableRoles , Build : buildInfo } )
2023-02-22 16:30:01 +01:00
}
2023-02-09 10:11:11 +01:00
}
default :
2023-08-17 10:29:00 +02:00
web . RenderTemplate ( rw , "message.tmpl" , & web . Page { Title : "Warning" , MsgType : "alert-warning" , Message : fmt . Sprintf ( "Unknown search type: %s" , strings . Trim ( splitSearch [ 0 ] , " " ) ) , User : * user , Roles : availableRoles , Build : buildInfo } )
2023-02-09 10:11:11 +01:00
}
2023-02-15 09:50:27 +01:00
} else if len ( splitSearch ) == 1 {
2023-02-09 10:11:11 +01:00
2023-06-23 16:09:33 +02:00
jobid , username , project , jobname := repo . FindUserOrProjectOrJobname ( user , strings . Trim ( search , " " ) )
2023-02-09 10:11:11 +01:00
2023-06-23 16:09:33 +02:00
if jobid != "" {
http . Redirect ( rw , r , "/monitoring/jobs/?jobId=" + url . QueryEscape ( jobid ) , http . StatusFound ) // JobId (Match)
} else if username != "" {
http . Redirect ( rw , r , "/monitoring/user/" + username , http . StatusFound ) // User: Redirect to user page of first match
2023-02-15 09:50:27 +01:00
} else if project != "" {
2023-06-23 16:09:33 +02:00
http . Redirect ( rw , r , "/monitoring/jobs/?projectMatch=eq&project=" + url . QueryEscape ( project ) , http . StatusFound ) // projectId (equal)
2023-02-15 09:50:27 +01:00
} else if jobname != "" {
2024-05-27 11:11:25 +02:00
// Add Last 30 Days to migitate timeouts
untilTime := strconv . FormatInt ( time . Now ( ) . Unix ( ) , 10 )
fromTime := strconv . FormatInt ( ( time . Now ( ) . Unix ( ) - int64 ( 30 * 24 * 3600 ) ) , 10 )
http . Redirect ( rw , r , "/monitoring/jobs/?startTime=" + fromTime + "-" + untilTime + "&jobName=" + url . QueryEscape ( jobname ) , http . StatusFound ) // 30D Fitler + JobName (contains)
2023-02-09 10:11:11 +01:00
} else {
2023-08-17 10:29:00 +02:00
web . RenderTemplate ( rw , "message.tmpl" , & web . Page { Title : "Info" , MsgType : "alert-info" , Message : "Search without result" , User : * user , Roles : availableRoles , Build : buildInfo } )
2023-02-09 10:11:11 +01:00
}
2023-06-23 16:09:33 +02:00
2023-02-09 10:11:11 +01:00
} else {
2023-08-17 10:29:00 +02:00
web . RenderTemplate ( rw , "message.tmpl" , & web . Page { Title : "Error" , MsgType : "alert-danger" , Message : "Searchbar query parameters malformed" , User : * user , Roles : availableRoles , Build : buildInfo } )
2023-02-09 10:11:11 +01:00
}
} else {
2023-08-17 10:29:00 +02:00
web . RenderTemplate ( rw , "message.tmpl" , & web . Page { Title : "Warning" , MsgType : "alert-warning" , Message : "Empty search" , User : * user , Roles : availableRoles , Build : buildInfo } )
2023-02-09 10:11:11 +01:00
}
}