commit b770e12885a8e50e4817741518cf28cba154616c Author: Benjamin Drieu Date: Tue Nov 9 15:21:21 2021 +0100 Initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fdf6dae --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +#* +schemas/gschemas.compiled diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aed4bc9 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +reload: schema + busctl --user call org.gnome.Shell /org/gnome/Shell org.gnome.Shell Eval s 'Meta.restart("Restarting…")' + +schema: + glib-compile-schemas --strict schemas/ + +pref: reload + sleep 1 && gnome-extensions prefs monito@drieu.org diff --git a/convenience.js b/convenience.js new file mode 100644 index 0000000..36e15c1 --- /dev/null +++ b/convenience.js @@ -0,0 +1,95 @@ +/* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (c) 2011-2012, Giovanni Campagna + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the GNOME nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +const Gettext = imports.gettext; +const Gio = imports.gi.Gio; + +const Config = imports.misc.config; +const ExtensionUtils = imports.misc.extensionUtils; + +/** + * initTranslations: + * @domain: (optional): the gettext domain to use + * + * Initialize Gettext to load translations from extensionsdir/locale. + * If @domain is not provided, it will be taken from metadata['gettext-domain'] + */ +function initTranslations(domain) { + let extension = ExtensionUtils.getCurrentExtension(); + + domain = domain || extension.metadata['gettext-domain']; + + // check if this extension was built with "make zip-file", and thus + // has the locale files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell + let localeDir = extension.dir.get_child('locale'); + if (localeDir.query_exists(null)) + Gettext.bindtextdomain(domain, localeDir.get_path()); + else + Gettext.bindtextdomain(domain, Config.LOCALEDIR); +} + +/** + * getSettings: + * @schema: (optional): the GSettings schema id + * + * Builds and return a GSettings schema for @schema, using schema files + * in extensionsdir/schemas. If @schema is not provided, it is taken from + * metadata['settings-schema']. + */ +function getSettings(schema,path=null) { + let extension = ExtensionUtils.getCurrentExtension(); + + schema = schema || extension.metadata['settings-schema']; + + const GioSSS = Gio.SettingsSchemaSource; + + // check if this extension was built with "make zip-file", and thus + // has the schema files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell (and therefore schemas are available + // in the standard folders) + let schemaDir = extension.dir.get_child('schemas'); + let schemaSource; + if (schemaDir.query_exists(null)) + schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), + GioSSS.get_default(), + false); + else + schemaSource = GioSSS.get_default(); + + let schemaObj = schemaSource.lookup(schema, true); + if (!schemaObj) + throw new Error('Schema ' + schema + ' could not be found for extension ' + + extension.metadata.uuid + '. Please check your installation.'); + + if ( path ) + return new Gio.Settings({ settings_schema: schemaObj, path: path }); + else + return new Gio.Settings({ settings_schema: schemaObj }); +} diff --git a/extension.js b/extension.js new file mode 100644 index 0000000..42b7fbf --- /dev/null +++ b/extension.js @@ -0,0 +1,299 @@ +/* extension.js + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/* exported init */ + +const GETTEXT_DOMAIN = 'monito'; + +let _httpSession; +let _status; +let _ok_text; +let _warning_text; +let _critical_text; + +const Gettext = imports.gettext.domain(GETTEXT_DOMAIN); +const _ = Gettext.gettext; +const Lang = imports.lang; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Main = imports.ui.main; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; + +const { GObject, St, Soup, Clutter } = imports.gi; + +const SETTINGS_SCHEMA = "org.gnome.shell.extensions.monito@drieu.org"; +const Convenience = Me.imports.convenience; +let settings = Convenience.getSettings(SETTINGS_SCHEMA); + + +const Indicator = GObject.registerClass( +class Indicator extends PanelMenu.Button { + _init() { + super._init(0.0, _('Monito Checker')); + + let box = new St.BoxLayout ( { } ); + this.add_child(box); + + this.initStatus ( ); + + // Main Box +/* + let ok_box = new St.BoxLayout({ style_class: 'monito-ok-box monito-box' }); + _ok_text = new St.Label({ text: String(_status['OK']) }) + ok_box.add_child(_ok_text); + box.add_child(ok_box); +*/ + let warning_box = new St.BoxLayout({ style_class: 'monito-warning-box monito-box' }); + _warning_text = new St.Label({ text: String(_status['WARNING']) }) + warning_box.add_child(_warning_text); + box.add_child(warning_box); + + let critical_box = new St.BoxLayout({ style_class: 'monito-critical-box monito-box' }); + _critical_text = new St.Label({ text: String(_status['CRITICAL']) }) + critical_box.add_child(_critical_text); + box.add_child(critical_box); + + box.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM)); + + + // Menu + this._buttonMenu = new PopupMenu.PopupBaseMenuItem({ + reactive: false, + style_class: 'monito-menu-button-container', + }); + this.menu.addMenuItem(this._buttonMenu); + + +// let item = new PopupMenu.PopupMenuItem(_('Reload')); +// item.connect('activate', () => { +// this.updateStatus ( ); +// }); +// this.menu.addMenuItem(item); + + this._mainLabel = new St.Label({ style_class: 'monito-title', text: 'Monito Checker', x_expand: true }); + this._buttonMenu.actor.add_actor(this._mainLabel); + + + this._prefsButton = this._createButton ( 'preferences-system-symbolic', 'Preferences', this._onPreferencesActivate ); + this._buttonMenu.actor.add_child (this._prefsButton); + + this._reloadButton = this._createButton ( 'view-refresh-symbolic', 'Reload', this.updateStatus ); + this._buttonMenu.actor.add_child (this._reloadButton ); + + + let _intermediate = new PopupMenu.PopupBaseMenuItem ( { + style_class: 'monito-services', + reactive: false + }); + + this.menu.addMenuItem(_intermediate); + this._box = new St.BoxLayout({ + style_class: 'monito-services', + vertical: true, + x_expand: true + }); + _intermediate.actor.add_actor(this._box); + +// let _bin = new St.Bin(); +// _intermediate.actor.add_actor(_bin); +// _bin.set_child(this._services_table); + + +// this._list = new FlatList() +// _bin.set_child(this._list); + + this.updateStatus ( ); + } + + initStatus ( ) { + _status = { 'OK': 0, + 'WARNING': 0, + 'CRITICAL': 0, + 'UNKNOWN': 0 }; + } + + updateStatus ( ) { + const SETTINGS_SCHEMA_ACCOUNT = "org.gnome.shell.extensions.monito@drieu.org.account"; + const Convenience = Me.imports.convenience; + let account_settings = Convenience.getSettings(SETTINGS_SCHEMA_ACCOUNT, '/org/gnome/shell/extensions/monito@drieu/org/account/0'); + + let username = account_settings.get_string("username"); + let password = account_settings.get_string("password"); + let urlcgi = account_settings.get_string("urlcgi"); + + if ( ! urlcgi ) + { + log ( 'Not updating monito because no URL configured' ); + return; + } + + urlcgi = urlcgi.replace ( /^(https?:\/\/)/, '$1' + username + ':' + password + '@' ); +// log ( 'monito >>> ' + urlcgi ); + + this.load_data_async ( urlcgi, { 'jsonoutput': '' }, function(res) { Main.notify(res); } ) + } + + createBin(status, text, col) { + let _widths = [ 300, 300, 200, 50, 600 ]; + let _bin = new St.Bin({ + style_class: 'monito-service-' + status, + width: _widths[col], + x_expand: ( col == 4 ? true : false ), + child: new St.Label({ style_class: 'monito-label', text: text }) + }); + return _bin; + } + + load_data_async(url, params, fun) { + if (_httpSession === undefined) { + _httpSession = new Soup.Session(); + _httpSession.user_agent = Me.metadata.uuid; + } else { + // abort previous requests. + _httpSession.abort(); + } + + let message = Soup.form_request_new_from_hash('GET', url, params); + + _httpSession.queue_message(message, Lang.bind(this, function(_httpSession, message) { + //Main.notify(message.response_body.data); + try { +// log ( message.response_body.data ); + let json = JSON.parse(message.response_body.data); + + this.initStatus ( ); + + this._box.remove_all_children(); + + for ( let i = 0 ; i < json.status.service_status.length ; i ++ ) + { + _status [ json.status.service_status[i].status ] ++; + if ( json.status.service_status[i].status != 'OK' ) + { + let infoBox = new St.BoxLayout({ + style_class: 'monito-service-line monito-service-line-' + json.status.service_status[i].status, + hover: true, + x_expand: true + }); + this._box.add_child(infoBox); + + infoBox.add_child ( this.createBin ( json.status.service_status[i].status, + json.status.service_status[i].host_name, + 0 ) ); + infoBox.add_child ( this.createBin ( json.status.service_status[i].status, + json.status.service_status[i].service_display_name, + 1 ) ); + infoBox.add_child ( this.createBin ( json.status.service_status[i].status, + json.status.service_status[i].last_check, + 2) ); + infoBox.add_child ( this.createBin ( json.status.service_status[i].status, + json.status.service_status[i].attempts, + 3 ) ); + infoBox.add_child ( this.createBin ( json.status.service_status[i].status, + json.status.service_status[i].status_information, + 4 ) ); + +// let url = 'https://xxx:xxx@host/cgi-bin/icinga/extinfo.cgi?type=2&host='+json.status.service_status[i].host_name+"&service="+json.status.service_status[i].service_description; +// activeLabel.connect('button-press-event', () => { +// Main.notify(url); +// Gtk.show_uri(null, url, global.get_current_time()); +// } ); + } + } + +// _ok_text.set_text ( String(_status.OK) ); + _warning_text.set_text ( String(_status.WARNING) ); + _critical_text.set_text ( String(_status.CRITICAL) ); + } catch (e) { + Main.notify(_('Zbeu!')); + _warning_text.set_text ( '…' ); + _critical_text.set_text ( '…' ); + log(e); + return; + } + })); + return; + } + + _onPreferencesActivate() { + this.menu.actor.hide(); + if (typeof ExtensionUtils.openPrefs === 'function') { + ExtensionUtils.openPrefs(); + } else { + Util.spawn([ + "gnome-shell-extension-prefs", + Me.uuid + ]); + } + return 0; + } + + _createButton ( icon, text, callback ) { + let button = new St.Button({ + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.CENTER, + reactive: true, + can_focus: true, + track_hover: true, + accessible_name: text, + style_class: 'button' + }); + + button.child = new St.Icon({ + style_class: 'monito-button-icon', + icon_name: icon, + icon_size: 24, + width: 24, + height: 24, + }); + + button.connect('clicked', Lang.bind(this, callback ) ); + + return button; + } + +}); + +class Extension { + constructor(uuid) { + this._uuid = uuid; + + ExtensionUtils.initTranslations(GETTEXT_DOMAIN); + } + + enable() { + this._indicator = new Indicator(); + Main.panel.addToStatusArea(this._uuid, this._indicator); + } + + disable() { + this._indicator.destroy(); + this._indicator = null; + + if (_httpSession !== undefined) + _httpSession.abort(); + + _httpSession = undefined; + } +} + +function init(meta) { + return new Extension(meta.uuid); +} diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..229e021 --- /dev/null +++ b/metadata.json @@ -0,0 +1,8 @@ +{ + "name": "Monito", + "description": "Checks various monitoring websites", + "uuid": "monito@drieu.org", + "shell-version": [ + "3.38" + ] +} diff --git a/prefs.js b/prefs.js new file mode 100644 index 0000000..9812c8c --- /dev/null +++ b/prefs.js @@ -0,0 +1,198 @@ +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; + +// It's common practice to keep GNOME API and JS imports in separate blocks +const Lang = imports.lang; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Convenience = Me.imports.convenience; + +const Gettext = imports.gettext.domain('monito'); +const _ = Gettext.gettext; +const N_ = function (e) { + return e; +}; + +const SETTINGS_SCHEMA = "org.gnome.shell.extensions.monito@drieu.org"; +const SETTINGS_SCHEMA_ACCOUNT = "org.gnome.shell.extensions.monito@drieu.org.account"; +const SETTINGS_SCHEMA_ACCOUNT_PATH = "/org/gnome/shell/extensions/monito@drieu.org/account"; + + +const prefs = [ { type: Gtk.Entry, label: _('Name'), key: 'name' }, + { type: Gtk.ComboBoxText, label: _('Type'), key: 'type' }, + { type: Gtk.Entry, label: _('Username'), key: 'username' }, + { type: Gtk.Entry, label: _('Password'), key: 'password' }, + { type: Gtk.Entry, label: _('URL CGI'), key: 'urlcgi' }, + ]; + +// Like 'extension.js' this is used for any one-time setup like translations. +function init() { +// log('initializing ${Me.metadata.name} Preferences'); +} + + +// This function is called when the preferences window is first created to build +// and return a Gtk widget. As an example we'll create and return a GtkLabel. +function buildPrefsWidget() { + + // Copy the same GSettings code from `extension.js` + this.settings = ExtensionUtils.getSettings(SETTINGS_SCHEMA); + + let mainVbox = new Gtk.Box( { orientation: Gtk.Orientation.VERTICAL } ); + + let mainWidget = new Gtk.Notebook( { } ); + + let prefsWidget = new Gtk.Grid({ + margin: 18, + column_spacing: 12, + row_spacing: 12, + column_homogeneous: false, + }); + + this.prefWidgets = { }; + + // Add a simple title and add it to the prefsWidget + let title = new Gtk.Label({ + label: `${Me.metadata.name} Preferences`, + halign: Gtk.Align.START, + use_markup: true, + }); + prefsWidget.attach(title, 0, 0, 2, 1); + + // Misc Settings (TBD) + mainWidget.append_page ( prefsWidget, + new Gtk.Label ( { label: 'General', } ) ); + + + // Accounts + let accountsWidgetContainer = new Gtk.Box( { orientation: Gtk.Orientation.HORIZONTAL, + homogeneous: false, } ); + mainWidget.append_page ( accountsWidgetContainer, + new Gtk.Label ( { label: 'Servers', } ) ); + + let accountsChooserContainer = new Gtk.Box( { orientation: Gtk.Orientation.VERTICAL, + homogeneous: false, } ); + accountsWidgetContainer.pack_start(accountsChooserContainer, true, true, 0); + + let accountsChooser = new Gtk.ListBox ( { valign: Gtk.Align.FILL, + hexpand: true, + vexpand: true } ); + accountsChooserContainer.pack_start(accountsChooser, true, true, 0); + + // Account list + for ( var server of this.settings.get_string ( 'servers' ) . split ( ',' ) ) + { + let _account_settings = getAccountSettings ( server ); + let row = new Gtk.ListBoxRow ( { hexpand: true, + halign: Gtk.Align.FILL } ); + row.server= server + let _label = new Gtk.Label ( { label: _account_settings.get_string('name'), + hexpand: true, + margin: 5, + halign: Gtk.Align.START, + expand: true } ); + row.add ( _label ); + _label.set_data ( 'server', GLib.strdup(server) ); + accountsChooser.add ( row ); + } + accountsChooser.connect('row-activated', Lang.bind(this, function () { + let _row = accountsChooser.get_selected_row(); + log('Active:' + _row.server); + let _account_settings = getAccountSettings ( _row.server ); + + for ( var prefEntry of prefs ) + { + if ( prefEntry.type == Gtk.Entry ) + { + // How to unbind previous one? + _account_settings.bind ( + prefEntry.key, + this.prefWidgets[prefEntry.key], + 'text', + Gio.SettingsBindFlags.DEFAULT + ); + } + else if ( prefEntry.type == Gtk.ComboBoxText ) + { + this.prefWidgets[prefEntry.key].set_active(_account_settings.get_enum('type')); + this.prefWidgets[prefEntry.key].connect('changed', Lang.bind(this, function () { + log('Active:' + this.prefWidgets[prefEntry.key].get_active()); + _account_settings.set_enum('type', this.prefWidgets[prefEntry.key].get_active()); + })); + } + } + } ) ); + + + + let accountsChooserActionBar = new Gtk.ActionBar ( { visible: true, + valign: Gtk.Align.END, + vexpand: false} ); + accountsChooserContainer.pack_start(accountsChooserActionBar, true, true, 0); + + let accountsWidget = new Gtk.Grid ( { + halign: Gtk.Align.FILL, + margin: 18, + column_spacing: 12, + row_spacing: 12, + visible: true, + column_homogeneous: false, + }); + accountsWidgetContainer.pack_start(accountsWidget, true, true, 0); + + let y = 1; + for ( var prefEntry of prefs ) + { + let _label = new Gtk.Label({ + label: prefEntry.label + ':', + visible: true, + halign: Gtk.Align.START, + }); + accountsWidget.attach ( _label, 0, y, 1, 1 ); + + this.prefWidgets[prefEntry.key] = new prefEntry.type({ + halign: Gtk.Align.FILL, + visible: true, + hexpand: true, + }); + + let boundValue = 'text'; + if ( prefEntry.key == 'password' ) + { + this.prefWidgets[prefEntry.key].set_input_purpose ( Gtk.InputPurpose.PASSWORD ); + this.prefWidgets[prefEntry.key].set_visibility ( false ); + } + else if ( prefEntry.key == 'type' ) + { + [ 'Icinga', 'Icinga2' ].forEach((item) => { + this.prefWidgets[prefEntry.key].append_text(item); + } ); + } + + accountsWidget.attach(this.prefWidgets[prefEntry.key], 1, y, 1, 1); + + if ( prefEntry.type != Gtk.ComboBoxText ) + { +// this.account_settings.bind( +// prefEntry.key, +// _entry, +// boundValue, +// Gio.SettingsBindFlags.DEFAULT +// ); + } + y++; + } + + mainVbox.pack_start(mainWidget, true, true, 0); + mainVbox.show_all(); + + return mainVbox; +} + +function getAccountSettings ( id ) +{ + let _path = SETTINGS_SCHEMA_ACCOUNT_PATH + '/' + id; + return Convenience.getSettings(SETTINGS_SCHEMA_ACCOUNT, _path); +} + diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml new file mode 100644 index 0000000..9e10b22 --- /dev/null +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + '0' + + + + "Short" + + + + + + + + + '' + + + + "Icinga" + + + + '' + + + + '' + + + + '' + + + + '' + + + + 1 + + + + diff --git a/stylesheet.css b/stylesheet.css new file mode 100644 index 0000000..670e79e --- /dev/null +++ b/stylesheet.css @@ -0,0 +1,62 @@ +/* Add your custom extension styling here */ +.panel-status-menu-box { + color: inherit; +} + +.monito-services { +/* padding: 0px; + margin: 0px; */ +} + +.monito-box { + color: white; + border: 1px solid white; + padding: 0px .5em; + vertical-align: middle; + font-size: 20px; + line-height: 20px; +} + +.monito-critical-box, .monito-service-CRITICAL, .monito-service-line-CRITICAL { + background-color: #FF3300; +} + +.monito-warning-box, .monito-service-WARNING, .monito-service-line-WARNING { + background-color: #ffa500; +} + +.monito-ok-box, .monito-service-OK, .monito-service-line-OK { + background-color: #00CC33; +} + +.monito-unknown-box, .monito-service-UNKNOWN, .monito-service-line-UNKNOWN { + background-color: purple; +} + +.monito-service-line { + padding: 3px; + margin: 0px; + color: white; + border: 1px solid white; + vertical-align: middle; + font-size: 20px; + line-height: 20px; +} + +.monito-title { + margin: 2px; + font-size: 200%; +} + +.monito-label { + margin: 2px; +} + +.monito-button-icon { + padding: 0px; + margin: 0px; +} + +.button { + padding: 12px !important; +}