diff --git a/packages/web/management/js/fog/fog.common.js b/packages/web/management/js/fog/fog.common.js index 268fcfeb0f..1a04a1b757 100644 --- a/packages/web/management/js/fog/fog.common.js +++ b/packages/web/management/js/fog/fog.common.js @@ -5328,6 +5328,7 @@ function reinitialize() { setupPasswordReveal(); setupUniversalSearch(); setupInfoCard(); + setupInfoCardActions(); }; /** @@ -5395,6 +5396,73 @@ function setupInfoCard() { }); } + +/** + * The info card's one-click task buttons. + * + * Straight to the create endpoint. The options form at + * ?node={node}&sub=deploy is skipped deliberately -- Deploy, Capture and + * Multi-Cast need nothing off it, and fetching it only to post it back + * unchanged is the click this exists to remove. Everything that form's POST + * is checked for -- a pending host, an assigned and enabled image, a + * protected image on a capture, one image across a multicast -- is checked + * in deployPost(), not in the form, so nothing is skipped but the rendering. + * + * scheduleType is the one field that must be sent: validateScheduleType() + * throws on an absent value rather than defaulting, so an empty body would + * come back "Invalid scheduling type". + * + * Every button confirms first. The list grid's equivalents do not, but there + * you have ticked a row to get them; here you arrive on this page just by + * clicking a host name, and one stray click would deploy over a running + * machine -- or, from a group, over all of them. The text is built server + * side (FOGPageRender::renderQuickTaskActions) because it is translated. + */ +function setupInfoCardActions() { + // Guards the window between the click and the server's answer. Per button + // rather than one flag for the card: Deploy and Capture are different + // taskings and there is no reason firing one should block the other. The + // button stays enabled underneath so nothing has to re-enable it on a + // refusal; this is only about the impatient double-click, which would + // otherwise be two identical taskings. + var running = {}; + + // Delegated and namespaced: the card is torn down and rebuilt with the + // page on every AJAX nav, so a direct binding would be lost on the first + // one and doPageLoad() would stack a new one per visit. + $(document) + .off('click.fogQuickTask') + .on('click.fogQuickTask', '.fog-quicktask', function(e) { + e.preventDefault(); + var btn = $(this), + node = btn.data('node'), + id = btn.data('id'), + type = btn.data('type'), + key = node + ':' + id + ':' + type; + if (running[key]) { + return; + } + if (!window.confirm(btn.data('confirm'))) { + return; + } + running[key] = true; + $.apiCall( + 'post', + '../management/index.php?node=' + encodeURIComponent(node) + + '&sub=deploy&id=' + encodeURIComponent(id) + + '&type=' + encodeURIComponent(type), + {scheduleType: 'instant'}, + function() { + // Both outcomes land here and both only need the lock released. + // apiCall has already drawn the toast, and there is nothing on + // this page to repaint: the card's Last Deployed is the date the + // last task FINISHED, which a task just queued has not. + running[key] = false; + } + ); + }); +} + // Select2 builds its search inputs with neither id/name nor a label, tripping // the browser's "form field should have an id or name" autofill advisory and the // "no label associated with a form field" accessibility advisory. There are two diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 4b47e8f510..3fe5ef2ae0 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -2410,6 +2410,10 @@ msgstr "Benutzer erfolgreich erstellt" msgid "Create Users On First Login" msgstr "" +#, fuzzy, php-format +msgid "Create a %1$s task for %2$s?" +msgstr "Benutzer erfolgreich erstellt" + #, fuzzy, php-format msgid "Create a %s" msgstr "Neue %s erstellen" @@ -7869,6 +7873,10 @@ msgstr "" msgid "Queued deletion is not active and cannot be canceled" msgstr "Snapin ist geschützt und kann nicht gelöscht werden" +#, fuzzy +msgid "Quick tasks" +msgstr "Aktive Multicast-Tasks" + msgid "RESOURCES" msgstr "" @@ -10412,7 +10420,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11852,6 +11859,10 @@ msgstr "" msgid "ago" msgstr "vor" +#, php-format +msgid "all %1$d hosts in group \"%2$s\"" +msgstr "" + #, fuzzy msgid "all current storage nodes" msgstr "momentanen Speicherknoten löschen." @@ -12147,6 +12158,10 @@ msgstr "" msgid "host" msgstr "Host" +#, fuzzy, php-format +msgid "host \"%s\"" +msgstr "Hosts" + #, fuzzy msgid "host is" msgstr "Hosts" @@ -13809,10 +13824,6 @@ msgstr "" #~ msgid "min (all)" #~ msgstr "Aktiviert" -#, fuzzy -#~ msgid "multicast tasks!" -#~ msgstr "Aktive Multicast-Tasks" - #, fuzzy #~ msgid "no database to" #~ msgstr "Keine Datenbank zum" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 394a393447..cac44bb7ba 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -2413,6 +2413,10 @@ msgstr "User created" msgid "Create Users On First Login" msgstr "" +#, fuzzy, php-format +msgid "Create a %1$s task for %2$s?" +msgstr "User created" + #, fuzzy, php-format msgid "Create a %s" msgstr "Create New %s" @@ -7880,6 +7884,10 @@ msgstr "" msgid "Queued deletion is not active and cannot be canceled" msgstr "Snapin is protected and cannot be deleted" +#, fuzzy +msgid "Quick tasks" +msgstr "Active Multicast Tasks" + msgid "RESOURCES" msgstr "" @@ -10421,7 +10429,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11860,6 +11867,10 @@ msgstr "" msgid "ago" msgstr " ago" +#, php-format +msgid "all %1$d hosts in group \"%2$s\"" +msgstr "" + #, fuzzy msgid "all current storage nodes" msgstr "Invalid storage node" @@ -12153,6 +12164,10 @@ msgstr "" msgid "host" msgstr "host" +#, fuzzy, php-format +msgid "host \"%s\"" +msgstr "host" + #, fuzzy msgid "host is" msgstr "host" @@ -13770,10 +13785,6 @@ msgstr "" #~ msgid "min (all)" #~ msgstr "Enabled" -#, fuzzy -#~ msgid "multicast tasks!" -#~ msgstr "Active Multicast Tasks" - #, fuzzy #~ msgid "no database to" #~ msgstr "No database to work off" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 1caee998b9..ad38dff2f0 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -2431,6 +2431,10 @@ msgstr "creado por el usuario" msgid "Create Users On First Login" msgstr "" +#, fuzzy, php-format +msgid "Create a %1$s task for %2$s?" +msgstr "creado por el usuario" + #, fuzzy, php-format msgid "Create a %s" msgstr "Crear nuevo grupo" @@ -7998,6 +8002,10 @@ msgstr "" msgid "Queued deletion is not active and cannot be canceled" msgstr "está protegido, no se permite la eliminación" +#, fuzzy +msgid "Quick tasks" +msgstr "MulticastTask" + msgid "RESOURCES" msgstr "" @@ -10579,7 +10587,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -12020,6 +12027,10 @@ msgstr "" msgid "ago" msgstr " hace" +#, php-format +msgid "all %1$d hosts in group \"%2$s\"" +msgstr "" + #, fuzzy msgid "all current storage nodes" msgstr "nodo de almacenamiento no válido" @@ -12314,6 +12325,10 @@ msgstr "" msgid "host" msgstr "anfitrión" +#, fuzzy, php-format +msgid "host \"%s\"" +msgstr "anfitrión" + #, fuzzy msgid "host is" msgstr "anfitrión" @@ -13915,10 +13930,6 @@ msgstr "" #~ msgid "min (all)" #~ msgstr "habilitado" -#, fuzzy -#~ msgid "multicast tasks!" -#~ msgstr "MulticastTask" - #, fuzzy #~ msgid "not found on disk" #~ msgstr "No se encontró Clase FOGPage para este nodo" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 03abe4584f..232108da8e 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -2410,6 +2410,10 @@ msgstr "Benutzer erfolgreich erstellt" msgid "Create Users On First Login" msgstr "" +#, fuzzy, php-format +msgid "Create a %1$s task for %2$s?" +msgstr "Benutzer erfolgreich erstellt" + #, fuzzy, php-format msgid "Create a %s" msgstr "Neue %s erstellen" @@ -7870,6 +7874,10 @@ msgstr "" msgid "Queued deletion is not active and cannot be canceled" msgstr "Snapin ist geschützt und kann nicht gelöscht werden" +#, fuzzy +msgid "Quick tasks" +msgstr "Aktive Multicast-Tasks" + msgid "RESOURCES" msgstr "" @@ -10413,7 +10421,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11853,6 +11860,10 @@ msgstr "" msgid "ago" msgstr "vor" +#, php-format +msgid "all %1$d hosts in group \"%2$s\"" +msgstr "" + #, fuzzy msgid "all current storage nodes" msgstr "momentanen Speicherknoten löschen." @@ -12148,6 +12159,10 @@ msgstr "" msgid "host" msgstr "Host" +#, fuzzy, php-format +msgid "host \"%s\"" +msgstr "Hosts" + #, fuzzy msgid "host is" msgstr "Hosts" @@ -13810,10 +13825,6 @@ msgstr "" #~ msgid "min (all)" #~ msgstr "Aktiviert" -#, fuzzy -#~ msgid "multicast tasks!" -#~ msgstr "Aktive Multicast-Tasks" - #, fuzzy #~ msgid "no database to" #~ msgstr "Keine Datenbank zum" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 34d4e9fc2b..537d138abd 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -2413,6 +2413,10 @@ msgstr "utilisateur créé" msgid "Create Users On First Login" msgstr "" +#, fuzzy, php-format +msgid "Create a %1$s task for %2$s?" +msgstr "utilisateur créé" + #, fuzzy, php-format msgid "Create a %s" msgstr "Créer un nouveau %s" @@ -7865,6 +7869,10 @@ msgstr "" msgid "Queued deletion is not active and cannot be canceled" msgstr "Snapin est protégé et ne peut être supprimé" +#, fuzzy +msgid "Quick tasks" +msgstr "Tâches Multicast actifs" + msgid "RESOURCES" msgstr "" @@ -10405,7 +10413,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11845,6 +11852,10 @@ msgstr "" msgid "ago" msgstr " depuis" +#, php-format +msgid "all %1$d hosts in group \"%2$s\"" +msgstr "" + #, fuzzy msgid "all current storage nodes" msgstr "nœud de stockage non valide" @@ -12138,6 +12149,10 @@ msgstr "" msgid "host" msgstr "hôte" +#, fuzzy, php-format +msgid "host \"%s\"" +msgstr "hôte" + #, fuzzy msgid "host is" msgstr "hôte" @@ -13759,10 +13774,6 @@ msgstr "" #~ msgid "min (all)" #~ msgstr "Activée" -#, fuzzy -#~ msgid "multicast tasks!" -#~ msgstr "Tâches Multicast actifs" - #, fuzzy #~ msgid "no database to" #~ msgstr "Aucune base de données de travailler hors" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 5f5ba1a7fd..112f91ab1d 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -2355,6 +2355,10 @@ msgstr "Creazione utente riuscita" msgid "Create Users On First Login" msgstr "" +#, fuzzy, php-format +msgid "Create a %1$s task for %2$s?" +msgstr "Creazione utente riuscita" + #, fuzzy, php-format msgid "Create a %s" msgstr "Crea nuovo %s" @@ -7662,6 +7666,10 @@ msgstr "" msgid "Queued deletion is not active and cannot be canceled" msgstr "Snapin è protetto e non può essere cancellato" +#, fuzzy +msgid "Quick tasks" +msgstr "Compiti multicast attivi" + msgid "RESOURCES" msgstr "" @@ -10128,7 +10136,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11528,6 +11535,10 @@ msgstr "" msgid "ago" msgstr "fa" +#, php-format +msgid "all %1$d hosts in group \"%2$s\"" +msgstr "" + msgid "all current storage nodes" msgstr "tutti i nodi di archiviazione attivi" @@ -11811,6 +11822,10 @@ msgstr "" msgid "host" msgstr "host" +#, fuzzy, php-format +msgid "host \"%s\"" +msgstr "hosts" + #, fuzzy msgid "host is" msgstr "hosts" @@ -13410,10 +13425,6 @@ msgstr "" #~ msgid "min (all)" #~ msgstr "Abilitato" -#, fuzzy -#~ msgid "multicast tasks!" -#~ msgstr "Compiti multicast attivi" - #~ msgid "no database to" #~ msgstr "Nessun database a" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 9ee1c1f9d7..ea294efbd8 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -2340,6 +2340,10 @@ msgstr "タスクタイプを作成" msgid "Create Users On First Login" msgstr "" +#, fuzzy, php-format +msgid "Create a %1$s task for %2$s?" +msgstr "タスク状態を作成" + #, fuzzy, php-format msgid "Create a %s" msgstr "新しい %s を作成" @@ -7642,6 +7646,10 @@ msgstr "" msgid "Queued deletion is not active and cannot be canceled" msgstr "スナップインは保護されているため削除できません" +#, fuzzy +msgid "Quick tasks" +msgstr "マルチキャストタスク" + msgid "RESOURCES" msgstr "" @@ -10085,7 +10093,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11484,6 +11491,10 @@ msgstr "" msgid "ago" msgstr "前" +#, php-format +msgid "all %1$d hosts in group \"%2$s\"" +msgstr "" + msgid "all current storage nodes" msgstr "現在のすべてのストレージノード" @@ -11767,6 +11778,10 @@ msgstr "ビットレート" msgid "host" msgstr "ホスト" +#, fuzzy, php-format +msgid "host \"%s\"" +msgstr "ホスト" + #, fuzzy msgid "host is" msgstr "ホスト" @@ -14918,10 +14933,6 @@ msgstr "" #~ msgid "more secure" #~ msgstr "より安全" -#, fuzzy -#~ msgid "multicast tasks!" -#~ msgstr "マルチキャストタスク" - #~ msgid "no login will appear" #~ msgstr "ログイン画面は表示されません" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 370ce07fc0..0f96014895 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -2083,6 +2083,10 @@ msgstr "" msgid "Create Users On First Login" msgstr "" +#, php-format +msgid "Create a %1$s task for %2$s?" +msgstr "" + #, php-format msgid "Create a %s" msgstr "" @@ -6741,6 +6745,9 @@ msgstr "" msgid "Queued deletion is not active and cannot be canceled" msgstr "" +msgid "Quick tasks" +msgstr "" + msgid "RESOURCES" msgstr "" @@ -8925,7 +8932,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10204,6 +10210,10 @@ msgstr "" msgid "ago" msgstr "" +#, php-format +msgid "all %1$d hosts in group \"%2$s\"" +msgstr "" + msgid "all current storage nodes" msgstr "" @@ -10460,6 +10470,10 @@ msgstr "" msgid "host" msgstr "" +#, php-format +msgid "host \"%s\"" +msgstr "" + msgid "host is" msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 6f8a33f5b2..498df9cbd2 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -2413,6 +2413,10 @@ msgstr "usuário criado" msgid "Create Users On First Login" msgstr "" +#, fuzzy, php-format +msgid "Create a %1$s task for %2$s?" +msgstr "usuário criado" + #, fuzzy, php-format msgid "Create a %s" msgstr "Criar novo %s" @@ -7867,6 +7871,10 @@ msgstr "" msgid "Queued deletion is not active and cannot be canceled" msgstr "Snapin está protegido e não pode ser excluído" +#, fuzzy +msgid "Quick tasks" +msgstr "Tarefas Multicast ativos" + msgid "RESOURCES" msgstr "" @@ -10408,7 +10416,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11848,6 +11855,10 @@ msgstr "" msgid "ago" msgstr " atrás" +#, php-format +msgid "all %1$d hosts in group \"%2$s\"" +msgstr "" + #, fuzzy msgid "all current storage nodes" msgstr "nó de armazenamento inválido" @@ -12141,6 +12152,10 @@ msgstr "" msgid "host" msgstr "anfitrião" +#, fuzzy, php-format +msgid "host \"%s\"" +msgstr "anfitrião" + #, fuzzy msgid "host is" msgstr "anfitrião" @@ -13762,10 +13777,6 @@ msgstr "" #~ msgid "min (all)" #~ msgstr "ativado" -#, fuzzy -#~ msgid "multicast tasks!" -#~ msgstr "Tarefas Multicast ativos" - #, fuzzy #~ msgid "no database to" #~ msgstr "Nenhum banco de dados para trabalhar fora" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index ee144b805d..4e7b96548f 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -2413,6 +2413,10 @@ msgstr "用户创建" msgid "Create Users On First Login" msgstr "" +#, fuzzy, php-format +msgid "Create a %1$s task for %2$s?" +msgstr "用户创建" + #, fuzzy, php-format msgid "Create a %s" msgstr "新建%s" @@ -7867,6 +7871,10 @@ msgstr "" msgid "Queued deletion is not active and cannot be canceled" msgstr "管理单元被保护,不能被删除" +#, fuzzy +msgid "Quick tasks" +msgstr "活动组播任务" + msgid "RESOURCES" msgstr "" @@ -10408,7 +10416,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11848,6 +11855,10 @@ msgstr "" msgid "ago" msgstr "前" +#, php-format +msgid "all %1$d hosts in group \"%2$s\"" +msgstr "" + #, fuzzy msgid "all current storage nodes" msgstr "无效的存储节点" @@ -12141,6 +12152,10 @@ msgstr "" msgid "host" msgstr "主办" +#, fuzzy, php-format +msgid "host \"%s\"" +msgstr "主办" + #, fuzzy msgid "host is" msgstr "主办" @@ -13762,10 +13777,6 @@ msgstr "" #~ msgid "min (all)" #~ msgstr "启用" -#, fuzzy -#~ msgid "multicast tasks!" -#~ msgstr "活动组播任务" - #, fuzzy #~ msgid "no database to" #~ msgstr "没有数据库工作关闭" diff --git a/packages/web/src/Base/FOGPage.php b/packages/web/src/Base/FOGPage.php index 16afb38253..c7d3635072 100644 --- a/packages/web/src/Base/FOGPage.php +++ b/packages/web/src/Base/FOGPage.php @@ -99,6 +99,18 @@ abstract class FOGPage extends FOGBase * @var array */ public $noteSources = []; + /** + * Pre-rendered action buttons for the info card, or ''. + * + * Sits to the right of the notes in the same card. Built by a page's + * edit() -- renderQuickTaskActions() is the only builder today -- and + * echoed by renderInfoCard(). A string rather than a spec array because + * the only thing the renderer does with it is echo it, and the pages + * that set it are already building markup with makeButton(). + * + * @var string + */ + public $noteActions = ''; /** * Table header data * diff --git a/packages/web/src/Base/FOGPageRender.php b/packages/web/src/Base/FOGPageRender.php index bde8ae1056..01b5f320da 100644 --- a/packages/web/src/Base/FOGPageRender.php +++ b/packages/web/src/Base/FOGPageRender.php @@ -546,23 +546,128 @@ protected static function noteSourceAttrs($source) return $attrs; } + /** + * The info card's one-click task buttons. + * + * The host and group LIST grids have carried these since + * HostManagement::_quickTaskItems(): the two or three task types that + * take no options, fired straight at the create endpoint instead of + * fetching an options form only to post it back untouched. This is the + * same affordance on the edit pages, so an admin already looking at a + * host or a group does not have to go back to the grid, find the row + * again and tick it to do the obvious thing to it. + * + * Deploy and Capture for a host, Deploy and Multi-Cast for a group -- + * the same pairing _quickTaskItems() documents, and for the same + * reason: those are the types that need no options, which is the whole + * reason they can be one click. + * + * btn-secondary, and neither of them primary. Nothing here is the card's + * commit action -- the General tab's Update is -- so these are two + * shortcuts in a header strip, not a decision cluster in a form footer, + * and the weight a red button would carry is carried by the + * confirmation instead. It is also what the list grid's own quick + * buttons are, since DataTables draws its button bar that way. + * + * Filled, NOT btn-outline-secondary, and that is a contrast decision + * rather than a taste one. Outline keeps #6c757d as the TEXT color, and + * against the dark card (#212529) that is 3.29:1 -- under the 4.5:1 AA + * floor for body-sized text. Filled puts white on #6c757d instead and + * holds 4.69:1 in both themes. Measured against the shipped + * adminlte4.min.css + fog-default-ui.min.css, not assumed. + * + * @param string $node Page node, e.g. 'host' or 'group'. Also decides + * the permission: ?node=X&sub=deploy resolves to + * X.task via Authorization::_subToAction(), so + * the gate here and the gate the POST hits are + * the same string by construction. + * @param int $id The entity being edited. + * @param array $typeIds TaskType ids, in the order they should appear. + * @param string $target Already-translated description of what the task + * lands on, e.g. 'host "foo"'. Interpolated into + * the confirmation. Built by the caller because + * only the caller knows whether it is one machine + * or a group of them. + * + * @return string The button group markup, or '' if none may be shown. + */ + public static function renderQuickTaskActions( + $node, + $id, + array $typeIds, + $target + ) { + // Same refusal _quickTaskItems() makes: a user without the + // permission would be shown buttons whose POST can only be denied. + if (!Authorization::can($node . '.task')) { + return ''; + } + + $buttons = ''; + foreach ($typeIds as $typeId) { + $TaskType = new TaskType($typeId); + // A server whose taskTypes row was deleted simply loses that + // button, the same way the accordion and the grid lose theirs. + if (!$TaskType->isValid()) { + continue; + } + $name = (string)$TaskType->get('name'); + // Built here, not in the script. gettext runs server side, so a + // sentence assembled in JS would never reach the .pot -- the + // same reason noteSourceAttrs() builds its on/off labels here. + $confirm = sprintf( + _('Create a %1$s task for %2$s?'), + $name, + $target + ); + $buttons .= self::makeButton( + 'quicktask-' . \Initiator::e($node) . '-' . (int)$TaskType->get('id'), + ' ' . \Initiator::e($name), + 'btn btn-secondary fog-quicktask', + 'type="button"' + . ' data-node="' . \Initiator::e($node) . '"' + . ' data-id="' . (int)$id . '"' + . ' data-type="' . (int)$TaskType->get('id') . '"' + . ' data-confirm="' . \Initiator::e($confirm) . '"' + ); + } + if ('' === $buttons) { + return ''; + } + + return '
' . $buttons . '
'; + } + protected function renderInfoCard() { $notes = (array)$this->notes; $sources = (array)$this->noteSources; + $actions = (string)$this->noteActions; // Mirrors PLUGINS_INJECT_TABDATA in tabFields(): a plugin that adds // a tab to a core page can add its line here too. 1.5's equivalent // rode SUB_MENULINK_DATA, which 1.6 repurposed for the sidebar node // menu, so there is no back-compat name to keep. + // + // 'actions' rides the same event rather than getting one of its own: + // a plugin adding a button to this card is doing the same thing as a + // plugin adding a line to it, and a second event would mean two + // registrations for one card. self::$HookManager->processEvent( 'EDIT_INFO_DATA', [ 'notes' => &$notes, 'noteSources' => &$sources, + 'noteActions' => &$actions, 'obj' => &$this->obj ] ); - if (!count($notes)) { + // Either half is enough to be worth drawing. A page with buttons and + // no notes is unusual but not wrong, and returning early on the note + // count alone would silently drop the buttons. + if (!count($notes) && '' === trim($actions)) { return; } echo '
'; @@ -593,6 +698,16 @@ protected function renderInfoCard() echo '
'; echo ''; } + // Last in the row and pushed to the far edge with ms-auto, so the + // buttons sit clear of the notes however many notes there are. A + // flex auto margin, not a float: the row is display:flex and a float + // would do nothing here. + if ('' !== trim($actions)) { + echo '
'; + echo $actions; + echo '
'; + } echo ''; // Once, and only when something above actually carries the marker. // A standing disclaimer on every edit page would be noise on the diff --git a/packages/web/src/Base/System.php b/packages/web/src/Base/System.php index 799288156b..d055b817ef 100644 --- a/packages/web/src/Base/System.php +++ b/packages/web/src/Base/System.php @@ -131,7 +131,7 @@ public function __construct() // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. define('FOG_SCHEMA', 430); - define('FOG_BCACHE_VER', 360); + define('FOG_BCACHE_VER', 361); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as // a release asset. Pinned here rather than tracked as "latest" so a diff --git a/packages/web/src/Pages/GroupManagement.php b/packages/web/src/Pages/GroupManagement.php index ceef9e3550..0219d9d84e 100644 --- a/packages/web/src/Pages/GroupManagement.php +++ b/packages/web/src/Pages/GroupManagement.php @@ -1757,9 +1757,12 @@ public function groupSnapinHistory() */ public function edit() { + // Read once: the note below and the quick-task confirmation both + // want it, and getHostCount() is a query. + $hostCount = (int)$this->obj->getHostCount(); $this->notes = [ _('Group') => $this->obj->get('name'), - _('Members') => (string)$this->obj->getHostCount() + _('Members') => (string)$hostCount ]; // Info-card notes that mirror a General-tab control, so the card // tracks the form instead of going stale until the next page @@ -1769,6 +1772,29 @@ public function edit() $this->noteSources = [ _('Group') => '#group' ]; + // Deploy and Multi-Cast, one click, from whichever tab is open. + // Multi-Cast rather than Capture: capturing is a thing you do to + // one machine, and a group is by definition more than one. Not a + // style call -- deployPost() below throws "Groups cannot create + // capture tasks" outright, so a Capture button here could only ever + // produce a toast saying no. The list grid draws the same line on + // ttIsAccess ('host' vs 'group'). + // + // The member count goes in the confirmation, not just the name. A + // group's size is the fact that decides whether you meant to press + // this, and it is the one thing the button itself cannot show. + if ($hostCount > 0) { + $this->noteActions = self::renderQuickTaskActions( + 'group', + (int)$this->obj->get('id'), + [TaskType::DEPLOY, TaskType::MULTICAST], + sprintf( + _('all %1$d hosts in group "%2$s"'), + $hostCount, + $this->obj->get('name') + ) + ); + } $tabData = []; // General diff --git a/packages/web/src/Pages/HostManagement.php b/packages/web/src/Pages/HostManagement.php index 7d7b39c6cb..ce5a2087be 100644 --- a/packages/web/src/Pages/HostManagement.php +++ b/packages/web/src/Pages/HostManagement.php @@ -4492,9 +4492,18 @@ public function hostInstalledSoftware() public function edit() { // Identity plus the facts you cannot see from the other twenty - // tabs: which image is assigned, when it was last imaged, and which - // group it belongs to. - $primaryGroup = new Group(self::minId($this->obj->get('groups'))); + // tabs: which image is assigned and when it was last imaged. + // + // No group line. It used to say "Primary Group", meaning + // minId($this->obj->get('groups')) -- the lowest-id group the host + // happened to be in. There is no primary group: a group GRANTS, and + // what a host ends up with is resolved from every group it belongs + // to at task time, ordered by groupOrder (ADR 0038, and see + // FOG\Assign\Resolver). So the label named a rank that does not + // exist and the value picked one membership arbitrarily. Listing + // them all instead was considered and dropped: a host in eight + // groups blows the card out, and the Group Associations tab already + // shows them properly. // Whichever of the two clients last spoke, named so the card is not // ambiguous about which one it is reporting. // @@ -4537,11 +4546,6 @@ public function edit() $this->obj->get('deployed'), 'hosts', 'hostLastDeploy' - ), - _('Primary Group') => ( - $primaryGroup->isValid() ? - $primaryGroup->get('name') : - _('None') ) ]; // Info-card notes that mirror a General-tab control, so the card @@ -4553,6 +4557,18 @@ public function edit() _('Host') => '#host', _('Assigned Image') => '#image' ]; + // Deploy and Capture, one click, from whichever tab is open. Gated + // on pending for the same reason the Tasks tab below is: a pending + // host cannot be tasked, and deployPost() refuses it anyway, so + // offering the button only produces a toast saying no. + if (!$this->obj->get('pending')) { + $this->noteActions = self::renderQuickTaskActions( + 'host', + (int)$this->obj->get('id'), + [TaskType::DEPLOY, TaskType::CAPTURE], + sprintf(_('host "%s"'), $this->obj->get('name')) + ); + } $tabData = []; // General diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 43fb65b065..f2ec80572e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3164,6 +3164,12 @@ parameters: count: 1 path: packages/web/src/Pages/ActivityManagement.php + - + message: '#^Constructor of class FOG\\Pages\\AgentActivityManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter + count: 1 + path: packages/web/src/Pages/AgentActivityManagement.php + - message: '#^Constructor of class FOG\\Pages\\ApiDocumentation has an unused parameter \$name\.$#' identifier: constructor.unusedParameter diff --git a/phpstan-tests-baseline.neon b/phpstan-tests-baseline.neon index 04c4a448d9..25dcb06ecd 100644 --- a/phpstan-tests-baseline.neon +++ b/phpstan-tests-baseline.neon @@ -12,6 +12,30 @@ parameters: count: 1 path: tests/activity-sources.test.php + - + message: '#^Call to function in_array\(\) with arguments ''already_joined''\|''joined''\|''refused''\|''unsupported'', array\{''joined'', ''already_joined'', ''failed'', ''unsupported'', ''refused''\} and true will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: tests/agent-directory-join.test.php + + - + message: '#^Call to function in_array\(\) with arguments ''refused'', array\{''joined'', ''already_joined'', ''failed'', ''unsupported'', ''refused''\} and true will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: tests/agent-directory-join.test.php + + - + message: '#^Call to function in_array\(\) with arguments ''refused'', array\{''joined'', ''already_joined''\} and true will always evaluate to false\.$#' + identifier: function.impossibleType + count: 1 + path: tests/agent-directory-join.test.php + + - + message: '#^Comparison operation "\>\=" between 3600 and 1800 is always true\.$#' + identifier: greaterOrEqual.alwaysTrue + count: 1 + path: tests/agent-directory-join.test.php + - message: '#^Offset ''directory'' on array\{inventory\: ''FOG\\\\Agent\\\\InventoryFacts'', software\: ''FOG\\\\Agent\\\\SoftwareFacts'', directory\: ''FOG\\\\Agent\\\\DirectoryFacts'', printers\: ''FOG\\\\Agent\\\\PrinterFacts''\} on left side of \?\? always exists and is not nullable\.$#' identifier: nullCoalesce.offset @@ -42,6 +66,12 @@ parameters: count: 1 path: tests/agent-directory.test.php + - + message: '#^Offset ''directory'' on array\{inventory\: ''FOG\\\\Agent\\\\InventoryFacts'', software\: ''FOG\\\\Agent\\\\SoftwareFacts'', directory\: ''FOG\\\\Agent\\\\DirectoryFacts'', printers\: ''FOG\\\\Agent\\\\PrinterFacts'', network\: ''FOG\\\\Agent\\\\NetworkFacts'', secureboot\: ''FOG\\\\Agent\\\\SecureBootFacts''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/agent-directory.test.php + - message: '#^Call to function in_array\(\) with arguments ''failed'', array\{''converged'', ''installed'', ''updated'', ''removed''\} and true will always evaluate to false\.$#' identifier: function.impossibleType @@ -108,6 +138,24 @@ parameters: count: 1 path: tests/agent-printer-facts.test.php + - + message: '#^Offset ''printers'' on array\{hostname\: ''hostnamechanger'', taskreboot\: ''taskreboot'', snapin\: ''snapinclient'', software\: ''software'', power\: ''powermanagement'', directory\: ''hostnamechanger'', printers\: ''printermanager'', wake\: ''powermanagement''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/agent-printer-facts.test.php + + - + message: '#^Offset ''printers'' on array\{inventory\: ''FOG\\\\Agent\\\\InventoryFacts'', software\: ''FOG\\\\Agent\\\\SoftwareFacts'', directory\: ''FOG\\\\Agent\\\\DirectoryFacts'', printers\: ''FOG\\\\Agent\\\\PrinterFacts'', network\: ''FOG\\\\Agent\\\\NetworkFacts'', secureboot\: ''FOG\\\\Agent\\\\SecureBootFacts''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/agent-printer-facts.test.php + + - + message: '#^Offset ''printers'' on array\{snapin\: ''FOG\\\\Agent\\\\Snapins'', software\: ''FOG\\\\Agent\\\\SoftwareSet'', printers\: ''FOG\\\\Agent\\\\PrinterSet'', directory\: ''FOG\\\\Agent\\\\DirectoryJoin'', wake\: ''FOG\\\\Agent\\\\WakeRelay''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/agent-printer-facts.test.php + - message: '#^Call to function in_array\(\) with arguments ''inferred'', array\{''logout'', ''disconnect'', ''service_stop''\} and true will always evaluate to false\.$#' identifier: function.impossibleType @@ -126,6 +174,114 @@ parameters: count: 1 path: tests/agent-user-sessions.test.php + - + message: '#^Call to function in_array\(\) with arguments ''expired''\|''pending'', array\{''sent'', ''failed''\} and true will always evaluate to false\.$#' + identifier: function.impossibleType + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Call to function in_array\(\) with arguments ''sent'', array\{''sent'', ''failed''\} and true will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Call to function is_int\(\) with 32 will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Comparison operation "\<\=" between 600 and 3600 is always true\.$#' + identifier: smallerOrEqual.alwaysTrue + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Comparison operation "\<\=" between 900 and 3600 is always true\.$#' + identifier: smallerOrEqual.alwaysTrue + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Comparison operation "\>" between 3 and 1 is always true\.$#' + identifier: greater.alwaysTrue + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Comparison operation "\>" between 32 and 0 is always true\.$#' + identifier: greater.alwaysTrue + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Comparison operation "\>" between 600 and 0 is always true\.$#' + identifier: greater.alwaysTrue + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Comparison operation "\>" between 900 and 0 is always true\.$#' + identifier: greater.alwaysTrue + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Offset ''network'' on array\{inventory\: ''FOG\\\\Agent\\\\InventoryFacts'', software\: ''FOG\\\\Agent\\\\SoftwareFacts'', directory\: ''FOG\\\\Agent\\\\DirectoryFacts'', printers\: ''FOG\\\\Agent\\\\PrinterFacts'', network\: ''FOG\\\\Agent\\\\NetworkFacts'', secureboot\: ''FOG\\\\Agent\\\\SecureBootFacts''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Offset ''wake'' on array\{hostname\: ''hostnamechanger'', taskreboot\: ''taskreboot'', snapin\: ''snapinclient'', software\: ''software'', power\: ''powermanagement'', directory\: ''hostnamechanger'', printers\: ''printermanager'', wake\: ''powermanagement''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 2 + path: tests/agent-wake-relay.test.php + + - + message: '#^Offset ''wake'' on array\{snapin\: ''FOG\\\\Agent\\\\Snapins'', software\: ''FOG\\\\Agent\\\\SoftwareSet'', printers\: ''FOG\\\\Agent\\\\PrinterSet'', directory\: ''FOG\\\\Agent\\\\DirectoryJoin'', wake\: ''FOG\\\\Agent\\\\WakeRelay''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Result of && is always true\.$#' + identifier: booleanAnd.alwaysTrue + count: 2 + path: tests/agent-wake-relay.test.php + + - + message: '#^Result of method FogChecks\:\:finish\(\) \(void\) is used\.$#' + identifier: method.void + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Strict comparison using \=\=\= between ''FOG\\\\Agent\\\\NetworkFacts'' and ''FOG\\\\Agent\\\\NetworkFacts'' will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Strict comparison using \=\=\= between ''FOG\\\\Agent\\\\WakeRelay'' and ''FOG\\\\Agent\\\\WakeRelay'' will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Strict comparison using \=\=\= between ''powermanagement'' and ''powermanagement'' will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-wake-relay.test.php + + - + message: '#^Strict comparison using \=\=\= between array\{''id'', ''macs''\} and array\{''id'', ''macs''\} will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-wake-relay.test.php + - message: '#^Call to new Initiator\(\) on a separate line has no effect\.$#' identifier: new.resultUnused diff --git a/tests/fixtures/upgrade-rehearsal-baseline.txt b/tests/fixtures/upgrade-rehearsal-baseline.txt index 4aaaea8655..8fcbc01e98 100644 --- a/tests/fixtures/upgrade-rehearsal-baseline.txt +++ b/tests/fixtures/upgrade-rehearsal-baseline.txt @@ -1,5 +1,5 @@ - constraints declared and applicable here: 96 - constraints actually present: 94 + constraints declared and applicable here: 99 + constraints actually present: 97 MISSING: 2 fk_hostMAC_hmHostID CASCADE orphan rows: 0 fk_nfsGroupMembers_ngmGroupID RESTRICT orphan rows: 1 diff --git a/tests/info-card-quick-tasks.test.php b/tests/info-card-quick-tasks.test.php new file mode 100644 index 0000000000..844f9fcf8f --- /dev/null +++ b/tests/info-card-quick-tasks.test.php @@ -0,0 +1,355 @@ +name` is null and `(int)$TaskType->id` is 0 with + * no warning that survives the page's output buffer. Here it would draw a + * nameless button pointed at type 0. + * + * 4. WHICH PAIR EACH PAGE OFFERS. Deploy and Capture for a host; Deploy and + * Multi-Cast for a group. Capturing is something you do to one machine + * and a group is by definition more than one, so a group offering Capture + * is a real defect -- and one the server would happily act on, since + * GroupManagement::deployPost() does not check ttIsAccess. + * + * Usage: php tests/info-card-quick-tasks.test.php + * Exit status 0 = pass, 1 = fail. + * + * PHP version 7.4+ + * + * @category Tests + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +use FOG\Items\TaskType; +use FOG\Items\User; +use FOG\Pages\GroupManagement; +use FOG\Pages\HostManagement; + +require_once __DIR__ . '/lib/fog-test-harness.php'; + +FogTestHarness::boot('info-card-quick-tasks'); + +$db = FogTestHarness::fakeDb(); + +/** + * One taskTypes row. Every column, not just the three the builder reads -- + * FOGController::setQuery() walks $databaseFields and warns on each one it + * cannot find, which buries the test's own output. + * + * @param int $id ttID + * @param string $name ttName + * @param string $icon ttIcon + * + * @return array + */ +$row = static function ($id, $name, $icon) { + return [ + 'ttID' => $id, + 'ttName' => $name, + 'ttDescription' => $name, + 'ttIcon' => $icon, + 'ttKernel' => '', + 'ttKernelArgs' => '', + 'ttType' => '', + 'ttIsAdvanced' => 0, + 'ttIsAccess' => 'both', + 'ttInitrd' => '' + ]; +}; +$rows = [ + TaskType::DEPLOY => $row(TaskType::DEPLOY, 'Deploy', 'download'), + TaskType::CAPTURE => $row(TaskType::CAPTURE, 'Capture', 'upload'), + TaskType::MULTICAST => $row(TaskType::MULTICAST, 'Multi-Cast', 'share-alt') +]; +// A single flat row, not a list of rows: FOGController::load() reads +// fetch()->get() as ONE record, and handing it a nested array leaves the +// object invalid with nothing said. +$db->responder = static function ($sql, $params) use ($db, $rows) { + $db->error = false; + if (false === strpos($sql, 'taskTypes')) { + return null; + } + foreach ($params as $value) { + if (isset($rows[(int)$value])) { + return $rows[(int)$value]; + } + } + + return []; +}; + +$t = new FogChecks(); + +/** + * Runs the builder as a user holding the given permissions. + * + * @param array $perms the effective permission list for the acting user + * @param string $node 'host' or 'group' + * @param array $typeIds the task types that page asks for + * @param string $target the confirmation's description of the target + * + * @return string the emitted markup + */ +$emit = static function ( + array $perms, + $node, + array $typeIds, + $target = 'host "bench-01"' +) { + $user = (new User())->set('id', 1)->set('name', 'fog'); + foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { + FogTestHarness::setStatic($cls, 'FOGUser', $user); + } + FogTestHarness::setStatic('Authorization', '_permCache', [1 => $perms]); + + return (string)HostManagement::renderQuickTaskActions( + $node, + 42, + $typeIds, + $target + ); +}; + +$hostTypes = [TaskType::DEPLOY, TaskType::CAPTURE]; +$groupTypes = [TaskType::DEPLOY, TaskType::MULTICAST]; + +// ------------------------------------------------------------------------- +// 1. The permission gate, per node. +// ------------------------------------------------------------------------- +$t->check( + 'a reader without host.task is offered no quick tasks on a host', + '' === $emit(['host.view', 'host.edit'], 'host', $hostTypes) +); +$t->check( + 'host.task alone is enough', + false !== strpos($emit(['host.task'], 'host', $hostTypes), 'fog-quicktask') +); +$t->check( + 'a reader without group.task is offered no quick tasks on a group', + '' === $emit(['group.view', 'group.edit'], 'group', $groupTypes) +); +$t->check( + 'host.task does NOT unlock the group card', + '' === $emit(['host.task'], 'group', $groupTypes) +); +$t->check( + 'group.task alone is enough', + false !== strpos( + $emit(['group.task'], 'group', $groupTypes), + 'fog-quicktask' + ) +); +$t->check( + 'and a wildcard unlocks both', + false !== strpos($emit(['*'], 'host', $hostTypes), 'fog-quicktask') + && false !== strpos($emit(['*'], 'group', $groupTypes), 'fog-quicktask') +); + +// ------------------------------------------------------------------------- +// 2. The confirmation. The reason a one-click deploy is safe to sit here. +// ------------------------------------------------------------------------- +/** + * Every button in a run of markup, as [type => [icon, name, confirm]]. + * + * @param string $markup the emitted button group + * + * @return array + */ +$parse = static function ($markup) { + preg_match_all( + '/