Skip to content
This repository was archived by the owner on Jul 15, 2025. It is now read-only.

feat: add layer visualization table #179

Merged
merged 10 commits into from
Apr 1, 2025
Merged

Conversation

kmendell
Copy link
Member

@kmendell kmendell commented Apr 1, 2025

Summary by Sourcery

Add layer visualization feature for Docker image metadata, including database schema updates and UI components to display layer composition

New Features:

  • Implement layer visualization table to show Docker image layer composition
  • Add detailed layer size breakdown in image details page

Enhancements:

  • Refactor TagModel to use static methods
  • Improve metadata parsing and handling in database models
  • Add more robust JSON parsing for metadata fields

Build:

  • Add database migration to support layer information storage

Tests:

  • Ensure layer data can be correctly parsed and stored
  • Validate layer visualization rendering

Copy link
Contributor

sourcery-ai bot commented Apr 1, 2025

Reviewer's Guide by Sourcery

This pull request introduces a new LayerVisualization component to display the size distribution of container image layers. It also adds a layers field to the database schema and data models to store image layer information. The manifest fetching logic is updated to extract and include layer information in the tag metadata. The TagModel was refactored to be a class with static methods. JSON parsing and data handling for command, entrypoint, and layers fields were improved.

Updated class diagram for TagModel

classDiagram
    class TagModel {
        +static create(imageId: number, name: string, digest: string): number
        +static getById(id: number): Tag | null
        +static getByImageId(imageId: number): Tag[]
        +static findByName(imageId: number, name: string): Tag | undefined
        +static saveMetadata(tagId: number, metadata: TagMetadata): void
        +static getWithMetadata(tagId: number): TagWithMetadata | undefined
        +static delete(id: number): void
        +static deleteByDigest(digest: string): void
        +static countByImage(imageId: number): number
        +static clear(): void
    }

    class Tag {
        id: number
        imageId: number
        name: string
        digest: string
        createdAt: Date
    }

    class TagMetadata {
        created: string
        os: string
        architecture: string
        author: string
        dockerFile: string
        exposedPorts: string[]
        totalSize: string
        workDir: string
        command: string
        description: string
        contentDigest: string
        entrypoint: string
        indexDigest: string
        isOCI: boolean
        layers: ImageLayer[]
    }

    class ImageLayer {
        size: number
        digest: string
    }

    class TagWithMetadata extends Tag {
        metadata: TagMetadata
    }

    TagModel -- Tag : returns
    TagModel -- TagWithMetadata : returns
    TagWithMetadata *-- TagMetadata : has
    TagMetadata *-- ImageLayer : contains
Loading

File-Level Changes

Change Details Files
Introduces a new LayerVisualization component to display the size distribution of container image layers.
  • Created LayerVisualization.svelte component.
  • Added a progress bar to visualize layer sizes.
  • Included layer details such as digest and size.
  • Added the component to the details page to display layer information.
src/routes/details/[repo]/[image]/[tag]/+page.svelte
src/lib/components/docker-metadata/LayerVisualization.svelte
src/lib/components/ui/progress/progress.svelte
src/lib/components/ui/progress/index.ts
Adds layers field to the database schema and data models to store image layer information.
  • Added layers column to the tag_metadata table in the database.
  • Modified TagMetadata interface in src/lib/types/db/models.ts to include a layers property.
  • Updated TagWithMetadataRecord interface in src/lib/types/db/records.ts to include meta_layers property.
  • Updated the database migration to add the layers column.
  • Modified the server load function to fetch and parse the layers data from the database.
  • Updated the TagMetadata type in src/lib/types/api/manifest.ts to include layers
src/lib/services/database/models/tag.ts
src/lib/types/db/models.ts
src/lib/types/api/manifest.ts
src/routes/details/[repo]/[image]/[tag]/+page.server.ts
src/lib/services/database/migrations.ts
src/lib/types/db/records.ts
Updates the manifest fetching logic to extract and include layer information in the tag metadata.
  • Modified fetchDockerMetadata function to extract layer size and digest information from the manifest.
  • Included the extracted layer information in the returned TagMetadata object.
src/lib/utils/api/manifest.ts
Refactors the TagModel to be a class with static methods.
  • Changed TagModel from a const object to a class.
  • Made all TagModel methods static.
src/lib/services/database/models/tag.ts
Improves JSON parsing and data handling for command, entrypoint, and layers fields.
  • Added parseJSON helper function to safely parse JSON strings with a default value.
  • Added parseCommandOrEntrypoint helper function to parse command/entrypoint values.
  • Utilized parseJSON to parse the layers data from the database.
  • Added debug logging for layers data.
src/lib/services/database/models/tag.ts
src/routes/details/[repo]/[image]/[tag]/+page.server.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!
  • Generate a plan of action for an issue: Comment @sourcery-ai plan on
    an issue to generate a plan of action for it.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @kmendell - I've reviewed your changes - here's some feedback:

Overall Comments:

  • Consider adding a database migration to populate the layers column for existing tags.
  • The parseJSON and parseCommandOrEntrypoint functions are used in multiple places; consider creating a dedicated utility module for them.
Here's what I looked at during the review
  • 🟢 General issues: all looks good
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

if (!result) return undefined;

// Parse JSON fields and format data
static getWithMetadata(tagId: number): TagWithMetadata | undefined {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider using a single join query to fetch tag and metadata, which simplifies the code and reduces database interactions, unless fine-grained logging or separate handling of missing metadata is required, in which case, consider using a debug flag for development-only logging to keep the production path cleaner..

The refactor’s decomposition of the metadata lookup into two separate queries (versus the original single join) and the numerous debug logs do add extra branching. If you don’t require the fine‑grained logging or separate handling of missing metadata, you could simplify by reverting to a single join query—while retaining the extra layers field parsing. For example:

static getWithMetadata(tagId: number): TagWithMetadata | undefined {
  const result = db.prepare(
    `SELECT
      t.id,
      t.image_id,
      t.name,
      t.digest,
      t.created_at,
      tm.created_at as meta_created,
      tm.os as meta_os,
      tm.architecture as meta_architecture,
      tm.author as meta_author,
      tm.dockerFile as meta_dockerFile,
      tm.exposedPorts as meta_exposedPorts,
      tm.totalSize as meta_totalSize,
      tm.workDir as meta_workDir,
      tm.command as meta_command,
      tm.description as meta_description,
      tm.contentDigest as meta_contentDigest,
      tm.entrypoint as meta_entrypoint,
      tm.isOCI as meta_isOCI,
      tm.indexDigest as meta_indexDigest,
      tm.layers as meta_layers
    FROM tags t
    LEFT JOIN tag_metadata tm ON tm.tag_id = t.id
    WHERE t.id = ?`
  ).get(tagId) as any;

  if (!result) return undefined;

  return {
    id: result.id,
    imageId: result.image_id,
    name: result.name,
    digest: result.digest,
    createdAt: new Date(result.created_at),
    metadata: result.meta_created ? {
      created: result.meta_created,
      os: result.meta_os,
      architecture: result.meta_architecture,
      author: result.meta_author,
      dockerFile: result.meta_dockerFile,
      exposedPorts: parseJSON(result.meta_exposedPorts, []),
      totalSize: result.meta_totalSize,
      workDir: result.meta_workDir,
      command: result.meta_command,
      description: result.meta_description,
      contentDigest: result.meta_contentDigest,
      entrypoint: result.meta_entrypoint,
      isOCI: result.meta_isOCI === 1,
      indexDigest: result.meta_indexDigest,
      layers: parseJSON(result.meta_layers, [])
    } : undefined
  };
}

This approach avoids a second database round-trip and reduces extra logging unless needed. If the additional logging is useful only during development, consider wrapping it in a debug flag to keep the production path cleaner.

export const TagModel = {
// Helper function to parse JSON safely - moved to the top to avoid circular imports
export function parseJSON<T>(str: string | null, defaultValue: T): T {
if (!str) return defaultValue;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (!str) return defaultValue;
if (!str) {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).


// Helper to parse command/entrypoint
export function parseCommandOrEntrypoint(value: string | null): string | string[] | null {
if (!value) return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (!value) return null;
if (!value) {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).

static getWithMetadata(tagId: number): TagWithMetadata | undefined {
const tag = db.prepare('SELECT * FROM tags WHERE id = ?').get(tagId) as TagRecord | undefined;

if (!tag) return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (!tag) return undefined;
if (!tag) {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).

@kmendell kmendell requested a review from Copilot April 1, 2025 04:33
Copy link

@Copilot 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

This PR introduces a new layer visualization feature for Docker image metadata, including database schema updates and UI components. Key changes include adding tests for layer visualization, updating API types and database models to support layer data, and refactoring the TagModel to static methods with additional helper functions.

Reviewed Changes

Copilot reviewed 13 out of 16 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/e2e/registry.spec.ts Adds end-to-end tests for layer visualization rendering and error handling when layer data is missing
src/routes/details/[repo]/[image]/[tag]/+page.server.ts Updates the load function to include parsing of layer JSON data from the database
src/lib/utils/api/manifest.ts Extracts and maps layer information from Docker manifest responses
src/lib/types/db/records.ts Renames metadata fields for consistency and adds a new "layers" field
src/lib/types/db/models.ts Updates the TagMetadata interface to include required fields and the new layer information
src/lib/types/api/manifest.ts Defines the ImageLayer interface and enhances TagMetadata with optional layer data
src/lib/services/database/models/tag.ts Refactors TagModel into a class with static methods and adds JSON parsing for layer data
src/lib/services/database/migrations.ts Adds a migration to include layer information in the tag_metadata table
src/lib/services/database/index.ts Provides default metadata values including an empty layers array when none is present
src/lib/components/ui/progress/index.ts Introduces a new progress UI component
.github/workflows/linter.yml & e2e-tests.yml Changes the CI configuration to use self-hosted runners and adjusts dependency cache paths
Files not reviewed (3)
  • src/lib/components/docker-metadata/LayerVisualization.svelte: Language not supported
  • src/lib/components/ui/progress/progress.svelte: Language not supported
  • src/routes/details/[repo]/[image]/[tag]/+page.svelte: Language not supported
Comments suppressed due to low confidence (2)

src/lib/services/database/models/tag.ts:155

  • The assignment of configDigest is sourced from metadata.contentDigest. If the intention is to use a dedicated configuration digest (as done in other parts of the code), verify whether this should instead reference a distinct field (e.g. metadata.configDigest) or derive from tag.digest.
configDigest: metadata.contentDigest || ''

src/lib/types/db/models.ts:29

  • The change making all TagMetadata fields non-optional could lead to runtime issues if the database returns null values for any of these fields. Consider ensuring that defaults are provided or updating the type definitions to reflect possible nulls.
export interface TagMetadata {

@kmendell kmendell merged commit c600ac1 into main Apr 1, 2025
4 checks passed
@kmendell kmendell deleted the feat/layer-visualization branch April 1, 2025 04:37
kmendell added a commit that referenced this pull request Apr 5, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant