diff --git a/modules/helfi_react_search/helfi_react_search.module b/modules/helfi_react_search/helfi_react_search.module
index 1d5df6ca1..2079621f7 100644
--- a/modules/helfi_react_search/helfi_react_search.module
+++ b/modules/helfi_react_search/helfi_react_search.module
@@ -37,3 +37,19 @@ function helfi_react_search_preprocess_paragraph(array &$variables) : void {
$variables['#attached']['drupalSettings']['helfi_react_search']['elastic_proxy_url'] = $proxyUrl;
}
}
+
+/**
+ * Implements hook_theme().
+ */
+function helfi_react_search_theme() : array {
+ return [
+ 'debug_item__search_api' => [
+ 'variables' => [
+ 'id' => NULL,
+ 'label' => NULL,
+ 'data' => [],
+ ],
+ 'template' => 'debug-item--search-api',
+ ],
+ ];
+}
diff --git a/modules/helfi_react_search/src/Plugin/DebugDataItem/SearchApiIndex.php b/modules/helfi_react_search/src/Plugin/DebugDataItem/SearchApiIndex.php
new file mode 100644
index 000000000..b6ff0ea9d
--- /dev/null
+++ b/modules/helfi_react_search/src/Plugin/DebugDataItem/SearchApiIndex.php
@@ -0,0 +1,106 @@
+entityTypeManager = $container->get('entity_type.manager');
+
+ return $instance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function collect(): array {
+ $data = [];
+
+ if (!$this->entityTypeManager->hasDefinition('search_api_index')) {
+ return [];
+ }
+ $indexes = $this->entityTypeManager
+ ->getStorage('search_api_index')
+ ->loadMultiple();
+
+ if (!$indexes) {
+ return [];
+ }
+ /** @var \Drupal\search_api\IndexInterface $index */
+ foreach ($indexes as $index) {
+ $result = $status = NULL;
+
+ try {
+ $status = $index->getServerInstance()?->isAvailable();
+ $tracker = $index->getTrackerInstance();
+
+ $result = $this->resolveResult(
+ $tracker->getIndexedItemsCount(),
+ $tracker->getTotalItemsCount()
+ );
+
+ }
+ catch (SearchApiException) {
+ }
+ $data[] = [
+ 'id' => $index->getOriginalId(),
+ 'result' => $result,
+ 'status' => $status,
+ ];
+ }
+
+ return $data;
+ }
+
+ /**
+ * Resolve return value based on index status.
+ *
+ * @param int $indexed
+ * Amount of up-to-date items in index.
+ * @param int $total
+ * Maximum number of items in index.
+ *
+ * @return string
+ * Status.
+ */
+ private function resolveResult(int $indexed, int $total): string {
+ if ($indexed == 0 || $total == 0) {
+ return 'indexing or index rebuild required';
+ }
+
+ if ($indexed === $total) {
+ return 'Index up to date';
+ }
+
+ return "$indexed/$total";
+ }
+
+}
diff --git a/modules/helfi_react_search/templates/debug-item--search-api.html.twig b/modules/helfi_react_search/templates/debug-item--search-api.html.twig
new file mode 100644
index 000000000..d3fa9fccf
--- /dev/null
+++ b/modules/helfi_react_search/templates/debug-item--search-api.html.twig
@@ -0,0 +1,45 @@
+{#
+/**
+ * To actually show any data from a custom plugin, you *MUST* override this template
+ * with a template called 'debug-item--{{ id }}.html.twig'.
+ *
+ * For example: debug-item--composer.html.twig, where 'composer' is your plugin's ID.
+ *
+ * You can then loop your data with something like this:
+ * {% for item in data.packages %}
+ * {{ item.name }}
+ * {{ item.version }}
+ * {% endfor %}
+ *
+ * Available variables:
+ * - id: The ID of your plugin
+ * - label The label of your plugin
+ * - data: An array of data returned by your plugin's collect() method.
+ */
+#}
+
+
{{ 'Search API indexes'|t }}
+
+
+
+ Index |
+ Status |
+ Result |
+
+
+
+ {% for value in data %}
+
+
+ {{ value.id }}
+ |
+
+ {{ value.status ? 'Online'|t : 'Offline'|t }}
+ |
+
+ {{ value.result }}
+ |
+
+ {% endfor %}
+
+
diff --git a/modules/helfi_react_search/tests/src/Unit/SearchApiIndexItemTest.php b/modules/helfi_react_search/tests/src/Unit/SearchApiIndexItemTest.php
new file mode 100644
index 000000000..3db830ffa
--- /dev/null
+++ b/modules/helfi_react_search/tests/src/Unit/SearchApiIndexItemTest.php
@@ -0,0 +1,139 @@
+prophesize(EntityTypeManagerInterface::class);
+ $entityTypeManager->hasDefinition('search_api_index')->willReturn(FALSE);
+ $container = new ContainerBuilder();
+ $container->set('entity_type.manager', $entityTypeManager->reveal());
+ $sut = SearchApiIndex::create($container, [], '', []);
+ $this->assertEmpty($sut->collect());
+ }
+
+ /**
+ * @covers ::create
+ * @covers ::collect
+ */
+ public function testNoIndex() : void {
+ $indexStorage = $this->prophesize(SearchApiConfigEntityStorage::class);
+ $indexStorage->loadMultiple()->willReturn([]);
+ $entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
+ $entityTypeManager->hasDefinition('search_api_index')
+ ->willReturn(TRUE);
+ $entityTypeManager->getStorage('search_api_index')
+ ->willReturn($indexStorage->reveal());
+ $container = new ContainerBuilder();
+ $container->set('entity_type.manager', $entityTypeManager->reveal());
+
+ $sut = SearchApiIndex::create($container, [], '', []);
+ $this->assertEmpty($sut->collect());
+ }
+
+ /**
+ * @covers ::create
+ * @covers ::collect
+ * @covers ::resolveResult
+ */
+ public function testCollect() : void {
+ $index1 = $this->prophesize(IndexInterface::class);
+ $index1->getOriginalId()->willReturn('index1');
+ $index1->getServerInstance()->willThrow(new SearchApiException());
+
+ $server = $this->prophesize(ServerInterface::class);
+ $server->isAvailable()->willReturn(TRUE);
+
+ $tracker2 = $this->prophesize(TrackerInterface::class);
+ $tracker2->getIndexedItemsCount()->willReturn(0);
+ $tracker2->getTotalItemsCount()->willReturn(0);
+
+ $index2 = $this->prophesize(IndexInterface::class);
+ $index2->getOriginalId()->willReturn('index2');
+ $index2->getServerInstance()->willReturn($server->reveal());
+ $index2->getTrackerInstance()->willReturn($tracker2->reveal());
+
+ $tracker3 = $this->prophesize(TrackerInterface::class);
+ $tracker3->getIndexedItemsCount()->willReturn(20);
+ $tracker3->getTotalItemsCount()->willReturn(20);
+
+ $index3 = $this->prophesize(IndexInterface::class);
+ $index3->getOriginalId()->willReturn('index3');
+ $index3->getServerInstance()->willReturn($server->reveal());
+ $index3->getTrackerInstance()->willReturn($tracker3->reveal());
+
+ $tracker4 = $this->prophesize(TrackerInterface::class);
+ $tracker4->getIndexedItemsCount()->willReturn(10);
+ $tracker4->getTotalItemsCount()->willReturn(20);
+ $index4 = $this->prophesize(IndexInterface::class);
+ $index4->getOriginalId()->willReturn('index4');
+ $index4->getServerInstance()->willReturn($server->reveal());
+ $index4->getTrackerInstance()->willReturn($tracker4->reveal());
+
+ $indexStorage = $this->prophesize(SearchApiConfigEntityStorage::class);
+ $indexStorage->loadMultiple()->willReturn([
+ $index1->reveal(),
+ $index2->reveal(),
+ $index3->reveal(),
+ $index4->reveal(),
+ ]);
+ $entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
+ $entityTypeManager->getStorage('search_api_index')
+ ->willReturn($indexStorage->reveal());
+ $entityTypeManager->hasDefinition('search_api_index')
+ ->willReturn(TRUE);
+ $container = new ContainerBuilder();
+ $container->set('entity_type.manager', $entityTypeManager->reveal());
+
+ $sut = SearchApiIndex::create($container, [], '', []);
+ $this->assertEquals([
+ [
+ 'id' => 'index1',
+ 'result' => NULL,
+ 'status' => NULL,
+ ],
+ [
+ 'id' => 'index2',
+ 'result' => 'indexing or index rebuild required',
+ 'status' => TRUE,
+ ],
+ [
+ 'id' => 'index3',
+ 'result' => 'Index up to date',
+ 'status' => TRUE,
+ ],
+ [
+ 'id' => 'index4',
+ 'result' => '10/20',
+ 'status' => TRUE,
+ ],
+ ], $sut->collect());
+ }
+
+}