Skip to content

feat(catalog): enable logo file upload in theme editor#4781

Merged
drernie merged 18 commits into
masterfrom
feat/logo-file-upload
Apr 29, 2026
Merged

feat(catalog): enable logo file upload in theme editor#4781
drernie merged 18 commits into
masterfrom
feat/logo-file-upload

Conversation

@drernie
Copy link
Copy Markdown
Member

@drernie drernie commented Mar 24, 2026

What is changing

  • useUploadFile in CatalogSettings.tsx — writes the dropped file to catalog/logo.<ext> in the service bucket and returns its s3:// URL.
  • ThemeEditor.tsx — drops the useThirdPartyDomainForLogo gate so the drag-and-drop InputFile renders alongside the URL field.
  • Logo render component is unchanged; it already signs s3:// URLs.

Why

The theme editor previously accepted only an externally hosted logo URL — every operator had to host their logo image themselves, adding onboarding friction. The display path already supported s3:// URLs; the missing piece was a write path from the browser into the service bucket.

Verification

  • E2E on quilt-staging (us-east-1): 9/9 Playwright checks pass — drag-and-drop upload, S3 PUT capture, dialog close, navbar render.
  • Live smoke on tf-dev-unstable: drag-and-drop and pasted-URL flows both succeed end-to-end.

Notes

  • Old logo files at differing extensions (e.g. logo.png after upload of logo.jpg) are not cleaned up — orphaned files are inert once settings.json updates the URL.

Related PRs

Greptile Summary

This PR activates the logo file-upload path in the theme editor: useUploadFile now writes to catalog/logo.<ext> in the service bucket (keyed by MIME type, SVG intentionally excluded for XSS reasons), and InputFile gains a URL text field alongside the dropzone so operators can use either input method. Both the upload hook and the InputFile component are covered by new unit tests.

Confidence Score: 5/5

Safe to merge; no logic-breaking defects found, only minor P2 polish items.

All findings are P2 (missing alt attribute and unforwarded disabled prop). The core upload and URL-conversion logic is correct, MIME allowlist aligns with the IAM policy, and SVG is excluded with a clear security rationale. New tests cover the critical paths.

ThemeEditor.tsx — InputFile should accept and forward a disabled prop

Important Files Changed

Filename Overview
catalog/app/utils/CatalogSettings.tsx Implements useUploadFile: derives catalog/logo.<ext> key from MIME type, uploads via S3 putObject, returns S3ObjectLocation. SVG is intentionally excluded with a clear security comment. Allowlist is pinned to IAM constraints.
catalog/app/containers/Admin/Settings/ThemeEditor.tsx Removes the useThirdPartyDomainForLogo flag and dead code; renders InputFile unconditionally. Refactors InputFile to show both a dropzone and a URL text field. disabled prop from RF.Field is silently ignored, leaving fields interactive during submission.
catalog/app/utils/CatalogSettings.spec.tsx New test suite for useUploadFile: covers MIME-to-key mapping for all four accepted types, rejects SVG and empty MIME, and verifies filename is ignored in favour of MIME type.
catalog/app/containers/Admin/Settings/ThemeEditor.spec.tsx New InputFile unit tests: verifies empty state, URL string rendering, object-URL lifecycle (create + revoke on unmount), and text-field onChange wiring.

Sequence Diagram

sequenceDiagram
    actor Operator
    participant InputFile
    participant RF.Field
    participant onSubmit
    participant useUploadFile
    participant S3
    participant useWriteSettings

    Operator->>InputFile: Drop image file
    InputFile->>RF.Field: onChange(File)
    Operator->>InputFile: (or) type URL
    InputFile->>RF.Field: onChange(string)

    Operator->>onSubmit: click Save
    alt value is File
        onSubmit->>useUploadFile: uploadFile(File)
        useUploadFile->>S3: putObject(catalog/logo.<ext>)
        S3-->>useUploadFile: VersionId
        useUploadFile-->>onSubmit: S3ObjectLocation
        onSubmit->>onSubmit: handleToS3Url
    else value is string URL
        onSubmit->>onSubmit: use string as-is
    end
    onSubmit->>useWriteSettings: writeSettings
    useWriteSettings->>S3: putObject(catalog/settings.json)
    S3-->>useWriteSettings: ok
    onSubmit-->>Operator: dialog closes
Loading

Reviews (2): Last reviewed commit: "fix(catalog): widen logoUrl type and gua..." | Re-trigger Greptile

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review.

Tip: disable this comment in your organization's Code Review settings.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 82.50000% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 45.83%. Comparing base (cea497e) to head (915893b).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
...alog/app/containers/Admin/Settings/ThemeEditor.tsx 73.07% 4 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4781      +/-   ##
==========================================
+ Coverage   45.69%   45.83%   +0.13%     
==========================================
  Files         829      829              
  Lines       33532    33551      +19     
  Branches     5698     5702       +4     
==========================================
+ Hits        15323    15377      +54     
+ Misses      16212    16178      -34     
+ Partials     1997     1996       -1     
Flag Coverage Δ
api-python 93.14% <ø> (ø)
catalog 19.78% <82.50%> (+0.23%) ⬆️
lambda 96.63% <ø> (ø)
py-shared 98.18% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread catalog/app/containers/Admin/Settings/ThemeEditor.tsx Outdated
@drernie
Copy link
Copy Markdown
Member Author

drernie commented Mar 25, 2026

Test results against quilt-staging (us-east-1)

All 9/9 checks passed after running the E2E test suite with a headed Playwright browser against the live nightly stack.

Results

Check Result
Catalog loads authenticated
Admin Settings page loads with Theme section
Drop-zone InputFile rendered (not URL text field)
URL text field absent
Logo preview visible after file selection
Dialog closed after Save
S3 PUT to catalog/logo.* captured
Logo image visible in navbar
settings.json persisted with s3://…/catalog/logo.png

Infrastructure change required

The catalog-config inline policy on the ReadWriteQuiltV2 IAM role only allowed s3:PutObject on catalog/settings.json. The logo upload writes to catalog/logo.<ext>, which was blocked (403). The policy needs one additional statement in the stack template that provisions this role:

{
  "Action": "s3:PutObject",
  "Resource": "arn:aws:s3:::${ServiceBucket}/catalog/logo.*",
  "Effect": "Allow"
}

Applied to staging manually for testing; needs to be added to the stack CDK/CFN definition before deploying to other stacks.

drernie and others added 5 commits April 25, 2026 21:34
Implement useUploadFile to upload the logo image to the service bucket
at catalog/logo.<ext> and return an s3:// URL, then wire it up by
removing the useThirdPartyDomainForLogo flag so the drag-and-drop
InputFile component is shown instead of the URL text field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pre-existing formatting issues caught by CI lint check after
lock file regeneration pulled updated prettier rules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-existing formatting issues detected by CI's prettier 3.4.2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The flag was a temporary guard for the URL-only logo input. Now that
file upload is implemented, remove the flag, the unused URL-input
branch, and the unused Form import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…itor

Allow admins to paste a logo URL in addition to drag-and-drop file upload,
improving flexibility for theme customization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@drernie drernie force-pushed the feat/logo-file-upload branch from 2d3f855 to a4f4cd8 Compare April 26, 2026 04:34
@drernie drernie requested a review from Copilot April 27, 2026 23:58
@drernie drernie requested a review from fiskus April 28, 2026 00:50
@drernie drernie self-assigned this Apr 28, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Member

@fiskus fiskus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand, how does it work, if it works.
I see that ThemeEditor uploads the file, and it returns s3:// URL and writes it to CatalogSettings. But we use that URL in app/containers/NavBar/NavBar.tsx to render the image. Shouldn't that URL be https://bucket/proxy/something instead of s3://?

Ah, I see

Comment thread .github/workflows/deploy-catalog.yaml Outdated
Comment thread catalog/app/utils/CatalogSettings.tsx Outdated
Comment thread catalog/app/utils/CatalogSettings.tsx Outdated
drernie and others added 3 commits April 28, 2026 06:25
Covers extension-based S3 key computation, no-extension fallback,
and multi-dot filename handling to satisfy codecov patch coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ThemeEditor: revoke object URLs in InputFile preview to fix memory leak
- ThemeEditor: restrict logo dropzone to image/* files
- CatalogSettings.spec: type putObject mock so call args are typed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers placeholder, URL string, File preview with createObjectURL/
revokeObjectURL lifecycle, and URL text-field onChange.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@drernie drernie force-pushed the feat/logo-file-upload branch from a4f4cd8 to 638e6ef Compare April 28, 2026 13:25
@drernie
Copy link
Copy Markdown
Member Author

drernie commented Apr 28, 2026

Split workflow_dispatch change out into #4861 per review feedback. Rebased and force-pushed to drop commit 97c66e8 from this branch.

@drernie drernie enabled auto-merge April 28, 2026 13:42
…ploadFile

Per fiskus review (PR 4781):
- Use `extname` from `path` instead of hand-rolled split logic.
- Capture `VersionId` from `putObject` and return `S3ObjectLocation`
  ({bucket, key, version}) instead of a stringified s3:// URL; the
  caller stringifies via `s3paths.handleToS3Url`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@drernie drernie requested a review from fiskus April 28, 2026 13:46
Copy link
Copy Markdown
Member

@fiskus fiskus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<Logo/> is able to show blob URLs, so we can simplify the state and components render: #4863

Comment thread catalog/app/utils/CatalogSettings.spec.tsx Outdated
Comment thread catalog/app/utils/CatalogSettings.tsx Outdated
Comment thread catalog/app/utils/CatalogSettings.spec.tsx Outdated
Comment thread catalog/app/utils/CatalogSettings.spec.tsx Outdated
Comment thread catalog/app/containers/Admin/Settings/ThemeEditor.tsx Outdated
Comment thread catalog/app/containers/Admin/Settings/ThemeEditor.tsx Outdated
Comment thread catalog/app/containers/Admin/Settings/ThemeEditor.tsx Outdated
drernie and others added 2 commits April 28, 2026 10:42
Co-authored-by: Maksim Chervonnyi <mail@redmax.dev>
- Use root style for grid layout instead of empty object
- Add error handling and meta props to InputFile component
- Use renderHook from @testing-library/react-hooks in tests
- Remove unused Model import
drernie and others added 5 commits April 28, 2026 11:25
Per fiskus review (PR #4781): switch to `import type * as Model from
'model'` so the upload return type and spec variable both reference
`Model.S3.S3ObjectLocation`. Replaces the broken `AWS.S3ObjectLocation`
introduced in bb72444, which doesn't exist on the AWS namespace and
broke the test-catalog build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Grid gap already provides spacing; the margin doubled it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@drernie drernie requested a review from fiskus April 29, 2026 13:40
@fiskus fiskus disabled auto-merge April 29, 2026 13:47
@fiskus
Copy link
Copy Markdown
Member

fiskus commented Apr 29, 2026

Decision to merge is up to you, but consider that after merging this PR, frontend will be in a slightly broken state: logo change would not work.

I disabled auto-merge for now.

fiskus
fiskus previously approved these changes Apr 29, 2026
@drernie
Copy link
Copy Markdown
Member Author

drernie commented Apr 29, 2026

Happy to wait on this merge until deployment is approved.
BUT: "frontend will be in a slightly broken state" -- this confuses me.

I agree that technically it is inconsistent, but no deployment will actually be "broken" until some stack actually pins the new image, right? Which is precisely what the deployment PR does.

Derive the upload key from file.type (MIME) rather than extname(file.name),
restrict the dropzone to the same allowlist, and reject anything else.
Drops SVG (inline <script> in SVG executes on direct navigation, which is
exactly the public-bucket scenario the IAM grant covers). Also fixes an
empty-extension AccessDenied when files arrived without a filename
extension (clipboard pastes, stripped names) — the pre-MIME logic produced
key='catalog/logo' which doesn't match the IAM allowlist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

catalog/app/containers/Admin/Settings/ThemeEditor.tsx:136

  • onDrop unconditionally calls onChange(files[0]). When a user drops a rejected file type (or no accepted files), files can be empty and this will set the form value to undefined (clearing a previously set URL/file). Guard on files.length (and optionally surface fileRejections for feedback) before calling onChange.
  const onDrop = React.useCallback(
    (files: FileWithPath[]) => {
      onChange(files[0])
    },

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread catalog/app/containers/Admin/Settings/ThemeEditor.tsx Outdated
Comment thread catalog/app/containers/Admin/Settings/ThemeEditor.spec.tsx
Address Copilot review on PR #4781:
- onSubmit values type now allows FileWithPath alongside string, so the
  upload branch type-checks accurately rather than narrowing to never.
- onDrop guards files.length so a rejected/empty drop no longer clears
  a previously set logo value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fiskus
Copy link
Copy Markdown
Member

fiskus commented Apr 29, 2026

@greptileai please re-review

Copy link
Copy Markdown
Member

@sir-sigurd sir-sigurd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was generated with Claude. I don't have frontend expertise — take at your own risk.

LGTM on the narrow-scope delta: the diff between 92fa91e6 (fiskus's last review touchpoint) and 915893b5 (current head) — two commits, 68246c09 (pin the MIME allowlist) and 915893b5 (widen logoUrl type + guard empty drops).

What I checked:

  • LOGO_MIME_TO_EXT keys + UnsupportedLogoTypeError align with the deployment side's CATALOG_LOGO_EXTENSIONS / CATALOG_LOGO_CONTENT_TYPES / s3:content-type IAM Condition. ContentType: file.type always set.
  • Dropzone accept derived from ACCEPTED_LOGO_MIME_TYPES — single source of truth across upload, dropzone, and the IAM allowlist on the deployment side.
  • onSubmit type-narrowing reads cleanly through string, '', FileWithPath, undefined.
  • onDrop empty-guard prevents clearing a previously-set logo on rejected/empty drops.
  • Tests cover MIME→key for the four allowed types, SVG rejection, empty-MIME rejection, filename-ignored.

Minor / take-or-leave:

  • UnsupportedLogoTypeError thrown from useUploadFile falls through onSubmit's generic catch as "Couldn't save settings, see console for details." The dropzone's accept filter is keyed on the same MIME allowlist, so this exception shouldn't fire from the UI in practice — but if it ever does (test path, future caller, browser MIME-sniffing oddity), the user gets a misleading error. Could surface a specific message via instanceof UnsupportedLogoTypeError in the catch.

Not in scope: the broader PR diff and Greptile's P2 polish items.

@drernie drernie added this pull request to the merge queue Apr 29, 2026
Merged via the queue into master with commit 837b9ae Apr 29, 2026
46 checks passed
@drernie drernie deleted the feat/logo-file-upload branch April 29, 2026 19:10
@nl0
Copy link
Copy Markdown
Member

nl0 commented Apr 29, 2026

@drernie no changelog entry -- intentional?

pull Bot pushed a commit to admariner/quilt that referenced this pull request Apr 30, 2026
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants