You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

276 lines
7.4 KiB

package main
import (
"fmt"
"html/template"
"io"
"net/url"
)
var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
"withparam": func(origURL, paramName, paramValue string) (string, error) {
if paramName == "" {
return "", fmt.Errorf("paramName is empty")
}
u, err := url.Parse(origURL)
if err != nil {
return "", err
}
v := u.Query()
v.Set(paramName, paramValue)
u.RawQuery = v.Encode()
return u.String(), nil
},
"rawurl": func(v interface{}) template.URL {
return template.URL(fmt.Sprint(v))
},
"rawcss": func(v interface{}) template.CSS {
return template.CSS(fmt.Sprint(v))
},
}).Parse(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{if .Config.Dashboard.Title}}{{.Config.Dashboard.Title}}{{else}}Dashboard{{end}}</title>
<link href="https://fonts.googleapis.com/css?family=Lato:400,400i,700" rel="stylesheet">
<style>
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
background: #fafafa;
}
body {
font-family: Lato, Roboto, sans-serif;
color: #111;
}
.content {
display: block;
}
.nav {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
white-space: nowrap;
padding: 8px;
font-size: 16px;
background: #dedede;
border-bottom: 1px solid #ccc;
}
.nav__title {
display: block;
flex: 0 0 auto;
margin: 8px;
font-weight: 700;
font-size: 20px;
}
.nav__login {
display: block;
flex: 0 0 auto;
margin: 8px;
}
.button,
.button:link,
.button:visited {
display: inline-block;
vertical-align: middle;
border: 1px solid rgba(0, 0, 0, 0.4);
color: #fff;
background: #216bbf;
text-decoration: none;
padding: 4px 8px;
border-radius: 3px;
margin: 0 4px;
}
.button:hover {
background: #2a77cc;
}
.button:active {
background: #1a5597;
}
.apps-category {
display: block;
background: linear-gradient(to bottom, #f3f3f3, #fafafa);
border-bottom: 1px solid #ddd;
padding: 16px;
}
.apps-category__title {
display: block;
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
}
.apps-category__apps {
display: grid;
grid-gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
grid-auto-flow: row dense;
margin: 16px 0;
}
.apps-category__apps__app {
display: flex;
position: relative;
flex-direction: row;
align-items: center;
justify-contents: flex-start;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.05);
background: #fff;
border: 1px solid #ddd;
border-radius: 3px;
}
.apps-category__apps__app:link,
.apps-category__apps__app:visited {
text-decoration: none;
outline: 0;
color: inherit;
}
.apps-category__apps__app:hover {
border-color: #c3c3c3;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.10);
}
.apps-category__apps__app:active,
.apps-category__apps__app:focus {
outline: 1px solid #229;
}
.apps-category__apps__app__icon {
display: block;
flex: 0 0 48px;
max-width: 48px;
height: 48px;
width: 48px;
margin: 8px;
}
.apps-category__apps__app__label {
padding: 8px;
}
.apps-category__apps__app__label__name {
font-size: 16px;
}
.apps-category__apps__app__label__desc {
font-size: 12px;
margin: 4px 0;
}
.apps-category__apps__app__label__domain,
.apps-category__apps__app__label__tags {
display: none;
font-size: 9px;
}
.apps-category__apps__app__badge {
display: block;
height: 16px;
width: 16px;
z-index: 100;
position: absolute;
top: 4px;
right: 4px;
}
</style>
{{with .Config.Dashboard.CustomCSS}}
<style>
{{rawcss .}}
</style>
{{end}}
</head>
<body>
<main class="content">
<nav class="nav">
<div class="nav__title">{{if .Config.Dashboard.Title}}{{.Config.Dashboard.Title}}{{else}}Dashboard{{end}}</div>
<div class="nav__login">
{{if .AuthUser}}
Logged in as {{.AuthUser}}.
{{with .Auth.JWT}}{{if .LogoutURL}}<a class="button button--red" href="{{if .CallbackParam}}{{withparam .LogoutURL .CallbackParam $.URL}}{{else}}{{.LogoutURL}}{{end}}">Logout</a>{{end}}{{end}}
{{else}}
{{with .Auth.JWT}}{{if .LoginURL}}<a class="button button--green" href="{{if .CallbackParam}}{{withparam .LoginURL .CallbackParam $.URL}}{{else}}{{.LoginURL}}{{end}}">Login</a>{{end}}{{end}}
{{end}}
</div>
</nav>
<!-- TODO: tag filter sidebar/navbar -->
{{range $catEntry := .Config.Category.Sorted}}
{{$catID := $catEntry.Key}}
{{$cat := $catEntry.Value}}
<section class="apps-category">
{{if not $cat.HideName}}
<div class="apps-category__title">{{$cat.Name}}</div>
{{end}}
<div class="apps-category__apps">
{{range $appEntry := $cat.App.Sorted}}
{{$appID := $appEntry.Key}}
{{$app := $appEntry.Value}}
<a href="{{rawurl $app.Link}}" title="{{$app.Domain}}" class="apps-category__apps__app">
{{with $app.Icon}}
<img class="apps-category__apps__app__icon" src="icons/{{.}}" alt=""/>
{{end}}
<div class="apps-category__apps__app__label">
<div class="apps-category__apps__app__label__name">{{$app.Name}}</div>
{{with $app.Desc}}
<div class="apps-category__apps__app__label__desc">{{$app.Desc}}</div>
{{end}}
<div class="apps-category__apps__app__label__domain">{{$app.Domain}}</div>
{{with $app.Tag}}
<div class="apps-category__apps__app__label__tags">
{{range $k, $vs := .Props}}
<div class="apps-category__apps__app__label__tags__tag">
<div class="apps-category__apps__app__label__tags__tag__key">{{$k}}</div>
<div class="apps-category__apps__app__label__tags__tag__val">
{{range $i, $v := $vs}}{{if $i}}, {{end}}{{$v}}{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
</div>
{{if or $app.User $cat.User}}
<svg title="Private" class="apps-category__apps__app__badge" fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="24px" height="24px"><path d="M 25 3 C 18.363281 3 13 8.363281 13 15 L 13 20 L 9 20 C 7.355469 20 6 21.355469 6 23 L 6 47 C 6 48.644531 7.355469 50 9 50 L 41 50 C 42.644531 50 44 48.644531 44 47 L 44 23 C 44 21.355469 42.644531 20 41 20 L 37 20 L 37 15 C 37 8.363281 31.636719 3 25 3 Z M 25 5 C 30.566406 5 35 9.433594 35 15 L 35 20 L 15 20 L 15 15 C 15 9.433594 19.433594 5 25 5 Z M 9 22 L 41 22 C 41.554688 22 42 22.445313 42 23 L 42 47 C 42 47.554688 41.554688 48 41 48 L 9 48 C 8.445313 48 8 47.554688 8 47 L 8 23 C 8 22.445313 8.445313 22 9 22 Z M 25 30 C 23.300781 30 22 31.300781 22 33 C 22 33.898438 22.398438 34.6875 23 35.1875 L 23 38 C 23 39.101563 23.898438 40 25 40 C 26.101563 40 27 39.101563 27 38 L 27 35.1875 C 27.601563 34.6875 28 33.898438 28 33 C 28 31.300781 26.699219 30 25 30 Z"/></svg>
{{end}}
</a>
{{end}}
</div>
</section>
{{end}}
</main>
</body>
</html>
`))
// RenderIndex renders the dashboard. The provided config must be filtered
// already, if needed. The URL must be the fully-qualified URL, including the
// hostname, of the current page (and is used for login/logouts callbacks).
func RenderIndex(w io.Writer, filteredConfig Config, authProvider AuthProviders, authUser, url string) error {
return tmpl.Execute(w, map[string]interface{}{
"Config": filteredConfig,
"Auth": authProvider,
"AuthUser": authUser,
"URL": url,
})
}