Skip to content

Commit

Permalink
Implement PWA Collector and profiler views (#162)
Browse files Browse the repository at this point in the history
* Implement PWA Collector and additional viewing templates

Implemented a new Progressive Web App (PWA) Collector to fetch and manage service worker and manifest details. Additionally, created multiple HTML templates for manifest, service worker, and other PWA tabs to properly display the collected data in UI.
  • Loading branch information
Spomky authored Apr 7, 2024
1 parent e518dac commit 27b1587
Show file tree
Hide file tree
Showing 7 changed files with 576 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/exported_files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: "Check exported files"
run: |
EXPECTED="LICENSE,README.md,RELEASES.md,SECURITY.md,composer.json"
CURRENT="$(git archive HEAD | tar --list --exclude="assets" --exclude="assets/*" --exclude="src" --exclude="src/*" | paste -s -d ",")"
CURRENT="$(git archive HEAD | tar --list --exclude="assets" --exclude="assets/*" --exclude="src" --exclude="src/*" --exclude="templates" --exclude="templates/*" | paste -s -d ",")"
echo "CURRENT =${CURRENT}"
echo "EXPECTED=${EXPECTED}"
test "${CURRENT}" == "${EXPECTED}"
121 changes: 121 additions & 0 deletions src/DataCollector/PwaCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\DataCollector;

use SpomkyLabs\PwaBundle\CachingStrategy\CacheStrategyInterface;
use SpomkyLabs\PwaBundle\CachingStrategy\HasCacheStrategiesInterface;
use SpomkyLabs\PwaBundle\Dto\Manifest;
use SpomkyLabs\PwaBundle\Dto\ServiceWorker;
use SpomkyLabs\PwaBundle\Dto\Workbox;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\VarDumper\Cloner\Data;
use Throwable;
use function count;
use function in_array;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;

final class PwaCollector extends DataCollector
{
/**
* @param iterable<HasCacheStrategiesInterface> $cachingServices
*/
public function __construct(
private readonly SerializerInterface $serializer,
#[TaggedIterator('spomky_labs_pwa.cache_strategy')]
private readonly iterable $cachingServices,
private readonly Manifest $manifest,
private readonly ServiceWorker $serviceWorker,
#[Autowire(param: 'spomky_labs_pwa.manifest.enabled')]
private readonly bool $manifestEnabled,
) {
}

public function collect(Request $request, Response $response, Throwable $exception = null): void
{
$jsonOptions = [
AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => true,
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
JsonEncode::OPTIONS => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT,
];
$this->data['cachingStrategies'] = [];
foreach ($this->cachingServices as $cachingService) {
foreach ($cachingService->getCacheStrategies() as $cacheStrategy) {
$this->data['cachingStrategies'][] = $cacheStrategy;
}
}
$this->data['serviceWorker'] = $this->serviceWorker;
$this->data['manifest'] = [
'enabled' => $this->manifestEnabled,
'data' => $this->manifest,
'installable' => $this->isInstallable(),
'output' => $this->serializer->serialize($this->manifest, 'json', $jsonOptions),
];
}

/**
* @return array<string, mixed>|Data
*/
public function getData(): array|Data
{
return $this->data;
}

/**
* @return array<CacheStrategyInterface>
*/
public function getCachingStrategies(): array
{
return $this->data['cachingStrategies'] ?? [];
}

public function getManifest(): Manifest
{
return $this->data['manifest']['data'];
}

public function getWorkbox(): Workbox
{
return $this->data['serviceWorker']->workbox;
}

public function getName(): string
{
return 'pwa';
}

/**
* @return array{status: bool, reasons: array<string, bool>}
*/
private function isInstallable(): array
{
$reasons = [
'The manifest must be enabled' => ! $this->manifestEnabled,
'The manifest must have a short name or a name' => $this->manifest->shortName === null && $this->manifest->name === null,
'The manifest must have a start URL' => $this->manifest->startUrl === null,
'The manifest must have a display value set to "standalone", "fullscreen" or "minimal-ui' => ! in_array(
$this->manifest->display,
['standalone', 'fullscreen', 'minimal-ui'],
true
),
'The manifest must have at least one icon' => count($this->manifest->icons) === 0,
'The manifest must have the "prefer_related_applications" property set to a value other than "true"' => $this->manifest->preferRelatedApplications === true,
];

return [
'status' => count(array_filter($reasons, fn (bool $v): bool => $v)) === 0,
'reasons' => $reasons,
];
}
}
14 changes: 12 additions & 2 deletions src/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use SpomkyLabs\PwaBundle\Command\CreateIconsCommand;
use SpomkyLabs\PwaBundle\Command\CreateScreenshotCommand;
use SpomkyLabs\PwaBundle\Command\ListCacheStrategiesCommand;
use SpomkyLabs\PwaBundle\DataCollector\PwaCollector;
use SpomkyLabs\PwaBundle\Dto\Manifest;
use SpomkyLabs\PwaBundle\Dto\ServiceWorker;
use SpomkyLabs\PwaBundle\ImageProcessor\GDImageProcessor;
Expand All @@ -28,8 +29,8 @@
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;

return static function (ContainerConfigurator $container): void {
$container = $container->services()
return static function (ContainerConfigurator $configurator): void {
$container = $configurator->services()
->defaults()
->private()
->autoconfigure()
Expand Down Expand Up @@ -124,4 +125,13 @@
->tag('spomky_labs_pwa.match_callback_handler')
;
$container->load('SpomkyLabs\\PwaBundle\\MatchCallbackHandler\\', '../../MatchCallbackHandler/*');

if ($configurator->env() !== 'prod') {
$container->set(PwaCollector::class)
->tag('data_collector', [
'template' => '@SpomkyLabsPwa/Collector/template.html.twig',
'id' => 'pwa',
])
;
}
};
170 changes: 170 additions & 0 deletions templates/Collector/manifest-tab.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<h3>General information</h3>
<p>
Status:
{% if collector.data.manifest.enabled %}
<span class="status-badge status-success">enabled</span>
{% else %}
<span class="status-badge status-warning">disabled</span>
{% endif %}
<br>
Can be installed:
{% if collector.data.manifest.installable.status %}
<span class="status-badge status-success">yes</span>
{% else %}
<span class="status-badge status-warning">no</span>
{% endif %}
</p>
<ul>
{% for reason, value in collector.data.manifest.installable.reasons %}
<li>
{% if not value %}
<span class="badge status-success">success</span>
{% else %}
<span class="badge status-error">failure</span>
{% endif %}:
{{ reason }}
</li>
{% endfor %}
</ul>
<h3>Details</h3>
<table>
<thead>
<tr>
<th scope="col" class="key">Key</th>
<th scope="col">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>ID</td>
<td>{{ collector.getManifest().id }}</td>
</tr>
<tr>
<td>Name</td>
<td>{{ collector.getManifest().name|trans }}</td>
</tr>
<tr>
<td>Short name</td>
<td>{{ collector.getManifest().shortName|trans }}</td>
</tr>
<tr>
<td>Description name</td>
<td>{{ collector.getManifest().description|trans }}</td>
</tr>
<tr>
<td>Theme color</td>
<td>
{% if collector.getManifest().themeColor %}
{{ collector.getManifest().themeColor }}
<span style="background-color: {{ collector.getManifest().themeColor }}; padding: 0.25rem; color: {{ collector.getManifest().themeColor }};">{{ collector.getManifest().themeColor }}</span>
{% else %}
n/a
{% endif %}
</td>
</tr>
<tr>
<td>Background color</td>
<td>
{% if collector.getManifest().backgroundColor %}
{{ collector.getManifest().backgroundColor }}
<span style="background-color: {{ collector.getManifest().backgroundColor }}; padding: 0.25rem; color: {{ collector.getManifest().backgroundColor }};">{{ collector.getManifest().backgroundColor }}</span>
{% else %}
n/a
{% endif %}
</td>
</tr>
<tr>
<td>Display</td>
<td>{{ collector.getManifest().display }}</td>
</tr>
<tr>
<td>Orientation</td>
<td>{{ collector.getManifest().orientation }}</td>
</tr>
<tr>
<td>Scope</td>
<td>{{ collector.getManifest().scope }}</td>
</tr>
<tr>
<td>Start URL</td>
<td>{{ collector.getManifest().startUrl }}</td>
</tr>
<tr>
<td>Categories</td>
<td>
{% if collector.getManifest().categories|length == 0 %}
<span class="badge">none</span>
{% else %}
<ul>
{% for category in collector.getManifest().categories %}
<li>{{ category|trans }}</li>
{% endfor %}
</ul>
{% endif %}
</td>
</tr>
</tbody>
</table>
<h3 class="tab-title">Application Icons</h3>
<div class="tab-content">
<table>
<thead>
<tr>
<th scope="col" class="key">Source</th>
<th scope="col">Sizes</th>
<th scope="col">Type</th>
<th scope="col">Purpose</th>
</tr>
</thead>
<tbody>
{% for icon in collector.getManifest().icons %}
<tr>
<td>{{ icon.src.src }}</td>
<td>{{ icon.getSizeList() }}</td>
<td>{{ icon.type|default('n/a') }}</td>
<td>{{ icon.purpose|default('n/a') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<h3 class="tab-title">Application screenshots</h3>
<div class="tab-content">
<table>
<thead>
<tr>
<th scope="col" class="key">Source</th>
<th scope="col">Size</th>
<th scope="col">Type</th>
<th scope="col">Form factor</th>
<th scope="col">Label</th>
<th scope="col">Platform</th>
</tr>
</thead>
<tbody>
{% for screenshot in collector.getManifest().screenshots %}
<tr>
<td>
{{ screenshot.src.src }}<br>
<span class="badge">{{ screenshot.reference|default('No reference') }}</span>
</td>
<td>
{% if screenshot.height and screenshot.width %}
{{ screenshot.width }}x{{ screenshot.height }}
{% else %}
n/a
{% endif %}
</td>
<td>{{ screenshot.type|default('n/a') }}</td>
<td>{{ screenshot.formFactor|default('n/a') }}</td>
<td>{{ screenshot.label|default('n/a') }}</td>
<td>{{ screenshot.platform|default('n/a') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<h3 class="tab-title">Output</h3>
<div class="tab-content">
<pre class="output">{{ collector.data.manifest.output|nl2br }}</pre>
</div>
12 changes: 12 additions & 0 deletions templates/Collector/pwa.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 27b1587

Please sign in to comment.