diff --git a/application/controllers/DashboardController.php b/application/controllers/DashboardController.php
new file mode 100644
index 00000000..142eb5a3
--- /dev/null
+++ b/application/controllers/DashboardController.php
@@ -0,0 +1,64 @@
+addTitleTab($this->translate('Kubernetes'));
+
+ $cluster = (new ClusterForm())
+ ->populate(
+ [
+ 'cluster_uuid' => Session::getSession()
+ ->getNamespace('kubernetes')
+ ->get('cluster_uuid', ClusterForm::ALL_CLUSTERS)
+ ]
+ )
+ ->on(ClusterForm::ON_SUCCESS, function (ClusterForm $form) {
+ $session = Session::getSession()
+ ->getNamespace('kubernetes');
+ $clusterUuid = $form->getElement('cluster_uuid')->getValue();
+ if ($clusterUuid === ClusterForm::ALL_CLUSTERS) {
+ $session->set('cluster_uuid', null);
+ } else {
+ $session->set('cluster_uuid', $clusterUuid);
+ }
+ })
+ ->handleRequest(ServerRequest::fromGlobals());
+
+ if ($this->isMultiCluster()) {
+ $this->addContent($cluster);
+ }
+
+ $this->content->addHtml(
+ new ClusterManagementDashboard(),
+ new WorkloadsDashboard(),
+ new StorageDashboard(),
+ new NetworkingDashboard(),
+ new ConfigurationDashboard(),
+ new ObservabilityDashboard(),
+ );
+ }
+
+ protected function isMultiCluster(): bool
+ {
+ return Cluster::on(Database::connection())->count() > 1;
+ }
+}
diff --git a/assets/Icinga-Kubernetes.svg b/assets/Icinga-Kubernetes.svg
index ae82af2d..09f5573e 100644
--- a/assets/Icinga-Kubernetes.svg
+++ b/assets/Icinga-Kubernetes.svg
@@ -10,7 +10,7 @@
-
+
@@ -18,10 +18,11 @@
-
+
-
+
+
\ No newline at end of file
diff --git a/assets/Icinga-Kubernetes.ttf b/assets/Icinga-Kubernetes.ttf
index ede13328..2743a38e 100644
Binary files a/assets/Icinga-Kubernetes.ttf and b/assets/Icinga-Kubernetes.ttf differ
diff --git a/assets/Icinga-Kubernetes.woff b/assets/Icinga-Kubernetes.woff
index 1931f87b..e0a22431 100644
Binary files a/assets/Icinga-Kubernetes.woff and b/assets/Icinga-Kubernetes.woff differ
diff --git a/configuration.php b/configuration.php
index 4739a225..9f937fa5 100644
--- a/configuration.php
+++ b/configuration.php
@@ -10,7 +10,8 @@
$section = $this->menuSection(
'Kubernetes',
[
- 'icon' => 'globe'
+ 'icon' => 'globe',
+ 'url' => 'kubernetes/dashboard',
]
);
diff --git a/library/Kubernetes/Common/Auth.php b/library/Kubernetes/Common/Auth.php
index 90d34303..1fe75d17 100644
--- a/library/Kubernetes/Common/Auth.php
+++ b/library/Kubernetes/Common/Auth.php
@@ -32,22 +32,22 @@ class Auth
public const SHOW_STATEFUL_SETS = 'kubernetes/stateful-sets/show';
public const PERMISSIONS = [
- 'ConfigMap' => self::SHOW_CONFIG_MAPS,
- 'CronJob' => self::SHOW_CRON_JOBS,
- 'DaemonSet' => self::SHOW_DAEMON_SETS,
- 'Deployment' => self::SHOW_DEPLOYMENTS,
- 'Event' => self::SHOW_EVENTS,
- 'Ingress' => self::SHOW_INGRESSES,
- 'Job' => self::SHOW_JOBS,
- 'Namespace' => self::SHOW_NAMESPACES,
- 'Node' => self::SHOW_NODES,
- 'PersistentVolume' => self::SHOW_PERSISTENT_VOLUMES,
- 'PersistentVolumeClaim' => self::SHOW_PERSISTENT_VOLUME_CLAIMS,
- 'Pod' => self::SHOW_PODS,
- 'ReplicaSet' => self::SHOW_REPLICA_SETS,
- 'Secret' => self::SHOW_SECRETS,
- 'Service' => self::SHOW_SERVICES,
- 'StatefulSet' => self::SHOW_STATEFUL_SETS,
+ 'configmap' => self::SHOW_CONFIG_MAPS,
+ 'cronjob' => self::SHOW_CRON_JOBS,
+ 'daemonset' => self::SHOW_DAEMON_SETS,
+ 'deployment' => self::SHOW_DEPLOYMENTS,
+ 'event' => self::SHOW_EVENTS,
+ 'ingress' => self::SHOW_INGRESSES,
+ 'job' => self::SHOW_JOBS,
+ 'namespace' => self::SHOW_NAMESPACES,
+ 'node' => self::SHOW_NODES,
+ 'persistentvolume' => self::SHOW_PERSISTENT_VOLUMES,
+ 'persistentvolumeclaim' => self::SHOW_PERSISTENT_VOLUME_CLAIMS,
+ 'pod' => self::SHOW_PODS,
+ 'replicaset' => self::SHOW_REPLICA_SETS,
+ 'secret' => self::SHOW_SECRETS,
+ 'service' => self::SHOW_SERVICES,
+ 'statefulset' => self::SHOW_STATEFUL_SETS,
];
protected IcingaAuth $auth;
diff --git a/library/Kubernetes/Common/BeforeAssemble.php b/library/Kubernetes/Common/BeforeAssemble.php
new file mode 100644
index 00000000..a9b7ae70
--- /dev/null
+++ b/library/Kubernetes/Common/BeforeAssemble.php
@@ -0,0 +1,30 @@
+hasBeenAssembled === false && ! $this->beforeAssemble) {
+ $this->beforeAssemble = true;
+ $this->beforeAssemble();
+ }
+
+ return parent::ensureAssembled();
+ }
+
+ /**
+ * Hook method to perform actions before assembling the object.
+ */
+ protected function beforeAssemble(): void
+ {
+ }
+}
diff --git a/library/Kubernetes/Common/FormatString.php b/library/Kubernetes/Common/FormatString.php
new file mode 100644
index 00000000..228b2045
--- /dev/null
+++ b/library/Kubernetes/Common/FormatString.php
@@ -0,0 +1,48 @@
+args = array_merge($this->args, $args);
+
+ return $this;
+ }
+
+ /**
+ * Convert the format string to a formatted string using the provided arguments.
+ *
+ * @return string The formatted string.
+ */
+ public function __toString(): string
+ {
+ return ! empty($this->args) ? vsprintf(strtr($this->format, $this->args), $this->args) : $this->format;
+ }
+}
diff --git a/library/Kubernetes/Dashboard/ClusterManagementDashboard.php b/library/Kubernetes/Dashboard/ClusterManagementDashboard.php
new file mode 100644
index 00000000..ed21fbe9
--- /dev/null
+++ b/library/Kubernetes/Dashboard/ClusterManagementDashboard.php
@@ -0,0 +1,34 @@
+translate('Cluster management');
+ }
+
+ protected function assemble(): void
+ {
+ $this->addHtml(
+ new KubernetesPhaseDashlet(
+ 'namespace',
+ $this->translate('Namespaces'),
+ $this->translate(
+ 'Out of {total} total Namespaces, {Active} are Active, {Terminating} are Terminating.'
+ )
+ ),
+ new IcingaStateDashlet(
+ 'node',
+ $this->translate('Nodes'),
+ $this->translate(
+ 'Out of {total} total Nodes, {ok} are in OK state, {critical} are Critical, {warning} are
+ in Warning state, and {unknown} are Unknown.'
+ )
+ )
+ );
+ }
+}
diff --git a/library/Kubernetes/Dashboard/ConfigurationDashboard.php b/library/Kubernetes/Dashboard/ConfigurationDashboard.php
new file mode 100644
index 00000000..1097515e
--- /dev/null
+++ b/library/Kubernetes/Dashboard/ConfigurationDashboard.php
@@ -0,0 +1,29 @@
+translate('Configuration');
+ }
+
+ protected function assemble(): void
+ {
+ $this->addHtml(
+ new Dashlet(
+ 'configmap',
+ $this->translate('Config Maps'),
+ $this->translate('Store configuration data as key-value pairs.')
+ ),
+ new Dashlet(
+ 'secret',
+ $this->translate('Secrets'),
+ $this->translate('Store sensitive data (e.g., passwords, tokens) in an encrypted format.')
+ )
+ );
+ }
+}
diff --git a/library/Kubernetes/Dashboard/Dashboard.php b/library/Kubernetes/Dashboard/Dashboard.php
new file mode 100644
index 00000000..f037096f
--- /dev/null
+++ b/library/Kubernetes/Dashboard/Dashboard.php
@@ -0,0 +1,31 @@
+setWrapper(new HtmlElement(
+ 'section',
+ new Attributes(['class' => 'kubernetes-dashboard']),
+ new HtmlElement('h2', null, new Text($this->getTitle()))
+ ));
+ }
+}
diff --git a/library/Kubernetes/Dashboard/Dashlet.php b/library/Kubernetes/Dashboard/Dashlet.php
new file mode 100644
index 00000000..229a8f8d
--- /dev/null
+++ b/library/Kubernetes/Dashboard/Dashlet.php
@@ -0,0 +1,50 @@
+url = $url !== null ? $url : Factory::createListUrl($kind);
+ $this->summary = new FormatString($summary);
+ }
+
+ protected function assemble(): void
+ {
+ $this->addHtml(new Link(
+ [
+ $this->title,
+ Factory::createIcon($this->kind),
+ new HtmlElement(
+ 'p',
+ null,
+ new Text($this->summary)
+ )
+ ],
+ $this->url
+ ));
+ }
+}
diff --git a/library/Kubernetes/Dashboard/IcingaStateDashlet.php b/library/Kubernetes/Dashboard/IcingaStateDashlet.php
new file mode 100644
index 00000000..c31efb21
--- /dev/null
+++ b/library/Kubernetes/Dashboard/IcingaStateDashlet.php
@@ -0,0 +1,64 @@
+withRestrictions(
+ Auth::PERMISSIONS[$this->kind],
+ Factory::createModel($this->kind)::on(Database::connection())
+ );
+
+ $clusterUuid = Session::getSession()->getNamespace('kubernetes')->get('cluster_uuid');
+ if ($clusterUuid !== null) {
+ $q->filter(Filter::equal('cluster_uuid', Uuid::fromString($clusterUuid)->getBytes()));
+ }
+
+ $q->columns([
+ 'icinga_state',
+ 'count' => new Expression('COUNT(*)')
+ ]);
+
+ $q->getSelectBase()->groupBy('icinga_state');
+
+ $counts = array_fill_keys(
+ [
+ 'ok',
+ 'warning',
+ 'critical',
+ 'unknown',
+ 'pending'
+ ],
+ 0
+ );
+
+ foreach ($q as $count) {
+ $counts[$count->icinga_state] = $count->count;
+ }
+
+ $counts['total'] = array_sum($counts);
+
+ return array_combine(
+ array_map(fn($key) => '{' . $key . '}', array_keys($counts)),
+ $counts
+ );
+ }
+
+ protected function beforeAssemble(): void
+ {
+ $this->summary->addArgs($this->getIcingaStateCounts());
+ }
+}
diff --git a/library/Kubernetes/Dashboard/KubernetesPhaseDashlet.php b/library/Kubernetes/Dashboard/KubernetesPhaseDashlet.php
new file mode 100644
index 00000000..ab79ac4c
--- /dev/null
+++ b/library/Kubernetes/Dashboard/KubernetesPhaseDashlet.php
@@ -0,0 +1,67 @@
+withRestrictions(
+ Auth::PERMISSIONS[$this->kind],
+ Factory::createModel($this->kind)::on(Database::connection())
+ );
+
+ $clusterUuid = Session::getSession()->getNamespace('kubernetes')->get('cluster_uuid');
+ if ($clusterUuid !== null) {
+ $q->filter(Filter::equal('cluster_uuid', Uuid::fromString($clusterUuid)->getBytes()));
+ }
+
+ $q->columns([
+ 'phase',
+ 'count' => new Expression('COUNT(*)')
+ ]);
+
+ $q->getSelectBase()->groupBy('phase');
+
+ $counts = array_fill_keys(
+ [
+ 'Bound',
+ 'Failed',
+ 'Lost',
+ 'Released',
+ 'Available',
+ 'Active',
+ 'Terminating',
+ 'Pending',
+ ],
+ 0
+ );
+
+ foreach ($q as $count) {
+ $counts[$count->phase] = $count->count;
+ }
+
+ $counts['total'] = array_sum($counts);
+
+ return array_combine(
+ array_map(fn($key) => '{' . $key . '}', array_keys($counts)),
+ $counts
+ );
+ }
+
+ protected function beforeAssemble(): void
+ {
+ $this->summary->addArgs($this->getKubernetesPhaseCounts());
+ }
+}
diff --git a/library/Kubernetes/Dashboard/NetworkingDashboard.php b/library/Kubernetes/Dashboard/NetworkingDashboard.php
new file mode 100644
index 00000000..8f1e3578
--- /dev/null
+++ b/library/Kubernetes/Dashboard/NetworkingDashboard.php
@@ -0,0 +1,41 @@
+translate('Networking');
+ }
+
+ protected function assemble(): void
+ {
+ $this->addHtml(
+ new Dashlet(
+ 'service',
+ $this->translate('Services'),
+ $this->translate(
+ 'Expose Pods within or outside the cluster. Types: ClusterIP, NodePort, LoadBalancer,
+ ExternalName.'
+ )
+ ),
+ new Dashlet(
+ 'service',
+ $this->translate('Cluster Services'),
+ $this->translate(
+ "Core components that manage and support the Kubernetes control plane, networking, and
+ storage, ensuring the cluster's overall health and operation."
+ ),
+ 'kubernetes/services?label.name=kubernetes.io%2Fcluster-service&label.value=true',
+ ),
+ new Dashlet(
+ 'ingress',
+ $this->translate('Ingresses'),
+ $this->translate('Manage HTTP/HTTPS traffic into the cluster.')
+ ),
+ );
+ }
+}
diff --git a/library/Kubernetes/Dashboard/ObservabilityDashboard.php b/library/Kubernetes/Dashboard/ObservabilityDashboard.php
new file mode 100644
index 00000000..4e57f236
--- /dev/null
+++ b/library/Kubernetes/Dashboard/ObservabilityDashboard.php
@@ -0,0 +1,24 @@
+translate('Observability');
+ }
+
+ protected function assemble(): void
+ {
+ $this->addHtml(
+ new Dashlet(
+ 'event',
+ $this->translate('Events'),
+ $this->translate('Record changes and issues within the cluster.')
+ )
+ );
+ }
+}
diff --git a/library/Kubernetes/Dashboard/StorageDashboard.php b/library/Kubernetes/Dashboard/StorageDashboard.php
new file mode 100644
index 00000000..d6da8222
--- /dev/null
+++ b/library/Kubernetes/Dashboard/StorageDashboard.php
@@ -0,0 +1,35 @@
+translate('Storage');
+ }
+
+ protected function assemble(): void
+ {
+ $this->addHtml(
+ new KubernetesPhaseDashlet(
+ 'persistentvolume',
+ $this->translate('Persistent Volumes'),
+ $this->translate(
+ 'Out of {total} total Persistent Volumes, {Bound} are Bound, {Available} are Available,
+ {Pending} are Pending, {Released} are Released, and {Failed} are Failed.'
+ )
+ ),
+ new KubernetesPhaseDashlet(
+ 'persistentvolumeclaim',
+ $this->translate('PVCs'),
+ $this->translate(
+ 'Out of {total} total Persistent Volume Claims, {Bound} are Bound, {Pending} are Pending,
+ {Lost} are Lost.'
+ )
+ )
+ );
+ }
+}
diff --git a/library/Kubernetes/Dashboard/WorkloadsDashboard.php b/library/Kubernetes/Dashboard/WorkloadsDashboard.php
new file mode 100644
index 00000000..db18f2ef
--- /dev/null
+++ b/library/Kubernetes/Dashboard/WorkloadsDashboard.php
@@ -0,0 +1,72 @@
+translate('Workloads');
+ }
+
+ protected function assemble(): void
+ {
+ $this->addHtml(
+ new Dashlet(
+ 'cronjob',
+ $this->translate('Cron Jobs'),
+ $this->translate('Schedule Jobs to run at specific times.')
+ ),
+ new IcingaStateDashlet(
+ 'daemonset',
+ $this->translate('Daemon Sets'),
+ $this->translate(
+ 'Out of {total} total Daemon Sets, {ok} are in OK state, {critical} are Critical,
+ {warning} are in Warning state, and {unknown} are Unknown.'
+ )
+ ),
+ new IcingaStateDashlet(
+ 'deployment',
+ $this->translate('Deployments'),
+ $this->translate(
+ 'Out of {total} total Deployments, {ok} are in OK state, {critical} are Critical,
+ {warning} are in Warning state, and {unknown} are Unknown.'
+ )
+ ),
+ new IcingaStateDashlet(
+ 'job',
+ $this->translate('Jobs'),
+ $this->translate(
+ 'Out of {total} total Jobs, {ok} are in OK state, {critical} are Critical, {warning} are in
+ Warning state, {unknown} are Unknown and {pending} are in Pending State.'
+ )
+ ),
+ new IcingaStateDashlet(
+ 'pod',
+ $this->translate('Pods'),
+ $this->translate(
+ 'Out of {total} total Pods, {ok} are in OK state, {critical} are Critical, {warning} are in
+ Warning state, {unknown} are Unknown, and {pending} are in Pending State.'
+ )
+ ),
+ new IcingaStateDashlet(
+ 'replicaset',
+ $this->translate('Replica Sets'),
+ $this->translate(
+ 'Out of {total} total Replica Sets, {ok} are in OK state, {critical} are Critical,
+ {warning} are in Warning state, and {unknown} are Unknown.'
+ )
+ ),
+ new IcingaStateDashlet(
+ 'statefulset',
+ $this->translate('Stateful Sets'),
+ $this->translate(
+ 'Out of {total} total Stateful Sets, {ok} are in OK state, {critical} are Critical,
+ {warning} are in Warning state, and {unknown} are Unknown.'
+ )
+ )
+ );
+ }
+}
diff --git a/library/Kubernetes/Web/ClusterForm.php b/library/Kubernetes/Web/ClusterForm.php
new file mode 100644
index 00000000..513aebb2
--- /dev/null
+++ b/library/Kubernetes/Web/ClusterForm.php
@@ -0,0 +1,44 @@
+columns(['uuid', 'name']);
+
+ foreach ($clusters as $cluster) {
+ $label = $cluster->name ?? (string) Uuid::fromBytes($cluster->uuid);
+
+ yield (string) Uuid::fromBytes($cluster->uuid) => $label;
+ }
+ }
+
+ protected function assemble(): void
+ {
+ $this->addElement(
+ 'select',
+ 'cluster_uuid',
+ [
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'label' => $this->translate('Cluster'),
+ 'options' => [
+ static::ALL_CLUSTERS => $this->translate('All clusters'),
+ ] + iterator_to_array($this->yieldClusters()),
+ ],
+ );
+ }
+}
diff --git a/library/Kubernetes/Web/EventListItem.php b/library/Kubernetes/Web/EventListItem.php
index c3b5e2ec..8a63130c 100644
--- a/library/Kubernetes/Web/EventListItem.php
+++ b/library/Kubernetes/Web/EventListItem.php
@@ -49,7 +49,7 @@ protected function assembleTitle(BaseHtmlElement $title): void
$kind = strtolower($this->item->reference_kind);
$icon = Factory::createIcon($kind);
- $url = Factory::createUrl($kind);
+ $url = Factory::createDetailUrl($kind);
if ($url !== null) {
$content = new HtmlDocument();
diff --git a/library/Kubernetes/Web/Factory.php b/library/Kubernetes/Web/Factory.php
index 2cd8ad59..c3b29257 100644
--- a/library/Kubernetes/Web/Factory.php
+++ b/library/Kubernetes/Web/Factory.php
@@ -25,6 +25,7 @@
use ipl\Html\Attributes;
use ipl\Html\HtmlElement;
use ipl\Html\ValidHtml;
+use ipl\Orm\Model;
use ipl\Stdlib\Filter\Rule;
use ipl\Web\Url;
use ipl\Web\Widget\EmptyState;
@@ -138,7 +139,32 @@ public static function createList(string $kind, Rule $filter): ValidHtml
}
}
- public static function createUrl(string $kind): ?Url
+ public static function createModel(string $kind): ?Model
+ {
+ $kind = strtolower(str_replace(['_', '-'], '', $kind));
+
+ return match ($kind) {
+ 'configmap' => new ConfigMap(),
+ 'cronjob' => new CronJob(),
+ 'daemonset' => new DaemonSet(),
+ 'deployment' => new Deployment(),
+ 'event' => new Event(),
+ 'ingress' => new Ingress(),
+ 'job' => new Job(),
+ 'namespace' => new NamespaceModel(),
+ 'node' => new Node(),
+ 'persistentvolume' => new PersistentVolume(),
+ 'persistentvolumeclaim' => new PersistentVolumeClaim(),
+ 'pod' => new Pod(),
+ 'replicaset' => new ReplicaSet(),
+ 'secret' => new Secret(),
+ 'service' => new Service(),
+ 'statefulset' => new StatefulSet(),
+ default => null,
+ };
+ }
+
+ public static function createDetailUrl(string $kind): ?Url
{
$kind = strtolower(str_replace(['_', '-'], '', $kind));
@@ -163,4 +189,36 @@ public static function createUrl(string $kind): ?Url
default => null
};
}
+
+ public static function createListUrl(string $kind): ?Url
+ {
+ $kind = strtolower(str_replace(['_', '-'], '', $kind));
+
+ $controller = match ($kind) {
+ 'configmap',
+ 'container',
+ 'cronjob',
+ 'daemonset',
+ 'deployment',
+ 'event',
+ 'job',
+ 'namespace',
+ 'node',
+ 'persistentvolume',
+ 'persistentvolumeclaim',
+ 'pod',
+ 'replicaset',
+ 'secret',
+ 'service',
+ 'statefulset' => "{$kind}s",
+ 'ingress' => 'ingresses',
+ default => null
+ };
+
+ if ($controller !== null) {
+ return Url::fromPath("kubernetes/$controller");
+ }
+
+ return null;
+ }
}
diff --git a/library/Kubernetes/Web/ListController.php b/library/Kubernetes/Web/ListController.php
index 7b4f3185..d7aa8854 100644
--- a/library/Kubernetes/Web/ListController.php
+++ b/library/Kubernetes/Web/ListController.php
@@ -6,10 +6,13 @@
use Icinga\Module\Kubernetes\Common\Auth;
use Icinga\Module\Kubernetes\TBD\ObjectSuggestions;
+use Icinga\Web\Session;
use ipl\Orm\Query;
+use ipl\Stdlib\Filter;
use ipl\Web\Compat\SearchControls;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
+use Ramsey\Uuid\Uuid;
abstract class ListController extends Controller
{
@@ -39,6 +42,13 @@ public function indexAction(): void
$q = Auth::getInstance()->withRestrictions($this->getPermission(), $this->getQuery());
+ $clusterUuid = Session::getSession()
+ ->getNamespace('kubernetes')
+ ->get('cluster_uuid');
+ if ($clusterUuid !== null) {
+ $q->filter(Filter::equal('cluster_uuid', Uuid::fromString($clusterUuid)->getBytes()));
+ }
+
$limitControl = $this->createLimitControl();
$sortControl = $this->createSortControl($q, $this->getSortColumns());
$paginationControl = $this->createPaginationControl($q);
diff --git a/public/css/common.less b/public/css/common.less
index 83e55852..122f6604 100644
--- a/public/css/common.less
+++ b/public/css/common.less
@@ -29,11 +29,6 @@ pre {
}
.controls {
- // Copied from Icinga DB Web
- -moz-box-shadow: 0 0 0 1px var(--gray-lighter, #4b4b4b);
- -webkit-box-shadow: 0 0 0 1px var(--gray-lighter, #4b4b4b);
- box-shadow: 0 0 0 1px var(--gray-lighter, #4b4b4b);
-
a.subject {
cursor: default;
pointer-events: none;
diff --git a/public/css/icons.less b/public/css/icons.less
index d5baae74..03cbd1e9 100644
--- a/public/css/icons.less
+++ b/public/css/icons.less
@@ -58,6 +58,10 @@
}
}
+.kicon-event:before {
+ content: "\e94d";
+}
+
.kicon-ingress:before {
content: "\e904";
}
diff --git a/public/css/module.less b/public/css/module.less
index 3d83db7d..c66f095b 100644
--- a/public/css/module.less
+++ b/public/css/module.less
@@ -30,3 +30,96 @@
padding-left: 0;
}
}
+
+.kubernetes-dashboard > ul {
+ margin: 0;
+ padding: 0;
+ min-width: 38em;
+ max-width: 80em;
+
+ li {
+ list-style-type: none;
+
+ text-align: left;
+ display: inline-block;
+ padding: 0;
+ clear: both;
+ width: 25em;
+ min-width: 16em;
+ vertical-align: top;
+
+ a {
+ i {
+ font-size: 3em;
+ display: block;
+ float: left;
+ line-height: 1em;
+ margin-right: 0.3em;
+ color: @text-color-light;
+ }
+
+ &.state-critical i {
+ color: @color-critical;
+ }
+
+ &.state-warning i {
+ color: @color-warning;
+ }
+
+ &.state-ok i {
+ color: @color-ok;
+ }
+
+ &.state-unknown i {
+ color: @color-unknown;
+ }
+
+ &.state-pending i {
+ color: @color-pending;
+ }
+
+ border-left: 0.5em solid transparent;
+ padding: 1em;
+ color: @text-color;
+ font-weight: bold;
+ display: block;
+ text-decoration: none;
+ min-height: 10em;
+
+ overflow: hidden;
+
+ &.active {
+ border-color: @icinga-blue;
+ background-color: @tr-active-color;
+ }
+
+ &:hover {
+ background-color: @tr-hover-color;
+ text-decoration: none;
+ }
+
+ &:active, &:focus {
+ background-color: @tr-hover-color;
+ outline: none;
+ }
+ }
+
+ p {
+ font-weight: normal;
+ margin-bottom: 0.5em;
+ padding-left: 4.5em;
+ color: @text-color-light;
+ }
+ }
+}
+
+.control-label-group {
+ flex-direction: row;
+ font-size: 1.2em;
+ text-align: left;
+ width: 5em;
+}
+
+.icinga-controls select {
+ max-width: 280px;
+}