mirror of
https://github.com/Icinga/icingadb-web.git
synced 2026-05-28 04:36:06 -04:00
Introduce new widget base class BaseItemTable
This commit is contained in:
parent
b4b1fffe18
commit
eee4a81d2e
5 changed files with 412 additions and 6 deletions
|
|
@ -22,6 +22,7 @@ use Icinga\Module\Icingadb\Common\SearchControls;
|
|||
use Icinga\Module\Icingadb\Data\CsvResultSet;
|
||||
use Icinga\Module\Icingadb\Data\JsonResultSet;
|
||||
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
|
||||
use Icinga\Module\Icingadb\Widget\ItemTable\BaseItemTable;
|
||||
use Icinga\Module\Pdfexport\PrintableHtmlDocument;
|
||||
use Icinga\Module\Pdfexport\ProvidedHook\Pdfexport;
|
||||
use Icinga\Security\SecurityException;
|
||||
|
|
@ -494,6 +495,8 @@ class Controller extends CompatController
|
|||
{
|
||||
if ($content instanceof BaseItemList) {
|
||||
$this->content->getAttributes()->add('class', 'full-width');
|
||||
} elseif ($content instanceof BaseItemTable) {
|
||||
$this->content->getAttributes()->add('class', 'full-height');
|
||||
}
|
||||
|
||||
return parent::addContent($content);
|
||||
|
|
|
|||
198
library/Icingadb/Widget/ItemTable/BaseItemTable.php
Normal file
198
library/Icingadb/Widget/ItemTable/BaseItemTable.php
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
<?php
|
||||
|
||||
/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
|
||||
|
||||
namespace Icinga\Module\Icingadb\Widget\ItemTable;
|
||||
|
||||
use Icinga\Module\Icingadb\Widget\EmptyState;
|
||||
use ipl\Html\Attributes;
|
||||
use ipl\Html\BaseHtmlElement;
|
||||
use ipl\Html\Form;
|
||||
use ipl\Html\Html;
|
||||
use ipl\Html\HtmlElement;
|
||||
use ipl\Html\HtmlString;
|
||||
use ipl\Orm\Common\SortUtil;
|
||||
use ipl\Orm\Query;
|
||||
use ipl\Web\Control\SortControl;
|
||||
use ipl\Web\Widget\Icon;
|
||||
|
||||
abstract class BaseItemTable extends BaseHtmlElement
|
||||
{
|
||||
protected $baseAttributes = [
|
||||
'class' => 'item-table'
|
||||
];
|
||||
|
||||
/** @var array<string, string> The columns to render */
|
||||
protected $columns;
|
||||
|
||||
/** @var iterable The datasource */
|
||||
protected $data;
|
||||
|
||||
/** @var string The sort rules */
|
||||
protected $sort;
|
||||
|
||||
protected $tag = 'table';
|
||||
|
||||
/**
|
||||
* Create a new item table
|
||||
*
|
||||
* @param iterable $data Datasource of the table
|
||||
* @param array<string, string> $columns The columns to render, keys are labels
|
||||
*/
|
||||
public function __construct(iterable $data, array $columns)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->columns = array_flip($columns);
|
||||
|
||||
$this->addAttributes($this->baseAttributes);
|
||||
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the item table
|
||||
*
|
||||
* If you want to adjust the item table after construction, override this method.
|
||||
*/
|
||||
protected function init()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the columns being rendered
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getColumns(): array
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sort rules (as returned by {@see SortControl::getSort()})
|
||||
*
|
||||
* @param ?string $sort
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSort(?string $sort): self
|
||||
{
|
||||
$this->sort = $sort;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
abstract protected function getItemClass(): string;
|
||||
|
||||
abstract protected function getVisualColumn(): string;
|
||||
|
||||
abstract protected function getVisualLabel();
|
||||
|
||||
protected function assembleColumnHeader(BaseHtmlElement $header, string $name, $label): void
|
||||
{
|
||||
$sortRules = [];
|
||||
if ($this->sort !== null) {
|
||||
$sortRules = SortUtil::createOrderBy($this->sort);
|
||||
}
|
||||
|
||||
$active = false;
|
||||
$sortDirection = null;
|
||||
foreach ($sortRules as $rule) {
|
||||
if ($rule[0] === $name) {
|
||||
$sortDirection = $rule[1];
|
||||
$active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($sortDirection === 'desc') {
|
||||
$value = "$name asc";
|
||||
} else {
|
||||
$value = "$name desc";
|
||||
}
|
||||
|
||||
$icon = 'sort';
|
||||
if ($active) {
|
||||
$icon = $sortDirection === 'desc' ? 'sort-up' : 'sort-down';
|
||||
}
|
||||
|
||||
$form = new Form();
|
||||
$form->setAttribute('method', 'GET');
|
||||
|
||||
$button = $form->createElement('button', 'sort', [
|
||||
'value' => $value,
|
||||
'type' => 'submit',
|
||||
'title' => is_string($label) ? $label : null,
|
||||
'class' => $active ? 'active' : null
|
||||
]);
|
||||
$button->addHtml(
|
||||
Html::tag(
|
||||
'span',
|
||||
null,
|
||||
// With to have the height sized the same as the others
|
||||
$label ?? HtmlString::create(' ')
|
||||
),
|
||||
new Icon($icon)
|
||||
);
|
||||
$form->addElement($button);
|
||||
|
||||
$header->add($form);
|
||||
}
|
||||
|
||||
protected function assemble()
|
||||
{
|
||||
$itemClass = $this->getItemClass();
|
||||
|
||||
$headerRow = new HtmlElement('tr');
|
||||
|
||||
$visualCell = new HtmlElement('th', Attributes::create(['class' => 'has-visual']));
|
||||
$this->assembleColumnHeader($visualCell, $this->getVisualColumn(), $this->getVisualLabel());
|
||||
$headerRow->addHtml($visualCell);
|
||||
|
||||
foreach ($this->columns as $name => $label) {
|
||||
$headerCell = new HtmlElement('th');
|
||||
$this->assembleColumnHeader($headerCell, $name, is_int($label) ? $name : $label);
|
||||
$headerRow->addHtml($headerCell);
|
||||
}
|
||||
|
||||
$this->addHtml(new HtmlElement('thead', null, $headerRow));
|
||||
|
||||
$body = new HtmlElement('tbody', Attributes::create(['data-base-target' => '_next']));
|
||||
foreach ($this->data as $item) {
|
||||
$body->addHtml(new $itemClass($item, $this));
|
||||
}
|
||||
|
||||
if ($body->isEmpty()) {
|
||||
$body->addHtml(new HtmlElement(
|
||||
'tr',
|
||||
null,
|
||||
new HtmlElement(
|
||||
'td',
|
||||
Attributes::create(['colspan' => count($this->columns)]),
|
||||
new EmptyState(t('No items found.'))
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
$this->addHtml($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich the given list of column names with appropriate labels
|
||||
*
|
||||
* @param Query $query
|
||||
* @param array $columns
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function applyColumnMetaData(Query $query, array $columns): array
|
||||
{
|
||||
$newColumns = [];
|
||||
foreach ($columns as $columnPath) {
|
||||
$label = $query->getResolver()->getColumnDefinition($columnPath)->getLabel();
|
||||
$newColumns[$label ?? $columnPath] = $columnPath;
|
||||
}
|
||||
|
||||
return $newColumns;
|
||||
}
|
||||
}
|
||||
106
library/Icingadb/Widget/ItemTable/BaseRowItem.php
Normal file
106
library/Icingadb/Widget/ItemTable/BaseRowItem.php
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
|
||||
|
||||
namespace Icinga\Module\Icingadb\Widget\ItemTable;
|
||||
|
||||
use ipl\Html\Attributes;
|
||||
use ipl\Html\BaseHtmlElement;
|
||||
use ipl\Html\HtmlElement;
|
||||
use ipl\Orm\Model;
|
||||
|
||||
abstract class BaseRowItem extends BaseHtmlElement
|
||||
{
|
||||
protected $defaultAttributes = ['class' => 'row-item'];
|
||||
|
||||
/** @var Model */
|
||||
protected $item;
|
||||
|
||||
/** @var BaseItemTable */
|
||||
protected $list;
|
||||
|
||||
protected $tag = 'tr';
|
||||
|
||||
/**
|
||||
* Create a new row item
|
||||
*
|
||||
* @param Model $item
|
||||
* @param BaseItemTable $list
|
||||
*/
|
||||
public function __construct(Model $item, BaseItemTable $list)
|
||||
{
|
||||
$this->item = $item;
|
||||
$this->list = $list;
|
||||
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the row item
|
||||
*
|
||||
* If you want to adjust the row item after construction, override this method.
|
||||
*/
|
||||
protected function init()
|
||||
{
|
||||
}
|
||||
|
||||
abstract protected function assembleVisual(BaseHtmlElement $visual);
|
||||
|
||||
abstract protected function assembleCell(BaseHtmlElement $cell, string $path, $value);
|
||||
|
||||
protected function createVisual(): BaseHtmlElement
|
||||
{
|
||||
$visual = new HtmlElement('td', Attributes::create(['class' => 'visual']));
|
||||
|
||||
$this->assembleVisual($visual);
|
||||
|
||||
return $visual;
|
||||
}
|
||||
|
||||
protected function assemble()
|
||||
{
|
||||
$this->addHtml($this->createVisual());
|
||||
|
||||
foreach ($this->list->getColumns() as $columnPath => $_) {
|
||||
$steps = explode('.', $columnPath);
|
||||
if ($steps[0] === $this->item->getTableName()) {
|
||||
array_shift($steps);
|
||||
$columnPath = implode('.', $steps);
|
||||
}
|
||||
|
||||
$column = null;
|
||||
$subject = $this->item;
|
||||
foreach ($steps as $i => $step) {
|
||||
if (isset($subject->$step)) {
|
||||
if ($subject->$step instanceof Model) {
|
||||
$subject = $subject->$step;
|
||||
} else {
|
||||
$column = $step;
|
||||
}
|
||||
} else {
|
||||
$columnCandidate = implode('.', array_slice($steps, $i));
|
||||
if (isset($subject->$columnCandidate)) {
|
||||
$column = $columnCandidate;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$value = null;
|
||||
if ($column !== null) {
|
||||
$value = $subject->$column;
|
||||
if (is_array($value)) {
|
||||
$value = empty($value) ? null : implode(',', $value);
|
||||
}
|
||||
}
|
||||
|
||||
$cell = new HtmlElement('td');
|
||||
if ($value !== null) {
|
||||
$this->assembleCell($cell, $columnPath, $value);
|
||||
}
|
||||
|
||||
$this->addHtml($cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
& > .content.full-height {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.plugin-output {
|
||||
.monospace();
|
||||
word-break: break-word;
|
||||
|
|
@ -79,6 +84,7 @@ div.show-more {
|
|||
.box-shadow(0, 0, 0, 1px, @gray-lighter);
|
||||
flex-shrink: 0;
|
||||
position: relative; // Required for the host meta info control
|
||||
z-index: 1; // The content may clip, this ensures the separator is always visible
|
||||
|
||||
> :not(:only-child) {
|
||||
margin-bottom: .5em;
|
||||
|
|
|
|||
|
|
@ -3,15 +3,62 @@
|
|||
.item-table {
|
||||
padding: 0;
|
||||
|
||||
> .empty-state {
|
||||
thead {
|
||||
th {
|
||||
font-weight: normal;
|
||||
|
||||
// Border styles start
|
||||
form {
|
||||
padding: 0 0 0 1px;
|
||||
border-bottom: 1px solid @gray-light;
|
||||
background: linear-gradient(to top, @gray-light, @body-bg-color);
|
||||
|
||||
button {
|
||||
background: @body-bg-color;
|
||||
}
|
||||
}
|
||||
&:first-child form {
|
||||
padding-left: 0;
|
||||
}
|
||||
// Border styles end
|
||||
}
|
||||
|
||||
button {
|
||||
.appearance(none);
|
||||
border: none;
|
||||
background: none;
|
||||
padding: .1em .5em;
|
||||
|
||||
text-align: left;
|
||||
color: @text-color-light;
|
||||
|
||||
> .icon {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
transition: opacity .25s linear, width .25s ease;
|
||||
}
|
||||
&:hover .icon,
|
||||
&:focus .icon,
|
||||
&.active .icon {
|
||||
opacity: 1;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .empty-state,
|
||||
> tbody > tr:first-child .empty-state {
|
||||
.rounded-corners();
|
||||
background-color: @gray-lightest;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
&:not(:last-child) > *:not(.visual) {
|
||||
border-bottom: 1px solid @gray-light;
|
||||
}
|
||||
.list-item:not(:last-child) > *:not(.visual),
|
||||
.row-item:not(:last-child) {
|
||||
border-bottom: 1px solid @gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -25,11 +72,46 @@
|
|||
|
||||
// Layout
|
||||
|
||||
table.item-table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.item-table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
th {
|
||||
// That's layout, yes, controls overflow when scrolling
|
||||
padding: 1em 0 0 0;
|
||||
background: @body-bg-color;
|
||||
}
|
||||
|
||||
th button {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
span {
|
||||
.text-ellipsis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
th.has-visual {
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
.text-ellipsis();
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: table-row;
|
||||
}
|
||||
|
|
@ -66,7 +148,8 @@
|
|||
padding: .5em 1em 0 0;
|
||||
}
|
||||
|
||||
> .empty-state {
|
||||
> .empty-state,
|
||||
> tbody > tr:first-child .empty-state {
|
||||
margin: 0 1em;
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
|
|
@ -81,3 +164,13 @@
|
|||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
#layout.twocols table.item-table {
|
||||
> thead > tr > th,
|
||||
> tbody > tr > td {
|
||||
&:nth-child(n+6) {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue