mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-25 21:09:05 +01:00
Merge branch 'master' into dev-job-archive-module
This commit is contained in:
commit
918a07735d
@ -718,14 +718,26 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Handle anything but roles...
|
||||
// Get Values
|
||||
newrole := r.FormValue("add-role")
|
||||
delrole := r.FormValue("remove-role")
|
||||
|
||||
// TODO: Handle anything but roles...
|
||||
if (newrole != "") {
|
||||
if err := api.Authentication.AddRole(r.Context(), mux.Vars(r)["id"], newrole); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Write([]byte("success"))
|
||||
rw.Write([]byte("Add Role Success"))
|
||||
} else if (delrole != "") {
|
||||
if err := api.Authentication.RemoveRole(r.Context(), mux.Vars(r)["id"], delrole); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
rw.Write([]byte("Remove Role Success"))
|
||||
} else {
|
||||
http.Error(rw, "Not Add or Del?", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request) {
|
||||
|
@ -120,7 +120,7 @@ func (auth *Authentication) AddRole(
|
||||
return err
|
||||
}
|
||||
|
||||
if role != RoleAdmin && role != RoleApi && role != RoleUser {
|
||||
if role != RoleAdmin && role != RoleApi && role != RoleUser && role != RoleSupport {
|
||||
return fmt.Errorf("invalid user role: %#v", role)
|
||||
}
|
||||
|
||||
@ -137,13 +137,40 @@ func (auth *Authentication) AddRole(
|
||||
return nil
|
||||
}
|
||||
|
||||
func FetchUser(
|
||||
ctx context.Context,
|
||||
db *sqlx.DB,
|
||||
username string) (*model.User, error) {
|
||||
func (auth *Authentication) RemoveRole(ctx context.Context, username string, role string) error {
|
||||
user, err := auth.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if role != RoleAdmin && role != RoleApi && role != RoleUser {
|
||||
return fmt.Errorf("invalid user role: %#v", role)
|
||||
}
|
||||
|
||||
var exists bool
|
||||
var newroles []string
|
||||
for _, r := range user.Roles {
|
||||
if r != role {
|
||||
newroles = append(newroles, r) // Append all roles not matching requested delete role
|
||||
} else {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
|
||||
if (exists == true) {
|
||||
var mroles, _ = json.Marshal(newroles)
|
||||
if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("user %#v already does not have role %#v", username, role)
|
||||
}
|
||||
}
|
||||
|
||||
func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) {
|
||||
me := GetUser(ctx)
|
||||
if me != nil && !me.HasRole(RoleAdmin) && me.Username != username {
|
||||
if me != nil && !me.HasRole(RoleAdmin) && !me.HasRole(RoleSupport) && me.Username != username {
|
||||
return nil, errors.New("forbidden")
|
||||
}
|
||||
|
||||
|
@ -152,9 +152,7 @@ func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user := auth.GetUser(ctx); user != nil &&
|
||||
!user.HasRole(auth.RoleAdmin) &&
|
||||
job.User != user.Username {
|
||||
if user := auth.GetUser(ctx); user != nil && !user.HasRole(auth.RoleAdmin) && !user.HasRole(auth.RoleSupport) && job.User != user.Username {
|
||||
return nil, errors.New("you are not allowed to see this job")
|
||||
}
|
||||
|
||||
|
@ -308,7 +308,7 @@ func (r *JobRepository) FindJobOrUser(ctx context.Context, searchterm string) (j
|
||||
user := auth.GetUser(ctx)
|
||||
if id, err := strconv.Atoi(searchterm); err == nil {
|
||||
qb := sq.Select("job.id").From("job").Where("job.job_id = ?", id)
|
||||
if user != nil && !user.HasRole(auth.RoleAdmin) {
|
||||
if user != nil && !user.HasRole(auth.RoleAdmin) && !user.HasRole(auth.RoleSupport) {
|
||||
qb = qb.Where("job.user = ?", user.Username)
|
||||
}
|
||||
|
||||
@ -320,7 +320,7 @@ func (r *JobRepository) FindJobOrUser(ctx context.Context, searchterm string) (j
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil || user.HasRole(auth.RoleAdmin) {
|
||||
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
|
||||
err := sq.Select("job.user").Distinct().From("job").
|
||||
Where("job.user = ?", searchterm).
|
||||
RunWith(r.stmtCache).QueryRow().Scan(&username)
|
||||
|
@ -94,7 +94,7 @@ func (r *JobRepository) CountJobs(
|
||||
|
||||
func SecurityCheck(ctx context.Context, query sq.SelectBuilder) sq.SelectBuilder {
|
||||
user := auth.GetUser(ctx)
|
||||
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleApi) {
|
||||
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleApi) || user.HasRole(auth.RoleSupport) {
|
||||
return query
|
||||
}
|
||||
|
||||
|
@ -270,15 +270,17 @@ func SetupRoutes(router *mux.Router) {
|
||||
title = strings.Replace(route.Title, "<ID>", id.(string), 1)
|
||||
}
|
||||
|
||||
username, isAdmin := "", true
|
||||
username, isAdmin, isSupporter := "", true, true
|
||||
|
||||
if user := auth.GetUser(r.Context()); user != nil {
|
||||
username = user.Username
|
||||
isAdmin = user.HasRole(auth.RoleAdmin)
|
||||
isSupporter = user.HasRole(auth.RoleSupport)
|
||||
}
|
||||
|
||||
page := web.Page{
|
||||
Title: title,
|
||||
User: web.User{Username: username, IsAdmin: isAdmin},
|
||||
User: web.User{Username: username, IsAdmin: isAdmin, IsSupporter: isSupporter},
|
||||
Config: conf,
|
||||
Infos: infos,
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
# In-Memory LRU Cache for Golang Applications
|
||||
|
||||
[![](https://pkg.go.dev/badge/github.com/iamlouk/lrucache?utm_source=godoc)](https://pkg.go.dev/github.com/iamlouk/lrucache)
|
||||
|
||||
This library can be embedded into your existing go applications
|
||||
and play the role *Memcached* or *Redis* might play for others.
|
||||
It is inspired by [PHP Symfony's Cache Components](https://symfony.com/doc/current/components/cache/adapters/array_cache_adapter.html),
|
||||
|
@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
|
||||
// All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
package lrucache
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
|
||||
// All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
package lrucache
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
|
||||
// All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
package lrucache
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
|
||||
// All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
package lrucache
|
||||
|
||||
import (
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "svelte-app",
|
||||
"name": "cc-frontend",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w"
|
||||
@ -12,7 +13,7 @@
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-svelte": "^7.0.0",
|
||||
"rollup-plugin-terser": "^7.0.0",
|
||||
"svelte": "^3.42.6"
|
||||
"svelte": "^3.49.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rollup/plugin-replace": "^2.4.1",
|
||||
|
@ -66,6 +66,6 @@ export default [
|
||||
entrypoint('systems', 'src/systems.entrypoint.js'),
|
||||
entrypoint('node', 'src/node.entrypoint.js'),
|
||||
entrypoint('analysis', 'src/analysis.entrypoint.js'),
|
||||
entrypoint('status', 'src/status.entrypoint.js')
|
||||
entrypoint('status', 'src/status.entrypoint.js'),
|
||||
entrypoint('config', 'src/config.entrypoint.js')
|
||||
];
|
||||
|
||||
|
31
web/frontend/src/Config.root.svelte
Normal file
31
web/frontend/src/Config.root.svelte
Normal file
@ -0,0 +1,31 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte'
|
||||
import { init } from './utils.js'
|
||||
import { Card, CardHeader, CardTitle } from 'sveltestrap'
|
||||
|
||||
import PlotSettings from './config/PlotSettings.svelte'
|
||||
import AdminSettings from './config/AdminSettings.svelte'
|
||||
|
||||
const { query: initq } = init()
|
||||
|
||||
const ccconfig = getContext('cc-config')
|
||||
|
||||
export let user
|
||||
|
||||
</script>
|
||||
|
||||
{#if user.IsAdmin}
|
||||
<Card style="margin-bottom: 1.5em;">
|
||||
<CardHeader>
|
||||
<CardTitle class="mb-1">Admin Options</CardTitle>
|
||||
</CardHeader>
|
||||
<AdminSettings/>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="mb-1">Plotting Options</CardTitle>
|
||||
</CardHeader>
|
||||
<PlotSettings config={ccconfig}/>
|
||||
</Card>
|
12
web/frontend/src/config.entrypoint.js
Normal file
12
web/frontend/src/config.entrypoint.js
Normal file
@ -0,0 +1,12 @@
|
||||
import {} from './header.entrypoint.js'
|
||||
import Config from './Config.root.svelte'
|
||||
|
||||
new Config({
|
||||
target: document.getElementById('svelte-app'),
|
||||
props: {
|
||||
user: user
|
||||
},
|
||||
context: new Map([
|
||||
['cc-config', clusterCockpitConfig]
|
||||
])
|
||||
})
|
36
web/frontend/src/config/AdminSettings.svelte
Normal file
36
web/frontend/src/config/AdminSettings.svelte
Normal file
@ -0,0 +1,36 @@
|
||||
<script>
|
||||
import { Row, Col } from 'sveltestrap'
|
||||
import { onMount } from 'svelte'
|
||||
import EditRole from './admin/EditRole.svelte'
|
||||
import AddUser from './admin/AddUser.svelte'
|
||||
import ShowUsers from './admin/ShowUsers.svelte'
|
||||
import Options from './admin/Options.svelte'
|
||||
|
||||
let users = []
|
||||
|
||||
function getUserList() {
|
||||
fetch('/api/users/?via-ldap=false¬-just-user=true')
|
||||
.then(res => res.json())
|
||||
.then(usersRaw => {
|
||||
users = usersRaw
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => getUserList())
|
||||
|
||||
</script>
|
||||
|
||||
<Row cols={2} class="p-2 g-2" >
|
||||
<Col class="mb-1">
|
||||
<AddUser on:reload={getUserList}/>
|
||||
</Col>
|
||||
<Col class="mb-1">
|
||||
<ShowUsers on:reload={getUserList} bind:users={users}/>
|
||||
</Col>
|
||||
<Col>
|
||||
<EditRole on:reload={getUserList}/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Options/>
|
||||
</Col>
|
||||
</Row>
|
171
web/frontend/src/config/PlotSettings.svelte
Normal file
171
web/frontend/src/config/PlotSettings.svelte
Normal file
@ -0,0 +1,171 @@
|
||||
<script>
|
||||
import { Button, Table, Row, Col, Card, CardBody, CardTitle } from 'sveltestrap'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
export let config
|
||||
|
||||
let message = {msg: '', target: '', color: '#d63384'}
|
||||
let displayMessage = false
|
||||
|
||||
const colorschemes = {
|
||||
'Default': ["#00bfff","#0000ff","#ff00ff","#ff0000","#ff8000","#ffff00","#80ff00"],
|
||||
'Autumn': ['rgb(255,0,0)','rgb(255,11,0)','rgb(255,20,0)','rgb(255,30,0)','rgb(255,41,0)','rgb(255,50,0)','rgb(255,60,0)','rgb(255,71,0)','rgb(255,80,0)','rgb(255,90,0)','rgb(255,101,0)','rgb(255,111,0)','rgb(255,120,0)','rgb(255,131,0)','rgb(255,141,0)','rgb(255,150,0)','rgb(255,161,0)','rgb(255,171,0)','rgb(255,180,0)','rgb(255,190,0)','rgb(255,201,0)','rgb(255,210,0)','rgb(255,220,0)','rgb(255,231,0)','rgb(255,240,0)','rgb(255,250,0)'],
|
||||
'Beach': ['rgb(0,252,0)','rgb(0,233,0)','rgb(0,212,0)','rgb(0,189,0)','rgb(0,169,0)','rgb(0,148,0)','rgb(0,129,4)','rgb(0,145,46)','rgb(0,162,90)','rgb(0,180,132)','rgb(29,143,136)','rgb(73,88,136)','rgb(115,32,136)','rgb(81,9,64)','rgb(124,51,23)','rgb(162,90,0)','rgb(194,132,0)','rgb(220,171,0)','rgb(231,213,0)','rgb(0,0,13)','rgb(0,0,55)','rgb(0,0,92)','rgb(0,0,127)','rgb(0,0,159)','rgb(0,0,196)','rgb(0,0,233)'],
|
||||
'BlueRed': ['rgb(0,0,131)','rgb(0,0,168)','rgb(0,0,208)','rgb(0,0,247)','rgb(0,27,255)','rgb(0,67,255)','rgb(0,108,255)','rgb(0,148,255)','rgb(0,187,255)','rgb(0,227,255)','rgb(8,255,247)','rgb(48,255,208)','rgb(87,255,168)','rgb(127,255,127)','rgb(168,255,87)','rgb(208,255,48)','rgb(247,255,8)','rgb(255,224,0)','rgb(255,183,0)','rgb(255,143,0)','rgb(255,104,0)','rgb(255,64,0)','rgb(255,23,0)','rgb(238,0,0)','rgb(194,0,0)','rgb(150,0,0)'],
|
||||
'Rainbow': ['rgb(125,0,255)','rgb(85,0,255)','rgb(39,0,255)','rgb(0,6,255)','rgb(0,51,255)','rgb(0,97,255)','rgb(0,141,255)','rgb(0,187,255)','rgb(0,231,255)','rgb(0,255,233)','rgb(0,255,189)','rgb(0,255,143)','rgb(0,255,99)','rgb(0,255,53)','rgb(0,255,9)','rgb(37,255,0)','rgb(83,255,0)','rgb(127,255,0)','rgb(173,255,0)','rgb(217,255,0)','rgb(255,248,0)','rgb(255,203,0)','rgb(255,159,0)','rgb(255,113,0)','rgb(255,69,0)','rgb(255,23,0)'],
|
||||
'Binary': ['rgb(215,215,215)','rgb(206,206,206)','rgb(196,196,196)','rgb(185,185,185)','rgb(176,176,176)','rgb(166,166,166)','rgb(155,155,155)','rgb(145,145,145)','rgb(136,136,136)','rgb(125,125,125)','rgb(115,115,115)','rgb(106,106,106)','rgb(95,95,95)','rgb(85,85,85)','rgb(76,76,76)','rgb(66,66,66)','rgb(55,55,55)','rgb(46,46,46)','rgb(36,36,36)','rgb(25,25,25)','rgb(16,16,16)','rgb(6,6,6)'],
|
||||
'GistEarth': ['rgb(0,0,0)','rgb(2,7,117)','rgb(9,30,118)','rgb(16,53,120)','rgb(23,73,122)','rgb(31,93,124)','rgb(39,110,125)','rgb(47,126,127)','rgb(51,133,119)','rgb(57,138,106)','rgb(62,145,94)','rgb(66,150,82)','rgb(74,157,71)','rgb(97,162,77)','rgb(121,168,83)','rgb(136,173,85)','rgb(153,176,88)','rgb(170,180,92)','rgb(185,182,94)','rgb(189,173,99)','rgb(192,164,101)','rgb(203,169,124)','rgb(215,178,149)','rgb(226,192,176)','rgb(238,212,204)','rgb(248,236,236)'],
|
||||
'BlueWaves': ['rgb(83,0,215)','rgb(43,6,108)','rgb(9,16,16)','rgb(8,32,25)','rgb(0,50,8)','rgb(27,64,66)','rgb(69,67,178)','rgb(115,62,210)','rgb(155,50,104)','rgb(178,43,41)','rgb(180,51,34)','rgb(161,78,87)','rgb(124,117,187)','rgb(78,155,203)','rgb(34,178,85)','rgb(4,176,2)','rgb(9,152,27)','rgb(4,118,2)','rgb(34,92,85)','rgb(78,92,203)','rgb(124,127,187)','rgb(161,187,87)','rgb(180,248,34)','rgb(178,220,41)','rgb(155,217,104)','rgb(115,254,210)'],
|
||||
'BlueGreenRedYellow': ['rgb(0,0,0)','rgb(0,0,20)','rgb(0,0,41)','rgb(0,0,62)','rgb(0,25,83)','rgb(0,57,101)','rgb(0,87,101)','rgb(0,118,101)','rgb(0,150,101)','rgb(0,150,69)','rgb(0,148,37)','rgb(0,141,6)','rgb(60,120,0)','rgb(131,87,0)','rgb(180,25,0)','rgb(203,13,0)','rgb(208,36,0)','rgb(213,60,0)','rgb(219,83,0)','rgb(224,106,0)','rgb(229,129,0)','rgb(233,152,0)','rgb(238,176,0)','rgb(243,199,0)','rgb(248,222,0)','rgb(254,245,0)']
|
||||
};
|
||||
|
||||
async function handleSettingSubmit(selector, target) {
|
||||
let form = document.querySelector(selector)
|
||||
let formData = new FormData(form)
|
||||
try {
|
||||
const res = await fetch(form.action, { method: 'POST', body: formData });
|
||||
if (res.ok) {
|
||||
let text = await res.text()
|
||||
popMessage(text, target, '#048109')
|
||||
} else {
|
||||
let text = await res.text()
|
||||
// console.log(res.statusText)
|
||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, target, '#d63384')
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function popMessage(response, restarget, rescolor) {
|
||||
message = {msg: response, target: restarget, color: rescolor}
|
||||
displayMessage = true
|
||||
setTimeout(function() {
|
||||
displayMessage = false
|
||||
}, 3500)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<Row cols={3} class="p-2 g-2">
|
||||
<!-- LINE WIDTH -->
|
||||
<Col><Card class="h-100">
|
||||
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
|
||||
<form id="line-width-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#line-width-form', 'lw')}>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
||||
<div>Line Width</div>
|
||||
<!-- Expand If-Clause for clarity once -->
|
||||
{#if displayMessage && message.target == 'lw'}
|
||||
<div style="margin-left: auto; font-size: 0.9em;">
|
||||
<code style="color: {message.color};" out:fade>
|
||||
Update: {message.msg}
|
||||
</code>
|
||||
</div>
|
||||
{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_lineWidth"/>
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Line Width</label>
|
||||
<input type="number" class="form-control" id="lwvalue" name="value" aria-describedby="lineWidthHelp" value="{config.plot_general_lineWidth}" min="1"/>
|
||||
<div id="lineWidthHelp" class="form-text">Width of the lines in the timeseries plots.</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col>
|
||||
|
||||
<!-- PLOTS PER ROW -->
|
||||
<Col><Card class="h-100">
|
||||
<form id="plots-per-row-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#plots-per-row-form', 'ppr')}>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
||||
<div>Plots per Row</div>
|
||||
{#if displayMessage && message.target == 'ppr'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_view_plotsPerRow"/>
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Plots per Row</label>
|
||||
<input type="number" class="form-control" id="pprvalue" name="value" aria-describedby="plotsperrowHelp" value="{config.plot_view_plotsPerRow }" min="1"/>
|
||||
<div id="plotsperrowHelp" class="form-text">How many plots to show next to each other on pages such as /monitoring/job/, /monitoring/system/...</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col>
|
||||
|
||||
<!-- BACKGROUND -->
|
||||
<Col><Card class="h-100">
|
||||
<form id="backgrounds-form" method="post" action="/api/configuration/" class="card-body" on:submit|preventDefault={() => handleSettingSubmit('#backgrounds-form', 'bg')}>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
||||
<div>Colored Backgrounds</div>
|
||||
{#if displayMessage && message.target == 'bg'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_colorBackground"/>
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
{#if config.plot_general_colorBackground}
|
||||
<input type="radio" id="true" name="value" value="true" checked/>
|
||||
{:else}
|
||||
<input type="radio" id="true" name="value" value="true" />
|
||||
{/if}
|
||||
<label for="true">Yes</label>
|
||||
</div>
|
||||
<div>
|
||||
{#if config.plot_general_colorBackground}
|
||||
<input type="radio" id="false" name="value" value="false" />
|
||||
{:else}
|
||||
<input type="radio" id="false" name="value" value="false" checked/>
|
||||
{/if}
|
||||
<label for="false">No</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col>
|
||||
</Row>
|
||||
|
||||
<Row cols={1} class="p-2 g-2">
|
||||
<!-- COLORSCHEME -->
|
||||
<Col><Card>
|
||||
<form id="colorscheme-form" method="post" action="/api/configuration/" class="card-body">
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle style="margin-bottom: 1em; display: flex; align-items: center;">
|
||||
<div>Color Scheme for Timeseries Plots</div>
|
||||
{#if displayMessage && message.target == 'cs'}<div style="margin-left: auto; font-size: 0.9em;"><code style="color: {message.color};" out:fade>Update: {message.msg}</code></div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_colorscheme"/>
|
||||
<Table hover>
|
||||
<tbody>
|
||||
{#each Object.entries(colorschemes) as [name, rgbrow]}
|
||||
<tr>
|
||||
<th scope="col">{name}</th>
|
||||
<td>
|
||||
{#if rgbrow.join(',') == config.plot_general_colorscheme}
|
||||
<input type="radio" name="value" value={JSON.stringify(rgbrow)} checked on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/>
|
||||
{:else}
|
||||
<input type="radio" name="value" value={JSON.stringify(rgbrow)} on:click={() => handleSettingSubmit("#colorscheme-form", "cs")}/>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#each rgbrow as rgb}
|
||||
<span class="color-dot" style="background-color: {rgb};"></span>
|
||||
{/each}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</form>
|
||||
</Card></Col>
|
||||
</Row>
|
||||
|
||||
<style>
|
||||
.color-dot {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
91
web/frontend/src/config/admin/AddUser.svelte
Normal file
91
web/frontend/src/config/admin/AddUser.svelte
Normal file
@ -0,0 +1,91 @@
|
||||
<script>
|
||||
import { Button, Card, CardTitle } from 'sveltestrap'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let message = {msg: '', color: '#d63384'}
|
||||
let displayMessage = false
|
||||
|
||||
async function handleUserSubmit() {
|
||||
let form = document.querySelector('#create-user-form')
|
||||
let formData = new FormData(form)
|
||||
|
||||
try {
|
||||
const res = await fetch(form.action, { method: 'POST', body: formData });
|
||||
if (res.ok) {
|
||||
let text = await res.text()
|
||||
popMessage(text, '#048109')
|
||||
reloadUserList()
|
||||
} else {
|
||||
let text = await res.text()
|
||||
// console.log(res.statusText)
|
||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, '#d63384')
|
||||
}
|
||||
}
|
||||
|
||||
function popMessage(response, rescolor) {
|
||||
message = {msg: response, color: rescolor}
|
||||
displayMessage = true
|
||||
setTimeout(function() {
|
||||
displayMessage = false
|
||||
}, 3500)
|
||||
}
|
||||
|
||||
function reloadUserList() {
|
||||
dispatch('reload')
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<form id="create-user-form" method="post" action="/api/users/" class="card-body" on:submit|preventDefault={handleUserSubmit}>
|
||||
<CardTitle class="mb-3">Create User</CardTitle>
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" aria-describedby="nameHelp"/>
|
||||
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp"/>
|
||||
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" aria-describedby="usernameHelp"/>
|
||||
<div id="usernameHelp" class="form-text">Must be unique.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" aria-describedby="passwordHelp"/>
|
||||
<div id="passwordHelp" class="form-text">Only API users are allowed to have a blank password. Users with a blank password can only authenticate via Tokens.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<p>Role:</p>
|
||||
<div>
|
||||
<input type="radio" id="user" name="role" value="user" checked/>
|
||||
<label for="user">User (regular user, same as if created via LDAP sync.)</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="api" name="role" value="api"/>
|
||||
<label for="api">API</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="support" name="role" value="support"/>
|
||||
<label for="support">Support</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="admin" name="role" value="admin"/>
|
||||
<label for="admin">Admin</label>
|
||||
</div>
|
||||
</div>
|
||||
<p style="display: flex; align-items: center;">
|
||||
<Button type="submit" color="primary">Submit</Button>
|
||||
{#if displayMessage}<div style="margin-left: 1.5em;"><b><code style="color: {message.color};" out:fade>{message.msg}</code></b></div>{/if}
|
||||
</p>
|
||||
</form>
|
||||
</Card>
|
103
web/frontend/src/config/admin/EditRole.svelte
Normal file
103
web/frontend/src/config/admin/EditRole.svelte
Normal file
@ -0,0 +1,103 @@
|
||||
<script>
|
||||
import { Card, CardTitle, CardBody } from 'sveltestrap'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let message = {msg: '', color: '#d63384'}
|
||||
let displayMessage = false
|
||||
|
||||
async function handleAddRole() {
|
||||
const username = document.querySelector('#role-username').value
|
||||
const role = document.querySelector('#role-select').value
|
||||
|
||||
if (username == "" || role == "") {
|
||||
alert('Please fill in a username and select a role.')
|
||||
return
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('username', username)
|
||||
formData.append('add-role', role)
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
|
||||
if (res.ok) {
|
||||
let text = await res.text()
|
||||
popMessage(text, '#048109')
|
||||
reloadUserList()
|
||||
} else {
|
||||
let text = await res.text()
|
||||
// console.log(res.statusText)
|
||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, '#d63384')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveRole() {
|
||||
const username = document.querySelector('#role-username').value
|
||||
const role = document.querySelector('#role-select').value
|
||||
|
||||
if (username == "" || role == "") {
|
||||
alert('Please fill in a username and select a role.')
|
||||
return
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('username', username)
|
||||
formData.append('remove-role', role)
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
|
||||
if (res.ok) {
|
||||
let text = await res.text()
|
||||
popMessage(text, '#048109')
|
||||
reloadUserList()
|
||||
} else {
|
||||
let text = await res.text()
|
||||
// console.log(res.statusText)
|
||||
throw new Error('Response Code ' + res.status + '-> ' + text)
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, '#d63384')
|
||||
}
|
||||
}
|
||||
|
||||
function popMessage(response, rescolor) {
|
||||
message = {msg: response, color: rescolor}
|
||||
displayMessage = true
|
||||
setTimeout(function() {
|
||||
displayMessage = false
|
||||
}, 3500)
|
||||
}
|
||||
|
||||
function reloadUserList() {
|
||||
dispatch('reload')
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Edit User Roles</CardTitle>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" placeholder="username" id="role-username"/>
|
||||
<select class="form-select" id="role-select">
|
||||
<option selected value="">Role...</option>
|
||||
<option value="user">User</option>
|
||||
<option value="support">Support</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="api">API</option>
|
||||
</select>
|
||||
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
||||
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
||||
<button class="btn btn-primary" type="button" id="add-role-button" on:click|preventDefault={handleAddRole}>Add</button>
|
||||
<button class="btn btn-danger" type="button" id="remove-role-button" on:click|preventDefault={handleRemoveRole}>Remove</button>
|
||||
</div>
|
||||
<p>
|
||||
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if}
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
29
web/frontend/src/config/admin/Options.svelte
Normal file
29
web/frontend/src/config/admin/Options.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import { Card, CardBody, CardTitle } from 'sveltestrap'
|
||||
|
||||
let scrambled
|
||||
|
||||
onMount(() => {
|
||||
scrambled = window.localStorage.getItem("cc-scramble-names") != null
|
||||
})
|
||||
|
||||
function handleScramble() {
|
||||
if (!scrambled) {
|
||||
scrambled = true
|
||||
window.localStorage.setItem("cc-scramble-names", "true")
|
||||
} else {
|
||||
scrambled = false
|
||||
window.localStorage.removeItem("cc-scramble-names")
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<Card class="h-100">
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Scramble Names / Presentation Mode</CardTitle>
|
||||
<input type="checkbox" id="scramble-names-checkbox" style="margin-right: 1em;" on:click={handleScramble} bind:checked={scrambled}/>
|
||||
Active?
|
||||
</CardBody>
|
||||
</Card>
|
67
web/frontend/src/config/admin/ShowUsers.svelte
Normal file
67
web/frontend/src/config/admin/ShowUsers.svelte
Normal file
@ -0,0 +1,67 @@
|
||||
<script>
|
||||
import { Button, Table, Card, CardTitle, CardBody } from 'sveltestrap'
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import ShowUsersRow from './ShowUsersRow.svelte'
|
||||
|
||||
export let users = []
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function reloadUserList() {
|
||||
dispatch('reload')
|
||||
}
|
||||
|
||||
function deleteUser(username) {
|
||||
if (confirm('Are you sure?')) {
|
||||
let formData = new FormData()
|
||||
formData.append('username', username)
|
||||
fetch('/api/users/', { method: 'DELETE', body: formData }).then(res => {
|
||||
if (res.status == 200) {
|
||||
reloadUserList()
|
||||
} else {
|
||||
confirm(res.statusText)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$: userList = users
|
||||
|
||||
</script>
|
||||
|
||||
<Card class="h-100">
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Special Users</CardTitle>
|
||||
<p>
|
||||
Not created by an LDAP sync and/or having a role other than <code>user</code>
|
||||
<Button color="secondary" size="sm" on:click={reloadUserList} style="float: right;">Reload</Button>
|
||||
</p>
|
||||
<div style="width: 100%; max-height: 500px; overflow-y: scroll;">
|
||||
<Table hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Roles</th>
|
||||
<th>JWT</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-list">
|
||||
{#each userList as user}
|
||||
<tr id="user-{user.username}">
|
||||
<ShowUsersRow {user}/>
|
||||
<td><button class="btn btn-danger del-user" on:click={deleteUser(user.username)}>Delete</button></td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
27
web/frontend/src/config/admin/ShowUsersRow.svelte
Normal file
27
web/frontend/src/config/admin/ShowUsersRow.svelte
Normal file
@ -0,0 +1,27 @@
|
||||
<script>
|
||||
import { Button } from 'sveltestrap'
|
||||
|
||||
export let user
|
||||
let jwt = ""
|
||||
|
||||
function getUserJwt(username) {
|
||||
fetch(`/api/jwt/?username=${username}`)
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
jwt = text
|
||||
navigator.clipboard.writeText(text).catch(reason => console.error(reason))
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<td>{user.username}</td>
|
||||
<td>{user.name}</td>
|
||||
<td>{user.email}</td>
|
||||
<td><code>{user.roles.join(', ')}</code></td>
|
||||
<td>
|
||||
{#if ! jwt}
|
||||
<Button color="success" on:click={getUserJwt(user.username)}>Gen. JWT</Button>
|
||||
{:else}
|
||||
<textarea rows="3" cols="20">{jwt}</textarea>
|
||||
{/if}
|
||||
</td>
|
@ -28,6 +28,46 @@
|
||||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052"
|
||||
integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==
|
||||
|
||||
"@jridgewell/gen-mapping@^0.3.0":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
|
||||
integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
|
||||
dependencies:
|
||||
"@jridgewell/set-array" "^1.0.1"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@jridgewell/resolve-uri@^3.0.3":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
|
||||
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
|
||||
|
||||
"@jridgewell/set-array@^1.0.1":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
|
||||
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
|
||||
|
||||
"@jridgewell/source-map@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
|
||||
integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
|
||||
dependencies:
|
||||
"@jridgewell/gen-mapping" "^0.3.0"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.10":
|
||||
version "1.4.14"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
||||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
||||
|
||||
"@jridgewell/trace-mapping@^0.3.9":
|
||||
version "0.3.14"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
|
||||
integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@popperjs/core@^2.9.2":
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.0.tgz#6734f8ebc106a0860dff7f92bf90df193f0935d7"
|
||||
@ -121,6 +161,11 @@
|
||||
"@urql/core" "^2.3.4"
|
||||
wonka "^4.0.14"
|
||||
|
||||
acorn@^8.5.0:
|
||||
version "8.8.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
|
||||
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
@ -432,11 +477,6 @@ source-map@^0.6.0:
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
source-map@~0.7.2:
|
||||
version "0.7.3"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
|
||||
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
|
||||
|
||||
sourcemap-codec@^1.4.4:
|
||||
version "1.4.8"
|
||||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
||||
@ -456,10 +496,10 @@ supports-color@^7.0.0:
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
svelte@^3.42.6:
|
||||
version "3.44.2"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.44.2.tgz#3e69be2598308dfc8354ba584cec54e648a50f7f"
|
||||
integrity sha512-jrZhZtmH3ZMweXg1Q15onb8QlWD+a5T5Oca4C1jYvSURp2oD35h4A5TV6t6MEa93K4LlX6BkafZPdQoFjw/ylA==
|
||||
svelte@^3.49.0:
|
||||
version "3.49.0"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029"
|
||||
integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==
|
||||
|
||||
sveltestrap@^5.6.1:
|
||||
version "5.6.3"
|
||||
@ -469,12 +509,13 @@ sveltestrap@^5.6.1:
|
||||
"@popperjs/core" "^2.9.2"
|
||||
|
||||
terser@^5.0.0:
|
||||
version "5.10.0"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc"
|
||||
integrity sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==
|
||||
version "5.14.2"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10"
|
||||
integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.2"
|
||||
acorn "^8.5.0"
|
||||
commander "^2.20.0"
|
||||
source-map "~0.7.2"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
uplot@^1.6.7:
|
||||
|
@ -1,295 +1,15 @@
|
||||
{{define "content"}}
|
||||
{{if .User.IsAdmin}}
|
||||
<div class="row">
|
||||
<div class="col card" style="margin: 10px;">
|
||||
<form id="create-user-form" method="post" action="/api/users/" class="card-body">
|
||||
<h5 class="card-title">Create User</h5>
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" aria-describedby="nameHelp"/>
|
||||
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp"/>
|
||||
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" aria-describedby="usernameHelp"/>
|
||||
<div id="usernameHelp" class="form-text">Must be unique.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" aria-describedby="passwordHelp"/>
|
||||
<div id="passwordHelp" class="form-text">Only API users are allowed to have a blank password. Users with a blank password can only authenticate via Tokens.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<p>Role:</p>
|
||||
<div>
|
||||
<input type="radio" id="user" name="role" value="user" checked/>
|
||||
<label for="user">User (regular user, same as if created via LDAP sync.)</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="api" name="role" value="api"/>
|
||||
<label for="api">API</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="admin" name="role" value="admin"/>
|
||||
<label for="admin">Admin</label>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<code class="form-result"></code>
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col card" style="margin: 10px;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Special Users</h5>
|
||||
<p>Not created by an LDAP sync and/or having a role other than <code>user</code></p>
|
||||
<!-- <p id="generated-jwt" style="font-weight: bold;"></p> -->
|
||||
<div style="width: 100%; max-height: 500px; overflow-y: scroll;">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Roles</th>
|
||||
<th>JWT</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-list">
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
fetch('/api/users/?via-ldap=false¬-just-user=true')
|
||||
.then(res => res.json())
|
||||
.then(users => {
|
||||
let listElement = document.querySelector('#users-list')
|
||||
listElement.innerHTML = users.map(user => `
|
||||
<tr id="user-${user.username}">
|
||||
<td>${user.username}</td>
|
||||
<td>${user.name}</td>
|
||||
<td>${user.email}</td>
|
||||
<td><code>${user.roles.join(', ')}</code></td>
|
||||
<td><button class="btn btn-success get-jwt">Gen. JWT</button></td>
|
||||
<td><button class="btn btn-danger del-user">Delete</button></td>
|
||||
</tr>
|
||||
`).join('')
|
||||
|
||||
listElement.querySelectorAll('button.get-jwt').forEach(e => e.addEventListener('click', event => {
|
||||
let row = event.target.parentElement.parentElement
|
||||
let username = row.children[0].innerText
|
||||
fetch(`/api/jwt/?username=${username}`)
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
// let elm = document.querySelector('#generated-jwt')
|
||||
// elm.innerText = `JWT: ${text}`
|
||||
// elm.scrollIntoView()
|
||||
event.target.parentElement.innerHTML = `<textarea rows="3" cols="20">${text}</textarea>`
|
||||
navigator.clipboard.writeText(text).catch(reason => console.error(reason))
|
||||
})
|
||||
}))
|
||||
|
||||
listElement.querySelectorAll('button.del-user').forEach(e => e.addEventListener('click', event => {
|
||||
let row = event.target.parentElement.parentElement
|
||||
let username = row.children[0].innerText
|
||||
if (confirm('Are you sure?')) {
|
||||
let formData = new FormData()
|
||||
formData.append('username', username)
|
||||
fetch('/api/users/', { method: 'DELETE', body: formData }).then(res => {
|
||||
if (res.status == 200) {
|
||||
row.remove()
|
||||
} else {
|
||||
event.target.innerText = res.statusText
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col card" style="margin: 10px;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Add Role to User</h5>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="username" id="add-role-username"/>
|
||||
<select class="form-select" id="add-role-select">
|
||||
<option selected value="">Role...</option>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="api">API</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" id="add-role-button">Button</button>
|
||||
</div>
|
||||
<script>
|
||||
document.querySelector('#add-role-button').addEventListener('click', () => {
|
||||
const username = document.querySelector('#add-role-username').value, role = document.querySelector('#add-role-select').value
|
||||
if (username == "" || role == "") {
|
||||
alert('Please fill in a username and select a role.')
|
||||
return
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('username', username)
|
||||
formData.append('add-role', role)
|
||||
fetch(`/api/user/${username}`, { method: 'POST', body: formData }).then(res => res.text()).then(status => alert(status))
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col card" style="margin: 10px;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Scramble Names / Presentation Mode</h5>
|
||||
<input type="checkbox" id="scramble-names-checkbox"/>
|
||||
<script>
|
||||
const scrambleNamesCheckbox = document.querySelector('#scramble-names-checkbox')
|
||||
scrambleNamesCheckbox.checked = window.localStorage.getItem("cc-scramble-names") != null
|
||||
scrambleNamesCheckbox.addEventListener('change', () => {
|
||||
if (scrambleNamesCheckbox.checked)
|
||||
window.localStorage.setItem("cc-scramble-names", "true")
|
||||
else
|
||||
window.localStorage.removeItem("cc-scramble-names")
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="svelte-app"></div>
|
||||
{{end}}
|
||||
<div class="row">
|
||||
<div class="col card" style="margin: 10px;">
|
||||
<form id="line-width-form" method="post" action="/api/configuration/" class="card-body">
|
||||
<h5 class="card-title">Line Width</h5>
|
||||
<input type="hidden" name="key" value="plot_general_lineWidth"/>
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Line Width</label>
|
||||
<input type="number" class="form-control" id="value" name="value" aria-describedby="lineWidthHelp" value="{{ .Config.plot_general_lineWidth }}" min="1"/>
|
||||
<div id="lineWidthHelp" class="form-text">Width of the lines in the timeseries plots.</div>
|
||||
</div>
|
||||
<p>
|
||||
<code class="form-result"></code>
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col card" style="margin: 10px;">
|
||||
<form id="plots-per-row-form" method="post" action="/api/configuration/" class="card-body">
|
||||
<h5 class="card-title">Plots per Row</h5>
|
||||
<input type="hidden" name="key" value="plot_view_plotsPerRow"/>
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Plots per Row</label>
|
||||
<input type="number" class="form-control" id="value" name="value" aria-describedby="plotsperrowHelp" value="{{ .Config.plot_view_plotsPerRow }}" min="1"/>
|
||||
<div id="plotsperrowHelp" class="form-text">How many plots to show next to each other on pages such as /monitoring/job/, /monitoring/system/...</div>
|
||||
</div>
|
||||
<p>
|
||||
<code class="form-result"></code>
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col card" style="margin: 10px;">
|
||||
<form id="backgrounds-form" method="post" action="/api/configuration/" class="card-body">
|
||||
<h5 class="card-title">Colored Backgrounds</h5>
|
||||
<input type="hidden" name="key" value="plot_general_colorBackground"/>
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
<input type="radio" id="true" name="value" value="true" {{if .Config.plot_general_colorBackground}}checked{{else}}{{end}} />
|
||||
<label for="true">Yes</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="false" name="value" value="false" {{if .Config.plot_general_colorBackground}}{{else}}checked{{end}} />
|
||||
<label for="false">No</label>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<code class="form-result"></code>
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col card" style="margin: 10px;">
|
||||
<form id="colorscheme-form" method="post" action="/api/configuration/" class="card-body">
|
||||
<h5 class="card-title">Colorscheme for Timeseries Plots</h5>
|
||||
<input type="hidden" name="key" value="plot_general_colorscheme"/>
|
||||
<style>
|
||||
.color-dot {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
<table class="table">
|
||||
<script>
|
||||
const colorschemes = {
|
||||
'Default': ["#00bfff","#0000ff","#ff00ff","#ff0000","#ff8000","#ffff00","#80ff00"],
|
||||
'Autumn': ['rgb(255,0,0)','rgb(255,11,0)','rgb(255,20,0)','rgb(255,30,0)','rgb(255,41,0)','rgb(255,50,0)','rgb(255,60,0)','rgb(255,71,0)','rgb(255,80,0)','rgb(255,90,0)','rgb(255,101,0)','rgb(255,111,0)','rgb(255,120,0)','rgb(255,131,0)','rgb(255,141,0)','rgb(255,150,0)','rgb(255,161,0)','rgb(255,171,0)','rgb(255,180,0)','rgb(255,190,0)','rgb(255,201,0)','rgb(255,210,0)','rgb(255,220,0)','rgb(255,231,0)','rgb(255,240,0)','rgb(255,250,0)'],
|
||||
'Beach': ['rgb(0,252,0)','rgb(0,233,0)','rgb(0,212,0)','rgb(0,189,0)','rgb(0,169,0)','rgb(0,148,0)','rgb(0,129,4)','rgb(0,145,46)','rgb(0,162,90)','rgb(0,180,132)','rgb(29,143,136)','rgb(73,88,136)','rgb(115,32,136)','rgb(81,9,64)','rgb(124,51,23)','rgb(162,90,0)','rgb(194,132,0)','rgb(220,171,0)','rgb(231,213,0)','rgb(0,0,13)','rgb(0,0,55)','rgb(0,0,92)','rgb(0,0,127)','rgb(0,0,159)','rgb(0,0,196)','rgb(0,0,233)'],
|
||||
'BlueRed': ['rgb(0,0,131)','rgb(0,0,168)','rgb(0,0,208)','rgb(0,0,247)','rgb(0,27,255)','rgb(0,67,255)','rgb(0,108,255)','rgb(0,148,255)','rgb(0,187,255)','rgb(0,227,255)','rgb(8,255,247)','rgb(48,255,208)','rgb(87,255,168)','rgb(127,255,127)','rgb(168,255,87)','rgb(208,255,48)','rgb(247,255,8)','rgb(255,224,0)','rgb(255,183,0)','rgb(255,143,0)','rgb(255,104,0)','rgb(255,64,0)','rgb(255,23,0)','rgb(238,0,0)','rgb(194,0,0)','rgb(150,0,0)'],
|
||||
'Rainbow': ['rgb(125,0,255)','rgb(85,0,255)','rgb(39,0,255)','rgb(0,6,255)','rgb(0,51,255)','rgb(0,97,255)','rgb(0,141,255)','rgb(0,187,255)','rgb(0,231,255)','rgb(0,255,233)','rgb(0,255,189)','rgb(0,255,143)','rgb(0,255,99)','rgb(0,255,53)','rgb(0,255,9)','rgb(37,255,0)','rgb(83,255,0)','rgb(127,255,0)','rgb(173,255,0)','rgb(217,255,0)','rgb(255,248,0)','rgb(255,203,0)','rgb(255,159,0)','rgb(255,113,0)','rgb(255,69,0)','rgb(255,23,0)'],
|
||||
'Binary': ['rgb(215,215,215)','rgb(206,206,206)','rgb(196,196,196)','rgb(185,185,185)','rgb(176,176,176)','rgb(166,166,166)','rgb(155,155,155)','rgb(145,145,145)','rgb(136,136,136)','rgb(125,125,125)','rgb(115,115,115)','rgb(106,106,106)','rgb(95,95,95)','rgb(85,85,85)','rgb(76,76,76)','rgb(66,66,66)','rgb(55,55,55)','rgb(46,46,46)','rgb(36,36,36)','rgb(25,25,25)','rgb(16,16,16)','rgb(6,6,6)'],
|
||||
'GistEarth': ['rgb(0,0,0)','rgb(2,7,117)','rgb(9,30,118)','rgb(16,53,120)','rgb(23,73,122)','rgb(31,93,124)','rgb(39,110,125)','rgb(47,126,127)','rgb(51,133,119)','rgb(57,138,106)','rgb(62,145,94)','rgb(66,150,82)','rgb(74,157,71)','rgb(97,162,77)','rgb(121,168,83)','rgb(136,173,85)','rgb(153,176,88)','rgb(170,180,92)','rgb(185,182,94)','rgb(189,173,99)','rgb(192,164,101)','rgb(203,169,124)','rgb(215,178,149)','rgb(226,192,176)','rgb(238,212,204)','rgb(248,236,236)'],
|
||||
'BlueWaves': ['rgb(83,0,215)','rgb(43,6,108)','rgb(9,16,16)','rgb(8,32,25)','rgb(0,50,8)','rgb(27,64,66)','rgb(69,67,178)','rgb(115,62,210)','rgb(155,50,104)','rgb(178,43,41)','rgb(180,51,34)','rgb(161,78,87)','rgb(124,117,187)','rgb(78,155,203)','rgb(34,178,85)','rgb(4,176,2)','rgb(9,152,27)','rgb(4,118,2)','rgb(34,92,85)','rgb(78,92,203)','rgb(124,127,187)','rgb(161,187,87)','rgb(180,248,34)','rgb(178,220,41)','rgb(155,217,104)','rgb(115,254,210)'],
|
||||
'BlueGreenRedYellow': ['rgb(0,0,0)','rgb(0,0,20)','rgb(0,0,41)','rgb(0,0,62)','rgb(0,25,83)','rgb(0,57,101)','rgb(0,87,101)','rgb(0,118,101)','rgb(0,150,101)','rgb(0,150,69)','rgb(0,148,37)','rgb(0,141,6)','rgb(60,120,0)','rgb(131,87,0)','rgb(180,25,0)','rgb(203,13,0)','rgb(208,36,0)','rgb(213,60,0)','rgb(219,83,0)','rgb(224,106,0)','rgb(229,129,0)','rgb(233,152,0)','rgb(238,176,0)','rgb(243,199,0)','rgb(248,222,0)','rgb(254,245,0)']
|
||||
};
|
||||
|
||||
for (const name in colorschemes) {
|
||||
const colorscheme = colorschemes[name]
|
||||
const json = JSON.stringify(colorscheme)
|
||||
const checked = json == `{{ .Config.plot_general_colorscheme }}`
|
||||
document.write(
|
||||
`<tr>
|
||||
<th scope="col">${name}</th>
|
||||
<td>
|
||||
<input type="radio" name="value" value='${json}' ${checked ? 'checked' : ''}/>
|
||||
</td>
|
||||
<td>${colorscheme.map(color => `<span class="color-dot" style="background-color: ${color};"></span>`).join('')}</td>
|
||||
</tr>`)
|
||||
}
|
||||
</script>
|
||||
</table>
|
||||
<p>
|
||||
<code class="form-result"></code>
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const handleSubmit = (formSel) => {
|
||||
let form = document.querySelector(formSel)
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault()
|
||||
let formData = new FormData(form)
|
||||
|
||||
fetch(form.action, { method: 'POST', body: formData })
|
||||
.then(res => res.text())
|
||||
.then(text => form.querySelector('.form-result').innerText = text)
|
||||
.catch(err => form.querySelector('.form-result').innerText = err.toString())
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
handleSubmit('#create-user-form')
|
||||
handleSubmit('#line-width-form')
|
||||
handleSubmit('#plots-per-row-form')
|
||||
handleSubmit('#backgrounds-form')
|
||||
handleSubmit('#colorscheme-form')
|
||||
</script>
|
||||
{{define "stylesheets"}}
|
||||
<link rel='stylesheet' href='/build/config.css'>
|
||||
{{end}}
|
||||
{{define "javascript"}}
|
||||
<script>
|
||||
const user = {{ .User }};
|
||||
const filterPresets = {{ .FilterPresets }};
|
||||
const clusterCockpitConfig = {{ .Config }};
|
||||
</script>
|
||||
<script src='/build/config.js'></script>
|
||||
{{end}}
|
@ -56,6 +56,7 @@ func init() {
|
||||
type User struct {
|
||||
Username string // Username of the currently logged in user
|
||||
IsAdmin bool
|
||||
IsSupporter bool
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
|
Loading…
Reference in New Issue
Block a user