diff options
Diffstat (limited to 'library/Reporting/Web')
-rw-r--r-- | library/Reporting/Web/Controller.php | 20 | ||||
-rw-r--r-- | library/Reporting/Web/Flatpickr.php | 77 | ||||
-rw-r--r-- | library/Reporting/Web/Forms/DecoratedElement.php | 17 | ||||
-rw-r--r-- | library/Reporting/Web/Forms/Decorator/CompatDecorator.php | 63 | ||||
-rw-r--r-- | library/Reporting/Web/Forms/ReportForm.php | 168 | ||||
-rw-r--r-- | library/Reporting/Web/Forms/ScheduleForm.php | 177 | ||||
-rw-r--r-- | library/Reporting/Web/Forms/SendForm.php | 47 | ||||
-rw-r--r-- | library/Reporting/Web/Forms/TemplateForm.php | 284 | ||||
-rw-r--r-- | library/Reporting/Web/Forms/TimeframeForm.php | 106 | ||||
-rw-r--r-- | library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php | 37 | ||||
-rw-r--r-- | library/Reporting/Web/Widget/CompatDropdown.php | 22 | ||||
-rw-r--r-- | library/Reporting/Web/Widget/CoverPage.php | 181 | ||||
-rw-r--r-- | library/Reporting/Web/Widget/HeaderOrFooter.php | 95 | ||||
-rw-r--r-- | library/Reporting/Web/Widget/Template.php | 183 |
14 files changed, 1477 insertions, 0 deletions
diff --git a/library/Reporting/Web/Controller.php b/library/Reporting/Web/Controller.php new file mode 100644 index 0000000..5040183 --- /dev/null +++ b/library/Reporting/Web/Controller.php @@ -0,0 +1,20 @@ +<?php +// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web; + +use ipl\Html\Form; +use ipl\Web\Compat\CompatController; + +class Controller extends CompatController +{ + protected function redirectForm(Form $form, $url) + { + if ($form->hasBeenSubmitted() + && ((isset($form->valid) && $form->valid === true) + || $form->isValid()) + ) { + $this->redirectNow($url); + } + } +} diff --git a/library/Reporting/Web/Flatpickr.php b/library/Reporting/Web/Flatpickr.php new file mode 100644 index 0000000..5f6605d --- /dev/null +++ b/library/Reporting/Web/Flatpickr.php @@ -0,0 +1,77 @@ +<?php +// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web; + +use Icinga\Application\Version; +use ipl\Html\Html; +use ipl\Web\Compat\CompatDecorator; + +class Flatpickr extends CompatDecorator +{ + protected $allowInput = true; + + /** + * Set whether to allow manual input + * + * @param bool $state + * + * @return $this + */ + public function setAllowInput(bool $state): self + { + $this->allowInput = $state; + + return $this; + } + + protected function assembleElement() + { + if (version_compare(Version::VERSION, '2.9.0', '>=')) { + $element = parent::assembleElement(); + } else { + $element = $this->formElement; + } + + if (version_compare(Version::VERSION, '2.10.0', '<')) { + $element->getAttributes()->set('data-use-flatpickr-fallback', true); + } else { + $element->getAttributes()->set('data-use-datetime-picker', true); + } + + if (! $this->allowInput) { + return $element; + } + + $element->getAttributes() + ->set('data-input', true) + ->set('data-flatpickr-wrap', true) + ->set('data-flatpickr-allow-input', true) + ->set('data-flatpickr-click-opens', 'false'); + + return [ + $element, + Html::tag('button', ['type' => 'button', 'class' => 'icon-calendar', 'data-toggle' => true]), + Html::tag('button', ['type' => 'button', 'class' => 'icon-cancel', 'data-clear' => true]) + ]; + } + + protected function assemble() + { + if (version_compare(Version::VERSION, '2.9.0', '>=')) { + parent::assemble(); + return; + } + + if ($this->formElement->hasBeenValidated() && ! $this->formElement->isValid()) { + $this->getAttributes()->add('class', 'has-error'); + } + + $this->add(array_filter([ + $this->assembleLabel(), + $this->assembleElement(), + $this->assembleDescription(), + $this->assembleErrors() + ])); + } +} diff --git a/library/Reporting/Web/Forms/DecoratedElement.php b/library/Reporting/Web/Forms/DecoratedElement.php new file mode 100644 index 0000000..2578681 --- /dev/null +++ b/library/Reporting/Web/Forms/DecoratedElement.php @@ -0,0 +1,17 @@ +<?php +// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web\Forms; + +use ipl\Html\Contract\FormElementDecorator; + +trait DecoratedElement +{ + protected function addDecoratedElement(FormElementDecorator $decorator, $type, $name, array $attributes) + { + $element = $this->createElement($type, $name, $attributes); + $decorator->decorate($element); + $this->registerElement($element); + $this->add($element); + } +} diff --git a/library/Reporting/Web/Forms/Decorator/CompatDecorator.php b/library/Reporting/Web/Forms/Decorator/CompatDecorator.php new file mode 100644 index 0000000..b2eb536 --- /dev/null +++ b/library/Reporting/Web/Forms/Decorator/CompatDecorator.php @@ -0,0 +1,63 @@ +<?php +// Icinga Reporting | (c) 2021 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web\Forms\Decorator; + +use Icinga\Application\Version; +use ipl\Html\Attributes; +use ipl\Html\FormElement\CheckboxElement; +use ipl\Html\HtmlElement; + +class CompatDecorator extends \ipl\Web\Compat\CompatDecorator +{ + protected function createCheckboxCompat(CheckboxElement $checkbox) + { + if (! $checkbox->getAttributes()->has('id')) { + $checkbox->setAttribute('id', base64_encode(random_bytes(8))); + } + + $checkbox->getAttributes()->add('class', 'sr-only'); + + $classes = ['toggle-switch']; + if ($checkbox->getAttributes()->get('disabled')->getValue()) { + $classes[] = 'disabled'; + } + + return [ + $checkbox, + new HtmlElement('label', Attributes::create([ + 'class' => $classes, + 'aria-hidden' => 'true', + 'for' => $checkbox->getAttributes()->get('id')->getValue() + ]), new HtmlElement('span', Attributes::create(['class' => 'toggle-slider']))) + ]; + } + + protected function assembleElementCompat() + { + if ($this->formElement instanceof CheckboxElement) { + return $this->createCheckboxCompat($this->formElement); + } + + return $this->formElement; + } + + protected function assemble() + { + if (version_compare(Version::VERSION, '2.9.0', '>=')) { + parent::assemble(); + return; + } + + if ($this->formElement->hasBeenValidated() && ! $this->formElement->isValid()) { + $this->getAttributes()->add('class', 'has-error'); + } + + $this->add(array_filter([ + $this->assembleLabel(), + $this->assembleElementCompat(), + $this->assembleDescription(), + $this->assembleErrors() + ])); + } +} diff --git a/library/Reporting/Web/Forms/ReportForm.php b/library/Reporting/Web/Forms/ReportForm.php new file mode 100644 index 0000000..6b1e692 --- /dev/null +++ b/library/Reporting/Web/Forms/ReportForm.php @@ -0,0 +1,168 @@ +<?php +// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web\Forms; + +use Icinga\Authentication\Auth; +use Icinga\Module\Reporting\Database; +use Icinga\Module\Reporting\ProvidedReports; +use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator; +use ipl\Html\Contract\FormSubmitElement; +use ipl\Html\Form; +use ipl\Web\Compat\CompatForm; + +class ReportForm extends CompatForm +{ + use Database; + use ProvidedReports; + + /** @var bool Hack to disable the {@link onSuccess()} code upon deletion of the report */ + protected $callOnSuccess; + + protected $id; + + public function setId($id) + { + $this->id = $id; + + return $this; + } + + protected function assemble() + { + $this->setDefaultElementDecorator(new CompatDecorator()); + + $this->addElement('text', 'name', [ + 'required' => true, + 'label' => 'Name' + ]); + + $this->addElement('select', 'timeframe', [ + 'required' => true, + 'label' => 'Timeframe', + 'options' => [null => 'Please choose'] + $this->listTimeframes(), + 'class' => 'autosubmit' + ]); + + $this->addElement('select', 'template', [ + 'label' => 'Template', + 'options' => [null => 'Please choose'] + $this->listTemplates() + ]); + + $this->addElement('select', 'reportlet', [ + 'required' => true, + 'label' => 'Report', + 'options' => [null => 'Please choose'] + $this->listReports(), + 'class' => 'autosubmit' + ]); + + $values = $this->getValues(); + + if (isset($values['reportlet'])) { + $config = new Form(); +// $config->populate($this->getValues()); + + /** @var \Icinga\Module\Reporting\Hook\ReportHook $reportlet */ + $reportlet = new $values['reportlet']; + + $reportlet->initConfigForm($config); + + foreach ($config->getElements() as $element) { + $this->addElement($element); + } + } + + $this->addElement('submit', 'submit', [ + 'label' => $this->id === null ? 'Create Report' : 'Update Report' + ]); + + if ($this->id !== null) { + /** @var FormSubmitElement $removeButton */ + $removeButton = $this->createElement('submit', 'remove', [ + 'label' => 'Remove Report', + 'class' => 'btn-remove', + 'formnovalidate' => true + ]); + $this->registerElement($removeButton); + $this->getElement('submit')->getWrapper()->prepend($removeButton); + + if ($removeButton->hasBeenPressed()) { + $this->getDb()->delete('report', ['id = ?' => $this->id]); + + // Stupid cheat because ipl/html is not capable of multiple submit buttons + $this->getSubmitButton()->setValue($this->getSubmitButton()->getButtonLabel()); + $this->callOnSuccess = false; + $this->valid = true; + + return; + } + } + } + + public function onSuccess() + { + if ($this->callOnSuccess === false) { + return; + } + + $db = $this->getDb(); + + $values = $this->getValues(); + + $now = time() * 1000; + + $db->beginTransaction(); + + if ($this->id === null) { + $db->insert('report', [ + 'name' => $values['name'], + 'author' => Auth::getInstance()->getUser()->getUsername(), + 'timeframe_id' => $values['timeframe'], + 'template_id' => $values['template'], + 'ctime' => $now, + 'mtime' => $now + ]); + + $reportId = $db->lastInsertId(); + } else { + $db->update('report', [ + 'name' => $values['name'], + 'timeframe_id' => $values['timeframe'], + 'template_id' => $values['template'], + 'mtime' => $now + ], ['id = ?' => $this->id]); + + $reportId = $this->id; + } + + unset($values['name']); + unset($values['timeframe']); + + if ($this->id !== null) { + $db->delete('reportlet', ['report_id = ?' => $reportId]); + } + + $db->insert('reportlet', [ + 'report_id' => $reportId, + 'class' => $values['reportlet'], + 'ctime' => $now, + 'mtime' => $now + ]); + + $reportletId = $db->lastInsertId(); + + unset($values['reportlet']); + + foreach ($values as $name => $value) { + $db->insert('config', [ + 'reportlet_id' => $reportletId, + 'name' => $name, + 'value' => $value, + 'ctime' => $now, + 'mtime' => $now + ]); + } + + $db->commitTransaction(); + } +} diff --git a/library/Reporting/Web/Forms/ScheduleForm.php b/library/Reporting/Web/Forms/ScheduleForm.php new file mode 100644 index 0000000..47f3ee3 --- /dev/null +++ b/library/Reporting/Web/Forms/ScheduleForm.php @@ -0,0 +1,177 @@ +<?php +// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web\Forms; + +use DateTime; +use Icinga\Application\Version; +use Icinga\Authentication\Auth; +use Icinga\Module\Reporting\Database; +use Icinga\Module\Reporting\ProvidedActions; +use Icinga\Module\Reporting\Report; +use Icinga\Module\Reporting\Web\Flatpickr; +use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator; +use ipl\Html\Contract\FormSubmitElement; +use ipl\Html\Form; +use ipl\Web\Compat\CompatForm; + +class ScheduleForm extends CompatForm +{ + use Database; + use DecoratedElement; + use ProvidedActions; + + /** @var Report */ + protected $report; + + protected $id; + + public function setReport(Report $report) + { + $this->report = $report; + + $schedule = $report->getSchedule(); + + if ($schedule !== null) { + $this->setId($schedule->getId()); + + $values = [ + 'start' => $schedule->getStart()->format('Y-m-d\\TH:i:s'), + 'frequency' => $schedule->getFrequency(), + 'action' => $schedule->getAction() + ] + $schedule->getConfig(); + + $this->populate($values); + } + + return $this; + } + + public function setId($id) + { + $this->id = $id; + + return $this; + } + + protected function assemble() + { + $this->setDefaultElementDecorator(new CompatDecorator()); + + $frequency = [ + 'minutely' => 'Minutely', + 'hourly' => 'Hourly', + 'daily' => 'Daily', + 'weekly' => 'Weekly', + 'monthly' => 'Monthly' + ]; + + if (version_compare(Version::VERSION, '2.9.0', '>=')) { + $this->addElement('localDateTime', 'start', [ + 'required' => true, + 'label' => t('Start'), + 'placeholder' => t('Choose date and time') + ]); + } else { + $this->addDecoratedElement((new Flatpickr())->setAllowInput(false), 'text', 'start', [ + 'required' => true, + 'label' => t('Start'), + 'placeholder' => t('Choose date and time') + ]); + } + + $this->addElement('select', 'frequency', [ + 'required' => true, + 'label' => 'Frequency', + 'options' => [null => 'Please choose'] + $frequency, + ]); + + $this->addElement('select', 'action', [ + 'required' => true, + 'label' => 'Action', + 'options' => [null => 'Please choose'] + $this->listActions(), + 'class' => 'autosubmit' + ]); + + $values = $this->getValues(); + + if (isset($values['action'])) { + $config = new Form(); +// $config->populate($this->getValues()); + + /** @var \Icinga\Module\Reporting\Hook\ActionHook $action */ + $action = new $values['action']; + + $action->initConfigForm($config, $this->report); + + foreach ($config->getElements() as $element) { + $this->addElement($element); + } + } + + $this->addElement('submit', 'submit', [ + 'label' => $this->id === null ? 'Create Schedule' : 'Update Schedule' + ]); + + if ($this->id !== null) { + /** @var FormSubmitElement $removeButton */ + $removeButton = $this->createElement('submit', 'remove', [ + 'label' => 'Remove Schedule', + 'class' => 'btn-remove', + 'formnovalidate' => true + ]); + $this->registerElement($removeButton); + $this->getElement('submit')->getWrapper()->prepend($removeButton); + + if ($removeButton->hasBeenPressed()) { + $this->getDb()->delete('schedule', ['id = ?' => $this->id]); + + // Stupid cheat because ipl/html is not capable of multiple submit buttons + $this->getSubmitButton()->setValue($this->getSubmitButton()->getButtonLabel()); + $this->valid = true; + + return; + } + } + } + + public function onSuccess() + { + $db = $this->getDb(); + + $values = $this->getValues(); + + $now = time() * 1000; + + if (! $values['start'] instanceof DateTime) { + $values['start'] = DateTime::createFromFormat('Y-m-d H:i:s', $values['start']); + } + + $data = [ + 'start' => $values['start']->getTimestamp() * 1000, + 'frequency' => $values['frequency'], + 'action' => $values['action'], + 'mtime' => $now + ]; + + unset($values['start']); + unset($values['frequency']); + unset($values['action']); + + $data['config'] = json_encode($values); + + $db->beginTransaction(); + + if ($this->id === null) { + $db->insert('schedule', $data + [ + 'author' => Auth::getInstance()->getUser()->getUsername(), + 'report_id' => $this->report->getId(), + 'ctime' => $now + ]); + } else { + $db->update('schedule', $data, ['id = ?' => $this->id]); + } + + $db->commitTransaction(); + } +} diff --git a/library/Reporting/Web/Forms/SendForm.php b/library/Reporting/Web/Forms/SendForm.php new file mode 100644 index 0000000..03b691c --- /dev/null +++ b/library/Reporting/Web/Forms/SendForm.php @@ -0,0 +1,47 @@ +<?php +// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web\Forms; + +use Icinga\Module\Reporting\Actions\SendMail; +use Icinga\Module\Reporting\Database; +use Icinga\Module\Reporting\ProvidedReports; +use Icinga\Module\Reporting\Report; +use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator; +use ipl\Web\Compat\CompatForm; + +class SendForm extends CompatForm +{ + use Database; + use ProvidedReports; + + /** @var Report */ + protected $report; + + public function setReport(Report $report) + { + $this->report = $report; + + return $this; + } + + protected function assemble() + { + $this->setDefaultElementDecorator(new CompatDecorator()); + + (new SendMail())->initConfigForm($this, $this->report); + + $this->addElement('submit', 'submit', [ + 'label' => 'Send Report' + ]); + } + + public function onSuccess() + { + $values = $this->getValues(); + + $sendMail = new SendMail(); + + $sendMail->execute($this->report, $values); + } +} diff --git a/library/Reporting/Web/Forms/TemplateForm.php b/library/Reporting/Web/Forms/TemplateForm.php new file mode 100644 index 0000000..bb062bb --- /dev/null +++ b/library/Reporting/Web/Forms/TemplateForm.php @@ -0,0 +1,284 @@ +<?php +// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web\Forms; + +use Icinga\Authentication\Auth; +use Icinga\Module\Reporting\Database; +use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator; +use ipl\Html\Contract\FormSubmitElement; +use ipl\Html\Html; +use ipl\Web\Compat\CompatForm; +use reportingipl\Html\FormElement\FileElement; + +class TemplateForm extends CompatForm +{ + use Database; + + /** @var bool Hack to disable the {@link onSuccess()} code upon deletion of the template */ + protected $callOnSuccess; + + protected $template; + + public function getTemplate() + { + return $this->template; + } + + public function setTemplate($template) + { + $this->template = $template; + + if ($template->settings) { + $this->populate(array_filter($template->settings, function ($value) { + // Don't populate files + return ! is_array($value); + })); + } + + return $this; + } + + protected function assemble() + { + $this->setDefaultElementDecorator(new CompatDecorator()); + + $this->setAttribute('enctype', 'multipart/form-data'); + + $this->add(Html::tag('h2', 'Template Settings')); + + $this->addElement('text', 'name', [ + 'label' => 'Name', + 'placeholder' => 'Template name', + 'required' => true + ]); + + $this->add(Html::tag('h2', 'Cover Page Settings')); + + $this->addElement(new FileElement('cover_page_background_image', [ + 'label' => 'Background Image', + 'accept' => 'image/png, image/jpeg' + ])); + + if ($this->template !== null + && isset($this->template->settings['cover_page_background_image']) + ) { + $this->add(Html::tag( + 'p', + ['style' => ['margin-left: 14em;']], + 'Upload a new background image to override the existing one' + )); + + $this->addElement('checkbox', 'remove_cover_page_background_image', [ + 'label' => 'Remove background image' + ]); + } + + $this->addElement(new FileElement('cover_page_logo', [ + 'label' => 'Logo', + 'accept' => 'image/png, image/jpeg' + ])); + + if ($this->template !== null + && isset($this->template->settings['cover_page_logo']) + ) { + $this->add(Html::tag( + 'p', + ['style' => ['margin-left: 14em;']], + 'Upload a new logo to override the existing one' + )); + + $this->addElement('checkbox', 'remove_cover_page_logo', [ + 'label' => 'Remove Logo' + ]); + } + + $this->addElement('textarea', 'title', [ + 'label' => 'Title', + 'placeholder' => 'Report title' + ]); + + $this->addElement('text', 'color', [ + 'label' => 'Color', + 'placeholder' => 'CSS color code' + ]); + + $this->add(Html::tag('h2', 'Header Settings')); + + $this->addColumnSettings('header_column1', 'Column 1'); + $this->addColumnSettings('header_column2', 'Column 2'); + $this->addColumnSettings('header_column3', 'Column 3'); + + $this->add(Html::tag('h2', 'Footer Settings')); + + $this->addColumnSettings('footer_column1', 'Column 1'); + $this->addColumnSettings('footer_column2', 'Column 2'); + $this->addColumnSettings('footer_column3', 'Column 3'); + + $this->addElement('submit', 'submit', [ + 'label' => $this->template === null ? 'Create Template' : 'Update Template' + ]); + + if ($this->template !== null) { + /** @var FormSubmitElement $removeButton */ + $removeButton = $this->createElement('submit', 'remove', [ + 'label' => 'Remove Template', + 'class' => 'btn-remove', + 'formnovalidate' => true + ]); + $this->registerElement($removeButton); + $this->getElement('submit')->getWrapper()->prepend($removeButton); + + if ($removeButton->hasBeenPressed()) { + $this->getDb()->delete('template', ['id = ?' => $this->template->id]); + + // Stupid cheat because ipl/html is not capable of multiple submit buttons + $this->getSubmitButton()->setValue($this->getSubmitButton()->getButtonLabel()); + $this->callOnSuccess = false; + $this->valid = true; + + return; + } + } + } + + public function onSuccess() + { + if ($this->callOnSuccess === false) { + return; + } + + ini_set('upload_max_filesize', '10M'); + + $settings = $this->getValues(); + + try { + /** @var $uploadedFile \GuzzleHttp\Psr7\UploadedFile */ + foreach ($this->getRequest()->getUploadedFiles() as $name => $uploadedFile) { + if ($uploadedFile->getError() === UPLOAD_ERR_NO_FILE) { + continue; + } + + $settings[$name] = [ + 'mime_type' => $uploadedFile->getClientMediaType(), + 'size' => $uploadedFile->getSize(), + 'content' => base64_encode((string) $uploadedFile->getStream()) + ]; + } + + $db = $this->getDb(); + + $now = time() * 1000; + + if ($this->template === null) { + $db->insert('template', [ + 'name' => $settings['name'], + 'author' => Auth::getInstance()->getUser()->getUsername(), + 'settings' => json_encode($settings), + 'ctime' => $now, + 'mtime' => $now + ]); + } else { + if (isset($settings['remove_cover_page_background_image'])) { + unset($settings['cover_page_background_image']); + unset($settings['remove_cover_page_background_image']); + } elseif (! isset($settings['cover_page_background_image']) + && isset($this->template->settings['cover_page_background_image']) + ) { + $settings['cover_page_background_image'] = $this->template->settings['cover_page_background_image']; + } + + if (isset($settings['remove_cover_page_logo'])) { + unset($settings['cover_page_logo']); + unset($settings['remove_cover_page_logo']); + } elseif (! isset($settings['cover_page_logo']) + && isset($this->template->settings['cover_page_logo']) + ) { + $settings['cover_page_logo'] = $this->template->settings['cover_page_logo']; + } + + foreach (['header', 'footer'] as $headerOrFooter) { + for ($i = 1; $i <= 3; ++$i) { + $type = "{$headerOrFooter}_column{$i}_type"; + + if ($settings[$type] === 'image') { + $value = "{$headerOrFooter}_column{$i}_value"; + + if (! isset($settings[$value]) + && isset($this->template->settings[$value]) + ) { + $settings[$value] = $this->template->settings[$value]; + } + } + } + } + + $db->update('template', [ + 'name' => $settings['name'], + 'settings' => json_encode($settings), + 'mtime' => $now + ], ['id = ?' => $this->template->id]); + } + } catch (\Exception $e) { + die($e->getMessage()); + } + } + + protected function addColumnSettings($name, $label) + { + $type = "{$name}_type"; + $value = "{$name}_value"; + + $this->addElement('select', $type, [ + 'class' => 'autosubmit', + 'label' => $label, + 'options' => [ + null => 'None', + 'text' => 'Text', + 'image' => 'Image', + 'variable' => 'Variable' + ] + ]); + + switch ($this->getValue($type, 'none')) { + case 'image': + $this->addElement(new FileElement($value, [ + 'label' => 'Image', + 'accept' => 'image/png, image/jpeg' + ])); + + if ($this->template !== null + && $this->template->settings[$type] === 'image' + && isset($this->template->settings[$value]) + ) { + $this->add(Html::tag( + 'p', + ['style' => ['margin-left: 14em;']], + 'Upload a new image to override the existing one' + )); + } + break; + case 'variable': + $this->addElement('select', $value, [ + 'label' => 'Variable', + 'options' => [ + 'report_title' => 'Report Title', + 'time_frame' => 'Time Frame', + 'time_frame_absolute' => 'Time Frame (absolute)', + 'page_number' => 'Page Number', + 'total_number_of_pages' => 'Total Number of Pages', + 'page_of' => 'Page Number + Total Number of Pages', + 'date' => 'Date' + ], + 'value' => 'report_title' + ]); + break; + case 'text': + $this->addElement('text', $value, [ + 'label' => 'Text', + 'placeholder' => 'Column text' + ]); + break; + } + } +} diff --git a/library/Reporting/Web/Forms/TimeframeForm.php b/library/Reporting/Web/Forms/TimeframeForm.php new file mode 100644 index 0000000..3d78709 --- /dev/null +++ b/library/Reporting/Web/Forms/TimeframeForm.php @@ -0,0 +1,106 @@ +<?php +// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web\Forms; + +use Icinga\Module\Reporting\Database; +use Icinga\Module\Reporting\Web\Flatpickr; +use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator; +use ipl\Html\Contract\FormSubmitElement; +use ipl\Web\Compat\CompatForm; + +class TimeframeForm extends CompatForm +{ + use Database; + use DecoratedElement; + + protected $id; + + public function setId($id) + { + $this->id = $id; + + return $this; + } + + protected function assemble() + { + $this->setDefaultElementDecorator(new CompatDecorator()); + + $this->addElement('text', 'name', [ + 'required' => true, + 'label' => 'Name' + ]); + + $flatpickr = new Flatpickr(); + + $this->addDecoratedElement($flatpickr, 'text', 'start', [ + 'required' => true, + 'label' => 'Start', + 'placeholder' => 'Select a start date or provide a textual datetime description', + 'data-flatpickr-default-hour' => '00' + ]); + + $this->addDecoratedElement($flatpickr, 'text', 'end', [ + 'required' => true, + 'label' => 'End', + 'placeholder' => 'Select a end date or provide a textual datetime description', + 'data-flatpickrDefaultHour' => '23', + 'data-flatpickrDefaultMinute' => '59', + 'data-flatpickrDefaultSeconds' => '59' + ]); + + $this->addElement('submit', 'submit', [ + 'label' => $this->id === null ? 'Create Time Frame' : 'Update Time Frame' + ]); + + if ($this->id !== null) { + /** @var FormSubmitElement $removeButton */ + $removeButton = $this->createElement('submit', 'remove', [ + 'label' => 'Remove Time Frame', + 'class' => 'btn-remove', + 'formnovalidate' => true + ]); + $this->registerElement($removeButton); + $this->getElement('submit')->getWrapper()->prepend($removeButton); + + if ($removeButton->hasBeenPressed()) { + $this->getDb()->delete('timeframe', ['id = ?' => $this->id]); + + // Stupid cheat because ipl/html is not capable of multiple submit buttons + $this->getSubmitButton()->setValue($this->getSubmitButton()->getButtonLabel()); + $this->valid = true; + + return; + } + } + } + + public function onSuccess() + { + $db = $this->getDb(); + + $values = $this->getValues(); + + $now = time() * 1000; + + $end = $db->quoteIdentifier('end'); + + if ($this->id === null) { + $db->insert('timeframe', [ + 'name' => $values['name'], + 'start' => $values['start'], + $end => $values['end'], + 'ctime' => $now, + 'mtime' => $now + ]); + } else { + $db->update('timeframe', [ + 'name' => $values['name'], + 'start' => $values['start'], + $end => $values['end'], + 'mtime' => $now + ], ['id = ?' => $this->id]); + } + } +} diff --git a/library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php b/library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php new file mode 100644 index 0000000..afb8b14 --- /dev/null +++ b/library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php @@ -0,0 +1,37 @@ +<?php +// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web; + +trait ReportsTimeframesAndTemplatesTabs +{ + /** + * Create tabs + * + * @return \Icinga\Web\Widget\Tabs + */ + protected function createTabs() + { + $tabs = $this->getTabs(); + + $tabs->add('reports', [ + 'title' => $this->translate('Show reports'), + 'label' => $this->translate('Reports'), + 'url' => 'reporting/reports' + ]); + + $tabs->add('timeframes', [ + 'title' => $this->translate('Show time frames'), + 'label' => $this->translate('Time Frames'), + 'url' => 'reporting/timeframes' + ]); + + $tabs->add('templates', [ + 'title' => $this->translate('Show templates'), + 'label' => $this->translate('Templates'), + 'url' => 'reporting/templates' + ]); + + return $tabs; + } +} diff --git a/library/Reporting/Web/Widget/CompatDropdown.php b/library/Reporting/Web/Widget/CompatDropdown.php new file mode 100644 index 0000000..cdd7b40 --- /dev/null +++ b/library/Reporting/Web/Widget/CompatDropdown.php @@ -0,0 +1,22 @@ +<?php +// Icinga Reporting | (c) 2021 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web\Widget; + +use ipl\Web\Widget\ActionLink; +use ipl\Web\Widget\Dropdown; + +class CompatDropdown extends Dropdown +{ + public function addLink($content, $url, $icon = null, array $attributes = null) + { + $link = new ActionLink($content, $url, $icon, ['class' => 'dropdown-item']); + if (! empty($attributes)) { + $link->addAttributes($attributes); + } + + $this->links[] = $link; + + return $this; + } +} diff --git a/library/Reporting/Web/Widget/CoverPage.php b/library/Reporting/Web/Widget/CoverPage.php new file mode 100644 index 0000000..545ef6a --- /dev/null +++ b/library/Reporting/Web/Widget/CoverPage.php @@ -0,0 +1,181 @@ +<?php + +namespace Icinga\Module\Reporting\Web\Widget; + +use Icinga\Module\Reporting\Common\Macros; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +class CoverPage extends BaseHtmlElement +{ + use Macros; + + /** @var array */ + protected $backgroundImage; + + /** @var string */ + protected $color; + + /** @var array */ + protected $logo; + + /** @var string */ + protected $title; + + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'cover-page page']; + + /** + * @return bool + */ + public function hasBackgroundImage() + { + return $this->backgroundImage !== null; + } + + /** + * @return array + */ + public function getBackgroundImage() + { + return $this->backgroundImage; + } + + /** + * @param array $backgroundImage + * + * @return $this + */ + public function setBackgroundImage($backgroundImage) + { + $this->backgroundImage = $backgroundImage; + + return $this; + } + + /** + * @return bool + */ + public function hasColor() + { + return $this->color !== null; + } + + /** + * @return string + */ + public function getColor() + { + return $this->color; + } + + /** + * @param string $color + * + * @return $this + */ + public function setColor($color) + { + $this->color = $color; + + return $this; + } + + /** + * @return bool + */ + public function hasLogo() + { + return $this->logo !== null; + } + + /** + * @return array + */ + public function getLogo() + { + return $this->logo; + } + + /** + * @param array $logo + * + * @return $this + */ + public function setLogo($logo) + { + $this->logo = $logo; + + return $this; + } + + public function hasTitle() + { + return $this->title !== null; + } + + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @param string $title + * + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + protected function assemble() + { + if ($this->hasBackgroundImage()) { + $this + ->getAttributes() + ->add('style', "background-image: url('" . Template::getDataUrl($this->getBackgroundImage()) . "');"); + } + + $content = Html::tag('div', ['class' => 'cover-page-content']); + + if ($this->hasColor()) { + $content->getAttributes()->add('style', "color: {$this->getColor()};"); + } + + if ($this->hasLogo()) { + $content->add(Html::tag( + 'img', + [ + 'class' => 'logo', + 'src' => Template::getDataUrl($this->getLogo()) + ] + )); + } + + if ($this->hasTitle()) { + $title = array_map(function ($part) { + $part = trim($part); + + if (! $part) { + return Html::tag('br'); + } else { + return Html::tag('div', null, $part); + } + }, explode("\n", $this->resolveMacros($this->getTitle()))); + + $content->add(Html::tag( + 'h2', + $title + )); + } + + $this->add($content); + } +} diff --git a/library/Reporting/Web/Widget/HeaderOrFooter.php b/library/Reporting/Web/Widget/HeaderOrFooter.php new file mode 100644 index 0000000..dcb37e7 --- /dev/null +++ b/library/Reporting/Web/Widget/HeaderOrFooter.php @@ -0,0 +1,95 @@ +<?php + +namespace Icinga\Module\Reporting\Web\Widget; + +use Icinga\Module\Reporting\Common\Macros; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; + +class HeaderOrFooter extends HtmlDocument +{ + use Macros; + + const HEADER = 'header'; + + const FOOTER = 'footer'; + + protected $type; + + protected $data; + + protected $tag = 'div'; + + public function __construct($type, array $data) + { + $this->type = $type; + $this->data = $data; + } + + protected function resolveVariable($variable) + { + switch ($variable) { + case 'report_title': + $resolved = Html::tag('span', ['class' => 'title']); + break; + case 'time_frame': + $resolved = Html::tag('p', $this->getMacro('time_frame')); + break; + case 'time_frame_absolute': + $resolved = Html::tag('p', $this->getMacro('time_frame_absolute')); + break; + case 'page_number': + $resolved = Html::tag('span', ['class' => 'pageNumber']); + break; + case 'total_number_of_pages': + $resolved = Html::tag('span', ['class' => 'totalPages']); + break; + case 'page_of': + $resolved = Html::tag('p', Html::sprintf( + '%s / %s', + Html::tag('span', ['class' => 'pageNumber']), + Html::tag('span', ['class' => 'totalPages']) + )); + break; + case 'date': + $resolved = Html::tag('span', ['class' => 'date']); + break; + default: + $resolved = $variable; + break; + } + + return $resolved; + } + + protected function createColumn(array $data, $key) + { + $typeKey = "${key}_type"; + $valueKey = "${key}_value"; + $type = isset($data[$typeKey]) ? $data[$typeKey] : null; + + switch ($type) { + case 'text': + $column = Html::tag('p', $data[$valueKey]); + break; + case 'image': + $column = Html::tag('img', ['height' => 13, 'src' => Template::getDataUrl($data[$valueKey])]); + break; + case 'variable': + $column = $this->resolveVariable($data[$valueKey]); + break; + default: + $column = Html::tag('div'); + break; + } + + return $column; + } + + protected function assemble() + { + for ($i = 1; $i <= 3; ++$i) { + $this->add($this->createColumn($this->data, "{$this->type}_column{$i}")); + } + } +} diff --git a/library/Reporting/Web/Widget/Template.php b/library/Reporting/Web/Widget/Template.php new file mode 100644 index 0000000..e780a3d --- /dev/null +++ b/library/Reporting/Web/Widget/Template.php @@ -0,0 +1,183 @@ +<?php +// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Reporting\Web\Widget; + +use Icinga\Module\Reporting\Common\Macros; +use Icinga\Module\Reporting\Database; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Sql\Select; + +class Template extends BaseHtmlElement +{ + use Database; + use Macros; + + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'template']; + + /** @var CoverPage */ + protected $coverPage; + + /** @var HeaderOrFooter */ + protected $header; + + /** @var HeaderOrFooter */ + protected $footer; + + protected $preview; + + public static function getDataUrl(array $image = null) + { + if (empty($image)) { + return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; + } + + return sprintf('data:%s;base64,%s', $image['mime_type'], $image['content']); + } + + public static function fromDb($id) + { + $template = new static(); + + $select = (new Select()) + ->from('template') + ->columns('*') + ->where(['id = ?' => $id]); + + $row = $template->getDb()->select($select)->fetch(); + + if ($row === false) { + return null; + } + + $row->settings = json_decode($row->settings, true); + + $coverPage = (new CoverPage()) + ->setColor($row->settings['color']) + ->setTitle($row->settings['title']); + + if (isset($row->settings['cover_page_background_image'])) { + $coverPage->setBackgroundImage($row->settings['cover_page_background_image']); + } + + if (isset($row->settings['cover_page_logo'])) { + $coverPage->setLogo($row->settings['cover_page_logo']); + } + + $template + ->setCoverPage($coverPage) + ->setHeader(new HeaderOrFooter(HeaderOrFooter::HEADER, $row->settings)) + ->setFooter(new HeaderOrFooter(HeaderOrFooter::FOOTER, $row->settings)); + + return $template; + } + + /** + * @return CoverPage + */ + public function getCoverPage() + { + return $this->coverPage; + } + + /** + * @param CoverPage $coverPage + * + * @return $this + */ + public function setCoverPage(CoverPage $coverPage) + { + $this->coverPage = $coverPage; + + return $this; + } + + /** + * @return HeaderOrFooter + */ + public function getHeader() + { + return $this->header; + } + + /** + * @param HeaderOrFooter $header + * + * @return $this + */ + public function setHeader($header) + { + $this->header = $header; + + return $this; + } + + /** + * @return HeaderOrFooter + */ + public function getFooter() + { + return $this->footer; + } + + /** + * @param HeaderOrFooter $footer + * + * @return $this + */ + public function setFooter($footer) + { + $this->footer = $footer; + + return $this; + } + + /** + * @return mixed + */ + public function getPreview() + { + return $this->preview; + } + + /** + * @param mixed $preview + * + * @return $this + */ + public function setPreview($preview) + { + $this->preview = $preview; + + return $this; + } + + protected function assemble() + { + if ($this->preview) { + $this->getAttributes()->add('class', 'preview'); + } + + $this->add($this->getCoverPage()->setMacros($this->macros)); + +// $page = Html::tag( +// 'div', +// ['class' => 'main'], +// Html::tag('div', ['class' => 'page-content'], [ +// $this->header->setMacros($this->macros), +// Html::tag( +// 'div', +// [ +// 'class' => 'main' +// ] +// ), +// $this->footer->setMacros($this->macros) +// ]) +// ); +// +// $this->add($page); + } +} |