Repository navigation
feat(desktop): preview image diffs in SCM pane #7035
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
01be731
8242d93
efd5c00
dd17792
2ee8291
bf2a3d7
5f261d9
0f604e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,8 @@ import { Button } from "@superset/ui/button"; | |
| import { toast } from "@superset/ui/sonner"; | ||
| import { useWorkspaceClient, workspaceTrpc } from "@superset/workspace-client"; | ||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||
| import { LuFileCode } from "react-icons/lu"; | ||
| import { LuFileCode, LuLoader } from "react-icons/lu"; | ||
| import { useWorkspaceEvent } from "renderer/hooks/host-service/useWorkspaceEvent"; | ||
| import { | ||
| createPaneScrollStateKey, | ||
| getPaneScrollState, | ||
|
|
@@ -31,6 +32,7 @@ import { DiffFileHeaderName } from "renderer/screens/main/components/DiffFileHea | |
| import { DiffViewToolbar } from "renderer/screens/main/components/DiffViewToolbar"; | ||
| import { MarkdownSearch } from "renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/MarkdownSearch"; | ||
| import { toAbsoluteWorkspacePath } from "shared/absolute-paths"; | ||
| import { getImageMimeType } from "shared/file-types"; | ||
| import type { DiffPaneData, PaneViewerData } from "../../../../types"; | ||
| import { | ||
| type ChangesetFile, | ||
|
|
@@ -119,9 +121,30 @@ export function DiffPane({ | |
| const workspaceQuery = workspaceTrpc.workspace.get.useQuery({ | ||
| id: workspaceId, | ||
| }); | ||
| const worktreePath = workspaceQuery.data?.worktreePath; | ||
| const writeFile = workspaceTrpc.filesystem.writeFile.useMutation(); | ||
| const utils = workspaceTrpc.useUtils(); | ||
| const { trpcClient } = useWorkspaceClient(); | ||
| // Image previews read the working tree, so a replaced image has to refetch | ||
| // when git reports it changed. Scope to the reported paths when we have | ||
| // them so an unrelated edit doesn't refetch every preview on screen. | ||
| useWorkspaceEvent( | ||
| "git:changed", | ||
| workspaceId, | ||
| ({ paths }) => { | ||
| if (!worktreePath || !paths) { | ||
| void utils.filesystem.readFile.invalidate({ workspaceId }); | ||
| return; | ||
| } | ||
| for (const path of paths) { | ||
| void utils.filesystem.readFile.invalidate({ | ||
| workspaceId, | ||
| absolutePath: toAbsoluteWorkspacePath(worktreePath, path), | ||
| }); | ||
| } | ||
| }, | ||
| !!worktreePath, | ||
| ); | ||
| const [editingSet, setEditingSet] = useState<ReadonlySet<string>>(new Set()); | ||
| const [dirtyItemIds, setDirtyItemIds] = useState<ReadonlySet<string>>( | ||
| new Set(), | ||
|
|
@@ -672,7 +695,14 @@ export function DiffPane({ | |
| if (item.type !== "file") return null; | ||
| const file = fileByItemId.get(item.id); | ||
| if (!file) return null; | ||
| return <BinaryDiffPlaceholder file={file} onOpenFile={onOpenFile} />; | ||
| return ( | ||
| <BinaryDiffPlaceholder | ||
| file={file} | ||
| workspaceId={workspaceId} | ||
| worktreePath={worktreePath} | ||
| onOpenFile={onOpenFile} | ||
| /> | ||
| ); | ||
| } | ||
| if (m.kind === "deferred-placeholder") { | ||
| if (item.type !== "file") return null; | ||
|
|
@@ -746,6 +776,7 @@ export function DiffPane({ | |
| commentNav.isNavFocused, | ||
| commentNav.navFocusTick, | ||
| t, | ||
| worktreePath, | ||
| ], | ||
| ); | ||
|
|
||
|
|
@@ -817,20 +848,79 @@ export function DiffPane({ | |
| ); | ||
| } | ||
|
|
||
| const IMAGE_PREVIEW_MAX_BYTES = 10 * 1024 * 1024; | ||
|
|
||
| function BinaryDiffPlaceholder({ | ||
| file, | ||
| workspaceId, | ||
| worktreePath, | ||
| onOpenFile, | ||
| }: { | ||
| file: ChangesetFile; | ||
| workspaceId: string; | ||
| worktreePath?: string; | ||
| onOpenFile: (path: string, openInNewTab?: boolean) => void; | ||
| }) { | ||
| const canOpen = file.status !== "deleted"; | ||
| // The preview reads the working tree, which is only the "new" side of a | ||
| // working-tree diff. A commit's image may since have changed or gone. | ||
| const mimeType = | ||
| file.source.kind === "commit" ? null : getImageMimeType(file.path); | ||
| const canPreview = canOpen && !!worktreePath && mimeType !== null; | ||
| const imageQuery = workspaceTrpc.filesystem.readFile.useQuery( | ||
| { | ||
| workspaceId, | ||
| absolutePath: toAbsoluteWorkspacePath(worktreePath ?? "", file.path), | ||
| maxBytes: IMAGE_PREVIEW_MAX_BYTES, | ||
| }, | ||
| { | ||
| enabled: canPreview, | ||
| retry: false, | ||
| staleTime: 30_000, | ||
| }, | ||
| ); | ||
| const image = | ||
| canPreview && !imageQuery.isError && imageQuery.data?.kind === "bytes" | ||
| ? imageQuery.data | ||
| : null; | ||
| const isLoading = canPreview && imageQuery.isLoading; | ||
|
|
||
| if (image && !image.exceededLimit) { | ||
| return ( | ||
| <div className="flex flex-col items-center justify-center gap-3 bg-muted/30 p-4"> | ||
| <div className="max-w-full bg-[conic-gradient(color-mix(in_srgb,var(--color-foreground)_10%,transparent)_25%,transparent_0_50%,color-mix(in_srgb,var(--color-foreground)_10%,transparent)_0_75%,transparent_0)] bg-[length:16px_16px]"> | ||
| <img | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When a supported image file contains corrupt or non-renderable bytes, the Prompt for AI agents |
||
| src={`data:${mimeType};base64,${image.content}`} | ||
| alt={file.path} | ||
| className="block max-h-96 max-w-full object-contain" | ||
| /> | ||
| </div> | ||
| <Button | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={() => onOpenFile(file.path)} | ||
| > | ||
| <Trans>Open file</Trans> | ||
| </Button> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex flex-col items-center justify-center gap-3 bg-muted/30 py-8 text-muted-foreground"> | ||
| <LuFileCode className="size-8" /> | ||
| {isLoading ? ( | ||
| <LuLoader className="size-6 animate-spin" /> | ||
| ) : ( | ||
| <LuFileCode className="size-8" /> | ||
| )} | ||
| <p className="cursor-text select-text text-sm"> | ||
| <Trans>Binary file hidden</Trans> | ||
| {isLoading ? ( | ||
| <Trans>Loading image…</Trans> | ||
| ) : image?.exceededLimit ? ( | ||
| <Trans>File is too large to preview</Trans> | ||
| ) : ( | ||
| <Trans>Binary file hidden</Trans> | ||
| )} | ||
| </p> | ||
| {canOpen ? ( | ||
| <Button | ||
|
|
||
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.