Merge branch 'master' into feature/discord
This commit is contained in:
+31
-5
@@ -159,10 +159,6 @@ type SetAlertStateCommand struct {
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type DeleteAlertCommand struct {
|
||||
AlertId int64
|
||||
}
|
||||
|
||||
//Queries
|
||||
type GetAlertsQuery struct {
|
||||
OrgId int64
|
||||
@@ -170,8 +166,9 @@ type GetAlertsQuery struct {
|
||||
DashboardId int64
|
||||
PanelId int64
|
||||
Limit int64
|
||||
User *SignedInUser
|
||||
|
||||
Result []*Alert
|
||||
Result []*AlertListItemDTO
|
||||
}
|
||||
|
||||
type GetAllAlertsQuery struct {
|
||||
@@ -191,6 +188,21 @@ type GetAlertStatesForDashboardQuery struct {
|
||||
Result []*AlertStateInfoDTO
|
||||
}
|
||||
|
||||
type AlertListItemDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
DashboardUid string `json:"dashboardUid"`
|
||||
DashboardSlug string `json:"dashboardSlug"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
Name string `json:"name"`
|
||||
State AlertStateType `json:"state"`
|
||||
NewStateDate time.Time `json:"newStateDate"`
|
||||
EvalDate time.Time `json:"evalDate"`
|
||||
EvalData *simplejson.Json `json:"evalData"`
|
||||
ExecutionError string `json:"executionError"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type AlertStateInfoDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
@@ -198,3 +210,17 @@ type AlertStateInfoDTO struct {
|
||||
State AlertStateType `json:"state"`
|
||||
NewStateDate time.Time `json:"newStateDate"`
|
||||
}
|
||||
|
||||
// "Internal" commands
|
||||
|
||||
type UpdateDashboardAlertsCommand struct {
|
||||
UserId int64
|
||||
OrgId int64
|
||||
Dashboard *Dashboard
|
||||
}
|
||||
|
||||
type ValidateDashboardAlertsCommand struct {
|
||||
UserId int64
|
||||
OrgId int64
|
||||
Dashboard *Dashboard
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type ReqContext struct {
|
||||
*macaron.Context
|
||||
*SignedInUser
|
||||
|
||||
Session session.SessionStore
|
||||
|
||||
IsSignedIn bool
|
||||
IsRenderCall bool
|
||||
AllowAnonymous bool
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
// Handle handles and logs error by given status.
|
||||
func (ctx *ReqContext) Handle(status int, title string, err error) {
|
||||
if err != nil {
|
||||
ctx.Logger.Error(title, "error", err)
|
||||
if setting.Env != setting.PROD {
|
||||
ctx.Data["ErrorMsg"] = err
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = title
|
||||
ctx.Data["AppSubUrl"] = setting.AppSubUrl
|
||||
ctx.Data["Theme"] = "dark"
|
||||
|
||||
ctx.HTML(status, "error")
|
||||
}
|
||||
|
||||
func (ctx *ReqContext) JsonOK(message string) {
|
||||
resp := make(map[string]interface{})
|
||||
resp["message"] = message
|
||||
ctx.JSON(200, resp)
|
||||
}
|
||||
|
||||
func (ctx *ReqContext) IsApiRequest() bool {
|
||||
return strings.HasPrefix(ctx.Req.URL.Path, "/api")
|
||||
}
|
||||
|
||||
func (ctx *ReqContext) JsonApiErr(status int, message string, err error) {
|
||||
resp := make(map[string]interface{})
|
||||
|
||||
if err != nil {
|
||||
ctx.Logger.Error(message, "error", err)
|
||||
if setting.Env != setting.PROD {
|
||||
resp["error"] = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
switch status {
|
||||
case 404:
|
||||
resp["message"] = "Not Found"
|
||||
case 500:
|
||||
resp["message"] = "Internal Server Error"
|
||||
}
|
||||
|
||||
if message != "" {
|
||||
resp["message"] = message
|
||||
}
|
||||
|
||||
ctx.JSON(status, resp)
|
||||
}
|
||||
|
||||
func (ctx *ReqContext) HasUserRole(role RoleType) bool {
|
||||
return ctx.OrgRole.Includes(role)
|
||||
}
|
||||
|
||||
func (ctx *ReqContext) HasHelpFlag(flag HelpFlags1) bool {
|
||||
return ctx.HelpFlags1.HasFlag(flag)
|
||||
}
|
||||
|
||||
func (ctx *ReqContext) TimeRequest(timer prometheus.Summary) {
|
||||
ctx.Data["perfmon.timer"] = timer
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PermissionType int
|
||||
|
||||
const (
|
||||
PERMISSION_VIEW PermissionType = 1 << iota
|
||||
PERMISSION_EDIT
|
||||
PERMISSION_ADMIN
|
||||
)
|
||||
|
||||
func (p PermissionType) String() string {
|
||||
names := map[int]string{
|
||||
int(PERMISSION_VIEW): "View",
|
||||
int(PERMISSION_EDIT): "Edit",
|
||||
int(PERMISSION_ADMIN): "Admin",
|
||||
}
|
||||
return names[int(p)]
|
||||
}
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrDashboardAclInfoMissing = errors.New("User id and team id cannot both be empty for a dashboard permission.")
|
||||
ErrDashboardPermissionDashboardEmpty = errors.New("Dashboard Id must be greater than zero for a dashboard permission.")
|
||||
ErrFolderAclInfoMissing = errors.New("User id and team id cannot both be empty for a folder permission.")
|
||||
ErrFolderPermissionFolderEmpty = errors.New("Folder Id must be greater than zero for a folder permission.")
|
||||
)
|
||||
|
||||
// Dashboard ACL model
|
||||
type DashboardAcl struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
DashboardId int64
|
||||
|
||||
UserId int64
|
||||
TeamId int64
|
||||
Role *RoleType // pointer to be nullable
|
||||
Permission PermissionType
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
type DashboardAclInfoDTO struct {
|
||||
OrgId int64 `json:"-"`
|
||||
DashboardId int64 `json:"dashboardId,omitempty"`
|
||||
FolderId int64 `json:"folderId,omitempty"`
|
||||
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
|
||||
UserId int64 `json:"userId"`
|
||||
UserLogin string `json:"userLogin"`
|
||||
UserEmail string `json:"userEmail"`
|
||||
TeamId int64 `json:"teamId"`
|
||||
Team string `json:"team"`
|
||||
Role *RoleType `json:"role,omitempty"`
|
||||
Permission PermissionType `json:"permission"`
|
||||
PermissionName string `json:"permissionName"`
|
||||
Uid string `json:"uid"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
func (dto *DashboardAclInfoDTO) hasSameRoleAs(other *DashboardAclInfoDTO) bool {
|
||||
if dto.Role == nil || other.Role == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return dto.UserId <= 0 && dto.TeamId <= 0 && dto.UserId == other.UserId && dto.TeamId == other.TeamId && *dto.Role == *other.Role
|
||||
}
|
||||
|
||||
func (dto *DashboardAclInfoDTO) hasSameUserAs(other *DashboardAclInfoDTO) bool {
|
||||
return dto.UserId > 0 && dto.UserId == other.UserId
|
||||
}
|
||||
|
||||
func (dto *DashboardAclInfoDTO) hasSameTeamAs(other *DashboardAclInfoDTO) bool {
|
||||
return dto.TeamId > 0 && dto.TeamId == other.TeamId
|
||||
}
|
||||
|
||||
// IsDuplicateOf returns true if other item has same role, same user or same team
|
||||
func (dto *DashboardAclInfoDTO) IsDuplicateOf(other *DashboardAclInfoDTO) bool {
|
||||
return dto.hasSameRoleAs(other) || dto.hasSameUserAs(other) || dto.hasSameTeamAs(other)
|
||||
}
|
||||
|
||||
//
|
||||
// COMMANDS
|
||||
//
|
||||
|
||||
type UpdateDashboardAclCommand struct {
|
||||
DashboardId int64
|
||||
Items []*DashboardAcl
|
||||
}
|
||||
|
||||
//
|
||||
// QUERIES
|
||||
//
|
||||
type GetDashboardAclInfoListQuery struct {
|
||||
DashboardId int64
|
||||
OrgId int64
|
||||
Result []*DashboardAclInfoDTO
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fmt"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDashboardAclModel(t *testing.T) {
|
||||
|
||||
Convey("When printing a PermissionType", t, func() {
|
||||
view := PERMISSION_VIEW
|
||||
printed := fmt.Sprint(view)
|
||||
|
||||
Convey("Should output a friendly name", func() {
|
||||
So(printed, ShouldEqual, "View")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -64,20 +64,24 @@ type DeleteDashboardSnapshotCommand struct {
|
||||
}
|
||||
|
||||
type DeleteExpiredSnapshotsCommand struct {
|
||||
DeletedRows int64
|
||||
}
|
||||
|
||||
type GetDashboardSnapshotQuery struct {
|
||||
Key string
|
||||
Key string
|
||||
DeleteKey string
|
||||
|
||||
Result *DashboardSnapshot
|
||||
}
|
||||
|
||||
type DashboardSnapshots []*DashboardSnapshot
|
||||
type DashboardSnapshotsList []*DashboardSnapshotDTO
|
||||
|
||||
type GetDashboardSnapshotsQuery struct {
|
||||
Name string
|
||||
Limit int
|
||||
OrgId int64
|
||||
Name string
|
||||
Limit int
|
||||
OrgId int64
|
||||
SignedInUser *SignedInUser
|
||||
|
||||
Result DashboardSnapshots
|
||||
Result DashboardSnapshotsList
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDashboardVersionNotFound = errors.New("Dashboard version not found")
|
||||
ErrNoVersionsForDashboardId = errors.New("No dashboard versions found for the given DashboardId")
|
||||
)
|
||||
|
||||
// A DashboardVersion represents the comparable data in a dashboard, allowing
|
||||
// diffs of the dashboard to be performed.
|
||||
type DashboardVersion struct {
|
||||
Id int64 `json:"id"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
ParentVersion int `json:"parentVersion"`
|
||||
RestoredFrom int `json:"restoredFrom"`
|
||||
Version int `json:"version"`
|
||||
|
||||
Created time.Time `json:"created"`
|
||||
CreatedBy int64 `json:"createdBy"`
|
||||
|
||||
Message string `json:"message"`
|
||||
Data *simplejson.Json `json:"data"`
|
||||
}
|
||||
|
||||
// DashboardVersionMeta extends the dashboard version model with the names
|
||||
// associated with the UserIds, overriding the field with the same name from
|
||||
// the DashboardVersion model.
|
||||
type DashboardVersionMeta struct {
|
||||
DashboardVersion
|
||||
CreatedBy string `json:"createdBy"`
|
||||
}
|
||||
|
||||
// DashboardVersionDTO represents a dashboard version, without the dashboard
|
||||
// map.
|
||||
type DashboardVersionDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
ParentVersion int `json:"parentVersion"`
|
||||
RestoredFrom int `json:"restoredFrom"`
|
||||
Version int `json:"version"`
|
||||
Created time.Time `json:"created"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
//
|
||||
// Queries
|
||||
//
|
||||
|
||||
type GetDashboardVersionQuery struct {
|
||||
DashboardId int64
|
||||
OrgId int64
|
||||
Version int
|
||||
|
||||
Result *DashboardVersion
|
||||
}
|
||||
|
||||
type GetDashboardVersionsQuery struct {
|
||||
DashboardId int64
|
||||
OrgId int64
|
||||
Limit int
|
||||
Start int
|
||||
|
||||
Result []*DashboardVersionDTO
|
||||
}
|
||||
|
||||
//
|
||||
// Commands
|
||||
//
|
||||
|
||||
type DeleteExpiredVersionsCommand struct {
|
||||
DeletedRows int64
|
||||
}
|
||||
+190
-21
@@ -2,20 +2,37 @@ package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrDashboardNotFound = errors.New("Dashboard not found")
|
||||
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
|
||||
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
|
||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
||||
ErrDashboardNotFound = errors.New("Dashboard not found")
|
||||
ErrDashboardFolderNotFound = errors.New("Folder not found")
|
||||
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
|
||||
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
|
||||
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
|
||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
||||
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
|
||||
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
|
||||
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
|
||||
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
|
||||
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
|
||||
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
|
||||
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
|
||||
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
|
||||
ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
|
||||
ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
|
||||
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
|
||||
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
|
||||
RootFolderName = "General"
|
||||
)
|
||||
|
||||
type UpdatePluginDashboardError struct {
|
||||
@@ -36,6 +53,7 @@ var (
|
||||
// Dashboard model
|
||||
type Dashboard struct {
|
||||
Id int64
|
||||
Uid string
|
||||
Slug string
|
||||
OrgId int64
|
||||
GnetId int64
|
||||
@@ -47,11 +65,38 @@ type Dashboard struct {
|
||||
|
||||
UpdatedBy int64
|
||||
CreatedBy int64
|
||||
FolderId int64
|
||||
IsFolder bool
|
||||
HasAcl bool
|
||||
|
||||
Title string
|
||||
Data *simplejson.Json
|
||||
}
|
||||
|
||||
func (d *Dashboard) SetId(id int64) {
|
||||
d.Id = id
|
||||
d.Data.Set("id", id)
|
||||
}
|
||||
|
||||
func (d *Dashboard) SetUid(uid string) {
|
||||
d.Uid = uid
|
||||
d.Data.Set("uid", uid)
|
||||
}
|
||||
|
||||
func (d *Dashboard) SetVersion(version int) {
|
||||
d.Version = version
|
||||
d.Data.Set("version", version)
|
||||
}
|
||||
|
||||
// GetDashboardIdForSavePermissionCheck return the dashboard id to be used for checking permission of dashboard
|
||||
func (d *Dashboard) GetDashboardIdForSavePermissionCheck() int64 {
|
||||
if d.Id == 0 {
|
||||
return d.FolderId
|
||||
}
|
||||
|
||||
return d.Id
|
||||
}
|
||||
|
||||
// NewDashboard creates a new dashboard
|
||||
func NewDashboard(title string) *Dashboard {
|
||||
dash := &Dashboard{}
|
||||
@@ -64,6 +109,16 @@ func NewDashboard(title string) *Dashboard {
|
||||
return dash
|
||||
}
|
||||
|
||||
// NewDashboardFolder creates a new dashboard folder
|
||||
func NewDashboardFolder(title string) *Dashboard {
|
||||
folder := NewDashboard(title)
|
||||
folder.IsFolder = true
|
||||
folder.Data.Set("schemaVersion", 16)
|
||||
folder.Data.Set("version", 0)
|
||||
folder.IsFolder = true
|
||||
return folder
|
||||
}
|
||||
|
||||
// GetTags turns the tags in data json into go string array
|
||||
func (dash *Dashboard) GetTags() []string {
|
||||
return dash.Data.Get("tags").MustStringArray()
|
||||
@@ -74,14 +129,21 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
|
||||
dash.Data = data
|
||||
dash.Title = dash.Data.Get("title").MustString()
|
||||
dash.UpdateSlug()
|
||||
update := false
|
||||
|
||||
if id, err := dash.Data.Get("id").Float64(); err == nil {
|
||||
dash.Id = int64(id)
|
||||
update = true
|
||||
}
|
||||
|
||||
if version, err := dash.Data.Get("version").Float64(); err == nil {
|
||||
dash.Version = int(version)
|
||||
dash.Updated = time.Now()
|
||||
}
|
||||
if uid, err := dash.Data.Get("uid").String(); err == nil {
|
||||
dash.Uid = uid
|
||||
update = true
|
||||
}
|
||||
|
||||
if version, err := dash.Data.Get("version").Float64(); err == nil && update {
|
||||
dash.Version = int(version)
|
||||
dash.Updated = time.Now()
|
||||
} else {
|
||||
dash.Data.Set("version", 0)
|
||||
dash.Created = time.Now()
|
||||
@@ -98,14 +160,17 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
|
||||
// GetDashboardModel turns the command into the savable model
|
||||
func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
|
||||
dash := NewDashboardFromJson(cmd.Dashboard)
|
||||
userId := cmd.UserId
|
||||
|
||||
if dash.Data.Get("version").MustInt(0) == 0 {
|
||||
dash.CreatedBy = cmd.UserId
|
||||
if userId == 0 {
|
||||
userId = -1
|
||||
}
|
||||
|
||||
dash.UpdatedBy = cmd.UserId
|
||||
dash.UpdatedBy = userId
|
||||
dash.OrgId = cmd.OrgId
|
||||
dash.PluginId = cmd.PluginId
|
||||
dash.IsFolder = cmd.IsFolder
|
||||
dash.FolderId = cmd.FolderId
|
||||
dash.UpdateSlug()
|
||||
return dash
|
||||
}
|
||||
@@ -117,8 +182,46 @@ func (dash *Dashboard) GetString(prop string, defaultValue string) string {
|
||||
|
||||
// UpdateSlug updates the slug
|
||||
func (dash *Dashboard) UpdateSlug() {
|
||||
title := strings.ToLower(dash.Data.Get("title").MustString())
|
||||
dash.Slug = slug.Make(title)
|
||||
title := dash.Data.Get("title").MustString()
|
||||
dash.Slug = SlugifyTitle(title)
|
||||
}
|
||||
|
||||
func SlugifyTitle(title string) string {
|
||||
return slug.Make(strings.ToLower(title))
|
||||
}
|
||||
|
||||
// GetUrl return the html url for a folder if it's folder, otherwise for a dashboard
|
||||
func (dash *Dashboard) GetUrl() string {
|
||||
return GetDashboardFolderUrl(dash.IsFolder, dash.Uid, dash.Slug)
|
||||
}
|
||||
|
||||
// Return the html url for a dashboard
|
||||
func (dash *Dashboard) GenerateUrl() string {
|
||||
return GetDashboardUrl(dash.Uid, dash.Slug)
|
||||
}
|
||||
|
||||
// GetDashboardFolderUrl return the html url for a folder if it's folder, otherwise for a dashboard
|
||||
func GetDashboardFolderUrl(isFolder bool, uid string, slug string) string {
|
||||
if isFolder {
|
||||
return GetFolderUrl(uid, slug)
|
||||
}
|
||||
|
||||
return GetDashboardUrl(uid, slug)
|
||||
}
|
||||
|
||||
// GetDashboardUrl return the html url for a dashboard
|
||||
func GetDashboardUrl(uid string, slug string) string {
|
||||
return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug)
|
||||
}
|
||||
|
||||
// GetFullDashboardUrl return the full url for a dashboard
|
||||
func GetFullDashboardUrl(uid string, slug string) string {
|
||||
return fmt.Sprintf("%sd/%s/%s", setting.AppUrl, uid, slug)
|
||||
}
|
||||
|
||||
// GetFolderUrl return the html url for a folder
|
||||
func GetFolderUrl(folderUid string, slug string) string {
|
||||
return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug)
|
||||
}
|
||||
|
||||
//
|
||||
@@ -126,26 +229,55 @@ func (dash *Dashboard) UpdateSlug() {
|
||||
//
|
||||
|
||||
type SaveDashboardCommand struct {
|
||||
Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
|
||||
UserId int64 `json:"userId"`
|
||||
OrgId int64 `json:"-"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
PluginId string `json:"-"`
|
||||
Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
|
||||
UserId int64 `json:"userId"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
Message string `json:"message"`
|
||||
OrgId int64 `json:"-"`
|
||||
RestoredFrom int `json:"-"`
|
||||
PluginId string `json:"-"`
|
||||
FolderId int64 `json:"folderId"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
|
||||
UpdatedAt time.Time
|
||||
|
||||
Result *Dashboard
|
||||
}
|
||||
|
||||
type DashboardProvisioning struct {
|
||||
Id int64
|
||||
DashboardId int64
|
||||
Name string
|
||||
ExternalId string
|
||||
Updated int64
|
||||
}
|
||||
|
||||
type SaveProvisionedDashboardCommand struct {
|
||||
DashboardCmd *SaveDashboardCommand
|
||||
DashboardProvisioning *DashboardProvisioning
|
||||
|
||||
Result *Dashboard
|
||||
}
|
||||
|
||||
type DeleteDashboardCommand struct {
|
||||
Slug string
|
||||
Id int64
|
||||
OrgId int64
|
||||
}
|
||||
|
||||
type ValidateDashboardBeforeSaveCommand struct {
|
||||
OrgId int64
|
||||
Dashboard *Dashboard
|
||||
Overwrite bool
|
||||
}
|
||||
|
||||
//
|
||||
// QUERIES
|
||||
//
|
||||
|
||||
type GetDashboardQuery struct {
|
||||
Slug string
|
||||
Slug string // required if no Id or Uid is specified
|
||||
Id int64 // optional if slug is set
|
||||
Uid string // optional if slug is set
|
||||
OrgId int64
|
||||
|
||||
Result *Dashboard
|
||||
@@ -166,6 +298,14 @@ type GetDashboardsQuery struct {
|
||||
Result []*Dashboard
|
||||
}
|
||||
|
||||
type GetDashboardPermissionsForUserQuery struct {
|
||||
DashboardIds []int64
|
||||
OrgId int64
|
||||
UserId int64
|
||||
OrgRole RoleType
|
||||
Result []*DashboardPermissionForUser
|
||||
}
|
||||
|
||||
type GetDashboardsByPluginIdQuery struct {
|
||||
OrgId int64
|
||||
PluginId string
|
||||
@@ -176,3 +316,32 @@ type GetDashboardSlugByIdQuery struct {
|
||||
Id int64
|
||||
Result string
|
||||
}
|
||||
|
||||
type GetProvisionedDashboardDataQuery struct {
|
||||
Name string
|
||||
|
||||
Result []*DashboardProvisioning
|
||||
}
|
||||
|
||||
type GetDashboardsBySlugQuery struct {
|
||||
OrgId int64
|
||||
Slug string
|
||||
|
||||
Result []*Dashboard
|
||||
}
|
||||
|
||||
type DashboardPermissionForUser struct {
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
Permission PermissionType `json:"permission"`
|
||||
PermissionName string `json:"permissionName"`
|
||||
}
|
||||
|
||||
type DashboardRef struct {
|
||||
Uid string
|
||||
Slug string
|
||||
}
|
||||
|
||||
type GetDashboardRefByIdQuery struct {
|
||||
Id int64
|
||||
Result *DashboardRef
|
||||
}
|
||||
|
||||
@@ -4,11 +4,24 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDashboardModel(t *testing.T) {
|
||||
|
||||
Convey("Generate full dashboard url", t, func() {
|
||||
setting.AppUrl = "http://grafana.local/"
|
||||
fullUrl := GetFullDashboardUrl("uid", "my-dashboard")
|
||||
So(fullUrl, ShouldEqual, "http://grafana.local/d/uid/my-dashboard")
|
||||
})
|
||||
|
||||
Convey("Generate relative dashboard url", t, func() {
|
||||
setting.AppUrl = ""
|
||||
fullUrl := GetDashboardUrl("uid", "my-dashboard")
|
||||
So(fullUrl, ShouldEqual, "/d/uid/my-dashboard")
|
||||
})
|
||||
|
||||
Convey("When generating slug", t, func() {
|
||||
dashboard := NewDashboard("Grafana Play Home")
|
||||
dashboard.UpdateSlug()
|
||||
@@ -16,6 +29,12 @@ func TestDashboardModel(t *testing.T) {
|
||||
So(dashboard.Slug, ShouldEqual, "grafana-play-home")
|
||||
})
|
||||
|
||||
Convey("Can slugify title", t, func() {
|
||||
slug := SlugifyTitle("Grafana Play Home")
|
||||
|
||||
So(slug, ShouldEqual, "grafana-play-home")
|
||||
})
|
||||
|
||||
Convey("Given a dashboard json", t, func() {
|
||||
json := simplejson.New()
|
||||
json.Set("title", "test dash")
|
||||
@@ -28,4 +47,27 @@ func TestDashboardModel(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a new dashboard folder", t, func() {
|
||||
json := simplejson.New()
|
||||
json.Set("title", "test dash")
|
||||
|
||||
cmd := &SaveDashboardCommand{Dashboard: json, IsFolder: true}
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
Convey("Should set IsFolder to true", func() {
|
||||
So(dash.IsFolder, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a child dashboard", t, func() {
|
||||
json := simplejson.New()
|
||||
json.Set("title", "test dash")
|
||||
|
||||
cmd := &SaveDashboardCommand{Dashboard: json, FolderId: 1}
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
Convey("Should set FolderId", func() {
|
||||
So(dash.FolderId, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
+50
-16
@@ -17,14 +17,18 @@ const (
|
||||
DS_CLOUDWATCH = "cloudwatch"
|
||||
DS_KAIROSDB = "kairosdb"
|
||||
DS_PROMETHEUS = "prometheus"
|
||||
DS_POSTGRES = "postgres"
|
||||
DS_MYSQL = "mysql"
|
||||
DS_MSSQL = "mssql"
|
||||
DS_ACCESS_DIRECT = "direct"
|
||||
DS_ACCESS_PROXY = "proxy"
|
||||
)
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrDataSourceNotFound = errors.New("Data source not found")
|
||||
ErrDataSourceNameExists = errors.New("Data source with same name already exists")
|
||||
ErrDataSourceNotFound = errors.New("Data source not found")
|
||||
ErrDataSourceNameExists = errors.New("Data source with same name already exists")
|
||||
ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource")
|
||||
ErrDatasourceIsReadOnly = errors.New("Data source is readonly. Can only be updated from configuration.")
|
||||
)
|
||||
|
||||
type DsAccess string
|
||||
@@ -48,25 +52,42 @@ type DataSource struct {
|
||||
IsDefault bool
|
||||
JsonData *simplejson.Json
|
||||
SecureJsonData securejsondata.SecureJsonData
|
||||
ReadOnly bool
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
var knownDatasourcePlugins map[string]bool = map[string]bool{
|
||||
DS_ES: true,
|
||||
DS_GRAPHITE: true,
|
||||
DS_INFLUXDB: true,
|
||||
DS_INFLUXDB_08: true,
|
||||
DS_KAIROSDB: true,
|
||||
DS_CLOUDWATCH: true,
|
||||
DS_PROMETHEUS: true,
|
||||
DS_OPENTSDB: true,
|
||||
"opennms": true,
|
||||
"druid": true,
|
||||
"dalmatinerdb": true,
|
||||
"gnocci": true,
|
||||
"zabbix": true,
|
||||
DS_ES: true,
|
||||
DS_GRAPHITE: true,
|
||||
DS_INFLUXDB: true,
|
||||
DS_INFLUXDB_08: true,
|
||||
DS_KAIROSDB: true,
|
||||
DS_CLOUDWATCH: true,
|
||||
DS_PROMETHEUS: true,
|
||||
DS_OPENTSDB: true,
|
||||
DS_POSTGRES: true,
|
||||
DS_MYSQL: true,
|
||||
DS_MSSQL: true,
|
||||
"opennms": true,
|
||||
"abhisant-druid-datasource": true,
|
||||
"dalmatinerdb-datasource": true,
|
||||
"gnocci": true,
|
||||
"zabbix": true,
|
||||
"alexanderzobnin-zabbix-datasource": true,
|
||||
"newrelic-app": true,
|
||||
"grafana-datadog-datasource": true,
|
||||
"grafana-simple-json": true,
|
||||
"grafana-splunk-datasource": true,
|
||||
"udoprog-heroic-datasource": true,
|
||||
"grafana-openfalcon-datasource": true,
|
||||
"opennms-datasource": true,
|
||||
"rackerlabs-blueflood-datasource": true,
|
||||
"crate-datasource": true,
|
||||
"ayoungprogrammer-finance-datasource": true,
|
||||
"monasca-datasource": true,
|
||||
"vertamedia-clickhouse-datasource": true,
|
||||
}
|
||||
|
||||
func IsKnownDataSourcePlugin(dsType string) bool {
|
||||
@@ -93,6 +114,7 @@ type AddDataSourceCommand struct {
|
||||
IsDefault bool `json:"isDefault"`
|
||||
JsonData *simplejson.Json `json:"jsonData"`
|
||||
SecureJsonData map[string]string `json:"secureJsonData"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
|
||||
OrgId int64 `json:"-"`
|
||||
|
||||
@@ -115,19 +137,27 @@ type UpdateDataSourceCommand struct {
|
||||
IsDefault bool `json:"isDefault"`
|
||||
JsonData *simplejson.Json `json:"jsonData"`
|
||||
SecureJsonData map[string]string `json:"secureJsonData"`
|
||||
Version int `json:"version"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
|
||||
OrgId int64 `json:"-"`
|
||||
Id int64 `json:"-"`
|
||||
|
||||
Result *DataSource
|
||||
}
|
||||
|
||||
type DeleteDataSourceByIdCommand struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
|
||||
DeletedDatasourcesCount int64
|
||||
}
|
||||
|
||||
type DeleteDataSourceByNameCommand struct {
|
||||
Name string
|
||||
OrgId int64
|
||||
|
||||
DeletedDatasourcesCount int64
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
@@ -138,6 +168,10 @@ type GetDataSourcesQuery struct {
|
||||
Result []*DataSource
|
||||
}
|
||||
|
||||
type GetAllDataSourcesQuery struct {
|
||||
Result []*DataSource
|
||||
}
|
||||
|
||||
type GetDataSourceByIdQuery struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
|
||||
@@ -3,6 +3,7 @@ package models
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
@@ -45,14 +46,23 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
|
||||
return t.Transport, nil
|
||||
}
|
||||
|
||||
var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool
|
||||
if ds.JsonData != nil {
|
||||
tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
|
||||
tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
|
||||
tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false)
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerify: tlsSkipVerify,
|
||||
Renegotiation: tls.RenegotiateFreelyAsClient,
|
||||
},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
@@ -60,30 +70,24 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
}
|
||||
|
||||
var tlsAuth, tlsAuthWithCACert bool
|
||||
if ds.JsonData != nil {
|
||||
tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
|
||||
tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
|
||||
}
|
||||
|
||||
if tlsAuth {
|
||||
transport.TLSClientConfig.InsecureSkipVerify = false
|
||||
|
||||
if tlsClientAuth || tlsAuthWithCACert {
|
||||
decrypted := ds.SecureJsonData.Decrypt()
|
||||
|
||||
if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
|
||||
caPool := x509.NewCertPool()
|
||||
ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
|
||||
if ok {
|
||||
transport.TLSClientConfig.RootCAs = caPool
|
||||
if !ok {
|
||||
return nil, errors.New("Failed to parse TLS CA PEM certificate")
|
||||
}
|
||||
transport.TLSClientConfig.RootCAs = caPool
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if tlsClientAuth {
|
||||
cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
ptc.cache[ds.Id] = cachedTransport{
|
||||
|
||||
@@ -29,56 +29,140 @@ func TestDataSourceCache(t *testing.T) {
|
||||
Convey("Should be using the cached proxy", func() {
|
||||
So(t2, ShouldEqual, t1)
|
||||
})
|
||||
Convey("Should verify TLS by default", func() {
|
||||
So(t1.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
|
||||
})
|
||||
Convey("Should have no TLS client certificate configured", func() {
|
||||
So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0)
|
||||
})
|
||||
Convey("Should have no user-supplied TLS CA onfigured", func() {
|
||||
So(t1.TLSClientConfig.RootCAs, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When getting kubernetes datasource proxy", t, func() {
|
||||
Convey("When caching a datasource proxy then updating it", t, func() {
|
||||
clearCache()
|
||||
setting.SecretKey = "password"
|
||||
|
||||
json := simplejson.New()
|
||||
json.Set("tlsAuthWithCACert", true)
|
||||
|
||||
tlsCaCert, err := util.Encrypt([]byte(caCert), "password")
|
||||
So(err, ShouldBeNil)
|
||||
ds := DataSource{
|
||||
Id: 1,
|
||||
Url: "http://k8s:8001",
|
||||
Type: "Kubernetes",
|
||||
SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert},
|
||||
Updated: time.Now().Add(-2 * time.Minute),
|
||||
}
|
||||
|
||||
t1, err := ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should verify TLS by default", func() {
|
||||
So(t1.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
|
||||
})
|
||||
Convey("Should have no TLS client certificate configured", func() {
|
||||
So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0)
|
||||
})
|
||||
Convey("Should have no user-supplied TLS CA configured", func() {
|
||||
So(t1.TLSClientConfig.RootCAs, ShouldBeNil)
|
||||
})
|
||||
|
||||
ds.JsonData = nil
|
||||
ds.SecureJsonData = map[string][]byte{}
|
||||
ds.Updated = time.Now()
|
||||
|
||||
t2, err := ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should have no user-supplied TLS CA configured after the update", func() {
|
||||
So(t2.TLSClientConfig.RootCAs, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When caching a datasource proxy with TLS client authentication enabled", t, func() {
|
||||
clearCache()
|
||||
setting.SecretKey = "password"
|
||||
|
||||
json := simplejson.New()
|
||||
json.Set("tlsAuth", true)
|
||||
|
||||
tlsClientCert, err := util.Encrypt([]byte(clientCert), "password")
|
||||
So(err, ShouldBeNil)
|
||||
tlsClientKey, err := util.Encrypt([]byte(clientKey), "password")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
ds := DataSource{
|
||||
Id: 1,
|
||||
Url: "http://k8s:8001",
|
||||
Type: "Kubernetes",
|
||||
JsonData: json,
|
||||
SecureJsonData: map[string][]byte{
|
||||
"tlsClientCert": tlsClientCert,
|
||||
"tlsClientKey": tlsClientKey,
|
||||
},
|
||||
}
|
||||
|
||||
tr, err := ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should verify TLS by default", func() {
|
||||
So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
|
||||
})
|
||||
Convey("Should have a TLS client certificate configured", func() {
|
||||
So(len(tr.TLSClientConfig.Certificates), ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When caching a datasource proxy with a user-supplied TLS CA", t, func() {
|
||||
clearCache()
|
||||
setting.SecretKey = "password"
|
||||
|
||||
json := simplejson.New()
|
||||
json.Set("tlsAuthWithCACert", true)
|
||||
|
||||
t := time.Now()
|
||||
tlsCaCert, err := util.Encrypt([]byte(caCert), "password")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
ds := DataSource{
|
||||
Url: "http://k8s:8001",
|
||||
Type: "Kubernetes",
|
||||
Updated: t.Add(-2 * time.Minute),
|
||||
Id: 1,
|
||||
Url: "http://k8s:8001",
|
||||
Type: "Kubernetes",
|
||||
JsonData: json,
|
||||
SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert},
|
||||
}
|
||||
|
||||
transport, err := ds.GetHttpTransport()
|
||||
tr, err := ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should have no cert", func() {
|
||||
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
|
||||
Convey("Should verify TLS by default", func() {
|
||||
So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
|
||||
})
|
||||
Convey("Should have a TLS CA configured", func() {
|
||||
So(len(tr.TLSClientConfig.RootCAs.Subjects()), ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
ds.JsonData = json
|
||||
ds.SecureJsonData = map[string][]byte{
|
||||
"tlsCACert": util.Encrypt([]byte(caCert), "password"),
|
||||
"tlsClientCert": util.Encrypt([]byte(clientCert), "password"),
|
||||
"tlsClientKey": util.Encrypt([]byte(clientKey), "password"),
|
||||
Convey("When caching a datasource proxy when user skips TLS verification", t, func() {
|
||||
clearCache()
|
||||
|
||||
json := simplejson.New()
|
||||
json.Set("tlsSkipVerify", true)
|
||||
|
||||
ds := DataSource{
|
||||
Id: 1,
|
||||
Url: "http://k8s:8001",
|
||||
Type: "Kubernetes",
|
||||
JsonData: json,
|
||||
}
|
||||
ds.Updated = t.Add(-1 * time.Minute)
|
||||
|
||||
transport, err = ds.GetHttpTransport()
|
||||
tr, err := ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should add cert", func() {
|
||||
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
|
||||
So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1)
|
||||
})
|
||||
|
||||
ds.JsonData = nil
|
||||
ds.SecureJsonData = map[string][]byte{}
|
||||
ds.Updated = t
|
||||
|
||||
transport, err = ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should remove cert", func() {
|
||||
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
|
||||
So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 0)
|
||||
Convey("Should skip TLS verification", func() {
|
||||
So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -110,7 +194,8 @@ FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n
|
||||
3lb92xM=
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
const clientCert string = `-----BEGIN CERTIFICATE-----
|
||||
const clientCert string = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
|
||||
YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
|
||||
GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrFolderNotFound = errors.New("Folder not found")
|
||||
ErrFolderVersionMismatch = errors.New("The folder has been changed by someone else")
|
||||
ErrFolderTitleEmpty = errors.New("Folder title cannot be empty")
|
||||
ErrFolderWithSameUIDExists = errors.New("A folder/dashboard with the same uid already exists")
|
||||
ErrFolderSameNameExists = errors.New("A folder or dashboard in the general folder with the same name already exists")
|
||||
ErrFolderFailedGenerateUniqueUid = errors.New("Failed to generate unique folder id")
|
||||
ErrFolderAccessDenied = errors.New("Access denied to folder")
|
||||
)
|
||||
|
||||
type Folder struct {
|
||||
Id int64
|
||||
Uid string
|
||||
Title string
|
||||
Url string
|
||||
Version int
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
|
||||
UpdatedBy int64
|
||||
CreatedBy int64
|
||||
HasAcl bool
|
||||
}
|
||||
|
||||
// GetDashboardModel turns the command into the savable model
|
||||
func (cmd *CreateFolderCommand) GetDashboardModel(orgId int64, userId int64) *Dashboard {
|
||||
dashFolder := NewDashboardFolder(strings.TrimSpace(cmd.Title))
|
||||
dashFolder.OrgId = orgId
|
||||
dashFolder.SetUid(strings.TrimSpace(cmd.Uid))
|
||||
|
||||
if userId == 0 {
|
||||
userId = -1
|
||||
}
|
||||
|
||||
dashFolder.CreatedBy = userId
|
||||
dashFolder.UpdatedBy = userId
|
||||
dashFolder.UpdateSlug()
|
||||
|
||||
return dashFolder
|
||||
}
|
||||
|
||||
// UpdateDashboardModel updates an existing model from command into model for update
|
||||
func (cmd *UpdateFolderCommand) UpdateDashboardModel(dashFolder *Dashboard, orgId int64, userId int64) {
|
||||
dashFolder.OrgId = orgId
|
||||
dashFolder.Title = strings.TrimSpace(cmd.Title)
|
||||
dashFolder.Data.Set("title", dashFolder.Title)
|
||||
|
||||
if cmd.Uid != "" {
|
||||
dashFolder.SetUid(cmd.Uid)
|
||||
}
|
||||
|
||||
dashFolder.SetVersion(cmd.Version)
|
||||
dashFolder.IsFolder = true
|
||||
|
||||
if userId == 0 {
|
||||
userId = -1
|
||||
}
|
||||
|
||||
dashFolder.UpdatedBy = userId
|
||||
dashFolder.UpdateSlug()
|
||||
}
|
||||
|
||||
//
|
||||
// COMMANDS
|
||||
//
|
||||
|
||||
type CreateFolderCommand struct {
|
||||
Uid string `json:"uid"`
|
||||
Title string `json:"title"`
|
||||
|
||||
Result *Folder
|
||||
}
|
||||
|
||||
type UpdateFolderCommand struct {
|
||||
Uid string `json:"uid"`
|
||||
Title string `json:"title"`
|
||||
Version int `json:"version"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
|
||||
Result *Folder
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package models
|
||||
|
||||
type GetDBHealthQuery struct{}
|
||||
@@ -0,0 +1,36 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type LoginAttempt struct {
|
||||
Id int64
|
||||
Username string
|
||||
IpAddress string
|
||||
Created int64
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// COMMANDS
|
||||
|
||||
type CreateLoginAttemptCommand struct {
|
||||
Username string
|
||||
IpAddress string
|
||||
|
||||
Result LoginAttempt
|
||||
}
|
||||
|
||||
type DeleteOldLoginAttemptsCommand struct {
|
||||
OlderThan time.Time
|
||||
DeletedRows int64
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// QUERIES
|
||||
|
||||
type GetUserLoginAttemptCountQuery struct {
|
||||
Username string
|
||||
Since time.Time
|
||||
Result int64
|
||||
}
|
||||
@@ -7,5 +7,5 @@ const (
|
||||
GOOGLE
|
||||
TWITTER
|
||||
GENERIC
|
||||
GRAFANANET
|
||||
GRAFANA_COM
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ package models
|
||||
import "errors"
|
||||
|
||||
var ErrInvalidEmailCode = errors.New("Invalid or expired email code")
|
||||
var ErrSmtpNotEnabled = errors.New("SMTP not configured, check your grafana.ini config file's [smtp] section.")
|
||||
|
||||
type SendEmailCommand struct {
|
||||
To []string
|
||||
|
||||
+19
-13
@@ -18,25 +18,25 @@ var (
|
||||
type RoleType string
|
||||
|
||||
const (
|
||||
ROLE_VIEWER RoleType = "Viewer"
|
||||
ROLE_EDITOR RoleType = "Editor"
|
||||
ROLE_READ_ONLY_EDITOR RoleType = "Read Only Editor"
|
||||
ROLE_ADMIN RoleType = "Admin"
|
||||
ROLE_VIEWER RoleType = "Viewer"
|
||||
ROLE_EDITOR RoleType = "Editor"
|
||||
ROLE_ADMIN RoleType = "Admin"
|
||||
)
|
||||
|
||||
func (r RoleType) IsValid() bool {
|
||||
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR
|
||||
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR
|
||||
}
|
||||
|
||||
func (r RoleType) Includes(other RoleType) bool {
|
||||
if r == ROLE_ADMIN {
|
||||
return true
|
||||
}
|
||||
if r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR {
|
||||
|
||||
if r == ROLE_EDITOR {
|
||||
return other != ROLE_ADMIN
|
||||
}
|
||||
|
||||
return r == other
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RoleType) UnmarshalJSON(data []byte) error {
|
||||
@@ -95,7 +95,10 @@ type UpdateOrgUserCommand struct {
|
||||
// QUERIES
|
||||
|
||||
type GetOrgUsersQuery struct {
|
||||
OrgId int64
|
||||
OrgId int64
|
||||
Query string
|
||||
Limit int
|
||||
|
||||
Result []*OrgUserDTO
|
||||
}
|
||||
|
||||
@@ -103,9 +106,12 @@ type GetOrgUsersQuery struct {
|
||||
// Projections and DTOs
|
||||
|
||||
type OrgUserDTO struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
UserId int64 `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
Login string `json:"login"`
|
||||
Role string `json:"role"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
UserId int64 `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
Login string `json:"login"`
|
||||
Role string `json:"role"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||
LastSeenAtAge string `json:"lastSeenAtAge"`
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package models
|
||||
|
||||
type GrafanaServer interface {
|
||||
Start()
|
||||
Shutdown(code int, reason string)
|
||||
}
|
||||
+18
-14
@@ -1,11 +1,14 @@
|
||||
package models
|
||||
|
||||
type SystemStats struct {
|
||||
DashboardCount int64
|
||||
UserCount int64
|
||||
OrgCount int64
|
||||
PlaylistCount int64
|
||||
AlertCount int64
|
||||
Dashboards int64
|
||||
Datasources int64
|
||||
Users int64
|
||||
ActiveUsers int64
|
||||
Orgs int64
|
||||
Playlists int64
|
||||
Alerts int64
|
||||
Stars int64
|
||||
}
|
||||
|
||||
type DataSourceStats struct {
|
||||
@@ -22,15 +25,16 @@ type GetDataSourceStatsQuery struct {
|
||||
}
|
||||
|
||||
type AdminStats struct {
|
||||
UserCount int `json:"user_count"`
|
||||
OrgCount int `json:"org_count"`
|
||||
DashboardCount int `json:"dashboard_count"`
|
||||
DbSnapshotCount int `json:"db_snapshot_count"`
|
||||
DbTagCount int `json:"db_tag_count"`
|
||||
DataSourceCount int `json:"data_source_count"`
|
||||
PlaylistCount int `json:"playlist_count"`
|
||||
StarredDbCount int `json:"starred_db_count"`
|
||||
AlertCount int `json:"alert_count"`
|
||||
Users int `json:"users"`
|
||||
Orgs int `json:"orgs"`
|
||||
Dashboards int `json:"dashboards"`
|
||||
Snapshots int `json:"snapshots"`
|
||||
Tags int `json:"tags"`
|
||||
Datasources int `json:"datasources"`
|
||||
Playlists int `json:"playlists"`
|
||||
Stars int `json:"stars"`
|
||||
Alerts int `json:"alerts"`
|
||||
ActiveUsers int `json:"activeUsers"`
|
||||
}
|
||||
|
||||
type GetAdminStatsQuery struct {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
Id int64
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func ParseTagPairs(tagPairs []string) (tags []*Tag) {
|
||||
if tagPairs == nil {
|
||||
return []*Tag{}
|
||||
}
|
||||
|
||||
for _, tagPair := range tagPairs {
|
||||
var tag Tag
|
||||
|
||||
if strings.Contains(tagPair, ":") {
|
||||
keyValue := strings.Split(tagPair, ":")
|
||||
tag.Key = strings.Trim(keyValue[0], " ")
|
||||
tag.Value = strings.Trim(keyValue[1], " ")
|
||||
} else {
|
||||
tag.Key = strings.Trim(tagPair, " ")
|
||||
}
|
||||
|
||||
if tag.Key == "" || ContainsTag(tags, &tag) {
|
||||
continue
|
||||
}
|
||||
|
||||
tags = append(tags, &tag)
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
func ContainsTag(existingTags []*Tag, tag *Tag) bool {
|
||||
for _, t := range existingTags {
|
||||
if t.Key == tag.Key && t.Value == tag.Value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func JoinTagPairs(tags []*Tag) []string {
|
||||
tagPairs := []string{}
|
||||
|
||||
for _, tag := range tags {
|
||||
if tag.Value != "" {
|
||||
tagPairs = append(tagPairs, tag.Key+":"+tag.Value)
|
||||
} else {
|
||||
tagPairs = append(tagPairs, tag.Key)
|
||||
}
|
||||
}
|
||||
|
||||
return tagPairs
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestParsingTags(t *testing.T) {
|
||||
Convey("Testing parsing a tag pairs into tags", t, func() {
|
||||
Convey("Can parse one empty tag", func() {
|
||||
tags := ParseTagPairs([]string{""})
|
||||
So(len(tags), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Can parse valid tags", func() {
|
||||
tags := ParseTagPairs([]string{"outage", "type:outage", "error"})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "outage")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "type")
|
||||
So(tags[1].Value, ShouldEqual, "outage")
|
||||
So(tags[2].Key, ShouldEqual, "error")
|
||||
So(tags[2].Value, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Can parse tags with spaces", func() {
|
||||
tags := ParseTagPairs([]string{" outage ", " type : outage ", "error "})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "outage")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "type")
|
||||
So(tags[1].Value, ShouldEqual, "outage")
|
||||
So(tags[2].Key, ShouldEqual, "error")
|
||||
So(tags[2].Value, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Can parse empty tags", func() {
|
||||
tags := ParseTagPairs([]string{" outage ", "", "", ":", "type : outage ", "error ", "", ""})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "outage")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "type")
|
||||
So(tags[1].Value, ShouldEqual, "outage")
|
||||
So(tags[2].Key, ShouldEqual, "error")
|
||||
So(tags[2].Value, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Can parse tags with extra colons", func() {
|
||||
tags := ParseTagPairs([]string{" outage", "type : outage:outage2 :outage3 ", "error :"})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "outage")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "type")
|
||||
So(tags[1].Value, ShouldEqual, "outage")
|
||||
So(tags[2].Key, ShouldEqual, "error")
|
||||
So(tags[2].Value, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Can parse tags that contains key and values with spaces", func() {
|
||||
tags := ParseTagPairs([]string{" outage 1", "type 1: outage 1 ", "has error "})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "outage 1")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "type 1")
|
||||
So(tags[1].Value, ShouldEqual, "outage 1")
|
||||
So(tags[2].Key, ShouldEqual, "has error")
|
||||
So(tags[2].Value, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Can filter out duplicate tags", func() {
|
||||
tags := ParseTagPairs([]string{"test", "test", "key:val1", "key:val2"})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "test")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "key")
|
||||
So(tags[1].Value, ShouldEqual, "val1")
|
||||
So(tags[2].Key, ShouldEqual, "key")
|
||||
So(tags[2].Value, ShouldEqual, "val2")
|
||||
})
|
||||
|
||||
Convey("Can join tag pairs", func() {
|
||||
tagPairs := []*Tag{
|
||||
{Key: "key1", Value: "val1"},
|
||||
{Key: "key2", Value: ""},
|
||||
{Key: "key3"},
|
||||
}
|
||||
tags := JoinTagPairs(tagPairs)
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0], ShouldEqual, "key1:val1")
|
||||
So(tags[1], ShouldEqual, "key2")
|
||||
So(tags[2], ShouldEqual, "key3")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrTeamNotFound = errors.New("Team not found")
|
||||
ErrTeamNameTaken = errors.New("Team name is taken")
|
||||
ErrTeamMemberNotFound = errors.New("Team member not found")
|
||||
)
|
||||
|
||||
// Team model
|
||||
type Team struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// COMMANDS
|
||||
|
||||
type CreateTeamCommand struct {
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Email string `json:"email"`
|
||||
OrgId int64 `json:"-"`
|
||||
|
||||
Result Team `json:"-"`
|
||||
}
|
||||
|
||||
type UpdateTeamCommand struct {
|
||||
Id int64
|
||||
Name string
|
||||
Email string
|
||||
OrgId int64 `json:"-"`
|
||||
}
|
||||
|
||||
type DeleteTeamCommand struct {
|
||||
OrgId int64
|
||||
Id int64
|
||||
}
|
||||
|
||||
type GetTeamByIdQuery struct {
|
||||
OrgId int64
|
||||
Id int64
|
||||
Result *Team
|
||||
}
|
||||
|
||||
type GetTeamsByUserQuery struct {
|
||||
OrgId int64
|
||||
UserId int64 `json:"userId"`
|
||||
Result []*Team `json:"teams"`
|
||||
}
|
||||
|
||||
type SearchTeamsQuery struct {
|
||||
Query string
|
||||
Name string
|
||||
Limit int
|
||||
Page int
|
||||
OrgId int64
|
||||
|
||||
Result SearchTeamQueryResult
|
||||
}
|
||||
|
||||
type SearchTeamDto struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
MemberCount int64 `json:"memberCount"`
|
||||
}
|
||||
|
||||
type SearchTeamQueryResult struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Teams []*SearchTeamDto `json:"teams"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrTeamMemberAlreadyAdded = errors.New("User is already added to this team")
|
||||
)
|
||||
|
||||
// TeamMember model
|
||||
type TeamMember struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
TeamId int64
|
||||
UserId int64
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// COMMANDS
|
||||
|
||||
type AddTeamMemberCommand struct {
|
||||
UserId int64 `json:"userId" binding:"Required"`
|
||||
OrgId int64 `json:"-"`
|
||||
TeamId int64 `json:"-"`
|
||||
}
|
||||
|
||||
type RemoveTeamMemberCommand struct {
|
||||
OrgId int64 `json:"-"`
|
||||
UserId int64
|
||||
TeamId int64
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// QUERIES
|
||||
|
||||
type GetTeamMembersQuery struct {
|
||||
OrgId int64
|
||||
TeamId int64
|
||||
Result []*TeamMemberDTO
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Projections and DTOs
|
||||
|
||||
type TeamMemberDTO struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
TeamId int64 `json:"teamId"`
|
||||
UserId int64 `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
Login string `json:"login"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
}
|
||||
@@ -60,6 +60,10 @@ type UpdateTempUserStatusCommand struct {
|
||||
Status TempUserStatus
|
||||
}
|
||||
|
||||
type UpdateTempUserWithEmailSentCommand struct {
|
||||
Code string
|
||||
}
|
||||
|
||||
type GetTempUsersQuery struct {
|
||||
OrgId int64
|
||||
Email string
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type InsertSqlTestDataCommand struct {
|
||||
}
|
||||
|
||||
type SqlTestData struct {
|
||||
Id int64
|
||||
Metric1 string
|
||||
Metric2 string
|
||||
ValueBigInt int64
|
||||
ValueDouble float64
|
||||
ValueFloat float32
|
||||
ValueInt int
|
||||
TimeEpoch int64
|
||||
TimeDateTime time.Time
|
||||
}
|
||||
+43
-7
@@ -33,8 +33,9 @@ type User struct {
|
||||
IsAdmin bool
|
||||
OrgId int64
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
LastSeenAt time.Time
|
||||
}
|
||||
|
||||
func (u *User) NameOrFallback() string {
|
||||
@@ -117,6 +118,7 @@ type GetSignedInUserQuery struct {
|
||||
UserId int64
|
||||
Login string
|
||||
Email string
|
||||
OrgId int64
|
||||
Result *SignedInUser
|
||||
}
|
||||
|
||||
@@ -126,6 +128,7 @@ type GetUserProfileQuery struct {
|
||||
}
|
||||
|
||||
type SearchUsersQuery struct {
|
||||
OrgId int64
|
||||
Query string
|
||||
Page int
|
||||
Limit int
|
||||
@@ -157,11 +160,41 @@ type SignedInUser struct {
|
||||
Name string
|
||||
Email string
|
||||
ApiKeyId int64
|
||||
OrgCount int
|
||||
IsGrafanaAdmin bool
|
||||
IsAnonymous bool
|
||||
HelpFlags1 HelpFlags1
|
||||
LastSeenAt time.Time
|
||||
}
|
||||
|
||||
func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {
|
||||
return u.UserId > 0 && time.Since(u.LastSeenAt) > time.Minute*5
|
||||
}
|
||||
|
||||
func (u *SignedInUser) NameOrFallback() string {
|
||||
if u.Name != "" {
|
||||
return u.Name
|
||||
} else if u.Login != "" {
|
||||
return u.Login
|
||||
} else {
|
||||
return u.Email
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateUserLastSeenAtCommand struct {
|
||||
UserId int64
|
||||
}
|
||||
|
||||
func (user *SignedInUser) HasRole(role RoleType) bool {
|
||||
if user.IsGrafanaAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
return user.OrgRole.Includes(role)
|
||||
}
|
||||
|
||||
type UserProfileDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
@@ -171,11 +204,14 @@ type UserProfileDTO struct {
|
||||
}
|
||||
|
||||
type UserSearchHitDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||
LastSeenAtAge string `json:"lastSeenAtAge"`
|
||||
}
|
||||
|
||||
type UserIdDTO struct {
|
||||
|
||||
Reference in New Issue
Block a user