dashboard.go 7.98 KB
Newer Older
Matthew Streatfield's avatar
Matthew Streatfield committed
1
2
package main

3
import (
StreatCodes's avatar
StreatCodes committed
4
5
6
7
8
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
9
	"net/http"
StreatCodes's avatar
StreatCodes committed
10
11
	"os"
	"path"
StreatCodes's avatar
StreatCodes committed
12
	"path/filepath"
StreatCodes's avatar
StreatCodes committed
13
	"regexp"
Max Reeves's avatar
Max Reeves committed
14
	"strconv"
StreatCodes's avatar
StreatCodes committed
15
	"strings"
Max Reeves's avatar
Max Reeves committed
16
	"unicode/utf8"
StreatCodes's avatar
StreatCodes committed
17
18

	"github.com/go-chi/chi"
19
)
Matthew Streatfield's avatar
Matthew Streatfield committed
20

StreatCodes's avatar
StreatCodes committed
21
22
//Dashboard contains all information to render a dashboard
type Dashboard struct {
23
	Title      string    `json:"title"`
StreatCodes's avatar
StreatCodes committed
24
	Slug       string    `json:"slug"`
25
	Background string    `json:"background"`
Max Reeves's avatar
Max Reeves committed
26
27
	Width      string    `json:"width"`
	Height     string    `json:"height"`
28
29
	Tags       []string  `json:"tags"`
	Elements   []Element `json:"elements"`
StreatCodes's avatar
StreatCodes committed
30
31
}

32
//Element contains any service/host information needed
StreatCodes's avatar
StreatCodes committed
33
34
//This is an incomplete representation of the Element
//options arn't included
35
type Element struct {
Max Reeves's avatar
Max Reeves committed
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
	Type    string `json:"type"`
	Title   string `json:"title"`
	Rect    Rect   `json:"rect"`
	Options Option `json:"options"`
}

//Option contains element options
type Option struct {
	CheckID             string `json:"checkId"`
	NameFontSize        int    `json:"nameFontSize"`
	StatusFontSize      int    `json:"statusFontSize"`
	RightArrow          bool   `json:"rightArrow"`
	LeftArrow           bool   `json:"leftArrow"`
	StrokeWidth         int    `json:"strokeWidth"`
	Image               string `json:"image"`
MaxSol1's avatar
svg bug    
MaxSol1 committed
51
	OKSvg               string `json:"okSvg"`
Max Reeves's avatar
Max Reeves committed
52
53
54
55
56
57
58
59
60
61
62
63
64
	OKStrokeColor       string `json:"okStrokeColor"`
	WarningStrokeColor  string `json:"warningStrokeColor"`
	WarningSvg          string `json:"warningSvg"`
	UnknownStrokeColor  string `json:"unknownStrokeColor"`
	UnknownSvg          string `json:"unknownSvg"`
	CriticalStrokeColor string `json:"criticalStrokeColor"`
	CriticalSvg         string `json:"criticalSvg"`
	CriticalImage       string `json:"criticalImage"`
	OkImage             string `json:"okImage"`
	UnknownImage        string `json:"unknownImage"`
	WarningImage        string `json:"warningImage"`
	Svg                 string `json:"svg"`
	StrokeColor         string `json:"strokeColor"`
MaxSol1's avatar
hls fix    
MaxSol1 committed
65
	Source              string `json:"source"`
MaxSol1's avatar
MaxSol1 committed
66
	AudioSource         string `json:"audioSource"`
StreatCodes's avatar
StreatCodes committed
67
68
69
70
}

//Rect helper struct for positions
type Rect struct {
Matthew Streatfield's avatar
Matthew Streatfield committed
71
72
73
74
	X float64 `json:"x"`
	Y float64 `json:"y"`
	W float64 `json:"w"`
	H float64 `json:"h"`
StreatCodes's avatar
StreatCodes committed
75
76
77
78
79
80
81
82
83
84
85
86
87
}

func titleToSlug(title string) string {
	title = strings.ToLower(title)                //convert upper case to lower case
	title = strings.TrimSpace(title)              //remove preceeding and trailing whitespace
	dashSpaceMatch := regexp.MustCompile(`[_\s]`) //convert spaces and underscores to dashes
	title = dashSpaceMatch.ReplaceAllString(title, "-")
	unwantedMatch := regexp.MustCompile(`[^a-z0-9\-]`) //Remove any other characters
	title = unwantedMatch.ReplaceAllString(title, "")

	return title
}

Max Reeves's avatar
Max Reeves committed
88
89
90
91
92
93
94
95
96
func arrayContains(array []string, value string) bool {
	for _, v := range array {
		if v == value {
			return true
		}
	}
	return false
}

StreatCodes's avatar
StreatCodes committed
97
98
99
100
func slugFromFileName(fileName string) string {
	return strings.TrimSuffix(fileName, filepath.Ext(fileName))
}

Matthew Streatfield's avatar
Matthew Streatfield committed
101
func handleListDashboards(w http.ResponseWriter, r *http.Request) {
102
103
104
105
106
107
108
	files, err := ioutil.ReadDir("dashboards")
	if err != nil {
		http.Error(w, "Failed to read directory: "+err.Error(), http.StatusInternalServerError)
		return
	}

	var dashboards []Dashboard
Max Reeves's avatar
Max Reeves committed
109
	tagParam := r.URL.Query().Get("tag")
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

	for _, f := range files {
		if strings.HasSuffix(f.Name(), ".json") {
			data, err := ioutil.ReadFile(path.Join("dashboards", f.Name()))
			if err != nil {
				http.Error(w, "Error reading file contents: "+err.Error(), http.StatusInternalServerError)
				return
			}

			var dashboard Dashboard
			err = json.Unmarshal(data, &dashboard)
			//skip files with invalid json
			if err != nil {
				http.Error(w, "Invalid file: "+err.Error(), http.StatusInternalServerError)
				fmt.Printf("Invalid dashboard file %s - %s\n", f.Name(), err)
				continue
			}

StreatCodes's avatar
StreatCodes committed
128
			dashboard.Slug = slugFromFileName(f.Name())
Max Reeves's avatar
Max Reeves committed
129
130

			if tagParam != "" {
StreatCodes's avatar
StreatCodes committed
131
				if arrayContains(dashboard.Tags, tagParam) {
Max Reeves's avatar
Max Reeves committed
132
133
134
135
136
					dashboards = append(dashboards, dashboard)
				}
			} else {
				dashboards = append(dashboards, dashboard)
			}
137
138
		}
	}
Matthew Streatfield's avatar
Matthew Streatfield committed
139

140
141
142
143
144
	enc := json.NewEncoder(w)
	err = enc.Encode(dashboards)
	if err != nil {
		fmt.Printf("Error encoding response: %s\n", err)
	}
Matthew Streatfield's avatar
Matthew Streatfield committed
145
146
147
}

func handleListDashboard(w http.ResponseWriter, r *http.Request) {
StreatCodes's avatar
StreatCodes committed
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
	slug := chi.URLParam(r, "slug")

	//Check dashboard exists
	if f, err := os.Open(path.Join("dashboards", slug+".json")); os.IsNotExist(err) {
		http.Error(w, "Not found", http.StatusNotFound)
		return
	} else if err != nil {
		http.Error(w, "Error checking file exists: "+err.Error(), http.StatusInternalServerError)
		return
	} else {
		w.Header().Add("content-type", "application/json")
		defer f.Close()
		_, err = io.Copy(w, f)
		if err != nil {
			http.Error(w, "Error writing response: "+err.Error(), http.StatusInternalServerError)
			return
		}
	}
}
Matthew Streatfield's avatar
Matthew Streatfield committed
167

StreatCodes's avatar
StreatCodes committed
168
169
170
//SlugResponse contains the slug for the client to route to
type SlugResponse struct {
	Slug string `json:"slug"`
Matthew Streatfield's avatar
Matthew Streatfield committed
171
172
173
}

func handleCreateDashboard(w http.ResponseWriter, r *http.Request) {
StreatCodes's avatar
StreatCodes committed
174
175
176
	//Decode body
	buf := new(bytes.Buffer)
	buf.ReadFrom(r.Body)
Matthew Streatfield's avatar
Matthew Streatfield committed
177

StreatCodes's avatar
StreatCodes committed
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
	var dashboard Dashboard
	err := json.Unmarshal(buf.Bytes(), &dashboard)
	if err != nil {
		http.Error(w, "Error decoding json body: "+err.Error(), http.StatusBadRequest)
		return
	}

	//Conver title to slug
	slug := titleToSlug(dashboard.Title)
	if len(slug) < 1 {
		http.Error(w, "Generated URL must be atleast one character", http.StatusBadRequest)
		return
	}

	outputFile := path.Join("dashboards", slug+".json")

	//Check dashboard exists
	if _, err := os.Stat(outputFile); os.IsNotExist(err) {
		// path/to/whatever does not exist
		err = ioutil.WriteFile(outputFile, buf.Bytes(), 0655)
		if err != nil {
			http.Error(w, "Error writing file: "+err.Error(), http.StatusInternalServerError)
			return
		}
	} else if err != nil {
		http.Error(w, "Error checking file exists: "+err.Error(), http.StatusInternalServerError)
		return
	}

	//Return slug
	enc := json.NewEncoder(w)
	enc.Encode(SlugResponse{Slug: slug})
Matthew Streatfield's avatar
Matthew Streatfield committed
210
211
}

Max Reeves's avatar
Max Reeves committed
212
213
214
215
216
func trimFirstRune(s string) string {
	_, i := utf8.DecodeRuneInString(s)
	return s[i:]
}

Matthew Streatfield's avatar
Matthew Streatfield committed
217
func handleUpdateDashboard(w http.ResponseWriter, r *http.Request) {
StreatCodes's avatar
StreatCodes committed
218
219
220
221
222
223
224
225
	slug := chi.URLParam(r, "slug")

	//Decode body
	buf := new(bytes.Buffer)
	buf.ReadFrom(r.Body)

	var dashboard Dashboard
	err := json.Unmarshal(buf.Bytes(), &dashboard)
Max Reeves's avatar
Max Reeves committed
226
227
228
229
230
231
	width, height := getImageDimension(trimFirstRune(dashboard.Background))
	dashboard.Height = strconv.Itoa(height)
	dashboard.Width = strconv.Itoa(width)

	// fmt.Println(width, height)

StreatCodes's avatar
StreatCodes committed
232
233
234
235
	if err != nil {
		http.Error(w, "Error decoding json body: "+err.Error(), http.StatusBadRequest)
		return
	}
Matthew Streatfield's avatar
Matthew Streatfield committed
236

StreatCodes's avatar
StreatCodes committed
237
238
239
240
241
242
243
244
245
246
247
248
249
	//Check dashboard exists
	if _, err := os.Stat(path.Join("dashboards", slug+".json")); os.IsNotExist(err) {
		http.Error(w, "Not found", http.StatusNotFound)
		return
	}

	//Convert title to slug
	slugNew := titleToSlug(dashboard.Title)
	if len(slug) < 1 {
		http.Error(w, "Generated URL must be atleast one character", http.StatusBadRequest)
		return
	}

Max Reeves's avatar
Max Reeves committed
250
251
	dashboardJSON, err := json.Marshal(dashboard)
	// fmt.Println(dashboardJSON, err)
StreatCodes's avatar
StreatCodes committed
252
	//Write updated file
Max Reeves's avatar
Max Reeves committed
253
	err = ioutil.WriteFile(path.Join("dashboards", slugNew+".json"), dashboardJSON, 0655)
StreatCodes's avatar
StreatCodes committed
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
	if err != nil {
		http.Error(w, "Error writing file: "+err.Error(), http.StatusInternalServerError)
		return
	}

	//Delete old file if slug updated
	if slug != slugNew {
		fmt.Printf("Slug updated %s -> %s deleting old data\n", slug, slugNew)
		err := os.Remove(path.Join("dashboards", slug+".json"))
		if err != nil {
			http.Error(w, "Failed to remove old file: "+err.Error(), http.StatusInternalServerError)
			return
		}
	}

	//Write slug to response so we can route to it
	enc := json.NewEncoder(w)
	enc.Encode(SlugResponse{Slug: slugNew})
Matthew Streatfield's avatar
Matthew Streatfield committed
272
273
274
}

func handleDeleteDashboard(w http.ResponseWriter, r *http.Request) {
StreatCodes's avatar
StreatCodes committed
275
276
277
278
279
280
281
	slug := chi.URLParam(r, "slug")

	//Check dashboard exists
	if _, err := os.Stat(path.Join("dashboards", slug+".json")); os.IsNotExist(err) {
		http.Error(w, "Not found", http.StatusNotFound)
		return
	}
Matthew Streatfield's avatar
Matthew Streatfield committed
282

StreatCodes's avatar
StreatCodes committed
283
284
285
286
287
288
	fmt.Printf("Deleting dashbaord %s\n", slug)
	err := os.Remove(path.Join("dashboards", slug+".json"))
	if err != nil {
		http.Error(w, "Failed to remove old file: "+err.Error(), http.StatusInternalServerError)
		return
	}
Matthew Streatfield's avatar
Matthew Streatfield committed
289
}