Skip to content
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

feat(overlay): added copy filename and changed styling of error frame #550

Merged
merged 6 commits into from
Nov 6, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/shaggy-dodos-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@spotlightjs/overlay': minor
---

- Added copy filename button on error frame.
- changed styling of error frames.
3 changes: 3 additions & 0 deletions packages/overlay/src/assets/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/overlay/src/assets/copy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion packages/overlay/src/assets/pen.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions packages/overlay/src/components/CopyToClipboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type React from 'react';
import { useCallback, useState } from 'react';
import { ReactComponent as CheckIcon } from '../assets/check.svg';
import { ReactComponent as CopyIcon } from '../assets/copy.svg';

export default function CopyToClipboard({ data }: { data: string }) {
const [isCopied, setIsCopied] = useState(false);

const handleCopy = useCallback(
(evt: React.MouseEvent) => {
evt.stopPropagation();
navigator.clipboard.writeText(data);
setTimeout(() => {
setIsCopied(false);
}, 1000);
setIsCopied(true);
},
[data],
);

if (isCopied) {
return <CheckIcon width={18} height={18} title="Copy filename" className="stroke-primary-50 transition-all" />;
}

return (
<CopyIcon
width={18}
height={18}
title="Copy filename"
className="stroke-primary-50 cursor-pointer transition-all"
onClick={handleCopy}
/>
);
}
27 changes: 27 additions & 0 deletions packages/overlay/src/components/OpenInEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type React from 'react';
import { useCallback } from 'react';
import { ReactComponent as PenIcon } from '../assets/pen.svg';

export default function OpenInEditor({ file }: { file: string }) {
const openInEditor = useCallback(
(evt: React.MouseEvent) => {
// TODO: Make this URL dynamic based on sidecarUrl!
fetch('http://localhost:8969/open', {
method: 'POST',
body: file,
credentials: 'omit',
});
evt.stopPropagation();
},
[file],
);
return (
<PenIcon
width={18}
height={18}
title="Open in editor"
className="stroke-primary-100 cursor-pointer"
onClick={openInEditor}
/>
);
}
Original file line number Diff line number Diff line change
@@ -36,7 +36,7 @@ export function ErrorItem({ event }: { event: SentryErrorEvent }) {
<strong className="text-xl">{value.type}</strong>
<pre>{value.value}</pre>
</h3>
<ul className="border-primary-600 border">
<ul>
{value.stacktrace?.frames.map((frame, frameIdx) => {
return (
<Frame
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { useState } from 'react';
import { ReactComponent as PenIcon } from '~/assets/pen.svg';
import { renderValue } from '~/utils/values';
import CopyToClipboard from '../../../../../components/CopyToClipboard';
import OpenInEditor from '../../../../../components/OpenInEditor';
import classNames from '../../../../../lib/classNames';
import { EventFrame, FrameVars } from '../../../types';
import { renderValue } from '../../../../../utils/values';
import type { EventFrame, FrameVars } from '../../../types';

function formatFilename(filename: string) {
const queryPos = filename.lastIndexOf('?');
if (queryPos > -1) {
filename = filename.slice(0, queryPos);
function resolveFilename(filename: string) {
try {
const url = new URL(filename);
filename = url.pathname.slice(1);
} catch {
// ignore
}
if (filename.indexOf('/node_modules/') === -1) return filename;
return `npm:${filename

return filename;
}

function formatFilename(filename: string) {
const resolvedFilename = resolveFilename(filename);
if (resolvedFilename.indexOf('/node_modules/') === -1) return resolvedFilename;
return `npm:${resolvedFilename
.replace(/\/node_modules\//gi, 'npm:')
.split('npm:')
.pop()}`;
@@ -37,6 +46,19 @@ function ContextLocals({ vars }: { vars: FrameVars }) {
);
}

function FileActions({ frame }: { frame: EventFrame }) {
if (!frame.filename) {
return null;
}
const resolvedFilename = resolveFilename(frame.filename);
return (
<div className="flex items-center gap-2">
<OpenInEditor file={`${resolvedFilename}:${frame.lineno}:${frame.colno}`} />
<CopyToClipboard data={resolvedFilename} />
</div>
);
}

export default function Frame({
frame,
defaultExpand = false,
@@ -48,51 +70,44 @@ export default function Frame({
}) {
const [isOpen, setOpen] = useState(defaultExpand);

const hasSource = !!frame.context_line;
const hasSource = Boolean(frame.context_line);
const fileName = platform === 'java' ? frame.module : frame.filename || frame.module;
return (
<li className="border-primary-900 bg-primary-900 border-b last:border-b-0">
<li
className={classNames(
hasSource ? 'cursor-pointer' : '',
!isOpen && hasSource ? 'hover:bg-primary-900' : '',
'bg-primary-950 border-primary-900 my-1 overflow-hidden rounded-md border',
)}
>
<div
className={classNames(
hasSource ? 'hover:bg-primary-800 cursor-pointer' : '',
'border-primary-950 text-primary-400 border-b px-2 py-1',
'text-primary-400 flex items-center justify-between px-2 py-1',
isOpen ? 'bg-primary-900' : '',
)}
onClick={hasSource ? () => setOpen(!isOpen) : undefined}
>
{fileName ? (
<span className="text-primary-100">
{formatFilename(fileName)}
{' in '}
</span>
) : null}

<span className="text-primary-100">{frame.function}</span>
{frame.lineno !== undefined && (
<>
{' '}
at line{' '}
<div>
{fileName ? (
<span className="text-primary-100">
{frame.lineno}
{frame.colno !== undefined && `:${frame.colno}`}
{formatFilename(fileName)}
{' in '}
</span>
</>
)}
{isOpen && !frame.filename?.includes(':') ? (
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

looks like we missed this condition !frame.filename?.includes(':')

Copy link
Member

Choose a reason for hiding this comment

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

This was intentional but we can revisit the decision in the future if necessary.

<PenIcon
width={18}
height={18}
title="Open in editor"
className="mx-2 inline fill-green-400 stroke-green-400 align-top"
onClick={evt => {
fetch('http://localhost:8969/open', {
method: 'POST',
body: `${frame.filename}:${frame.lineno}:${frame.colno}`,
credentials: 'omit',
});
evt.stopPropagation();
}}
/>
) : null}
) : null}

<span className="text-primary-100">{frame.function}</span>
{frame.lineno !== undefined && (
<>
{' '}
at line{' '}
<span className="text-primary-100">
{frame.lineno}
{frame.colno !== undefined && `:${frame.colno}`}
</span>
</>
)}
</div>
<FileActions frame={frame} />
</div>
{isOpen && (
<div className="bg-primary-950">
1 change: 1 addition & 0 deletions packages/sidecar/src/main.ts
Original file line number Diff line number Diff line change
@@ -258,6 +258,7 @@ function openRequestHandler(basePath: string = process.cwd()) {

req.on('end', () => {
const targetPath = resolve(basePath, requestBody);
logger.debug(`Launching editor for ${targetPath}`);
launchEditor(
// filename:line:column
// both line and column are optional
Loading