Compare commits

..

58 Commits

Author SHA1 Message Date
Alejandro Sirgo Rica 4e0bb82f9f views: skip invalid partitions in software inventory
Skip the invalid partition types in the list of selectable
partitions in software inventory.
2025-02-14 13:41:48 +01:00
Alejandro Sirgo Rica c6adc0f29b views: remove outdated ogLive checks
Remove checks for a running ogLive based on an empty client setup
reponse. The check is dead code as the partition setup is cached in
the database so the payload always constains the information.
2025-02-14 13:41:48 +01:00
Alejandro Sirgo Rica 35269b31a7 views: validate client partitions in /action/software
Redirect the user when /action/software is accessed on a client
without valid partitions.
2025-02-14 13:41:48 +01:00
Alejandro Sirgo Rica 1cf6fbc49e views: remove rendundant partition checks in image/restore
Partition validation is already performed before the removed checks
image restore, additional checks are redundant.
Remove redundant partition validation.
2025-02-14 13:39:46 +01:00
Alejandro Sirgo Rica d9597e4e01 views: fix partition scheme default values
Assign EMPTY when no scheme code is found.

Select GPT by default when the partition scheme is EMPTY in
partition and format view.

Add missing EMPTY value to SetupForm.
2025-02-13 10:25:00 +01:00
Alejandro Sirgo Rica b3b47ac0cb views: accept only user credentials without whitespace
Check the presence of whitespace for new usernames or passwords.

Usernames with whitespace can't be deleted because whitespace
is the separator of each element selected in the form.
2025-02-13 09:04:35 +01:00
OpenGnSys Support Team 4c21e370a3 views: remove implicit refresh when getting client details
do not force a client refresh when showing client details, use current
information in database
2025-02-12 13:46:32 +01:00
Alejandro Sirgo Rica 4616e3dcf5 views: remove unused variable partition_text 2025-02-11 11:18:59 +01:00
Alejandro Sirgo Rica d840e4af37 views: fix partition checks in script/run
Skip check of disk data in multi-disk clients in check for
uniform setup across all the clients selected.

Disk data should not be considered for client configuration
comparison.
2025-02-11 11:18:02 +01:00
Alejandro Sirgo Rica 91465fc269 views: skip disk data entries in multi-disk clients
Skip partition id zero for the partition list in the software
inventory form.

Partitions of id zero contain disk data and that data is not
relevant to compare client configurations.
2025-02-11 11:17:06 +01:00
Alejandro Sirgo Rica c33fe3ca77 views: report no valid partitions in image create/restore/update
Inform the user when the selected clients don't have a valid
partition type for an image create/restore/update operation and
return to the commands view.
2025-02-07 12:24:51 +01:00
Alejandro Sirgo Rica 9bb40d267f templates: use m-5 CSS class for every form
Use m-5 class instead of mx-5 CSS class for every main view form.
m-5 adds padding to all the borders while mx-5 only adds padding
for the x axis causing elements to be too close to the top and
bottom of the form.
2025-02-07 12:24:51 +01:00
Alejandro Sirgo Rica 00be07352d templates: warn about image create/restore on disks other than 1
Show a warning when the user selects a partition from a disk >1
in the image update, create or restore views.
Show the complete list of valid partitions from all disk.
2025-02-07 12:24:47 +01:00
Alejandro Sirgo Rica b2aa0e3dbb client_search: hide results when the search is empty
Check if the search fields are empty and clear the results if
no data is provided.
2025-02-05 14:31:05 +01:00
Alejandro Sirgo Rica 89cc9d3f9f views: remove MAC data in /action/client/search
Remove MAC from clients in client search view. The generation of
the MAC data requires individual /client/info requests for each
client causing huge server load for big deployments.
2025-02-05 14:20:02 +01:00
Alejandro Sirgo Rica 159f4c56a5 views: process view after selection validation
Access selection values after selection validation to prevent
the access of null values. Prevent backtrace with no sidebar
elements selected.
2025-01-30 10:30:19 +01:00
Alejandro Sirgo Rica ea9310f97a templates: disable boot OS components when no system is available
Hide the OS table and the "Boot" button when no OS is found.
2025-01-29 10:56:09 +01:00
Alejandro Sirgo Rica dd4b7ad229 views: add client search to scopes
Add a view to search clients by IP or MAC.

Adapt parse_scopes_from_tree() to include the path of the scope
in the tree.
2025-01-22 13:11:39 +01:00
Alejandro Sirgo Rica 655ffbc0bb views: add missing @login_required restrictions
Add checks for logged user in folder/add and folder/update
endpoints.
2024-12-17 16:04:23 +01:00
Alejandro Sirgo Rica f75a72b1cf views: remove col value from the dashboard template arguments
Remove "col" argments from the render_template() invocation and
set a good default value in the html template.
2024-12-17 16:04:23 +01:00
Alejandro Sirgo Rica 05cba727e0 views: refactor http error handling
Throw ServerError in get(), post(), delete() server methods
in case of connection error or status code with an error.
Log the cause of the error to show it in the web.

Add a function wrapper into every endpoint to handle the
error redirection needed for the ServerError exception.
The wrapper is defined by adding @handle_server_errors('XXX')
on top of the function declaration, where XXX is the name
of the function (endpoint) to be invoked by the redirection.
This change removes the need of specific checks after every
request and cleanups the endpoint code.

Fix the endpoint of the main views to work with an unavailable
ogserver.
2024-12-17 16:04:09 +01:00
Alejandro Sirgo Rica fd8da5de26 views: select the most used repo in Set repository view
Preselect the most used repository among the selected clients
for the view Commands -> Setup -> Set repository

Change get_clients_oglive() into def get_client_list_by_key()
to obtain a dictionary with the list of ips of the clients with
the same value in the field of the client payload passed as key.
For example for the key 'repo_id' it would return a dictionary
{repo_id: [ips of the clients with that repo_id]}
2024-12-13 13:39:09 +01:00
Alejandro Sirgo Rica c7c28d6e92 templates: use "edit" instead of "update" in menus
Use "Edit" instead of "Update" for every menu except Update image
in Commands view.

Use "Command" instad of "Cmd" for shell run menu in Commands view.
2024-12-12 17:38:01 +01:00
Alejandro Sirgo Rica a58587dc80 views: improve request error reporting
Add specific error messages for each http status code in the
function ogserver_error(). Pass the request object to obtain the
status code.

Standarize the error handling code for every get(), delete() and
post() as:
r = server.get('/scopes')
if not r:
    return ogserver_down('scopes')
if r.status_code != requests.codes.ok:
    return ogserver_error(r, 'scopes')
2024-12-11 16:35:17 +01:00
Alejandro Sirgo Rica 17644e584e views: cleanup action_setup_modify()
Check form.validate() for errors at the begining of the view
handler for an early return in case of error.
2024-12-11 16:33:43 +01:00
Alejandro Sirgo Rica edd44da64c ogcp: add Lives category to the main navbar
Add Lives view and show the lives installed in each server in the
sidebar.

Add view to set a new default live image in the Lives view.
2024-12-04 16:36:04 +01:00
Alejandro Sirgo Rica f02c899e3e views: select default live when it is the most used live
Select the default live entry instead of the entry of the live
that corresponds to the default live in the "Set ogLive" view.
2024-11-29 09:21:25 +01:00
Alejandro Sirgo Rica a241ce1bcd ogcp: update copyright headers 2024-11-28 16:52:06 +01:00
Alejandro Sirgo Rica a353cbcaa1 views: show correct live when the most used live is default
Use the correct livedir when the most used live is "ogLive"
2024-11-28 16:35:55 +01:00
Alejandro Sirgo Rica 76fe1b775a views: add direct cmd execution view
Reorganize "Run" section of Commands view as follows:

Commands
  └── Run
    ├── Script: run script from folder
    ├── Cmd: direct command execution
    └── Display output: results of last execution

Adapt API REST call to the new interface. Remove strange legacy
;|\n\r terminator. Remove "echo" field and add "inline" field.
2024-11-27 14:45:34 +01:00
Alejandro Sirgo Rica 92ab31650c log: show the end of the log and make it scrollable
Show the latest lines of the log first as they contain the
information relevant to the latest operations.

Show the logs inside an scrollable widget.
2024-11-12 10:37:31 +01:00
Alejandro Sirgo Rica 9c7a687d56 views: select the most used oglive in action/oglive
Set the most used oglive as the first element of the <select>
form component containing the list of available oglives.
2024-10-25 13:10:01 +02:00
Alejandro Sirgo Rica 270089983a js: fix sidebar client selection
Fix the JQuery selector to properly filter checboxes in the sidebar
2024-10-11 13:19:57 +02:00
Alejandro Sirgo Rica 66b663e051 views: update Boot OS to consume the new GET /session
Add code to handle the consumption of the new GET /session payload
from ogServer and ignore partitions with 'unknown' content.
2024-10-11 12:07:20 +02:00
Alejandro Sirgo Rica bcffdff135 views.py: prevent backtrace with unregistered client
Add check to skip unregistered clients in get_server_data_from_scopes
to prevent KeyError exception.
2024-10-10 11:48:29 +02:00
Alejandro Sirgo Rica 75cd6d9883 tempates: fix System log layout to prevent overflow
Restrict the <pre> component where the logs are contained to
prevent text overflow from happening.
2024-10-08 09:47:35 +02:00
Alejandro Sirgo Rica 6af4330016 ogcp: add view to identify clients setup diferences before restore
Add view to provide information before a restore operation where
the selected clients have a not uniform partition setup.

Show the view if only clients with not partition valid for a
restore operation are selected.
2024-10-02 10:14:04 +02:00
Alejandro Sirgo Rica d6a896628f views: rename reference partitions variable in /image/restore
Rename part_choices variable to reference_patitioning to improve
readability and intent in the code that checks the uniformity of
the client's partitions.
2024-09-26 14:49:18 +02:00
Alejandro Sirgo Rica 6feffeab5d views: ignore unsuported part types in /image/restore checks
Evaluate only the viable partitions for a restore operation
during checks for partition uniformity.
2024-09-26 14:45:04 +02:00
Alejandro Sirgo Rica 6fecb9d34b views: report clients without cache in fetch and restore
Report clients without cache partition inthe decks of the target
clients with enough cache to fit the target image.
2024-09-26 13:21:13 +02:00
Alejandro Sirgo Rica 39371747db ogcp: improve cache report wording
Use "Cache size" instead of "Disk size" in cache inspector.
Inform that the missing space when the image does not fit in cache
is additional space on top of the available space.
2024-09-26 13:21:13 +02:00
Alejandro Sirgo Rica c1d9018e21 templates: show real sizes in cache inspector
Use real free and available cache in cache_inspector.html
2024-09-26 13:21:02 +02:00
Alejandro Sirgo Rica 77a60b717a views: check if image fits in cache before /cache/fetch
Check if the image fits in cache before a /cache/fetch request.
Report the clients unable to store the image.
2024-09-26 10:21:19 +02:00
Alejandro Sirgo Rica 340b7fde54 views: improve checks for space available in cache
Use the new "free_cache" field in GET /cache/list to check
against the real available space to check if an image fits in
cache.
2024-09-26 10:19:37 +02:00
Alejandro Sirgo Rica ec209480ea templates: add free partition size in client details
Add column with available space in partitions only in the client
details views.
2024-09-25 17:01:48 +02:00
Alejandro Sirgo Rica 9bf161fc7a templates: reduce size of client list title
Reduce the size of the tittle message containing "Selected clients"
in the bock showing the client pills.
2024-09-17 13:18:57 +02:00
Alejandro Sirgo Rica 19295f8158 templates: disable sidebar in images and repos
Disable sidebar interaction in Images and Repos views.
2024-09-16 17:32:41 +02:00
Alejandro Sirgo Rica f03077edb7 js: add ogStorage to prevent localStorage key collission
Define ogStorage class to manage the localStorage operations.
The new keys are constructed with the following structure:
"group-context-id"
Where group is either "show" for the collapsed items in the
sidebar, or "check" for the selected checkboxes of the sidebar.

Add sotrage versioning to delete obsolete localStorage when a
new design for the storage is included in ogCP.
2024-09-16 17:29:14 +02:00
Alejandro Sirgo Rica 7296372e9c js: remove outdated local storage data
Remove the invalid keys from local storage when the page loads.
Creating and deleting elements from the sidebar accumulates
dead entries in the local storage.
2024-09-16 17:11:30 +02:00
Alejandro Sirgo Rica 053519beae templates: save checkbox state in images and repos
Store the checked checkboxes of the sidebar in Images and Repos.
Autoselect the correct server after updating the checkboxes.
2024-09-16 13:57:31 +02:00
Alejandro Sirgo Rica 2ca2215ed6 js: restrict the checkbox filter to the sidebar
Prevent accidental processing of checkboxes outside the sidebar.
2024-09-16 13:46:28 +02:00
Alejandro Sirgo Rica db2869088f js: remove unused function unfoldAll
Remove dead code.
2024-09-16 13:09:44 +02:00
Alejandro Sirgo Rica 233156b19a js: consolidate sidebar collapse persistence logic
Consolidate all the sidebar collapse persistence logic.
Implement a single function to handle all the views.
2024-09-16 12:34:32 +02:00
Alejandro Sirgo Rica f85c61df99 templates: fix initial repos disclosure widget status
Show the proper state of the disclosure widget when a repo starts
as collapsed.
2024-09-16 12:18:29 +02:00
Alejandro Sirgo Rica 65d2d75ddb templates: improve client report in image create view
Remove the line reporting the IP of the selected client. No other
view does that and that information is already available in the
client pills.
Add client pills widget.
2024-09-16 12:05:04 +02:00
Alejandro Sirgo Rica 696a81fd11 templates: add name and status to client list
Add a client name column to the client list view accessed through
the Dashboard.

Add status column to the client list.

Move client status leyend into a separate file.

Show the client status leyend in client list.

Fix the 0 link speed conditional.

Reuse the data returned by get_scopes() to reduce the number of
requests.
2024-09-11 12:24:44 +02:00
Alejandro Sirgo Rica 6b33268b5c ogcp: add view to assign repo to clients
Add /action/repo/set in Commands to assign a repository to
multiple clients.

The view includes the actual repo assigned in the client pills
and shows a table with the clients grouped by repo when multiple
repos are assigned among the selected clients.
2024-09-10 15:11:11 +02:00
Alejandro Sirgo Rica a1b164b106 templates: add efi data to /client/info
Show EFI info obtained through GET /efi.
2024-09-10 15:11:11 +02:00
64 changed files with 1400 additions and 1043 deletions

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -73,7 +73,8 @@ class SetupForm(FlaskForm):
ips = HiddenField()
disk = SelectField(label='Disk', validate_choice=False)
disk_type = SelectField(label=_l('Type'),
choices=[('MSDOS', 'MBR'),
choices=[('EMPTY', 'EMPTY'),
('MSDOS', 'MBR'),
('GPT', 'GPT')])
partitions = FieldList(FormField(PartitionForm),
min_entries=1,
@ -172,6 +173,11 @@ class RunScriptForm(FlaskForm):
arguments = StringField(label=_l('Arguments'))
submit = SubmitField(label=_l('Submit'))
class RunCmdForm(FlaskForm):
ips = HiddenField()
command = StringField(label=_l('Command'))
submit = SubmitField(label=_l('Submit'))
class ImportClientsForm(FlaskForm):
server = HiddenField()
room = SelectField(label=_l('Room'))
@ -186,9 +192,15 @@ class BootModeForm(FlaskForm):
class OgliveForm(FlaskForm):
ips = HiddenField()
server = HiddenField()
oglive = SelectField(label=_l('ogLive'))
ok = SubmitField(label=_l('Submit'))
class SetRepoForm(FlaskForm):
ips = HiddenField()
repo = SelectField(label=_l('Repository'))
ok = SubmitField(label=_l('Submit'))
class ImageCreateForm(FlaskForm):
ip = HiddenField()
os = SelectField(label=_l('Partition'), choices=[])

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -6,10 +6,22 @@
# (at your option) any later version.
from ogcp import app
from flask import session, flash
from flask_babel import _
import requests
import json
class ServerError(Exception):
pass
def flash_once(message, category='message'):
if '_flashes' not in session:
session['_flashes'] = []
if (category, message) not in session['_flashes']:
flash(message, category)
class OGServer:
def __init__(self, name, ip, port, api_token):
self.name = name
@ -22,26 +34,57 @@ class OGServer:
self.URL = f'http://{self.ip}:{self.port}'
self.HEADERS = {'Authorization' : self.api_token}
def get(self, path, payload=None):
def _log_http_status_code(self, res):
if res.status_code == 400:
err_msg = _('Invalid payload')
elif res.status_code == 404:
err_msg = _('Object not found')
elif res.status_code == 405:
err_msg = _('Method not allowed')
elif res.status_code == 409:
err_msg = _('Object already exists')
elif res.status_code == 423:
err_msg = _('Object in use')
elif res.status_code == 501:
err_msg = _('Cannot connect to database')
elif res.status_code == 507:
err_msg = _('Disk full')
else:
err_msg = _(f'Received status code {res.status_code}')
flash_once(err_msg, category='error')
def _request(self, method, path, payload, expected_status):
try:
r = requests.get(f'{self.URL}{path}',
headers=self.HEADERS,
json=payload)
res = requests.request(
method,
f'{self.URL}{path}',
headers=self.HEADERS,
json=payload,
)
if res.status_code not in expected_status:
self._log_http_status_code(res)
raise ServerError
return res
except requests.exceptions.ConnectionError:
return None
return r
flash_once(_('Cannot connect to ogserver'), category='error')
except requests.exceptions.Timeout:
flash_once(_('Request to ogserver timed out'), category='error')
except requests.exceptions.TooManyRedirects:
flash_once(_('Too many redirects occurred while contacting ogserver'), category='error')
except requests.exceptions.RequestException as e:
flash_once(_('An error occurred while contacting ogserver: %(error)s', error=str(e)), category='error')
raise ServerError
def get(self, path, payload=None):
return self._request('GET', path, payload, expected_status={200})
def post(self, path, payload):
r = requests.post(f'{self.URL}{path}',
headers=self.HEADERS,
json=payload)
return r
return self._request('POST', path, payload, expected_status={200, 202})
def delete(self, path, payload):
r = requests.delete(f'{self.URL}{path}',
headers=self.HEADERS,
json=payload)
return r
return self._request('DELETE', path, payload, expected_status={200})
@property
def id(self):

View File

@ -3,6 +3,61 @@ const macs = new Map();
const Interval = 1000;
let updateTimeoutId = null;
const StorageGroup = Object.freeze({
SHOW: 'show',
CHECK: 'check',
});
class ogStorage {
static STORAGE_VERSION = Object.freeze(1);
static store(group, context, elemId, value) {
const key = `${group}-${context}-${elemId}`;
localStorage.setItem(key, value);
}
static remove(group, context, elemId) {
const key = `${group}-${context}-${elemId}`;
localStorage.removeItem(key);
}
static hasKey(group, context, elemId) {
const key = `${group}-${context}-${elemId}`;
return localStorage.getItem(key) !== null;
}
static deleteInvalidStorage(items, group, context) {
if (localStorage.getItem('storageVersion') < ogStorage.STORAGE_VERSION) {
localStorage.clear();
localStorage.setItem('storageVersion', ogStorage.STORAGE_VERSION);
return
}
const prefix = `${group}-${context}`;
const existingKeys = items.map(function() {
return `${group}-${context}-${this.id}`;
}).get();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key.startsWith(prefix)) {
continue;
}
if (!existingKeys.includes(key)) {
localStorage.removeItem(key);
}
}
}
static storeCheckboxStatus(checkbox, context) {
if (checkbox.checked)
ogStorage.store(StorageGroup.CHECK, context, checkbox.id, "");
else
ogStorage.remove(StorageGroup.CHECK, context, checkbox.id);
}
}
async function show_client_mac(pill_id) {
const pill = $('#' +pill_id);
@ -47,20 +102,13 @@ function showSelectedClient(client_checkbox) {
}
function showSelectedClientsOnEvents() {
const checkboxes = $('input:checkbox[form|="scopesForm"]');
const checkboxes = $('#sidebar input:checkbox');
checkboxes.on('change show-client', function () {
showSelectedClient(this);
});
}
function storeCheckboxStatus(checkbox, context) {
if (checkbox.checked)
localStorage.setItem(context + checkbox.id, "check");
else
localStorage.removeItem(context + checkbox.id);
}
function findParentCheckboxes(element) {
const $element = $(element);
const parents = $element.parentsUntil('#scopes').not('ul');
@ -92,6 +140,8 @@ function setParentStatus(checkboxes) {
function configureCommandCheckboxes(context) {
const checkboxes = $('input:checkbox[form="scopesForm"]');
ogStorage.deleteInvalidStorage(checkboxes, StorageGroup.CHECK, context);
// Ensure the form fields are sent
$('#scopesForm').on('submit', function() {
checkboxes.each(function() {
@ -111,7 +161,7 @@ function configureCommandCheckboxes(context) {
checkboxes.on('change', function () {
const checked = this.checked;
const childrenCheckboxes = $('input:checkbox[form|="scopesForm"]', this.parentNode);
const childrenCheckboxes = $('input[type="checkbox"][form="scopesForm"]', this.parentNode);
// Uncheck all other checkboxes outside of the actual center branch
if (checked) {
@ -132,79 +182,44 @@ function configureCommandCheckboxes(context) {
checkboxes.each(function() {
showSelectedClient(this);
storeCheckboxStatus(this, context);
ogStorage.storeCheckboxStatus(this, context);
});
});
}
function keepSelectedClients(context) {
const checkboxes = $('input:checkbox[form|="scopesForm"]')
const checkboxes = $('#sidebar input:checkbox')
checkboxes.on('change', function (event) {
storeCheckboxStatus(this, context);
ogStorage.storeCheckboxStatus(this, context);
});
ogStorage.deleteInvalidStorage(checkboxes, StorageGroup.CHECK, context);
checkboxes.each(function () {
if (localStorage.getItem(context + this.id) == 'check') {
if (ogStorage.hasKey(StorageGroup.CHECK, context, this.id)) {
this.checked = true;
$(this).trigger('show-client');
}
});
}
function keepImagesTreeState() {
const images_tree = $('#servers .collapse')
images_tree.on('hidden.bs.collapse', function (event) {
function keepTreeState(selector, context) {
const tree_items = $(selector + ' .collapse');
ogStorage.deleteInvalidStorage(tree_items, StorageGroup.SHOW, context);
tree_items.on('hidden.bs.collapse', function (event) {
event.stopPropagation();
localStorage.removeItem(this.id);
ogStorage.remove(StorageGroup.SHOW, context, this.id)
});
images_tree.on('shown.bs.collapse', function (event) {
tree_items.on('shown.bs.collapse', function (event) {
event.stopPropagation();
localStorage.setItem(this.id, 'show');
ogStorage.store(StorageGroup.SHOW, context, this.id, "")
});
images_tree.each(function () {
if (localStorage.getItem(this.id) == 'show') {
$(this).collapse('show');
} else {
$(this).siblings('a').addClass('collapsed');
}
});
}
function keepReposTreeState() {
const repos_tree = $('#repos-list .collapse')
repos_tree.on('hidden.bs.collapse', function (event) {
event.stopPropagation();
localStorage.removeItem(this.id);
});
repos_tree.on('shown.bs.collapse', function (event) {
event.stopPropagation();
localStorage.setItem(this.id, 'show');
});
repos_tree.each(function () {
if (localStorage.getItem(this.id) == 'show') {
$(this).collapse('show');
}
});
}
function keepScopesTreeState() {
const scopes_tree = $('#scopes .collapse')
scopes_tree.on('hidden.bs.collapse', function (event) {
event.stopPropagation();
localStorage.removeItem(this.id);
});
scopes_tree.on('shown.bs.collapse', function (event) {
event.stopPropagation();
localStorage.setItem(this.id, 'show');
});
scopes_tree.each(function () {
if (localStorage.getItem(this.id) == 'show') {
tree_items.each(function () {
if (ogStorage.hasKey(StorageGroup.SHOW, context, this.id)) {
$(this).collapse('show');
} else {
$(this).siblings('a').addClass('collapsed');
@ -337,56 +352,19 @@ function updateScopes(scopes) {
return hasLiveChildren;
}
function unfoldAll() {
$('#scopes .collapse').collapse('show');
}
function checkImageServer() {
const images = $('input:checkbox[form|="imagesForm"][name!="image-server"]')
images.on('change', function() {
const selectedServer = $('#' + $.escapeSelector(this.dataset.server));
const serversSelector = 'input:checkbox[name|="image-server"]';
const nonSelectedServers = $(serversSelector).not(selectedServer);
selectedServer.prop('checked', true);
nonSelectedServers.each(function() {
$(this).prop('checked', false);
const checkboxes = $('input:checkbox[data-server|="' + this.id + '"]');
checkboxes.prop('checked', false);
});
});
}
function checkRepoServer() {
const repos = $('input:checkbox[form|="reposForm"][name!="repos-server"]')
repos.on('change', function() {
const selectedServer = $('#' + $.escapeSelector(this.dataset.server));
const serversSelector = 'input:checkbox[name|="repos-server"]';
const nonSelectedServers = $(serversSelector).not(selectedServer);
selectedServer.prop('checked', true);
nonSelectedServers.each(function() {
$(this).prop('checked', false);
const checkboxes = $('input:checkbox[data-server|="' + this.id + '"]');
checkboxes.prop('checked', false);
});
});
}
function checkFolderParent(context) {
const folder = $('input:checkbox[form|="scopesForm"][name="folder"]')
const folder = $('#sidebar input:checkbox[name="folder"]')
folder.on('change', function() {
const folder_parent = $('#' + $.escapeSelector(this.dataset.parentInput));
folder_parent.prop('checked', this.checked);
storeCheckboxStatus(folder_parent.get(0), context);
ogStorage.storeCheckboxStatus(folder_parent.get(0), context);
});
}
function limitCheckboxes(context) {
const checkboxes = $('input:checkbox[form|="scopesForm"]');
const checkboxes = $('#sidebar input:checkbox');
ogStorage.deleteInvalidStorage(checkboxes, StorageGroup.CHECK, context);
checkboxes.on('change', function () {
const currentCheckbox = $(this);
@ -405,18 +383,18 @@ function limitCheckboxes(context) {
}
});
checkScopeServer();
checkCheckbox('scope-server');
checkboxes.each(function() {
storeCheckboxStatus(this, context);
ogStorage.storeCheckboxStatus(this, context);
showSelectedClient(this);
});
});
}
function checkScopeServer() {
const servers = $('input:checkbox[form|="scopesForm"][name="scope-server"]');
servers.each(function() {
function checkCheckbox(inputName) {
const checkboxes = $('#sidebar input:checkbox[name="' + inputName + '"]');
checkboxes.each(function() {
const checkbox = this;
const checkboxChildren = $('input:checkbox', this.parentNode).not(this);
if (checkboxChildren.length == 0) return;
@ -425,3 +403,15 @@ function checkScopeServer() {
checkbox.checked = checkedChildren.length > 0;
});
}
function checkOnChange(inputName) {
const checkboxes = $('#sidebar input:checkbox')
checkboxes.on('change', function (event) {
checkCheckbox(inputName);
});
checkboxes.each(function () {
checkCheckbox(inputName)
});
}

View File

@ -14,7 +14,7 @@
action=url_for('action_center_add'),
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -14,7 +14,7 @@
action=url_for('action_room_add'),
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -12,6 +12,7 @@
{{ wtf.quick_form(form,
action=url_for('server_add_post'),
method='post',
button_map={'submit_btn':'primary'}) }}
button_map={'submit_btn':'primary'},
extra_classes="m-5") }}
{% endblock %}

View File

@ -13,6 +13,6 @@
{{ wtf.quick_form(form,
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -13,6 +13,6 @@
{{ wtf.quick_form(form,
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -14,6 +14,6 @@
{{ wtf.quick_form(form,
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -38,10 +38,16 @@
</div>
{% set show_part_images = True %}
{% set show_free_size = True %}
{% set readonly_disk_inspector = True %}
{% include 'disk_inspector.html' %}
<br>
{% include 'cache_inspector.html' %}
<br>
{% include 'efi_inspector.html' %}
{% endblock %}

View File

@ -20,6 +20,6 @@
{{ wtf.quick_form(form,
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -0,0 +1,89 @@
{% extends 'scopes.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "macros.html" as macros %}
{% set btn_back = true %}
{% block nav_client %} active {% endblock %}
{% block nav_client_search %} active {% endblock %}
{% block content %}
<h2 class="mx-5 subhead-heading">
{{ _('Search clients') }}
</h2>
<div class="mx-5 my-3">
<label for="name-filter">{{ _('Name') }}</label>
<input type="text" id="name-filter" class="form-control mb-2">
<label for="ip-filter">{{ _('IP Address') }}</label>
<input type="text" id="ip-filter" class="form-control mb-2">
<button id="search-button" class="btn btn-primary">{{ _('Search') }}</button>
</div>
<div id="clients-container" class="mx-5 mt-3"></div>
<script>
let clients = {{ clients|tojson|safe }};
function renderClients(data) {
const container = document.getElementById('clients-container');
container.innerHTML = '';
let currentPath = null;
let ul = null;
data.forEach(client => {
if (client.tree_path !== currentPath) {
currentPath = client.tree_path;
const pathElement = document.createElement('p');
pathElement.innerHTML = `<strong>${currentPath}</strong>:`;
container.appendChild(pathElement);
ul = document.createElement('ul');
container.appendChild(ul);
}
const li = document.createElement('li');
li.textContent = `${client.name} (IP: ${client.ip.join(', ')})`;
ul.appendChild(li);
});
}
function filterClients() {
const nameFilter = document.getElementById('name-filter').value.toLowerCase();
const ipFilter = document.getElementById('ip-filter').value;
// If both filters are empty, don't display any clients
if (!nameFilter && !ipFilter) {
document.getElementById('clients-container').innerHTML = '';
return;
}
const filtered = clients.filter(client => {
const matchesName = nameFilter ? client.name.toLowerCase().includes(nameFilter) : true;
const matchesIP = ipFilter ? client.ip.some(ip => ip.includes(ipFilter)) : true;
return matchesName && matchesIP;
});
renderClients(filtered);
}
filterClients();
document.getElementById('search-button').addEventListener('click', filterClients);
// Search on Enter key press
document.querySelectorAll('#name-filter, #ip-filter').forEach(input => {
input.addEventListener('keydown', event => {
if (event.key === 'Enter') {
event.preventDefault();
filterClients();
}
});
});
</script>
{% endblock %}

View File

@ -45,7 +45,7 @@
action=url_for('action_center_delete'),
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -21,7 +21,7 @@
action=url_for('action_client_delete'),
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -17,7 +17,7 @@
action=url_for('action_image_delete'),
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -1,5 +1,7 @@
{% extends 'repos.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% set sidebar_state = 'disabled' %}
{% set btn_back = true %}
{% block nav_repos %} active{% endblock %}
@ -9,7 +11,7 @@
<h2 class="mx-5 subhead-heading">{{_('Delete repo')}}</h2>
<form class="form mx-5" method="POST">
<form class="form m-5" method="POST">
{{ form.hidden_tag() }}
{{ form.server() }}

View File

@ -48,7 +48,7 @@
action=url_for('action_room_delete'),
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -16,7 +16,7 @@
action=url_for('server_delete_post'),
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -13,7 +13,7 @@
{{ wtf.quick_form(form,
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -45,6 +45,6 @@
{{ wtf.quick_form(form,
method='post',
button_map={'submit': 'danger'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -13,7 +13,7 @@
{{ wtf.quick_form(form,
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -16,7 +16,7 @@
action=url_for('action_hardware'),
method='post',
button_map={'refresh': 'primary'},
extra_classes='m-2')}}
extra_classes='m-5')}}
<table class="table table-striped">
<thead class="thead-dark">

View File

@ -1,13 +1,14 @@
{% extends 'images.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% set sidebar_state = 'disabled' %}
{% set btn_back = true %}
{% block content %}
<h2 class="mx-5 subhead-heading">{{_('Update image')}} {{ form.name.data }}</h2>
<form class="form mx-5" method="POST" action="{{ url_for('action_image_config') }}">
<form class="form m-5" method="POST" action="{{ url_for('action_image_config') }}">
{{ form.hidden_tag() }}
{{ form.image_id() }}

View File

@ -10,14 +10,15 @@
<h2 class="mx-5 subhead-heading">{{_('Create a partition image')}}</h2>
<h2 class="mx-5">
{{ _('Selected client') }}: {{ form.ip.data }}
</h1>
{{ macros.cmd_selected_clients(selected_clients) }}
{% set partition_field_id = 'os' %}
{% include 'partition_warning.html' %}
{{ wtf.quick_form(form,
action=url_for('action_image_create'),
method='post',
button_map={'create': 'primary'},
extra_classes='mx-5') }}
extra_classes='m-5') }}
{% endblock %}

View File

@ -1,13 +1,14 @@
{% extends 'images.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% set sidebar_state = 'disabled' %}
{% set btn_back = true %}
{% block content %}
<h2 class="mx-5 subhead-heading">{{_('Image details')}}</h2>
<div class="container mx-5">
<div class="container m-5">
<form class="form" method="POST">
{{ form.hidden_tag() }}

View File

@ -17,6 +17,9 @@
{{ macros.cmd_selected_clients(selected_clients) }}
{% set partition_field_id = 'partition' %}
{% include 'partition_warning.html' %}
{{ wtf.quick_form(form,
action=url_for('action_image_restore'),
method='post',

View File

@ -13,6 +13,9 @@
{{ macros.cmd_selected_clients(selected_clients) }}
{% set partition_field_id = 'os' %}
{% include 'partition_warning.html' %}
<form class="form mx-5" method="POST" action="{{ url_for('action_image_update') }}">
{{ form.hidden_tag() }}

View File

@ -14,6 +14,6 @@
action=url_for('action_clients_import_post'),
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -10,6 +10,30 @@
<h2 class="mx-5 subhead-heading">{{_('Client log')}}</h2>
<pre>{{ log }}</pre>
<style>
/* Prevent overflow */
pre {
max-width: 100%;
overflow: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
<div class="container-fluid d-flex flex-column" style="height: 90vh;">
<div class="border p-3 overflow-auto flex-grow-1" id="logContainer">
<pre>{{ log }}</pre>
</div>
</div>
<script>
function scrollToBottom() {
const logContainer = document.getElementById('logContainer');
logContainer.scrollTo({ top: logContainer.scrollHeight, behavior: 'instant' });
}
document.addEventListener("DOMContentLoaded", scrollToBottom);
</script>
{% endblock %}

View File

@ -1,6 +1,7 @@
{% extends 'images.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% set sidebar_state = 'disabled' %}
{% set btn_back = true %}
{% block content %}

View File

@ -0,0 +1,21 @@
{% extends 'lives.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% set sidebar_state = 'disabled' %}
{% set btn_back = true %}
{% block content %}
<h2 class="mx-5 subhead-heading">
{{ _('Set default ogLive') }}
</h2>
<p class="mx-5">{{ _('Default live: %(default_live)s', default_live=default_live) }}</p>
{{ wtf.quick_form(form,
action=url_for('action_live_default'),
method='post',
button_map={'ok': 'primary'},
extra_classes="m-5") }}
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends 'commands.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "macros.html" as macros %}
{% set sidebar_state = 'disabled' %}
{% set btn_back = true %}
{% block content %}
<h2 class="mx-5 subhead-heading">
{{ _('Partition scheme mismatch') }}
</h2>
{{ macros.cmd_selected_clients(selected_clients) }}
</br>
<div class="container mx-5">
<b>{{ _('Cannot proceed with this command, selected clients have non-uniform or valid partition scheme') }}</b>
</div>
<table class="table table-bordered table-hover">
<thead class="text-center">
<tr>
<th style="min-width: 15em;">{{ _('Partitions') }}</th>
<th>{{ _('Clients') }}</th>
</tr>
</thead>
<tbody>
{% for idx in range(part_data | length) %}
<tr>
<td>
{% for disk_id, part_id, part_type, fs_type, part_size in part_data.get_partition_setup(idx) %}
<div>Part {{ part_id }} | {{ fs_type }} | {{ (part_size / 1024) | int}} MiB</div>
{% else %}
{{ _('Empty') }}
{% endfor %}
</td>
<td>
{% for ip in part_data.get_clients(idx) %}<div class="card d-inline-block" style="padding: 5px; margin: 3px;">{{ ip }}</div>{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -21,7 +21,7 @@
action=url_for('action_poweroff'),
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -21,7 +21,7 @@
action=url_for('action_reboot'),
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -1,5 +1,7 @@
{% extends 'repos.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% set sidebar_state = 'disabled' %}
{% set btn_back = true %}
{% block nav_repos %} active{% endblock %}
@ -9,7 +11,7 @@
<h2 class="mx-5 subhead-heading">{{_('Repo details')}}</h2>
<form class="form mx-5" method="POST">
<form class="form m-5" method="POST">
{{ form.hidden_tag() }}
{{ form.server() }}

View File

@ -0,0 +1,69 @@
{% extends 'commands.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "macros.html" as macros %}
{% set sidebar_state = 'disabled' %}
{% set btn_back = true %}
{% block nav_setup %} active{% endblock %}
{% block content %}
{% set ip_list = form.ips.data.split(' ') %}
{% set ip_count = ip_list | length %}
<h2 class="mx-5 subhead-heading">
{{ _('Changing repository of %(ip_count)d computer(s)', ip_count=ip_count) }}
</h2>
{{ macros.cmd_selected_clients(selected_clients) }}
{% if repos_set|length > 1 %}
<div class="mx-5 form-group">
<p>Selected clients have different ogLive</p>
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th>Repository</th>
<th>Clients</th>
</tr>
</thead>
<tbody class="text-left">
{% for repo, clients in repos_set.items() %}
<tr>
<th>{{repo}}</th>
<td>
{% for ip in clients %}<div class="card d-inline-block" style="padding: 5px; margin: 3px;">{{ ip }}</div>{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{{ wtf.quick_form(form,
action=url_for('action_repo_set'),
method='post',
button_map={'ok': 'primary'},
extra_classes="m-5") }}
<!-- jQuery -->
<script src="{{ url_for('static', filename='AdminLTE/plugins/jquery/jquery.min.js') }}"></script>
<script>
var reposSet = {{ repos_set|tojson|safe }};
// Update pill data
$('.badge-pill').each(function(index) {
for (const repo in reposSet) {
for (const clientName of reposSet[repo]) {
if ($(this).html().includes(clientName)) {
$(this).html($(this).html() + '<br>' + repo);
break;
}
}
}
});
</script>
{% endblock %}

View File

@ -1,5 +1,7 @@
{% extends 'repos.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% set sidebar_state = 'disabled' %}
{% set btn_back = true %}
{% block nav_repos %} active{% endblock %}

View File

@ -1,5 +1,7 @@
{% extends 'repos.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% set sidebar_state = 'disabled' %}
{% set btn_back = true %}
{% block nav_repos %} active{% endblock %}

View File

@ -13,6 +13,6 @@
{{ wtf.quick_form(form,
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -13,7 +13,7 @@
{{ wtf.quick_form(form,
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -20,6 +20,6 @@
{{ wtf.quick_form(form,
method='post',
button_map={'submit': 'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -9,7 +9,7 @@
<h2 class="mx-5 subhead-heading">{{_('Update server')}}</h2>
<form class="form mx-5" method="POST">
<form class="form m-5" method="POST">
{{ form.hidden_tag() }}
{{ form.server_addr() }}

View File

@ -16,13 +16,15 @@
{{ macros.cmd_selected_clients(selected_clients) }}
<p>
{% if os_groups|length > 0 %}
<p class="mx-5">
{% if os_groups|length > 1 %}
The selected clients have different installed OS:
{% endif %}
</p>
<form class="form-inline" method="POST" id="sessionForm">
<form class="form-inline m-5" method="POST" id="sessionForm">
<table class="table table-hover">
<thead class="thead-light">
<tr>
@ -51,4 +53,10 @@ The selected clients have different installed OS:
</button>
</form>
{% else %}
<div class="card text-center p-3">
<b>{{ _('No bootable OS') }}</b>
</div>
{% endif %}
{% endblock %}

View File

@ -14,6 +14,6 @@
action=url_for('action_software'),
method='post',
button_map={'view': 'primary', 'update': 'primary'},
extra_classes="mx-5")}}
extra_classes="m-5")}}
{% endblock %}

View File

@ -13,6 +13,6 @@
action=url_for('user_delete_post'),
method='post',
button_map={'submit_btn':'primary'},
extra_classes="mx-5") }}
extra_classes="m-5") }}
{% endblock %}

View File

@ -43,6 +43,9 @@
<li class="nav-item {% block nav_servers %}{% endblock %}">
<a class="nav-link" href="{{ url_for('manage_servers') }}">{{ _('Servers') }}</a>
</li>
<li class="nav-item {% block nav_lives %}{% endblock %}">
<a class="nav-link" href="{{ url_for('manage_lives') }}">{{ _('Lives') }}</a>
</li>
<li class="nav-item {% block nav_users %}{% endblock %}">
<a class="nav-link" href="{{ url_for('users') }}">{{ _('Users') }}</a>
</li>
@ -111,7 +114,7 @@
<!-- ChartJS -->
<script src="{{ url_for('static', filename='AdminLTE/plugins/chart.js/Chart.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/ogcp.js') }}?v=16"></script>
<script src="{{ url_for('static', filename='js/ogcp.js') }}?v=25"></script>
<script>
// error messages

View File

@ -27,7 +27,7 @@
</ul>
<ul class="list-group list-group-horizontal">
<li class="list-group-item w-50">
{{ _('Disk size') }}
{{ _('Cache size') }}
</li>
<li class="list-group-item w-50">
{{ _('used') }} (%)
@ -100,9 +100,9 @@
}
function updateChart(ip) {
var totalCache = toGiB(storageData[ip].total, 3);
var totalCache = toGiB(storageData[ip].used + storageData[ip].free, 3);
var usedCache = toGiB(storageData[ip].used, 3);
var freeCache = toGiB(storageData[ip].total - storageData[ip].used, 3)
var freeCache = toGiB(storageData[ip].free, 3);
cacheChart.data.datasets[0].data = [
usedCache,
@ -134,9 +134,7 @@
$('.badge-pill').each(function(index) {
for (var ip in storageData) {
if ($(this).html().includes(ip)) {
var totalCache = storageData[ip].total;
var usedCache = storageData[ip].used;
var freeCache = toGiB(totalCache - usedCache, 1)
var freeCache = toGiB(storageData[ip].free, 1)
$(this).html($(this).html() + '<br>free: ' + freeCache + ' GiB');
break;
}

View File

@ -8,6 +8,8 @@
<div class="container mx-5">
{% include 'client_status_leyend.html' %}
{% for server_id, server_data in servers_data.items() %}
<div class="accordion card" id="shellAccordion">
<div class="card-header" id="heading_1">
@ -22,8 +24,10 @@
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th>{{ _('Name') }}</th>
<th>{{ _('IP') }}</th>
<th>{{ _('Link speed') }}</th>
<th>{{ _('Status') }}</th>
<th>{{ _('Details') }}</th>
</tr>
</thead>
@ -31,9 +35,10 @@
<tbody data-target="cache-fieldset" id="cacheTable" class="text-left">
{% for client_data in server_data.clients %}
<tr data-toggle="fieldset-entry">
<td>{{ client_data.name }}</td>
<td>{{ client_data.addr }}</td>
<td>
{% if client_data.speed is not none %}
{% if client_data.speed is not none and client_data.speed > 0 %}
{% if client_data.speed >= 1000 %}
{{ (client_data.speed / 1000) | int }} Gb/s
{% else %}
@ -43,6 +48,31 @@
{{ _('Not available') }}
{% endif %}
</td>
<td>
<i class="nav-icon fa-circle
{% if client_data.state == 'OPG' and client_data.last_cmd.result == 'failure' %}
fas text-warning fa-times-circle
{% elif client_data.state == 'OPG' %}
fas text-warning
{% elif client_data.state == 'LNX' %}
fas text-linux
{% elif client_data.state == 'LNX' %}
fas fa-user-circle text-linux
{% elif client_data.state == 'WIN' %}
fas text-windows
{% elif client_data.state == 'WIN' %}
fas fa-user-circle text-windows
{% elif client_data.state == 'BSY' %}
fas text-danger
{% elif client_data.state == 'VDI' %}
fas text-success
{% elif client_data.state == 'WOL_SENT' %}
fas text-wol
{% else %}
far
{% endif %}
"></i>
</td>
<td><a href="{{ url_for('action_client_info', client_ip = client_data.addr) }}">{{ _('View details') }}</a></td>
</tr>
{% endfor %}

View File

@ -0,0 +1,15 @@
<div class="card">
<div class="card-body">
<ul id="clients-color-legend" class="d-flex flex-wrap justify-content-center nav ogcp-nav nav-pills">
<li class="nav-item"><i class="nav-icon far fa-circle"></i> {{_('Shutdown')}} </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-wol"></i> {{_('WoL sent')}} </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-warning"></i> ogLive </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-danger"></i> {{_('Busy')}} </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-linux"></i> Linux </li>
<li class="nav-item"><i class="nav-icon fas fa-user-circle text-linux"></i> {{_('Linux session')}} </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-windows"></i> Windows </li>
<li class="nav-item"><i class="nav-icon fas fa-user-circle text-windows"></i> {{_('Windows session')}} </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-success"></i> VDI </li>
</ul>
</div>
</div>

View File

@ -40,6 +40,8 @@
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<input class="btn btn-light dropdown-item{% block nav_setup_set_bootmode %}{% endblock %}" type="submit" value="{{ _('Set boot mode') }}"
form="scopesForm" formaction="{{ url_for('action_mode') }}" formmethod="get">
<input class="btn btn-light dropdown-item{% block nav_setup_set_repo %}{% endblock %}" type="submit" value="{{ _('Set repository') }}"
form="scopesForm" formaction="{{ url_for('action_repo_set') }}" formmethod="get">
<input class="btn btn-light dropdown-item{% block nav_setup_set_oglive %}{% endblock %}" type="submit" value="{{ _('Set ogLive') }}"
form="scopesForm" formaction="{{ url_for('action_oglive') }}" formmethod="get">
<input class="btn btn-light dropdown-item{% block nav_setup_setup %}{% endblock %}" type="submit" value="{{ _('Partition & Format') }}"
@ -95,11 +97,13 @@
<div class="dropdown btn">
<button class="btn btn-secondary btn-light dropdown-toggle{% block nav_script %}{% endblock %}" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-expanded="false">
{{ _('Script') }}
{{ _('Run') }}
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<input class="btn btn-light dropdown-item{% block nav_run_script %}{% endblock %}" type="submit" value="{{ _('Run') }}"
<input class="btn btn-light dropdown-item{% block nav_run_script %}{% endblock %}" type="submit" value="{{ _('Script') }}"
form="scopesForm" formaction="{{ url_for('action_run_script') }}" formmethod="get">
<input class="btn btn-light dropdown-item{% block nav_run_cmd %}{% endblock %}" type="submit" value="{{ _('Command') }}"
form="scopesForm" formaction="{{ url_for('action_run_cmd') }}" formmethod="get">
<input class="btn btn-light dropdown-item{% block nav_display_output %}{% endblock %}" type="submit" value="{{ _('Display output') }}"
form="scopesForm" formaction="{{ url_for('action_script_display_output') }}" formmethod="get">
</div>

View File

@ -93,7 +93,7 @@
<div class="row">
<!-- disk stats -->
<div class="col-{{ colsize }}">
<div class="col-6">
<div class="card text-center">
<div class="card-header">
{{ _('Disk stats') }}
@ -129,7 +129,7 @@
</div>
<!-- Memory stats -->
<div class="col-{{ colsize }}">
<div class="col-6">
<div class="card text-center">
<div class="card-header">
{{ _('Memory') }}
@ -165,7 +165,7 @@
</div>
<!-- Swap stats -->
<div class="col-{{ colsize }}">
<div class="col-6">
<div class="card text-center">
<div class="card-header">
{{ _('Swap') }}
@ -205,7 +205,7 @@
</div>
<!-- latest images -->
<div class="col-{{ colsize }}">
<div class="col-6">
<div class="card text-center">
<div class="card-header">
{{ _('Latest images') }}
@ -224,7 +224,7 @@
</div>
<!-- ogLives -->
<div class="col-{{ colsize }}">
<div class="col-6">
<div class="card text-center">
<div class="card-header">
{{ _('ogLive images') }}

View File

@ -1,6 +1,6 @@
{% if selected_disk is defined and setup_data is defined %}
<form class="form-inline mx-5" method="POST" id="setupForm">
<form class="form-inline m-5" method="POST" id="setupForm">
{{ disk_form.hidden_tag() }}
{{ disk_form.ips() }}
@ -37,6 +37,9 @@
<th>{{ _('Type') }}</th>
<th>{{ _('Filesystem') }}</th>
<th>{{ _('Size') }} (MiB)</th>
{% if show_free_size is defined %}
<th>{{ _('Free') }} (MiB)</th>
{% endif %}
{% if show_part_images is defined %}
<th>{{ _('Image') }}</th>
{% endif %}
@ -64,12 +67,13 @@
{% endif %}
</td>
<td>
{% if readonly_disk_inspector is defined %}
{{ partition.size(class_="form-control", oninput="handleEdit(this)", readonly="readonly") }}
{% else %}
{% if not readonly_disk_inspector is defined %}
{{ partition.size(class_="form-control", oninput="handleEdit(this)") }}
{% endif %}
</td>
{% if show_free_size is defined %}
<td></td>
{% endif %}
{% if show_part_images is defined %}
<td></td>
{% endif %}
@ -176,7 +180,12 @@
let freeSpace = diskSize;
let partNum = 1;
$('#partitionsTable tr').each(function() {
{% if readonly_disk_inspector is defined %}
let partitionSize = parseInt($(this).find('td').eq(3).text().trim());
{% else %}
let partitionSize = parseInt($(this).find('td').eq(3).find('input').val().trim());
{% endif %}
if (isNaN(partitionSize)) {
partitionSize = 0;
}
@ -291,12 +300,23 @@
let row = partitionsTable.find('tr').eq(i - 1);
row.find('td').eq(0).text(p.partition);
row.find('td').eq(1).find('select').val(p.code);
row.find('td').eq(2).find('select').val(p.filesystem);
row.find('td').eq(3).find('input').val(Math.floor(p.size / 1024));
var idx = 0;
row.find('td').eq(idx++).text(p.partition);
row.find('td').eq(idx++).find('select').val(p.code);
row.find('td').eq(idx++).find('select').val(p.filesystem);
{% if readonly_disk_inspector is defined %}
row.find('td').eq(idx++).text(Math.floor(p.size / 1024));
{% else %}
row.find('td').eq(idx++).find('input').val(Math.floor(p.size / 1024));
{% endif %}
{% if show_free_size is defined %}
row.find('td').eq(idx++).text(Math.floor(p.free_size / (1024 * 1024)));
{% endif %}
{% if show_part_images is defined %}
row.find('td').eq(4).text(p.image);
row.find('td').eq(idx++).text(p.image);
{% endif %}
}

View File

@ -0,0 +1,55 @@
{% if efi_data is defined %}
{% if efi_data['entries']|length > 0 %}
<div class="form-group mx-5">
<label class="control-label">{{ _('Boot entries') }}</label>
<table class="table table-bordered">
<thead class="thead-light">
<tr>
<th>{{ _('Order') }}</th>
<th>{{ _('Active') }}</th>
<th>{{ _('Name') }}</th>
<th>{{ _('Description') }}</th>
</tr>
</thead>
<tbody>
{% for entry_data in efi_data['entries'] %}
<tr>
<td>
<p>
{% if entry_data['order'] is defined %}
{{ entry_data['order'] }}
{% else %}
-
{% endif %}
</p>
</td>
<td>
<p>
{% if entry_data['active'] == 1 %}
{{ _('yes') }}
{% else %}
{{ _('no') }}
{% endif %}
</p>
</td>
<td>
<p>{{ entry_data['name'] }}</p>
</td>
<td>
<p>{{ entry_data['description'] }}</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="card text-center p-3">
<b>{{ _('No EFI contents') }}</b>
</div>
{% endif %}
{% endif %}

View File

@ -14,8 +14,9 @@
// in the scope
document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') {
keepImagesTreeState();
checkImageServer();
keepTreeState('#servers', 'images');
keepSelectedClients('images');
checkOnChange('image-server');
}
});
</script>
@ -28,6 +29,7 @@
{% set parent_id = "repos-" ~ loop.index0 %}
<li class="nav-item">
<input class="form-check-input" type="checkbox" form="imagesForm"
{% if sidebar_state %}style="filter: grayscale(100%);" onclick="return false;"{% endif %}
id="{{ server_str }}" value="{{ server_str }}"
onclick="return false;" name="image-server" hidden/>
<a class="nav-link" data-toggle="collapse" data-target="#repos-{{ loop.index0 }}">
@ -43,9 +45,11 @@
{% for image in repo_data["images"] %}
<li id="{{ image["name"] }}_{{ image["id"] }}" class="nav-item">
<input class="form-check-input" type="checkbox" form="imagesForm"
{% if sidebar_state %}style="filter: grayscale(100%);" onclick="return false;"{% endif %}
data-server="{{ server_str }}" value="{{ image["id"] }}"
{% if image.get("selected", False) %}checked{% endif %}
name="{{ image["name"] }}_{{ image["id"] }}" />
name="{{ image["name"] }}_{{ image["id"] }}"
id="image{{ image["id"] }}"/>
{{ image["name"] }}
</li>
{% endfor %}
@ -68,7 +72,7 @@
form="imagesForm" formaction="{{ url_for('action_image_delete') }}" formmethod="get">
{% endif %}
{% if current_user.get_permission('IMAGE', 'UPDATE') %}
<input class="btn btn-light" type="submit" value="{{ _('Update image') }}"
<input class="btn btn-light" type="submit" value="{{ _('Edit image') }}"
form="imagesForm" formaction="{{ url_for('action_image_config') }}" formmethod="get">
{% endif %}
{% endif %}

View File

@ -0,0 +1,57 @@
{% extends 'base.html' %}
{% block nav_lives %}active{% endblock %}
{% block container %}
<form id="livesForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>
{{ super() }}
</form>
<script>
// Launch the javascript on document ready, so all the global functions exists
// in the scope
document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') {
keepTreeState('#servers', 'lives');
}
});
</script>
{% endblock %}
{% block sidebar %}
<ul id="servers" class="nav ogcp-nav flex-column nav-pills">
{% for lives_data in oglive_list %}
<li class="nav-item">
{% set server_ip_port = lives_data["server"].ip ~ ":" ~ lives_data["server"].port %}
<input id="{{ server_ip_port }}" class="form-check-input" type="checkbox" form="livesForm"
{% if sidebar_state %}style="filter: grayscale(100%);" onclick="return false;"{% endif %}
value="{{ server_ip_port }}" name="server"
{% if loop.index == 1 %}checked{% endif %}></input>
<a class="nav-link" data-toggle="collapse" href="#server{{loop.index}}">
{{ lives_data["server"]["name"] }}
</a>
<ul class="nav flex-column collapse" id="server{{loop.index}}">
{% for oglive in lives_data["json"]["oglive"] %}
<li class="nav-item">
<a>{{ oglive["directory"] }}</a>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endblock %}
{% block commands %}
{% if current_user.is_authenticated %}
<input class="btn btn-light {% block nav_live_default %}{% endblock %}" type="submit" value="{{ _('Set default') }}"
form="livesForm" formaction="{{ url_for('action_live_default') }}" formmethod="get">
{% if btn_back %}
<button class="btn btn-danger ml-3" type="button" id="backButton" onclick="history.back()">
{{ _("Back") }}
</button>
{% endif %}
{% endif %}
{% endblock %}

View File

@ -10,7 +10,7 @@
if (document.readyState === 'complete') {
showSelectedClientsOnEvents();
updateScopeState();
keepScopesTreeState();
keepTreeState('#scopes', 'scopes');
let context = {{ selection_mode | tojson | safe }};
{% if selection_mode == 'commands' %}
configureCommandCheckboxes(context);
@ -92,22 +92,10 @@
{% endmacro %}
{% macro selected_clients() -%}
<h2 class="mx-5 subhead-heading">{{_('Selected clients')}}</h2>
<div class="card">
<div class="card-body">
<ul id="clients-color-legend" class="d-flex flex-wrap justify-content-center nav ogcp-nav nav-pills">
<li class="nav-item"><i class="nav-icon far fa-circle"></i> {{_('Shutdown')}} </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-wol"></i> {{_('WoL sent')}} </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-warning"></i> ogLive </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-danger"></i> {{_('Busy')}} </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-linux"></i> Linux </li>
<li class="nav-item"><i class="nav-icon fas fa-user-circle text-linux"></i> {{_('Linux session')}} </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-windows"></i> Windows </li>
<li class="nav-item"><i class="nav-icon fas fa-user-circle text-windows"></i> {{_('Windows session')}} </li>
<li class="nav-item"><i class="nav-icon fas fa-circle text-success"></i> VDI </li>
</ul>
</div>
</div>
<h3 class="mx-5 subhead-heading">{{_('Selected clients')}}</h3>
{% include 'client_status_leyend.html' %}
<div id="selected-clients" class="d-flex flex-wrap justify-content-center"></div>
{% endmacro %}

View File

@ -0,0 +1,30 @@
<div class="mx-5" id="partition-warning" style="display:none; color:red; font-weight: bold;">
{{ _('Warning: You have selected a partition from a disk other than 1. This will be considered for storage only.') }}
</div>
<script>
function checkPartition() {
var partitionSelect = document.getElementById('{{ partition_field_id }}');
var selectedValue = partitionSelect.value;
var warningDiv = document.getElementById('partition-warning');
// Extract the disk_id
var diskId = parseInt(selectedValue.split(' ')[0]);
if (diskId !== 1) {
warningDiv.style.display = 'block';
} else {
warningDiv.style.display = 'none';
}
}
document.addEventListener("DOMContentLoaded", function () {
var partitionSelect = document.getElementById('{{ partition_field_id }}');
if (partitionSelect) {
partitionSelect.addEventListener('change', checkPartition);
checkPartition();
} else {
console.error("Element with ID '{{ partition_field_id }}' not found.");
}
});
</script>

View File

@ -18,6 +18,7 @@
{% set repos_list = repos["json"]["repositories"] %}
<li class="nav-item">
<input id="{{ server_ip_port }}"class="form-check-input" type="checkbox" form="reposForm"
{% if sidebar_state %}style="filter: grayscale(100%);" onclick="return false;"{% endif %}
value="{{ server_ip_port }}" name="repos-server" />
<a class="nav-link {% if not repos_list %}disabled{% endif %}" href="#server{{loop.index}}"
{% if repos_list %}data-toggle="collapse"{% endif %}>
@ -27,6 +28,8 @@
{% for r in repos_list %}
<li class="nav-item">
<input class="form-check-input" type="checkbox" form="reposForm"
{% if sidebar_state %}style="filter: grayscale(100%);" onclick="return false;"{% endif %}
id="repo{{ r["id"] }}"
data-server="{{server_ip_port}}"
value="{{ r["id"] }}"
name="{{ r["name"]~_~r["id"] }}" />
@ -42,8 +45,9 @@
// in the scope
document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') {
keepReposTreeState()
checkRepoServer()
keepTreeState('#repos-list', 'repos');
keepSelectedClients('repos');
checkOnChange('repos-server');
}
});
</script>
@ -62,7 +66,7 @@
form="reposForm" formaction="{{ url_for('action_repo_delete') }}" formmethod="get">
{% endif %}
{% if current_user.get_permission('REPOSITORY', 'UPDATE') %}
<input class="btn btn-light {% block nav_repo_update %}{% endblock %}" type="submit" value="{{ _('Update repo') }}"
<input class="btn btn-light {% block nav_repo_update %}{% endblock %}" type="submit" value="{{ _('Edit repo') }}"
form="reposForm" formaction="{{ url_for('action_repo_update') }}" formmethod="get">
{% endif %}
{% endif %}

View File

@ -28,7 +28,7 @@
<input class="btn btn-light dropdown-item {% block nav_client_add %}{% endblock %}" type="submit" value="{{ _('Add client') }}"
form="scopesForm" formaction="{{ url_for('action_client_add') }}" formmethod="get">
{% endif %}
<input class="btn btn-light dropdown-item {% block nav_client_update %}{% endblock %}" type="submit" value="{{ _('Update client') }}"
<input class="btn btn-light dropdown-item {% block nav_client_update %}{% endblock %}" type="submit" value="{{ _('Edit client') }}"
form="scopesForm" formaction="{{ url_for('action_client_update') }}" formmethod="get">
{% if current_user.get_permission('CLIENT', 'UPDATE') %}
<input class="btn btn-light dropdown-item {% block nav_client_move %}{% endblock %}" type="submit" value="{{ _('Move client') }}"
@ -42,6 +42,8 @@
<input class="btn btn-light dropdown-item {% block nav_client_delete %}{% endblock %}" type="submit" value="{{ _('Delete client') }}"
form="scopesForm" formaction="{{ url_for('action_client_delete') }}" formmethod="get">
{% endif %}
<input class="btn btn-light dropdown-item {% block nav_client_search %}{% endblock %}" type="submit" value="{{ _('Search client') }}"
form="scopesForm" formaction="{{ url_for('action_client_search') }}" formmethod="get">
</div>
</div>
{% endif %}
@ -56,7 +58,7 @@
form="scopesForm" formaction="{{ url_for('action_room_add') }}" formmethod="get">
{% endif %}
{% if current_user.get_permission('ROOM', 'UPDATE') %}
<input class="btn btn-light dropdown-item {% block nav_room_update %}{% endblock %}" type="submit" value="{{ _('Update room') }}"
<input class="btn btn-light dropdown-item {% block nav_room_update %}{% endblock %}" type="submit" value="{{ _('Edit room') }}"
form="scopesForm" formaction="{{ url_for('action_room_update') }}" formmethod="get">
{% endif %}
{% if current_user.get_permission('ROOM', 'DELETE') %}
@ -78,7 +80,7 @@
form="scopesForm" formaction="{{ url_for('action_center_add') }}" formmethod="get">
{% endif %}
{% if current_user.get_permission('CENTER', 'UPDATE') %}
<input class="btn btn-light dropdown-item {% block nav_center_update %}{% endblock %}" type="submit" value="{{ _('Update center') }}"
<input class="btn btn-light dropdown-item {% block nav_center_update %}{% endblock %}" type="submit" value="{{ _('Edit center') }}"
form="scopesForm" formaction="{{ url_for('action_center_update') }}" formmethod="get">
{% endif %}
{% if current_user.get_permission('CENTER', 'DELETE') %}
@ -101,7 +103,7 @@
form="scopesForm" formaction="{{ url_for('action_folder_add') }}" formmethod="get">
{% endif %}
{% if current_user.get_permission('FOLDER', 'UPDATE') %}
<input class="btn btn-light dropdown-item {% block nav_folder_update %}{% endblock %}" type="submit" value="{{ _('Update folder') }}"
<input class="btn btn-light dropdown-item {% block nav_folder_update %}{% endblock %}" type="submit" value="{{ _('Edit folder') }}"
form="scopesForm" formaction="{{ url_for('action_folder_update') }}" formmethod="get">
{% endif %}
{% if current_user.get_permission('FOLDER', 'DELETE') %}

View File

@ -27,7 +27,7 @@
{% block commands %}
{% if current_user.is_authenticated %}
<input class="btn btn-light {% block nav_server_update %}{% endblock %}" type="submit" value="{{ _('Update server') }}"
<input class="btn btn-light {% block nav_server_update %}{% endblock %}" type="submit" value="{{ _('Edit server') }}"
form="serversForm" formaction="{{ url_for('server_update_get') }}" formmethod="get">
{% if btn_back %}
<button class="btn btn-danger ml-3" type="button" id="backButton" onclick="history.back()">

File diff suppressed because it is too large Load Diff