mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-11-04 09:35:07 +01:00 
			
		
		
		
	Merge pull request #44 from ClusterCockpit/config-component
Include config component
This commit is contained in:
		@@ -580,14 +580,26 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// TODO: Handle anything but roles...
 | 
						// Get Values
 | 
				
			||||||
	newrole := r.FormValue("add-role")
 | 
						newrole := r.FormValue("add-role")
 | 
				
			||||||
	if err := api.Authentication.AddRole(r.Context(), mux.Vars(r)["id"], newrole); err != nil {
 | 
						delrole := r.FormValue("remove-role")
 | 
				
			||||||
		http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	rw.Write([]byte("success"))
 | 
						// 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("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) {
 | 
					func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -129,6 +129,37 @@ func (auth *Authentication) AddRole(ctx context.Context, username string, role s
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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) {
 | 
					func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) {
 | 
				
			||||||
	me := GetUser(ctx)
 | 
						me := GetUser(ctx)
 | 
				
			||||||
	if me != nil && !me.HasRole(RoleAdmin) && !me.HasRole(RoleSupport) && me.Username != username {
 | 
						if me != nil && !me.HasRole(RoleAdmin) && !me.HasRole(RoleSupport) && me.Username != username {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,6 +65,6 @@ export default [
 | 
				
			|||||||
	entrypoint('systems',  'src/systems.entrypoint.js'),
 | 
						entrypoint('systems',  'src/systems.entrypoint.js'),
 | 
				
			||||||
	entrypoint('node',     'src/node.entrypoint.js'),
 | 
						entrypoint('node',     'src/node.entrypoint.js'),
 | 
				
			||||||
	entrypoint('analysis', 'src/analysis.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>
 | 
				
			||||||
@@ -1,300 +1,15 @@
 | 
				
			|||||||
{{define "content"}}
 | 
					{{define "content"}}
 | 
				
			||||||
{{if .User.IsAdmin}}
 | 
					    <div id="svelte-app"></div>
 | 
				
			||||||
<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>
 | 
					 | 
				
			||||||
                    <input type="radio" id="support" name="role" value="support"/>
 | 
					 | 
				
			||||||
                    <label for="support">Support</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="support">Support</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>
 | 
					 | 
				
			||||||
{{end}}
 | 
					{{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) {
 | 
					{{define "stylesheets"}}
 | 
				
			||||||
                        const colorscheme = colorschemes[name]
 | 
					    <link rel='stylesheet' href='/build/config.css'>
 | 
				
			||||||
                        const json = JSON.stringify(colorscheme)
 | 
					{{end}}
 | 
				
			||||||
                        const checked = json == `{{ .Config.plot_general_colorscheme }}`
 | 
					{{define "javascript"}}
 | 
				
			||||||
                        document.write(
 | 
					    <script>
 | 
				
			||||||
                        `<tr>
 | 
					        const user = {{ .User }};
 | 
				
			||||||
                            <th scope="col">${name}</th>
 | 
					        const filterPresets = {{ .FilterPresets }};
 | 
				
			||||||
                            <td>
 | 
					        const clusterCockpitConfig = {{ .Config }};
 | 
				
			||||||
                                <input type="radio" name="value" value='${json}' ${checked ? 'checked' : ''}/>
 | 
					    </script>
 | 
				
			||||||
                            </td>
 | 
					    <script src='/build/config.js'></script>
 | 
				
			||||||
                            <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>
 | 
					 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user