Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ddev/commands/web/dkan-site-install
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
set -eu -o pipefail

drush site:install minimal --site-name="DKAN" --account-name="admin" -y
drush pm-enable dkan dkan_harvest dkan_datastore automated_cron admin_toolbar admin_toolbar_tools -y
drush pm-enable dkan dkan_harvest dkan_datastore dkan_datastore_preview automated_cron admin_toolbar admin_toolbar_tools -y

# Create administrator role and assign to user 1
drush role:create "administrator" "Administrator" -y
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
.dkan-datastore-preview {
margin: 1em 0;
}

.dkan-datastore-preview__controls {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 0.75em;
}

.dkan-datastore-preview__page-size {
display: inline-flex;
align-items: center;
gap: 0.3em;
}

.dkan-datastore-preview__summary {
font-size: 0.9em;
}

.dkan-datastore-preview__table-wrapper {
overflow-x: auto;
}

.dkan-datastore-preview__table {
width: 100%;
border-collapse: collapse;
}

.dkan-datastore-preview__table thead th {
background-color: #f5f5f5;
font-weight: 600;
text-align: left;
padding: 0.75em 1em;
border-bottom: 2px solid #ccc;
white-space: nowrap;
}

.dkan-datastore-preview__table thead th a {
display: inline-flex;
align-items: center;
gap: 0.25em;
}

.dkan-datastore-preview__table tbody td {
padding: 0.5em 1em;
border-bottom: 1px solid #e0e0e0;
}

.dkan-datastore-preview__table tbody tr:hover {
background-color: #fafafa;
}

.dkan-datastore-preview__footer {
margin-top: 0.75em;
}

.dkan-datastore-preview__message {
padding: 0.75em 1em;
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: DKAN Datastore Preview
description: Paginated, sortable HTML table previews of datastore data on dataset pages.
type: module
core_version_requirement: ^10.4 || ^11
package: DKAN
dependencies:
- dkan:dkan_common
- dkan:dkan_datastore
- dkan:dkan_metastore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/**
* @file
* Install hooks for the DKAN Datastore Preview module.
*/

/**
* Implements hook_install().
*
* Enables the data_preview extra field on the default view display for the
* data node type so previews appear out of the box.
*/
function dkan_datastore_preview_install() {
$display = \Drupal::entityTypeManager()
->getStorage('entity_view_display')
->load('node.data.default');

if ($display) {
$display->setComponent('data_preview', [
'weight' => 100,
]);
$display->save();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
preview:
css:
component:
css/dkan-datastore-preview.css: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php

/**
* @file
* Hook implementations for the DKAN Datastore Preview module.
*/

use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\dkan_common\DataResource;
use Drupal\node\NodeInterface;

/**
* Implements hook_theme().
*/
function dkan_datastore_preview_theme() {
return [
'dkan_datastore_preview' => [
'variables' => [
'table' => NULL,
'pager' => NULL,
'page_size_form' => NULL,
'result_summary' => NULL,
],
'template' => 'dkan-datastore-preview',
],
];
}

/**
* Implements hook_entity_extra_field_info().
*/
function dkan_datastore_preview_entity_extra_field_info() {
$extra = [];
if (\Drupal::entityTypeManager()->getStorage('node_type')->load('data')) {
$extra['node']['data']['display']['data_preview'] = [
'label' => t('Data Preview'),
'description' => t('Paginated preview tables for tabular distributions.'),
'weight' => 100,
'visible' => TRUE,
];
}
return $extra;
}

/**
* Implements hook_ENTITY_TYPE_view() for node entities.
*
* Adds a data preview table for each importable distribution on dataset
* nodes. Rendered on the server-side dataset page via the data_preview
* extra field printed by dkan_metastore's node--data template.
*/
function dkan_datastore_preview_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
if (!$entity instanceof NodeInterface) {
return;
}
if ($entity->bundle() !== 'data') {
return;
}
if (($entity->get('field_data_type')->value ?? '') !== 'dataset') {
return;
}
if (!$display->getComponent('data_preview')) {
return;
}

// During node preview of a new (unsaved) node, datastore tables don't exist.
if (!empty($entity->in_preview) && $entity->isNew()) {
$build['data_preview'] = [
'#markup' => '<p>' . t('Data preview is available after the dataset is saved and data is imported.') . '</p>',
];
return;
}

/** @var \Drupal\dkan_common\DatasetInfo $datasetInfo */
$datasetInfo = \Drupal::service('dkan.common.dataset_info');
$info = $datasetInfo->gather($entity->uuid());

$revision = $info['published_revision'] ?? $info['latest_revision'] ?? NULL;
if (!$revision || empty($revision['distributions'])) {
return;
}

$previews = [];
$index = 0;
foreach ($revision['distributions'] as $dist) {
if (!is_array($dist) || empty($dist['resource_id']) || empty($dist['resource_version'])) {
continue;
}
if (!in_array($dist['mime_type'] ?? '', DataResource::IMPORTABLE_FILE_TYPES)) {
continue;
}

$resourceId = $dist['resource_id'] . '__' . $dist['resource_version'];

$preview = [
'#type' => 'container',
'#attributes' => ['class' => ['dkan-datastore-preview-distribution']],
'#cache' => [
'tags' => _dkan_datastore_preview_distribution_cache_tags($dist['distribution_uuid'] ?? ''),
],
];

// Caption the table with the source filename.
$label = '';
if (!empty($dist['source_path'])) {
$label = basename(parse_url($dist['source_path'], PHP_URL_PATH) ?: $dist['source_path']);
}

$preview['table'] = [
'#type' => 'dkan_datastore_preview',
'#resource_id' => $resourceId,
'#pager_element' => $index,
'#query_prefix' => 'dp' . $index . '_',
'#caption' => $label ? t('Preview: @label', ['@label' => $label]) : NULL,
];
$previews[] = $preview;
$index++;
}

if ($previews) {
$build['data_preview'] = $previews;
}
}

/**
* Get the cache tags for a distribution metastore item.
*
* The datastore invalidates the distribution node's cache tags after an
* import completes (via PostImport), so attaching them here flips a "not yet
* available" message to the data table without a manual cache rebuild.
*
* @param string $distribution_uuid
* The distribution's UUID.
*
* @return array
* Cache tags, or an empty array when the item cannot be loaded.
*/
function _dkan_datastore_preview_distribution_cache_tags(string $distribution_uuid): array {
if ($distribution_uuid === '') {
return [];
}
try {
$item = \Drupal::service('dkan.metastore.metastore_item_factory')->getInstance($distribution_uuid);
return $item->getCacheTags();
}
catch (\Throwable) {
return [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
dkan.datastore_preview.builder:
class: \Drupal\dkan_datastore_preview\Service\DataPreviewBuilder
arguments:
- '@request_stack'
- '@pager.manager'
Drupal\dkan_datastore_preview\Service\DataPreviewBuilder: '@dkan.datastore_preview.builder'

dkan.datastore_preview.data_source.database:
class: \Drupal\dkan_datastore_preview\DataSource\DatabaseDataSource
arguments:
- '@dkan.datastore.service'
Drupal\dkan_datastore_preview\DataSource\DatabaseDataSource: '@dkan.datastore_preview.data_source.database'

dkan.datastore_preview.import_status_message:
class: \Drupal\dkan_datastore_preview\Service\ImportStatusMessage
arguments:
- '@dkan.datastore.import_info'
- '@dkan.metastore.resource_mapper'
Drupal\dkan_datastore_preview\Service\ImportStatusMessage: '@dkan.datastore_preview.import_status_message'
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Drupal\dkan_datastore_preview\DataSource;

/**
* Contract for data preview sources.
*
* A data source resolves a resource identifier to tabular data. The MVP ships
* a single database implementation; this interface is the seam for a future
* plugin-based data source system.
*/
interface DataSourceInterface {

/**
* Internal datastore column that must never be displayed.
*/
const HIDDEN_FIELD = 'record_number';

/**
* Get the Drupal Schema API description of the resource's table.
*
* @param string $resource_id
* Resource identifier, in "identifier__version" format.
*
* @return array
* Schema array with a 'fields' key mapping machine names to field info
* ('type', 'description'). MUST return an empty array when the resource
* has no queryable table (not yet imported, unknown, etc.) and MUST NOT
* include self::HIDDEN_FIELD.
*/
public function getSchema(string $resource_id): array;

/**
* Fetch a page of data from the source.
*
* @param string $resource_id
* Resource identifier, in "identifier__version" format.
* @param int $limit
* Maximum number of rows to return.
* @param int $offset
* Number of rows to skip.
* @param string|null $sort_field
* Machine name of the column to sort by, or NULL for storage order.
* @param string $sort_direction
* Either 'asc' or 'desc'.
* @param array $conditions
* Filter conditions; each an array with 'property', 'value' and an
* optional 'operator' (defaults to '=').
* @param array $properties
* Machine names of columns to return (empty = all).
*
* @return \Drupal\dkan_datastore_preview\DataSource\DataSourceResult
* The rows and total count.
*/
public function fetchData(
string $resource_id,
int $limit,
int $offset,
?string $sort_field,
string $sort_direction,
array $conditions = [],
array $properties = [],
): DataSourceResult;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Drupal\dkan_datastore_preview\DataSource;

/**
* Value object holding one page of data source results.
*/
final class DataSourceResult {

/**
* Constructor.
*
* @param array $rows
* Row objects (\stdClass) keyed by column machine name.
* @param int $totalCount
* Total number of rows matching the conditions, ignoring limit/offset.
*/
public function __construct(
public readonly array $rows,
public readonly int $totalCount,
) {}

}
Loading