From 5c286c3334820beef894438eab12299e88cd19e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 8 Feb 2025 15:53:55 +0100 Subject: [PATCH] Implement preview branding --- app/controllers/general.php | 18 ++- app/http.php | 2 + public/scripts/preview.js | 129 ++++++++++++++++++ .../Modules/Sites/Transformers/Preview.php | 58 ++++++++ tests/e2e/Services/Sites/SitesBase.php | 23 ++++ .../Services/Sites/SitesCustomServerTest.php | 59 ++++++++ 6 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 public/scripts/preview.js create mode 100644 src/Appwrite/Platform/Modules/Sites/Transformers/Preview.php diff --git a/app/controllers/general.php b/app/controllers/general.php index 6764cbae40..18dc81a102 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -11,6 +11,7 @@ use Appwrite\Event\Usage; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Network\Validator\Origin; use Appwrite\Platform\Appwrite; +use Appwrite\Platform\Modules\Sites\Transformers\Preview; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; @@ -130,7 +131,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw }; } - if ($type === 'function' || $type === 'site') { + if ($type === 'function' || $type === 'site' || $type === 'deployment') { $method = $utopia->getRoute()?->getLabel('sdk', null); if (empty($method)) { @@ -446,6 +447,21 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw requestTimeout: 30 ); + + // Branded banner for previews + if ($type === 'deployment' && Preview::isValid($executionResponse['headers'])) { + $transformer = new Preview($executionResponse['body']); + $transformer->transform(); + $executionResponse['body'] = $transformer->getBody(); + + $execution->setAttribute('responseBody', $body); + foreach ($executionResponse['headers'] as $key => $value) { + if (\strtolower($key) === 'content-length') { + $executionResponse['headers'][$key] = \strlen($executionResponse['body']); + } + } + } + $headersFiltered = []; foreach ($executionResponse['headers'] as $key => $value) { if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { diff --git a/app/http.php b/app/http.php index 608ac2ec12..611e373ffa 100644 --- a/app/http.php +++ b/app/http.php @@ -35,6 +35,8 @@ $domains = new Table(1_000_000); // 1 million rows $domains->column('value', Table::TYPE_INT, 1); $domains->create(); +Files::load(__DIR__ . '/../public'); + $http = new Server( host: "0.0.0.0", port: System::getEnv('PORT', 80), diff --git a/public/scripts/preview.js b/public/scripts/preview.js new file mode 100644 index 0000000000..66eac6153d --- /dev/null +++ b/public/scripts/preview.js @@ -0,0 +1,129 @@ +var banner = document.createElement('button'); +banner.id = 'appwrite-preview'; +banner.innerHTML = ` +

Preview by

+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; +banner.addEventListener("click", function() { + banner.style.opacity = 0; + setTimeout(() => { + banner.style.display = 'none'; + }, 350); +}); + +var css = document.createElement('style'); +css.innerHTML += ` +#appwrite-preview { + padding: 0; + margin: 0; + position: absolute; + right: 16px; + bottom: 16px; + z-index: 1; + border-radius: var(--border-radius-S, 8px); + border: var(--border-width-S, 1px) solid var(--color-border-neutral, #EDEDF0); + background: var(--color-bgColor-neutral-primary, #FFF); + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.03), 0px 4px 4px 0px rgba(0, 0, 0, 0.04); + padding: var(--space-3, 6px) var(--space-4, 8px); + display: flex; + justify-content: center; + align-items: center; + gap: var(--gap-XXS, 4px); + cursor: pointer; + + transition: opacity 0.3s; +} + +#appwrite-preview-close { + position: absolute; + right: 0px; + bottom: 0px; + border-radius: var(--border-radius-S, 8px); + background: linear-gradient(270deg, #FFF 69.64%, rgba(255, 255, 255, 0.00) 114.29%); + height: 100%; + aspect-ratio: 1 / 1; + display: flex; + justify-content: center; + align-items: center; + opacity: 0; + transition: opacity 0.3s; +} + +#appwrite-preview-logo-dark { + display: none; +} + +#appwrite-preview:hover #appwrite-preview-close { + opacity: 1; +} + +#appwrite-preview-text { + padding: 0; + margin: 0; + color: var(--color-fgColor-neutral-secondary, #56565C); + font-family: var(--font-family-sansSerif, Inter); + font-size: var(--font-size-XS, 12px); + font-style: normal; + font-weight: 500; + line-height: 130%; + letter-spacing: -0.12px; +} + +@media (prefers-color-scheme: dark) { + #appwrite-preview { + border: var(--border-width-S, 1px) solid var(--color-border-neutral, #2D2D31); + background: var(--color-bgColor-neutral-primary, #1D1D21); + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.03), 0px 4px 4px 0px rgba(0, 0, 0, 0.04); + } + + #appwrite-preview-text { + color: var(--color-fgColor-neutral-secondary, #C3C3C6); + font-family: var(--font-family-sansSerif, Inter); + font-size: var(--font-size-XS, 12px); + } + + #appwrite-preview-logo-dark { + display: block; + } + + #appwrite-preview-logo-light { + display: none; + } +} +`; + +document.head.appendChild(css); +document.body.appendChild(banner); \ No newline at end of file diff --git a/src/Appwrite/Platform/Modules/Sites/Transformers/Preview.php b/src/Appwrite/Platform/Modules/Sites/Transformers/Preview.php new file mode 100644 index 0000000000..365ce70d18 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Transformers/Preview.php @@ -0,0 +1,58 @@ + $headers + */ + public static function isValid(array $headers): bool + { + $contentType = ''; + + foreach ($headers as $key => $value) { + if (\strtolower($key) === 'content-type') { + $contentType = $value; + break; + } + } + + if (\str_contains($contentType, 'text/html')) { + return true; + } + + return false; + } + + public function transform(): bool + { + $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; + $hostname = System::getEnv('_APP_DOMAIN'); + + // TODO: Temporary fix for development + if (App::isDevelopment()) { + $hostname = 'localhost'; + } + + $source = "{$protocol}://{$hostname}/scripts/preview.js"; + + $this->body .= ''; + + return true; + } + + public function getBody(): string + { + return $this->body; + } +} diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 277064ead5..88805317fd 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -238,4 +238,27 @@ trait SitesBase return $domain; } + + + protected function getDeploymentDomain(string $deploymentId): string + { + $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::equal('resourceId', [$deploymentId])->toString(), + Query::equal('resourceType', ['deployment'])->toString(), + ], + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertGreaterThanOrEqual(1, $rules['body']['total']); + $this->assertGreaterThanOrEqual(1, \count($rules['body']['rules'])); + $this->assertNotEmpty($rules['body']['rules'][0]['domain']); + + $domain = $rules['body']['rules'][0]['domain']; + + return $domain; + } } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index ff3aa7b348..e471651ef6 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1088,5 +1088,64 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($site['body']['$id']); } + public function testSitePreviewBranding(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'A site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertNotEmpty($siteId); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => 'true' + ]); + + $this->assertNotEmpty($deploymentId); + + $domain = $this->getSiteDomain($siteId); + $previewDomain = $this->getDeploymentDomain($deploymentId); + + $this->assertNotEmpty($domain); + $this->assertNotEmpty($previewDomain); + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Hello Appwrite", $response['body']); + $this->assertStringNotContainsString("setEndpoint('http://' . $previewDomain); + + $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Hello Appwrite", $response['body']); + $this->assertStringContainsString("assertGreaterThan($contentLength, $response['headers']['content-length']); + + $this->cleanupSite($siteId); + } + // TODO: Add tests for deletion of resources when site is deleted }